Repository navigation
perf(moe): speed up the nvfp4 moe prefill gemm on rtx gpus - #733
Merged
Merged
Conversation
pretor
pushed a commit
to pretor/FreeToken
that referenced
this pull request
Oct 18, 2026
…l), PR FlashML-org#734 (bench hybrid fetch), PR FlashML-org#732, PR FlashML-org#731
pretor
pushed a commit
to pretor/FreeToken
that referenced
this pull request
Oct 18, 2026
…optimizations and benchmark results
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prefill path of the inline-dequant NVFP4 MoE (
fused_experts_nvfp4). Same banks and layout, no repack; decode is unchanged.Changes
kernel/nvfp4_moe.py(new): the NVFP4 MoE GEMM dispatch (kernel choice, tiles, switches, launch), moved out ofmoe/fused_nvfp4.py, which keeps only the layer flow (align -> gate_up -> act -> down -> sum). The kernels are templates; every tile and switch comes from the dispatch.mul.rn.bf16x2(fma.rn.bf16x2before sm_90), bitwise equal to the old decode; the decode form is picked per device and tokens per expert (sm_89 keeps the pair decode below 48 tokens per expert);kernel/gluon/(new): Gluon kernels that replace a Triton kernel on a given GPU when Triton 3.8 is present;FREETOKEN_DISABLE_GLUON=1turns them off.gluon/nvfp4/moe/sm120.py: warp-specialized prefill GEMM for sm_120 (TMA loader warp, decode warps, mma.sync warps); aPERSISTENTconstexpr runs BLOCK_SIZE_M 64 tiles persistently with a third pipeline stage. The dispatch routes BLOCK_SIZE_M 64 / 128 to it on sm_120.Results
Measured against main at 6b8f200 (the branch now sits on 31c6ef4; #732 only touches GGUF). torch 2.14.1+cu130, Triton 3.8.0.
Kernel:
fused_experts_nvfp4(align + gate_up + act + down + sum) with random banks in 6 checkpoint shapes (Qwen3.6-35B-A3B, Qwen3.5-122B-A10B, Qwen3.8-Flash, Gemma-4-26B-A4B, MiniMax-M2.5, GLM-4.7) x T in {16, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384};triton.testing.do_benchmedian, main / PR / main / PR. Speedup over main, geomean (min - max), by tokens per expert:Gluon at T = 8192 (
fused_experts_nvfp4, one run with main, the PR withFREETOKEN_DISABLE_GLUON=1(Triton only) and the PR interleaved, two rounds; Gluon / Triton and PR / main are speedups, i.e. the Triton-only and main times over the PR time):RTX PRO 6000:
RTX 5090:
PR MFU: useful GEMM FLOPs (2 x T x top_k x 3 x hidden x moe_intermediate) over the whole layer time, against the bf16 mma.sync peak with fp32 accumulate at the rated boost clock: RTX PRO 6000 188 SMs x 2.43 GHz x 1024 FLOP/clk = 468 TFLOP/s, RTX 5090 170 SMs x 2.41 GHz x 512 FLOP/clk = 210 TFLOP/s (its cuBLAS bf16 GEMM reaches 219 - 227 TFLOP/s, so the card clocks above the rated boost). The RTX PRO 6000 runs BLOCK_SIZE_M 128 tiles here and the RTX 5090 BLOCK_SIZE_M 64 (GeForce cards run fp32-accumulate mma at half rate, so the decode the warp specialization hides is a smaller share).
On sm_120 the output differs only where the Gluon kernel runs (it accumulates in a different order). Decode (
fused_experts_decode_nvfp4_marlin, M = 1 - 16, CUDA graph replay) is 1.00x and bitwise equal on all four GPUs.End to end, RTX PRO 6000,
nvidia/Qwen3.6-35B-A3B-NVFP4, time to first token of fresh random-word prompts (max_tokens 1), median over 2 server starts per tree (3 prompts each; 6 each for ~1k / ~2k):A greedy 128-token answer is identical across server starts for each tree; main and PR diverge after ~375 characters (one word choice, both coherent).
Host time: on sm_120 the PR adds host time per MoE layer: ~40 us for the TMA descriptors on the Triton path (BLOCK_SIZE_M 16 / 32) and ~55 us for the Gluon launch. A ~1k-token prefill of Qwen3.6-35B-A3B is host-bound, so it gets slower even though its MoE GPU time drops from 21.4 to 16.5 ms (nsys); with TMA off it is within 1% of main. On a busy host, small layers (Qwen3.6-35B-A3B shapes, T <= 512) also measure host-bound in the kernel A/B on the RTX 5090 now and then.
Tests
pytest tests/moe/test_nvfp4_backends.py tests/kernels/test_e4m3_compat.py tests/moe/test_fused_moe.py, with Gluon on and withFREETOKEN_DISABLE_GLUON=1:test_e4m3_compat.py::test_forced_emu_matches_native(blk_aq_y) fails the same way on main on this GPU.Related: #578 also replaces the table decode of the Triton prefill kernel; it predates #723.