Repository navigation
perf(kernel): rewrite the triton nvfp4 moe prefill gemm - #723
Conversation
GeForce runs fp32-accumulate tensor MMA at half rate, so RTX parts want smaller BLOCK_SIZE_M than H100 and their own BLOCK_N / BLOCK_KB / stages. Tables come from tile sweeps on RTX PRO 6000, RTX 5090, RTX 4090 and RTX 3080 Ti; H100-class GPUs and AMD keep the previous tuning.
RTX results and per-GPU tuning (c4acce3)The kernel table and the tile sweep were rerun on four RTX GPUs. The kernel is a clear win on all of them, but Tested on
torch 2.14.1+cu130, Triton 3.8.0. Same method as the PR: main -> this PR (6b4086c), kernel table shapes, T = 128 to 32768
The relative error against the fp32 reference is the same on both sides on every GPU (0.0039). The 3080 Ti runs the emulated e4m3 path. Why RTX needs other tilesGeForce runs bf16 MMA with fp32 accumulate at half rate. Per SM per clock, bf16 tensor over fp32 ALU throughput (rho) is 16 on H100, 4 on RTX PRO / RTX Ada / A10 / L40S, and 2 on GeForce RTX 30/40/50 (NVIDIA whitepapers; measured 4.5 / 2.3 / 1.9 / 1.6 on the four GPUs above). Each M block decodes its weight tile once on the ALUs, and the MMA also runs on the padding up to Tile sweep144 configs (BLOCK_M 16..128, BLOCK_N 32..128, BLOCK_KB 32/64, 4/8 warps, 2..4 stages) on the six shapes at T = 256, 512, 1024, 2048, 4096 and 16384. Loss against the best config at each point, mean (max):
Limiting the SM count with a green context (24 to 188 SMs on the RTX PRO 6000) does not change the best tile, so the SM count is not an input. c4acce3
this PR -> c4acce3Geometric mean over the six shapes at T = 128, 256, 512, 1024, 2048, 4096 and 16384:
The RTX PRO 6000 numbers use the second round only; another process used the GPU during the first round. The slower points depend on the model shape: GLM-4.7 at T = 2048 (best Other checks
|
Upstream 0bca88f..11c1adb: GGUF on vendored ggml (FlashML-org#690 and the per-arch loaders), MoE slot pools (FlashML-org#689, FlashML-org#700), GGUF experts on the cpu/hybrid executor (FlashML-org#704), the NVFP4 MoE prefill GEMM rewrite (FlashML-org#723), triton decode split-k by context length (FlashML-org#729), graph upload at capture (FlashML-org#697), transformers 5.18. Resolution: - GGUF: upstream's stack replaces ours (dense llama/qwen2/qwen3, qwen3moe, qwen35moe loaders, q4_0/gguf expert banks, our ggml kernels and their tests). Re-ported on top: rope-only --hf-overrides (YaRN) for the llama/qwen2/qwen3/qwen3moe GGUF parsers, and the scalar head_count_kv of Gemma-4 GGUFs (upstream PR FlashML-org#190). Gemma-4 E2B/E4B GGUF is not re-ported. - offload cache: upstream's pooled cache, with our disk tier (one pool only), fill_slots preload per pool, sync-free prefill invalidation for a contiguous staging range, hit-D2D-by-default wording and the HIP graph per-bank copy. - NVFP4 prefill: upstream's GEMM and per-GPU tuning supersede our sm_89 tile config and JOIN_B. - triton decode attention: upstream's split-k over our fp8/nvfp4 KV paths and HIP head padding; DFlash verify graphs are uploaded at capture too. - embedding: upstream's embedding quant method, with --embed-device cpu in front and the tied head's kernel choice kept for unquantized tables. - engine: DFlash, --kv-reserve-share, shrink-first rebuild, startup hybrid split and the WSL pin probe combined with the slot pools. GPU-checked on the RTX 4090: full suite green; Qwen3.6-35B-A3B NVFP4 prefill ~13.2k / decode ~131 tok/s (baseline 13.6k / 131), Qwen3-30B-A3B Q4_K_M GGUF 14.6k / 259 (was 8.3k / 225). Assisted-by: Claude Opus 5.5
…r RTX), PR FlashML-org#729 (split decode attention), PR FlashML-org#726, PR FlashML-org#700 and GGUF updates
Summary
NVFP4 MoE prefill goes
TritonNvfp4MoEKernel.apply(theautochoice) ->fused_experts_nvfp4->_prefill_nvfp4_moe_kernel, in both--moe-strategy fusedandoffload. On H100 that kernel ran at 16 to 18 TFLOP/s on long prompts, so prefill of Gemma 4 26B-A4B NVFP4 stayed near 5k tok/s.This PR rewrites the kernel. It is 6.6x to 16.1x faster across six NVFP4 MoE shapes at 128 to 32k tokens, with the same error. Gemma 4 prefill goes from 5,571 to 35,732 tok/s at 4.6k tokens with
fused, and from 4,783 to 16,400 tok/s at 37.8k tokens. Decode does not use this kernel and is unchanged.Changes
kernel/triton/nvfp4_fused_moe.py,_prefill_nvfp4_moe_kernel:_e2m1_f16_x2p14) instead of a lookup-table gather. The decoded value times its e4m3 block scale is exact in bf16 and fp16.tl.dotcovers the step. The old kernel issued two dots on even and odd activation columns loaded with stride 2.e4m3_native_cx()) are kept.moe/fused_nvfp4.py:_prefill_config(M, top_k, num_experts)picksBLOCK_SIZE_Mfrom the mean tokens per expert: 128 at 64 or more, 64 at 24, 32 at 12, else 16. It uses BLOCK_N 64, 4 warps and 3 stages._prefill_gemmstops passing it.Tested on
NVIDIA H100 80GB HBM3, Intel Xeon Platinum 8480+, driver 580.95.05, CUDA 13.0, torch 2.14.1+cu130, Triton 3.8.0.
End-to-end results
Model: nvidia/Gemma-4-26B-A4B-NVFP4.
/v1/chat/completionsrequest with a 4,582-token or 37,811-token prompt and 8 output tokens. Prefill tok/s is prompt tokens / TTFT. Median of 3 runs.With greedy sampling and a 60-token prompt, the first 128 output tokens match main token for token, with both
fusedandoffload.Kernel results
fused_experts_nvfp4alone with random weights and routing, bf16 activations, one CUDA graph replay after an L2 flush, median ms (main -> this PR).BLOCK_SIZE_Mcomes from an H100 sweep over 96 configurations (BLOCK_M, BLOCK_N, BLOCK_KB, warps, stages) on these six shapes at T = 256, 1024, 4096 and 16384. At every point, the best configuration uses the same BLOCK_M as the heuristic, and the heuristic is within 3.3% of the best.Other checks
PYTHONPATH=python pytest tests/moe/test_nvfp4_backends.py tests/kernels/test_e4m3_compat.py: 13 passed, 7 skipped (vllm not installed, b12x needs sm_120, ROCm only). Theslowcompile gates for sm_80, 86, 89 and 120 intest_e4m3_compat.pyare included.