qwen4exp: support draft-head-only GGUFs (unsloth layout) + fix draft-load regression - #28097
qwen4exp: support draft-head-only GGUFs (unsloth layout) + fix draft-load regression#28097TheArchitectit wants to merge 4 commits into
Conversation
Adds the MTP head's own hyper-connection mixer tensor names and lists the NextN tensors under the qwen4exp architecture.
Adds --spec-type draft-mtp support for Qwen3.8-Flash-Next. The MTP head folds the next token's embedding into the trunk's wide hyper-connection residual, runs one trunk-style block (dense attention + MoE) over it, and collapses the result with its own mixer before reusing the trunk's LM head. - read nextn_predict_layers so n_layer() excludes the MTP block - load the trailing block through the existing trunk path: is_recr() and is_ple() are already false past the trunk, so it needs no special casing - eh_proj fuses the checkpoint's fc_embedding and fc_hidden side by side, so one matmul computes fc_embedding@e + fc_hidden@h - the head carries its own hyper-connection mixer, mirroring the trunk's hc_head_*, which stands in for the output norm qwen4exp does not have - export the wide pre-collapse residual as t_h_nextn from both graphs, so the driver can feed it back for the next draft step - route MTP contexts to a plain KV cache filtered to the trailing layer The draft block attends densely for now: the trunk's QSA only prunes context past a 2048-token budget, so dense is a numerical superset and drafts are verified either way. Indexer tensors are still loaded.
The MTP block is one trunk-shaped block (dense attention + MoE wrapped in hyper-connections) plus a head-level combiner, so once _QwenMtpMixin renames mtp.layers.0.* to the trailing block index its tensors ride the existing qwen4exp mappings unchanged. Two head-level pieces need handling: - fc_embedding and fc_hidden fuse into the eh_proj the shared NextN code expects, since W_e@e + W_h@h == [W_e|W_h] @ concat(e, h) - mtp.hyper_connection_mixer.* is the head's own copy of the trunk's hc_head_* output mixer, unindexed in the checkpoint and per-block in the GGUF compress_ratios is read with length block_count, so it gains a trailing 0 for the MTP block, which attends densely. --no-nextn drops the head; --mtp exports it on its own.
Draft-head files carry no trunk tensors and no hc_head mixer; they use nextn.shared_head_norm plus the block's own ffn projections instead. Detect such files (n_layer_nextn > 0 and no blk.0 hc_attn_norm), make the trunk hc_head tensors optional for them, and fall back in the MTP graph. Also fix the draft load using the target path instead of the draft path. Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
|
Hi @TheArchitectit, thanks for your contribution! Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:
Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below. |
|
Status/plan note: per the automated review on #28389 (new contributors are limited to 1 open PR at a time), I am keeping this PR in draft until #28389 (cuda: fix CUB argsort corruption) is reviewed/approved. It is independent of #28389 code-wise; this one is a companion to #27836 and depends on it landing regardless. I will also update the description to follow the PR template. No action needed from maintainers on this PR in the meantime. |
|
/bot review |
|
❌ Code review failed. |
Overview
This patch lets
--spec-type draft-mtpwork with the draft-head-only GGUFsthat unsloth ships for Qwen3.8-Flash-Next, and fixes a one-line bug where the
draft loader used the target model's path instead of the
-mdpath. It is acompanion to #27836 (qwen4exp architecture + NextN/MTP speculative decoding)
and depends on that PR landing.
Both problems showed up while trying to run speculative decoding on a
147 GB MoE model on a pure-CPU box: the shipped draft pack does not fit
what #27836's loader demands, and the draft-load bug made it impossible to
use a separate draft file at all.
Fix 1: draft-head-only GGUFs
#27836's qwen4exp loader requires
hc_head_norm/hc_head_down/hc_head_upand the PLE block unconditionally. The unsloth GGUFs ship draft-head-only
files that lack these tensors — the target carries
output_hc_*in itsshards; the draft pack has only
nextn.shared_head_norm+ the block's ownffn projections.
The patch adds an
mtp_onlyprobe (n_layer_nextn > 0 && blk.0.hc_attn_norm absent) and, when it fires:hc_head_*tensors markedTENSOR_NOT_REQUIREDwhenmtp_only!mtp_onlymtp_only(layers 0..n_layer-1 absent)shared_head_norm+hc_ffn_down/hc_ffn_upwhen the head's own mixer is absent (mirrors the fallback path in the
reference implementation)
nextn.hc_head_*markedTENSOR_NOT_REQUIRED; newnextn.shared_head_normtensor loaded when present
Fix 2: draft-load regression
common/speculative.cpp:2585loadedparams.model.path(the targetmodel path) instead of
model_path(the draft model path) — the variableholding the user-specified
-mdpath. This caused the 147 GB target modelto be loaded again as the draft, consuming all RAM and failing. One-line fix.
Additional information
Keeping this PR in draft until #28389 (my other open PR, cuda: fix CUB argsort
corruption) is reviewed, since new contributors are limited to one open PR.
It is independent of #28389 code-wise and waits on #27836 regardless.
Testing
Measured on a 48-core Haswell Xeon (E5-2660 v3) running Qwen3.8-Flash-Next
(UD-Q4_K_XL target, 4-shard GGUF, ~147 GB mlock), with the native MTP head
requantized to Q8_0 as the draft model.
Comparing the per-stream combiner from #27836 against a mean-pooled variant
(same head, same flags): acceptance was 0.51–0.75 vs 0.18–0.20. That lines
up with the note in #27836 that the combiner has to run per hc stream.
Drafter quant ladder (same upstream binary, dn=4)
Each rung is a requantization of the native BF16 MTP head pack (bit-identical
lineage); I arm-tested every one with a journal-verified load.
Acceptance is only the limiting factor below ~5 bits; every rung beats stock
(3.33 t/s). Q8_0 is the practical pick (identical speed to F16 at half the
RAM; F16 ties at 4.29 t/s with highest acceptance 2.51 but costs 7.3 GB).
Draft-n-max sweep (Q8_0)
dn=4 is the sweet spot — past that, the extra verify work costs more than
the acceptance it buys back.
Files changed
This branch carries three commits from #27836, so the GitHub diff against
mastershows 10 files. The changes authored here are two:src/models/qwen4exp.cpp— mtp_only detection, optional tensors, graph fallback (+25 lines)common/speculative.cpp— draft-load path fix (1 line)The remaining files in the diff (
conversion/qwen4exp.py,gguf-py/gguf/constants.py,gguf-py/gguf/tensor_mapping.py,src/llama-arch.{h,cpp},src/llama-model.{h,cpp},src/models/models.h) come from #27836 and are shown for merge context only.Requirements