Skip to content

[Bugfix][Qwen3-Omni] Pass Thinker lm_head prefix so NVFP4 exclude-list is honored - #4528

Merged
hsliuustc0106 merged 1 commit into
vllm-project:mainfrom
YIHONG-JIN:fix/qwen3-omni-lmhead-nvfp4-prefix
Jun 22, 2026
Merged

hsliuustc0106 merged 1 commit into
vllm-project:mainfrom
YIHONG-JIN:fix/qwen3-omni-lmhead-nvfp4-prefix

Conversation

@YIHONG-JIN

@YIHONG-JIN YIHONG-JIN commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

What

Follow-up to #4025 (which landed the NVFP4 W4A4 NaN-clamp). Adds the
prefix=maybe_prefix(prefix, "lm_head") argument that the custom
Qwen3MoeLLMForCausalLM (the Thinker text backbone) was missing when building
lm_head, mirroring vLLM's stock Qwen3MoeForCausalLM.

Why

Without it, loading an NVFP4 W4A4 Qwen3-Omni checkpoint on vLLM 0.23.0 crashes at
load (in vocab_parallel_embedding.py, loading lm_head.weight):

RuntimeError: The size of tensor a (1024) must match the size of tensor b (2048) at non-singleton dimension 1

Root cause: on 0.23.0 the Thinker lm_head is kept BF16 only via ModelOpt's
prefix-based exclude-list (the W4A4 checkpoint ignores *.lm_head); with
prefix="" that match fails, the head is wrongly NVFP4-quantized (FP4-packed →
hidden 1024), and the BF16 [vocab, 2048] lm_head weight then fails to copy into
the 1024-wide param.

This only manifests on vLLM ≥ 0.23.0 — vLLM 0.22.x is unaffected. On 0.22,
ModelOpt.get_quant_method only matched LinearBase, so ParallelLMHead was
never a quantization candidate and the missing prefix had no effect — the
W4A4 checkpoint loaded fine (this is the stack the original W4A4 accuracy /
throughput validation ran on). 0.23.0 added ParallelLMHead to the
quantizable set, which is what makes the head's BF16 status depend on the
prefix. So this is a 0.23.0-migration fix, not a regression introduced here —
the omission has been latent since the original Qwen3-Omni port (#55).

BF16 Qwen3-Omni is also unaffected on any version (no quant_config, so the
head is never a quant candidate regardless of the prefix).

Reproduce

vLLM 0.23.0 + vllm-omni on Blackwell (B200 sm_100 / RTX 5090 sm_120), serving the
published W4A4 checkpoint:

vllm serve YihongJin/Qwen3-Omni-30B-A3B-Instruct-NVFP4-W4A4-full-thinker-awqclip --omni

The Thinker worker dies at weight load (StageEngineCoreProc died during READY,
worker traceback qwen3_omni_moe_thinker.py:load_weights -> ... -> vocab_parallel_embedding.py) with the 1024 vs 2048 RuntimeError above. A debug
print at Qwen3MoeLLMForCausalLM.__init__ shows lm_head built as
[152064, 1024] (FP4-packed) instead of [152064, 2048]. With this PR it is
[152064, 2048] and the model loads.

To then serve end-to-end (independent of this PR) also set
VLLM_USE_FLASHINFER_MOE_FP4=1 VLLM_FLASHINFER_MOE_BACKEND=throughput to select
the Cutlass NvFp4 MoE backend — the default selects TRT-LLM, which separately
fails on this checkpoint's expert shapes.

Test

Verified on B200 (sm_100, FlashInfer Cutlass FP4) and RTX 5090 (sm_120): with the
fix the W4A4 Thinker loads (embed_tokens and lm_head both [152064, 2048])
and serves coherent text (no !!!!). Before the fix it crashes at load as above.

🤖 Generated with Claude Code

@YIHONG-JIN YIHONG-JIN changed the title [Bugfix][Qwen3-Omni] Keep Thinker lm_head unquantized under NVFP4 (pass prefix) [Bugfix][Qwen3-Omni] Pass Thinker lm_head prefix so NVFP4 exclude-list is honored Jun 18, 2026
…t is honored

The custom Qwen3MoeLLMForCausalLM built lm_head via ParallelLMHead without the
`prefix=maybe_prefix(prefix, "lm_head")` that vLLM's stock Qwen3MoeForCausalLM
passes. vLLM 0.23.0 made ParallelLMHead quantizable by ModelOpt NVFP4 (0.22's
get_quant_method only matched LinearBase, so the head was never a quant
candidate and the missing prefix was harmless). On 0.23.0 the head is kept BF16
only via the prefix-based exclude-list (the W4A4 checkpoint ignores "*.lm_head");
with prefix="" that match fails, the head is wrongly NVFP4-quantized
(FP4-packed -> hidden 1024), and the BF16 [vocab, 2048] lm_head weight fails to
load (RuntimeError: size a(1024) must match b(2048)).

Required for the W4A4 Qwen3-Omni checkpoint to load on the rebased vLLM 0.23.0
target. Latent since the original Qwen3-Omni port (vllm-project#55); surfaced by the 0.23.0
rebase. BF16 Qwen3-Omni is unaffected (no quant_config). Verified on B200
(sm_100) and RTX 5090 (sm_120): the W4A4 thinker loads and serves coherent text.

Signed-off-by: Yihong Jin <yihongjin404@gmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@YIHONG-JIN
YIHONG-JIN force-pushed the fix/qwen3-omni-lmhead-nvfp4-prefix branch from c4c9ea1 to 26b19e9 Compare June 18, 2026 07:25
@amy-why-3459

Copy link
Copy Markdown
Collaborator

Thanks, LGTM.

@hsliuustc0106 hsliuustc0106 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.

Bugfix looks correct.

@YIHONG-JIN

Copy link
Copy Markdown
Contributor Author

@hsliuustc0106 Can you help approve this if everything looks fine? I suspect the impact is not limited to NVFP4

@david6666666

Copy link
Copy Markdown
Collaborator

LGTM

@david6666666 david6666666 added the ready label to trigger buildkite CI label Jun 22, 2026
@david6666666
david6666666 enabled auto-merge (squash) June 22, 2026 02:40
@hsliuustc0106
hsliuustc0106 disabled auto-merge June 22, 2026 04:16
@hsliuustc0106
hsliuustc0106 merged commit a602420 into vllm-project:main Jun 22, 2026
6 of 8 checks passed
khairulkabir1661 pushed a commit to khairulkabir1661/vllm-omni that referenced this pull request Sep 25, 2026
…t is honored (vllm-project#4528)

Signed-off-by: Yihong Jin <yihongjin404@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready label to trigger buildkite CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants