|
| 1 | +// block_sparse_bwd_launch_sm100a.cuh -- host surface of the VSA block-sparse backward drop: |
| 2 | +// argument struct, workspace sizes, the support predicate and the stream-chained launch |
| 3 | +// (preprocess -> order -> main -> postprocess). Tensor maps are encoded per call (no static |
| 4 | +// cache: a torch caller hands us fresh pointers every time). |
| 5 | +#ifndef BLOCK_SPARSE_VSA_BWD_LAUNCH_SM100A_CUH |
| 6 | +#define BLOCK_SPARSE_VSA_BWD_LAUNCH_SM100A_CUH |
| 7 | + |
| 8 | +#include <algorithm> |
| 9 | +#include <cmath> |
| 10 | +#include "block_sparse_bwd_kernel_sm100a.cuh" |
| 11 | + |
| 12 | +#ifndef VSA_BHSD |
| 13 | +#define VSA_BHSD false |
| 14 | +#endif |
| 15 | +#ifndef VSA_BWD_DQ_F16 |
| 16 | +#define VSA_BWD_DQ_F16 false |
| 17 | +#endif |
| 18 | +#ifndef VSA_BWD_USE_CLC |
| 19 | +#define VSA_BWD_USE_CLC true |
| 20 | +#endif |
| 21 | + |
| 22 | +namespace vsa_bwd_blk64 { |
| 23 | + |
| 24 | +#if VSA_BWD_DQ_F16 |
| 25 | +using dq_accum_t = uint16_t; |
| 26 | +#else |
| 27 | +using dq_accum_t = float; |
| 28 | +#endif |
| 29 | + |
| 30 | +struct BlockSparseVsaBwdArgs { |
| 31 | + // Activations are bf16, contiguous, [B, H, S, 128] under VSA_BHSD, else [B*S, H, 128]. |
| 32 | + // nb below = num_kv_blocks_per_seq = S / 64. |
| 33 | + |
| 34 | + // Forward operands and results. |
| 35 | + const __nv_bfloat16* q; |
| 36 | + const __nv_bfloat16* k; |
| 37 | + const __nv_bfloat16* v; |
| 38 | + const __nv_bfloat16* o; |
| 39 | + // Gradient of the forward output. |
| 40 | + const __nv_bfloat16* dout; |
| 41 | + // [B, H, S] fp32 log-sum-exp in Triton's M form: max(qk * sm_scale * log2e) + log2(l). |
| 42 | + const float* lse; |
| 43 | + |
| 44 | + // Sparsity metadata, FastVideo's invert_indices layout. |
| 45 | + // [B*H*nb, max_q_blocks] int32: q blocks selecting each kv block; entries past the count unread. |
| 46 | + const int* k2q_idx; |
| 47 | + // [B*H*nb] int32: valid entries per k2q_idx row (0 allowed). |
| 48 | + const int* k2q_num; |
| 49 | + // [nb] int32: valid kv tokens per block (<= 64); kv rows at or past the count are masked. |
| 50 | + const int* variable_block_sizes; |
| 51 | + |
| 52 | + // Work order: which (batch, head, kv block) item each CTA processes. |
| 53 | + // [B*H*nb] int32 work id -> item ((b*H + h)*nb + kv); nullptr = the launch computes the order. |
| 54 | + const int* workitem_remap; |
| 55 | + // [B*H*nb] int32; required when workitem_remap is nullptr. |
| 56 | + int* order_workspace; |
| 57 | + |
| 58 | + // Outputs, inputs' layout; dk/dv rows of unselected kv blocks are zeroed by the preprocess. |
| 59 | + __nv_bfloat16* dq; |
| 60 | + __nv_bfloat16* dk; |
| 61 | + __nv_bfloat16* dv; |
| 62 | + |
| 63 | + // Scratch, caller-allocated; byte sizes from the block_sparse_bwd_*_bytes helpers below. |
| 64 | + // [B*H*S*128] dq_accum_t, drain-native; preprocess zeroes, main reduce-adds, postprocess reads. |
| 65 | + dq_accum_t* dqaccum; |
| 66 | + // [H*128, B*S] Q^T, written by the preprocess. |
| 67 | + __nv_bfloat16* qt; |
| 68 | + // [H*128, B*S] dO^T, written by the preprocess. |
| 69 | + __nv_bfloat16* dot; |
| 70 | + // [B*H*S] fp32 rowsum(bf16(o) * dout), written by the preprocess. |
| 71 | + float* delta; |
| 72 | + |
| 73 | + int batch; |
| 74 | + int num_heads; |
| 75 | + // S; a multiple of 128 (the preprocess works in 128-token blocks). |
| 76 | + int seqlen; |
| 77 | + // Must be 128. |
| 78 | + int head_dim; |
| 79 | + // nb = seqlen / 64. |
| 80 | + int num_kv_blocks_per_seq; |
| 81 | + // k2q_idx row stride (FastVideo passes nb). |
| 82 | + int max_q_blocks; |
| 83 | + // Softmax scale; dq and dk carry it, dv does not. |
| 84 | + float sm_scale; |
| 85 | +}; |
| 86 | + |
| 87 | +__host__ inline size_t block_sparse_bwd_dqaccum_bytes(int batch, int num_heads, int seqlen) { |
| 88 | + return (size_t)batch * num_heads * seqlen * HEAD_DIM * sizeof(dq_accum_t); |
| 89 | +} |
| 90 | +__host__ inline size_t block_sparse_bwd_order_bytes(int batch, int num_heads, |
| 91 | + int num_kv_blocks_per_seq) { |
| 92 | + return (size_t)batch * num_heads * num_kv_blocks_per_seq * sizeof(int); |
| 93 | +} |
| 94 | +__host__ inline size_t block_sparse_bwd_transposed_bytes(int batch, int num_heads, int seqlen) { |
| 95 | + return (size_t)num_heads * HEAD_DIM * (size_t)batch * seqlen * sizeof(__nv_bfloat16); |
| 96 | +} |
| 97 | +__host__ inline size_t block_sparse_bwd_delta_bytes(int batch, int num_heads, int seqlen) { |
| 98 | + return (size_t)batch * num_heads * seqlen * sizeof(float); |
| 99 | +} |
| 100 | + |
| 101 | +__host__ inline cudaError_t block_sparse_bwd_supported(const BlockSparseVsaBwdArgs& args) { |
| 102 | + if (args.head_dim != HEAD_DIM) { |
| 103 | + return cudaErrorInvalidValue; |
| 104 | + } |
| 105 | + if (args.num_kv_blocks_per_seq < 1 || args.num_kv_blocks_per_seq % PRE_QBLOCKS != 0) { |
| 106 | + return cudaErrorInvalidValue; |
| 107 | + } |
| 108 | + if (args.seqlen != args.num_kv_blocks_per_seq * BLOCK) { |
| 109 | + return cudaErrorInvalidValue; |
| 110 | + } |
| 111 | + if (args.batch < 1 || args.num_heads < 1 || args.max_q_blocks < 1) { |
| 112 | + return cudaErrorInvalidValue; |
| 113 | + } |
| 114 | + if (!std::isfinite(args.sm_scale)) { |
| 115 | + return cudaErrorInvalidValue; |
| 116 | + } |
| 117 | + if (!args.q || !args.k || !args.v || !args.o || !args.dout || !args.lse) { |
| 118 | + return cudaErrorInvalidValue; |
| 119 | + } |
| 120 | + if (!args.dq || !args.dk || !args.dv) { |
| 121 | + return cudaErrorInvalidValue; |
| 122 | + } |
| 123 | + if (!args.k2q_idx || !args.k2q_num || !args.variable_block_sizes) { |
| 124 | + return cudaErrorInvalidValue; |
| 125 | + } |
| 126 | + if (!args.dqaccum || !args.qt || !args.dot || !args.delta) { |
| 127 | + return cudaErrorInvalidValue; |
| 128 | + } |
| 129 | + // No explicit order: the order kernel needs the workspace and two ints of SMEM per kv block. |
| 130 | + if (!args.workitem_remap && |
| 131 | + (!args.order_workspace || args.num_kv_blocks_per_seq > ORDER_MAX_BLOCKS)) { |
| 132 | + return cudaErrorInvalidValue; |
| 133 | + } |
| 134 | + return cudaSuccess; |
| 135 | +} |
| 136 | + |
| 137 | +// K, V, dK, dV tensor maps, one 64-token x 64-hd box per TMA (two per tile): |
| 138 | +// BSHD: 3D [64 hd, B*S tokens, H*2 hd units], strides {H*128*2, 128} bytes. |
| 139 | +// BHSD: 4D [64 hd, S tokens, 2 hd units, B*H], strides {128*2, 128, S*128*2} bytes. |
| 140 | +__host__ inline cudaError_t make_tma_kv_units(CUtensorMap* map, const __nv_bfloat16* ptr, int B, |
| 141 | + int H, int S) { |
| 142 | + CUresult r; |
| 143 | + if (VSA_BHSD) { |
| 144 | + uint64_t gd[4] = {(uint64_t)SUB_COLS_BF16, (uint64_t)S, (uint64_t)KV_SUBTILES, (uint64_t)B * H}; |
| 145 | + uint64_t gs[3] = {(uint64_t)HEAD_DIM * 2, (uint64_t)SUB_COLS_BYTES, (uint64_t)S * HEAD_DIM * 2}; |
| 146 | + uint32_t bd[4] = {(uint32_t)SUB_COLS_BF16, (uint32_t)BLOCK, 1u, 1u}; |
| 147 | + uint32_t es[4] = {1u, 1u, 1u, 1u}; |
| 148 | + r = cuTensorMapEncodeTiled( |
| 149 | + map, CU_TENSOR_MAP_DATA_TYPE_BFLOAT16, 4, const_cast<__nv_bfloat16*>(ptr), gd, gs, bd, es, |
| 150 | + CU_TENSOR_MAP_INTERLEAVE_NONE, CU_TENSOR_MAP_SWIZZLE_128B, |
| 151 | + CU_TENSOR_MAP_L2_PROMOTION_L2_128B, CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE); |
| 152 | + } else { |
| 153 | + uint64_t gd[3] = {(uint64_t)SUB_COLS_BF16, (uint64_t)B * S, (uint64_t)H * KV_SUBTILES}; |
| 154 | + uint64_t gs[2] = {(uint64_t)H * HEAD_DIM * 2, (uint64_t)SUB_COLS_BYTES}; |
| 155 | + uint32_t bd[3] = {(uint32_t)SUB_COLS_BF16, (uint32_t)BLOCK, 1u}; |
| 156 | + uint32_t es[3] = {1u, 1u, 1u}; |
| 157 | + r = cuTensorMapEncodeTiled( |
| 158 | + map, CU_TENSOR_MAP_DATA_TYPE_BFLOAT16, 3, const_cast<__nv_bfloat16*>(ptr), gd, gs, bd, es, |
| 159 | + CU_TENSOR_MAP_INTERLEAVE_NONE, CU_TENSOR_MAP_SWIZZLE_128B, |
| 160 | + CU_TENSOR_MAP_L2_PROMOTION_L2_128B, CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE); |
| 161 | + } |
| 162 | + return (r == CUDA_SUCCESS) ? cudaSuccess : cudaErrorInvalidValue; |
| 163 | +} |
| 164 | + |
| 165 | +// Above the L2-capacity transition the main kernel is DRAM-bound and keeping a fractional |
| 166 | +// subset of the repeatedly reduced dQ lines resident pays (~0.6% at 524K/1M tokens); below it the |
| 167 | +// policy register costs more than it saves. Threshold scales with accumulator BYTES. |
| 168 | +constexpr int CACHE_WAVE_MIN_SEQ_LEN = 524288; |
| 169 | + |
| 170 | +// Below this many kv blocks per sequence (S < 65536) the identity order is as fast as the |
| 171 | +// length-binned one and the order kernel's own time is not (fv_perf_log.md 2026-09-04: -9% at |
| 172 | +// 4k, -2.8% at 16k), so callers pass a cached identity array as workitem_remap there and leave |
| 173 | +// the order kernel to the larger shapes. |
| 174 | +constexpr int ORDER_MIN_KV_BLOCKS = 1024; |
| 175 | + |
| 176 | +template <bool DQ_L2_KEEP, bool USE_CLC, bool BHSD> |
| 177 | +__host__ inline cudaError_t launch_main(const BlockSparseVsaBwdArgs& args, const int* work_remap, |
| 178 | + const CUtensorMap& tk, const CUtensorMap& tv, |
| 179 | + const CUtensorMap& tqt, const CUtensorMap& tdot, |
| 180 | + const CUtensorMap& tdk, const CUtensorMap& tdv, int sms, |
| 181 | + cudaStream_t stream) { |
| 182 | + auto kernel = vsa_bwd_main_kernel<DQ_L2_KEEP, USE_CLC, BHSD, dq_accum_t>; |
| 183 | + cudaError_t e = |
| 184 | + cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, SMEM_TOTAL); |
| 185 | + if (e != cudaSuccess) { |
| 186 | + return e; |
| 187 | + } |
| 188 | + const float scale_log2 = args.sm_scale * 1.4426950408889634f; |
| 189 | + const int B = args.batch, H = args.num_heads, S = args.seqlen; |
| 190 | + const int total = B * H * args.num_kv_blocks_per_seq; |
| 191 | + if constexpr (USE_CLC) { |
| 192 | + // Above the L2 transition, one SM-wide launch at a time keeps each list neighbourhood |
| 193 | + // resident; below it one launch of every item lets CLC steal freely. |
| 194 | + const int chunk = DQ_L2_KEEP ? std::min(sms, total) : total; |
| 195 | + cudaLaunchConfig_t cfg = {}; |
| 196 | + cfg.blockDim = dim3(N_WARPS * 32, 1, 1); |
| 197 | + cfg.dynamicSmemBytes = SMEM_TOTAL; |
| 198 | + cfg.stream = stream; |
| 199 | + cudaLaunchAttribute at[1]; |
| 200 | + at[0].id = cudaLaunchAttributeClusterDimension; |
| 201 | + at[0].val.clusterDim.x = 1; |
| 202 | + at[0].val.clusterDim.y = 1; |
| 203 | + at[0].val.clusterDim.z = 1; |
| 204 | + cfg.attrs = at; |
| 205 | + cfg.numAttrs = 1; |
| 206 | + for (int base = 0; base < total; base += chunk) { |
| 207 | + const int count = std::min(chunk, total - base); |
| 208 | + cfg.gridDim = dim3((unsigned)count, 1, 1); |
| 209 | + // A chunk starts at work id `base`: it gets the order's sub-array. |
| 210 | + e = cudaLaunchKernelEx(&cfg, kernel, tk, tv, tqt, tdot, tdk, tdv, args.dqaccum, args.lse, |
| 211 | + args.delta, args.k2q_idx, args.k2q_num, work_remap + base, |
| 212 | + args.variable_block_sizes, args.max_q_blocks, B, H, S, scale_log2, |
| 213 | + args.sm_scale); |
| 214 | + if (e != cudaSuccess) { |
| 215 | + return e; |
| 216 | + } |
| 217 | + } |
| 218 | + return cudaSuccess; |
| 219 | + } else { |
| 220 | + const int grid = std::min(total, sms); |
| 221 | + kernel<<<dim3((unsigned)grid, 1, 1), dim3(N_WARPS * 32, 1, 1), SMEM_TOTAL, stream>>>( |
| 222 | + tk, tv, tqt, tdot, tdk, tdv, args.dqaccum, args.lse, args.delta, args.k2q_idx, args.k2q_num, |
| 223 | + work_remap, args.variable_block_sizes, args.max_q_blocks, B, H, S, scale_log2, |
| 224 | + args.sm_scale); |
| 225 | + return cudaGetLastError(); |
| 226 | + } |
| 227 | +} |
| 228 | + |
| 229 | +__host__ inline cudaError_t launch_block_sparse_bwd_sm100a(const BlockSparseVsaBwdArgs& args, |
| 230 | + cudaStream_t stream) { |
| 231 | + const cudaError_t supported = block_sparse_bwd_supported(args); |
| 232 | + if (supported != cudaSuccess) { |
| 233 | + return supported; |
| 234 | + } |
| 235 | + const int B = args.batch, H = args.num_heads, S = args.seqlen; |
| 236 | + const long n_tokens = (long)B * S; |
| 237 | + |
| 238 | + CUtensorMap tk, tv, tqt, tdot, tdk, tdv; |
| 239 | + if (make_tma_kv_units(&tk, args.k, B, H, S) != cudaSuccess) { |
| 240 | + return cudaErrorInvalidValue; |
| 241 | + } |
| 242 | + if (make_tma_kv_units(&tv, args.v, B, H, S) != cudaSuccess) { |
| 243 | + return cudaErrorInvalidValue; |
| 244 | + } |
| 245 | + if (make_tma_kv_units(&tdk, args.dk, B, H, S) != cudaSuccess) { |
| 246 | + return cudaErrorInvalidValue; |
| 247 | + } |
| 248 | + if (make_tma_kv_units(&tdv, args.dv, B, H, S) != cudaSuccess) { |
| 249 | + return cudaErrorInvalidValue; |
| 250 | + } |
| 251 | + // Q^T, dO^T ([H*hd rows, B*S cols], token contiguous): box [hd rows, BLOCK cols] = one q64 |
| 252 | + // block per TMA. |
| 253 | + if (make_tma_2d_tiled(&tqt, args.qt, H * HEAD_DIM, (int)n_tokens, HEAD_DIM, BLOCK, 2, |
| 254 | + CU_TENSOR_MAP_DATA_TYPE_BFLOAT16) != cudaSuccess) { |
| 255 | + return cudaErrorInvalidValue; |
| 256 | + } |
| 257 | + if (make_tma_2d_tiled(&tdot, args.dot, H * HEAD_DIM, (int)n_tokens, HEAD_DIM, BLOCK, 2, |
| 258 | + CU_TENSOR_MAP_DATA_TYPE_BFLOAT16) != cudaSuccess) { |
| 259 | + return cudaErrorInvalidValue; |
| 260 | + } |
| 261 | + |
| 262 | + int dev = 0, sms = 0; |
| 263 | + cudaError_t e = cudaGetDevice(&dev); |
| 264 | + if (e != cudaSuccess) { |
| 265 | + return e; |
| 266 | + } |
| 267 | + e = cudaDeviceGetAttribute(&sms, cudaDevAttrMultiProcessorCount, dev); |
| 268 | + if (e != cudaSuccess) { |
| 269 | + return e; |
| 270 | + } |
| 271 | + |
| 272 | + vsa_bwd_preprocess_kernel<VSA_BHSD, dq_accum_t> |
| 273 | + <<<dim3((unsigned)(S / PRE_TOKENS), (unsigned)(B * H), 1), dim3(256, 1, 1), 0, stream>>>( |
| 274 | + args.q, args.o, args.dout, args.delta, args.dqaccum, args.qt, args.dot, args.dk, args.dv, |
| 275 | + args.k2q_num, B, H, S); |
| 276 | + e = cudaGetLastError(); |
| 277 | + if (e != cudaSuccess) { |
| 278 | + return e; |
| 279 | + } |
| 280 | + |
| 281 | + const bool keep_dq_l2 = (size_t)S * sizeof(dq_accum_t) >= (size_t)CACHE_WAVE_MIN_SEQ_LEN * 2; |
| 282 | + |
| 283 | + // Work-item order: explicit (the binding passes its cached identity array below |
| 284 | + // ORDER_MIN_KV_BLOCKS), else computed on device into order_workspace (length bins; the same L2 |
| 285 | + // transition that selects DQ_L2_KEEP selects the wider bins plus the midpoint snake). |
| 286 | + const int* work_remap = args.workitem_remap; |
| 287 | + if (work_remap == nullptr) { |
| 288 | + const int order_smem = 2 * args.num_kv_blocks_per_seq * (int)sizeof(int); |
| 289 | + if (order_smem > 48 * 1024) { |
| 290 | + e = cudaFuncSetAttribute(vsa_bwd_order_kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, |
| 291 | + order_smem); |
| 292 | + if (e != cudaSuccess) { |
| 293 | + return e; |
| 294 | + } |
| 295 | + } |
| 296 | + const unsigned item_chunks = |
| 297 | + (unsigned)((args.num_kv_blocks_per_seq + ORDER_THREADS - 1) / ORDER_THREADS); |
| 298 | + vsa_bwd_order_kernel<<<dim3((unsigned)(B * H), item_chunks, 1), dim3(ORDER_THREADS, 1, 1), |
| 299 | + order_smem, stream>>>(args.k2q_idx, args.k2q_num, args.max_q_blocks, |
| 300 | + args.num_kv_blocks_per_seq, keep_dq_l2 ? 12 : 8, |
| 301 | + keep_dq_l2, args.order_workspace); |
| 302 | + e = cudaGetLastError(); |
| 303 | + if (e != cudaSuccess) { |
| 304 | + return e; |
| 305 | + } |
| 306 | + work_remap = args.order_workspace; |
| 307 | + } |
| 308 | + |
| 309 | + e = keep_dq_l2 ? launch_main<true, VSA_BWD_USE_CLC, VSA_BHSD>(args, work_remap, tk, tv, tqt, tdot, |
| 310 | + tdk, tdv, sms, stream) |
| 311 | + : launch_main<false, VSA_BWD_USE_CLC, VSA_BHSD>(args, work_remap, tk, tv, tqt, |
| 312 | + tdot, tdk, tdv, sms, stream); |
| 313 | + if (e != cudaSuccess) { |
| 314 | + return e; |
| 315 | + } |
| 316 | + |
| 317 | + vsa_bwd_postprocess_kernel<VSA_BHSD, dq_accum_t> |
| 318 | + <<<dim3((unsigned)(S / BLOCK), (unsigned)(B * H), 1), dim3(128, 1, 1), 0, stream>>>( |
| 319 | + args.dqaccum, args.dq, H, S, args.sm_scale); |
| 320 | + return cudaGetLastError(); |
| 321 | +} |
| 322 | + |
| 323 | +} // namespace vsa_bwd_blk64 |
| 324 | + |
| 325 | +using namespace vsa_bwd_blk64; |
| 326 | + |
| 327 | +#endif // BLOCK_SPARSE_VSA_BWD_LAUNCH_SM100A_CUH |
0 commit comments