/* 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 #include #include #include #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`) 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, ""); 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 : 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); }