Repository navigation
kolibri1: n-gram spec-decode on the engine path — 14.1 tok/s bench, byte-identical chains - #3450
Conversation
The probe (ISSUE-LOCAL-01M4P22PX6BPRQAY1CK20FZQF6) found the n-gram spec-decode machinery generic end to end and pinned exactly one blocker: LoadedEngine::MakeKVCacheMaybeSpec built MakeQwen3_5KVCacheSpec for ANY speculative config, ignoring the model's own make_kv_cache, so a spec config on kolibri injected a MambaSpec group with empty shapes plus an unused fa_draft group and the runner refused at construction (runner.cpp:1074). The seam now builds the model's own KV spec and takes the widened qwen3.5 branch only when spec->use_eagle() or the model's own groups carry a recurrent/mamba group. A draft-free n-gram config on a model with no recurrent group keeps the model's own spec: the drafts are ordinary scheduled tokens, so the target's groups are the whole cache need and no draft-proposal group is added (fa_draft serves only the MTP draft head's paged KV, a forward n-gram does not have). The default (no spec config) path is byte-identical. qwen3.5's ngram path keeps the widened spec, so the landed 27B ngram gate is untouched. No new engine wiring was needed: EngineParams::speculative_config (the C ABI v6 field / --speculative-config) already fed the generic scheduler slots, engine draft pull, runner verify and n-gram proposer; the e2e gate proves the loop ran. test_kolibri1_ngram_spec flips from the pinned blocker into a checkpoint-free seam unit (kolibri keeps its own 2 groups under an ngram spec config — no MambaSpec, no fa_draft, byte-identical to spec-off — while qwen3.5 still widens) plus the engine e2e gate: spec-ON == spec-OFF byte-identical on 9/9 prompts (bench prompt + 8 W3 goldens), acceptance exactly 53/53 on the bench prompt and 146/270 on the goldens (the probe harness's numbers), engine decode tok/s 4.34 -> 13.39 on the bench prompt (3.08x, clears the 12 tok/s target; harness reference 4.92 -> 14.51) and 6.80 -> 8.17 on the goldens (1.20x), measured with the synchronous scheduler on both arms (ngram forces sync). W3 unchanged: 900/900 with spec off; with spec ON the chains are byte-identical to spec-OFF, so the torch-golden relationship (4/8 end-to-end identical, 4 adjudicated near-tie flips) is unchanged. test_capi gains the ABI reach pin (a valid ngram speculative_config reaches the engine load). The row spec's ## Now and the canonical issue's Resolution carry the record; the engine-matrix anchor for ENG-MOE-LOADSTREAM is re-anchored to the shifted FromModelDir line. Gates: seam unit 21/21; e2e 83/83 (104/104 both cases); acceptance sim 90/90; probe harness 17/17; W3 900/900; decode bench anchor 109726; test_kolibri1 234/234; test_kolibri1_numa 11/11; seam-adjacent suites (test_loaded_engine_dense 157, test_model_registry 1229, test_deepseek_v4_scaffold 684, test_kv_pool_min_fit 16, test_runner 1918, test_kv_cache_fp8_wiring 487) green; preflight green; record-anchor rot 0. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:zai/glm-5.3-flash [maki]
localai-org-maint-bot
left a comment
There was a problem hiding this comment.
@mudler @richiejp Focused source review at 7675d31: the Kolibri cache dispatch addresses the injected Mamba group, and the added engine test checks both token equality and nonzero speculation. Two validation claims need attention before treating this as merge-ready.
-
The new C API test (
tests/capi/test_capi.cpp, "a valid ngram speculative_config reaches the engine load") does not prove forwarding into the engine. It loads a nonexistent directory and expectsVLLM_ERR_MODEL_LOAD. Ifep.speculative_configis discarded immediately beforeFromModelDirinsrc/capi/vllm_c.cpp, the missing-directory error and null handle still satisfy both assertions. The C++ engine test bypasses the C API, so it does not cover that break either. Please add a C API behavior check that fails when forwarding is removed, or narrow the claim to acceptance of the JSON document and leave C API reachability pending. -
The new throughput calculation divides the full output count (64, or 8*32) by elapsed time starting after the first output arrives. Tokens already present at
t_firstare outside that interval. Please record that initial count per request and subtract it when calculating decode throughput; retain full counts for wall throughput. This matters for speculative output, where an engine step can emit several tokens. Update the reported measurements with the corrected denominator/numerator pairing.
These are source-level findings. I could not run the native or model-backed gates here: Python, CMake, and a C++ compiler are unavailable. I have not independently confirmed the timing/parity claims or completed the repository's mutation-review gate.
…ode denominator Two review findings on 7675d31 (PR localai-org#3450), both addressed: (1) The C API reach pin was vacuous. "a valid ngram speculative_config reaches the engine load" loads a nonexistent directory and expects VLLM_ERR_MODEL_LOAD, so it passes whether or not the parsed config is forwarded into EngineParams. test_capi.cpp now pins the field in two halves (new case "capi: speculative_config is parsed AND forwarded into the engine"). The PARSE pin: an invalid ngram window is a caller error, VLLM_ERR_INVALID_ARGUMENT, never MODEL_LOAD, handle null. The FORWARDING pin: a drafter-chain document the ABI layer accepts is refused by name by the engine's up-front ResolveSpecConfig (FromModelDir's first act, ahead of every path/config/tokenizer/weight operation), so against a missing checkpoint the load fails with the spec-validation refusal, not the missing-directory error. The valid-ngram case's claim is narrowed to what it actually proves (parse acceptance). The engine-side refusal surfaces through the load's generic catch, so its STATUS is VLLM_ERR_MODEL_LOAD either way — the MESSAGE is the discriminator, pinned deliberately. Mutation-verified both ways: reverting the whole parse+assignment block in src/capi/vllm_c.cpp fails the parse pin (and the pre-existing malformed case) while the valid-ngram case stays green — the reviewer's exact vacuousness; dropping only the assignment into EngineParams fails the forwarding pin. vllm_c.cpp restored byte-for-byte after each mutation. (2) The e2e gate's decode tok/s divided the FULL output count by the interval from the first output to finish, billing tokens already present at t_first (several at once under spec decode) with no generation time. The gate now records the per-request output count at t_first and divides (final_count - count_at_first) by the decode interval; wall tok/s keeps the full count. Re-measured on this box (t32 + 32 GiB cache, under flock, VT_ASYNC_SCHED=0, the A/B of record): bench prompt decode 4.53 -> 13.78 tok/s (3.04x; wall 3.04 -> 5.89, 1.93x); goldens aggregate decode 6.27 -> 8.13 tok/s (1.30x; wall 5.46 -> 6.91, 1.27x). The corrected decode figures read modestly lower than the overcounted ones (the operator's verification measured 14.06 bench / 8.60 goldens decode; its wall figures 3.08 -> 5.86 and 5.47 -> 7.08 are unchanged within noise). Default-scheduler run for the record: bench decode 4.80 -> 15.72 (wall 3.18 -> 6.34), goldens decode 6.35 -> 8.98 (wall 5.39 -> 7.60). The fix is reporting-only: acceptance (53/53 bench, 146/270 goldens) and byte-identity (9/9) are unchanged, and the 12 tok/s target is still cleared through the engine. The canonical issue's Resolution and the row spec carry the corrected figures and denominator. Gates: e2e 83/83 in BOTH VT_ASYNC_SCHED states; seam unit 21/21; acceptance sim 90/90; W3 900/900; test_capi full suite 75/75 (743/743 assertions, including the new two-pin case 8/8); staged preflight green; record-anchor rot 0. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:zai/glm-5.3-flash [maki]
The spec-decode probe (#3449) proved the mechanism; this lands it on the engine's serving path. Review findings on this PR are addressed (the two-pin C API reach test, the corrected decode denominator) — see below.
The one seam —
LoadedEngine::MakeKVCacheMaybeSpec(src/vllm/entrypoints/model_loader.cpp:1757-1781) builtMakeQwen3_5KVCacheSpecfor ANY spec config, ignoring the model's own cache spec, so kolibri under a spec config got a MambaSpec and the runner refused. Now the model's own spec is built first; the widened qwen3.5 branch is taken only when the config uses EAGLE or the model's groups carry a recurrent/mamba group. Verified howfa_draftis consumed before deciding kolibri needs no draft-proposal group: it is allocated only for the MTP draft head's own paged KV, a forward the n-gram proposer does not have. Default (no spec config) path byte-identical; qwen3.5's landed n-gram gate untouched.The wiring — no new engine code:
EngineParams::speculative_config(C ABI v6 field + the server's--speculative-config) already feeds the generic scheduler slots, draft install/rollback,sample_tokens_with_rejectiongreedy accept-iff-argmax, and the n-gram proposer. The e2e gate proves the whole loop throughLoadedEngine.Review finding 1 (C API reach pin was vacuous) — fixed in tests/capi/test_capi.cpp: a two-pin case now proves the field is parsed AND forwarded: a malformed spec (bad n-gram window) fails with the spec-validation error class (
VLLM_ERR_INVALID_ARGUMENT, notVLLM_ERR_MODEL_LOAD), and a drafter-chain document the ABI parse accepts is refused BY NAME by the engine's up-frontResolveSpecConfig— the error message discriminates (vllm_cpp.drafter_chainrefusal vsmodel path is not a directory); the status isVLLM_ERR_MODEL_LOADeither way through the load's generic catch, documented in the test. Mutation-proven: reverting the parse block reds both pins (the pre-existing malformed case reds too — the reviewer's exact vacuousness), reverting only the assignment reds the forwarding pin. The valid-ngram case's claim is narrowed to parse-acceptance.Review finding 2 (decode denominator overcounted) — fixed in the e2e gate: the per-request output count at
t_firstis recorded; decode tok/s = (final_count − count_at_first) / decode_interval; wall keeps the full count. Corrected figures (t32+32GiB, sync scheduler, the A/B of record): engine bench prompt decode 4.53 -> 13.78 tok/s (3.04x; wall 3.04 -> 5.89); goldens aggregate decode 6.27 -> 8.13 tok/s (1.30x; wall 5.46 -> 6.91). Default-scheduler run for the record: bench decode 4.80 -> 15.72 (wall 3.18 -> 6.34), goldens 6.35 -> 8.99. The 12 tok/s target is cleared through the engine under the corrected denominator. Operator verification reproduced the corrected figures within run noise (13.52 bench / 7.87 goldens decode).Acceptance is exact by construction (accept iff target greedy argmax agrees): spec-ON == spec-OFF byte-identical on 9/9 prompts (bench + 8 W3 goldens); acceptance matches the pinned simulation (53/53 bench, 146/270 goldens). W3: 900/900 with spec off; spec-on chains identical so the torch-golden relationship is unchanged (4/8 end-to-end identical, 4 adjudicated near-ties). Gates: e2e 83/83 in both scheduler modes; seam unit 21/21; acceptance sim 90/90; test_capi 75/75 (743/743, incl. the new two-pin case); test_kolibri1 234/234; test_kolibri1_numa 11/11; seam-adjacent suites green; preflight green; record-anchor rot 0.
Issue: ISSUE-LOCAL-01M4P22PX6BPRQAY1CK20FZQF6 (Resolution carries the record and the corrected figures).
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:zai/glm-5.3-flash [maki]