Skip to content

perf(speculative): draft a tree instead of a chain, so an MTP round emits more than 2.33 tokens #1204

Description

@inureyes

Why now

Phases 0 and 3 of #1185 have landed and the profile has been re-measured. The drafter side is no longer where the time is.

Round split on M5 Max, qwen3.8-27b-4bit with the qwen3_5_mtp drafter, --draft-block-size 3 --temp 0, after #1203:

component ms/round share
verify forward (T=3) ~40.0 ~85%
draft_block 2.70 ~5.7%
accept hook 2.74 ~5.8%
finalize + residual ~1.6 ~3.4%

A round costs about 47 ms and emits 2.33 tokens. Driving the drafter to zero cost leaves 41.6 ms and the same 2.33 tokens: 56.0 tok/s, or 1.77x classic decode. We are at roughly 1.5x. Everything left is within 0.27x, and none of it is on the drafter.

Past that ceiling the round has to emit more tokens, not cost less.

Deeper linear chains do not do it, and that is measured

The obvious move is a wider block. It fails, and not marginally. Sweeping --draft-block-size on this pairing (single sweep, thermally degraded host, so read the trend and not the absolute tok/s):

block acceptance emitted/verify tok/s
3 0.65 2.3 21.30
4 0.49 2.47 16.24
6 0.35 2.72 12.12
8 0.25 2.74 9.12
12 0.17 2.82 5.80

Acceptance collapses by a factor of four while emitted-per-verify moves 2.3 to 2.8. A linear chain spends its whole budget on one guess about the future, so each extra position multiplies the chance that the prefix already diverged. The verify cost rises with the block and the yield does not.

That is the argument for branching rather than lengthening: spend the extra verify positions on alternatives at uncertain steps instead of on more of a single guess.

The headroom exists on the verify side

From the M5 Max report on #1182 (verify_forward_cost_scaling): this hardware verifies T=4 for 1.29x the price of T=1 and T=8 for 2.48x. A multi-token verify forward is close to free per extra position here, which is precisely the property tree drafting needs and the property a linear chain cannot convert into tokens.

Rough arithmetic with today's numbers: if a tree lifted emitted-per-round from 2.33 to about 3.2 while the verify forward grew to T=4 cost, the round lands near 48 ms for 3.2 tokens, about 66 tok/s, or 2.1x classic. That is a projection from the cost model, not a measurement, and the acceptance term is the part that has to be measured rather than assumed.

What this needs

Substantially more than Phases 0 through 4. Three pieces, none of which exist today:

  1. A tree attention mask on the verify path. The verify forward currently builds a causal mask over a linear block. A tree needs each node to attend to its ancestors only. The mask is a function of the tree topology, which changes per round, so it cannot be cached the way the causal mask can.
  2. A tree-aware accept walk. speculative_walk accepts the longest matching prefix of a chain. For a tree it has to descend the branch the target actually agreed with, which changes both the walk and what emitted_per_verify means.
  3. Tree-aware rollback. verify_finalize trims the KV cache back to the accepted prefix. With a tree, the rejected siblings' positions were also written and have to be trimmed correctly. This is the piece most likely to produce silent corruption if it is wrong, and it interacts with the recurrent state in the GatedDeltaNet layers, which cannot be trimmed the way a KV cache can (see fix(speculative): Qwen 3.5 MTP output is not byte-identical to classic decode, and the exactness probe does not catch it #1201 for what that failure mode looks like in practice).

Where the branches go is a fourth decision: EAGLE-2 branches by drafter confidence, SpecInfer by a fixed topology. Confidence-based branching needs the drafter's logits, which the current step already computes and discards past the argmax.

Exactness

Unchanged in principle: the target verifies every node, so tree drafting can only cost acceptance, never output. But the byte-identity contract is a per-position claim against the single-token chain, and #1186 shows it depends on which MLX kernel the verify forward dispatches at each M. A tree changes M per round, so the #1189 probe has to be re-derived for the tree case rather than assumed to carry over.

Do not build this on the assumption that a passing probe implies end-to-end parity. It does not, which is what #1201 established the hard way.

Acceptance criteria

  • Tree topology, mask construction, walk and rollback implemented behind a flag, defaulting off.
  • Rollback correctness pinned by test, including the recurrent-state path, before any throughput number is quoted.
  • Acceptance and emitted-per-round measured against the linear chain at the same block budget, on the Qwen 3.8 27B pairing and regression-checked on Gemma 4.
  • Temperature-0 output compared against classic decode end to end, by diffing generated text, not by trusting the probe.
  • The fix(speculative): gate MTP on a measured block-vs-chain exactness probe #1189 exactness gate extended to cover the tree case, or the tree explicitly gated to configurations where it holds.
  • Measured on M5-class and on M1 Ultra, since the verify cost scaling that motivates this differs by generation.

Out of scope

The verify-side early-exit walk (#1179), which is a different way to spend the same verify forward and should be measured against this rather than combined with it.

Refs #1185, #1186, #1189, #1201

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:coremlxcel-core: MLX FFI, primitives, KV cache, layersarea:inferenceGeneration, sampling, decoding (incl. speculative, DRY)priority:mediumMedium prioritystatus:readyReady to be worked ontype:performancePerformance improvements

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions