Skip to content

Commit 749a9c5

Browse files
lpc0220claude
andcommitted
[kernel] sm_100a CUDA backward for VSA block-sparse attention (64-token blocks)
Native GB200 (sm_100a) backward pairing with the sm_100a forward: one warp-specialized tcgen05 kernel per (batch, head, kv64 block) computing dK, dV and the dQ partials (fp32 accumulator, cp.reduce.async.bulk), plus a preprocess (Delta, Q^T, dO^T, dqaccum zero, exact-zero dK/dV rows for unselected kv blocks) and a postprocess (dQ unscramble + sm_scale). Consumes the forward's lse (Triton M format) and invert_indices' k2q metadata unchanged; returns (dq, dk, dv) in bf16 with the Triton backward's scaling. Wiring: with FASTVIDEO_VSA_SM100A=1 the autograd backward of the sm_100a forward op runs this kernel when block_sparse_attn_bwd_sm100a.is_supported passes (bf16, head_dim 128, 64-token blocks, even block count, sm_100a device, op built) and the Triton backward otherwise. No new environment variable. primitives.cuh gains only the backward's primitives (additions only; the forward's definitions and SASS are untouched). Built for sm_100a only: the kernel is validated on GB200, not yet on B300/GB300, so sm_103a devices keep the Triton backward. The H3 backend's grad gate is unchanged (separate PR). Semantics: per-row k2q counts may differ arbitrarily (one work item is one kv row; count 0 is skipped identically by every warp and gets zero dK/dV rows); variable_block_sizes masks padded kv rows per lane; is_supported reads no tensor contents. Work-item order: a device kernel sorts by list length from 1024 kv blocks per sequence on; below that a cached identity array is passed. Tests (GB200, extension built from this branch): tests/test_block_sparse_bwd_sm100a.py 15 passed (fp32 masked-dense autograd reference; ragged vbs, zero-count kv blocks, top-k 1/2/3/5/7, batch 2), tests/test_block_sparse_sm100a_dispatch.py 10 passed (sm_100a route vs all-Triton grads, ragged vbs, Triton backward monkeypatched to fail on a supported input), tests/test_block_sparse_sm100a.py 37 passed (forward regression). Perf (kernels only, same window, B=1 H=8 D=128, 25% density, fp32 accumulator, TFLOPS on selected blocks) Triton backward vs this kernel: 4k 198/381 (1.93x), 8k 313/575 (1.84x), 16k 389/715 (1.84x), 32k 433/794 (1.84x), 65k 448/821 (1.83x), 131k 452/736 (1.63x), 262k 452/784 (1.73x), 524k 437/771 (1.76x). FastVideo autograd path incl. invert_indices: 1.6-1.7x at Wan 480P/720P-like shapes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 7bb76b5 commit 749a9c5

11 files changed

Lines changed: 2239 additions & 24 deletions

fastvideo-kernel/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,12 @@ if(BUILD_CXX_KERNELS)
411411
set_source_files_properties(csrc/attention/block_sparse_sm100a.cu
412412
csrc/attention/block_sparse_blk128_sm100a.cu PROPERTIES
413413
COMPILE_OPTIONS "-gencode;arch=compute_100a,code=sm_100a;-gencode;arch=compute_103a,code=sm_103a;-DVSA_BHSD=true")
414+
# VSA block-sparse attention BACKWARD, 64-token blocks. sm_100a only for now: validated
415+
# on GB200, not yet on B300/GB300, so no sm_103a image is built and the Python side keeps
416+
# the Triton backward for sm_103a devices.
417+
list(APPEND EXTENSION_SOURCES csrc/attention/block_sparse_bwd_sm100a.cu)
418+
set_source_files_properties(csrc/attention/block_sparse_bwd_sm100a.cu PROPERTIES
419+
COMPILE_OPTIONS "-gencode;arch=compute_100a,code=sm_100a;-DVSA_BHSD=true")
414420
endif()
415421

416422
Python_add_library(fastvideo_kernel_ops MODULE USE_SABI ${SKBUILD_SABI_VERSION} WITH_SOABI

fastvideo-kernel/csrc/attention/block_sparse_bwd_kernel_sm100a.cuh

Lines changed: 1081 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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

Comments
 (0)