Skip to content

feat(gguf): load mtp drafts for the qwen3.5 family from nextn blocks and mtp-*.gguf sidecars - #856

Merged
jason-fxz merged 1 commit into
FlashML-org:mainfrom
void-mckenzie:feat/mtp-gguf-qwen35
Nov 4, 2026
Merged

jason-fxz merged 1 commit into
FlashML-org:mainfrom
void-mckenzie:feat/mtp-gguf-qwen35

Conversation

@void-mckenzie

@void-mckenzie void-mckenzie commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

feat(gguf): load mtp drafts for the qwen3.5 family from nextn blocks and mtp-*.gguf sidecars

The GGUF side branch of #828. #844 gave the Qwen3.5 family MTP from safetensors; GGUF targets were the only ones still refused. This adds only weight loading, the verify path is #844's unchanged.

  • The draft reads the trunk's own nextn block, the block llama.cpp keeps after the last decoder layer (block_count includes it, {arch}.nextn_predict_layers says how many). Exactly one nextn layer is accepted; a --no-mtp conversion or a Strata-style MTP pack is refused with a message that names both.
  • The block's tensors land under the draft's state-dict names: nextn.* as fc and norms, the block itself as layers.0, q output-gate rows packed in attn_q, gate|up fused. The draft's quant comes from the file's own tensor table, so each ggml mix loads as stored; the MoE draft's routed experts load as its one resident bank set.
  • --speculative-draft-model-path also takes an mtp-*.gguf sidecar (unsloth's MTP/ releases). Those files keep the trunk's full block_count KVs so blk.N addressing matches; a sidecar is accepted only when it holds exactly the one nextn block, the target's own architecture and hidden size, otherwise the refusal says what the draft of a GGUF target is. A --no-mtp trunk plus a sidecar works: the trunk config's shapes, the sidecar's weights.
  • docs/cli.md speculative section updated to match.

Verified

  • pytest tests/models/test_qwen3_5_moe_weight.py tests/models/test_models_registry.py tests/spec/: 67 passed, 104 skipped (skips are pre-existing hardware/checkpoint gates). The new tests cover the block-to-state-dict mapping, quantized packed leaves, resident expert banks, the no-MTP refusal message, and the sidecar draft loading under its own quant. There is no numeric parity test against the HF mtp.* draft of the same checkpoint yet; the mapping is checked tensor by tensor against a synthetic file instead.
  • Live serving, GPU 0 (RTX 5090), greedy:
    • Dense unsloth/Qwen3.8-27B-GGUF UD-Q5_K_XL, ft serve ... --speculative-algorithm mtp --num-tokens 24576: 191.3 t/s mean steady-state decode at accept len 3.88-4.00 (k=3, at the theoretical max); the same boot without spec runs 73.2 t/s, 2.61x. Both boots answer "What is 15% of 240?" with byte-identical content and reasoning fields ("36"), and a 2048-token long-form prompt is also byte-identical between them.
    • MoE Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf (trunk's own nextn): 442-486 t/s, accept len 3.65-3.85; the draft's nextn experts resident on GPU (0.5 GiB) beside the trunk's 40 offloaded layers.
    • Sidecar: the same 27B trunk with --speculative-draft-model-path .../MTP/mtp-Qwen3.8-27B-Q4_0.gguf: greedy 17*23+45/9 answers 396, accept len 2.0-3.5, 113 t/s steady state.

@github-actions github-actions Bot added the gguf label Nov 3, 2026

@jason-fxz jason-fxz left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested d1b3690 against its base 9cacd08 on real GGUF files; LGTM.

Environment: NVIDIA H100 80GB HBM3, driver 580.95.05, CUDA 13.0, Intel Xeon Platinum 8480+ (26 vCPU), torch 2.14.1+cu130, transformers 5.18.0.

Checkpoints:

  • unsloth/Qwen3.5-2B-MTP-GGUF BF16 and Q8_0 (trunk with its nextn block), unsloth/Qwen3.5-2B-GGUF Q8_0 (no nextn)
  • unsloth/Qwen3.8-27B-GGUF UD-Q4_K_XL and MTP/mtp-Qwen3.8-27B-Q4_0.gguf
  • unsloth/Qwen3.6-35B-A3B-MTP-GGUF UD-Q4_K_XL
  • bf16 sidecars converted from Qwen/Qwen3.5-2B and Qwen/Qwen3.6-35B-A3B with llama.cpp e937b17a2 convert_hf_to_gguf.py --mtp --outtype bf16
  • HF references: Qwen/Qwen3.5-2B, Qwen/Qwen3.6-35B-A3B (run with moe_strategy="offload", moe_cache_auto=True), Qwen3.8-27B, all bf16

Workload: AIME25 problem 1 through the offline LLM API with max_running_req=4, speculative_algorithm="mtp" (and speculative_draft_model_path for sidecars). Greedy runs end naturally (cap 16384) and are compared token by token; speed runs use temperature 1.0, top_p 0.95, top_k 20, natural end, 5 runs, median.

Greedy, token by token:

  • spec off, PR vs base: identical on 2B BF16, 2B Q8_0, 2B no-nextn Q8_0, 27B UD-Q4_K_XL and 35B UD-Q4_K_XL (fused).
  • HF Qwen3.5-2B with MTP, PR vs base: identical.
  • 35B fused vs offload with moe_cache_rate=0.25: identical with spec off and with MTP.
  • own nextn vs sidecar draft: identical output on 2B, 27B and 35B.
  • 2B no-nextn trunk with --spec-algo mtp and no sidecar: refused at startup with the nextn ValueError.

Sampled speed (tok/s, median of 5; tokens per step in brackets):

target draft spec off MTP HF bf16 MTP, same model
2B Q8_0 own nextn 431.7 818.7 (3.34) 753.0 (3.23)
2B no-nextn Q8_0 bf16 sidecar 785.3 (3.35)
27B UD-Q4_K_XL own nextn 86.7 161.0 (3.17) 123.9 (3.10)
27B UD-Q4_K_XL mtp-Q4_0 sidecar 163.4 (3.20)
35B UD-Q4_K_XL, fused own nextn 208.7 428.6 (3.22) 360.6 (3.25)
35B UD-Q4_K_XL, fused bf16 sidecar 427.8 (3.23)
35B UD-Q4_K_XL, offload 25% own nextn 123.8 (3.22)
35B UD-Q4_K_XL, hybrid 25% (1024 tokens, 3 runs) own nextn 127.5 190.9 (3.44)

ft serve with the 27B trunk, --spec-algo mtp, with and without --spec-draft-model .../MTP/mtp-Qwen3.8-27B-Q4_0.gguf: both answer a chat request correctly, and only the sidecar run maps the sidecar file.

ft serve --model-path Qwen3.8-27B-UD-Q4_K_XL.gguf --spec-algo mtp
ft serve --model-path Qwen3.8-27B-UD-Q4_K_XL.gguf --spec-algo mtp --spec-draft-model MTP/mtp-Qwen3.8-27B-Q4_0.gguf

Not covered: a --no-mtp MoE trunk with a sidecar, prism or NVFP4 sidecars, ROCm.

@jason-fxz
jason-fxz merged commit 9ee2e5c into FlashML-org:main Nov 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants