Pantheon/tests/bench_ngram_accept.c

851 lines
34 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file bench_ngram_accept.c
* @brief PHASE B's CHEAP GATE: what acceptance rate does prompt-lookup (n-gram) drafting
* actually reach on this model, on real traffic — measured BEFORE any verify
* machinery exists.
*
* ---------------------------------------------------------------------------
* WHY THIS NEEDS NO VERIFY PATH, NO KERNEL AND NO ROLLBACK
* ---------------------------------------------------------------------------
* Greedy speculative decoding accepts a drafted token iff it equals the model's own argmax
* at that position, and an accepted token leaves the prefix identical to the plain greedy
* run's. So the whole acceptance distribution is a function of ONE ordinary greedy decode.
* See ngram_accept_stream.h for the argument written out. This harness therefore:
*
* --generate DUMP MODEL decode the corpus greedily on the SHIPPED arm, one token per
* forward, and record every token id. Needs the card.
* --score DUMP [DUMP...] replay the recorded streams as speculative rounds and report
* acceptance by K, by corpus, by phase and by context length.
* Loads no model, opens no card, costs seconds.
* --selftest known-answer arms that need neither.
*
* The drafter is `pn_ngram_draft` — the one that ships in src/core/ngram_draft.c, not a copy
* written for the measurement.
*
* ---------------------------------------------------------------------------
* WHAT MAKES THE NUMBER FALSIFIABLE
* ---------------------------------------------------------------------------
* This project has hit EIGHT vacuous-gate classes, including a gate that passed 132 checks on
* a shape the model does not have. A drafting hit counter that can only count hits measures
* nothing, so the same code path is run three more ways over the same bytes:
*
* --selftest a periodic stream (must hit the CEILING of K+1 tokens per forward) and
* an i.i.d. stream (must sit at 1.00). Both ends of the scale, no card.
* control=shuffle the real streams with the searched history permuted and only the query
* suffix left intact — same tokens, no structure. Must collapse to ~1.00.
* control=random the real rounds with the drafter's proposals replaced by uniform
* vocabulary ids. Must collapse to ~1.00.
*
* ---------------------------------------------------------------------------
* TEMPERATURE AND SPARSE LOGITS
* ---------------------------------------------------------------------------
* The generate pass decodes GREEDILY, which is what `--bench --temperature 0` does and what
* every speed figure on this card was taken at. Only the ARGMAX of each row is used, so
* `PN_TT_SPARSE_LOGITS` is safe here and is reported rather than refused — the card selects
* the exact top-N and the argmax over the sparse row is the argmax over the dense one. Any
* figure other than an argmax over that row would be noise, and this harness computes none.
*
* Under sampling the accept test becomes a rejection-sampling step rather than an equality,
* so every acceptance figure below is the GREEDY UPPER BOUND.
*
* USAGE
* ./tests/bench_ngram_accept --generate OUT.bin MODEL.gguf --corpus F --tag NAME
* [--n-gen N] [--max-docs N] [--system TEXT]
* ./tests/bench_ngram_accept --score DUMP.bin [DUMP.bin ...]
* ./tests/bench_ngram_accept --selftest
*
* Dumps are not shipped: record your own with --generate (tests/data/ngram_accept_dumps/
* is gitignored for that purpose).
*
* NOT part of `make test`: --generate needs a model and a card.
*
* @status Working
* @issues None
* @todo None
*/
#include "../include/pantheon.h"
#include "../src/core/engine.h"
#include "tensor/tensor.h"
#include "../src/core/chat.h"
#include "chat_corpus.h"
#include "ngram_accept_stream.h"
#include "parity_accel_arm.h"
#include "tokenizer/tokenizer.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define ACCEPT_ECHO_CHARS 96
#define ACCEPT_MAX_GEN 4096
#define ACCEPT_CONTROL_SEED 0x5DEECE66DULL
/** The K values reported for every corpus. The task names 2, 4 and 8; the neighbours are
* free and show whether the curve has already flattened. */
static const int accept_k_ladder[] = { 1, 2, 3, 4, 6, 8, 16 };
#define ACCEPT_K_COUNT ((int)(sizeof(accept_k_ladder) / sizeof(accept_k_ladder[0])))
/** Trailing-history windows, in tokens; 0 = the whole context. Answers "does MORE history
* help", which is the mechanism behind any context-length trend. */
static const int accept_window_ladder[] = { 128, 512, 2048, 8192, 0 };
#define ACCEPT_WINDOW_COUNT ((int)(sizeof(accept_window_ladder) / sizeof(accept_window_ladder[0])))
static double accept_now_s(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
}
static int accept_argmax(const float *x, int n)
{
int best = 0;
for (int i = 1; i < n; i++)
if (x[i] > x[best])
best = i;
return best;
}
/* ================================================================
* Pass 1 — the recorded greedy stream (needs the card)
* ================================================================ */
/**
* Report, once, whether the logit row is the sparse presentation.
*
* Not a refusal: this harness reads only the argmax, which is exact on that row. It is
* printed because a reader of the dump has to know which presentation produced it — the
* two-pass parity gate REFUSES the same row, and the difference has to be deliberate.
*/
static void accept_report_row_density(const float *row, int n)
{
static int checked;
int non_finite = 0;
if (checked)
return;
checked = 1;
for (int i = 0; i < n; i++)
if (!(row[i] > -1e30f && row[i] < 1e30f))
non_finite++;
printf("ROW n=%d non_finite=%d presentation=%s (only the ARGMAX is read)\n", n, non_finite,
non_finite * 2 >= n ? "SPARSE" : "dense");
}
/**
* Prefill `prompt` and greedily decode up to `n_gen` tokens into `doc`.
*
* @return 0 on success, -1 if a forward failed
*/
static int accept_decode_document(pn_model *model, const uint32_t *prompt, int n_prompt,
int n_gen, int eos, int think_end, ngram_document *doc)
{
pn_state *state = pn_state_new(model);
tm_tensor *logits = NULL;
double t_prefill, t0;
int n = 0;
if (!state)
return -1;
doc->prompt = malloc((size_t)n_prompt * sizeof(*doc->prompt));
doc->gen = malloc((size_t)n_gen * sizeof(*doc->gen));
if (!doc->prompt || !doc->gen) {
pn_state_free(state);
return -1;
}
memcpy(doc->prompt, prompt, (size_t)n_prompt * sizeof(*doc->prompt));
doc->n_prompt = n_prompt;
doc->think_end_pos = -1;
t0 = accept_now_s();
for (int i = 0; i < n_prompt; i++) {
if (logits)
tm_free(logits);
logits = model->forward(model, prompt[i], state);
if (!logits) {
fprintf(stderr, "generate: forward failed prefilling position %d\n", i);
pn_state_free(state);
return -1;
}
}
t_prefill = accept_now_s() - t0;
accept_report_row_density(tm_data_f32(logits), (int)logits->shape[0]);
t0 = accept_now_s();
while (n < n_gen) {
uint32_t tok = (uint32_t)accept_argmax(tm_data_f32(logits), model->config.n_vocab);
tm_free(logits);
doc->gen[n] = tok;
if (think_end >= 0 && doc->think_end_pos < 0 && (int)tok == think_end)
doc->think_end_pos = n;
n++;
if ((int)tok == eos || n >= n_gen)
break;
logits = model->forward(model, tok, state);
if (!logits) {
fprintf(stderr, "generate: forward failed generating token %d\n", n);
break;
}
}
doc->n_gen = n;
doc->decode_s = accept_now_s() - t0;
printf(" prefill %d tok in %.1f s (%.3f s/tok) | decode %d tok in %.1f s (%.3f tok/s) "
"| think_end_pos=%d\n", n_prompt, t_prefill, t_prefill / (double)n_prompt, n,
doc->decode_s, (double)n / doc->decode_s, doc->think_end_pos);
pn_state_free(state);
return n > 1 ? 0 : -1;
}
/** Echo the first characters of a token run so a reader can see it is real text. */
static void accept_echo(pn_model *model, const uint32_t *ids, int n, const char *label)
{
char *text = chat_ids_to_text(model, ids, n);
printf(" %s \"", label);
for (int i = 0; text && text[i] && i < ACCEPT_ECHO_CHARS; i++)
putchar(text[i] == '\n' ? ' ' : text[i]);
printf("...\"\n");
free(text);
}
/**
* Slurp a corpus and split it into one system prompt and N user messages.
*
* The blocks are separated by lines reading exactly `===PROMPT===`; a block introduced by
* `===SYSTEM===` is the system prompt instead. A line-tagged format (the chat-parity
* corpus's `U<TAB>`) cannot carry a source file, because source has blank lines in it and a
* blank line ends a document there — and a code-edit prompt with its blank lines stripped is
* not the traffic this is trying to measure.
*
* @return number of user messages; out[] points into the returned buffer, which the caller
* keeps alive for the run
*/
static int accept_load_corpus(const char *path, char **buffer_out, const char **system_out,
char **out, int max_out)
{
char *buf, *cur;
long len;
int n = 0, next_is_system = 0;
FILE *f = fopen(path, "rb");
if (!f)
return -1;
fseek(f, 0, SEEK_END);
len = ftell(f);
fseek(f, 0, SEEK_SET);
buf = malloc((size_t)len + 2);
if (!buf || len <= 0 || fread(buf, 1, (size_t)len, f) != (size_t)len) {
fclose(f);
free(buf);
return -1;
}
buf[len] = '\0';
fclose(f);
*buffer_out = buf;
cur = buf;
while (cur && *cur && n < max_out) {
char *sep = strstr(cur, "\n===PROMPT===\n");
char *sys = strstr(cur, "\n===SYSTEM===\n");
int sys_first = sys && (!sep || sys < sep);
char *at = sys_first ? sys : sep;
if (strncmp(cur, "===SYSTEM===\n", 13) == 0) {
next_is_system = 1;
cur += 13;
continue;
}
if (strncmp(cur, "===PROMPT===\n", 13) == 0) {
next_is_system = 0;
cur += 13;
continue;
}
if (at)
*at = '\0';
if (*cur) {
if (next_is_system)
*system_out = cur;
else
out[n++] = cur;
}
next_is_system = sys_first;
cur = at ? at + 14 : NULL;
}
return n;
}
#define ACCEPT_MAX_PROMPTS 32
static int accept_run_generate(const char *dump_path, const char *model_path,
const char *corpus_path, const char *tag, int n_gen,
int max_docs, const char *system_override)
{
static uint32_t ids[CHAT_CORPUS_MAX_IDS];
char *prompts[ACCEPT_MAX_PROMPTS];
const char *system = NULL;
char *corpus_buffer = NULL;
const char *arm_name = "?";
pn_forward_fn arm;
pn_model *model;
FILE *dump = NULL;
int n_prompts, docs = 0, think_end, eos, rc = 0;
n_prompts = accept_load_corpus(corpus_path, &corpus_buffer, &system, prompts,
ACCEPT_MAX_PROMPTS);
if (n_prompts <= 0) {
fprintf(stderr, "generate: no prompts in %s\n", corpus_path);
free(corpus_buffer);
return 1;
}
if (system_override)
system = system_override;
if (n_prompts > max_docs)
n_prompts = max_docs;
model = pn_load(model_path);
if (!model) {
fprintf(stderr, "generate: load failed\n");
free(corpus_buffer);
return 1;
}
/* THE ARM HAS TO BE THE ONE THAT SHIPS. Acceptance is a property of the model's own
* output distribution, so a CPU-forward stream would be measuring a different model.
* parity_select_accel_arm returns NULL when the engine left the model on its CPU
* forward, which is a refusal rather than a quiet substitution. */
arm = parity_select_accel_arm(model, &arm_name);
if (!arm) {
fprintf(stderr, "generate: REFUSED — no accelerator arm. Acceptance measured on a "
"different forward is not the shipped model's acceptance.\n");
pn_model_free(model);
free(corpus_buffer);
return 1;
}
model->forward = arm;
eos = ft_eos_id(model->tokenizer);
think_end = ft_token_to_id(model->tokenizer, "</think>");
printf("GENERATE arm=%s arch=%d n_vocab=%d chat_style=%d eos=%d think_end=%d corpus=%s "
"prompts=%d tag=%s n_gen=%d out=%s\n", arm_name, (int)model->config.arch,
model->config.n_vocab, model->chat_style, eos, think_end, corpus_path, n_prompts,
tag, n_gen, dump_path);
printf("SYSTEM \"%.*s%s\"\n", 160, system ? system : "(none)",
system && strlen(system) > 160 ? "..." : "");
fflush(stdout);
if (ngram_stream_open_write(dump_path, (uint32_t)model->config.n_vocab,
(uint32_t)model->config.arch, (uint32_t)eos,
think_end < 0 ? NGRAM_STREAM_NONE : (uint32_t)think_end,
arm_name, tag, &dump) != 0) {
pn_model_free(model);
free(corpus_buffer);
return 1;
}
for (docs = 0; docs < n_prompts; ) {
ngram_document doc = {0};
pn_token_seq seq = pn_chat_format_first((pn_chat_style)model->chat_style,
model->tokenizer, system, prompts[docs]);
int n_ids = seq.n_tokens;
if (!seq.tokens || n_ids <= 0 || n_ids > CHAT_CORPUS_MAX_IDS) {
fprintf(stderr, "generate: prompt %d did not tokenise (%d ids)\n", docs, n_ids);
free(seq.tokens);
rc = 1;
break;
}
memcpy(ids, seq.tokens, (size_t)n_ids * sizeof(*ids));
free(seq.tokens);
printf("-- doc %d: %d prompt tokens\n", docs, n_ids);
accept_echo(model, ids, n_ids, "prompt");
fflush(stdout);
if (accept_decode_document(model, ids, n_ids, n_gen, eos, think_end, &doc) != 0) {
free(doc.prompt);
free(doc.gen);
rc = 1;
break;
}
accept_echo(model, doc.gen, doc.n_gen, "output");
if (ngram_stream_write_document(dump, &doc) != 0) {
fprintf(stderr, "generate: dump write failed at document %d\n", docs);
rc = 1;
}
free(doc.prompt);
free(doc.gen);
if (rc != 0)
break;
docs++;
fflush(stdout);
}
/* The end marker is what tells the scorer the writer FINISHED. It is written on a clean
* early break too — the case it exists to catch is a killed process, which never reaches
* here at all, and throwing away hours of card time because the last document refused to
* tokenise would be its own defect. The document count is printed on both sides. */
if (ngram_stream_close_write(dump) != 0)
rc = 1;
printf("GENERATE %s: %d document(s)\n", rc == 0 ? "complete" : "FAILED", docs);
pn_model_free(model);
free(corpus_buffer);
return rc;
}
/* ================================================================
* Pass 2 — scoring (no model, no card)
* ================================================================ */
/** The drafter's key lengths. 3..2 is what the shipped throttle and the CUDA bench use. */
#define ACCEPT_NGRAM_MAX 3
#define ACCEPT_NGRAM_MIN 2
/** Run one (K, window, control, n-gram) cell over a whole dump. */
static void accept_cell_ng(const ngram_stream *s, int max_k, int window, ngram_control control,
int ng_max, int ng_min, ngram_accept_stats *phase)
{
memset(phase, 0, sizeof(*phase) * NGRAM_PHASE_COUNT);
for (int d = 0; d < s->n_docs; d++)
ngram_accept_replay(&s->docs[d], max_k, ng_max, ng_min, window, control, s->n_vocab,
ACCEPT_CONTROL_SEED + (uint64_t)d, phase);
}
/** The same at the shipped key lengths. */
static void accept_cell(const ngram_stream *s, int max_k, int window, ngram_control control,
ngram_accept_stats *phase)
{
accept_cell_ng(s, max_k, window, control, ACCEPT_NGRAM_MAX, ACCEPT_NGRAM_MIN, phase);
}
static void accept_print_row(const char *label, const ngram_accept_stats *a, int max_k)
{
long rounds = a->rounds + a->plain_rounds;
if (!rounds) {
printf(" %-14s (no rounds)\n", label);
return;
}
printf(" %-14s tok/fwd=%.3f | draft_rounds=%.0f%% accept=%.1f%% (%ld/%ld) | "
"E[delivered|drafted]=%.3f | emitted=%ld forwards=%ld\n",
label, ngram_tokens_per_forward(a), 100.0 * (double)a->rounds / (double)rounds,
a->drafted ? 100.0 * (double)a->accepted / (double)a->drafted : 0.0, a->accepted,
a->drafted, a->rounds ? 1.0 + (double)a->accepted / (double)a->rounds : 0.0,
a->emitted, a->forwards);
printf(" %-14s delivered histogram:", "");
for (int i = 1; i <= max_k + 1; i++)
printf(" %d:%ld", i, a->hist[i]);
printf("\n");
}
/** The K ladder, split into reasoning and answer, plus the combined figure. */
static void accept_report_k_ladder(const ngram_stream *s)
{
printf("\n== K LADDER (window=all, ngram 3..2, control=none)\n");
for (int i = 0; i < ACCEPT_K_COUNT; i++) {
ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0};
accept_cell(s, accept_k_ladder[i], 0, NGRAM_CONTROL_NONE, phase);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]);
printf("-- K=%d (verify %d rows)\n", accept_k_ladder[i], accept_k_ladder[i] + 1);
if (phase[NGRAM_PHASE_REASONING].forwards) {
accept_print_row("reasoning", &phase[NGRAM_PHASE_REASONING], accept_k_ladder[i]);
accept_print_row("answer", &phase[NGRAM_PHASE_ANSWER], accept_k_ladder[i]);
}
accept_print_row("combined", &all, accept_k_ladder[i]);
}
}
/** Acceptance against how much history the drafter is allowed to search. */
static void accept_report_windows(const ngram_stream *s, int max_k)
{
printf("\n== HISTORY WINDOW at K=%d — does MORE context help?\n", max_k);
for (int i = 0; i < ACCEPT_WINDOW_COUNT; i++) {
ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0};
char label[32];
accept_cell(s, max_k, accept_window_ladder[i], NGRAM_CONTROL_NONE, phase);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]);
if (accept_window_ladder[i])
snprintf(label, sizeof(label), "window %d", accept_window_ladder[i]);
else
snprintf(label, sizeof(label), "window all");
accept_print_row(label, &all, max_k);
}
}
/**
* Per-document tokens/forward, so a corpus mean cannot hide a bimodal split.
*
* A single blended figure over eight documents is exactly the shape that hid the reasoning
* / answer bimodality the phase note warned about; the same argument applies one level down.
*/
static void accept_report_per_document(const ngram_stream *s, int max_k)
{
printf("\n== PER DOCUMENT at K=%d — is the mean a mean, or one document?\n", max_k);
for (int d = 0; d < s->n_docs; d++) {
ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0};
memset(phase, 0, sizeof(phase));
ngram_accept_replay(&s->docs[d], max_k, ACCEPT_NGRAM_MAX, ACCEPT_NGRAM_MIN, 0,
NGRAM_CONTROL_NONE, s->n_vocab, ACCEPT_CONTROL_SEED, phase);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]);
printf(" doc %d %5d prompt %5d gen tok/fwd=%.3f accept=%.1f%% reasoning=%.3f "
"answer=%.3f%s\n", d, s->docs[d].n_prompt, s->docs[d].n_gen,
ngram_tokens_per_forward(&all),
all.drafted ? 100.0 * (double)all.accepted / (double)all.drafted : 0.0,
ngram_tokens_per_forward(&phase[NGRAM_PHASE_REASONING]),
ngram_tokens_per_forward(&phase[NGRAM_PHASE_ANSWER]),
s->docs[d].think_end_pos < 0
? " [NO </think>: the whole run is reasoning, counted as answer]" : "");
}
}
/** Acceptance against the context length the round actually ran at. */
static void accept_report_length(const ngram_stream *s, int max_k)
{
ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0};
accept_cell(s, max_k, 0, NGRAM_CONTROL_NONE, phase);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]);
printf("\n== CONTEXT LENGTH at K=%d — tok/fwd by history length at round start\n", max_k);
for (int b = 0; b < NGRAM_LEN_BUCKETS; b++) {
if (!all.bucket_forwards[b])
continue;
printf(" %-8s tok/fwd=%.3f (%ld forwards, %ld tokens)\n", ngram_length_bucket_name[b],
(double)all.bucket_emitted[b] / (double)all.bucket_forwards[b],
all.bucket_forwards[b], all.bucket_emitted[b]);
}
}
/**
* The drafter's KEY LENGTH, swept.
*
* Free to vary offline and it is a real lever: a short key hits more often and proposes worse,
* a long key the reverse. Reporting only the shipped 3..2 would leave the phase's verdict
* resting on a parameter nobody had varied.
*/
static void accept_report_ngram(const ngram_stream *s, int max_k)
{
static const int ng[][2] = { {2, 1}, {2, 2}, {3, 2}, {4, 2}, {6, 3}, {8, 4} };
char label[32];
printf("\n== N-GRAM KEY LENGTH at K=%d\n", max_k);
for (int i = 0; i < (int)(sizeof(ng) / sizeof(ng[0])); i++) {
ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0};
accept_cell_ng(s, max_k, 0, NGRAM_CONTROL_NONE, ng[i][0], ng[i][1], phase);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]);
snprintf(label, sizeof(label), "ngram %d..%d", ng[i][0], ng[i][1]);
accept_print_row(label, &all, max_k);
}
}
/** The two controls that must read LOW on the same bytes as the measurement. */
static void accept_report_controls(const ngram_stream *s, int max_k)
{
static const ngram_control ladder[] = { NGRAM_CONTROL_NONE, NGRAM_CONTROL_SHUFFLE,
NGRAM_CONTROL_RANDOM };
static const char *const name[] = { "measurement", "CTRL shuffle", "CTRL random" };
printf("\n== CONTROLS at K=%d — the two below must collapse to ~1.00 tok/fwd\n", max_k);
for (int i = 0; i < 3; i++) {
ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0};
accept_cell(s, max_k, 0, ladder[i], phase);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]);
accept_print_row(name[i], &all, max_k);
}
}
/** One line of the cross-corpus summary — the table the verdict is read off. */
typedef struct {
char tag[NGRAM_STREAM_NAME_LEN];
int docs;
long gen_tokens;
double control_tok_s;
double tpf[3]; /**< tokens per forward at K = 2, 4, 8 */
double accept_k4; /**< accepted / drafted at K=4, per cent */
double shuffle_k4; /**< the control that must read ~1.00 */
double random_k4;
long reasoning_forwards;
double tpf_reasoning_k4;
double tpf_answer_k4;
} accept_summary_row;
/** tokens/forward at one K over both phases. */
static double accept_tpf_at(const ngram_stream *s, int max_k, ngram_control control)
{
ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0};
accept_cell(s, max_k, 0, control, phase);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]);
return ngram_tokens_per_forward(&all);
}
/** Fill one summary row from a dump. */
static void accept_fill_summary(const ngram_stream *s, accept_summary_row *row)
{
ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0};
static const int k_of[3] = { 2, 4, 8 };
snprintf(row->tag, sizeof(row->tag), "%s", s->tag);
row->docs = s->n_docs;
for (int d = 0; d < s->n_docs; d++) {
row->gen_tokens += s->docs[d].n_gen;
row->control_tok_s += s->docs[d].decode_s;
}
row->control_tok_s = row->control_tok_s > 0.0
? (double)row->gen_tokens / row->control_tok_s : 0.0;
for (int i = 0; i < 3; i++)
row->tpf[i] = accept_tpf_at(s, k_of[i], NGRAM_CONTROL_NONE);
row->shuffle_k4 = accept_tpf_at(s, 4, NGRAM_CONTROL_SHUFFLE);
row->random_k4 = accept_tpf_at(s, 4, NGRAM_CONTROL_RANDOM);
accept_cell(s, 4, 0, NGRAM_CONTROL_NONE, phase);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]);
ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]);
row->accept_k4 = all.drafted ? 100.0 * (double)all.accepted / (double)all.drafted : 0.0;
row->reasoning_forwards = phase[NGRAM_PHASE_REASONING].forwards;
row->tpf_reasoning_k4 = ngram_tokens_per_forward(&phase[NGRAM_PHASE_REASONING]);
row->tpf_answer_k4 = ngram_tokens_per_forward(&phase[NGRAM_PHASE_ANSWER]);
}
/**
* The verdict table.
*
* `tokens per forward` is a CEILING on any speculative speedup, whatever the verify ends up
* costing: a verify that were free would still deliver exactly this many tokens per forward.
* The two control columns beside it are the same code over the same bytes with the structure
* removed, and they are printed on every run so nobody has to take the headline on trust.
*/
static void accept_print_summary(const accept_summary_row *rows, int n)
{
printf("\n================================================================\n");
printf("== SUMMARY — TOKENS PER FORWARD (a CEILING on any speculative speedup)\n");
printf("%-10s %4s %8s %9s | %6s %6s %6s | %8s %8s %8s | %8s %8s\n", "corpus", "docs",
"gen_tok", "ctl tok/s", "K=2", "K=4", "K=8", "acc@K4", "shuffle", "random",
"K4 reas", "K4 answ");
for (int i = 0; i < n; i++)
printf("%-10s %4d %8ld %9.3f | %6.3f %6.3f %6.3f | %7.1f%% %8.3f %8.3f | %8.3f %8.3f\n",
rows[i].tag, rows[i].docs, rows[i].gen_tokens, rows[i].control_tok_s,
rows[i].tpf[0], rows[i].tpf[1], rows[i].tpf[2], rows[i].accept_k4,
rows[i].shuffle_k4, rows[i].random_k4,
rows[i].reasoning_forwards ? rows[i].tpf_reasoning_k4 : 0.0,
rows[i].tpf_answer_k4);
printf("shuffle/random are the SAME code path with the history permuted and with the\n"
"proposals replaced by uniform ids. Both must sit at ~1.000 or no figure to their\n"
"left means anything.\n");
}
#define ACCEPT_MAX_DUMPS 16
static int accept_run_score(int n_paths, char **paths)
{
accept_summary_row rows[ACCEPT_MAX_DUMPS];
int n_rows = 0;
memset(rows, 0, sizeof(rows));
for (int i = 0; i < n_paths; i++) {
ngram_stream s;
double gen_tokens = 0.0, gen_seconds = 0.0;
if (ngram_stream_read(paths[i], &s) != 0)
return 1;
for (int d = 0; d < s.n_docs; d++) {
gen_tokens += s.docs[d].n_gen;
gen_seconds += s.docs[d].decode_s;
}
printf("\n================================================================\n");
printf("DUMP %s tag=%s arm=%s arch=%u n_vocab=%u docs=%d\n", paths[i], s.tag, s.arm,
s.arch, s.n_vocab, s.n_docs);
printf(" recorded plain greedy decode: %.0f tokens in %.1f s = %.3f tok/s "
"(%.4f s/token) — TODAY'S CONTROL\n", gen_tokens, gen_seconds,
gen_tokens / gen_seconds, gen_seconds / gen_tokens);
for (int d = 0; d < s.n_docs; d++)
printf(" doc %d: %d prompt + %d generated, think_end_pos=%d\n", d,
s.docs[d].n_prompt, s.docs[d].n_gen, s.docs[d].think_end_pos);
accept_report_k_ladder(&s);
accept_report_windows(&s, 4);
accept_report_ngram(&s, 4);
accept_report_per_document(&s, 4);
accept_report_length(&s, 4);
accept_report_controls(&s, 4);
if (n_rows < ACCEPT_MAX_DUMPS)
accept_fill_summary(&s, &rows[n_rows++]);
ngram_stream_free(&s);
}
accept_print_summary(rows, n_rows);
return 0;
}
/* ================================================================
* Known-answer self-test — proves the counter reads BOTH ends
* ================================================================ */
static int accept_selftest(void)
{
const int k = 4, period = 64, vocab = 248320, n_gen = 1024;
/* THE CEILING IS NOT k+1 EXACTLY. A recorded stream ends, so the last round delivers
* whatever is left: n_gen-1 tokens over ceil((n_gen-1)/(k+1)) rounds. Asserting the
* closed form rather than a tolerance is what makes this a known-answer arm — a
* simulator that mis-advanced by one token would miss it. */
const long deliverable = n_gen - 1;
const long full_rounds = (deliverable + k) / (k + 1);
const double ceiling = (double)deliverable / (double)full_rounds;
ngram_document periodic, random_doc;
ngram_accept_stats phase[NGRAM_PHASE_COUNT];
ngram_accept_stats ctrl[NGRAM_PHASE_COUNT];
double hi, lo, hi_shuffled;
int failures = 0;
ngram_selftest_periodic(&periodic, 1024, n_gen, period, (uint32_t)vocab);
ngram_selftest_random(&random_doc, 1024, n_gen, (uint32_t)vocab, 0xC0FFEEULL);
memset(phase, 0, sizeof(phase));
ngram_accept_replay(&periodic, k, 3, 2, 0, NGRAM_CONTROL_NONE, (uint32_t)vocab, 1, phase);
hi = ngram_tokens_per_forward(&phase[NGRAM_PHASE_ANSWER]);
memset(phase, 0, sizeof(phase));
ngram_accept_replay(&random_doc, k, 3, 2, 0, NGRAM_CONTROL_NONE, (uint32_t)vocab, 1, phase);
lo = ngram_tokens_per_forward(&phase[NGRAM_PHASE_ANSWER]);
memset(ctrl, 0, sizeof(ctrl));
ngram_accept_replay(&periodic, k, 3, 2, 0, NGRAM_CONTROL_SHUFFLE, (uint32_t)vocab, 1, ctrl);
hi_shuffled = ngram_tokens_per_forward(&ctrl[NGRAM_PHASE_ANSWER]);
printf("SELFTEST K=%d\n", k);
printf(" periodic (period %d, the CEILING) tok/fwd=%.4f expect %.4f\n", period,
hi, ceiling);
printf(" i.i.d. (vocab %d, the FLOOR) tok/fwd=%.4f expect 1.0000\n", vocab, lo);
printf(" periodic + shuffled history tok/fwd=%.4f expect ~1.0000\n",
hi_shuffled);
if (hi < ceiling - 1e-4 || hi > ceiling + 1e-4) {
printf(" FAIL: the simulator missed the closed-form ceiling %.4f\n", ceiling);
failures++;
}
if (lo > 1.02) {
printf(" FAIL: an i.i.d. stream must not accept drafts\n");
failures++;
}
if (hi_shuffled > 1.05) {
printf(" FAIL: the shuffle control did not destroy the structure\n");
failures++;
}
ngram_selftest_free(&periodic);
ngram_selftest_free(&random_doc);
printf("SELFTEST %s (%d failure(s))\n", failures ? "FAILED" : "PASSED", failures);
return failures ? 1 : 0;
}
/**
* Print what a corpus file parses to, without a model or a card.
*
* A corpus is the one input this measurement cannot check any other way: a separator typo
* silently merges two prompts into one, which changes the very repetition the drafter is
* being scored on. Cheap to look at, so look at it.
*/
static int accept_corpus_check(const char *path)
{
char *prompts[ACCEPT_MAX_PROMPTS];
const char *system = NULL;
char *buffer = NULL;
int n = accept_load_corpus(path, &buffer, &system, prompts, ACCEPT_MAX_PROMPTS);
if (n <= 0) {
fprintf(stderr, "corpus-check: %s parsed to %d prompts\n", path, n);
free(buffer);
return 1;
}
printf("CORPUS %s: %d prompt(s), system=%s\n", path, n, system ? "present" : "none");
if (system)
printf(" SYSTEM (%zu B): %.100s\n", strlen(system), system);
for (int i = 0; i < n; i++)
printf(" prompt %d: %7zu B | first line: %.90s\n", i, strlen(prompts[i]),
prompts[i]);
free(buffer);
return 0;
}
/* ================================================================
* Entry
* ================================================================ */
static void accept_usage(const char *argv0)
{
fprintf(stderr,
"usage:\n"
" %s --generate OUT.bin MODEL.gguf --corpus F --tag NAME [--n-gen N] "
"[--max-docs N]\n"
" %s --score DUMP.bin [DUMP.bin ...]\n"
" %s --selftest\n"
" %s --corpus-check CORPUS.txt\n",
argv0, argv0, argv0, argv0);
}
int main(int argc, char **argv)
{
const char *dump = NULL, *model = NULL, *corpus = NULL, *tag = "untagged";
const char *system = NULL;
int n_gen = 512, max_docs = ACCEPT_MAX_PROMPTS;
if (argc < 2) {
accept_usage(argv[0]);
return 1;
}
if (strcmp(argv[1], "--selftest") == 0)
return accept_selftest();
if (strcmp(argv[1], "--corpus-check") == 0 && argc > 2)
return accept_corpus_check(argv[2]);
if (strcmp(argv[1], "--score") == 0) {
if (argc < 3) {
accept_usage(argv[0]);
return 1;
}
return accept_run_score(argc - 2, argv + 2);
}
if (strcmp(argv[1], "--generate") != 0 || argc < 4) {
accept_usage(argv[0]);
return 1;
}
dump = argv[2];
model = argv[3];
for (int i = 4; i < argc; i++) {
if (strcmp(argv[i], "--corpus") == 0 && i + 1 < argc)
corpus = argv[++i];
else if (strcmp(argv[i], "--tag") == 0 && i + 1 < argc)
tag = argv[++i];
else if (strcmp(argv[i], "--n-gen") == 0 && i + 1 < argc)
n_gen = atoi(argv[++i]);
else if (strcmp(argv[i], "--max-docs") == 0 && i + 1 < argc)
max_docs = atoi(argv[++i]);
else if (strcmp(argv[i], "--system") == 0 && i + 1 < argc)
system = argv[++i];
else {
fprintf(stderr, "unknown argument %s\n", argv[i]);
return 1;
}
}
if (!corpus) {
accept_usage(argv[0]);
return 1;
}
if (n_gen < 8)
n_gen = 8;
if (n_gen > ACCEPT_MAX_GEN)
n_gen = ACCEPT_MAX_GEN;
if (max_docs < 1 || max_docs > ACCEPT_MAX_PROMPTS)
max_docs = ACCEPT_MAX_PROMPTS;
return accept_run_generate(dump, model, corpus, tag, n_gen, max_docs, system);
}