Skip to content

[Quantization][Qwen3-Omni] Enable NVFP4 W4A4 serving on Blackwell - #4025

Merged
david6666666 merged 1 commit into
vllm-project:mainfrom
YIHONG-JIN:feat/qwen3-omni-modelopt-nvfp4-w4a4
Jun 18, 2026
Merged

david6666666 merged 1 commit into
vllm-project:mainfrom
YIHONG-JIN:feat/qwen3-omni-modelopt-nvfp4-w4a4

Conversation

@YIHONG-JIN

@YIHONG-JIN YIHONG-JIN commented May 31, 2026 •

Copy link
Copy Markdown
Contributor

Tracks: #1854 (Continuous Quantization Support — Qwen3-Omni / ModelOpt NVFP4 W4A4 row).

Purpose

Add support for serving ModelOpt NVFP4 W4A4 quantized checkpoints of
Qwen3-Omni-30B-A3B-Instruct through vllm-omni.

The runtime change is a single defensive override in vllm_omni/patch.py
of ModelOptNvFp4LinearMethod.process_weights_after_loading. ModelOpt
0.44's float32 -> torch.float8_e4m3fn cast of per-block weight_scale
occasionally emits literal NaN bytes (E4M3 encoding 0x7F / 0xFF) when
the pre-cast scale rounds above the FP8 max of 448 after the global-scale
division. A single NaN byte in any weight_scale propagates through the
FlashInfer FP4 GEMM into the residual stream and collapses the served
model output to !!!!. The override scans layer.weight_scale for NaN
bytes after the original PWAL runs and clamps them to the FP8 E4M3 max in
place (via a uint8 view, since PyTorch does not implement masked_fill_
for float8_e4m3fn).

The override is defensive against checkpoints already in the wild.
Public NVFP4 W4A4 Qwen3-Omni checkpoints exported with vanilla
nvidia-modelopt==0.44.0 (e.g. the precursor preview checkpoint
YihongJin/...-W4A4-experts-oproj-mse-preview
and several community checkpoints) contain literal NaN bytes in
per-layer weight_scale tensors (e.g. that preview ckpt has 4 NaN
bytes spread across o_proj layers 4 and 9). Without this runtime
clamp, those checkpoints currently serve only !!!!. The clamp lets
vllm-omni serve them coherently while the underlying ModelOpt 0.44 bug
gets fixed upstream.

The override self-extinguishes once vllm-omni's vllm pin moves to a
release containing the upstream fix (detected by inspecting the upstream
PWAL's __code__.co_names for masked_fill_), and can be disabled at
load time with VLLM_OMNI_SKIP_NVFP4_NAN_CLAMP=1 for diagnostics.

Sibling upstream fixes against vllm-project/vllm (the same PWAL clamp
in vLLM's own ModelOptNvFp4LinearMethod) and NVIDIA/TensorRT-Model-Optimizer
(clamping the FP32 -> FP8 cast at the calibration-export source so future
checkpoints ship clean) are in progress and will be linked here once
filed.

How we identified the bug

The root cause and fix in this PR are the result of a checkpoint-by-checkpoint
bisection. We published the intermediate checkpoints to HF as we went so each
step is reproducible.

Step Checkpoint (HF) Quant scope ModelOpt build Result Conclusion
1 ...-W4A4-experts-mse MoE experts only Vanilla 0.44 Serves coherently on stock vllm-omni; Daily-Omni n=50 0.72 = BF16 FP4 GEMM works for MoE experts. Confirm the kernel path is sound at narrow scope.
2 ...-W4A4-experts-oproj-mse-preview Experts + o_proj Vanilla 0.44 Server emits only !!!! within the first decode step Adding o_proj to the FP4 surface breaks it. Initial hypothesis was a "small-alpha NaN" in the FlashInfer FP4 apply path.
3 ...-W4A4-full-thinker-mse-preview Full thinker Vanilla 0.44 Also !!!! Confirms the failure is not specific to o_proj — any wider scope triggers it.
4 (control experiment, no new ckpt) — — Byte-edited the 4 NaN bytes (E4M3 encoding 0x7F / 0xFF) out of *.weight_scale in step-2's safetensors, replaced with the FP8 E4M3 max byte (0x7E). Loaded the edited file on stock vllm-omni. Result: coherent text, no !!!!. The NaN was already on-disk in weight_scale. The FlashInfer FP4 apply path was not generating it — it was reading it. Initial "small-alpha NaN in apply" hypothesis was wrong; the real bug is upstream in calibration export.
5 (ModelOpt source patch) — — Inspected modelopt/torch/quantization/qtensor/nvfp4_tensor.py and modelopt/torch/export/quant_utils.py. Both files call .to(torch.float8_e4m3fn) on float32 per-block scales without first clamping to the FP8 max of 448. When the pre-cast value rounds above 448, PyTorch's FP32→FP8 cast emits NaN bytes. Added .clamp_(max=torch.finfo(torch.float8_e4m3fn).max) before each cast (6 sites total). Located the calibration-side root cause and produced a patched ModelOpt build.
6 ...-W4A4-experts-oproj-mse Experts + o_proj Patched 0.44 0 NaN bytes on disk; Daily-Omni n=50 0.72 = BF16 Calibration-side fix works. Confirms diagnosis.
7 ...-W4A4-full-thinker-awqclip Full thinker Patched 0.44 0 NaN bytes; Daily-Omni n=1197 0.675 vs BF16 0.694 on B200; +33% tok/s, -29% TPOT at conc=32 Production W4A4 checkpoint with the broadest tested quantization surface.

Why this fix sits in vllm-omni

Steps 1-4 establish that the bug is emitted at calibration time — the
exported safetensors already carry NaN bytes; the FlashInfer FP4 apply
path is just a faithful reader. The load-time clamp added here is
mathematically equivalent to clamping at calibration write-side
: by the
time the FP4 kernel reads weight_scale, every position that would have
been NaN is the FP8 E4M3 max byte (0x7E = 448.0), regardless of which
point in the pipeline did the clamping. The proper fix sits upstream in
two places: at the ModelOpt cast sites (step 5 demonstrates the
calibration-side fix works) and at the corresponding PWAL in vLLM itself.
Both are planned as follow-up PRs after this one merges so we can
keep one coordinated narrative across vllm-omni / vLLM / ModelOpt.

Until those upstream fixes land in vllm-omni's pinned vLLM, any NVFP4 W4A4
Qwen3-Omni checkpoint published by the community — exported with stock
ModelOpt 0.44 — will silently serve only !!!! on vllm-omni. Users
hitting that failure will reasonably file bugs against vllm-omni
, not
against ModelOpt, because the visible symptom is degenerate output from a
vllm-omni server. The load-time clamp added here keeps those community
checkpoints serving coherently in the interim.

Design choice (PWAL vs per-forward apply()): the clamp runs once at
worker init — O(num_layers) — and prevents NaN from entering the FP4 GEMM
at all. Newly calibrated, clean checkpoints (e.g. steps 6-7) pay no
runtime cost (no-op when no NaN bytes are present). The
_already_patched_upstream check inspects vLLM's own PWAL for a
masked_fill_ call — the structure the upstream fix will take — so this
override skips itself once that upstream fix lands in vllm-omni's vllm
pin, and the patch block can be deleted.

Test Plan

  • Unit test (CPU-only, core_model + cpu marks) at
    tests/model_executor/quantization/test_modelopt_nvfp4_nan_clamp.py
    covering the override's clamp behavior, no-op-on-finite-input behavior,
    install assertion, and env-var escape hatch.
  • E2E text-to-text smoke test (full_model + omni, H100 x2) at
    tests/e2e/offline_inference/test_qwen3_omni_modelopt_nvfp4_w4a4.py
    loading the published W4A4 checkpoint and asserting the response does
    not match the !!!! degenerate-output signature.
  • Manual end-to-end on NVIDIA B200 (HBM3e, sm_100) against
    YihongJin/Qwen3-Omni-30B-A3B-Instruct-NVFP4-W4A4-full-thinker-awqclip:
    • Daily-Omni n=1197 full accuracy run via the in-tree harness.
    • OmniBench n=200 via evalscope.
    • Throughput / latency sweep over concurrency=[1, 8, 32, 64, 128] x
      output_len=[128, 256, 512, 1024] with --dataset-name random.
    • 30-min stability stress at conc=32 to validate the NaN regression
      guard under sustained load.
vllm serve YihongJin/Qwen3-Omni-30B-A3B-Instruct-NVFP4-W4A4-full-thinker-awqclip \
    --omni --port 8000

Do not pass --enforce-eager for production / benchmarks. CUDA
graphs amortize launch overhead and unlock the FP4 throughput wins
below; with --enforce-eager set, W4A4 TPOT degrades 10x relative to
the CUDA-graph configuration.

Test Result

Unit tests: 8 passed, 0 skipped, 0 failed (4 base + 4 parametrized
env-var cases locking in the {1, true, yes, on} skip semantics and
{0, false} install-still-active semantics).

Accuracy — Daily-Omni n=1197 + OmniBench n=200 (B200, CUDA graphs)

Model Daily-Omni n=1197 OmniBench n=200
Qwen/Qwen3-Omni-30B-A3B-Instruct (BF16) 0.694 0.455
W4A4 NVFP4 (awq_clip, 1024 text-only calib) 0.675 (-1.9 pp) 0.450 (-0.5 pp)

W4A4 stays within 2 pp of BF16 on Daily-Omni and within 0.5 pp on
OmniBench.

Hardware coverage — load-clamp ≡ calibration-clamp, verified

To verify the equivalence claim above, the same NaN-containing reproducer
checkpoint (...-full-thinker-mse-preview)
was loaded with the load-time clamp active on two additional hardware
classes covering both NVFP4 kernel paths. On Blackwell the test exercises
the same FlashInfer Cutlass NvFp4 Linear class as the B200 production
numbers; on H100 it exercises the Marlin weight-only FP4 fallback.

Hardware sm NvFp4 kernel Ckpt Clamp Daily-Omni n=200
B200 (production target, ref above) sm_100 FlashInfer Cutlass awqclip (clean) no-op 0.675 (n=1197)
RTX PRO 6000 WS sm_120 FlashInfer Cutlass (same class as B200) mse-preview (NaN) clamped 4 bytes 0.670
H100 SXM sm_90 Marlin fallback (W4A16 GEMM, no native FP4) mse-preview (NaN) clamped 0.665

The 0.5 pp gap on the same kernel surface (Pro 6000 Cutlass 0.670 vs B200
Cutlass clean awq_clip 0.675) is attributable to the calibration algorithm
(mse vs awq_clip), not the clamp — consistent with the equivalence claim.

Checkpoint size

Model Total checkpoint Reduction vs BF16
BF16 ~66 GiB —
W4A4 NVFP4 (full thinker) 27.6 GiB ~58%

(Audio encoder, vision encoder, talker, code2wav, and lm_head stay in BF16.)

Throughput / latency — vLLM serve on B200, random workload

Synthetic random workload (--random-input-len 256, --random-output-len 512,
--num-prompts 200, no --enforce-eager). CUDA graphs captured for prefill
and decode batch sizes 1..256.

Concurrency Metric BF16 W4A4 NVFP4 W4A4 vs BF16
1 Output tok/s 156 150 -4%
1 TPOT (ms) 5.87 5.84 tied
1 TTFT (ms) 72 111 +54% (BF16 wins prefill)
8 Output tok/s 869 1002 +15%
8 TPOT (ms) 8.33 6.73 -19%
32 Output tok/s 2083 2766 +33%
32 TPOT (ms) 12.76 9.12 -29%
64 Output tok/s 3197 4112 +29%
64 TPOT (ms) 15.87 10.96 -31%
128 Output tok/s 4923 5665 +15%
128 TPOT (ms) 18.01 15.36 -15%

W4A4 wins on every metric at concurrency >= 8. The single-stream TTFT
loss reflects the small FP4 prefill dequant overhead that BF16 native
tensor cores don't pay; once concurrency saturates the GEMM, FP4 weight
bandwidth savings dominate and W4A4 pulls ahead by 15-33% in output
tok/s and 15-31% in TPOT across batch sizes.

Kernel selection (verified at server boot)

INFO ... Using FlashInferCutlassNvFp4LinearKernel for NVFP4 GEMM
INFO ... Using 'FLASHINFER_TRTLLM' NvFp4 MoE backend
INFO ... cudagraph_mode: FULL_AND_PIECEWISE captured 35 piecewise + 19 full decode graphs

No fallback to Marlin / Triton dequant. End-to-end native FP4.

Stability

30-minute stress at concurrency=32 against the W4A4 server:
3,200 successful requests, 0 errors, 0 !!!! collapses — the patch
holds under sustained load (this is the regression guard for the failure
mode that motivated this PR).

Hardware footnote

All B200 numbers above use a single GPU at --gpu-memory-utilization 0.85.
B100 / B200 with NVLink + TP=2 will scale differently and is intentionally
out of scope for this PR.

The PR's earlier Pro 6000 Blackwell WS draft is preserved in the commit
history; we've replaced it with B200 numbers because (a) B200 is the
production target, and (b) the Pro 6000's GDDR7 memory makes it
bandwidth-bound on this workload, masking the FP4 throughput win that
HBM3e exposes.

How the +4 pp early reading turned into a real -1.9 pp number

An earlier in-process smoke check on Daily-Omni n=50 had W4A4 at
0.76 vs BF16 0.72 (apparent +4 pp for the quantized model). The full
n=1197 run lands the same W4A4 ckpt at 0.675 vs BF16 0.694
— well within the binomial 95% confidence band the n=50 slice allows.
Recording this here so reviewers don't have to triage the same surprise.

Files

  • vllm_omni/patch.py (+119 / -88 net): PWAL-time NaN-clamp override
    with WHY / WHY NOT / SCOPE / FRAGILITY / TODO block comment, env-var
    escape hatch, self-extinguishing upstream-already-patched check, and
    install assert.
  • tests/model_executor/quantization/__init__.py: new test subpackage.
  • tests/model_executor/quantization/test_modelopt_nvfp4_nan_clamp.py
    (+176): CPU-only unit test of the override.
  • tests/e2e/offline_inference/test_qwen3_omni_modelopt_nvfp4_w4a4.py
    (+76): hardware-gated e2e smoke test asserting non-degenerate output
    on the published W4A4 checkpoint.
  • docs/user_guide/quantization/modelopt.md (+37): adds Qwen3-Omni W4A4
    section with HF repo link, hardware requirement, calibration recipe
    pointer, and a note describing the ModelOpt 0.44 NaN regression and
    the workaround installed by vllm_omni.patch.

@YIHONG-JIN
YIHONG-JIN force-pushed the feat/qwen3-omni-modelopt-nvfp4-w4a4 branch 4 times, most recently from 7e6f7d9 to df4dda7 Compare June 1, 2026 00:04
@YIHONG-JIN

Copy link
Copy Markdown
Contributor Author

Hi @lishunyang12 @baonudesifeizhai — flagging this NVFP4 W4A4 enablement PR for visibility given your prior work on the unified quantization framework (#1764) and the ModelOpt FP8 / mixed FP8-NVFP4 paths (#2913, #3570). The change here is a load-time clamp in vllm_omni.patch that protects existing community NVFP4 W4A4 checkpoints (exported with stock ModelOpt 0.44) from collapsing to !!!!. Would appreciate your review when convenient.

@YIHONG-JIN
YIHONG-JIN force-pushed the feat/qwen3-omni-modelopt-nvfp4-w4a4 branch from df4dda7 to d5f296c Compare June 1, 2026 00:25
@YIHONG-JIN
YIHONG-JIN marked this pull request as ready for review June 1, 2026 00:37
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Comment thread vllm_omni/patch.py Outdated
)

def _patched_nvfp4_pwal(self, layer, *args, **kwargs):
_original_nvfp4_pwal(self, layer, *args, **kwargs)

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.

Clamping after the original PWAL is too late for the Marlin fallback: prepare_fp4_layer_for_marlin() converts/permutates weight_scale before this runs, so NaN bytes can already have been folded into the Marlin scale format and may no longer be detected here. Please clamp before calling the original PWAL when the upstream method is not already patched.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks. Will test the Marlin fallback path

quant_params = [(QUANTIZED_MODEL, _stage_config())]


@hardware_test(res={"cuda": "H100"}, num_cards=2)

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.

This is marked for H100, but the PR/docs say the native W4A4 FP4 path requires sm_100+ Blackwell. On H100 init_nvfp4_linear_kernel() falls back to Marlin weight-only FP4, so this smoke does not exercise the FlashInfer/CUTLASS W4A4 path being enabled. Please add/select a Blackwell resource, or keep this as an explicit fallback test and add a Blackwell smoke.


current = ModelOptNvFp4LinearMethod.process_weights_after_loading
name = getattr(current, "__qualname__", "") or getattr(current, "__name__", "")
assert "patched_nvfp4_pwal" in name or "_patched_nvfp4_pwal" in name, (

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.

This assertion will fail as soon as the self-extinguish path works: when upstream vLLM adds its own clamp, patch.py intentionally skips installing _patched_nvfp4_pwal, so the qualname will not contain this marker. Please accept the upstream-patched case as passing, or skip this assertion when _already_patched_upstream is true.

@hsliuustc0106

Copy link
Copy Markdown
Collaborator

is acc maintained?

@YIHONG-JIN
YIHONG-JIN force-pushed the feat/qwen3-omni-modelopt-nvfp4-w4a4 branch from 02fc0f9 to 98937b4 Compare June 2, 2026 21:10
@YIHONG-JIN

Copy link
Copy Markdown
Contributor Author

Hi @hsliuustc0106 — thanks for the question. Yes. Updated the Test Result section with the relevant evidence:

  • B200 / FlashInfer Cutlass / clean awqclip ckpt n=1197: 0.675 vs BF16 0.694 (-1.9 pp) on Daily-Omni, -0.5 pp on OmniBench. (Original validation.)
  • Pro 6000 WS / FlashInfer Cutlass (same kernel class as B200) / NaN-laden mse-preview ckpt + load-clamp active n=200: 0.670 — within calibration-algorithm noise of the production awqclip number (0.5 pp gap between mse and awqclip on the same kernel).
  • H100 SXM / Marlin fallback / same NaN ckpt + clamp n=200: 0.665 (different kernel path, included for completeness).

The clamp doesn't degrade accuracy because it's mathematically equivalent to clamping at the calibration write-side — same weight_scale tensor reaches the FP4 GEMM either way (NaN positions = FP8 max byte 0x7E = 448.0). The Pro 6000 number reflects what a re-calibration with patched ModelOpt would produce for the same mse calibration algorithm.

@hsliuustc0106

Copy link
Copy Markdown
Collaborator

Hi @hsliuustc0106 — thanks for the question. Yes. Updated the Test Result section with the relevant evidence:

  • B200 / FlashInfer Cutlass / clean awqclip ckpt n=1197: 0.675 vs BF16 0.694 (-1.9 pp) on Daily-Omni, -0.5 pp on OmniBench. (Original validation.)
  • Pro 6000 WS / FlashInfer Cutlass (same kernel class as B200) / NaN-laden mse-preview ckpt + load-clamp active n=200: 0.670 — within calibration-algorithm noise of the production awqclip number (0.5 pp gap between mse and awqclip on the same kernel).
  • H100 SXM / Marlin fallback / same NaN ckpt + clamp n=200: 0.665 (different kernel path, included for completeness).

The clamp doesn't degrade accuracy because it's mathematically equivalent to clamping at the calibration write-side — same weight_scale tensor reaches the FP4 GEMM either way (NaN positions = FP8 max byte 0x7E = 448.0). The Pro 6000 number reflects what a re-calibration with patched ModelOpt would produce for the same mse calibration algorithm.

@lishunyang12 does qwen3-omni support online/offline fp8 quantization now?

Comment thread vllm_omni/patch.py Outdated
# for-shape with the original tensor; pin that here so a future
# packed scale format would fail loudly rather than silently corrupt
# unrelated bytes.
byte_view = weight_scale.data.view(torch.uint8)

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.

.view(torch.uint8) raises on a non-contiguous weight_scale, so a checkpoint whose per-block scale tensor isn't contiguous would crash here before ever reaching the shape assert below. A weight_scale.contiguous() (or an is_contiguous() guard) before taking the byte view would make this robust.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — fixed in 5a83ec3. The clamp now normalizes a non-contiguous weight_scale with .contiguous() and writes the copy back to the Parameter before the uint8 view (a bare .contiguous() copy would be discarded, leaving the original tensor — the one the kernel reads — un-clamped). Added a test_clamps_non_contiguous_weight_scale regression test that plants NaN bytes in a strided view.

Comment thread vllm_omni/patch.py Outdated
_OriginalModelOptNvFp4LinearMethod.process_weights_after_loading = _patched_nvfp4_pwal
# Fail loudly if install dropped silently (e.g. another plugin
# patched the same class) rather than degrade to `!!!!` at decode.
assert _OriginalModelOptNvFp4LinearMethod.process_weights_after_loading is _patched_nvfp4_pwal, (

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.

This assert is compiled out under python -O / PYTHONOPTIMIZE, so in optimized runs the "install failed" guard silently disappears and a conflicting plugin would degrade straight to !!!! at decode — exactly the case this block exists to catch. Since the intent here is to fail loudly, raise RuntimeError(...) would be safer than assert (same goes for the shape assert at L156).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — switched both the install self-check (L223) and the byte-view shape check (L156) from assert to raise RuntimeError in 5a83ec3, so they survive python -O / PYTHONOPTIMIZE. These guards exist precisely to fail loudly before a conflicting plugin or a changed scale layout degrades to !!!! at decode, so they are the last things that should be stripped under -O.

Comment thread vllm_omni/patch.py Outdated
_nan_clamp_import_err,
)
else:
_original_nvfp4_pwal = _OriginalModelOptNvFp4LinearMethod.process_weights_after_loading

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.

On a module reload (e.g. importlib.reload in a test, or a second import path) this captures the already-patched method, so _patched_nvfp4_pwal ends up wrapping itself and the clamp runs twice per load. It is idempotent so not a correctness bug, but stashing a sentinel attribute on the class (or checking _original_nvfp4_pwal.__name__ != "_patched_nvfp4_pwal") would keep the install truly idempotent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 5a83ec3. The wrapper now stamps a _vllm_omni_wrapped_pwal sentinel pointing at the genuine upstream method, and the install reads it via getattr(..., "_vllm_omni_wrapped_pwal", _current) before re-wrapping — so a module reload recovers the real upstream PWAL instead of capturing our own wrapper and nesting the clamp (which would also have made the co_names self-extinguish heuristic introspect our wrapper rather than upstream). Added test_reload_does_not_nest_wrapper asserting the chain stays flat after an importlib.reload.

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

Thanks for this — the load-time NaN-clamp is a well-scoped, well-documented fix for the !!!! failure on community NVFP4 W4A4 checkpoints, and the self-extinguish heuristic plus the clamp-before-PWAL ordering rationale are carefully thought through. My inline notes are all minor robustness/hygiene points, not blockers: (1) the weight_scale.view(torch.uint8) byte view should guard contiguity; (2) the assert-based install/shape guards are compiled out under python -O/PYTHONOPTIMIZE, so the intended loud failure is lost in optimized runs — prefer raise; and (3) the override is not reload-idempotent (it can wrap itself on re-import). None affect the happy path — addressing them just makes the guard hold under -O and on module reload.

@YIHONG-JIN
YIHONG-JIN force-pushed the feat/qwen3-omni-modelopt-nvfp4-w4a4 branch from ba4d056 to 5a83ec3 Compare June 7, 2026 21:22
@YIHONG-JIN

Copy link
Copy Markdown
Contributor Author

Pushed 5a83ec3 addressing @lishunyang12's three review nits (replies inline):

  • non-contiguous weight_scale is now made contiguous + written back to the Parameter before the uint8 view
  • install self-check and byte-view shape check use raise RuntimeError instead of assert (survive python -O)
  • module-reload idempotency via a _vllm_omni_wrapped_pwal sentinel so a reload recovers the genuine upstream method instead of nesting the clamp

Re-validated on 2x H100 (vllm 0.22.0): 10/10 CPU unit tests pass (incl. new non-contiguous + reload tests), and the e2e smoke on the NaN-reproducer ckpt (full-thinker-mse-preview) passes — load logs show Clamped 1 + Clamped 3 = 4 NaN bytes clamped, coherent output, no !!!!. All three changed code paths are inert on the production happy path (contiguous scale, fresh process, no -O), so the existing B200 / Pro 6000 Cutlass accuracy numbers are unaffected.

@YIHONG-JIN
YIHONG-JIN force-pushed the feat/qwen3-omni-modelopt-nvfp4-w4a4 branch from 5a83ec3 to bb5d38c Compare June 7, 2026 21:27

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

One more test issue.

# Falsy values must leave the install in place — this is the case
# the repo-wide bool-env idiom in vllm_omni.patch guards against
# (naive `if os.environ.get(...)` would wrongly skip on "0"/"false").
("0", "INSTALLED"),

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.

This still conflicts with the self-extinguish path. Once upstream vLLM has its own clamp, "0" / "false" should mean "do not use the escape hatch", but the wrapper should still be skipped because _already_patched_upstream is true. This test would then print NOT_INSTALLED and fail even though the behavior is correct. Please make the falsey cases accept the upstream-patched state too, similar to test_install_is_self_asserting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks. Verified the falsey + upstream-patched case you flagged by simulating an upstream clamp before import for the most recent commit.

@YIHONG-JIN
YIHONG-JIN force-pushed the feat/qwen3-omni-modelopt-nvfp4-w4a4 branch 2 times, most recently from a78dd4e to 079d0c1 Compare June 10, 2026 04:09
@david6666666 david6666666 added ready label to trigger buildkite CI and removed ready label to trigger buildkite CI labels Jun 15, 2026
@david6666666

Copy link
Copy Markdown
Collaborator

please fix ci, thx

Adds a defensive runtime override in vllm_omni.patch of
ModelOptNvFp4LinearMethod.process_weights_after_loading that clamps NaN
bytes in per-block weight_scale tensors at load time.

ModelOpt 0.44's float32 -> torch.float8_e4m3fn cast of per-block
weight_scale occasionally emits literal NaN bytes (E4M3 encoding 0x7F /
0xFF) when the pre-cast scale rounds above the FP8 E4M3 max of 448
after the global-scale division. A single NaN byte in any weight_scale
propagates through the FlashInfer FP4 GEMM and collapses the served
model output to `!!!!`.

The load-time clamp is mathematically equivalent to clamping at
calibration write-side: by the time the kernel reads weight_scale,
every position that would have been NaN is the FP8 E4M3 max byte
(0x7E = 448.0). The proper fixes sit upstream — at the ModelOpt cast
sites, so newly calibrated checkpoints export cleanly, and in vLLM's
own PWAL, so vllm-omni can drop this override. Both are planned as
follow-up PRs after this one merges. Until those land in vllm-omni's
pinned vLLM, any NVFP4 W4A4 Qwen3-Omni checkpoint published by the
community — exported with stock ModelOpt 0.44 — would silently serve
only `!!!!`, which looks like a vllm-omni inference bug to the user.
Newly calibrated clean checkpoints pay no runtime cost (no-op when no
NaN bytes are present).

The override clamps NaN bytes BEFORE delegating to the original PWAL.
The non-Blackwell Marlin fallback's kernel PWAL casts weight_scale
FP8 -> bf16 and permutes; clamping after would trip the byte-view
shape assertion (FP8 1 B/elem vs bf16 2 B/elem) or operate on already-
transformed bytes. Blackwell Cutlass/TRTLLM/cuDNN PWALs only swizzle
FP8 bytes, so clamp-before is equivalent there. Clamping uses a uint8
view + masked_fill_ (PyTorch does not implement masked_fill_ for
float8_e4m3fn directly).

Self-extinguish: an __code__.co_names check requires all three of
{masked_fill_, isnan, weight_scale} in vLLM's PWAL — the structural
signature of an in-place NaN clamp on weight_scale — before declaring
upstream patched and skipping install. Env var
VLLM_OMNI_SKIP_NVFP4_NAN_CLAMP={1,true,yes,on} disables the override
for diagnostics (repo-wide bool-env idiom; "0"/"false" correctly mean
"do not skip"). A dtype guard skips the clamp when weight_scale.dtype
!= torch.float8_e4m3fn, defending against re-invocation on a Marlin-
transformed layer.

Robustness: a non-contiguous weight_scale is made contiguous and
written back to the Parameter before the uint8 view (a bare
.contiguous() copy would be discarded, leaving the original un-clamped);
the install self-check and byte-view shape check raise RuntimeError
rather than assert so they survive python -O / PYTHONOPTIMIZE; and the
wrapper stamps a sentinel attribute so a module reload recovers the
genuine upstream method instead of nesting the clamp.

Hardware validation:

B200 / FlashInfer Cutlass NvFp4 Linear + FLASHINFER_TRTLLM NvFp4 MoE
backend, YihongJin/Qwen3-Omni-30B-A3B-Instruct-NVFP4-W4A4-full-thinker-
awqclip (clean ckpt; clamp is a no-op):

- accuracy:   Daily-Omni n=1197 W4A4 0.675 vs BF16 0.694 (-1.9 pp);
              OmniBench n=200 W4A4 0.450 vs BF16 0.455 (-0.5 pp)
- throughput: at concurrency=32, output-len=512 random workload,
              W4A4 2766 tok/s vs BF16 2083 tok/s (+33%);
              TPOT W4A4 9.12 ms vs BF16 12.76 ms (-29%)
- stability:  30-min sustained concurrency=32 produces 3200 successful
              responses with 0 errors and 0 `!!!!` collapses

2x H100 SXM / Marlin fallback path, YihongJin/Qwen3-Omni-30B-A3B-
Instruct-NVFP4-W4A4-full-thinker-mse-preview (NaN-containing
reproducer ckpt; clamp active):

- e2e smoke passes (no `!!!!` collapse)
- Daily-Omni n=200: 0.665

2x RTX PRO 6000 WS (sm_120) / FlashInfer Cutlass NvFp4 — same kernel
class as B200, full-thinker-mse-preview (clamp active):

- e2e smoke passes; load logs show 4 NaN bytes detected and clamped
- Daily-Omni n=200: 0.670

The 0.5 pp gap on the same kernel surface (Pro 6000 Cutlass 0.670 vs
B200 Cutlass clean awq_clip 0.675) is attributable to calibration
algorithm (mse vs awq_clip), not the clamp.

Tests:

- tests/model_executor/quantization/test_modelopt_nvfp4_nan_clamp.py:
  CPU-only unit tests covering clamp behavior on a planted-NaN
  weight_scale (contiguous and non-contiguous), no-op on finite input,
  no-op when weight_scale is absent, the install self-check with 3-way
  branching on {_already_patched_upstream, _clamp_installed, neither},
  reload idempotency (a module reload does not nest the wrapper), and
  the env-var escape hatch parametrized over {"1","true"} (expect
  NOT_INSTALLED) and {"0","false"} (expect our wrapper INSTALLED OR
  upstream already clamping — the self-extinguish state is accepted so
  the test still passes once the upstream fix lands) via subprocess,
  reporting state from vllm_omni.patch's module flags rather than method
  qualname introspection.
- tests/e2e/offline_inference/test_qwen3_omni_modelopt_nvfp4_w4a4.py:
  hardware-gated e2e smoke on the H100 Marlin fallback path that
  loads the published W4A4 checkpoint and asserts the response does
  not match the `!!!!` degenerate-output signature.

Docs: docs/user_guide/quantization/modelopt.md adds a Qwen3-Omni W4A4
section with the HF repo link, hardware requirement (sm_100+),
calibration recipe pointer, and a note describing the ModelOpt 0.44
NaN regression and the workaround installed by vllm_omni.patch.

Signed-off-by: Yihong Jin <yihongjin404@gmail.com>
@YIHONG-JIN
YIHONG-JIN force-pushed the feat/qwen3-omni-modelopt-nvfp4-w4a4 branch from b1138d5 to debb2ad Compare June 16, 2026 18:37
@hsliuustc0106 hsliuustc0106 added ready label to trigger buildkite CI and removed ready label to trigger buildkite CI labels Jun 17, 2026

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

can you help check the profiling with w4a4 quant to see which part/ops benefits most

@Gaohan123 Gaohan123 added this to the v0.24.0 milestone Jun 17, 2026
@YIHONG-JIN

Copy link
Copy Markdown
Contributor Author

@david6666666 the current AMD/Intel failures are unrelated to this PR's changes (NVFP4 clamp + a test file) — CUDA (buildkite/vllm-omni) is green, including the diffusion and model-executor suites.

  • AMD Diffusion · Batch Test and CosyVoice3-TTS E2E → exit status 124 (timeout wrapper killing a hung test).
  • AMD Diffusion GPU Worker Test → tests/diffusion/test_diffusion_worker.py::TestDiffusionWorkerWakeUp sleep/wake-buffer failures — ROCm-specific; the same suite passes on CUDA.
  • Intel XPU run → terminated at ~1h (wall-clock timeout).

Could a maintainer retry the AMD/Intel builds? Happy to dig into any specific one if it looks related to the change.

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

LGTM now

@david6666666
david6666666 merged commit 2f64c71 into vllm-project:main Jun 18, 2026
6 of 8 checks passed
Nughm3 pushed a commit to Nughm3/vllm-omni that referenced this pull request Jun 18, 2026
alex-jw-brooks pushed a commit to alex-jw-brooks/vllm-omni that referenced this pull request Jun 23, 2026
khairulkabir1661 pushed a commit to khairulkabir1661/vllm-omni that referenced this pull request Sep 25, 2026
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.

5 participants