Repository navigation
perf(linear): run nvfp4 linears on nvfp4 activations on sm_120 - #737
Merged
Merged
Conversation
jason-fxz
added this pull request to stack #740
October 19, 2026 03:09
jason-fxz
force-pushed
the
perf/nvfp4-skinny-linear
branch
from
October 19, 2026 03:09
aaa9de3 to
23f3212
Compare
jason-fxz
force-pushed
the
perf/nvfp4-skinny-linear
branch
from
October 19, 2026 03:10
23f3212 to
e8273b0
Compare
jason-fxz
force-pushed
the
perf/nvfp4-skinny-linear
branch
from
October 19, 2026 05:59
ac5357c to
f1abf0b
Compare
jason-fxz
force-pushed
the
perf/nvfp4-skinny-linear
branch
from
October 19, 2026 06:17
f1abf0b to
1f886fe
Compare
pretor
pushed a commit
to pretor/FreeToken
that referenced
this pull request
Oct 19, 2026
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.
NVFP4 linears on sm_120 for checkpoints whose linears carry static activation scales (ModelOpt
NVFP4, compressed-tensors NVFP4 withdynamic: localinputs): prefill runs W4A4 GEMMs on NVFP4 activations, decode-sized batches run new W4A16 kernels on the same row-major weight. One weight copy, no repack; W4A16 checkpoints are unchanged.Changes
b12x(FlashInferfp4_quantize+mm_fp4(backend="b12x"), picked when flashinfer is installed) andcublas(our activation quant + torchscaled_mm, cuBLASLt). Both keep the checkpoint's row-major[N, K/2]codes with 128x4-swizzled block scales and take one weight global per tensor: a fused weight uses its largest per-part global with the block scales unchanged, as vLLM does, and a load-time warning names the case when the parts differ by more than 5%.--quant-backend linear.nvfp4=tritonkeeps main's W4A16 with each part's global.kernel/gluon/nvfp4/linear/sm120_gemv.py: W4A16 kernels for small batches on that weight.gemv_fma(CUDA cores, a warp owns whole rows,cvt.rn.f16x2.e2m1x2decode, T = 1-2) andgemv_mma(bf16mma.sync, weight rows on M and tokens on N, each warp a K slice; a fixed permutation of k inside each 128-k block makes plain 16-byte code loads and 64-byte activation loads the mma fragments). K is a constexpr and the K loop is unrolled.gemv_configpicks the kernel and tile. b12x routes T <= 8 to them, cublas T <= 16.scaled_mm/mm_fp4take.gemv_configtile against a dequant reference, and the swizzled scale layout. b12x refuses N % 8 != 0 (flashinfer returns wrong output there) and checks flashinfer's own CUDA toolkit version.Cache policy: the activation loads use
evict_last; the weight keeps the default policy.evict_firston the weight made these kernels faster alone but, in a model step, left earlier dirty L2 lines (GDN state) for the next layer's attention-side kernels to write back (nsys: FP8 GEMMs / GDN update / attention +12%, net slower; a CUDA microbench with ncu DRAM byte counts shows the same write-back moving from the stream to the kernels after it).Results
Against main's W4A16 linear (
--quant-backend linear.nvfp4=triton, the same code as main). torch 2.14.1+cu130, Triton 3.8.0, flashinfer 0.6.18.End to end (
LLM, CUDA graphs, naive cache; TTFT at ~1k / 4k / 8k prompt tokens; decode tok/s over 256 tokens at bs concurrent requests, median of 3):RTX PRO 6000 (driver 595.91)
* radix cache with unique prompts: Gemma-4-31B cannot allocate its KV pool at bs=16 under the naive cache.
RTX 5090 (driver 580.105.08), Qwen3.8-27B-NVFP4 (Gemma-4-31B does not fit):
Decode linears: each model's NVFP4 linears stacked to ~2 GB of weights (every layer reads from DRAM), one CUDA graph, through
finalize+apply; speedup over main, b12x / cublas:The Muse-Glimmer-30B rows were measured with the gate fused into
qkvg_proj.From T = 48 main's W4A16 drops a tier and both W4A4 kernels run 1.6-2.8x faster. cublas trails main from T ~ 17 to ~ 40 (cuBLAS runs one 128x128 tile shape at every M): 0.71-0.91x at T = 32 above, -2.0% / -0.6% at bs=16 end to end.
In that measurement the small-batch kernels read the weight at 87-96% of the DRAM read bandwidth for T <= 8 (RTX PRO 6000, 1530 GB/s measured), at or above ninfer's NVFP4 linear in the same measurement (Qwen3.8-27B MLP, best of its A16 / A4 paths: 102.7 / 105.4 / 108.3 vs 104.5 / 108.7 / 116.3 us per layer at T = 1 / 4 / 8).
Test plan
pytest tests/kernels/test_nvfp4_w4a4_linear.pyon RTX PRO 6000 (and on RTX 5090 before the per-part global change): 56 passed, 20 skipped (N = 12, refused by both kernels).pytest tests/moe/test_nvfp4_backends.py tests/models/test_quant_config.py tests/kernels/test_e4m3_compat.py: 5 failures intest_quant_config.py, the same at the base commit (they come from the local HF cache).linear.nvfp4=triton) on a three-sentence prompt.