Repository navigation
fix(cuda-device): select tcgen05 alloc warp by linear thread id - #1351
Merged
Merged
Conversation
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>
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>
Collaborator
|
/ok to test 5528df9 |
Collaborator
|
Good catch, and the same bug sat in your mbarrier PR, so I gave both one shared helper:
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 |
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>
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.
What this adds
TmemGuard::alloc_byanddealloc_byselect the warp that runs the warp-collectivetcgen05.allocandtcgen05.dealloc. They usedwarp::warp_id(), which isthreadIdx.x / 32and names a hardware warp only in a 1D block. In a 2D or 3D block every row with the samethreadIdx.xrange 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.What we fixed
alloc_byanddealloc_byreadthreadIdx.{x,y,z}andblockDim.{x,y}and compare the block-linear warp id, so one hardware warp executes each.sync.alignedinstruction in every block layout.TmemGuard, the cooperative-groups block reductions andthread_rank(); the private copy incooperative_groups.rsis gone.warp_id()rustdoc and the book no longer presentthreadIdx.x / 32as adequate for multi-dimensional blocks.Contract
alloc_by(n)anddealloc_by(n)take a block-linear warp id: warp 0 is linear tids0..=31. In a 1D block this equalsthreadIdx.x / 32, so existing callers are unchanged.32 * (n + 1)threads.warp::warp_id()is unchanged and remains documented as 1D-only.Implementation
thread::block_linear_thread_id(tx, ty, tz, dx, dy)is a pureconst fn, host-testable;block_linear_thread_id_here()reads the special registers through the generated stubs.warp::block_linear_warp_iddivides it by 32;block_linear_warp_id_here()is whatalloc_byanddealloc_bycall.blockDim.zis not needed: z is the slowest axis.blockDim.x = 48.tcgen05example gains two 32x4TmemGuardkernels behind its existing sm_100 gate, one TMEM allocation each (warp 0 and warp 3).Verification
Checked at
5528df9fd(the PR head merged with maine84e5b3d7):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 18scripts/check-*.shguards,scripts/sync-example-locks.sh --checkandjust bookpass. All 62 CI checks passed.TmemGuardkernels: onetcgen05.allocand onetcgen05.dealloceach, predicated on the block-linear thread id (%tid.y,%tid.z,%ntid.x,%ntid.y, twomad.lo); a kernel built with the old rule compares%tid.xalone.ptxas -arch=sm_100aaccepts the PTX; the libNVVM route (--emit-nvvm-ir --arch sm_100a) builds the same selection.warp_id() == 0holds for 128 threads in four full warps, the linear rule for 32; withblockDim.x = 16the old rule selects half-warps; in a 256x1 block both rules agree. memcheck, synccheck and racecheck clean.tcgen05,tcgen05_matmulandgemm_solPTX unchanged (identical per-kernel opcode histograms); they use rawtcgen05_allocon 1D blocks.Caveats
tcgen05.allocandtcgen05.dealloccannot 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.tcgen05_allocpath still select their warp themselves;this_thread_block().thread_rank() / 32is the public block-linear id.