320 lines
12 KiB
C
320 lines
12 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/* Phase 9.1 gate bench: does verifying D rows in ONE forward cost materially
|
|
* less than D sequential single-token forwards?
|
|
*
|
|
* This is the decision gate for speculative decoding on Kepler-class hardware.
|
|
* Pure measurement: it changes no engine behaviour and is not part of the test
|
|
* suite (needs a model and a GPU).
|
|
*
|
|
* Three cost curves, all at the SAME context depth, on ONE model load:
|
|
*
|
|
* A. SEQUENTIAL single-token forward (`model->forward`) — the legacy
|
|
* single-user decode step. The thing speculation must beat D-for-D.
|
|
* B. SPECULATIVE VERIFY of D rows (`pn_qwen35moe_spec_verify` +
|
|
* `pn_qwen35moe_spec_accept`) — one forward carrying D candidate rows of
|
|
* ONE session. accept(1) is timed with it: that is the WORST-CASE commit
|
|
* (full SSM rollback); accept(D) is free by construction (the commit
|
|
* returns immediately when n_accept == n).
|
|
* C. BATCHED SLOTS step (`pn_qwen35moe_decode_slots`) at K slots — the
|
|
* serving path's cost as a function of rows-in-one-forward, which is what
|
|
* a K-user speculative verify would have to pay.
|
|
*
|
|
* The K=1 gate is B(D) vs D x A. The K=2 gate is C(2D) vs D x C(2): per round
|
|
* two users each advance up to D tokens in one 2D-row forward, versus D
|
|
* ordinary two-row steps.
|
|
*
|
|
* Build (CUDA host): make USE_CUDA=1 tests/bench_spec_verify_gate
|
|
* Run: QWEN35_GPU_SPLIT_LAYER=20 QWEN35_GPU_DIE_A=6 QWEN35_GPU_DIE_B=7 \
|
|
* QWEN35_KV_Q4=1 QWEN35_MAX_SEQ=8192 QWEN35_FUSE=0x37 \
|
|
* ./tests/bench_spec_verify_gate <model.gguf> [depth] [reps] [chunk]
|
|
* Env: QWEN35_GATE_ROWS ("1,2,4,8") verify widths D to sweep
|
|
* QWEN35_GATE_SLOTS ("1,2,4,8") slot counts K to sweep (0 skips C)
|
|
*/
|
|
#include "../include/pantheon.h"
|
|
#include "../src/core/engine.h"
|
|
#include "../src/models/qwen35moe.h"
|
|
#include "tensor/tensor.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#ifndef USE_CUDA
|
|
int main(void) { fprintf(stderr, "built without USE_CUDA\n"); return 1; }
|
|
#else
|
|
|
|
#define GATE_MAX_POINTS 12
|
|
#define GATE_MAX_SLOTS 16
|
|
#define GATE_MAX_ROWS 64
|
|
|
|
static double now_s(void)
|
|
{
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
|
|
}
|
|
|
|
static int argmax_f32(const float *x, int n)
|
|
{
|
|
int best = 0;
|
|
for (int i = 1; i < n; i++)
|
|
if (x[i] > x[best]) best = i;
|
|
return best;
|
|
}
|
|
|
|
/* Parse "1,2,4,8" into out[]; returns count (0 on empty/"0"). */
|
|
static int parse_int_list(const char *spec, int *out, int max_out)
|
|
{
|
|
int n = 0;
|
|
while (spec && *spec && n < max_out) {
|
|
char *end = NULL;
|
|
long v = strtol(spec, &end, 10);
|
|
if (end == spec) break;
|
|
if (v > 0) out[n++] = (int)v;
|
|
spec = (*end == ',') ? end + 1 : end;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
/* Deterministic, non-degenerate seed context so the KV/attention is realistic
|
|
* (not one repeated id). `user` keeps concurrent slots distinct. */
|
|
static void fill_seed_context(uint32_t *buf, int depth, int n_vocab, int user)
|
|
{
|
|
uint32_t base = 100u + (uint32_t)(user * 131);
|
|
for (int i = 0; i < depth; i++) {
|
|
uint32_t t = base + (uint32_t)((i * 7u + user * 13u) % 4096u);
|
|
buf[i] = t % (uint32_t)n_vocab;
|
|
}
|
|
}
|
|
|
|
/* Chunked cold prefill of `depth` tokens; returns the last position's argmax
|
|
* (the first decode token) or -1. Untimed warm-up in every section. */
|
|
static int prefill_to_depth(pn_model *model, pn_state *st, const uint32_t *seed,
|
|
int depth, int chunk, int n_vocab)
|
|
{
|
|
int last = -1;
|
|
for (int off = 0; off < depth; off += chunk) {
|
|
int n = depth - off < chunk ? depth - off : chunk;
|
|
tm_tensor *out =
|
|
pn_qwen35moe_forward_batch_cuda(model, seed + off, n, st, NULL);
|
|
if (!out) return -1;
|
|
if (off + n >= depth) last = argmax_f32(tm_data_f32(out), n_vocab);
|
|
tm_free(out);
|
|
}
|
|
return last;
|
|
}
|
|
|
|
/* --- A. sequential single-token forward -------------------------------- */
|
|
|
|
/* Times `reps` ordinary single-token forwards from `tok`. Advances st->pos by
|
|
* reps. Returns ms/forward, or -1 on failure. */
|
|
static double measure_sequential_step_ms(pn_model *model, pn_state *st,
|
|
uint32_t tok, int reps, int n_vocab)
|
|
{
|
|
tm_tensor *out = model->forward(model, tok, st); /* warm-up, untimed */
|
|
if (!out) return -1.0;
|
|
tok = (uint32_t)argmax_f32(tm_data_f32(out), n_vocab);
|
|
tm_free(out);
|
|
double t0 = now_s();
|
|
for (int i = 0; i < reps; i++) {
|
|
out = model->forward(model, tok, st);
|
|
if (!out) return -1.0;
|
|
tok = (uint32_t)argmax_f32(tm_data_f32(out), n_vocab);
|
|
tm_free(out);
|
|
}
|
|
return 1e3 * (now_s() - t0) / reps;
|
|
}
|
|
|
|
/* --- B. D-row speculative verify ---------------------------------------- */
|
|
|
|
/* Times `reps` rounds of verify(D) + accept(1) at the state's current depth.
|
|
* accept(1) is the WORST-CASE commit (full SSM rollback from snapshot 0); it
|
|
* advances st->pos by exactly 1 per round, so the depth drifts by `reps`.
|
|
* Splits the round into *verify_ms and *commit_ms. Returns 0, or -1. */
|
|
static int measure_verify_round_ms(pn_model *model, pn_state *st, int rows,
|
|
int reps, int n_vocab,
|
|
double *verify_ms, double *commit_ms)
|
|
{
|
|
uint32_t block[GATE_MAX_ROWS];
|
|
int32_t argmax_out[GATE_MAX_ROWS];
|
|
double verify_total = 0.0, commit_total = 0.0;
|
|
for (int r = 0; r < rows; r++)
|
|
block[r] = (uint32_t)((1000 + r * 37) % n_vocab);
|
|
|
|
if (pn_qwen35moe_spec_verify(model, block, rows, st, argmax_out) != 0)
|
|
return -1; /* warm-up, untimed */
|
|
if (pn_qwen35moe_spec_accept(model, st, 1) != 0)
|
|
return -1;
|
|
for (int i = 0; i < reps; i++) {
|
|
double t0 = now_s();
|
|
if (pn_qwen35moe_spec_verify(model, block, rows, st, argmax_out) != 0)
|
|
return -1;
|
|
double t1 = now_s();
|
|
if (pn_qwen35moe_spec_accept(model, st, 1) != 0)
|
|
return -1;
|
|
double t2 = now_s();
|
|
verify_total += t1 - t0;
|
|
commit_total += t2 - t1;
|
|
}
|
|
*verify_ms = 1e3 * verify_total / reps;
|
|
*commit_ms = 1e3 * commit_total / reps;
|
|
return 0;
|
|
}
|
|
|
|
/* --- C. batched slots step ---------------------------------------------- */
|
|
|
|
/* Seeds K users to `depth`, binds them, and times `reps` decode_slots steps.
|
|
* Returns ms/step (one step = K rows in one forward), or -1 on failure. */
|
|
static double measure_slots_step_ms(pn_model *model, int n_slots, int depth,
|
|
int reps, int chunk, int n_vocab)
|
|
{
|
|
pn_state *states[GATE_MAX_SLOTS] = { 0 };
|
|
uint32_t tokens[GATE_MAX_SLOTS];
|
|
float *logits[GATE_MAX_SLOTS];
|
|
double step_ms = -1.0;
|
|
uint32_t *seed = malloc((size_t)depth * sizeof(uint32_t));
|
|
if (!seed) return -1.0;
|
|
|
|
for (int u = 0; u < n_slots; u++) {
|
|
fill_seed_context(seed, depth, n_vocab, u);
|
|
states[u] = pn_state_new(model);
|
|
if (!states[u]) goto done;
|
|
int first = prefill_to_depth(model, states[u], seed, depth, chunk,
|
|
n_vocab);
|
|
if (first < 0) goto done;
|
|
if (pn_qwen35moe_decode_slot_bind(model, states[u], u, n_slots) != 0)
|
|
goto done;
|
|
tokens[u] = (uint32_t)first;
|
|
}
|
|
if (pn_qwen35moe_decode_slots(model, tokens, states, n_slots, logits) != 0)
|
|
goto done; /* warm-up, untimed */
|
|
for (int u = 0; u < n_slots; u++)
|
|
tokens[u] = (uint32_t)argmax_f32(logits[u], n_vocab);
|
|
|
|
double t0 = now_s();
|
|
for (int i = 0; i < reps; i++) {
|
|
if (pn_qwen35moe_decode_slots(model, tokens, states, n_slots,
|
|
logits) != 0)
|
|
goto done;
|
|
for (int u = 0; u < n_slots; u++)
|
|
tokens[u] = (uint32_t)argmax_f32(logits[u], n_vocab);
|
|
}
|
|
step_ms = 1e3 * (now_s() - t0) / reps;
|
|
|
|
done:
|
|
for (int u = 0; u < n_slots; u++)
|
|
if (states[u]) pn_state_free(states[u]);
|
|
free(seed);
|
|
return step_ms;
|
|
}
|
|
|
|
/* --- driver -------------------------------------------------------------- */
|
|
|
|
/* Section A+B: sequential baseline then the verify sweep, on one session. */
|
|
static int run_single_session_sections(pn_model *model, int depth, int reps,
|
|
int chunk, int n_vocab,
|
|
const int *rows, int n_rows,
|
|
double *seq_ms_out)
|
|
{
|
|
uint32_t *seed = malloc((size_t)depth * sizeof(uint32_t));
|
|
pn_state *st = pn_state_new(model);
|
|
int rc = -1;
|
|
if (!seed || !st) goto done;
|
|
fill_seed_context(seed, depth, n_vocab, 0);
|
|
int first = prefill_to_depth(model, st, seed, depth, chunk, n_vocab);
|
|
if (first < 0) { fprintf(stderr, "prefill failed (depth=%d)\n", depth); goto done; }
|
|
|
|
double seq_ms = measure_sequential_step_ms(model, st, (uint32_t)first,
|
|
reps, n_vocab);
|
|
if (seq_ms < 0) { fprintf(stderr, "sequential forward failed\n"); goto done; }
|
|
*seq_ms_out = seq_ms;
|
|
printf("\n[A] sequential single-token forward depth~%d reps=%d\n",
|
|
depth, reps);
|
|
printf(" %.2f ms/token %.2f tok/s\n", seq_ms, 1e3 / seq_ms);
|
|
|
|
printf("\n[B] speculative verify of D rows (one session, one forward)\n");
|
|
printf(" D pos verify_ms commit_ms round_ms ms/row "
|
|
"round/(D*seq) breakeven_acc\n");
|
|
for (int i = 0; i < n_rows; i++) {
|
|
int d = rows[i];
|
|
if (d > GATE_MAX_ROWS) continue;
|
|
double verify_ms = 0, commit_ms = 0;
|
|
int pos_before = st->pos;
|
|
if (measure_verify_round_ms(model, st, d, reps, n_vocab,
|
|
&verify_ms, &commit_ms) != 0) {
|
|
printf(" %2d %5d FAILED\n", d, pos_before);
|
|
continue;
|
|
}
|
|
double round_ms = verify_ms + commit_ms;
|
|
printf(" %2d %5d %8.2f %8.2f %8.2f %7.2f %10.3f "
|
|
"%10.2f\n",
|
|
d, pos_before, verify_ms, commit_ms, round_ms, round_ms / d,
|
|
round_ms / (d * seq_ms), round_ms / seq_ms);
|
|
}
|
|
rc = 0;
|
|
done:
|
|
if (st) pn_state_free(st);
|
|
free(seed);
|
|
return rc;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
if (argc < 2) {
|
|
fprintf(stderr,
|
|
"usage: %s <model.gguf> [depth] [reps] [chunk]\n", argv[0]);
|
|
return 1;
|
|
}
|
|
int depth = argc > 2 ? atoi(argv[2]) : 1024;
|
|
int reps = argc > 3 ? atoi(argv[3]) : 20;
|
|
int chunk = argc > 4 ? atoi(argv[4]) : 256;
|
|
if (depth < 8) depth = 8;
|
|
if (reps < 1) reps = 1;
|
|
|
|
int rows[GATE_MAX_POINTS], slots[GATE_MAX_POINTS];
|
|
const char *rows_env = getenv("QWEN35_GATE_ROWS");
|
|
const char *slots_env = getenv("QWEN35_GATE_SLOTS");
|
|
int n_rows = parse_int_list(rows_env ? rows_env : "1,2,4,8", rows,
|
|
GATE_MAX_POINTS);
|
|
int n_slots = parse_int_list(slots_env ? slots_env : "1,2,4,8", slots,
|
|
GATE_MAX_POINTS);
|
|
|
|
pn_model *model = pn_load(argv[1]);
|
|
if (!model) { fprintf(stderr, "load failed: %s\n", argv[1]); return 1; }
|
|
int n_vocab = model->config.n_vocab;
|
|
printf("phase 9.1 verify gate: %s\n", argv[1]);
|
|
printf(" n_vocab=%d n_layer=%d depth=%d reps=%d chunk=%d\n",
|
|
n_vocab, model->config.n_layer, depth, reps, chunk);
|
|
|
|
double seq_ms = 0;
|
|
if (run_single_session_sections(model, depth, reps, chunk, n_vocab,
|
|
rows, n_rows, &seq_ms) != 0) {
|
|
pn_model_free(model);
|
|
return 1;
|
|
}
|
|
|
|
if (n_slots > 0) {
|
|
printf("\n[C] batched slots step: K rows of K sessions in one forward\n");
|
|
printf(" K step_ms ms/row step/(K*seq) agg_tok/s\n");
|
|
for (int i = 0; i < n_slots; i++) {
|
|
int k = slots[i];
|
|
if (k > GATE_MAX_SLOTS) continue;
|
|
double step_ms = measure_slots_step_ms(model, k, depth, reps,
|
|
chunk, n_vocab);
|
|
if (step_ms < 0) { printf(" %2d FAILED\n", k); continue; }
|
|
printf(" %2d %8.2f %7.2f %10.3f %9.2f\n",
|
|
k, step_ms, step_ms / k, step_ms / (k * seq_ms),
|
|
1e3 * k / step_ms);
|
|
}
|
|
}
|
|
|
|
printf("\ndone\n");
|
|
fflush(stdout);
|
|
if (!getenv("QWEN35_EXIT_CLEAN"))
|
|
_Exit(0);
|
|
pn_model_free(model);
|
|
return 0;
|
|
}
|
|
#endif
|