Wrappers
For ops whose operand semantics break the chain default, PTX.jl ships hand-written wrapper methods on the matching Operation{...} singleton. The user-facing call site is identical to the chain default — the wrapper just provides a typed method that takes priority over the @generated chain dispatch when its argument types match.
There are four patterns that force a wrapper:
- Mixed address-space pointer constraints. Shared-AS pointers need the
r(32-bit) constraint; the chain emitsl(64-bit) for anyLLVMPtr.cp.async,ldmatrix,stmatrix,mbarrieruse this override to satisfy ptxas. - Multi-output return. The chain returns one value; ldmatrix x2/x4, mma fragments, and shfl-with-pred-output all return tuples.
- Special operand layout. Braced register-vector operands (
{$N, $N+1, ...}), tensor-coord forms ([ptr, {c0, c1, c2}]), tied accumulators for wgmma — none have a chain-default rendering. - Compile-time-constant operand types. TMA's
<N>drank, mma shape/dtype, fragment counts — these need to be pinned at the registration boundary so each variant gets its own typed method.
Each wrapper file follows the same shape: a small declarative table maps (shape, dtype, …) to register counts and constraints, a _register function builds the family-local spec (asm template, llvmcall body, or intrinsic name) and emits a typed method per valid combination from a for loop. Bookkeeping is shared, not per-file: every generator records what it defined in the wrapper registry (src/wrappers/registry.jl) — one WrapperRecord per (family, op, mods, tier, intrinsic) key behind a single idempotency set, so re-invoking a registration helper cannot inflate the inventories the test count pins protect. Tier-2 generators obtain their IntrinsicCall through wrapper_intrinsic_call, which validates the generated name against the pinned NVVM registry and records it in one step; asm/core-IR generators call register_wrapper! directly. The test-side oracles read the inventories back through wrapper_records / wrapper_intrinsic_names / wrapper_asm_forms / wrapper_missing_intrinsics.
Literal spellings are declared with the @ptx_str spelling as the method head (@inline ptx"tcgen05.alloc.cta_group::1.sync.aligned.b32"(...) = ...); only generator loops that build the mods tuple programmatically define methods on the Operation{op, mods} singleton directly.
Adding a new dtype/shape combination is one entry in the table plus one line in the loop.
Family overview
| Family | File | Surface |
|---|---|---|
bar / barrier (CTA) | wrappers/barrier.jl | bar.{sync,arrive}, bar.warp.sync, barrier.{sync,arrive}[.aligned], and the reductions bar.red.{popc.u32,and.pred,or.pred} / barrier.red.{popc,and,or}[.aligned].{u32,pred} lower to llvm.nvvm.barrier.cta.* for Val, UInt32, and Int32 barrier ids and thread counts. A reduction takes its predicate as a trailing Bool (spell {!}c as !c); .popc returns UInt32, .and/.or return Bool. bar.* and barrier.*.aligned emit the same bar.* instruction. Wider integer operands fall back to the chain's inline asm. |
cp.async (scalar) | wrappers/cp_async.jl | cp.async.{ca,cg}.shared.global [smem], [global], Val(N) — needs shared r constraint and N-baked size |
cp.async.bulk.tensor (TMA) | wrappers/tma.jl | cp.async.bulk.tensor.{1..5}d.{shared::cluster|shared::cta}.global.tile.mbarrier::complete_tx::bytes (load), .global.shared::cta.tile.bulk_group (store), and cp.async.bulk.prefetch.tensor.{1..5}d.L2.global.tile[.L2::cache_hint] plus the base {3..5}d.L2.global.im2col[.L2::cache_hint] modes. Tile coordinates become positional Int32 arguments; base im2col takes exactly rank-many Int32 coordinates followed by rank-minus-two Int16 offsets. A cache-hinted prefetch adds one UInt64 policy operand. Prefetch is a weak, fire-and-forget performance hint, so no runtime result is exposed. The later .im2col::w[::128] and .tile::gather4 grammars are not part of this surface. |
cvt sub-byte FP packing | wrappers/cvt.jl | cvt.rn.satfinite.e2m1x2.{f32,f16x2,bf16x2} pack and cvt.rn.{f16x2,bf16x2}.e2m1x2 unpack — .b8 carrier through a mov.b16 brace-pair shim because NVPTX inline asm has no .b8 register class |
| extended precision | wrappers/extended_precision.jl | All 48 add.cc / addc / sub.cc / subc / mad.cc / madc scalar forms with explicit Bool carry/borrow, plus fused arbitrary-limb add/sub and unsigned 2×2-limb multiply |
fence.proxy.tensormap::generic | wrappers/fence.jl | Exact PTX 8.3 / sm_90+ acquire and release forms at {cta,cluster,gpu,sys} scope. Acquire takes a generic address that resolves to global memory plus the ISA-fixed Val(128) range; release takes no operands. All eight lower through NVVM as per-thread, non-convergent ordering operations. |
ldmatrix | wrappers/ldmatrix.jl | m8n8.{x1,x2,x4}[.trans].b16; m16n16.{x1,x2}.trans.b8; and Blackwell optional decompression from {b6x16_p32,b4x16_p64} to b8x16 for m8n16.{x1,x2,x4} and m16n16.{x1,x2}.trans, in {shared,shared::cta}. Returns one UInt32 per m8n16 matrix and two per m16n16 matrix. Generic state-space addressing is not part of the typed wrapper surface. |
mbarrier | wrappers/mbarrier.jl, ledgers/mbarrier_forms.jl | Closed PTX 9.3 form schema for lifecycle, tx-count, arrive/drop, waits, pending-count, layout, sem/scope, and CTA/cluster spaces; exact wrappers accelerate common forms while delegating to the same schema emitter. Results are explicit sink (.sink → PTX _), UInt64 state, Bool, UInt32 pending count, (waitComplete, reportPredicate), or (waitComplete, reportPredicate, reportValueCarrier::UInt16). The ISA's two noncanonical arrive_drop example heads are provenance-marked aliases and normalize on emission. PTX's 1-byte report value occupies the carrier's low byte because NVPTX inline asm has no .b8 register class. Explicitly shared addresses use the NVPTX r constraint; every route carries convergent nomerge. |
mma.sync.aligned | wrappers/mma.jl | mma.sync.aligned.<shape>.<layA>.<layB>.<d>.<a>.<b>.<c> for bf16/f16/tf32/FP8, modern m16n8 u8/s8 and u4/s4 integer forms, the exact six s32.b1.b1.s32.{xor,and}.popc forms, and kind::f8f6f4 5×5 sub-byte FP A/B (consumer Blackwell). Single-bit fragments are (A,B,C/D) = (1,1,2) registers at m8n8k128, (2,1,4) at m16n8k128, and (4,2,4) at m16n8k256; A/B use packed UInt32, while C/D use Int32. XOR starts at PTX 7.0, AND at PTX 7.1; only m8n8k128.xor has an sm75 floor, with the other five at sm80. LLVM 23 rejects its m8 XOR intrinsic at sm_75, so that one form uses typed convergent inline assembly while the other five use NVVM. |
mma.sp[::ordered_metadata].sync.aligned | wrappers/mma.jl | A closed sparse surface comprising 12 floating ABIs per metadata variant plus 64 integer forms: u8/s8 at m16n8k{32,64} and u4/s4 at m16n8k{64,128}, every A/B signedness pair, optional .satfinite, and ordinary/ordered metadata. A/B fragments and metadata use UInt32; integer C/D use Int32. Ordered metadata requires increasing retained indices, with exact shape-dependent selectors (0:3, 0:1, or 0). |
mma.sync.aligned.kind::mxf* (block-scaled) | wrappers/mma_scaled.jl | Three Blackwell-introduced kinds: mxf4, mxf4nvf4, mxf8f6f4. Operand layout (scale_data::UInt32, byte_id::UInt16, thread_id::UInt16) per side per PTX 9.2 §9.7.14.3 |
shfl.sync | wrappers/shfl.jl | up / down / bfly / idx × b32 × {data-only, data+pred} |
stmatrix | wrappers/stmatrix.jl | mirror of ldmatrix — m8n8.b16 (sm_70+) and m16n8.b8 (Hopper) |
vec_ldst | wrappers/vec_ldst.jl | ld.global.v{2,4}.{f32,b32,b16} / st.global.v{2,4}.{f32,b32,b16} — braced register-vector I/O for HBM-saturating bandwidth |
wgmma.mma_async (Hopper sm_90a) | wrappers/wgmma.jl | wgmma.mma_async.sync.aligned.m64nNk{8,16,32}.<d>.<a>.<b> — accumulator passed by value (tied operands). Floating forms use all 32 N values stepped by 8 through 256; integer forms use the ISA's 16-value grid 8,16,24,32,48:16:224. The closed surface is 256 floating + 64 integer shape/type forms, each with SS runtime/constant scale_d and RF-A runtime variants. |
tcgen05 (Blackwell sm100a/sm110a and family targets) | wrappers/tcgen05.jl | Exact lifecycle, fence/wait, TMEM address, load/store, dense-MMA, and MX block-scale forms. MX uses the complete seven-operand schema (d, a_desc_or_tmem, b_desc, idesc, scale_a_tmem, scale_b_tmem, enable_input_d) and all eight legal kind × {scale_vec,block} spellings; the former five-argument MX surface is rejected. shift / dealloc / cp / ld / st take a 32-bit TMEM address returned by tcgen05.alloc, while alloc/commit use reviewed shared-memory carriers. ld/st cover the complete Table-52 grid — all five shapes including 16x32bx2, whose immHalfSplitoff is a positional Val(off) immediate — each with the optional .pack::16b/.unpack::16b repack qualifier; ld.red exposes the loaded registers and scalar reduction through the reviewed assembly route; the backend's separate reduction intrinsic namespace is covered by conformance probes. cp covers all six shape spellings — 64x128b/32x128b only with their ISA-mandated warpx2::*/warpx4 multicast — each optionally with the .b8x16.{b6x16_p32,b4x16_p64} decompression pair. Dense MMA covers both A sources (UInt64 SMEM descriptor vs UInt32 TMEM address, dispatched like the ISA's a-desc vs [a-tmem]), the collector::a::{fill,use,lastuse} qualifiers (discard is the spelled-nothing default), .ashift (TMEM A; discard/lastuse only), an optional positional disable-output-lane mask (`NTuple{4 |
Mixed-precision packed arithmetic
PTX ISA 9.4 adds 36 packed mixed-precision add, sub, mul, and fma forms for sm_107f (or higher in the same family, including sm_107a). Their type tokens specify the destination first, then the sources. f32x2 uses a UInt64 bit carrier; f16x2 and bf16x2 use UInt32 bit carriers.
sum = ptx"add.rn.f32x2.f16x2.f32x2"(half_pair_bits, float_pair_bits) # UInt64
result = ptx"add.rz.bf16x2.f32x2.f32x2"(sum, float_pair_bits) # UInt32The packed mixed fma forms require an explicit rounding mode. Their second and third inputs are both f32x2 (UInt64). Narrowing to f16x2 uses .rz.ftz for add/sub and .ftz.rz for mul; these modifier orders follow the instruction grammars. Unsupported spellings and incorrect operand widths fail before LLVM compilation, including through ptx"..."raw.
The assembler tests cover every form on sm_107f and sm_107a when the managed compiler supports PTX ISA 9.4. Older compilers explicitly skip that partition. Execution on sm_107 hardware remains unverified.
Alternate packed arithmetic
PTX ISA 9.4 alternate floating-point add, sub, mul, and fma support four-lane FP8/FP6/FP4 arithmetic on sm_100a and sm_103a only. These forms are unavailable on sm_100f, sm_103f, and sm_107a.
The result is always packed e4m3x4 or e5m2x4 in a UInt32 bit carrier. Source carriers are fixed by the instruction schema:
| Source format | Julia bit carrier | Layout |
|---|---|---|
e4m3x4, e5m2x4, ue8m0x4 | UInt32 | Four bytes |
e2m3x4, e3m2x4 | UInt32 | Six bits per byte, with two padding bits |
e2m1p4x4 | UInt32 | Four bits per byte, with four padding bits |
e2m1x4 | UInt16 | Four contiguous four-bit values |
compact = UInt16(0x2222) # four e2m1 values
padded = UInt32(0x02020202) # the same values in e2m1p4x4
acc = UInt32(0x38383838) # four e4m3 values
sum = ptx"add.rn.e4m3x4.e2m1x4"(compact, acc)
product = ptx"mul.rn.satfinite.e5m2x4.e2m1x4.e2m1p4x4"(compact, padded)
result = ptx"fma.e4m3x4.e2m1x4.e2m1x4"(compact, compact, sum)add/sub use a shared destination/last-source type followed by the first source type. mul specifies destination, first source, and second source types; fma shares its destination type with the accumulator and then specifies its two multiplicand types. Thus the final type token alone cannot determine the result or all input widths.
Rounding defaults to .rn; .satfinite optionally clamps overflow to the largest finite value of the destination format, preserving the sign. The canonical order puts .rn and .satfinite before the type tokens. The ledger admits all 896 canonical forms plus seven exact specification examples with postfix .satfinite, marked :ptxas_compat. Other modifier permutations and incorrect carrier widths are rejected, including through ptx"..."raw.
The assembler suite checks every form with stored results on both supported targets when PTX ISA 9.4 is available. Execution on B200/B300 hardware remains unverified for these new forms.
set comparison results
set compares two values and returns a numeric result. Its closed grammar covers 1,152 general scalar forms, 1,960 half/BF16 forms, and the 32 packed integer forms introduced in PTX ISA 9.4. An optional .and, .or, or .xor combines the comparison with a trailing Bool input.
mask = ptx"set.lt.u32.s64"(Int64(-2), Int64(1)) # UInt32(0xffffffff)
value = ptx"set.lt.f32.s64"(Int64(-2), Int64(1)) # 1.0f0
gated = ptx"set.lt.and.f32.s64"(a, b, gate) # gate::Bool
lanes = ptx"set.ge.s16x2"(packed_a, packed_b) # UInt32 lane masksFor scalar forms, the destination type precedes the source type. Integer destinations return an all-ones mask for true and zero for false; floating destinations return one or zero. .f16 results use Float16, while .bf16 results use BFloat16. Integer result types preserve their signedness and width.
Scalar .f16 results are formed from a 32-bit integer comparison mask, then converted to the half zero/one encoding. This avoids a ptxas widening defect where native set.f16.f16 can retain a duplicate high half through cvt.u32.u16. The public result remains Float16, including on the raw path. For .f16 sources, this lowering uses PTX ISA 6.5 instructions.
Half/BF16 x2 comparisons return two packed results. Integer destinations pack 16-bit masks; .f16x2/.bf16x2 destinations pack the corresponding floating one/zero encodings. Packed integer set uses a single type token and returns one 8-bit or 16-bit mask per lane in UInt32, including signed input formats.
Ordered floating comparisons return false when either input is NaN; unordered comparisons and .num/.nan follow their PTX definitions. .ftz is restricted to the reviewed f32/f16 comparison forms. Incorrect modifier order, comparison operators, operand widths, or predicate inputs fail before LLVM compilation, including through ptx"..."raw.
All admitted spellings have offline assembly coverage. Packed integer forms require PTX ISA 9.4 and the sm_107f family (also sm_107a); older compilers explicitly skip that partition. Their hardware execution remains unverified.
Schema-driven structured results
setp, lop3, match.sync, and elect.sync no longer rely on a small set of hand-written dual-result methods. src/ledgers/structured_results.jl expands the complete PTX 9.3 grammar into 1,134 reviewed schemas and the generic chain emitter consumes those schemas directly. This keeps scalar and grouped siblings in the same closed boundary: modifier legality, source carriers, result tuple shape, sink positions, and target metadata cannot drift between a wrapper and the fallback.
General setp uses the Julia-only leading .dual selector for the optional compare/complement result. Packed .f16x2 and .bf16x2 always return the two lane predicates. BoolOp lop3 returns (UInt32, Bool) and requires a Val{N} LUT with N in 0:255; match.all.sync.*.pred selects the optional predicate, and elect.sync always returns (UInt32, Bool). The two warp-collective families use the same convergent nomerge LLVM call-site path as the existing MMA and mbarrier wrappers. The single-predicate queries testp (source-type tail) and isspacep (unbracketed .u32/.u64 generic-address value) live in the same ledger because the generic trailing-dtype rule would misread their destination ABI.
For the mbarrier full-report form, reportValue is a PTX .b8 destination. This agrees with the CUDA Runtime API's description of cudaFabricOpStatusSourceMbarrierV1 as 1-byte aligned and 1-byte wide, and is enforced by ptxas evidence at sm_90 and sm_121. The Julia result is a zero-extended UInt16 carrier because LLVM's NVPTX inline-asm interface has no i8 register constraint; only its low byte is the opaque status value.
Arrival calls that spell destination _ use the synthetic .sink selector, for example ptx"mbarrier.arrive.sink.release.cluster.b64"(remote_addr). This distinction cannot be inferred from argument types: the otherwise identical local/generic form returns a UInt64 state token. It is especially important for a generic address produced for a remote cluster mbarrier, where the ISA requires the sink destination. Explicit shared::cluster forms are always sinks and need no selector. Base-plus-constant-offset addresses remain valid; tensor-coordinate address lists are rejected rather than truncated.
The simple WGMMA synchronization ops (wgmma.fence, wgmma.commit_group, wgmma.wait_group) still flow through the reviewed generic chain contract. Every tcgen05 form is typed-wrapper-only, including the ptxas-covered pointer forms of alloc and non-multicast commit and the before/after thread-sync fences. An exact method is authoritative and a dispatch miss fails before generic asm rendering. This is necessary because the shared opcode spans address destinations, register vectors, sinks, descriptors, fences, and MMA operands with incompatible schemas.
MX target evidence is deliberately split by PTX target class. The explicit .scale_vec::* spellings are compiled offline through ptxas at sm_100a and sm_110a; the equivalent .block16/.block32 aliases are compiled at sm_100f and sm_110f. Those checks validate source schema, inline-asm constraints, target gating, and assembler acceptance without launching a kernel. CUDACore 6.2.1 cannot emit an sm_110* module, so those cases emit the identical body at the matching sm_100a/sm_100f feature level, assert that only the .target directive changes, and invoke CUDA 13.3 ptxas directly on the sm_110a/sm_110f text. No tcgen05 runtime claim is made for the available CC 12.1 GB10, which does not implement tcgen05.
Ordered sparse MMA metadata
mma.sp::ordered_metadata is not a looser spelling of ordinary sparse MMA. For 2:4 f16/bf16/FP8 storage, each metadata nibble must list its two retained element indices in increasing order; other orderings have undefined behavior. (tf32 uses one retained index per 1:2 chunk.) PTX.jl cannot infer or repair that property from an opaque UInt32 metadata word, so callers must establish it while pruning and packing. The runtime evidence in test/gpu/ampere/gemm_sparse.jl does this by sorting each retained pair before encoding the nibble.
The typed surface deliberately mirrors only the 12 sparse MMA ABIs already supported by mma.sp. The ordered qualifier was introduced in PTX 8.5 and the classic f16/bf16/tf32 forms target sm_80+; the FP8 forms require sm_89+. Selector legality is enforced by exact Val dispatch: k16 16-bit and k8 tf32 accept Val(0) through Val(3), k32 16-bit and k16 tf32 accept Val(0) or Val(1), and k64 FP8 accepts only Val(0). A wrong selector, fragment width, or unreviewed shape/type combination fails before the generic scalar emitter. Like all mma.sync operations, these calls are warp-collective and carry convergent nomerge; they remain memory-free arithmetic operations.
The classic integer sparse surface is separate from tcgen05.mma.sp. It uses register fragments rather than tensor-memory descriptors and contains exactly 64 forms: u8/s8 at m16n8k32 and m16n8k64, u4/s4 at m16n8k64 and m16n8k128, all four A/B signedness pairs, with and without .satfinite, and both ordinary and ordered metadata. A/B register tuples contain 2/2, 4/4, 2/2, and 4/4 packed UInt32 values respectively; C/D are always four Int32 values, and metadata is one UInt32. The k32 u8/s8 and k64 u4/s4 products use selector Val(0) or Val(1); k64 u8/s8 and k128 u4/s4 require Val(0). For ordered u4/s4 storage, each 4:8 group is encoded as two ordered 2-element retained subgroups; the wrapper cannot validate those opaque metadata nibbles. Ordinary integer sparse MMA requires PTX 7.1, ordered metadata requires PTX 8.5, and all forms require sm_80+.
Extended-precision arithmetic
The extended-precision wrapper is a correctness boundary, not only an ergonomic overload. PTX CC.CF is implicit state that LLVM cannot name as an input/output constraint. Scalar typed wrappers reify it as Bool; aggregate helpers keep the entire chain in one asm unit. Generic, raw, and transpiled instruction-at-a-time fallbacks are rejected instead of emitting optimizer- unsafe calls.
The public helpers are PTX.add_with_carry, PTX.sub_with_borrow, and PTX.mul_wide; their full API documentation is in the reference.
The blocks are per-thread (convergent=false), carry sideeffect and ~{cc} but no false memory clobber, and use early-clobber word outputs so a low result cannot alias a high input that a later instruction still needs. Subtraction seeds and materializes the flag through sub.cc/subc, directly mirroring PTX borrow semantics instead of relying on a cross-family flag representation.
Host-side descriptor builders
Two opcodes consume packed 64-bit shared-memory descriptors plus (Blackwell only) a 32-bit instruction descriptor. PTX.jl ships pure bit-packing helpers for both:
| Helper | Used by |
|---|---|
wgmma_descriptor | Hopper wgmma.mma_async SMEM operand encoding (14-bit field windows + swizzle + base offset) |
tcgen05_descriptor | Blackwell tcgen05.mma SMEM operand encoding (3-bit layout vs wgmma's 2-bit; legal swizzle values only). The ISA-fixed 0b001 field is inserted internally; lbo_mode=1 selects the restricted sm_103a absolute leading-address mode. |
tcgen05_instr_desc_f16bf16_f32 | Blackwell tcgen05.mma 32-bit instruction descriptor for F16/BF16/TF32 → F32 paths. The integer-only saturation bit and reserved bits are fixed at zero. Mirrors CUTLASS/CuTe's UMMA::make_instr_desc. |
tcgen05_instr_desc_i8 | .kind::i8 instruction descriptor (u8/s8 → s32). Negation is ISA-unsupported for the integer kind, so those bits are not caller-controlled. |
tcgen05_instr_desc_ti16 | .kind::ti16 instruction descriptor (s1z4m11 → s32, PTX ISA 9.4, sm_107f family). Negation is supported; transpose is not caller-controlled because the ISA's descriptor table and its transpose-support table disagree. |
tcgen05_instr_desc_f8f6f4 | .kind::f8f6f4 instruction descriptor (independent e4m3/e5m2/e2m3/e3m2/e2m1 A/B types, f16 or f32 destination). k = 64 selects the sm107f-family wide-K encoding; `lutb = truechecks the.decompress::lut::b` operand rules. |
tcgen05_instr_desc_mxf8f6f4 | .kind::mxf8f6f4 block-scale instruction descriptor (scale-factor data IDs; scale matrix type is the ISA-fixed UE8M0; sm107f-family k and `scalealayoutencodings;lutb` operand rules). |
tcgen05_instr_desc_mxf4, tcgen05_instr_desc_mxf4nvf4 | .kind::mxf4 / .kind::mxf4nvf4 block-scale instruction descriptors (E2M1 elements; transpose ISA-unsupported; nvf4 additionally chooses the scale matrix type; sm107f-family k and `scalea_layout` encodings). |
spcompress_desc, spcompress_elemsize | spcompress (PTX 9.4, sm_107a) sparsity descriptor: selection rule plus dense element type, and the .elemsize qualifier that type requires. |
smem_addr_u32 | Convert a Core.LLVMPtr{T, AS.Shared} to its 32-bit in-CTA SMEM offset (used as the smem_addr_u32 argument to the descriptor builders). |
step_desc | Advance a wgmma SMEM-operand descriptor's start address by a byte offset — walking a SMEM ring buffer or stepping K within a tile without re-packing. |
These are not exported but are part of the documented API. Access them as PTX.wgmma_descriptor, PTX.tcgen05_descriptor, etc.
The tcgen05 builders deliberately expose semantic fields rather than every bit in the packed values. In particular, tcgen05_descriptor has no version keyword: PTX 9.3 §9.7.17.4.1 defines bits 46–48 as the fixed constant 0b001, not a version selector. Likewise, only tcgen05_instr_desc_i8 has a saturate keyword (the bit is integer-only), the i8 builder has no negate keywords and the mxf4 and ti16 builders no transpose keywords (ISA-unsupported, or ISA-contradictory, for those kinds). The architecture-gated encodings (the sm_107f-family K-dimension and scale-layout bits, the 15-bit shared-descriptor fields, the .decompress::lut::b segment bit) default to zero and are set only through their explicit keywords (k, scale_a_layout, wide, lut_k_offset). An illegal descriptor is unrepresentable through the builders, not merely rejected. Matrix addresses and byte offsets must be 16-byte aligned and fit the 18-bit descriptor input window (19-bit with wide=true); invalid swizzle encodings and values that would spill into reserved fields fail with ArgumentError instead of being truncated.
Absolute leading-address mode (lbo_mode=1) is the PTX 9.3 §9.7.17.3.1.2 escape hatch for a 48-byte K dimension. The builder enforces its shared-descriptor restrictions: 128-byte swizzling with 16-byte atomicity (BlackwellLayout.B128, code 2) and base_offset=0. Because K and major axes live outside the shared descriptor, the caller must still pair it with K-major A and B (instruction-descriptor transpose bits 15 and 16 both zero), K=48B, and the architecture-specific sm_103a target.
PTX.wgmma_descriptor — Function
wgmma_descriptor(smem_addr_u32; leading_byte_offset, stride_byte_offset,
swizzle = WgmmaSwizzle.NONE, base_offset = 0) -> UInt64Pack a wgmma.mma_async shared-memory operand descriptor. Byte offsets encode as 14-bit fields covering the high bits of an 18-bit, 16-byte-aligned window; swizzle is one of the WgmmaSwizzle codes (0 = none, 1 = 128B, 2 = 64B, 3 = 32B). Get smem_addr_u32 from smem_addr_u32.
PTX.smem_addr_u32 — Function
smem_addr_u32(p::Core.LLVMPtr{T, AS.Shared}) -> UInt32The 32-bit in-CTA SMEM offset of a shared-address-space pointer — the smem_addr_u32 argument the descriptor builders take. Bounces the pointer through inline asm with the r constraint to surface the 32-bit offset NVPTX represents internally.
PTX.step_desc — Function
step_desc(desc::UInt64, byte_offset::Integer) -> UInt64Advance a wgmma SMEM-operand descriptor's start address by byte_offset bytes. The descriptor's low 14 bits encode (smem_addr & 0x3FFFF) >> 4, so adding byte_offset >> 4 moves the base by the equivalent byte distance — as long as the carry stays within the 14-bit start_addr field (true for any in-CTA SMEM offset on H100). Used to walk a SMEM ring buffer (per-stage offset) or to step within a tile (per-wgmma-K offset on the inner loop).
byte_offset must be a multiple of 16 — the operand encoding has no representation for sub-16-byte offsets. Caller guarantees this; the helper truncates silently if violated.
PTX.tcgen05_descriptor — Function
tcgen05_descriptor(smem_addr_u32; leading_bytes, stride_bytes,
swizzle=BlackwellLayout.NONE, base_offset=0, lbo_mode=0,
lut_k_offset=0, wide=false) -> UInt64Pack the tcgen05 shared-memory descriptor from PTX 9.4 §9.7.18.4.1 Table 49. The matrix address and both byte fields must be 16-byte aligned and fit the descriptor's 18-bit input window. swizzle must be one of the five encodings in BlackwellLayout.
Bits 46–48 are always the ISA-mandated constant 0b001; reserved/fixed-zero bits are never caller-controlled. lbo_mode=1 selects the absolute leading- dimension byte-address mode. PTX 9.3 §9.7.17.3.1.2 restricts that mode to BlackwellLayout.B128 (128-byte swizzle with 16-byte atomicity) and a zero base_offset, which this builder enforces. The caller must additionally pair it with K-major A and B descriptors (both instruction-descriptor transpose bits zero), a 48-byte K dimension, and the architecture-specific sm_103a target. The default lbo_mode=0 is the relative byte-offset mode.
lut_k_offset (bit 53) is 0 or 24: with .decompress::lut::b the instruction reads a 48-byte K=128 block of compressed B and uses either its first or its second 24-byte segment. wide=true widens the matrix-start and leading-dimension fields to Table 49's 15 bits (a 19-bit aligned input window); inputs that fit the 18-bit window encode identically either way. No assembler or hardware evidence covers the widened fields — ptxas cannot inspect a register operand — so the default keeps the 14-bit encoding.
PTX.tcgen05_instr_desc_f16bf16_f32 — Function
tcgen05_instr_desc_f16bf16_f32(; m, n, ab_dtype,
a_major=:K, b_major=:K, scale_a=1, scale_b=1,
sparse=false, max_shift=0) -> UInt32Pack the PTX 9.3 §9.7.17.4.2 Table 45 instruction descriptor for the .kind::f16 / .kind::tf32 paths whose destination type is .f32 and whose A/B types are the same. ab_dtype is :f16, :bf16, or :tf32.
The integer-only saturation bit and every reserved bit are fixed at zero and are not caller-controlled. sparse=true selects the .sp descriptor bit with sparsity selector zero. A nonzero max_shift is meaningful only when the descriptor is consumed by a .ws form; legality of the complete MMA shape and form remains the responsibility of the consuming instruction wrapper.
PTX.tcgen05_instr_desc_i8 — Function
tcgen05_instr_desc_i8(; m, n, a_dtype, b_dtype,
a_major=:K, b_major=:K, saturate=false,
sparse=false, max_shift=0) -> UInt32Pack the PTX 9.4 §9.7.18.4.2 Table 51 instruction descriptor for the .kind::i8 path (destination type fixed at .s32). a_dtype and b_dtype are :u8 or :s8, independently.
Negation (bits 13–14) is not supported for .kind::i8 (§9.7.18.10 Table 62) and is not caller-controlled; transpose (a_major/b_major = :MN) is. Bit 29 (the wider-K encoding) is architecture-gated and fixed at zero, so the descriptor always encodes the base K (dense 32 / sparse 64). .kind::i8 itself is a-variant-exclusive (§9.7.18.10 target notes); legality of the complete MMA form remains the consuming wrapper's responsibility.
PTX.tcgen05_instr_desc_ti16 — Function
tcgen05_instr_desc_ti16(; m, n, scale_a=1, scale_b=1,
sparse=false, max_shift=0) -> UInt32Pack the PTX 9.4 §9.7.18.4.2 Table 51 instruction descriptor for the .kind::ti16 path: A and B are the ISA-fixed .s1z4m11 element type and the destination type .s32. Negation (scale_a/scale_b = -1) is supported (Table 51 and §9.7.18.10 Table 62 agree).
Transpose is not caller-controlled: Table 51 lists only "No Transpose" for this kind while Table 62 lists transpose as supported, and no assembler or hardware evidence resolves the contradiction, so bits 15–16 stay zero until it is. The integer saturation bit is N/A for this kind and bit 29 has no defined value; both stay zero.
PTX.tcgen05_instr_desc_f8f6f4 — Function
tcgen05_instr_desc_f8f6f4(; m, n, a_dtype, b_dtype, d_dtype=:f32,
a_major=:K, b_major=:K, scale_a=1, scale_b=1,
sparse=false, max_shift=0, k=nothing, lut_b=false) -> UInt32Pack the PTX 9.4 §9.7.18.4.2 Table 51 instruction descriptor for the .kind::f8f6f4 path. a_dtype and b_dtype are independently :e4m3, :e5m2, :e2m3, :e3m2, or :e2m1; d_dtype is :f16 or :f32.
k selects the K dimension: nothing (the default) or the base K (dense 32 / sparse 64) leaves bit 29 clear; dense k = 64 sets it. Table 51 lists no wide sparse K for this kind, so a sparse k other than 64 is rejected. The wide dense encoding does not transpose the 6-/4-bit element types (§9.7.18.10 Table 62), and lut_b = true (the .decompress::lut::b consumer) requires an E4M3, K-major B; both are enforced rather than encoded. Bit 29 is valid on sm_107f or higher in the same family only. The integer saturation bit is N/A for float kinds and not caller-controlled.
PTX.tcgen05_instr_desc_mxf8f6f4 — Function
tcgen05_instr_desc_mxf8f6f4(; m, n, a_dtype, b_dtype,
scale_a_id, scale_b_id, a_major=:K, b_major=:K,
scale_a=1, scale_b=1, sparse=false,
k=nothing, scale_a_layout=32, lut_b=false) -> UInt32Pack the PTX 9.4 §9.7.18.4.2 Table 52 instruction descriptor for the .kind::mxf8f6f4 path. a_dtype/b_dtype take the same five element types as .kind::f8f6f4; scale_a_id/scale_b_id are the scale-factor data IDs (0–3). The scale matrix type is the ISA-fixed UE8M0 (bit 23).
k selects the K dimension: nothing or the base K (dense 32 / sparse 64) leaves bit 31 clear; dense 64 / sparse 128 sets it. scale_a_layout is 32 (the default) or 128 lanes (bit 26). Both encodings are valid on sm_107f or higher in the same family only. The wide dense encoding does not transpose the 6-/4-bit element types and lut_b = true requires an E4M3, K-major B (§9.7.18.10 Table 62); both are enforced rather than encoded.
PTX.tcgen05_instr_desc_mxf4 — Function
tcgen05_instr_desc_mxf4(; m, n, scale_a_id, scale_b_id,
scale_a=1, scale_b=1, sparse=false, sparsity_version=0) -> UInt32Pack the PTX 9.4 §9.7.18.4.2 Table 53 instruction descriptor for the .kind::mxf4 path. Element types are the ISA-fixed E2M1 and the scale matrix type the ISA-fixed UE8M0; scale_a_id/scale_b_id must be 0 or 2. Transpose is not supported for the mxf4 kinds (§9.7.18.10 Table 62) and the transpose bits are not caller-controlled; negation is.
sparsity_version (bit 12) selects both the target and the metadata layout: 0 uses pair-wise 4:8 sparsity on sm100a/sm103a/sm110a; 1 uses element-wise 2:4 sparsity on sm107a. A mismatch is undefined behavior. The default is 0; callers targeting sm107a must explicitly pass 1 and supply matching metadata. k selects the K dimension (bits 3 and 31): nothing or the base K (dense 64 / sparse 128) encodes 0; dense 96 / sparse 192 encodes 1; dense 128 encodes 2. `scalealayoutis 32 (the default) or 128 lanes (bit 26). The wider K codes and the 128-lane layout are valid onsm107f` or higher in the same family only.
PTX.tcgen05_instr_desc_mxf4nvf4 — Function
tcgen05_instr_desc_mxf4nvf4(; m, n, scale_dtype, scale_a_id, scale_b_id,
scale_a=1, scale_b=1, sparse=false, sparsity_version=0,
k=nothing, scale_a_layout=32) -> UInt32Pack the PTX 9.4 §9.7.18.4.2 Table 53 instruction descriptor for the .kind::mxf4nvf4 path — identical to tcgen05_instr_desc_mxf4 except the scale matrix type is caller-chosen: scale_dtype is :ue4m3, :ue8m0, or :ue5m3 (bits 23–24).
PTX.spcompress_desc — Function
spcompress_desc(; op, dtype) -> UInt32Pack the spcompress sparsity descriptor (PTX 9.4 §9.7.10.30, Table 38). op is :max, :maxabs, :min, or :minabs; dtype is :f16, :bf16, :u8, :s8, :e5m2, :e4m3, :e3m2, or :e2m3. The instruction's .elemsize qualifier must match spcompress_elemsize(dtype).
PTX.spcompress_elemsize — Function
spcompress_elemsize(dtype) -> SymbolThe .elemsize qualifier (:b8 or :b16) that spcompress requires for the element type named in its descriptor.
GMMA layout helpers
For wgmma.mma_async SMEM operands, the descriptor's leading_byte_offset / stride_byte_offset / swizzle triple is fully determined by the tile geometry (dtype, M-or-N, K, major axis). The four canonical GMMA layout families (INTERLEAVE / B32 / B64 / B128) cover all wgmma-compatible SMEM tile widths.
PTX.pick_gmma_layout — Function
pick_gmma_layout(; elem_bytes, m_or_n, k, major) -> GmmaLayoutCanonical GMMA layout for a (dtype, M-or-N, K, major) SMEM tile. major is :K or :MN. See module-top comment for the full mapping.
PTX.layout_for_a — Function
layout_for_a(; dtype, m, k) -> GmmaLayoutK-major GMMA layout — natural for row-major A (MxK with K-fast), and also the right pick for operand B when the kernel lays B as row-major KxN in SMEM with K-fast (the common case across the Hopper kernels in this repo). Use this for either operand whose K-dimension is fastest-varying in SMEM.
PTX.layout_for_mn_major — Function
layout_for_mn_major(; dtype, k, n) -> GmmaLayoutMN-major GMMA layout — operand B laid as col-major KxN in SMEM with N-fast (i.e. the N-or-M dimension is fastest-varying). Required only when the SMEM tile genuinely needs MN-fastness and a transposed wgmma trans_b=1 flag is used. Most Hopper kernels in this repo use layout_for_a for both operands and let wgmma run with trans_b=0 against a K-fast B tile; reach for layout_for_mn_major only when the data layout forces the issue.
Register dependencies on waits
PTX.wait_registers attaches register values to a wait's input/output operands. Consumers must use the returned values; calling a void wait and returning the original values does not establish that compiler dependency. The wait keeps its existing memory clobber and collective semantics.
The initial surface covers tcgen05.wait::ld.sync.aligned and tcgen05.wait::st.sync.aligned, with scalar or homogeneous tuple values. Adjacent 32-bit tuple elements use paired 64-bit operands. The ptxas/wait_registers test compiles six attention configurations with and without register bindings and compares machine-instruction encodings under the same toolchain, including any spill instructions. Debug metadata is stripped before backend code generation so it cannot perturb the instruction comparison.
Register operands and memory clobbers describe different compiler effects; see NVIDIA's inline-PTX documentation and LLVM's tied input constraints. This is an API addition for expressing register dependencies; no wrong-result bug has been demonstrated in the existing no-argument wait.
PTX.wait_registers — Function
wait_registers(wait_instruction, values)Execute wait_instruction and return values with an explicit compiler register dependency on that wait. Use the returned values for subsequent computation. Their bits, types, and tuple structure are preserved.
Supported waits are ptx"tcgen05.wait::ld.sync.aligned" and ptx"tcgen05.wait::st.sync.aligned". Values may be a 32- or 64-bit integer or floating-point scalar, or a homogeneous tuple of those scalars (including an empty tuple). The instruction retains its full hardware wait scope; listing values does not restrict which asynchronous operations it waits for.
values = ptx"tcgen05.ld.sync.aligned.32x32b.x32.b32"(tmem_addr)
# Independent work can run while the load is in flight.
values = PTX.wait_registers(ptx"tcgen05.wait::ld.sync.aligned", values)
# Consume the returned values here.The wait remains side-effecting and convergent, with a memory clobber. Tied input/output operands additionally express register dependencies; a memory clobber alone does not express this dataflow. Adjacent 32-bit tuple elements travel through paired 64-bit operands to preserve register packing. The wait emits no arithmetic on the values.
The no-argument instruction call remains available. This API makes a compiler dependency expressible; it does not imply a demonstrated wrong-result bug in that existing call.
Host-side TMA descriptors
Hopper TMA (cp.async.bulk.tensor.*) consumes a 128-byte CUtensorMap blob built host-side by the CUDA driver's cuTensorMapEncodeTiled. PTX.jl wraps the driver call so descriptors take Julia types / symbols instead of raw CUtensorMapDataType enums, with a thin convenience helper for the common 2D row-major case.
PTX.upload_tma_descriptor uploads the encoded descriptor and returns (; ptr, blob): blob owns the allocation, and ptr is the borrowed PTX.TMADescriptorPtr passed to kernels. Its AS.Const address-space type is the TMA wrapper carrier convention; the allocation lives in device global memory. Pointer conversion happens on the host.
Preserve the returned owner and the source tensor until GPU work completes, including synchronization after an asynchronous launch. The uploader's example shows the GC.@preserve pattern. Uploading copies the descriptor; it does not take ownership of the tensor whose address it contains.
Encoding and upload live in ext/CUDACoreExt.jl and become available when CUDA.jl or CUDACore is loaded alongside PTX.
PTX.tensor_map_encode_tiled — Function
tensor_map_encode_tiled(dtype, global_addr, global_dim, global_strides,
box_dim; kwargs...) -> CuTensorMapBuild a CUtensorMap for a tiled TMA descriptor. Calls cuTensorMapEncodeTiled.
Arguments (innermost-first convention, matching the driver):
dtype— Julia type (Float32,UInt16, …) or symbol (:bf16,:tf32, …)global_addr—Ptr{T},CuPtr, orUInt. Caller-owned; the driver stores it in the descriptor (usecuTensorMapReplaceAddressto swap later).global_dim::NTuple{N, <:Integer}— tensor shape, innermost first.global_strides::NTuple{N-1, <:Integer}— outer-dim strides in bytes. Length must beN-1(the innermost stride iselem_bytes * 1, implicit).box_dim::NTuple{N, <:Integer}— per-launch tile shape, innermost first.
Keyword arguments:
elem_strides::NTuple{N, <:Integer} = (1, 1, …)— sub-tile stride.interleave::Symbol = :NONE—:NONE/:B16/:B32.swizzle::Symbol = :NONE—:NONE/:B32/:B64/:B128(plus Blackwell atom variants).l2_promotion::Symbol = :NONE—:NONE/:B64/:B128/:B256.oob_fill::Symbol = :NONE—:NONE/:NAN_REQUEST_ZERO_FMA.
Requires the CUDACore package extension: load CUDA.jl (which depends on CUDACore) or CUDACore itself. Without it this function has no methods, and calling it raises a MethodError whose hint names the missing package.
PTX.tensor_map_tile_2d — Function
tensor_map_tile_2d(dtype, global_addr, rows, cols, box_rows, box_cols;
swizzle=:B128, oob_fill=:NONE) -> CuTensorMapConvenience for a 2D row-major (rows, cols) tensor + (box_rows, box_cols) tile. Innermost dim is cols. Stride is cols * elem_bytes.
Mirrors pyptx synthesize_tma_descriptor (2D path). Caller is responsible for keeping box_cols * elem_bytes consistent with the swizzle (e.g. 128B for :B128).
PTX.upload_tma_descriptor — Function
upload_tma_descriptor(tmap::CuTensorMap) -> (; ptr, blob)Copy the 128 bytes of tmap into a new device global-memory allocation. Returns a NamedTuple with:
blob: theCuArray{UInt8,1}that owns the descriptor allocation.ptr::TMADescriptorPtr: its borrowed kernel-argument pointer, converted from the allocation's raw address on the host.AS.Constis the TMA wrapper carrier convention; the allocation is in global memory.
The upload is a snapshot: later changes to tmap.data do not update blob. Keep the returned owner (or its blob) alive until all GPU work using ptr completes. The source tensor whose address was encoded in tmap remains caller-owned and must also stay alive. Keeping only ptr retains neither allocation. Use GC.@preserve around launch and completion, for example:
using PTX, CUDA
src = CuArray(reshape(UInt16.(1:64), 8, 8))
GC.@preserve src begin
tmap = PTX.tensor_map_tile_2d(:u16, pointer(src), 8, 8, 8, 8;
swizzle=:NONE)
descriptor = PTX.upload_tma_descriptor(tmap)
GC.@preserve descriptor begin
@cuda kernel!(descriptor.ptr) # kernel accepts PTX.TMADescriptorPtr
CUDA.synchronize()
end
endAllocation and upload use the current CUDA device and stream. Order work on another stream after the upload before using ptr there.
Requires the CUDACore package extension: load CUDA.jl or CUDACore itself. Without it this function has no methods; a MethodError hint names the missing package.
PTX.TMADescriptorPtr — Type
TMADescriptorPtr = Core.LLVMPtr{UInt8, AS.Const}Borrowed kernel-argument pointer for a 128-byte TMA descriptor. AS.Const is the carrier convention used by PTX.jl's TMA wrappers; the allocation created by upload_tma_descriptor lives in device global memory. The uploader converts the allocation's raw address to this type on the host.
This pointer keeps neither the descriptor allocation nor its referenced tensor alive. Preserve their owners until all GPU work using them completes.
Pitfalls in warp-specialized kernels
Things ptxas and LLVM do silently that cost hours to rediscover:
setmaxnregis ignored by ptxas unless the kernel declares.reqntid; passminthreads=Nthrough@cudaor the compiler config, and checkEIATTR_REG_COUNTin the cubin.@inbounds CuDynamicSharedArray(...): the@boundscheckemits agpu_report_exceptioncall in the entry region.- Indexing an
NTuplewith a runtime index demotes the tuple to local memory; make the index aValtype parameter. Largentuple(...) doclosures become real device calls; write explicit tuples or checkcode_llvmforcall .*julia_. UInt32(x)on a loop-carriedIntleaves liveInexactErrorbranches; use% UInt32when the value is exact by construction.- Debug loop:
code_llvmand grepcall .*julia_|report_exception|alloca;nvdisasm -cand grepLDL|STL|CALL;ptxas -vfor spill counts.
When to extend
Most chain-default coverage is sufficient. Reach for a wrapper when:
- ptxas rejects the chain output ("Arguments mismatch", wrong constraint letter on a shared-AS pointer, missing brackets on a memory operand);
- the op returns multiple values;
- the operand layout has a brace group, tensor-coord vector, or other shape with no
$Nrendering; - the dispatch needs to key on a fragment shape or count that the chain can't see.
The pattern: copy the closest existing wrapper file, adjust the table and asm template, add a for loop that emits one typed method per combination — spelling literal forms as ptx"..."(...) = ... heads and routing the generator's bookkeeping through register_wrapper! / wrapper_intrinsic_call (see src/wrappers/registry.jl). ~80 LOC for a new family in most cases.
PTX 9.4 readonly loads and proxy fences
The readonly-load spelling places the proxy after the result type:
value = ptx"ld.global.u32.proxy::readonly"(global_ptr)
value = ptx"ld.u32.proxy::readonly"(generic_ptr)The address must refer to global memory, or to global/constant memory through generic addressing; its contents must remain read-only for the kernel's lifetime. Scalar result types preserve their width and signedness. The finite transpiler grammar does not yet include these loads.
The generic fence chains include:
ptx"fence.proxy.alias.release.sys"()
ptx"fence.proxy.alias.acquire.sys"()
ptx"fence.proxy.async::generic.release.sync_restrict::shared::cluster::read.cluster"()These require PTX ISA 9.4 and sm_90 or higher. The cluster-read fence orders shared-cluster reads across the specified proxies and is non-cumulative: do not rely on it to transitively order other accesses. The assembly tests check the emitted instructions and target floors; concurrent ordering still depends on the surrounding synchronization protocol.
Stochastic conversion (cvt.rs) does not permit .pzo. Other rounding and saturation modifier combinations continue to be checked by ptxas.