317 lines
13 KiB
C
317 lines
13 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_sparse_sampler_identity.c
|
|
* @brief Ship gate for the sparse-survivor sampler: identical token, or fail.
|
|
*
|
|
* @status Complete. CPU-only — no GPU, no model.
|
|
* @issues None
|
|
* @todo None
|
|
*
|
|
* The fast path in engine.c samples over the <=k finite survivors of a sparse
|
|
* logits row instead of walking all n_vocab entries eight times. It ships only
|
|
* if it is BIT-EXACT: for the same row, the same params and the same RNG state
|
|
* it must return the token the full-vocab path returns. Not "close", not
|
|
* "same distribution" — the same token, or generation diverges from every
|
|
* existing reference.
|
|
*
|
|
* TWO THINGS THIS GATE DOES THAT THE PREVIOUS ONE DID NOT
|
|
*
|
|
* 1. It asserts the fast path RAN. pn_sampler_sparse_hits() must advance on
|
|
* every arm-B call. The previous attempt at this optimisation detected
|
|
* survivors with `logits[i] > -INFINITY`, which -ffast-math folds to a
|
|
* constant true; every row therefore looked dense, the fast path never once
|
|
* executed, and the bit-exactness gate passed 18/18 by comparing the slow
|
|
* path against itself. A green gate over an inert change is worse than a
|
|
* red one.
|
|
*
|
|
* 2. It proves it can go red. `--selftest` runs the same comparison against a
|
|
* deliberately WRONG arm (top_p nudged off the requested value, which moves
|
|
* the nucleus cut and therefore the draw). If that still reports a match,
|
|
* the comparison is not sensitive to the sampler's decision and no green
|
|
* result from it means anything. `--selftest` exits 0 only when the
|
|
* mismatch is detected.
|
|
*
|
|
* Usage: test_sparse_sampler_identity [--selftest]
|
|
*/
|
|
#include "../src/core/engine.h"
|
|
#include "../src/core/sampling_modifiers.h"
|
|
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* Smaller than the real 248320 so the grid runs in a second; the fast path's
|
|
* correctness does not depend on n_vocab, only on the row being sparse. A
|
|
* final case re-runs at the production vocab to be sure. */
|
|
#define VOCAB 20011
|
|
|
|
static int g_fail = 0;
|
|
static int g_checks = 0;
|
|
|
|
#define CHECK(cond, ...) do { \
|
|
g_checks++; \
|
|
if (!(cond)) { g_fail++; printf(" FAIL: "); printf(__VA_ARGS__); printf("\n"); } \
|
|
} while (0)
|
|
|
|
/* Exactly what gpu_present_topk leaves in the host row: -inf everywhere, k
|
|
* survivors at scattered ascending-scanned ids. `spread` shapes the survivor
|
|
* logits so the grid covers ties, wide gaps and underflow-to-zero probs. */
|
|
typedef enum { SPREAD_WIDE, SPREAD_TIGHT, SPREAD_ALL_EQUAL, SPREAD_EXTREME } spread_t;
|
|
|
|
static void build_row(float *row, int n_vocab, int k, spread_t sp, uint64_t seed)
|
|
{
|
|
for (int i = 0; i < n_vocab; i++) row[i] = -INFINITY;
|
|
uint64_t s = seed ? seed : 1;
|
|
int placed = 0;
|
|
while (placed < k) {
|
|
s = s * 6364136223846793005ULL + 1442695040888963407ULL;
|
|
int id = (int)((s >> 33) % (uint64_t)n_vocab);
|
|
uint32_t bits;
|
|
memcpy(&bits, &row[id], sizeof bits);
|
|
if (bits != 0xff800000u) continue; /* already a survivor */
|
|
float v;
|
|
switch (sp) {
|
|
case SPREAD_WIDE: v = 12.0f - 0.9f * (float)placed; break;
|
|
case SPREAD_TIGHT: v = 4.0f - 0.0009f * (float)placed; break;
|
|
case SPREAD_ALL_EQUAL: v = 3.25f; break;
|
|
default: v = 30.0f - 25.0f * (float)placed; break; /* underflow tail */
|
|
}
|
|
row[id] = v;
|
|
placed++;
|
|
}
|
|
}
|
|
|
|
/* One arm: fresh copy of the row, fresh RNG state from `seed`, one sample. */
|
|
static uint32_t run_arm(const float *row, int n_vocab, pn_sampling_params p,
|
|
uint32_t *recent, int n_recent, uint64_t seed, int sparse_on)
|
|
{
|
|
float *buf = malloc((size_t)n_vocab * sizeof(float));
|
|
if (!buf) { fprintf(stderr, "oom\n"); exit(1); }
|
|
memcpy(buf, row, (size_t)n_vocab * sizeof(float));
|
|
p.rng_seeded = 1;
|
|
p.rng_state = pn_seed_to_rng_state(seed);
|
|
pn_sampler_set_sparse(sparse_on);
|
|
uint32_t tok = pn_sample_next_token(buf, n_vocab, &p, recent, n_recent);
|
|
pn_sampler_clear_sparse();
|
|
free(buf);
|
|
return tok;
|
|
}
|
|
|
|
static const char *spread_name(spread_t s)
|
|
{
|
|
switch (s) {
|
|
case SPREAD_WIDE: return "wide";
|
|
case SPREAD_TIGHT: return "tight";
|
|
case SPREAD_ALL_EQUAL: return "all-equal";
|
|
default: return "extreme";
|
|
}
|
|
}
|
|
|
|
/* Compare the two arms over one configuration. Returns 1 when they match. */
|
|
static int compare(int n_vocab, int k, spread_t sp, float temp, int top_k,
|
|
float top_p, float rp, uint64_t seed, int verbose,
|
|
float top_p_arm_b)
|
|
{
|
|
float *row = malloc((size_t)n_vocab * sizeof(float));
|
|
if (!row) { fprintf(stderr, "oom\n"); exit(1); }
|
|
build_row(row, n_vocab, k, sp, seed * 7919u + 13u);
|
|
|
|
/* A repetition window that deliberately straddles the survivor set: some
|
|
* entries are survivors (the penalty moves their logit), some are -inf
|
|
* non-survivors (the penalty must be a no-op that leaves them -inf, which
|
|
* is what lets the fast path ignore them). */
|
|
uint32_t recent[64];
|
|
int n_recent = 64;
|
|
int found = 0;
|
|
for (int i = 0; i < n_vocab && found < 8; i++) {
|
|
uint32_t bits; memcpy(&bits, &row[i], sizeof bits);
|
|
if (bits != 0xff800000u) recent[found++] = (uint32_t)i;
|
|
}
|
|
for (int i = found; i < n_recent; i++)
|
|
recent[i] = (uint32_t)((i * 977u + 5u) % (unsigned)n_vocab);
|
|
|
|
pn_sampling_params p = pn_sampling_defaults();
|
|
p.temperature = temp;
|
|
p.top_k = top_k;
|
|
p.top_p = top_p;
|
|
p.repeat_penalty = rp;
|
|
p.repeat_last_n = n_recent;
|
|
|
|
unsigned long hits_before = pn_sampler_sparse_hits();
|
|
uint32_t a = run_arm(row, n_vocab, p, recent, n_recent, seed, 0);
|
|
pn_sampling_params pb = p;
|
|
pb.top_p = top_p_arm_b; /* == top_p except in --selftest */
|
|
uint32_t b = run_arm(row, n_vocab, pb, recent, n_recent, seed, 1);
|
|
unsigned long hits_after = pn_sampler_sparse_hits();
|
|
|
|
int ran = (hits_after == hits_before + 1);
|
|
int match = (a == b);
|
|
|
|
if (verbose || !match || !ran) {
|
|
printf(" k=%-4d %-9s temp=%.2f top_k=%-6d top_p=%.2f rp=%.2f seed=%llu"
|
|
" -> A=%u B=%u %s%s\n",
|
|
k, spread_name(sp), (double)temp, top_k, (double)top_p, (double)rp,
|
|
(unsigned long long)seed, a, b,
|
|
match ? "match" : "MISMATCH",
|
|
ran ? "" : " [fast path DID NOT RUN]");
|
|
}
|
|
|
|
free(row);
|
|
if (!ran) return -1; /* vacuous: the arms were the same code */
|
|
return match;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
int selftest = (argc > 1 && strcmp(argv[1], "--selftest") == 0);
|
|
|
|
if (selftest) {
|
|
/* Falsification: arm B samples with a top_p that is NOT the requested
|
|
* one. The nucleus cut moves, so the draw must land somewhere else at
|
|
* least once. A harness that reports "all match" here is measuring
|
|
* nothing and no green run from it can be trusted. */
|
|
printf("SELFTEST: arm B given the wrong top_p — the gate must go RED\n");
|
|
int mismatches = 0, vacuous = 0, cases = 0;
|
|
for (uint64_t seed = 1; seed <= 60; seed++) {
|
|
int r = compare(VOCAB, 32, SPREAD_TIGHT, 0.7f, 40, 0.95f, 1.1f,
|
|
seed, 0, 0.30f);
|
|
cases++;
|
|
if (r < 0) vacuous++;
|
|
else if (r == 0) mismatches++;
|
|
}
|
|
printf(" %d cases, %d mismatches detected, %d vacuous\n",
|
|
cases, mismatches, vacuous);
|
|
if (vacuous) {
|
|
printf("SELFTEST FAILED: the fast path did not run — comparison is vacuous\n");
|
|
return 1;
|
|
}
|
|
if (mismatches == 0) {
|
|
printf("SELFTEST FAILED: a deliberately wrong arm still matched — "
|
|
"this gate cannot detect a broken sampler\n");
|
|
return 1;
|
|
}
|
|
printf("SELFTEST PASSED: the gate detects a wrong sampler (%d/%d cases)\n",
|
|
mismatches, cases);
|
|
return 0;
|
|
}
|
|
|
|
printf("Sparse-survivor sampler — token-identity gate (vocab %d)\n", VOCAB);
|
|
|
|
const float temps[] = { 0.3f, 0.7f, 1.0f, 2.0f };
|
|
const int topks[] = { 1, 5, 40, 0, 100000 };
|
|
const float toppz[] = { 1.0f, 0.95f, 0.5f, 0.1f };
|
|
const float rps[] = { 1.0f, 1.1f };
|
|
const int ks[] = { 1, 2, 8, 32, 40, 256 };
|
|
const spread_t sps[] = { SPREAD_WIDE, SPREAD_TIGHT, SPREAD_ALL_EQUAL, SPREAD_EXTREME };
|
|
|
|
int vacuous = 0;
|
|
uint64_t seed = 1;
|
|
for (size_t a = 0; a < sizeof temps / sizeof *temps; a++)
|
|
for (size_t b = 0; b < sizeof topks / sizeof *topks; b++)
|
|
for (size_t c = 0; c < sizeof toppz / sizeof *toppz; c++)
|
|
for (size_t d = 0; d < sizeof rps / sizeof *rps; d++)
|
|
for (size_t e = 0; e < sizeof ks / sizeof *ks; e++)
|
|
for (size_t f = 0; f < sizeof sps / sizeof *sps; f++) {
|
|
int r = compare(VOCAB, ks[e], sps[f], temps[a], topks[b], toppz[c],
|
|
rps[d], seed++, 0, toppz[c]);
|
|
if (r < 0) { vacuous++; g_fail++; g_checks++; }
|
|
else CHECK(r == 1, "token mismatch at case %llu", (unsigned long long)seed - 1);
|
|
}
|
|
printf(" grid: %d comparisons, %d failures, %d vacuous\n",
|
|
g_checks, g_fail, vacuous);
|
|
|
|
/* Production shape: the real vocab, the real serve defaults, many seeds. */
|
|
printf("Production shape (vocab 248320, k=40, temp 0.7, top_k 40, top_p 0.9, rp 1.1)\n");
|
|
int prod_bad = 0, prod_vacuous = 0;
|
|
for (uint64_t s = 1000; s < 1200; s++) {
|
|
int r = compare(248320, 40, SPREAD_TIGHT, 0.7f, 40, 0.9f, 1.1f, s, 0, 0.9f);
|
|
if (r < 0) prod_vacuous++;
|
|
else if (r == 0) prod_bad++;
|
|
}
|
|
g_checks += 200;
|
|
if (prod_bad || prod_vacuous) g_fail += prod_bad + prod_vacuous;
|
|
printf(" 200 turns: %d mismatches, %d vacuous\n", prod_bad, prod_vacuous);
|
|
|
|
/* Greedy (temp 0) takes the SAME fast path since the argmax addition: it
|
|
* must RUN and it must return the identical token. The spreads matter
|
|
* here — SPREAD_ALL_EQUAL is the tie-break case, where every survivor
|
|
* carries the same logit and both scans must land on the LOWEST survivor
|
|
* id, and SPREAD_EXTREME puts the winner in a 25-per-step tail. */
|
|
printf("Greedy (temp 0) must take the fast path and return the same token\n");
|
|
{
|
|
const spread_t gsp[] = { SPREAD_WIDE, SPREAD_TIGHT, SPREAD_ALL_EQUAL,
|
|
SPREAD_EXTREME };
|
|
const int gks[] = { 1, 2, 40, 256 };
|
|
for (size_t i = 0; i < sizeof gsp / sizeof *gsp; i++)
|
|
for (size_t j = 0; j < sizeof gks / sizeof *gks; j++) {
|
|
float *row = malloc(248320 * sizeof(float));
|
|
if (!row) { fprintf(stderr, "oom\n"); exit(1); }
|
|
build_row(row, 248320, gks[j], gsp[i], 4242 + i * 31 + j);
|
|
pn_sampling_params p = pn_sampling_defaults();
|
|
p.temperature = 0.0f;
|
|
unsigned long h0 = pn_sampler_sparse_hits();
|
|
uint32_t ga = run_arm(row, 248320, p, NULL, 0, 7, 0);
|
|
uint32_t gb = run_arm(row, 248320, p, NULL, 0, 7, 1);
|
|
unsigned long h1 = pn_sampler_sparse_hits();
|
|
CHECK(ga == gb, "greedy token changed (%s k=%d): %u vs %u",
|
|
spread_name(gsp[i]), gks[j], ga, gb);
|
|
CHECK(h1 == h0 + 1, "greedy fast path did not run (%s k=%d, %lu hits)",
|
|
spread_name(gsp[i]), gks[j], h1 - h0);
|
|
free(row);
|
|
}
|
|
}
|
|
|
|
/* Greedy on a DENSE row must still fall through — full_logits mode hands
|
|
* the sampler a real row and there is no survivor set to shortcut. */
|
|
printf("Greedy on a dense row must fall through to the full scan\n");
|
|
{
|
|
int nv = 5000;
|
|
float *row = malloc((size_t)nv * sizeof(float));
|
|
if (!row) { fprintf(stderr, "oom\n"); exit(1); }
|
|
uint64_t s = 424242;
|
|
for (int i = 0; i < nv; i++) {
|
|
s = s * 6364136223846793005ULL + 1442695040888963407ULL;
|
|
row[i] = (float)((int)((s >> 33) % 2000) - 1000) * 0.01f;
|
|
}
|
|
pn_sampling_params p = pn_sampling_defaults();
|
|
p.temperature = 0.0f;
|
|
unsigned long h0 = pn_sampler_sparse_hits();
|
|
uint32_t da = run_arm(row, nv, p, NULL, 0, 31, 0);
|
|
uint32_t db = run_arm(row, nv, p, NULL, 0, 31, 1);
|
|
unsigned long h1 = pn_sampler_sparse_hits();
|
|
CHECK(da == db, "dense greedy row diverged: %u vs %u", da, db);
|
|
CHECK(h1 == h0, "the fast path ran on a dense greedy row (%lu hits)",
|
|
h1 - h0);
|
|
free(row);
|
|
}
|
|
|
|
/* A DENSE row (what full_logits mode hands the sampler for a tool-grammar,
|
|
* logit_bias or logprobs turn) must fall through, not be mis-sampled. */
|
|
printf("Dense row must fall through to the full path\n");
|
|
{
|
|
int nv = 5000;
|
|
float *row = malloc((size_t)nv * sizeof(float));
|
|
uint64_t s = 99;
|
|
for (int i = 0; i < nv; i++) {
|
|
s = s * 6364136223846793005ULL + 1442695040888963407ULL;
|
|
row[i] = (float)((int)((s >> 33) % 2000) - 1000) * 0.01f;
|
|
}
|
|
pn_sampling_params p = pn_sampling_defaults();
|
|
p.temperature = 0.7f;
|
|
unsigned long h0 = pn_sampler_sparse_hits();
|
|
uint32_t da = run_arm(row, nv, p, NULL, 0, 31, 0);
|
|
uint32_t db = run_arm(row, nv, p, NULL, 0, 31, 1);
|
|
unsigned long h1 = pn_sampler_sparse_hits();
|
|
CHECK(da == db, "dense row diverged: %u vs %u", da, db);
|
|
CHECK(h1 == h0, "the fast path ran on a dense row (%lu hits)", h1 - h0);
|
|
free(row);
|
|
}
|
|
|
|
printf("\n%s: %d checks, %d failures\n", g_fail ? "FAILED" : "PASSED",
|
|
g_checks, g_fail);
|
|
return g_fail ? 1 : 0;
|
|
}
|