281 lines
9.0 KiB
C
281 lines
9.0 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_sampling_modifiers.c
|
|
* @brief Unit tests for the OpenAI logits-row sampling controls
|
|
* (src/core/sampling_modifiers.c): `logit_bias`,
|
|
* `presence_penalty`/`frequency_penalty`, the seeded `seed` RNG, and
|
|
* `logprobs`/`top_logprobs` extraction.
|
|
*
|
|
* These assert on the ARITHMETIC and the ORDERING, not on a generated string —
|
|
* the bug class this area already shipped once was an ordering bug (a penalty
|
|
* applied on the wrong side of the greedy branch), which no end-to-end text
|
|
* check would have localized.
|
|
*
|
|
* @status Complete for the four fields implemented.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "lib/test.h"
|
|
#include "../src/core/sampling_modifiers.h"
|
|
|
|
#define VOCAB 8
|
|
|
|
static void reset(float *logits)
|
|
{
|
|
for (int i = 0; i < VOCAB; i++) logits[i] = 0.0f;
|
|
}
|
|
|
|
/* ── logit_bias ── */
|
|
|
|
static void test_logit_bias_adds_to_the_named_tokens(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
logits[3] = 1.0f;
|
|
const pn_logit_bias_entry bias[] = { { 3, -5.0f }, { 5, 2.5f } };
|
|
pn_apply_logit_bias(logits, VOCAB, bias, 2);
|
|
ASSERT_FLT_NEAR(logits[3], -4.0f, 1e-6f);
|
|
ASSERT_FLT_NEAR(logits[5], 2.5f, 1e-6f);
|
|
ASSERT_FLT_NEAR(logits[0], 0.0f, 1e-6f); /* untouched */
|
|
}
|
|
|
|
static void test_logit_bias_ignores_out_of_range_ids(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
const pn_logit_bias_entry bias[] = { { VOCAB, 100.0f }, { 999999, -100.0f } };
|
|
pn_apply_logit_bias(logits, VOCAB, bias, 2);
|
|
for (int i = 0; i < VOCAB; i++) ASSERT_FLT_NEAR(logits[i], 0.0f, 1e-6f);
|
|
}
|
|
|
|
static void test_logit_bias_off_is_a_noop(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
logits[2] = 7.0f;
|
|
pn_apply_logit_bias(logits, VOCAB, NULL, 0);
|
|
ASSERT_FLT_NEAR(logits[2], 7.0f, 1e-6f);
|
|
}
|
|
|
|
/* ── presence / frequency penalties ── */
|
|
|
|
static void test_frequency_penalty_scales_with_count(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
/* token 4 appears three times, token 6 once. */
|
|
const uint32_t recent[] = { 4, 6, 4, 4 };
|
|
pn_apply_presence_frequency_penalties(logits, VOCAB, 0.0f, 1.0f, recent, 4);
|
|
ASSERT_FLT_NEAR(logits[4], -3.0f, 1e-6f);
|
|
ASSERT_FLT_NEAR(logits[6], -1.0f, 1e-6f);
|
|
ASSERT_FLT_NEAR(logits[0], 0.0f, 1e-6f);
|
|
}
|
|
|
|
static void test_presence_penalty_is_charged_once(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
const uint32_t recent[] = { 4, 4, 4 };
|
|
pn_apply_presence_frequency_penalties(logits, VOCAB, 2.0f, 0.0f, recent, 3);
|
|
ASSERT_FLT_NEAR(logits[4], -2.0f, 1e-6f); /* NOT -6 */
|
|
}
|
|
|
|
static void test_both_penalties_compose(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
const uint32_t recent[] = { 1, 1 };
|
|
pn_apply_presence_frequency_penalties(logits, VOCAB, 0.5f, 0.25f, recent, 2);
|
|
/* 2 occurrences * 0.25 frequency + 1 * 0.5 presence */
|
|
ASSERT_FLT_NEAR(logits[1], -1.0f, 1e-6f);
|
|
}
|
|
|
|
static void test_penalties_off_is_a_noop(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
logits[1] = 3.0f;
|
|
const uint32_t recent[] = { 1, 1 };
|
|
pn_apply_presence_frequency_penalties(logits, VOCAB, 0.0f, 0.0f, recent, 2);
|
|
ASSERT_FLT_NEAR(logits[1], 3.0f, 1e-6f);
|
|
/* …and an empty window is a no-op even with penalties set. */
|
|
pn_apply_presence_frequency_penalties(logits, VOCAB, 1.0f, 1.0f, NULL, 0);
|
|
ASSERT_FLT_NEAR(logits[1], 3.0f, 1e-6f);
|
|
}
|
|
|
|
static void test_negative_penalty_boosts(void)
|
|
{
|
|
/* OpenAI allows -2.0..2.0; a negative value must ENCOURAGE repetition. */
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
const uint32_t recent[] = { 2 };
|
|
pn_apply_presence_frequency_penalties(logits, VOCAB, -1.0f, 0.0f, recent, 1);
|
|
ASSERT_FLT_NEAR(logits[2], 1.0f, 1e-6f);
|
|
}
|
|
|
|
/* ── seeded RNG ── */
|
|
|
|
static void test_same_seed_gives_the_same_stream(void)
|
|
{
|
|
uint64_t a = pn_seed_to_rng_state(42);
|
|
uint64_t b = pn_seed_to_rng_state(42);
|
|
for (int i = 0; i < 64; i++) {
|
|
float x = pn_seeded_rng_next_float(&a);
|
|
float y = pn_seeded_rng_next_float(&b);
|
|
ASSERT_FLT_NEAR(x, y, 0.0f);
|
|
}
|
|
}
|
|
|
|
static void test_different_seeds_diverge_immediately(void)
|
|
{
|
|
/* Neighbouring small seeds are the common case (0, 1, 42) — they must not
|
|
* produce a correlated FIRST draw, which is what the splitmix64 diffuse in
|
|
* pn_seed_to_rng_state is for. */
|
|
uint64_t a = pn_seed_to_rng_state(0);
|
|
uint64_t b = pn_seed_to_rng_state(1);
|
|
float x = pn_seeded_rng_next_float(&a);
|
|
float y = pn_seeded_rng_next_float(&b);
|
|
ASSERT_TRUE(fabsf(x - y) > 0.01f);
|
|
}
|
|
|
|
static void test_seed_zero_is_not_a_degenerate_state(void)
|
|
{
|
|
/* 0 is xorshift's fixed point: an undiffused seed of 0 would return 0.0
|
|
* forever and pin every draw to token 0. */
|
|
uint64_t s = pn_seed_to_rng_state(0);
|
|
ASSERT_TRUE(s != 0);
|
|
int nonzero = 0;
|
|
for (int i = 0; i < 32; i++)
|
|
if (pn_seeded_rng_next_float(&s) > 0.0f) nonzero++;
|
|
ASSERT_TRUE(nonzero > 25);
|
|
}
|
|
|
|
static void test_rng_stays_in_unit_interval(void)
|
|
{
|
|
uint64_t s = pn_seed_to_rng_state(12345);
|
|
for (int i = 0; i < 4096; i++) {
|
|
float v = pn_seeded_rng_next_float(&s);
|
|
ASSERT_TRUE(v >= 0.0f);
|
|
ASSERT_TRUE(v < 1.0f);
|
|
}
|
|
}
|
|
|
|
/* ── logprobs ── */
|
|
|
|
static void test_logprobs_sum_to_one(void)
|
|
{
|
|
float logits[VOCAB];
|
|
for (int i = 0; i < VOCAB; i++) logits[i] = (float)i * 0.5f;
|
|
uint32_t ids[PN_LOGPROBS_MAX_TOP];
|
|
float lps[PN_LOGPROBS_MAX_TOP];
|
|
float chosen = 0.0f;
|
|
int n = pn_logprobs_extract(logits, VOCAB, 3, VOCAB, &chosen, ids, lps);
|
|
ASSERT_INT_EQ(n, VOCAB);
|
|
float total = 0.0f;
|
|
for (int i = 0; i < n; i++) total += expf(lps[i]);
|
|
ASSERT_FLT_NEAR(total, 1.0f, 1e-4f);
|
|
/* Every logprob is <= 0 (a probability). */
|
|
for (int i = 0; i < n; i++) ASSERT_TRUE(lps[i] <= 1e-6f);
|
|
}
|
|
|
|
static void test_logprobs_are_descending_and_top_is_argmax(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
logits[5] = 10.0f;
|
|
logits[2] = 9.0f;
|
|
logits[7] = 8.0f;
|
|
uint32_t ids[PN_LOGPROBS_MAX_TOP];
|
|
float lps[PN_LOGPROBS_MAX_TOP];
|
|
int n = pn_logprobs_extract(logits, VOCAB, 5, 3, NULL, ids, lps);
|
|
ASSERT_INT_EQ(n, 3);
|
|
ASSERT_INT_EQ((int)ids[0], 5);
|
|
ASSERT_INT_EQ((int)ids[1], 2);
|
|
ASSERT_INT_EQ((int)ids[2], 7);
|
|
ASSERT_TRUE(lps[0] > lps[1]);
|
|
ASSERT_TRUE(lps[1] > lps[2]);
|
|
}
|
|
|
|
static void test_chosen_logprob_matches_its_entry(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
logits[5] = 10.0f;
|
|
logits[2] = 9.0f;
|
|
uint32_t ids[PN_LOGPROBS_MAX_TOP];
|
|
float lps[PN_LOGPROBS_MAX_TOP];
|
|
float chosen = 0.0f;
|
|
/* Chosen is the RUNNER-UP: its reported logprob must equal its top-list
|
|
* entry, not the argmax's. */
|
|
pn_logprobs_extract(logits, VOCAB, 2, 2, &chosen, ids, lps);
|
|
ASSERT_INT_EQ((int)ids[1], 2);
|
|
ASSERT_FLT_NEAR(chosen, lps[1], 1e-6f);
|
|
}
|
|
|
|
static void test_logprobs_top_is_clamped(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
uint32_t ids[PN_LOGPROBS_MAX_TOP];
|
|
float lps[PN_LOGPROBS_MAX_TOP];
|
|
/* Asking for more than the cap must not overrun the caller's arrays. */
|
|
int n = pn_logprobs_extract(logits, VOCAB, 0, 10000, NULL, ids, lps);
|
|
ASSERT_TRUE(n <= PN_LOGPROBS_MAX_TOP);
|
|
ASSERT_INT_EQ(n, VOCAB); /* vocab is smaller than the cap here */
|
|
}
|
|
|
|
static void test_logprobs_zero_top_still_reports_chosen(void)
|
|
{
|
|
float logits[VOCAB];
|
|
reset(logits);
|
|
logits[4] = 5.0f;
|
|
float chosen = 123.0f;
|
|
int n = pn_logprobs_extract(logits, VOCAB, 4, 0, &chosen, NULL, NULL);
|
|
ASSERT_INT_EQ(n, 0);
|
|
ASSERT_TRUE(chosen < 0.0f); /* a real logprob was written */
|
|
ASSERT_TRUE(chosen > -1.0f); /* and token 4 dominates, so it is near 0 */
|
|
}
|
|
|
|
static void test_logprobs_does_not_modify_the_row(void)
|
|
{
|
|
float logits[VOCAB];
|
|
for (int i = 0; i < VOCAB; i++) logits[i] = (float)i;
|
|
float before[VOCAB];
|
|
memcpy(before, logits, sizeof(before));
|
|
uint32_t ids[PN_LOGPROBS_MAX_TOP];
|
|
float lps[PN_LOGPROBS_MAX_TOP];
|
|
pn_logprobs_extract(logits, VOCAB, 1, 4, NULL, ids, lps);
|
|
for (int i = 0; i < VOCAB; i++) ASSERT_FLT_NEAR(logits[i], before[i], 0.0f);
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
printf("\n=== sampling modifiers (logit_bias / penalties / seed / logprobs) ===\n\n");
|
|
RUN_TEST(logit_bias_adds_to_the_named_tokens);
|
|
RUN_TEST(logit_bias_ignores_out_of_range_ids);
|
|
RUN_TEST(logit_bias_off_is_a_noop);
|
|
RUN_TEST(frequency_penalty_scales_with_count);
|
|
RUN_TEST(presence_penalty_is_charged_once);
|
|
RUN_TEST(both_penalties_compose);
|
|
RUN_TEST(penalties_off_is_a_noop);
|
|
RUN_TEST(negative_penalty_boosts);
|
|
RUN_TEST(same_seed_gives_the_same_stream);
|
|
RUN_TEST(different_seeds_diverge_immediately);
|
|
RUN_TEST(seed_zero_is_not_a_degenerate_state);
|
|
RUN_TEST(rng_stays_in_unit_interval);
|
|
RUN_TEST(logprobs_sum_to_one);
|
|
RUN_TEST(logprobs_are_descending_and_top_is_argmax);
|
|
RUN_TEST(chosen_logprob_matches_its_entry);
|
|
RUN_TEST(logprobs_top_is_clamped);
|
|
RUN_TEST(logprobs_zero_top_still_reports_chosen);
|
|
RUN_TEST(logprobs_does_not_modify_the_row);
|
|
TEST_REPORT();
|
|
return __ft_failed == 0 ? 0 : 1;
|
|
}
|