Repository navigation
feat: extend speculative_bench MTP path past Gemma 4 Unified #1613
Description
Problem / Background
run_mtp hard-matches one LoadedModel variant at src/bin/speculative_bench.rs:436-443, so speculative_bench can produce MTP numbers for exactly one checkpoint:
let unified = match &model {
LoadedModel::Gemma4Unified(u) => u,
other => anyhow::bail!(
"MTP bench currently supports a Gemma 4 Unified target; \
load_model returned a different variant ({:?})",
std::mem::discriminant(other)
),
};Measured on the 2026-09-04 M5 Max sweep (mlxcel 0.6.0 at b2ff1eee, MLX pin 9a795735, speculative_bench --sweep --max-tokens 128, CSV benchmarks/metal_m5max_spec_2026-09-04.csv), that restriction costs two pairings:
- The Gemma 4 31B MTP pairing (
src/bin/speculative_bench.rs:260-266) fails at all three K withMTP run failed: MTP bench currently supports a Gemma 4 Unified target; load_model returned a different variant (Discriminant(26)). Not a missing checkpoint:models/gemma-4-31b-it-assistant-bf16is on disk and the target measures 28.3 tok/s on its baseline row.models/gemma-4-31b-it-4bit/config.jsonisGemma4ForConditionalGeneration(model_type: gemma4) and loads asLoadedModel::Gemma4VLM, while the 12B isGemma4UnifiedForConditionalGeneration. - Qwen 3.8 MTP cannot be measured at all.
models/qwen3.8-27b-4bit(Qwen3_5ForConditionalGeneration,qwen3_5),models/qwen3.8-27b-mtp-4bitandmodels/qwen3.8-27b-mtp-bf16are all present, and the target measures 32.88 tok/s decode inbenchmarks/metal_m5max_2026-09-03.csv, butREACHABLE_PAIRINGS(:236-284) has no Qwen 3.8 entry. A catalog entry alone would not help, since the same match rejects the target; lifting the restriction is the prerequisite.
The supported pairing is healthy on this host (Gemma 4 Unified 12B: 45.3 tok/s baseline, 62.9 / 70.9 / 70.0 tok/s at K=2/4/8, acceptance 55.6% / 35.0% / 34.6%, matching the GB10 matrix in docs/benchmark_results/model_tests.md exactly), so this is a harness limit, not missing model support.
Current Behavior
Every piece the bench needs already exists in the tree:
| Concern | Existing implementation |
|---|---|
Per-variant &dyn LanguageModel selection |
src/server/batch/speculative_burst.rs:1113-1150 |
Per-variant MtpTarget adapter selection (Gemma 4 text / VLM / Unified, Qwen 3.5 text / MoE / VLM, Inkling) |
src/server/batch/speculative_burst.rs:1247-1360 |
| Gemma 4 VLM adapter the 31B target needs | Gemma4VLMtpTargetAdapter, src/models/gemma4_mtp_target.rs:975 |
| Qwen 3.5 adapter the Qwen 3.8 target needs | Qwen35MtpTargetAdapter, src/models/qwen3_5_mtp_target.rs:195 |
qwen3_5_mtp drafter resolution |
src/lib/mlxcel-core/src/drafter/mod.rs:1140 |
LanguageModel on the enum itself, including release_sequence_state_by_id |
src/loaded_model.rs:225 (delegate_language_model!), impl at :389, method at :518 |
Proposed Solution
- Replace the
Gemma4Unifiedunwrap with per-variant adapter selection mirroringspeculative_burst.rs:1247-1360. Bind the drafter against&model as &dyn LanguageModeland release withmodel.release_sequence_state_by_id(seq_id)on the enum, so only theMtpTargetconstruction needs a match. MtpGeneratoris generic overMtpTarget, so factor the timed run (warm-up burst, timed generate, acceptance capture, sequence release) into one function generic overT: MtpTargetand call it from each arm rather than copying the body per variant.- Keep an explicit fallback arm that names the loaded variant and the supported families instead of printing
std::mem::discriminant. Widenmodel_variant_label(src/server/batch/speculative_burst.rs:3034) topub(crate)and reuse it. - Add Qwen 3.8 to
REACHABLE_PAIRINGSonce the restriction is lifted: aqwen3.8-27b-4bitbaseline withBenchKind::None, plus the same target with theqwen3.8-27b-mtp-4bitdrafter,BenchKind::Mtp,block_size: Some(4).
Rejected: keeping the restriction and deleting the 31B pairing. The adapters exist and the server already dispatches them, so the bench would be the only place pretending the support is absent.
Scope
In scope: src/bin/speculative_bench.rs (run_mtp dispatch, REACHABLE_PAIRINGS); src/server/batch/speculative_burst.rs (visibility of model_variant_label only); docs/benchmark_results/model_tests.md (M5 Max table at :308-340, CSV index row at :25); docs/benchmarks.md:949.
Out of scope: the three --kind dflash rows, a separate known deferral (DFlash loader plus a public Qwen3NextCache API) that keeps its current status string; batched (B > 1) MTP adapters; Inkling pairings, which have no local checkpoint.
Implementation Notes
- Constraints: Qwen 3.5 MTP exactness is Metal-only in the server path, which declines when
mlxcel_core::metal_is_available()is false (speculative_burst.rs:1123-1135). Mirror that guard so a CUDA sweep records the reason as a row status instead of emitting an unsound number. - Edge cases: a drafter that loads but fails
validate_target_compat(backbone hidden size or vocab mismatch) stays a per-row status string, never a sweep abort; a missing checkpoint directory keeps the existingresolve_model_dirfallback and skip;block_size < 2keeps its early bail. - Error handling: every failure stays a row status in the CSV and the Markdown table, and the sweep still completes all remaining rows.
Acceptance Criteria
-
run_mtpacceptsGemma4,Gemma4VLM,Gemma4Unified,Qwen35,Qwen35Moe,Qwen35VLMandQwen35MoeVLMtargets, and an unsupported variant fails with a message naming the loaded variant and the supported families. (PR feat(bench): dispatch speculative_bench MTP past Gemma 4 Unified #1621; the check runs before the drafter load and also names the target directory, because the shared label table reports variants outside the speculative-capable set asother.) -
Gemma 4 31B + MTP assistantrows carry numeric tok/s, speedup, acceptance rate and mean accepted length at K=2/4/8, or the pairing is dropped fromREACHABLE_PAIRINGSwith the reason recorded in the docs. (Measured on M1 Ultra, not M5 Max: 18.4 / 14.9 / 14.9 tok/s, 0.93x / 0.75x / 0.75x against a 19.9 tok/s baseline.) - A Qwen 3.8 27B baseline row and MTP pairing are in
REACHABLE_PAIRINGSand both produce numbers. (24.9 tok/s baseline, 22.5 / 17.2 / 9.3 tok/s at K=2/4/8 on M1 Ultra.) - Gemma 4 Unified 12B rows are unchanged within run-to-run noise (K=4 near 70.9 tok/s, acceptance 35.0%, mean accepted length 1.05). (Not assertable as written: 70.9 tok/s is an M5 Max figure and this run is M1 Ultra. The rows still produce numbers, 36.0 / 28.5 / 29.3 tok/s at K=2/4/8 against a 38.4 tok/s baseline, acceptance 54.5% / 39.6% / 39.6%. An M5 Max re-run against the fixed binary is still pending.)
-
docs/benchmark_results/model_tests.mdcarries the new rows, drops the "unsupported target" note, and updates the CSV index row count at:25. (Onbench/0.7.0-refreshatf4ff3752, not onmain; those anchors exist only on that branch.) -
cargo clippy --all-targets -- -D warningsandcargo fmt --checkare clean. (Run scoped to the touched target ascargo clippy --bin speculative_bench --features metal,accelerate -- -D warnings, pluscargo fmt --all -- --check; the unscoped workspace form exceeds this host's run budget.)
Verification
cargo build --release --features metal,accelerate --bin speculative_bench
cargo clippy --all-targets -- -D warnings && cargo fmt --check
# The two single pairings that fail today.
./target/release/speculative_bench --target models/gemma-4-31b-it-4bit \
--draft models/gemma-4-31b-it-assistant-bf16 --kind mtp --block-size 4 --max-tokens 128
./target/release/speculative_bench --target models/qwen3.8-27b-4bit \
--draft models/qwen3.8-27b-mtp-4bit --kind mtp --block-size 4 --max-tokens 128
# Full sweep, which regenerates the table pasted into the docs.
./target/release/speculative_bench --sweep --batch 1 --max-tokens 128 2>&1 | tee /tmp/bench-sweep.logPass: both single runs print a [bench/mtp] Done: line with non-zero tok/s and acceptance_rate, the sweep table shows numeric cells for Gemma 4 31B MTP and Qwen 3.8 MTP, and no row carries a status starting with MTP bench currently supports.
Related: #154 (Gemma 4 Unified MTP drafter), #1165 (qwen3_5_mtp drafter and its Metal exactness gate), #638 (the K sweep behind --k-values).
Activity
feat(bench): dispatch speculative_bench MTP past Gemma 4 Unified
docs(bench): record the M1 Ultra speculative sweep for #1613
inureyes commentedon Sep 4, 2026
Implemented across two branches, because the code and the benchmark artifacts belong in different places. Recording which acceptance criterion lives where, so this issue closing is not read as all of them landing on
main.Code, in PR #1621 off
main(closes this issue).run_mtpnow selects theMtpTargetadapter perLoadedModelvariant, mirroringsrc/server/batch/speculative_burst.rs; the warm-up, timed generate, acceptance capture and sequence release live in onerun_mtp_timedgeneric overT: MtpTargetrather than one copy per arm; an unsupported variant is rejected before any drafter IO with a message naming the loaded variant, the target directory and the supported families; the Qwen 3.5 Metal-only guard is mirrored as a row status; andREACHABLE_PAIRINGSgains the Qwen 3.8 27B baseline and MTP pairing.Benchmark artifacts, in
bench/0.7.0-refresh(PR #1617), commitf4ff3752. Thedocs/benchmark_results/model_tests.mdedits that drop the unsupported-target limitation and record the new rows, plusbenchmarks/metal_m1ultra_spec_2026-09-04.csv. That branch is the repository's benchmark-refresh PR and stays open until the remaining host sweeps land in it, so those edits reachmainwhen it merges, not when #1621 does.Host substitution. This issue's figures are M5 Max. The work was implemented and verified on Apple M1 Ultra 128 GB (Metal), mlxcel 0.7.0-beta.1, MLX pin
9a795735. The M5 Max table is not overwritten with M1 Ultra numbers and an M5 Max re-run against a binary carrying the fix is still pending. The criterion asking that Gemma 4 Unified 12B stay near 70.9 tok/s at K=4 is not assertable here: that is an M5 Max number, and this host reads 28.5 tok/s at K=4 against its own 38.4 tok/s baseline.Measured on M1 Ultra,
--sweep --batch 1 --max-tokens 128, 16 rows, 9 measured MTP rows, no row carrying an unsupported-target status:Every ratio below 1.00x is the host, not a defect:
docs/benchmark_results/speculative-decoding-m1ultra-2026-08-19.mdmeasures a block-4 verify round at 2.70 classic decode steps on M1 Ultra against 1.27 on M5 Max, which is also why the staticMLXCEL_ENABLE_MTP_B1gate declines B=1 MTP on this Apple GPU generation.One fix beyond the issue text.
resolve_model_dirprobed<CARGO_MANIFEST_DIR>/models/<name>and../mlxcel-internal/models/<name>only. On a host whose checkpoint store ismodels/mlx/<name>neither probe resolves, so every pairing in a--sweepfell through to the missing-checkpoint skip: the sweep completed, reported no error, and measured nothing. Twomodels/mlx/<name>probes now run after the existing two, so no host that resolves today changes behavior. Without it none of the criteria above could be verified.style: drop an em dash from the Qwen 3.8 pairing comment
feat(bench): dispatch speculative_bench MTP past Gemma 4 Unified (#1621)
perf(cuda): convert bf16 weights to f16 at load on pre-Ampere GPUs (#…
perf(cuda/quant): add a Volta tensor-core arm to qmm_naive (#1678)
docs(benchmarks): refresh M5 Max results for 0.6.0