Speculative decoding is supposed to be output-invariant. Rejected drafts are discarded, accepted tokens must match what the target model alone would have produced, and that guarantee is the entire point of the algorithm. This post reports a measured exception: on two models of the Qwen3.5-MoE lineage, running llama.cpp's native multi-token-prediction (MTP) path on the Strix Halo box, enabling MTP measurably reduced code correctness on hard tasks. For Ornith 1.5 35B A3B the cost was 17% overall and 25% on the hardest task. A paired control on Qwen3.6-35B-A3B replicated the direction at about a third of the magnitude.
The scope claim first, because it matters: this is two models, one lineage, one inference stack, small samples. I am not claiming speculative decoding is broken in general. I am claiming that on these two models it demonstrably is not free, that open llama.cpp bugs make the mechanism plausible, and that the shared lineage may be part of the story. If you run Qwen-lineage MTP under llama.cpp, this is a measure-before-trusting situation.
Where this came from
The Ornith 1.5 benchmark found that the model's native MTP head is a real throughput win on this hardware, +14% to +40% at every concurrency level, once the draft depth is tuned to n=1. That post picked Q8_0 + MTP n=1 as the production config on speed alone and explicitly deferred the quality question. This is the quality question.
The harness
Execution-based scoring only, no LLM judge. Every task ships a real pytest suite; the model's code passes real tests or it does not. Every suite was verified against a hand-written reference solution before any model touched it, which catches bugs in our own tests rather than blaming them on the model. Every config runs identical seeds, so differences are attributable to the config, not to which random generations got drawn. Scoring is partial-credit (fraction of tests passed), which gives materially more resolution than all-or-nothing at small sample sizes. Full raw data kept: every model response, reasoning trace, and pytest output.
The off-the-shelf suites (SWE-bench, BigCodeBench, LiveCodeBench, EvalPlus and friends) were surveyed and passed over: most assume cloud-scale compute or need adapters to point at a local llama-server endpoint, and the question here needed paired seeds against a live server anyway.
Phase one: five easy tasks, at ceiling, telling us nothing
The first suite was five single-function problems with canonical solutions (parse a duration string, fix a buggy LRU cache, aggregate transactions, merge intervals, a token-bucket rate limiter). Three seeds per config across BF16, Q8_0 with and without MTP, Q6_K, Q4_K_M.
Result: near-perfect everywhere. Three configs at 111/111, Q8_0 dropping a single seed on a single task, and MTP exactly quality-neutral: Q8_0 with and without it produced identical outcomes across all 15 seed-runs. I briefly reported that as closing the quality question. It closed nothing. A suite everyone aces cannot separate anything, and the one Q8_0 failure did not even correlate with precision, since the lowest-precision quant was flawless.
The ceiling effect is worth a paragraph of its own because it is the trap in this kind of work. Easy tasks produce clean, confident, useless numbers. If I had stopped there, the conclusion would have been "MTP is quality-neutral," backed by 75 seed-runs, and wrong in the direction that matters.
Phase two: three hard tasks, real signal
The second suite was designed for the model's capability frontier: a dependency-injection container (lifetimes, constructor injection, circular-dependency detection, reverse-order teardown), a two-phase-commit coordinator (the nasty case: a resource that already committed must still roll back when a later commit fails), and a recursive-descent expression parser (precedence, right-associative exponentiation, unary-minus disambiguation). Piloted on BF16 first to confirm real per-seed spread before spending GPU-hours on the grid. Five paired seeds per config, partial credit.
Ornith 1.5, mean fraction of tests passed:
| Config | Overall | Expr parser |
|---|---|---|
| BF16 | 0.812 | 0.620 |
| Q8_0, no MTP | 0.901 | 0.837 |
| Q8_0, MTP n=1 | 0.745 | 0.666 |
| Q6_K | 0.811 | 0.604 |
| Q4_K_M | 0.822 | 0.873 |
Two findings.
MTP has a real correctness cost on hard tasks. Q8_0 falls from 0.901 to 0.745 with MTP n=1, a 17% relative drop, concentrated on the hardest task: the DI container falls from 0.868 to 0.649, 25%. The easy-task neutrality was real but meaningless. A concrete flavor of the failures: under MTP, one run produced code referencing a _container variable that was never defined anywhere, an error class that appeared in no non-MTP run.
Quantization does not behave monotonically. Q8_0 without MTP scored highest overall, above BF16. Q4_K_M was worst on the DI container (0.592) and best on the parser (0.873); Q6_K mirrored it. No config was uniformly better across the three tasks, which matches the published large-scale quant studies: quantization magnifies a model's existing per-task unevenness rather than degrading cleanly with bit-width.
Why would MTP change outputs at all?
The invariance guarantee should make this result impossible, so I went looking for a mechanism. The evidence ranks like this.
llama.cpp's draft-mtp path has open, reproduced non-determinism bugs on this exact lineage. Two independent reports (#23302, #23335), both on Qwen3.6 MTP models under greedy, seeded, fully deterministic sampling, show the committed token stream changing with the draft setting. Under greedy decoding the invariance should be trivially exact (accept only if the draft equals the argmax), and it is not. The second report shows divergence at n_max=1, the tuned setting used here, not just at deeper drafts. Both issues are open with no public root cause: a reproduced symptom, not a diagnosed line of code, but sufficient on its own to explain wrong-but-plausible tokens landing in generated code.
The Qwen3.5 lineage looks unusually hard to speculate against. A recent paper on self-speculation in hybrid models measured draft/target divergence by architecture: parallel hybrids like Falcon-H1 sit at a total variation distance around 0.3, while the sequential hybrid design (their example is a Qwen3.5 model) measures 0.8, with perplexity 82x more sensitive to attention ablation. Attention is load-bearing and non-redundant in this family. The same paper found acceptance drops sharply on tasks needing long-range dependencies, which maps onto the easy/hard split here: the hard tasks are exactly the ones threading state across a whole file. Different model, same lineage; corroborating, not proof.
The general MTP literature predicts the shape. MTP heads are trained on near-term objectives and their proposal quality decays with distance from the last verified token; reported strengths concentrate on low-entropy, repetitive, structured continuations. Hard multi-constraint code is close to the worst case.
The control: same tasks, same seeds, Qwen3.6-35B-A3B
If the cost were purely architectural (lineage plus llama.cpp bugs), any model in the family should take a comparable hit. If it were purely Ornith's own MTP head being immature, a mature head should show little or none. Qwen3.6-35B-A3B is the same lineage, ships a separately published MTP variant, and is this box's production daily-driver: a natural control. Same three hard tasks, same five paired seeds, same harness.
| Config | Overall | DI container | 2PC | Expr parser |
|---|---|---|---|---|
| Q8_0, no MTP | 0.832 | 0.650 | 1.000 | 0.847 |
| Q8_0, MTP n=1 | 0.775 | 0.738 | 0.886 | 0.701 |
The direction replicates: 6.8% relative cost overall, with the two-phase-commit task losing a previously perfect score. The magnitude is roughly a third of Ornith's, and the effect is not uniform: the DI container actually improved under MTP. Read at face value, that moves the weight of evidence away from "purely architectural" toward a mix: a small, family-wide cost, consistent with the open llama.cpp bugs firing at some rate, compounded by a larger model-specific cost from Ornith's own younger MTP head. Five seeds by three tasks per arm is a small sample and the mixed per-task signal is real, so treat the attribution as suggestive. What is not ambiguous is that on neither model was MTP free on hard tasks.
What this does and does not claim
Does claim: on these two models, this quant, this stack (llama.cpp draft-mtp on ROCm/gfx1151, though the upstream bug reports come from other backends, so this does not look hardware-specific), MTP n=1 buys throughput at a measurable correctness cost that concentrates on complex, multi-constraint code. And: the easy-task eval that showed perfect neutrality was true and useless at the same time, which is a warning about eval design, not about MTP.
Does not claim: that this generalizes beyond the Qwen3.5-MoE lineage, that the exact magnitudes would survive more seeds, or that speculative decoding with a separate draft model has the same problem (it was not tested here). The shared lineage of both affected models may well be part of the story; two models cannot establish that.
One gap worth naming: everything published on MTP quality that I could find measures perplexity, acceptance rate, or QA benchmarks. I could not find a prior measurement of MTP's effect on execution-graded code correctness, the metric that actually matters for a coding agent. If you know of one, I would like to read it.
Takeaways
- Verify spec-decode invariance on your own stack, against tests that execute. "Speculative decoding never changes outputs" is a property of the algorithm, not of every implementation of it. On this stack it did not hold.
- Eval difficulty is a validity condition. The easy suite produced 75 seed-runs of confident neutrality that phase two overturned. If every config is near-ceiling, the eval is measuring the ceiling.
- Speed and correctness need the same rigor. The throughput bench that picked this config was careful: seeds, paired arms, acceptance-rate diagnosis. It still almost shipped a config that costs a quarter of the hardest task's correctness, because quality was deferred. Bench both before wiring anything into production.
Reproducibility: AMD Ryzen AI MAX+ 395 (Radeon 8060S, gfx1151), 128 GB unified memory, llama.cpp draft-mtp (--spec-type draft-mtp --spec-draft-n-max 1). Ornith 1.5 35B A3B (bartowski GGUFs) on llama.cpp b10530; Qwen3.6-35B-A3B control on the production b10038 container. Custom execution-based harness: pytest-scored tasks self-verified against reference solutions, paired seeds, partial-credit scoring, temp 0.6 per the vendors' agentic-use recommendations. Throughput context in the Ornith post; base stack in the setup guide.