Skip to content

fix(cuda-device): select tcgen05 alloc warp by linear thread id - #1351

Merged
nihalpasham merged 9 commits into
NVIDIA:mainfrom
YeonwooSung:fix/tmem-alloc-linear-warp
Oct 17, 2026
Merged

nihalpasham merged 9 commits into
NVIDIA:mainfrom
YeonwooSung:fix/tmem-alloc-linear-warp

Conversation

@YeonwooSung

@YeonwooSung YeonwooSung commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

What this adds

TmemGuard::alloc_by and dealloc_by select the warp that runs the warp-collective tcgen05.alloc and tcgen05.dealloc. They used warp::warp_id(), which is threadIdx.x / 32 and names a hardware warp only in a 1D block. In a 2D or 3D block every row with the same threadIdx.x range matched, so several warps ran the allocation. The two methods now select by block-linear warp id, the numbering the hardware uses to form warps.

linear_tid = threadIdx.x + blockDim.x * (threadIdx.y + blockDim.y * threadIdx.z)
warp       = linear_tid / 32

blockDim = (32, 4, 1), alloc_by(0):
  before: threadIdx.x / 32 == 0   selects 128 threads, four full warps, four tcgen05.alloc
  after:  linear_tid / 32 == 0    selects 32 threads, the y == 0 row, one tcgen05.alloc
blockDim = (256, 1, 1): both rules select threads 0..=31, unchanged
static mut TMEM_SLOT: SharedArray<u32, 1, 4> = SharedArray::UNINIT;

// Every thread of the (32, 4, 1) block calls both; only block-linear warp 0 issues the instructions.
let tmem = TmemGuard::<TmemUninit, 128>::from_static(&raw mut TMEM_SLOT as *mut u32);
let tmem = unsafe { tmem.alloc() };
// ... MMA through tmem.address() ...
let _done = unsafe { tmem.dealloc() };

What we fixed

  • alloc_by and dealloc_by read threadIdx.{x,y,z} and blockDim.{x,y} and compare the block-linear warp id, so one hardware warp executes each .sync.aligned instruction in every block layout.
  • One crate-private linearization helper now serves TmemGuard, the cooperative-groups block reductions and thread_rank(); the private copy in cooperative_groups.rs is gone.
  • The tcgen05 usage pattern, the warp_id() rustdoc and the book no longer present threadIdx.x / 32 as adequate for multi-dimensional blocks.

Contract

  • alloc_by(n) and dealloc_by(n) take a block-linear warp id: warp 0 is linear tids 0..=31. In a 1D block this equals threadIdx.x / 32, so existing callers are unchanged.
  • The selected warp must be a full 32-thread warp: the block must hold at least 32 * (n + 1) threads.
  • Any one full warp of the CTA may deallocate once all TMEM accesses are complete; using the allocating warp is the convention. A kernel that allocates more than once must issue every allocation from the same warp.
  • warp::warp_id() is unchanged and remains documented as 1D-only.
Implementation
  • thread::block_linear_thread_id(tx, ty, tz, dx, dy) is a pure const fn, host-testable; block_linear_thread_id_here() reads the special registers through the generated stubs.
  • warp::block_linear_warp_id divides it by 32; block_linear_warp_id_here() is what alloc_by and dealloc_by call.
  • blockDim.z is not needed: z is the slowest axis.
  • Host tests enumerate every thread of 256x1x1, 32x4x1 and 8x4x2 blocks and check warp membership, plus a control that the old rule selects 128 threads of a 32x4 block and straddles two warps at blockDim.x = 48.
  • The tcgen05 example gains two 32x4 TmemGuard kernels behind its existing sm_100 gate, one TMEM allocation each (warp 0 and warp 3).

Verification

Checked at 5528df9fd (the PR head merged with main e84e5b3d7):

  • ✅ cargo test -p cuda-device --all-targets: 111 lib tests, integration, compile-fail and doctests pass.
  • ✅ cargo clippy -p cuda-device --all-targets -- -D warnings, RUSTDOCFLAGS='-D warnings' cargo doc -p cuda-device, cargo oxide fmt --check, all 18 scripts/check-*.sh guards, scripts/sync-example-locks.sh --check and just book pass. All 62 CI checks passed.
  • ✅ sm_100a PTX of the two 32x4 TmemGuard kernels: one tcgen05.alloc and one tcgen05.dealloc each, predicated on the block-linear thread id (%tid.y, %tid.z, %ntid.x, %ntid.y, two mad.lo); a kernel built with the old rule compares %tid.x alone. ptxas -arch=sm_100a accepts the PTX; the libNVVM route (--emit-nvvm-ir --arch sm_100a) builds the same selection.
  • ✅ Hardware census on an RTX 5090 (sm_120): in a 32x4 block warp_id() == 0 holds for 128 threads in four full warps, the linear rule for 32; with blockDim.x = 16 the old rule selects half-warps; in a 256x1 block both rules agree. memcheck, synccheck and racecheck clean.
  • ✅ tcgen05, tcgen05_matmul and gemm_sol PTX unchanged (identical per-kernel opcode histograms); they use raw tcgen05_alloc on 1D blocks.

Caveats

  • tcgen05.alloc and tcgen05.dealloc cannot execute on sm_120, so the 2D path is verified by PTX, ptxas and the census of the selection rule, not by a TMEM allocation on hardware; the example's new kernels run only on an sm_100 machine.
  • Users of the manual tcgen05_alloc path still select their warp themselves; this_thread_block().thread_rank() / 32 is the public block-linear id.

TmemGuard::alloc_by and dealloc_by selected the allocating warp with
warp::warp_id(), which is threadIdx.x / 32 and only correct for 1D
blocks. In a 2D block every row with a matching x coordinate ran the
warp-collective tcgen05.alloc / tcgen05.dealloc.

Number that warp by linear_tid / 32, where linear_tid is
threadIdx.x + blockDim.x * (threadIdx.y + blockDim.y * threadIdx.z).
alloc() and alloc_by(0) still mean linear tids 0..31, matching the old
1D result. warp::warp_id() stays x-only.

Signed-off-by: YeonwooSung <neos960518@gmail.com>
…-alloc-linear-warp

Signed-off-by: YeonwooSung <neos960518@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Oct 5, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@nihalpasham nihalpasham added the cuda-oxide SIMT programming model: rustc backend, cargo oxide, cuda-device, cuda-host, book label Oct 11, 2026
Add thread::block_linear_thread_id, a pure const fn that numbers the
threads of a block x fastest, then y, then z, and
block_linear_thread_id_here, which feeds it the special registers.
This is the order the hardware uses to form warps, so linear_tid / 32
names a hardware warp in every block layout.

cooperative_groups.rs kept a private copy of the same formula for
thread_rank and the block reductions. It now calls the shared helper,
so the crate has one source of truth for the linearization and other
warp or thread selections can reuse it.

Host tests cover 1D, 2D and 3D blocks.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
warp::block_linear_warp_id now divides thread::block_linear_thread_id
by 32 instead of repeating the formula, and the cooperative-groups
block reductions call warp::block_linear_warp_id_here in place of
their private warp_in_block_linear, so TmemGuard and the block
reductions select warps from one definition.

warp_id() stays threadIdx.x / 32. Its rustdoc now points at the public
block-linear route, this_thread_block().thread_rank() / 32, instead of
asking callers to derive their own id.

Two control tests pin the pre-fix behaviour: in a (32, 4, 1) block the
x-only rule selects all 128 threads in four full warps where the
block-linear rule selects 32, and in a (48, 2, 1) block it selects two
half warps, which a .sync.aligned instruction cannot accept.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
The tcgen05 module's usage pattern, the alloc_by and dealloc_by docs
and two book pages still presented threadIdx.x / 32 as an adequate
warp selection for any block. That value names a hardware warp only
in a 1D block, which is the bug TmemGuard just fixed.

- The usage pattern says warp_id() is 1D-only and that a 2D or 3D
  block must select the warp by block-linear thread id, as
  TmemGuard::alloc and dealloc do.
- alloc_by and dealloc_by state the caller contract: the selected warp
  must be a full 32-thread warp, because the instruction is
  .sync.aligned and is undefined once a lane of the warp has exited.
  Any one full warp of the CTA may deallocate once TMEM accesses are
  complete; the allocating warp is the convention. The range is now
  written 0..=31.
- The book no longer calls the x-only value "usually fine" and no
  longer describes warp_id() as a register read; both pages point at
  this_thread_block().thread_rank() / 32.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
No in-tree kernel went through TmemGuard::alloc_by or dealloc_by, so CI
compiled nothing on the path the block-linear warp selection fixed.

The tcgen05 example gains tmem_guard_2d_test: a (32, 4, 1) block in
which every thread calls alloc() and dealloc(), then alloc_by(3) and
dealloc_by(3). Only block-linear warp 0 (the y == 0 row) and then warp
3 run tcgen05.alloc and tcgen05.dealloc; thread (0, 0, 0) reports both
TMEM addresses, and the host fails if a slot still holds the sentinel.
The launch sits behind the example's existing sm_100 gate, so hosts
without tensor memory keep the PTX-only path and ptxas check, and
examples-compile builds the kernel on every pull request.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
tmem_guard_2d_test allocated tensor memory twice in one CTA, first from
block-linear warp 0 and then from warp 3. The CUTLASS TMEM allocator
states as a precondition that repeated allocations must be issued by
the same warp, and the dealloc_by rustdoc on this branch cites that
rule. The example should not contradict it, and it can only execute on
sm_100 hardware, where a hang or fault would surface late.

The warp 3 case moves into its own kernel, tmem_guard_2d_warp3_test.
Each kernel now issues one tcgen05.alloc and one tcgen05.dealloc. The
host launches both with block_dim (32, 4, 1) into one output buffer and
keeps the sentinel check. The README lists the new kernel, and its
expected output line changes to match.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
The dealloc_by parameter text said the PTX ISA requires one warp for
allocation and one for deallocation, "not the same warp", which reads
as a prohibition. The ISA asks for one warp of the CTA to perform each
operation and is silent on whether they match. The sentence now says
so directly.

The same-warp rule for repeated allocations, which only dealloc_by
mentioned, now also appears on alloc_by, where allocations are issued.

Signed-off-by: nihalpasham <nihalp@nvidia.com>
@nihalpasham

Copy link
Copy Markdown
Collaborator

/ok to test 5528df9

@nihalpasham
nihalpasham merged commit 51c737f into NVIDIA:main Oct 17, 2026
62 checks passed
@nihalpasham nihalpasham added bug Something isn't working device-apis User-facing device-side and kernel-authoring APIs (cuda-device, cuda-macros) cuda-feature A CUDA hardware/toolkit feature (TMA, tcgen05, WGMMA) labels Oct 17, 2026
@nihalpasham

Copy link
Copy Markdown
Collaborator

Good catch, and the same bug sat in your mbarrier PR, so I gave both one shared helper:

  • The block-linear thread id now lives once in thread.rs; TmemGuard, the cooperative-groups reductions and init_by/inval_by (fix(cuda-device): run mbarrier init on one linear thread #1349) all use it instead of carrying their own formula.
  • The tcgen05 usage notes, the warp_id() docs and two book lines still presented threadIdx.x / 32 as fine for 2D blocks; fixed.
  • alloc_by/dealloc_by now state the rules: a full 32-thread warp, any one full warp may deallocate once TMEM accesses are done, and repeated allocations must come from the same warp.
  • A control test for the old rule (128 threads selected in a 32x4 block, two half warps at blockDim.x = 48) and two 32x4 TmemGuard kernels in the tcgen05 example, one allocation each, so CI compiles the path.

On the 5090 the old rule picks four full warps of a 32x4 block and the new one picks one; the sm_100a PTX gates one tcgen05.alloc and one tcgen05.dealloc per kernel on the linear id, and ptxas accepts it. Running tcgen05.alloc in a 2D block for real needs sm_100 hardware, which I do not have here. 111 cuda-device tests, clippy, rustdoc, the book, guards and all 62 CI checks green at 5528df9fd.

@YeonwooSung
YeonwooSung deleted the fix/tmem-alloc-linear-warp branch October 18, 2026 03:16
nihalpasham pushed a commit that referenced this pull request Oct 19, 2026
ManagedBarrier::init_by and inval_by elected the acting thread with threadIdx.x == selector, which names one thread only in a 1D block: in a (32, 4, 1) block selector 0 ran mbarrier.init on four threads and selector 127 on none, so the launch failed with error 719. Elect by the block-linear thread id through the shared helper from #1351, document the all-threads protocol and the block-uniform selector, and add a (32, 4, 1) regression to future_apis.

Signed-off-by: YeonwooSung <neos960518@gmail.com>
Signed-off-by: nihalpasham <nihalp@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cuda-feature A CUDA hardware/toolkit feature (TMA, tcgen05, WGMMA) cuda-oxide SIMT programming model: rustc backend, cargo oxide, cuda-device, cuda-host, book device-apis User-facing device-side and kernel-authoring APIs (cuda-device, cuda-macros)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants