- Core-matrix layout: each 8x8 BF16 tile (128B) contiguous in SMEM - K-major NONE descriptor: LBO=1 (16B), SBO=block_k/8, lbo_mode=0 - MMA K-tiling: tcgen05.mma uses K=16 per call, tile for hd>16 - write_smem_kmajor: converts row-major to core-matrix layout - write_smem_ktile: extracts single K-tile in core-matrix layout - test_umma_qk.cu: minimal hd=16, sk=128 test (single MMA call) - Previous UMMA descriptors were wrong (row-major SMEM, wrong LBO/SBO)
40 KiB
You're right. Let me study the canonical layout properly and implement it correctly.Now I have the complete canonical layout specification. Let me also get the Atom layouts:Now I understand the full picture. Let me get the atom layout definitions:Now I see — the SM100 uses the same atoms as SM90 for the swizzle patterns. The Layout_MN_SW128_Atom is the MN-major atom with 128B swizzle, and Layout_K_SW128_Atom is the K-major atom.
The atom layouts are built from ComposedLayout<Swizzle<3,4,3>, ptr, Layout<Shape<_1024,_8>, Stride<_1,_1024>>>. For BF16 (16-bit), the upcast gives us the element-level layout.
Let me understand the canonical layout by working through the math for BF16 MN-major with SWIZZLE_128B at shape (128, 64):
The Layout_MN_SW128_Atom<bf16> in element-level is Shape<(1024, 8)> with stride (1, 1024) plus Swizzle<3,4,3>. In uint128_t units (8 BF16 per uint128_t): Shape<(128, 8)> with stride (1, 128).
Then tile_to_shape tiles this atom to cover (128, 64):
- MN dimension: 128 rows. Atom covers 128 rows (the first 128). Need 1 tile.
- K dimension: 64 columns = 8 uint128_t per row. Atom covers 8 uint128_t. Need 1 tile.
- So for (128, 64) with 128B swizzle, the entire matrix is exactly one atom! The layout IS the atom.
Wait, but the canonical form from make_uma_desc has ((SwizzleAtomMNSize, n), (SwizzleAtomKSize, k/AtomK)). For SWIZZLE_128B: SwizzleAtomMNSize = 8, SwizzleAtomKSize = 8. So the canonical is ((8, 8), (8, 1)) in uint128_t units.
That's 8 × 8 = 64 uint128_t = 64 * 16 = 1024 bytes per "page". The first dimension has 8 of these, giving 8 * 1024 = 8192 bytes. That's only 8192 / 128 = 64 rows. But we need 128 rows.
Hmm, I'm getting confused by the CuTe layout abstraction. Let me just implement the SMEM layout from first principles.
The key insight: For the UMMA descriptor, the SMEM data must be laid out so that the descriptor's leading_byte_offset and stride_byte_offset correctly describe how to walk the matrix. The descriptor is NOT a general matrix descriptor — it's specifically designed for the tiled access pattern of the MMA operation.
Let me take the most pragmatic correct approach: **study the exact SMEM layout used by the working CuTeDSL FMHA kernel, then replicate it byte-for-byte in raw CUDA.**Now I understand the atom selection. For BF16 (16-bit) with MN-major and major_mode_size = 128 (M=128 rows):
- major_mode_size_bits = 128 * 16 = 2048 bits
- 2048 % 1024 == 0 → MN_SW128 (128B swizzle, the best option)
For BF16 with K-major and major_mode_size = 64 (K=64 in our case):
- major_mode_size_bits = 64 * 16 = 1024 bits
- 1024 % 1024 == 0 → K_SW128 (128B swizzle)
So the FMHA kernel uses SW128 swizzle for both A (Q, MN-major) and B (K^T, K-major). Now I need to understand the exact SMEM layout that SW128 produces.
The swizzle atom MN_SW128 for BF16 is defined in the CUTLASS C++ as Layout_MN_SW128_Atom<bf16_t>. This creates a layout where:
- 8 consecutive uint128_t values form a "row" in the swizzled domain
- The swizzle pattern XORs bits to avoid bank conflicts
Instead of trying to replicate the CuTe layout algebra in raw CUDA, let me take the most direct approach: **write a CuTeDSL script that prints the SMEM layout for our specific shapes, extract the offset table, and implement the equivalent layout in raw CUDA.**Now update the QK verify kernel to use the swizzled SMEM write functions:Also need to add <cstdint> to the QK verify header:Also need to fix the SMEM size in the test:All zeros from the GPU. The MMA didn't write anything to TMEM, or the SMEM data isn't in the right place. The kernel didn't crash (no "misaligned address" error), so the descriptor format is at least somewhat valid now. But the MMA didn't produce output.
The zero output suggests either: (1) the MMA ran but produced zeros because SMEM data was in the wrong layout, or (2) the MMA didn't run at all.
Let me check: the if (tid == 0) printf("[qk] desc_q=...") debug line didn't appear. That means the kernel might have crashed before reaching it, or the printf was from a different code path. Let me look at the SMEM write functions — the zeroing of rows 1-127 might be the issue (the loop structure is wrong).The zeroing loop for rows 1-127 is wrong — NTHREADS / HD = 192 / 64 = 3, so we only cover rows 1, 4, 7, ... That's a bug but shouldn't cause zeros in the output (row 0 is loaded correctly).
The bigger issue: the SMEM for Q is 8192 * 8 = 65536 BF16 per atom. For HD=64 with atoms_n = 8, that's 65536 BF16 = 128 KB per matrix. Two matrices = 256 KB. The SMEM budget is 232 KB. This overflows!
Actually wait, I calculated atoms_n = (HD + 7) / 8 = 8 for HD=64. And smem_q = 1024 * 8 = 65536 BF16 = 128 KB. That's too much. The atom has 1024 rows and 8 columns. But our matrix only has 128 rows. We don't need 8 MN-tiles — we need 1 atom (which covers 1024 rows, way more than 128).
The issue is that the MN_SW128 atom is (1024, 8) — it already covers 128 rows. For (128, 64) we need:
- 128 rows / 1024 = 1 atom in M dimension (partially used)
- 64 columns / 8 = 8 atoms in N dimension
So smem_q = 1024 * 8 = 65536 BF16. That IS 128 KB. For both Q and K, that's 256 KB, which exceeds the 232 KB budget.
But the CuTeDSL FMHA works at hd=64 with 128 KB SMEM. The difference is that the CuTeDSL kernel uses double-buffering with kv_stage=2, and the SMEM is:
- sQ: 32 KB (1 stage, 128×64×2)
- sK: 64 KB (2 stages, 2×128×64×2)
- sV: 64 KB (2 stages)
- Total: 160 KB
So the CuTeDSL FMHA uses only 128×64×2 = 16 KB per matrix per stage, not 128 KB. The SW128 swizzle doesn't actually increase the data size — it's just a permutation of the same 16 KB of data within the same address space.
The key realization: the SW128 swizzle is a PERMUTATION of the data, not a padding. The same number of bytes is needed. The UMA descriptor just needs to know the strides in the canonical (swizzled) layout, and the data must be written in the correct (swizzled) order so the hardware reads it correctly.
So the SMEM needed for (128, 64) BF16 is still 128 × 64 × 2 = 16 KB, not 128 KB. The atom size of 1024 × 8 = 8192 BF16 = 16 KB is exactly one "page" of the swizzled layout. Our matrix fits in 8 such pages (64/8 = 8 columns). But 8 pages × 16 KB = 128 KB? No — the 1024 rows in the atom are VIRTUAL. The physical data for 128 rows × 8 columns is 128 × 8 × 2 = 2 KB per atom column. With 8 atom columns, that's 16 KB total. The swizzle just permutes where each element is stored within those 16 KB.
OK so the SMEM is 16 KB per matrix, not 128 KB. My test was computing it wrong. The swizzle layout uses the same total space as row-major — it just rearranges elements within that space.
But then my umma_smem_write_mn_sw128 function computes logical = row + (col / 8) * 8192 + (col % 8) * 1024 which gives offsets up to 7 * 8192 + 7 * 1024 = 65536 BF16. That's 128 KB! The swizzle doesn't expand the data, but my logical offset calculation is wrong — it's computing offsets as if the atom is repeated with stride 8192, but the actual data fits in 16 KB.
The issue is my understanding of tile_to_shape. Let me think about this correctly:
The MN_SW128 atom for BF16: Shape<(1024, 8)>, Stride<(1, 1024)>. This means:
- Element at (m, n) within the atom is at offset m + n * 1024
- The atom covers 1024 BF16 in M and 8 BF16 in N
- Total size: 1024 * 8 = 8192 BF16 = 16 KB
For our (128, 64) matrix:
- M = 128, which fits within the atom's 1024 rows
- N = 64, which needs 64/8 = 8 repetitions of the atom's 8-column width
tile_to_shape repeats the atom: Shape<8, 8> with Stride<1, 8192> (repeating the 8192-element atom).
- Element at (m, n): offset = (m % 1024) + ((n % 8) * 1024) + (m / 1024) * 8192 * 8 + (n / 8) * 8192
But m is always < 1024 (our matrix has 128 rows), so m / 1024 = 0. Thus:
- offset = m + (n % 8) * 1024 + (n / 8) * 8192
For m=0..127, n=0..63:
- Max offset = 127 + 7 * 1024 + 7 * 8192 = 127 + 7168 + 57344 = 64639 BF16
- That's 128 KB!
This IS 128 KB, not 16 KB. The SW128 atom with 1024 rows uses 16 KB per 8 columns. With 8 repetitions (64 columns), it's 8 × 16 KB = 128 KB.
But the CuTeDSL FMHA only uses 32 KB for Q at hd=64! How? Because the Q matrix is (128, 64) = 16 KB, but the SW128 layout wastes space (only 128 of 1024 rows used, 8 of 8 columns used per atom). So the swizzled layout is 12.5% efficient (128/1024 = 1/8).
This means the CuTeDSL FMHA must NOT use the SW128 atom for Q. Let me re-check what the FMHA actually uses:The layout is computed from qk_tiled_mma which includes the tile shape. Let me check what tile shapes are used. The FMHA kernel configuration is complex — the MMA tile shape determines the SMEM layout. The layout function returns the right SMEM layout for the given MMA tile.
The problem is clear: I'm trying to reconstruct the SMEM layout without understanding the full pipeline. The UMA descriptor is tightly coupled to the MMA tile shape, the swizzle pattern, AND the data layout. Getting any one of these wrong means the MMA produces garbage.
Let me take a completely different approach. Instead of trying to reverse-engineer the UMA descriptor format, let me write a CuTeDSL Python script that prints the exact descriptor values and SMEM layout for our specific configuration (hd=64, MN_SW128, shape (128, 64)). Then I can hardcode those descriptor values and replicate the exact SMEM layout.Need to push first:The CuTeDSL import requires the MLIR bindings. Let me try importing cute directly:CRITICAL FINDINGS:
- Q layout footprint is 126 KB, K is 241 KB — way more than the 16 KB row-major size
- Only 1024 unique offsets out of 8192 expected — the layout has MASSIVE collisions!
- K layout: (0,0), (0,1), (0,7) all map to offset 0 — multiple elements overwrite each other
My swizzle layout calculation is fundamentally wrong. The offsets are colliding because my tile_to_shape logic is incorrect. The atom has 1024 rows, but I'm mapping multiple (row, col) pairs to the same offset.
The problem: my K_SW128 offset formula (row % 8) + col * 8 + (row // 8) * 8192 gives the same offset for (0,0), (0,1), (0,7) because for row=0, the formula is 0 + col * 8 + 0 = col * 8. But for col=0,1,7 with mn % 1024 = col, the offset is 0 + 0*8 + 0 = 0, 0 + 1*8 + 0 = 8, 0 + 7*8 + 0 = 56. Wait, that's different values. But the script says they're all 0. Let me re-check...
Oh! The swizzle is XORing values that happen to cancel out. For K_SW128, offset (0,0) = 0 + 0 + 0 = 0, swizzle(0) = 0. Offset (0,1) = 0 + 1*8 + 0 = 8, swizzle(8) = 8 ^ ((((8>>3) ^ (8>>7)) & 7) << 3) = 8 ^ (((1^0) & 7) < 3) = 8 ^ 8 = 0. So the swizzle maps 8 to 0! That's a collision with offset 0.
The swizzle is NOT a permutation — it's a bank-conflict-avoidance transform that must be applied to the CORDINATE, not to the offset. The layout description in CuTe is: the swizzle is applied to the 1D address within the atom. The atom's internal layout is contiguous, and the swizzle permutes the addresses within the atom's footprint. The tile_to_shape then tiles atoms.
I've been computing it wrong. The correct approach:
- Compute the 1D offset within the atom (before swizzle)
- Apply the swizzle to get the actual SMEM address
- The atom is then tiled across the full matrix
But the atom for MN_SW128 is (1024, 8) with stride (1, 1024). The atom's total size is 1024 * 8 = 8192 BF16. The swizzle permutes addresses 0..8191. Then we tile this atom to cover (128, 64).
But we only use 128 of the 1024 rows. The swizzle doesn't reduce the space — it permutes within the full 8192-element atom. So the SMEM for the atom IS 16 KB, and we need 8 atoms for 64 columns = 128 KB.
Wait — but the CuTeDSL FMHA uses 32 KB for Q at hd=64. Let me re-check what SMEM layout the FMHA actually uses. Maybe it uses a different swizzle atom.
Let me look at the actual Q SMEM layout computation in the FMHA Python:The make_smem_layout_a takes the MMA tile shape and the MMA tiler, NOT the full matrix shape. This computes the SMEM layout for a SINGLE MMA tile (which is the unit of computation). The MMA tile is (128, 64) for our case.
But the function takes qk_tiled_mma and qk_ma_tiler. The MMA tiler determines the inner tile dimensions. The actual Q SMEM layout is for the MMA tile, not the full matrix.
The SMEM for Q is q_stage * tile_size, where tile_size is the MMA tile. For stage=1 and tile (128, 64) BF16 = 16 KB, Q SMEM = 16 KB. The swizzle operates WITHIN this 16 KB tile, not across the full 128 KB that my calculation implied.
I think my offset computation was wrong because I was treating the tiling wrong. The atom (1024, 8) with stride (1, 1024) means that within the atom, column 1 starts at offset 1024. But our matrix only has 128 rows — we don't use rows 128-1023. The atom still reserves 1024 elements per column, but only 128 are populated.
Actually no — tile_to_shape for (128, 64) with atom (1024, 8) would need to REPEAT the atom to cover 64 columns (8 atoms) but NOT repeat for rows (the atom already covers 1024 rows). The total space is 8 atoms × 8192 elements = 65536 elements = 128 KB.
But the CuTeDSL FMHA only uses 32 KB for Q at hd=64 with 1 stage. So either my understanding of the layout is wrong, or the FMHA uses a different atom.
Let me just print the actual SMEM sizes from the CuTeDSL FMHA on the B200:OK, I'm going in circles trying to reconstruct the CuTe layout algebra in Python. Let me take the most direct approach: **write a tiny CuTeDSL FMHA kernel that prints the UMA descriptors at JIT time, then extract the values.**CuTeDSL requires the MLIR runtime. Let me try using the venv that the FMHA tests use:The CuTeDSL imports need the MLIR bindings. Let me use the system path:The MLIR bindings aren't compiled. But the venv's nvidia_cutlass_dsl has them:It works! Let me now print the actual layouts:The cute module isn't in the venv. It's part of the CuTeDSL runtime that requires the MLIR bindings. The FMHA test scripts work because they use the CUDA JIT compilation which has the bindings.The cute Python module isn't installed. It needs to be compiled from the CuTeDSL source. This is a toolchain dependency I can't easily set up.
Let me take a different approach entirely. Instead of trying to use CuTeDSL Python to print descriptors, let me study the C++ make_uma_desc function in detail and replicate its logic exactly. I have the full source on the B200.Now I have the COMPLETE descriptor construction. The key is:
For MN-major with SWIZLE_128B:
- Canonical layout:
((8, n), (8, k))with strides((1, LBO), (8, SBO))in uint128_t units stride_byte_offset = stride_11 = SBO(the "outer" K-dimension stride)leading_byte_offset = stride_01 = LBO(the "inner" K-dimension stride)
For K-major with SWIZZLE_128B:
- Canonical layout:
((8, n), (2, 1))with strides((8, SBO), (1, LBO)) stride_byte_offset = stride_01 = SBOleading_byte_offset = stride_10 = 1
The descriptor takes the strides from the logical_divide of the SMEM layout. I need to compute the logical_divide of the SW128 atom tiled to shape (128, 64).
For MN_SW128 with shape (128, 64) in uint128_t:
- Shape = (16, 8) (128 BF16 / 8 per uint128_t = 16, 64 / 8 = 8)
- The atom is (128, 1) with stride (1, 128) in uint128_t units
Wait, the MN_SW128 atom for BF16 has shape (1024, 8) in BF16 = (128, 1) in uint128_t units, with stride (1, 128).
For the full (128, 64) matrix in uint128_t = (16, 8):
- Tiling the atom (128, 1) to (16, 8):
- We need 16/128 = 0.125... that doesn't work.
Actually, tile_to_shape uses logical_divide, not simple tiling. The atom (128, 1) with stride (1, 128) tiled to (16, 8) would be:
- 16 = 128 * 0 + 16 (only 16 of the 128 rows used)
- 8 = 1 * 8 (8 tiles of the 1-column atom)
This is the problem — the atom has 128 uint128_t rows (1024 BF16), but we only use 16 (128 BF16). The rest is padding.
The logical_divide of (16, 8) layout with Tile (128, 1):
- Shape: ((128, 0), (1, 8)) — 0 complete 128-groups in M, 8 complete 1-groups in N
- This doesn't work — we have 16 rows, not a multiple of 128.
So the atom (128, 1) is TOO BIG for our (16, 8) shape. The tile_to_shape function would need to handle this. Looking at the CuTe code:
tile_to_shape for shape (16, 8) with atom (128, 1) stride (1, 128):
- logical_divide: M: 16 / 128 → ((128, 16), (1, 8))? No, 16 doesn't divide 128.
Actually, logical_divide doesn't require the shape to be a multiple of the tile. It produces a layout where the inner dimension is the tile and the outer is the remainder. For 16 with tile 128: logical_divide(16, 128) = (16, 1) — just 16 elements, 0 complete tiles.
So the make_umma_desc would compute canonical_layout from logical_divide(u128_tensor.layout(), Tile<_8, _8>{}). The Tile is (8, 8) because SwizzleAtomMNSize = 8 and SwizzleAtomKSize = 8 for SW128.
For our (16, 8) uint128_t shape with layout stride (1, 128):
logical_divide(shape(16, 8), Tile(8, 8)):- M: 16 / 8 = 2 complete tiles, 0 remainder
- K: 8 / 8 = 1 complete tile, 0 remainder
- Result: ((8, 2), (8, 1)) with strides ((1, 8), (16, 128))
Wait, the stride of the original layout is (1, 128). After logical_divide with Tile(8, 8):
- The 8 sub-tile in M gets stride 1 (contiguous)
- The 2 outer tiles in M get stride 8 (jump 8 uint128_t)
- The 8 sub-tile in K gets stride 16 (jump 16 = stride for 2 M-groups)
- Hmm, this doesn't seem right. Let me compute it properly.
For a layout with shape (16, 8) and stride (1, 16):
- Element at (m, k) = m + k * 16
- logical_divide with Tile (8, 8):
- (m, k) → ((m%8, m//8), (k%8, k//8))
- Shape: ((8, 2), (8, 1))
- Stride: inner M = 1, outer M = 8, inner K = 16, outer K = 16*8 = 128
Wait, but the original stride is (1, 16) not (1, 128). The layout depends on how the data is arranged in SMEM.
I realize I need to know the ACTUAL SMEM layout produced by make_smem_layout_a for our specific shape. Without being able to run the CuTeDSL Python on the B200, I can't get the exact layout.
Let me try one more thing — the existing FMHA test on the B200:Now I see — the FMHA uses smem.allocate_tensor with swizzle=q_smem_s.inner. The SMEM layout has an inner (swizzle) and outer (layout) component. The outer is the canonical layout, and the inner is the swizzle atom.
The key is line 258-259: sQ = smem.allocate_tensor(element_type=..., layout=q_smem_s.outer, byte_alignment=128, swizzle=q_smem_s.inner). The CuTe runtime handles the swizzle automatically when you allocate a tensor this way.
For our raw CUDA code, we need to manually compute the swizzled addresses. The swizzle is applied by the SMEM allocator, and the UMMA descriptor describes the un-swizzled (canonical) layout. The hardware then applies the inverse swizzle when reading.
Wait — that changes everything! The UMMA descriptor describes the CANONICAL (pre-swizzle) layout, and the hardware applies the swizzle when reading from SMEM. So the data in SMEM IS in the canonical layout, and the swizzle is implicit in the descriptor.
But smem.allocate_tensor with swizzle=q_smem_s.inner suggests the data is SWIZZLED in SMEM (the allocator applies the swizzle). And the UMMA descriptor needs to know this swizzle to UN-swizzle during the read.
Actually, looking at the CUTLASS C++ code for make_umma_desc:
uint64_t desc = 0;
auto [canonical_layout, logical_shape] = logical_divide(tensor.layout(), Tile<Shape<_8,_8>>{});
desc = (start_address & 0x3FFF);
desc |= ((canonical_layout.stride_01() & 0x3FFF) << 16); // LBO
desc |= ((canonical_layout.stride_11() & 0x3FFF) << 32); // SBO
desc |= (1ULL << 46); // version
desc |= (static_cast<uint64_t>(layout_type) << 61);
``
The descriptor is computed from the `canonical_layout` which is the result of `logical_divide` of the SWIZZLED SMEM layout. So the swizzle IS applied to the SMEM data, and the descriptor describes the strides of the SWIZLED layout.
The hardware uses the descriptor strides to walk through the SWIZZLED SMEM data. The swizzle type in the descriptor tells the hardware how the data is permuted so it can read it correctly.
OK — I now understand the full picture. Let me implement it properly. The steps are:
1. **Apply the SW128 swizzle to the SMEM data** (write Q and K in swizzled order)
2. **Compute the canonical layout strides** from `logical_divide` of the swizzled layout with Tile(8, 8)
3. **Build the UMMA descriptor** with these strides and the SW128 layout_type
The canonical layout for MN_SW128 BF16 with shape (128, 64):
- In uint128_t: shape (16, 8), swizzled layout strides need logical_divide with Tile(8, 8)
- But the swizzled layout is the CuTe `ComposedLayout<Swizzle, ptr, Layout>`
The `logical_divide` of a ComposedLayout with a Tile produces a canonical layout. For SW128:
- The atom layout is (128, 1) uint128_t, stride (1, 128), with Swizzle<3,4,3>
- Tiled to (16, 8): shape ((8, 2), (1, 8)), strides ((1, 8), (128, 128))
Wait, the logical_divide of a swizzled layout is complex. Let me just compute it by evaluating the layout for a grid of coordinates and extracting the strides.
Actually, I think the simplest approach now is: **write the SMEM data in UN-SWIZZLED (canonical) order, use SWIZZLE_NONE (layout_type=0), and construct the descriptor for the un-swizzled layout.** The SW128 swizzle is an optimization for bank conflicts, not a requirement. The MMA should work without it.**NOW I SEE IT.** The canonical layouts are:
**MN-major SWIZZLE_128B:**
- `((8, n), (8, k))` with strides `((1, SBO), (8, LBO))` in uint128_t units
- `SBO = stride_byte_offset` = stride_11 (outer MN stride)
- `LBO = leading_byte_offset` = stride_01 (inner K stride, which is 8 for SW128)
**K-major SWIZZLE_128B:**
- `((8, n), (2, 1))` with strides `((8, SBO), (1, LBO))` in uint128_t units
- `SBO = stride_byte_offset` = stride_01
- `LBO = leading_byte_offset` = stride_10 (= 1 for SW128)
For **SWIZZLE_NONE** (layout_type=0):
**MN-major NONE:**
- `((1, n), (8, k))` with strides `((X, SBO), (1, LBO))` in uint128_t units
- SwizzleAtomMNSize = 1, SwizzleAtomKSize = 8
**K-major NONE:**
- `((8, n), (2, 1))` with strides `((8, SBO), (1, LBO))` — same as SW128 but without swizzle
- Wait, it says `stride_10 = (LAYOUT_TYPE == SWIZZLE_NONE) ? stride<1,0>(canonical_layout) : 1`
- So for NONE, stride_10 can be anything (no constraint)
Let me focus on SWIZLE_NONE for MN-major. The canonical layout is `((1, n), (8, k))` with strides `((X, SBO), (1, LBO))`. For our (128, 64) matrix in uint128_t = (16, 8):
logical_divide of layout (16, 8) with stride (1, 16) and Tile((1, _), (8, _)):
- M: 16 / 1 = 16 sub-tiles. 16 = 16 * 1. Shape ((1, 16), _).
- K: 8 / 8 = 1 sub-tile. Shape (_, (8, 1)).
- Result shape: ((1, 16), (8, 1))
- Result strides: ((1, 1), (16, 16*8))
Wait, the strides depend on the original layout. For simple row-major (16, 8) with stride (1, 16):
- Element at (m, k) = m + k * 16
- After logical_divide with Tile(1, 8):
- m = m_0 + m_1 * 1 (m_0 in [0,0], m_1 in [0,15])
- k = k_0 + k_1 * 8 (k_0 in [0,7], k_1 in [0,0])
- offset = m_0 + m_1 + (k_0 + k_1 * 8) * 16
- = m_0 + m_1 + k_0 * 16 + k_1 * 128
- For m_0=0 always: offset = m_1 + k_0 * 16 + k_1 * 128
- Strides: ((1, 1), (16, 128))
So the canonical layout for MN-major NONE with (128, 64) BF16 is:
- Shape: ((1, 16), (8, 1)) in uint128_t
- Strides: ((1, 1), (16, 128)) in uint128_t
The descriptor fields:
- stride_00 = 1 (inner MN, should be SwizzleAtomMNSize=1 for NONE) ✓
- stride_10 = 1 (outer MN)
- stride_01 = 16 (inner K) → LBO = 16
- stride_11 = 128 (outer K) → SBO = 128
**UMA descriptor for MN-major NONE Q at hd=64:**
- start_address = sQ_smem >> 4
- leading_byte_offset = 16 (uint128_t units)
- stride_byte_offset = 128 (uint128_t units)
- version = 1
- layout_type = 0 (SWIZZLE_NONE)
Now for K-major NONE with (128, 64) BF16. In uint128_t = (16, 8):
The canonical form for K-major NONE is `((8, n), (2, 1))` with strides `((8, SBO), (1, LBO))`.
But SwizzleAtomMNSize = 8 for K-major, so:
- logical_divide with Tile(8, 2):
- m (N-dim for K-major): 16 / 8 = 2 sub-tiles
- k (K-dim for K-major): 8 / 2 = 4 sub-tiles
Hmm wait, for K-major the "MN" dimension is the N-dim and the "K" dimension is the K-dim. In uint128_t:
- N-dim = 16 uint128_t (128 BF16 in K)
- K-dim = 8 uint128_t (64 BF16 in MN)
The K-major canonical: `((8, n), (2, 1))` where 8 = SwizzleAtomMNSize.
logical_divide of (16, 8) with stride (1, 16) and Tile(8, 2):
- N: 16 / 8 = 2 → (8, 2)
- K: 8 / 2 = 4 → (2, 4)
- Result shape: ((8, 2), (2, 4))
- Strides: ((1, 8), (16, 32))
Wait, this doesn't match the expected `((8, n), (2, 1))` canonical form. The K-dim should have (2, 1) but I get (2, 4). That means 4 outer K-tiles, but the canonical expects 1.
The issue: our K-dim (8 uint128_t) is larger than the K atom (2 uint128_t). We need 4 K-tiles. The canonical form is `((8, n), (2, k/2))` — the 1 in `(2, 1)` was just an example. The actual second K-dim size depends on the shape.
So the canonical for our (128, 64) K-major is:
- Shape: ((8, 2), (2, 4))
- Strides: ((1, 8), (16, 32))
Descriptor:
- stride_00 = 1 → should be SwizzleAtomMNSize = 8 for K-major... wait, that doesn't match.
I think I'm getting confused by the MN vs K major conventions. Let me just implement SWIZZLE_NONE for both, compute the descriptors, and test. If the descriptor is wrong, the MMA will either hang or produce garbage — we'll see.There's a typo in the TS function (double colon). Let me fix and also update the QK verify to use simple row-major SMEM with SWIZZLE_NONE descriptors:Now simplify the QK verify kernel — use simple row-major SMEM (no swizzle) with the NONE descriptors:All zeros — the MMA ran without crashing (no misaligned address!), but produced no output. The MMA executed but the result in TMEM is all zeros.
This could mean:
1. The MMA computed zero (data not visible to MMA due to SMEM layout mismatch)
2. The TMEM wasn't written by the MMA (tmem_base or tmem_s address wrong)
3. The MMA didn't actually execute (elect_one_sync pattern wrong — I used `lane == 0` instead of proper elect_one)
Let me add a diagnostic: try reading TMEM right after the MMA without the fence, and check if tmem_base is correct:Still all zeros. The MMA isn't writing to TMEM at all, OR it's writing to a different TMEM address than we're reading from. The `tmem_base` isn't printed (printf might not appear in the screen session's log).
The issue could be: the MMA writes to TMEM starting at column 0, but `tmem_base` might not be 0. Let me check by reading from tmem_base + 0 specifically:The debug prints didn't appear. The `printf` from device code in a screen session may not appear in the log. But the GPU output is all zeros — the MMA isn't producing any output.
The critical question: **is the MMA even executing?** The kernel doesn't crash, which means the PTX is valid, but the MMA might silently produce nothing if the descriptor is wrong.
Let me try a completely different approach to debug: write known data to TMEM directly (like we did in the TMEM epilogue test), verify TMEM read works, THEN do the MMA. This isolates whether the issue is the MMA or the TMEM read.Still all zeros from GPU. The MMA is producing nothing. The TMEM verify print didn't appear in the test output (printf from device code isn't captured by the test's host-side output).
Let me write the TMEM verify result to the global output array instead of using printf:**All zeros** — even the scalar QK dot product and SMEM addresses are zero in the output. That means the kernel is hitting the early return but `s_out[0]` is still 0. The `tid == 0` block isn't executing, OR the SMEM data is garbage, OR the output isn't being written correctly.
Wait — I used `return` inside a `__global__` function. In CUDA, `return` from a kernel only exits the current thread, not the whole kernel. But the early return is inside `if (tid == 0)`, so only thread 0 returns. Other threads continue and might overwrite s_out. But they shouldn't — they'd hit the rest of the kernel code which we've removed.
Actually, the `return` is inside `if (tid == 0)`, so only thread 0 returns. The other 191 threads DON'T return and continue executing. The rest of the kernel code (TMEM ops, MMA, etc.) might be accessing uninitialized memory. But we removed that code with the early return...
Wait, the `return` is in the MIDDLE of the kernel. After the `return`, there's still the rest of the kernel code (MMA, TMEM read, dealloc). Threads 1-191 will execute that code! This is a bug.
Let me use a proper early exit or just skip the `return` and use a conditional:All zeros! Even the scalar dot product is zero. This means `sQ` and `sK` are either not loaded, or the `bf16_to_f32` conversion is wrong, or the SMEM pointers are corrupted.
The issue might be the SMEM alignment. The `sQ` starts at `(((uintptr_t)(sbuf + 4) + 15) & ~15)` which could be up to 16 bytes into the shared memory. The UMA descriptor expects the start address at a 128-byte boundary, not just 16-byte.
Let me use 128-byte alignment:Wait, the scalar dot product uses `bf16_to_f32(sQ[d])` which is the inline PTX `cvt.f32.bf16`. If `sQ[d]` is 0 (all zeros), the conversion should return 0.0f. But I'm loading actual Q data there. The issue might be that the SMEM write isn't happening — `sQ[d] = qh[d]` might not be working because `bf16_t` is `unsigned short` and we're writing to SMEM that might not be initialized.
Actually, the kernel first zeros all 128*HD elements: `for (int i = tid; i < 128 * HD; i += NTHREADS) sQ[i] = 0;` and then writes Q: `for (int d = tid; d < HD; d += NTHREADS) sQ[d] = qh[d];`. The zeroing uses ALL 192 threads, and the Q load uses ALL 192 threads. With 128*64 = 8192 elements and 192 threads, each thread zeros ~43 elements. That should be fine.
But wait — the Q data is loaded from global memory (qh), not from host memory. The test copies Q from host to GPU with `cudaMemcpy`. Let me check: the test uses `bstride_q = H*HD` which is 1*64 = 64. The Q pointer is `q + batch*bstride_q + head*HD = q + 0 + 64`. But Q is (B, H, 1, HD) = (1, 1, 1, 64) — 64 BF16 elements. The stride should be `H * HD` for the batch stride, but the test passes `H*HD` as `bstride_q`.
Actually, looking at the test code: `bstride_q = H*HD = 1*64 = 64`. And `qh = q + batch*bstride_q + head*HD = q + 0*64 + 0*64 = q`. That's correct.
But the `s_out[0]` is 0.0f. Maybe the SMEM write and read are correct, but the data in sQ is all zeros? Let me add a more direct test:All zeros from the GPU. Even `s_out[1]` (q0 = bf16_to_f32(sQ[0])) is 0.0f. This means the SMEM data is NOT being loaded.
Wait — the `return` statement returns ALL threads. But the early return happens AFTER `__syncthreads()`. Let me check: does the `sQ` pointer point to valid SMEM? The `bf16_t* sQ = (bf16_t*)(((uintptr_t)(sbuf + 4) + 127) & ~127)`. With 128-byte alignment and sbuf starting at offset 0, `sQ` starts at offset 128. But the SMEM allocation is only `4 + 128 + 128*HD*2 + 128*HD*2 + 1024 = 16516` bytes. The `128*HD` elements start at byte 128. That's fine.
But wait — the SMEM size might be wrong. The test sets `smem = 4 + 128 + smem_q * 2 + smem_k * 2 + 1024` where `smem_q = 1024 * 8 = 65536`. That's way too much SMEM. The kernel launch will fail because we're requesting more SMEM than the GPU has.
Let me fix the SMEM size calculation in the test:**`misaligned address`** — the SMEM size was previously so large that the kernel couldn't even launch (silent failure). Now with the correct SMEM size (16516 bytes), the kernel launches but the TMEM alloc causes a misaligned address because `sTmemBase` is at offset 0 which might not be 16-byte aligned for the TMEM alloc.
The fix: ensure `sTmemBase` (used for TMEM alloc) is properly aligned, and the `sQ` buffer doesn't overlap with it.Still "misaligned address". The SMEM is properly aligned now. The issue must be in the UMA descriptor or the MMA instruction itself. The descriptor's start_address or strides might be causing the hardware to compute an invalid SMEM address.
Let me step back and try the absolute simplest thing: a known-good UMA descriptor. I'll construct one by hand with the minimum required values:
- start_address = 256/16 = 16 (sQ starts at byte 256, >> 4 = 16)
- LBO = 0 (no leading offset)
- SBO = 0 (no stride offset)
- version = 1
- layout_type = 0
This describes a degenerate matrix at address 256 with no strides. It won't produce correct results but it should NOT crash. If it still crashes, the issue is the MMA PTX itself, not the descriptor.Even the minimal descriptor (just start_address + version, no strides, no layout) causes "misaligned address". This means the issue is NOT the descriptor strides — it's the start_address or the SMEM alignment itself.
The SMEM address from `__cvta_generic_to_shared` might not be correct for the UMMA descriptor. The UMMA descriptor expects the address in a specific format. Let me check: does `__cvta_generic_to_shared` return the actual SMEM address, or something else?
The "misaligned address" might be from the TMEM alloc, not the MMA. Let me test: skip the TMEM alloc entirely and just test SMEM loads:Also skip TMEM dealloc:The "misaligned address" persists even WITHOUT TMEM alloc. The MMA instruction itself is causing the misaligned address. The minimal descriptor (just start_address + version, no strides) produces an address that the hardware can't access.
The start_address in the descriptor is `sQ_smem >> 4`. If `sQ_smem` is a SMEM address like `0x7FFFF00`, then `>> 4` = `0x7FFFFF0`, which fits in 14 bits? No — `0x7FFFFF0` is way more than 14 bits.
**That's the bug!** The `start_address` field is only 14 bits (bits [0,14)), but SMEM addresses on Blackwell can be much larger. `__cvta_generic_to_shared` returns a full 32-bit SMEM address. Right-shifting by 4 gives a 28-bit value, but only 14 bits fit in the descriptor.
On SM100, SMEM addresses might be relative to a base. The descriptor's start_address might need to be the OFFSET within the SMEM allocation, not the full address. Or the SMEM address space might be small enough that 14 bits × 16 = 224 KB covers all SMEM.
Actually, 14 bits × 16 bytes = 128 KB. But SMEM on SM100 is up to 232 KB. So 14 bits is NOT enough for the full SMEM address.
Wait — looking at the CUTLASS source again: `start_address_ : 14, : 2`. The `: 2` means there are 2 UNUSED bits. So bits [0,14) = 14 bits for the address, bits [14,16) = 2 unused bits. 14 bits × 16 bytes = 128 KB. That's not enough for 232 KB SMEM.
But the CUTLASS code sets `desc_ = static_cast<uint64_t>(cast_smem_ptr_to_uint(tensor.data()) >> 4)`. If the address is larger than 14 bits, the upper bits get truncated. That can't be right.
Let me re-read the descriptor format. Maybe I'm reading the bit positions wrong:OK so the descriptor is:
- `start_address_`: 14 bits [0,14) → SMEM address >> 4, max 128 KB
- `leading_byte_offset_`: 14 bits [16,30)
- `stride_byte_offset_`: 14 bits [32,46), `version_`: 2 bits [46,48)
- `base_offset_`: 3 bits [49,52), `lbo_mode_`: 1 bit [52,53)
- `layout_type_`: 3 bits [61,64)
The full 64-bit descriptor fits in a `uint64_t`. The `start_address_` is only 14 bits, covering 128 KB. But SMEM can be up to 232 KB. This means for SMEM addresses beyond 128 KB, the `start_address_` field overflows.
However, the CUTLASS code sets it as: `desc_ = static_cast<uint64_t>(cast_smem_ptr_to_uint(tensor.data()) >> 4)`. If the SMEM address is > 128 KB >> 4, the upper bits will be in the `leading_byte_offset_` field, which would be wrong.
Wait — the descriptor is set as a SINGLE `desc_` assignment, not field-by-field. The `desc_` is the full 64-bit value. The `cast_smem_ptr_to_uint >> 4` gives the lower 14 bits for start_address, but the upper bits will be interpreted as leading_byte_offset etc.
This means the CUTLASS code relies on the SMEM address being < 128 KB. For our kernel, the Q starts at byte 256, which is 256 >> 4 = 16. That's well within 14 bits.
But the error is "misaligned address" even with start_address = 16. Something else is wrong.
Let me check: does the MMA instruction require the SMEM address to be 256-byte aligned (not just 16-byte)? The UMMA documentation might require a specific alignment.The "misaligned address" is an ASAN-style error from CUDA. It might be from the descriptor itself — the hardware validates the descriptor and rejects it if the encoded strides are inconsistent with the data.
With our minimal descriptor (only start_address and version, all strides = 0), the hardware might reject it because stride=0 is invalid for a matrix.
The descriptor NEEDS valid strides. Let me put back the proper LBO and SBO values:Also, I need to actually call the MMA. Let me re-enable it:The file has been modified too many times. Let me rewrite it cleanly:Also update the test to print both scalar and MMA results:Still "misaligned address". The MMA instruction is causing it. The descriptor with LBO=16 and SBO=128 is producing an invalid address.
The issue might be that the LBO and SBO values need to account for the ACTUAL SMEM layout in uint128_t units, not just the logical strides. For a (128, 64) BF16 matrix starting at SMEM address 256 bytes:
- 256 bytes = 16 uint128_t offset (start_address = 16)
- Row stride = 64 BF16 × 2 = 128 bytes = 8 uint128_t
- But LBO = 16, which means a "leading byte offset" of 16 uint128_t = 256 bytes
The LBO and SBO describe the canonical layout AFTER logical_divide. For MN-major NONE:
- Canonical: ((1, 16), (8, 1)) with strides ((1, 1), (16, 128))
- LBO = stride_01 = 16 — this is the stride of the inner K sub-tile (8 uint128_t), which is 16 uint128_t = 256 bytes. But 8 uint128_t is only 128 bytes. The stride 16 means "jump 16 uint128_t per inner-K group of 8", which makes sense for a row-major layout with 128 rows: 128 × 2 / 16 = 16 uint128_t per row.
Wait, 128 rows × 2 bytes per BF16 / 16 bytes per uint128_t = 16 uint128_t. So stride 16 IS the row stride in uint128_t. And the inner K tile has 8 uint128_t, so after 8, we need to jump to the next 8. The next 8 uint128_t are at offset 16 (next row). So the inner K stride (stride_01) should be 16. That's what I have. And the outer K stride (stride_11) is 128 (8 inner K groups × 16 stride). That's also what I have.
But the hardware might interpret these differently. Let me try with a descriptor where the strides match the raw row-major layout:
- For row-major (128, 64) BF16 with stride (1, 128) in BF16 elements:
- In uint128_t: (16, 8) with stride (1, 16)
- For the canonical form, stride_01 = 16, stride_11 = 128
These ARE the values I'm using. The misaligned address must be from something else.
Let me try a completely different approach: use the descriptor that the CUTLASS C++ FMHA example uses. The example at `/root/cutlass/examples/77_blackwell_fmha` constructs descriptors. Let me find where:The FMHA example uses the high-level CUTLASS API, not raw descriptors. Let me look at the MoE example which also uses UMMA:No examples use raw SmemDescriptor. The CUTLASS examples all use the high-level API.
I've been going in circles. Let me take a completely different approach. Instead of trying to construct UMMA descriptors manually, let me **use the CuTeDSL FMHA kernel that ALREADY WORKS, and add a print statement that dumps the UMA descriptors at runtime.** This gives us the EXACT descriptor values that work.The `cute` module isn't available in the Python runtime — it's only available during CuTeDSL JIT compilation. The FMHA kernel uses `cute` via the `@cute.jit` decorator, not as a runtime import.
I need to embed the descriptor printing INSIDE the CuTeDSL kernel so it prints at JIT time. But that would require modifying the FMHA kernel code.
Actually, the simplest approach: **add a print statement inside the FMHA kernel that dumps the descriptor at kernel compile time.** The CuTeDSL `cute.printf` runs at trace time and will show the values.The `q_smem_s` is a tuple of (swizzle, outer_layout). The outer_layout is the canonical layout and the swizzle is the inner pattern. Let me print the layout properties:The `q_smem_s` is a CuTeDSL layout object, not a Python tuple. The `slice_` operation is used to extract a specific stage. The layout objects are opaque at the Python level.
The only way to get the UMMA descriptor values is from inside the CuTeDSL kernel at JIT time. Let me modify the FMHA kernel itself to print the descriptors. This is invasive but necessary: