[Bugfix] Support non-gated MoE in online quantization and Marlin MoE tile padding - #48028
puririshi98 wants to merge 10 commits into
Conversation
OnlineMoEMethodBase.create_weights unconditionally allocates w13_weight (and w13_bias) at 2 * intermediate_size_per_partition, assuming a fused gate_up_proj. Non-gated MoE models (is_act_and_mul=False, e.g. NemotronH relu2 in Nemotron-3 Nano/Super/Ultra) have no gate projection, so the kernel-format conversion packs a 2N-wide w13 while the Marlin MoE gemm computes size_n = N (w13_num_shards = 1), tripping 'size_n == actual_size_n' in marlin_moe_wna16. Size w13 by is_act_and_mul, matching UnquantizedFusedMoEMethod. This is the shared allocation base inherited by all online MoE methods (fp8 per-tensor/per-block/ptpc, mxfp8, int8). Also make Fp8PerBlockOnlineMoEMethod._zero_padding shard-aware so the roundup padding of a non-gated w13 is zeroed correctly (previously it assumed two gate/up shards and would leave uninitialized pad rows that contaminate the shared per-block scales). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Rishi Puri <riship@nvidia.com>
… MoE The Marlin MoE thread-tile padding (vllm-project#45703) hardcodes the gated w13 layout: _moe_pad_shard_rows views (E, 2, n, ...) and the fp8 scale pad branch views (e, g, 2, n). For non-gated MoE (is_act_and_mul=False) w13 is (E, N, K), so a tile-misaligned rank-local intermediate size (e.g. Nemotron-3-Super-120B-A12B: moe_intermediate_size 2688 at TP4 -> 672, not a multiple of the 64-column Marlin thread tile) fails at the view instead of being padded, even though MarlinExperts supports RELU2_NO_MUL. Derive the w13 shard count from the tensor shapes (w13_n // n: 2 gated, 1 non-gated) and thread it through the weight and scale padding in prepare_fp8_moe_layer_for_marlin and prepare_mxfp8_moe_layer_for_marlin (num_shards is a required parameter to keep future callers explicit). Extend the padded round-trip tests to cover both layouts and use unscaled inputs so the tolerance is discriminative. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Rishi Puri <riship@nvidia.com>
bf7586f to
8eca7df
Compare
Seed the fp8/mxfp8 Marlin MoE padded round-trip tests (torch.manual_seed(0)) so the unscaled random inputs are deterministic against the fixed atol=8e-2 tolerance, removing a latent flakiness risk introduced when the /10 input downscale was dropped. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Rishi Puri <riship@nvidia.com>
|
@TomerBN-Nvidia do you know who can merge this? |
@pavanimajety can you help with this? |
pavanimajety
left a comment
There was a problem hiding this comment.
A few nits, thanks for the PR!
| @pytest.mark.parametrize("is_act_and_mul", [True, False]) | ||
| @pytest.mark.parametrize("has_bias", [True, False]) | ||
| def test_online_moe_create_weights_w13_dim(is_act_and_mul, has_bias): | ||
| """w13 holds gate+up (2N) for gated MoE and up only (N) for non-gated MoE | ||
| (is_act_and_mul=False, e.g. NemotronH relu2).""" |
There was a problem hiding this comment.
nit: I think this test is unnecessary. it might be more helpful to add one test that tests the whole online_moe weight creation, post weight loading and kernel run.
There was a problem hiding this comment.
Agreed, replaced both narrow tests with a single end-to-end test_online_moe_fp8_per_block_end_to_end that covers weight creation, weight loading, post-loading processing and a kernel run (parametrized gated / non-gated).
Two details worth calling out:
- It uses a non-block-aligned intermediate size (
n=96, rounded up to 128) and leaves the roundup pad rows non-zero, so the kernel output only matches the bf16 reference if_zero_paddingactually zeroes them — that turns the old shape/zero-padding assertions into a behavioural guard. - It pins
VLLM_TEST_FORCE_FP8_MARLIN=1. Marlin is the fp8 MoE backend that supports non-gated MoE; on Blackwell the defaulttrtllm_fp8_block_scale_moehardcodes the gated layout and rejects a single-shard w13 (gemm1_weights_scale.size(1) == intermediate_size_factor * intermediate_size / 128 (1 vs 2)), which is a separate gap outside this PR.
Verified on GB200: 2 passed. Negative controls confirm it catches both bugs — reverting the w13 allocation fix, or making _zero_padding assume 2 shards again, fails only the non-gated case.
| w13_bias_shard_size = layer.w13_bias.shape[1] // num_w13_shards | ||
| if w13_bias_shard_size > intermediate_size: | ||
| for s in range(num_w13_shards): | ||
| start = s * w13_bias_shard_size + intermediate_size | ||
| layer.w13_bias[:, start : (s + 1) * w13_bias_shard_size] = 0 |
There was a problem hiding this comment.
nit: Combine with the above loop
There was a problem hiding this comment.
Done — the weight and bias pad-zeroing now share one loop:
num_w13_shards = 2 if self.moe.is_act_and_mul else 1
for w13 in (layer.w13_weight, getattr(layer, "w13_bias", None)):
if w13 is None:
continue
shard_size = w13.shape[1] // num_w13_shards
if shard_size > intermediate_size:
for s in range(num_w13_shards):
start = s * shard_size + intermediate_size
w13[:, start : (s + 1) * shard_size] = 0Semantics are unchanged: for the 3-D weight, w13[:, a:b] is equivalent to the previous w13[:, a:b, :].
| def _moe_pad_shard_rows( | ||
| x: torch.Tensor, n: int, padded_n: int, num_shards: int | ||
| ) -> torch.Tensor: |
There was a problem hiding this comment.
are there other places where _moe_pad_shard_rows is utilized? Do we need to give a default number here for num_shards?
There was a problem hiding this comment.
_moe_pad_shard_rows is module-private and has exactly three call sites, all in marlin_utils_fp8.py, and all now pass num_w13_shards explicitly:
prepare_fp8_moe_layer_for_marlin—w13_weightprepare_mxfp8_moe_layer_for_marlin—w13andw13_scale
I would prefer not to give num_shards a default. A default of 2 would silently reinstate the gated-only assumption this PR is fixing: a non-gated layer would then be mis-padded rather than failing loudly at the view. Keeping it required means any future caller has to state the layout. Happy to add one if you feel strongly.
Per review feedback: - Replace the two narrow online-MoE unit tests with a single end-to-end test covering weight creation, weight loading, post-loading processing and a kernel run, for gated and non-gated layouts. It uses a non-block-aligned intermediate size and leaves the roundup pad rows non-zero, so the result only matches the reference if the pad rows are zeroed. Pinned to the Marlin MoE backend, the fp8 backend that supports non-gated MoE. - Combine the w13 weight and bias pad-zeroing loops in _zero_padding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Rishi Puri <riship@nvidia.com>
- ruff-format the online MoE test (the pre-commit CI failure). - Use the dist_init fixture instead of a hand-rolled distributed init: it uses a file:// store rather than a fixed TCP port and tears the distributed environment down after the test. - Make the numeric comparison meaningful: the previous tolerance was larger than the signal, so a zeroed output would have passed. Tighten it and assert the reference is non-trivial. - Assert directly that the w13 pad rows are zeroed. w2's pad columns are zeroed unconditionally, so a non-zeroed w13 pad row cannot be observed in the kernel output, but it does contaminate the shared per-block scale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Rishi Puri <riship@nvidia.com>
|
Hi, thanks for the pr. I've solve the problem partially in #51125 and list you as an author. Hope it works for you, lmk if any other problems. |
|
This pull request has merge conflicts that must be resolved before it can be |
Purpose
Non-gated MoE models — where
w13holds only an up projection instead of fusedgate+up shards — currently break in two places on the online-quantization +
Marlin path. The concrete motivation is the NemotronH architecture
(
mlp_hidden_act="relu2",is_act_and_mul=False) used by the publicNemotron-3 family:
MarlinExpertsalready supports non-gated MoE (_supports_no_act_and_mul() -> True,RELU2_NO_MUL,w13_num_shards = 2 if activation.is_gated else 1), butthe weight-prep side does not:
Bug 1 — online-quant allocation.
OnlineMoEMethodBase.create_weightsunconditionally allocatesw13_weight(and
w13_bias) at2 * intermediate_size_per_partition. For non-gatedmodels the kernel-format conversion then packs a
2N-widew13while theMarlin MoE gemm computes
size_n = N, trippingSTD_TORCH_CHECK(size_n == actual_size_n)inmarlin_moe_wna16. This is theshared allocation base inherited by all online MoE methods (fp8
per-tensor/per-block/ptpc, mxfp8, int8), so one fix covers all of them; it
mirrors
UnquantizedFusedMoEMethod.create_weights(w13_up_dim). The fix alsomakes
Fp8PerBlockOnlineMoEMethod._zero_paddingshard-aware: itpreviously assumed two gate/up shards, so for a non-gated model whose
intermediate size is rounded up to the 128 quant block, uninitialized pad rows
were never zeroed and would contaminate the shared per-block scales.
Bug 2 — Marlin MoE tile padding is gated-only.
The MoE thread-tile padding added in #45703 hardcodes the gated layout:
_moe_pad_shard_rowsviews(E, 2, n, ...)and the fp8 scale-pad branchviews
(e, g, 2, n). A non-gated model with a tile-misaligned rank-localintermediate size fails at the
viewinstead of being padded. Concrete case:Nemotron-3-Super-120B-A12B has
moe_intermediate_size = 2688; at TP=4 therank-local shard is
672, which is not a multiple of the 64-column Marlinthread tile (
min_thread_n). The fix derives the shard count from the tensorshapes (
w13_n // n: 2 gated, 1 non-gated) and threads it through the weightand scale padding in
prepare_fp8_moe_layer_for_marlinandprepare_mxfp8_moe_layer_for_marlin. Zero-padding is mathematically exact fornon-gated activations with
act(0) = 0(relu²): the paddedw13outputchannels are zero, and the matching zero
w2input columns drop out.No behavior change for existing gated models:
num_w13_shardsresolvesto 2 and reproduces the prior layout exactly; the allocation and zero-padding
changes only affect
is_act_and_mul=Falseconfigurations, which previouslycrashed (or, for per-block zero-padding, silently corrupted scales).
Why this does not duplicate existing PRs
w13_up_dimfix toFp8MoEMethodinquantization/fp8.py(checkpoint-fp8 path); its second hunk targets anonline create_weights that no longer exists after the
online/moe_base.pyrefactor. This PR fixes the shared online allocation base that all online
methods inherit from — complementary, not overlapping (no common lines).
[Kernel] Consolidate Marlin thread-tile padding across all dense Marlin paths #45295 (dense) / [Kernel] Extend Marlin thread-tile padding to MoE (WNA16 + FP8/MXFP8) #45703 (MoE) mechanism; this PR extends the merged
mechanism to non-gated layouts rather than introducing a parallel one.
fused_moe/config.py).Changes
vllm/model_executor/layers/quantization/online/moe_base.py— sizew13_weight/w13_biasbyself.moe.is_act_and_mul.vllm/model_executor/layers/quantization/online/fp8.py— shard-awareFp8PerBlockOnlineMoEMethod._zero_padding.vllm/model_executor/layers/quantization/utils/marlin_utils_fp8.py—_moe_pad_shard_rows(..., num_shards)(required parameter);shard-count-aware weight and scale padding in both MoE prep functions.
tests/quantization/test_online.py— CPU unit teststest_online_moe_create_weights_w13_dim(gated × bias) andtest_fp8_per_block_zero_padding(gated/non-gated).tests/kernels/quantization/test_marlin_tile_padding.py— CPU shape testtest_moe_pad_shard_rows[1|2];gated=[True, False]parametrization oftest_fp8_marlin_moe_padded_round_tripandtest_mxfp8_marlin_moe_padded_round_trip(non-gated usesMoEActivation.RELU2_NO_MULin both the kernel call and thetorch_expertsreference); round-trip inputs are no longer downscaled sothe tolerance is discriminative, and the round-trips are seeded
(
torch.manual_seed(0)) so the unscaled inputs stay deterministic acrossruns.
Known related issue left out of scope: the int-WNA16 oracle pad helpers
(
_pad_w13_shard_cols/_pad_w13_biasinfused_moe/oracle/int_wna16.py)are still gated-only, so a non-gated WNA16 model with a tile-misaligned
intermediate fails loudly at the analogous
view; same fix pattern appliesand can follow up.
Test Plan
Test Result
All runs on GB200 (aarch64, CUDA 13.0), editable install of this branch with
the precompiled wheel pinned to the
maincommit this branch merges(
1ff942965), so the compiled ops match the source.Negative controls. The end-to-end test was confirmed to fail when either
fix is reverted, and only for the non-gated parametrization:
create_weightsw13 allocation back to unconditional2 * intermediate[False]fails,[True]passes_zero_paddingback to assuming 2 shards[False]fails,[True]passesThe test uses a non-block-aligned intermediate size (
n=96, rounded up to the128 quant block) and leaves the roundup pad rows non-zero, so the kernel output
only matches the bf16 reference if the pad rows are zeroed — this is what makes
the second control fail rather than silently pass.
The test pins
VLLM_TEST_FORCE_FP8_MARLIN=1. Marlin is the fp8 MoE backendthat supports non-gated MoE; the Blackwell default
trtllm_fp8_block_scale_moehardcodes the gated layout and rejects asingle-shard w13 (
gemm1_weights_scale.size(1) == intermediate_size_factor * intermediate_size / 128, 1 vs 2). That is a separate gap, not addressed here.The 13 skips are pre-existing shape/group-divisibility guards in
test_gptq_marlin_padded_round_trip/test_marlin_moe_padded_intermediate("group straddles the boundary", "group must divide the rank-local K") and
skip identically on upstream
main; none are in tests added or modified bythis PR.
Additional validation: a functionally equivalent backport of both fixes to
v0.20.0 was validated on GB200:
(Nano-30B-A3B, Super-120B-A12B at TP=4 exercising the 672->704 pad,
Ultra-550B-A55B at TP=8/PP=2).
a bf16 reference reaches the same agreement as an already-tile-aligned
control (cosine similarity 0.999180 padded vs 0.999174 control, i.e. the
padding contributes no error beyond MXFP8 quantization noise). Since the
change only enables previously-crashing configurations and gated layouts are
byte-identical, this parity evidence stands in for a model-quality eval.
AI assistance disclosure
This PR was developed with AI assistance (Claude Code), including an
adversarial multi-agent review pass. Every changed line has been reviewed by
the human submitter, who ran the tests and can defend the change end-to-end.