Repository navigation
[Common, PyTorch] Fuse NVFP4 scale swizzling on SM120 and SM121 - #3749
Open
kimchi-developer wants to merge 1 commit into
Open
kimchi-developer wants to merge 1 commit into
kimchi-developer wants to merge 1 commit into
Conversation
On SM120/SM121 every NVFP4 quantization with optimize_for_gemm wrote compact scales and then ran the standalone swizzle kernel: the 2D in-kernel path was gated to SM100/110, and the SM12x RHT path rejected swizzled output. Write GEMM-swizzled scales from the generic 1D, columnwise-RHT 1D and tuned 1D kernels, enable the existing 2D path on SM120/SM121, and report 128x128 alignment for these paths in gemm_swizzled_scales_alignment so eager allocation and torch.compile metadata agree. Aligned groups of four scales are written with one 32-bit store. Row-scaled NVFP4 and 4over6 keep compact scales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: ceo <ceo@logictree.app>
7 of 13 tasks
kimchi-developer
marked this pull request as ready for review
October 27, 2026 07:44
kimchi-developer
requested review from
Oleg-Goncharov and
ksivaman
as code owners
October 27, 2026 07:44
Contributor
[Medium impact] The PR appears safe to merge; no actionable issues were found. SummaryThis PR lets eligible NVFP4 kernels write GEMM-ready scales directly on SM120/SM121.
No actionable issues were found. This review checked the code paths; it did not run GPU tests. Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[NVFP4 output allocation] --> B{Eligible recipe and shape?}
B -->|SM120/121, supported recipe, 128-aligned| C[Allocate scales marked GEMM-ready]
B -->|Otherwise| D[Allocate compact scales]
C --> E[Quantize and write swizzled scales]
E --> F[Skip later swizzle]
D --> G[Quantize with compact scales]
G --> H[Swizzle later when GEMM needs it]
Reviews (1) · Last reviewed commit: "[Common, PyTorch] Fuse NVFP4 scale swizz..." · Reviewed by Greptile |
6 of 13 tasks
This branch has not been deployed
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.
Description
On SM120/SM121, an NVFP4 quantizer that requests GEMM-ready scales (
optimize_for_gemm) always getscompact scales from the quantize kernel, followed by the standalone post-quantize swizzle pass:
gemm_swizzled_scales_alignmentgates the existing in-kernel 2D path to SM100/110, andnvte_quantize_with_hadamard_transformrejects swizzled output on the SM12x no-TMEM RHT path.This PR writes the scales directly in the cuBLAS GEMM layout on SM120/SM121 when rows and columns are
multiples of 128: the generic 1D, columnwise-RHT 1D and tuned 1D kernels gain a swizzled store, and the
existing 2D in-kernel path is enabled there. FP4 data, scale values and amaxes are unchanged; shapes with
partial scale tiles keep compact allocation and the existing post-quantize fallback.
An eligible quantize call then launches two fewer kernels (the row-wise and column-wise swizzle passes) and
does not allocate replacement scale buffers. One consequence: the existing cached-weight tests in
tests/pytorch/nvfp4/test_nvfp4_weight_swizzle_cache.pyfail on SM120 atmain: the post-quantize swizzlereallocates a cached weight's scale buffers on every update, so a captured CUDA graph would replay against
a stale address. They pass with this PR for these 128-aligned weights.
Tested on an RTX 5050 (SM 12.0) against
mainbuilt the same way (details under Validation): no test that passes onmainfails here, eight tests that fail onmainpass, and withoptimize_for_gemm=Truea BF16 quantize call takes 3.4% to 28.3% less time for 1D, 2D and RHT quantization at the five measured 128-aligned shapes (1024x1024 to 8192x5120).Type of change
Changes
quantize_transpose_nvfp4.cuh,quantize_transpose_nvfp4_tuned_1D.cuh: add aWITH_GEMM_SWIZZLED_SCALESinstantiation of the generic 1D, columnwise-RHT 1D and tuned 1D kernels,selected when the output requests swizzled scales. Swizzled output is accepted on every non-row-scaled
path (previously 2D only) and still requires rows and cols to be multiples of 128.
swizzle.cuh:store_gemm_swizzled_scaleswrites each aligned group of four scales with one 32-bitstore (same bytes at the same addresses as four byte stores) and falls back to byte stores otherwise.
The existing 2D columnwise swizzled store uses it as well.
row_cast_col_hadamard_transform_cast_fusion.cu: drop the SM12x rejection of swizzled output; thatpath dispatches to the kernels above.
NVFP4Quantizer::gemm_swizzled_scales_alignment: report 128x128 alignment on SM120/SM121 for plain 1D,2D, and RHT with cast fusion enabled; keep row-scaled and 4over6 compact on every architecture. Eager
allocation and the torch.compile metadata path (
dynamo/fake.py) both use this function, so they stayconsistent.
tests/pytorch/nvfp4/test_nvfp4_sm12x_swizzle_fusion.pycompares FP4 bytes, both amaxes andan independent permutation of the compact scales for 1D/2D/RHT x row/column/both x stochastic rounding
x E4M3/UE5M3 scales x BF16/FP32 input, and checks the allocation gates.
test_nvfp4_rht_swizzle_fusion_shape_gategains SM12x expectations (128-row tiles instead of theTMEM path's 64). In
tests/cpp/operator/test_cast_nvfp4_transpose.cu, the SM12x RHT test that expectedswizzled output to be rejected now checks that partial scale tiles are rejected
(
RejectsUnalignedSwizzledScalesOnSM12x), and a new test checks that swizzled RHT output equals a CPUpermutation of the compact output (
SwizzledScalesMatchCompactOnSM12x).Behavior on SM100/110 is unchanged for PyTorch callers, except that a quantizer combining RHT with
row-scaled NVFP4 or 4over6 (which
quantize_implrejects) now allocates compact scales. C API callersmay now request swizzled output from the 1D kernels on any architecture; only SM120 was run.
Validation
main@ fac8d4a and this branch were built the same way (NVTE_FRAMEWORK=pytorch NVTE_CUDA_ARCHS=120a NVTE_WITH_NCCL_EP=0 pip wheel . --no-build-isolation, GCC 15.2, CUDA 13.4.92) and run on one NVIDIA GeForceRTX 5050 (SM 12.0, 20 SMs, 8 GB, driver 615.71.10) with PyTorch 2.16.0.dev20261016+cu134 and Python 3.14.8.
The wheel was built from this branch before its last change, which touches only
tests/cpp/operator/test_cast_nvfp4_transpose.cu; the C++ tests were built from the final branch. The NVFP4directory and
test_numerics.pyran with PyTorch's caching allocator capped at 6 GiB, releasing cachedblocks between tests so that runtime-compiled kernels could still load (64 large-shape cases need more
memory and fail with OOM on both builds). The other Python modules ran with a 2 GiB cap while another job
shared the GPU. Counts come from the JUnit and gtest reports: on both builds most pytest processes, and
some runs of the C++ test binary, exit with SIGSEGV after the last test, consistent with the exit-time crash
that #3733 fixes.
mainpytest tests/pytorch/nvfp4mainand pass here¹; none pass onmainand fail heretest_nvfp4_sm12x_swizzle_fusion.pypytest tests/pytorch/test_numerics.py -k nvfp4test_weight_swizzle_in_layers.py;test_torch_compile.py,test_quantized_tensor.py,test_fusible_ops.pywith-k nvfp4test_cast_nvfp4_transpose,test_dequantize_nvfp4,test_swizzle,test_cast_current_scaling,test_cast_transpose_current_scalingCastNVFP4RHTTestcasesmain's library⁴compute-sanitizer --tool memcheckover the new pytest file, caching allocator off¹
test_nvfp4_2d_swizzle_fusion_shape_gate(128x128, 256x512) andtest_nvfp4_rht_swizzle_fusion_shape_gate(128x256), which expect in-kernel swizzled scales for aligned shapes, and the four cached-weight tests in
test_nvfp4_weight_swizzle_cache.py(test_cached_weight_scale_pointers_stablefor Linear and LayerNormMLP,test_cached_weight_swizzle_scale_cuda_graph_replayfor Linear and LayerNormLinear). The eighth,test_nvfp4_rht_swizzle_fusion_shape_gateat 64x128, passes because this PR adds the SM12x expectation(128-row scale tiles; the 64-row case is specific to the TMEM kernel).
² 530 in
test_nvfp4_group_quantize_graph_safe.py, where the graph-safe grouped RHT cast kernel fails tolaunch on SM120 (
invalid argument); 152 RHT reference comparisons intest_nvfp4_group_quantize.py(120,swizzled_sfgrouped cases) andtest_nvfp4_rht_quantize_exact.py(32); 64 OOM intest_nvfp4_quantize_exact.py.³ On
mainthe 183 failures are the allocation assertion (make_emptynever allocates swizzled scales onSM120); the 10 passes are the cases that expect compact scales.
⁴ Both throw "NVFP4 RHT quantization on SM120/SM121 does not support GEMM-swizzled scales".
⁵ The 128-aligned shapes of
benchmarks/benchmark_rht_cast_swizzle_fusion.py(8192x{2560, 3584, 5120, 10240},{2560, 3584, 5120, 10240}x8192, 4096x16384, 14336x16384), for 1D, 2D and RHT quantization with and without
stochastic rounding: FP4 data, both amaxes, and the scales compared with
swizzle_nvfp4_scaleof thecompact result. On
mainthe same comparison of the post-quantize fallback matches in all 60 cases.No new compiler warnings: the build logs show the same warnings, with the same counts, as
main.libtransformer_engine.sofor sm_120a grows from 170,060,488 to 172,578,416 bytes (+1.5%) with theswizzled kernel instantiations.
What the
LintandLicenseworkflows run passes locally on this branch:qa/L0_pytorch_lintandqa/L0_jax_lint(cpplint 1.6.0, pylint 3.3.1, Python 3.12) andqa/L0_license. Beyond the sm_120a build above, the 29libtransformer_enginetranslation units that include the changed files compile for the default architecture list (75;80;89;90;100;120with CUDA 13.4: sm_75, sm_80, sm_89, sm_90, sm_100a, sm_103a, sm_107a, sm_120f).GPU work per quantize call
torch.profilerover one warmq(x)call, BF16 1024x1024 input, row-wise and column-wise output,optimize_for_gemm=True:mainswizzle_row_scaling_kernel(6)swizzle_row_scaling_kernel(5)At 1088x1024, which is not 128-aligned, both builds launch the same 6 and 5 operations (compact scales plus
the fallback swizzle).
The amax copy in the 1D and 2D rows is the device-to-device copy that #3750 removes; the two PRs are independent.
Time per quantize call
torch.utils.benchmarkblocked_autorangemedian ofq(x), the method ofbenchmarks/benchmark_rht_cast_swizzle_fusion.pyextended to the 1D and 2D quantizers: BF16 input (torch.randn, seed 0), row-wise and column-wise output. The three builds alternated over two rounds of three measurements per cell with nothing else on the GPU; each value is the lowest of the six medians, and a cell's two round minima differ by at most 1.6 µs (5.7%). Times are host wall-clock per call, so at 1024x1024 they include each launch's host overhead.optimize_for_gemm=True:main(µs)With
optimize_for_gemm=False, where both builds run the same code, the 30 corresponding cells (q(x), andq(x)followed byswizzle_scales_for_gemm_) differ by at most 0.66 µs (2.3%). On this branch, writing swizzled instead of compact scales changes the time ofq(x)by -6.7% (2D, 5120x8192) to +1.8% (2D, 2048x2048).bench_nvfp4_paths.pyNot covered
row-scaled/4over6 allocation noted above; the new kernel variants are compiled for every architecture but
are requested there only by C API callers.
new kernel variants.
optimize_for_gemm.How to reproduce
Checklist:
tests fail on this SM120 GPU on
mainas well, see Validation)🤖 Generated with Claude Code