MTP / speculative decoding β evaluation
Status (2026-07-14): both flavours shipped, characterized, and at throughput parity-or-better vs upstream b9859. Gemma assistant-MTP runs upstream's dual-context execution (Β§3e β bug-858's two root causes found+fixed). #609 depth-3 CLOSED (bug-2103 TinyBLAS f32 mmvf fix). #675 (Β§3h, patch
0128) closed the last per-cycle host gap (bug-2180common_sampler_clone~1.8 MiB deep-copy under cosmo dlmalloc = 1.07 ms/cycle + mmvf f16/bf16 verify lift): Qwen 35B NextN β12% β β1.7%..+3%, and A4B assistant now BEATS b9859 at every depth (9-prompt decode mean +6.6/+12.9/+7.6% at n1/n2/n3), accept bit-identical (pure-latency fix). Residuals: accept deficit at depth β₯2 (#495-class chained-draft numerics, β4.4pp aggregate at n3) and 31B/27B β2..β6% decode. Recommend n2βn3 on A4B, n3 on Qwen NextN. Current cross-engine numbers incl. the 9-prompt per-prompt table:three-way-tps.mdΒ§2026-07-14.
This document is the rigorous record for Multi-Token Prediction (MTP) speculative decoding in the opencoti llamafile fork, across both flavours and all supported architectures. It is the MTP-specific companion to context.md (RULER retrieval / DCA long-context) β where context.md proves quality at length, this proves speed without quality loss from speculation.
Throughput (gen tok/s), draft acceptance, and the correctness gate are reported per model. The single invariant across everything here:
MTP speculative decoding is lossless by construction. The target model's verify pass decides every emitted token; the draft head only proposes. So the only correctness question is whether our wiring preserves that invariant β proven per model by logit-equivalence (MTP-on vs the plain target), never by greedy token-match (FP-unstable; bug-270/356).
1. The two MTP flavours
Post-0.10.3 the two flavours are distinct --spec-type strings (the old unified --spec-type mtp
token is RETIRED) with distinct enums/graph types, dispatched in common/speculative.cpp /
server-context.cpp: the Gemma external assistant is --spec-type draft-assistant
(COMMON_SPECULATIVE_TYPE_MTP, our patch 0079), the Qwen NextN self-spec is --spec-type draft-mtp
(COMMON_SPECULATIVE_TYPE_DRAFT_MTP, upstream-native). They differ in where the draft head lives:
| Flavour | Models | Draft head | Cache | Rollback |
|---|---|---|---|---|
| External assistant (M6-S4) | Gemma-4 A4B/12B/27B/E2B/E4B | a separate gemma4_assistant GGUF loaded INTO the target (--mtp-head) |
iSWA (attention, seq_rm-able) | FULL β plain seq_rm of the rejected tail |
| NextN self-spec (M6-S5) | Qwen3.5-9B (qwen35), Qwen3.6-27B-Omnimerge (qwen35), Qwen3.6-35B-A3B (qwen35moe) | baked into the target GGUF (last nextn_predict_layers layer) β no second model |
hybrid (gated delta-net recurrent backbone + full-attention layers) | CKPT β recurrent state checkpoint + re-decode |
Selection is explicit per engine: --spec-type draft-assistant --mtp-head <gemma4_assistant.gguf> for
the Gemma external assistant; --spec-type draft-mtp (no --mtp-head; nextn_predict_layers>0 baked
into the target) for Qwen self-speculation. The TS adapter emits the right string from
specType:"draft-assistant" (+mtpHead) or specType:"draft-mtp" respectively (config.ts
resolveEnvSpecType; the retired "mtp" value is no longer accepted).
1a. Why qwen NextN needs a different rollback (checkpoint; upstream-native on 0.10.3 β W3b was opencoti's pre-bump port)
The qwen NextN models are hybrid: most backbone layers are gated delta-net (a recurrent
linear-attention whose state has no per-token history), and only the NextN/full-attention layers
keep a seq_rm-able KV cache. On a partial draft acceptance, the recurrent backbone has already
advanced through the rejected draft tokens and cannot erase its tail β llama_memory_seq_rm
returns false, and the upstream gate would simply disable speculation for these models.
We do not disable it (locked user directive). Instead we port upstream PR#19493's checkpoint idea, adapted for self-spec (the NextN draft head is stateless/regenerating, so upstream's discard-and-retry would re-emit the identical draft and livelock):
- Compat probe is three-way now (
common_speculative_is_compatβ NO / FULL / CKPT). A target that decodes but can't partial-erase its tail returns CKPT β spec stays enabled. - At draft time (CKPT mode) the server snapshots the committed-prefix recurrent state
(
llama_state_seq_get_data_ext,PARTIAL_ONLYβ only the recurrent half of the hybrid cache). - On a partial accept the server restores that checkpoint, trims the attention KV to the
prefix, then re-decodes the accepted tokens to re-advance both halves to exactly the accepted
sequence (the last token carries logits to seed the next draft's
h_prev).
Cost: in principle one extra small decode per partial-accept step + one PARTIAL_ONLY snapshot per draft
step (the per-token ring buffer of PR#22673 would make both O(1) β tracked as #457). PROFILED
2026-06-18 (Β§4b): on the 0.10.3 binary this path is DORMANT and the cost is MEASURED = 0 β upstream's
hybrid context supports bounded partial sequence removal (common_context_can_seq_rm), so for the
default n_max=3 draft the recurrent rollback (n_rollback β€ 3) is erased natively by seq_rm and the
checkpoint-restore/re-decode branch (use_ckpt_*) is never entered. The "seq_rm returns false" premise
above was the pre-bump W3b situation; the upstream-native 0.10.3 path no longer needs the re-decode at
shipped draft widths.
On the post-0.10.3 binary the shipped draft-mtp path is the upstream-native
common_speculative_impl_draft_mtp (NextN self-attention drafter + the checkpoint/recurrent-rollback above;
server spec_ckpt, server-context.cpp), and it is n_seq-aware β its samplers / backend-chains /
pending_h are all sized per seq, so it runs per-slot under --parallel (proven Β§5a/#485). This
supersedes the --parallel 1 limit of opencoti's retired pre-bump W2/W3b CKPT self-spec. There are
no // opencoti F5 M6-S5 source markers in the vendored tree: the NextN drafter and its rollback are
upstream-native; opencoti's only NextN-side vendored change is the W1 loader fix (#436 β load the NextN head
- skip DeltaNet on the NextN layer). See the W2/W3b design record under
.opencoti/reconcile/phase0-20260615/.
2. Hosts & methodology
| Host | GPU | Role |
|---|---|---|
| solidpc | 1Γ RTX 3090 (24 GB) | Throughput (off/on gen-tps) + correctness gates (logit-equiv). Q4_K_M / Q8_0 / IQ3 quantized models. |
| bs2 | RTX 6000-class | 256k-context MTP + DCA-on cells (large KV). |
- Throughput: same fixed decode prompt,
--temp 0, gen tok/s from the server completiontimings.predicted_per_second. off = no--spec-type; on = self-spec /--mtp-head. Gate:.opencoti/w6-qwen-mtp-tps.sh. - Acceptance: from the MTP NDJSON trace (
LLAMA_MTP_ACC_TRACE) β accepted/drafted draft tokens and accept events (statistics mtpin the server log corroborates). - Correctness: logit-equivalence β teacher-force shared prompts, compare per-position top-20
logprobs MTP-on vs plain; PRIMARY =
real_frac(real, non-benign-tie argmax disagreements; target 0).mean_tvis advisory only (it false-fails on spec-accepted tokens whose server returns empty top-logprobs β see the gemma precedent below). Gate:.opencoti/w4-qwen-nextn-logit-equiv.sh.
3. Gemma external-assistant MTP (M6-S4) β shipped
3.0 Recommended config (the adapter productizes this)
MTP is the largest single decode-throughput lever for the turbo / turbo_tcq KV tiers (it amortizes the
bandwidth-bound expert-weight reads across the draft window β see docs/evaluations/decode-levers.md L1/L2),
and it is logit-equivalent (Β§3a). The engine needs three llamafile flags acting together β
--mtp-head <gguf> --spec-type draft-assistant -ngld 99 β but as of #533 the opencoti adapter
(packages/opencoti-llamafile/src/config.ts, buildConfig) derives the companions so you set just one knob:
| You set | Adapter derives | Result |
|---|---|---|
mtpHead: "<assistant.gguf>" |
specType="draft-assistant", draftNGpuLayers=99 |
full gemma assistant-MTP, GPU-offloaded draft head |
specType: "draft-mtp" (no mtpHead) |
(nothing) | qwen NextN self-spec (rides the target's own offload) |
| (neither) | (nothing) | plain decode |
Explicit values always win (e.g. set draftNGpuLayers: 40 to override the 99 default). Env equivalents:
OPENCOTI_LLAMAFILE_MTP_HEAD, OPENCOTI_LLAMAFILE_SPEC_TYPE, OPENCOTI_LLAMAFILE_DRAFT_NGL. The -ngld 99
default matters: a CPU-resident draft head stalls the GPU and erases the win. Recommended pairing for turbo
tiers: ctypeK/ctypeV = turbo3_tcq + mtpHead = <A4B assistant> β turbo3_tcq decode β 89 tok/s on RTX 3090
(graphs+MTP, +69% over the 52.9 plain baseline). Covered by test/mtp-defaults.test.ts.
3.0a. Per-KV-tier MTP gain β re-confirmed on coherent output (#553, A4B v6-coder Q4_K_M, solidpc, 2026-06-24)
The Β§3.0 claim holds across the KV ladder. A4B chat coding prompt (in-domain, /v1/chat/completions,
--temp 0, 256 gen tok, graphs default, coherent-mtp.sh):
| KV tier | MTP-off tok/s | MTP-on tok/s | MTP gain | draft acceptance |
|---|---|---|---|---|
| f16 | 88.95 | 112.37 | +26% | 0.787 |
| turbo3 | 69.66 | 94.44 | +36% | 0.827 |
| turbo3_tcq | 56.63 | 75.63 | +34% | 0.652 |
Re-confirmed on the fresh 0121 binary (#654, A4B v7-coder Q4_K_M, solidPC RTX 3090, ctx 8192,
sp-mtp30a.sh, 2026-07-12): the per-tier win holds and is larger on the current engine β
| KV tier | MTP-off tok/s | MTP-on tok/s | MTP gain | draft acceptance |
|---|---|---|---|---|
| f16 | 76.58 | 137.03 | +79% | 0.912 |
| turbo3 | 65.67 | 97.10 | +48% | 0.864 |
| turbo3_tcq | 53.18 | 74.03 | +39% | 0.663 |
Same ordering (biggest on quantized tiers), same acceptance profile (turbo3_tcq's lower single-prompt 0.66 is the documented prompt-noise, not a deficit) β a clean reproduction on the shipping binary.
MTP is a real decode-tps win on every tier, biggest on the quantized-KV tiers (the Β§3.0 lever), and the
two graph layers compose (graphs survive turbo3_tcq + sparse-V: +13.4% / +13.1% on today's DSO, matching the
decode-levers L6 reference +13.1%). The single-prompt turbo3_tcq acceptance (0.65, BST prompt) is prompt
noise, not a deficit: averaged over 3 diverse coherent coding prompts (coherent-mtp-multi.sh) the means are
f16 0.745 / turbo3 0.757 / turbo3_tcq 0.703 β a ~0.05 spread within each tier's own prompt-to-prompt
variance, and on one prompt turbo3_tcq (0.766) beat turbo3 (0.721) and f16 (0.729). Consistent with
turbo3_tcq being the more accurate tier (Β§3a is logit-equivalent; 27B KLD 0.0346 < turbo3 0.047): no spec bug.
β οΈ Acceptance/tps MUST be measured on coherent output. Feeding an instruction-less prompt (e.g. a raw RULER/WikiText haystack to
/completion) collapses greedy decode into a degenerate repetition loop where logits are near-tied; draft acceptance then becomes meaningless noise and even f16 shows greedy MTP-off β MTP-on (impossible on real tasks β f16 is provenreal_frac=0, Β§3a). An earlier sweep on that prompt mis-measured turbo3_tcq acceptance at 0.25 and read "MTP hurts turbo" β a pure harness artifact (bug-710), not a kernel/codebook defect. Use/v1/chat/completionswith a real instruction. Gemma-4 thinking-coder writes its answer tochoices[0].message.reasoning_content, not.content(.contentis empty withfinish_reason=length) β parsereasoning_content. Latent note: theTURBO_TCQ_CBruntime codebook override reaches only the FA-VEC TU's__constant__, not the MMA-lift TU (d_turbo3_tcq_cb_cpy); the two baked tables are byte-identical so default decode is consistent, but an Aβ² codebook sweep would diverge draft (VEC) vs verify (MMA-lift) β fix before relying onTURBO_TCQ_CBfor spec/MTP runs.
3a. Correctness β logit-equivalence (A4B v6-coder Q4_K_M, solidpc)
s4-p3-logit-equiv (MTP verify path vs plain target, 8 teacher-forced prompts):
| metric | value | meaning |
|---|---|---|
| real_frac | 0.000 | no real argmax divergence β logit-equivalent |
| real_disagreements | 0 | β |
| frac_full_agree | 1.000 | every position's argmax matched over the shared prefix |
| mean_top1_mae | 0.000 | top-1 logprob identical |
| mean_tv (advisory) | 0.444 | trips the catastrophe guard but is the empty-top artifact, NOT a real fail |
Verdict: lossless. (The compare script's literal VERDICT printed FAIL purely on the advisory
mean_tv guard β this is the documented spec-accepted-token artifact; the gate reads real_frac.)
Acceptance on these prompts: 1.00 (e.g. statistics mtp β¦ #acc drafts = N / #gen drafts = N).
3b. Throughput β MTP off vs on (all 5 Gemma targets) + ours-vs-upstream (#441/#442)
Two regimes. The canonical figure is the A4B RULER@32k milestone (long-context, the representative
win). The series sweep (.opencoti/gemma-mtp-series-sweep.sh) fills all five targets at a fast,
consistent 4k-ctx / n_predict-256 / temp-0 / 2-prompt regime, and β answering #441's "also include the
llama.cpp PR benchmark" β is run a second time against upstream llama.cpp's native Gemma assistant-MTP
(b9660, PR #23398/#24282) at the same regime on the same RTX 3090 (.opencoti/upstream-mtp-tps.sh), so
ours-vs-upstream is apples-to-apples. accept = accepted/generated drafts (identical log metric both sides;
upstream reads it from the completion JSON draft_n_accepted/draft_n, ours from the server draft acceptance
line β same AtomicBot-lineage definition). Measured 2026-06-18 on solidpc RTX 3090.
Canonical (A4B, RULER@32k): off 56.6 β on 88.8 tok/s = 1.57Γ (#484, post-bump), accept 0.87. Absolute tps is lower than the 4k sweep below (long KV); the ~1.5β2Γ speedup is the regime-robust signal.
Series sweep β ours (llamafile 0.10.3 + #491 host-guard; single-context --mtp-head / --spec-type draft-assistant; FA on throughout):
| model | arch | off tok/s | on tok/s | speedupΓ | acceptance |
|---|---|---|---|---|---|
| Gemma-4 A4B-26B | gemma4 (iSWA) | 79.14 | 109.01 | 1.38Γ | 0.631 |
| Gemma-4 E2B | gemma4 (elastic) | 132.84 | 195.23 | 1.47Γ | 0.533 |
| Gemma-4 E4B | gemma4 (elastic) | 86.79 | 145.03 | 1.67Γ | 0.448 |
| Gemma-4 12B | gemma4 | 48.57 | 100.44 | 2.07Γ | 0.792 |
| Gemma-4 31B | gemma4 | 19.64 | 39.13 | 1.99Γ | 0.662 |
Series sweep β upstream b9660 native (ctx_other engine; -md β¦ --spec-type draft-mtp --spec-draft-n-max 3 -fa on; same HW + regime):
| model | off tok/s | on tok/s | speedupΓ | acceptance | note |
|---|---|---|---|---|---|
| A4B | 92.92 | 147.97 | 1.59Γ | 0.839 | |
| E2B | 157.81 | 264.43 | 1.68Γ | 0.628 | |
| E4B | 99.28 | CRASH | β | β | -fa on aborts (D512/gqa=2 FA-kernel gap, bug-567); upstream's own documented workaround is global -fa off β 98.47 β 182.07 (1.85Γ, 0.542) |
| 12B | 51.70 | 137.59 | 2.66Γ | 0.928 | |
| 31B | 20.45 | 51.09 | 2.50Γ | 0.788 |
Upstream PR-published references (PR #23398/#24282, mixed HW β not directly comparable; cited as the
"PR benchmark" the request asked for): 31B Q4 1.55Γ / Q8 1.97Γ (RTX PRO 6000); 31B 1.74Γ
(2ΓRTX 3090, the closest HW match); DGX Spark wall 290.0 s β 120.7 s (β2.4Γ), accept 0.588; "dense
>2Γ on average, MoE none"; E4B accept 0.479 (125/261) β within noise of our 0.448. Repro:
python3 scripts/mtp-bench.py against llama-server β¦ --spec-type draft-mtp --spec-draft-n-max 4.
Upstream's own caveats, both load-bearing for the Β§3d decision: quantized-KV β 0% acceptance (later
fixed for Q only, still composition-fragile) and -fa on crashes E4B (PR #24282, verbatim).
Three honest readings.
- The off-baseline gap is a base-version artifact, not an engine gap. Upstream out-decodes us even
with MTP off (A4B 92.9 vs 79.1; 12B 51.7 vs 48.6) β that's newer
b9660kernels vs our0.10.3pin, the price of carrying the opencoti KV moat upstream's base cannot. The MTP speedup ratio is the engine-comparable number; there upstream's separate-context engine is modestly ahead (1.6β2.7Γ vs our 1.4β2.1Γ). - E4B is a capability divergence, not a 4k-speed win. Upstream crashes with FA on and must disable FA globally; opencoti's surgical #491 host non-FA guard disables FA only for the one uncovered op (D512/gqaβ€2) and keeps it everywhere else β E4B runs at 1.67Γ with FA on. At 4k, upstream's FA-off workaround is actually faster in raw tps (182 vs our 145; newer base + no-FA penalty is negligible at short KV) β so the #491 advantage is latent at 4k; it pays off at long context (FA-off is O(nΒ²)) and for FA-dependent composition, not in this short-ctx cell. Plainly: ours keeps a capability upstream sacrifices; upstream is faster on the trivial case.
- The moat is composition, not raw speed. Upstream's
ctx_otherengine cannot compose with PolyKV / rolling-KV / M2 split / TurboQuant / DCA and collapses under quantized KV β see Β§3d. That, plus E4B-FA-on, is why opencoti keeps its single-context engine despite conceding the f16/short-ctx tps race.
Regime caveat: the sweep rows are 4k/n256 (fast, same-regime), not RULER@32k β absolute tps is higher at 4k (short KV). Compare speedups and the ours/upstream deltas, not absolutes, against the canonical A4B row; A4B appears in both as the cross-regime anchor.
3bβ². am17an PR benchmark β A4B, single-harness ours-vs-upstream (consistent acceptance, 2026-06-18)
The Β§3b series tables read ours-acceptance from the per-request draft acceptance server line but
upstream-acceptance from the cumulative completion JSON β an asymmetric aggregation, fine for the
speedup column but not a like-for-like acceptance read. To settle ours-vs-upstream on one metric from
one script, the canonical am17an PR benchmark (mtp-bench.py, gist 228edfb84 / PR #23398 β 9 fixed
prompts, max_tokens=192, seed=42) is run verbatim against every engine; it reads
timings.draft_n / draft_n_accepted identically for ours and upstream. A4B v6-coder Q4_K_M, ctx 8192,
draft depth 2 (ours' measured optimum: --draft-block-size 3 β upstream --spec-draft-n-max 2),
--temp 0, FA on, RTX 3090 (.opencoti/am17an/run-gemma-a4b.sh):
| engine | spec path | accept | drafted | accepted | mean tok/s | Γours-off |
|---|---|---|---|---|---|---|
| ours-off | base, no spec | β | 0 | 0 | 78.04 | 1.000 |
| ours-seq | --mtp-head draft-assistant, per-step sync |
0.6628 | 1474 | 977 | 109.66 | 1.405 |
| ours-fused | --mtp-head draft-assistant, fused (shipped) |
0.6635 | 1474 | 978 | 110.07 | 1.410 |
| up-off | b9660 base, no spec | β | 0 | 0 | 82.88 | 1.062 |
| up-mtp | b9660 -md β¦ --spec-type draft-mtp (ctx_other) |
0.7738 | 1344 | 1040 | 123.00 | 1.576 |
Upstream MTP-on is 1.118Γ ours MTP-on (123.00/110.07), which factors cleanly into two independent levers, βhalf each:
- Base decode β 1.062Γ (base-version-bound). ours-off 78.04 vs up-off 82.88. CUDA graphs are reused
on both sides (
graphs reused = 190β¦1700+in our server log), so per-token cost is GPU-kernel-bound, not launch overhead β it is purely newer kernels in b9660 (7dad2f1a1) vs our pinned basedbe9c0c8c(a confirmed direct ancestor; 20 CUDA commits between them). The A4B-relevant ones: #24087 enrollmul_mat_vec_q_moeinto PDL β A4B is MoE, and that GEMV is its single hottest decode kernel β and #23729 MMVQ Turing parameter table (Q4_K_M decode matmuls); the broader PDL (Programmatic Dependent Launch) cluster trims dependent-launch stalls even inside captured graphs. This 6% is the cost of the KV-moat pin: we holddbe9c0c8cso the 24-patch stack applies, foregoing these June kernel PRs. Recoverable only by a base bump (re-triggering the rebase friction). - MTP speedup ratio β 1.053Γ (engine/acceptance-bound). ours speedup-over-its-own-base 1.410Γ vs
upstream 1.484Γ (123.00/82.88), driven by acceptance 0.664 vs 0.774 on the identical drafter GGUF.
Note ours drafts more (1474) yet accepts fewer than upstream (1344 drafted, 1040 accepted): our
single-context cross-read over-proposes lower-quality 2nd-step drafts where upstream's
ctx_otherrecompute does not. Base-independent; a genuine future opportunity inside our control-plane (the same-draft-position hypothesis #492 was tested and proved inert β prompt-1 152β151 accepted, a 1-token regression; the lever is elsewhere, the 2nd-token hidden-state source). Not chased here.
Neither lever is the E4B FA crash β A4B is dense-MoE with gqa>2 and never hits bug-567; E4B's FA-on capability is the Β§3b row. This A4B block is the authoritative consistent-acceptance ours-vs-upstream comparison; the Β§3b series tables remain the 5-model coverage (with the noted aggregation caveat on their A4B acceptance cell).
3bβ³. E4B D512/gqaβ€2 β real flash-attn, #491 non-FA fallback retired (#493, patch 0080, 2026-06-18)
The Β§3b E4B row ran on the #491 host stop-gap: a build_attn_mha guard that routed the drafter's
single GLOBAL layer (head4/kv2 β gqa 2 at head_dim 512 β the bug-567 kernel hole) to the non-FA
explicit-softmax branch, so -fa on no longer aborts but that one layer is "fa off". #493 (patch
0080) replaces that with a real D512 f16 flash-attn β a new FATTN_VEC_CASE(512, F16, F16) VEC
instance + selector route + VEC dispatch case, and the #491 guard narrowed to fire only for the
residual no-kernel case (quantized/bf16 D512/gqaβ€2, which the assistant drafter never produces). The
VEC kernel tiles Q->ne[1] (cols_per_block 1 decode / 2 prefill+verify), so it is a real FA at any n_q.
E4B -fa on, assistant-MTP |
decode tps | acceptance | verified output |
|---|---|---|---|
| OLD (#491 non-FA soft_max on the global layer) | 150.97 | 0.67910 | reference |
| NEW (#493 D512 f16 VEC flash-attn) | 147.86 | 0.67910 | byte-identical |
Correctness is decisive: assistant-MTP is exact spec-decode (the target re-validates every drafted
token), so a wrong drafter FA cannot corrupt the verified greedy output but would collapse
acceptance β equal acceptance (Ξ=0) and a byte-identical verified prefix together prove the VEC FA
numerically correct. Decode tps is within boot-to-boot noise (the global layer is one of many; β2% is
not a regression). The two head-size switches in fattn.cu (selector and VEC dispatch) both need
the D512 case or the selector picks VEC while the dispatch aborts at decode time (fattn.cu:363).
A4B is gqa 8 (head16/kv2 on its D512 globals) β untouched MMA path, no #493 code; its raw-/completion
"garbage" is a thinking-coder prompt-template artifact (bug-573), coherent via /v1/chat/completions.
Gate: .opencoti/m493-fa-d512-gate.sh. DSO rebuilt byte-identical both paths (sha256 a8ddcd82).
3bβ΄. #654 re-run on the fresh 0121 binary β both hosts (2026-07-12)
Refreshes Β§3b on the current shipping engine (patch chain through 0121; the Β§3b "ours" table
above was the retired #491 host-guard stopgap, now superseded by the #493 proper D512/gqaβ€2 FA).
Two hosts, both single-GPU pinned (no cross-GPU host contention), robust decode-tps method (8-tok
cache_prompt warmup β 256-tok cache-reused measured completion, timings.predicted_per_second);
accept = draft_n_accepted/draft_n. bs2 = Β§3b family (ctx 32768); solidPC = sp-mtp-family (ctx 8192).
bs2 (RTX 6000, mtp3b-family.sh, ctx 32768):
| model | arch | off tps | on tps | speedupΓ | accept |
|---|---|---|---|---|---|
| Gemma-4 A4B | assistant | 188.90 | 320.04 | 1.69Γ | 0.903 |
| Gemma-4 E2B | assistant | 270.38 | 452.02 | 1.67Γ | 0.985 |
| Gemma-4 E4B | assistant | 185.25 | 365.72 | 1.97Γ | 1.000 |
| Gemma-4 12B | assistant | 127.61 | 266.17 | 2.09Γ | 0.907 |
| Gemma-4 31B | assistant | 42.61 | 86.55 | 2.03Γ | 0.563 |
| Qwen3.6-27B-Omni | nextn | 52.55 | 99.55 | 1.89Γ | 0.637 |
| Qwen3.6-35B-A3B | nextn | 195.26 | 275.79 | 1.41Γ | 0.868 |
The E4B -fa on cell is the direct #493/bug-567 verification: 1.97Γ at accept 1.000, no abort β
confirming the D512/gqaβ€2 FA gap is fixed on ours (the "-fa off" caveat in Β§3b belongs to upstream
b9660, not opencoti).
solidPC (RTX 3090, sp-mtp-family.sh, ctx 8192):
| model | off tps | on tps | speedupΓ | accept |
|---|---|---|---|---|
| Gemma-4 E2B | 151.28 | 328.82 | 2.17Γ | 0.959 |
| Gemma-4 E4B | 92.26 | 233.57 | 2.53Γ | 0.964 |
| Gemma-4 12B | 48.43 | 117.84 | 2.43Γ | 0.851 |
| Gemma-4 A4B (iq4xs) | 80.34 | 156.15 | 1.94Γ | 0.990 |
| Qwen3.6-27B-Omni | 54.37 | 85.49 | 1.57Γ | 0.629 |
| Qwen3.5-9B NextN | 45.29 | 67.62 | 1.49Γ | 0.527 |
Gemma-4 31B (resident, --vram-target 23000) |
18.93 | 31.88 | 1.68Γ | 0.616 |
Gemma-4 31B (spilled, --vram-target 22000) |
11.12 | 8.42 | 0.76Γ (loss) | 0.605 |
The 3090's smaller absolute base tps leaves more headroom to hide draft cost β higher multipliers than bs2 (E4B 2.53Γ, 12B 2.43Γ). The Gemma-4 31B is a 1.68Γ win when the KV stays resident β the two rows above are the SAME model/drafter/prompt/accept (~0.61); the only difference is VRAM headroom, and it is decisive. This closes two earlier confusions:
- Coherent-output fix (harness): the 31B is a thinking model (
<|turn>framing, writesreasoning_content); the original bench fed it a raw RULER haystack via/completionwithignore_eos:true+ greedy + no instruction (the Β§3.0a/bug-710 "measure on coherent output" violation), collapsing decode into a</u>Γ256 loop and a meaningless flat 11.14β11.28 / accept 0.926. Fixed by routingsp-mtp-familythrough/v1/chat/completions(chat template applied; chat response still carries the fulltimingsincl.draft_n/draft_n_accepted). - "MTP slower on 3090" (config, NOT a bug): at the harness default
--vram-target 22000the drafter (0.51 GB) + its dual-ctx shared-KV path squeeze the resident-KV window to 0 MiB budget, so the whole KV streams from host over PCIe Gen3 (6.5 GB/s) β and with2Γ the autoregressive KV traffic) β net loss. Lift the cap 1 GB to 23000 and the KV fits resident (host tail 62 MiB, negligible): base decode 11β18.93 and MTP flips to 31.88 tps (+68%). So the 22000 default is simply too conservative for the 31B on a 24 GB card.n_max=3the drafter re-reads that spilled KV per draft step, so each cycle pays ~4 host-KV streams to produce ~1.8 accepted tokens (
The GGUF/quant/template were never at fault (byte-identical to canonical
bartowski/google_gemma-4-31B-it-GGUF; <|turn> is the genuine Gemma-4 turn format β vocab has no
<start_of_turn>). On bs2 (RTX 6000, roomy VRAM, ctx 32768) the same pair is 2.03Γ; the 3090's
1.68Γ is the same win on a tighter card. bug-1116.
Β§4c niah ladder re-run (bs2, A4B-128e Q4_K_M native, DCA-on chunk 32768, q8_0/q8_0 KV,
short3-niah.sh): niah_single_1 = 100.0 at 16k / 32k / 64k / 128k β clean retrieval at every
length on the fresh binary.
3c. 0.10.3 restore (#423) β --spec-type draft-assistant, patch 0079
The assistant drafter is restored onto the llamafile 0.10.3 base as the first-class
--spec-type draft-assistant (patch 0079-gemma-assistant-mtp), coexisting with upstream-native
NextN (--spec-type draft-mtp) β distinct enums (COMMON_SPECULATIVE_TYPE_MTP vs β¦_DRAFT_MTP)
and graph types (LLM_GRAPH_TYPE_MTP vs β¦_DECODER_MTP). Gemma-4 is iSWA with no NextN head, so
the assistant is its ONLY MTP path β which is why the bump restores rather than drops it. Live
boot-smoke on the rebuilt CUDA host binary (A4B v6-coder Q4_K_M target + 26B-A4B assistant Q8_0,
solidpc RTX 3090, .opencoti/0079-boot-smoke.sh):
- server loads the assistant INTO the target (
MTP assistant loaded into target) and the spec engine selectsdraft-assistant, not thedraft-simplefallback that segfaulted (bug-517); - decodes coherently and prefix-stable vs spec-off: 405-char byte-identical greedy prefix, then the expected benign FP-tie paraphrase split (verify-batch matmul reduction order β single-token decode β the bug-263/270 artifact; free-running greedy is never the gate, Β§3a logit-equiv is);
- assistant actively drafts: 238/545 accepted over a 512-tok run (0.44; 0.83 over the first 96 tok).
Throughput here is NOT the Β§3b figure β at 512 tokens the per-step draft overhead is unamortized
(on 47 vs off 78 tok/s); the representative win is RULER@32k (#414 1.46Γ; re-measured 1.57Γ / +57%
after the #484 tok_embdβOUTPUT-buft re-claim β off 56.6 β on 88.8 tok/s, 87% accept; the 0.10.3 bump
had dropped the head to the CPU buft, bug-540, restored via the additive create_tensor_output_head
helper). The Β§5 composition cells (turbo / DCA / PolyKV / POSITION_WINDOW Γ assistant-MTP) remain the
locked logit-equiv gate (#461), now unblocked by this restore.
3d. Upstream-alignment (#476) β adopt #23398/#24282 data-plane, keep our control-plane
Upstream merged Gemma-4 MTP (PR #23398 = b9549, + E-series #24282). To make a future llamafile
bump onto a #23398 base a mechanical reconcile (clean delete of the identity layer), 0079 now carries
upstream's data-plane identity while keeping the opencoti control-plane engine. Decision +
rejection rationale: docs/protocols/UPSTREAM_SYNC.md (the 0079 block) and LLAMAFILE_UPGRADE.md G34.
- Adopted (data-plane): arch
gemma4-assistant(hyphen); tensorsnextn.pre_projection/nextn.post_projection+ E-seriesmasked_embd_centroids/masked_embd_ordering(bare, no.weight); KV%s.embedding_length_out+%s.nextn_predict_layers; matching internal C++ symbols (LLM_TENSOR_NEXTN_PROJ_*,LLM_TENSOR_MASKED_EMBD_*, hparamsn_embd_out_impl). Drafter GGUFs re-formatted in place (Q8_0 bytes verbatim) via.opencoti/gguf-upstream-rename.py. - Rejected β
ctx_other: upstream's assistant runs a secondllama_contextaliasing the target KV (assumes vanilla single-owner/contiguous/unquantized KV; 0% accept under quantized KV upstream). It would break all five gate features (PolyKV / rolling-KV / M2 split / turbo / DCA), so we keep our single-contextbuild_attn_mtpcross-read + ordered head +tok_embdβOUTPUT buft + fused N-step +--spec-type draft-assistant. β SUPERSEDED by Β§3e (2026-07): the single-context facade never reached upstream accept parity (bug-858), so thectx_otherexecution model WAS ported after all (dual-ctx,OPENCOTI_MTP_DUAL_CTX=1) β with the read-only shared-cache guards extended to opencoti's own KV machinery (bug-2102).
Inert-rename proof (A4B v6-coder Q4_K_M, solidpc, canonical gemma4-assistant drafter): real_frac = 0
(s4-p3-logit-equiv.sh) β same as Β§3a, the rename changed no logits; boot-smoke routes draft-assistant,
44% accept, coherent. The 0079 patch is surgically confined: 16/23 file-stanzas byte-identical to the
pre-rename 0079; only the 7-file data-plane surface changed. Chain proof (G33/G35): full 0001..0079
strict-applies from pristine (0 rejects), final tree == live byte-identical across 2941 source files,
reverse round-trips. All five drafters re-formatted (E2B/E4B ordered head, 12B/31B/A4B dense) to the
canonical gemma4-assistant identity and verified byte-faithful (per-tensor sha256 source==output, arch
flipped, embedding_length_out + nextn_predict_layers present, no residual mtp.*); the transitional
.upstream.gguf artifacts were renamed to the canonical <name>.gguf and the old underscore-arch sources
removed. The full KV-composition matrix on the renamed engine is FOLLOW-ON (#461).
3e. bug-858 dual-ctx port + the two root causes β FIXED (2026-07-05, bs2, native A4B pair)
The #607 wholesale port runs the assistant as upstream does β a real second llama_context
(ctx_dft, gated by OPENCOTI_MTP_DUAL_CTX=1) sharing the target's KV cells β reversing the Β§3d
ctx_other rejection. Getting it to parity surfaced two independent defects, both ours:
- bug-2101 β
llm_graph_input_attn_kv_iswa::can_reusebase drift (llama-graph.cpp). The llamafile-0.10.3 base nested the kq-mask width check underif (self_k_idxs && β¦->buffer); the KV-less drafter's k/v_idxs are dead nodes β check skipped β the boot reserve graph (full-cache-width mask) was silently reused for every draft decode;set_inputfilled onlyn_kvcolumns, the rest read scheduler-pool garbage. Signature: accept collapsed with prompt length (0.83 @114 tok β 0.00 @1314; upstream flat). Fix = upstream-verbatim independent if-blocks (b9859 restructured exactly when KV-less graphs were introduced). - bug-2102 β opencoti lazy-clear on the aliased shared cache (
llama-kv-cache.cpp). The F4-M3 residency callsensure_cleared()/shrink_if_idle()ininit_batchran UNGUARDED on the draft cache, whose layers alias the target's k/v tensors (layer_share) with a zeron_cells_clearedwatermark β the drafter's first decode memset the head of the target's live KV. Signature: deterministic dual-vs-plain trajectory flips at 4β17-logit margins, persisting with p_min=1.01 (zero drafts); isolated by a 2-factor probe (tap-on + drafter-decodes-skipped == plain byte-identical). Fix =if (other) return;read-only guards in both (upstream has neither function). Rule extracted: after adding cache sharing, EVERY mutating member needs the guard β including fork-specific ones upstream doesn't have.
(Also reverted an earlier fattn.cu Q->ne[1]<=2 VEC experiment back to upstream ==1 β its
apparent win had been bug-2102's masked effect.)
Post-fix gates (native pair: google 26B-A4B Q4_K_M + HF-canonical assistant Q8_0, bs2, vs b9859):
- Accept (9-prompt compare, greedy 200-tok): n1 0.862 vs 0.876 (parity; 8/9 prompts within noise), n2 0.800 vs 0.792 (above), n3 0.679 vs 0.736 (#609-class target numerics β characterized and closed below; recommend n2 for A4B).
- Accept vs prompt length (fillerΓk): ours flat at/above upstream at every length (0.85/0.83/0.92/0.88/0.92/0.92 vs 0.87/0.83/0.91/0.88/0.87/0.87 at 34β2594 tok) β the bug-2101 cliff is gone.
- Losslessness (dual==plain, temp 0): now matches upstream's own flip profile β n1 fib/fr byte-identical end-to-end; the two residual flip prompts (clamp@43, forces@126) are positions where ours-plain also differs from up-plain (pre-existing #495 base numerics; upstream itself flips clamp@96). n3: 3/4 == upstream's 3/4.
- Throughput (mtp-matrix, 400-tok): base 204 vs 190 (ours +7%); MTP-on n2 314.7 vs
319.4 (β1.4%), n1 261 vs 288. Full matrix incl. 31B / Qwen-35B / Qwen-27B in
three-way-tps.mdΒ§2026-07-05.
#609 depth-3 rung: CLOSED (bug-2103, 2026-07-05). The Qwen-35B n3 decode collapse was
TinyBLAS's mmvf ne11<=3 cap dropping the 4-token verify batch's f32 GEMVs into a 1-block
GemmEx; post-fix n3 is monotone (225β237β247) and ours' best Qwen rung. A4B n3 accept recovered
to 0.784 cell / 0.688 9-prompt aggregate vs upstream 0.736 β the residual β4.8pp is spread
thinly across prompts (#495-class target numerics, not depth machinery). 2026-07-05 final
#614-binary re-verification (all four pairs β A4B + 31B assistant, Qwen 35B + 27B NextN):
decodeΒ·accept unchanged within noise (A4B n1/n2/n3 257Β·0.975 / 302Β·0.845 / 329Β·0.784; 31B
100Β·0.849 / 119Β·0.791; q35 225Β·0.858 / 237Β·0.752 / 247Β·0.662; q27 99Β·0.752 / 105Β·0.626), and
the 9-prompt aggregates hold at n1 0.856 vs 0.876, n2 0.784 vs 0.792, n3 0.688 vs 0.736
(per-prompt table in three-way-tps.md). Recommended depth: n2 on A4B/31B assistant, n3 on
Qwen NextN. Historic diagnostic ladder: buglog bug-2101/2102/2103; probes at
bs2:/srv/ml/opencoti-c1/gate{4,5,10,13,14,15,16,18}.sh, matrix at mtp614-out/.
β The decode-tps columns above are SUPERSEDED by Β§3h (#675, 2026-07-14) β every ours spec
cell gained +4β20% from the spec-clone-cheap fix; accepts are unchanged (bit-identical).
3f. #611 (2026-07-05): single-context facade REMOVED β dual-ctx is the only assistant path
The retired single-context in-target engine (llama_decode_mtp facade, build_attn_mtp
cross-read, llm_build_gemma4_mtp, common_speculative_impl_draft_assistant, async worker) is
deleted (patch 0096, ~1600 lines); the OPENCOTI_MTP_DUAL_CTX env gate is gone and
--spec-type draft-assistant now ALWAYS runs the Β§3e dual context (server creates ctx_dft
with ctx_other=target; CLI surface unchanged). Gate on the #611 binary (bs2, GPU0, no env):
A4B n2 305.4 tps / 0.849 accept (== Β§3e reference), q35 NextN n3 248.8 / 0.662.
bug-2110 β RESOLVED-NOT-A-BUG (harness artifact, same day). The tps-gate harness showed
accept 0.849β0.635 at --parallel 2 --kv-unified, but its prompt (160Γ repeated filler, temp 0,
ignore_eos) drives the model into degenerate repetition loops; par2 merely shifts greedy decode
onto a different garbage trajectory (#495-class knife-edge), and accept-on-garbage swings wildly
between trajectories (upstream b9859 flips the same way on that harness). On the REAL 9-prompt
mtp-bench (/srv/ml/opencoti-c1/2110-real/): ours par1 0.785 vs par2 0.783 aggregate (max
per-prompt delta 2pp), upstream 0.793 vs 0.792 β the dual-ctx assistant HOLDS accept at
--parallel 2 --kv-unified, within 1pp of upstream at both. PolyKV Γ assistant-MTP composes
cleanly. Lesson (cerebrum Do-Not-Repeat): never read accept off the repetitive-filler tps harness.
3g. WS2 turbo-V in-register Γ MTP-on β the ship-gate closer (#629, A4B, bs2)
WS2 (#629) reads a rotated turboN V-cache in-register inside the multichunk DCA fused flash-attn
kernel (killing the per-forward whole-cache cpy_turboN_f16 cast; n_qβ€16 gate; DEFAULT-ON;
full decode-tps matrix in context.md Β§5x). The remaining question for MTP was whether
the in-register path stays lossless through the draft-assistant verify batch (n_q > 1). It does β
airtight, by measurement not inference:
- Determinism floor (MTP-on,
WS2=0twice): byte-IDENTICAL. The WS2=0 reference path is deterministic, so any WS2=0-vs-1 diff is real signal, not sampling noise. - n_q=1 teacher-forced logit-equiv (no MTP):
real_frac 0.0,mean_tv 0.00041,ffa 1.0β logit-lossless on the single-token decode. - n_q>1 teacher-forced logit-equiv (MTP draft-assistant verify batch, n_q = draft+1):
real_frac 0.0,mean_tv 2e-05,ffa 1.0β logit-lossless in the multi-token verify batch. This is the airtight closer β the verify batch is exactly where ann_q-gated in-register path could have diverged, and it does not (measurement, not inference). - Free-running divergence (MTP-on
WS2=0vsWS2=1): forks at char ~82 (and the n_q=1 no-MTP arm at char ~486), both coherent / greedy / same GGUF β a pre-existing #495-class base FP-tie autoregressive fork, NOT a WS2 defect. Accept parity: 90/90. - Compose: WS2 default-on in-register (+89% decode, Β§5x A4B turbo2_0) composes with the MTP draft-assistant path; both are lossless and stack.
Ship-gate: WS2 Γ MTP is closed β in-register turbo-V is logit-lossless through the verify batch, accept holds at parity, and the only free-running divergence is the known base FP-tie fork. Cross-ref: context.md Β§5x (WS2 decode-tps matrix + caveats).
3h. #675 (2026-07-14): the last per-cycle host gap β bug-2180 spec-clone under cosmo dlmalloc (patch 0128)
After #609/#614, Qwen 35B NextN still trailed b9859 12% at every spec depth with base decode at
parity β a constant per-cycle cost, invisible to nsys/GPU profiling. Per-phase wall-clock timers
(1.8 MiB); under the
cosmo dlmalloc runtime every β₯256 KiB allocation is a fresh mmap + page-fault-on-write +
munmap, while b9859's glibc reuses the freed block. A runtime-allocator asymmetry, not an
algorithmic one (bug-2180).OPENCOTI_SPEC_PROF, temporary) in BOTH servers produced a phase-parity table: draft, verify,
and process each at parity; the whole gap sat in the processβsample seam. A direct timer
nailed it: common_sampler_clone cost 1.071 ms/cycle on ours vs ~0.07 ms on b9859 β from
IDENTICAL source. The clone deep-copies the n_vocab-sized cur scratch (
Fix (spec-clone-cheap, common/sampling.cpp): clone with empty cur/cur_p. Safe because
cur is write-only scratch that set_logits() repopulates before any read, and the copied
cur_p.data pointed into the SOURCE sampler's buffer anyway (latent upstream aliasing β no
consumer can rely on the clone's copy). Restore via move-assign unaffected. Clone 1.071 β
0.005 ms/cycle. Companion fix in the same patch: the mmvf f16/bf16 ampere_mma TinyBLAS
lift (ne11β€8, src0_small-guarded) β the bf16 shared-expert gate GEMVs
(ffn_gate_inp_shexp) in the 2β4-token verify batch were the dominant residual tinyblasGE
cliff after #609's f32 fix. Both fixes are output-identical: accept bit-identical pre/post at
every depth.
Gate (bs2 GPU1, binary 05240e8e57c7191e, all diag stripped): q35 base 212.6 = parity
(unregressed); n3 282.6 vs b9859 287.4 (β1.7%, was β12%); full same-day matrix β A4B
307.9/353.8/378.3 vs 289.1/321.8/369.1 (ours WINS all depths), q35 265.2/270.0/278.8 vs
261.3/277.7/270.7, 9-prompt decode mean 317.7/361.6/365.7 vs 298.1/320.2/339.8 β tables in
three-way-tps.md Β§2026-07-14. Characterized non-blockers: ~0.15β0.2
ms/cycle pre-draft glue; #495-class accept deficit at depth β₯2 (chained-draft numerics), worth
its own investigation only if the 31B/27B β6% matters. Diagnostic lesson (cerebrum): a constant
ms/cycle host gap with GPU parity on identical source β suspect the RUNTIME (allocator mmap
threshold), and measure with host wall timers, not nsys windows. Build gotcha hit during
cleanup: bug-2181 (ccache+cosmocc fat-object stale .aarch64 sidecar β delete both .o twins
CCACHE_DISABLE=1, verify the APE withstrings).
4. Qwen NextN self-speculation (M6-S5) β all 3 models GATED (correctness + tps)
Status (2026-06-18). All 3 Qwen models gated on BOTH axes. Correctness (
real_frac=0, Β§4a): 9B Q8_0 (K=48, full KV-composition sweep), 27B-Omnimerge Q4_K_M (K=256), 35B-A3B IQ3 MoE (K=256) β the spec path is lossless on every model (recall matches plain on all prompts). Throughput (Β§4b): dense 9B 1.92Γ / 27B 1.89Γ (NextN head survives the Omnimerge merge), MoE 35B-A3B 1.18Γ (MoE wins less). Fused draft (#454) β‘ sequential (no win β fusing draft-token graph steps buys nothing; the recurrent-snapshot cost #457 targets is a separate, unmeasured residual β not "moot"). CoT-budget finding (bug-560): the needle harness default K=48 spuriously inflatesreal_fracon reasoning models (plain decode cut off mid-<think>); Kβ₯256 βreal_frac=0. Both 256k inter-chunk-DCA cells now PASSreal_frac=0(Β§4c): Qwen NextN (27B-MTP Q6_K, 2026-06-29) and Gemma assistant-MTP (A4B-it + adapted drafter, 2026-06-30).
Models: Qwen3.5-9B-Q8_0 (qwen35, nextn@32) Β· Qwen3.6-27B-Omnimerge-v4-Q4_K_M (qwen35, nextn@64)
Β· Qwen3.6-35B-A3B-UD-IQ3_XXS (qwen35moe, nextn@40). Loader fix #622 (W1); drafter graph W2;
self-spec + CKPT rollback W3/W3b.
4a. Correctness β logit-equivalence (self-spec vs plain), solidpc
All 3 models DONE β real_frac=0 (9B at K=48; 27B/35B at K=256 β see CoT-budget caveat below).
w4-qwen-nextn-logit-equiv.sh (a wrong recurrent rollback would surface here as large real_frac; it does
not β the spec path is lossless on every model, recall matches plain on all prompts). The 9B cell also
carries the full KV-composition sweep (the Qwen column of Β§5): base + asymmetric + DCA + q4/q8 +
turbo2/3/4/8, all real_frac=0.
VERDICT prints FAIL from the mean_tv advisory only β the known empty-top artifact (spec-accepted
tokens return top:[]); the decisive signal is real_frac/real_disagreements (bug-263/270), both 0.
| model | real_frac | frac_full_agree | draft acceptance | verdict |
|---|---|---|---|---|
| Qwen3.5-9B (Q8_0) | 0.0 (K=48) | 1.0 | 0.79β0.94 | PASS (real_frac; TV-FAIL=empty-top artifact) |
| Qwen3.6-27B-Omnimerge (Q4_K_M) | 0.0 (K=256) | 0.875 | 0.685 (engaged, Β§4b) | PASS β K=48 showed 0.125β0.167, a CoT-budget artifact (bug-560), not a spec defect |
| Qwen3.6-35B-A3B (IQ3) | 0.0 (K=256) | 0.75 | 0.705 (engaged, Β§4b) | PASS β both recall 8/8; 2 benign divs (1 near-tie pos-7, 1 post-answer tail pos-193) |
CoT-budget caveat (bug-560). The needle harness default
K=n-predict=48 is too short for a reasoning/CoT model. The 27B-Omnimerge emits<think>β¦</think>before answering; at K=48 a benign pos-3 near-tie (tok 198β271= "keep thinking" vs "close think+answer", margin 0.02β0.25 nats <<tie_margin0.7,top1_mae=0) tips plain decode toward verbose CoT, which then runs out of budget before emitting the needle β spuriousref_recall=Falsewhile the spec path answered correctly (cand_recall=True). That scored asreal_frac0.125 (n=8) / 0.167 (n=24) β but every divergence was cand-better, never cand-worse. At K=256 both paths recall 8/8 βreal_frac=0.0: the spec path is lossless (the flip is the documented batch-vs-sequential FP micro-bias, surfacing on Q4_K_M where the Q8_0 9B had none). Run reasoning-model cells withKβ₯256.
4b. Throughput β off vs on, solidpc (ctx 8192, fixed decode prompt, --temp 0, 2026-06-18)
All 3 Qwen models DONE β self-spec is a decode win on every one; qwen-nextn-tps-confirm.sh (tps
from server timings.predicted_per_second, bug-470). The win is large on the dense models (~1.9Γ) and
small on the MoE (1.18Γ) β see the MoE note below.
| model | arch | off gen tok/s | on gen tok/s | speedupΓ | draft acceptance |
|---|---|---|---|---|---|
| Qwen3.5-9B (Q8_0) | dense | 44.2 | 84.7 | 1.92Γ | 0.76β0.80 |
| Qwen3.6-27B-Omnimerge (Q4_K_M) | dense | 22.8 | 43.0 | 1.89Γ | 0.685 |
| Qwen3.6-35B-A3B (IQ3_XXS) | MoE (A3B) | 76.8 | 90.9 | 1.18Γ | 0.705 |
9B is two fixed prompts (off 44.23/44.09 β on 82.80/86.5 tok/s, 1.87Γ/1.96Γ @ accept 0.757/0.795); the table reports their mean. 27B confirms the win holds on a Q4_K_M merge whose NextN head survived the Omnimerge (draft engaged at 0.685).
MoE self-spec wins less. The 35B-A3B is a MoE with only ~3B active params, so its baseline decode is already fast (76.8 tok/s off, vs 44/22.8 for the dense 9B/27B) and memory-bandwidth-light per token. Speculation trades a fixed draft+verify overhead for skipped target steps; when each target step is already cheap (few active params) that overhead amortizes far less, so the net is 1.18Γ rather than ~1.9Γ β despite comparable acceptance (0.705). Self-spec still helps MoE, just modestly; the headline win is a dense-model effect.
Fused draft (#454) β‘ sequential β no win. OPENCOTI_MTP_FUSED=1 (one fused N-step graph launch) is
byte-identical to the per-step sequential path and no faster (9B: seq 82.80 vs fused 83.15 tok/s = 1.004Γ;
identical acceptance 0.75652 and identical per-iteration drafted-token NDJSON, w6b-β¦-equiv.sh). Fusing the
draft-token graph steps buys nothing β the native drafter's per-step graph work is already minimal β so
#454 lands as "sequential is optimal".
#454 β #457 (don't conflate the two costs). #454 fused the draft-token graph steps (measured: no
win). #457 targets a different cost: the hybrid backbone is recurrent (gated delta-net), so on a
partial draft accept the server would restore a PARTIAL_ONLY recurrent-state checkpoint and re-decode
the accepted tokens to re-advance past the rejected AR draft (server spec_ckpt, server-context.cpp
~3578-3606). PR#22673's ring-buffer rewind would make that snapshot/rollback O(1).
#457 PROFILED (2026-06-18, Qwen3.5-9B Q8_0, solidpc, .opencoti/m457-ckpt-profile.sh) β re-decode cost
MEASURED = 0; PR#22673 port NOT justified at shipped config. Driving the binary's own LLAMA_TRACE=1
instrumentation over a 512-token decode (71.2 tps):
| metric | value |
|---|---|
| spec iterations | 183 |
partial accepts (accepted < drafted) |
108 (59%) β dist: 40Γ0/3, 31Γ1/3, 37Γ2/3, 74Γ3/3 |
(restore checkpoint) re-decode events |
0 |
| draft acceptance | 0.60 (328/547) |
#457 speedup ceiling (1+P) |
1.000Γ |
The decisive log line is common_context_can_seq_rm: the context supports bounded partial sequence removal: upstream 0.10.3's hybrid context natively partial-rewinds the recurrent state up to a bound that
covers the default n_max=3 draft, so all 108 partial rollbacks were handled by seq_rm and the
checkpoint-restore/re-decode branch (use_ckpt_*, gated on ctx_*_seq_rm_type == FULL or rollback >
llama_n_rs_seq) was never entered. The snapshot is also skipped (same gate), and even if taken it is
an ON_DEVICE D2D copy of recurrent state (~Β΅s). So the ~1.9Γ win has no re-decode residual to absorb β
the rollback is already effectively O(1). Caveat (the only path that would revive #457): a draft width
large enough that n_rollback > llama_n_rs_seq (e.g. raising --draft-max well past the bound) would start
entering the checkpoint path; at that point re-profile before porting. Not a #462 blocker; closed.
The earlier "0.75Γ W6 regression" was a stale-token artifact (retired --spec-type mtp on the older binary
β 32.46 tok/s); draft-mtp on the current binary gives the wins above (bug-557).
R-series closure β the "3-3.5Γ" headline was architecture-capped at the measured ~1.9Γ; #452/#453 are
subsumed (2026-06-30). The Qwen NextN self-spec line was scoped (#451) with an aspirational 3-3.5Γ
target; the delivered, dual-axis-gated ceiling is 1.92Γ (9B) / 1.89Γ (27B) / 1.18Γ (MoE 35B) β Β§4b
above, post-#457. The gap is structural, not a missing optimization: the hybrid gated-delta-net backbone's
verify step bounds the win, the fused-graph path is byte-identical-and-no-faster (#454), and the
checkpoint/re-decode cost #457 would have absorbed is already 0 (native seq_rm partial-rewind covers
the default draft width β re-decode events measured = 0, (1+P) ceiling = 1.000Γ). #452's nominal
deliverable (DCA composing in the MTP block) is done β Qwen NextN 256k inter-chunk DCA-on PASSES
real_frac=0 (Β§4c). #453's generalize/build/eval/ship is done β all three archs (qwen35 / qwen35moe)
gated in Β§4b and shipped via 0079. No remaining work pushes 1.9Γ β 3.5Γ on this design; chasing it would
need a fundamentally different (non-recurrent) drafter, not a continuation of this line. Closed as
delivered-at-1.9Γ.
4c. MTP + DCA-on interaction (256k) β Qwen NextN PASS + Gemma assistant PASS (bs2, 2026-06-29/30)
Qwen NextN self-spec β PASS (real_frac=0, bs2 GPU1, 2026-06-29). Model:
Qwen3.6-27B-Omnimerge-v4-Q6_K from the dedicated MTP-quant repo
(ManniX-ITA/Qwen3.6-27B-Omnimerge-v4-MTP-GGUF, 22.4 GB, carries the 4 blk.64.nextn.* tensors β the
plain β¦-v4-GGUF Q6_K has no NextN head and fails MTP load). Harness w4-dca256k-bs2.sh: -c 262144, --dca on --dca-chunk-size 32768 (8 chunks β genuine inter-chunk DCA on this native-256k
model), ref = plain target, cand = --spec-type draft-mtp, both DCA-on so the comparison isolates
MTP composition over DCA-laid-out KV + the W3b CKPT rollback. Verdict: real_frac=0.0,
real_disagreements=0, frac_full_agree=1.0 β NextN self-spec is logit-equivalent to plain under
inter-chunk DCA at 256k. (mean_tv_advisory=0.458 is the known empty-top-logprobs artifact for
spec-accepted tokens β advisory only; real_frac is the gate, per [[mtp-s4-p3]].) Caveat: 1 of 2
~228k prompts overflowed n_ctx_train; the compare ran over the 1 clean prompt Γ 64 decode positions
(clean real_frac=0, sufficient for the composition gate which Β§4a + the 9B sweep already established
mechanistically).
Gemma assistant-MTP β PASS (real_frac=0, bs2 GPU1, 2026-06-30, 2/2 non-empty). Target = Gemma-4
A4B-it (gemma-4-A4B-62e-β¦-it-Q6_K, arch gemma4/iSWA, native 262144); drafter = the adapted
gemma4-assistant (regenerated on bs2 via gguf-upstream-rename.py from the stale underscore
ollama_staging copy β the binary rejects the old gemma4_assistant underscore arch). cand adds
--mtp-head <drafter> --spec-type draft-assistant -ngld 99; both boots --dca on --dca-chunk-size 32768. Verdict real_frac=0.0, real_disagreements=0, frac_full_agree=1.0 β assistant-MTP verify
composes logit-equivalently with DCA-laid-out KV at 256k. (real_frac is robust to drafter/target
quality β verify always uses target logits β so it isolates the MTP+DCA composition, not acceptance.)
5. Composition matrix β every KV development Γ MTP (the locked two-engine gate, #461)
MTP is TWO engines (Β§1) and both must compose with every other KV feature; where an engine is
limited we extend it (never "N/A" β user directive 2026-06-17). Rows = features; cols = the engine
that carries each model. The assistant-drafter restore (#423) is DONE β the engine boots, loads
into the target, and drafts on 0.10.3 (Β§3c). The gemma gate scripts' mtpβdraft-assistant refresh is
done (#473); 9 of the Gemma cells PASS real_frac=0 (Β§5b, 2026-06-17), DCA composes, PolyKV
composes under --kv-unified (#485, corrects the earlier bug-550 misdiagnosis). The last
Gemma-side item β POSITION_WINDOW-at-overflow at long ctx β is architecturally N/A, not open:
--kv-residency-mode window is ineligible for Gemma's iSWA caches (see the POSITION_WINDOW row of
the composition matrix below); the cell stands on logit-equivalence.
| feature | NextN-MTP (Qwen, --spec-type draft-mtp) |
assistant-MTP (Gemma, --spec-type draft-assistant, 0079) |
|---|---|---|
| scalar f16 / q8_0 / q4_0 | PASS real_frac=0 (GAP-1) | PASS real_frac=0 β f16 (#474) + q8_0/q4_0 (gemma-mtp-compose.sh, A4B v6-coder, 2026-06-17) |
| asymmetric q8_0/q4_0 | PASS real_frac=0 (GAP-2) | PASS real_frac=0 (2026-06-17; V dequant-on-lift under the draft ctx) |
| turbo2/3/4/8 (fused FA-VEC) | PASS real_frac=0 (GAP-3) | PASS real_frac=0 all 4 tiers (2026-06-17; InnerQ tier-family scale, hd-map 2048:256,1024:512) |
| DCA-on INTRA (ctx β€ n_ctx_train) | PASS real_frac=0 | composes real_frac=0 (--dca on --dca-chunk-size 1024, 4 chunks @4096; no boot banner β genuine inter-chunk = 256k bs2) |
PolyKV (SharedKVPool, --parallel β₯2) |
PASS β composes (Β§5a) | PASS β composes under --kv-unified (which PolyKV forces); boot guard still rejects non-unified multi-slot. Re-proven on the #611 dual-ctx path with the real 9-prompt bench: accept 0.783 @ --parallel 2 --kv-unified vs 0.785 single-slot (upstream 0.792/0.793) β bug-2110 investigation, Β§3f |
| rolling-KV POSITION_WINDOW (iSWA) | n/a β Qwen non-iSWA | PASS on correctness real_frac=0 (logit-inert, Β§5b) + forward path proven by rolling-kv-equiv-gate + #409. 256k engagement is architecturally N/A on the post-bump binary (bs2, 2026-06-30): --kv-residency-mode window is ineligible for Gemma's iSWA caches β the only windowable cache is the SWA one (swa_type=1, bounded 1536 cells β never overflows), and window-mode is disqualified by swa_type=1/v_trans=0/offload=1 (llama_kv_cache: β¦ ineligible β¦ falling back to head/resident). The 2-KV-head global cache spills on the head-axis path (M2/#485), not position-window. So this cell stands on logit-equivalence; "engage POSITION_WINDOW at real 256k overflow" is not exercisable for Gemma without a binary-level eligibility change (not pursued β composition correctness already established) |
| 256k stream / inter-chunk DCA | PASS real_frac=0 (27B-MTP Q6_K, chunk 32768/8 chunks @256k, #439, 2026-06-29 Β§4c) | PASS real_frac=0 (A4B-it + adapted assistant drafter, chunk 32768 @256k, #431, 2026-06-30 Β§4c) |
Β§5b β Gemma assistant-MTP composition (solidpc, A4B v6-coder Q4_K_M, 2026-06-17). Eight cells
PASS (real_frac=0, gemma-mtp-compose.sh β s4-p3-logit-equiv.sh with KVARGS/DRAFT_KVARGS hooks,
#473): scalar f16/q8_0/q4_0, asymmetric q8_0/q4_0, turbo2/turbo3/turbo4/turbo8. ref and cand BOTH carry
the feature flags so the feature's own accuracy cancels and real_frac isolates the draft/verify path;
the renamed binary loads the canonical gemma-4-β¦-assistant-Q8_0.gguf drafter (post-reformat) under
each KV type and routes draft-assistant. DCA-on (chunk 1024, 4 chunks) composes (real_frac=0) but has
no boot banner and is positionally inert at ctx β€ n_ctx_train (262144) β the decisive inter-chunk DCA
test is 256k/bs2. POSITION_WINDOW is logit-inert (real_frac=0) yet the residency planner keeps the tiny
2-KV-head global iSWA cache fully resident at 4096 even under --vram-target 1/window (the historical
--vram-target 1100+auto that windowed 256/4096 no longer forces overflow on the post-bump binary) β so
its real regime is long-context overflow (256k). An engagement guard in the driver (MARK[] greps the
cand server.log for the feature banner) prevents a silently-disengaged feature from masquerading as a
vacuous f16 pass.
PolyKV Γ assistant-MTP β COMPOSES under --kv-unified (#485; corrects the earlier bug-550 misdiagnosis).
gemma-polykv-probe.sh boots the full PolyKV combo (--parallel 2 --kv-unified --no-cache-idle-slots -ctk/-ctv q8_0 --mtp-head β¦ --spec-type draft-assistant) β boots, routes draft-assistant,
real_frac=0 + ~93% draft acceptance across the shared-prefix slots. PolyKV (SharedKVPool) forces
--kv-unified, so the PolyKV config lands on the supported path by construction.
What the earlier session got wrong (bug-550). The first probe reported "BOOT FAIL / --parallel 1 only".
That was a probe false-positive: the readiness loop grepped a bare failed to, which matched the
benign, caught warning [spec] failed to measure draft model memory (the spec-fit pre-measure tries to
build a standalone draft llama_context; gemma4-assistant correctly refuses to be a primary -m model,
the warning is logged, and the server continues). The server was in fact booting fine. Two fixes folded
into 0079: (a) skip the impossible standalone fit-measure for the assistant engine
(spec_assistant_fit, server-context.cpp:809); (b) tighten the probe grep to genuine fatal signatures.
The one real limit β non-unified multi-slot β is now a clean boot guard, not a crash. --parallel >1
without --kv-unified selects the per-stream-split KV layout (patch 0031). The single-context
assistant draft cross-reads the target KV via the shared build_attn_mha (build_attn_mtp β
get_k/get_v), and build_attn_mha infers the batch's stream count from the target K tensor's 4th
axis β const auto n_stream = k->ne[3] (llama-graph.cpp:2115) β then splits the query's token axis by
it (:2117). Under the per-stream split get_k concats one view per covered stream on dim 3, so
k->ne[3] = n_streams > 1; but the assistant draft query is single-stream, single-token
(q->ne[2] = 1, gemma4_mtp_build_one_step builds [.,1] step tensors, ctx_dft==nullptr). Partitioning
a 1-token query by n_streams (1/n_streams β 0) corrupts the stream/token geometry β ggml reshape
nelements assert (ggml.c:3754) at the first draft decode. The boot guard (server-context.cpp:940)
requires --kv-unified for --parallel >1 and fails clean with an actionable message instead of the
mid-request SIGABRT. Under --kv-unified the cache is not per-stream-partitioned (n_stream=1,
get_k returns ne[3]=1), the line-2117 split is the identity, and the cross-read matches the draft
query exactly β the byte-identical path the engine is proven on. Making the draft itself n_seq-aware
(one drafted token per active stream, like Qwen NextN Β§5a) would lift the non-unified limit too, but is
not needed for PolyKV (which is unified) β tracked under the #469 extend-native-MTP family.
Head-axis KV residency Γ assistant-MTP β COMPOSES on a real 31B (Gemma-4-31B-it Q6_K, bs2 GPU1,
2026-06-30). This is the first 31B-specific MTP-composition cell (the Β§5b matrix above was A4B-only) and
the first over a genuinely spilled head-split KV. headaxis-mtp-31b-bs2.sh boots the official
google_gemma-4-31B-it-Q6_K.gguf at 128k with f16 KV and --kv-residency-mode auto --vram-target 7000,
forcing the dense global cache to split GPU/host on the head axis (M2/PolyKV residency β the path the
ineligible window mode falls back to on Gemma's iSWA caches, cf. Β§5 POSITION_WINDOW). Engagement is real,
not vacuous: GPU1 sits at 33.4 GB (ref) / 33.9 GB (cand) vs ~40 GB for full f16-resident, i.e.
~6 GB of KV is held in pinned host RAM and lifted on read. ref-plain (head-split) vs cand-MTP (head-split +
gemma-4-31B-it-assistant.Q8_0 drafter, --spec-type draft-assistant -ngld 99) β real_frac=0,
real_disagreements=0, frac_full_agree=1.0 (PASS): the assistant draft/verify path is logit-identical
even when half the global KV is dequant-on-lifted from host. (mean_tv_advisory=0.44 is the known
empty-top-logprobs artifact for spec-accepted tokens, not a divergence β real_frac is the gate.) Closes
the open "did we validate 31B on the head axis?" question: previously 31B was proven with MTP decode/accept
(Β§1/Β§3) and sparse-V long-ctx (#566), but the head-axis residency Γ MTP cell had only been run on A4B.
5a. PolyKV Γ NextN-MTP β COMPOSES (Qwen3.5-9B Q8_0, solidpc, 2026-06-17)
Overturns the plan's GAP-5 "architecturally exclusive (parallel-1)". Native MTP is n_seq-aware by
construction (common_speculative_impl_draft_mtp does smpls.resize(n_seq) +
backend_chains.assign(n_seq); server-context.cpp:1066 inits spec with n_parallel), so it runs
per-slot under --parallel. The "--parallel 1" limit was a property of our retired W2/W3b CKPT
self-spec, not the native path. Probe .opencoti/polykv-mtp-probe.sh:
--parallel 3 --kv-unified --no-cache-idle-slots --spec-type draft-mtp -ctk/-ctv/-ctkd/-ctvd q8_0.
- boot OK; spec engaged (MTP draft ctx 519 MiB; checkpoints max=32, min-spacing=256).
- 2 agents share a read-only prefix (P=873) via SharedKVPool with draft-mtp on: both recall the
needle, isolation holds (A=Alice¬Bob / B=Bob¬Alice), counted-once (each
cache_n=873βP,prompt_n=36βsuffixβ prefix entered KV once, seq-shared across 3 slots), hook fired β₯2. - MTP actively drafting during the shared decodes: slots 1 & 2
draft acceptance = 0.19 (4/21). - Re-verified 2026-06-18 on the post-#493/#485 binary (DSO
a8ddcd82, host 13:11): byte-for-byte identical PASS β boot OK, spec engaged (MTP draft ctx 519 MiB), all Phase-2 asserts green (P=873, both recall needle, isolation holds, counted-once, hook β₯2), MTP drafting+accepting (slots 1 & 2 4/21) during the shared decodes. Confirms #485's two sharedserver-context.cppspec hooks are inert for native NextN:spec_assistant_fitskip isgemma4-assistant-specific, and the--kv-unified-when---parallel>1boot guard is satisfied by construction (SharedKVPool forces--kv-unified). #493's fattn D512-VEC case is unreachable here (Qwen is head_dim 128 β 512). So the #469 composition scope is closed on the current binary; the only NextN remainder is the bs2-blocked 256k-stream / genuine inter-chunk DCA cell (#431/#439).
Flag note: the 0.10.1β0.10.3 bump renamed --no-clear-idle β --no-cache-idle-slots (bug-500;
runbook G29). The PolyKV mechanism (0072) is unchanged β only the residency boot flag moved.
6. Provenance
- Source markers:
// opencoti F5 M6-S4 mtp(gemma),// opencoti F5 M6-S5 nextn-mtp(qwen). - Patches:
0079-gemma-assistant-mtp(gemma assistant, #423 β restored onto the 0.10.3 base; the pre-bump0074/0076/0077captures are RETIRED, #464). Qwen NextN is upstream-native (--spec-type draft-mtp) β no source capture beyond the W1 loader fix. Snapshot-diff (23 files, 0 index lines), reverse+forward byte-identical, registered indocs/protocols/UPSTREAM_SYNC.md. - Build integrity (bug-460): every server boot aborted in
common_params_parser_init(arg.cpp:2200,ggml_type_name(params.cache_type_v)) because the incremental cosmocc make did not track thecommon.hheader dep, leaving acommon_paramslayout split across TUs after W3b addeduse_checkpointsβ the consumer read an uninitializedggml_typeβ₯GGML_TYPE_COUNT. Fixed by a clean host rebuild (rm -rf o/+OPENCOTI_NO_CCACHE=1 build:llamafile:make). The DSO was never at fault; the q1_0-string proxy was a red herring (Q1_0 has no CUDA kernel, so its name is absent from a correct DSO too). Both/root/ggml-cuda.sopaths remain byte-identical (67e3171bβ¦). - Upstream convergence: gemma assistant MTP restored onto the 0.10.3 bump as
0079(#423, DONE); qwen CKPT rollback ports PR#19493 (enum names match upstream for clean re-sync).