Skip to content

[Bug]: NVFP4 MoE monolithic backend (trtllm_fp4_block_scale_moe) produces semantically corrupt output for non-gated ReLU² MoE (Nemotron-3-Nano) on SM100; flashinfer_cutlass is correct on the same GPU #52308

Description

@ldkhang1201

Summary

Serving nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4 (NemotronH hybrid Mamba-Transformer MoE, ModelOpt NVFP4, FP8 KV cache) with default backend selection on SM100-family GPUs (tested: B200) produces grammatically fluent but semantically corrupted output — 100% deterministic under greedy decoding. The same checkpoint on the same vLLM version is correct on SM120 (RTX PRO 4500 Blackwell).

An A/B/A experiment on the same B200 isolates the fault to the auto-selected FLASHINFER_TRTLLM monolithic NVFP4 MoE backend: adding --moe-backend=flashinfer_cutlass fully fixes the output on the same GPU with the same checkpoint; reverting to the default reproduces the broken logprobs bit-identically.

Environment

  • vLLM v0.27.0 (the relevant dispatch code is byte-identical in v0.26.0; the flashinfer pin changed 0.6.14 → 0.6.16.post3 between the tags)
  • flashinfer-python 0.6.16.post3 (per requirements/cuda.txt)
  • Broken: B200 (compute capability 10.0). Clean: RTX PRO 4500 Blackwell (12.0)
  • Model: nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4 — non-gated ReLU² MoE, 128 experts, top-6, sigmoid+bias grouped top-k with n_group=1, hidden_size=2688, kv_cache_quant_algo: FP8, group_size: 16
  • TP=1, PP=1, otherwise default flags

Reproduction

vllm serve nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4 --tensor-parallel-size 1

curl -s localhost:8000/v1/completions -H 'Content-Type: application/json' \
  -d '{"model":"nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4","prompt":"The capital of France is","max_tokens":16,"temperature":0}'

Expected: Paris… — Actual on B200 with the default backend: **"The Last of Us"** (2013) – a

Other 100%-deterministic failures at temperature 0: Question: What is 2+2? Answer: never contains 4; the model cannot continue 1, 2, 3, 4,.

Evidence

Top-5 logprobs for The capital of France is (greedy, 1 token), same B200, same checkpoint:

MoE backend top-5 logprobs
default (FLASHINFER_TRTLLM monolithic) ' **' -2.0724, '.' -2.3224, ' ' -2.5099, ',' -2.7599, '**' -2.8849 — no content token in the top-5
--moe-backend=flashinfer_cutlass ' Paris' -0.0278 — healthy

Two forensic details:

  1. A/B/A determinism: switching back to the default reproduces the broken logprobs bit-identically to 10 decimal places across restarts — this is a deterministic contract/kernel defect, not a race.
  2. The broken logits sit on a 1/16 lattice: the gaps between the top-5 raw logits are exactly 4/16, 3/16, 4/16, 2/16. Healthy bf16 logits don't do that; it suggests the MoE contribution to the residual stream is coarsely mis-scaled/degenerate rather than "wrong but rich" — pointing at scale application rather than e.g. wrong expert selection.

During the healthy flashinfer_cutlass run, every other SM100-default path was still active (trtllm-gen FP8-Q attention, CuTe-DSL dense NVFP4 GEMMs), so those are exonerated; the defect is fully contained in what --moe-backend swaps.

Analysis (v0.27.0 tree)

Selection chain: ModelOptNvFp4FusedMoEselect_nvfp4_moe_backend (vllm/model_executor/layers/fused_moe/oracle/nvfp4.py) tries FLASHINFER_TRTLLM first. TrtLlmNvFp4ExpertsMonolithic._supports_current_device() requires is_device_capability_family(100) — which is why SM120 never runs this path (matching the clean/broken hardware split) — and _supports_no_act_and_mul() returns True with the comment "(i.e. Nemotron-Nano)", making this model the primary user of the non-gated variant.

The monolithic bundle differs from the modular flashinfer_cutlass path in several coupled ways; the defect must live in one of these:

  1. Hidden-size padding contract: expert weights zero-padded 2688 → 2816 (round_up(…, 256)), activations padded at runtime and the output sliced back; after nvfp4_block_scale_interleave + epilogue tile shuffle, each row has 168 real + 8 padded scale groups. A scale offset/stride mismatch here best fits the 1/16-lattice logits.
  2. Non-gated (ReLU²) scale plumbing: g1_scale_c = a2_gscale.clone(), output1_scale_scalar = 1/a2_scale, output1_scale_gate_scalar = w13_scale_2 * a13_scale — gate-slot scalars reused for a MoE that has no gate half; the contract with the flashinfer kernel is implicit.
  3. In-kernel routing: flashinfer.fused_moe.trtllm_fp4_block_scale_moe computes DeepSeekV3-style routing internally (degenerate n_group=1, top-6 of 128, fp32 sigmoid+bias) vs vLLM's own ops.grouped_topk on the modular path.
  4. Activation scale layout: linear (non-swizzled) — unique to this backend.
  5. routing_replay_out kwarg (new call-site argument in v0.27.0) ties behavior to the installed flashinfer signature.

Suggested immediate mitigation

Until the monolithic non-gated path passes a numeric parity test, TrtLlmNvFp4ExpertsMonolithic._supports_no_act_and_mul() should return False so auto-selection falls back to FLASHINFER_CUTLASS (verified correct for this model on both SM100 and SM120). Longer-term: a CI parity test (monolithic vs modular vs QDQ emulation) on a small non-gated ReLU² MoE with hidden size not divisible by 256 would catch this entire defect class.

User workaround until then: --moe-backend=flashinfer_cutlass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions