Speculative Decoding for Mixture-of-Experts Models: Expert Bytes, Kernel Bandwidth and the Best Draft Length
Organizations: ReClan AI Studio, LLC
Abstract
Speculative decoding checks several drafted tokens in one verification pass. On a mixture-of-experts (MoE) model, consecutive tokens often select the same experts. A deduplicating engine reads each selected expert once per pass, so it requests fewer bytes than a per-slot engine, which reads it once per token. We write a step's time as the bytes each kernel requests over its effective bandwidth, plus fixed time. Under geometric acceptance, we derive from this step time when speculation is faster than plain decoding. The best draft length is a monotone staircase in the acceptance rate. We bound how a bandwidth error moves each result, and we check the main steps in Lean. Deduplication is faster exactly where its share of the per-slot bytes is below its ratio of the per-slot bandwidth. On llama.cpp Metal, MLX-LM and llama.cpp CUDA, which we modified to deduplicate, the per-slot kernel has the higher effective bandwidth (r = 0.32 to 0.75). Thus deduplication is faster only from 5 to 8 tokens per pass, or never. We fit the bandwidths on one layer's expert operation, run alone, in under an hour per model. Bytes from routing traces over these bandwidths predict the time that deduplication saves. The measured saving lies inside its error bound at every point on CUDA and MLX-LM and at 36 of 48 points on Metal. The crossover is the first pass size at which deduplication is faster. The prediction places it within one pass size in 10 of 12 configurations of engine, model and depth. We computed these predictions after the measurements. On DeepSeek-V2-Lite, operation times measured before its speculative runs give the best draft length on CUDA within one at all 22 acceptance values.
Figures & tables
| re-read | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| engine | model | cost | ||||||||
| llama.cpp CUDA | OLMoE | 0.18-0.22 | 618 | 464 | 0.75 | 0.82 | 933 | 546 | 0.58 | 0.50 |
| llama.cpp CUDA | gpt-oss | 0.34 | 654 | 346 | 0.53 | 0.72 | 1018 | 434 | 0.43 | 0.36 |
| llama.cpp CUDA | DeepSeek-V2-Lite | 523 | 355 | 0.68 | 0.83 | 814 | 480 | 0.59 | 0.62 | |
| llama.cpp Metal | OLMoE | 0.88-0.90 | 250 | 147 | 0.59 | 0.82 | 428 | 219 | 0.51 | 0.50 |
| llama.cpp Metal | Qwen3 | 0.92-0.98 | 131 | 65 | 0.50 | 0.77 | 280 | 98 | 0.35 | 0.43 |
| prediction | bound |
|---|---|
| verify time | (17) |
| saving | (18) |
| faster kernel | sides of ( 7 ) agree if (19) |
| best draft length | : at most of the throughput lost; if for every (20) |
| best speedup | (21) |
| saving error (ms) | crossover | within one | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| engine | model | wiki | text | op. time | meas. | wiki | text | op. time | wiki | text | op. time | |
| Metal | OLMoE | 512 | 0.43 | 0.42 | 0.77 | 7 | 7 | 7 | 8 | 9 | 9 | 9 |
| Metal | OLMoE | 4096 | 0.98 | 0.91 | 2.28 | 7 | 7 | 7 | 8 | 8 | 8 | 8 |
| Metal | Qwen3 | 512 | 1.51 | 0.92 | 2.24 | 7 | 8 | 7 | ||||
| Metal | Qwen3 | 4096 | 3.70 | 1.83 | 1.25 | 8 | 8 | 8 | ||||
| Metal | gpt-oss | 512 | 2.23 | 3.77 | 3.89 | 8 | 10 | 9 | 10 | |||
| engine | version | crossover | sign | saving error | within 1 | loss | error / | |
|---|---|---|---|---|---|---|---|---|
| llama.cpp CUDA | measured-time | 512 | / | 7 of 7 | 0.23 | 11 | 2.9% | 9.9% / 0.072 |
| measured-time | 4096 | / | 7 of 7 | 0.28 | 11 | 4.1% | 10.1% / 0.077 | |
| llama.cpp Metal | measured-time | 512 | / | 8 of 8 | 1.6 | 9 | 20.1% | 34.0% / 0.27 |
| measured-time | 4096 | / | 8 of 8 | 4.4 | 6 | 22.5% | 28.4% / 0.25 | |
| calibrated | 512 | / | 8 of 8 | 1.5 | 11 | 6.4% | 4.3% / 0.036 | |
| calibrated | 4096 | / | 8 of 8 | 4.4 | 9 | 8.3% | 10.5% / 0.098 |
Appendix figures & tables6 assets
Supplementary material from the paper’s appendix.
Appendix
| Symbol | Meaning |
|---|---|
| acceptance rate (geometric acceptance) | |
| , the acceptance above which one draft is faster than none | |
| -th step point | |
| bytes of one expert of the timed layer, | |
| , the time the -th draft adds |
| engine | tested sizes | other sizes | where the source decides |
|---|---|---|---|
| llama.cpp Metal b11617 | per-slot | per-slot for ; tiled (32 tokens per tile) for , deduplicating only at for two models | dispatch test “batch rows” in ggml-metal-common.cpp ; matrix-vector kernel in mul_mv.metal , one grid slice per token-slot pair |
| MLX-LM 0.32.0 | per-slot | per-slot for (32 or 64); tiled (16 per tile), neither, above | dispatch branch on rows per expert in mlx/backend/metal/quantized.cpp ; MoE call in mlx_lm/models/switch_layers.py |
| llama.cpp CUDA b11617 | upstream; run in Sec. 4 | both at ; per-slot for ; deduplicating for (OLMoE, Qwen3), times union (gpt-oss) | batch limit 8 for the matrix-vector kernel in ggml-cuda/mmvq.cuh ; dispatch in ggml_cuda_mul_mat_id ( ggml-cuda.cu ); tiles in ggml-cuda/mmq.cu |
| engine | switch | deduplication off | deduplication on | , deduplicating |
|---|---|---|---|---|
| llama.cpp Metal, modified | deduplicating kernel from 2 rows (32 upstream) | mul_mv_id , per-slot, | mul_mm_id , deduplicating, | 1 (OLMoE Q8_0, gpt-oss MXFP4); 2.14 or 2.33 (Qwen3 Q4_K_M, K-quant reload) |
| MLX-LM, modified | sort rows by expert, gather_qmm_rhs | gather_qmv , per-slot, | sorted rows, deduplicating, | 10/9 (16-row tile) |
| llama.cpp CUDA, modified | per-slot batch cap 8 to 1 | MMVQ, per-slot, ; MMQ at | MMQ, deduplicating, | 1 (OLMoE); 500/459 (gpt-oss, padded unit) |
| verify-time error | ||||||||
|---|---|---|---|---|---|---|---|---|
| engine | model | amplification | implied | engine | prediction | measured-time | calibrated | |
| Metal | OLMoE | 512 | 6.0 to 79 | 0.014 | 0.031 | 19% | 20% | 6% |
| Metal | OLMoE | 4096 | 6.2 to 86 | 0.028 | 0.031 | 24% | 24% | 6% |
| Metal | Qwen3 | 512 | 3.9 to 7.8 | 0.020 | 0.031 | 21% | 22% | 6% |
| Metal | Qwen3 | 4096 | 4.2 to 19 | 0.047 | 0.031 | 25% | 31% | 4% |
| Metal | gpt-oss | 512 | 3.3 to 343 | 0.033 | 0.031 | 20% | 15% | 9% |
| rule | CUDA 512 | CUDA 4096 | Metal 512 | Metal 4096 |
|---|---|---|---|---|
| crossover within one | / , holds | /9, holds | / , holds | / , holds |
| within one at of 11 values | 11, holds | 11, holds | 9, holds | 6, fails |
| loss at most 6% at every value | 2.9%, holds | 4.1%, holds | 20.1%, fails | 22.5%, fails |
| sign of where the interval excludes 0 | 7 of 7, holds | 7 of 7, holds | 8 of 8, holds | 8 of 8, holds |
| verify time within 15%, both kernels | 9.9%, holds | 10.1%, holds | 34.0%, fails | 28.4%, fails |
| best speedup inside ( 2 ) | , holds | 0.077, holds | 0.27, holds | 0.25, holds |
Speculative Decoding
Speculative decoding accelerates generation by drafting multiple tokens and verifying them in parallel with a larger target model.
See all 311 papers