Repository navigation
spec: MTP draft-head subset + even drafts / small-batch verify for qwen4exp - #108
gaetan-puleo wants to merge 252 commits into
Conversation
Removed the prohibition on using AI for writing posts.
Revise AI usage guidelines in CONTRIBUTING.md
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…#6) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ngs, grace, usage telemetry) (#5) Co-authored-by: voidsurfer <18734507+voidsurfer@users.noreply.github.com>
…#4) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: voidsurfer <18734507+voidsurfer@users.noreply.github.com>
Co-authored-by: Marshall <assistant@llama.cpp>
Co-authored-by: Harkirat Gill <harkirat.gill@amd.com> Co-authored-by: Bartowski <3266127+bartowski1182@users.noreply.github.com> Co-authored-by: Xuan Son Nguyen <son@huggingface.co> Co-authored-by: Aparna M P <aparmp@qti.qualcomm.com> Co-authored-by: cqderek <cqderek@gmail.com> Co-authored-by: Daniele Zannotti <d.zannotti@me.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ruixiang Wang <wangruixiang07@outlook.com> Co-authored-by: Sigbjørn Skjæret <sigbjorn.skjaeret@huggingface.co> Co-authored-by: Georgi Gerganov <ggerganov@gmail.com> Co-authored-by: Brad Smith <1472326+infinitewarp@users.noreply.github.com> Co-authored-by: Nathanw1014 <67372905+Nathanw1014@users.noreply.github.com> Co-authored-by: Titaniumtown <titaniumtown@proton.me> Co-authored-by: Ozymandias_EBON <112784549+johnkarlhill@users.noreply.github.com> Co-authored-by: Zijun Yu <zijun.yu@intel.com> Co-authored-by: Mostafa Faheem <mostafaaafaheem@gmail.com> Co-authored-by: Ravi Panchumarthy <ravi.panchumarthy@intel.com> Co-authored-by: zhaixuejun1993 <xuejun.zhai@intel.com> Co-authored-by: Strongtut <Strongtut@users.noreply.github.com> Co-authored-by: Strongtut <8432058+Strongtut@users.noreply.github.com> Co-authored-by: ravel7524 <58877666+ravel7524@users.noreply.github.com> Co-authored-by: Jeff Bolz <jbolz@nvidia.com> Co-authored-by: Tekin Ertekin <tekin.ertekin@gmail.com> Co-authored-by: Eric A Stalee <87948564+Eric-A-Stalee@users.noreply.github.com> Co-authored-by: kurquhar <kurquhar@qti.qualcomm.com> Co-authored-by: Kristopher Urquhart <kurquhar@qti.qualcom.com> Co-authored-by: Jhen-Jie Hong <iainst0409@gmail.com> Co-authored-by: Nick Farrell <nick.farrell@aiven.io> Co-authored-by: Niklas Wenzel <dev@nikwen.de> Co-authored-by: Hongqiang Wang <wangh@qti.qualcomm.com> Co-authored-by: Li He <lih@qti.qualcomm.com> Co-authored-by: codemonkey <441345965@qq.com> Co-authored-by: Aman Gupta <amangupta052@gmail.com> Co-authored-by: fairydreaming <166155368+fairydreaming@users.noreply.github.com> Co-authored-by: Stanisław Szymczyk <sszymczy@gmail.com> Co-authored-by: AesSedai <7980540+AesSedai@users.noreply.github.com> Co-authored-by: QuintinShaw <github@xyt.email> Co-authored-by: Max Krasnyansky <maxk@qti.qualcomm.com> Co-authored-by: Marco Colombo <mcolombo@qti.qualcomm.com> Co-authored-by: Alexander Lu <alexlu@qti.qualcomm.com> Co-authored-by: Ehsan Bateni <ebateni@qti.qualcomm.com> Co-authored-by: Ryan C <ryan5rdx@users.noreply.github.com> Co-authored-by: Alessandro de Oliveira Faria (A.K.A.CABELO) <cabelo@opensuse.org> Co-authored-by: Daya Adianto <addianto@users.noreply.github.com> Co-authored-by: Nils Gladitz <nilsgladitz@gmail.com> Co-authored-by: JamePeng <jame_peng@sina.com> Co-authored-by: LunalFresh <165352784+LunalFresh@users.noreply.github.com> Co-authored-by: itterative <190138728+itterative@users.noreply.github.com> Co-authored-by: Pascal <admin@serveurperso.com> Co-authored-by: Marshall <assistant@llama.cpp>
#11) Co-authored-by: Marshall <assistant@llama.cpp> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#13) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…UM_COLS > 4) (#15) Co-authored-by: Marshall <assistant@llama.cpp>
Co-authored-by: Marshall <assistant@llama.cpp>
Tune MMQ and MMVQ scheduling, add routed expert kernels, share Q8_1 activation quantization, and add measured four-column kernels for quant formats that outperform the generic path on gfx1151. Assisted-by: OpenCode
Add gfx1151 paths for paired matmuls, SwiGLU quantization, expert aggregation, contiguous binary operations, transposed concat, normalization, and conversion operations. Assisted-by: OpenCode
Dequantize Q8 KV data directly in tiles, specialize GQA decode grouping, reduce D256 register pressure, and reuse masks across grouped heads on gfx1151. Assisted-by: OpenCode
Increase gfx1151 column parallelism, tile KDA inputs in LDS, preserve recurrent graph ordering, and schedule compatible Qwen projections together. Assisted-by: OpenCode
Use full MTP rollback checkpoints and retain speculative sequence state on the device to avoid unnecessary host transfers. Assisted-by: OpenCode
init_tensor zeroed the padding of quantized tensors with a synchronous
cudaMemset. On HIP, a synchronous memset or memcpy fails with
hipErrorStreamCaptureImplicit while any stream in the process is
capturing a graph, in every capture mode including relaxed, and it
invalidates that capture too (CHECK_STREAM_CAPTURING in clr). CUDA only
rejects this for blocking streams. test-backend-ops -j <n> allocates
tensors on one worker while another captures, so with GGML_HIP_GRAPHS
every -j run aborted after a random number of cases ("operation would
make the legacy stream depend on a capturing blocking stream" from
init_tensor, then "operation failed due to a previous error during
capture" from hipStreamEndCapture). Upstream master has the same memset
and aborts the same way.
Use cudaMemsetAsync on cudaStreamPerThread plus a sync of that stream,
as memset_tensor and set_tensor already do. Thread-local capture mode
would not help: HIP applies the check in every mode.
Checked on gfx1151 (ROCm 7.15 runtime), test-backend-ops -b ROCm0 on
halo-box master 0fc7226: without the fix -j 8 aborted in 2/2 runs,
and passed 29800/29800 with GGML_CUDA_DISABLE_GRAPHS=1; with the fix
-j 8 passed 29800/29800 in 5/5 runs with graphs on (about 10.5k captures
per run), and serial passed 29800/29800. One other serial run failed
only ADD_ADD(type=f16,type_addend=f16,ne=[64,5,4,3]) on NMSE 1.03e-7 >
1e-7; that case fails 5/100 runs without the fix and 3/100 on upstream
master too.
Assisted-by: Claude Opus 5.5
cuda: enable the BF16 WMMA matmul path only for qwen4exp
fix: guard the qwen4exp PLE prefetch on the model architecture (SIGSEGV at ubatch >= 4096)
The non-hipCUB fallback for rows wider than 1024 used an atomic gather for scores equal to the k-th largest. QSA scores often tie at exactly zero, so different schedules selected different blocks and changed long-context results. Use the existing stable-tie gather and cover the 1025/2047/2048/2049-column boundary with exact selected-index checks. Assisted-by: OpenCode (gpt-6-sol)
When the allocator reuses dead logits storage for routing outputs, decline the fusion only if non-logit inputs overlap. Copy contiguous logits to the device pool before the fused kernel when needed; otherwise unrelated buffer placement chooses between fused and unfused routing and changes the model output. Existing TOPK_MOE_LIFETIME scheduler-allocation tests cover the production 512-expert shape and alias layouts. Assisted-by: OpenCode (gpt-6-sol)
The detached prefetch thread captured the model and hparams by reference; a context can release them before prefetch finishes. Hold a shared pointer to the disk table and copy the small immutable hash arrays into the thread. This addresses the detached-thread lifetime hazard without changing row selection or prefetch I/O. The reported dense-model crash in issue #79 occurs before thread launch and is not claimed fixed here. Assisted-by: OpenCode (gpt-6-sol)
With --spec-draft-mtp-vocab N, the MTP context copies the LM head rows of token ids < N plus control and user-defined tokens into a smaller head when it is created. The qwen35 and qwen35moe MTP graphs compute the draft logits over that head and scatter them into a full-vocabulary row of -inf (ggml_fill + ggml_set_rows). Only the draft changes: the draft cannot propose other tokens, verification is unchanged. The subset is built only when token ids follow BPE merge order, so that low ids are the frequent tokens; otherwise the option is ignored with an error. Probe contexts with unallocated weights skip the build. New field llama_context_params::mtp_draft_vocab (0 = off). Assisted-by: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MkuKwgPNgpAg7EbxKR2iWC
The subset was stored in mutable llama_model state by the first MTP context that asked for one, and the qwen35 and qwen35moe MTP graphs used it whenever it existed. A second context over the same model with mtp_draft_vocab = 0, or a different N, silently drafted over the first context's subset. The subset is now an immutable llama_mtp_draft_vocab obtained with llama_model::mtp_draft_vocab_get(n_keep). The model keeps weak references keyed by (n_keep, buffer type) under a mutex, so contexts that ask for the same N at the same time share one copy, and it is freed with the last context holding it. Each context holds its own reference and passes it to its graphs through llm_graph_params; the MTP graphs use only that one, and graph reuse compares it. llama_cparams::mtp_draft_vocab records the N actually in use (0 when no subset was built: probe contexts with unallocated weights, a tokenizer that fails the merge-order guard, or a model whose MTP block has its own LM head, which now logs a warning instead of building an unused subset). Assisted-by: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MkuKwgPNgpAg7EbxKR2iWC
Contexts over one model with different mtp_draft_vocab settings (N then 0, 0 then N, N1 then N2, the same N twice, two contexts created from two threads at once, a subset requested again after its last context was freed) are decoded interleaved with the same inputs. A context with N > 0 must have -inf exactly outside its own draft vocabulary and match a full-vocabulary context inside it; a context with N = 0 must have finite logits everywhere. Needs a qwen35 or qwen35moe model with an MTP head (-m), so it is built but not registered with ctest. Assisted-by: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01MkuKwgPNgpAg7EbxKR2iWC
err(a, b) receives the backend under test as a and the CPU reference as b. The stable-tie check read b, so it validated the CPU's selection and passed regardless of what ROCm returned. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014qCLEUsSbxtwb71y5qRXAi
spec : MTP draft vocabulary subset --spec-draft-mtp-vocab (split 3/4 of #58)
qwen4exp: keep detached PLE prefetch inputs alive
hip: keep topk-moe fusion stable across buffer aliasing
hip: make tied TOP_K block selection deterministic
The HC residual has an extra stream dimension; using ggml_nrows to decide when its BF16-only store is safe applies the mark to small microbatches and makes the output depend on scheduler split placement. Check the actual token dimension for batches of at most 512, then allow incremental indexer-key updates through 512 tokens. QSA_FAST_MAX=127 retains the full re-pool comparison path. With the MoE alias fix as the base, 256- and 512-token runs at 4096 context produced identical probability files with both paths. Assisted-by: OpenCode (gpt-6-sol)
A residual whose next fused combine lands in a later split used to lose its BF16-only mark: ggml_cuda_marks_readers_local only sees one split, so precision depended on where the input-count cuts (GGML_SCHED_MAX_SPLIT_INPUTS) fell. The QSA fast path adds three host inputs per layer and moves those cuts, which is what made 128..512-token incremental keys disagree with a full re-pool. Optimize still runs per split, but marks live for the whole graph and every split is optimized before any is computed, so carry the remaining reader count forward (mmb_res16_pending) and mark from the split that completes the check. Restores the res16 store at 512-token ubatches that 063d804 had disabled and removes the now-redundant ne[2] test. Add test-qsa-fast: CPU-only regression for the lifted qsa_fast gate (size boundaries 1..2048, incremental block selection, prefix/commit/ truncate gates, QSA_FAST_MAX parsing). Validated on gfx1151 (ROCm 10): fast vs full re-pool logits byte-identical at ub 64/128/256/512/1024/4096, with and without split cuts; ctest -R ^test-qsa passes; test-backend-ops MUL_MAT/GET_ROWS passes. Assisted-by: OpenCode (kimi-k3)
qwen4exp: stabilize 128–512-token incremental QSA keys
--spec-draft-mtp-vocab was accepted for qwen35/qwen35moe only, so on a qwen4exp MTP sidecar the flag was silently ignored and every drafted token was scored against the full 248k-row Q6_K LM head. Allow LLM_ARCH_QWEN4EXP in mtp_draft_vocab_get and add the row-subset head branch to graph_mtp, mirroring qwen35moe: score build_lora_mm on the runtime subset (rows copied from the existing head at load - no file or weight change) and scatter its logits into a full-vocab -inf row, so token ids and the verification path stay unchanged. Measured on Qwen3.8-Flash-Next UD-IQ4_XS + mtp-...-Q4_K_M sidecar, 128 greedy tokens, same-session A/B: 38.0 -> 41.5 t/s at N=65536 (+8%), identical output and acceptance. N=131072 is slower than the control; N=32768/16384 are about equal with a small acceptance loss. With the subset the draft phase is 340 ms of ~3.4 s and the target verify is the remainder.
The target hybrid memory splits a verify batch into ubatches with an equal number of tokens per sequence, so unequal drafts (e.g. 2,2,1) become extra whole-model passes. The MTP drafter now pads an unconfident sequence and cuts all drafts to the longest confident run; the server cuts every draft to the shortest when a slot drafted nothing (unless one replays accepted tokens). STRIX_SPEC_EVEN_DRAFTS=0 restores the previous behaviour. Single sequence is unchanged (even only applies to >1 drafting sequence). Measured, 4 sequences adaptive drafting, 192 tokens each, same build A/B: even off: 31.39 31.51 31.17 tok/s summed even on : 34.18 36.36 38.24 tok/s summed
A verify step of several conversations carries 5-16 tokens, one more than the vector kernel takes for some types. The F32 MoE router [2560x512] at 9-32 columns now runs MMVF over column chunks (STRIX_F32_VEC_CHUNK_MAX, default 32) instead of the GEMM path; quantized routed experts at 5-16 tokens run MMVQ over chunks (STRIX_MOE_VEC_CHUNK, default 4; STRIX_MOE_VEC_CHUNK_MAX_T, default 16) instead of the tiled MMQ kernel. Batches of 1-4 tokens and >32 columns are unchanged. test-backend-ops MUL_MAT,MUL_MAT_ID: OK (ROCm0, 14576/14577, one flaky q4_1 case which also fails intermittently on the control build). Measured, 3 sequences fixed drafting, 128 tokens each, same build A/B: before: 37.3 / 35.4 tok/s summed (patch 1 only) after : 43.1 / 42.2 tok/s summed Single sequence (4-token verify) byte-exact unchanged.
Quantized weights of at most STRIX_Q_VEC_CHUNK_ROWS (1024) output rows at 9-32 columns (the HC down projection, attention k/v, shared expert) now run MMVQ over 8-column chunks instead of a handful of MMQ tiles (STRIX_Q_VEC_CHUNK_MAX=0 disables). Taller weights stay on MMQ. The GDN conv input concat takes the tiled transpose from 2 tokens on RDNA3.5 F32 (STRIX_CONCAT_T_MIN, default 2) instead of the non-contiguous kernel. The patch's BF16 vec-chunk hunk does not map: our tree has no combined vec_chunks block, MMB is disabled below T=512 and BF16 small batches use MMF. Measured, 3 sequences fixed drafting, 384 tokens each, same build A/B: off: 46.6 / 50.3 tok/s summed on : 48.9 / 50.5 tok/s summed Single sequence byte-exact unchanged.
dzannotti
left a comment
There was a problem hiding this comment.
Tested on Strix Halo (gfx1151, HIP), base ec01a7dfc vs this PR. Our Flash-Next GGUF has no MTP head, so the speculative part was measured on Qwen3.6-35B-A3B (MTP).
3-slot MTP serving (-np 3 --spec-type draft-mtp --spec-draft-n-max 3, llama-benchy pp512/tg256, concurrency 3, ABBA):
| base | PR | Δ | |
|---|---|---|---|
| decode throughput | 109.2 t/s | 125.6 t/s | +15.0% |
| prefill throughput | 1174 t/s | 1134 t/s | −3.4% |
| TTFT | 1317 ms | 1386 ms | +5.3% |
Non-speculative batches (no draft model; -r 8 ABBA, 16 samples per arm, slowest quarter dropped as warm-up):
| tokens | Qwen3-1.7B Q8_0 (dense) | gpt-oss-20b MXFP4 (MoE) |
|---|---|---|
| 4 | – | 167.8 → 167.1 (−0.4%) |
| 8 | – | 228.7 → 225.1 (−1.6%) |
| 9 | 759.0 → 749.4 (−1.3%) | – |
| 12 | – | 360.5 → 294.1 (−18.4%) |
| 16 | 1334.6 → 1287.6 (−3.5%) | 452.6 → 327.2 (−27.7%) |
| 24 | 1458.6 → 1432.5 (−1.8%) | – |
| 32 | 1910.6 → 1818.3 (−4.8%) | – |
Regressions:
- Chunked dispatch, not gated: the chunked MMVQ / F32 vector / MoE vector dispatch in
ggml-cuda.cuapplies to every CUDA/HIP device (only the concat change checks RDNA3.5). It slows models it was not tuned for, including a 28% loss on gpt-oss at 16 tokens, a common multi-slot and verify width. - Even drafts: default-on in both
common/speculative.cppand the server, for every spec type and architecture.
Please gate these to the measured target, and make even drafts opt-in.
Minor: 7 new STRIX_* env vars are parsed with unvalidated atoi, and concat.cu calls getenv on every concat.
Comment generated via automated review of clanker Claude Code.
|
Nice one. One catch we hit: with the shared Flash-Next MTP sidecar (Unsloth's The fix is small: take the subset rows from the target's head when the draft has none, and use the subset whenever the MTP block scores with the model head. With it, decode goes from 32.5 to 34.3 t/s on a Strix Halo (Vulkan), with the same acceptance. It also needs a one-line rebase after #111, which renamed the batch size accessor in if (batch.size() == 0 || (even && n_confident == 0)) {I have both on a branch (your four commits rebased on master, plus the fix as a fifth). Happy to push it here if that's easiest, or I can leave suggestions instead. Assisted-by: Claude Opus 5.5 |
Summary
Speculative-decoding (MTP) improvements for
qwen4exp(Qwen3.8-Flash-Next UD-IQ4_XS) on gfx1151. Two kinds of change: the--spec-draft-mtp-vocabdraft-head row subset (previously accepted only forqwen35/qwen35moe), and three fixes that make MTP actually pay off at several sequences and on small verify steps.Measured on the same machine, server interleaved A/B, medians.
Changes
qwen4exp(src/llama-model.cpp,src/models/qwen4exp.cpp).--spec-draft-mtp-vocab <n>scores drafted tokens against only the firstnrows of the target LM head (a load-time row copy, no GGUF/weight change) and scatters them into a full-vocab-infrow. On a qwen4exp MTP sidecar the flag used to be silently ignored, leaving the draft on the full 248k-row Q6_K head. N=65536 is the sweet spot.STRIX_SPEC_EVEN_DRAFTS, default on;common/speculative.cpp,tools/server/server-context.cpp). The target's hybrid memory splits a batch into ubatches that need the same token count per sequence, so drafts of different lengths turned one verify into extra whole-model passes, each a fresh graph. All drafting sequences now pad to the longest confident run. Multi-sequence aggregate: 31.4 -> 36.3 t/s (adaptive).STRIX_F32_VEC_CHUNK_MAX=32,STRIX_MOE_VEC_CHUNK=4/_MAX_T=16;ggml/src/ggml-cuda/ggml-cuda.cu). A 5-16 token verify step's F32 router[2560x512]and its routed experts (past the vector limits: IQ3_S 4, IQ4_NL 6) fell to tiny GEMM/MMQ tiles; they now run the vector kernel over column chunks. Multi-sequence-np 3: 37.0 -> 42.7 t/s.STRIX_Q_VEC_CHUNK_MAX/ROWS,STRIX_CONCAT_T_MIN;ggml-cuda.cu,concat.cu). Quant weights of <=1024 output rows (hc_down [10240->320], attn k/v, shared-expert gate/up) chunked through the vector kernel, and the GDN conv concat uses the tiled transpose from 2 tokens. Multi-sequence np=3: +2.6 %.Results
-np 3)--spec-draft-mtp-vocab 65536Correctness
test-backend-ops test -b ROCm0 -o MUL_MAT,MUL_MAT_ID: 14577/14577.Not included
The adaptive/cost-aware draft path (#102) is intentionally not part of this change.
Note
This branch was split out cleanly: the non-speculative decode kernels (Q8_0 deep-prefetch, HIP graph keys, IQ3_S vec-dot) live on the decode PR #106, not here.