/** * DSV4 FMHA — TMA async load infrastructure for Blackwell SM100. * * ================================================================== * DESIGN * ================================================================== * * Replaces scalar GMEM reads in the load warp with async TMA bulk * copies via cp.async.bulk.tensor.3d. The pipeline: * * Host: CUtensorMap creation for Q, K, V tiles * Kernel: * 1. TMA warp issues cp.async.bulk.tensor.3d → SMEM (row-major) * 2. mbarrier wait for TMA completion * 3. Load warp transposes row-major SMEM → canonical K-major SMEM * 4. MMA warp runs tcgen05.mma as before * * For double-buffered pipeline overlap (future): * - Two SMEM buffers per tensor (sQ0/sQ1, sK0/sK1) * - TMA load of K-tile (kt+1) overlaps with MMA on K-tile (kt) * - Pipeline stages managed via mbarrier arrive/wait * * ================================================================== * TMA DESCRIPTOR LAYOUT * ================================================================== * * We create 2D CUtensorMap descriptors for each tile the kernel needs: * * Q tile: (T, HD) — one tile for the full Q * K tile: (s_k, HD) — one tile for the full K (or (128, 16) per K-sub-tile) * V tile: (HD, s_k) — transposed, one tile for the full V * * TMA copies data from GMEM to SMEM in row-major order. After TMA * completion, the load warp transposes from row-major to the * canonical K-major core-matrix layout that tcgen05.mma expects. * * For the multirow kernel, Q is (T, HD) and K is (s_k, HD). * Since TMA operates on 2D tiles and our SMEM is (128, 16) per * MMA K-tile, we have two choices: * * Option A: TMA load full (T, HD) → row-major SMEM → transpose * - One TMA descriptor for Q, one for K * - Larger SMEM footprint (need row-major + canonical) * - Simpler descriptor management * * Option B: TMA load per (128, 16) K-sub-tile * - One TMA descriptor, multiple TMA issues with different coords * - Same SMEM as current (no double buffer needed for single-stage) * - Matches the existing K-tiling loop structure * * We choose Option B: TMA per (128, 16) K-sub-tile. This: * - Reuses the exact same SMEM layout as the current kernel * - Fits the existing QK loop structure (kt = 0..NKT_QK-1) * - Enables future pipeline overlap with minimal changes * - The TMA descriptor covers the full (T, HD) or (s_k, HD) tensor, * and we issue TMA loads for specific (col, row) coordinates * targeting each 128×16 tile * * ================================================================== * MBARRIER PROTOCOL * ================================================================== * * TMA async copies use mbarrier for completion signaling: * * 1. Init mbarrier with expected transaction count = 1 * 2. Issue cp.async.bulk.tensor.3d with the mbarrier * 3. Wait on mbarrier parity (spin or yield) * 4. After wait returns, SMEM data is ready * * The mbarrier lives in SMEM. One mbarrier per outstanding TMA * operation. For single-stage (no overlap), we use one mbarrier * and wait immediately after issue. * * ================================================================== * SWIZZLE CONSIDERATIONS * ================================================================== * * TMA descriptors support SWIZZLE_NONE, SWIZZLE_32B, SWIZZLE_64B, * SWIZZLE_128B. The swizzle pattern in SMEM matches what UMMA * descriptors expect when using make_umma_desc_kmajor_sw128. * * Current kernel uses SWIZZLE_NONE (make_umma_desc_kmajor_none). * With TMA, we have two paths: * * Path 1: TMA with SWIZZLE_NONE → SMEM is row-major → transpose to canonical * Path 2: TMA with SWIZZLE_128B → SMEM is swizzled → UMMA reads directly * * Path 2 is the production target: no transpose needed, TMA writes * in the exact layout MMA reads. But getting the swizzle right is * tricky and needs careful verification. * * We start with Path 1 (SWIZZLE_NONE + transpose) to get TMA working, * then upgrade to Path 2 (SWIZZLE_128B, zero-copy) for performance. * ================================================================== */ #pragma once #include "fmha_common.cuh" #include namespace dsv4::kernels::attention { // ================================================================== // TMA descriptor helpers (host-side) // ================================================================== // These are called from host code to create CUtensorMap objects // that the kernel uses for TMA async copies. // ================================================================== /** * Create a 2D TMA descriptor for a BF16 tensor of shape (rows, cols). * The tensor is row-major in GMEM with stride = cols. * TMA tile dimensions are (tile_rows, tile_cols). * * The descriptor is written to `out` (host memory). * Must be copied to device memory before kernel launch. */ inline bool create_tma_desc_2d_bf16( CUtensorMap* out, const void* gmem_ptr, // device pointer to the BF16 tensor uint64_t rows, // global dimension 0 (number of rows) uint64_t cols, // global dimension 1 (number of columns) uint32_t tile_rows, // TMA tile dimension 0 uint32_t tile_cols, // TMA tile dimension 1 CUtensorMapSwizzle swizzle = CU_TENSOR_MAP_SWIZZLE_NONE ) { // CUDA 13: globalStrides are in BYTES, not elements! // globalStrides[0] = globalDim[0] * elementSizeInBytes = cols * 2 (BF16) // globalStrides[1] = globalStrides[0] * globalDim[1] = cols * 2 * rows // But for rank=2, only 1 stride is needed (rank-1) // // For rank=3 (recommended for CUDA 13), 2 strides are needed. // We use rank=3 with degenerate 3rd dimension = 1. // This avoids rank=2 edge cases and matches CUTLASS convention. // // 3D: (cols, rows, 1) innermost-first uint64_t global_dim[] = {cols, rows, 1}; uint64_t global_str[] = {cols * 2, cols * 2 * rows}; // byte strides // Tile: (tile_cols, tile_rows, 1) innermost-first uint32_t tile_dim[] = {tile_cols, tile_rows, 1}; // Element strides within tile: 1 for each dim uint32_t tile_str[] = {1, 1, 1}; CUresult res = cuTensorMapEncodeTiled( out, CU_TENSOR_MAP_DATA_TYPE_BFLOAT16, // Proper BF16 type 3, // 3D tensor (degenerate 3rd dim) const_cast(gmem_ptr), global_dim, global_str, tile_dim, tile_str, CU_TENSOR_MAP_INTERLEAVE_NONE, swizzle, CU_TENSOR_MAP_L2_PROMOTION_NONE, CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE ); if (res != CUDA_SUCCESS) { fprintf(stderr, "cuTensorMapEncodeTiled failed: error=%d, gdim=[%lu,%lu,%lu], gstr=[%lu,%lu], tdim=[%u,%u,%u], tstr=[%u,%u,%u]\n", (int)res, global_dim[0], global_dim[1], global_dim[2], global_str[0], global_str[1], tile_dim[0], tile_dim[1], tile_dim[2], tile_str[0], tile_str[1], tile_str[2]); } return res == CUDA_SUCCESS; } // ================================================================== // TMA kernel-side operations // ================================================================== /** * Initialize an mbarrier in SMEM with expected byte count. * Only one thread should call this. * For TMA with complete_tx::bytes, the expected count is the number * of bytes that will be transferred. */ __device__ __forceinline__ void tma_mbarrier_init(uint32_t smem_mbar, uint32_t expected_bytes) { asm volatile("mbarrier.init.shared.b64 [%0], %1;" :: "r"(smem_mbar), "r"(expected_bytes)); } /** * Issue a 3D TMA async copy from GMEM to SMEM. * * The TMA descriptor must be in device memory (GMEM). * Only ONE thread per CTA should issue the TMA copy. * * After issue, the data will be written to SMEM asynchronously. * Use tma_mbarrier_wait to wait for completion. * * @param smem_dst SMEM destination address (via __cvta_generic_to_shared) * @param tma_desc Pointer to CUtensorMap in device memory (uint64_t cast) * @param smem_mbar SMEM mbarrier address (via __cvta_generic_to_shared) * @param coord_x Innermost dimension coordinate (columns) * @param coord_y Middle dimension coordinate (rows) * @param coord_z Outermost dimension coordinate (degenerate = 0) */ __device__ __forceinline__ void tma_load_3d( uint32_t smem_dst, uint64_t tma_desc, uint32_t smem_mbar, int coord_x, int coord_y, int coord_z = 0 ) { asm volatile( "cp.async.bulk.tensor.3d.shared::cluster.global.mbarrier::complete_tx::bytes " "[%0], [%1, {%3, %4, %5}], [%2];" :: "r"(smem_dst), "l"(tma_desc), "r"(smem_mbar), "r"(coord_x), "r"(coord_y), "r"(coord_z) : "memory" ); } /** * Wait for mbarrier completion (spin-wait). * Only ONE thread should wait (or all threads, but typically just the * thread that issued the TMA copy). * * @param smem_mbar SMEM mbarrier address (via __cvta_generic_to_shared) */ __device__ __forceinline__ void tma_mbarrier_wait(uint32_t smem_mbar) { int phase = 0; asm volatile( "{\n\t" ".reg .pred p;\n\t" "LOOP:\n\t" "mbarrier.try_wait.parity.shared.b64 p, [%0], %1;\n\t" "@p bra DONE;\n\t" "bra LOOP;\n\t" "DONE:\n\t" "}" :: "r"(smem_mbar), "r"(phase) : "memory" ); } /** * Invalidate L2 prefetch to ensure TMA sees fresh data. * Call before issuing TMA loads if the data was recently written. */ __device__ __forceinline__ void tma_cp_commit() { asm volatile("cp.async.commit_group;" ::: "memory"); } // ================================================================== // TMA parameter structure // ================================================================== struct FmhaTmaDescriptors { CUtensorMap* __restrict__ tma_q; // Q descriptor: (T, HD) row-major CUtensorMap* __restrict__ tma_k; // K descriptor: (s_k, HD) row-major CUtensorMap* __restrict__ tma_v; // V descriptor: (HD, s_k) row-major }; } // namespace dsv4::kernels::attention