Skip to content

qwen4exp: support draft-head-only GGUFs (unsloth layout) + fix draft-load regression - #28097

Draft
TheArchitectit wants to merge 4 commits into
ggml-org:masterfrom
TheArchitectit:qwen4exp-draft-head-fix
Draft

qwen4exp: support draft-head-only GGUFs (unsloth layout) + fix draft-load regression#28097
TheArchitectit wants to merge 4 commits into
ggml-org:masterfrom
TheArchitectit:qwen4exp-draft-head-fix

Conversation

@TheArchitectit

Copy link
Copy Markdown

qwen4exp: support draft-head-only GGUFs (unsloth layout) + fix draft-load regression

Companion patch to #27836 (qwen4exp architecture + NextN/MTP speculative decoding).

Two fixes that were necessary to run --spec-type draft-mtp with the unsloth-shipped Qwen3.8-Flash-Next GGUFs on CPU:

Fix 1: draft-head-only GGUFs

PR #27836's qwen4exp loader requires hc_head_norm/hc_head_down/hc_head_up and the PLE block unconditionally. The unsloth GGUFs ship draft-head-only files that lack these tensors — the target carries output_hc_* in its shards; the draft pack has only nextn.shared_head_norm + the block's own ffn projections.

This patch adds mtp_only detection (n_layer_nextn > 0 && blk.0.hc_attn_norm absent):

  • trunk hc_head_* tensors marked TENSOR_NOT_REQUIRED when mtp_only
  • PLE block guarded behind !mtp_only
  • trunk tensor loop skipped when mtp_only (layers 0..n_layer-1 absent)
  • MTP graph falls back to shared_head_norm + hc_ffn_down/hc_ffn_up when the head's own mixer is absent (mirrors the fallback path in the reference implementation)
  • nextn.hc_head_* marked TENSOR_NOT_REQUIRED; new nextn.shared_head_norm tensor loaded when present

Fix 2: draft-load regression

common/speculative.cpp:2585 loaded params.model.path (the target model path) instead of model_path (the draft model path) — the variable holding the user-specified -md path. This caused the 147 GB target model to be loaded again as the draft, consuming all RAM and failing. One-line fix.

Testing

Tested on a 48-core Haswell Xeon (E5-2660 v3) with Qwen3.8-Flash-Next (UD-Q4_K_XL target, 4-shard GGUF, ~147 GB mlock) and the native MTP head quantized to Q8_0 as the draft model.

# upstream binary + PR #27836 + this patch
--spec-type draft-mtp --spec-draft-n-max 4 -md mtp-qwen4exp-Q8_0draft.gguf

# 8-prompt diverse suite (ab_8prompt.py)
ON  (draft-mtp Q8_0): mean 4.28 t/s, acceptance 0.51–0.75, mean_len 3.0–4.0
OFF (stock twin):     mean 3.33 t/s
Δ: +29% mean, 2.0× peak (best prompt 6.62 t/s vs 3.33)

Acceptance on the upstream per-stream combiner is dramatically higher than the mean-pooled alternative (0.51–0.75 vs 0.18–0.20, same head/flags), confirming PR #27836's note that the combiner must run per hc stream.

Drafter quant ladder (same upstream binary, dn=4)

All rungs requantized from the native BF16 MTP head pack (bit-identical lineage), arm-tested with journal-verified loads:

Quant Size Suite t/s Mean acc_len vs stock
Q8_0 3.9 GB 4.28 2.25 +29%
Q4_K_M 2.8 GB 3.97 2.34 +19%
Q5_0 2.8 GB 3.95 2.40 +19%
Q3_K_M 2.2 GB 3.44 2.15 +3%
Q2_K 1.8 GB 3.44 2.07 +3%

Acceptance binds only 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 Suite t/s Mean acc_len vs stock
2 3.72 1.88 +12%
4 4.28 2.25 +29%
6 3.52 2.47 +6%
8 3.38 2.60 +2%

dn=4 optimal; higher dn costs more verify than acceptance recovers.

Files changed

  • src/models/qwen4exp.cpp — mtp_only detection, optional tensors, graph fallback (+25 lines)
  • common/speculative.cpp — draft-load path fix (1 line)

ryanmonsurate and others added 4 commits August 30, 2026 21:19
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>
@TheArchitectit
TheArchitectit requested review from a team and CISC as code owners August 31, 2026 15:14
@github-actions github-actions Bot added model Model specific conversion labels Aug 31, 2026
@ggml-gh-bot

ggml-gh-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

Hi @TheArchitectit, thanks for your contribution!

Per our contribution guidelines, the automated PR checker found the following issue(s) that need your attention:

  • PR Template not respected: Please respect the template when creating a new pull request. Make sure to fill out all required sections.

  • AI-generated content: While code is allowed to be generated by AI, please write the PR description and commit messages on your own without the help of AI.


Please note that maintainers reserve the right to make final decisions on PRs. If you believe there is a mistake, please comment below.

@ggml-gh-bot ggml-gh-bot Bot added the draft PR will be changed to draft by github-actions bot label Aug 31, 2026
@github-actions
github-actions Bot marked this pull request as draft August 31, 2026 15:19
@github-actions github-actions Bot removed the draft PR will be changed to draft by github-actions bot label Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conversion model Model specific

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants