Repository navigation
Conversation
1be0c19 to
ce60d7c
Compare
… DSL Add inference_moe_nvfp4_per_token: routed experts run as NVFP4 weights x per-token NVFP4 activations on FlashInfer's CuTe DSL grouped GEMMs. CUDA-graph capturable and batch invariant. Bumps flashinfer-python to <0.8.0 (0.7.0.post1) for the per-token Relu2 kernels. Signed-off-by: YigongQin <yigongq@nvidia.com>
ce60d7c to
fab9c13
Compare
Signed-off-by: YigongQin <yigongq@nvidia.com>
|
/ok to test 466c868 |
Signed-off-by: YigongQin <yigongq@nvidia.com>
| "av", | ||
| "Pillow", | ||
| "flashinfer-python>=0.5.0,<0.7.0", | ||
| "flashinfer-python>=0.5.0,<0.8.0", |
There was a problem hiding this comment.
What's the version in CI?
There was a problem hiding this comment.
CI will use the updated version
|
|
||
| torch.testing.assert_close(inference_output, training_output, atol=0, rtol=0) | ||
|
|
||
| @staticmethod |
There was a problem hiding this comment.
have we added launch on gb200 flag?
| num_experts=self.num_local_experts * self.ep_group.size(), | ||
| local_expert_offset=self.ep_group.rank() * self.num_local_experts, | ||
| activation_type=self._flashinfer_activation_type.value, | ||
| out=(NVLSAllGatherVDispatcher._get_rsv_tensor() if self._nvls_dispatcher else None), |
There was a problem hiding this comment.
At EP>1 we quantize and run the whole max-size NVLS buffer? I think the bounded work is useful here?
There was a problem hiding this comment.
trying to reuse self._flashinfer_mxfp8_token_capacity and bounded rows
| weights.fc2_scale, | ||
| weights.fc2_alpha, | ||
| per_token_scale=per_token_scale, | ||
| )[:, : weights.hidden_size] |
There was a problem hiding this comment.
can we add a contiguous call for safety here. will be no op if aligned
| func = self.config.activation_func | ||
| if func == F.silu: | ||
| return ActivationType.Silu | ||
| return ActivationType.Swiglu if self.config.gated_linear_unit else ActivationType.Silu |
There was a problem hiding this comment.
was stuck on this line for a while. It'ssafe because config validation blocks gated + FlashInfer without NVFP4? if so could we just put like a small note
There was a problem hiding this comment.
added one comment on gated with non-nvfp4 flashinfer are rejected
|
nice work! can we have the system error if it tries to refit something with fp4? |
Signed-off-by: YigongQin <yigongq@nvidia.com>
added not implement error in refit.py, thank you |
What does this PR do?
Adds a per-token NVFP4 x NVFP4 routed-MoE path to Megatron inference for squared-ReLU (Nemotron-family) and SwiGLU experts, running on FlashInfer's CuTe DSL block-scaled grouped GEMMs. It is CUDA-graph capturable and batch invariant, and comes with the same test coverage the torch MXFP8 path has: a bitwise quantizer reference, a BF16 accuracy check, CUDA-graph replay, batch invariance, and a train-gen parity test against TE NVFP4 training. The NVFP4 weight path, CUDA-graph-stable refresh, batch-invariant finalize and tests are shared by FlashInfer's other NVFP4-weight recipes (per-tensor NVFP4 x NVFP4 and BF16 x NVFP4), so adding those later only changes the activation side; see "Strength: one integration for every NVFP4-weight recipe" and "How to extend".
Background
Recipe. Weights are NVFP4 with 1x16 E4M3 block scales and one FP32 global scale per expert. Activations are quantized online with one FP32 scale per token instead of a static, calibrated per-tensor input scale. That removes the calibration step and the activation-outlier sensitivity of a per-tensor scale, which matters in RL because the policy weights change every step.
Kernel level (FlashInfer). All kernels already exist upstream; this PR only integrates them.
nvfp4_quantize(..., per_token_activation=True)) and the per-token TRT-LLM-gen MoE.CuteDslMoEWrapper.run(..., per_token_scale=...)), the backend this PR uses.use_fused_finalize=False) to the CuTe DSL NVFP4 MoE, which batch-invariant mode uses.CuteDslMoEWrapper,quant_mode="w4a4") MoE backends.Engine level (peers of Megatron inference).
SGLANG_FLASHINFER_NVFP4_PER_TOKEN_ACTIVATION, which replaces the checkpoint's static input scales with 1 so FlashInfer computes per-token scales at runtime.RL level.
NVFP4Quantizerproducing the checkpoint and SGLang (FlashInfer TRT-LLM routed MoE) serving it.What Transformer Engine already has. TE's
NVFP4BlockScalingrecipe supportsrow_scaled_activation=True(one scale per token), and withdisable_2d_quantization=True,disable_rht=Trueanddisable_stochastic_rounding=Trueit is the training-side form of this recipe. That is what NeMo-RL#3566 trains with, and it is the reference for the train-gen parity test below, in the same way TE MXFP8 is the reference for the MXFP8 inference path (#8195). TE has no inference MoE kernel for this recipe, so inference uses FlashInfer.What is added
New config field.
inference_moe_nvfp4_recipe: Optional[Literal['per_token']] = NoneinTransformerConfig(--inference-moe-nvfp4-recipe per_token). It is a recipe choice rather than a bool so the other NVFP4 recipes (see "How to extend") can be added as new values. It requirestransformer_impl='inference_optimized',inference_grouped_gemm_backend='flashinfer', non-gatedsquared_reluexperts or gatedF.silu(SwiGLU,gated_linear_unit=True) experts, withoutactivation_func_tanh_clamp_scaleand, for SwiGLU, withoutactivation_func_clamp_value,glu_linear_offsetormoe_mlp_glu_interleave_size, BF16 expert parameters (nofp8/fp4_param), and FlashInfer >= 0.7.0 on SM100/SM103. Validation rejects every other combination with a specific error.batch_invariant_modeis allowed with this recipe (with the NVLS dispatcher when EP > 1), as it is for FlashInfer MXFP8.megatron/core/inference/moe/flashinfer_nvfp4.py(new).prepare_routed_nvfp4_weightsquantizes the BF16 FC1/FC2 expert stacks into the CuTe DSL layout: packed E2M1, 128x4-swizzled E4M3 block scales plus their MMA view, and per-expert FP32 alphas. SwiGLU is detected from the shapes (FC1 has twice FC2's input width): MCore's[gate; up]FC1 is padded per half and reordered into FlashInfer's alternating 64-row[up, gate]blocks. Without=it refreshes the existing buffers in place, so addresses captured by CUDA graphs stay valid across refits.quantize_routed_nvfp4_inputpads and quantizes activations per token. The global scale is the recipe constant1 / (448 * 6)passed as a Python float, so there is no host sync and it is graph-safe.flashinfer_routed_nvfp4_moerunsCuteDslMoEWrapper. Wrappers are cached per shape and created outside graph capture. Undertorch.use_deterministic_algorithmsor batch-invariant mode it uses the two-stage finalize, which reduces each token's top-k routes in a fixed order, instead of the fused atomic finalize. With the NVLS dispatcher it writes straight into the reduce-scatter buffer, soordered_reduce_scatter_vkeeps the cross-rank reduction order fixed.InferenceGroupedMLP(experts.py). The BF16 expert stacks stay resident as the canonical weights and refit destination. The NVFP4 copy is built on the first inference forward, and the FlashInfer forward dispatches to it.refresh_flashinfer_nvfp4_weights()requantizes it in place.Not in this PR: refit and RL. Nothing calls
refresh_flashinfer_nvfp4_weights()after a refit or an optimizer step yet, so don't setinference_moe_nvfp4_recipefor RL rollouts until the follow-up PR (TODO: link) lands. That PR wires the refresh intomegatron/core/resharding/refit.pyandmegatron/rl/rl_utils.py; it is split out to keep the refit and RL reviewer groups off this PR. Static inference from a loaded checkpoint is unaffected.moe_layer.py. The NVFP4 path checks for the FlashInfer per-token APIs instead offlashinfer-jit-cache, because CuTe DSL kernels are JIT-compiled throughnvidia-cutlass-dslrather than shipped in the jit-cache wheel.Dependencies.
flashinfer-pythonis relaxed from<0.7.0to<0.8.0anduv.lockmoves to 0.7.0.post1, which addsnccl-extensions.nvidia-cutlass-dslis already a dependency offlashinfer-python.Why CuTe DSL and not TRT-LLM-gen
Both FlashInfer backends support per-token NVFP4 with ReLU2 and SwiGLU on SM100/SM103. An earlier revision of this PR supported both; we kept only CuTe DSL:
~/.cache/flashinfer/cubinsunlessflashinfer-cubinis installed). On our test node that download failed with "Disk quota exceeded" and the GEMM then failed at 128 and 256 tokens. CuTe DSL compiles locally throughnvidia-cutlass-dsl.use_fused_finalize(flashinfer#3976), which gives a fixed-order top-k reduction for batch-invariant mode. Make args and kwargs optional positional arguments for the Module hooks. #3976 measures its cost at about 1.5-14% over the atomic finalize, highest at 1-4 tokens.bench_moe_deepseek.py --use-per-token-activation, DeepSeek-V3 shapes, B200, CUDA graphs) has TRT-LLM-gen about 12% faster at 128-256 tokens and CuTe DSL 1.12-1.29x faster from 512 to 4096 tokens, a 1.08x geomean in CuTe DSL's favor. Our earlier Nemotron-shape runs on GB300 showed the same pattern.One backend also halves the test matrix and removes a config field.
Performance
MoE layer forward (router, dispatcher, experts and combine) under CUDA-graph replay, on one GB300 with EP = 1. The shapes are the DeepSeek-V3 shapes that FlashInfer uses for its per-token CuTe DSL vs TRT-LLM-gen comparison in flashinfer#3645: hidden 7168, intermediate 2048, 256 experts (all local) and top-8. There are two differences from #3645. The experts are squared-ReLU, Nemotron's activation, where #3645 uses SwiGLU. TODO: add the SwiGLU numbers (
bench_moe_layer.py --activation swiglu). Routing is plain top-8 without group-limited routing, because Megatron's inference router does not support it. All three backends load the same BF16 weights. Times are in µs, the mean over 50 replays.Batch-invariant mode uses the
te_nativebackend for the non-MoE GEMMs (router) in all three columns.Known gap with EP > 1. With the NVLS dispatcher, the expert input buffer is sized for the largest batch across all ranks. The vLLM and MXFP8 paths process only the valid rows (vLLM reads a device-side
valid_tokenscount; FlashInfer MXFP8 takesinference_flashinfer_mxfp8_token_capacity), but the NVFP4 path quantizes and routes the whole buffer. On Nemotron-3 Super shapes at EP = 4 with an 8192-token buffer, this gives NVFP4 a floor of about 820 µs per layer, against 80 µs for BF16 at 1 token, although NVFP4 is still 1.4x faster than BF16 at 8192 tokens per rank. TODO: bound the NVFP4 rows the same way before marking this PR ready.Requantizing a layer's expert weights for a refit takes 16.7 ms for 32 Nemotron-3 Super experts.
Strength: one integration for every NVFP4-weight recipe
FlashInfer's
CuteDslMoEWrapperruns three NVFP4-weight recipes on the same expert-weight layout: per-token NVFP4 x NVFP4 (this PR), per-tensor NVFP4 x NVFP4, and BF16 x NVFP4. FlashInfer's own tests (tests/moe/test_cute_dsl_fused_moe.py, casesw4a4-per-token,w4a4-per-tensor,w4a16andw4a16-relu2) feed identical weight tensors to all three. Everything this PR builds is therefore shared, and the other two recipes only differ in how activations enter the kernel:prepare_routed_nvfp4_weights(BF16 -> packed E2M1 with swizzled E4M3 block scales, padded to 256) and its in-place, CUDA-graph-stable refresh after refit.InferenceGroupedMLP, and the config validation.inference_moe_nvfp4_recipeis a recipe choice rather than a bool for this reason: each variant below is a new value, not a new flag.How to extend
'per_token''per_tensor''weight_only'quant_modew4a4w4a4w4a16nvfp4_quantize(x, 1/(448*6), per_token_activation=True)nvfp4_quantize(x, a1_scale)with a fixed scale tensorx_sfNoneper_token_scaleNoneNonefc1_alpha/fc2_alpha1 / w_scale1 / (w_scale * a1_scale)/1 / (w_scale * a2_scale)1 / w_scalefc2_input_scale1/(448*6)a2_scaleNonew_scaleis the per-expert weight global scale thatprepare_routed_nvfp4_weightsalready computes.a1_scale/a2_scaleare the static FC1 / FC2 input scales.Per-tensor NVFP4 x NVFP4 (
'per_tensor')'per_tensor'to theLiteral, plus a way to supply the calibrated FC1 and FC2 input scales per layer (from a ModelOpt checkpoint'sinput_scale, or a calibration pass).flashinfer_nvfp4.py:quant_modean argument of_get_cute_dsl_wrapperand part of its cache key; it staysw4a4here.nvfp4_quantize(padded_hidden, a1_scale)withoutper_token_activation, and passper_token_scale=None.prepare_routed_nvfp4_weights, fold the activation scales intofc1_alpha/fc2_alphaand storea2_scaleinfc2_input_scale. The packed weights and block scales don't change.a1_scale/a2_scalein fixed device tensors, and confirm the tensor-scale quantizer path triggers no host sync under CUDA graphs.'per_tensor'to the recipe parametrization. The quantizer reference test gains a per-tensor case. The train-gen parity test doesn't apply: TE's per-tensor NVFP4 computes the activation scale from each tensor's amax at runtime, so it has no static-scale counterpart.BF16 x NVFP4 (
'weight_only')'weight_only'to theLiteral. No calibration and no new inputs are needed.flashinfer_nvfp4.py:quant_mode="w4a16".padded_hidden_sizeand pass them asx, withx_sf,fc2_input_scaleandper_token_scaleallNone.fc1_alpha,fc2_alphaand the refresh don't change.'weight_only'to the recipe parametrization. The quantizer reference test doesn't apply because there's no activation quantizer. The BF16 reference tolerance can be tighter, since only weights are quantized.Both variants inherit the rest unchanged: batch invariance through the two-stage finalize (
w4a16supports it too), CUDA-graph capture, NVLS, refit refresh, and support for squared-ReLU and SwiGLU experts.What is tested
The tests mirror the torch MXFP8 coverage from #7302 (batch invariance) and #8195 (train-gen parity) and sit next to those tests. GPU tests skip unless FlashInfer >= 0.7.0 and SM100/SM103 are available; kernel tests are marked
launch_on_gb200. Results below are from a GB300 4-GPU node (torch 2.11, FlashInfer 0.7.0, TE 2.18).Run all commands from the repository root. Every NVFP4 kernel, layer, batch-invariance, NVLS and parity test is parametrized over squared-ReLU and SwiGLU.
1. Correctness
tests/unit_tests/inference/test_flashinfer_nvfp4.pytest_per_token_nvfp4_quantize_matches_reference): FlashInfer's per-token quantizer matches a pure-torch reference of the recipe exactly (per-token scales, block-scale bytes and E2M1 codes), including an all-zero row and logspace-spread magnitudes. The reference mirrors TE's NVFP4 quantizer. It runs withFLASHINFER_DISABLE_FP4_QUANT_FAST_MATH=1; production keeps FlashInfer's default fast math.test_gated_fc1_is_padded_per_half_and_interleaved_up_gate, CPU): the SwiGLU FC1 reordering matches FlashInfer's interleaved[up, gate]layout exactly, including per-half padding.test_flashinfer_routed_nvfp4_moe_matches_bf16_reference): the fused MoE stays within the expected NVFP4 error of a BF16 squared-ReLU or SwiGLU MoE (per-token cosine mean > 0.97, minimum > 0.95), including non-256-aligned shapes that exercise padding. Like MXFP8, the MoE itself is checked against BF16, not an emulated reference.TestNVFP4MoELayer, NCCL and NVLS dispatchers): aninference_optimizedMoE layer matches the BF16 training layer; refreshing after a BF16 weight update tracks the update; CUDA-graph replay follows new inputs and refreshed weights.Result: 57 passed (squared ReLU and SwiGLU).
2. CUDA-graph capture and bitwise replay
tests/unit_tests/determinism/kernels/test_inference_kernels.py(next totest_mxfp8_swiglu_moe_replays):test_per_token_nvfp4_quantize_replaysandtest_flashinfer_nvfp4_moe_replays.tests/unit_tests/determinism/kernels/test_moe_kernels.py:TestMoEModules::test_inference_nvfp4_moe_layer_replaysreplays the whole MoE layer.The new kernels are registered in
tests/unit_tests/determinism/kernels/manifest.pyasinference_flashinfer_nvfp4_moe, andtest_manifest.pychecks the registration.uv run python -m torch.distributed.run --nproc-per-node 4 -m pytest -q \ tests/unit_tests/determinism/kernels/test_inference_kernels.py -k "nvfp4 or mxfp8" uv run python -m torch.distributed.run --nproc-per-node 4 -m pytest -q \ tests/unit_tests/determinism/kernels/test_moe_kernels.py -k nvfp4 uv run python -m torch.distributed.run --nproc-per-node 4 -m pytest -q \ tests/unit_tests/determinism/kernels/test_manifest.pyResults: 9 passed (NVFP4 together with the existing MXFP8 cases), 2 passed, 269 passed.
3. Batch invariance
test_inference_kernels.py::test_flashinfer_nvfp4_moe_is_batch_invariant, next totest_torch_mxfp8_moe_is_batch_invariant): a target batch gives bitwise identical output alone and inside batches of 193 and 1024 tokens, where the co-batched tokens are 100x larger and routed to the same experts.test_flashinfer_nvfp4.py::test_flashinfer_routed_nvfp4_is_batch_invariant): the same check across local expert offsets, in deterministic and batch-invariant modes.test_moe_dispatching_and_routing.py::TestNVLSAllGatherVDispatcher::test_cuda_graph_batch_invariant_nvfp4_moe_layer_uses_ordered_rsv, next to the MXFP8 equivalent): with EP = 4 the layer writes into the reduce-scatter buffer, reduces withordered_reduce_scatter_v, and CUDA-graph replay equals eager execution bitwise.These run with the commands in sections 1 and 2, plus:
uv run python -m torch.distributed.run --nproc-per-node 4 -m pytest -q \ tests/unit_tests/inference/test_moe_dispatching_and_routing.py \ -k "nvfp4 or test_batch_invariant_moe_matches_training"4. Train-gen parity (expected failure for now)
test_moe_dispatching_and_routing.py::TestNVLSAllGatherVDispatcher::test_batch_invariant_nvfp4_moe_matches_training, next to the MXFP8test_batch_invariant_moe_matches_trainingfrom #8195. It loads the same weights into a TE training MoE layer running NVFP4 with row-scaled activations (fp4="e2m1"with the recipe from the Background section) and into the batch-invariant inference layer, then compares outputs.TE does not match bitwise yet. The test asserts that generation is much closer to TE NVFP4 training than to BF16 (relative error less than half of the error against BF16), then calls
pytest.xfailwhen the outputs are not bitwise equal. It measures about 0.03 relative error against TE NVFP4 training versus about 0.20 against BF16 for squared ReLU (maximum absolute difference about 0.0018), and about 0.035 versus 0.23 for SwiGLU (about 0.0008). The test reports XFAIL, which does not fail CI, and will start passing once the kernels are aligned. The known gaps are:FLASHINFER_DISABLE_FP4_QUANT_FAST_MATHonly affects the standalone quantizer, so this step can't be made IEEE-exact today.Command: the
test_moe_dispatching_and_routing.pycommand in section 3.Result: 6 passed, 4 skipped, 2 xfailed (the parity test for squared ReLU and SwiGLU). The skips are pre-existing MXFP8 parity cases that need DeepGEMM BF16 or cuBLASLt 13.5.1.
5. Regressions
Results: 28 passed for
test_flashinfer_mxfp8.py; 61 passed, 5 skipped and 2 xfailed (the parity test above) for the dispatching file. The skips are pre-existing torch and MXFP8 cases that need DeepGEMM BF16 or cuBLASLt 13.5.1.Contribution process
Pre-checks
jit_fuser/torch.compile, CUDA extension, TE or external-library dispatch, or a scatter/index accumulation), I have added or updated its bit-exact determinism test and registered it intests/unit_tests/determinism/kernels/manifest.py(guide)