141 lines
6.0 KiB
C
141 lines
6.0 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/* test_spec_generate.c - end-to-end check + speedup measurement for the lossless
|
|
* speculative generation loop (pn_spec_generate).
|
|
*
|
|
* This is the Part 2 core in isolation from Apple Vision: instead of a live
|
|
* VNRecognizeTextRequest draft it uses a synthetic draft provider that agrees
|
|
* with the model's true greedy tokens a tunable fraction of the time (a
|
|
* stand-in for a Vision transcript at a given acceptance). The provider aligns
|
|
* to the accepted prefix on every call, exactly as the real Vision provider will
|
|
* re-tokenize the remaining transcript conditioned on the accepted prefix (the
|
|
* BPE-seam fix), so it always proposes the next slots.
|
|
*
|
|
* It proves the whole generation loop is lossless (spec output == plain greedy
|
|
* output, bit for bit) and reports the realized decode speedup = tokens emitted
|
|
* / verify passes, at several draft acceptance levels and draft widths. It also
|
|
* checks the low-acceptance fallback to plain decode.
|
|
*
|
|
* Usage: test_spec_generate [model.gguf] [max_tokens]
|
|
*/
|
|
|
|
#include "../include/pantheon.h"
|
|
#include "../src/core/engine.h"
|
|
#include "../src/models/llama.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define PROMPT_LEN 32
|
|
|
|
static int g_nv = 0;
|
|
|
|
static uint32_t argmax(const float *l, int n) {
|
|
int b = 0; for (int i = 1; i < n; i++) if (l[i] > l[b]) b = i; return (uint32_t)b;
|
|
}
|
|
static void make_prompt(uint32_t *p, int n, int idx) {
|
|
uint32_t x = 0x9e3779b9u * (uint32_t)(idx + 1);
|
|
for (int i = 0; i < n; i++) { x^=x<<13; x^=x>>17; x^=x<<5; p[i]=(x%(uint32_t)(g_nv-4))+2; }
|
|
}
|
|
/* Prefill + greedy seed. */
|
|
static int prefill_seed(pn_model *m, pn_state *s, const uint32_t *pr, int P, uint32_t *seed) {
|
|
pn_state_reset(s);
|
|
tm_tensor *l = pn_llama_prefill(m, pr, NULL, P, s);
|
|
if (!l) return -1;
|
|
*seed = argmax((const float *)l->data, g_nv);
|
|
tm_free(l);
|
|
return 0;
|
|
}
|
|
/* Plain greedy reference via pn_llama_forward; fills ref[0..G-1]. */
|
|
static int ref_forward(pn_model *m, pn_state *s, const uint32_t *pr, int P, uint32_t *ref, int G) {
|
|
uint32_t seed;
|
|
if (prefill_seed(m, s, pr, P, &seed) != 0) return -1;
|
|
ref[0] = seed;
|
|
for (int t = 1; t < G; t++) {
|
|
tm_tensor *l = m->forward(m, ref[t-1], s);
|
|
if (!l) return -1;
|
|
ref[t] = argmax((const float *)l->data, g_nv);
|
|
tm_free(l);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Synthetic Vision-style draft provider: knows the model's true continuation
|
|
* (ref) and proposes it with probability keep_prob per token, else a wrong
|
|
* token. Aligned by the accepted-prefix length, so it self-resyncs each call. */
|
|
typedef struct { const uint32_t *ref; int ref_len; unsigned rng; double keep; } draft_ctx;
|
|
static int vision_like_draft(const uint32_t *prefix, int n_prefix,
|
|
uint32_t *out, int max_k, void *ud) {
|
|
(void)prefix;
|
|
draft_ctx *c = (draft_ctx *)ud;
|
|
int k = 0;
|
|
for (int i = 0; i < max_k; i++) {
|
|
int pos = n_prefix + i;
|
|
if (pos >= c->ref_len) break;
|
|
c->rng ^= c->rng<<13; c->rng ^= c->rng>>17; c->rng ^= c->rng<<5;
|
|
double u = (double)(c->rng & 0xFFFFFF) / (double)0x1000000;
|
|
if (u < c->keep) out[i] = c->ref[pos];
|
|
else out[i] = (c->ref[pos] ^ 1u) % (uint32_t)g_nv; /* wrong guess */
|
|
k++;
|
|
}
|
|
return k;
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *path = (argc > 1) ? argv[1] : "models/lightonocr-lm-Q4_K_M.gguf";
|
|
int G = (argc > 2) ? atoi(argv[2]) : 128;
|
|
pn_model *model = pn_load(path);
|
|
if (!model) { fprintf(stderr, "load failed: %s\n", path); return 1; }
|
|
pn_state *state = pn_state_new(model);
|
|
if (!state) return 1;
|
|
g_nv = model->config.n_vocab;
|
|
printf("model: %s n_vocab=%d n_layer=%d max_tokens=%d\n",
|
|
path, g_nv, model->config.n_layer, G);
|
|
|
|
uint32_t prompt[PROMPT_LEN];
|
|
make_prompt(prompt, PROMPT_LEN, 0);
|
|
|
|
uint32_t *ref = malloc((size_t)G * sizeof(uint32_t));
|
|
uint32_t *spec = malloc((size_t)G * sizeof(uint32_t));
|
|
if (ref_forward(model, state, prompt, PROMPT_LEN, ref, G) != 0) {
|
|
fprintf(stderr, "ref failed\n"); return 1;
|
|
}
|
|
|
|
const int Ks[] = { 4, 8 };
|
|
const double kps[] = { 0.86, 0.60, 0.30 }; /* target draft acceptance */
|
|
int fail = 0;
|
|
|
|
printf("\n%-4s %-6s %-8s %-8s %-9s %-8s %s\n",
|
|
"K", "keep", "accept", "passes", "emitted", "speedup", "lossless");
|
|
for (int ki = 0; ki < 2; ki++) {
|
|
for (int qi = 0; qi < 3; qi++) {
|
|
uint32_t seed;
|
|
if (prefill_seed(model, state, prompt, PROMPT_LEN, &seed) != 0) { fail=1; break; }
|
|
draft_ctx ctx = { ref, G, 0xABCDEF01u ^ (unsigned)(ki*131 + qi*7), kps[qi] };
|
|
pn_spec_stats st;
|
|
int n = pn_spec_generate(model, state, seed, G, Ks[ki], /*stop_id=*/-1,
|
|
vision_like_draft, &ctx,
|
|
/*min_accept_frac=*/0.45, /*probe_cycles=*/8,
|
|
spec, &st);
|
|
if (n < 0) { fprintf(stderr, "spec generate failed/unsupported\n"); fail=1; break; }
|
|
int diffs = 0;
|
|
for (int t = 0; t < n && t < G; t++) if (spec[t] != ref[t]) diffs++;
|
|
if (n != G) diffs++; /* must generate the same count with stop_id=-1 */
|
|
double accept = st.drafted ? (double)st.accepted/st.drafted : 0.0;
|
|
double sp = st.verify_passes ? (double)st.tokens_emitted/st.verify_passes : 0.0;
|
|
printf("%-4d %-6.2f %-8.3f %-8ld %-9ld %-7.2fx %s%s\n",
|
|
Ks[ki], kps[qi], accept, st.verify_passes, st.tokens_emitted, sp,
|
|
diffs==0 ? "IDENTICAL" : "DIFFERS", st.fell_back ? " (fellback)" : "");
|
|
if (diffs) fail = 1;
|
|
}
|
|
if (fail) break;
|
|
}
|
|
|
|
printf("\nRESULT: %s\n", fail ?
|
|
"FAIL" :
|
|
"PASS (speculative generation == plain greedy, bit for bit; speedup scales with acceptance)");
|
|
free(ref); free(spec);
|
|
pn_state_free(state); pn_model_free(model);
|
|
return fail ? 1 : 0;
|
|
}
|