Repository navigation
[Common, PyTorch] Write both NVFP4 amax outputs in the reduction - #3750
kimchi-developer wants to merge 1 commit into
Conversation
Plain NVFP4 quantization with row-wise and column-wise outputs reduced the amax into one buffer and then copied it into the other with a separate device-to-device copy. Let the amax kernels zero and update an optional second output, used for the column-wise amax of NVFP4 outputs, and drop the copy. Other callers of the amax reduction keep one output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: ceo <ceo@logictree.app>
[Medium impact] The PR appears safe to merge; no actionable issues were found. SummaryThe PR writes the input maximum directly to both NVFP4 maximum buffers, removing a separate GPU copy.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Plain NVFP4 quantization] --> B[Select first maximum buffer]
B --> C{Separate column-wise buffer?}
C -->|Yes| D[Pass both buffers]
C -->|No| E[Pass one buffer]
D --> F[Clear selected buffers]
E --> F
F --> G[Reduce input maximum into selected buffers]
G --> H[Quantize without a separate copy]
Reviews (1) · Last reviewed commit: "[Common, PyTorch] Write both NVFP4 amax ..." · Reviewed by Greptile |
vthumbe1503
left a comment
There was a problem hiding this comment.
Thanks for creating the PR! I think the better solution for this is to create a single tensor for both amax_rowwise and amax_columnwise and let amax_columnwise alias to amax_rowwise when NVFP4 has a single amax. We can make that code change in quantizer.cpp
|
@vthumbe1503 Aliasing would drop the copy without touching the common amax kernel, so I tried it before changing the PR. It breaks NVFP4 under torch.compile. The prototype is this PR's base (
¹ The SM120 failures listed in the PR description; they fail on both builds. All 19 failures are the aliasing check of
A second case:
The amax reduction itself is fine with aliasing: with So aliasing needs more than the Prototype diff (against
|
Description
For plain NVFP4 quantization with both row-wise and column-wise outputs,
NVFP4Quantizer::quantize_implcomputes the per-tensor amax into one buffer and then copies the four-byte scalar into the other with a
separate
cudaMemcpyAsync. This PR lets the amax reduction write the value to both buffers: the zeroingkernel clears both, and each thread block applies its atomic max to both. The device-to-device copy
disappears, with no extra input read or kernel launch.
Tested on an RTX 5050 (SM 12.0) against
mainbuilt the same way (details under Validation): no test changes outcome, and a 1D or 2D quantize call issues one GPU operation fewer and takes 0.3 to 1.8 µs less (8.6% at 1024x1024).This is independent of #3749 (in-kernel NVFP4 scale swizzling on SM120/SM121); either can merge first.
Type of change
Changes
current_scaling.cu:zero_amax_kernelandamax_kerneltake an optional second output.compute_amax_implpasses the column-wise amax as that output only for NVFP4 outputs whose column-wiseamax is distinct from the buffer being reduced into; every other caller (FP8 current scaling,
compute_amax, the 2D partial cast) keeps one output.quantizer.cpp: remove the row-wise/column-wise amax copies from the non-RHT, non-row-scaled path.RHT post-transform amax and row-scaled per-row amax are untouched.
recipe.h: document thatnvte_compute_amax[_with_config]fills both amax buffers of an NVFP4 output.tests/pytorch/nvfp4/test_nvfp4_dual_amax.pyreuses one output tensor across inputs whose maximumdecreases, reaches zero and rises again, and checks both amaxes against the input maximum for
row-only, column-only and two-direction outputs, 1D and 2D quantization, BF16 and FP32 input, and
aligned and tail shapes.
The public API, scale computation and quantized values are unchanged. With a noop flag set, both buffers
are now left untouched; previously the row-wise value was still copied into the column-wise buffer.
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 NVFP4 directory and
test_numerics.pyran with PyTorch's caching allocator capped at 6 GiB, releasingcached blocks 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/nvfp4test_nvfp4_dual_amax.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_scalingcompute-sanitizer --tool memcheckover the 24 new tests, caching allocator off¹ 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.pyandtest_nvfp4_rht_quantize_exact.py; 64 OOM intest_nvfp4_quantize_exact.py; 8 SM120 swizzle-fusionexpectations that #3749 addresses.
²
main's copy produces the same values, so the test passes there too (run with the 2 GiB cap). Itguards the new path: the zeroing kernel must clear both buffers, or a later input with a smaller maximum
leaves a stale column-wise amax.
No new compiler warnings: the build emits the same warnings as
main.libtransformer_engine.sofor sm_120agrows by 200 bytes (170,060,488 to 170,060,688).
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,recipe/current_scaling.cucompiles for the default architecture list (75;80;89;90;100;120with CUDA 13.4: sm_75, sm_80, sm_89, sm_90, sm_100, sm_120).GPU work per quantize call
torch.profilerover one warmq(x)call, BF16 1024x1024 input, row-wise and column-wise output:mainzero_amax_kernel,amax_kernel,Memcpy DtoD, quantize (4)zero_amax_kernel,amax_kernel, quantize (3)ZeroAmaxKernel,HadamardAmaxTmaKernel, quantize (3)With
optimize_for_gemm=True, both builds add the twoswizzle_row_scaling_kernellaunches on SM120, so1D and 2D go from 6 to 5 operations (#3749 removes those two launches for 128-aligned shapes). The
same holds at 1088x1024.
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=False:main(µs)From 4096x4096 up, the saving is 0.8 to 1.2 µs (0.1% to 0.3%). With
optimize_for_gemm=Trueit is 0.6 to 2.0 µs. Every 1D and 2D cell is faster on this branch, while RHT quantization, which this PR does not change, differs frommainby at most 0.86 µs over its 15 cells.bench_nvfp4_paths.pyNot covered
common PyTorch path, so SM100/103/110 take the same code.
nvte_compute_amax. Other callers (FP8 current scaling, the NVFP4 2D partial cast)pass outputs without a distinct column-wise amax and keep one reduction output; the C++ current-scaling
suites above cover that path.
with_amax_reduction, the cross-rank MAX all-reduce still runs afterwards onboth buffers, which hold the same local amax before it as they did with the copy.
How to reproduce
python -m pytest tests/pytorch/nvfp4/test_nvfp4_dual_amax.py # timing and kernel lists, once per build (script in the details block above) python bench_nvfp4_paths.py --label pr --out pr.json --kinds 1d,2d python bench_nvfp4_paths.py --label pr --out pr-trace.json --trace --shapes 1024x1024,1088x1024Checklist:
tests fail on this SM120 GPU on
mainas well, see Validation)🤖 Generated with Claude Code