Repository navigation
[Common] Use .shared::cta for 2D TMA loads on sm_12x - #3708
Draft
kimchi-developer wants to merge 1 commit into
Draft
kimchi-developer wants to merge 1 commit into
kimchi-developer wants to merge 1 commit into
Conversation
On sm_120/sm_121, ptxas lowers cp.async.bulk.tensor.2d with a
.shared::cluster destination into a run-time branch: a native UTMALDG
when the destination is the executing CTA, and otherwise a call to the
driver syscall __cuda_syscall_cp_async_bulk_tensor_2d_tile_unicast.
The first launch of any kernel that contains that call raises the
context's per-thread stack limit from 1,024 B to 14,624 B, and the
driver keeps the matching local memory reserved for the lifetime of
the context, outside the framework's caching allocator: 1,196 MiB on
a 60-SM RTX 5080 Laptop GPU, 400 MiB on a 20-SM RTX 5050. On sm_90a
and sm_100a both destination forms compile to the same native code.
cp_async_bulk_tensor_2d_global_to_shared always targets the executing
CTA's own shared memory (its addresses come from
__cvta_generic_to_shared), so on sm_12x it can issue the .shared::cta
form, which cp_async_bulk_tensor_2d_global_to_shared_cta already uses.
This covers every TMA cast kernel that loads through the helper or
through copy_2d_to_shared{,x2,x3}: NVFP4, MXFP8, FP8 and the Hadamard
amax kernels. Other architectures compile unchanged.
Add an sm_12x regression test that checks, in a fresh process, that
the first NVFP4 cast (1D and 2D block scaling) leaves the stack limit
unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01TadBEfH5zvffbgRV7hesgt
Signed-off-by: ceo <[email protected]>
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 sm_120 and sm_121 (RTX 50 series, RTX PRO Blackwell, GB10), the first launch of a TE cast kernel that loads
through
ptx::cp_async_bulk_tensor_2d_global_to_sharedraises the CUDA context's per-thread stack limit from1,024 B to 14,624 B. That covers every NVFP4, MXFP8 and FP8 TMA cast kernel and the Hadamard amax kernels. The
driver then keeps local memory for that stack resident for the life of the context. On an RTX 5080 Laptop GPU
(60 SMs), the first NVFP4 quantization takes 1,196 MiB of device memory (400 MiB on a 20-SM RTX 5050), none of
it visible to PyTorch's allocator (
torch.cuda.memory_reserved()does not grow). The model does not need thatmemory: it is reserved for a driver syscall that TE never calls.
This PR makes the helper use the
.shared::ctadestination on sm_12x. The stack limit then stays at 1,024 B, thatmemory stays free, and outputs are bitwise identical.
Related: #2255 (NVFP4 on RTX 50 series), #3300 (NVFP4 RHT for SM120/SM121, whose sm_12x path loads through this
helper and is also covered).
Root cause
cp_async_bulk_tensor_2d_global_to_shared(transformer_engine/common/util/ptx.cuh) issuesOn sm_12x, ptxas lowers the
.shared::clusterdestination into a run-time branch. A destination in the executing CTAgets a native
UTMALDG; any other destination calls (CALL.ABS.NOINC) the driver-provided__cuda_syscall_cp_async_bulk_tensor_2d_tile_unicast, which the kernel records as anEIATTR_EXTERNSentry. TEnever takes the remote branch, because its destinations are always CTA-local. Still, any kernel that contains the
call is launched with the syscall's stack requirement. At that first launch the driver raises the context-wide stack
limit and reserves local memory for it: roughly SMs × max resident threads per SM × stack size, here
60 × 1,536 × 14,624 B. Both last for the lifetime of the context.
The lowering reproduces without TE (
tma_forms.cu, attached: one kernel per destination form; nvcc 13.4.92,-O3):.shared::cta.shared::clusterUTMALDG, no call, REG 10, STACK 0UTMALDG, no call, REG 10, STACK 0UTMALDG, no call, REG 10, STACK 0UTMALDGandCALL.ABS.NOINC→__cuda_syscall_cp_async_bulk_tensor_2d_tile_unicast; REG 24, STACK 8Runtime of the same pair (
tma_stack_repro.cu, NVIDIA GeForce RTX 5050, 20 SMs, CUDA 13.4):.shared::ctakeeps the stack limit at 1024 → 1024 B with 0.0 MiB taken;.shared::clusterraises it 1024 → 14624 B and the device loses 400.0 MiB. Both copies are verified correct, andcudaFuncGetAttributes().localSizeBytesreports only 8 B for the cluster kernel.Fix
dst_shmemandmbaralways come from__cvta_generic_to_shared, which yields.shared::ctawindow addresses. A.shared::ctaaddress used in the.shared::clusterform already refers to the executing CTA's own shared memory.So on sm_12x the helper can issue the
.shared::ctaform of the same copy: same bytes, same mbarrier transactioncount, no remote path. TE already uses that form in
cp_async_bulk_tensor_2d_global_to_shared_cta(FP8block-scaling grouped quantize), and the 1D bulk-copy helper
cp_async_bulk_tensor_1d_global_to_shareduses the.shared::ctadestination on every arch ≥ sm_100. The.shared::ctadestination needs PTX ISA 8.6 (CUDA 12.8),which every sm_12x build already requires.
The change is confined by the preprocessor to
__CUDA_ARCH__ >= 1200, so sm_90/100/103/107/110 code compiles exactlyas before. In the minimal test, sm_90a and sm_100a produce identical SASS for both forms. Switching the helper to
.shared::ctaunconditionally, like the 1D helper, would therefore be equally valid if maintainers prefer a singleform. I kept the narrow version so that datacenter targets need no re-validation.
Scope
The helper is used directly, or through
copy_2d_to_shared{,x2,x3}, by every TMA cast kernel (main@ eb3c860):cast/core/grouped_tma.cuh(192, 197),cast/mxfp8/dequantize_mxfp8.cuh(113, 136),cast/mxfp8/group_dequantize_mxfp8.cuh(282, 301),cast/mxfp8/group_scaled_swiglu_mxfp8.cuh(309, 313, 333,337),
cast/mxfp8/specialized/quantize_mxfp8.cuh(955, 1503, 1534),cast/nvfp4/group_quantize_transpose_nvfp4.cuh(169, 193),
cast/nvfp4/quantize_transpose_nvfp4.cuh(345, 369),cast/nvfp4/specialized/quantize_transpose_nvfp4_tuned_1D.cuh(505, 580), and the wrappers inutil/ptx.cuh(1981, 2005, 2009, 2032, 2036, 2040);
copy_2d_to_shared{,x2,x3}:cast/fp8/gated_fp8.cuh,cast/fp8/quantize_fp8.cuh,cast/mxfp8/gated_mxfp8.cuh,cast/mxfp8/quantize_mxfp8.cuh,cast/nvfp4/group_quantize_transpose_nvfp4.cuh,cast/nvfp4/quantize_transpose_nvfp4.cuh,hadamard_transform/{hadamard_transform,group_hadamard_transform,graph_safe_group_hadamard_transform}.cu.Static scan of the sm_120 slice of
libtransformer_engine.so(cuobjdump -elf; kernels whoseEIATTR_EXTERNSnames a
__cuda_syscall_*symbol):__cuda_syscall_cp_async_bulk_tensor_2d_tile_unicastNVTE_CUDA_ARCHS=120(sm_120f) → with this change (sm_120a)main@ eb3c860,NVTE_CUDA_ARCHS=120(sm_120f), without → with this changeBy family on
mainbefore the change: group_quantize_mxfp8_kernel 4,896, quantize_mxfp8_kernel 1,224, quantize_gated_mxfp8_kernel 576, quantize_transpose_nvfp4_tuned_1D_kernel 288, quantize_transpose_nvfp4_2D_kernel 256, cast_fp8_gated_kernel 240, quantize_transpose_nvfp4_kernel 176, group_compute_fused_amax_kernel 112, cast_fp8_2D_kernel 102, quantize_mxfp8_kernel_cast_only 96, dequantize_mxfp8_kernel 96, group_scaled_swiglu_mxfp8_kernel 48, compute_fused_amax_kernel 29, group_dequantize_mxfp8_kernel 24, group_quantize_transpose_nvfp4_kernel 8, GroupHadamardAmaxTmaKernel 8, GraphSafeGroupHadamardAmaxTmaKernel 8, HadamardAmaxTmaKernel 8. With the change, the 28 kernels that still reference a syscall are the out-of-scope sites below: 22 × 4D unicast and 6 × 2D multicast.Evidence (driver 615.71.09, CUDA 13.4, PyTorch 2.16.0.dev20261016+cu134)
TE 2.20.2 without vs with this change, on an RTX 5080 Laptop GPU (sm_120, 60 SMs), on fixed synthetic inputs with
the stochastic-rounding/RHT RNG pinned:
te.Linearfwd+bwd under FP8 current scaling, NVFP4, NVFP4+RHT+SR+2Dte.Linearruns¹ plain 1D, 2D (16×16) weight, stochastic rounding, SR+RHT, RHT input; x = 4096×1280 BF16, grads 4096×5120.
² CUDA events, mean of 50 calls after 5 warm-ups, single run: an incidental observation, not a claim of this PR.
An Nsight Compute launch trace of one NVFP4
te.Linearforward+backward shows the mechanism directly.launch__stack_sizeis 1,024 for every kernel before the firstquantize_transpose_nvfp4_tuned_1D_kernel<0,0,1,0>launch. From that launch onward it is 14,624 for every kernel, unrelated PyTorch elementwise and reduction kernels
included.
main@ eb3c860 without vs with this change, built forNVTE_CUDA_ARCHS=120, on an NVIDIA GeForce RTX 5050 (sm_120, 20 SMs), same driver and toolkit:repro_stack_limit.py)test_sm12x_tma_stack_limit.py³
nvfp4_1d,nvfp4_rowwise,nvfp4_2d,nvfp4_sr,mxfp8_q,mxfp8_dq,mxfp8_2d,fp8_cs,fp8_ds,linear_fp8cs; 1024×2048 BF16 inputs,te.Linear2048→1024 with batch 512.⁴ CUDA events; median of 5 × 100 calls after 20 warm-ups; an incidental observation, not a claim of this PR.
How to reproduce
repro_stack_limit.py(attached) quantizes one 1024×2048 BF16 tensor withte.NVFP4Quantizer. Before and after thecast, it prints
cuCtxGetLimit(CU_LIMIT_STACK_SIZE)andtotal − free − memory_reserved().tma_stack_repro.cu(attached) reproduces the effect without TE:
nvcc -std=c++17 -O3 -arch=sm_120a tma_stack_repro.cu -lcuda, then runit with
ctaand withcluster.Tests
tests/pytorch/nvfp4/test_sm12x_tma_stack_limit.py(new; L0 runstests/pytorch/nvfp4as a directory). On sm_12x,it runs the first NVFP4 cast (1D and 2D block scaling) in a fresh process, because the limit is per context and
never shrinks. It asserts that
CU_LIMIT_STACK_SIZEis unchanged, and skips on other GPUs. Onmain@ eb3c860(NVIDIA GeForce RTX 5050): 2 failed in 5.86s without the fix (
The first 1d NVFP4 cast raised the per-thread stack limit from 1024 to 14624 bytes), 2 passed in 5.97s with it.Out of scope (same lowering, separate concerns)
fused_attn/flash_attn.cuhas a file-localcp_async_bulk_tensor_4d_global_to_sharedthat uses the.shared::clusterform too. On sm_12x, its 22multi_tensor_transpose_to_bhsd::transpose_to_bhsd_kernel<…>instantiations still reference
__cuda_syscall_cp_async_bulk_tensor_4d_tile_unicast. The TMA path ofmulti_tensor_transpose_to_bhsdis taken forsm_arch >= 100, which includes sm_12x, so the same change mostlikely applies there. It deserves its own PR with attention-path validation.
gemm/cutlass_grouped_gemm.cu: 6 CUTLASS SM90 grouped-GEMM kernels (cluster shape 1×2×1), built for the sm_120arch-specific target, reference
__cuda_syscall_cp_async_bulk_tensor_2d_tile_multicast. That is genuine multicast,which
.shared::ctacannot express.Type of change
Changes
transformer_engine/common/util/ptx.cuh:cp_async_bulk_tensor_2d_global_to_sharedissues the.shared::ctadestination form when
__CUDA_ARCH__ >= 1200; every other architecture keeps the.shared::clusterform.tests/pytorch/nvfp4/test_sm12x_tma_stack_limit.py: an sm_12x stack-limit regression test (first NVFP4 cast,1D and 2D block scaling).
Checklist:
change; none mention
ptx.cuh)tests/pytorch/nvfp4/test_nvfp4_rht_sm12x.pyfails on sm_120 atmain@ eb3c860 with and without this change(16 failed, 1 skipped in 2.06s / 16 failed, 1 skipped in 2.17s;
quantize_transpose_nvfp4.cuh:1854"Columnwise RHT requires an RHT matrix."). That is apre-existing issue, unrelated to this change.
References (outside TE)
cudaLimitStackSize1024 → 14448 Band ~3.5 GB of local memory, both cleared by moving bulk copies to
.shared::cta..shared::ctaavoids thesyscall path on SM12x. SM12x does support peer destinations through the emulated path, so a by-target switch is
sound only where every destination is CTA-local, as it is for this helper.
Reproduction files
The three reproduction files referenced above (
repro_stack_limit.py,tma_stack_repro.cu, andtma_forms.cu) are available in this public gist.