quadrants.lang.simt.subgroup#
Functions#
|
Subgroup-scope thread-converging barrier: every lane in the subgroup must reach the call before any lane |
Subgroup-scope memory fence: orders memory operations within the subgroup without requiring thread convergence. |
|
|
|
|
Return |
|
Return a |
|
Return a |
|
Deprecated alias for |
|
AND-reduce |
|
OR-reduce |
|
Return |
|
Bitmask of lanes strictly below |
|
Bitmask of lanes |
|
Bitmask with exactly one bit set at |
|
Bitmask of lanes strictly above |
|
Bitmask of lanes |
|
Broadcast lane 0's |
|
Active subgroup size for the current launch, as a Python |
|
|
Return this lane's subgroup-local index, in |
|
|
Each lane returns the |
|
Lane |
|
Lane |
|
Lane |
|
|
|
|
|
|
Module Contents#
- quadrants.lang.simt.subgroup.sync()[source]#
Subgroup-scope thread-converging barrier: every lane in the subgroup must reach the call before any lane proceeds. Call from uniform control flow with all lanes active; calling from divergent control flow is implementation-defined (CUDA’s
nvvm.bar.warp.syncdeadlocks if the mask does not match the active set).Lowers to
OpControlBarrier(Subgroup, Subgroup, 0)on SPIR-V,__syncwarp(0xFFFFFFFF)on CUDA (reconverges lanes that diverged under independent thread scheduling on Volta+), andllvm.amdgcn.wave.barrieron AMDGPU (a compiler reordering barrier on lockstep GCN waves, a real wave-scope barrier on RDNA). The legacy namesubgroup.barrier()is a deprecated alias that forwards here.
- quadrants.lang.simt.subgroup.mem_fence()[source]#
Subgroup-scope memory fence: orders memory operations within the subgroup without requiring thread convergence.
Lowers to
OpMemoryBarrier(Subgroup, AcquireRelease | UniformMemory | WorkgroupMemory)on SPIR-V,__threadfence_block()on CUDA, andfence syncscope("workgroup") seq_cston AMDGPU. The CUDA and AMDGPU forms are workgroup-scope – over-strict for the subgroup-scope ask but correct, since the subgroup is a strict subset of the workgroup. Call from uniform control flow with all lanes active (same caveats as sync). The legacy namesubgroup.memory_barrier()is a deprecated alias that forwards here.
- quadrants.lang.simt.subgroup.elect()[source]#
Return
1on lane0of every subgroup and0on every other lane.Implemented portably as
invocation_id() == 0: every backend that lowersinvocation_id()therefore lowerselect()at zero extra cost (it inlines at compile time into a single compare + zext).Note that this narrows SPIR-V’s
OpGroupNonUniformElectsemantics, which may pick any active lane as the elected one. Under the documented uniform-CF + all-lanes-active contract forqd.simt.subgroupthis distinction is invisible (lane 0 is always active and is a legal choice), and pinning down the elected lane keeps the behaviour consistent across backends.
- quadrants.lang.simt.subgroup.ballot_first_n(predicate, n: template())[source]#
Return a
u32bitmask whose bitiis set iffi < nAND lanei’spredicateis non-zero.nis aqd.template()compile-time constant in[1, 32]; bits>= nof the result are always zero. Passn = 32for “ballot all 32 representable lanes” (the most common case, used bysegmented_reduce_*and every other consumer of the u32 ballot in this module).Backend lowering:
CUDA:
__ballot_sync(0xFFFFFFFF, predicate)— the warp is always 32 lanes, so theu32result naturally packs every lane.AMDGPU:
llvm.amdgcn.ballot.i64followed bytrunc to i32; bits[32, 64)of the i64 (lanes 32..63 on wave64) are always discarded, matching theballot_first_n(p, n <= 32)contract. This is a workaround for an LLVM AMDGPU isel bug —ballot.i32is documented as well-defined on wave64 (PR llvm/llvm-project#71556) but in practice still failsCannot selecton gfx942 in LLVM 20 / 22 for non-constant predicates. Seecodegen_amdgpu.cppfor the full bug + workaround comment.SPIR-V:
OpGroupNonUniformBallot(returns a uvec4); we extract component 0 = lanes 0..31’s ballot.
For
n < 32we mask the predicate bylane < nbefore issuing the ballot, so bits[n, 32)of the result are forced to zero regardless of what those lanes’ actual predicates are. Atn == 32the masking is provably a no-op on every backend (lanes>= 32are either non-existent on wave32 or already not represented in the u32 result on wave64), so we shortcut and emit the ballot directly with no extra arithmetic.Caller contract: uniform CF + all lanes active. If you need a mask covering more than 32 lanes (for wave64 callers who want the full subgroup), use ballot and check the high 32 bits.
- quadrants.lang.simt.subgroup.ballot(predicate)[source]#
Return a
u64bitmask covering the entire subgroup; bitiis set iff lanei’spredicateis non-zero. On wave32 backends (CUDA, RDNA wave32, most Vulkan / Metal) the high 32 bits of the result are always zero, since lanes>= 32do not exist; on wave64 backends (AMDGPU CDNA, GFX9, RDNA explicit-wave64) all 64 bits are meaningful. Use this when you need a subgroup-wide population count, prefix-mask, or compaction that has to cover more than 32 lanes; use ballot_first_n when you only care about the first 32 lanes.Backend lowering:
CUDA:
__ballot_sync(0xFFFFFFFF, predicate)zero-extended tou64— the warp is 32 lanes so the high half is always zero.AMDGPU:
llvm.amdgcn.ballot.i64— returns the full 64-bit ballot on wave64; on wave32 the AMDGPU backend lowers it to the wave32 ballot zero-extended to 64 bits, so the API stays uniform across wavefront modes. The i64 form selects cleanly in current LLVM (unlike the i32 form’s isel bug noted inballot_first_n); see llvm/llvm-project#71556 andcodegen_amdgpu.cppfor the full background.SPIR-V: extract components 0 and 1 of the
OpGroupNonUniformBallotuvec4 (lanes 0..31 and 32..63 respectively) and pack them:u64(hi) << 32 | u64(lo).
Caller contract: uniform CF + all lanes active.
- quadrants.lang.simt.subgroup.ballot_full_subgroup(predicate)[source]#
Deprecated alias for
ballot().Emits a
DeprecationWarningon first use and forwards toballot(). Will be removed in a future release; rename call sites toballot(predicate). Full-subgroup ops are unsuffixed throughout this module (reduce_add/all_true/inclusive_max/ etc.); tiled variants take a_tiledsuffix and an extralog2_sizetemplate parameter.
- quadrants.lang.simt.subgroup.all_true_tiled(predicate, log2_size: template())[source]#
AND-reduce
predicate != 0across2**log2_sizeconsecutive lanes. Returns1(i32) on every lane of the group iff every lane in the group has a non-zeropredicate, else0.Caller must ensure
2**log2_sizedoes not exceed the active subgroup size on the target (32 on CUDA / Metal, 64 on AMDGPU — wave64 is forced on every AMDGPU target).log2_sizeis a compile-time template; the body is fully unrolled.
- quadrants.lang.simt.subgroup.any_true_tiled(predicate, log2_size: template())[source]#
OR-reduce
predicate != 0across2**log2_sizeconsecutive lanes. Returns1(i32) on every lane of the group iff at least one lane in the group has a non-zeropredicate, else0.See all_true_tiled for the size contract.
- quadrants.lang.simt.subgroup.all_equal_tiled(value, log2_size: template())[source]#
Return
1(i32) on every lane in each2**log2_sizegroup iff every lane in the group has the samevalue, else0.Equality is the backend’s native
==onvalue’s dtype: for floats this meansNaN != NaN(a group with anyNaNreturns0) and+0.0 == -0.0, matching SPIR-VOpGroupNonUniformAllEqual. Callers wanting bit-equality on floats should bit-cast to the same-width integer dtype before calling.Implementation: each lane reads the value at the start of its group via
shuffle, thenall_true_tiledAND-reduces the per-lane equality bit. Cost: oneshuffleplus oneall_true_tiled(onevote.allon CUDA atlog2_size == 5, otherwise alog2_size-deepshuffle_xorbutterfly).
- quadrants.lang.simt.subgroup.lanemask_lt(lane_id)[source]#
Bitmask of lanes strictly below
lane_id— bitiis set iffi < lane_id.Pass
invocation_id()for the classic CUDA__lanemask_lt()(current lane’s mask). Equivalent to(u32(1) << u32(lane_id)) - u32(1); inlined at compile time into 1 shift + 1 subtract.See the module-level note for the
lane_id ∈ [0, 31]contract and the AMDGPU CDNA wave64 caveat.
- quadrants.lang.simt.subgroup.lanemask_le(lane_id)[source]#
Bitmask of lanes
<= lane_id— bitiis set iffi <= lane_id.Equivalent to
lanemask_lt(lane_id) | lanemask_eq(lane_id). See lanemask_lt for the contract.
- quadrants.lang.simt.subgroup.lanemask_eq(lane_id)[source]#
Bitmask with exactly one bit set at
lane_id— equivalent tou32(1) << u32(lane_id).See lanemask_lt for the contract.
- quadrants.lang.simt.subgroup.lanemask_gt(lane_id)[source]#
Bitmask of lanes strictly above
lane_id— bitiis set iffi > lane_id.Equivalent to
~lanemask_le(lane_id). See lanemask_lt for the contract.
- quadrants.lang.simt.subgroup.lanemask_ge(lane_id)[source]#
Bitmask of lanes
>= lane_id— bitiis set iffi >= lane_id.Equivalent to
~lanemask_lt(lane_id). See lanemask_lt for the contract.
- quadrants.lang.simt.subgroup.broadcast_first(value)[source]#
Broadcast lane 0’s
valueto every lane in the subgroup.Equivalent to
broadcast(value, qd.u32(0));0is trivially dynamically uniform, so the SPIR-VOpGroupNonUniformBroadcastrequirement is satisfied. Decorated with@qd.funcand inlined into the calling kernel.
- quadrants.lang.simt.subgroup.group_size() int[source]#
Active subgroup size for the current launch, as a Python
int.Resolves once at compile time by querying the live
Program— 32 on CUDA, 64 on AMDGPU (every AMDGPU target is pinned to+wavefrontsize64), and the device-probed value on the SPIR-V backends (read fromVkPhysicalDeviceSubgroupProperties::subgroupSizeon Vulkan, fixed at 32 on Metal). Because the return type is a plainint, the value can be used as aqd.template()argument inside@qd.kernel/@qd.funcbodies — this is how the full-subgroup reductions (e.g.reduce_add(v)) pick up the rightlog2_sizeper backend without the caller having to plumb it manually.For use inside
@qd.kernel/@qd.funcbodies: the value is folded into the kernel IR as a constant on every backend, including SPIR-V (so MoltenVK / desktop Vulkan see a literal subgroup size rather than a runtimeOpLoadofBuiltInSubgroupSize). Calling it from plain host Python afterqd.init()is also legal and returns the same number, which is handy for setting up grid dimensions on the host side.
- quadrants.lang.simt.subgroup.log2_group_size() int[source]#
log2(group_size())as a Pythonint, asserting the subgroup size is a power of two.Equivalent to
int(math.log2(group_size()))but emits a clearer error if the device ever reports a non-power-of-two subgroup width (no current SPIR-V driver does, but the spec allows it). Likegroup_size()this is a compile-time constant on every backend — callers feed it straight intoqd.template()to pick the rightlog2_sizefor a full-subgroup reduction (e.g.reduce_add_tiled(v, qd.simt.subgroup.log2_group_size())).
- quadrants.lang.simt.subgroup.invocation_id()[source]#
Return this lane’s subgroup-local index, in
0 .. group_size() - 1(ani32), on every backend.Used both as a lane id when computing a target lane for shuffle / broadcast, and as a per-lane identifier in cooperative algorithms.
- quadrants.lang.simt.subgroup.shuffle(value, index)[source]#
Each lane returns the
valueheld by the lane whose subgroup-local id equalsindex.valueis a scalar in a register; supported dtypes are 32- and 64-bit signed / unsigned integers andf32/f64(64-bit types are split into two 32-bit shuffles on AMDGPU; CUDA dispatches to its native 64-bit helpers).indexis au32; if it is out of range for the active subgroup the result is implementation-defined, so pass invocation_id-derived values or known-good lane ids.Lowers to
__shfl_syncon CUDA andOpGroupNonUniformShuffleon SPIR-V. On AMDGPU it goes throughds_bpermute; wave64 shuffles that cross the 32-lane boundary use a single wave-wideds_bpermuteon CDNA, or apermlane64 + ds_bpermute + selectsequence on RDNA (a few extra cycles). See the cross-half lowering comments inruntime.cppfor the per-ISA details.
- quadrants.lang.simt.subgroup.shuffle_xor(value, mask)[source]#
Lane
ireadsvaluefrom lanei ^ mask.Implemented portably as
shuffle(value, lane ^ mask): every backend that lowersshuffletherefore lowersshuffle_xor.maskis au32and must place the XOR partner inside the active subgroup; results outside that range are implementation-defined (same caveat asshuffle). Decorated with@qd.funcand inlined into the calling kernel.
- quadrants.lang.simt.subgroup.shuffle_up(value, offset)[source]#
Lane
ireturns thevalueheld by lanei - offset.Lanes near the bottom of the subgroup – where
i - offset < 0– receive an implementation-defined value (typically their ownvalue), so the bottomoffsetlanes’ results should be ignored or masked. Same dtype rules as shuffle;offsetis au32. Lowers to__shfl_up_syncon CUDA andOpGroupNonUniformShuffleUpon SPIR-V; on AMDGPU it is emulated withds_bpermute, with wave64 cross-half cases handled the same way as shuffle.
- quadrants.lang.simt.subgroup.shuffle_down(value, offset)[source]#
Lane
ireturns thevalueheld by lanei + offset.Lanes near the top of the subgroup – where
i + offset >= group_size()– receive an implementation-defined value (typically their ownvalue), so reduction patterns must only trust lane 0’s final result or mask out the out-of-range lanes. Same dtype rules as shuffle;offsetis au32. Lowers to__shfl_down_syncon CUDA andOpGroupNonUniformShuffleDownon SPIR-V; on AMDGPU it is emulated withds_bpermute, with wave64 cross-half cases handled the same way as shuffle.
- quadrants.lang.simt.subgroup.all_true(predicate)[source]#
all_true_tiledacross the entire subgroup – seeall_true_tiled(..., log2_size=log2_group_size()).