perf(speculative): single-slice the MTP accept hook and drop the drafter's materialized causal mask - #1253
Merged
Conversation
…s materialized causal mask Two structural cleanups from issue #1185 Phase 4, both confined to the qwen3_5_mtp drafter: - accept_verified_tokens built its paired-hidden block with a chain of per-position concatenate calls. The positions are the contiguous range starting at keep, so one slice of verify_hidden replaces n slices and n-1 intermediate buffers. - forward_hidden_stack materialized an [s, s+offset] additive causal mask on every multi-token call. Multi-token forwards now route through the shared causal_attention wrapper, which derives the same bottom-right alignment from the cache K length and selects MLX's native causal mask mode (and the M5 NAX causal kernel where present) without building a mask array. Single-token draft steps keep the maskless dispatch unchanged. The caching suggested in the issue does not apply: the offset advances every round, so a (s, offset) key never repeats. Drafter numerics only influence acceptance (module docs, layer.rs): the target verifies every proposal, so output remains byte-identical to classic decode either way.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The two Phase 4 structural cleanups from #1185, plus the measurement that closes out the drafter side of that issue.
accept_verified_tokensbuilt its paired-hidden block with a chain of per-positionconcatenatecalls. The positions are the contiguous range starting atkeep(draftskeep..accepted, then positionacceptedfor the bonus), so onesliceofverify_hiddenreplaces n slices plus n-1 intermediate buffers.forward_hidden_stackmaterialized an[s, s+offset]additive causal mask on every multi-token call. Multi-token forwards now route through the sharedcausal_attentionwrapper, which derives the same bottom-right alignment from the cache K length and selects MLX's native"causal"mask mode (and the M5 NAX causal kernel where present) without building a mask array. Single-token draft steps keep the maskless dispatch byte-for-byte unchanged.The caching the issue suggested for the mask does not apply: the offset advances every round, so an
(s, offset)key never repeats. Removing the materialization is the version of that cleanup that actually exists.Related issues
Refs #1185 (Phase 4).
Type of change
perf— structural cleanup on the drafter's per-round pathWhy this cannot move the output
Drafter numerics only influence acceptance (
layer.rsmodule docs): the target verifies every proposal, so emitted tokens are the target's own greedy choices regardless of what the drafter proposes. Verified directly below anyway, and on this run even the drafted blocks came out identical.Measurements
Apple M5 Max 128 GB, macOS 26.6.1,
qwen3.8-27b-4bit+qwen3.8-27b-mtp-4bit, block 3, temperature 0, offline CLI. Arms:mainat9e2c6675(before) and this branch (after). ABBA blocks, two warm-ups discarded, 6 samples per arm, 8 s cooldowns.Byte identity, 400 tokens: classic == MTP(before) == MTP(after). The two MTP arms also produced identical round statistics (113 rounds, acceptance 0.8274), so the causal-mode switch did not perturb the drafter's proposals on this run.
Throughput, 300 tokens (tok/s): before median 54.81 (52.93-55.90), after median 55.27 (53.52-56.29). +0.8%, inside the spread. This is a wash, as expected at a ~370-token context where the mask is small; the change is structural, not a headline number.
Round-loop diagnostics (one logged run per arm, same 113 rounds):
The drafter-side terms move at the sub-1% level; the verify difference is run noise on an untouched path.
Gemma 4 regression check (
gemma-4-12b-it-4bit+ 4-bit assistant, block 5, 300 tokens, branch binary): 71.05 tok/s, acceptance 0.7841, emitted_per_verify 3.6463 — the same chain figure PR #1214 recorded for this pairing. The gemma4_assistant drafter and the shared round loop are untouched by this PR.Phase 2a, considered and declined
Phase 2a (fused LM-head + argmax) targeted 21.4% of a drafter step that #1203 has since cut from 10.5 ms to 2.7 ms per round. The remaining prize is ~0.6 ms per forward, ~1.2 ms per round, under 2% of a 62 ms round, and it costs a custom quantized-matmul-with-argmax Metal kernel that has to survive every future MLX pin bump. Declined on that ratio; recorded here so the issue can close over it.
Test plan
cargo fmt --checkcargo clippy --workspace --all-targets --features metal,accelerate -- -D warningscargo test --release -p mlxcel-core --features metal,accelerate qwen3_5_mtp(22 tests) andspeculative(129 tests)