Files
nvfp4-megamoe-kernel/dsv4/kernels/attention/fmha_common.cuh
biondizzle 6fb3d54c02 docs: update here-docs with CuTeDSL rationale for NVIDIA
Updated fmha_common.cuh, fmha_sm100.cuh, fmha_epilogue_sm100.cuh,
and fmha_sm100_launch.cuh with comprehensive here-docs explaining:

1. The 4 CuTeDSL gaps that forced us to raw CUDA C++:
   - TMEM round-trip broken (Ld32x32bOp/St32x32bOp column mismatch)
   - Float→int impossible (arith.fptosi not lowerable)
   - epilogue_tma_store blocks multi-CTA
   - hd=512 MLIR optimizer hangs

2. TMEM lane mapping (verified on B200):
   - Lane i → positions i*4+0..3, 128 FP32 per column
   - Warp-collective: ALL 32 lanes must call ld/st or HANG
   - Column address = tmem_base + column_index

3. Key insight for NVIDIA: float→int gap is the single most
   impactful limitation, blocking ALL quantization-epilogue fusion
2026-05-28 07:54:01 +00:00

180 lines
8.2 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* DSV4 FMHA shared definitions — base header for raw CUDA C++ kernels.
*
* ==================================================================
* WHY RAW CUDA C++ INSTEAD OF CUTEDSL
* ==================================================================
*
* CuTeDSL (the Python DSL for CUTLASS) has FOUR fundamental gaps on
* Blackwell SM100 that made it impossible to ship a production FMHA
* kernel. We spent weeks hitting these walls before switching to raw
* CUDA C++ with inline PTX. The reference kernel you see here was
* written and verified correct in ~2 hours.
*
* GAP 1: TMEM round-trip is BROKEN
* ---------------------------------
* CuTeDSL's Ld32x32bOp and St32x32bOp atoms have different column
* mappings — they address DIFFERENT TMEM columns even when constructed
* with the same layout. A NO-OP round-trip (load → store unchanged)
* corrupts data by ~3% (cos ~0.97). This makes in-kernel O rescale
* (the D1.5 fix) impossible in CuTeDSL — you can't read the TMEM
* accumulator, multiply by acc_scale, and write it back. The MoE
* kernel avoids this because it only does a ONE-WAY TMEM → regs →
* SMEM → GMEM epilogue. FMHA needs a round-trip for per-KV-tile
* rescaling. Raw CUDA with tcgen05.ld/st PTX gives us full control
* over TMEM addressing.
*
* GAP 2: Float-to-int conversion is IMPOSSIBLE
* ---------------------------------------------
* The CuTeDSL MLIR pipeline CANNOT lower ANY float→int operation.
* arith.fptosi, nvvm.inline_ptx with cvt.rni.s32.f32, llvm.bitcast
* Float32→Int32 — ALL fail with "LLVM ERROR: unsupported operation."
* This makes quantize-in-epilogue fusion (NVFP4-1.1) impossible in
* CuTeDSL. Every quantization kernel needs f32→i32. Raw CUDA has
* __float2int_rn which works perfectly.
*
* GAP 3: epilogue_tma_store BLOCKS multi-CTA
* -------------------------------------------
* The epilogue_tma_store helper cannot accept flat_divide-based GMEM
* coordinates, which are required for multi-CTA grid launches. This
* means FMHA is stuck at per-head Python launch (128 launches per Pro
* decode step) instead of a single GPU launch. The MoE kernel avoids
* this by using the explicit epilogue_tmem_copy + epilogue_smem_copy
+ tma_partition pipeline. Raw CUDA gives us full control over TMA
* descriptors and GMEM writes.
*
* GAP 4: hd=512 MLIR backend HANGS
* ---------------------------------
* The CuTeDSL MLIR optimizer cannot process the hd=512 kernel in
* reasonable time. Tracer completes in 0.8s (kernel is structurally
* correct), but the MLIR optimizer runs for 3+ hours before we kill
* it. This is a CuTeDSL/MLIR toolchain limitation, not a kernel bug.
* Raw CUDA compiles hd=512 in seconds.
*
* ==================================================================
* WHAT WORKS (tested on B200, CUDA 13.2, SM100)
* ==================================================================
* - BF16 conversion via inline PTX cvt.rn.bf16.f32 / cvt.f32.bf16
* - Warp reductions (fmax, sum) via shfl_xor
* - TMEM alloc/dealloc via tcgen05.alloc/dealloc PTX
* - TMEM load/store via tcgen05.ld/st PTX (uint32_t b32 registers)
* - TMEM fence via tcgen05.wait::st/ld.sync.aligned
* - One-way TMEM epilogue: TMEM → regs → normalize → BF16 → GMEM
* (verified cos 0.999999 at hd=64, cos 0.999998 at hd=128)
*
* ==================================================================
* TMEM LANE MAPPING (verified on B200 via test_tmem_lane_mapping.cu)
* ==================================================================
* tcgen05.st/ld 16x256b.x1.b32 are warp-collective operations:
* - ALL 32 lanes in a warp MUST execute them (or HANG)
* - Each lane i reads/writes positions i*4+0..i*4+3 within the column
* - 32 lanes × 4 FP32 = 128 FP32 per column
* - For row 0: lane 0 = positions 0-3, lane 1 = 4-7, ..., lane 31 = 124-127
* - HD values need ceil(HD/128) TMEM columns
* - Column address = tmem_base + column_index
* - tmem_base is WRITTEN to SMEM by tcgen05.alloc (read it back after alloc)
*
* CRITICAL: If fewer than 32 lanes call tmem_store/tmem_load, the
* warp is divergent on a collective operation and the GPU HANGS.
* Always loop over enough columns that all 32 lanes participate,
* even if some write don't-care data to unused columns.
*
* ==================================================================
* TMEM ALLOC/DEALLOC
* ==================================================================
* - tcgen05.alloc: pass SMEM pointer (via __cvta_generic_to_shared)
* and num_columns (power of 2, minimum 32). Must be called by an
* ENTIRE fully active warp (all 32 lanes). The alloc WRITES the
* tmem_base value to the SMEM location.
* - tcgen05.dealloc: pass tmem_base (the VALUE from SMEM, not the
* SMEM pointer) and num_columns. Must also be warp-collective.
*
* ==================================================================
* KEY INSIGHT FOR NVIDIA
* ==================================================================
* The CuTeDSL float→int gap is the single most impactful limitation.
* It blocks ALL quantization-epilogue fusion (NVFP4-1.1, NVFP4-1.2)
* and forces a separate kernel launch for every quantize step. The
* TMEM round-trip issue is the second most impactful — it blocks
* in-kernel O rescaling, forcing 5-9 Python kernel launches per
* decode step instead of 1 GPU launch. Both of these work trivially
* in raw CUDA C++ with inline PTX.
*/
#pragma once
#include <cuda_runtime.h>
#include <cstdint>
#include <cstdio>
#include <cmath>
namespace dsv4::kernels::attention {
typedef unsigned short bf16_t;
__device__ __forceinline__ bf16_t f32_to_bf16(float f) {
bf16_t h; asm("cvt.rn.bf16.f32 %0, %1;" : "=h"(h) : "f"(f)); return h;
}
__device__ __forceinline__ float bf16_to_f32(bf16_t h) {
float f; asm("cvt.f32.bf16 %0, %1;" : "=f"(f) : "h"(h)); return f;
}
// Float ↔ uint32_t bitcast (for TMEM operations)
__device__ __forceinline__ uint32_t f32_to_u32(float f) { uint32_t u; memcpy(&u,&f,4); return u; }
__device__ __forceinline__ float u32_to_f32(uint32_t u) { float f; memcpy(&f,&u,4); return f; }
constexpr int WARP = 32;
constexpr int NTHREADS = 192;
constexpr int NWARPS = 6;
__device__ __forceinline__ float wmax(float v) {
for(int o=16;o>0;o>>=1) v=fmaxf(v,__shfl_xor_sync(0xFFFFFFFF,v,o)); return v;
}
__device__ __forceinline__ float wsum(float v) {
for(int o=16;o>0;o>>=1) v+=__shfl_xor_sync(0xFFFFFFFF,v,o); return v;
}
// ==================================================================
// TMEM operations — inline PTX for Blackwell SM100
// ==================================================================
// These wrap the tcgen05 PTX instructions for TMEM management.
// All TMEM load/store ops are WARP-COLLECTIVE: all 32 lanes must
// execute them. See the header comment for the lane mapping.
// ==================================================================
__device__ void tmem_alloc(uint32_t smem_ptr, int num_cols) {
asm volatile("tcgen05.alloc.cta_group::1.sync.aligned.shared::cta.b32 [%0], %1;"
:: "r"(smem_ptr), "r"(num_cols));
}
__device__ void tmem_dealloc(uint32_t tmem_ptr, int num_cols) {
asm volatile("tcgen05.dealloc.cta_group::1.sync.aligned.b32 %0, %1;"
:: "r"(tmem_ptr), "r"(num_cols));
}
/** Load 16 rows × 256 bits from TMEM column. Warp-collective. 4 uint32_t per lane. */
__device__ void tmem_load(uint32_t col_addr,
uint32_t& r0, uint32_t& r1, uint32_t& r2, uint32_t& r3) {
asm volatile("tcgen05.ld.sync.aligned.16x256b.x1.b32 {%0, %1, %2, %3}, [%4];"
: "=r"(r0), "=r"(r1), "=r"(r2), "=r"(r3) : "r"(col_addr));
}
/** Store 16 rows × 256 bits to TMEM column. Warp-collective. 4 uint32_t per lane. */
__device__ void tmem_store(uint32_t col_addr,
uint32_t r0, uint32_t r1, uint32_t r2, uint32_t r3) {
asm volatile("tcgen05.st.sync.aligned.16x256b.x1.b32 [%0], {%1, %2, %3, %4};"
:: "r"(col_addr), "r"(r0), "r"(r1), "r"(r2), "r"(r3));
}
/** Wait for prior TMEM stores to be visible. Call after tmem_store. */
__device__ void tmem_fence_store() {
asm volatile("tcgen05.wait::st.sync.aligned;" ::: "memory");
}
/** Wait for prior TMEM loads to complete. Call after tmem_load. */
__device__ void tmem_fence_load() {
asm volatile("tcgen05.wait::ld.sync.aligned;" ::: "memory");
}
} // namespace