Software engineer / Miami, FL

Writing

Field note · llama.cpp

A per-request speculative.type of "none" still drafted 198 tokens

llama-server accepts per-request speculative.n_max, p_min and type, which makes sweeping a drafter on one load look safe. It is not: the per-request value appends to the launch-time list rather than replacing it, so the echoed settings read speculative.types = none,draft-mtp and every row of a six-configuration sweep was the same configuration. The "off" arm reported 198 drafted tokens. A speculative-decoding baseline needs a server restart, and the echoed settings block is the only thing that reveals it.

Hardware
NVIDIA DGX Spark · GB10 (SM121) · aarch64 · 128 GB unified
Software
llama.cpp master + PR #27836 (draft) · unsloth/Qwen3.8-Flash-Next-GGUF UD-IQ4_XS with a grafted MTP head

The symptom

Six configurations swept on a single server load — varying speculative.n_max and p_min, plus a "speculative.type": "none" arm as the baseline — came back within 2% of each other. That looked like a clean, if disappointing, result: speculative decoding making no measurable difference.

The none arm reported 198 drafted tokens.

The cause

The give-away was in the echoed settings the server returns:

speculative.types = none,draft-mtp

Plural. The per-request value appended to the launch-time list rather than replacing it. Every row in that sweep was the same configuration, and the arm labelled "off" was running MTP like all the others. The 2% spread was run-to-run noise across six identical arms.

The fix

Restart the server for the baseline. MTP=0 ./start.sh (or simply omitting --spec-type) is the only arm that is genuinely off, and it must be its own process.

Correctly measured, with a real restart between arms — UD-IQ4_XS, q8_0 KV, temp 0, 300 tokens, single stream:

workloadbareMTP (n-max 3, p-min 0.75)acceptance
code27.75 tok/s43.30 (+56%)91.6% (197/215)
prose28.28 tok/s33.47 (+18%)86.5% (154/178)

A 56% effect, invisible in the original sweep.

What to actually check

The failure here is not really about a flag. It is that the sweep had no arm capable of producing a different answer, and every symptom of that was consistent with a true negative result. Six rows agreeing looks like a robust finding. It is the same shape as a fixture that cannot exhibit the behaviour under test.

Two habits that would have caught it in seconds:

  • Read the echoed settings on every arm, not just the first. The server told me exactly what it was running and I did not look until the numbers were already written up.
  • Assert the baseline is inert. spec_decode_* counters at /metrics should be zero on the off arm. Non-zero drafted tokens with speculation "disabled" is a contradiction the test should fail on, not something a human has to notice.

Had I not checked the echo, I would have published a well-formatted table showing speculative decoding making no difference whatsoever. That is the third time this year I have shipped an instrument that agreed with me for structural reasons rather than empirical ones — the longer version of that argument is in my guardrail checked the config, not the corpus, and the full graft write-up is in I grafted a speculative decoding head into a 90 GB model file.

A related proxy failure on the same server

Same root cause, different surface. A script here checked whether an upstream multi-slot fix had landed by testing for its commit hash. The pull request was squash-merged, so none of the original SHAs exist in master, and the checker announced "not fixed, keep PARALLEL_SLOTS=1" against a master that plainly contained the fix.

An earlier version of the same script had tested whether a GGML_ASSERT had disappeared — but the assert was correct and deliberately kept, so that check would have reported "not fixed" forever.

Both failures share one cause: testing for a proxy instead of for the thing itself. Grepping the code for the changed identifier survives rebases, squashes, cherry-picks and reverts alike:

grep -c LLM_ARCH_QWEN4EXP src/llama-context.cpp        # expect 1
grep -c llama_mul_mat_hadamard src/models/qwen4exp.cpp # expect 4
Status
workaround
First seen
August 28, 2026

Have a thought about this?

Questions, corrections, and your own experience are welcome. Reply by email

All writing
  • Essay

    I grafted a speculative decoding head into a 90 GB model file

    The model card advertised a 4B multi-token-prediction head. No published GGUF contained it, and the architecture had no code path to run it. Both halves arrived within 36 hours from two different strangers, in incompatible forms — so I merged the head into the target file myself. It went from 27.75 to 43.30 tok/s, and three of my four mistakes along the way were about verification, not tensors.

  • Essay

    Tokens per second told me the wrong model was faster

    A 125B model decoding at 26.6 tok/s looks unremarkable beside a 31B at 29.3 — until you notice the 31B is running a drafter, so its number is a forward-pass rate multiplied by an acceptance length. Per forward pass the big model was 3x faster. Here is how to decompose the number, and what the bandwidth arithmetic says is still on the table.