danieldk HF Staff commited on
Commit
bd33335
·
verified ·
1 Parent(s): 5d95645

Build uploaded using `kernels`.

Browse files
Files changed (36) hide show
  1. build/torch-universal/__init__.py +21 -0
  2. build/torch-universal/_ops.py +8 -0
  3. build/torch-universal/ampere_helpers.py +103 -0
  4. build/torch-universal/barrier.py +71 -0
  5. build/torch-universal/benchmark.py +268 -0
  6. build/torch-universal/benchmark_mask_mod.py +686 -0
  7. build/torch-universal/blackwell_helpers.py +753 -0
  8. build/torch-universal/block_info.py +104 -0
  9. build/torch-universal/block_sparse_utils.py +419 -0
  10. build/torch-universal/block_sparsity.py +650 -0
  11. build/torch-universal/copy_utils.py +340 -0
  12. build/torch-universal/cute_dsl_utils.py +124 -0
  13. build/torch-universal/fast_math.py +97 -0
  14. build/torch-universal/flash_attn_cute/__init__.py +26 -0
  15. build/torch-universal/flash_bwd.py +1261 -0
  16. build/torch-universal/flash_bwd_postprocess.py +729 -0
  17. build/torch-universal/flash_bwd_preprocess.py +363 -0
  18. build/torch-universal/flash_bwd_sm100.py +0 -0
  19. build/torch-universal/flash_bwd_sm90.py +1244 -0
  20. build/torch-universal/flash_fwd.py +0 -0
  21. build/torch-universal/flash_fwd_combine.py +644 -0
  22. build/torch-universal/flash_fwd_sm100.py +0 -0
  23. build/torch-universal/hopper_helpers.py +102 -0
  24. build/torch-universal/interface.py +1483 -0
  25. build/torch-universal/mask.py +413 -0
  26. build/torch-universal/mask_definitions.py +220 -0
  27. build/torch-universal/metadata.json +1 -0
  28. build/torch-universal/mma_sm100_desc.py +291 -0
  29. build/torch-universal/named_barrier.py +31 -0
  30. build/torch-universal/pack_gqa.py +166 -0
  31. build/torch-universal/pipeline.py +365 -0
  32. build/torch-universal/seqlen_info.py +99 -0
  33. build/torch-universal/softmax.py +440 -0
  34. build/torch-universal/testing.py +404 -0
  35. build/torch-universal/tile_scheduler.py +695 -0
  36. build/torch-universal/utils.py +788 -0
build/torch-universal/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flash Attention CUTE (CUDA Template Engine) implementation."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ import cutlass.cute as cute
6
+
7
+ from .interface import (
8
+ flash_attn_func,
9
+ flash_attn_varlen_func,
10
+ )
11
+
12
+ from .cute_dsl_utils import cute_compile_patched
13
+
14
+ # Patch cute.compile to optionally dump SASS
15
+ cute.compile = cute_compile_patched
16
+
17
+
18
+ __all__ = [
19
+ "flash_attn_func",
20
+ "flash_attn_varlen_func",
21
+ ]
build/torch-universal/_ops.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ ops = torch.ops._flash_attn_cute_cae4d59_dirty
3
+
4
+ def add_op_namespace_prefix(op_name: str):
5
+ """
6
+ Prefix op by namespace.
7
+ """
8
+ return f"_flash_attn_cute_cae4d59_dirty::{op_name}"
build/torch-universal/ampere_helpers.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ from typing import Type, Callable, Optional
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+
7
+
8
+ def get_smem_layout_atom(dtype: Type[cutlass.Numeric], k_dim: int) -> cute.ComposedLayout:
9
+ dtype_byte = cutlass.const_expr(dtype.width // 8)
10
+ bytes_per_row = cutlass.const_expr(k_dim * dtype_byte)
11
+ smem_k_block_size = (
12
+ cutlass.const_expr(
13
+ 128
14
+ if bytes_per_row % 128 == 0
15
+ else (64 if bytes_per_row % 64 == 0 else (32 if bytes_per_row % 32 == 0 else 16))
16
+ )
17
+ // dtype_byte
18
+ )
19
+ swizzle_bits = (
20
+ 4
21
+ if smem_k_block_size == 128
22
+ else (3 if smem_k_block_size == 64 else (2 if smem_k_block_size == 32 else 1))
23
+ )
24
+ swizzle_base = 2 if dtype_byte == 4 else (3 if dtype_byte == 2 else 4)
25
+ return cute.make_composed_layout(
26
+ cute.make_swizzle(swizzle_bits, swizzle_base, swizzle_base),
27
+ 0,
28
+ cute.make_ordered_layout(
29
+ (8 if cutlass.const_expr(k_dim % 32 == 0) else 16, smem_k_block_size), order=(1, 0)
30
+ ),
31
+ )
32
+
33
+
34
+ @cute.jit
35
+ def gemm(
36
+ tiled_mma: cute.TiledMma,
37
+ acc: cute.Tensor,
38
+ tCrA: cute.Tensor,
39
+ tCrB: cute.Tensor,
40
+ tCsA: cute.Tensor,
41
+ tCsB: cute.Tensor,
42
+ smem_thr_copy_A: cute.TiledCopy,
43
+ smem_thr_copy_B: cute.TiledCopy,
44
+ hook_fn: Optional[Callable] = None,
45
+ A_in_regs: cutlass.Constexpr[bool] = False,
46
+ B_in_regs: cutlass.Constexpr[bool] = False,
47
+ swap_AB: cutlass.Constexpr[bool] = False,
48
+ ) -> None:
49
+ if cutlass.const_expr(swap_AB):
50
+ gemm(
51
+ tiled_mma,
52
+ acc,
53
+ tCrB,
54
+ tCrA,
55
+ tCsB,
56
+ tCsA,
57
+ smem_thr_copy_B,
58
+ smem_thr_copy_A,
59
+ hook_fn,
60
+ A_in_regs=B_in_regs,
61
+ B_in_regs=A_in_regs,
62
+ swap_AB=False,
63
+ )
64
+ else:
65
+ tCrA_copy_view = smem_thr_copy_A.retile(tCrA)
66
+ tCrB_copy_view = smem_thr_copy_B.retile(tCrB)
67
+ if cutlass.const_expr(not A_in_regs):
68
+ cute.copy(smem_thr_copy_A, tCsA[None, None, 0], tCrA_copy_view[None, None, 0])
69
+ if cutlass.const_expr(not B_in_regs):
70
+ cute.copy(smem_thr_copy_B, tCsB[None, None, 0], tCrB_copy_view[None, None, 0])
71
+ for k in cutlass.range_constexpr(cute.size(tCsA.shape[2])):
72
+ if k < cute.size(tCsA.shape[2]) - 1:
73
+ if cutlass.const_expr(not A_in_regs):
74
+ cute.copy(
75
+ smem_thr_copy_A, tCsA[None, None, k + 1], tCrA_copy_view[None, None, k + 1]
76
+ )
77
+ if cutlass.const_expr(not B_in_regs):
78
+ cute.copy(
79
+ smem_thr_copy_B, tCsB[None, None, k + 1], tCrB_copy_view[None, None, k + 1]
80
+ )
81
+ cute.gemm(tiled_mma, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
82
+ if cutlass.const_expr(k == 0 and hook_fn is not None):
83
+ hook_fn()
84
+
85
+
86
+ @cute.jit
87
+ def gemm_rs(
88
+ tiled_mma: cute.TiledMma,
89
+ acc: cute.Tensor,
90
+ tCrA: cute.Tensor,
91
+ tCrB: cute.Tensor,
92
+ tCsB: cute.Tensor,
93
+ smem_thr_copy_B: cute.TiledCopy,
94
+ hook_fn: Optional[Callable] = None,
95
+ ) -> None:
96
+ tCrB_copy_view = smem_thr_copy_B.retile(tCrB)
97
+ cute.copy(smem_thr_copy_B, tCsB[None, None, 0], tCrB_copy_view[None, None, 0])
98
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
99
+ if cutlass.const_expr(k < cute.size(tCrA.shape[2]) - 1):
100
+ cute.copy(smem_thr_copy_B, tCsB[None, None, k + 1], tCrB_copy_view[None, None, k + 1])
101
+ cute.gemm(tiled_mma, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
102
+ if cutlass.const_expr(k == 0 and hook_fn is not None):
103
+ hook_fn()
build/torch-universal/barrier.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cutlass
2
+ import cutlass.cute as cute
3
+ from cutlass import Int32
4
+ from cutlass.cutlass_dsl import T, dsl_user_op
5
+ from cutlass._mlir.dialects import llvm
6
+
7
+
8
+ @dsl_user_op
9
+ def ld_acquire(lock_ptr: cute.Pointer, *, loc=None, ip=None) -> cutlass.Int32:
10
+ lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
11
+ state = llvm.inline_asm(
12
+ T.i32(),
13
+ [lock_ptr_i64],
14
+ "ld.global.acquire.gpu.b32 $0, [$1];",
15
+ "=r,l",
16
+ has_side_effects=True,
17
+ is_align_stack=False,
18
+ asm_dialect=llvm.AsmDialect.AD_ATT,
19
+ )
20
+ return cutlass.Int32(state)
21
+
22
+
23
+ @dsl_user_op
24
+ def red_relaxed(
25
+ lock_ptr: cute.Pointer, val: cutlass.Constexpr[Int32], *, loc=None, ip=None
26
+ ) -> None:
27
+ lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
28
+ llvm.inline_asm(
29
+ None,
30
+ [lock_ptr_i64, Int32(val).ir_value(loc=loc, ip=ip)],
31
+ "red.relaxed.gpu.global.add.s32 [$0], $1;",
32
+ "l,r",
33
+ has_side_effects=True,
34
+ is_align_stack=False,
35
+ asm_dialect=llvm.AsmDialect.AD_ATT,
36
+ )
37
+
38
+
39
+ @dsl_user_op
40
+ def red_release(
41
+ lock_ptr: cute.Pointer, val: cutlass.Constexpr[Int32], *, loc=None, ip=None
42
+ ) -> None:
43
+ lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
44
+ llvm.inline_asm(
45
+ None,
46
+ [lock_ptr_i64, Int32(val).ir_value(loc=loc, ip=ip)],
47
+ "red.release.gpu.global.add.s32 [$0], $1;",
48
+ "l,r",
49
+ has_side_effects=True,
50
+ is_align_stack=False,
51
+ asm_dialect=llvm.AsmDialect.AD_ATT,
52
+ )
53
+
54
+
55
+ @cute.jit
56
+ def wait_eq(lock_ptr: cute.Pointer, thread_idx: int | Int32, flag_offset: int, val: Int32) -> None:
57
+ flag_ptr = lock_ptr + flag_offset
58
+ if thread_idx == 0:
59
+ read_val = Int32(0)
60
+ while read_val != val:
61
+ read_val = ld_acquire(flag_ptr)
62
+
63
+
64
+ @cute.jit
65
+ def arrive_inc(
66
+ lock_ptr: cute.Pointer, thread_idx: int | Int32, flag_offset: int, val: cutlass.Constexpr[Int32]
67
+ ) -> None:
68
+ flag_ptr = lock_ptr + flag_offset
69
+ if thread_idx == 0:
70
+ red_release(flag_ptr, val)
71
+ # red_relaxed(flag_ptr, val)
build/torch-universal/benchmark.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2023, Tri Dao.
2
+ """Useful functions for writing test code."""
3
+
4
+ import torch
5
+ import torch.utils.benchmark as benchmark
6
+
7
+
8
+ def benchmark_forward(
9
+ fn, *inputs, repeats=10, desc="", verbose=True, amp=False, amp_dtype=torch.float16, **kwinputs
10
+ ):
11
+ """Use Pytorch Benchmark on the forward pass of an arbitrary function."""
12
+ if verbose:
13
+ print(desc, "- Forward pass")
14
+
15
+ def amp_wrapper(*inputs, **kwinputs):
16
+ with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
17
+ fn(*inputs, **kwinputs)
18
+
19
+ t = benchmark.Timer(
20
+ stmt="fn_amp(*inputs, **kwinputs)",
21
+ globals={"fn_amp": amp_wrapper, "inputs": inputs, "kwinputs": kwinputs},
22
+ num_threads=torch.get_num_threads(),
23
+ )
24
+ m = t.timeit(repeats)
25
+ if verbose:
26
+ print(m)
27
+ return t, m
28
+
29
+
30
+ def benchmark_backward(
31
+ fn,
32
+ *inputs,
33
+ grad=None,
34
+ repeats=10,
35
+ desc="",
36
+ verbose=True,
37
+ amp=False,
38
+ amp_dtype=torch.float16,
39
+ **kwinputs,
40
+ ):
41
+ """Use Pytorch Benchmark on the backward pass of an arbitrary function."""
42
+ if verbose:
43
+ print(desc, "- Backward pass")
44
+ with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
45
+ y = fn(*inputs, **kwinputs)
46
+ if type(y) is tuple:
47
+ y = y[0]
48
+ if grad is None:
49
+ grad = torch.randn_like(y)
50
+ else:
51
+ if grad.shape != y.shape:
52
+ raise RuntimeError("Grad shape does not match output shape")
53
+
54
+ def f(*inputs, y, grad):
55
+ # Set .grad to None to avoid extra operation of gradient accumulation
56
+ for x in inputs:
57
+ if isinstance(x, torch.Tensor):
58
+ x.grad = None
59
+ y.backward(grad, retain_graph=True)
60
+
61
+ t = benchmark.Timer(
62
+ stmt="f(*inputs, y=y, grad=grad)",
63
+ globals={"f": f, "inputs": inputs, "y": y, "grad": grad},
64
+ num_threads=torch.get_num_threads(),
65
+ )
66
+ m = t.timeit(repeats)
67
+ if verbose:
68
+ print(m)
69
+ return t, m
70
+
71
+
72
+ def benchmark_combined(
73
+ fn,
74
+ *inputs,
75
+ grad=None,
76
+ repeats=10,
77
+ desc="",
78
+ verbose=True,
79
+ amp=False,
80
+ amp_dtype=torch.float16,
81
+ **kwinputs,
82
+ ):
83
+ """Use Pytorch Benchmark on the forward+backward pass of an arbitrary function."""
84
+ if verbose:
85
+ print(desc, "- Forward + Backward pass")
86
+ with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
87
+ y = fn(*inputs, **kwinputs)
88
+ if type(y) is tuple:
89
+ y = y[0]
90
+ if grad is None:
91
+ grad = torch.randn_like(y)
92
+ else:
93
+ if grad.shape != y.shape:
94
+ raise RuntimeError("Grad shape does not match output shape")
95
+
96
+ def f(grad, *inputs, **kwinputs):
97
+ for x in inputs:
98
+ if isinstance(x, torch.Tensor):
99
+ x.grad = None
100
+ with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
101
+ y = fn(*inputs, **kwinputs)
102
+ if type(y) is tuple:
103
+ y = y[0]
104
+ y.backward(grad, retain_graph=True)
105
+
106
+ t = benchmark.Timer(
107
+ stmt="f(grad, *inputs, **kwinputs)",
108
+ globals={"f": f, "fn": fn, "inputs": inputs, "grad": grad, "kwinputs": kwinputs},
109
+ num_threads=torch.get_num_threads(),
110
+ )
111
+ m = t.timeit(repeats)
112
+ if verbose:
113
+ print(m)
114
+ return t, m
115
+
116
+
117
+ def benchmark_fwd_bwd(
118
+ fn,
119
+ *inputs,
120
+ grad=None,
121
+ repeats=10,
122
+ desc="",
123
+ verbose=True,
124
+ amp=False,
125
+ amp_dtype=torch.float16,
126
+ **kwinputs,
127
+ ):
128
+ """Use Pytorch Benchmark on the forward+backward pass of an arbitrary function."""
129
+ return (
130
+ benchmark_forward(
131
+ fn,
132
+ *inputs,
133
+ repeats=repeats,
134
+ desc=desc,
135
+ verbose=verbose,
136
+ amp=amp,
137
+ amp_dtype=amp_dtype,
138
+ **kwinputs,
139
+ ),
140
+ benchmark_backward(
141
+ fn,
142
+ *inputs,
143
+ grad=grad,
144
+ repeats=repeats,
145
+ desc=desc,
146
+ verbose=verbose,
147
+ amp=amp,
148
+ amp_dtype=amp_dtype,
149
+ **kwinputs,
150
+ ),
151
+ )
152
+
153
+
154
+ def benchmark_all(
155
+ fn,
156
+ *inputs,
157
+ grad=None,
158
+ repeats=10,
159
+ desc="",
160
+ verbose=True,
161
+ amp=False,
162
+ amp_dtype=torch.float16,
163
+ **kwinputs,
164
+ ):
165
+ """Use Pytorch Benchmark on the forward+backward pass of an arbitrary function."""
166
+ return (
167
+ benchmark_forward(
168
+ fn,
169
+ *inputs,
170
+ repeats=repeats,
171
+ desc=desc,
172
+ verbose=verbose,
173
+ amp=amp,
174
+ amp_dtype=amp_dtype,
175
+ **kwinputs,
176
+ ),
177
+ benchmark_backward(
178
+ fn,
179
+ *inputs,
180
+ grad=grad,
181
+ repeats=repeats,
182
+ desc=desc,
183
+ verbose=verbose,
184
+ amp=amp,
185
+ amp_dtype=amp_dtype,
186
+ **kwinputs,
187
+ ),
188
+ benchmark_combined(
189
+ fn,
190
+ *inputs,
191
+ grad=grad,
192
+ repeats=repeats,
193
+ desc=desc,
194
+ verbose=verbose,
195
+ amp=amp,
196
+ amp_dtype=amp_dtype,
197
+ **kwinputs,
198
+ ),
199
+ )
200
+
201
+
202
+ def pytorch_profiler(
203
+ fn,
204
+ *inputs,
205
+ trace_filename=None,
206
+ backward=False,
207
+ amp=False,
208
+ amp_dtype=torch.float16,
209
+ cpu=False,
210
+ verbose=True,
211
+ **kwinputs,
212
+ ):
213
+ """Wrap benchmark functions in Pytorch profiler to see CUDA information."""
214
+ if backward:
215
+ with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
216
+ out = fn(*inputs, **kwinputs)
217
+ if type(out) is tuple:
218
+ out = out[0]
219
+ g = torch.randn_like(out)
220
+ for _ in range(30): # Warm up
221
+ if backward:
222
+ for x in inputs:
223
+ if isinstance(x, torch.Tensor):
224
+ x.grad = None
225
+ with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
226
+ out = fn(*inputs, **kwinputs)
227
+ if type(out) is tuple:
228
+ out = out[0]
229
+ # Backward should be done outside autocast
230
+ if backward:
231
+ out.backward(g, retain_graph=True)
232
+ activities = ([torch.profiler.ProfilerActivity.CPU] if cpu else []) + [
233
+ torch.profiler.ProfilerActivity.CUDA
234
+ ]
235
+ with torch.profiler.profile(
236
+ activities=activities,
237
+ record_shapes=True,
238
+ # profile_memory=True,
239
+ with_stack=True,
240
+ ) as prof:
241
+ if backward:
242
+ for x in inputs:
243
+ if isinstance(x, torch.Tensor):
244
+ x.grad = None
245
+ with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
246
+ out = fn(*inputs, **kwinputs)
247
+ if type(out) is tuple:
248
+ out = out[0]
249
+ if backward:
250
+ out.backward(g, retain_graph=True)
251
+ if verbose:
252
+ # print(prof.key_averages().table(sort_by="self_cuda_time_total", row_limit=50))
253
+ print(prof.key_averages().table(row_limit=50))
254
+ if trace_filename is not None:
255
+ prof.export_chrome_trace(trace_filename)
256
+
257
+
258
+ def benchmark_memory(fn, *inputs, desc="", verbose=True, **kwinputs):
259
+ torch.cuda.empty_cache()
260
+ torch.cuda.reset_peak_memory_stats()
261
+ torch.cuda.synchronize()
262
+ fn(*inputs, **kwinputs)
263
+ torch.cuda.synchronize()
264
+ mem = torch.cuda.max_memory_allocated() / ((2**20) * 1000)
265
+ if verbose:
266
+ print(f"{desc} max memory: {mem}GB")
267
+ torch.cuda.empty_cache()
268
+ return mem
build/torch-universal/benchmark_mask_mod.py ADDED
@@ -0,0 +1,686 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FlashAttention benchmarking script with Flex Attention-style
3
+ mask mod support and varlen sequences.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ import math
8
+ from typing import Any, Dict, Optional, Tuple
9
+
10
+ import cuda.bindings.driver as cuda
11
+ import cutlass
12
+ import cutlass.cute as cute
13
+ from cutlass.cute.runtime import from_dlpack
14
+ import numpy as np
15
+ import torch
16
+
17
+ from flash_fwd import FlashAttentionForwardSm90
18
+ from mask_definitions import (
19
+ get_mask_pair,
20
+ random_doc_id_tensor,
21
+ )
22
+ from .block_sparsity import (
23
+ compute_block_sparsity,
24
+ BlockSparseTensorsTorch,
25
+ to_cute_block_sparse_tensors,
26
+ )
27
+
28
+
29
+ @dataclass
30
+ class BenchmarkConfig:
31
+ """Benchmark configuration"""
32
+
33
+ # Model parameters
34
+ headdim: int
35
+ headdim_v: int
36
+ nheads: int
37
+ nheads_kv: int
38
+ dtype: torch.dtype
39
+
40
+ # Sequence parameters
41
+ batch_size: int = 2
42
+ seqlen_q: int = 8192
43
+ seqlen_k: int = 8192
44
+
45
+ # Varlen parameters
46
+ use_varlen: bool = False
47
+ min_seqlen_q: Optional[int] = None # If None, use seqlen_q // 2
48
+ max_seqlen_q: Optional[int] = None # If None, use seqlen_q
49
+ min_seqlen_k: Optional[int] = None # If None, use seqlen_k // 2
50
+ max_seqlen_k: Optional[int] = None # If None, use seqlen_k
51
+
52
+ # Mask parameters
53
+ use_mask_mod: bool = True
54
+ mask_mod_name: str = "causal"
55
+ has_aux_tensors: bool = mask_mod_name == "document"
56
+
57
+ # Sliding window parameter (used when mask_mod_name == "sliding_window")
58
+ window_size: int = 128
59
+
60
+ # Attention parameters
61
+ causal: bool = False
62
+ is_local: bool = False
63
+ window_left: Optional[int] = 128 # For base Flash Attention local
64
+ window_right: Optional[int] = 0 # For base Flash Attention local
65
+ softcap: Optional[float] = None
66
+ use_learnable_sink: bool = False
67
+
68
+ # Kernel configuration
69
+ tile_m: int = 128
70
+ tile_n: int = 128
71
+ num_stages: int = 2
72
+ num_threads: int = 384
73
+ intra_wg_overlap: bool = True
74
+ mma_pv_is_rs: bool = True
75
+
76
+ # Benchmark parameters
77
+ warmup_iters: int = 5
78
+ benchmark_iters: int = 20
79
+ verbose: bool = False
80
+ seed: int = 42
81
+
82
+
83
+ class FlashAttentionBenchmark:
84
+ def __init__(self, config: BenchmarkConfig):
85
+ self.config = config
86
+
87
+ torch.manual_seed(config.seed)
88
+ np.random.seed(config.seed)
89
+
90
+ # Verify SM90 compute capability
91
+ compute_capability = torch.cuda.get_device_capability()
92
+ assert compute_capability >= (9, 0), (
93
+ f"Requires SM90+, got SM{compute_capability[0]}{compute_capability[1]}"
94
+ )
95
+ # causal overrides use_mask_mod
96
+ if config.causal:
97
+ config.use_mask_mod = False
98
+
99
+ if config.use_mask_mod:
100
+ self.mask_mod_cute, self.mask_mod_flex = get_mask_pair(
101
+ config.mask_mod_name,
102
+ seqlen_q=config.seqlen_q,
103
+ seqlen_k=config.seqlen_k,
104
+ window_size=config.window_size,
105
+ )
106
+ else:
107
+ self.mask_mod_cute = None
108
+ self.mask_mod_flex = None
109
+
110
+ self._validate_config()
111
+
112
+ def _validate_config(self):
113
+ config = self.config
114
+
115
+ assert config.headdim <= 256, "headdim must be <= 256"
116
+ assert config.headdim_v <= 256, "headdim_v must be <= 256"
117
+ assert config.nheads % config.nheads_kv == 0, "nheads must be divisible by nheads_kv"
118
+
119
+ alignment = 16 // config.dtype.itemsize
120
+ assert config.headdim % alignment == 0, f"headdim must be divisible by {alignment}"
121
+ assert config.headdim_v % alignment == 0, f"headdim_v must be divisible by {alignment}"
122
+
123
+ # Validate is_local configuration
124
+ if config.is_local:
125
+ assert config.window_left is not None or config.window_right is not None, (
126
+ "When is_local=True, at least one of window_left or window_right must be set"
127
+ )
128
+ assert not config.use_mask_mod, (
129
+ "Cannot use both is_local and use_mask_mod simultaneously"
130
+ )
131
+ assert not config.causal, "Cannot use both is_local and causal simultaneously"
132
+
133
+ # Validate mask_mod configuration
134
+ if config.use_mask_mod and config.mask_mod_name == "sliding_window":
135
+ assert config.window_size > 0, (
136
+ "window_size must be positive when using sliding_window mask"
137
+ )
138
+
139
+ def _generate_varlen_seqlens(self, min_len: int, max_len: int) -> Tuple[torch.Tensor, int]:
140
+ """Generate random sequence lengths and compute cumulative lengths."""
141
+ seqlens = torch.randint(
142
+ min_len, max_len + 1, (self.config.batch_size,), dtype=torch.int32, device="cuda"
143
+ )
144
+ cu_seqlens = torch.cat(
145
+ [
146
+ torch.zeros(1, dtype=torch.int32, device="cuda"),
147
+ torch.cumsum(seqlens, dtype=torch.int32, dim=0),
148
+ ]
149
+ )
150
+
151
+ total_tokens = cu_seqlens[-1].item()
152
+ return cu_seqlens, total_tokens
153
+
154
+ def _create_tensors(self) -> Dict[str, torch.Tensor]:
155
+ config = self.config
156
+ device = "cuda"
157
+
158
+ if config.use_varlen:
159
+ # Set defaults for varlen range
160
+ min_q = config.min_seqlen_q if config.min_seqlen_q is not None else config.seqlen_q // 2
161
+ max_q = config.max_seqlen_q if config.max_seqlen_q is not None else config.seqlen_q
162
+ min_k = config.min_seqlen_k if config.min_seqlen_k is not None else config.seqlen_k // 2
163
+ max_k = config.max_seqlen_k if config.max_seqlen_k is not None else config.seqlen_k
164
+
165
+ # Generate cu_seqlens
166
+ cu_seqlens_q, total_q = self._generate_varlen_seqlens(min_q, max_q)
167
+ cu_seqlens_k, total_k = self._generate_varlen_seqlens(min_k, max_k)
168
+
169
+ # Varlen shape: (total_tokens, nheads, headdim)
170
+ q = torch.randn(
171
+ total_q, config.nheads, config.headdim, dtype=config.dtype, device=device
172
+ )
173
+ k = torch.randn(
174
+ total_k, config.nheads_kv, config.headdim, dtype=config.dtype, device=device
175
+ )
176
+ v = torch.randn(
177
+ total_k, config.nheads_kv, config.headdim_v, dtype=config.dtype, device=device
178
+ )
179
+ out = torch.empty(
180
+ total_q, config.nheads, config.headdim_v, dtype=config.dtype, device=device
181
+ )
182
+ lse = torch.empty(config.nheads, total_q, dtype=torch.float32, device=device)
183
+
184
+ tensors = {
185
+ "q": q.contiguous(),
186
+ "k": k.contiguous(),
187
+ "v": v.contiguous(),
188
+ "out": out.contiguous(),
189
+ "lse": lse.contiguous(),
190
+ "cu_seqlens_q": cu_seqlens_q.contiguous(),
191
+ "cu_seqlens_k": cu_seqlens_k.contiguous(),
192
+ }
193
+
194
+ if config.verbose:
195
+ print(f"Varlen: total_q={total_q}, total_k={total_k}")
196
+ print(f"Q seqlens: {cu_seqlens_q[1:] - cu_seqlens_q[:-1]}")
197
+ print(f"K seqlens: {cu_seqlens_k[1:] - cu_seqlens_k[:-1]}")
198
+ else:
199
+ # Standard shape: (batch, seqlen, nheads, headdim)
200
+ q = torch.randn(
201
+ config.batch_size,
202
+ config.seqlen_q,
203
+ config.nheads,
204
+ config.headdim,
205
+ dtype=config.dtype,
206
+ device=device,
207
+ )
208
+ k = torch.randn(
209
+ config.batch_size,
210
+ config.seqlen_k,
211
+ config.nheads_kv,
212
+ config.headdim,
213
+ dtype=config.dtype,
214
+ device=device,
215
+ )
216
+ v = torch.randn(
217
+ config.batch_size,
218
+ config.seqlen_k,
219
+ config.nheads_kv,
220
+ config.headdim_v,
221
+ dtype=config.dtype,
222
+ device=device,
223
+ )
224
+ out = torch.empty(
225
+ config.batch_size,
226
+ config.seqlen_q,
227
+ config.nheads,
228
+ config.headdim_v,
229
+ dtype=config.dtype,
230
+ device=device,
231
+ )
232
+ lse = torch.empty(
233
+ config.batch_size,
234
+ config.nheads,
235
+ config.seqlen_q,
236
+ dtype=torch.float32,
237
+ device=device,
238
+ )
239
+
240
+ tensors = {
241
+ "q": q.contiguous(),
242
+ "k": k.contiguous(),
243
+ "v": v.contiguous(),
244
+ "out": out.contiguous(),
245
+ "lse": lse.contiguous(),
246
+ }
247
+
248
+ if config.use_learnable_sink:
249
+ learnable_sink = torch.rand(config.nheads, dtype=torch.bfloat16, device=device)
250
+
251
+ tensors["learnable_sink"] = learnable_sink.contiguous()
252
+
253
+ # Compute block sparsity when using mask_mod
254
+ if config.use_mask_mod:
255
+ if config.mask_mod_name == "document":
256
+ doc_id = random_doc_id_tensor(
257
+ config.batch_size, config.nheads, config.seqlen_q, device=device
258
+ )
259
+ tensors["aux_tensors"] = [doc_id.contiguous()]
260
+ full_cnt, full_idx, mask_cnt, mask_idx = compute_block_sparsity(
261
+ config=self.config,
262
+ mask_mod_flex=self.mask_mod_flex,
263
+ device=device,
264
+ cu_seqlens_q=tensors.get("cu_seqlens_q"),
265
+ cu_seqlens_k=tensors.get("cu_seqlens_k"),
266
+ aux_tensors=tensors.get("aux_tensors"),
267
+ )
268
+
269
+ if all(t is not None for t in [full_cnt, full_idx, mask_cnt, mask_idx]):
270
+ tensors["block_sparse_tensors"] = BlockSparseTensorsTorch(
271
+ mask_block_cnt=mask_cnt.contiguous(),
272
+ mask_block_idx=mask_idx.contiguous(),
273
+ full_block_cnt=full_cnt.contiguous(),
274
+ full_block_idx=full_idx.contiguous(),
275
+ )
276
+
277
+ if config.verbose:
278
+ total_full = full_cnt.sum().item()
279
+ total_partial = mask_cnt.sum().item()
280
+
281
+ if config.use_varlen:
282
+ # Compute max possible blocks across all sequences
283
+ max_blocks = 0
284
+ for i in range(config.batch_size):
285
+ seq_len_q = (
286
+ tensors["cu_seqlens_q"][i + 1] - tensors["cu_seqlens_q"][i]
287
+ ).item()
288
+ seq_len_k = (
289
+ tensors["cu_seqlens_k"][i + 1] - tensors["cu_seqlens_k"][i]
290
+ ).item()
291
+ n_blocks_q = (seq_len_q + config.tile_m - 1) // config.tile_m
292
+ n_blocks_k = (seq_len_k + config.tile_n - 1) // config.tile_n
293
+ max_blocks += n_blocks_q * n_blocks_k * config.nheads
294
+ else:
295
+ n_blocks_k = (config.seqlen_k + config.tile_n - 1) // config.tile_n
296
+ n_blocks_q = (config.seqlen_q + config.tile_m - 1) // config.tile_m
297
+ max_blocks = n_blocks_k * n_blocks_q * config.nheads * config.batch_size
298
+
299
+ skipped = max_blocks - total_full - total_partial
300
+ print(
301
+ f"Block stats: Full={total_full}, Partial={total_partial}, "
302
+ f"Skipped={skipped}/{max_blocks}"
303
+ )
304
+
305
+ return tensors
306
+
307
+ def _compile_kernel(self, tensors: Dict[str, torch.Tensor]) -> Tuple[Any, tuple]:
308
+ config = self.config
309
+
310
+ dtype_map = {
311
+ torch.float16: cutlass.Float16,
312
+ torch.bfloat16: cutlass.BFloat16,
313
+ torch.float32: cutlass.Float32,
314
+ }
315
+ cute_dtype = dtype_map[config.dtype]
316
+
317
+ qhead_per_kvhead = config.nheads // config.nheads_kv
318
+ kernel = FlashAttentionForwardSm90(
319
+ cute_dtype,
320
+ config.headdim,
321
+ config.headdim_v,
322
+ qhead_per_kvhead,
323
+ is_causal=config.causal,
324
+ is_local=config.is_local,
325
+ pack_gqa=False,
326
+ tile_m=config.tile_m,
327
+ tile_n=config.tile_n,
328
+ num_stages=config.num_stages,
329
+ num_threads=config.num_threads,
330
+ intra_wg_overlap=config.intra_wg_overlap,
331
+ mma_pv_is_rs=config.mma_pv_is_rs,
332
+ mask_mod=self.mask_mod_cute,
333
+ Q_in_regs=False,
334
+ has_aux_tensors=config.has_aux_tensors,
335
+ )
336
+
337
+ softmax_scale = 1.0 / math.sqrt(config.headdim)
338
+ current_stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
339
+
340
+ # Convert tensors to cute
341
+ q_cute = from_dlpack(tensors["q"].detach(), assumed_align=16).mark_layout_dynamic(
342
+ leading_dim=tensors["q"].ndim - 1
343
+ )
344
+ k_cute = from_dlpack(tensors["k"].detach(), assumed_align=16).mark_layout_dynamic(
345
+ leading_dim=tensors["k"].ndim - 1
346
+ )
347
+ v_cute = from_dlpack(tensors["v"].detach(), assumed_align=16).mark_layout_dynamic(
348
+ leading_dim=tensors["v"].ndim - 1
349
+ )
350
+ out_cute = from_dlpack(tensors["out"].detach(), assumed_align=16).mark_layout_dynamic(
351
+ leading_dim=tensors["out"].ndim - 1
352
+ )
353
+ lse_cute = from_dlpack(tensors["lse"].detach(), assumed_align=4).mark_layout_dynamic(
354
+ leading_dim=tensors["lse"].ndim - 1
355
+ )
356
+
357
+ # Varlen tensors
358
+ cu_seqlens_q_cute = (
359
+ from_dlpack(tensors["cu_seqlens_q"].detach(), assumed_align=4).mark_layout_dynamic(
360
+ leading_dim=0
361
+ )
362
+ if "cu_seqlens_q" in tensors
363
+ else None
364
+ )
365
+ cu_seqlens_k_cute = (
366
+ from_dlpack(tensors["cu_seqlens_k"].detach(), assumed_align=4).mark_layout_dynamic(
367
+ leading_dim=0
368
+ )
369
+ if "cu_seqlens_k" in tensors
370
+ else None
371
+ )
372
+ learnable_sink_cute = (
373
+ from_dlpack(tensors["learnable_sink"].detach(), assumed_align=4).mark_layout_dynamic(
374
+ leading_dim=0
375
+ )
376
+ if "learnable_sink" in tensors
377
+ else None
378
+ )
379
+
380
+ blocksparse_tensors_cute = (
381
+ to_cute_block_sparse_tensors(tensors["block_sparse_tensors"])
382
+ if "block_sparse_tensors" in tensors
383
+ else None
384
+ )
385
+
386
+ if "aux_tensors" in tensors:
387
+ aux_tensors_cute = []
388
+ for i in range(len(tensors["aux_tensors"])):
389
+ buf = from_dlpack(tensors["aux_tensors"][i].detach(), assumed_align=4)
390
+ aux_tensors_cute.append(buf.mark_layout_dynamic(leading_dim=2))
391
+
392
+ else:
393
+ aux_tensors_cute = None
394
+
395
+ # Window parameters for is_local
396
+ window_left_cute = (
397
+ cutlass.Int32(config.window_left) if config.window_left is not None else None
398
+ )
399
+ window_right_cute = (
400
+ cutlass.Int32(config.window_right) if config.window_right is not None else None
401
+ )
402
+
403
+ compiled = cute.compile(
404
+ kernel,
405
+ q_cute,
406
+ k_cute,
407
+ v_cute,
408
+ out_cute,
409
+ lse_cute,
410
+ softmax_scale,
411
+ current_stream,
412
+ cu_seqlens_q_cute,
413
+ cu_seqlens_k_cute,
414
+ None, # seqused_q
415
+ None, # seqused_k
416
+ None, # page_table
417
+ window_left_cute,
418
+ window_right_cute,
419
+ learnable_sink_cute,
420
+ blocksparse_tensors_cute,
421
+ aux_tensors_cute,
422
+ # None,
423
+ )
424
+
425
+ args = (
426
+ q_cute,
427
+ k_cute,
428
+ v_cute,
429
+ out_cute,
430
+ lse_cute,
431
+ softmax_scale,
432
+ current_stream,
433
+ cu_seqlens_q_cute,
434
+ cu_seqlens_k_cute,
435
+ None,
436
+ None,
437
+ None,
438
+ window_left_cute,
439
+ window_right_cute,
440
+ learnable_sink_cute,
441
+ blocksparse_tensors_cute,
442
+ aux_tensors_cute,
443
+ # None,
444
+ )
445
+
446
+ return compiled, args
447
+
448
+ def _calculate_flops(self, tensors: Dict[str, torch.Tensor]) -> float:
449
+ config = self.config
450
+
451
+ # Estimate sparsity for known mask patterns
452
+ if config.is_local:
453
+ # Local attention with window_left and window_right
454
+ window_left = config.window_left if config.window_left is not None else 0
455
+ window_right = config.window_right if config.window_right is not None else 0
456
+ total_window = window_left + window_right + 1 # +1 for current position
457
+ sparsity_ratio = min(1.0, total_window / config.seqlen_k)
458
+ elif config.use_mask_mod:
459
+ if config.mask_mod_name in ["identity", "identity_partial"]:
460
+ sparsity_ratio = 1.0
461
+ elif config.mask_mod_name in ["causal", "block_causal"]:
462
+ sparsity_ratio = 0.5
463
+ elif config.mask_mod_name == "sliding_window":
464
+ # Use configured window size
465
+ sparsity_ratio = min(1.0, config.window_size / config.seqlen_k)
466
+ elif config.mask_mod_name == "block_diagonal":
467
+ block_size = 64
468
+ num_blocks = (config.seqlen_k + block_size - 1) // block_size
469
+ sparsity_ratio = 1.0 / num_blocks if num_blocks > 1 else 1.0
470
+ elif config.mask_mod_name == "document":
471
+ vals = tensors["aux_tensors"][0]
472
+ val_mask = torch.ones_like(vals, dtype=torch.bool)
473
+ val_mask[..., 1:] = vals[..., 1:] != vals[..., :-1]
474
+ total = torch.where(val_mask, vals.square(), 0).sum()
475
+ sparsity_ratio = total / (config.seqlen_q * config.seqlen_k)
476
+ else:
477
+ sparsity_ratio = 1.0
478
+ elif config.causal:
479
+ sparsity_ratio = 0.5
480
+ else:
481
+ sparsity_ratio = 1.0
482
+
483
+ if config.use_varlen:
484
+ # Compute FLOPs per sequence and sum
485
+ total_flops = 0
486
+ cu_q = tensors["cu_seqlens_q"]
487
+ cu_k = tensors["cu_seqlens_k"]
488
+ for i in range(config.batch_size):
489
+ seq_len_q = (cu_q[i + 1] - cu_q[i]).item()
490
+ seq_len_k = (cu_k[i + 1] - cu_k[i]).item()
491
+
492
+ # Adjust sparsity for local attention in varlen case
493
+ if config.is_local:
494
+ window_left = config.window_left if config.window_left is not None else 0
495
+ window_right = config.window_right if config.window_right is not None else 0
496
+ total_window = window_left + window_right + 1
497
+ seq_sparsity = min(1.0, total_window / seq_len_k)
498
+ elif config.use_mask_mod and config.mask_mod_name == "sliding_window":
499
+ seq_sparsity = min(1.0, config.window_size / seq_len_k)
500
+ else:
501
+ seq_sparsity = sparsity_ratio
502
+
503
+ num_cells = int(seq_len_q * seq_len_k * seq_sparsity)
504
+
505
+ if config.headdim == config.headdim_v:
506
+ flops_this_seq = 4 * config.nheads * num_cells * config.headdim
507
+ else:
508
+ flops_this_seq = (
509
+ 2 * config.nheads * num_cells * config.headdim
510
+ + 2 * config.nheads * num_cells * config.headdim_v
511
+ )
512
+ total_flops += flops_this_seq
513
+ return total_flops
514
+ else:
515
+ num_cells = int(config.seqlen_q * config.seqlen_k * sparsity_ratio)
516
+ if config.headdim == config.headdim_v:
517
+ flops_per_batch = 4 * config.nheads * num_cells * config.headdim
518
+ else:
519
+ flops_per_batch = (
520
+ 2 * config.nheads * num_cells * config.headdim
521
+ + 2 * config.nheads * num_cells * config.headdim_v
522
+ )
523
+ return flops_per_batch * config.batch_size
524
+
525
+ def benchmark(self) -> Dict[str, Any]:
526
+ config = self.config
527
+
528
+ tensors = self._create_tensors()
529
+ compiled_kernel, args = self._compile_kernel(tensors)
530
+
531
+ # Warmup
532
+ for _ in range(config.warmup_iters):
533
+ compiled_kernel(*args)
534
+ torch.cuda.synchronize()
535
+
536
+ # Benchmark
537
+ times = []
538
+ for _ in range(config.benchmark_iters):
539
+ start = torch.cuda.Event(enable_timing=True)
540
+ end = torch.cuda.Event(enable_timing=True)
541
+
542
+ start.record()
543
+ compiled_kernel(*args)
544
+ end.record()
545
+ torch.cuda.synchronize()
546
+
547
+ times.append(start.elapsed_time(end))
548
+
549
+ times_tensor = torch.tensor(times)
550
+ mean_time = times_tensor.mean().item()
551
+ std_time = times_tensor.std().item() if len(times) > 1 else 0.0
552
+
553
+ total_flops = self._calculate_flops(tensors)
554
+ tflops = total_flops / (mean_time * 1e-3) / 1e12
555
+
556
+ # Bandwidth calculation
557
+ bytes_per_element = config.dtype.itemsize
558
+ if config.use_varlen:
559
+ total_q = tensors["q"].shape[0]
560
+ total_k = tensors["k"].shape[0]
561
+ memory_accessed = (
562
+ total_q * config.nheads * config.headdim * bytes_per_element
563
+ + total_k * config.nheads_kv * config.headdim * bytes_per_element
564
+ + total_k * config.nheads_kv * config.headdim_v * bytes_per_element
565
+ + total_q * config.nheads * config.headdim_v * bytes_per_element
566
+ )
567
+ else:
568
+ memory_accessed = (
569
+ config.batch_size
570
+ * config.seqlen_q
571
+ * config.nheads
572
+ * config.headdim
573
+ * bytes_per_element
574
+ + config.batch_size
575
+ * config.seqlen_k
576
+ * config.nheads_kv
577
+ * config.headdim
578
+ * bytes_per_element
579
+ + config.batch_size
580
+ * config.seqlen_k
581
+ * config.nheads_kv
582
+ * config.headdim_v
583
+ * bytes_per_element
584
+ + config.batch_size
585
+ * config.seqlen_q
586
+ * config.nheads
587
+ * config.headdim_v
588
+ * bytes_per_element
589
+ )
590
+ bandwidth_gbps = memory_accessed / (mean_time * 1e-3) / 1e9
591
+
592
+ results = {
593
+ "mean_time_ms": mean_time,
594
+ "std_time_ms": std_time,
595
+ "tflops": tflops,
596
+ "bandwidth_gbps": bandwidth_gbps,
597
+ }
598
+
599
+ if config.verbose:
600
+ self._print_results(results)
601
+
602
+ return results
603
+
604
+ def _print_results(self, results: Dict[str, Any]):
605
+ config = self.config
606
+
607
+ # Basic configuration
608
+ if config.use_varlen:
609
+ print(
610
+ f"Shape: B={config.batch_size} (varlen), HD={config.headdim}, "
611
+ f"NH={config.nheads}, NKV={config.nheads_kv}"
612
+ )
613
+ else:
614
+ print(
615
+ f"Shape: B={config.batch_size}, Q={config.seqlen_q}, K={config.seqlen_k}, "
616
+ f"HD={config.headdim}, NH={config.nheads}, NKV={config.nheads_kv}"
617
+ )
618
+
619
+ # Attention pattern
620
+ attn_info = []
621
+ if config.causal:
622
+ attn_info.append("causal")
623
+ if config.is_local:
624
+ window_info = f"local(L={config.window_left},R={config.window_right})"
625
+ attn_info.append(window_info)
626
+ if config.use_mask_mod:
627
+ if config.mask_mod_name == "sliding_window":
628
+ attn_info.append(f"mask_mod={config.mask_mod_name}(w={config.window_size})")
629
+ else:
630
+ attn_info.append(f"mask_mod={config.mask_mod_name}")
631
+ if config.use_varlen:
632
+ attn_info.append("varlen")
633
+ if attn_info:
634
+ print(f"Attention: {', '.join(attn_info)}")
635
+
636
+ # Performance metrics
637
+ print(f"Time: {results['mean_time_ms']:.3f} ± {results['std_time_ms']:.3f} ms")
638
+ print(f"Throughput: {results['tflops']:.2f} TFLOPS")
639
+ print(f"Bandwidth: {results['bandwidth_gbps']:.1f} GB/s")
640
+
641
+
642
+ if __name__ == "__main__":
643
+ B = 2
644
+ config = BenchmarkConfig(
645
+ headdim=128,
646
+ headdim_v=128,
647
+ nheads=16,
648
+ nheads_kv=16,
649
+ dtype=torch.bfloat16,
650
+ batch_size=B,
651
+ # batch_size=1,
652
+ seqlen_q=16384 // B,
653
+ # seqlen_q=128,
654
+ seqlen_k=16384 // B,
655
+ # seqlen_k=192,
656
+ use_varlen=False,
657
+ use_mask_mod=True,
658
+ mask_mod_name="causal",
659
+ window_size=128, # Configurable window size for mask_mod
660
+ use_learnable_sink=False,
661
+ causal=False,
662
+ is_local=False,
663
+ verbose=True,
664
+ )
665
+
666
+ # Example 2: Base Flash Attention Local
667
+ # config = BenchmarkConfig(
668
+ # headdim=64,
669
+ # headdim_v=64,
670
+ # nheads=64,
671
+ # nheads_kv=8,
672
+ # dtype=torch.bfloat16,
673
+ # batch_size=2,
674
+ # seqlen_q=8192,
675
+ # seqlen_k=8192,
676
+ # use_varlen=False,
677
+ # use_mask_mod=False,
678
+ # causal=False,
679
+ # is_local=True,
680
+ # window_left=128, # Left window size for base local attention
681
+ # window_right=0, # Right window size for base local attention
682
+ # verbose=True,
683
+ # )
684
+
685
+ benchmark = FlashAttentionBenchmark(config)
686
+ results = benchmark.benchmark()
build/torch-universal/blackwell_helpers.py ADDED
@@ -0,0 +1,753 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ from typing import Optional, Tuple
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+ from cutlass import Int32, Boolean, const_expr
7
+ from cutlass.cute.nvgpu import tcgen05
8
+ from cutlass._mlir.dialects import llvm
9
+
10
+ from . import mma_sm100_desc as sm100_desc
11
+ from .utils import parse_swizzle_from_pointer
12
+
13
+
14
+ @cute.jit
15
+ def gemm_w_idx(
16
+ tiled_mma: cute.TiledMma,
17
+ acc: cute.Tensor,
18
+ tCrA: cute.Tensor,
19
+ tCrB: cute.Tensor,
20
+ A_idx: Optional[Int32] = None,
21
+ B_idx: Optional[Int32] = None,
22
+ zero_init: bool | Boolean = False,
23
+ swap_AB: bool = False,
24
+ ) -> None:
25
+ if const_expr(swap_AB):
26
+ return gemm_w_idx(
27
+ tiled_mma, acc, tCrB, tCrA, B_idx, A_idx, zero_init=zero_init, swap_AB=False
28
+ )
29
+ else:
30
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
31
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
32
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
33
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
34
+ mma_atom.set(tcgen05.Field.ACCUMULATE, not zero_init or k != 0)
35
+ cute.gemm(mma_atom, acc, rA[None, None, k], rB[None, None, k], acc)
36
+
37
+
38
+ @cute.jit
39
+ def gemm_ptx_w_idx(
40
+ tiled_mma: cute.TiledMma,
41
+ acc: cute.Tensor,
42
+ tCrA: cute.Tensor,
43
+ tCrB: cute.Tensor,
44
+ sA: Optional[cute.Tensor],
45
+ sB: cute.Tensor,
46
+ A_idx: Optional[Int32] = None,
47
+ B_idx: Optional[Int32] = None,
48
+ zero_init: bool | Boolean = False,
49
+ **kwargs,
50
+ ) -> None:
51
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
52
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
53
+ sA_cur = None
54
+ if const_expr(sA is not None):
55
+ sA_cur = sA if const_expr(A_idx is None) else sA[None, None, None, A_idx]
56
+ sB_cur = sB if const_expr(B_idx is None) else sB[None, None, None, B_idx]
57
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
58
+ acc_tmem_addr = acc.iterator.toint()
59
+ gemm_ptx_partial(
60
+ mma_atom.op, acc_tmem_addr, rA, rB, sA_cur, sB_cur, zero_init=zero_init, **kwargs
61
+ )
62
+
63
+
64
+ @cute.jit
65
+ def gemm(
66
+ tiled_mma: cute.TiledMma,
67
+ acc: cute.Tensor,
68
+ tCrA: cute.Tensor,
69
+ tCrB: cute.Tensor,
70
+ zero_init: bool | Boolean = False,
71
+ ) -> cute.TiledMma:
72
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
73
+ tiled_mma.set(tcgen05.Field.ACCUMULATE, not zero_init or k != 0)
74
+ cute.gemm(tiled_mma, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
75
+ return tiled_mma
76
+
77
+
78
+ def i64_to_i32x2(i: int) -> Tuple[int, int]:
79
+ """Convert a 64-bit integer to a tuple of two 32-bit integers."""
80
+ return i & 0xFFFF_FFFF, (i >> 32) & 0xFFFF_FFFF
81
+
82
+
83
+ @cute.jit
84
+ def gemm_ptx(
85
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
86
+ acc: cute.Tensor,
87
+ tCrA: cute.Tensor,
88
+ tCrB: cute.Tensor,
89
+ sA: Optional[cute.Tensor],
90
+ sB: cute.Tensor,
91
+ zero_init: bool | Boolean = False,
92
+ ) -> None:
93
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
94
+ if const_expr(not is_ts):
95
+ assert sA is not None, "sA must be provided when a_src is not TMEM"
96
+ sA_layout = sA.layout if sA is not None else None
97
+ sB_layout = sB.layout
98
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
99
+ if const_expr(not is_ts):
100
+ sA_swizzle = parse_swizzle_from_pointer(sA.iterator)
101
+ smem_desc_base_a: int = const_expr(
102
+ sm100_desc.make_smem_desc_base(
103
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
104
+ sA_swizzle,
105
+ sm100_desc.Major.K
106
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
107
+ else sm100_desc.Major.MN,
108
+ )
109
+ )
110
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
111
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
112
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
113
+ else:
114
+ smem_desc_base_a = None
115
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
116
+ sB_swizzle = parse_swizzle_from_pointer(sB.iterator)
117
+ smem_desc_base_b: int = const_expr(
118
+ sm100_desc.make_smem_desc_base(
119
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
120
+ sB_swizzle,
121
+ sm100_desc.Major.K
122
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
123
+ else sm100_desc.Major.MN,
124
+ )
125
+ )
126
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
127
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
128
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
129
+
130
+ if const_expr(not is_ts):
131
+ smem_desc_start_a_lo = Int32(smem_desc_base_a_lo) | sm100_desc.make_smem_desc_start_addr(
132
+ sA[None, None, 0].iterator
133
+ )
134
+ else:
135
+ smem_desc_start_a_lo = None
136
+ smem_desc_start_b_lo = Int32(smem_desc_base_b_lo) | sm100_desc.make_smem_desc_start_addr(
137
+ sB[None, None, 0].iterator
138
+ )
139
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
140
+ if const_expr(not is_ts):
141
+ smem_desc_a_lo = smem_desc_start_a_lo + (
142
+ (cute.crd2idx((0, 0, k), sA_layout) * sA.element_type.width // 8) >> 4
143
+ )
144
+ smem_desc_b_lo = smem_desc_start_b_lo + (
145
+ (cute.crd2idx((0, 0, k), sB_layout) * sB.element_type.width // 8) >> 4
146
+ )
147
+ # with cute.arch.elect_one():
148
+ # cute.printf("smem_desc_a_lo = {}, smem_desc_b_lo = {}", smem_desc_a_lo, smem_desc_b_lo)
149
+ # cute.printf("smem_desc_a_lo_correct = {}, smem_desc_b_lo_correct = {}", smem_desc_a_lo_correct, smem_desc_b_lo_correct)
150
+ with cute.arch.elect_one():
151
+ if const_expr(not is_ts):
152
+ llvm.inline_asm(
153
+ None,
154
+ [
155
+ acc.iterator.toint().ir_value(),
156
+ smem_desc_a_lo.ir_value(),
157
+ smem_desc_b_lo.ir_value(),
158
+ Int32(not zero_init or k != 0).ir_value(),
159
+ ],
160
+ "{\n\t"
161
+ ".reg .pred p;\n\t"
162
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
163
+ ".reg .b32 idesc;\n\t"
164
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
165
+ f"mov.b64 smem_desc_a, {{$1, {hex(smem_desc_a_hi)}}};\n\t"
166
+ f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
167
+ "setp.ne.b32 p, $3, 0;\n\t"
168
+ f"tcgen05.mma.cta_group::1.kind::f16 [$0], smem_desc_a, smem_desc_b, idesc, p;\n\t"
169
+ "}\n",
170
+ "r,r,r,r",
171
+ has_side_effects=True,
172
+ is_align_stack=False,
173
+ asm_dialect=llvm.AsmDialect.AD_ATT,
174
+ )
175
+ else:
176
+ llvm.inline_asm(
177
+ None,
178
+ [
179
+ acc.iterator.toint().ir_value(),
180
+ tCrA[None, None, k].iterator.toint().ir_value(),
181
+ smem_desc_b_lo.ir_value(),
182
+ Int32(not zero_init or k != 0).ir_value(),
183
+ ],
184
+ "{\n\t"
185
+ ".reg .pred p;\n\t"
186
+ ".reg .b64 smem_desc_b;\n\t"
187
+ f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
188
+ "setp.ne.b32 p, $3, 0;\n\t"
189
+ f"tcgen05.mma.cta_group::1.kind::f16 [$0], [$1], smem_desc_b, {hex(idesc)}, p;\n\t"
190
+ "}\n",
191
+ "r,r,r,r",
192
+ has_side_effects=True,
193
+ is_align_stack=False,
194
+ asm_dialect=llvm.AsmDialect.AD_ATT,
195
+ )
196
+
197
+
198
+ @cute.jit
199
+ def gemm_ptx_loop(
200
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
201
+ acc: cute.Tensor,
202
+ tCrA: cute.Tensor,
203
+ tCrB: cute.Tensor,
204
+ sA: Optional[cute.Tensor],
205
+ sB: cute.Tensor,
206
+ zero_init: bool | Boolean = False,
207
+ ) -> None:
208
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
209
+ if const_expr(not is_ts):
210
+ assert sA is not None, "sA must be provided when a_src is not TMEM"
211
+ sA_layout = sA.layout if sA is not None else tCrA.layout
212
+ sB_layout = sB.layout
213
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
214
+ if const_expr(not is_ts):
215
+ sA_swizzle = parse_swizzle_from_pointer(sA.iterator)
216
+ smem_desc_base_a: int = const_expr(
217
+ sm100_desc.make_smem_desc_base(
218
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
219
+ sA_swizzle,
220
+ sm100_desc.Major.K
221
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
222
+ else sm100_desc.Major.MN,
223
+ )
224
+ )
225
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
226
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
227
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
228
+ else:
229
+ smem_desc_base_a = None
230
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
231
+ sB_swizzle = parse_swizzle_from_pointer(sB.iterator)
232
+ smem_desc_base_b: int = const_expr(
233
+ sm100_desc.make_smem_desc_base(
234
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
235
+ sB_swizzle,
236
+ sm100_desc.Major.K
237
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
238
+ else sm100_desc.Major.MN,
239
+ )
240
+ )
241
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
242
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
243
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
244
+
245
+ if const_expr(not is_ts):
246
+ offset_a = [
247
+ (cute.crd2idx((0, 0, k), sA_layout) * sA.element_type.width // 8) >> 4
248
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2]))
249
+ ]
250
+ else:
251
+ offset_a = [
252
+ cute.crd2idx((0, 0, k), sA_layout) * op.a_dtype.width // 32
253
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2]))
254
+ ]
255
+ offset_a_diff = [
256
+ offset_a[k] - offset_a[k - 1] for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
257
+ ]
258
+ offset_b = [
259
+ (cute.crd2idx((0, 0, k), sB_layout) * sB.element_type.width // 8) >> 4
260
+ for k in cutlass.range_constexpr(cute.size(tCrB.shape[2]))
261
+ ]
262
+ offset_b_diff = [
263
+ offset_b[k] - offset_b[k - 1] for k in cutlass.range_constexpr(1, cute.size(tCrB.shape[2]))
264
+ ]
265
+
266
+ if const_expr(not is_ts):
267
+ smem_desc_start_a_lo = Int32(
268
+ smem_desc_base_a_lo | sm100_desc.make_smem_desc_start_addr(sA[None, None, 0].iterator)
269
+ )
270
+ else:
271
+ smem_desc_start_a_lo = None
272
+ smem_desc_start_b_lo = Int32(
273
+ smem_desc_base_b_lo | sm100_desc.make_smem_desc_start_addr(sB[None, None, 0].iterator)
274
+ )
275
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
276
+ if const_expr(not is_ts):
277
+ llvm.inline_asm(
278
+ None,
279
+ [
280
+ acc.iterator.toint().ir_value(),
281
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
282
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
283
+ Int32(not zero_init).ir_value(),
284
+ ],
285
+ "{\n\t"
286
+ ".reg .pred leader_thread;\n\t"
287
+ ".reg .pred p;\n\t"
288
+ ".reg .b32 idesc;\n\t"
289
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
290
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
291
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
292
+ "elect.sync _|leader_thread, -1;\n\t"
293
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
294
+ "mov.b32 smem_desc_a_lo, $1;\n\t"
295
+ "mov.b32 smem_desc_b_lo, $2;\n\t"
296
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
297
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
298
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
299
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
300
+ "setp.ne.b32 p, $3, 0;\n\t"
301
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
302
+ + "".join(
303
+ (
304
+ f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
305
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
306
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
307
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
308
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
309
+ )
310
+ for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
311
+ )
312
+ + "}\n",
313
+ "r,r,r,r",
314
+ has_side_effects=True,
315
+ is_align_stack=False,
316
+ asm_dialect=llvm.AsmDialect.AD_ATT,
317
+ )
318
+ else:
319
+ llvm.inline_asm(
320
+ None,
321
+ [
322
+ acc.iterator.toint().ir_value(),
323
+ Int32(tCrA[None, None, 0].iterator.toint()).ir_value(),
324
+ Int32(smem_desc_start_b_lo).ir_value(),
325
+ Int32(not zero_init).ir_value(),
326
+ ],
327
+ "{\n\t"
328
+ ".reg .pred leader_thread;\n\t"
329
+ ".reg .pred p;\n\t"
330
+ ".reg .b32 idesc;\n\t"
331
+ ".reg .b32 tmem_a;\n\t"
332
+ ".reg .b32 smem_desc_b_lo;\n\t"
333
+ ".reg .b32 smem_desc_b_hi;\n\t"
334
+ ".reg .b64 smem_desc_b;\n\t"
335
+ "elect.sync _|leader_thread, -1;\n\t"
336
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
337
+ "mov.b32 tmem_a, $1;\n\t"
338
+ "mov.b32 smem_desc_b_lo, $2;\n\t"
339
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
340
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
341
+ "setp.ne.b32 p, $3, 0;\n\t"
342
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
343
+ + "".join(
344
+ (
345
+ # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
346
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
347
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
348
+ # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, 1;\n\t"
349
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
350
+ )
351
+ for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
352
+ )
353
+ + "}\n",
354
+ "r,r,r,r",
355
+ has_side_effects=True,
356
+ is_align_stack=False,
357
+ asm_dialect=llvm.AsmDialect.AD_ATT,
358
+ )
359
+
360
+
361
+ @cute.jit
362
+ def gemm_ptx_partial(
363
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
364
+ acc_tmem_addr: Int32,
365
+ tCrA: cute.Tensor,
366
+ tCrB: cute.Tensor,
367
+ sA: Optional[cute.Tensor],
368
+ sB: cute.Tensor,
369
+ mbar_ptr: Optional[cutlass.Pointer] = None,
370
+ mbar_phase: Optional[Int32] = None,
371
+ zero_init: bool | Boolean = False,
372
+ # sA_offset: Int32 = 0,
373
+ # acc_offset: Int32 = 0,
374
+ tA_addr: Optional[Int32] = None,
375
+ ) -> None:
376
+ # acc_tmem_addr += acc_offset
377
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
378
+ if const_expr(not is_ts):
379
+ assert sA is not None, "sA must be provided when a_src is not TMEM"
380
+ sA_layout = sA.layout if sA is not None else tCrA.layout
381
+ sB_layout = sB.layout
382
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
383
+ if const_expr(not is_ts):
384
+ sA_swizzle = parse_swizzle_from_pointer(sA.iterator)
385
+ smem_desc_base_a: int = const_expr(
386
+ sm100_desc.make_smem_desc_base(
387
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
388
+ sA_swizzle,
389
+ sm100_desc.Major.K
390
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
391
+ else sm100_desc.Major.MN,
392
+ )
393
+ )
394
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
395
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
396
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
397
+ else:
398
+ smem_desc_base_a = None
399
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
400
+ sB_swizzle = parse_swizzle_from_pointer(sB.iterator)
401
+ smem_desc_base_b: int = const_expr(
402
+ sm100_desc.make_smem_desc_base(
403
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
404
+ sB_swizzle,
405
+ sm100_desc.Major.K
406
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
407
+ else sm100_desc.Major.MN,
408
+ )
409
+ )
410
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
411
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
412
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
413
+
414
+ tCrA_layout = (
415
+ tCrA.layout
416
+ if const_expr(not is_ts)
417
+ else cute.recast_layout(32, tCrA.element_type.width, tCrA.layout)
418
+ )
419
+ offset_a = [cute.crd2idx((0, 0, k), tCrA_layout) for k in range(cute.size(tCrA.shape[2]))]
420
+ offset_a_diff = [offset_a[k] - offset_a[k - 1] for k in range(1, cute.size(tCrA.shape[2]))]
421
+ offset_b = [cute.crd2idx((0, 0, k), tCrB.layout) for k in range(cute.size(tCrB.shape[2]))]
422
+ offset_b_diff = [offset_b[k] - offset_b[k - 1] for k in range(1, cute.size(tCrB.shape[2]))]
423
+
424
+ if const_expr(not is_ts):
425
+ smem_desc_start_a_lo = Int32(
426
+ smem_desc_base_a_lo | sm100_desc.make_smem_desc_start_addr(sA[None, None, 0].iterator)
427
+ )
428
+ # ) + sA_offset
429
+ else:
430
+ smem_desc_start_a_lo = None
431
+ smem_desc_start_b_lo = Int32(
432
+ smem_desc_base_b_lo | sm100_desc.make_smem_desc_start_addr(sB[None, None, 0].iterator)
433
+ )
434
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
435
+ if const_expr(not is_ts):
436
+ assert mbar_ptr is None, "mbar_ptr must be None when a_src is not TMEM"
437
+ llvm.inline_asm(
438
+ None,
439
+ [
440
+ # acc.iterator.toint().ir_value(),
441
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
442
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
443
+ Int32(not zero_init).ir_value(),
444
+ Int32(cute.arch.make_warp_uniform(acc_tmem_addr)).ir_value(),
445
+ ],
446
+ "{\n\t"
447
+ ".reg .pred leader_thread;\n\t"
448
+ ".reg .pred p;\n\t"
449
+ ".reg .b32 idesc;\n\t"
450
+ ".reg .b32 tmem_acc;\n\t"
451
+ ".reg .b32 smem_desc_a_lo_start, smem_desc_b_lo_start;\n\t"
452
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
453
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
454
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
455
+ "elect.sync _|leader_thread, -1;\n\t"
456
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
457
+ # f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
458
+ f"mov.b32 tmem_acc, $3;\n\t"
459
+ "mov.b32 smem_desc_a_lo_start, $0;\n\t"
460
+ "mov.b32 smem_desc_b_lo_start, $1;\n\t"
461
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
462
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
463
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo_start, smem_desc_a_hi}};\n\t"
464
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
465
+ "setp.ne.b32 p, $2, 0;\n\t"
466
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
467
+ + "".join(
468
+ (
469
+ # f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
470
+ # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
471
+ f"add.u32 smem_desc_a_lo, smem_desc_a_lo_start, {hex(offset_a[k])};\n\t"
472
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
473
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
474
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
475
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
476
+ )
477
+ for k in range(1, cute.size(tCrA.shape[2]))
478
+ )
479
+ + "}\n",
480
+ # "r,r,r",
481
+ "r,r,r,r",
482
+ has_side_effects=True,
483
+ is_align_stack=False,
484
+ asm_dialect=llvm.AsmDialect.AD_ATT,
485
+ )
486
+ else:
487
+ # For TS gemm, somehow tCrA.iterator.toint() returns 0 no matter what, so we need to
488
+ # explicitly pass in the tA_addr for correctness.
489
+ tA_addr = tCrA[None, None, 0].iterator.toint() if tA_addr is None else tA_addr
490
+ input_args = [
491
+ # Int32(cute.arch.make_warp_uniform(tCrA[None, None, 0].iterator.toint())).ir_value(),
492
+ Int32(cute.arch.make_warp_uniform(tA_addr)).ir_value(),
493
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
494
+ Int32(not zero_init).ir_value(),
495
+ Int32(cute.arch.make_warp_uniform(acc_tmem_addr)).ir_value(),
496
+ ]
497
+ if const_expr(mbar_ptr is not None):
498
+ assert mbar_phase is not None, "mbar_phase must be provided when mbar_ptr is not None"
499
+ input_args.append(mbar_ptr.toint().ir_value())
500
+ input_args.append(Int32(mbar_phase).ir_value())
501
+ mbar_wait_str = (
502
+ ".reg .pred P1; \n\t"
503
+ "LAB_WAIT: \n\t"
504
+ "mbarrier.try_wait.parity.shared::cta.b64 P1, [$4], $5, 10000000; \n\t"
505
+ "@P1 bra DONE; \n\t"
506
+ "bra LAB_WAIT; \n\t"
507
+ "DONE: \n\t"
508
+ )
509
+ else:
510
+ mbar_wait_str = ""
511
+ llvm.inline_asm(
512
+ None,
513
+ # [
514
+ # # acc.iterator.toint().ir_value(),
515
+ # Int32(tCrA[None, None, 0].iterator.toint()).ir_value(),
516
+ # Int32(smem_desc_start_b_lo).ir_value(),
517
+ # Int32(not zero_init).ir_value(),
518
+ # ],
519
+ input_args,
520
+ "{\n\t"
521
+ ".reg .pred leader_thread;\n\t"
522
+ ".reg .pred p;\n\t"
523
+ ".reg .b32 idesc;\n\t"
524
+ ".reg .b32 tmem_acc;\n\t"
525
+ ".reg .b32 tmem_a;\n\t"
526
+ ".reg .b32 smem_desc_b_lo_start;\n\t"
527
+ ".reg .b32 smem_desc_b_lo;\n\t"
528
+ ".reg .b32 smem_desc_b_hi;\n\t"
529
+ ".reg .b64 smem_desc_b;\n\t"
530
+ "elect.sync _|leader_thread, -1;\n\t"
531
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
532
+ # f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
533
+ f"mov.b32 tmem_acc, $3;\n\t"
534
+ f"mov.b32 tmem_a, $0;\n\t"
535
+ f"mov.b32 smem_desc_b_lo_start, $1;\n\t"
536
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
537
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
538
+ "setp.ne.b32 p, $2, 0;\n\t"
539
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
540
+ + "".join(
541
+ (
542
+ # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
543
+ # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
544
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
545
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
546
+ # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, 1;\n\t"
547
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
548
+ )
549
+ for k in range(
550
+ 1,
551
+ cute.size(tCrA.shape[2])
552
+ if const_expr(mbar_ptr is None)
553
+ else cute.size(tCrA.shape[2]) // 4 * 3,
554
+ )
555
+ )
556
+ + mbar_wait_str
557
+ + (
558
+ "".join(
559
+ (
560
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
561
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
562
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
563
+ )
564
+ for k in range(cute.size(tCrA.shape[2]) // 4 * 3, cute.size(tCrA.shape[2]))
565
+ )
566
+ if const_expr(mbar_ptr is not None)
567
+ else ""
568
+ )
569
+ + "}\n",
570
+ "r,r,r,r" if const_expr(mbar_ptr is None) else "r,r,r,r,r,r",
571
+ has_side_effects=True,
572
+ is_align_stack=False,
573
+ asm_dialect=llvm.AsmDialect.AD_ATT,
574
+ )
575
+
576
+
577
+ @cute.jit
578
+ def gemm_ptx_partial1(
579
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
580
+ acc_tmem_addr: cutlass.Constexpr[int],
581
+ tCrA: cute.Tensor,
582
+ tCrB: cute.Tensor,
583
+ sA_base_addr_for_desc: Int32,
584
+ sA_addr_offset_for_desc: cutlass.Constexpr[int],
585
+ sA_stage: Int32,
586
+ sB_base_addr_for_desc: Int32,
587
+ sB_addr_offset_for_desc: cutlass.Constexpr[int],
588
+ sB_stage: Int32,
589
+ sA_layout: Optional[cute.Layout],
590
+ sB_layout: Optional[cute.Layout],
591
+ sA_swizzle: Optional[cute.Swizzle],
592
+ sB_swizzle: cute.Swizzle,
593
+ zero_init: bool | Boolean = False,
594
+ ) -> None:
595
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
596
+ if const_expr(not is_ts):
597
+ assert sA_layout is not None, "sA_layout must be provided when a_src is not TMEM"
598
+ assert sA_swizzle is not None, "sA_swizzle must be provided when a_src is not TMEM"
599
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
600
+ if const_expr(not is_ts):
601
+ smem_desc_base_a: int = const_expr(
602
+ sm100_desc.make_smem_desc_base(
603
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
604
+ sA_swizzle,
605
+ sm100_desc.Major.K
606
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
607
+ else sm100_desc.Major.MN,
608
+ )
609
+ )
610
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
611
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
612
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
613
+ else:
614
+ smem_desc_base_a = None
615
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
616
+ smem_desc_base_b: int = const_expr(
617
+ sm100_desc.make_smem_desc_base(
618
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
619
+ sB_swizzle,
620
+ sm100_desc.Major.K
621
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
622
+ else sm100_desc.Major.MN,
623
+ )
624
+ )
625
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
626
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
627
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
628
+ mask = [Int32(0)] * 4
629
+
630
+ if const_expr(not is_ts):
631
+ offset_a = [
632
+ (cute.crd2idx((0, 0, k), sA_layout) * op.a_dtype.width // 8) >> 4
633
+ for k in range(cute.size(tCrA.shape[2]))
634
+ ]
635
+ else:
636
+ offset_a = [
637
+ cute.crd2idx((0, 0, k), sA_layout) * op.a_dtype.width // 32
638
+ for k in range(cute.size(tCrA.shape[2]))
639
+ ]
640
+ offset_a_diff = [offset_a[k] - offset_a[k - 1] for k in range(1, cute.size(tCrA.shape[2]))]
641
+ offset_b = [
642
+ (cute.crd2idx((0, 0, k), sB_layout) * op.b_dtype.width // 8) >> 4
643
+ for k in range(cute.size(tCrB.shape[2]))
644
+ ]
645
+ offset_b_diff = [offset_b[k] - offset_b[k - 1] for k in range(1, cute.size(tCrB.shape[2]))]
646
+
647
+ if const_expr(not is_ts):
648
+ # smem_desc_start_a_lo = Int32(smem_desc_base_a_lo | sm100_desc.make_smem_desc_start_addr(sA[None, None, 0].iterator))
649
+ smem_desc_start_a_lo = const_expr(smem_desc_base_a_lo)
650
+ else:
651
+ smem_desc_start_a_lo = None
652
+ # smem_desc_start_b_lo = Int32(smem_desc_base_b_lo | sm100_desc.make_smem_desc_start_addr(sB[None, None, 0].iterator))
653
+ smem_desc_start_b_lo = const_expr(smem_desc_base_b_lo)
654
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
655
+ if const_expr(not is_ts):
656
+ llvm.inline_asm(
657
+ None,
658
+ [
659
+ # acc.iterator.toint().ir_value(),
660
+ # Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
661
+ Int32(sA_base_addr_for_desc).ir_value(),
662
+ Int32(sA_stage).ir_value(),
663
+ # Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
664
+ Int32(sB_base_addr_for_desc).ir_value(),
665
+ Int32(sB_stage).ir_value(),
666
+ Int32(not zero_init).ir_value(),
667
+ mask[0].ir_value(),
668
+ mask[1].ir_value(),
669
+ mask[2].ir_value(),
670
+ mask[3].ir_value(),
671
+ ],
672
+ "{\n\t"
673
+ ".reg .pred leader_thread;\n\t"
674
+ ".reg .pred p;\n\t"
675
+ ".reg .b32 idesc;\n\t"
676
+ ".reg .b32 tmem_acc;\n\t"
677
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
678
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
679
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
680
+ "elect.sync _|leader_thread, -1;\n\t"
681
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
682
+ f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
683
+ # "mov.b32 smem_desc_a_lo, $0;\n\t"
684
+ # f"add.u32 smem_desc_a_lo, $0, {hex(smem_desc_start_a_lo)};\n\t"
685
+ f"mad.lo.u32 smem_desc_a_lo, $1, {hex(sA_addr_offset_for_desc)}, $0;\n\t"
686
+ # "mov.b32 smem_desc_b_lo, $2;\n\t"
687
+ f"mad.lo.u32 smem_desc_b_lo, $3, {hex(sB_addr_offset_for_desc)}, $2;\n\t"
688
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
689
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
690
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
691
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
692
+ "setp.ne.b32 p, $4, 0;\n\t"
693
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, {{$5, $6, $7, $8}}, {pred_str};\n\t"
694
+ + "".join(
695
+ (
696
+ f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
697
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
698
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
699
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
700
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, {{$5, $6, $7, $8}}, 1;\n\t"
701
+ )
702
+ for k in range(1, cute.size(tCrA.shape[2]))
703
+ )
704
+ + "}\n",
705
+ "r,r,r,r,r,r,r,r,r",
706
+ has_side_effects=True,
707
+ is_align_stack=False,
708
+ asm_dialect=llvm.AsmDialect.AD_ATT,
709
+ )
710
+ else:
711
+ llvm.inline_asm(
712
+ None,
713
+ [
714
+ # acc.iterator.toint().ir_value(),
715
+ Int32(tCrA[None, None, 0].iterator.toint()).ir_value(),
716
+ Int32(smem_desc_start_b_lo).ir_value(),
717
+ Int32(not zero_init).ir_value(),
718
+ mask[0].ir_value(),
719
+ mask[1].ir_value(),
720
+ mask[2].ir_value(),
721
+ mask[3].ir_value(),
722
+ ],
723
+ "{\n\t"
724
+ ".reg .pred leader_thread;\n\t"
725
+ ".reg .pred p;\n\t"
726
+ ".reg .b32 idesc;\n\t"
727
+ ".reg .b32 tmem_a;\n\t"
728
+ ".reg .b32 smem_desc_b_lo;\n\t"
729
+ ".reg .b32 smem_desc_b_hi;\n\t"
730
+ ".reg .b64 smem_desc_b;\n\t"
731
+ "elect.sync _|leader_thread, -1;\n\t"
732
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
733
+ f"mov.b32 tmem_a, $1;\n\t"
734
+ f"mov.b32 smem_desc_b_lo, $2;\n\t"
735
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
736
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
737
+ "setp.ne.b32 p, $3, 0;\n\t"
738
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, {{$4, $5, $6, $7}}, {pred_str};\n\t"
739
+ + "".join(
740
+ (
741
+ f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
742
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
743
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
744
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, {{$4, $5, $6, $7}}, 1;\n\t"
745
+ )
746
+ for k in range(1, cute.size(tCrA.shape[2]))
747
+ )
748
+ + "}\n",
749
+ "r,r,r,r,r,r,r,r",
750
+ has_side_effects=True,
751
+ is_align_stack=False,
752
+ asm_dialect=llvm.AsmDialect.AD_ATT,
753
+ )
build/torch-universal/block_info.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ from typing import Tuple, Optional
3
+ from dataclasses import dataclass
4
+
5
+ import cutlass
6
+ import cutlass.cute as cute
7
+ from cutlass import Int32, const_expr
8
+
9
+ from .seqlen_info import SeqlenInfoQK
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class BlockInfo:
14
+ tile_m: cutlass.Constexpr[int]
15
+ tile_n: cutlass.Constexpr[int]
16
+ is_causal: cutlass.Constexpr[bool]
17
+ is_local: cutlass.Constexpr[bool] = False
18
+ is_split_kv: cutlass.Constexpr[bool] = False
19
+ window_size_left: Optional[Int32] = None
20
+ window_size_right: Optional[Int32] = None
21
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
22
+
23
+ @cute.jit
24
+ def get_n_block_min_max(
25
+ self,
26
+ seqlen_info: SeqlenInfoQK,
27
+ m_block: Int32,
28
+ split_idx: cutlass.Int32 = 0,
29
+ num_splits: cutlass.Int32 = 1,
30
+ ) -> Tuple[Int32, Int32]:
31
+ n_block_max = cute.ceil_div(seqlen_info.seqlen_k, self.tile_n)
32
+ if const_expr(self.is_causal or (self.is_local and self.window_size_right is not None)):
33
+ m_idx_max = (m_block + 1) * self.tile_m
34
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
35
+ m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
36
+ n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
37
+ n_idx_right = n_idx if const_expr(self.is_causal) else n_idx + self.window_size_right
38
+ n_block_max = min(n_block_max, cute.ceil_div(n_idx_right, self.tile_n))
39
+ n_block_min = 0
40
+ if const_expr(self.is_local and self.window_size_left is not None):
41
+ m_idx_min = m_block * self.tile_m
42
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
43
+ m_idx_min = m_idx_min // self.qhead_per_kvhead_packgqa
44
+ n_idx = m_idx_min + seqlen_info.seqlen_k - seqlen_info.seqlen_q
45
+ n_idx_left = n_idx - self.window_size_left
46
+ n_block_min = cutlass.max(n_idx_left // self.tile_n, 0)
47
+ if cutlass.const_expr(self.is_split_kv):
48
+ num_n_blocks_per_split = (
49
+ cutlass.Int32(0)
50
+ if n_block_max <= n_block_min
51
+ else (n_block_max - n_block_min + num_splits - 1) // num_splits
52
+ )
53
+ n_block_min = n_block_min + split_idx * num_n_blocks_per_split
54
+ n_block_max = cutlass.min(n_block_min + num_n_blocks_per_split, n_block_max)
55
+ return n_block_min, n_block_max
56
+
57
+ @cute.jit
58
+ def get_m_block_min_max(self, seqlen_info: SeqlenInfoQK, n_block: Int32) -> Tuple[Int32, Int32]:
59
+ m_block_max = cute.ceil_div(seqlen_info.seqlen_q, self.tile_m)
60
+ m_block_min = 0
61
+ if const_expr(self.is_causal):
62
+ m_block_min = max(
63
+ m_block_min,
64
+ (n_block * self.tile_n + seqlen_info.seqlen_q - seqlen_info.seqlen_k)
65
+ // self.tile_m,
66
+ )
67
+ return m_block_min, m_block_max
68
+
69
+ @cute.jit
70
+ def get_n_block_min_causal_local_mask(
71
+ self,
72
+ seqlen_info: SeqlenInfoQK,
73
+ m_block: Int32,
74
+ n_block_min: Int32,
75
+ ) -> Int32:
76
+ """If we have separate iterations with causal or local masking at the start, where do we stop"""
77
+ m_idx_min = m_block * self.tile_m
78
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
79
+ m_idx_min = m_idx_min // self.qhead_per_kvhead_packgqa
80
+ n_idx = m_idx_min + seqlen_info.seqlen_k - seqlen_info.seqlen_q
81
+ n_idx_right = (
82
+ n_idx
83
+ if const_expr(not self.is_local or self.window_size_right is None)
84
+ else n_idx + self.window_size_right
85
+ )
86
+ return cutlass.max(n_block_min, n_idx_right // self.tile_n)
87
+
88
+ @cute.jit
89
+ def get_n_block_min_before_local_mask(
90
+ self,
91
+ seqlen_info: SeqlenInfoQK,
92
+ m_block: Int32,
93
+ n_block_min: Int32,
94
+ ) -> Int32:
95
+ """If we have separate iterations with local masking at the end, where do we stop the non-masked iterations"""
96
+ if const_expr(not self.is_local or self.window_size_left is None):
97
+ return n_block_min
98
+ else:
99
+ m_idx_max = (m_block + 1) * self.tile_m
100
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
101
+ m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
102
+ n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
103
+ n_idx_left = n_idx - self.window_size_left
104
+ return cutlass.max(n_block_min, cute.ceil_div(n_idx_left, self.tile_n))
build/torch-universal/block_sparse_utils.py ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Block-sparse runtime utilities for CUTE DSL kernels.
3
+
4
+ This module contains runtime execution functions for block-sparse attention kernels.
5
+ These utilities are used by CUTE DSL kernels to produce and consume block-sparse loads.
6
+ """
7
+
8
+ from typing import Callable
9
+ from functools import partial
10
+ import cutlass
11
+ import cutlass.cute as cute
12
+ from cutlass import const_expr
13
+
14
+ # Import data structures from block_sparsity
15
+ from .block_sparsity import BlockSparseTensors
16
+
17
+
18
+ @cute.jit
19
+ def load_block_list(
20
+ block_indices: cute.Tensor,
21
+ block_count,
22
+ load_q_with_first: cutlass.Constexpr,
23
+ first_block_preloaded: cutlass.Constexpr,
24
+ kv_producer_state,
25
+ load_Q,
26
+ load_K,
27
+ load_V,
28
+ pipeline_k,
29
+ pipeline_v,
30
+ use_tma_q: cutlass.Constexpr,
31
+ tma_q_bytes: cutlass.Constexpr,
32
+ intra_wg_overlap: cutlass.Constexpr,
33
+ ):
34
+ """Iterate over the sparse blocks and load K, V (and Q) into the pipeline.
35
+ for the intra_wg_overlap case, we overlap the loads of K and V. And this
36
+ means we need to pipeline the last V load from the partial block case,
37
+ with the loads for the full blocks. Set first_block_preloaded when the
38
+ caller has already issued the first K load for the list.
39
+
40
+ Note:
41
+ we iterate along the block_n indices in reverse.
42
+
43
+ Returns:
44
+ Updated kv_producer_state after processing the block list.
45
+
46
+ """
47
+ if block_count > 0:
48
+ if const_expr(not intra_wg_overlap):
49
+ # Peel first iteration: the first block may need to load Q alongside K,
50
+ # Parameters are already Constexpr, so no need to wrap in const_expr()
51
+ n_block_first = block_indices[block_count - 1]
52
+ extra_tx = tma_q_bytes if const_expr(load_q_with_first) and const_expr(use_tma_q) else 0
53
+ pipeline_k.producer_acquire(kv_producer_state, extra_tx_count=extra_tx)
54
+
55
+ if const_expr(load_q_with_first and use_tma_q):
56
+ load_Q(tma_bar_ptr=pipeline_k.producer_get_barrier(kv_producer_state))
57
+
58
+ load_K(src_idx=n_block_first, producer_state=kv_producer_state)
59
+ pipeline_v.producer_acquire(kv_producer_state)
60
+ load_V(src_idx=n_block_first, producer_state=kv_producer_state)
61
+ kv_producer_state.advance()
62
+
63
+ for offset in cutlass.range(1, block_count):
64
+ n_block = block_indices[block_count - 1 - offset]
65
+ pipeline_k.producer_acquire(kv_producer_state)
66
+ load_K(src_idx=n_block, producer_state=kv_producer_state)
67
+ pipeline_v.producer_acquire(kv_producer_state)
68
+ load_V(src_idx=n_block, producer_state=kv_producer_state)
69
+ kv_producer_state.advance()
70
+ else:
71
+ n_block_first = block_indices[block_count - 1]
72
+ if const_expr(not first_block_preloaded):
73
+ extra_tx = (
74
+ tma_q_bytes if const_expr(load_q_with_first) and const_expr(use_tma_q) else 0
75
+ )
76
+ pipeline_k.producer_acquire(kv_producer_state, extra_tx_count=extra_tx)
77
+
78
+ if const_expr(load_q_with_first and use_tma_q):
79
+ load_Q(tma_bar_ptr=pipeline_k.producer_get_barrier(kv_producer_state))
80
+
81
+ load_K(src_idx=n_block_first, producer_state=kv_producer_state)
82
+
83
+ for idx in cutlass.range(block_count - 1, unroll=1):
84
+ n_block_prev = block_indices[block_count - 1 - idx]
85
+ n_block = block_indices[block_count - 2 - idx]
86
+ kv_producer_state_prev = kv_producer_state.clone()
87
+ kv_producer_state.advance()
88
+ pipeline_k.producer_acquire(kv_producer_state)
89
+ load_K(src_idx=n_block, producer_state=kv_producer_state)
90
+ pipeline_v.producer_acquire(kv_producer_state_prev)
91
+ load_V(src_idx=n_block_prev, producer_state=kv_producer_state_prev)
92
+
93
+ return kv_producer_state
94
+
95
+
96
+ @cute.jit
97
+ def finish_overlap_v_load(
98
+ block_indices: cute.Tensor,
99
+ block_count,
100
+ load_V,
101
+ pipeline_v,
102
+ kv_producer_state,
103
+ ):
104
+ """Load the final V block after overlapped K/V loads."""
105
+ if block_count > 0:
106
+ n_block_last = block_indices[0]
107
+ pipeline_v.producer_acquire(kv_producer_state)
108
+ load_V(src_idx=n_block_last, producer_state=kv_producer_state)
109
+ kv_producer_state.advance()
110
+
111
+ return kv_producer_state
112
+
113
+
114
+ @cute.jit
115
+ def produce_block_sparse_loads(
116
+ blocksparse_tensors: BlockSparseTensors,
117
+ batch_idx,
118
+ head_idx,
119
+ m_block,
120
+ kv_producer_state,
121
+ load_Q,
122
+ load_K,
123
+ load_V,
124
+ pipeline_k,
125
+ pipeline_v,
126
+ use_tma_q: cutlass.Constexpr,
127
+ tma_q_bytes: cutlass.Constexpr,
128
+ intra_wg_overlap: cutlass.Constexpr,
129
+ ):
130
+ """Iterate over the mask and full block lists for a single tile.
131
+
132
+ The masked (partial) list may leave the last V load pending when intra-warp-group
133
+ overlap is enabled. The first full block must consume that pending V while
134
+ issuing its own K load on the next pipeline stage.
135
+
136
+ In the intra-wg-overlap path, the last masked block leaves its V copy in flight
137
+ while we advance the producer state to start the next full K. Either the full list
138
+ overlaps that pending V load, or, if no full blocks exist, we explicitly drain it.
139
+
140
+ """
141
+
142
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
143
+
144
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block]
145
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block, None]
146
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block]
147
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block, None]
148
+
149
+ mask_empty = curr_mask_block_cnt == 0
150
+ full_empty = curr_full_block_cnt == 0
151
+
152
+ if mask_empty:
153
+ # No masked blocks: the full list owns the initial Q+K load.
154
+ kv_producer_state = load_block_list(
155
+ curr_full_block_idx,
156
+ curr_full_block_cnt,
157
+ load_q_with_first=True,
158
+ first_block_preloaded=False,
159
+ kv_producer_state=kv_producer_state,
160
+ load_Q=load_Q,
161
+ load_K=load_K,
162
+ load_V=load_V,
163
+ pipeline_k=pipeline_k,
164
+ pipeline_v=pipeline_v,
165
+ use_tma_q=use_tma_q,
166
+ tma_q_bytes=tma_q_bytes,
167
+ intra_wg_overlap=intra_wg_overlap,
168
+ )
169
+
170
+ if const_expr(intra_wg_overlap) and curr_full_block_cnt > 0:
171
+ kv_producer_state = finish_overlap_v_load(
172
+ curr_full_block_idx,
173
+ curr_full_block_cnt,
174
+ load_V,
175
+ pipeline_v,
176
+ kv_producer_state,
177
+ )
178
+ else:
179
+ # Masked blocks present: load Q together with the first masked K so consumers can
180
+ # start immediately. When overlap is disabled this fully drains the list.
181
+ kv_producer_state = load_block_list(
182
+ curr_mask_block_idx,
183
+ curr_mask_block_cnt,
184
+ load_q_with_first=True,
185
+ first_block_preloaded=False,
186
+ kv_producer_state=kv_producer_state,
187
+ load_Q=load_Q,
188
+ load_K=load_K,
189
+ load_V=load_V,
190
+ pipeline_k=pipeline_k,
191
+ pipeline_v=pipeline_v,
192
+ use_tma_q=use_tma_q,
193
+ tma_q_bytes=tma_q_bytes,
194
+ intra_wg_overlap=intra_wg_overlap,
195
+ )
196
+
197
+ if full_empty:
198
+ if const_expr(intra_wg_overlap):
199
+ kv_producer_state = finish_overlap_v_load(
200
+ curr_mask_block_idx,
201
+ curr_mask_block_cnt,
202
+ load_V,
203
+ pipeline_v,
204
+ kv_producer_state,
205
+ )
206
+ else:
207
+ if const_expr(intra_wg_overlap):
208
+ # Bridge the masked list to the full list by overlapping the pending masked V
209
+ # with the first full K load.
210
+ n_block_mask_last = curr_mask_block_idx[0]
211
+ n_block_full_first = curr_full_block_idx[curr_full_block_cnt - 1]
212
+ kv_producer_state_prev = kv_producer_state.clone()
213
+ kv_producer_state.advance()
214
+ pipeline_k.producer_acquire(kv_producer_state)
215
+ load_K(src_idx=n_block_full_first, producer_state=kv_producer_state)
216
+ pipeline_v.producer_acquire(kv_producer_state_prev)
217
+ load_V(src_idx=n_block_mask_last, producer_state=kv_producer_state_prev)
218
+
219
+ kv_producer_state = load_block_list(
220
+ curr_full_block_idx,
221
+ curr_full_block_cnt,
222
+ load_q_with_first=False,
223
+ first_block_preloaded=True,
224
+ kv_producer_state=kv_producer_state,
225
+ load_Q=load_Q,
226
+ load_K=load_K,
227
+ load_V=load_V,
228
+ pipeline_k=pipeline_k,
229
+ pipeline_v=pipeline_v,
230
+ use_tma_q=use_tma_q,
231
+ tma_q_bytes=tma_q_bytes,
232
+ intra_wg_overlap=intra_wg_overlap,
233
+ )
234
+
235
+ kv_producer_state = finish_overlap_v_load(
236
+ curr_full_block_idx,
237
+ curr_full_block_cnt,
238
+ load_V,
239
+ pipeline_v,
240
+ kv_producer_state,
241
+ )
242
+ else:
243
+ # Non-overlap path with both lists: run the full list normally (skipping the Q
244
+ # reload because the masked list already issued it).
245
+ kv_producer_state = load_block_list(
246
+ curr_full_block_idx,
247
+ curr_full_block_cnt,
248
+ load_q_with_first=False,
249
+ first_block_preloaded=False,
250
+ kv_producer_state=kv_producer_state,
251
+ load_Q=load_Q,
252
+ load_K=load_K,
253
+ load_V=load_V,
254
+ pipeline_k=pipeline_k,
255
+ pipeline_v=pipeline_v,
256
+ use_tma_q=use_tma_q,
257
+ tma_q_bytes=tma_q_bytes,
258
+ intra_wg_overlap=intra_wg_overlap,
259
+ )
260
+
261
+ return kv_producer_state
262
+
263
+
264
+ @cute.jit
265
+ def consume_block_sparse_loads(
266
+ blocksparse_tensors: BlockSparseTensors,
267
+ batch_idx,
268
+ head_idx,
269
+ m_block,
270
+ kv_consumer_state,
271
+ mma_pv_fn,
272
+ mma_one_n_block,
273
+ process_first_half_block,
274
+ process_last_half_block,
275
+ mask_fn,
276
+ score_mod_fn,
277
+ O_should_accumulate,
278
+ mask_mod,
279
+ intra_wg_overlap: cutlass.Constexpr,
280
+ warp_scheduler_barrier_sync: Callable,
281
+ warp_scheduler_barrier_arrive: Callable,
282
+ ):
283
+ """Consume the mask and full block lists for a single tile on the consumer side.
284
+
285
+ Mirrors `produce_block_sparse_loads` so that the consumer pipeline
286
+ """
287
+
288
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
289
+
290
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block]
291
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block, None]
292
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block]
293
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block, None]
294
+
295
+ processed_any = curr_mask_block_cnt + curr_full_block_cnt > 0
296
+
297
+ if const_expr(not intra_wg_overlap):
298
+ if curr_mask_block_cnt > 0:
299
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
300
+ warp_scheduler_barrier_sync()
301
+ kv_consumer_state = mma_one_n_block(
302
+ kv_consumer_state,
303
+ n_block=mask_n_block,
304
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
305
+ mask_fn=partial(mask_fn, mask_mod=mask_mod, mask_seqlen=True),
306
+ is_first_n_block=True,
307
+ )
308
+ O_should_accumulate = True
309
+ for i in cutlass.range(1, curr_mask_block_cnt):
310
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1 - i]
311
+ kv_consumer_state = mma_one_n_block(
312
+ kv_consumer_state,
313
+ n_block=mask_n_block,
314
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
315
+ mask_fn=partial(mask_fn, mask_mod=mask_mod, mask_seqlen=False),
316
+ is_first_n_block=False,
317
+ )
318
+ O_should_accumulate = True
319
+ if curr_full_block_cnt == 0:
320
+ warp_scheduler_barrier_arrive()
321
+
322
+ if curr_full_block_cnt > 0:
323
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1]
324
+ if curr_mask_block_cnt == 0:
325
+ warp_scheduler_barrier_sync()
326
+ kv_consumer_state = mma_one_n_block(
327
+ kv_consumer_state,
328
+ n_block=full_n_block,
329
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
330
+ mask_fn=partial(mask_fn, mask_seqlen=True),
331
+ is_first_n_block=True,
332
+ )
333
+ O_should_accumulate = True
334
+ for i in cutlass.range(1, curr_full_block_cnt):
335
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
336
+ kv_consumer_state = mma_one_n_block(
337
+ kv_consumer_state,
338
+ n_block=full_n_block,
339
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
340
+ mask_fn=partial(mask_fn, mask_seqlen=False),
341
+ is_first_n_block=False,
342
+ )
343
+ O_should_accumulate = True
344
+ else:
345
+ kv_consumer_state = mma_one_n_block(
346
+ kv_consumer_state,
347
+ n_block=full_n_block,
348
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
349
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
350
+ is_first_n_block=False,
351
+ )
352
+ O_should_accumulate = True
353
+ for i in cutlass.range(1, curr_full_block_cnt):
354
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
355
+ kv_consumer_state = mma_one_n_block(
356
+ kv_consumer_state,
357
+ n_block=full_n_block,
358
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
359
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=False),
360
+ is_first_n_block=False,
361
+ )
362
+ O_should_accumulate = True
363
+ warp_scheduler_barrier_arrive()
364
+ else:
365
+ if curr_mask_block_cnt > 0:
366
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
367
+ kv_consumer_state = process_first_half_block(
368
+ n_block=mask_n_block,
369
+ kv_consumer_state=kv_consumer_state,
370
+ mask_fn=partial(mask_fn, mask_mod=mask_mod),
371
+ score_mod_fn=score_mod_fn,
372
+ is_first_block=True,
373
+ )
374
+ for i in cutlass.range(1, curr_mask_block_cnt):
375
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1 - i]
376
+ kv_consumer_state = mma_one_n_block(
377
+ kv_consumer_state,
378
+ n_block=mask_n_block,
379
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
380
+ mask_fn=partial(mask_fn, mask_mod=mask_mod, mask_seqlen=False),
381
+ )
382
+ O_should_accumulate = True
383
+
384
+ if curr_full_block_cnt > 0:
385
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1]
386
+ if curr_mask_block_cnt == 0:
387
+ kv_consumer_state = process_first_half_block(
388
+ n_block=full_n_block,
389
+ kv_consumer_state=kv_consumer_state,
390
+ mask_fn=partial(mask_fn, mask_mod=None),
391
+ score_mod_fn=score_mod_fn,
392
+ is_first_block=True,
393
+ )
394
+ else:
395
+ kv_consumer_state = mma_one_n_block(
396
+ kv_consumer_state,
397
+ n_block=full_n_block,
398
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
399
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
400
+ )
401
+ O_should_accumulate = True
402
+ for i in cutlass.range(1, curr_full_block_cnt):
403
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
404
+ kv_consumer_state = mma_one_n_block(
405
+ kv_consumer_state,
406
+ n_block=full_n_block,
407
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
408
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=False),
409
+ )
410
+ O_should_accumulate = True
411
+
412
+ if curr_mask_block_cnt + curr_full_block_cnt > 0:
413
+ kv_consumer_state = process_last_half_block(
414
+ kv_consumer_state=kv_consumer_state,
415
+ zero_init=not O_should_accumulate,
416
+ )
417
+ O_should_accumulate = True
418
+
419
+ return kv_consumer_state, O_should_accumulate, processed_any
build/torch-universal/block_sparsity.py ADDED
@@ -0,0 +1,650 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Computes block-sparse attention masks for Flex Attention.
3
+
4
+ This utility generates block sparsity patterns based on common attention masking
5
+ strategies (e.g., causal, sliding window). The resulting tensors define which
6
+ blocks are fully computed, which are partially computed (requiring a mask), and
7
+ which are skipped entirely. This is a temporary solution intended to be replaced
8
+ by a more robust preprocessing kernel in the future.
9
+ """
10
+
11
+ from typing import Tuple, Optional, Callable, List, NamedTuple
12
+ import torch
13
+ import cutlass.cute as cute
14
+ from cutlass.cute.runtime import from_dlpack
15
+
16
+
17
+ # placeholder
18
+ Config = type("Config", (), {})
19
+
20
+
21
+ class BlockSparseTensors(NamedTuple):
22
+ mask_block_cnt: cute.Tensor
23
+ mask_block_idx: cute.Tensor
24
+ full_block_cnt: Optional[cute.Tensor]
25
+ full_block_idx: Optional[cute.Tensor]
26
+
27
+ def __new_from_mlir_values__(self, values):
28
+ if len(values) == 2:
29
+ values = (*values, None, None)
30
+ return BlockSparseTensors(*values)
31
+
32
+
33
+ class BlockSparseTensorsTorch(NamedTuple):
34
+ mask_block_cnt: torch.Tensor
35
+ mask_block_idx: torch.Tensor
36
+ full_block_cnt: Optional[torch.Tensor] = None
37
+ full_block_idx: Optional[torch.Tensor] = None
38
+
39
+
40
+ def _expand_sparsity_tensor(
41
+ tensor: torch.Tensor,
42
+ expected_shape: Tuple[int, ...],
43
+ tensor_name: str,
44
+ ) -> torch.Tensor:
45
+ """Check if we need to expand the tensor to expected shape, and do so if possible."""
46
+ needs_expand = tensor.shape != expected_shape
47
+ if not needs_expand:
48
+ return tensor
49
+ can_expand = all(map(lambda cur, tgt: cur == tgt or cur == 1, tensor.shape, expected_shape))
50
+ if not can_expand:
51
+ raise ValueError(
52
+ f"{tensor_name} with shape {tensor.shape} cannot be expanded to expected shape {expected_shape}."
53
+ )
54
+ return tensor.expand(*expected_shape).contiguous()
55
+
56
+
57
+ def _check_and_expand_block(
58
+ name: str,
59
+ cnt: Optional[torch.Tensor],
60
+ idx: Optional[torch.Tensor],
61
+ expected_count_shape: Tuple[int, int, int],
62
+ expected_index_shape: Tuple[int, int, int, int],
63
+ ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor]]:
64
+ if (cnt is None) != (idx is None):
65
+ raise ValueError(
66
+ f"{name}_block_cnt and {name}_block_idx must both be provided or both be None"
67
+ )
68
+ if cnt is None or idx is None:
69
+ return None, None
70
+ if cnt.dtype != torch.int32 or idx.dtype != torch.int32:
71
+ raise ValueError(f"{name}_block tensors must have dtype torch.int32")
72
+ if cnt.device != idx.device:
73
+ raise ValueError(f"{name}_block_cnt and {name}_block_idx must be on the same device")
74
+ if not cnt.is_cuda or not idx.is_cuda:
75
+ raise ValueError(f"{name}_block tensors must live on CUDA")
76
+ expanded_cnt = _expand_sparsity_tensor(cnt, expected_count_shape, f"{name}_block_cnt")
77
+ expanded_idx = _expand_sparsity_tensor(idx, expected_index_shape, f"{name}_block_idx")
78
+ return expanded_cnt, expanded_idx
79
+
80
+
81
+ def normalize_block_sparse_tensors(
82
+ tensors: BlockSparseTensorsTorch,
83
+ *,
84
+ expected_count_shape: Tuple[int, int, int],
85
+ expected_index_shape: Tuple[int, int, int, int],
86
+ ) -> BlockSparseTensorsTorch:
87
+ if tensors.mask_block_cnt is None or tensors.mask_block_idx is None:
88
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
89
+
90
+ mask_cnt, mask_idx = _check_and_expand_block(
91
+ "mask",
92
+ tensors.mask_block_cnt,
93
+ tensors.mask_block_idx,
94
+ expected_count_shape,
95
+ expected_index_shape,
96
+ )
97
+ if mask_cnt is None or mask_idx is None:
98
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
99
+
100
+ full_cnt, full_idx = _check_and_expand_block(
101
+ "full",
102
+ tensors.full_block_cnt,
103
+ tensors.full_block_idx,
104
+ expected_count_shape,
105
+ expected_index_shape,
106
+ )
107
+ if full_cnt is not None and mask_cnt.device != full_cnt.device:
108
+ raise ValueError("All block sparse tensors must be on the same device")
109
+
110
+ return BlockSparseTensorsTorch(
111
+ mask_block_cnt=mask_cnt,
112
+ mask_block_idx=mask_idx,
113
+ full_block_cnt=full_cnt,
114
+ full_block_idx=full_idx,
115
+ )
116
+
117
+
118
+ def is_block_sparsity_enabled(tensors: BlockSparseTensorsTorch) -> bool:
119
+ return any(t is not None for t in (tensors.full_block_cnt, tensors.mask_block_cnt))
120
+
121
+
122
+ def to_cute_block_sparse_tensors(tensors: BlockSparseTensorsTorch) -> Optional[BlockSparseTensors]:
123
+ if not is_block_sparsity_enabled(tensors):
124
+ return None
125
+
126
+ mask_block_cnt_tensor = from_dlpack(
127
+ tensors.mask_block_cnt.detach(), assumed_align=4
128
+ ).mark_layout_dynamic(leading_dim=2)
129
+ mask_block_idx_tensor = from_dlpack(
130
+ tensors.mask_block_idx.detach(), assumed_align=4
131
+ ).mark_layout_dynamic(leading_dim=3)
132
+ full_block_cnt_tensor = (
133
+ from_dlpack(tensors.full_block_cnt.detach(), assumed_align=4).mark_layout_dynamic(
134
+ leading_dim=2
135
+ )
136
+ if tensors.full_block_cnt is not None
137
+ else None
138
+ )
139
+ full_block_idx_tensor = (
140
+ from_dlpack(tensors.full_block_idx.detach(), assumed_align=4).mark_layout_dynamic(
141
+ leading_dim=3
142
+ )
143
+ if tensors.full_block_idx is not None
144
+ else None
145
+ )
146
+
147
+ return BlockSparseTensors(
148
+ mask_block_cnt_tensor,
149
+ mask_block_idx_tensor,
150
+ full_block_cnt_tensor,
151
+ full_block_idx_tensor,
152
+ )
153
+
154
+
155
+ def compute_block_sparsity(
156
+ config: Config,
157
+ mask_mod_flex: Optional[Callable],
158
+ device: str,
159
+ cu_seqlens_q: Optional[torch.Tensor] = None,
160
+ cu_seqlens_k: Optional[torch.Tensor] = None,
161
+ aux_tensors: Optional[List[torch.Tensor]] = None,
162
+ ) -> Tuple[
163
+ Optional[torch.Tensor], Optional[torch.Tensor], Optional[torch.Tensor], Optional[torch.Tensor]
164
+ ]:
165
+ """
166
+ Computes block sparsity tensors from a given masking function.
167
+
168
+ This function serves as the main entry point for generating block-sparse masks.
169
+ It dispatches to specialized handlers for variable-length and fixed-length
170
+ sequences.
171
+
172
+ Args:
173
+ config: A configuration object containing model and tiling parameters.
174
+ mask_mod_flex: The mask function for generic flex attention patterns.
175
+ device: The device to create tensors on (e.g., 'cuda').
176
+ cu_seqlens_q: Cumulative sequence lengths for Q (for varlen).
177
+ cu_seqlens_k: Cumulative sequence lengths for K (for varlen).
178
+ aux_tensors: A list of auxiliary tensors, e.g., for document masking.
179
+
180
+ Returns:
181
+ A tuple of four tensors:
182
+ - `full_block_cnt`: (batch, nheads, n_blocks_q) - Count of full n blocks per m block.
183
+ - `full_block_idx`: (batch, nheads, n_blocks_q, max_n_blocks) - Indices of full n blocks.
184
+ - `mask_block_cnt`: (batch, nheads, n_blocks_q) - Count of partial n blocks per m block.
185
+ - `mask_block_idx`: (batch, nheads, n_blocks_q, max_n_blocks) - Indices of partial n blocks.
186
+ Returns (None, None, None, None) if masking is disabled.
187
+ """
188
+ if not config.use_mask_mod or mask_mod_flex is None:
189
+ return None, None, None, None
190
+
191
+ if cu_seqlens_q is not None:
192
+ # Handle variable-length sequences
193
+ return _compute_varlen_sparsity(config, mask_mod_flex, device, cu_seqlens_q, cu_seqlens_k)
194
+ else:
195
+ # Handle fixed-length sequences
196
+ return _compute_sparsity(config, device, aux_tensors)
197
+
198
+
199
+ ## ---------------------------------------------------------------------------
200
+ ## Fixed-Length Sequence Kernels
201
+ ## ---------------------------------------------------------------------------
202
+
203
+
204
+ def _compute_sparsity(
205
+ config: Config, device: str, aux_tensors: Optional[List[torch.Tensor]]
206
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
207
+ """Computes block sparsity for fixed-length sequences."""
208
+ n_blocks_q = (config.seqlen_q + config.tile_m - 1) // config.tile_m
209
+ n_blocks_k = (config.seqlen_k + config.tile_n - 1) // config.tile_n
210
+
211
+ # Pre-allocate output tensors
212
+ full_block_cnt = torch.zeros(
213
+ (config.batch_size, config.nheads, n_blocks_q), device=device, dtype=torch.int32
214
+ )
215
+ mask_block_cnt = torch.zeros(
216
+ (config.batch_size, config.nheads, n_blocks_q), device=device, dtype=torch.int32
217
+ )
218
+ full_block_idx = torch.zeros(
219
+ (config.batch_size, config.nheads, n_blocks_q, n_blocks_k), device=device, dtype=torch.int32
220
+ )
221
+ mask_block_idx = torch.zeros(
222
+ (config.batch_size, config.nheads, n_blocks_q, n_blocks_k), device=device, dtype=torch.int32
223
+ )
224
+
225
+ # --- Identity Mask ---
226
+ # All blocks are fully computed.
227
+ if config.mask_mod_name == "identity":
228
+ k_blocks = torch.arange(n_blocks_k, device=device)
229
+ for q_block_idx in range(n_blocks_q):
230
+ full_block_cnt[:, :, q_block_idx] = n_blocks_k
231
+ full_block_idx[:, :, q_block_idx, :n_blocks_k] = k_blocks
232
+
233
+ # --- Identity Partial Mask ---
234
+ # All blocks are partially computed (masked).
235
+ elif config.mask_mod_name == "identity_partial":
236
+ k_blocks = torch.arange(n_blocks_k, device=device)
237
+ for q_block_idx in range(n_blocks_q):
238
+ mask_block_cnt[:, :, q_block_idx] = n_blocks_k
239
+ mask_block_idx[:, :, q_block_idx, :n_blocks_k] = k_blocks
240
+
241
+ # --- Block Causal Mask ---
242
+ elif config.mask_mod_name == "block_causal":
243
+ k_blocks = torch.arange(n_blocks_k, device=device)
244
+ for q_block_idx in range(n_blocks_q):
245
+ causal_indices = k_blocks[k_blocks <= q_block_idx]
246
+ num_causal_indices = len(causal_indices)
247
+ if num_causal_indices > 0:
248
+ full_block_cnt[:, :, q_block_idx] = num_causal_indices
249
+ full_block_idx[:, :, q_block_idx, :num_causal_indices] = causal_indices
250
+
251
+ # --- Causal and Sliding Window Masks ---
252
+ elif config.mask_mod_name in ["causal", "sliding_window"]:
253
+ q_block_indices = torch.arange(n_blocks_q, device=device)
254
+ k_block_indices = torch.arange(n_blocks_k, device=device)
255
+
256
+ q_starts = q_block_indices * config.tile_m
257
+ q_ends = torch.minimum(
258
+ (q_block_indices + 1) * config.tile_m, torch.tensor(config.seqlen_q, device=device)
259
+ )
260
+ k_starts = k_block_indices * config.tile_n
261
+ k_ends = torch.minimum(
262
+ (k_block_indices + 1) * config.tile_n, torch.tensor(config.seqlen_k, device=device)
263
+ )
264
+
265
+ # Expand dims for broadcasting: (n_blocks_q, 1) and (1, n_blocks_k)
266
+ q_starts, q_ends = q_starts.unsqueeze(1), q_ends.unsqueeze(1)
267
+ k_starts, k_ends = k_starts.unsqueeze(0), k_ends.unsqueeze(0)
268
+
269
+ offset = config.seqlen_k - config.seqlen_q
270
+
271
+ if config.mask_mod_name == "causal":
272
+ is_full = (k_ends - 1) <= (q_starts + offset)
273
+ # min(k_pos) <= max(q_pos) AND not is_full.
274
+ is_partial = (k_starts <= (q_ends - 1 + offset)) & ~is_full
275
+
276
+ else: # sliding_window
277
+ window_size = getattr(config, "window_size", 1024)
278
+ is_full = (k_ends - 1 <= q_starts + offset) & (
279
+ k_starts >= q_ends - 1 + offset - (window_size - 1)
280
+ )
281
+ # A block is EMPTY if no (q, k) pairs satisfy the constraint.
282
+ is_empty = (k_starts > q_ends - 1 + offset) | (
283
+ k_ends - 1 < q_starts + offset - (window_size - 1)
284
+ )
285
+ # A block is PARTIAL if it's not empty and not full.
286
+ is_partial = ~is_empty & ~is_full
287
+
288
+ # Populate indices based on the computed block classifications
289
+ for q_block_idx in range(n_blocks_q):
290
+ full_indices = k_block_indices[is_full[q_block_idx]]
291
+ if len(full_indices) > 0:
292
+ full_block_cnt[:, :, q_block_idx] = len(full_indices)
293
+ full_block_idx[:, :, q_block_idx, : len(full_indices)] = full_indices
294
+
295
+ partial_indices = k_block_indices[is_partial[q_block_idx]]
296
+ if len(partial_indices) > 0:
297
+ mask_block_cnt[:, :, q_block_idx] = len(partial_indices)
298
+ mask_block_idx[:, :, q_block_idx, : len(partial_indices)] = partial_indices
299
+
300
+ elif config.mask_mod_name == "document":
301
+ raise NotImplementedError("Block sparsity for document masking not yet implemented")
302
+
303
+ return full_block_cnt, full_block_idx, mask_block_cnt, mask_block_idx
304
+
305
+
306
+ ## ---------------------------------------------------------------------------
307
+ ## Variable-Length Sequence Kernels
308
+ ## ---------------------------------------------------------------------------
309
+
310
+
311
+ def _compute_varlen_sparsity(
312
+ config: Config,
313
+ mask_mod_flex: Callable,
314
+ device: str,
315
+ cu_seqlens_q: torch.Tensor,
316
+ cu_seqlens_k: torch.Tensor,
317
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
318
+ """Computes block sparsity for variable-length sequences."""
319
+ assert cu_seqlens_k is not None, "cu_seqlens_k is required for varlen attention"
320
+ assert cu_seqlens_q.shape[0] == config.batch_size + 1
321
+ assert cu_seqlens_k.shape[0] == config.batch_size + 1
322
+
323
+ # In varlen, each sequence can have a different number of Q blocks.
324
+ # We pad up to the maximum number of Q blocks in the batch.
325
+ max_m_blocks = 0
326
+ for seq_idx in range(config.batch_size):
327
+ seq_len_q = (cu_seqlens_q[seq_idx + 1] - cu_seqlens_q[seq_idx]).item()
328
+ n_blocks_q = (seq_len_q + config.tile_m - 1) // config.tile_m
329
+ max_m_blocks = max(max_m_blocks, n_blocks_q)
330
+
331
+ # The number of K blocks is determined by the total length of all sequences.
332
+ total_k_len = cu_seqlens_k[-1].item()
333
+ max_n_blocks = (total_k_len + config.tile_n - 1) // config.tile_n
334
+
335
+ # Pre-allocate padded output tensors
336
+ full_block_cnt = torch.zeros(
337
+ (config.batch_size, config.nheads, max_m_blocks), device=device, dtype=torch.int32
338
+ )
339
+ mask_block_cnt = torch.zeros(
340
+ (config.batch_size, config.nheads, max_m_blocks), device=device, dtype=torch.int32
341
+ )
342
+ full_block_idx = torch.zeros(
343
+ (config.batch_size, config.nheads, max_m_blocks, max_n_blocks),
344
+ device=device,
345
+ dtype=torch.int32,
346
+ )
347
+ mask_block_idx = torch.zeros(
348
+ (config.batch_size, config.nheads, max_m_blocks, max_n_blocks),
349
+ device=device,
350
+ dtype=torch.int32,
351
+ )
352
+
353
+ # Process each sequence in the batch individually
354
+ for seq_idx in range(config.batch_size):
355
+ seq_start_q = cu_seqlens_q[seq_idx].item()
356
+ seq_end_q = cu_seqlens_q[seq_idx + 1].item()
357
+ seq_len_q = seq_end_q - seq_start_q
358
+
359
+ seq_start_k = cu_seqlens_k[seq_idx].item()
360
+ seq_end_k = cu_seqlens_k[seq_idx + 1].item()
361
+ seq_len_k = seq_end_k - seq_start_k
362
+
363
+ n_blocks_q = (seq_len_q + config.tile_m - 1) // config.tile_m
364
+ n_blocks_k = (seq_len_k + config.tile_n - 1) // config.tile_n
365
+
366
+ # Global block indices are relative to the start of the entire batch tensor
367
+ first_m_block_global = seq_start_q // config.tile_m
368
+ first_n_block_global = seq_start_k // config.tile_n
369
+
370
+ common_args = {
371
+ "full_block_cnt": full_block_cnt,
372
+ "full_block_idx": full_block_idx,
373
+ "mask_block_cnt": mask_block_cnt,
374
+ "mask_block_idx": mask_block_idx,
375
+ "seq_idx": seq_idx,
376
+ "n_blocks_q": n_blocks_q,
377
+ "n_blocks_k": n_blocks_k,
378
+ "seq_start_q": seq_start_q,
379
+ "seq_end_q": seq_end_q,
380
+ "seq_start_k": seq_start_k,
381
+ "seq_end_k": seq_end_k,
382
+ "first_n_block_global": first_n_block_global,
383
+ "tile_m": config.tile_m,
384
+ "tile_n": config.tile_n,
385
+ "device": device,
386
+ }
387
+
388
+ if config.mask_mod_name == "causal":
389
+ _compute_causal_varlen_blocks(**common_args)
390
+ elif config.mask_mod_name == "sliding_window":
391
+ window_size = getattr(config, "window_size", 1024)
392
+ _compute_sliding_window_varlen_blocks(**common_args, window_size=window_size)
393
+ elif config.mask_mod_name == "identity":
394
+ _compute_identity_varlen_blocks(
395
+ full_block_cnt,
396
+ full_block_idx,
397
+ seq_idx,
398
+ n_blocks_q,
399
+ n_blocks_k,
400
+ first_n_block_global,
401
+ device,
402
+ )
403
+ else:
404
+ # Generic case relies on sampling the user-provided mask function
405
+ _compute_generic_varlen_blocks(
406
+ **common_args,
407
+ mask_mod_flex=mask_mod_flex,
408
+ seq_len_q=seq_len_q,
409
+ seq_len_k=seq_len_k,
410
+ num_heads=config.nheads,
411
+ nheads_kv=config.nheads_kv,
412
+ )
413
+
414
+ return full_block_cnt, full_block_idx, mask_block_cnt, mask_block_idx
415
+
416
+
417
+ def _classify_varlen_block(
418
+ m_local: int,
419
+ n_local: int,
420
+ seq_start_q: int,
421
+ seq_end_q: int,
422
+ seq_start_k: int,
423
+ seq_end_k: int,
424
+ tile_m: int,
425
+ tile_n: int,
426
+ is_full_fn: Callable,
427
+ is_partial_fn: Callable,
428
+ ) -> Tuple[bool, bool]:
429
+ """Helper to classify a varlen block as full, partial, or empty."""
430
+ m_start_global = seq_start_q + m_local * tile_m
431
+ m_end_global = min(seq_start_q + (m_local + 1) * tile_m, seq_end_q)
432
+ n_start_global = seq_start_k + n_local * tile_n
433
+ n_end_global = min(seq_start_k + (n_local + 1) * tile_n, seq_end_k)
434
+
435
+ # Use sequence-local coordinates for the logical check
436
+ m_start_local = m_start_global - seq_start_q
437
+ m_end_local = m_end_global - seq_start_q
438
+ n_start_local = n_start_global - seq_start_k
439
+ n_end_local = n_end_global - seq_start_k
440
+
441
+ is_full = is_full_fn(m_start_local, m_end_local, n_start_local, n_end_local)
442
+ is_partial = (
443
+ is_partial_fn(m_start_local, m_end_local, n_start_local, n_end_local) and not is_full
444
+ )
445
+
446
+ # Any block that touches the sequence boundary is partial because it requires masking.
447
+ at_boundary = (m_end_global > seq_end_q) or (n_end_global > seq_end_k)
448
+
449
+ return is_full and not at_boundary, is_partial or (is_full and at_boundary)
450
+
451
+
452
+ def _compute_causal_varlen_blocks(
453
+ full_block_cnt,
454
+ full_block_idx,
455
+ mask_block_cnt,
456
+ mask_block_idx,
457
+ seq_idx,
458
+ n_blocks_q,
459
+ n_blocks_k,
460
+ seq_start_q,
461
+ seq_end_q,
462
+ seq_start_k,
463
+ seq_end_k,
464
+ first_n_block_global,
465
+ tile_m,
466
+ tile_n,
467
+ device,
468
+ **kwargs,
469
+ ):
470
+ """Computes causal block sparsity for a single varlen sequence."""
471
+ is_full_fn = lambda m_start, m_end, n_start, n_end: (m_start >= n_end - 1)
472
+ is_partial_fn = lambda m_start, m_end, n_start, n_end: (m_end - 1 >= n_start)
473
+
474
+ for m_local in range(n_blocks_q):
475
+ full_blocks, partial_blocks = [], []
476
+ for n_local in range(n_blocks_k):
477
+ is_full, is_partial = _classify_varlen_block(
478
+ m_local,
479
+ n_local,
480
+ seq_start_q,
481
+ seq_end_q,
482
+ seq_start_k,
483
+ seq_end_k,
484
+ tile_m,
485
+ tile_n,
486
+ is_full_fn,
487
+ is_partial_fn,
488
+ )
489
+ n_block_global = first_n_block_global + n_local
490
+ if is_full:
491
+ full_blocks.append(n_block_global)
492
+ elif is_partial:
493
+ partial_blocks.append(n_block_global)
494
+
495
+ if full_blocks:
496
+ full_block_cnt[seq_idx, :, m_local] = len(full_blocks)
497
+ full_block_idx[seq_idx, :, m_local, : len(full_blocks)] = torch.tensor(
498
+ full_blocks, device=device
499
+ )
500
+ if partial_blocks:
501
+ mask_block_cnt[seq_idx, :, m_local] = len(partial_blocks)
502
+ mask_block_idx[seq_idx, :, m_local, : len(partial_blocks)] = torch.tensor(
503
+ partial_blocks, device=device
504
+ )
505
+
506
+
507
+ def _compute_sliding_window_varlen_blocks(
508
+ full_block_cnt,
509
+ full_block_idx,
510
+ mask_block_cnt,
511
+ mask_block_idx,
512
+ seq_idx,
513
+ n_blocks_q,
514
+ n_blocks_k,
515
+ seq_start_q,
516
+ seq_end_q,
517
+ seq_start_k,
518
+ seq_end_k,
519
+ first_n_block_global,
520
+ tile_m,
521
+ tile_n,
522
+ window_size,
523
+ device,
524
+ **kwargs,
525
+ ):
526
+ """Computes sliding window block sparsity for a single varlen sequence."""
527
+ is_full_fn = lambda m_start, m_end, n_start, n_end: (n_end - 1 <= m_start) and (
528
+ n_start >= m_start - window_size + 1
529
+ )
530
+ is_partial_fn = lambda m_start, m_end, n_start, n_end: not (
531
+ (n_start > m_end - 1) or (n_end - 1 < m_start - window_size + 1)
532
+ )
533
+
534
+ for m_local in range(n_blocks_q):
535
+ full_blocks, partial_blocks = [], []
536
+ for n_local in range(n_blocks_k):
537
+ is_full, is_partial = _classify_varlen_block(
538
+ m_local,
539
+ n_local,
540
+ seq_start_q,
541
+ seq_end_q,
542
+ seq_start_k,
543
+ seq_end_k,
544
+ tile_m,
545
+ tile_n,
546
+ is_full_fn,
547
+ is_partial_fn,
548
+ )
549
+ n_block_global = first_n_block_global + n_local
550
+ if is_full:
551
+ full_blocks.append(n_block_global)
552
+ elif is_partial:
553
+ partial_blocks.append(n_block_global)
554
+
555
+ if full_blocks:
556
+ full_block_cnt[seq_idx, :, m_local] = len(full_blocks)
557
+ full_block_idx[seq_idx, :, m_local, : len(full_blocks)] = torch.tensor(
558
+ full_blocks, device=device
559
+ )
560
+ if partial_blocks:
561
+ mask_block_cnt[seq_idx, :, m_local] = len(partial_blocks)
562
+ mask_block_idx[seq_idx, :, m_local, : len(partial_blocks)] = torch.tensor(
563
+ partial_blocks, device=device
564
+ )
565
+
566
+
567
+ def _compute_identity_varlen_blocks(
568
+ full_block_cnt,
569
+ full_block_idx,
570
+ seq_idx,
571
+ n_blocks_q,
572
+ n_blocks_k,
573
+ first_n_block_global,
574
+ device,
575
+ **kwargs,
576
+ ):
577
+ """Computes identity (all-attend) block sparsity for a single varlen sequence."""
578
+ n_blocks_global = torch.arange(
579
+ first_n_block_global, first_n_block_global + n_blocks_k, device=device, dtype=torch.int32
580
+ )
581
+ for m_local in range(n_blocks_q):
582
+ full_block_cnt[seq_idx, :, m_local] = n_blocks_k
583
+ full_block_idx[seq_idx, :, m_local, :n_blocks_k] = n_blocks_global
584
+
585
+
586
+ def _compute_generic_varlen_blocks(
587
+ full_block_cnt,
588
+ full_block_idx,
589
+ mask_block_cnt,
590
+ mask_block_idx,
591
+ mask_mod_flex,
592
+ seq_idx,
593
+ num_heads,
594
+ n_blocks_q,
595
+ n_blocks_k,
596
+ seq_len_q,
597
+ seq_len_k,
598
+ first_n_block_global,
599
+ tile_m,
600
+ tile_n,
601
+ nheads_kv,
602
+ device,
603
+ **kwargs,
604
+ ):
605
+ """Generic sampling-based block classification for a varlen sequence."""
606
+ qhead_per_kvhead = num_heads // nheads_kv
607
+
608
+ for h_q in range(num_heads):
609
+ h_kv = h_q // qhead_per_kvhead
610
+ for m_local in range(n_blocks_q):
611
+ m_start_local = m_local * tile_m
612
+ m_end_local = min((m_local + 1) * tile_m, seq_len_q)
613
+
614
+ full_blocks, partial_blocks = [], []
615
+ for n_local in range(n_blocks_k):
616
+ n_start_local = n_local * tile_n
617
+ n_end_local = min((n_local + 1) * tile_n, seq_len_k)
618
+
619
+ # Sample points within the block (corners and center) to classify it.
620
+ # Coordinates are sequence-local, as required by mask_mod_flex.
621
+ sample_positions = [
622
+ (m_start_local, n_start_local),
623
+ (m_start_local, n_end_local - 1),
624
+ (m_end_local - 1, n_start_local),
625
+ (m_end_local - 1, n_end_local - 1),
626
+ ((m_start_local + m_end_local) // 2, (n_start_local + n_end_local) // 2),
627
+ ]
628
+
629
+ unmasked_count = sum(
630
+ 1
631
+ for q_pos, k_pos in sample_positions
632
+ if mask_mod_flex(seq_idx, h_q, q_pos, k_pos, seq_len_q, seq_len_k)
633
+ )
634
+
635
+ n_block_global = first_n_block_global + n_local
636
+ if unmasked_count == len(sample_positions): # All samples unmasked -> full
637
+ full_blocks.append(n_block_global)
638
+ elif unmasked_count > 0: # Some unmasked -> partial
639
+ partial_blocks.append(n_block_global)
640
+
641
+ if full_blocks:
642
+ full_block_cnt[seq_idx, h_q, m_local] = len(full_blocks)
643
+ full_block_idx[seq_idx, h_q, m_local, : len(full_blocks)] = torch.tensor(
644
+ full_blocks, device=device
645
+ )
646
+ if partial_blocks:
647
+ mask_block_cnt[seq_idx, h_q, m_local] = len(partial_blocks)
648
+ mask_block_idx[seq_idx, h_q, m_local, : len(partial_blocks)] = torch.tensor(
649
+ partial_blocks, device=device
650
+ )
build/torch-universal/copy_utils.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+
3
+ import math
4
+ from typing import Optional, Type, Tuple, Callable
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+ from cutlass import Float32, Int32, Boolean, const_expr
9
+ from cutlass.cute.nvgpu import cpasync
10
+ import cutlass.utils.blackwell_helpers as sm100_utils
11
+ from cutlass.cutlass_dsl import T, dsl_user_op
12
+ from cutlass._mlir.dialects import llvm
13
+ import cutlass.pipeline
14
+
15
+
16
+ @dsl_user_op
17
+ def cvt_copy(
18
+ atom: cute.CopyAtom,
19
+ src: cute.Tensor,
20
+ dst: cute.Tensor,
21
+ *,
22
+ pred: Optional[cute.Tensor] = None,
23
+ loc=None,
24
+ ip=None,
25
+ **kwargs,
26
+ ) -> None:
27
+ assert isinstance(src.iterator, cute.Pointer) and src.memspace == cute.AddressSpace.rmem
28
+ if const_expr(src.element_type != dst.element_type):
29
+ src_cvt = cute.make_fragment_like(src, dst.element_type, loc=loc, ip=ip)
30
+ src_cvt.store(src.load().to(dst.element_type))
31
+ src = src_cvt
32
+ cute.copy(atom, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
33
+
34
+
35
+ @dsl_user_op
36
+ def load_s2r(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
37
+ dst = cute.make_fragment_like(src, src.element_type, loc=loc, ip=ip)
38
+ cute.autovec_copy(src, dst, loc=loc, ip=ip)
39
+ return dst
40
+
41
+
42
+ @dsl_user_op
43
+ def get_copy_atom(
44
+ dtype: Type[cutlass.Numeric], num_copy_elems: int, is_async: bool = False, *, loc=None, ip=None
45
+ ) -> cute.CopyAtom:
46
+ num_copy_bits = const_expr(min(128, num_copy_elems * dtype.width))
47
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
48
+ return cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
49
+
50
+
51
+ @dsl_user_op
52
+ def make_tmem_copy(
53
+ tmem_copy_atom: cute.CopyAtom, num_wg: int = 1, *, loc=None, ip=None
54
+ ) -> cute.CopyAtom:
55
+ num_dp, num_bits, num_rep, _ = sm100_utils.get_tmem_copy_properties(tmem_copy_atom)
56
+ assert num_dp == 32
57
+ assert num_bits == 32
58
+ tiler_mn = (cute.make_layout((128 * num_rep * num_wg // 32, 32), stride=(32, 1)),)
59
+ layout_tv = cute.make_layout(
60
+ ((32, 4, num_wg), (num_rep, 32)), stride=((0, 1, 4 * num_rep), (4, 4 * num_rep * num_wg))
61
+ )
62
+ return cute.make_tiled_copy(tmem_copy_atom, layout_tv, tiler_mn)
63
+
64
+
65
+ @dsl_user_op
66
+ def copy(
67
+ src: cute.Tensor,
68
+ dst: cute.Tensor,
69
+ *,
70
+ pred: Optional[cute.Tensor] = None,
71
+ num_copy_elems: int = 1,
72
+ is_async: bool = False,
73
+ loc=None,
74
+ ip=None,
75
+ **kwargs,
76
+ ) -> None:
77
+ copy_atom = get_copy_atom(src.element_type, num_copy_elems, is_async)
78
+ cute.copy(copy_atom, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
79
+
80
+
81
+ def tiled_copy_1d(
82
+ dtype: Type[cutlass.Numeric], num_threads: int, num_copy_elems: int = 1, is_async: bool = False
83
+ ) -> cute.TiledCopy:
84
+ num_copy_bits = num_copy_elems * dtype.width
85
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
86
+ copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
87
+ thr_layout = cute.make_layout(num_threads)
88
+ val_layout = cute.make_layout(num_copy_elems)
89
+ return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
90
+
91
+
92
+ def tiled_copy_2d(
93
+ dtype: Type[cutlass.Numeric], major_mode_size: int, num_threads: int, is_async: bool = False
94
+ ) -> cute.TiledCopy:
95
+ num_copy_bits = math.gcd(major_mode_size, 128 // dtype.width) * dtype.width
96
+ copy_elems = num_copy_bits // dtype.width
97
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
98
+ copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
99
+ gmem_threads_per_row = major_mode_size // copy_elems
100
+ assert num_threads % gmem_threads_per_row == 0
101
+ thr_layout = cute.make_ordered_layout(
102
+ (num_threads // gmem_threads_per_row, gmem_threads_per_row),
103
+ order=(1, 0),
104
+ )
105
+ val_layout = cute.make_layout((1, copy_elems))
106
+ return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
107
+
108
+
109
+ @dsl_user_op
110
+ def atomic_add_fp32x4(
111
+ a: Float32, b: Float32, c: Float32, d: Float32, gmem_ptr: cute.Pointer, *, loc=None, ip=None
112
+ ) -> None:
113
+ gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
114
+ # cache_hint = cutlass.Int64(0x12F0000000000000)
115
+ llvm.inline_asm(
116
+ None,
117
+ [
118
+ gmem_ptr_i64,
119
+ Float32(a).ir_value(loc=loc, ip=ip),
120
+ Float32(b).ir_value(loc=loc, ip=ip),
121
+ Float32(c).ir_value(loc=loc, ip=ip),
122
+ Float32(d).ir_value(loc=loc, ip=ip),
123
+ ],
124
+ # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip), cache_hint.ir_value()],
125
+ "{\n\t"
126
+ # ".reg .b128 abcd;\n\t"
127
+ # "mov.b128 abcd, {$1, $2, $3, $4};\n\t"
128
+ ".reg .v4 .f32 abcd;\n\t"
129
+ # "mov.b128 abcd, {$1, $2, $3, $4};\n\t"
130
+ "mov.f32 abcd.x, $1;\n\t"
131
+ "mov.f32 abcd.y, $2;\n\t"
132
+ "mov.f32 abcd.z, $3;\n\t"
133
+ "mov.f32 abcd.w, $4;\n\t"
134
+ "red.global.add.v4.f32 [$0], abcd;\n\t"
135
+ # "red.global.add.L2::cache_hint.v4.f32 [$0], abcd, 0x14F0000000000000;\n\t"
136
+ "}\n",
137
+ # "red.global.add.L2::cache_hint.f32 [$0], $1, 0x12F0000000000000;",
138
+ # "red.global.add.L2::cache_hint.f32 [$0], $1, $2;",
139
+ "l,f,f,f,f",
140
+ # "l,f,l",
141
+ has_side_effects=True,
142
+ is_align_stack=False,
143
+ asm_dialect=llvm.AsmDialect.AD_ATT,
144
+ )
145
+
146
+
147
+ @dsl_user_op
148
+ def set_block_rank(
149
+ smem_ptr: cute.Pointer, peer_cta_rank_in_cluster: Int32, *, loc=None, ip=None
150
+ ) -> Int32:
151
+ """Map the given smem pointer to the address at another CTA rank in the cluster."""
152
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
153
+ return Int32(
154
+ llvm.inline_asm(
155
+ T.i32(),
156
+ [smem_ptr_i32, peer_cta_rank_in_cluster.ir_value()],
157
+ "mapa.shared::cluster.u32 $0, $1, $2;",
158
+ "=r,r,r",
159
+ has_side_effects=False,
160
+ is_align_stack=False,
161
+ asm_dialect=llvm.AsmDialect.AD_ATT,
162
+ )
163
+ )
164
+
165
+
166
+ @dsl_user_op
167
+ def store_shared_remote_fp32x4(
168
+ a: Float32,
169
+ b: Float32,
170
+ c: Float32,
171
+ d: Float32,
172
+ smem_ptr: cute.Pointer,
173
+ mbar_ptr: cute.Pointer,
174
+ peer_cta_rank_in_cluster: Int32,
175
+ *,
176
+ loc=None,
177
+ ip=None,
178
+ ) -> None:
179
+ remote_smem_ptr_i32 = set_block_rank(
180
+ smem_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
181
+ ).ir_value()
182
+ remote_mbar_ptr_i32 = set_block_rank(
183
+ mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
184
+ ).ir_value()
185
+ llvm.inline_asm(
186
+ None,
187
+ [
188
+ remote_smem_ptr_i32,
189
+ remote_mbar_ptr_i32,
190
+ Float32(a).ir_value(loc=loc, ip=ip),
191
+ Float32(b).ir_value(loc=loc, ip=ip),
192
+ Float32(c).ir_value(loc=loc, ip=ip),
193
+ Float32(d).ir_value(loc=loc, ip=ip),
194
+ ],
195
+ "{\n\t"
196
+ ".reg .v4 .f32 abcd;\n\t"
197
+ "mov.f32 abcd.x, $2;\n\t"
198
+ "mov.f32 abcd.y, $3;\n\t"
199
+ "mov.f32 abcd.z, $4;\n\t"
200
+ "mov.f32 abcd.w, $5;\n\t"
201
+ "st.async.shared::cluster.mbarrier::complete_tx::bytes.v4.f32 [$0], abcd, [$1];\n\t"
202
+ "}\n",
203
+ "r,r,f,f,f,f",
204
+ has_side_effects=True,
205
+ is_align_stack=False,
206
+ asm_dialect=llvm.AsmDialect.AD_ATT,
207
+ )
208
+
209
+
210
+ @dsl_user_op
211
+ def cpasync_bulk_g2s(
212
+ gmem_ptr: cute.Pointer,
213
+ smem_ptr: cute.Pointer,
214
+ tma_bar_ptr: cute.Pointer,
215
+ size: int | Int32,
216
+ *,
217
+ loc=None,
218
+ ip=None,
219
+ ):
220
+ gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
221
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
222
+ mbar_ptr_i32 = tma_bar_ptr.toint(loc=loc, ip=ip).ir_value()
223
+ llvm.inline_asm(
224
+ None,
225
+ [gmem_ptr_i64, smem_ptr_i32, mbar_ptr_i32, Int32(size).ir_value()],
226
+ "cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes [$1], [$0], $3, [$2];",
227
+ "l,r,r,r",
228
+ has_side_effects=True,
229
+ is_align_stack=False,
230
+ asm_dialect=llvm.AsmDialect.AD_ATT,
231
+ )
232
+
233
+
234
+ @dsl_user_op
235
+ def cpasync_reduce_bulk_add_f32(
236
+ smem_ptr: cute.Pointer,
237
+ gmem_ptr: cute.Pointer,
238
+ store_bytes: int | Int32,
239
+ *,
240
+ loc=None,
241
+ ip=None,
242
+ ):
243
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
244
+ # cache_hint = cutlass.Int64(0x14F0000000000000) # EVICT_LAST
245
+ llvm.inline_asm(
246
+ None,
247
+ [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value()],
248
+ "cp.reduce.async.bulk.global.shared::cta.bulk_group.add.f32 [$0], [$1], $2;",
249
+ "l,r,r",
250
+ # [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value(), cache_hint.ir_value()],
251
+ # "cp.reduce.async.bulk.global.shared::cta.bulk_group.L2::cache_hint.add.f32 [$0], [$1], $2, $3;",
252
+ # "l,r,r,l",
253
+ has_side_effects=True,
254
+ is_align_stack=False,
255
+ asm_dialect=llvm.AsmDialect.AD_ATT,
256
+ )
257
+
258
+
259
+ def cpasync_bulk_get_copy_fn(
260
+ src_tensor: cute.Tensor,
261
+ dst_tensor: cute.Tensor,
262
+ single_stage: bool = False,
263
+ **kwargs,
264
+ ) -> Callable:
265
+ # src_is_smem = const_expr(
266
+ # isinstance(src_tensor.iterator, cute.Pointer)
267
+ # and src_tensor.memspace == cute.AddressSpace.smem
268
+ # )
269
+ group_rank_src = const_expr(cute.rank(src_tensor) - (1 if not single_stage else 0))
270
+ group_rank_dst = const_expr(cute.rank(dst_tensor) - (1 if not single_stage else 0))
271
+ # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
272
+ src = cute.group_modes(src_tensor, 0, group_rank_src)
273
+ dst = cute.group_modes(dst_tensor, 0, group_rank_dst)
274
+
275
+ def copy_bulk(src_idx, dst_idx, **new_kwargs):
276
+ size = const_expr(cute.size(src.shape[:-1]) * src.element_type.width // 8)
277
+ cpasync_bulk_g2s(
278
+ src[None, src_idx].iterator,
279
+ dst[None, dst_idx].iterator,
280
+ size=size,
281
+ **new_kwargs,
282
+ **kwargs
283
+ )
284
+
285
+ def copy_bulk_single_stage(**new_kwargs):
286
+ size = const_expr(cute.size(src.shape) * src.element_type.width // 8)
287
+ cpasync_bulk_g2s(src.iterator, dst.iterator, size=size, **new_kwargs, **kwargs)
288
+
289
+ return copy_bulk if const_expr(not single_stage) else copy_bulk_single_stage
290
+
291
+
292
+ def tma_get_copy_fn(
293
+ atom: cute.CopyAtom,
294
+ cta_coord: cute.Coord,
295
+ cta_layout: cute.Layout,
296
+ src_tensor: cute.Tensor,
297
+ dst_tensor: cute.Tensor,
298
+ filter_zeros: bool = False,
299
+ single_stage: bool = False,
300
+ **kwargs,
301
+ ) -> Callable:
302
+ src_is_smem = const_expr(
303
+ isinstance(src_tensor.iterator, cute.Pointer)
304
+ and src_tensor.memspace == cute.AddressSpace.smem
305
+ )
306
+ smem_tensor, gmem_tensor = (src_tensor, dst_tensor) if src_is_smem else (dst_tensor, src_tensor)
307
+ group_rank_smem = const_expr(cute.rank(smem_tensor) - (1 if not single_stage else 0))
308
+ group_rank_gmem = const_expr(cute.rank(gmem_tensor) - (1 if not single_stage else 0))
309
+ # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
310
+ s, g = cpasync.tma_partition(
311
+ atom,
312
+ cta_coord,
313
+ cta_layout,
314
+ cute.group_modes(smem_tensor, 0, group_rank_smem),
315
+ cute.group_modes(gmem_tensor, 0, group_rank_gmem),
316
+ )
317
+ if const_expr(filter_zeros):
318
+ s = cute.filter_zeros(s)
319
+ g = cute.filter_zeros(g)
320
+ src, dst = (s, g) if src_is_smem else (g, s)
321
+
322
+ def copy_tma(src_idx, dst_idx, **new_kwargs):
323
+ cute.copy(atom, src[None, src_idx], dst[None, dst_idx], **new_kwargs, **kwargs)
324
+
325
+ def copy_tma_single_stage(**new_kwargs):
326
+ cute.copy(atom, src, dst, **new_kwargs, **kwargs)
327
+
328
+ return (copy_tma if const_expr(not single_stage) else copy_tma_single_stage), s, g
329
+
330
+
331
+ def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsync):
332
+ def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
333
+ copy(
334
+ src_idx=src_idx,
335
+ dst_idx=producer_state.index,
336
+ tma_bar_ptr=pipeline.producer_get_barrier(producer_state),
337
+ **new_kwargs,
338
+ )
339
+
340
+ return copy_fn
build/torch-universal/cute_dsl_utils.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import os
4
+ import pathlib
5
+ from typing import Tuple
6
+ from functools import partial, lru_cache
7
+ from dataclasses import dataclass, fields
8
+
9
+ import torch
10
+
11
+ try:
12
+ from triton.tools.disasm import extract
13
+ except ImportError:
14
+ extract = None
15
+
16
+ import cutlass
17
+ import cutlass.cute as cute
18
+ from cutlass.base_dsl.typing import JitArgument
19
+ from cutlass.cutlass_dsl import NumericMeta
20
+
21
+
22
+ StaticTypes = (cutlass.Constexpr, NumericMeta, int, bool, str, float, type(None))
23
+
24
+
25
+ load_cubin_module_data_og = cutlass.base_dsl.runtime.cuda.load_cubin_module_data
26
+ cute_compile_og = cute.compile
27
+
28
+
29
+ torch2cute_dtype_map = {
30
+ torch.float16: cutlass.Float16,
31
+ torch.bfloat16: cutlass.BFloat16,
32
+ torch.float32: cutlass.Float32,
33
+ }
34
+
35
+
36
+ @lru_cache
37
+ def get_max_active_clusters(cluster_size):
38
+ return cutlass.utils.HardwareInfo().get_max_active_clusters(cluster_size=cluster_size)
39
+
40
+
41
+ @lru_cache
42
+ def get_device_capacity(device: torch.device = None) -> Tuple[int, int]:
43
+ return torch.cuda.get_device_capability(device)
44
+
45
+
46
+ @dataclass
47
+ class ParamsBase:
48
+ def __extract_mlir_values__(self):
49
+ all_fields = [getattr(self, field.name) for field in fields(self)]
50
+ non_constexpr_fields = [f for f in all_fields if not isinstance(f, StaticTypes)]
51
+ values, self._values_pos = [], []
52
+ for obj in non_constexpr_fields:
53
+ obj_values = cutlass.extract_mlir_values(obj)
54
+ values += obj_values
55
+ self._values_pos.append(len(obj_values))
56
+ return values
57
+
58
+ def __new_from_mlir_values__(self, values):
59
+ all_fields = {field.name: getattr(self, field.name) for field in fields(self)}
60
+ constexpr_fields = {n: f for n, f in all_fields.items() if isinstance(f, StaticTypes)}
61
+ non_constexpr_fields = {
62
+ n: f for n, f in all_fields.items() if not isinstance(f, StaticTypes)
63
+ }
64
+ for (name, field), n_items in zip(non_constexpr_fields.items(), self._values_pos):
65
+ non_constexpr_fields[name] = cutlass.new_from_mlir_values(field, values[:n_items])
66
+ values = values[n_items:]
67
+ return self.__class__(**non_constexpr_fields, **constexpr_fields)
68
+
69
+
70
+ @dataclass
71
+ class ArgumentsBase(JitArgument):
72
+ def __c_pointers__(self):
73
+ all_fields = [getattr(self, field.name) for field in fields(self)]
74
+ non_constexpr_fields = [f for f in all_fields if not isinstance(f, StaticTypes)]
75
+ c_ptrs = []
76
+ for obj in non_constexpr_fields:
77
+ if hasattr(obj, "__c_pointers__"):
78
+ c_ptrs.extend(obj.__c_pointers__())
79
+ return c_ptrs
80
+
81
+ def __get_mlir_types__(self):
82
+ all_fields = [getattr(self, field.name) for field in fields(self)]
83
+ non_constexpr_fields = [f for f in all_fields if not isinstance(f, StaticTypes)]
84
+ types, self._values_pos = [], []
85
+ for obj in non_constexpr_fields:
86
+ if hasattr(obj, "__get_mlir_types__"):
87
+ obj_types = obj.__get_mlir_types__()
88
+ types.extend(obj_types)
89
+ self._values_pos.append(len(obj_types))
90
+ else:
91
+ self._values_pos.append(0)
92
+ return types
93
+
94
+ def __new_from_mlir_values__(self, values):
95
+ all_fields = {field.name: getattr(self, field.name) for field in fields(self)}
96
+ constexpr_fields = {n: f for n, f in all_fields.items() if isinstance(f, StaticTypes)}
97
+ non_constexpr_fields = {
98
+ n: f for n, f in all_fields.items() if not isinstance(f, StaticTypes)
99
+ }
100
+ for (name, field), n_items in zip(non_constexpr_fields.items(), self._values_pos):
101
+ non_constexpr_fields[name] = cutlass.new_from_mlir_values(field, values[:n_items])
102
+ values = values[n_items:]
103
+ return self.__class__(**non_constexpr_fields, **constexpr_fields)
104
+
105
+
106
+ def load_cubin_module_data_patched(cubin_data, filepath):
107
+ pathlib.Path(filepath).write_bytes(cubin_data)
108
+ return load_cubin_module_data_og(cubin_data)
109
+
110
+
111
+ def cute_compile_patched(*args, **kwargs):
112
+ """A patched version of cute.compile that dump the SASS to a file if CUTE_CUBIN_PATH is set."""
113
+ cubin_path = os.getenv("CUTE_CUBIN_PATH", None)
114
+ if cubin_path is not None:
115
+ cutlass.base_dsl.runtime.cuda.load_cubin_module_data = partial(
116
+ load_cubin_module_data_patched, filepath=cubin_path
117
+ )
118
+ output = cute_compile_og(*args, **kwargs)
119
+ if cubin_path is not None:
120
+ cutlass.base_dsl.runtime.cuda.load_cubin_module_data = load_cubin_module_data_og
121
+ if extract is not None:
122
+ sass = extract(cubin_path, None)
123
+ pathlib.Path(cubin_path).with_suffix(".annotated.sass").write_text(sass)
124
+ return output
build/torch-universal/fast_math.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Tuple
4
+
5
+ import cutlass
6
+ import cutlass.cute as cute
7
+ from cutlass import Int32, Uint32
8
+ from cutlass.cutlass_dsl import T, dsl_user_op
9
+ from cutlass._mlir.dialects import llvm
10
+
11
+
12
+ @cute.jit
13
+ def clz(x: Int32) -> Int32:
14
+ # for i in cutlass.range_constexpr(32):
15
+ # if (1 << (31 - i)) & x:
16
+ # return Int32(i)
17
+ # return Int32(32)
18
+ # Early exit is not supported yet
19
+ res = Int32(32)
20
+ done = False
21
+ for i in cutlass.range(32):
22
+ if ((1 << (31 - i)) & x) and not done:
23
+ res = Int32(i)
24
+ done = True
25
+ return res
26
+
27
+
28
+ def find_log2(x: Int32) -> Int32:
29
+ a: Int32 = Int32(31 - clz(x))
30
+ return a + ((x & (x - 1)) != 0) # Round up, add 1 if not a power of 2.
31
+
32
+
33
+ @dsl_user_op
34
+ def umulhi(a: Int32, b: Int32, *, loc=None, ip=None) -> Uint32:
35
+ return Uint32(
36
+ llvm.inline_asm(
37
+ T.i32(),
38
+ [Int32(a).ir_value(loc=loc, ip=ip), Int32(b).ir_value(loc=loc, ip=ip)],
39
+ "mul.hi.u32 $0, $1, $2;",
40
+ "=r,r,r",
41
+ has_side_effects=False,
42
+ is_align_stack=False,
43
+ asm_dialect=llvm.AsmDialect.AD_ATT,
44
+ )
45
+ )
46
+
47
+
48
+ class FastDivmod:
49
+ def __init__(
50
+ self, divisor: Int32, multipler: Uint32, shift_right: Uint32, *, loc=None, ip=None
51
+ ):
52
+ self.divisor = divisor
53
+ self.multiplier = multipler
54
+ self.shift_right = shift_right
55
+ self._loc = loc
56
+
57
+ # called by host
58
+ @staticmethod
59
+ def create(divisor: Int32, *, loc=None, ip=None) -> "FastDivmod":
60
+ """Construct the FastDivmod object, in host code.
61
+ This precomputes some values based on the divisor and is computationally expensive.
62
+ """
63
+ p = Uint32(31 + find_log2(divisor))
64
+ divisor_u32 = Uint32(divisor)
65
+ multiplier = Uint32(((cutlass.Uint64(1) << p) + divisor_u32 - 1) // divisor_u32)
66
+ shift_right = Uint32(p - 32)
67
+ return FastDivmod(divisor, multiplier, shift_right, loc=loc, ip=ip)
68
+
69
+ @cute.jit
70
+ def div(self, dividend: Int32) -> Int32:
71
+ return (
72
+ Int32(umulhi(dividend, self.multiplier) >> self.shift_right)
73
+ if self.divisor != 1
74
+ else dividend
75
+ )
76
+
77
+ def divmod(self, dividend: Int32) -> Tuple[Int32, Int32]:
78
+ quotient = self.div(dividend)
79
+ remainder = dividend - quotient * self.divisor
80
+ return quotient, remainder
81
+
82
+ def __extract_mlir_values__(self):
83
+ values, self._values_pos = [], []
84
+ for obj in [self.divisor, self.multiplier, self.shift_right]:
85
+ obj_values = cutlass.extract_mlir_values(obj)
86
+ values += obj_values
87
+ self._values_pos.append(len(obj_values))
88
+ return values
89
+
90
+ def __new_from_mlir_values__(self, values):
91
+ obj_list = []
92
+ for obj, n_items in zip(
93
+ [self.divisor, self.multiplier, self.shift_right], self._values_pos
94
+ ):
95
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
96
+ values = values[n_items:]
97
+ return FastDivmod(*(tuple(obj_list)), loc=self._loc)
build/torch-universal/flash_attn_cute/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ctypes
2
+ import sys
3
+
4
+ import importlib
5
+ from pathlib import Path
6
+ from types import ModuleType
7
+
8
+ def _import_from_path(file_path: Path) -> ModuleType:
9
+ # We cannot use the module name as-is, after adding it to `sys.modules`,
10
+ # it would also be used for other imports. So, we make a module name that
11
+ # depends on the path for it to be unique using the hex-encoded hash of
12
+ # the path.
13
+ path_hash = "{:x}".format(ctypes.c_size_t(hash(file_path.absolute())).value)
14
+ module_name = path_hash
15
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
16
+ if spec is None:
17
+ raise ImportError(f"Cannot load spec for {module_name} from {file_path}")
18
+ module = importlib.util.module_from_spec(spec)
19
+ if module is None:
20
+ raise ImportError(f"Cannot load module {module_name} from spec")
21
+ sys.modules[module_name] = module
22
+ spec.loader.exec_module(module) # type: ignore
23
+ return module
24
+
25
+
26
+ globals().update(vars(_import_from_path(Path(__file__).parent.parent / "__init__.py")))
build/torch-universal/flash_bwd.py ADDED
@@ -0,0 +1,1261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/mainloop_bwd_sm80.hpp
3
+ # from Cutlass C++ to Cute-DSL.
4
+ import math
5
+ from types import SimpleNamespace
6
+ from typing import Type, Callable, Optional
7
+ from functools import partial
8
+
9
+ import cuda.bindings.driver as cuda
10
+
11
+ import cutlass
12
+ import cutlass.cute as cute
13
+ from cutlass.cute.nvgpu import cpasync, warp
14
+ from cutlass import Float32, Int32
15
+ import cutlass.utils as utils_basic
16
+
17
+ from . import ampere_helpers as sm80_utils
18
+ from . import utils
19
+ from .mask import AttentionMask
20
+ from .seqlen_info import SeqlenInfoQK
21
+ from .tile_scheduler import ParamsBase, SingleTileScheduler, SingleTileVarlenScheduler, TileSchedulerArguments
22
+
23
+
24
+ class FlashAttentionBackwardSm80:
25
+ def __init__(
26
+ self,
27
+ dtype: Type[cutlass.Numeric],
28
+ head_dim: int,
29
+ head_dim_v: Optional[int] = None,
30
+ qhead_per_kvhead: int = 1,
31
+ m_block_size: int = 64,
32
+ n_block_size: int = 128,
33
+ num_stages_Q: int = 2,
34
+ num_stages_dO: int = 2,
35
+ num_threads: int = 256,
36
+ pack_gqa: bool = False,
37
+ is_causal: bool = False,
38
+ SdP_swapAB: bool = False,
39
+ dKV_swapAB: bool = False,
40
+ dQ_swapAB: bool = False,
41
+ AtomLayoutMSdP: int = 1,
42
+ AtomLayoutNdKV: int = 8,
43
+ AtomLayoutMdQ: int = 1,
44
+ V_in_regs: bool = False,
45
+ ):
46
+ """Initializes the configuration for a flash attention v2 kernel.
47
+
48
+ All contiguous dimensions must be at least 16 bytes aligned which indicates the head dimension
49
+ should be a multiple of 8.
50
+
51
+ :param head_dim: head dimension
52
+ :type head_dim: int
53
+ :param m_block_size: m block size
54
+ :type m_block_size: int
55
+ :param n_block_size: n block size
56
+ :type n_block_size: int
57
+ :param num_threads: number of threads
58
+ :type num_threads: int
59
+ :param is_causal: is causal
60
+ """
61
+ self.dtype = dtype
62
+ # padding head_dim to a multiple of 16 as k_block_size
63
+ hdim_multiple_of = 32
64
+ self.head_dim_padded = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
65
+ head_dim_v = head_dim_v if head_dim_v is not None else head_dim
66
+ self.same_hdim_kv = head_dim == head_dim_v
67
+ self.head_dim_v_padded = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
68
+ # Can save registers (and hence be faster) if we don't have to check hdim predication
69
+ self.check_hdim_oob = head_dim != self.head_dim_padded
70
+ self.check_hdim_v_oob = head_dim_v != self.head_dim_v_padded
71
+ self.qhead_per_kvhead = qhead_per_kvhead
72
+ self.m_block_size = m_block_size
73
+ self.n_block_size = n_block_size
74
+ self.num_threads = num_threads
75
+ self.pack_gqa = pack_gqa
76
+ self.is_causal = is_causal
77
+ self.num_stages_Q = num_stages_Q
78
+ self.num_stages_dO = num_stages_dO
79
+ self.SdP_swapAB = SdP_swapAB
80
+ self.dKV_swapAB = dKV_swapAB
81
+ self.dQ_swapAB = dQ_swapAB
82
+ self.AtomLayoutMSdP = AtomLayoutMSdP
83
+ self.AtomLayoutNdKV = AtomLayoutNdKV
84
+ self.AtomLayoutMdQ = AtomLayoutMdQ
85
+ num_mma_warps = self.num_threads // cute.arch.WARP_SIZE
86
+ self.Mma_dKV_is_RS = AtomLayoutMSdP == 1 and AtomLayoutNdKV == num_mma_warps and SdP_swapAB and not dKV_swapAB
87
+ self.V_in_regs = V_in_regs
88
+ self.share_QV_smem = V_in_regs
89
+
90
+ @staticmethod
91
+ def can_implement(
92
+ dtype, head_dim, head_dim_v, m_block_size, n_block_size, num_stages_Q, num_stages_dO,
93
+ num_threads, is_causal,
94
+ V_in_regs=False
95
+ ) -> bool:
96
+ """Check if the kernel can be implemented with the given parameters.
97
+
98
+ :param dtype: data type
99
+ :type dtype: cutlass.Numeric
100
+ :param head_dim: head dimension
101
+ :type head_dim: int
102
+ :param m_block_size: m block size
103
+ :type m_block_size: int
104
+ :param n_block_size: n block size
105
+ :type n_block_size: int
106
+ :param num_threads: number of threads
107
+ :type num_threads: int
108
+ :param is_causal: is causal
109
+ :type is_causal: bool
110
+
111
+ :return: True if the kernel can be implemented, False otherwise
112
+ :rtype: bool
113
+ """
114
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
115
+ return False
116
+ if head_dim % 8 != 0:
117
+ return False
118
+ if head_dim_v % 8 != 0:
119
+ return False
120
+ if n_block_size % 16 != 0:
121
+ return False
122
+ if num_threads % 32 != 0:
123
+ return False
124
+ # Check if block size setting is out of shared memory capacity
125
+ # Shared memory usage: Q tile + (K tile + V tile) where K and V use the same tile size
126
+ smem_usage_Q = m_block_size * head_dim * num_stages_Q * 2
127
+ smem_usage_dO = m_block_size * head_dim_v * num_stages_dO * 2
128
+ smem_usage_K = n_block_size * head_dim * 2
129
+ smem_usage_V = n_block_size * head_dim_v * 2
130
+ smem_usage_QV = (smem_usage_Q + smem_usage_V) if not V_in_regs else max(smem_usage_Q, smem_usage_V)
131
+ smem_usage = smem_usage_QV + smem_usage_dO + smem_usage_K
132
+ smem_capacity = utils_basic.get_smem_capacity_in_bytes("sm_80")
133
+ if smem_usage > smem_capacity:
134
+ return False
135
+ return True
136
+
137
+ def _check_type(
138
+ self,
139
+ mQ_type: Type[cutlass.Numeric],
140
+ mK_type: Type[cutlass.Numeric],
141
+ mV_type: Type[cutlass.Numeric],
142
+ mdO_type: Type[cutlass.Numeric],
143
+ mLSE_type: Type[cutlass.Numeric],
144
+ mdPsum_type: Type[cutlass.Numeric],
145
+ mdQaccum_type: Type[cutlass.Numeric],
146
+ mdK_type: Type[cutlass.Numeric],
147
+ mdV_type: Type[cutlass.Numeric],
148
+ mCuSeqlensQ_type: Type[cutlass.Numeric] | None,
149
+ mCuSeqlensK_type: Type[cutlass.Numeric] | None,
150
+ mSeqUsedQ_type: Type[cutlass.Numeric] | None,
151
+ mSeqUsedK_type: Type[cutlass.Numeric] | None,
152
+ ):
153
+ if cutlass.const_expr(not (mQ_type == mK_type == mV_type == mdO_type)):
154
+ raise TypeError("All tensors must have the same data type")
155
+ if cutlass.const_expr(self.qhead_per_kvhead == 1):
156
+ if cutlass.const_expr(not (mdK_type == mdV_type == mQ_type)):
157
+ raise TypeError("mdK and mdV tensors must have the same data type as mQ")
158
+ else:
159
+ if cutlass.const_expr(not (mdK_type == mdV_type == cutlass.Float32)):
160
+ raise TypeError("mdKaccum and mdVaccum tensors must have the data type Float32")
161
+ if cutlass.const_expr(not mQ_type in [cutlass.Float16, cutlass.BFloat16]):
162
+ raise TypeError("Only Float16 or BFloat16 is supported")
163
+ if cutlass.const_expr(not mLSE_type in [cutlass.Float32]):
164
+ raise TypeError("LSE tensor must be Float32")
165
+ if cutlass.const_expr(not mdPsum_type in [cutlass.Float32]):
166
+ raise TypeError("dPsum tensor must be Float32")
167
+ if cutlass.const_expr(not mdQaccum_type in [cutlass.Float32]):
168
+ raise TypeError("dQaccum tensor must be Float32")
169
+ if cutlass.const_expr(mCuSeqlensQ_type not in [None, cutlass.Int32]):
170
+ raise TypeError("cuSeqlensQ tensor must be Int32")
171
+ if cutlass.const_expr(mCuSeqlensK_type not in [None, cutlass.Int32]):
172
+ raise TypeError("cuSeqlensK tensor must be Int32")
173
+ if cutlass.const_expr(mSeqUsedQ_type not in [None, cutlass.Int32]):
174
+ raise TypeError("SeqUsedQ tensor must be Int32")
175
+ if cutlass.const_expr(mSeqUsedK_type not in [None, cutlass.Int32]):
176
+ raise TypeError("SeqUsedK tensor must be Int32")
177
+ assert mQ_type == self.dtype
178
+
179
+ def _setup_attributes(self):
180
+ # ///////////////////////////////////////////////////////////////////////////////
181
+ # Shared memory layout: Q/K/V
182
+ # ///////////////////////////////////////////////////////////////////////////////
183
+ sQ_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.head_dim_padded)
184
+ self.sQ_layout = cute.tile_to_shape(
185
+ sQ_layout_atom, (self.m_block_size, self.head_dim_padded, self.num_stages_Q), (0, 1, 2),
186
+ )
187
+ sK_layout_atom = sQ_layout_atom
188
+ self.sK_layout = cute.tile_to_shape(
189
+ sK_layout_atom, (self.n_block_size, self.head_dim_padded), (0, 1),
190
+ )
191
+ sV_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.head_dim_v_padded)
192
+ self.sV_layout = cute.tile_to_shape(
193
+ sV_layout_atom, (self.n_block_size, self.head_dim_v_padded), (0, 1),
194
+ )
195
+ sdO_layout_atom = sV_layout_atom
196
+ self.sdO_layout = cute.tile_to_shape(
197
+ sdO_layout_atom, (self.m_block_size, self.head_dim_v_padded, self.num_stages_dO), (0, 1, 2),
198
+ )
199
+ # TODO: do we set swizzle to be 3 here explicitly?
200
+ sPdS_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.n_block_size)
201
+ self.sPdS_layout = cute.tile_to_shape(
202
+ sPdS_layout_atom, (self.m_block_size, self.n_block_size), (0, 1),
203
+ )
204
+ # We set stride to be multiple of 64 so that if ShuffleLSE, even if threads read from sLSE but out of bounds,
205
+ # it's still a valid smem address.
206
+ self.sLSE_layout = cute.make_layout(
207
+ (self.m_block_size, self.num_stages_Q),
208
+ stride=(1, cute.round_up(self.m_block_size, 64)),
209
+ )
210
+ sLSEMma_layout = cute.make_layout(
211
+ (self.m_block_size, self.n_block_size, self.num_stages_Q),
212
+ stride=(1, 0, cute.round_up(self.m_block_size, 64)),
213
+ )
214
+ sLSEMma_layout_transposed = cute.make_layout(
215
+ (self.n_block_size, self.m_block_size, self.num_stages_Q),
216
+ stride=(0, 1, cute.round_up(self.m_block_size, 64)),
217
+ )
218
+ self.sLSEMma_layout = sLSEMma_layout if not self.SdP_swapAB else sLSEMma_layout_transposed
219
+
220
+ # ///////////////////////////////////////////////////////////////////////////////
221
+ # GMEM Tiled copy:
222
+ # ///////////////////////////////////////////////////////////////////////////////
223
+ # Thread layouts for copies
224
+ universal_copy_bits = 128
225
+ async_copy_elems = universal_copy_bits // self.dtype.width
226
+ # atom_async_copy: async copy atom for QKV load
227
+ atom_async_copy = cute.make_copy_atom(
228
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
229
+ self.dtype,
230
+ num_bits_per_copy=universal_copy_bits,
231
+ )
232
+ # atom_universal_copy: universal copy atom for O store
233
+ atom_universal_copy = cute.make_copy_atom(
234
+ cute.nvgpu.CopyUniversalOp(), self.dtype, num_bits_per_copy=universal_copy_bits,
235
+ )
236
+ # tQK_layout: thread layout for QK load
237
+ tQK_shape_dim_1 = sQ_layout_atom.outer.shape[1] // async_copy_elems
238
+ assert self.num_threads % tQK_shape_dim_1 == 0, "num_threads must be divisible by tQK_shape_dim_1"
239
+ tQK_layout = cute.make_ordered_layout(
240
+ (self.num_threads // tQK_shape_dim_1, tQK_shape_dim_1), order=(1, 0),
241
+ )
242
+ # Do we need to check if we overshot kBlockM when we load Q?
243
+ self.is_even_m_smem_q = self.m_block_size % tQK_layout.shape[0] == 0
244
+ # Do we need to check if we overshot kBlockN when we load K?
245
+ self.is_even_n_smem_k = self.n_block_size % tQK_layout.shape[0] == 0
246
+ tVdO_shape_dim_1 = sV_layout_atom.outer.shape[1] // async_copy_elems
247
+ assert self.num_threads % tVdO_shape_dim_1 == 0, "num_threads must be divisible by tVdO_shape_dim_1"
248
+ tVdO_layout = cute.make_ordered_layout(
249
+ (self.num_threads // tVdO_shape_dim_1, tVdO_shape_dim_1), order=(1, 0),
250
+ )
251
+ # Do we need to check if we overshot kBlockN when we load V?
252
+ self.is_even_n_smem_v = self.n_block_size % tVdO_layout.shape[0] == 0
253
+ self.is_even_m_smem_do = self.m_block_size % tVdO_layout.shape[0] == 0
254
+
255
+ # Value layouts for copies
256
+ vQKVdO_layout = cute.make_layout((1, async_copy_elems))
257
+
258
+ # gmem_tiled_copy_QK: tiled copy for QK load
259
+ self.gmem_tiled_copy_QK = cute.make_tiled_copy_tv(atom_async_copy, tQK_layout, vQKVdO_layout)
260
+ self.gmem_tiled_copy_VdO = cute.make_tiled_copy_tv(atom_async_copy, tVdO_layout, vQKVdO_layout)
261
+ self.gmem_tiled_copy_dK = cute.make_tiled_copy_tv(atom_universal_copy, tQK_layout, vQKVdO_layout)
262
+ self.gmem_tiled_copy_dV = cute.make_tiled_copy_tv(atom_universal_copy, tVdO_layout, vQKVdO_layout)
263
+ async_copy_elems_accum = universal_copy_bits // cutlass.Float32.width
264
+
265
+ # I think we wouldn't require this with smarter padding
266
+ if cutlass.const_expr(not self.varlen_q):
267
+ async_copy_elems_accum = universal_copy_bits // cutlass.Float32.width
268
+ atom_async_copy_accum = cute.make_copy_atom(
269
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
270
+ cutlass.Float32,
271
+ num_bits_per_copy=universal_copy_bits,
272
+ )
273
+ else:
274
+ async_copy_elems_accum = 1
275
+ atom_async_copy_accum = cute.make_copy_atom(
276
+ cute.nvgpu.CopyUniversalOp(),
277
+ cutlass.Float32,
278
+ num_bits_per_copy=cutlass.Float32.width,
279
+ )
280
+ self.gmem_tiled_copy_LSE = cute.make_tiled_copy_tv(
281
+ atom_async_copy_accum,
282
+ cute.make_layout(self.num_threads),
283
+ cute.make_layout(async_copy_elems_accum),
284
+ )
285
+ self.gmem_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
286
+ cute.make_copy_atom(
287
+ cute.nvgpu.CopyUniversalOp(), cutlass.Float32, num_bits_per_copy=cutlass.Float32.width
288
+ ),
289
+ cute.make_layout(self.num_threads),
290
+ cute.make_layout(1)
291
+ )
292
+ if cutlass.const_expr(self.qhead_per_kvhead > 1):
293
+ self.gmem_tiled_copy_dK = self.gmem_tiled_copy_dQaccum
294
+ self.gmem_tiled_copy_dV = self.gmem_tiled_copy_dQaccum
295
+
296
+ def _get_tiled_mma(self):
297
+ num_mma_warps = self.num_threads // 32
298
+ AtomLayoutSdP = (self.AtomLayoutMSdP, num_mma_warps // self.AtomLayoutMSdP, 1) if cutlass.const_expr(not self.SdP_swapAB) else (num_mma_warps // self.AtomLayoutMSdP, self.AtomLayoutMSdP, 1)
299
+ tiled_mma_sdp = cute.make_tiled_mma(
300
+ warp.MmaF16BF16Op(self.dtype, cutlass.Float32, (16, 8, 16)),
301
+ AtomLayoutSdP,
302
+ permutation_mnk=(AtomLayoutSdP[0] * 16, AtomLayoutSdP[1] * 16, 16),
303
+ )
304
+ AtomLayoutdKV = (self.AtomLayoutNdKV, num_mma_warps // self.AtomLayoutNdKV, 1) if cutlass.const_expr(not self.dKV_swapAB) else (num_mma_warps // self.AtomLayoutNdKV, self.AtomLayoutNdKV, 1)
305
+ tiled_mma_dkv = cute.make_tiled_mma(
306
+ warp.MmaF16BF16Op(self.dtype, cutlass.Float32, (16, 8, 16)),
307
+ AtomLayoutdKV,
308
+ permutation_mnk=(AtomLayoutdKV[0] * 16, AtomLayoutdKV[1] * 16, 16),
309
+ )
310
+ AtomLayoutdQ = (self.AtomLayoutMdQ, num_mma_warps // self.AtomLayoutMdQ, 1) if cutlass.const_expr(not self.dQ_swapAB) else (num_mma_warps // self.AtomLayoutMdQ, self.AtomLayoutMdQ, 1)
311
+ tiled_mma_dq = cute.make_tiled_mma(
312
+ warp.MmaF16BF16Op(self.dtype, cutlass.Float32, (16, 8, 16)),
313
+ AtomLayoutdQ,
314
+ permutation_mnk=(AtomLayoutdQ[0] * 16, AtomLayoutdQ[1] * 16, 16),
315
+ )
316
+ return tiled_mma_sdp, tiled_mma_dkv, tiled_mma_dq
317
+
318
+ def _get_shared_storage_cls(self):
319
+ sQ_struct, sK_struct, sV_struct, sdO_struct = [
320
+ cute.struct.Align[cute.struct.MemRange[self.dtype, cute.cosize(layout)], 1024]
321
+ for layout in (self.sQ_layout, self.sK_layout, self.sV_layout, self.sdO_layout)
322
+ ]
323
+ cosize_sQV = max(cute.cosize(self.sQ_layout), cute.cosize(self.sV_layout))
324
+ sQV_struct = cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sQV], 1024]
325
+ sLSE_struct, sdPsum_struct = [
326
+ cute.struct.Align[cute.struct.MemRange[cutlass.Float32, cute.cosize(layout)], 128]
327
+ for layout in (self.sLSE_layout, self.sLSE_layout)
328
+ ]
329
+ sP_struct, sdS_struct = [
330
+ cute.struct.Align[cute.struct.MemRange[self.dtype, cute.cosize(layout)], 128]
331
+ for layout in (self.sPdS_layout, self.sPdS_layout)
332
+ ]
333
+
334
+ @cute.struct
335
+ class SharedStorageSeparateQV:
336
+ sK: sK_struct
337
+ sV: sV_struct
338
+ sQ: sQ_struct
339
+ sdO: sdO_struct
340
+ sLSE: sLSE_struct
341
+ sdPsum: sdPsum_struct
342
+ sP: sP_struct
343
+ sdS: sdS_struct
344
+ # TODO: the case where there's no sP
345
+
346
+ @cute.struct
347
+ class SharedStorageSharedQV:
348
+ sK: sK_struct
349
+ sV: sV_struct
350
+ sQ: sQV_struct
351
+ sdO: sdO_struct
352
+ sLSE: sLSE_struct
353
+ sdPsum: sdPsum_struct
354
+ sP: sP_struct
355
+ sdS: sdS_struct
356
+
357
+ return SharedStorageSeparateQV if cutlass.const_expr(not self.share_QV_smem) else SharedStorageSharedQV
358
+
359
+ @cute.jit
360
+ def __call__(
361
+ self,
362
+ mQ: cute.Tensor,
363
+ mK: cute.Tensor,
364
+ mV: cute.Tensor,
365
+ mdO: cute.Tensor,
366
+ mLSE: cute.Tensor,
367
+ mdPsum: cute.Tensor,
368
+ mdQaccum: cute.Tensor,
369
+ mdK: cute.Tensor,
370
+ mdV: cute.Tensor,
371
+ softmax_scale: cutlass.Float32,
372
+ stream: cuda.CUstream,
373
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
374
+ mCuSeqlensK: Optional[cute.Tensor] = None,
375
+ mSeqUsedQ: Optional[cute.Tensor] = None,
376
+ mSeqUsedK: Optional[cute.Tensor] = None,
377
+ softcap: Float32 | float | None = None,
378
+ window_size_left: Int32 | int | None = None,
379
+ window_size_right: Int32 | int | None = None,
380
+ mdQ_semaphore: Optional[cute.Tensor] = None,
381
+ ):
382
+ assert mdQ_semaphore is None, "semaphore not supported yet"
383
+ # Get the data type and check if it is fp16 or bf16
384
+ self._check_type(*(t.element_type if t is not None else None
385
+ for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV, mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK)))
386
+ # Assume all strides are divisible by 128 bits except the last stride
387
+ new_stride = lambda t: (*(cute.assume(s, divby=128 // t.element_type.width) for s in t.stride[:-1]), t.stride[-1])
388
+ mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV = [cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t))) if t is not None else None for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV)]
389
+ self.varlen_q = (mCuSeqlensQ is not None)
390
+ self._setup_attributes()
391
+ SharedStorage = self._get_shared_storage_cls()
392
+ tiled_mma_sdp, tiled_mma_dkv, tiled_mma_dq = self._get_tiled_mma()
393
+
394
+ num_head = mQ.shape[1] if cutlass.const_expr(mCuSeqlensQ is not None) else mQ.shape[2]
395
+
396
+ if cutlass.const_expr(mCuSeqlensK is not None):
397
+ TileScheduler = SingleTileVarlenScheduler
398
+ num_batch = mCuSeqlensK.shape[0] - 1
399
+ else:
400
+ TileScheduler = SingleTileScheduler
401
+ num_batch = mK.shape[0]
402
+
403
+ # Uses seqlen k, etc. since main bwd kernel's blocks are over n
404
+ tile_sched_args = TileSchedulerArguments(
405
+ num_block=cute.ceil_div(mK.shape[1], self.n_block_size),
406
+ num_head=num_head,
407
+ num_batch=num_batch,
408
+ num_splits=1,
409
+ seqlen_k=0,
410
+ headdim=mK.shape[2],
411
+ headdim_v=mV.shape[2],
412
+ total_q=mK.shape[0],
413
+ tile_shape_mn=(self.n_block_size, self.m_block_size),
414
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if cutlass.const_expr(self.pack_gqa) else 1,
415
+ mCuSeqlensQ=mCuSeqlensK,
416
+ mSeqUsedQ=mSeqUsedK,
417
+ )
418
+
419
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
420
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
421
+
422
+ softmax_scale_log2 = softmax_scale * math.log2(math.e)
423
+ self.kernel(
424
+ mQ,
425
+ mK,
426
+ mV,
427
+ mdO,
428
+ mLSE,
429
+ mdPsum,
430
+ mdQaccum,
431
+ mdK,
432
+ mdV,
433
+ mCuSeqlensQ,
434
+ mCuSeqlensK,
435
+ mSeqUsedQ,
436
+ mSeqUsedK,
437
+ softmax_scale,
438
+ softmax_scale_log2,
439
+ self.sQ_layout,
440
+ self.sK_layout,
441
+ self.sV_layout,
442
+ self.sdO_layout,
443
+ self.sPdS_layout,
444
+ self.sLSE_layout,
445
+ self.sLSEMma_layout,
446
+ self.gmem_tiled_copy_QK,
447
+ self.gmem_tiled_copy_VdO,
448
+ self.gmem_tiled_copy_dK,
449
+ self.gmem_tiled_copy_dV,
450
+ self.gmem_tiled_copy_LSE,
451
+ self.gmem_tiled_copy_dQaccum,
452
+ tiled_mma_sdp,
453
+ tiled_mma_dkv,
454
+ tiled_mma_dq,
455
+ SharedStorage,
456
+ tile_sched_params,
457
+ TileScheduler,
458
+ ).launch(
459
+ grid=grid_dim,
460
+ block=[self.num_threads, 1, 1],
461
+ smem=SharedStorage.size_in_bytes(),
462
+ stream=stream,
463
+ )
464
+
465
+ @cute.kernel
466
+ def kernel(
467
+ self,
468
+ mQ: cute.Tensor,
469
+ mK: cute.Tensor,
470
+ mV: cute.Tensor,
471
+ mdO: cute.Tensor,
472
+ mLSE: cute.Tensor,
473
+ mdPsum: cute.Tensor,
474
+ mdQaccum: cute.Tensor,
475
+ mdK: cute.Tensor,
476
+ mdV: cute.Tensor,
477
+ mCuSeqlensQ: Optional[cute.Tensor],
478
+ mCuSeqlensK: Optional[cute.Tensor],
479
+ mSeqUsedQ: Optional[cute.Tensor],
480
+ mSeqUsedK: Optional[cute.Tensor],
481
+ softmax_scale: cutlass.Float32,
482
+ softmax_scale_log2: cutlass.Float32,
483
+ sQ_layout: cute.ComposedLayout,
484
+ sK_layout: cute.ComposedLayout,
485
+ sV_layout: cute.ComposedLayout,
486
+ sdO_layout: cute.ComposedLayout,
487
+ sPdS_layout: cute.ComposedLayout,
488
+ sLSE_layout: cute.Layout,
489
+ sLSEMma_layout: cute.Layout,
490
+ gmem_tiled_copy_QK: cute.TiledCopy,
491
+ gmem_tiled_copy_VdO: cute.TiledCopy,
492
+ gmem_tiled_copy_dK: cute.TiledCopy,
493
+ gmem_tiled_copy_dV: cute.TiledCopy,
494
+ gmem_tiled_copy_LSE: cute.TiledCopy,
495
+ gmem_tiled_copy_dQaccum: cute.TiledCopy,
496
+ tiled_mma_sdp: cute.TiledMma,
497
+ tiled_mma_dkv: cute.TiledMma,
498
+ tiled_mma_dq: cute.TiledMma,
499
+ SharedStorage: cutlass.Constexpr,
500
+ tile_sched_params: ParamsBase,
501
+ TileScheduler: cutlass.Constexpr[Callable],
502
+ ):
503
+ # Thread index, block index
504
+ tidx, _, _ = cute.arch.thread_idx()
505
+
506
+ tile_scheduler = TileScheduler.create(tile_sched_params)
507
+ work_tile = tile_scheduler.initial_work_tile_info()
508
+
509
+ n_block, head_idx, batch_idx, _ = work_tile.tile_idx
510
+
511
+ if work_tile.is_valid_tile:
512
+ seqlen = SeqlenInfoQK.create(batch_idx, mQ.shape[1], mK.shape[1], mCuSeqlensQ=mCuSeqlensQ, mCuSeqlensK=mCuSeqlensK, mSeqUsedQ=mSeqUsedQ, mSeqUsedK=mSeqUsedK)
513
+
514
+ m_block_max = cute.ceil_div(seqlen.seqlen_q, self.m_block_size)
515
+ m_block_min = 0
516
+ if cutlass.const_expr(self.is_causal):
517
+ m_block_min = max(
518
+ (n_block * self.n_block_size + seqlen.seqlen_q - seqlen.seqlen_k) // self.m_block_size,
519
+ m_block_min,
520
+ )
521
+ # TODO: return early if m_block_max == 0
522
+
523
+ # ///////////////////////////////////////////////////////////////////////////////
524
+ # Get the appropriate tiles for this thread block.
525
+ # ///////////////////////////////////////////////////////////////////////////////
526
+ blkQ_shape = (self.m_block_size, self.head_dim_padded)
527
+ blkK_shape = (self.n_block_size, self.head_dim_padded)
528
+ blkV_shape = (self.n_block_size, self.head_dim_v_padded)
529
+ blkdO_shape = (self.m_block_size, self.head_dim_v_padded)
530
+
531
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
532
+ mQ_cur = mQ[batch_idx, None, head_idx, None]
533
+ mLSE_cur = mLSE[batch_idx, head_idx, None]
534
+ mdO_cur = mdO[batch_idx, None, head_idx, None]
535
+ mdPsum_cur = mdPsum[batch_idx, head_idx, None]
536
+ mdQaccum_cur = mdQaccum[batch_idx, head_idx, None]
537
+ else:
538
+ padded_offset_q = seqlen.offset_q + batch_idx * self.m_block_size
539
+ mQ_cur = cute.domain_offset((seqlen.offset_q, 0), mQ[None, head_idx, None])
540
+ mLSE_cur = cute.domain_offset((padded_offset_q,), mLSE[head_idx, None])
541
+ mdO_cur = cute.domain_offset((seqlen.offset_q, 0), mdO[None, head_idx, None])
542
+ mdPsum_cur = cute.domain_offset((padded_offset_q,), mdPsum[head_idx, None])
543
+ mdQaccum_cur = cute.domain_offset((padded_offset_q * self.head_dim_padded,), mdQaccum[head_idx, None])
544
+ head_idx_kv = head_idx // self.qhead_per_kvhead if cutlass.const_expr(not self.pack_gqa) else head_idx
545
+
546
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_k):
547
+ mK_cur, mV_cur = [t[batch_idx, None, head_idx_kv, None] for t in (mK, mV)]
548
+ else:
549
+ mK_cur, mV_cur = [cute.domain_offset((seqlen.offset_k, 0), t[None, head_idx_kv, None]) for t in (mK, mV)]
550
+
551
+ # (m_block_size, head_dim, m_block)
552
+ gQ = cute.local_tile(mQ_cur, blkQ_shape, (None, 0))
553
+ # (n_block_size, head_dim)
554
+ gK = cute.local_tile(mK_cur, blkK_shape, (n_block, 0))
555
+ # (n_block_size, head_dim_v)
556
+ gV = cute.local_tile(mV_cur, blkV_shape, (n_block, 0))
557
+ # (m_block_size, head_dim_v, m_block)
558
+ gdO = cute.local_tile(mdO_cur, blkdO_shape, (None, 0))
559
+ gLSE = cute.local_tile(mLSE_cur, (self.m_block_size,), (None,))
560
+ gdPsum = cute.local_tile(mdPsum_cur, (self.m_block_size,), (None,))
561
+ gdQaccum = cute.local_tile(mdQaccum_cur, (self.m_block_size * self.head_dim_padded,), (None,))
562
+
563
+ # ///////////////////////////////////////////////////////////////////////////////
564
+ # Get shared memory buffer
565
+ # ///////////////////////////////////////////////////////////////////////////////
566
+ smem = cutlass.utils.SmemAllocator()
567
+ storage = smem.allocate(SharedStorage)
568
+ sQ = storage.sQ.get_tensor(sQ_layout)
569
+ sK = storage.sK.get_tensor(sK_layout)
570
+ if cutlass.const_expr(not self.share_QV_smem):
571
+ sV = storage.sV.get_tensor(sV_layout)
572
+ else:
573
+ sV = cute.make_tensor(cute.recast_ptr(sQ.iterator, dtype=self.dtype), sV_layout)
574
+ sdO = storage.sdO.get_tensor(sdO_layout)
575
+ sP = storage.sP.get_tensor(sPdS_layout)
576
+ sdS = storage.sdS.get_tensor(sPdS_layout)
577
+ sLSE = storage.sLSE.get_tensor(sLSE_layout)
578
+ sdPsum = storage.sdPsum.get_tensor(sLSE_layout)
579
+ sLSEMma = storage.sLSE.get_tensor(sLSEMma_layout)
580
+ sdPsumMma = storage.sdPsum.get_tensor(sLSEMma_layout)
581
+
582
+ # Transpose view of tensors for tiled mma
583
+ sQt, sdOt, sKt, sPt, sdSt = [utils.transpose_view(t) for t in (sQ, sdO, sK, sP, sdS)]
584
+
585
+ gmem_thr_copy_QK = gmem_tiled_copy_QK.get_slice(tidx)
586
+ gmem_thr_copy_VdO = gmem_tiled_copy_VdO.get_slice(tidx)
587
+ gmem_thr_copy_lse = gmem_tiled_copy_LSE.get_slice(tidx)
588
+ gmem_thr_copy_dQaccum = gmem_tiled_copy_dQaccum.get_slice(tidx)
589
+ # (CPY_Atom, CPY_M, CPY_K, m_block)
590
+ tQgQ = gmem_thr_copy_QK.partition_S(gQ)
591
+ tQsQ = gmem_thr_copy_QK.partition_D(sQ)
592
+ # (CPY_Atom, CPY_N, CPY_K)
593
+ tKgK = gmem_thr_copy_QK.partition_S(gK)
594
+ tKsK = gmem_thr_copy_QK.partition_D(sK)
595
+ # (CPY_Atom, CPY_N, CPY_K)
596
+ tVgV = gmem_thr_copy_VdO.partition_S(gV)
597
+ tVsV = gmem_thr_copy_VdO.partition_D(sV)
598
+ # (CPY_Atom, CPY_M, CPY_K, m_block)
599
+ tdOgdO = gmem_thr_copy_VdO.partition_S(gdO)
600
+ tdOsdO = gmem_thr_copy_VdO.partition_D(sdO)
601
+ tLSEgLSE = gmem_thr_copy_lse.partition_S(gLSE)
602
+ tLSEsLSE = gmem_thr_copy_lse.partition_D(sLSE)
603
+ tLSEgdPsum = gmem_thr_copy_lse.partition_S(gdPsum)
604
+ tLSEsdPsum = gmem_thr_copy_lse.partition_D(sdPsum)
605
+ tdQgdQaccum = gmem_thr_copy_dQaccum.partition_S(gdQaccum)
606
+
607
+ # ///////////////////////////////////////////////////////////////////////////////
608
+ # Tile MMA compute thread partitions and allocate accumulators
609
+ # ///////////////////////////////////////////////////////////////////////////////
610
+ thr_mma_sdp = tiled_mma_sdp.get_slice(tidx)
611
+ thr_mma_dkv = tiled_mma_dkv.get_slice(tidx)
612
+ thr_mma_dq = tiled_mma_dq.get_slice(tidx)
613
+ acc_shape_dK = thr_mma_dkv.partition_shape_C((self.n_block_size, self.head_dim_padded))
614
+ acc_shape_dV = thr_mma_dkv.partition_shape_C((self.n_block_size, self.head_dim_v_padded))
615
+ acc_dK = cute.make_fragment(acc_shape_dK, cutlass.Float32)
616
+ acc_dV = cute.make_fragment(acc_shape_dV, cutlass.Float32)
617
+ acc_dK.fill(0.0)
618
+ acc_dV.fill(0.0)
619
+
620
+ tSrQ = utils.mma_make_fragment_A(sQ[None, None, 0], thr_mma_sdp, swapAB=self.SdP_swapAB)
621
+ tSrK = utils.mma_make_fragment_B(sK, thr_mma_sdp, swapAB=self.SdP_swapAB)
622
+ tdPrdO = utils.mma_make_fragment_A(sdO[None, None, 0], thr_mma_sdp, swapAB=self.SdP_swapAB)
623
+ tdPrV = utils.mma_make_fragment_B(sV, thr_mma_sdp, swapAB=self.SdP_swapAB)
624
+ tdVrP = utils.mma_make_fragment_A(sPt, thr_mma_dkv, swapAB=self.dKV_swapAB)
625
+ tdVrdO = utils.mma_make_fragment_B(sdOt[None, None, 0], thr_mma_dkv, swapAB=self.dKV_swapAB)
626
+ tdKrdS = utils.mma_make_fragment_A(sdSt, thr_mma_dkv, swapAB=self.dKV_swapAB)
627
+ tdKrQ = utils.mma_make_fragment_B(sQt[None, None, 0], thr_mma_dkv, swapAB=self.dKV_swapAB)
628
+ tdQrdS = utils.mma_make_fragment_A(sdS, thr_mma_dq, swapAB=self.dQ_swapAB)
629
+ tdQrK = utils.mma_make_fragment_B(sKt, thr_mma_dq, swapAB=self.dQ_swapAB)
630
+
631
+ LSEslice = (None, 0, None) if cutlass.const_expr(not self.SdP_swapAB) else (0, None, None)
632
+ tSsLSEMma = utils.make_acc_tensor_mn_view(thr_mma_sdp.partition_C(sLSEMma))[LSEslice]
633
+ tSsdPsumMma = utils.make_acc_tensor_mn_view(thr_mma_sdp.partition_C(sdPsumMma))[LSEslice]
634
+
635
+ # ///////////////////////////////////////////////////////////////////////////////
636
+ # Smem copy atom tiling
637
+ # ///////////////////////////////////////////////////////////////////////////////
638
+ smem_copy_atom = cute.make_copy_atom(
639
+ warp.LdMatrix8x8x16bOp(transpose=False, num_matrices=4), self.dtype,
640
+ )
641
+ smem_copy_atom_transposed = cute.make_copy_atom(
642
+ warp.LdMatrix8x8x16bOp(transpose=True, num_matrices=4), self.dtype,
643
+ )
644
+ smem_thr_copy_QdO = utils.make_tiled_copy_A(
645
+ smem_copy_atom, tiled_mma_sdp, swapAB=self.SdP_swapAB
646
+ ).get_slice(tidx)
647
+ smem_thr_copy_KV = utils.make_tiled_copy_B(
648
+ smem_copy_atom, tiled_mma_sdp, swapAB=self.SdP_swapAB
649
+ ).get_slice(tidx)
650
+ # TODO: should this be smem_copy_atom_transposed?
651
+ smem_thr_copy_PdSt = utils.make_tiled_copy_A(
652
+ smem_copy_atom_transposed, tiled_mma_dkv, swapAB=self.dKV_swapAB
653
+ ).get_slice(tidx)
654
+ smem_thr_copy_QdOt = utils.make_tiled_copy_B(
655
+ smem_copy_atom_transposed, tiled_mma_dkv, swapAB=self.dKV_swapAB
656
+ ).get_slice(tidx)
657
+ smem_thr_copy_dS = utils.make_tiled_copy_A(
658
+ smem_copy_atom, tiled_mma_dq, swapAB=self.dQ_swapAB
659
+ ).get_slice(tidx)
660
+ smem_thr_copy_Kt = utils.make_tiled_copy_B(
661
+ smem_copy_atom_transposed, tiled_mma_dq, swapAB=self.dQ_swapAB
662
+ ).get_slice(tidx)
663
+ # TODO: what's the number of bits? What if SdP_swapAB
664
+ r2s_thr_copy_PdS = cute.make_tiled_copy_C(
665
+ cute.make_copy_atom(
666
+ cute.nvgpu.CopyUniversalOp(), self.dtype, num_bits_per_copy=2 * self.dtype.width
667
+ ),
668
+ tiled_mma_sdp,
669
+ ).get_slice(tidx)
670
+
671
+ tSsQ = smem_thr_copy_QdO.partition_S(sQ)
672
+ tdPsdO = smem_thr_copy_QdO.partition_S(sdO)
673
+ tSsK = smem_thr_copy_KV.partition_S(sK)
674
+ tdPsV = smem_thr_copy_KV.partition_S(sV)
675
+ tdVsPt = smem_thr_copy_PdSt.partition_S(sPt)
676
+ tdKsdSt = smem_thr_copy_PdSt.partition_S(sdSt)
677
+ tdVsdOt = smem_thr_copy_QdOt.partition_S(sdOt)
678
+ tdKsQt = smem_thr_copy_QdOt.partition_S(sQt)
679
+ tdQsdS = smem_thr_copy_dS.partition_S(sdS)
680
+ tdQsKt = smem_thr_copy_Kt.partition_S(sKt)
681
+ tPsP = r2s_thr_copy_PdS.partition_D(sP)
682
+ tdSsdS = r2s_thr_copy_PdS.partition_D(sdS)
683
+
684
+ # ///////////////////////////////////////////////////////////////////////////////
685
+ # Predicate: Mark indices that need to copy when problem_shape isn't a multiple
686
+ # of tile_shape
687
+ # ///////////////////////////////////////////////////////////////////////////////
688
+ # Construct identity layout for KV
689
+ cQ = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
690
+ tQcQ = gmem_thr_copy_QK.partition_S(cQ)
691
+ t0QcQ = gmem_thr_copy_QK.get_slice(0).partition_S(cQ)
692
+ if cutlass.const_expr(self.head_dim_padded == self.head_dim_v_padded):
693
+ tdOcdO = tQcQ
694
+ t0dOcdO = t0QcQ
695
+ else:
696
+ cdO = cute.make_identity_tensor((self.m_block_size, self.head_dim_v_padded))
697
+ tdOcdO = gmem_thr_copy_VdO.partition_S(cdO)
698
+ t0dOcdO = gmem_thr_copy_VdO.get_slice(0).partition_S(cdO)
699
+ cLSE = cute.make_identity_tensor((self.m_block_size,))
700
+ tLSEcLSE = gmem_thr_copy_lse.partition_S(cLSE)
701
+
702
+ # Allocate predicate tensors for m and n, here we only allocate the tile of k, and
703
+ # use "if" on the mn dimension.
704
+ # This is to reduce register pressure and gets 2-3% performance gain.
705
+
706
+ d_head = mQ.shape[cute.rank(mQ) - 1]
707
+ d_head_v = mdO.shape[cute.rank(mdO) - 1]
708
+
709
+ tQpQ = utils.predicate_k(tQcQ, limit=d_head)
710
+ if cutlass.const_expr(self.same_hdim_kv):
711
+ tdOpdO = tQpQ
712
+ else:
713
+ tdOpdO = utils.predicate_k(tdOcdO, limit=d_head_v)
714
+
715
+ # group parameters for compute_one_m_block
716
+ mma_params = SimpleNamespace(
717
+ thr_mma_sdp=thr_mma_sdp, thr_mma_dkv=thr_mma_dkv, thr_mma_dq=thr_mma_dq,
718
+ tSrQ=tSrQ, tSrK=tSrK, tdPrdO=tdPrdO, tdPrV=tdPrV,
719
+ tdVrP=tdVrP, tdVrdO=tdVrdO, tdKrdS=tdKrdS, tdKrQ=tdKrQ,
720
+ tdQrdS=tdQrdS, tdQrK=tdQrK,
721
+ acc_dK=acc_dK, acc_dV=acc_dV,
722
+ )
723
+ smem_copy_params = SimpleNamespace(
724
+ smem_thr_copy_QdO=smem_thr_copy_QdO,
725
+ smem_thr_copy_KV=smem_thr_copy_KV,
726
+ smem_thr_copy_PdSt=smem_thr_copy_PdSt,
727
+ smem_thr_copy_QdOt=smem_thr_copy_QdOt,
728
+ smem_thr_copy_dS=smem_thr_copy_dS,
729
+ smem_thr_copy_Kt=smem_thr_copy_Kt,
730
+ r2s_thr_copy_PdS=r2s_thr_copy_PdS,
731
+ tSsQ=tSsQ, tSsK=tSsK, tdPsdO=tdPsdO, tdPsV=tdPsV,
732
+ tSsLSEMma=tSsLSEMma, tSsdPsumMma=tSsdPsumMma,
733
+ tPsP=tPsP, tdSsdS=tdSsdS,
734
+ tdVsPt=tdVsPt, tdVsdOt=tdVsdOt, tdKsdSt=tdKsdSt, tdKsQt=tdKsQt,
735
+ tdQsdS=tdQsdS, tdQsKt=tdQsKt,
736
+ )
737
+ gmem_copy_params = SimpleNamespace(
738
+ gmem_thr_copy_dQaccum=gmem_thr_copy_dQaccum, tdQgdQaccum=tdQgdQaccum
739
+ )
740
+ load_Q_LSE = partial(
741
+ self.load_Q_LSE, gmem_tiled_copy_QK, gmem_tiled_copy_LSE,
742
+ tQgQ, tQsQ, tQcQ, t0QcQ, tQpQ,
743
+ tLSEgLSE, tLSEsLSE, tLSEcLSE, seqlen=seqlen.seqlen_q
744
+ )
745
+ load_dO_dPsum = partial(
746
+ self.load_dO_dPsum, gmem_tiled_copy_VdO, gmem_tiled_copy_LSE,
747
+ tdOgdO, tdOsdO, tdOcdO, t0dOcdO, tdOpdO,
748
+ tLSEgdPsum, tLSEsdPsum, tLSEcLSE, seqlen=seqlen.seqlen_q
749
+ )
750
+ compute_one_m_block = partial(
751
+ self.compute_one_m_block, mma_params=mma_params,
752
+ smem_copy_params=smem_copy_params, gmem_copy_params=gmem_copy_params,
753
+ load_Q_LSE=load_Q_LSE, load_dO_dPsum=load_dO_dPsum,
754
+ m_block_max=m_block_max,
755
+ softmax_scale_log2=softmax_scale_log2,
756
+ )
757
+
758
+ # ///////////////////////////////////////////////////////////////////////////////
759
+ # Prologue
760
+ # ///////////////////////////////////////////////////////////////////////////////
761
+ # Start async loads of the last mn-tile, where we take care of the mn residue
762
+ self.load_V(gmem_thr_copy_VdO, tVgV, tVsV, n_block, seqlen=seqlen.seqlen_k,
763
+ headdim=d_head_v)
764
+ if cutlass.const_expr(self.V_in_regs):
765
+ cute.arch.cp_async_commit_group()
766
+ self.load_K(gmem_thr_copy_QK, tKgK, tKsK, n_block, seqlen=seqlen.seqlen_k,
767
+ headdim=d_head)
768
+ cute.arch.cp_async_commit_group()
769
+
770
+ if cutlass.const_expr(self.V_in_regs):
771
+ cute.arch.cp_async_wait_group(1)
772
+ cute.arch.barrier()
773
+ tdPrV_copy_view = smem_thr_copy_KV.retile(tdPrV)
774
+ cute.copy(smem_thr_copy_KV, tdPsV, tdPrV_copy_view)
775
+ # Sync to avoid loading Q to smem_q, which overlaps with smem_v
776
+ cute.arch.barrier()
777
+
778
+ m_block = m_block_min
779
+ assert self.num_stages_Q >= self.num_stages_dO
780
+ for stage in cutlass.range_constexpr(self.num_stages_Q):
781
+ if cutlass.const_expr(self.num_stages_Q == 1 or stage < self.num_stages_Q - 1):
782
+ if stage == 0 or m_block + stage < m_block_max:
783
+ load_Q_LSE(m_block + stage, smem_pipe_write_q=stage)
784
+ cute.arch.cp_async_commit_group()
785
+ if cutlass.const_expr(stage < self.num_stages_dO):
786
+ if stage == 0 or m_block + stage < m_block_max:
787
+ load_dO_dPsum(m_block + stage, smem_pipe_write_q=stage)
788
+ cute.arch.cp_async_commit_group()
789
+
790
+ # ///////////////////////////////////////////////////////////////////////////////
791
+ # Mainloop
792
+ # ///////////////////////////////////////////////////////////////////////////////
793
+ # Start processing of the first n-block.
794
+ mask = AttentionMask(self.m_block_size, self.n_block_size, seqlen.seqlen_q, seqlen.seqlen_k)
795
+ mask_fn = partial(
796
+ mask.apply_mask, n_block=n_block, thr_mma=thr_mma_sdp,
797
+ mask_seqlen=True, mask_causal=self.is_causal
798
+ )
799
+ smem_pipe_read_q = cutlass.Int32(0)
800
+ smem_pipe_read_do = cutlass.Int32(0)
801
+ smem_pipe_write_q = cutlass.Int32(self.num_stages_Q - 1)
802
+ smem_pipe_write_do = cutlass.Int32(0)
803
+ for m_tile in cutlass.range(m_block_min, m_block_max, unroll=1):
804
+ compute_one_m_block(
805
+ m_tile, smem_pipe_read_q, smem_pipe_read_do, smem_pipe_write_q, smem_pipe_write_do,
806
+ mask_fn=mask_fn,
807
+ )
808
+ smem_pipe_read_q = self.advance_pipeline(smem_pipe_read_q, self.num_stages_Q)
809
+ smem_pipe_read_do = self.advance_pipeline(smem_pipe_read_do, self.num_stages_dO)
810
+ smem_pipe_write_q = self.advance_pipeline(smem_pipe_write_q, self.num_stages_Q)
811
+ smem_pipe_write_do = self.advance_pipeline(smem_pipe_write_do, self.num_stages_dO)
812
+
813
+ # ///////////////////////////////////////////////////////////////////////////////
814
+ # Epilogue
815
+ # ///////////////////////////////////////////////////////////////////////////////
816
+ # If GQA, we scale dK in the postprocessing kernel instead
817
+ if cutlass.const_expr(self.qhead_per_kvhead == 1):
818
+ acc_dK.store(acc_dK.load() * softmax_scale)
819
+ # reuse sK and sV data iterator
820
+ sdK = cute.make_tensor(sK.iterator, sK_layout)
821
+ sdV = cute.make_tensor(sV.iterator, sV_layout)
822
+ self.epilogue(
823
+ acc_dK, acc_dV, mdK, mdV, sdK, sdV,
824
+ gmem_tiled_copy_dK, gmem_tiled_copy_dV, tiled_mma_dkv,
825
+ tidx, n_block, head_idx, batch_idx, seqlen, d_head, d_head_v
826
+ )
827
+
828
+ @cute.jit
829
+ def compute_one_m_block(
830
+ self,
831
+ m_block: cutlass.Int32,
832
+ smem_pipe_read_q: cutlass.Int32,
833
+ smem_pipe_read_do: cutlass.Int32,
834
+ smem_pipe_write_q: cutlass.Int32,
835
+ smem_pipe_write_do: cutlass.Int32,
836
+ mma_params: SimpleNamespace,
837
+ smem_copy_params: SimpleNamespace,
838
+ gmem_copy_params: SimpleNamespace,
839
+ load_Q_LSE: Callable,
840
+ load_dO_dPsum: Callable,
841
+ m_block_max: cutlass.Int32,
842
+ softmax_scale_log2: cutlass.Float32,
843
+ mask_fn: Optional[Callable] = None,
844
+ ):
845
+ def load_Q_next():
846
+ m_block_next = m_block + (self.num_stages_Q - 1 if cutlass.const_expr(self.num_stages_Q > 1) else 1)
847
+ if m_block_next < m_block_max:
848
+ load_Q_LSE(m_block_next, smem_pipe_write_q)
849
+ cute.arch.cp_async_commit_group()
850
+
851
+ def load_dO_next():
852
+ if m_block + self.num_stages_dO < m_block_max:
853
+ load_dO_dPsum(m_block + self.num_stages_dO, smem_pipe_write_do)
854
+ cute.arch.cp_async_commit_group()
855
+
856
+ # MMA S
857
+ acc_shape_SdP = mma_params.thr_mma_sdp.partition_shape_C(
858
+ (self.m_block_size, self.n_block_size) if cutlass.const_expr(not self.SdP_swapAB) else (self.n_block_size, self.m_block_size)
859
+ )
860
+ acc_S = cute.make_fragment(acc_shape_SdP, cutlass.Float32)
861
+ acc_S.fill(0.0)
862
+ cute.arch.cp_async_wait_group(1 if cutlass.const_expr(self.num_stages_Q > 1) else 0)
863
+ cute.arch.barrier()
864
+ sm80_utils.gemm(
865
+ mma_params.thr_mma_sdp, acc_S, mma_params.tSrQ, mma_params.tSrK,
866
+ smem_copy_params.tSsQ[None, None, None, smem_pipe_read_q if cutlass.const_expr(self.num_stages_Q > 1) else 0],
867
+ smem_copy_params.tSsK,
868
+ smem_copy_params.smem_thr_copy_QdO, smem_copy_params.smem_thr_copy_KV,
869
+ swap_AB=self.SdP_swapAB,
870
+ )
871
+ tLSErLSE = cute.make_fragment_like(smem_copy_params.tSsLSEMma[None, 0])
872
+ cute.autovec_copy(
873
+ smem_copy_params.tSsLSEMma[None, smem_pipe_read_q if cutlass.const_expr(self.num_stages_Q > 1) else 0], tLSErLSE
874
+ )
875
+ if cutlass.const_expr(mask_fn is not None):
876
+ mask_fn(acc_S, m_block=m_block)
877
+ acc_S_mn = utils.make_acc_tensor_mn_view(acc_S)
878
+ bidx = 0
879
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_S_mn)
880
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == 1: cute.print_tensor(tLSErLSE)
881
+ assert cute.size(acc_S_mn, mode=[0]) == cute.size(tLSErLSE)
882
+ for r in cutlass.range(cute.size(acc_S_mn, mode=[0]), unroll_full=True):
883
+ acc_S_mn[r, None].store(utils.exp2f(acc_S_mn[r, None].load() * softmax_scale_log2 - tLSErLSE[r]))
884
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_S_mn)
885
+
886
+ # MMA dP
887
+ acc_dP = cute.make_fragment(acc_shape_SdP, cutlass.Float32)
888
+ acc_dP.fill(0.0)
889
+ cute.arch.cp_async_wait_group(1 if cutlass.const_expr(self.num_stages_dO > 1) else 0)
890
+ cute.arch.barrier()
891
+ sm80_utils.gemm(
892
+ mma_params.thr_mma_sdp, acc_dP, mma_params.tdPrdO, mma_params.tdPrV,
893
+ smem_copy_params.tdPsdO[None, None, None, smem_pipe_read_do if cutlass.const_expr(self.num_stages_dO > 1) else 0],
894
+ smem_copy_params.tdPsV,
895
+ smem_copy_params.smem_thr_copy_QdO, smem_copy_params.smem_thr_copy_KV,
896
+ hook_fn=load_Q_next if cutlass.const_expr(self.num_stages_Q > 1) else None,
897
+ swap_AB=self.SdP_swapAB,
898
+ )
899
+ tLSErdPsum = cute.make_fragment_like(smem_copy_params.tSsdPsumMma[None, 0])
900
+ cute.autovec_copy(
901
+ smem_copy_params.tSsdPsumMma[None, smem_pipe_read_do if cutlass.const_expr(self.num_stages_dO > 1) else 0], tLSErdPsum
902
+ )
903
+ acc_dP_mn = utils.make_acc_tensor_mn_view(acc_dP)
904
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dP_mn)
905
+ assert cute.size(acc_dP_mn, mode=[0]) == cute.size(tLSErdPsum)
906
+ for r in cutlass.range(cute.size(acc_dP_mn, mode=[0]), unroll_full=True):
907
+ acc_dP_mn[r, None].store(acc_S_mn[r, None].load() * (acc_dP_mn[r, None].load() - tLSErdPsum[r]))
908
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dP_mn)
909
+ rP = cute.make_fragment_like(acc_S, self.dtype)
910
+ rP.store(acc_S.load().to(self.dtype))
911
+ if cutlass.const_expr(not self.Mma_dKV_is_RS):
912
+ tPrP = smem_copy_params.r2s_thr_copy_PdS.retile(rP) # ((Atom,AtomNum), MMA_N, MMA_N)
913
+ cute.copy(smem_copy_params.r2s_thr_copy_PdS, tPrP, smem_copy_params.tPsP)
914
+ rdS = cute.make_fragment_like(acc_dP, self.dtype)
915
+ rdS.store(acc_dP.load().to(self.dtype))
916
+ if cutlass.const_expr(not self.Mma_dKV_is_RS):
917
+ cute.arch.barrier() # Make sure P is written
918
+ # For hdim 64, It's faster to write to smem_dS first before the dV gemm
919
+ if cutlass.const_expr(not self.Mma_dKV_is_RS):
920
+ tdSrdS = smem_copy_params.r2s_thr_copy_PdS.retile(rdS)
921
+ cute.copy(smem_copy_params.r2s_thr_copy_PdS, tdSrdS, smem_copy_params.tdSsdS)
922
+ if cutlass.const_expr(self.Mma_dKV_is_RS):
923
+ tdVrP = cute.make_tensor(rP.iterator, utils.convert_layout_acc_frgA(rP.layout))
924
+ else:
925
+ tdVrP = mma_params.tdVrP
926
+
927
+ # MMA dK
928
+ sm80_utils.gemm(
929
+ mma_params.thr_mma_dkv, mma_params.acc_dV, tdVrP, mma_params.tdVrdO,
930
+ smem_copy_params.tdVsPt,
931
+ smem_copy_params.tdVsdOt[None, None, None, smem_pipe_read_do if cutlass.const_expr(self.num_stages_dO > 1) else 0],
932
+ smem_copy_params.smem_thr_copy_PdSt, smem_copy_params.smem_thr_copy_QdOt,
933
+ A_in_regs=self.Mma_dKV_is_RS,
934
+ swap_AB=self.dKV_swapAB,
935
+ )
936
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(mma_params.acc_dV)
937
+ cute.arch.barrier() # Make sure dS is written
938
+
939
+ # MMA dQ
940
+ def dQ_mma(hook_fn):
941
+ acc_shape_dQ = mma_params.thr_mma_dq.partition_shape_C(
942
+ (self.m_block_size, self.head_dim_padded) if cutlass.const_expr(not self.dQ_swapAB) else (self.head_dim_padded, self.m_block_size)
943
+ )
944
+ acc_dQ = cute.make_fragment(acc_shape_dQ, cutlass.Float32)
945
+ acc_dQ.fill(0.0)
946
+ sm80_utils.gemm(
947
+ mma_params.thr_mma_dq, acc_dQ, mma_params.tdQrdS, mma_params.tdQrK,
948
+ smem_copy_params.tdQsdS, smem_copy_params.tdQsKt,
949
+ smem_copy_params.smem_thr_copy_dS, smem_copy_params.smem_thr_copy_Kt,
950
+ swap_AB=self.dQ_swapAB,
951
+ hook_fn=hook_fn
952
+ )
953
+ # ((1, 1), num_elements)
954
+ acc_dQ_atomic = gmem_copy_params.gmem_thr_copy_dQaccum.retile(acc_dQ)
955
+ tdQgdQaccum_atomic = gmem_copy_params.tdQgdQaccum[None, None, m_block]
956
+ assert cute.size(acc_dQ_atomic) == cute.size(tdQgdQaccum_atomic)
957
+ for i in cutlass.range(cute.size(acc_dQ_atomic), unroll_full=True):
958
+ utils.atomic_add_fp32(acc_dQ_atomic[i], utils.elem_pointer(tdQgdQaccum_atomic, i))
959
+ # utils.atomic_add_fp32(acc_dQ[i], tdQgdQaccum_atomic.iterator + i * tdQgdQaccum_atomic.stride[1])
960
+ # if cute.arch.thread_idx()[0] == 64 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dQ)
961
+
962
+ # If num_stages_Q == 1, we want to do Mma_dK first so we can start loading Q for the next iteration
963
+ if cutlass.const_expr(self.num_stages_Q > 1):
964
+ dQ_mma(load_dO_next)
965
+
966
+ # MMA dK
967
+ if cutlass.const_expr(self.Mma_dKV_is_RS):
968
+ tdKrdS = cute.make_tensor(rdS.iterator, utils.convert_layout_acc_frgA(rdS.layout))
969
+ else:
970
+ tdKrdS = mma_params.tdKrdS
971
+ sm80_utils.gemm(
972
+ mma_params.thr_mma_dkv, mma_params.acc_dK, tdKrdS, mma_params.tdKrQ,
973
+ smem_copy_params.tdKsdSt,
974
+ smem_copy_params.tdKsQt[None, None, None, smem_pipe_read_q if cutlass.const_expr(self.num_stages_Q > 1) else 0],
975
+ smem_copy_params.smem_thr_copy_PdSt, smem_copy_params.smem_thr_copy_QdOt,
976
+ A_in_regs=self.Mma_dKV_is_RS,
977
+ swap_AB=self.dKV_swapAB,
978
+ hook_fn=load_dO_next if cutlass.const_expr(self.num_stages_Q == 1) else None,
979
+ )
980
+ # if cute.arch.thread_idx()[0] == 0: cute.print_tensor(mma_params.acc_dK)
981
+ if cutlass.const_expr(self.num_stages_Q == 1):
982
+ cute.arch.barrier()
983
+ dQ_mma(load_Q_next)
984
+
985
+ @cute.jit
986
+ def epilogue(
987
+ self,
988
+ acc_dK: cute.Tensor,
989
+ acc_dV: cute.Tensor,
990
+ mdK: cute.Tensor,
991
+ mdV: cute.Tensor,
992
+ sdK: cute.Tensor,
993
+ sdV: cute.Tensor,
994
+ gmem_tiled_copy_dK: cute.TiledCopy,
995
+ gmem_tiled_copy_dV: cute.TiledCopy,
996
+ tiled_mma: cute.TiledMma,
997
+ tidx: cutlass.Int32,
998
+ n_block: cutlass.Int32,
999
+ num_head: cutlass.Int32,
1000
+ batch_size: cutlass.Int32,
1001
+ seqlen: SeqlenInfoQK,
1002
+ d_head: cutlass.Int32,
1003
+ d_head_v: cutlass.Int32
1004
+ ):
1005
+ rdV = cute.make_fragment_like(acc_dV, self.dtype)
1006
+ rdV.store(acc_dV.load().to(self.dtype))
1007
+ rdK = cute.make_fragment_like(acc_dK, self.dtype)
1008
+ rdK.store(acc_dK.load().to(self.dtype))
1009
+ gmem_thr_copy_dK = gmem_tiled_copy_dK.get_slice(tidx)
1010
+ gmem_thr_copy_dV = gmem_tiled_copy_dV.get_slice(tidx)
1011
+
1012
+ batch_idx = batch_size
1013
+ head_idx_kv = num_head // self.qhead_per_kvhead if cutlass.const_expr(not self.pack_gqa) else num_head
1014
+
1015
+ if cutlass.const_expr(self.qhead_per_kvhead == 1):
1016
+ # Make sure all threads have finished reading K and V, otherwise we get racy dQ
1017
+ # because smem_q could be changed.
1018
+ cute.arch.barrier()
1019
+ # smem copy atom for dKV
1020
+ smem_copy_atom_dKV = cute.make_copy_atom(
1021
+ cute.nvgpu.CopyUniversalOp(), self.dtype, num_bits_per_copy=2 * self.dtype.width
1022
+ )
1023
+ smem_thr_copy_dKV = cute.make_tiled_copy_C(smem_copy_atom_dKV, tiled_mma).get_slice(tidx)
1024
+ taccdVrdV = smem_thr_copy_dKV.retile(rdV)
1025
+ taccdKrdK = smem_thr_copy_dKV.retile(rdK)
1026
+ taccdVsdV = smem_thr_copy_dKV.partition_D(sdV)
1027
+ taccdKsdK = smem_thr_copy_dKV.partition_D(sdK)
1028
+ # copy acc O from rmem to smem with the smem copy atom
1029
+ cute.copy(smem_copy_atom_dKV, taccdVrdV, taccdVsdV)
1030
+ cute.copy(smem_copy_atom_dKV, taccdKrdK, taccdKsdK)
1031
+
1032
+
1033
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_k):
1034
+ mdK_cur, mdV_cur = [t[batch_idx, None, head_idx_kv, None] for t in (mdK, mdV)]
1035
+ else:
1036
+ mdK_cur, mdV_cur = [cute.domain_offset((seqlen.offset_k, 0), t[None, head_idx_kv, None]) for t in (mdK, mdV)]
1037
+
1038
+ blkdK_shape = (self.n_block_size, self.head_dim_padded)
1039
+ blkdV_shape = (self.n_block_size, self.head_dim_v_padded)
1040
+ gdK = cute.local_tile(mdK_cur, blkdK_shape, (n_block, 0))
1041
+ gdV = cute.local_tile(mdV_cur, blkdV_shape, (n_block, 0))
1042
+ tdKsdK = gmem_thr_copy_dK.partition_S(sdK)
1043
+ tdKgdK = gmem_thr_copy_dK.partition_D(gdK)
1044
+ tdVsdV = gmem_thr_copy_dV.partition_S(sdV)
1045
+ tdVgdV = gmem_thr_copy_dV.partition_D(gdV)
1046
+ tdKrdK = cute.make_fragment_like(tdKgdK, self.dtype)
1047
+ tdVrdV = cute.make_fragment_like(tdVgdV, self.dtype)
1048
+ # sync before all smem stores are done.
1049
+ cute.arch.barrier()
1050
+ # load acc dK and dV from smem to rmem for wider vectorization
1051
+ # Need to check OOB when reading from smem if kBlockN isn't evenly tiled
1052
+ # TODO
1053
+ cute.autovec_copy(tdKsdK, tdKrdK)
1054
+ cute.autovec_copy(tdVsdV, tdVrdV)
1055
+
1056
+ cdK = cute.make_identity_tensor((self.n_block_size, self.head_dim_padded))
1057
+ tdKcdK = gmem_thr_copy_dK.partition_S(cdK)
1058
+ t0dKcdK = gmem_tiled_copy_dK.get_slice(0).partition_S(cdK)
1059
+ if cutlass.const_expr(self.head_dim_padded == self.head_dim_v_padded):
1060
+ tdVcdV = tdKcdK
1061
+ t0dVcdV = t0dKcdK
1062
+ else:
1063
+ cdV = cute.make_identity_tensor((self.n_block_size, self.head_dim_v_padded))
1064
+ tdVcdV = gmem_thr_copy_dV.partition_S(cdV)
1065
+ t0dVcdV = gmem_tiled_copy_dV.get_slice(0).partition_S(cdV)
1066
+ tdKpdK = utils.predicate_k(tdKcdK, limit=d_head)
1067
+ if cutlass.const_expr(self.same_hdim_kv):
1068
+ tdVpdV = tdKpdK
1069
+ else:
1070
+ tdVpdV = utils.predicate_k(tdVcdV, limit=d_head_v)
1071
+ # copy acc dK and acc_dV from rmem to gmem
1072
+ for rest_m in cutlass.range_constexpr(cute.size(tdKrdK.shape[1])):
1073
+ if t0dKcdK[0, rest_m, 0][0] < seqlen.seqlen_k - n_block * self.n_block_size - tdKcdK[0][0]:
1074
+ cute.copy(
1075
+ gmem_tiled_copy_dK,
1076
+ tdKrdK[None, rest_m, None],
1077
+ tdKgdK[None, rest_m, None],
1078
+ pred=tdKpdK[None, rest_m, None] if cutlass.const_expr(self.check_hdim_oob) else None,
1079
+ )
1080
+ for rest_m in cutlass.range_constexpr(cute.size(tdVrdV.shape[1])):
1081
+ if t0dVcdV[0, rest_m, 0][0] < seqlen.seqlen_k - n_block * self.n_block_size - tdVcdV[0][0]:
1082
+ cute.copy(
1083
+ gmem_tiled_copy_dV,
1084
+ tdVrdV[None, rest_m, None],
1085
+ tdVgdV[None, rest_m, None],
1086
+ pred=tdVpdV[None, rest_m, None] if cutlass.const_expr(self.check_hdim_v_oob) else None,
1087
+ )
1088
+
1089
+ else: # qhead_per_kvhead > 1, do atomic add
1090
+ # For Sm90, we need to sync to avoid racy writes to smem_q
1091
+ # For Sm80, we don't need to sync since we're not touching smem
1092
+ head_idx_kv = num_head // self.qhead_per_kvhead if cutlass.const_expr(not self.pack_gqa) else num_head
1093
+
1094
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_k):
1095
+ mdK_cur, mdV_cur = [t[batch_idx, head_idx_kv, None] for t in (mdK, mdV)]
1096
+ else:
1097
+ padded_offset_k = seqlen.offset_k + batch_idx * self.n_block_size
1098
+ mdK_cur = cute.domain_offset((padded_offset_k * self.head_dim_padded,), mdK[head_idx_kv, None])
1099
+ mdV_cur = cute.domain_offset((padded_offset_k * self.head_dim_v_padded,), mdV[head_idx_kv, None])
1100
+
1101
+ gdV = cute.local_tile(mdV_cur, (self.n_block_size * self.head_dim_v_padded,), (n_block,))
1102
+ gdK = cute.local_tile(mdK_cur, (self.n_block_size * self.head_dim_padded,), (n_block,))
1103
+ tdVgdVaccum = gmem_thr_copy_dV.partition_S(gdV)
1104
+ tdKgdKaccum = gmem_thr_copy_dK.partition_S(gdK)
1105
+ acc_dV_atomic = gmem_thr_copy_dV.retile(acc_dV)
1106
+ acc_dK_atomic = gmem_thr_copy_dK.retile(acc_dK)
1107
+ assert cute.size(acc_dV_atomic) == cute.size(tdVgdVaccum)
1108
+ assert cute.size(acc_dK_atomic) == cute.size(tdKgdKaccum)
1109
+ for i in cutlass.range(cute.size(acc_dV_atomic), unroll_full=True):
1110
+ utils.atomic_add_fp32(acc_dV_atomic[i], utils.elem_pointer(tdVgdVaccum, i))
1111
+ for i in cutlass.range(cute.size(acc_dK_atomic), unroll_full=True):
1112
+ utils.atomic_add_fp32(acc_dK_atomic[i], utils.elem_pointer(tdKgdKaccum, i))
1113
+
1114
+ @cute.jit
1115
+ def advance_pipeline(self, pipeline_index, num_stages: cutlass.Constexpr):
1116
+ return pipeline_index + 1 if pipeline_index < num_stages - 1 else 0
1117
+
1118
+ @cute.jit
1119
+ def load_K(
1120
+ self,
1121
+ gmem_thr_copy: cute.TiledCopy,
1122
+ tKgK: cute.Tensor,
1123
+ tKsK: cute.Tensor,
1124
+ block: cutlass.Int32,
1125
+ seqlen: cutlass.Int32,
1126
+ headdim: cutlass.Int32,
1127
+ ):
1128
+ cK = cute.make_identity_tensor((self.n_block_size, self.head_dim_padded))
1129
+ tKcK = gmem_thr_copy.partition_S(cK)
1130
+ t0KcK = gmem_thr_copy.get_slice(0).partition_S(cK)
1131
+ tKpK = utils.predicate_k(tKcK, limit=headdim)
1132
+ for n in cutlass.range_constexpr(cute.size(tKsK.shape[1])):
1133
+ # If kBlockN doesn't evenly divide the tiled copy, only the last `n` needs to be checked
1134
+ if self.is_even_n_smem_k or n < cute.size(tKsK.shape[1]) - 1 or tKcK[0, n, 0][0] < self.n_block_size:
1135
+ # Instead of using tKcK, we using t0KcK and subtract the offset from the limit
1136
+ # (seqlen - block * kBlockN). This is because the entries of t0KcK are known at compile time.
1137
+ predicate_n = t0KcK[0, n, 0][0] < seqlen - block * self.n_block_size - tKcK[0][0]
1138
+ predicate = cute.make_fragment_like(tKpK[None, 0, None])
1139
+ for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
1140
+ for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
1141
+ predicate[i, k] = (tKpK[i, n, k] if cutlass.const_expr(self.check_hdim_oob) else True) and predicate_n
1142
+ cute.copy(
1143
+ gmem_thr_copy, tKgK[None, n, None], tKsK[None, n, None], pred=predicate,
1144
+ )
1145
+ # We need to clear the sK smem tiles since we'll use sKt for mma_dq
1146
+
1147
+ @cute.jit
1148
+ def load_V(
1149
+ self,
1150
+ gmem_thr_copy: cute.TiledCopy,
1151
+ tVgV: cute.Tensor,
1152
+ tVsV: cute.Tensor,
1153
+ block: cutlass.Int32,
1154
+ seqlen: cutlass.Int32,
1155
+ headdim: cutlass.Int32,
1156
+ ):
1157
+ cV = cute.make_identity_tensor((self.n_block_size, self.head_dim_v_padded))
1158
+ tVcV = gmem_thr_copy.partition_S(cV)
1159
+ t0VcV = gmem_thr_copy.get_slice(0).partition_S(cV)
1160
+ tVpV = utils.predicate_k(tVcV, limit=headdim)
1161
+ for n in cutlass.range_constexpr(cute.size(tVsV.shape[1])):
1162
+ # If kBlockN doesn't evenly divide the tiled copy, only the last `n` needs to be checked
1163
+ if self.is_even_n_smem_v or n < cute.size(tVsV.shape[1]) - 1 or tVcV[0, n, 0][0] < self.n_block_size:
1164
+ # Instead of using tVcV, we using t0VcV and subtract the offset from the limit
1165
+ # (seqlen - block * kBlockN). This is because the entries of t0VcV are known at compile time.
1166
+ predicate_n = t0VcV[0, n, 0][0] < seqlen - block * self.n_block_size - tVcV[0][0]
1167
+ predicate = cute.make_fragment_like(tVpV[None, 0, None])
1168
+ for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
1169
+ for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
1170
+ predicate[i, k] = (tVpV[i, n, k] if cutlass.const_expr(self.check_hdim_oob) else True) and predicate_n
1171
+ cute.copy(
1172
+ gmem_thr_copy, tVgV[None, n, None], tVsV[None, n, None], pred=predicate,
1173
+ )
1174
+
1175
+ @cute.jit
1176
+ def load_Q_LSE(
1177
+ self,
1178
+ gmem_tiled_copy_Q: cute.TiledCopy,
1179
+ gmem_tiled_copy_LSE: cute.TiledCopy,
1180
+ tQgQ: cute.Tensor,
1181
+ tQsQ: cute.Tensor,
1182
+ tQcQ: cute.Tensor,
1183
+ t0QcQ: cute.Tensor,
1184
+ tQpQ: cute.Tensor,
1185
+ tLSEgLSE: cute.Tensor,
1186
+ tLSEsLSE: cute.Tensor,
1187
+ tLSEcLSE: cute.Tensor,
1188
+ block: cutlass.Int32,
1189
+ smem_pipe_write_q: cutlass.Int32,
1190
+ seqlen: cutlass.Int32,
1191
+ ):
1192
+ for m in cutlass.range_constexpr(cute.size(tQsQ.shape[1])):
1193
+ # If kBlockM doesn't evenly divide the tiled copy, only the last `m` needs to be checked
1194
+ if self.is_even_m_smem_q or m < cute.size(tQsQ.shape[1]) - 1 or tQcQ[0, m, 0][0] < self.m_block_size:
1195
+ # Instead of using tQcQ, we using t0QcQ and subtract the offset from the limit
1196
+ # (seqlen - block * kBlockM). This is because the entries of t0QcQ are known at compile time.
1197
+ predicate_m = t0QcQ[0, m, 0][0] < seqlen - block * self.m_block_size - tQcQ[0][0]
1198
+ predicate = cute.make_fragment_like(tQpQ[None, 0, None])
1199
+ for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
1200
+ for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
1201
+ predicate[i, k] = (tQpQ[i, m, k] if cutlass.const_expr(self.check_hdim_oob) else True) and predicate_m
1202
+ cute.copy(
1203
+ gmem_tiled_copy_Q,
1204
+ tQgQ[None, m, None, block],
1205
+ tQsQ[None, m, None, smem_pipe_write_q if cutlass.const_expr(self.num_stages_Q) > 1 else 0],
1206
+ pred=predicate,
1207
+ )
1208
+ # We need to clear the sQ smem tiles since we'll use sQt for mma_dK
1209
+ # We made sure LSE length is padded so we read `kBlockM` elements so that all
1210
+ # elements in sLSE are filled. Without this we might have uninitialized sLSE values.
1211
+ for m in cutlass.range_constexpr(cute.size(tLSEsLSE.shape[1])):
1212
+ if tLSEcLSE[0, m][0] < self.m_block_size:
1213
+ cute.copy(
1214
+ gmem_tiled_copy_LSE,
1215
+ tLSEgLSE[None, m, block],
1216
+ tLSEsLSE[None, m, smem_pipe_write_q if cutlass.const_expr(self.num_stages_Q > 1) else 0],
1217
+ )
1218
+
1219
+ @cute.jit
1220
+ def load_dO_dPsum(
1221
+ self,
1222
+ gmem_tiled_copy_dO: cute.TiledCopy,
1223
+ gmem_tiled_copy_dPsum: cute.TiledCopy,
1224
+ tdOgdO: cute.Tensor,
1225
+ tdOsdO: cute.Tensor,
1226
+ tdOcdO: cute.Tensor,
1227
+ t0dOcdO: cute.Tensor,
1228
+ tdOpdO: cute.Tensor,
1229
+ tdPsumgdPsum: cute.Tensor,
1230
+ tdPsumsdPsum: cute.Tensor,
1231
+ tdPsumcdPsum: cute.Tensor,
1232
+ block: cutlass.Int32,
1233
+ smem_pipe_write_q: cutlass.Int32,
1234
+ seqlen: cutlass.Int32,
1235
+ ):
1236
+ for m in cutlass.range_constexpr(cute.size(tdOsdO.shape[1])):
1237
+ # If kBlockM doesn't evenly divide the tiled copy, only the last `m` needs to be checked
1238
+ if self.is_even_m_smem_do or m < cute.size(tdOsdO.shape[1]) - 1 or tdOcdO[0, m, 0][0] < self.m_block_size:
1239
+ # Instead of using tdOcdO, we using t0dOcdO and subtract the offset from the limit
1240
+ # (seqlen - block * kBlockM). This is because the entries of t0dOcdO are known at compile time.
1241
+ predicate_m = t0dOcdO[0, m, 0][0] < seqlen - block * self.m_block_size - tdOcdO[0][0]
1242
+ predicate = cute.make_fragment_like(tdOpdO[None, 0, None])
1243
+ for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
1244
+ for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
1245
+ predicate[i, k] = (tdOpdO[i, m, k] if cutlass.const_expr(self.check_hdim_oob) else True) and predicate_m
1246
+ cute.copy(
1247
+ gmem_tiled_copy_dO,
1248
+ tdOgdO[None, m, None, block],
1249
+ tdOsdO[None, m, None, smem_pipe_write_q if cutlass.const_expr(self.num_stages_dO > 1) else 0],
1250
+ pred=predicate,
1251
+ )
1252
+ # We need to clear the sQ smem tiles since we'll use sQt for mma_dK
1253
+ # We made sure LSE length is padded so we read `kBlockM` elements so that all
1254
+ # elements in sLSE are filled. Without this we might have uninitialized sLSE values.
1255
+ for m in cutlass.range_constexpr(cute.size(tdPsumgdPsum.shape[1])):
1256
+ if tdPsumcdPsum[0, m][0] < self.m_block_size:
1257
+ cute.copy(
1258
+ gmem_tiled_copy_dPsum,
1259
+ tdPsumgdPsum[None, m, block],
1260
+ tdPsumsdPsum[None, m, smem_pipe_write_q if cutlass.const_expr(self.num_stages_dO > 1) else 0],
1261
+ )
build/torch-universal/flash_bwd_postprocess.py ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_bwd_postprocess_kernel.h
3
+ # from Cutlass C++ to Cute-DSL.
4
+ import math
5
+ from typing import Callable, Optional, Type, Literal
6
+
7
+ import cuda.bindings.driver as cuda
8
+
9
+ import cutlass
10
+ import cutlass.cute as cute
11
+ import cutlass.utils.hopper_helpers as sm90_utils_basic
12
+ import cutlass.utils.blackwell_helpers as sm100_utils_basic
13
+ from cutlass.cute.nvgpu import cpasync, warp, warpgroup
14
+ from cutlass import Float32, const_expr
15
+ from cutlass.utils import LayoutEnum
16
+
17
+ from . import utils
18
+ from . import copy_utils
19
+ from . import ampere_helpers as sm80_utils
20
+ from . import hopper_helpers as sm90_utils
21
+ from .seqlen_info import SeqlenInfoQK
22
+ import cutlass.cute.nvgpu.tcgen05 as tcgen05
23
+ from .tile_scheduler import (
24
+ ParamsBase,
25
+ SingleTileScheduler,
26
+ SingleTileVarlenScheduler,
27
+ TileSchedulerArguments,
28
+ )
29
+
30
+
31
+ class FlashAttentionBackwardPostprocess:
32
+ def __init__(
33
+ self,
34
+ dtype: Type[cutlass.Numeric],
35
+ head_dim: int,
36
+ arch: Literal[80, 90, 100],
37
+ tile_m: int = 128,
38
+ num_threads: int = 256,
39
+ AtomLayoutMdQ: int = 1,
40
+ dQ_swapAB: bool = False,
41
+ ):
42
+ """
43
+ :param head_dim: head dimension
44
+ :type head_dim: int
45
+ :param tile_m: m block size
46
+ :type tile_m: int
47
+ """
48
+ self.dtype = dtype
49
+ self.tile_m = tile_m
50
+ assert arch in [80, 90, 100], (
51
+ "Only Ampere (80), Hopper (90), and Blackwell (100) are supported"
52
+ )
53
+ self.arch = arch
54
+ # padding head_dim to a multiple of 32 as k_block_size
55
+ hdim_multiple_of = 32
56
+ self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
57
+ self.check_hdim_oob = head_dim != self.tile_hdim
58
+ self.num_threads = num_threads
59
+ self.AtomLayoutMdQ = AtomLayoutMdQ
60
+ self.dQ_swapAB = dQ_swapAB
61
+
62
+ @staticmethod
63
+ def can_implement(dtype, head_dim, tile_m, num_threads) -> bool:
64
+ """Check if the kernel can be implemented with the given parameters.
65
+
66
+ :param dtype: data type
67
+ :type dtype: cutlass.Numeric
68
+ :param head_dim: head dimension
69
+ :type head_dim: int
70
+ :param tile_m: m block size
71
+ :type tile_m: int
72
+
73
+ :return: True if the kernel can be implemented, False otherwise
74
+ :rtype: bool
75
+ """
76
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
77
+ return False
78
+ if head_dim % 8 != 0:
79
+ return False
80
+ if num_threads % 32 != 0:
81
+ return False
82
+ return True
83
+
84
+ def _get_tiled_mma(self):
85
+ if const_expr(self.arch == 80):
86
+ num_mma_warps = self.num_threads // 32
87
+ atom_layout_dQ = (
88
+ (self.AtomLayoutMdQ, num_mma_warps // self.AtomLayoutMdQ, 1)
89
+ if const_expr(not self.dQ_swapAB)
90
+ else (num_mma_warps // self.AtomLayoutMdQ, self.AtomLayoutMdQ, 1)
91
+ )
92
+ tiled_mma = cute.make_tiled_mma(
93
+ warp.MmaF16BF16Op(self.dtype, Float32, (16, 8, 16)),
94
+ atom_layout_dQ,
95
+ permutation_mnk=(atom_layout_dQ[0] * 16, atom_layout_dQ[1] * 16, 16),
96
+ )
97
+ elif const_expr(self.arch == 90):
98
+ num_mma_warp_groups = self.num_threads // 128
99
+ atom_layout_dQ = (self.AtomLayoutMdQ, num_mma_warp_groups // self.AtomLayoutMdQ)
100
+ tiler_mn_dQ = (self.tile_m // atom_layout_dQ[0], self.tile_hdim // atom_layout_dQ[1])
101
+ tiled_mma = sm90_utils_basic.make_trivial_tiled_mma(
102
+ self.dtype,
103
+ self.dtype,
104
+ warpgroup.OperandMajorMode.K, # These don't matter, we only care about the accum
105
+ warpgroup.OperandMajorMode.K,
106
+ Float32,
107
+ atom_layout_mnk=(atom_layout_dQ if not self.dQ_swapAB else atom_layout_dQ[::-1])
108
+ + (1,),
109
+ tiler_mn=tiler_mn_dQ if not self.dQ_swapAB else tiler_mn_dQ[::-1],
110
+ )
111
+ else:
112
+ cta_group = tcgen05.CtaGroup.ONE
113
+ tiled_mma = sm100_utils_basic.make_trivial_tiled_mma(
114
+ self.dtype,
115
+ tcgen05.OperandMajorMode.MN, # dS_major_mode
116
+ tcgen05.OperandMajorMode.MN, # Kt_major_mode
117
+ Float32,
118
+ cta_group,
119
+ (self.tile_m, self.tile_hdim),
120
+ )
121
+ if const_expr(self.arch in [80, 90]):
122
+ assert self.num_threads == tiled_mma.size
123
+ return tiled_mma
124
+
125
+ def _setup_attributes(self):
126
+ # ///////////////////////////////////////////////////////////////////////////////
127
+ # GMEM Tiled copy:
128
+ # ///////////////////////////////////////////////////////////////////////////////
129
+ # Thread layouts for copies
130
+ universal_copy_bits = 128
131
+ async_copy_elems_accum = universal_copy_bits // Float32.width
132
+ atom_async_copy_accum = cute.make_copy_atom(
133
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
134
+ Float32,
135
+ num_bits_per_copy=universal_copy_bits,
136
+ )
137
+ # We don't do bound checking for the gmem -> smem load so we just assert here.
138
+ assert (self.tile_m * self.tile_hdim // async_copy_elems_accum) % self.num_threads == 0
139
+ self.g2s_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
140
+ atom_async_copy_accum,
141
+ cute.make_layout(self.num_threads),
142
+ cute.make_layout(async_copy_elems_accum),
143
+ )
144
+ num_s2r_copy_elems = 1 if const_expr(self.arch == 80) else 4
145
+ if const_expr(self.arch == 80):
146
+ self.s2r_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
147
+ Float32, self.num_threads, num_s2r_copy_elems
148
+ )
149
+ self.sdQaccum_layout = cute.make_layout(self.tile_m * self.tile_hdim)
150
+ elif const_expr(self.arch == 90):
151
+ num_threads_per_warp_group = 128
152
+ num_mma_warp_groups = self.num_threads // 128
153
+ self.s2r_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
154
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
155
+ cute.make_layout((num_threads_per_warp_group, num_mma_warp_groups)), # thr_layout
156
+ cute.make_layout(128 // Float32.width), # val_layout
157
+ )
158
+ self.sdQaccum_layout = cute.make_layout(
159
+ (self.tile_m * self.tile_hdim // num_mma_warp_groups, num_mma_warp_groups)
160
+ )
161
+ else:
162
+ self.dQ_reduce_ncol = 32
163
+ dQaccum_reduce_stage = self.tile_hdim // self.dQ_reduce_ncol
164
+ assert self.num_threads == 128 # TODO: currently hard-coded
165
+ self.s2r_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
166
+ Float32, self.num_threads, num_s2r_copy_elems
167
+ )
168
+ self.sdQaccum_layout = cute.make_layout(
169
+ (self.tile_m * self.tile_hdim // dQaccum_reduce_stage, dQaccum_reduce_stage)
170
+ )
171
+
172
+ self.gmem_tiled_copy_dQ = copy_utils.tiled_copy_2d(
173
+ self.dtype, self.tile_hdim, self.num_threads
174
+ )
175
+ # ///////////////////////////////////////////////////////////////////////////////
176
+ # Shared memory layout: dQ
177
+ # ///////////////////////////////////////////////////////////////////////////////
178
+ # We can't just use kHeadDim here. E.g. if MMA shape is 64 x 96 but split across 2 WGs,
179
+ # then setting kBlockKSmem to 32 will cause "Static shape_div failure".
180
+ # We want to treat it as 64 x 48, so kBlockKSmem should be 16.
181
+ mma_shape_n = self.tiled_mma.get_tile_size(1)
182
+ if const_expr(self.arch == 80):
183
+ sdQ_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, mma_shape_n)
184
+ self.sdQ_layout = cute.tile_to_shape(
185
+ sdQ_layout_atom, (self.tile_m, self.tile_hdim), (0, 1)
186
+ )
187
+ elif const_expr(self.arch == 90):
188
+ self.sdQ_layout = sm90_utils.make_smem_layout(
189
+ self.dtype, LayoutEnum.ROW_MAJOR, (self.tile_m, self.tile_hdim)
190
+ )
191
+ else:
192
+ # TODO: this is hard-coded for hdim 128
193
+ self.sdQ_layout = sm100_utils_basic.make_smem_layout_epi(
194
+ self.dtype, LayoutEnum.ROW_MAJOR, (self.tile_m, self.tile_hdim), 1
195
+ )
196
+
197
+ @cute.jit
198
+ def __call__(
199
+ self,
200
+ mdQaccum: cute.Tensor,
201
+ mdQ: cute.Tensor,
202
+ scale: cutlass.Float32,
203
+ mCuSeqlensQ: Optional[cute.Tensor],
204
+ mSeqUsedQ: Optional[cute.Tensor],
205
+ stream: cuda.CUstream,
206
+ ):
207
+ # Get the data type and check if it is fp16 or bf16
208
+ if const_expr(mdQ.element_type not in [cutlass.Float16, cutlass.BFloat16]):
209
+ raise TypeError("Only Float16 or BFloat16 is supported")
210
+ if const_expr(mdQaccum is not None):
211
+ if const_expr(mdQaccum.element_type not in [cutlass.Float32]):
212
+ raise TypeError("dQaccum tensor must be Float32")
213
+
214
+ # Assume all strides are divisible by 128 bits except the last stride
215
+ new_stride = lambda t: (
216
+ *(cute.assume(s, divby=128 // t.element_type.width) for s in t.stride[:-1]),
217
+ t.stride[-1],
218
+ )
219
+ mdQaccum, mdQ = [
220
+ cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
221
+ for t in (mdQaccum, mdQ)
222
+ ]
223
+
224
+ self.tiled_mma = self._get_tiled_mma()
225
+ self._setup_attributes()
226
+
227
+ smem_size = max(
228
+ cute.size_in_bytes(cutlass.Float32, self.sdQaccum_layout),
229
+ cute.size_in_bytes(self.dtype, self.sdQ_layout),
230
+ )
231
+
232
+ if const_expr(mCuSeqlensQ is not None):
233
+ TileScheduler = SingleTileVarlenScheduler
234
+ num_head = mdQ.shape[1]
235
+ num_batch = mCuSeqlensQ.shape[0] - 1
236
+ else:
237
+ TileScheduler = SingleTileScheduler
238
+ num_head = mdQ.shape[2]
239
+ num_batch = mdQ.shape[0]
240
+
241
+ tile_sched_args = TileSchedulerArguments(
242
+ num_block=cute.ceil_div(mdQ.shape[1], self.tile_m),
243
+ num_head=num_head,
244
+ num_batch=num_batch,
245
+ num_splits=1,
246
+ seqlen_k=0,
247
+ headdim=mdQ.shape[2],
248
+ headdim_v=0,
249
+ total_q=mdQ.shape[0],
250
+ tile_shape_mn=(self.tile_m, 1),
251
+ mCuSeqlensQ=mCuSeqlensQ,
252
+ mSeqUsedQ=mSeqUsedQ,
253
+ )
254
+
255
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
256
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
257
+
258
+ # grid_dim: (m_block, num_head, batch_size)
259
+ self.kernel(
260
+ mdQaccum,
261
+ mdQ,
262
+ mCuSeqlensQ,
263
+ mSeqUsedQ,
264
+ scale,
265
+ self.tiled_mma,
266
+ self.dQ_swapAB,
267
+ self.sdQaccum_layout,
268
+ self.sdQ_layout,
269
+ self.g2s_tiled_copy_dQaccum,
270
+ self.s2r_tiled_copy_dQaccum,
271
+ self.gmem_tiled_copy_dQ,
272
+ tile_sched_params,
273
+ TileScheduler,
274
+ ).launch(
275
+ grid=grid_dim,
276
+ block=[self.num_threads, 1, 1],
277
+ smem=smem_size,
278
+ stream=stream,
279
+ )
280
+
281
+ @cute.kernel
282
+ def kernel(
283
+ self,
284
+ mdQaccum: cute.Tensor,
285
+ mdQ: cute.Tensor,
286
+ mCuSeqlensQ: Optional[cute.Tensor],
287
+ mSeqUsedQ: Optional[cute.Tensor],
288
+ scale: cutlass.Float32,
289
+ tiled_mma: cute.TiledMma,
290
+ dQ_swapAB: cutlass.Constexpr,
291
+ sdQaccum_layout: cute.Layout,
292
+ sdQ_layout: cute.ComposedLayout,
293
+ g2s_tiled_copy_dQaccum: cute.TiledCopy,
294
+ s2r_tiled_copy_dQaccum: cute.TiledCopy,
295
+ gmem_tiled_copy_dQ: cute.TiledCopy,
296
+ tile_sched_params: ParamsBase,
297
+ TileScheduler: cutlass.Constexpr[Callable],
298
+ ):
299
+ # ///////////////////////////////////////////////////////////////////////////////
300
+ # Get shared memory buffer
301
+ # ///////////////////////////////////////////////////////////////////////////////
302
+ smem = cutlass.utils.SmemAllocator()
303
+ sdQaccum = smem.allocate_tensor(cutlass.Float32, sdQaccum_layout, byte_alignment=1024)
304
+ sdQaccum_flat = cute.make_tensor(sdQaccum.iterator, cute.make_layout(cute.size(sdQaccum)))
305
+ if const_expr(self.arch in [80, 90]):
306
+ sdQ = cute.make_tensor(cute.recast_ptr(sdQaccum.iterator, dtype=self.dtype), sdQ_layout)
307
+ else:
308
+ # extra stage dimension
309
+ sdQ = cute.make_tensor(
310
+ cute.recast_ptr(sdQaccum.iterator, sdQ_layout.inner, dtype=self.dtype),
311
+ sdQ_layout.outer,
312
+ )[None, None, 0]
313
+ sdQt = utils.transpose_view(sdQ)
314
+
315
+ # Thread index, block index
316
+ tidx, _, _ = cute.arch.thread_idx()
317
+
318
+ tile_scheduler = TileScheduler.create(tile_sched_params)
319
+ work_tile = tile_scheduler.initial_work_tile_info()
320
+
321
+ m_block, num_head, batch_size, _ = work_tile.tile_idx
322
+
323
+ if work_tile.is_valid_tile:
324
+ # ///////////////////////////////////////////////////////////////////////////////
325
+ # Get the appropriate tiles for this thread block.
326
+ # ///////////////////////////////////////////////////////////////////////////////
327
+
328
+ seqlen = SeqlenInfoQK.create(
329
+ batch_size,
330
+ mdQ.shape[1],
331
+ 0,
332
+ mCuSeqlensQ=mCuSeqlensQ,
333
+ mCuSeqlensK=None,
334
+ mSeqUsedQ=mSeqUsedQ,
335
+ mSeqUsedK=None,
336
+ )
337
+ if const_expr(not seqlen.has_cu_seqlens_q):
338
+ mdQ_cur = mdQ[batch_size, None, num_head, None]
339
+ mdQaccum_cur = mdQaccum[batch_size, num_head, None]
340
+ head_dim = mdQ.shape[3]
341
+ else:
342
+ padded_offset_q = seqlen.offset_q + batch_size * self.tile_m
343
+ mdQ_cur = cute.domain_offset((seqlen.offset_q, 0), mdQ[None, num_head, None])
344
+ mdQaccum_cur = cute.domain_offset(
345
+ (padded_offset_q * self.tile_hdim,), mdQaccum[num_head, None]
346
+ )
347
+ head_dim = mdQ.shape[2]
348
+
349
+ # HACK: Compiler doesn't seem to recognize that padding
350
+ # by padded_offset_q * self.tile_hdim keeps alignment
351
+ # since statically divisible by 4
352
+
353
+ mdQaccum_cur_ptr = cute.make_ptr(
354
+ dtype=mdQaccum_cur.element_type,
355
+ value=mdQaccum_cur.iterator.toint(),
356
+ mem_space=mdQaccum_cur.iterator.memspace,
357
+ assumed_align=mdQaccum.iterator.alignment,
358
+ )
359
+ mdQaccum_cur = cute.make_tensor(mdQaccum_cur_ptr, mdQaccum_cur.layout)
360
+
361
+ gdQaccum = cute.local_tile(mdQaccum_cur, (self.tile_m * self.tile_hdim,), (m_block,))
362
+ gdQ = cute.local_tile(mdQ_cur, (self.tile_m, self.tile_hdim), (m_block, 0))
363
+
364
+ seqlen_q = seqlen.seqlen_q
365
+ seqlen_q_rounded = cute.round_up(seqlen_q, self.tile_m)
366
+
367
+ # Step 1: load dQaccum from gmem to smem
368
+ g2s_thr_copy_dQaccum = g2s_tiled_copy_dQaccum.get_slice(tidx)
369
+ tdQgdQaccum = g2s_thr_copy_dQaccum.partition_S(gdQaccum)
370
+ tdQsdQaccumg2s = g2s_thr_copy_dQaccum.partition_D(sdQaccum_flat)
371
+ cute.copy(g2s_tiled_copy_dQaccum, tdQgdQaccum, tdQsdQaccumg2s)
372
+ cute.arch.cp_async_commit_group()
373
+ cute.arch.cp_async_wait_group(0)
374
+ cute.arch.barrier()
375
+
376
+ # Step 2: load dQ from smem to rmem
377
+ s2r_thr_copy_dQaccum = s2r_tiled_copy_dQaccum.get_slice(tidx)
378
+ tdQsdQaccum = s2r_thr_copy_dQaccum.partition_S(sdQaccum)
379
+ tile_shape = (self.tile_m, self.tile_hdim)
380
+ acc = None
381
+ tiled_copy_t2r = None
382
+ if const_expr(self.arch in [80, 90]):
383
+ acc_shape = tiled_mma.partition_shape_C(
384
+ tile_shape if const_expr(not dQ_swapAB) else tile_shape[::-1]
385
+ )
386
+ acc = cute.make_fragment(acc_shape, cutlass.Float32)
387
+ assert cute.size(acc) == cute.size(tdQsdQaccum)
388
+ else:
389
+ thr_mma = tiled_mma.get_slice(0) # 1-CTA
390
+ dQacc_shape = tiled_mma.partition_shape_C((self.tile_m, self.tile_hdim))
391
+ tdQtdQ = tiled_mma.make_fragment_C(dQacc_shape)
392
+ tdQcdQ = thr_mma.partition_C(
393
+ cute.make_identity_tensor((self.tile_m, self.tile_hdim))
394
+ )
395
+ tmem_load_atom = cute.make_copy_atom(
396
+ tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(self.dQ_reduce_ncol)), Float32
397
+ )
398
+ tiled_copy_t2r = tcgen05.make_tmem_copy(tmem_load_atom, tdQtdQ)
399
+ thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
400
+ tdQrdQ_t2r_shape = thr_copy_t2r.partition_D(tdQcdQ).shape
401
+ acc = cute.make_fragment(tdQrdQ_t2r_shape, Float32)
402
+ tdQrdQaccum = cute.make_tensor(acc.iterator, cute.make_layout(tdQsdQaccum.shape))
403
+ cute.autovec_copy(tdQsdQaccum, tdQrdQaccum)
404
+ # Convert tdQrdQaccum from fp32 to fp16/bf16
405
+ rdQ = cute.make_fragment_like(acc, self.dtype)
406
+ rdQ.store((acc.load() * scale).to(self.dtype))
407
+
408
+ # Step 3: Copy dQ from register to smem
409
+ cute.arch.barrier() # make sure all threads have finished loading dQaccum
410
+ if const_expr(self.arch in [80, 90]):
411
+ copy_atom_r2s_dQ = utils.get_smem_store_atom(
412
+ self.arch, self.dtype, transpose=self.dQ_swapAB
413
+ )
414
+ tiled_copy_r2s_dQ = cute.make_tiled_copy_C(copy_atom_r2s_dQ, tiled_mma)
415
+ else:
416
+ # copy_atom_r2s_dQ = sm100_utils_basic.get_smem_store_op(
417
+ # LayoutEnum.ROW_MAJOR, self.dtype, Float32, tiled_copy_t2r,
418
+ # )
419
+ # tiled_copy_r2s_dQ = cute.make_tiled_copy_D(copy_atom_r2s_dQ, tiled_copy_t2r)
420
+ thr_layout_r2s_dQ = cute.make_layout((self.num_threads, 1)) # 128 threads
421
+ val_layout_r2s_dQ = cute.make_layout((1, 128 // self.dtype.width))
422
+ copy_atom_r2s_dQ = cute.make_copy_atom(
423
+ cute.nvgpu.CopyUniversalOp(),
424
+ self.dtype,
425
+ num_bits_per_copy=128,
426
+ )
427
+ tiled_copy_r2s_dQ = cute.make_tiled_copy_tv(
428
+ copy_atom_r2s_dQ, thr_layout_r2s_dQ, val_layout_r2s_dQ
429
+ )
430
+ thr_copy_r2s_dQ = tiled_copy_r2s_dQ.get_slice(tidx)
431
+ cdQ = cute.make_identity_tensor((self.tile_m, self.tile_hdim))
432
+ if const_expr(self.arch in [80, 90]):
433
+ taccdQrdQ = thr_copy_r2s_dQ.retile(rdQ)
434
+ else:
435
+ taccdQcdQ_shape = thr_copy_r2s_dQ.partition_S(cdQ).shape
436
+ taccdQrdQ = cute.make_tensor(rdQ.iterator, taccdQcdQ_shape)
437
+ taccdQsdQ = thr_copy_r2s_dQ.partition_D(sdQ if const_expr(not self.dQ_swapAB) else sdQt)
438
+ cute.copy(thr_copy_r2s_dQ, taccdQrdQ, taccdQsdQ)
439
+
440
+ # Step 4: Copy dQ from smem to register to prepare for coalesced write to gmem
441
+ cute.arch.barrier() # make sure all smem stores are done
442
+ gmem_thr_copy_dQ = gmem_tiled_copy_dQ.get_slice(tidx)
443
+ tdQgdQ = gmem_thr_copy_dQ.partition_S(gdQ)
444
+ tdQsdQ = gmem_thr_copy_dQ.partition_D(sdQ)
445
+ tdQrdQ = cute.make_fragment_like(tdQsdQ, self.dtype)
446
+ # TODO: check OOB when reading from smem if kBlockM isn't evenly tiled
447
+ cute.autovec_copy(tdQsdQ, tdQrdQ)
448
+
449
+ # Step 5: Copy dQ from register to gmem
450
+ tdQcdQ = gmem_thr_copy_dQ.partition_S(cdQ)
451
+ tdQpdQ = utils.predicate_k(tdQcdQ, limit=head_dim)
452
+ for rest_m in cutlass.range(cute.size(tdQrdQ.shape[1]), unroll_full=True):
453
+ if tdQcdQ[0, rest_m, 0][0] < seqlen_q - m_block * self.tile_m:
454
+ cute.copy(
455
+ gmem_tiled_copy_dQ,
456
+ tdQrdQ[None, rest_m, None],
457
+ tdQgdQ[None, rest_m, None],
458
+ pred=tdQpdQ[None, rest_m, None],
459
+ )
460
+
461
+
462
+ class FlashAttentionBackwardPostprocess_sm100(FlashAttentionBackwardPostprocess):
463
+ def __init__(
464
+ self,
465
+ dtype: Type[cutlass.Numeric],
466
+ head_dim: int,
467
+ tile_m: int = 128,
468
+ num_threads: int = 256,
469
+ AtomLayoutMdQ: int = 1,
470
+ dQ_swapAB: bool = False,
471
+ ):
472
+ super().__init__(
473
+ dtype=dtype,
474
+ head_dim=head_dim,
475
+ arch=90, # tmp dummy placement for now
476
+ tile_m=tile_m,
477
+ num_threads=num_threads,
478
+ AtomLayoutMdQ=AtomLayoutMdQ,
479
+ dQ_swapAB=dQ_swapAB,
480
+ )
481
+
482
+ def _setup_attributes(self):
483
+ self.num_stages = self.tile_hdim // 32 # 2 for D=64, 4 for D=128
484
+
485
+ self.sdQaccum_layout = cute.make_layout(
486
+ shape=(self.tile_m * 32, 2), stride=(1, self.tile_m * 32)
487
+ )
488
+ self.epi_tile_q = (self.tile_m, self.tile_hdim)
489
+ self.sdQ_layout = sm100_utils_basic.make_smem_layout_epi(
490
+ self.dtype,
491
+ LayoutEnum.ROW_MAJOR,
492
+ self.epi_tile_q,
493
+ 1,
494
+ )
495
+
496
+ @cute.jit
497
+ def __call__(
498
+ self,
499
+ mdQaccum: cute.Tensor,
500
+ mdQ: cute.Tensor,
501
+ scale: cutlass.Float32,
502
+ stream: cuda.CUstream,
503
+ ):
504
+ # Assume all strides are divisible by 128 bits except the last stride
505
+ new_stride = lambda t: (
506
+ *(cute.assume(s, divby=128 // t.element_type.width) for s in t.stride[:-1]),
507
+ t.stride[-1],
508
+ )
509
+ mdQaccum, mdQ = [
510
+ cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
511
+ for t in (mdQaccum, mdQ)
512
+ ]
513
+ # (b, h, s*d) -> (s*d, h, b)
514
+ mdQaccum = cute.make_tensor(mdQaccum.iterator, cute.select(mdQaccum.layout, mode=[2, 1, 0]))
515
+ # (b, s, h, d) -> (s, d, h, b)
516
+ mdQ = cute.make_tensor(mdQ.iterator, cute.select(mdQ.layout, mode=[1, 3, 2, 0]))
517
+
518
+ self._setup_attributes()
519
+
520
+ grid_dim = [
521
+ cute.ceil_div(mdQ.shape[0], self.tile_m),
522
+ cute.size(mdQ.shape[2]),
523
+ cute.size(mdQ.shape[3]),
524
+ ]
525
+
526
+ cta_group = tcgen05.CtaGroup.ONE
527
+ self.mma_tiler_dsk = (self.tile_m, self.tile_hdim)
528
+
529
+ dS_major_mode = tcgen05.OperandMajorMode.MN
530
+ kt_major_mode_dsq = tcgen05.OperandMajorMode.MN
531
+
532
+ tiled_mma_dsk = sm100_utils_basic.make_trivial_tiled_mma(
533
+ cutlass.BFloat16,
534
+ dS_major_mode,
535
+ kt_major_mode_dsq,
536
+ cutlass.Float32,
537
+ cta_group,
538
+ self.mma_tiler_dsk,
539
+ )
540
+
541
+ dQ_cta_v_layout = cute.composition(cute.make_identity_layout(mdQ.shape), self.mma_tiler_dsk)
542
+ tma_store_op = cpasync.CopyBulkTensorTileS2GOp()
543
+ tma_atom_dQ, tma_tensor_dQ = cute.nvgpu.cpasync.make_tiled_tma_atom(
544
+ tma_store_op,
545
+ mdQ,
546
+ cute.select(self.sdQ_layout, mode=[0, 1]),
547
+ dQ_cta_v_layout,
548
+ )
549
+
550
+ buffer_align_bytes = 1024
551
+
552
+ @cute.struct
553
+ class SharedStorage:
554
+ sdQaccum: cute.struct.Align[
555
+ cute.struct.MemRange[cutlass.Float32, cute.cosize(self.sdQaccum_layout)],
556
+ 128,
557
+ ]
558
+
559
+ sdQ: cute.struct.Align[
560
+ cute.struct.MemRange[self.dtype, cute.cosize(self.sdQ_layout)],
561
+ buffer_align_bytes,
562
+ ]
563
+
564
+ self.shared_storage = SharedStorage
565
+
566
+ self.kernel(
567
+ mdQaccum,
568
+ tma_tensor_dQ,
569
+ tma_atom_dQ,
570
+ self.sdQaccum_layout,
571
+ self.sdQ_layout,
572
+ tiled_mma_dsk,
573
+ scale,
574
+ ).launch(
575
+ grid=grid_dim,
576
+ block=[self.num_threads, 1, 1],
577
+ smem=self.shared_storage.size_in_bytes(),
578
+ stream=stream,
579
+ )
580
+
581
+ @cute.kernel
582
+ def kernel(
583
+ self,
584
+ mdQaccum: cute.Tensor,
585
+ mdQ: cute.Tensor,
586
+ tma_atom_dQ: cute.CopyAtom,
587
+ sdQaccum_layout: cute.Layout,
588
+ sdQ_layout: cute.ComposedLayout,
589
+ tiled_mma_dsk: cute.TiledMma,
590
+ scale: cutlass.Float32,
591
+ ):
592
+ tidx = cute.arch.thread_idx()[0]
593
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
594
+ m_block, head_idx, batch_idx = cute.arch.block_idx()
595
+
596
+ # SMEM
597
+ smem = cutlass.utils.SmemAllocator()
598
+ storage = smem.allocate(self.shared_storage)
599
+ swz128 = cute.make_swizzle(3, 4, 3)
600
+ sdQaccum = storage.sdQaccum.get_tensor(sdQaccum_layout, swizzle=swz128)
601
+
602
+ sdQ = storage.sdQ.get_tensor(sdQ_layout.outer, swizzle=sdQ_layout.inner)
603
+
604
+ mdQaccum_cur = mdQaccum[None, head_idx, batch_idx]
605
+ mdQ_cur = mdQ[None, None, head_idx, batch_idx]
606
+
607
+ thr_mma_dsk = tiled_mma_dsk.get_slice(tidx)
608
+ dQacc_shape = thr_mma_dsk.partition_shape_C(self.mma_tiler_dsk[:2])
609
+ tdQtdQ = thr_mma_dsk.make_fragment_C(dQacc_shape)
610
+ tdQtdQ = cute.make_tensor(tdQtdQ.iterator, tdQtdQ.layout)
611
+
612
+ tmem_ld_atom = cute.make_copy_atom(
613
+ tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(32)), cutlass.Float32
614
+ )
615
+ tiled_tmem_ld = tcgen05.make_tmem_copy(tmem_ld_atom, tdQtdQ)
616
+ thr_tmem_ld = tiled_tmem_ld.get_slice(tidx)
617
+
618
+ cdQ = cute.make_identity_tensor((self.mma_tiler_dsk[0], self.mma_tiler_dsk[1]))
619
+ tdQcdQ = thr_mma_dsk.partition_C(cdQ)
620
+ tdQcdQ_tensor = cute.make_tensor(tdQcdQ.iterator, tdQcdQ.layout)
621
+ tdQrdQ = thr_tmem_ld.partition_D(tdQcdQ_tensor)
622
+
623
+ gdQaccum = cute.local_tile(mdQaccum_cur, (self.tile_m * self.tile_hdim,), (m_block,))
624
+
625
+ num_reduce_warps = 4
626
+ num_reduce_threads = cute.arch.WARP_SIZE * num_reduce_warps
627
+
628
+ atom_universal_copy = cute.make_copy_atom(
629
+ cute.nvgpu.CopyUniversalOp(), cutlass.Float32, num_bits_per_copy=128
630
+ )
631
+ tiler_mn, layout_tv = cute.make_layout_tv(
632
+ thr_layout=cute.make_layout(shape=num_reduce_threads, stride=1),
633
+ val_layout=cute.make_layout(shape=4, stride=1),
634
+ )
635
+ G2S_tiled_copy_dQaccum = cute.make_tiled_copy(
636
+ atom_universal_copy, layout_tv=layout_tv, tiler_mn=tiler_mn
637
+ )
638
+
639
+ smem_thr_copy_g2s = G2S_tiled_copy_dQaccum.get_slice(tidx)
640
+
641
+ # S->R
642
+ tdQrdQ_t2r = cute.make_fragment(tdQrdQ.shape, cutlass.Float32)
643
+ tiled_smem_store_s2r = cute.make_tiled_copy(
644
+ atom_universal_copy, layout_tv=layout_tv, tiler_mn=tiler_mn
645
+ )
646
+
647
+ s2r_thr_copy_dQaccum = tiled_smem_store_s2r.get_slice(tidx)
648
+ tdQsdQ_s2r = s2r_thr_copy_dQaccum.partition_S(sdQaccum)
649
+ tdQrdQ_s2r = cute.make_tensor(tdQrdQ_t2r.iterator, tdQrdQ_t2r.shape)
650
+
651
+ # R->S
652
+ smem_copy_atom = sm100_utils_basic.get_smem_store_op(
653
+ LayoutEnum.ROW_MAJOR, self.dtype, cutlass.Float32, tiled_tmem_ld
654
+ )
655
+ tiled_smem_store_r2s = cute.make_tiled_copy(
656
+ smem_copy_atom,
657
+ layout_tv=tiled_tmem_ld.layout_dst_tv_tiled,
658
+ tiler_mn=tiled_tmem_ld.tiler_mn,
659
+ )
660
+ tdQsdQ_r2s = thr_tmem_ld.partition_D(thr_mma_dsk.partition_C(sdQ))
661
+ tdQrdQ_r2s = cute.make_fragment(tdQsdQ_r2s.shape, self.dtype)
662
+
663
+ num_stages = cute.size(tdQrdQ_t2r, mode=[1])
664
+ for stage in cutlass.range_constexpr(num_stages):
665
+ # G->S
666
+ gdQaccum_stage = cute.local_tile(
667
+ gdQaccum,
668
+ (self.tile_m * 32,),
669
+ (stage,),
670
+ )
671
+
672
+ gdQaccum_layout_g2s = cute.make_layout(shape=(self.tile_m * 32, 1), stride=(1, 0))
673
+ gdQaccum_stage_g2s = cute.make_tensor(
674
+ cute.recast_ptr(gdQaccum_stage.iterator, swizzle_=swz128), gdQaccum_layout_g2s
675
+ )
676
+
677
+ tdQgdQ = smem_thr_copy_g2s.partition_S(gdQaccum_stage_g2s)
678
+ tdQsdQ = smem_thr_copy_g2s.partition_D(sdQaccum)
679
+
680
+ cute.copy(smem_thr_copy_g2s, tdQgdQ[None, None, 0], tdQsdQ[None, None, 0])
681
+
682
+ cute.arch.fence_proxy(
683
+ cute.arch.ProxyKind.async_shared, space=cute.arch.SharedSpace.shared_cta
684
+ )
685
+ cute.arch.barrier(barrier_id=6, number_of_threads=num_reduce_threads)
686
+
687
+ # S -> R
688
+ tdQrdQ_s2r_cpy = tdQrdQ_s2r[None, stage, None, None]
689
+ tdQsdQ_s2r_p = tdQsdQ_s2r[None, None, 0]
690
+ tdQrdQ_r2s_cpy = cute.make_tensor(
691
+ tdQrdQ_s2r_cpy.iterator, cute.make_layout(tdQsdQ_s2r_p.shape)
692
+ )
693
+
694
+ cute.copy(s2r_thr_copy_dQaccum, tdQsdQ_s2r_p, tdQrdQ_r2s_cpy)
695
+
696
+ cute.arch.fence_proxy(
697
+ cute.arch.ProxyKind.async_shared, space=cute.arch.SharedSpace.shared_cta
698
+ )
699
+ cute.arch.barrier(barrier_id=7, number_of_threads=num_reduce_threads)
700
+
701
+ # R->S
702
+ tdQrdQ_r2s_cpy = cute.make_tensor(
703
+ cute.recast_ptr(tdQrdQ_r2s_cpy.iterator),
704
+ tdQrdQ_r2s[((None, 0), stage, 0, 0, 0)].shape,
705
+ )
706
+ dQ_vec = tdQrdQ_r2s_cpy.load() * scale
707
+ tdQrdQ_r2s[((None, 0), stage, 0, 0, 0)].store(dQ_vec.to(self.dtype))
708
+
709
+ cute.copy(
710
+ tiled_smem_store_r2s,
711
+ tdQrdQ_r2s[None, None, None, None, 0],
712
+ tdQsdQ_r2s[None, None, None, None, 0],
713
+ )
714
+ cute.arch.fence_proxy(
715
+ cute.arch.ProxyKind.async_shared, space=cute.arch.SharedSpace.shared_cta
716
+ )
717
+ cute.arch.barrier(barrier_id=8, number_of_threads=num_reduce_threads)
718
+
719
+ # S-> G
720
+ gdQ = cute.local_tile(mdQ_cur, (self.tile_m, self.tile_hdim), (None, 0))
721
+ tdQsdQ, tdQgdQ = cpasync.tma_partition(
722
+ tma_atom_dQ,
723
+ 0,
724
+ cute.make_layout(1),
725
+ cute.group_modes(sdQ, 0, 2),
726
+ cute.group_modes(gdQ, 0, 2),
727
+ )
728
+
729
+ cute.copy(tma_atom_dQ, tdQsdQ[None, 0], tdQgdQ[None, m_block])
build/torch-universal/flash_bwd_preprocess.py ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_bwd_preprocess_kernel.h
3
+ # from Cutlass C++ to Cute-DSL.
4
+ import math
5
+ import operator
6
+ from typing import Callable, Type, Optional
7
+
8
+ import cuda.bindings.driver as cuda
9
+
10
+ import cutlass
11
+ import cutlass.cute as cute
12
+ from cutlass import Float32
13
+
14
+ from . import utils
15
+ from . import copy_utils
16
+ from .seqlen_info import SeqlenInfoQK
17
+ from .tile_scheduler import (
18
+ ParamsBase,
19
+ SingleTileScheduler,
20
+ SingleTileVarlenScheduler,
21
+ TileSchedulerArguments,
22
+ )
23
+
24
+
25
+ class FlashAttentionBackwardPreprocess:
26
+ def __init__(
27
+ self,
28
+ dtype: Type[cutlass.Numeric],
29
+ head_dim: int,
30
+ m_block_size: int = 128,
31
+ num_threads: int = 128,
32
+ ):
33
+ """
34
+ All contiguous dimensions must be at least 16 bytes aligned which indicates the head dimension
35
+ should be a multiple of 8.
36
+
37
+ :param head_dim: head dimension
38
+ :type head_dim: int
39
+ :param m_block_size: m block size
40
+ :type m_block_size: int
41
+ :param num_threads: number of threads
42
+ :type num_threads: int
43
+ """
44
+ self.dtype = dtype
45
+ self.m_block_size = m_block_size
46
+ # padding head_dim to a multiple of 32 as k_block_size
47
+ hdim_multiple_of = 32
48
+ self.head_dim_padded = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
49
+ self.check_hdim_oob = head_dim != self.head_dim_padded
50
+ self.num_threads = num_threads
51
+
52
+ @staticmethod
53
+ def can_implement(dtype, head_dim, m_block_size, num_threads) -> bool:
54
+ """Check if the kernel can be implemented with the given parameters.
55
+
56
+ :param dtype: data type
57
+ :type dtype: cutlass.Numeric
58
+ :param head_dim: head dimension
59
+ :type head_dim: int
60
+ :param m_block_size: m block size
61
+ :type m_block_size: int
62
+ :param num_threads: number of threads
63
+ :type num_threads: int
64
+
65
+ :return: True if the kernel can be implemented, False otherwise
66
+ :rtype: bool
67
+ """
68
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
69
+ return False
70
+ if head_dim % 8 != 0:
71
+ return False
72
+ if num_threads % 32 != 0:
73
+ return False
74
+ if num_threads < m_block_size: # For multiplying lse with log2
75
+ return False
76
+ return True
77
+
78
+ def _setup_attributes(self):
79
+ # ///////////////////////////////////////////////////////////////////////////////
80
+ # GMEM Tiled copy:
81
+ # ///////////////////////////////////////////////////////////////////////////////
82
+ # Thread layouts for copies
83
+ # We want kBlockKGmem to be a power of 2 so that when we do the summing,
84
+ # it's just between threads in the same warp
85
+ gmem_k_block_size = (
86
+ 128
87
+ if self.head_dim_padded % 128 == 0
88
+ else (
89
+ 64
90
+ if self.head_dim_padded % 64 == 0
91
+ else (32 if self.head_dim_padded % 32 == 0 else 16)
92
+ )
93
+ )
94
+ self.gmem_tiled_copy_O = copy_utils.tiled_copy_2d(
95
+ self.dtype, gmem_k_block_size, self.num_threads
96
+ )
97
+ universal_copy_bits = 128
98
+ num_copy_elems_dQaccum = universal_copy_bits // Float32.width
99
+ assert (
100
+ self.m_block_size * self.head_dim_padded // num_copy_elems_dQaccum
101
+ ) % self.num_threads == 0
102
+ self.gmem_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
103
+ Float32, self.num_threads, num_copy_elems_dQaccum
104
+ )
105
+
106
+ @cute.jit
107
+ def __call__(
108
+ self,
109
+ mO: cute.Tensor,
110
+ mdO: cute.Tensor,
111
+ mdPsum: cute.Tensor,
112
+ mLSE: Optional[cute.Tensor],
113
+ mLSElog2: Optional[cute.Tensor],
114
+ mdQaccum: Optional[cute.Tensor],
115
+ mCuSeqlensQ: Optional[cute.Tensor],
116
+ mSeqUsedQ: Optional[cute.Tensor],
117
+ stream: cuda.CUstream,
118
+ ):
119
+ # Get the data type and check if it is fp16 or bf16
120
+ if cutlass.const_expr(not (mO.element_type == mdO.element_type)):
121
+ raise TypeError("All tensors must have the same data type")
122
+ if cutlass.const_expr(mO.element_type not in [cutlass.Float16, cutlass.BFloat16]):
123
+ raise TypeError("Only Float16 or BFloat16 is supported")
124
+ if cutlass.const_expr(mdPsum.element_type not in [Float32]):
125
+ raise TypeError("dPsum tensor must be Float32")
126
+ if cutlass.const_expr(mdQaccum is not None):
127
+ if cutlass.const_expr(mdQaccum.element_type not in [Float32]):
128
+ raise TypeError("dQaccum tensor must be Float32")
129
+ if cutlass.const_expr(mLSE is not None):
130
+ assert mLSElog2 is not None, "If mLSE is provided, mLSElog2 must also be provided"
131
+ if cutlass.const_expr(mLSE.element_type not in [Float32]):
132
+ raise TypeError("LSE tensor must be Float32")
133
+ if cutlass.const_expr(mLSElog2.element_type not in [Float32]):
134
+ raise TypeError("LSElog2 tensor must be Float32")
135
+
136
+ # Assume all strides are divisible by 128 bits except the last stride
137
+ new_stride = lambda t: (
138
+ *(cute.assume(s, divby=128 // t.element_type.width) for s in t.stride[:-1]),
139
+ t.stride[-1],
140
+ )
141
+ mO, mdO, mdQaccum = [
142
+ cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
143
+ if t is not None
144
+ else None
145
+ for t in (mO, mdO, mdQaccum)
146
+ ]
147
+
148
+ self._setup_attributes()
149
+
150
+ if cutlass.const_expr(mCuSeqlensQ is not None):
151
+ TileScheduler = SingleTileVarlenScheduler
152
+ num_head = mO.shape[1]
153
+ num_batch = mCuSeqlensQ.shape[0] - 1
154
+ else:
155
+ TileScheduler = SingleTileScheduler
156
+ num_head = mO.shape[2]
157
+ num_batch = mO.shape[0]
158
+
159
+ tile_sched_args = TileSchedulerArguments(
160
+ num_block=cute.ceil_div(mO.shape[1], self.m_block_size),
161
+ num_head=num_head,
162
+ num_batch=num_batch,
163
+ num_splits=1,
164
+ seqlen_k=0,
165
+ headdim=0,
166
+ headdim_v=mO.shape[2],
167
+ total_q=mO.shape[0],
168
+ tile_shape_mn=(self.m_block_size, 1),
169
+ mCuSeqlensQ=mCuSeqlensQ,
170
+ mSeqUsedQ=mSeqUsedQ,
171
+ )
172
+
173
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
174
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
175
+
176
+ self.kernel(
177
+ mO,
178
+ mdO,
179
+ mdPsum,
180
+ mLSE,
181
+ mLSElog2,
182
+ mdQaccum,
183
+ mCuSeqlensQ,
184
+ mSeqUsedQ,
185
+ self.gmem_tiled_copy_O,
186
+ self.gmem_tiled_copy_dQaccum,
187
+ tile_sched_params,
188
+ TileScheduler,
189
+ ).launch(
190
+ grid=grid_dim,
191
+ block=[self.num_threads, 1, 1],
192
+ stream=stream,
193
+ )
194
+
195
+ @cute.kernel
196
+ def kernel(
197
+ self,
198
+ mO: cute.Tensor,
199
+ mdO: cute.Tensor,
200
+ mdPsum: cute.Tensor,
201
+ mLSE: Optional[cute.Tensor],
202
+ mLSElog2: Optional[cute.Tensor],
203
+ mdQaccum: Optional[cute.Tensor],
204
+ mCuSeqlensQ: Optional[cute.Tensor],
205
+ mSeqUsedQ: Optional[cute.Tensor],
206
+ gmem_tiled_copy_O: cute.TiledCopy,
207
+ gmem_tiled_copy_dQaccum: cute.TiledCopy,
208
+ tile_sched_params: ParamsBase,
209
+ TileScheduler: cutlass.Constexpr[Callable],
210
+ ):
211
+ # Thread index, block index
212
+ tidx, _, _ = cute.arch.thread_idx()
213
+
214
+ tile_scheduler = TileScheduler.create(tile_sched_params)
215
+ work_tile = tile_scheduler.initial_work_tile_info()
216
+ m_block, num_head, batch_size, _ = work_tile.tile_idx
217
+
218
+ if work_tile.is_valid_tile:
219
+ # ///////////////////////////////////////////////////////////////////////////////
220
+ # Get the appropriate tiles for this thread block.
221
+ # ///////////////////////////////////////////////////////////////////////////////
222
+ seqlen = SeqlenInfoQK.create(
223
+ batch_size,
224
+ mO.shape[1],
225
+ 0,
226
+ mCuSeqlensQ=mCuSeqlensQ,
227
+ mCuSeqlensK=None,
228
+ mSeqUsedQ=mSeqUsedQ,
229
+ mSeqUsedK=None,
230
+ )
231
+
232
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
233
+ mO_cur = mO[batch_size, None, num_head, None]
234
+ mdO_cur = mdO[batch_size, None, num_head, None]
235
+ mdPsum_cur = mdPsum[batch_size, num_head, None]
236
+ headdim_v = mO.shape[3]
237
+ else:
238
+ mO_cur = cute.domain_offset((seqlen.offset_q, 0), mO[None, num_head, None])
239
+ mdO_cur = cute.domain_offset((seqlen.offset_q, 0), mdO[None, num_head, None])
240
+
241
+ padded_offset_q = seqlen.offset_q + batch_size * self.m_block_size
242
+ mdPsum_cur = cute.domain_offset((padded_offset_q,), mdPsum[num_head, None])
243
+ headdim_v = mO.shape[2]
244
+
245
+ blkOdO_shape = (self.m_block_size, self.head_dim_padded)
246
+ # (m_block_size, head_dim)
247
+ gO = cute.local_tile(mO_cur, blkOdO_shape, (m_block, 0))
248
+ gdO = cute.local_tile(mdO_cur, blkOdO_shape, (m_block, 0))
249
+
250
+ gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
251
+ # (CPY_Atom, CPY_M, CPY_K)
252
+ tOgO = gmem_thr_copy_O.partition_S(gO)
253
+ tOgdO = gmem_thr_copy_O.partition_S(gdO)
254
+
255
+ # ///////////////////////////////////////////////////////////////////////////////
256
+ # Predicate: Mark indices that need to copy when problem_shape isn't a multiple
257
+ # of tile_shape
258
+ # ///////////////////////////////////////////////////////////////////////////////
259
+ # Construct identity layout for KV
260
+ cO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
261
+ tOcO = gmem_thr_copy_O.partition_S(cO)
262
+ t0OcO = gmem_thr_copy_O.get_slice(0).partition_S(cO)
263
+ tOpO = utils.predicate_k(tOcO, limit=headdim_v)
264
+ tOpdO = utils.predicate_k(tOcO, limit=headdim_v)
265
+
266
+ seqlen_q = seqlen.seqlen_q
267
+ seqlen_q_rounded = cute.round_up(seqlen_q, self.m_block_size)
268
+
269
+ if cutlass.const_expr(mLSE is not None):
270
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
271
+ mLSE_cur = mLSE[batch_size, num_head, None]
272
+ else:
273
+ mLSE_cur = cute.domain_offset((seqlen.offset_q,), mLSE[num_head, None])
274
+
275
+ gLSE = cute.local_tile(mLSE_cur, (self.m_block_size,), (m_block,))
276
+ lse = Float32.inf
277
+ if tidx < seqlen_q - m_block * self.m_block_size:
278
+ lse = gLSE[tidx]
279
+
280
+ tOrO = cute.make_fragment_like(tOgO)
281
+ tOrdO = cute.make_fragment_like(tOgdO)
282
+ assert cute.size(tOgO, mode=[0]) == cute.size(tOgdO, mode=[0])
283
+ assert cute.size(tOgO, mode=[1]) == cute.size(tOgdO, mode=[1])
284
+ assert cute.size(tOgO, mode=[2]) == cute.size(tOgdO, mode=[2])
285
+ for m in cutlass.range(cute.size(tOrO.shape[1]), unroll_full=True):
286
+ # Instead of using tOcO, we using t0OcO and subtract the offset from the limit
287
+ # (seqlen_q - m_block * kBlockM). This is because the entries of t0OcO are known at compile time.
288
+ if t0OcO[0, m, 0][0] < seqlen_q - m_block * self.m_block_size - tOcO[0][0]:
289
+ cute.copy(
290
+ gmem_thr_copy_O,
291
+ tOgO[None, m, None],
292
+ tOrO[None, m, None],
293
+ pred=tOpO[None, m, None]
294
+ if cutlass.const_expr(self.check_hdim_oob)
295
+ else None,
296
+ )
297
+ cute.copy(
298
+ gmem_thr_copy_O,
299
+ tOgdO[None, m, None],
300
+ tOrdO[None, m, None],
301
+ pred=tOpdO[None, m, None]
302
+ if cutlass.const_expr(self.check_hdim_oob)
303
+ else None,
304
+ )
305
+ # Sum across the "k" dimension
306
+ dpsum = (tOrO.load().to(Float32) * tOrdO.load().to(Float32)).reduce(
307
+ cute.ReductionOp.ADD, init_val=0.0, reduction_profile=(0, None, 1)
308
+ )
309
+ threads_per_row = gmem_tiled_copy_O.layout_src_tv_tiled[0].shape[0]
310
+ assert cute.arch.WARP_SIZE % threads_per_row == 0
311
+ dpsum = utils.warp_reduce(dpsum, operator.add, width=threads_per_row)
312
+ dP_sum = cute.make_fragment(cute.size(tOrO, mode=[1]), Float32)
313
+ dP_sum.store(dpsum)
314
+
315
+ # Write dPsum from rmem -> gmem
316
+ gdPsum = cute.local_tile(mdPsum_cur, (self.m_block_size,), (m_block,))
317
+ # Only the thread corresponding to column 0 writes out the dPsum to gmem
318
+ if tOcO[0, 0, 0][1] == 0:
319
+ for m in cutlass.range(cute.size(dP_sum), unroll_full=True):
320
+ row = tOcO[0, m, 0][0]
321
+ gdPsum[row] = dP_sum[m] if row < seqlen_q - m_block * self.m_block_size else 0.0
322
+
323
+ # Clear dQaccum
324
+ if cutlass.const_expr(mdQaccum is not None):
325
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
326
+ mdQaccum_cur = mdQaccum[batch_size, num_head, None]
327
+ else:
328
+ padded_offset_q = seqlen.offset_q + batch_size * self.m_block_size
329
+ mdQaccum_cur = cute.domain_offset(
330
+ (padded_offset_q * self.head_dim_padded,), mdQaccum[num_head, None]
331
+ )
332
+
333
+ # HACK: Compiler doesn't seem to recognize that padding
334
+ # by padded_offset_q * self.head_dim_padded keeps alignment
335
+ # since statically divisible by 4
336
+
337
+ mdQaccum_cur_ptr = cute.make_ptr(
338
+ dtype=mdQaccum_cur.element_type,
339
+ value=mdQaccum_cur.iterator.toint(),
340
+ mem_space=mdQaccum_cur.iterator.memspace,
341
+ assumed_align=mdQaccum.iterator.alignment,
342
+ )
343
+ mdQaccum_cur = cute.make_tensor(mdQaccum_cur_ptr, mdQaccum_cur.layout)
344
+
345
+ blkdQaccum_shape = (self.m_block_size * self.head_dim_padded,)
346
+ gdQaccum = cute.local_tile(mdQaccum_cur, blkdQaccum_shape, (m_block,))
347
+ gmem_thr_copy_dQaccum = gmem_tiled_copy_dQaccum.get_slice(tidx)
348
+ tdQgdQaccum = gmem_thr_copy_dQaccum.partition_S(gdQaccum)
349
+ zero = cute.make_fragment_like(tdQgdQaccum)
350
+ zero.fill(0.0)
351
+ cute.copy(gmem_tiled_copy_dQaccum, zero, tdQgdQaccum)
352
+
353
+ if cutlass.const_expr(mLSE is not None):
354
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
355
+ mLSElog2_cur = mLSElog2[batch_size, num_head, None]
356
+ else:
357
+ padded_offset_q = seqlen.offset_q + batch_size * self.m_block_size
358
+ mLSElog2_cur = cute.domain_offset((padded_offset_q,), mLSElog2[num_head, None])
359
+
360
+ gLSElog2 = cute.local_tile(mLSElog2_cur, (self.m_block_size,), (m_block,))
361
+ LOG2_E = math.log2(math.e)
362
+ if tidx < seqlen_q_rounded - m_block * self.m_block_size:
363
+ gLSElog2[tidx] = lse * LOG2_E if lse != -Float32.inf else 0.0
build/torch-universal/flash_bwd_sm100.py ADDED
The diff for this file is too large to render. See raw diff
 
build/torch-universal/flash_bwd_sm90.py ADDED
@@ -0,0 +1,1244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Callable, Optional, Type
3
+ from functools import partial
4
+
5
+ import cuda.bindings.driver as cuda
6
+
7
+ import cutlass
8
+ import cutlass.cute as cute
9
+ import cutlass.utils.hopper_helpers as sm90_utils_basic
10
+ from cutlass.cute.nvgpu import cpasync, warpgroup
11
+ from cutlass.cute.arch import ProxyKind, SharedSpace
12
+ from cutlass import Float32, Int32, Boolean, const_expr
13
+ from cutlass.utils import LayoutEnum
14
+
15
+ from . import hopper_helpers as sm90_utils
16
+ from . import utils
17
+ from . import copy_utils
18
+ from .hopper_helpers import gemm_zero_init, gemm_w_idx
19
+ from .mask import AttentionMask
20
+ from .seqlen_info import SeqlenInfoQK
21
+ from .block_info import BlockInfo
22
+ from . import pipeline
23
+ from .tile_scheduler import TileSchedulerArguments, SingleTileScheduler, ParamsBase
24
+ from .named_barrier import NamedBarrierFwd, NamedBarrierBwd
25
+
26
+
27
+ def mma_partition_fragment_AB(
28
+ thr_mma: cute.core.ThrMma, sA: Optional[cute.Tensor], sB: Optional[cute.Tensor], swap_AB: bool
29
+ ):
30
+ if const_expr(not swap_AB):
31
+ return (
32
+ thr_mma.make_fragment_A(thr_mma.partition_A(sA)) if sA is not None else None,
33
+ thr_mma.make_fragment_B(thr_mma.partition_B(sB)) if sB is not None else None,
34
+ )
35
+ else:
36
+ return (
37
+ thr_mma.make_fragment_B(thr_mma.partition_B(sA)) if sA is not None else None,
38
+ thr_mma.make_fragment_A(thr_mma.partition_A(sB)) if sB is not None else None,
39
+ )
40
+
41
+
42
+ class FlashAttentionBackwardSm90:
43
+ arch = 90
44
+
45
+ def __init__(
46
+ self,
47
+ dtype: Type[cutlass.Numeric],
48
+ head_dim: int,
49
+ head_dim_v: Optional[int] = None,
50
+ qhead_per_kvhead: int = 1,
51
+ is_causal: bool = False,
52
+ tile_m: int = 64,
53
+ tile_n: int = 128,
54
+ Q_stage: int = 2,
55
+ dO_stage: int = 2,
56
+ PdS_stage: int = 2,
57
+ SdP_swapAB: bool = False,
58
+ dKV_swapAB: bool = False,
59
+ dQ_swapAB: bool = False,
60
+ AtomLayoutMSdP: int = 1,
61
+ AtomLayoutNdKV: int = 2,
62
+ AtomLayoutMdQ: int = 1,
63
+ num_threads: int = 384,
64
+ V_in_regs: bool = False,
65
+ ):
66
+ self.dtype = dtype
67
+ # padding head_dim to a multiple of 16 as k_block_size
68
+ hdim_multiple_of = 16
69
+ self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
70
+ head_dim_v = head_dim_v if head_dim_v is not None else head_dim
71
+ self.same_hdim_kv = head_dim == head_dim_v
72
+ self.tile_hdimv = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
73
+ # Can save registers (and hence be faster) if we don't have to check hdim predication
74
+ self.check_hdim_oob = head_dim != self.tile_hdim
75
+ self.check_hdim_v_oob = head_dim_v != self.tile_hdimv
76
+ self.qhead_per_kvhead = qhead_per_kvhead
77
+ self.is_causal = is_causal
78
+ self.is_local = False
79
+ self.tile_m = tile_m
80
+ self.tile_n = tile_n
81
+ self.num_threads = num_threads
82
+ self.Q_stage = Q_stage
83
+ self.dO_stage = dO_stage
84
+ self.PdS_stage = PdS_stage
85
+ assert self.dO_stage in [1, self.Q_stage]
86
+ assert self.PdS_stage in [1, self.Q_stage]
87
+ self.SdP_swapAB = SdP_swapAB
88
+ self.dKV_swapAB = dKV_swapAB
89
+ self.dQ_swapAB = dQ_swapAB
90
+ self.AtomLayoutMSdP = AtomLayoutMSdP
91
+ self.AtomLayoutNdKV = AtomLayoutNdKV
92
+ self.AtomLayoutMdQ = AtomLayoutMdQ
93
+ self.num_mma_warp_groups = (self.num_threads // 128) - 1
94
+ self.mma_dkv_is_rs = (
95
+ AtomLayoutMSdP == 1
96
+ and AtomLayoutNdKV == self.num_mma_warp_groups
97
+ and SdP_swapAB
98
+ and not dKV_swapAB
99
+ )
100
+ self.V_in_regs = V_in_regs
101
+ # These are tuned for speed
102
+ # Do we keep the LSE and dPsum in each thread, or split them across 8 threads that share
103
+ # them and then shuffle to get the value whenever we need? This can reduce register
104
+ # pressure when SdP_swapAB, where each thread needs to keep statistics for (kBlockM / 4)
105
+ # rows. If !SdP_swapAB, each thread only needs to keep statistics for 2 rows.
106
+ # TODO: impl these for hdim 64
107
+ self.shuffle_LSE = self.SdP_swapAB and self.tile_hdim <= 64
108
+ self.shuffle_dPsum = self.SdP_swapAB and self.tile_hdim <= 64
109
+
110
+ @staticmethod
111
+ def can_implement(
112
+ dtype,
113
+ head_dim,
114
+ head_dim_v,
115
+ tile_m,
116
+ tile_n,
117
+ Q_stage,
118
+ num_threads,
119
+ V_in_regs=False,
120
+ ) -> bool:
121
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
122
+ return False
123
+ if head_dim % 8 != 0:
124
+ return False
125
+ if head_dim_v % 8 != 0:
126
+ return False
127
+ if tile_n % 16 != 0:
128
+ return False
129
+ if num_threads % 32 != 0:
130
+ return False
131
+ if (tile_m * 2) % num_threads != 0:
132
+ return False
133
+ return True
134
+
135
+ def _check_type(
136
+ self,
137
+ mQ_type: Type[cutlass.Numeric],
138
+ mK_type: Type[cutlass.Numeric],
139
+ mV_type: Type[cutlass.Numeric],
140
+ mdO_type: Type[cutlass.Numeric],
141
+ mLSE_type: Type[cutlass.Numeric],
142
+ mdPsum_type: Type[cutlass.Numeric],
143
+ mdQaccum_type: Type[cutlass.Numeric],
144
+ mdK_type: Type[cutlass.Numeric],
145
+ mdV_type: Type[cutlass.Numeric],
146
+ ):
147
+ # Get the data type and check if it is fp16 or bf16
148
+ if const_expr(not (mQ_type == mK_type == mV_type == mdO_type)):
149
+ raise TypeError("All tensors must have the same data type")
150
+ if const_expr(mQ_type not in [cutlass.Float16, cutlass.BFloat16]):
151
+ raise TypeError("Only Float16 or BFloat16 is supported")
152
+ if const_expr(mLSE_type not in [Float32]):
153
+ raise TypeError("LSE tensor must be Float32")
154
+ if const_expr(mdPsum_type not in [Float32]):
155
+ raise TypeError("dPsum tensor must be Float32")
156
+ if const_expr(mdQaccum_type not in [Float32]):
157
+ raise TypeError("dQaccum tensor must be Float32")
158
+ if const_expr(self.qhead_per_kvhead == 1):
159
+ if const_expr(not (mdK_type == mdV_type == mQ_type)):
160
+ raise TypeError("mdK and mdV tensors must have the same data type as mQ")
161
+ else:
162
+ if const_expr(not (mdK_type == mdV_type == Float32)):
163
+ raise TypeError("mdKaccum and mdVaccum tensors must have the data type Float32")
164
+ assert mQ_type == self.dtype
165
+
166
+ def _setup_attributes(self):
167
+ self.sQ_layout, self.sK_layout, self.sV_layout, self.sdO_layout, self.sPdS_layout = [
168
+ sm90_utils.make_smem_layout(self.dtype, LayoutEnum.ROW_MAJOR, shape, stage)
169
+ for shape, stage in [
170
+ ((self.tile_m, self.tile_hdim), self.Q_stage),
171
+ ((self.tile_n, self.tile_hdim), None),
172
+ ((self.tile_n, self.tile_hdimv), None),
173
+ ((self.tile_m, self.tile_hdimv), self.dO_stage),
174
+ ((self.tile_m, self.tile_n), self.PdS_stage),
175
+ ]
176
+ ]
177
+ self.sdQaccum_layout = cute.make_layout(
178
+ (self.tile_m * self.tile_hdim // self.num_mma_warp_groups, self.num_mma_warp_groups)
179
+ )
180
+ # dQaccum R->S
181
+ self.r2s_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
182
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
183
+ # thr_layout
184
+ cute.make_layout((self.num_threads_per_warp_group, self.num_mma_warp_groups)),
185
+ cute.make_layout(128 // Float32.width), # val_layout
186
+ )
187
+
188
+ def _get_tiled_mma(self):
189
+ # S = Q @ K.T, dP = dO @ V.T
190
+ atom_layout_SdP = (self.AtomLayoutMSdP, self.num_mma_warp_groups // self.AtomLayoutMSdP)
191
+ tiler_mn_SdP = (self.tile_m // atom_layout_SdP[0], self.tile_n // atom_layout_SdP[1])
192
+ tiled_mma_SdP = sm90_utils_basic.make_trivial_tiled_mma(
193
+ self.dtype,
194
+ self.dtype,
195
+ warpgroup.OperandMajorMode.K,
196
+ warpgroup.OperandMajorMode.K,
197
+ Float32,
198
+ atom_layout_mnk=(atom_layout_SdP if not self.SdP_swapAB else atom_layout_SdP[::-1])
199
+ + (1,),
200
+ tiler_mn=tiler_mn_SdP if not self.SdP_swapAB else tiler_mn_SdP[::-1],
201
+ )
202
+ # dV = P.T @ dO, dK = dS.T @ Q
203
+ atom_layout_dKV = (self.AtomLayoutNdKV, self.num_mma_warp_groups // self.AtomLayoutNdKV)
204
+ tiler_mn_dK = (self.tile_n // atom_layout_dKV[0], self.tile_hdim // atom_layout_dKV[1])
205
+ tiler_mn_dV = (self.tile_n // atom_layout_dKV[0], self.tile_hdimv // atom_layout_dKV[1])
206
+ tiled_mma_dK, tiled_mma_dV = [
207
+ sm90_utils_basic.make_trivial_tiled_mma(
208
+ self.dtype,
209
+ self.dtype,
210
+ warpgroup.OperandMajorMode.MN
211
+ if not self.mma_dkv_is_rs
212
+ else warpgroup.OperandMajorMode.K,
213
+ warpgroup.OperandMajorMode.MN,
214
+ Float32,
215
+ atom_layout_mnk=(atom_layout_dKV if not self.dKV_swapAB else atom_layout_dKV[::-1])
216
+ + (1,),
217
+ tiler_mn=tiler_mn_d if not self.dKV_swapAB else tiler_mn_d[::-1],
218
+ a_source=warpgroup.OperandSource.RMEM
219
+ if self.mma_dkv_is_rs
220
+ else warpgroup.OperandSource.SMEM,
221
+ )
222
+ for tiler_mn_d in (tiler_mn_dK, tiler_mn_dV)
223
+ ]
224
+ # dQ = dS @ K
225
+ atom_layout_dQ = (self.AtomLayoutMdQ, self.num_mma_warp_groups // self.AtomLayoutMdQ)
226
+ tiler_mn_dQ = (self.tile_m // atom_layout_dQ[0], self.tile_hdim // atom_layout_dQ[1])
227
+ tiled_mma_dQ = sm90_utils_basic.make_trivial_tiled_mma(
228
+ self.dtype,
229
+ self.dtype,
230
+ warpgroup.OperandMajorMode.K if not self.dQ_swapAB else warpgroup.OperandMajorMode.MN,
231
+ warpgroup.OperandMajorMode.MN if not self.dQ_swapAB else warpgroup.OperandMajorMode.K,
232
+ Float32,
233
+ atom_layout_mnk=(atom_layout_dQ if not self.dQ_swapAB else atom_layout_dQ[::-1]) + (1,),
234
+ tiler_mn=tiler_mn_dQ if not self.dQ_swapAB else tiler_mn_dQ[::-1],
235
+ )
236
+ return tiled_mma_SdP, tiled_mma_dK, tiled_mma_dV, tiled_mma_dQ
237
+
238
+ def _get_shared_storage_cls(self):
239
+ sQ_alignment = sK_alignment = sV_alighment = sdQaccum_alignment = sdO_alignment = 1024
240
+
241
+ sQ_struct, sK_struct, sV_struct, sdO_struct, sdQaccum_struct = [
242
+ cute.struct.Align[cute.struct.MemRange[type, cute.cosize(layout)], alignment]
243
+ for (layout, type, alignment) in [
244
+ (self.sQ_layout, self.dtype, sQ_alignment),
245
+ (self.sK_layout, self.dtype, sK_alignment),
246
+ (self.sV_layout, self.dtype, sV_alighment),
247
+ (self.sdO_layout, self.dtype, sdO_alignment),
248
+ (self.sdQaccum_layout, Float32, sdQaccum_alignment),
249
+ ]
250
+ ]
251
+
252
+ cosize_sdS = cute.cosize(self.sPdS_layout)
253
+ cosize_sP = cute.cosize(self.sPdS_layout) if const_expr(not self.mma_dkv_is_rs) else 0
254
+ sLSE_struct = cute.struct.Align[
255
+ cute.struct.MemRange[Float32, cute.round_up(self.tile_m, 64) * self.Q_stage], 128
256
+ ]
257
+ sdPsum_struct = cute.struct.Align[
258
+ cute.struct.MemRange[Float32, cute.round_up(self.tile_m, 64) * self.dO_stage], 128
259
+ ]
260
+
261
+ @cute.struct
262
+ class SharedStorageQKV:
263
+ mbar_ptr_Q: cute.struct.MemRange[cutlass.Int64, self.Q_stage * 2]
264
+ mbar_ptr_dO: cute.struct.MemRange[cutlass.Int64, self.dO_stage * 2]
265
+ sLSE: sLSE_struct
266
+ sdPsum: sdPsum_struct
267
+ sQ: sQ_struct
268
+ sV: sV_struct
269
+ sK: sK_struct
270
+ sdO: sdO_struct
271
+ sP: cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sP], 1024]
272
+ sdS: cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sdS], 1024]
273
+ sdQaccum: sdQaccum_struct
274
+
275
+ return SharedStorageQKV
276
+
277
+ @cute.jit
278
+ def __call__(
279
+ self,
280
+ mQ: cute.Tensor,
281
+ mK: cute.Tensor,
282
+ mV: cute.Tensor,
283
+ mdO: cute.Tensor,
284
+ mLSE: cute.Tensor,
285
+ mdPsum: cute.Tensor,
286
+ mdQaccum: cute.Tensor,
287
+ mdK: cute.Tensor,
288
+ mdV: cute.Tensor,
289
+ softmax_scale: Float32,
290
+ stream: cuda.CUstream,
291
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
292
+ mCuSeqlensK: Optional[cute.Tensor] = None,
293
+ mSeqUsedQ: Optional[cute.Tensor] = None,
294
+ mSeqUsedK: Optional[cute.Tensor] = None,
295
+ softcap: Float32 | float | None = None,
296
+ window_size_left: Int32 | int | None = None,
297
+ window_size_right: Int32 | int | None = None,
298
+ ):
299
+ self._check_type(
300
+ *(
301
+ t.element_type if t is not None else None
302
+ for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV)
303
+ )
304
+ )
305
+
306
+ # Assume all strides are divisible by 128 bits except the last stride
307
+ new_stride = lambda t: (
308
+ *(cute.assume(s, divby=128 // t.element_type.width) for s in t.stride[:-1]),
309
+ t.stride[-1],
310
+ )
311
+ mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV = [
312
+ cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
313
+ if t is not None
314
+ else None
315
+ for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV)
316
+ ]
317
+
318
+ layout_transpose = [1, 3, 2, 0] # (b, s, n, h) --> (s, h, n, b)
319
+ mQ, mK, mV, mdK, mdV, mdO = [
320
+ utils.select(t, layout_transpose) for t in (mQ, mK, mV, mdK, mdV, mdO)
321
+ ]
322
+ LSE_dPsum_dQaccum_transpose = [2, 1, 0] # (b, n, s) -> (s, n, b)
323
+ mLSE, mdPsum, mdQaccum = [
324
+ utils.select(t, LSE_dPsum_dQaccum_transpose) for t in (mLSE, mdPsum, mdQaccum)
325
+ ]
326
+
327
+ tiled_mma_SdP, tiled_mma_dK, tiled_mma_dV, tiled_mma_dQ = self._get_tiled_mma()
328
+
329
+ self.num_mma_threads = tiled_mma_SdP.size
330
+ assert self.num_mma_threads + 128 == self.num_threads
331
+
332
+ self.num_threads_per_warp_group = 128
333
+ self.num_producer_threads = 32
334
+
335
+ self.num_mma_regs = 240
336
+ self.num_producer_regs = 24
337
+ # self.num_mma_regs = 232
338
+ # self.num_producer_regs = 40
339
+
340
+ self._setup_attributes()
341
+ SharedStorage = self._get_shared_storage_cls()
342
+
343
+ self.tma_copy_bytes = {
344
+ name: cute.size_in_bytes(mX.element_type, cute.select(layout, mode=[0, 1]))
345
+ for name, mX, layout in [
346
+ ("Q", mQ, self.sQ_layout),
347
+ ("K", mK, self.sK_layout),
348
+ ("V", mV, self.sV_layout),
349
+ ("dO", mdO, self.sdO_layout),
350
+ ]
351
+ }
352
+ self.tma_copy_bytes["LSE"] = self.tile_m * Float32.width // 8
353
+ self.tma_copy_bytes["dPsum"] = self.tile_m * Float32.width // 8
354
+ self.tma_copy_bytes["dQ"] = (
355
+ self.tile_m * self.tile_hdim * Float32.width // 8 // self.num_mma_warp_groups
356
+ )
357
+
358
+ tma_atom_Q, tma_tensor_Q = cpasync.make_tiled_tma_atom(
359
+ cpasync.CopyBulkTensorTileG2SOp(),
360
+ mQ,
361
+ cute.select(self.sQ_layout, mode=[0, 1]),
362
+ (self.tile_m, self.tile_hdim),
363
+ )
364
+ tma_atom_K, tma_tensor_K = cpasync.make_tiled_tma_atom(
365
+ cpasync.CopyBulkTensorTileG2SOp(),
366
+ mK,
367
+ cute.select(self.sK_layout, mode=[0, 1]),
368
+ (self.tile_n, self.tile_hdim),
369
+ )
370
+ tma_atom_V, tma_tensor_V = cpasync.make_tiled_tma_atom(
371
+ cpasync.CopyBulkTensorTileG2SOp(),
372
+ mV,
373
+ cute.select(self.sV_layout, mode=[0, 1]),
374
+ (self.tile_n, self.tile_hdimv),
375
+ )
376
+ tma_atom_dO, tma_tensor_dO = cpasync.make_tiled_tma_atom(
377
+ cpasync.CopyBulkTensorTileG2SOp(),
378
+ mdO,
379
+ cute.select(self.sdO_layout, mode=[0, 1]),
380
+ (self.tile_m, self.tile_hdimv),
381
+ )
382
+ tma_atom_dK, tma_tensor_dK = cpasync.make_tiled_tma_atom(
383
+ cpasync.CopyBulkTensorTileS2GOp(),
384
+ mdK,
385
+ cute.select(self.sK_layout, mode=[0, 1]),
386
+ (self.tile_n, self.tile_hdim),
387
+ )
388
+ tma_atom_dV, tma_tensor_dV = cpasync.make_tiled_tma_atom(
389
+ cpasync.CopyBulkTensorTileS2GOp(),
390
+ mdV,
391
+ cute.select(self.sV_layout, mode=[0, 1]),
392
+ (self.tile_n, self.tile_hdimv),
393
+ )
394
+
395
+ TileScheduler = SingleTileScheduler
396
+ tile_sched_args = TileSchedulerArguments(
397
+ cute.ceil_div(cute.size(mK.shape[0]), self.tile_n),
398
+ cute.size(mK.shape[2]),
399
+ cute.size(mK.shape[3]),
400
+ 1, # num_splits
401
+ cute.size(mK.shape[0]),
402
+ mQ.shape[1],
403
+ mV.shape[1],
404
+ total_q=cute.size(mQ.shape[0]) * cute.size(mQ.shape[3]),
405
+ tile_shape_mn=(self.tile_m, self.tile_n),
406
+ mCuSeqlensQ=None,
407
+ mSeqUsedQ=None,
408
+ qhead_per_kvhead_packgqa=1,
409
+ element_size=self.dtype.width // 8,
410
+ is_persistent=False,
411
+ lpt=False,
412
+ )
413
+
414
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
415
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
416
+
417
+ LOG2_E = math.log2(math.e)
418
+ softmax_scale_log2 = softmax_scale * LOG2_E
419
+
420
+ self.kernel(
421
+ tma_tensor_Q,
422
+ tma_tensor_K,
423
+ tma_tensor_V,
424
+ tma_tensor_dO,
425
+ tma_tensor_dK,
426
+ tma_tensor_dV,
427
+ tma_atom_Q,
428
+ tma_atom_K,
429
+ tma_atom_V,
430
+ tma_atom_dO,
431
+ tma_atom_dK,
432
+ tma_atom_dV,
433
+ mLSE,
434
+ mdPsum,
435
+ mdQaccum,
436
+ self.sQ_layout,
437
+ self.sK_layout,
438
+ self.sV_layout,
439
+ self.sPdS_layout,
440
+ self.sdO_layout,
441
+ self.sdQaccum_layout,
442
+ self.r2s_tiled_copy_dQaccum,
443
+ tiled_mma_SdP,
444
+ tiled_mma_dK,
445
+ tiled_mma_dV,
446
+ tiled_mma_dQ,
447
+ softmax_scale_log2,
448
+ softmax_scale,
449
+ tile_sched_params,
450
+ TileScheduler,
451
+ SharedStorage,
452
+ ).launch(
453
+ grid=grid_dim,
454
+ block=[self.num_threads, 1, 1],
455
+ smem=SharedStorage.size_in_bytes(),
456
+ stream=stream,
457
+ min_blocks_per_mp=1,
458
+ )
459
+
460
+ @cute.kernel
461
+ def kernel(
462
+ self,
463
+ mQ: cute.Tensor,
464
+ mK: cute.Tensor,
465
+ mV: cute.Tensor,
466
+ mdO: cute.Tensor,
467
+ mdK: cute.Tensor,
468
+ mdV: cute.Tensor,
469
+ tma_atom_Q: cute.CopyAtom,
470
+ tma_atom_K: cute.CopyAtom,
471
+ tma_atom_V: cute.CopyAtom,
472
+ tma_atom_dO: cute.CopyAtom,
473
+ tma_atom_dK: cute.CopyAtom,
474
+ tma_atom_dV: cute.CopyAtom,
475
+ mLSE: cute.Tensor,
476
+ mdPsum: cute.Tensor,
477
+ mdQaccum: cute.Tensor,
478
+ sQ_layout: cute.ComposedLayout,
479
+ sK_layout: cute.ComposedLayout,
480
+ sV_layout: cute.ComposedLayout,
481
+ sPdS_layout: cute.ComposedLayout,
482
+ sdO_layout: cute.ComposedLayout,
483
+ sdQaccum_layout: cute.Layout,
484
+ r2s_tiled_copy_dQaccum: cute.TiledCopy,
485
+ tiled_mma_SdP: cute.TiledMma,
486
+ tiled_mma_dK: cute.TiledMma,
487
+ tiled_mma_dV: cute.TiledMma,
488
+ tiled_mma_dQ: cute.TiledMma,
489
+ softmax_scale_log2,
490
+ softmax_scale,
491
+ tile_sched_params: ParamsBase,
492
+ TileScheduler: cutlass.Constexpr[Callable],
493
+ SharedStorage: cutlass.Constexpr[Callable],
494
+ ):
495
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
496
+
497
+ # prefetch TMA descriptors
498
+ if warp_idx == 0:
499
+ cpasync.prefetch_descriptor(tma_atom_Q)
500
+ cpasync.prefetch_descriptor(tma_atom_K)
501
+ cpasync.prefetch_descriptor(tma_atom_V)
502
+ cpasync.prefetch_descriptor(tma_atom_dO)
503
+
504
+ smem = cutlass.utils.SmemAllocator()
505
+ storage = smem.allocate(SharedStorage)
506
+
507
+ pipeline_producer_group = cutlass.pipeline.CooperativeGroup(cutlass.pipeline.Agent.Thread)
508
+ pipeline_consumer_group = cutlass.pipeline.CooperativeGroup(
509
+ cutlass.pipeline.Agent.Thread, self.num_mma_threads // self.num_threads_per_warp_group
510
+ )
511
+ pipeline_Q = pipeline.PipelineTmaAsync.create(
512
+ barrier_storage=storage.mbar_ptr_Q.data_ptr(),
513
+ num_stages=self.Q_stage,
514
+ producer_group=pipeline_producer_group,
515
+ consumer_group=pipeline_consumer_group,
516
+ tx_count=self.tma_copy_bytes["Q"] + self.tma_copy_bytes["LSE"],
517
+ init_wait=False,
518
+ )
519
+ pipeline_dO = pipeline.PipelineTmaAsync.create(
520
+ barrier_storage=storage.mbar_ptr_dO.data_ptr(),
521
+ num_stages=self.dO_stage,
522
+ producer_group=pipeline_producer_group,
523
+ consumer_group=pipeline_consumer_group,
524
+ tx_count=self.tma_copy_bytes["dO"] + self.tma_copy_bytes["dPsum"],
525
+ init_wait=True,
526
+ )
527
+
528
+ sQ = storage.sQ.get_tensor(sQ_layout.outer, swizzle=sQ_layout.inner)
529
+ sdO = storage.sdO.get_tensor(sdO_layout.outer, swizzle=sdO_layout.inner)
530
+ sK = storage.sK.get_tensor(sK_layout.outer, swizzle=sK_layout.inner)
531
+ sV = storage.sV.get_tensor(sV_layout.outer, swizzle=sV_layout.inner)
532
+ sP = None
533
+ if const_expr(not self.mma_dkv_is_rs):
534
+ sP = storage.sP.get_tensor(sPdS_layout.outer, swizzle=sPdS_layout.inner)
535
+ sdS = storage.sdS.get_tensor(sPdS_layout.outer, swizzle=sPdS_layout.inner)
536
+ sLSE = storage.sLSE.get_tensor(
537
+ cute.make_layout(
538
+ (self.tile_m, self.Q_stage),
539
+ stride=(1, cute.round_up(self.tile_m, 64)),
540
+ )
541
+ )
542
+ sdPsum = storage.sdPsum.get_tensor(
543
+ cute.make_layout(
544
+ (self.tile_m, self.dO_stage),
545
+ stride=(1, cute.round_up(self.tile_m, 64)),
546
+ )
547
+ )
548
+ sdQaccum = storage.sdQaccum.get_tensor(sdQaccum_layout)
549
+
550
+ block_info = BlockInfo(
551
+ self.tile_m,
552
+ self.tile_n,
553
+ self.is_causal,
554
+ self.is_local,
555
+ False, # is_split_kv
556
+ None,
557
+ None,
558
+ qhead_per_kvhead_packgqa=1,
559
+ )
560
+ SeqlenInfoCls = partial(
561
+ SeqlenInfoQK.create,
562
+ seqlen_q_static=mQ.shape[0],
563
+ seqlen_k_static=mK.shape[0],
564
+ mCuSeqlensQ=None,
565
+ mCuSeqlensK=None,
566
+ mSeqUsedQ=None,
567
+ mSeqUsedK=None,
568
+ )
569
+ AttentionMaskCls = partial(
570
+ AttentionMask,
571
+ self.tile_m,
572
+ self.tile_n,
573
+ window_size_left=None,
574
+ window_size_right=None,
575
+ )
576
+ TileSchedulerCls = partial(TileScheduler.create, tile_sched_params)
577
+
578
+ if warp_idx < 4:
579
+ cute.arch.warpgroup_reg_dealloc(self.num_producer_regs)
580
+ if warp_idx == 0:
581
+ self.load(
582
+ mQ,
583
+ mK,
584
+ mV,
585
+ mdO,
586
+ mLSE,
587
+ mdPsum,
588
+ sQ,
589
+ sK,
590
+ sV,
591
+ sdO,
592
+ sLSE,
593
+ sdPsum,
594
+ tma_atom_Q,
595
+ tma_atom_K,
596
+ tma_atom_V,
597
+ tma_atom_dO,
598
+ pipeline_Q,
599
+ pipeline_dO,
600
+ block_info,
601
+ SeqlenInfoCls,
602
+ TileSchedulerCls,
603
+ )
604
+ if warp_idx == 1:
605
+ for warp_group_idx in cutlass.range(self.num_mma_warp_groups):
606
+ cute.arch.barrier_arrive(
607
+ barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
608
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
609
+ )
610
+ self.dQaccum_store(mdQaccum, sdQaccum, block_info, TileSchedulerCls, SeqlenInfoCls)
611
+ else:
612
+ cute.arch.warpgroup_reg_alloc(self.num_mma_regs)
613
+ tidx, _, _ = cute.arch.thread_idx()
614
+ tidx = tidx - 128
615
+ self.mma(
616
+ tiled_mma_SdP,
617
+ tiled_mma_dK,
618
+ tiled_mma_dV,
619
+ tiled_mma_dQ,
620
+ mdK,
621
+ mdV,
622
+ mdQaccum,
623
+ sQ,
624
+ sK,
625
+ sV,
626
+ sdO,
627
+ sP,
628
+ sdS,
629
+ sLSE,
630
+ sdPsum,
631
+ sdQaccum,
632
+ pipeline_Q,
633
+ pipeline_dO,
634
+ tidx,
635
+ tma_atom_dK,
636
+ tma_atom_dV,
637
+ r2s_tiled_copy_dQaccum,
638
+ softmax_scale_log2,
639
+ softmax_scale,
640
+ block_info,
641
+ SeqlenInfoCls,
642
+ AttentionMaskCls,
643
+ TileSchedulerCls,
644
+ )
645
+
646
+ @cute.jit
647
+ def load(
648
+ self,
649
+ mQ: cute.Tensor,
650
+ mK: cute.Tensor,
651
+ mV: cute.Tensor,
652
+ mdO: cute.Tensor,
653
+ mLSE: cute.Tensor,
654
+ mdPsum: cute.Tensor,
655
+ sQ: cute.Tensor,
656
+ sK: cute.Tensor,
657
+ sV: cute.Tensor,
658
+ sdO: cute.Tensor,
659
+ sLSE: cute.Tensor,
660
+ sdPsum: cute.Tensor,
661
+ tma_atom_Q: cute.CopyAtom,
662
+ tma_atom_K: cute.CopyAtom,
663
+ tma_atom_V: cute.CopyAtom,
664
+ tma_atom_dO: cute.CopyAtom,
665
+ pipeline_Q: cutlass.pipeline.PipelineAsync,
666
+ pipeline_dO: cutlass.pipeline.PipelineAsync,
667
+ block_info: BlockInfo,
668
+ SeqlenInfoCls: Callable,
669
+ TileSchedulerCls: Callable,
670
+ ):
671
+ warp_idx_in_wg = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % 4
672
+
673
+ if warp_idx_in_wg == 0:
674
+ producer_state_Q = cutlass.pipeline.make_pipeline_state(
675
+ cutlass.pipeline.PipelineUserType.Producer, self.Q_stage
676
+ )
677
+ producer_state_dO = cutlass.pipeline.make_pipeline_state(
678
+ cutlass.pipeline.PipelineUserType.Producer, self.dO_stage
679
+ )
680
+ tile_scheduler = TileSchedulerCls()
681
+ work_tile = tile_scheduler.initial_work_tile_info()
682
+ while work_tile.is_valid_tile:
683
+ n_block, head_idx, batch_idx, _ = work_tile.tile_idx
684
+ seqlen = SeqlenInfoCls(batch_idx)
685
+ mK_cur = mK[None, None, head_idx, batch_idx]
686
+ gK = cute.local_tile(mK_cur, (self.tile_n, self.tile_hdim), (n_block, 0))
687
+ mV_cur = mV[None, None, head_idx, batch_idx]
688
+ gV = cute.local_tile(mV_cur, (self.tile_n, self.tile_hdimv), (n_block, 0))
689
+
690
+ mQ_cur = mQ[None, None, head_idx, batch_idx]
691
+ gQ = cute.local_tile(mQ_cur, (self.tile_m, self.tile_hdim), (None, 0))
692
+ mdO_cur = mdO[None, None, head_idx, batch_idx]
693
+ gdO = cute.local_tile(mdO_cur, (self.tile_m, self.tile_hdimv), (None, 0))
694
+ mLSE_cur = mLSE[None, head_idx, batch_idx]
695
+ gLSE = cute.local_tile(mLSE_cur, (self.tile_m,), (None,))
696
+ mdPsum_cur = mdPsum[None, head_idx, batch_idx]
697
+ gdPsum = cute.local_tile(mdPsum_cur, (self.tile_m,), (None,))
698
+
699
+ load_K, _, _ = copy_utils.tma_get_copy_fn(
700
+ tma_atom_K, 0, cute.make_layout(1), gK, sK, single_stage=True
701
+ )
702
+ load_V, _, _ = copy_utils.tma_get_copy_fn(
703
+ tma_atom_V, 0, cute.make_layout(1), gV, sV, single_stage=True
704
+ )
705
+ load_Q, _, _ = copy_utils.tma_get_copy_fn(
706
+ tma_atom_Q, 0, cute.make_layout(1), gQ, sQ
707
+ )
708
+ load_Q = copy_utils.tma_producer_copy_fn(load_Q, pipeline_Q)
709
+ load_dO, _, _ = copy_utils.tma_get_copy_fn(
710
+ tma_atom_dO, 0, cute.make_layout(1), gdO, sdO
711
+ )
712
+ load_dO = copy_utils.tma_producer_copy_fn(load_dO, pipeline_dO)
713
+ load_LSE = copy_utils.cpasync_bulk_get_copy_fn(gLSE, sLSE)
714
+ load_LSE = copy_utils.tma_producer_copy_fn(load_LSE, pipeline_Q)
715
+ load_dPsum = copy_utils.cpasync_bulk_get_copy_fn(gdPsum, sdPsum)
716
+ load_dPsum = copy_utils.tma_producer_copy_fn(load_dPsum, pipeline_dO)
717
+
718
+ m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
719
+ # First iteration: load K together w Q & LSE, then V together w dO & dPsum
720
+ m_block = m_block_min
721
+ pipeline_Q.producer_acquire(
722
+ producer_state_Q, extra_tx_count=self.tma_copy_bytes["K"]
723
+ )
724
+ load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q))
725
+ load_Q(m_block, producer_state=producer_state_Q)
726
+ # cp.async.bulk is using ptx, so we need to elect one thread to do it
727
+ with cute.arch.elect_one():
728
+ load_LSE(m_block, producer_state=producer_state_Q)
729
+ producer_state_dO_cur = (
730
+ producer_state_dO
731
+ if const_expr(self.Q_stage != self.dO_stage)
732
+ else producer_state_Q
733
+ )
734
+ pipeline_dO.producer_acquire(
735
+ producer_state_dO_cur, extra_tx_count=self.tma_copy_bytes["V"]
736
+ )
737
+ load_V(tma_bar_ptr=pipeline_dO.producer_get_barrier(producer_state_dO_cur))
738
+ load_dO(m_block, producer_state=producer_state_dO_cur)
739
+ with cute.arch.elect_one():
740
+ load_dPsum(m_block, producer_state=producer_state_dO_cur)
741
+ producer_state_Q.advance()
742
+ producer_state_dO.advance()
743
+ # Subsequent iterations: load Q & LSE, then dO & dPsum
744
+ for m_block in cutlass.range(m_block_min + 1, m_block_max, unroll=1):
745
+ pipeline_Q.producer_acquire(producer_state_Q)
746
+ load_Q(m_block, producer_state=producer_state_Q)
747
+ # cp.async.bulk is using ptx, so we need to elect one thread to do it
748
+ with cute.arch.elect_one():
749
+ load_LSE(m_block, producer_state=producer_state_Q)
750
+ producer_state_dO_cur = (
751
+ producer_state_dO
752
+ if const_expr(self.Q_stage != self.dO_stage)
753
+ else producer_state_Q
754
+ )
755
+ pipeline_dO.producer_acquire(producer_state_dO_cur)
756
+ load_dO(m_block, producer_state=producer_state_dO_cur)
757
+ with cute.arch.elect_one():
758
+ load_dPsum(m_block, producer_state=producer_state_dO_cur)
759
+ producer_state_Q.advance()
760
+ producer_state_dO.advance()
761
+
762
+ tile_scheduler.prefetch_next_work()
763
+ tile_scheduler.advance_to_next_work()
764
+ work_tile = tile_scheduler.get_current_work()
765
+
766
+ @cute.jit
767
+ def mma(
768
+ self,
769
+ tiled_mma_SdP: cute.TiledMma,
770
+ tiled_mma_dK: cute.TiledMma,
771
+ tiled_mma_dV: cute.TiledMma,
772
+ tiled_mma_dQ: cute.TiledMma,
773
+ mdK: cute.Tensor,
774
+ mdV: cute.Tensor,
775
+ mdQaccum: cute.Tensor,
776
+ sQ: cute.Tensor,
777
+ sK: cute.Tensor,
778
+ sV: cute.Tensor,
779
+ sdO: cute.Tensor,
780
+ sP: Optional[cute.Tensor],
781
+ sdS: cute.Tensor,
782
+ sLSE: cute.Tensor,
783
+ sdPsum: cute.Tensor,
784
+ sdQaccum: cute.Tensor,
785
+ pipeline_Q: cutlass.pipeline.PipelineAsync,
786
+ pipeline_dO: cutlass.pipeline.PipelineAsync,
787
+ tidx: Int32,
788
+ tma_atom_dK: cute.CopyAtom,
789
+ tma_atom_dV: cute.CopyAtom,
790
+ r2s_tiled_copy_dQaccum: cute.TiledCopy,
791
+ softmax_scale_log2: Float32,
792
+ softmax_scale: Float32,
793
+ block_info: BlockInfo,
794
+ SeqlenInfoCls: Callable,
795
+ AttentionMaskCls: Callable,
796
+ TileSchedulerCls: Callable,
797
+ ):
798
+ warp_group_idx = cute.arch.make_warp_uniform(tidx // self.num_threads_per_warp_group)
799
+ warp_group_thread_layout = cute.make_layout(
800
+ self.num_mma_warp_groups, stride=self.num_threads_per_warp_group
801
+ )
802
+ thr_mma_SdP = tiled_mma_SdP.get_slice(tidx)
803
+ wg_mma_SdP = tiled_mma_SdP.get_slice(warp_group_thread_layout(warp_group_idx))
804
+ wg_mma_dK = tiled_mma_dK.get_slice(warp_group_thread_layout(warp_group_idx))
805
+ wg_mma_dV = tiled_mma_dV.get_slice(warp_group_thread_layout(warp_group_idx))
806
+ wg_mma_dQ = tiled_mma_dQ.get_slice(warp_group_thread_layout(warp_group_idx))
807
+ # S = Q @ K.T
808
+ tSrQ, tSrK = mma_partition_fragment_AB(wg_mma_SdP, sQ, sK, self.SdP_swapAB)
809
+ # dP = dO @ V.T
810
+ tdPrdO, tdPrV = mma_partition_fragment_AB(wg_mma_SdP, sdO, sV, self.SdP_swapAB)
811
+ # dV += P.T @ dO
812
+ sPt = utils.transpose_view(sP) if sP is not None else None
813
+ sdOt = utils.transpose_view(sdO)
814
+ tdVrPt, tdVrdOt = mma_partition_fragment_AB(wg_mma_dV, sPt, sdOt, self.dKV_swapAB)
815
+ # dK += dS.T @ Q
816
+ sdSt = utils.transpose_view(sdS)
817
+ sQt = utils.transpose_view(sQ)
818
+ tdKrdSt, tdKrQt = mma_partition_fragment_AB(wg_mma_dK, sdSt, sQt, self.dKV_swapAB)
819
+ # dQ = dS @ K
820
+ sKt = utils.transpose_view(sK)
821
+ tdQrdS, tdQrKt = mma_partition_fragment_AB(wg_mma_dQ, sdS, sKt, self.dQ_swapAB)
822
+
823
+ # Smem copy atom tiling
824
+ smem_copy_atom_PdS = utils.get_smem_store_atom(
825
+ self.arch, self.dtype, transpose=self.SdP_swapAB
826
+ )
827
+ smem_thr_copy_PdS = cute.make_tiled_copy_C(smem_copy_atom_PdS, tiled_mma_SdP).get_slice(
828
+ tidx
829
+ )
830
+ tPsP = None
831
+ if const_expr(sP is not None):
832
+ tPsP = smem_thr_copy_PdS.partition_D(sP if const_expr(not self.SdP_swapAB) else sPt)
833
+ tdSsdS = smem_thr_copy_PdS.partition_D(sdS if const_expr(not self.SdP_swapAB) else sdSt)
834
+
835
+ sLSE_mma = cute.make_tensor(
836
+ sLSE.iterator,
837
+ cute.make_layout(
838
+ (self.tile_m, self.tile_n, self.Q_stage),
839
+ stride=(1, 0, cute.round_up(self.tile_m, 64)),
840
+ ),
841
+ )
842
+ sdPsum_mma = cute.make_tensor(
843
+ sdPsum.iterator,
844
+ cute.make_layout(
845
+ (self.tile_m, self.tile_n, self.dO_stage),
846
+ stride=(1, 0, cute.round_up(self.tile_m, 64)),
847
+ ),
848
+ )
849
+ if const_expr(self.SdP_swapAB):
850
+ sLSE_mma = utils.transpose_view(sLSE_mma)
851
+ sdPsum_mma = utils.transpose_view(sdPsum_mma)
852
+ LSEslice = (None, 0, None) if const_expr(not self.SdP_swapAB) else (0, None, None)
853
+ tLSEsLSE = utils.make_acc_tensor_mn_view(thr_mma_SdP.partition_C(sLSE_mma))[LSEslice]
854
+ tLSEsdPsum = utils.make_acc_tensor_mn_view(thr_mma_SdP.partition_C(sdPsum_mma))[LSEslice]
855
+
856
+ smem_thr_copy_dQaccum = r2s_tiled_copy_dQaccum.get_slice(tidx)
857
+ tdQsdQaccum = smem_thr_copy_dQaccum.partition_D(sdQaccum)
858
+
859
+ dV_shape = (self.tile_n, self.tile_hdimv)
860
+ acc_dV = cute.make_fragment(
861
+ tiled_mma_dV.partition_shape_C(dV_shape if not self.dKV_swapAB else dV_shape[::-1]),
862
+ Float32,
863
+ )
864
+ dK_shape = (self.tile_n, self.tile_hdim)
865
+ acc_dK = cute.make_fragment(
866
+ tiled_mma_dK.partition_shape_C(dK_shape if not self.dKV_swapAB else dK_shape[::-1]),
867
+ Float32,
868
+ )
869
+
870
+ mma_qk_fn = partial(
871
+ gemm_zero_init,
872
+ tiled_mma_SdP,
873
+ (self.tile_m, self.tile_n),
874
+ tSrQ,
875
+ tSrK,
876
+ swap_AB=self.SdP_swapAB,
877
+ )
878
+ mma_dov_fn = partial(
879
+ gemm_zero_init,
880
+ tiled_mma_SdP,
881
+ (self.tile_m, self.tile_n),
882
+ tdPrdO,
883
+ tdPrV,
884
+ swap_AB=self.SdP_swapAB,
885
+ )
886
+ if const_expr(not self.mma_dkv_is_rs):
887
+ mma_pdo_fn = partial(
888
+ gemm_w_idx, tiled_mma_dV, acc_dV, tdVrPt, tdVrdOt, swap_AB=self.dKV_swapAB
889
+ )
890
+ mma_dsq_fn = partial(
891
+ gemm_w_idx, tiled_mma_dK, acc_dK, tdKrdSt, tdKrQt, swap_AB=self.dKV_swapAB
892
+ )
893
+ else:
894
+ assert not self.dKV_swapAB
895
+ mma_pdo_fn = partial(gemm_w_idx, tiled_mma_dV, acc_dV, tCrB=tdVrdOt)
896
+ mma_dsq_fn = partial(gemm_w_idx, tiled_mma_dK, acc_dK, tCrB=tdKrQt)
897
+ mma_dsk_fn = partial(
898
+ gemm_zero_init,
899
+ tiled_mma_dQ,
900
+ (self.tile_m, self.tile_hdim),
901
+ tdQrdS,
902
+ tdQrKt,
903
+ swap_AB=self.dQ_swapAB,
904
+ )
905
+
906
+ mma_one_m_block_all = partial(
907
+ self.mma_one_m_block,
908
+ warp_group_idx=warp_group_idx,
909
+ mma_qk_fn=mma_qk_fn,
910
+ mma_dov_fn=mma_dov_fn,
911
+ mma_pdo_fn=mma_pdo_fn,
912
+ mma_dsq_fn=mma_dsq_fn,
913
+ mma_dsk_fn=mma_dsk_fn,
914
+ pipeline_Q=pipeline_Q,
915
+ pipeline_dO=pipeline_dO,
916
+ tLSEsLSE=tLSEsLSE,
917
+ tLSEsdPsum=tLSEsdPsum,
918
+ tPsP=tPsP,
919
+ tdSsdS=tdSsdS,
920
+ tdQsdQaccum=tdQsdQaccum,
921
+ smem_thr_copy_PdS=smem_thr_copy_PdS,
922
+ smem_thr_copy_dQaccum=smem_thr_copy_dQaccum,
923
+ softmax_scale_log2=softmax_scale_log2,
924
+ # acc_dV=acc_dV,
925
+ # acc_dK=acc_dK,
926
+ )
927
+
928
+ consumer_state_Q = cutlass.pipeline.make_pipeline_state(
929
+ cutlass.pipeline.PipelineUserType.Consumer, self.Q_stage
930
+ )
931
+ consumer_state_dO = cutlass.pipeline.make_pipeline_state(
932
+ cutlass.pipeline.PipelineUserType.Consumer, self.dO_stage
933
+ )
934
+ tile_scheduler = TileSchedulerCls()
935
+ work_tile = tile_scheduler.initial_work_tile_info()
936
+ while work_tile.is_valid_tile:
937
+ n_block, head_idx, batch_idx, _ = work_tile.tile_idx
938
+ seqlen = SeqlenInfoCls(batch_idx)
939
+ mask = AttentionMaskCls(seqlen.seqlen_q, seqlen.seqlen_k)
940
+ mask_fn = partial(
941
+ mask.apply_mask,
942
+ batch_idx=None,
943
+ head_idx=None,
944
+ n_block=n_block,
945
+ thr_mma=thr_mma_SdP,
946
+ mask_seqlen=True,
947
+ mask_causal=self.is_causal,
948
+ mask_local=self.is_local,
949
+ )
950
+ m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
951
+ # if cute.arch.thread_idx()[0] % 32 == 0: cute.printf("tidx = {}, m_block_min = {}, m_block_max = {}", cute.arch.thread_idx()[0], m_block_min, m_block_max)
952
+ dKV_accumulate = False
953
+ for m_block in cutlass.range(m_block_min, m_block_max, unroll=1):
954
+ consumer_state_Q, consumer_state_dO = mma_one_m_block_all(
955
+ m_block,
956
+ consumer_state_Q,
957
+ consumer_state_dO,
958
+ mask_fn=mask_fn,
959
+ dKV_accumulate=dKV_accumulate,
960
+ )
961
+ dKV_accumulate = True
962
+
963
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dV)
964
+ # scale dK
965
+ acc_dK.store(acc_dK.load() * softmax_scale)
966
+ self.epilogue_dKV(
967
+ acc_dV,
968
+ mdV,
969
+ sV,
970
+ acc_dK,
971
+ mdK,
972
+ sK,
973
+ seqlen,
974
+ tma_atom_dK,
975
+ tma_atom_dV,
976
+ tiled_mma_dK,
977
+ tiled_mma_dV,
978
+ tidx,
979
+ n_block,
980
+ head_idx,
981
+ batch_idx,
982
+ )
983
+
984
+ tile_scheduler.advance_to_next_work()
985
+ work_tile = tile_scheduler.get_current_work()
986
+
987
+ @cute.jit
988
+ def mma_one_m_block(
989
+ self,
990
+ m_block: Int32,
991
+ consumer_state_Q: cutlass.pipeline.PipelineState | pipeline.PipelineStateSimple,
992
+ consumer_state_dO: cutlass.pipeline.PipelineState | pipeline.PipelineStateSimple,
993
+ warp_group_idx: Int32,
994
+ mma_qk_fn: Callable,
995
+ mma_dov_fn: Callable,
996
+ mma_pdo_fn: Callable,
997
+ mma_dsq_fn: Callable,
998
+ mma_dsk_fn: Callable,
999
+ pipeline_Q: cutlass.pipeline.PipelineAsync,
1000
+ pipeline_dO: cutlass.pipeline.PipelineAsync,
1001
+ tLSEsLSE: cute.Tensor,
1002
+ tLSEsdPsum: cute.Tensor,
1003
+ tPsP: Optional[cute.Tensor],
1004
+ tdSsdS: Optional[cute.Tensor],
1005
+ tdQsdQaccum: cute.Tensor,
1006
+ smem_thr_copy_PdS: cute.TiledCopy,
1007
+ smem_thr_copy_dQaccum: cute.TiledCopy,
1008
+ softmax_scale_log2: Float32,
1009
+ mask_fn: Optional[Callable] = None,
1010
+ # acc_dV,
1011
+ # acc_dK,
1012
+ dKV_accumulate: Boolean = True,
1013
+ ):
1014
+ consumer_state_dO_cur = (
1015
+ consumer_state_dO if const_expr(self.Q_stage == self.dO_stage) else consumer_state_Q
1016
+ )
1017
+ smem_idx_Q = consumer_state_Q.index
1018
+ smem_idx_dO = consumer_state_dO_cur.index if const_expr(self.dO_stage > 1) else 0
1019
+ smem_idx_PdS = smem_idx_Q if const_expr(self.PdS_stage > 1) else 0
1020
+ # (1) [GEMM 1] S = Q @ K^T
1021
+ pipeline_Q.consumer_wait(consumer_state_Q, pipeline_Q.consumer_try_wait(consumer_state_Q))
1022
+ acc_S = mma_qk_fn(A_idx=smem_idx_Q, wg_wait=-1)
1023
+ tLSErLSE = copy_utils.load_s2r(tLSEsLSE[None, smem_idx_Q])
1024
+ # (2) [GEMM 2] dP = dO @ V.T
1025
+ pipeline_dO.consumer_wait(
1026
+ consumer_state_dO_cur, pipeline_dO.consumer_try_wait(consumer_state_dO_cur)
1027
+ )
1028
+ acc_dP = mma_dov_fn(A_idx=smem_idx_Q, wg_wait=1)
1029
+ # (3) [Pointwise 1] P = exp(S - LSE)
1030
+ if cutlass.const_expr(mask_fn is not None):
1031
+ mask_fn(acc_S, m_block=m_block)
1032
+ acc_S_mn = utils.make_acc_tensor_mn_view(acc_S, transpose=self.SdP_swapAB)
1033
+ # if cute.arch.thread_idx()[0] == 256: cute.print_tensor(acc_S_mn)
1034
+ for r in cutlass.range_constexpr(cute.size(acc_S_mn, mode=[0])):
1035
+ for c in cutlass.range(cute.size(acc_S_mn, mode=[1]), unroll_full=True):
1036
+ acc_S_mn[r, c] = cute.math.exp2(
1037
+ acc_S_mn[r, c] * softmax_scale_log2 - tLSErLSE[r], fastmath=True
1038
+ )
1039
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_S_mn)
1040
+ tLSErdPsum = copy_utils.load_s2r(tLSEsdPsum[None, smem_idx_dO])
1041
+
1042
+ # Convert P from f32 -> f16
1043
+ tdVrP = utils.cvt_f16(utils.make_acc_tensor_frgA_view(acc_S), self.dtype)
1044
+ # R2S for P
1045
+ if const_expr(not self.mma_dkv_is_rs):
1046
+ # sync to ensure P has already been used in the previous iteration before overwriting
1047
+ if const_expr(self.PdS_stage == 1):
1048
+ cute.arch.barrier(
1049
+ barrier_id=int(NamedBarrierBwd.PdS), number_of_threads=self.num_mma_threads
1050
+ )
1051
+ tPrP = smem_thr_copy_PdS.retile(tdVrP)
1052
+ cute.copy(smem_thr_copy_PdS, tPrP, tPsP[None, None, None, smem_idx_PdS])
1053
+
1054
+ # (4) [Pointwise 2] dS = P*(dP-dPsum)
1055
+ warpgroup.wait_group(0)
1056
+ acc_dP_mn = utils.make_acc_tensor_mn_view(acc_dP, transpose=self.SdP_swapAB)
1057
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dP_mn)
1058
+ for r in cutlass.range_constexpr(cute.size(acc_dP_mn, mode=[0])):
1059
+ for c in cutlass.range(cute.size(acc_dP_mn, mode=[1]), unroll_full=True):
1060
+ acc_dP_mn[r, c] = acc_S_mn[r, c] * (acc_dP_mn[r, c] - tLSErdPsum[r])
1061
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dP_mn)
1062
+ # Convert dS from f32 -> f16
1063
+ tdKrdS = utils.cvt_f16(utils.make_acc_tensor_frgA_view(acc_dP), self.dtype)
1064
+
1065
+ # If there's double buffering on dS, we don't need to sync here.
1066
+ # Otherwise we might have WG1 writing to dS before WG2 is done reading from it during MmadQ.
1067
+ # But because both WGs have to sync at the end of the loop and double buffering,
1068
+ # this race condition is not possible.
1069
+ # This sync is to ensure (1) P is written in case of !mma_dkv_is_rs and
1070
+ # (2) dS is already read by the Mma in the previous iteration in case of mma_dkv_is_rs.
1071
+ if const_expr(not self.mma_dkv_is_rs or (self.PdS_stage == 1 and self.mma_dkv_is_rs)):
1072
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1073
+ cute.arch.barrier(
1074
+ barrier_id=int(NamedBarrierBwd.PdS), number_of_threads=self.num_mma_threads
1075
+ )
1076
+
1077
+ # R2S for dS
1078
+ tdSrdS = smem_thr_copy_PdS.retile(tdKrdS)
1079
+ cute.copy(smem_thr_copy_PdS, tdSrdS, tdSsdS[None, None, None, smem_idx_PdS])
1080
+
1081
+ # (5) [GEMM 3] dV += P.T @ dO
1082
+ if const_expr(not self.mma_dkv_is_rs):
1083
+ mma_pdo_fn(
1084
+ A_idx=smem_idx_PdS, B_idx=smem_idx_dO, zero_init=not dKV_accumulate, wg_wait=-1
1085
+ )
1086
+ else:
1087
+ mma_pdo_fn(tCrA=tdVrP, B_idx=smem_idx_dO, zero_init=not dKV_accumulate, wg_wait=-1)
1088
+
1089
+ # smem fence to make sure sdS is written before it's read by WGMMA
1090
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1091
+ cute.arch.barrier(
1092
+ barrier_id=int(NamedBarrierBwd.PdS), number_of_threads=self.num_mma_threads
1093
+ )
1094
+ # (6) [GEMM 4] dQ = dS @ K
1095
+ acc_dQ = mma_dsk_fn(A_idx=smem_idx_PdS, wg_wait=1)
1096
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dV)
1097
+ pipeline_dO.consumer_release(consumer_state_dO_cur) # release dO as dV mma is done
1098
+
1099
+ # (7) [GEMM 5] dK += dS.T @ Q
1100
+ if const_expr(not self.mma_dkv_is_rs):
1101
+ mma_dsq_fn(
1102
+ A_idx=smem_idx_PdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1
1103
+ )
1104
+ else:
1105
+ mma_dsq_fn(tCrA=tdKrdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1)
1106
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dQ)
1107
+
1108
+ cute.arch.barrier(
1109
+ barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1110
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1111
+ )
1112
+ tdQrdQaccum_flat = cute.make_tensor(acc_dQ.iterator, cute.make_layout(tdQsdQaccum.shape))
1113
+ cute.autovec_copy(tdQrdQaccum_flat, tdQsdQaccum)
1114
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1115
+ cute.arch.barrier_arrive(
1116
+ barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1117
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1118
+ )
1119
+
1120
+ warpgroup.wait_group(0)
1121
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dK)
1122
+ pipeline_Q.consumer_release(consumer_state_Q)
1123
+ # if cute.arch.thread_idx()[0] % 32 == 0: cute.printf("tidx = {}, m_block = {}, after pipeline_Q consumer release", cute.arch.thread_idx()[0], m_block)
1124
+
1125
+ consumer_state_Q.advance()
1126
+ consumer_state_dO.advance()
1127
+ return consumer_state_Q, consumer_state_dO
1128
+
1129
+ @cute.jit
1130
+ def epilogue_dKV(
1131
+ self,
1132
+ acc_dV: cute.Tensor,
1133
+ mdV: cute.Tensor,
1134
+ sV: cute.Tensor,
1135
+ acc_dK: cute.Tensor,
1136
+ mdK: cute.Tensor,
1137
+ sK: cute.Tensor,
1138
+ seqlen: SeqlenInfoQK,
1139
+ tma_atom_dK: cute.CopyAtom,
1140
+ tma_atom_dV: cute.CopyAtom,
1141
+ tiled_mma_dK: cute.TiledMma,
1142
+ tiled_mma_dV: cute.TiledMma,
1143
+ tidx: Int32,
1144
+ n_block: Int32,
1145
+ head_idx: Int32,
1146
+ batch_idx: Int32,
1147
+ ):
1148
+ rdV = cute.make_fragment_like(acc_dV, self.dtype)
1149
+ rdV.store(acc_dV.load().to(self.dtype))
1150
+ rdK = utils.cvt_f16(acc_dK, self.dtype)
1151
+
1152
+ cute.arch.barrier(
1153
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1154
+ )
1155
+
1156
+ smem_copy_atom_dKV = cute.make_copy_atom(
1157
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=self.dKV_swapAB, num_matrices=4),
1158
+ self.dtype,
1159
+ )
1160
+ smem_thr_copy_dK = cute.make_tiled_copy_C(smem_copy_atom_dKV, tiled_mma_dK).get_slice(tidx)
1161
+ smem_thr_copy_dV = cute.make_tiled_copy_C(smem_copy_atom_dKV, tiled_mma_dV).get_slice(tidx)
1162
+ mdV_cur = mdV[None, None, head_idx, batch_idx]
1163
+ mdK_cur = mdK[None, None, head_idx, batch_idx]
1164
+ gdK = cute.local_tile(mdK_cur, (self.tile_n, self.tile_hdim), (n_block, 0))
1165
+ gdV = cute.local_tile(mdV_cur, (self.tile_n, self.tile_hdimv), (n_block, 0))
1166
+ store_dK, _, _ = copy_utils.tma_get_copy_fn(
1167
+ tma_atom_dK, 0, cute.make_layout(1), sK, gdK, single_stage=True
1168
+ )
1169
+ store_dV, _, _ = copy_utils.tma_get_copy_fn(
1170
+ tma_atom_dV, 0, cute.make_layout(1), sV, gdV, single_stage=True
1171
+ )
1172
+
1173
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
1174
+ # rmem -> smem
1175
+ taccdVrdV = smem_thr_copy_dV.retile(rdV)
1176
+ sdV = sV if const_expr(not self.dKV_swapAB) else utils.transpose_view(sV) # reuse sV SMEM
1177
+ taccdVsdV = smem_thr_copy_dV.partition_D(sdV)
1178
+ cute.copy(smem_copy_atom_dKV, taccdVrdV, taccdVsdV)
1179
+ # ensure smem writes are visible to TMA
1180
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1181
+ cute.arch.barrier(
1182
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1183
+ )
1184
+ if warp_idx == 4:
1185
+ store_dV()
1186
+ taccdKrdK = smem_thr_copy_dK.retile(rdK)
1187
+ sdK = sK if const_expr(not self.dKV_swapAB) else utils.transpose_view(sK) # reuse sK SMEM
1188
+ taccdKsdK = smem_thr_copy_dK.partition_D(sdK) # reuse sK SMEM
1189
+ cute.copy(smem_copy_atom_dKV, taccdKrdK, taccdKsdK)
1190
+ # ensure smem writes are visible to TMA
1191
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1192
+ cute.arch.barrier(
1193
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1194
+ )
1195
+ # smem -> gmem
1196
+ if warp_idx == 4:
1197
+ store_dK()
1198
+ cute.arch.cp_async_bulk_commit_group()
1199
+ cute.arch.cp_async_bulk_wait_group(0, read=True)
1200
+
1201
+ @cute.jit
1202
+ def dQaccum_store(
1203
+ self,
1204
+ mdQaccum: cute.Tensor,
1205
+ sdQaccum: cute.Tensor,
1206
+ block_info: BlockInfo,
1207
+ TileSchedulerCls: cutlass.Constexpr[Callable],
1208
+ SeqlenInfoCls: cutlass.Constexpr[Callable],
1209
+ ):
1210
+ tile_scheduler = TileSchedulerCls()
1211
+ work_tile = tile_scheduler.initial_work_tile_info()
1212
+ while work_tile.is_valid_tile:
1213
+ n_block, head_idx, batch_idx, _ = work_tile.tile_idx
1214
+ seqlen = SeqlenInfoCls(batch_idx)
1215
+ mdQaccum_cur = mdQaccum[None, head_idx, batch_idx]
1216
+ gdQaccum_ = cute.local_tile(mdQaccum_cur, (self.tile_m * self.tile_hdim,), (None,))
1217
+ # (M * K / WG, WG, _)
1218
+ gdQaccum = cute.flat_divide(
1219
+ gdQaccum_, (self.tile_m * self.tile_hdim // self.num_mma_warp_groups,)
1220
+ )
1221
+ m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
1222
+ for m_block in cutlass.range(m_block_min, m_block_max, unroll=1):
1223
+ for warp_group_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
1224
+ cute.arch.barrier(
1225
+ barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1226
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1227
+ )
1228
+ with cute.arch.elect_one():
1229
+ copy_utils.cpasync_reduce_bulk_add_f32(
1230
+ sdQaccum[None, warp_group_idx].iterator,
1231
+ gdQaccum[None, warp_group_idx, m_block].iterator,
1232
+ self.tma_copy_bytes["dQ"],
1233
+ )
1234
+ cute.arch.cp_async_bulk_commit_group()
1235
+ for warp_group_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
1236
+ cute.arch.cp_async_bulk_wait_group(
1237
+ self.num_mma_warp_groups - 1 - warp_group_idx, read=True
1238
+ )
1239
+ cute.arch.barrier_arrive(
1240
+ barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1241
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1242
+ )
1243
+ tile_scheduler.advance_to_next_work()
1244
+ work_tile = tile_scheduler.get_current_work()
build/torch-universal/flash_fwd.py ADDED
The diff for this file is too large to render. See raw diff
 
build/torch-universal/flash_fwd_combine.py ADDED
@@ -0,0 +1,644 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_combine_kernel.h
3
+ # from Cutlass C++ to Cute-DSL.
4
+ import math
5
+ import operator
6
+ from typing import Type, Optional
7
+ from functools import partial
8
+
9
+ import cuda.bindings.driver as cuda
10
+
11
+ import cutlass
12
+ import cutlass.cute as cute
13
+ from cutlass.cute.nvgpu import cpasync
14
+ from cutlass import Float32, Int32, const_expr
15
+
16
+ from . import utils
17
+ from .fast_math import FastDivmod
18
+ from .seqlen_info import SeqlenInfo
19
+
20
+
21
+ class FlashAttentionForwardCombine:
22
+ def __init__(
23
+ self,
24
+ dtype: Type[cutlass.Numeric],
25
+ dtype_partial: Type[cutlass.Numeric],
26
+ head_dim: int,
27
+ m_block_size: int = 8,
28
+ k_block_size: int = 64,
29
+ log_max_splits: int = 4,
30
+ num_threads: int = 256,
31
+ stages: int = 4,
32
+ ):
33
+ """
34
+ Forward combine kernel for split attention computation.
35
+
36
+ :param dtype: output data type
37
+ :param dtype_partial: partial accumulation data type
38
+ :param head_dim: head dimension
39
+ :param m_block_size: m block size
40
+ :param k_block_size: k block size
41
+ :param log_max_splits: log2 of maximum splits
42
+ :param num_threads: number of threads
43
+ :param varlen: whether using variable length sequences
44
+ :param stages: number of pipeline stages
45
+ """
46
+ self.dtype = dtype
47
+ self.dtype_partial = dtype_partial
48
+ self.head_dim = head_dim
49
+ self.m_block_size = m_block_size
50
+ self.k_block_size = k_block_size
51
+ self.max_splits = 1 << log_max_splits
52
+ self.num_threads = num_threads
53
+ self.is_even_k = head_dim % k_block_size == 0
54
+ self.stages = stages
55
+
56
+ @staticmethod
57
+ def can_implement(
58
+ dtype, dtype_partial, head_dim, m_block_size, k_block_size,
59
+ log_max_splits, num_threads,
60
+ ) -> bool:
61
+ """Check if the kernel can be implemented with the given parameters."""
62
+ if dtype not in [cutlass.Float16, cutlass.BFloat16, cutlass.Float32]:
63
+ return False
64
+ if dtype_partial not in [cutlass.Float16, cutlass.BFloat16, Float32]:
65
+ return False
66
+ if head_dim % 8 != 0:
67
+ return False
68
+ if num_threads % 32 != 0:
69
+ return False
70
+ if m_block_size % 8 != 0:
71
+ return False
72
+ max_splits = 1 << log_max_splits
73
+ if max_splits > 256:
74
+ return False
75
+ if (m_block_size * max_splits) % num_threads != 0:
76
+ return False
77
+ return True
78
+
79
+ def _setup_attributes(self):
80
+ # GMEM copy setup for O partial
81
+ universal_copy_bits = 128
82
+ async_copy_elems = universal_copy_bits // self.dtype_partial.width
83
+ assert self.k_block_size % async_copy_elems == 0
84
+
85
+ k_block_gmem = (
86
+ 128 if self.k_block_size % 128 == 0 else
87
+ (64 if self.k_block_size % 64 == 0 else 32)
88
+ )
89
+ gmem_threads_per_row = k_block_gmem // async_copy_elems
90
+ assert self.num_threads % gmem_threads_per_row == 0
91
+
92
+ # Async copy atom for O partial load
93
+ atom_async_copy_partial = cute.make_copy_atom(
94
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
95
+ self.dtype_partial,
96
+ num_bits_per_copy=universal_copy_bits,
97
+ )
98
+ tOpartial_layout = cute.make_ordered_layout(
99
+ (self.num_threads // gmem_threads_per_row, gmem_threads_per_row),
100
+ order=(1, 0),
101
+ )
102
+ vOpartial_layout = cute.make_layout((1, async_copy_elems)) # 4 vals per load
103
+ self.gmem_tiled_copy_O_partial = cute.make_tiled_copy_tv(
104
+ atom_async_copy_partial, tOpartial_layout, vOpartial_layout
105
+ )
106
+
107
+ # GMEM copy setup for final O (use universal copy for store)
108
+ atom_universal_copy = cute.make_copy_atom(
109
+ cute.nvgpu.CopyUniversalOp(),
110
+ self.dtype,
111
+ num_bits_per_copy=async_copy_elems * self.dtype.width,
112
+ )
113
+ self.gmem_tiled_copy_O = cute.make_tiled_copy_tv(
114
+ atom_universal_copy, tOpartial_layout, vOpartial_layout # 4 vals per store
115
+ )
116
+
117
+ # LSE copy setup with async copy (alignment = 1)
118
+ lse_copy_bits = Float32.width # 1 element per copy, width is in bits
119
+ m_block_smem = (
120
+ 128 if self.m_block_size % 128 == 0 else
121
+ (64 if self.m_block_size % 64 == 0 else
122
+ (32 if self.m_block_size % 32 == 0 else
123
+ (16 if self.m_block_size % 16 == 0 else 8)))
124
+ )
125
+ gmem_threads_per_row_lse = m_block_smem
126
+ assert self.num_threads % gmem_threads_per_row_lse == 0
127
+
128
+ # Async copy atom for LSE load
129
+ atom_async_copy_lse = cute.make_copy_atom(
130
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.ALWAYS),
131
+ Float32,
132
+ num_bits_per_copy=lse_copy_bits,
133
+ )
134
+ tLSE_layout = cute.make_ordered_layout(
135
+ (self.num_threads // gmem_threads_per_row_lse, gmem_threads_per_row_lse),
136
+ order=(1, 0),
137
+ )
138
+ vLSE_layout = cute.make_layout(1)
139
+ self.gmem_tiled_copy_LSE = cute.make_tiled_copy_tv(
140
+ atom_async_copy_lse, tLSE_layout, vLSE_layout
141
+ )
142
+
143
+ # ///////////////////////////////////////////////////////////////////////////////
144
+ # Shared memory
145
+ # ///////////////////////////////////////////////////////////////////////////////
146
+
147
+ # Shared memory to register copy for LSE
148
+ self.smem_threads_per_col_lse = self.num_threads // m_block_smem
149
+ assert 32 % self.smem_threads_per_col_lse == 0 # Must divide warp size
150
+
151
+ s2r_layout_atom_lse = cute.make_ordered_layout(
152
+ (self.smem_threads_per_col_lse, self.num_threads // self.smem_threads_per_col_lse),
153
+ order=(0, 1),
154
+ )
155
+ self.s2r_tiled_copy_LSE = cute.make_tiled_copy_tv(
156
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32),
157
+ s2r_layout_atom_lse,
158
+ cute.make_layout(1),
159
+ )
160
+
161
+ # LSE shared memory layout with swizzling to avoid bank conflicts
162
+ # This works for kBlockMSmem = 8, 16, 32, 64, 128, no bank conflicts
163
+ if const_expr(m_block_smem == 8):
164
+ smem_lse_swizzle = cute.make_swizzle(5, 0, 5)
165
+ elif const_expr(m_block_smem == 16):
166
+ smem_lse_swizzle = cute.make_swizzle(4, 0, 4)
167
+ else:
168
+ smem_lse_swizzle = cute.make_swizzle(3, 2, 3)
169
+ smem_layout_atom_lse = cute.make_composed_layout(
170
+ smem_lse_swizzle,
171
+ 0,
172
+ cute.make_ordered_layout((8, m_block_smem), order=(1, 0))
173
+ )
174
+ self.smem_layout_lse = cute.tile_to_shape(
175
+ smem_layout_atom_lse, (self.max_splits, self.m_block_size), (0, 1)
176
+ )
177
+
178
+ # O partial shared memory layout (simple layout for pipeline stages)
179
+ self.smem_layout_o = cute.make_ordered_layout(
180
+ (self.m_block_size, self.k_block_size, self.stages),
181
+ order=(1, 0, 2)
182
+ )
183
+
184
+
185
+ @cute.jit
186
+ def __call__(
187
+ self,
188
+ mO_partial: cute.Tensor,
189
+ mLSE_partial: cute.Tensor,
190
+ mO: cute.Tensor,
191
+ mLSE: Optional[cute.Tensor] = None,
192
+ cu_seqlens: Optional[cute.Tensor] = None,
193
+ seqused: Optional[cute.Tensor] = None,
194
+ num_splits_dynamic_ptr: Optional[cute.Tensor] = None,
195
+ semaphore_to_reset: Optional[cute.Tensor] = None,
196
+ stream: cuda.CUstream = None,
197
+ ):
198
+ # Type checking
199
+ if const_expr(not (mO_partial.element_type == self.dtype_partial)):
200
+ raise TypeError("O partial tensor must match dtype_partial")
201
+ if const_expr(not (mO.element_type == self.dtype)):
202
+ raise TypeError("O tensor must match dtype")
203
+ if const_expr(not mLSE_partial.element_type in [Float32]):
204
+ raise TypeError("LSE partial tensor must be Float32")
205
+ if const_expr(mLSE is not None and not mLSE.element_type in [Float32]):
206
+ raise TypeError("LSE tensor must be Float32")
207
+
208
+ # Shape validation - input tensors are in user format, need to be converted to kernel format
209
+ if const_expr(len(mO_partial.shape) not in [4, 5]):
210
+ raise ValueError("O partial tensor must have 4 or 5 dimensions: (num_splits, batch, seqlen, nheads, headdim) or (num_splits, total_q, nheads, headdim)")
211
+ if const_expr(len(mLSE_partial.shape) not in [3, 4]):
212
+ raise ValueError("LSE partial tensor must have 3 or 4 dimensions: (num_splits, batch, seqlen, nheads) or (num_splits, total_q, nheads)")
213
+ if const_expr(len(mO.shape) not in [3, 4]):
214
+ raise ValueError("O tensor must have 3 or 4 dimensions: (batch, seqlen, nheads, headdim) or (total_q, nheads, headdim)")
215
+ if const_expr(mLSE is not None and len(mLSE.shape) not in [2, 3]):
216
+ raise ValueError("LSE tensor must have 2 or 3 dimensions: (batch, seqlen, nheads) or (total_q, nheads)")
217
+
218
+ # Assume all strides are divisible by 128 bits except the last stride
219
+ new_stride = lambda t: (*(cute.assume(s, divby=128 // t.element_type.width) for s in t.stride[:-1]), t.stride[-1])
220
+ mO_partial, mO = [cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t))) for t in (mO_partial, mO)]
221
+ # (num_splits, b, seqlen, h, d) -> (seqlen, d, num_splits, h, b)
222
+ # or (num_splits, total_q, h, d) -> (total_q, d, num_splits, h)
223
+ O_partial_layout_transpose = [2, 4, 0, 3, 1] if const_expr(cu_seqlens is None) else [1, 3, 0, 2]
224
+ # (b, seqlen, h, d) -> (seqlen, d, h, b) or (total_q, h, d) -> (total_q, d, h)
225
+ mO_partial = cute.make_tensor(mO_partial.iterator, cute.select(mO_partial.layout, mode=O_partial_layout_transpose))
226
+ O_layout_transpose = [1, 3, 2, 0] if const_expr(cu_seqlens is None) else [0, 2, 1]
227
+ mO = cute.make_tensor(mO.iterator, cute.select(mO.layout, mode=O_layout_transpose))
228
+ # (num_splits, b, seqlen, h) -> (seqlen, num_splits, h, b)
229
+ # or (num_splits, total_q, h) -> (total_q, num_splits, h)
230
+ LSE_partial_layout_transpose = [2, 0, 3, 1] if const_expr(cu_seqlens is None) else [1, 0, 2]
231
+ mLSE_partial = cute.make_tensor(mLSE_partial.iterator, cute.select(mLSE_partial.layout, mode=LSE_partial_layout_transpose))
232
+ # (b, seqlen, h) -> (seqlen, h, b) or (total_q, h) -> (total_q, h)
233
+ LSE_layout_transpose = [1, 2, 0] if const_expr(cu_seqlens is None) else [0, 1]
234
+ mLSE = cute.make_tensor(mLSE.iterator, cute.select(mLSE.layout, mode=LSE_layout_transpose)) if mLSE is not None else None
235
+
236
+ # Determine if we have variable length sequences
237
+ varlen = const_expr(cu_seqlens is not None or seqused is not None)
238
+
239
+ self._setup_attributes()
240
+
241
+ @cute.struct
242
+ class SharedStorage:
243
+ sLSE: cute.struct.Align[
244
+ cute.struct.MemRange[Float32, cute.cosize(self.smem_layout_lse)], 128
245
+ ]
246
+ sMaxValidSplit: cute.struct.Align[
247
+ cute.struct.MemRange[Int32, self.m_block_size], 128
248
+ ]
249
+ sO: cute.struct.Align[
250
+ cute.struct.MemRange[self.dtype_partial, cute.cosize(self.smem_layout_o)], 128
251
+ ]
252
+
253
+ smem_size = SharedStorage.size_in_bytes()
254
+
255
+ # Grid dimensions: (ceil_div(seqlen, m_block), ceil_div(head_dim, k_block), num_head * batch)
256
+ seqlen = mO_partial.shape[0]
257
+ num_head = mO_partial.shape[3]
258
+ batch_size = mO_partial.shape[4] if const_expr(cu_seqlens is None) else Int32(cu_seqlens.shape[0] - 1)
259
+
260
+ # Create FastDivmod objects for efficient division
261
+ seqlen_divmod = FastDivmod.create(seqlen)
262
+ head_divmod = FastDivmod.create(num_head)
263
+
264
+ grid_dim = (
265
+ cute.ceil_div(seqlen * num_head, self.m_block_size),
266
+ cute.ceil_div(self.head_dim, self.k_block_size),
267
+ batch_size,
268
+ )
269
+
270
+ self.kernel(
271
+ mO_partial,
272
+ mLSE_partial,
273
+ mO,
274
+ mLSE,
275
+ cu_seqlens,
276
+ seqused,
277
+ num_splits_dynamic_ptr,
278
+ semaphore_to_reset,
279
+ SharedStorage,
280
+ self.smem_layout_lse,
281
+ self.smem_layout_o,
282
+ self.gmem_tiled_copy_O_partial,
283
+ self.gmem_tiled_copy_O,
284
+ self.gmem_tiled_copy_LSE,
285
+ self.s2r_tiled_copy_LSE,
286
+ seqlen_divmod,
287
+ head_divmod,
288
+ varlen,
289
+ ).launch(
290
+ grid=grid_dim,
291
+ block=[self.num_threads, 1, 1],
292
+ smem=smem_size,
293
+ stream=stream,
294
+ )
295
+
296
+ @cute.kernel
297
+ def kernel(
298
+ self,
299
+ mO_partial: cute.Tensor,
300
+ mLSE_partial: cute.Tensor,
301
+ mO: cute.Tensor,
302
+ mLSE: Optional[cute.Tensor],
303
+ cu_seqlens: Optional[cute.Tensor],
304
+ seqused: Optional[cute.Tensor],
305
+ num_splits_dynamic_ptr: Optional[cute.Tensor],
306
+ semaphore_to_reset: Optional[cute.Tensor],
307
+ SharedStorage: cutlass.Constexpr,
308
+ smem_layout_lse: cute.Layout | cute.ComposedLayout,
309
+ smem_layout_o: cute.Layout,
310
+ gmem_tiled_copy_O_partial: cute.TiledCopy,
311
+ gmem_tiled_copy_O: cute.TiledCopy,
312
+ gmem_tiled_copy_LSE: cute.TiledCopy,
313
+ s2r_tiled_copy_LSE: cute.TiledCopy,
314
+ seqlen_divmod: FastDivmod,
315
+ head_divmod: FastDivmod,
316
+ varlen: cutlass.Constexpr[bool],
317
+ ):
318
+ # Thread and block indices
319
+ tidx, _, _ = cute.arch.thread_idx()
320
+ m_block, k_block, batch_idx = cute.arch.block_idx()
321
+
322
+ # ///////////////////////////////////////////////////////////////////////////////
323
+ # Get shared memory buffer
324
+ # ///////////////////////////////////////////////////////////////////////////////
325
+ smem = cutlass.utils.SmemAllocator()
326
+ storage = smem.allocate(SharedStorage)
327
+ sLSE = storage.sLSE.get_tensor(smem_layout_lse)
328
+ sMaxValidSplit = storage.sMaxValidSplit.get_tensor((self.m_block_size,))
329
+ sO = storage.sO.get_tensor(smem_layout_o)
330
+
331
+ # Handle semaphore reset
332
+ if const_expr(semaphore_to_reset is not None):
333
+ if (tidx == 0 and m_block == cute.arch.grid_dim()[0] - 1 and
334
+ k_block == cute.arch.grid_dim()[1] - 1 and
335
+ batch_idx == cute.arch.grid_dim()[2] - 1):
336
+ semaphore_to_reset[0] = 0
337
+
338
+ # Get number of splits
339
+ num_splits = (
340
+ num_splits_dynamic_ptr[batch_idx] if const_expr(num_splits_dynamic_ptr is not None)
341
+ else mLSE_partial.shape[1]
342
+ )
343
+ # Handle variable length sequences using SeqlenInfo
344
+ seqlen_info = SeqlenInfo.create(
345
+ batch_idx=batch_idx,
346
+ seqlen_static=mO_partial.shape[0],
347
+ cu_seqlens=cu_seqlens,
348
+ seqused=seqused
349
+ )
350
+ seqlen, offset = seqlen_info.seqlen, seqlen_info.offset
351
+
352
+ # Extract number of heads (head index will be determined dynamically)
353
+ num_head = mO_partial.shape[3]
354
+ max_idx = seqlen * num_head
355
+
356
+ # Early exit for single split if dynamic
357
+ if (const_expr(num_splits_dynamic_ptr is None) or num_splits > 1) and (const_expr(not varlen) or m_block * self.m_block_size < max_idx):
358
+
359
+ # ===============================
360
+ # Step 1: Load LSE_partial from gmem to shared memory
361
+ # ===============================
362
+
363
+ if const_expr(cu_seqlens is None):
364
+ # mLSE_partial_cur = mLSE_partial[None, None, None, batch_idx]
365
+ mLSE_partial_cur = utils.coord_offset_i64(mLSE_partial, batch_idx, dim=3)
366
+ else:
367
+ # mLSE_partial_cur = cute.domain_offset((offset, 0, 0), mLSE_partial)
368
+ mLSE_partial_cur = utils.domain_offset_i64((offset, 0, 0), mLSE_partial)
369
+ mLSE_partial_copy = cute.tiled_divide(mLSE_partial_cur, (1,))
370
+
371
+ gmem_thr_copy_LSE = gmem_tiled_copy_LSE.get_slice(tidx)
372
+ tLSEsLSE = gmem_thr_copy_LSE.partition_D(sLSE)
373
+
374
+ # Create identity tensor for coordinate tracking
375
+ cLSE = cute.make_identity_tensor((self.max_splits, self.m_block_size))
376
+ tLSEcLSE = gmem_thr_copy_LSE.partition_S(cLSE)
377
+
378
+ # Load LSE partial values
379
+ for m in cutlass.range(cute.size(tLSEcLSE, mode=[2]), unroll_full=True):
380
+ mi = tLSEcLSE[0, 0, m][1] # Get m coordinate
381
+ idx = m_block * self.m_block_size + mi
382
+ if idx < max_idx:
383
+ # Calculate actual sequence position and head using FastDivmod
384
+ if const_expr(not varlen):
385
+ head_idx, m_idx = seqlen_divmod.divmod(idx)
386
+ else:
387
+ head_idx = idx // seqlen
388
+ m_idx = idx - head_idx * seqlen
389
+ mLSE_partial_cur_copy = mLSE_partial_copy[None, m_idx, None, head_idx]
390
+ for s in cutlass.range(cute.size(tLSEcLSE, mode=[1]), unroll_full=True):
391
+ si = tLSEcLSE[0, s, 0][0] # Get split coordinate
392
+ if si < num_splits:
393
+ cute.copy(gmem_thr_copy_LSE, mLSE_partial_cur_copy[None, si], tLSEsLSE[None, s, m])
394
+ else:
395
+ tLSEsLSE[None, s, m].fill(-Float32.inf)
396
+ # Don't need to zero out the rest of the LSEs, as we will not write the output to gmem
397
+ cute.arch.cp_async_commit_group()
398
+
399
+ # ===============================
400
+ # Step 2: Load O_partial for pipeline stages
401
+ # ===============================
402
+
403
+ gmem_thr_copy_O_partial = gmem_tiled_copy_O_partial.get_slice(tidx)
404
+ cO = cute.make_identity_tensor((self.m_block_size, self.k_block_size))
405
+ tOcO = gmem_thr_copy_O_partial.partition_D(cO)
406
+ tOsO_partial = gmem_thr_copy_O_partial.partition_D(sO)
407
+ if const_expr(cu_seqlens is None):
408
+ # mO_partial_cur = mO_partial[None, None, None, None, batch_idx]
409
+ mO_partial_cur = utils.coord_offset_i64(mO_partial, batch_idx, dim=4)
410
+ else:
411
+ # mO_partial_cur = cute.domain_offset((offset, 0, 0, 0), mO_partial)
412
+ mO_partial_cur = utils.domain_offset_i64((offset, 0, 0, 0), mO_partial)
413
+
414
+ # Precompute these values to avoid recomputing them in the loop
415
+ num_rows = const_expr(cute.size(tOcO, mode=[1]))
416
+ tOmidx = cute.make_fragment(num_rows, cutlass.Int32)
417
+ tOhidx = cute.make_fragment(num_rows, cutlass.Int32)
418
+ tOrOptr = cute.make_fragment(num_rows, cutlass.Int64)
419
+ for m in cutlass.range(num_rows, unroll_full=True):
420
+ mi = tOcO[0, m, 0][0] # m coordinate
421
+ idx = m_block * self.m_block_size + mi
422
+ if const_expr(not varlen):
423
+ tOhidx[m], tOmidx[m] = seqlen_divmod.divmod(idx)
424
+ else:
425
+ tOhidx[m] = idx // seqlen
426
+ tOmidx[m] = idx - tOhidx[m] * seqlen
427
+ tOrOptr[m] = utils.elem_pointer_i64(mO_partial_cur, (tOmidx[m], k_block * self.k_block_size, 0, tOhidx[m])).toint()
428
+ if idx >= max_idx:
429
+ tOhidx[m] = -1
430
+
431
+ tOpO = cute.make_fragment(cute.size(tOcO, [2]), cutlass.Boolean)
432
+ if const_expr(not self.is_even_k):
433
+ for k in cutlass.range(cute.size(tOpO), unroll_full=True):
434
+ tOpO[k] = tOcO[0, 0, k][1] < mO_partial.shape[1] - k_block * self.k_block_size
435
+ # if cute.arch.thread_idx()[0] == 0 and k_block == 1: cute.print_tensor(tOpO)
436
+
437
+ load_O_partial = partial(
438
+ self.load_O_partial,
439
+ gmem_tiled_copy_O_partial,
440
+ tOrOptr,
441
+ tOsO_partial,
442
+ tOhidx,
443
+ tOpO,
444
+ tOcO,
445
+ mO_partial_cur.layout,
446
+ )
447
+
448
+ # Load first few stages of O_partial
449
+ for stage in cutlass.range(self.stages - 1, unroll_full=True):
450
+ if stage < num_splits:
451
+ load_O_partial(stage, stage)
452
+ cute.arch.cp_async_commit_group()
453
+
454
+ # ===============================
455
+ # Step 3: Load and transpose LSE from smem to registers
456
+ # ===============================
457
+
458
+ # Wait for LSE and initial O partial stages to complete
459
+ cute.arch.cp_async_wait_group(self.stages - 1)
460
+ cute.arch.sync_threads()
461
+ # if cute.arch.thread_idx()[0] == 0:
462
+ # # cute.print_tensor(sLSE)
463
+ # for i in range(64):
464
+ # cute.printf("sLSE[%d, 0] = %f", i, sLSE[i, 0])
465
+ # cute.arch.sync_threads()
466
+
467
+ s2r_thr_copy_LSE = s2r_tiled_copy_LSE.get_slice(tidx)
468
+ ts2rsLSE = s2r_thr_copy_LSE.partition_S(sLSE)
469
+ ts2rrLSE = cute.make_fragment_like(ts2rsLSE)
470
+ cute.copy(s2r_tiled_copy_LSE, ts2rsLSE, ts2rrLSE)
471
+
472
+ # ===============================
473
+ # Step 4: Compute final LSE along split dimension
474
+ # ===============================
475
+
476
+ lse_sum = cute.make_fragment(cute.size(ts2rrLSE, mode=[2]), Float32)
477
+ ts2rcLSE = s2r_thr_copy_LSE.partition_D(cLSE)
478
+ # We compute the max valid split for each row to short-circuit the computation later
479
+ max_valid_split = cute.make_fragment(cute.size(ts2rrLSE, mode=[2]), Int32)
480
+ assert cute.size(ts2rrLSE, mode=[0]) == 1
481
+ # Compute max, scales, and final LSE for each row
482
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
483
+ # Find max LSE value across splits
484
+ threads_per_col = const_expr(self.smem_threads_per_col_lse)
485
+ lse_max = utils.warp_reduce(
486
+ ts2rrLSE[None, None, m].load().reduce(cute.ReductionOp.MAX, init_val=-Float32.inf, reduction_profile=0),
487
+ op=cute.arch.fmax,
488
+ width=threads_per_col,
489
+ )
490
+ # if cute.arch.thread_idx()[0] == 0: cute.printf(lse_max)
491
+ # Find max valid split index
492
+ max_valid_idx = -1
493
+ for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
494
+ if ts2rrLSE[0, s, m] != -Float32.inf:
495
+ max_valid_idx = ts2rcLSE[0, s, 0][0] # Get split coordinate
496
+ # if cute.arch.thread_idx()[0] < 32: cute.printf(max_valid_idx)
497
+ max_valid_split[m] = utils.warp_reduce(max_valid_idx, max, width=threads_per_col)
498
+ # Compute exp scales and sum
499
+ lse_max_cur = 0.0 if lse_max == -Float32.inf else lse_max # In case all local LSEs are -inf
500
+ LOG2_E = math.log2(math.e)
501
+ lse_sum_cur = 0.0
502
+ for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
503
+ scale = utils.exp2f(ts2rrLSE[0, s, m] * LOG2_E - (lse_max_cur * LOG2_E))
504
+ lse_sum_cur += scale
505
+ ts2rrLSE[0, s, m] = scale # Store scale for later use
506
+ lse_sum_cur = utils.warp_reduce(lse_sum_cur, operator.add, width=threads_per_col)
507
+ lse_sum[m] = utils.logf(lse_sum_cur) + lse_max
508
+ # Normalize scales
509
+ inv_sum = 0.0 if (lse_sum_cur == 0.0 or lse_sum_cur != lse_sum_cur) else 1.0 / lse_sum_cur
510
+ ts2rrLSE[None, None, m].store(ts2rrLSE[None, None, m].load() * inv_sum)
511
+ # Store the scales exp(lse - lse_logsum) back to smem
512
+ cute.copy(s2r_tiled_copy_LSE, ts2rrLSE, ts2rsLSE)
513
+
514
+ # Store max valid split to smem
515
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
516
+ if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
517
+ mi = ts2rcLSE[0, 0, m][1]
518
+ if mi < self.m_block_size:
519
+ sMaxValidSplit[mi] = max_valid_split[m]
520
+
521
+ # ===============================
522
+ # Step 5: Store final LSE to gmem
523
+ # ===============================
524
+
525
+ if const_expr(mLSE is not None):
526
+ if const_expr(cu_seqlens is None):
527
+ # mLSE_cur = mLSE[None, None, batch_idx]
528
+ mLSE_cur = utils.coord_offset_i64(mLSE, batch_idx, dim=2)
529
+ else:
530
+ # mLSE_cur = cute.domain_offset((offset, 0), mLSE)
531
+ mLSE_cur = utils.domain_offset_i64((offset, 0), mLSE)
532
+ if k_block == 0: # Only first k_block writes LSE when mLSE is provided
533
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
534
+ if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
535
+ mi = ts2rcLSE[0, 0, m][1]
536
+ idx = m_block * self.m_block_size + mi
537
+ if idx < max_idx:
538
+ if const_expr(not varlen):
539
+ head_idx, m_idx = seqlen_divmod.divmod(idx)
540
+ else:
541
+ head_idx = idx // seqlen
542
+ m_idx = idx - head_idx * seqlen
543
+ mLSE_cur[m_idx, head_idx] = lse_sum[m]
544
+
545
+ # ===============================
546
+ # Step 6: Read O_partial and accumulate final O
547
+ # ===============================
548
+
549
+ cute.arch.sync_threads()
550
+
551
+ # Get max valid split for this thread
552
+ thr_max_valid_split = sMaxValidSplit[tOcO[0, 0, 0][0]]
553
+ for m in cutlass.range(1, cute.size(tOcO, mode=[1])):
554
+ thr_max_valid_split = max(thr_max_valid_split, sMaxValidSplit[tOcO[0, m, 0][0]])
555
+
556
+ tOrO_partial = cute.make_fragment_like(tOsO_partial[None, None, None, 0])
557
+ tOrO = cute.make_fragment_like(tOrO_partial, Float32)
558
+ tOrO.fill(0.0)
559
+
560
+ stage_load = self.stages - 1
561
+ stage_compute = 0
562
+
563
+ # Main accumulation loop
564
+ for s in cutlass.range(thr_max_valid_split + 1, unroll=4):
565
+ # Get scales for this split
566
+ scale = cute.make_fragment(num_rows, Float32)
567
+ for m in cutlass.range(num_rows, unroll_full=True):
568
+ scale[m] = sLSE[s, tOcO[0, m, 0][0]] # Get scale from smem
569
+
570
+ # Load next stage if needed
571
+ split_to_load = s + self.stages - 1
572
+ if split_to_load <= thr_max_valid_split:
573
+ load_O_partial(split_to_load, stage_load)
574
+ cute.arch.cp_async_commit_group()
575
+ stage_load = 0 if stage_load == self.stages - 1 else stage_load + 1
576
+
577
+ # Wait for the current stage to be ready
578
+ cute.arch.cp_async_wait_group(self.stages - 1)
579
+ # We don't need __syncthreads() because each thread is just reading its own data from smem
580
+ # Copy from smem to registers
581
+ cute.autovec_copy(tOsO_partial[None, None, None, stage_compute], tOrO_partial)
582
+ stage_compute = 0 if stage_compute == self.stages - 1 else stage_compute + 1
583
+
584
+ # Accumulate scaled partial results
585
+ for m in cutlass.range(num_rows, unroll_full=True):
586
+ if tOhidx[m] >= 0 and scale[m] > 0.0:
587
+ tOrO[None, m, None].store(tOrO[None, m, None].load() + scale[m] * tOrO_partial[None, m, None].load().to(Float32))
588
+
589
+ # ===============================
590
+ # Step 7: Write final O to gmem
591
+ # ===============================
592
+
593
+ rO = cute.make_fragment_like(tOrO, self.dtype)
594
+ rO.store(tOrO.load().to(self.dtype))
595
+ if const_expr(cu_seqlens is None):
596
+ # mO_cur = mO[None, None, None, batch_idx]
597
+ mO_cur = utils.coord_offset_i64(mO, batch_idx, dim=3)
598
+ else:
599
+ # mO_cur = cute.domain_offset((offset, 0, 0), mO)
600
+ mO_cur = utils.domain_offset_i64((offset, 0, 0), mO)
601
+ mO_cur = utils.domain_offset_aligned((0, k_block * self.k_block_size, 0), mO_cur)
602
+ elems_per_store = const_expr(cute.size(gmem_tiled_copy_O.layout_tv_tiled[1]))
603
+ # mO_cur_copy = cute.tiled_divide(mO_cur, (1, elems_per_store,))
604
+ gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
605
+ # Write final results
606
+ for m in cutlass.range(num_rows, unroll_full=True):
607
+ if tOhidx[m] >= 0:
608
+ mO_cur_copy = cute.tiled_divide(mO_cur[tOmidx[m], None, tOhidx[m]], (elems_per_store,))
609
+ for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
610
+ k_idx = tOcO[0, 0, k][1] // elems_per_store
611
+ if const_expr(self.is_even_k) or tOpO[k]:
612
+ cute.copy(gmem_thr_copy_O, rO[None, m, k], mO_cur_copy[None, k_idx])
613
+
614
+ @cute.jit
615
+ def load_O_partial(
616
+ self,
617
+ gmem_tiled_copy_O_partial: cute.TiledCopy,
618
+ tOrOptr: cute.Tensor,
619
+ tOsO_partial: cute.Tensor,
620
+ tOhidx: cute.Tensor,
621
+ tOpO: cute.Tensor,
622
+ tOcO: cute.Tensor,
623
+ mO_cur_partial_layout: cute.Layout,
624
+ split: Int32,
625
+ stage: Int32,
626
+ ) -> None:
627
+ elems_per_load = const_expr(cute.size(gmem_tiled_copy_O_partial.layout_tv_tiled[1]))
628
+ tOsO_partial_cur = tOsO_partial[None, None, None, stage]
629
+ for m in cutlass.range(cute.size(tOcO, [1]), unroll_full=True):
630
+ if tOhidx[m] >= 0:
631
+ o_gmem_ptr = cute.make_ptr(
632
+ tOsO_partial.element_type, tOrOptr[m], cute.AddressSpace.gmem, assumed_align=16
633
+ )
634
+ mO_partial_cur = cute.make_tensor(o_gmem_ptr, cute.slice_(mO_cur_partial_layout, (0, None, None, 0)))
635
+ mO_partial_cur_copy = cute.tiled_divide(mO_partial_cur, (elems_per_load,))
636
+ for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
637
+ k_idx = tOcO[0, 0, k][1] // elems_per_load
638
+ if const_expr(self.is_even_k) or tOpO[k]:
639
+ cute.copy(
640
+ gmem_tiled_copy_O_partial,
641
+ # mO_partial_cur_copy[None, k_idx, split],
642
+ utils.coord_offset_i64(mO_partial_cur_copy, split, dim=2)[None, k_idx],
643
+ tOsO_partial_cur[None, m, k]
644
+ )
build/torch-universal/flash_fwd_sm100.py ADDED
The diff for this file is too large to render. See raw diff
 
build/torch-universal/hopper_helpers.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ from typing import Type, Union, Optional
3
+ import cutlass
4
+ import cutlass.cute as cute
5
+ from cutlass import Int32, Float32, Boolean, const_expr
6
+ from cutlass.cute.nvgpu import warpgroup
7
+ from cutlass._mlir.dialects import llvm
8
+ from cutlass.cutlass_dsl import Numeric, dsl_user_op
9
+ from cutlass.utils import LayoutEnum
10
+ import cutlass.utils.hopper_helpers as sm90_utils_og
11
+
12
+
13
+ @cute.jit
14
+ def gemm(
15
+ tiled_mma: cute.TiledMma,
16
+ acc: cute.Tensor,
17
+ tCrA: cute.Tensor,
18
+ tCrB: cute.Tensor,
19
+ zero_init: cutlass.Constexpr[bool] = False,
20
+ wg_wait: cutlass.Constexpr[int] = 0,
21
+ # A_in_regs: cutlass.Constexpr[bool] = False,
22
+ swap_AB: cutlass.Constexpr[bool] = False,
23
+ ) -> None:
24
+ if const_expr(swap_AB):
25
+ gemm(tiled_mma, acc, tCrB, tCrA, zero_init=zero_init, wg_wait=wg_wait, swap_AB=False)
26
+ else:
27
+ warpgroup.fence()
28
+ # We make a new mma_atom since we'll be modifying its attribute (accumulate).
29
+ # Otherwise the compiler complains "operand #0 does not dominate this use"
30
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
31
+ mma_atom.set(warpgroup.Field.ACCUMULATE, not zero_init)
32
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
33
+ cute.gemm(mma_atom, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
34
+ mma_atom.set(warpgroup.Field.ACCUMULATE, True)
35
+ warpgroup.commit_group()
36
+ if const_expr(wg_wait >= 0):
37
+ warpgroup.wait_group(wg_wait)
38
+
39
+
40
+ def gemm_zero_init(
41
+ tiled_mma: cute.TiledMma,
42
+ shape: cute.Shape,
43
+ tCrA: cute.Tensor,
44
+ tCrB: cute.Tensor,
45
+ A_idx: Optional[Int32] = None,
46
+ B_idx: Optional[Int32] = None,
47
+ wg_wait: int = -1,
48
+ swap_AB: bool = False,
49
+ ) -> cute.Tensor:
50
+ if const_expr(swap_AB):
51
+ return gemm_zero_init(
52
+ tiled_mma, shape[::-1], tCrB, tCrA, B_idx, A_idx, wg_wait, swap_AB=False
53
+ )
54
+ else:
55
+ acc = cute.make_fragment(tiled_mma.partition_shape_C(shape), Float32)
56
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
57
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
58
+ gemm(tiled_mma, acc, rA, rB, zero_init=True, wg_wait=wg_wait)
59
+ return acc
60
+
61
+
62
+ def gemm_w_idx(
63
+ tiled_mma: cute.TiledMma,
64
+ acc: cute.Tensor,
65
+ tCrA: cute.Tensor,
66
+ tCrB: cute.Tensor,
67
+ zero_init: Boolean,
68
+ A_idx: Optional[Int32] = None,
69
+ B_idx: Optional[Int32] = None,
70
+ wg_wait: int = -1,
71
+ swap_AB: bool = False,
72
+ ) -> None:
73
+ if const_expr(swap_AB):
74
+ gemm_w_idx(tiled_mma, acc, tCrB, tCrA, zero_init, B_idx, A_idx, wg_wait, swap_AB=False)
75
+ else:
76
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
77
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
78
+ gemm(tiled_mma, acc, rA, rB, zero_init=zero_init, wg_wait=wg_wait)
79
+
80
+
81
+ @dsl_user_op
82
+ def make_smem_layout(
83
+ dtype: Type[Numeric],
84
+ layout: LayoutEnum,
85
+ shape: cute.Shape,
86
+ stage: Optional[int] = None,
87
+ *,
88
+ loc=None,
89
+ ip=None,
90
+ ) -> Union[cute.Layout, cute.ComposedLayout]:
91
+ major_mode_size = shape[1] if layout.is_n_major_c() else shape[0]
92
+ smem_layout_atom = warpgroup.make_smem_layout_atom(
93
+ sm90_utils_og.get_smem_layout_atom(layout, dtype, major_mode_size),
94
+ dtype,
95
+ )
96
+ order = (1, 0, 2) if const_expr(layout.is_m_major_c()) else (0, 1, 2)
97
+ smem_layout_staged = cute.tile_to_shape(
98
+ smem_layout_atom,
99
+ cute.append(shape, stage) if const_expr(stage is not None) else shape,
100
+ order=order if const_expr(stage is not None) else order[:2],
101
+ )
102
+ return smem_layout_staged
build/torch-universal/interface.py ADDED
@@ -0,0 +1,1483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # [2025-07-04] Version in Cute-DSL, for Hopper and Blackwell. You'll need install nvidia-cutlass-dsl==4.2.0.
3
+ # [2025-07-04] Version in Cute-DSL, for Hopper and Blackwell. You'll need install nvidia-cutlass-dsl==4.2.0.
4
+ # [2025-07-04] Version in Cute-DSL, for Hopper and Blackwell. You'll need install nvidia-cutlass-dsl==4.2.0.
5
+
6
+ # Supported features:
7
+ # - BF16 & FP16 dtype
8
+ # - noncausal & causal attention
9
+ # - MHA, GQA, MQA
10
+ # - hdim 64, 96, 128.
11
+ # - (hdim_qk, hdim_v) = (192, 128) for Blackwell (i.e. DeepSeek shape)
12
+ # - varlen
13
+ # - sliding window
14
+ # - bwd pass for Ampere (will also run on Hopper/Blackwell, but will be slow)
15
+
16
+ # Features not supported yet:
17
+ # - split (i.e. FlashDecoding)
18
+ # - tuned block sizes
19
+ # - paged KV
20
+ # - append KV to existing KV cache
21
+ # - FP8
22
+ # - bwd pass optimized for Hopper/Blackwell
23
+
24
+ import math
25
+ from typing import Optional, Tuple, Callable
26
+
27
+ import torch
28
+
29
+ import cuda.bindings.driver as cuda
30
+
31
+ import cutlass
32
+ import cutlass.cute as cute
33
+ from cutlass.cute.runtime import from_dlpack
34
+
35
+ from . import utils
36
+ from .flash_fwd import FlashAttentionForwardSm80, FlashAttentionForwardSm90
37
+ from .flash_fwd_sm100 import FlashAttentionForwardSm100
38
+ from .flash_bwd_preprocess import FlashAttentionBackwardPreprocess
39
+ from .flash_bwd import FlashAttentionBackwardSm80
40
+ from .flash_bwd_sm90 import FlashAttentionBackwardSm90
41
+ from .flash_bwd_sm100 import FlashAttentionBackwardSm100
42
+ from .flash_bwd_postprocess import FlashAttentionBackwardPostprocess
43
+ from .flash_fwd_combine import FlashAttentionForwardCombine
44
+
45
+ from .block_sparsity import (
46
+ BlockSparseTensorsTorch,
47
+ to_cute_block_sparse_tensors,
48
+ normalize_block_sparse_tensors,
49
+ )
50
+
51
+ def maybe_contiguous(x):
52
+ return x.contiguous() if x is not None and x.stride(-1) != 1 else x
53
+
54
+
55
+ torch2cute_dtype_map = {
56
+ torch.float16: cutlass.Float16,
57
+ torch.bfloat16: cutlass.BFloat16,
58
+ torch.float32: cutlass.Float32,
59
+ }
60
+
61
+
62
+ def num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, max_splits):
63
+ # If num_n_blocks is too small, use 1 split. For example, we never split for hdim = 128 and seqlen_k = 512.
64
+ if num_n_blocks <= 4:
65
+ return 1
66
+
67
+ # NOTE: We should revisit this heuristic after persistence is supported for split KV.
68
+ # Sometimes, it's ideal to over-schedule splits for better efficiency.
69
+ return min(num_SMs // total_mblocks, max_splits, num_n_blocks)
70
+
71
+
72
+ def _flash_attn_fwd(
73
+ q: torch.Tensor,
74
+ k: torch.Tensor,
75
+ v: torch.Tensor,
76
+ cu_seqlens_q: Optional[torch.Tensor] = None,
77
+ cu_seqlens_k: Optional[torch.Tensor] = None,
78
+ seqused_q: Optional[torch.Tensor] = None,
79
+ seqused_k: Optional[torch.Tensor] = None,
80
+ page_table: Optional[torch.Tensor] = None,
81
+ softmax_scale: Optional[float] = None,
82
+ causal: bool = False,
83
+ softcap: Optional[float] = None,
84
+ window_size_left: Optional[int] = None,
85
+ window_size_right: Optional[int] = None,
86
+ learnable_sink: Optional[torch.Tensor] = None,
87
+ # m_block_size: int = 128,
88
+ # n_block_size: int = 64,
89
+ # num_threads: int = 128,
90
+ m_block_size: int = 128,
91
+ n_block_size: int = 128,
92
+ num_threads: int = 384,
93
+ num_splits: int = 1,
94
+ pack_gqa: Optional[bool] = None,
95
+ _compute_capability: Optional[int] = None,
96
+ score_mod: Optional[Callable] = None,
97
+ mask_mod: Optional[Callable] = None,
98
+ block_sparse_tensors: Optional[BlockSparseTensorsTorch] = None,
99
+ return_lse: bool = False,
100
+ out: Optional[torch.Tensor] = None,
101
+ lse: Optional[torch.Tensor] = None,
102
+ aux_tensors: Optional[list[torch.Tensor]] = None,
103
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
104
+ """Forward pass for FlashAttention.
105
+
106
+ Args:
107
+ ...
108
+ score_mod: A callable that takes the attention scores and applies a modification.
109
+ return_lse: Whether to return the log softmax of the attention scores. If set to True will always calculate
110
+ out: Optional pre-allocated output tensor. If None, will be allocated internally.
111
+ lse: Optional pre-allocated log-sum-exp tensor. If None, will be allocated when needed.
112
+ aux_tensors: Some score_mods will want to read from global aux_tensors. This is how we thread them through to the inner kernel.
113
+ """
114
+ q, k, v = [maybe_contiguous(t) for t in (q, k, v)]
115
+ num_head, head_dim = q.shape[-2:]
116
+ if cu_seqlens_q is None:
117
+ batch_size, seqlen_q = q.shape[:2]
118
+ total_q = batch_size * seqlen_q
119
+ else:
120
+ batch_size = cu_seqlens_q.shape[0] - 1
121
+ seqlen_q = None
122
+ total_q = q.shape[0]
123
+ if page_table is not None:
124
+ assert cu_seqlens_k is None, "page_table is not supported with cu_seqlens_k"
125
+ assert page_table.dtype == torch.int32, "page_table must be int32"
126
+ assert page_table.stride(-1) == 1, "page_table must be contiguous in the last dimension"
127
+ max_num_pages_per_seq = page_table.shape[1]
128
+ assert page_table.shape == (batch_size, max_num_pages_per_seq)
129
+ num_pages, page_size = k.shape[:2]
130
+ seqlen_k = num_pages * page_size
131
+ else:
132
+ num_pages, page_size = None, None
133
+ seqlen_k = k.shape[-3]
134
+ num_head_kv = k.shape[-2]
135
+ head_dim_v = v.shape[-1]
136
+ if cu_seqlens_k is None:
137
+ if page_table is None:
138
+ assert k.shape == (batch_size, seqlen_k, num_head_kv, head_dim)
139
+ assert v.shape == (batch_size, seqlen_k, num_head_kv, head_dim_v)
140
+ else:
141
+ assert k.shape == (num_pages, page_size, num_head_kv, head_dim)
142
+ assert v.shape == (num_pages, page_size, num_head_kv, head_dim_v)
143
+ else:
144
+ assert k.shape == (seqlen_k, num_head_kv, head_dim)
145
+ assert v.shape == (seqlen_k, num_head_kv, head_dim_v)
146
+ assert cu_seqlens_k.shape == (batch_size + 1,), (
147
+ "cu_seqlens_k must have shape (batch_size + 1,)"
148
+ )
149
+
150
+ if cu_seqlens_q is not None:
151
+ assert cu_seqlens_q.shape == (batch_size + 1,), (
152
+ "cu_seqlens_q must have shape (batch_size + 1,)"
153
+ )
154
+ assert seqused_q is None or seqused_q.shape == (batch_size,), (
155
+ "seqused_q must have shape (batch_size,)"
156
+ )
157
+ assert seqused_k is None or seqused_k.shape == (batch_size,), (
158
+ "seqused_k must have shape (batch_size,)"
159
+ )
160
+ assert q.dtype in [torch.float16, torch.bfloat16], "inputs must be float16 or bfloat16"
161
+ assert q.dtype == k.dtype == v.dtype, "inputs must have the same dtype"
162
+ for t in [cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k]:
163
+ if t is not None:
164
+ assert t.dtype == torch.int32, (
165
+ "cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k must be int32"
166
+ )
167
+ assert t.stride(0) == 1, (
168
+ "cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k must be contiguous"
169
+ )
170
+ if learnable_sink is not None:
171
+ assert learnable_sink.shape == (num_head,)
172
+ assert learnable_sink.dtype == torch.bfloat16, "learnable_sink must be bfloat16"
173
+
174
+ assert all(
175
+ t is None or t.is_cuda
176
+ for t in (
177
+ q,
178
+ k,
179
+ v,
180
+ cu_seqlens_q,
181
+ cu_seqlens_k,
182
+ seqused_q,
183
+ seqused_k,
184
+ page_table,
185
+ learnable_sink,
186
+ )
187
+ ), "inputs must be on CUDA device"
188
+ assert num_head % num_head_kv == 0, "num_head must be divisible by num_head_kv"
189
+ assert head_dim <= 256, "head_dim must be less than or equal to 256"
190
+ alignment = 16 // q.element_size()
191
+ assert head_dim % alignment == 0, f"head_dim must be divisible by {alignment}"
192
+ assert head_dim_v % alignment == 0, f"head_dim_v must be divisible by {alignment}"
193
+ if softmax_scale is None:
194
+ softmax_scale = 1.0 / math.sqrt(head_dim)
195
+ if softcap == 0.0:
196
+ softcap = None
197
+ qhead_per_kvhead = num_head // num_head_kv
198
+ if pack_gqa is None:
199
+ pack_gqa = qhead_per_kvhead > 1
200
+
201
+ out_torch_dtype = q.dtype
202
+ device = q.device
203
+ q_batch_seqlen_shape = (batch_size, seqlen_q) if cu_seqlens_q is None else (total_q,)
204
+ lse_shape = (batch_size, num_head, seqlen_q) if cu_seqlens_q is None else (num_head, total_q)
205
+ requires_grad = q.requires_grad or k.requires_grad or v.requires_grad
206
+
207
+ if out is None:
208
+ out = torch.empty(
209
+ *q_batch_seqlen_shape, num_head, head_dim_v, dtype=out_torch_dtype, device=device
210
+ )
211
+ else:
212
+ expected_out_shape = (*q_batch_seqlen_shape, num_head, head_dim_v)
213
+ assert out.shape == expected_out_shape, (
214
+ f"out tensor shape {out.shape} does not match expected shape {expected_out_shape}"
215
+ )
216
+ assert out.dtype == out_torch_dtype, (
217
+ f"out tensor dtype {out.dtype} does not match expected dtype {out_torch_dtype}"
218
+ )
219
+ assert out.device == device, (
220
+ f"out tensor device {out.device} does not match input device {device}"
221
+ )
222
+ assert out.is_cuda, "out tensor must be on CUDA device"
223
+
224
+ if lse is None:
225
+ lse = (
226
+ torch.empty(lse_shape, dtype=torch.float32, device=device)
227
+ if requires_grad or return_lse
228
+ else None
229
+ )
230
+ elif lse is not None:
231
+ assert lse.shape == lse_shape, (
232
+ f"lse tensor shape {lse.shape} does not match expected shape {lse_shape}"
233
+ )
234
+ assert lse.dtype == torch.float32, (
235
+ f"lse tensor dtype {lse.dtype} does not match expected dtype torch.float32"
236
+ )
237
+ assert lse.device == device, (
238
+ f"lse tensor device {lse.device} does not match input device {device}"
239
+ )
240
+ assert lse.is_cuda, "lse tensor must be on CUDA device"
241
+
242
+ dtype = torch2cute_dtype_map[q.dtype]
243
+ (
244
+ cu_seqlens_q_tensor,
245
+ cu_seqlens_k_tensor,
246
+ seqused_q_tensor,
247
+ seqused_k_tensor,
248
+ learnable_sink_tensor,
249
+ ) = [
250
+ from_dlpack(t.detach(), assumed_align=4).mark_layout_dynamic(leading_dim=0)
251
+ if t is not None
252
+ else None
253
+ for t in (cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k, learnable_sink)
254
+ ]
255
+ page_table_tensor = (
256
+ from_dlpack(page_table.detach(), assumed_align=4).mark_layout_dynamic(leading_dim=1)
257
+ if page_table is not None
258
+ else None
259
+ )
260
+ sparse_tensors = None
261
+ if block_sparse_tensors is not None:
262
+ if seqlen_q is None:
263
+ raise ValueError("Block sparsity requires fixed-length sequences (seqlen_q must be known).")
264
+ expected_m_blocks = (seqlen_q + m_block_size - 1) // m_block_size
265
+ expected_n_blocks = (seqlen_k + n_block_size - 1) // n_block_size
266
+ block_sparse_tensors = normalize_block_sparse_tensors(
267
+ block_sparse_tensors,
268
+ expected_count_shape=(batch_size, num_head, expected_m_blocks),
269
+ expected_index_shape=(batch_size, num_head, expected_m_blocks, expected_n_blocks),
270
+ )
271
+ sparse_tensors = to_cute_block_sparse_tensors(block_sparse_tensors)
272
+
273
+ use_block_sparsity = sparse_tensors is not None
274
+
275
+ if mask_mod is None:
276
+ if causal:
277
+ window_size_right = 0
278
+ local = window_size_left is not None or window_size_right is not None
279
+ if window_size_left is not None or window_size_right is not None:
280
+ if window_size_left is None and window_size_right == 0:
281
+ causal, local = True, False
282
+ else:
283
+ causal, local = False, True
284
+ else:
285
+ causal, local = False, False
286
+
287
+ compute_capability = (
288
+ torch.cuda.get_device_capability()[0]
289
+ if _compute_capability is None
290
+ else _compute_capability
291
+ )
292
+ assert compute_capability in [9, 10], "Unsupported compute capability. Supported: 9.x, 10.x"
293
+ current_stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
294
+
295
+ if compute_capability == 9: # TODO: tune block size according to hdim.
296
+ if head_dim == head_dim_v == 128 and not causal and not local and not use_block_sparsity:
297
+ n_block_size = 192
298
+ if compute_capability == 10:
299
+ # TODO: fix the varlen case
300
+ if (
301
+ pack_gqa
302
+ and (128 % qhead_per_kvhead != 0)
303
+ or (cu_seqlens_q is not None or seqused_q is not None)
304
+ ):
305
+ pack_gqa = False
306
+ # TODO: fix GQA + SplitKV + non-varlen
307
+ if pack_gqa and num_splits != 1 and cu_seqlens_q is None:
308
+ pack_gqa = False
309
+
310
+ if num_splits < 1:
311
+ max_seqlen_k = seqlen_k if cu_seqlens_k is None else (cu_seqlens_k[1:] - cu_seqlens_k[:-1]).max().item()
312
+ max_seqlen_q = seqlen_q if cu_seqlens_q is None else (cu_seqlens_q[1:] - cu_seqlens_q[:-1]).max().item()
313
+ seqlen_q_packgqa = max_seqlen_q * qhead_per_kvhead
314
+ seqlen_k_loaded = max_seqlen_k if not local else max(0, min(max_seqlen_k, window_size_right + window_size_left + 1 + m_block_size))
315
+ num_n_blocks = (seqlen_k_loaded + n_block_size - 1) // n_block_size
316
+ num_m_blocks = (seqlen_q_packgqa + m_block_size - 1) // m_block_size
317
+ total_mblocks = batch_size * num_head_kv * num_m_blocks
318
+ num_splits = num_splits_heuristic(
319
+ total_mblocks,
320
+ torch.cuda.get_device_properties(device).multi_processor_count,
321
+ num_n_blocks,
322
+ 128,
323
+ )
324
+
325
+ is_split_kv = num_splits > 1
326
+ if is_split_kv:
327
+ out_partial = torch.empty(num_splits, *q_batch_seqlen_shape, num_head, head_dim_v, dtype=torch.float32, device=device)
328
+ lse_partial = torch.empty(num_splits, *lse_shape, dtype=torch.float32, device=device)
329
+
330
+ q_tensor, k_tensor, v_tensor, o_tensor = [
331
+ from_dlpack(t.detach(), assumed_align=16).mark_layout_dynamic(leading_dim=t.ndim - 1)
332
+ for t in (q, k, v, out if not is_split_kv else out_partial)
333
+ ]
334
+ if is_split_kv:
335
+ lse_tensor = from_dlpack(lse_partial.detach(), assumed_align=4).mark_layout_dynamic(leading_dim=lse_partial.ndim - 1)
336
+ elif lse is not None:
337
+ lse_tensor = from_dlpack(lse.detach(), assumed_align=4).mark_layout_dynamic(leading_dim=lse.ndim - 1)
338
+ else:
339
+ lse_tensor = None
340
+
341
+ # hash score and mask mods for compile cache
342
+ score_mod_hash = utils.hash_callable(score_mod) if score_mod is not None else False
343
+ mask_mod_hash = utils.hash_callable(mask_mod) if mask_mod is not None else False
344
+
345
+ if softcap is not None:
346
+ assert score_mod is None, "softcap and score_mod cannot be used together"
347
+ score_mod = utils.create_softcap_scoremod(softcap)
348
+
349
+ is_varlen = (
350
+ cu_seqlens_q is not None
351
+ or cu_seqlens_k is not None
352
+ or seqused_q is not None
353
+ or seqused_k is not None
354
+ )
355
+ if score_mod is not None:
356
+ if is_varlen:
357
+ raise NotImplementedError(
358
+ "score_mod with aux_tensors is not yet supported for varlen sequences. This will be fixed in a future PR."
359
+ )
360
+
361
+ if mask_mod is not None:
362
+ if not use_block_sparsity:
363
+ raise NotImplementedError(
364
+ "mask_mod requires the use of block sparsity. This will be fixed in a future PR."
365
+ )
366
+ if is_varlen:
367
+ raise NotImplementedError(
368
+ "mask_mod with aux_tensors is not yet supported for varlen sequences. This will be fixed in a future PR."
369
+ )
370
+ if pack_gqa:
371
+ raise NotImplementedError(
372
+ "mask_mod with aux_tensors is not yet supported with pack_gqa=True. This will be fixed in a future PR."
373
+ )
374
+
375
+ if use_block_sparsity:
376
+ if is_varlen:
377
+ raise NotImplementedError(
378
+ "Block sparsity is not yet supported for varlen sequences. This will be fixed in a future PR."
379
+ )
380
+ if pack_gqa:
381
+ raise NotImplementedError(
382
+ "Block sparsity is not yet supported with pack_gqa=True. This will be fixed in a future PR."
383
+ )
384
+
385
+ cute_aux_tensors = None
386
+ if aux_tensors is not None:
387
+ cute_aux_tensors = [from_dlpack(buf).mark_layout_dynamic() for buf in aux_tensors]
388
+
389
+ compile_key = (
390
+ dtype,
391
+ head_dim,
392
+ head_dim_v,
393
+ qhead_per_kvhead,
394
+ causal,
395
+ score_mod_hash,
396
+ mask_mod_hash,
397
+ use_block_sparsity,
398
+ len(aux_tensors) if aux_tensors is not None else 0,
399
+ lse is None,
400
+ cu_seqlens_q is None,
401
+ cu_seqlens_k is None,
402
+ seqused_q is None,
403
+ seqused_k is None,
404
+ page_table is not None,
405
+ window_size_left is not None,
406
+ window_size_right is not None,
407
+ learnable_sink is not None,
408
+ m_block_size,
409
+ n_block_size,
410
+ num_threads,
411
+ is_split_kv,
412
+ pack_gqa,
413
+ compute_capability,
414
+ )
415
+
416
+ if compile_key not in _flash_attn_fwd.compile_cache:
417
+ if compute_capability == 9:
418
+ assert page_table is None, "paged KV not supported on SM 9.0"
419
+ assert not is_split_kv, "SplitKV not supported on SM 9.0"
420
+ # fa_fwd = FlashAttentionForwardSm80(
421
+ fa_fwd = FlashAttentionForwardSm90(
422
+ dtype,
423
+ head_dim,
424
+ head_dim_v,
425
+ qhead_per_kvhead,
426
+ is_causal=causal,
427
+ is_local=local,
428
+ pack_gqa=pack_gqa,
429
+ tile_m=m_block_size,
430
+ tile_n=n_block_size,
431
+ # num_stages=1,
432
+ num_stages=2,
433
+ num_threads=num_threads,
434
+ Q_in_regs=False,
435
+ intra_wg_overlap=True,
436
+ mma_pv_is_rs=True,
437
+ mask_mod=mask_mod,
438
+ score_mod=score_mod,
439
+ has_aux_tensors=aux_tensors is not None,
440
+ )
441
+ elif compute_capability == 10:
442
+ assert page_size in [None, 128], (
443
+ "Only page_size=128 is supported for paged KV on SM 10.0"
444
+ )
445
+ if sparse_tensors is not None:
446
+ raise NotImplementedError("BlockSparsity not yet supported on SM 10.0")
447
+ fa_fwd = FlashAttentionForwardSm100(
448
+ head_dim,
449
+ head_dim_v,
450
+ qhead_per_kvhead=qhead_per_kvhead,
451
+ is_causal=causal,
452
+ is_local=local,
453
+ is_split_kv=is_split_kv,
454
+ pack_gqa=pack_gqa,
455
+ is_persistent=not causal
456
+ and not local
457
+ and cu_seqlens_q is None
458
+ and seqused_q is None
459
+ and not is_split_kv,
460
+ score_mod=score_mod,
461
+ has_aux_tensors=aux_tensors is not None,
462
+ )
463
+ else:
464
+ raise ValueError(
465
+ f"Unsupported compute capability: {compute_capability}. Supported: 9.x, 10.x"
466
+ )
467
+ # TODO: check @can_implement
468
+ _flash_attn_fwd.compile_cache[compile_key] = cute.compile(
469
+ fa_fwd,
470
+ q_tensor,
471
+ k_tensor,
472
+ v_tensor,
473
+ o_tensor,
474
+ lse_tensor,
475
+ softmax_scale,
476
+ current_stream,
477
+ cu_seqlens_q_tensor,
478
+ cu_seqlens_k_tensor,
479
+ seqused_q_tensor,
480
+ seqused_k_tensor,
481
+ page_table_tensor,
482
+ window_size_left,
483
+ window_size_right,
484
+ learnable_sink_tensor,
485
+ sparse_tensors,
486
+ cute_aux_tensors,
487
+ )
488
+ _flash_attn_fwd.compile_cache[compile_key](
489
+ q_tensor,
490
+ k_tensor,
491
+ v_tensor,
492
+ o_tensor,
493
+ lse_tensor,
494
+ softmax_scale,
495
+ current_stream,
496
+ cu_seqlens_q_tensor,
497
+ cu_seqlens_k_tensor,
498
+ seqused_q_tensor,
499
+ seqused_k_tensor,
500
+ page_table_tensor,
501
+ window_size_left,
502
+ window_size_right,
503
+ learnable_sink_tensor,
504
+ sparse_tensors,
505
+ cute_aux_tensors,
506
+ )
507
+ if is_split_kv:
508
+ _flash_attn_fwd_combine(
509
+ out_partial,
510
+ lse_partial.transpose(-1, -2),
511
+ out,
512
+ lse.transpose(-1, -2) if lse is not None else None,
513
+ cu_seqlens_q,
514
+ seqused_q,
515
+ )
516
+ return out, lse
517
+
518
+
519
+ _flash_attn_fwd.compile_cache = {}
520
+
521
+
522
+ def _flash_attn_bwd(
523
+ q: torch.Tensor,
524
+ k: torch.Tensor,
525
+ v: torch.Tensor,
526
+ out: torch.Tensor,
527
+ dout: torch.Tensor,
528
+ lse: torch.Tensor,
529
+ softmax_scale: Optional[float] = None,
530
+ causal: bool = False,
531
+ softcap: float = 0.0,
532
+ m_block_size: int = 64,
533
+ n_block_size: int = 128,
534
+ num_threads: int = 256,
535
+ pack_gqa: bool = False,
536
+ num_stages_Q: int = 2,
537
+ num_stages_dO: int = 2,
538
+ SdP_swapAB: bool = False,
539
+ dKV_swapAB: bool = False,
540
+ dQ_swapAB: bool = False,
541
+ AtomLayoutMSdP: int = 2,
542
+ AtomLayoutNdKV: int = 2,
543
+ AtomLayoutMdQ: int = 2,
544
+ V_in_regs: bool = False,
545
+ cu_seqlens_q: Optional[torch.Tensor] = None,
546
+ cu_seqlens_k: Optional[torch.Tensor] = None,
547
+ seqused_q: Optional[torch.Tensor] = None,
548
+ seqused_k: Optional[torch.Tensor] = None,
549
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
550
+ compute_capability = torch.cuda.get_device_capability()[0]
551
+ assert compute_capability in [9, 10], "Unsupported compute capability. Supported: 9.x, 10.x"
552
+
553
+ if compute_capability == 9:
554
+ m_block_size = 80 if not causal else 64
555
+ n_block_size = 128
556
+ num_stages_Q = 2
557
+ num_stages_dO = 2
558
+ num_stages_PdS = 2
559
+ SdP_swapAB = True
560
+ dKV_swapAB = False
561
+ dQ_swapAB = not causal
562
+ AtomLayoutMSdP = 1
563
+ AtomLayoutNdKV = 2
564
+ AtomLayoutMdQ = 1
565
+ cluster_size = 1
566
+ else:
567
+ m_block_size = 128
568
+ n_block_size = 128
569
+ dQ_swapAB = False
570
+ dKV_swapAB = False
571
+ AtomLayoutMdQ = 1
572
+ AtomLayoutNdKV = 1
573
+ # TODO: support cluster size 2
574
+ cluster_size = 1
575
+ q, k, v, out, dout, lse, cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k = [
576
+ maybe_contiguous(t)
577
+ for t in (q, k, v, out, dout, lse, cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k)
578
+ ]
579
+ num_head, head_dim = q.shape[-2:]
580
+ if cu_seqlens_q is None:
581
+ batch_size, seqlen_q = q.shape[:2]
582
+ total_q = batch_size * seqlen_q
583
+ else:
584
+ batch_size = cu_seqlens_q.shape[0] - 1
585
+ seqlen_q = None
586
+ total_q = q.shape[0]
587
+
588
+ if cu_seqlens_k is None:
589
+ batch_size, seqlen_k = k.shape[:2]
590
+ total_k = batch_size * seqlen_k
591
+ else:
592
+ batch_size = cu_seqlens_k.shape[0] - 1
593
+ seqlen_k = None
594
+ total_k = k.shape[0]
595
+
596
+ num_head_kv = k.shape[-2]
597
+ head_dim_v = v.shape[-1]
598
+
599
+ if cu_seqlens_k is None:
600
+ assert k.shape == (batch_size, seqlen_k, num_head_kv, head_dim)
601
+ assert v.shape == (batch_size, seqlen_k, num_head_kv, head_dim_v)
602
+ else:
603
+ assert k.shape == (total_k, num_head_kv, head_dim)
604
+ assert v.shape == (total_k, num_head_kv, head_dim_v)
605
+ assert cu_seqlens_k.shape == (batch_size + 1,), (
606
+ "cu_seqlens_k must have shape (batch_size + 1,)"
607
+ )
608
+
609
+ if cu_seqlens_q is not None:
610
+ assert cu_seqlens_q.shape == (batch_size + 1,), (
611
+ "cu_seqlens_q must have shape (batch_size + 1,)"
612
+ )
613
+
614
+ assert out.shape == (total_q, num_head, head_dim_v)
615
+ assert dout.shape == (total_q, num_head, head_dim_v)
616
+ assert lse.shape == (num_head, total_q), "lse must have shape (num_head, total_q)"
617
+ else:
618
+ assert out.shape == (batch_size, seqlen_q, num_head, head_dim_v)
619
+ assert dout.shape == (batch_size, seqlen_q, num_head, head_dim_v)
620
+ assert lse.shape == (batch_size, num_head, seqlen_q), (
621
+ "lse must have shape (batch_size, num_head, seqlen_q)"
622
+ )
623
+
624
+ assert q.dtype in [torch.float16, torch.bfloat16], "inputs must be float16 or bfloat16"
625
+ assert q.dtype == k.dtype == v.dtype == out.dtype == dout.dtype, (
626
+ "inputs must have the same dtype"
627
+ )
628
+ for t in [cu_seqlens_q, cu_seqlens_k]:
629
+ if t is not None:
630
+ assert t.dtype == torch.int32, "cu_seqlens_q, cu_seqlens_k must be int32"
631
+ assert lse.dtype == torch.float32, "lse must be float32"
632
+ assert all(
633
+ t is None or t.is_cuda for t in (q, k, v, out, dout, lse, cu_seqlens_q, cu_seqlens_k)
634
+ ), "inputs must be on CUDA device"
635
+ assert num_head % num_head_kv == 0, "num_head must be divisible by num_head_kv"
636
+ assert head_dim <= 256, "head_dim must be less than or equal to 256"
637
+ alignment = 16 // q.element_size()
638
+ assert head_dim % alignment == 0, f"head_dim must be divisible by {alignment}"
639
+ assert head_dim_v % alignment == 0, f"head_dim_v must be divisible by {alignment}"
640
+ if softmax_scale is None:
641
+ softmax_scale = 1.0 / math.sqrt(head_dim)
642
+ qhead_per_kvhead = num_head // num_head_kv
643
+ if pack_gqa is None:
644
+ pack_gqa = qhead_per_kvhead > 1
645
+ if compute_capability == 10:
646
+ pack_gqa = False # override for now
647
+
648
+ device = q.device
649
+ # TODO: check if this is the right rounding
650
+ dq = torch.empty_like(q)
651
+ dk = torch.empty_like(k)
652
+ dv = torch.empty_like(v)
653
+
654
+ head_dim_rounded = (head_dim + 32 - 1) // 32 * 32
655
+
656
+ if cu_seqlens_q is None:
657
+ seqlen_q_rounded = (seqlen_q + m_block_size - 1) // m_block_size * m_block_size
658
+ dq_accum = torch.empty(
659
+ batch_size,
660
+ num_head,
661
+ seqlen_q_rounded * head_dim_rounded,
662
+ dtype=torch.float32,
663
+ device=device,
664
+ )
665
+ dpsum = torch.empty(
666
+ batch_size, num_head, seqlen_q_rounded, dtype=torch.float32, device=device
667
+ )
668
+ lse_log2 = torch.empty(
669
+ batch_size, num_head, seqlen_q_rounded, dtype=torch.float32, device=device
670
+ )
671
+ else:
672
+ total_q_rounded_padded = (
673
+ (total_q + cu_seqlens_q.shape[0] * m_block_size - 1) // m_block_size * m_block_size
674
+ )
675
+ dq_accum = torch.empty(
676
+ num_head, total_q_rounded_padded * head_dim_rounded, dtype=torch.float32, device=device
677
+ )
678
+ dpsum = torch.empty(num_head, total_q_rounded_padded, dtype=torch.float32, device=device)
679
+ lse_log2 = torch.empty(num_head, total_q_rounded_padded, dtype=torch.float32, device=device)
680
+
681
+ if qhead_per_kvhead > 1:
682
+ head_dim_v_rounded = (head_dim_v + 32 - 1) // 32 * 32
683
+ if cu_seqlens_k is None:
684
+ seqlen_k_rounded = (seqlen_k + n_block_size - 1) // n_block_size * n_block_size
685
+ num_n_blocks = seqlen_k_rounded // n_block_size
686
+ if cluster_size == 2 and num_n_blocks % cluster_size != 0:
687
+ seqlen_k_rounded = seqlen_k_rounded + n_block_size
688
+ dk_accum = torch.zeros(
689
+ batch_size,
690
+ num_head_kv,
691
+ seqlen_k_rounded * head_dim_rounded,
692
+ dtype=torch.float32,
693
+ device=device,
694
+ )
695
+ dv_accum = torch.zeros(
696
+ batch_size,
697
+ num_head_kv,
698
+ seqlen_k_rounded * head_dim_v_rounded,
699
+ dtype=torch.float32,
700
+ device=device,
701
+ )
702
+ else:
703
+ total_k_rounded_padded = (
704
+ (total_k + cu_seqlens_k.shape[0] * n_block_size - 1) // n_block_size * n_block_size
705
+ )
706
+ num_n_blocks = total_k_rounded_padded // n_block_size
707
+ if cluster_size == 2 and num_n_blocks % cluster_size != 0:
708
+ total_k_rounded_padded = total_k_rounded_padded + n_block_size
709
+ dk_accum = torch.zeros(
710
+ num_head_kv,
711
+ total_k_rounded_padded * head_dim_rounded,
712
+ dtype=torch.float32,
713
+ device=device,
714
+ )
715
+ dv_accum = torch.zeros(
716
+ num_head_kv,
717
+ total_k_rounded_padded * head_dim_v_rounded,
718
+ dtype=torch.float32,
719
+ device=device,
720
+ )
721
+
722
+ dtype = torch2cute_dtype_map[q.dtype]
723
+ q_tensor, k_tensor, v_tensor, o_tensor, do_tensor, dq_tensor, dk_tensor, dv_tensor = [
724
+ from_dlpack(t.detach(), assumed_align=16).mark_layout_dynamic(leading_dim=t.ndim - 1)
725
+ for t in (q, k, v, out, dout, dq, dk, dv)
726
+ ]
727
+ lse_tensor = from_dlpack(lse.detach(), assumed_align=4).mark_layout_dynamic(
728
+ leading_dim=lse.ndim - 1
729
+ )
730
+ dq_accum_tensor, dpsum_tensor, lse_log2_tensor = [
731
+ from_dlpack(t.detach(), assumed_align=16).mark_layout_dynamic(leading_dim=t.ndim - 1)
732
+ for t in (dq_accum, dpsum, lse_log2)
733
+ ]
734
+ if qhead_per_kvhead > 1:
735
+ dk_accum_tensor, dv_accum_tensor = [
736
+ from_dlpack(t.detach(), assumed_align=16).mark_layout_dynamic(leading_dim=t.ndim - 1)
737
+ for t in (dk_accum, dv_accum)
738
+ ]
739
+ cu_seqlens_q_tensor, cu_seqlens_k_tensor, seqused_q_tensor, seqused_k_tensor = [
740
+ from_dlpack(t.detach(), assumed_align=4).mark_layout_dynamic(leading_dim=t.ndim - 1)
741
+ if t is not None
742
+ else None
743
+ for t in (cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k)
744
+ ]
745
+ current_stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
746
+
747
+ # Preprocess kernel: compute (o * dout).sum(dim=-1), lse * log2_e, and zero out dq_accum.
748
+ compile_key_pre = (compute_capability, dtype, head_dim_v, m_block_size, num_threads)
749
+ if compile_key_pre not in _flash_attn_bwd.compile_cache_pre:
750
+ fa_bwd_pre = FlashAttentionBackwardPreprocess(
751
+ dtype,
752
+ head_dim_v,
753
+ m_block_size,
754
+ num_threads=num_threads,
755
+ )
756
+ # TODO: check @can_implement
757
+ _flash_attn_bwd.compile_cache_pre[compile_key_pre] = cute.compile(
758
+ fa_bwd_pre,
759
+ o_tensor,
760
+ do_tensor,
761
+ dpsum_tensor,
762
+ lse_tensor,
763
+ lse_log2_tensor,
764
+ dq_accum_tensor,
765
+ cu_seqlens_q_tensor,
766
+ seqused_q_tensor,
767
+ current_stream,
768
+ )
769
+ _flash_attn_bwd.compile_cache_pre[compile_key_pre](
770
+ o_tensor,
771
+ do_tensor,
772
+ dpsum_tensor,
773
+ lse_tensor,
774
+ lse_log2_tensor,
775
+ dq_accum_tensor,
776
+ cu_seqlens_q_tensor,
777
+ seqused_q_tensor,
778
+ current_stream,
779
+ )
780
+
781
+ # Backward kernel: compute dk, dv, dq_accum.
782
+ if compute_capability == 9:
783
+ compile_key = (
784
+ compute_capability,
785
+ dtype,
786
+ head_dim,
787
+ head_dim_v,
788
+ qhead_per_kvhead,
789
+ causal,
790
+ softcap != 0.0,
791
+ m_block_size,
792
+ n_block_size,
793
+ num_threads,
794
+ pack_gqa,
795
+ num_stages_Q,
796
+ num_stages_dO,
797
+ SdP_swapAB,
798
+ dKV_swapAB,
799
+ dQ_swapAB,
800
+ AtomLayoutMSdP,
801
+ AtomLayoutNdKV,
802
+ AtomLayoutMdQ,
803
+ V_in_regs,
804
+ )
805
+ else:
806
+ compile_key = (
807
+ compute_capability,
808
+ dtype,
809
+ head_dim,
810
+ head_dim_v,
811
+ qhead_per_kvhead,
812
+ causal,
813
+ softcap != 0.0,
814
+ m_block_size,
815
+ n_block_size,
816
+ num_threads,
817
+ pack_gqa,
818
+ cluster_size,
819
+ )
820
+ num_threads = 384
821
+ if compile_key not in _flash_attn_bwd.compile_cache:
822
+ fa_bwd_sm80 = FlashAttentionBackwardSm80(
823
+ dtype,
824
+ head_dim,
825
+ head_dim_v,
826
+ qhead_per_kvhead,
827
+ m_block_size,
828
+ n_block_size,
829
+ num_stages_Q,
830
+ num_stages_dO,
831
+ num_threads,
832
+ pack_gqa,
833
+ causal,
834
+ SdP_swapAB,
835
+ dKV_swapAB,
836
+ dQ_swapAB,
837
+ AtomLayoutMSdP,
838
+ AtomLayoutNdKV,
839
+ AtomLayoutMdQ,
840
+ V_in_regs=V_in_regs,
841
+ )
842
+ if compute_capability == 9:
843
+ fa_bwd_obj = FlashAttentionBackwardSm90(
844
+ dtype,
845
+ head_dim,
846
+ head_dim_v,
847
+ qhead_per_kvhead,
848
+ causal,
849
+ m_block_size,
850
+ n_block_size,
851
+ num_stages_Q,
852
+ num_stages_dO,
853
+ num_stages_PdS,
854
+ SdP_swapAB,
855
+ dKV_swapAB,
856
+ dQ_swapAB,
857
+ AtomLayoutMSdP,
858
+ AtomLayoutNdKV,
859
+ AtomLayoutMdQ,
860
+ num_threads,
861
+ V_in_regs=V_in_regs,
862
+ )
863
+ else:
864
+ fa_bwd_obj = FlashAttentionBackwardSm100(
865
+ head_dim,
866
+ head_dim_v,
867
+ is_causal=causal,
868
+ qhead_per_kvhead=qhead_per_kvhead,
869
+ # tile_m=m_block_size,
870
+ # tile_n=n_block_size,
871
+ cluster_size=cluster_size,
872
+ # cluster_size=1,
873
+ )
874
+ # TODO: check @can_implement
875
+ _flash_attn_bwd.compile_cache[compile_key] = cute.compile(
876
+ fa_bwd_obj,
877
+ q_tensor,
878
+ k_tensor,
879
+ v_tensor,
880
+ do_tensor,
881
+ lse_log2_tensor,
882
+ dpsum_tensor,
883
+ dq_accum_tensor,
884
+ dk_tensor if qhead_per_kvhead == 1 else dk_accum_tensor,
885
+ dv_tensor if qhead_per_kvhead == 1 else dv_accum_tensor,
886
+ softmax_scale,
887
+ current_stream,
888
+ cu_seqlens_q_tensor,
889
+ cu_seqlens_k_tensor,
890
+ seqused_q_tensor,
891
+ seqused_k_tensor,
892
+ )
893
+ _flash_attn_bwd.compile_cache[compile_key](
894
+ q_tensor,
895
+ k_tensor,
896
+ v_tensor,
897
+ do_tensor,
898
+ lse_log2_tensor,
899
+ dpsum_tensor,
900
+ dq_accum_tensor,
901
+ dk_tensor if qhead_per_kvhead == 1 else dk_accum_tensor,
902
+ dv_tensor if qhead_per_kvhead == 1 else dv_accum_tensor,
903
+ softmax_scale,
904
+ current_stream,
905
+ cu_seqlens_q_tensor,
906
+ cu_seqlens_k_tensor,
907
+ seqused_q_tensor,
908
+ seqused_k_tensor,
909
+ )
910
+
911
+ num_threads = 256 if compute_capability == 9 else 128
912
+ # Postprocess kernel: convert dq_accum from float32 to dq in bf16/fp16
913
+ compile_key_post = (dtype, head_dim, m_block_size, num_threads, AtomLayoutMdQ, dQ_swapAB)
914
+ if compile_key_post not in _flash_attn_bwd.compile_cache_post:
915
+ arch = compute_capability * 10
916
+ fa_bwd_post = FlashAttentionBackwardPostprocess(
917
+ dtype, head_dim, arch, m_block_size, num_threads, AtomLayoutMdQ, dQ_swapAB
918
+ )
919
+ # TODO: check @can_implement
920
+ _flash_attn_bwd.compile_cache_post[compile_key_post] = cute.compile(
921
+ fa_bwd_post,
922
+ dq_accum_tensor,
923
+ dq_tensor,
924
+ softmax_scale,
925
+ cu_seqlens_q_tensor,
926
+ seqused_q_tensor,
927
+ current_stream,
928
+ )
929
+ _flash_attn_bwd.compile_cache_post[compile_key_post](
930
+ dq_accum_tensor,
931
+ dq_tensor,
932
+ softmax_scale,
933
+ cu_seqlens_q_tensor,
934
+ seqused_q_tensor,
935
+ current_stream,
936
+ )
937
+
938
+ if qhead_per_kvhead > 1:
939
+ # Postprocess kernel: convert dk_accum & dv_accum from float32 to bf16/fp16
940
+ compile_key_post = (dtype, head_dim, n_block_size, num_threads, AtomLayoutNdKV, dKV_swapAB)
941
+ if compile_key_post not in _flash_attn_bwd.compile_cache_post:
942
+ fa_bwd_post = FlashAttentionBackwardPostprocess(
943
+ dtype, head_dim, n_block_size, num_threads, AtomLayoutNdKV, dKV_swapAB
944
+ )
945
+ # TODO: check @can_implement
946
+ _flash_attn_bwd.compile_cache_post[compile_key_post] = cute.compile(
947
+ fa_bwd_post,
948
+ dk_accum_tensor,
949
+ dk_tensor,
950
+ softmax_scale,
951
+ cu_seqlens_k_tensor,
952
+ seqused_k_tensor,
953
+ current_stream,
954
+ )
955
+ _flash_attn_bwd.compile_cache_post[compile_key_post](
956
+ dk_accum_tensor,
957
+ dk_tensor,
958
+ softmax_scale,
959
+ cu_seqlens_k_tensor,
960
+ seqused_k_tensor,
961
+ current_stream,
962
+ )
963
+ compile_key_post = (
964
+ dtype,
965
+ head_dim_v,
966
+ n_block_size,
967
+ num_threads,
968
+ AtomLayoutNdKV,
969
+ dKV_swapAB,
970
+ )
971
+ if compile_key_post not in _flash_attn_bwd.compile_cache_post:
972
+ fa_bwd_post = FlashAttentionBackwardPostprocess(
973
+ dtype, head_dim_v, n_block_size, num_threads, AtomLayoutNdKV, dKV_swapAB
974
+ )
975
+ # TODO: check @can_implement
976
+ _flash_attn_bwd.compile_cache_post[compile_key_post] = cute.compile(
977
+ fa_bwd_post,
978
+ dv_accum_tensor,
979
+ dv_tensor,
980
+ cutlass.Float32(1.0),
981
+ cu_seqlens_k_tensor,
982
+ seqused_k_tensor,
983
+ current_stream,
984
+ )
985
+ _flash_attn_bwd.compile_cache_post[compile_key_post](
986
+ dv_accum_tensor,
987
+ dv_tensor,
988
+ cutlass.Float32(1.0),
989
+ cu_seqlens_k_tensor,
990
+ seqused_k_tensor,
991
+ current_stream,
992
+ )
993
+
994
+ return dq, dk, dv
995
+
996
+
997
+ _flash_attn_bwd.compile_cache_pre = {}
998
+ _flash_attn_bwd.compile_cache = {}
999
+ _flash_attn_bwd.compile_cache_post = {}
1000
+
1001
+
1002
+ class FlashAttnFunc(torch.autograd.Function):
1003
+ @staticmethod
1004
+ def forward(
1005
+ ctx,
1006
+ q: torch.Tensor,
1007
+ k: torch.Tensor,
1008
+ v: torch.Tensor,
1009
+ softmax_scale: Optional[float] = None,
1010
+ causal: bool = False,
1011
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1012
+ learnable_sink: Optional[torch.Tensor] = None,
1013
+ softcap: float = 0.0,
1014
+ num_splits: int = 1,
1015
+ pack_gqa: Optional[bool] = None,
1016
+ mask_mod: Optional[Callable] = None,
1017
+ full_block_cnt: Optional[torch.Tensor] = None,
1018
+ full_block_idx: Optional[torch.Tensor] = None,
1019
+ mask_block_cnt: Optional[torch.Tensor] = None,
1020
+ mask_block_idx: Optional[torch.Tensor] = None,
1021
+ ):
1022
+ # Only create block sparse tensors if at least one block sparse parameter is provided
1023
+ block_sparse_tensors = None
1024
+ if any(t is not None for t in [full_block_cnt, full_block_idx, mask_block_cnt, mask_block_idx]):
1025
+ block_sparse_tensors = BlockSparseTensorsTorch(
1026
+ full_block_cnt=full_block_cnt,
1027
+ full_block_idx=full_block_idx,
1028
+ mask_block_cnt=mask_block_cnt,
1029
+ mask_block_idx=mask_block_idx,
1030
+ )
1031
+ out, lse = _flash_attn_fwd(
1032
+ q,
1033
+ k,
1034
+ v,
1035
+ softmax_scale=softmax_scale,
1036
+ causal=causal,
1037
+ window_size_left=window_size[0],
1038
+ window_size_right=window_size[1],
1039
+ learnable_sink=learnable_sink,
1040
+ softcap=softcap,
1041
+ num_splits=num_splits,
1042
+ pack_gqa=pack_gqa,
1043
+ mask_mod=mask_mod,
1044
+ block_sparse_tensors=block_sparse_tensors
1045
+ )
1046
+ ctx.save_for_backward(q, k, v, out, lse)
1047
+ ctx.softmax_scale = softmax_scale
1048
+ ctx.causal = causal
1049
+ ctx.window_size = window_size
1050
+ ctx.softcap = softcap
1051
+ return out, lse
1052
+
1053
+ @staticmethod
1054
+ def backward(ctx, dout, *args):
1055
+ q, k, v, out, lse = ctx.saved_tensors
1056
+ dq, dk, dv = _flash_attn_bwd(
1057
+ q,
1058
+ k,
1059
+ v,
1060
+ out,
1061
+ dout,
1062
+ lse,
1063
+ ctx.softmax_scale,
1064
+ ctx.causal,
1065
+ ctx.softcap,
1066
+ )
1067
+ return dq, dk, dv, *((None,) * 20) # Extra Nones is fine
1068
+
1069
+
1070
+ class FlashAttnVarlenFunc(torch.autograd.Function):
1071
+ @staticmethod
1072
+ def forward(
1073
+ ctx,
1074
+ q: torch.Tensor,
1075
+ k: torch.Tensor,
1076
+ v: torch.Tensor,
1077
+ cu_seqlens_q: Optional[torch.Tensor],
1078
+ cu_seqlens_k: Optional[torch.Tensor],
1079
+ seqused_q: Optional[torch.Tensor] = None,
1080
+ seqused_k: Optional[torch.Tensor] = None,
1081
+ page_table: Optional[torch.Tensor] = None,
1082
+ softmax_scale: Optional[float] = None,
1083
+ causal: bool = False,
1084
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1085
+ learnable_sink: Optional[torch.Tensor] = None,
1086
+ softcap: float = 0.0,
1087
+ num_splits: int = 1,
1088
+ pack_gqa: Optional[bool] = None,
1089
+ ):
1090
+ out, lse = _flash_attn_fwd(
1091
+ q,
1092
+ k,
1093
+ v,
1094
+ cu_seqlens_q,
1095
+ cu_seqlens_k,
1096
+ seqused_q,
1097
+ seqused_k,
1098
+ page_table=page_table,
1099
+ softmax_scale=softmax_scale,
1100
+ causal=causal,
1101
+ window_size_left=window_size[0],
1102
+ window_size_right=window_size[1],
1103
+ learnable_sink=learnable_sink,
1104
+ softcap=softcap,
1105
+ num_splits=num_splits,
1106
+ pack_gqa=pack_gqa,
1107
+ )
1108
+ ctx.save_for_backward(q, k, v, out, lse, cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k)
1109
+ ctx.softmax_scale = softmax_scale
1110
+ ctx.causal = causal
1111
+ ctx.window_size = window_size
1112
+ ctx.softcap = softcap
1113
+ return out, lse
1114
+
1115
+ @staticmethod
1116
+ def backward(ctx, dout, *args):
1117
+ q, k, v, out, lse, cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k = ctx.saved_tensors
1118
+ assert seqused_q == seqused_k == None
1119
+ assert ctx.softcap == 0.0
1120
+ dq, dk, dv = _flash_attn_bwd(
1121
+ q,
1122
+ k,
1123
+ v,
1124
+ out,
1125
+ dout,
1126
+ lse,
1127
+ ctx.softmax_scale,
1128
+ ctx.causal,
1129
+ ctx.softcap,
1130
+ cu_seqlens_q=cu_seqlens_q,
1131
+ cu_seqlens_k=cu_seqlens_k,
1132
+ seqused_q=seqused_q,
1133
+ seqused_k=seqused_k,
1134
+ )
1135
+
1136
+ return dq, dk, dv, *((None,) * 20)
1137
+
1138
+
1139
+ def flash_attn_func(
1140
+ q: torch.Tensor,
1141
+ k: torch.Tensor,
1142
+ v: torch.Tensor,
1143
+ softmax_scale: Optional[float] = None,
1144
+ causal: bool = False,
1145
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1146
+ learnable_sink: Optional[torch.Tensor] = None,
1147
+ softcap: float = 0.0,
1148
+ num_splits: int = 1,
1149
+ pack_gqa: Optional[bool] = None,
1150
+ mask_mod: Optional[Callable] = None,
1151
+ full_block_cnt: Optional[torch.Tensor] = None,
1152
+ full_block_idx: Optional[torch.Tensor] = None,
1153
+ mask_block_cnt: Optional[torch.Tensor] = None,
1154
+ mask_block_idx: Optional[torch.Tensor] = None,
1155
+ ):
1156
+ return FlashAttnFunc.apply(
1157
+ q,
1158
+ k,
1159
+ v,
1160
+ softmax_scale,
1161
+ causal,
1162
+ window_size,
1163
+ learnable_sink,
1164
+ softcap,
1165
+ num_splits,
1166
+ pack_gqa,
1167
+ mask_mod,
1168
+ full_block_cnt,
1169
+ full_block_idx,
1170
+ mask_block_cnt,
1171
+ mask_block_idx,
1172
+ )
1173
+
1174
+
1175
+ def flash_attn_varlen_func(
1176
+ q: torch.Tensor,
1177
+ k: torch.Tensor,
1178
+ v: torch.Tensor,
1179
+ cu_seqlens_q: Optional[torch.Tensor] = None,
1180
+ cu_seqlens_k: Optional[torch.Tensor] = None,
1181
+ seqused_q: Optional[torch.Tensor] = None,
1182
+ seqused_k: Optional[torch.Tensor] = None,
1183
+ page_table: Optional[torch.Tensor] = None,
1184
+ softmax_scale: Optional[float] = None,
1185
+ causal: bool = False,
1186
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1187
+ learnable_sink: Optional[torch.Tensor] = None,
1188
+ softcap: float = 0.0,
1189
+ num_splits: int = 1,
1190
+ pack_gqa: Optional[bool] = None,
1191
+ ):
1192
+ return FlashAttnVarlenFunc.apply(
1193
+ q,
1194
+ k,
1195
+ v,
1196
+ cu_seqlens_q,
1197
+ cu_seqlens_k,
1198
+ seqused_q,
1199
+ seqused_k,
1200
+ page_table,
1201
+ softmax_scale,
1202
+ causal,
1203
+ window_size,
1204
+ learnable_sink,
1205
+ softcap,
1206
+ num_splits,
1207
+ pack_gqa,
1208
+ )
1209
+
1210
+
1211
+ def _flash_attn_fwd_combine(
1212
+ out_partial: torch.Tensor,
1213
+ lse_partial: torch.Tensor,
1214
+ out: torch.Tensor,
1215
+ lse: Optional[torch.Tensor] = None,
1216
+ cu_seqlens: Optional[torch.Tensor] = None,
1217
+ seqused: Optional[torch.Tensor] = None,
1218
+ num_splits_dynamic_ptr: Optional[torch.Tensor] = None,
1219
+ semaphore_to_reset: Optional[torch.Tensor] = None,
1220
+ ) -> None:
1221
+ """Forward combine kernel for split attention computation.
1222
+
1223
+ Combines partial outputs and log-sum-exp values from multiple splits
1224
+ of attention computation into final outputs.
1225
+
1226
+ Args:
1227
+ out_partial: Partial outputs tensor (num_splits, batch, seqlen, nheads, headdim) or
1228
+ (num_splits, total_q, nheads, headdim) if there's cu_seqlens
1229
+ lse_partial: Partial LSE tensor (num_splits, batch, seqlen, nheads) or
1230
+ (num_splits, total_q, nheads) if there's cu_seqlens
1231
+ out: Output tensor (batch, seqlen, nheads, headdim) or (total_q, nheads, headdim) if there's cu_seqlens
1232
+ lse: Output LSE tensor (batch, seqlen, nheads) or (total_q, nheads) if there's cu_seqlens.
1233
+ cu_seqlens: Cumulative sequence lengths for variable length sequences
1234
+ seqused: Used sequence lengths for each batch
1235
+ num_splits_dynamic_ptr: Dynamic number of splits per batch
1236
+ semaphore_to_reset: Semaphore for synchronization
1237
+ k_block_size: Block size for head dimension
1238
+
1239
+ Returns:
1240
+ None
1241
+ """
1242
+ # Input validation
1243
+ assert out_partial.dim() in [4, 5], "out_partial must have 4 or 5 dimensions"
1244
+ assert lse_partial.dim() in [3, 4], "lse_partial must have 3 or 4 dimensions"
1245
+ assert out_partial.dtype in [torch.float16, torch.bfloat16, torch.float32], (
1246
+ "out_partial must be fp16, bf16, or fp32"
1247
+ )
1248
+ assert lse_partial.dtype == torch.float32, "lse_partial must be fp32"
1249
+ assert out_partial.is_cuda and lse_partial.is_cuda, "tensors must be on CUDA device"
1250
+ assert out_partial.stride(-1) == 1, "out_partial must be contiguous in the last dimension"
1251
+ assert lse_partial.stride(-2) == 1, "lse_partial must be contiguous in the seqlen dimension"
1252
+ assert lse_partial.shape == out_partial.shape[:-1]
1253
+
1254
+ # Determine if this is variable length based on dimensions
1255
+ is_varlen = out_partial.dim() == 4
1256
+
1257
+ # Validate output tensor shapes and types
1258
+ assert out.shape == out_partial.shape[1:], "out shape mismatch"
1259
+ if lse is not None:
1260
+ assert lse.shape == lse_partial.shape[1:], "lse shape mismatch"
1261
+ assert lse.dtype == torch.float32, "lse must be fp32"
1262
+
1263
+ # Validate optional tensors
1264
+ for t, name in [
1265
+ (cu_seqlens, "cu_seqlens"),
1266
+ (seqused, "seqused"),
1267
+ (num_splits_dynamic_ptr, "num_splits_dynamic_ptr"),
1268
+ ]:
1269
+ if t is not None:
1270
+ assert t.dtype == torch.int32, f"{name} must be int32"
1271
+ assert t.is_cuda, f"{name} must be on CUDA device"
1272
+ assert t.is_contiguous(), f"{name} must be contiguous"
1273
+
1274
+ head_dim = out_partial.shape[-1]
1275
+ num_splits = out_partial.shape[0]
1276
+ assert num_splits <= 256
1277
+ # If hdim is 96 or 192, it's faster to round them to 128 or 256 respectively
1278
+ # so that kBlockM is smaller and we have more parallelism.
1279
+ k_block_size = 64 if head_dim <= 64 else 128
1280
+ # We want kBlockM to be as small as possible to maximize parallelism.
1281
+ # E.g., if hdim is 64, we want kBlockM to be 16 so that we can use 256 threads, each reading 4 elements (floats).
1282
+ m_block_size = 8 if k_block_size % 128 == 0 else (16 if k_block_size % 64 == 0 else 32)
1283
+ log_max_splits = max(math.ceil(math.log2(num_splits)), 4)
1284
+ if m_block_size == 8:
1285
+ # If kBlockM == 8 then the minimum number of splits is 32.
1286
+ # TODO: we can deal w this by using 128 threads instead
1287
+ log_max_splits = max(log_max_splits, 5)
1288
+
1289
+ # Convert to cute tensors (using kernel-formatted tensors)
1290
+ out_partial_tensor = from_dlpack(out_partial.detach(), assumed_align=16).mark_layout_dynamic(
1291
+ leading_dim=4 if not is_varlen else 3
1292
+ )
1293
+ lse_partial_tensor = from_dlpack(lse_partial.detach(), assumed_align=4).mark_layout_dynamic(
1294
+ leading_dim=lse_partial.ndim - 2
1295
+ )
1296
+ out_tensor = from_dlpack(out.detach(), assumed_align=16).mark_layout_dynamic(leading_dim=3 if not is_varlen else 2)
1297
+ lse_tensor = (
1298
+ from_dlpack(lse.detach(), assumed_align=4).mark_layout_dynamic(leading_dim=lse.ndim - 2)
1299
+ if lse is not None
1300
+ else None
1301
+ )
1302
+
1303
+ optional_tensors = [
1304
+ from_dlpack(t.detach(), assumed_align=4).mark_layout_dynamic(leading_dim=0)
1305
+ if t is not None
1306
+ else None
1307
+ for t in (cu_seqlens, seqused, num_splits_dynamic_ptr, semaphore_to_reset)
1308
+ ]
1309
+ cu_seqlens_tensor, seqused_tensor, num_splits_dynamic_tensor, semaphore_tensor = (
1310
+ optional_tensors
1311
+ )
1312
+
1313
+ current_stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
1314
+
1315
+ # Create combine kernel configuration
1316
+ dtype = torch2cute_dtype_map[out.dtype]
1317
+ dtype_partial = torch2cute_dtype_map[out_partial.dtype]
1318
+
1319
+ compile_key = (
1320
+ dtype,
1321
+ dtype_partial,
1322
+ head_dim,
1323
+ m_block_size,
1324
+ k_block_size,
1325
+ log_max_splits,
1326
+ cu_seqlens is not None,
1327
+ seqused is not None,
1328
+ lse is not None,
1329
+ )
1330
+
1331
+ if compile_key not in _flash_attn_fwd_combine.compile_cache:
1332
+ fa_combine = FlashAttentionForwardCombine(
1333
+ dtype=dtype,
1334
+ dtype_partial=dtype_partial,
1335
+ head_dim=head_dim,
1336
+ m_block_size=m_block_size,
1337
+ k_block_size=k_block_size,
1338
+ log_max_splits=log_max_splits,
1339
+ )
1340
+
1341
+ # Check if implementation is supported
1342
+ if not fa_combine.can_implement(
1343
+ dtype,
1344
+ dtype_partial,
1345
+ head_dim,
1346
+ m_block_size,
1347
+ k_block_size,
1348
+ log_max_splits,
1349
+ num_threads=256,
1350
+ ):
1351
+ raise RuntimeError(
1352
+ "FlashAttention combine kernel cannot be implemented with given parameters"
1353
+ )
1354
+
1355
+ _flash_attn_fwd_combine.compile_cache[compile_key] = cute.compile(
1356
+ fa_combine,
1357
+ out_partial_tensor,
1358
+ lse_partial_tensor,
1359
+ out_tensor,
1360
+ lse_tensor,
1361
+ cu_seqlens_tensor,
1362
+ seqused_tensor,
1363
+ num_splits_dynamic_tensor,
1364
+ semaphore_tensor,
1365
+ current_stream,
1366
+ )
1367
+
1368
+ _flash_attn_fwd_combine.compile_cache[compile_key](
1369
+ out_partial_tensor,
1370
+ lse_partial_tensor,
1371
+ out_tensor,
1372
+ lse_tensor,
1373
+ cu_seqlens_tensor,
1374
+ seqused_tensor,
1375
+ num_splits_dynamic_tensor,
1376
+ semaphore_tensor,
1377
+ current_stream,
1378
+ )
1379
+
1380
+
1381
+ _flash_attn_fwd_combine.compile_cache = {}
1382
+
1383
+
1384
+ def flash_attn_combine(
1385
+ out_partial: torch.Tensor,
1386
+ lse_partial: torch.Tensor,
1387
+ out: Optional[torch.Tensor] = None,
1388
+ out_dtype: Optional[torch.dtype] = None,
1389
+ cu_seqlens: Optional[torch.Tensor] = None,
1390
+ seqused: Optional[torch.Tensor] = None,
1391
+ return_lse: bool = True,
1392
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
1393
+ """Flash Attention combine function for split attention computation.
1394
+
1395
+ Combines partial outputs and log-sum-exp values from multiple splits
1396
+ of attention computation into final outputs. This is the main user-facing
1397
+ interface for the combine kernel.
1398
+
1399
+ Args:
1400
+ out_partial: Partial outputs tensor with shape:
1401
+ - (num_splits, batch_size, seqlen, num_heads, head_size) for regular batched input
1402
+ - (num_splits, total_q, num_heads, head_size) for variable length input
1403
+ lse_partial: Partial LSE tensor with shape:
1404
+ - (num_splits, batch_size, seqlen, num_heads) for regular batched input
1405
+ - (num_splits, total_q, num_heads) for variable length input
1406
+ out: Optional output tensor. If None, will be created automatically.
1407
+ out_dtype: Optional output dtype. If None, will use fp16/bf16 based on input.
1408
+ cu_seqlens: Cumulative sequence lengths for variable length sequences
1409
+ seqused: Used sequence lengths for each batch
1410
+ return_lse: Whether to return the combined LSE tensor. Default is True.
1411
+
1412
+ Returns:
1413
+ Tuple of (out, lse) where:
1414
+ - out: Combined output tensor with shape (batch_size, seqlen, num_heads, head_size)
1415
+ or (total_q, num_heads, head_size) for varlen
1416
+ - lse: Combined log-sum-exp tensor with shape (batch_size, seqlen, num_heads)
1417
+ or (total_q, num_heads) for varlen. None if return_lse=False
1418
+
1419
+ Note:
1420
+ This function expects the input tensors to be in the format produced by
1421
+ split attention computation, where the first dimension is num_splits.
1422
+ The permuting from user format to kernel format is now done inside the kernel.
1423
+ """
1424
+ # Input validation
1425
+ assert out_partial.dim() in [4, 5], "out_partial must have 4 or 5 dimensions"
1426
+ assert lse_partial.dim() in [3, 4], "lse_partial must have 3 or 4 dimensions"
1427
+ assert out_partial.dtype == torch.float32, "out_partial must be fp32 (from accumulation)"
1428
+ assert lse_partial.dtype == torch.float32, "lse_partial must be fp32"
1429
+
1430
+ # Determine if this is variable length based on dimensions
1431
+ is_varlen = out_partial.dim() == 4
1432
+
1433
+ if is_varlen:
1434
+ # Variable length: (num_splits, total_q, num_heads, head_size)
1435
+ num_splits, total_q, num_heads, head_size = out_partial.shape
1436
+ assert lse_partial.shape == (num_splits, total_q, num_heads), (
1437
+ "lse_partial shape mismatch for varlen"
1438
+ )
1439
+ batch_size = 1 # Treat as single batch for varlen
1440
+ seqlen = total_q
1441
+ else:
1442
+ # Regular batched: (num_splits, batch_size, seqlen, num_heads, head_size)
1443
+ num_splits, batch_size, seqlen, num_heads, head_size = out_partial.shape
1444
+ assert lse_partial.shape == (num_splits, batch_size, seqlen, num_heads), (
1445
+ "lse_partial shape mismatch"
1446
+ )
1447
+
1448
+ # Determine output dtype
1449
+ if out_dtype is None:
1450
+ out_dtype = out_partial.dtype
1451
+
1452
+ # Create output if not provided
1453
+ device = out_partial.device
1454
+ if out is None:
1455
+ if is_varlen:
1456
+ out = torch.empty(total_q, num_heads, head_size, dtype=out_dtype, device=device)
1457
+ else:
1458
+ out = torch.empty(
1459
+ batch_size, seqlen, num_heads, head_size, dtype=out_dtype, device=device
1460
+ )
1461
+
1462
+ # Create lse output only if requested
1463
+ if return_lse:
1464
+ if is_varlen:
1465
+ lse = torch.empty(num_heads, total_q, dtype=torch.float32, device=device).transpose(
1466
+ 0, 1
1467
+ )
1468
+ else:
1469
+ lse = torch.empty(
1470
+ batch_size, num_heads, seqlen, dtype=torch.float32, device=device
1471
+ ).transpose(1, 2)
1472
+ else:
1473
+ lse = None
1474
+
1475
+ _flash_attn_fwd_combine(
1476
+ out_partial,
1477
+ lse_partial,
1478
+ out,
1479
+ lse,
1480
+ cu_seqlens,
1481
+ seqused,
1482
+ )
1483
+ return out, lse
build/torch-universal/mask.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Optional, Callable
4
+ from dataclasses import dataclass
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+ from cutlass import Float32, Int32, const_expr
9
+
10
+ from . import utils
11
+
12
+
13
+ @cute.jit
14
+ def mask_r2p(X: cute.Tensor, col_limit: Int32, arch: int = 90, rank1: bool = False) -> None:
15
+ # Bit manipulation, compiles down to the R2P instruction
16
+ # For sm100: we know that tScS_t2r[i][1] == i, for the particular tmem copy atom we're using.
17
+ # For sm90: instead of comparing limit to 0, 1, 8, 9, 16, 17, ...,
18
+ # we compare a transformed version of limit to 0, 1, 2, 3, 4, 5, ...
19
+ if const_expr(arch == 90):
20
+ col_limit_transformed = col_limit // 8 * 2 + min(col_limit % 8, 2)
21
+ else:
22
+ col_limit_transformed = col_limit
23
+ ncol = const_expr(cute.size(X.shape[cute.rank(X) - 1]) if not rank1 else cute.size(X.shape))
24
+ # Ideally we'd move by 32 instead of 24, but mask >> i isn't correct for i == 31
25
+ for s in cutlass.range_constexpr(cute.ceil_div(ncol, 24)):
26
+ # Don't need to clamp to 32 since the shr.u32 instruction does that already
27
+ col_limit_right_s = max(col_limit_transformed - s * 24, 0)
28
+ # 0 -> 0b00...00, 1 -> 0b00...01, ..., 31 -> 0b01...11, 32 -> 0b11...11
29
+ mask = (1 << col_limit_right_s) - 1
30
+ # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
31
+ for i in cutlass.range_constexpr(min(24, ncol - s * 24)):
32
+ in_bound = cutlass.Boolean(mask & (1 << i))
33
+ c = s * 24 + i
34
+ if const_expr(rank1):
35
+ X[c] = X[c] if in_bound else -Float32.inf
36
+ # This is the equivalent of:
37
+ # X[s * 24 + i] = X[s * 24 + i] if col_limit_right_s <= i else -Float32.inf
38
+ else:
39
+ for r in cutlass.range_constexpr(cute.size(X.shape[0])):
40
+ X[r, c] = X[r, c] if in_bound else -Float32.inf
41
+
42
+
43
+ @cute.jit
44
+ def mask_r2p_transposed(X: cute.Tensor, row_limit_top: Int32, num_rep: int) -> None:
45
+ # Bit manipulation, compiles down to the R2P instruction
46
+ # For sm100: we know that tScS_t2r[i][0] has the form 0, 1, ..., 31, 64, ..., 127
47
+ # or 0, 1, ..., 15, 32, ..., 47, 64, ...
48
+ # We compare a transformed version of limit to 0, 1, 2, 3, 4, 5, ...
49
+ # Here we hardcode for the case of 2 warp groups.
50
+ num_wg = 2
51
+ row_limit_top_transformed = row_limit_top // (num_rep * num_wg) * num_rep + min(
52
+ row_limit_top % (num_rep * num_wg), num_rep
53
+ )
54
+ ncol = cute.size(X.shape)
55
+ # Ideally we'd move by 32 instead of 24, but mask >> i isn't correct for i == 31
56
+ for s in cutlass.range_constexpr(cute.ceil_div(ncol, 24)):
57
+ row_limit_top_s = max(row_limit_top_transformed - s * 24, 0)
58
+ # 0 -> 0b00...00, 1 -> 0b00...01, ..., 31 -> 0b01...11, 32 -> 0b11...11
59
+ mask = (1 << row_limit_top_s) - 1
60
+ # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
61
+ for i in cutlass.range_constexpr(min(24, ncol - s * 24)):
62
+ out_bound = cutlass.Boolean(mask & (1 << i))
63
+ c = s * 24 + i
64
+ X[c] = -Float32.inf if out_bound else X[c]
65
+ # tidx = cute.arch.thread_idx()[0] % 256
66
+ # if tidx == 128:
67
+ # cute.printf("tidx = {}, s = {}, i = {}, row_limit_top = {}, row_limit_top_s = {}, mask = {}, out_bound = {}", tidx, s, i, row_limit_top, row_limit_top_s, mask, out_bound)
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class AttentionMask:
72
+ tile_m: cutlass.Constexpr[int]
73
+ tile_n: cutlass.Constexpr[int]
74
+ seqlen_q: Int32
75
+ seqlen_k: Int32
76
+ window_size_left: Optional[Int32] = None
77
+ window_size_right: Optional[Int32] = None
78
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1 # only pass in if we're doing PackGQA
79
+ swap_AB: cutlass.Constexpr[bool] = False
80
+
81
+ @cute.jit
82
+ def apply_mask(
83
+ self,
84
+ acc_S: cute.Tensor,
85
+ batch_idx: cutlass.Int32,
86
+ head_idx: cutlass.Int32,
87
+ m_block: cutlass.Int32,
88
+ n_block: cutlass.Int32,
89
+ thr_mma: cute.TiledMma,
90
+ mask_seqlen: cutlass.Constexpr[bool],
91
+ mask_causal: cutlass.Constexpr[bool],
92
+ mask_local: cutlass.Constexpr[bool] = False,
93
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
94
+ aux_tensors: Optional[list] = None,
95
+ ) -> None:
96
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
97
+ acc_S_mn = utils.make_acc_tensor_mn_view(acc_S, transpose=self.swap_AB)
98
+ acc_shape = (self.tile_m, self.tile_n)
99
+ cS = cute.make_identity_tensor(acc_shape if not self.swap_AB else acc_shape[::-1])
100
+ tScS_mn = utils.make_acc_tensor_mn_view(thr_mma.partition_C(cS), transpose=self.swap_AB)
101
+ # We use t0ScS as these indices are known at compile time. We then must subtract the
102
+ # column limit by the thread column offset.
103
+ t0ScS_mn = utils.make_acc_tensor_mn_view(
104
+ thr_mma.get_slice(0).partition_C(cS), transpose=self.swap_AB
105
+ )
106
+ ROW = 0 if const_expr(not self.swap_AB) else 1
107
+ COL = 1 if const_expr(not self.swap_AB) else 0
108
+ thr_col_offset = tScS_mn[0][COL]
109
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
110
+ if const_expr(not mask_causal and not mask_local and mask_mod is None):
111
+ if const_expr(mask_seqlen):
112
+ # The compiler now choses not to use R2P
113
+ r2p = const_expr(False and not self.swap_AB)
114
+ if const_expr(not r2p):
115
+ # traverse column index.
116
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
117
+ oob = t0ScS_mn[0, c][COL] >= seqlenk_col_limit
118
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
119
+ acc_S_mn[r, c] = -Float32.inf if oob else acc_S_mn[r, c]
120
+ else:
121
+ mask_r2p(acc_S_mn, seqlenk_col_limit, arch=90)
122
+
123
+ elif const_expr(
124
+ not mask_causal and not mask_local and mask_mod is not None
125
+ ): # FlexAttention mask mod
126
+ nrow = const_expr(cute.size(tScS_mn.shape[0]))
127
+ ncol = const_expr(cute.size(tScS_mn.shape[1]))
128
+ thr_col_offset = tScS_mn[0, 0][1]
129
+
130
+ for r in cutlass.range_constexpr(nrow):
131
+ global_row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
132
+
133
+ for col in cutlass.range_constexpr(ncol):
134
+ col_idx_local = t0ScS_mn[0, col][1]
135
+ # Convert to absolute column index
136
+ global_col_idx = thr_col_offset + col_idx_local + n_block * self.tile_n
137
+
138
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
139
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32)
140
+ q_idx_ssa = utils.scalar_to_ssa(
141
+ tScS_mn[r, 0][0] + m_block * self.tile_m, cutlass.Int32
142
+ )
143
+ kv_idx_ssa = utils.scalar_to_ssa(
144
+ thr_col_offset + t0ScS_mn[0, col][1] + n_block * self.tile_n,
145
+ cutlass.Int32,
146
+ )
147
+ mask_value = mask_mod(
148
+ batch_idx_ssa,
149
+ head_idx_ssa,
150
+ q_idx_ssa,
151
+ kv_idx_ssa,
152
+ aux_tensors,
153
+ )
154
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
155
+ if const_expr(mask_seqlen):
156
+ out_of_bounds = (global_row_idx >= self.seqlen_q) or (
157
+ global_col_idx >= self.seqlen_k
158
+ )
159
+ if out_of_bounds:
160
+ acc_S_mn[r, col] = -cutlass.Float32.inf
161
+ else:
162
+ acc_S_mn[r, col] = acc_S_mn[r, col] if cond else -cutlass.Float32.inf
163
+ else:
164
+ acc_S_mn[r, col] = acc_S_mn[r, col] if cond else -cutlass.Float32.inf
165
+
166
+ else: # Causal or local
167
+ if const_expr(not self.swap_AB):
168
+ # If PackGQA, we split the work of compute divmod among threads in the same row
169
+ threads_per_row = thr_mma.tv_layout_C.shape[0][0]
170
+ mma_m_idx = None
171
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
172
+ assert not self.swap_AB, "swap_AB with PackGQA not supported yet"
173
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, (
174
+ "threads_per_row must divide WARP_SIZE"
175
+ )
176
+ assert cute.size(acc_S_mn.shape[0]) <= threads_per_row
177
+ tidx = thr_mma.thr_idx
178
+ mma_m_idx = (
179
+ m_block * self.tile_m + tScS_mn[tidx % threads_per_row, 0][0]
180
+ ) // self.qhead_per_kvhead_packgqa
181
+ causal_row_offset = (
182
+ 1 + self.seqlen_k - n_block * self.tile_n - self.seqlen_q - thr_col_offset
183
+ )
184
+ if const_expr(mask_causal):
185
+ r2p = const_expr(not self.swap_AB) # R2P trick, see apply_mask_sm100
186
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
187
+ # get the column index limit based on current row. Only consider the row index, so the column index sets to 0.
188
+ if const_expr(self.qhead_per_kvhead_packgqa == 1):
189
+ row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
190
+ else:
191
+ row_idx = utils.shuffle_sync(
192
+ mma_m_idx, r % threads_per_row, width=threads_per_row
193
+ )
194
+ col_limit_right = row_idx + causal_row_offset
195
+ if const_expr(mask_seqlen):
196
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
197
+ if const_expr(not r2p):
198
+ # traverse column index.
199
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
200
+ acc_S_mn[r, c] = (
201
+ -Float32.inf
202
+ if t0ScS_mn[0, c][1] >= col_limit_right
203
+ else acc_S_mn[r, c]
204
+ )
205
+ else:
206
+ mask_r2p(acc_S_mn[r, None], col_limit_right, arch=90, rank1=True)
207
+ else: # Local
208
+ local_row_offset_right = (
209
+ causal_row_offset + self.window_size_right
210
+ if const_expr(self.window_size_right is not None)
211
+ else None
212
+ )
213
+ local_row_offset_left = (
214
+ causal_row_offset - 1 - self.window_size_left
215
+ if const_expr(self.window_size_left is not None)
216
+ else None
217
+ )
218
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
219
+ if const_expr(self.qhead_per_kvhead_packgqa == 1):
220
+ row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
221
+ else:
222
+ row_idx = utils.shuffle_sync(
223
+ mma_m_idx, r % threads_per_row, width=threads_per_row
224
+ )
225
+ if const_expr(self.window_size_right is not None):
226
+ col_limit_right = row_idx + local_row_offset_right
227
+ if const_expr(mask_seqlen):
228
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
229
+ else:
230
+ col_limit_right = self.tile_n
231
+ col_limit_left = (
232
+ row_idx + local_row_offset_left
233
+ if const_expr(self.window_size_left is not None)
234
+ else 0
235
+ )
236
+ # if cute.arch.thread_idx()[0] == 128: cute.printf("n_block = {}, r = {}, row_idx = {}, causal_row_offset = {}, col_limit_right = {}, col_limit_left = {}", n_block, r, row_idx, causal_row_offset, col_limit_right, col_limit_left)
237
+ # traverse column index.
238
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
239
+ col_idx = t0ScS_mn[0, c][1]
240
+ # only consider the column index, so the row index sets to 0.
241
+ if col_idx >= col_limit_right or col_idx < col_limit_left:
242
+ acc_S_mn[r, c] = -Float32.inf
243
+ else: # swap_AB
244
+ assert self.qhead_per_kvhead_packgqa == 1
245
+ thr_row_offset = tScS_mn[0][ROW]
246
+ causal_row_offset = (
247
+ seqlenk_col_limit - self.seqlen_q + m_block * self.tile_m + thr_row_offset
248
+ )
249
+ if const_expr(mask_causal):
250
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
251
+ col0 = t0ScS_mn[0, c][COL]
252
+ # If col0 is beyond the column limit, we want to mask out the entire
253
+ # column, by setting row limit to be self.tile_m.
254
+ row_limit_top = (
255
+ self.tile_m
256
+ if col0 >= seqlenk_col_limit and mask_seqlen
257
+ else col0 - causal_row_offset
258
+ )
259
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
260
+ acc_S_mn[r, c] = (
261
+ -Float32.inf
262
+ if t0ScS_mn[r, 0][ROW] < row_limit_top
263
+ else acc_S_mn[r, c]
264
+ )
265
+ else:
266
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
267
+ col0 = t0ScS_mn[0, c][COL]
268
+ # If col0 is beyond the column limit, we want to mask out the entire
269
+ # column, by setting row limit to be self.tile_m.
270
+ row_limit_top = (
271
+ self.tile_m
272
+ if col0 >= seqlenk_col_limit
273
+ else col0 - causal_row_offset - self.window_size_right
274
+ )
275
+ # TODO: do we need col_limit_sink?
276
+ row_limit_bot = col0 - causal_row_offset + self.window_size_left
277
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
278
+ row_idx = t0ScS_mn[r, 0][ROW]
279
+ acc_S_mn[r, c] = (
280
+ -Float32.inf
281
+ if row_idx < row_limit_top or row_idx > row_limit_bot
282
+ else acc_S_mn[r, c]
283
+ )
284
+
285
+ @cute.jit
286
+ def apply_mask_sm100(
287
+ self,
288
+ acc_S: cute.Tensor,
289
+ m_block: Int32,
290
+ n_block: Int32,
291
+ thr_mma: cute.TiledMma,
292
+ thr_tmem_load: cute.TiledCopy,
293
+ mask_seqlen: cutlass.Constexpr[bool],
294
+ mask_causal: cutlass.Constexpr[bool],
295
+ mask_local: cutlass.Constexpr[bool] = False,
296
+ ) -> None:
297
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
298
+ acc_shape = (self.tile_m, self.tile_n)
299
+ cS = cute.make_identity_tensor(acc_shape if not self.swap_AB else acc_shape[::-1])
300
+ tScS = thr_mma.partition_C(cS)
301
+ tScS_t2r = thr_tmem_load.partition_D(tScS)
302
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n
303
+ r2p = True
304
+ if const_expr(not mask_causal and not mask_local):
305
+ if const_expr(mask_seqlen):
306
+ if const_expr(not r2p):
307
+ for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
308
+ # if tScS_t2r[i][1] >= seqlenk_col_limit:
309
+ # acc_S[i] = -Float32.inf
310
+ # For some reason the 2 lines above generate really bad SASS
311
+ acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= seqlenk_col_limit else acc_S[i]
312
+ else:
313
+ mask_r2p(acc_S, seqlenk_col_limit, arch=100, rank1=True)
314
+ else: # Causal or local
315
+ causal_row_offset = 1 + self.seqlen_k - n_block * self.tile_n - self.seqlen_q
316
+ row_idx = tScS_t2r[0][0] + m_block * self.tile_m
317
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
318
+ row_idx = row_idx // self.qhead_per_kvhead_packgqa
319
+ if const_expr(mask_causal):
320
+ col_limit_right = row_idx + causal_row_offset
321
+ if const_expr(mask_seqlen):
322
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
323
+ # if cute.arch.thread_idx()[0] % 32 == 0:
324
+ # cute.printf("tidx = %d, tidx tmem = %d, row_idx = %d, col_limit_right = %d, causal_row_offset = %d\n", cute.arch.thread_idx()[0], thr_tmem_load.thr_idx, row_idx, col_limit_right, causal_row_offset)
325
+ ncol = const_expr(cute.size(tScS_t2r.shape))
326
+ if const_expr(not r2p):
327
+ for i in cutlass.range(ncol, unroll_full=True):
328
+ acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= col_limit_right else acc_S[i]
329
+ else:
330
+ mask_r2p(acc_S, col_limit_right, arch=100, rank1=True)
331
+ else:
332
+ local_row_offset_right = (
333
+ causal_row_offset + self.window_size_right
334
+ if const_expr(self.window_size_right is not None)
335
+ else None
336
+ )
337
+ local_row_offset_left = (
338
+ causal_row_offset - 1 - self.window_size_left
339
+ if const_expr(self.window_size_left is not None)
340
+ else None
341
+ )
342
+ if const_expr(self.window_size_right is not None):
343
+ col_limit_right = row_idx + local_row_offset_right
344
+ if const_expr(mask_seqlen):
345
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
346
+ else:
347
+ col_limit_right = self.tile_n
348
+ col_limit_left = (
349
+ row_idx + local_row_offset_left
350
+ if const_expr(self.window_size_left is not None)
351
+ else 0
352
+ )
353
+ # if cute.arch.thread_idx()[0] == 0 or cute.arch.thread_idx()[0] == 128: cute.printf("m_block = {}, n_block = {}, row_idx = {}, causal_row_offset = {}, col_limit_right = {}, col_limit_left = {}", m_block, n_block, row_idx, causal_row_offset, col_limit_right, col_limit_left)
354
+ for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
355
+ col_idx = tScS_t2r[i][1]
356
+ acc_S[i] = (
357
+ -Float32.inf
358
+ if col_idx >= col_limit_right or col_idx < col_limit_left
359
+ else acc_S[i]
360
+ )
361
+
362
+ @cute.jit
363
+ def apply_mask_sm100_transposed(
364
+ self,
365
+ acc_S: cute.Tensor,
366
+ tScS_t2r: cute.Tensor,
367
+ t0ScS_t2r: cute.Tensor,
368
+ m_block: cutlass.Int32,
369
+ n_block: cutlass.Int32,
370
+ mask_seqlen: cutlass.Constexpr,
371
+ mask_causal: cutlass.Constexpr,
372
+ mask_local: cutlass.Constexpr,
373
+ ) -> None:
374
+ """
375
+ Backward pass: mask S = K @ Q.T where n_block tiles seqlen_k and m_block tiles seqlen_q.
376
+ """
377
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
378
+ ROW = 0 if const_expr(not self.swap_AB) else 1
379
+ COL = 1 if const_expr(not self.swap_AB) else 0
380
+ thr_col_offset = tScS_t2r[0][COL]
381
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
382
+ if const_expr(not mask_causal and not mask_local):
383
+ if const_expr(mask_seqlen):
384
+ if t0ScS_t2r[0][COL] >= seqlenk_col_limit:
385
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
386
+ acc_S[i] = -cutlass.Float32.inf
387
+ else: # Causal or local
388
+ thr_row_offset = tScS_t2r[0][ROW]
389
+ causal_row_offset = (
390
+ seqlenk_col_limit - self.seqlen_q + m_block * self.tile_m + thr_row_offset
391
+ )
392
+ if const_expr(mask_causal):
393
+ col0 = t0ScS_t2r[0][COL]
394
+ row_limit_top = col0 - causal_row_offset
395
+ # tidx = cute.arch.thread_idx()[0] % 256
396
+ # if tidx < 32:
397
+ # cute.printf("tidx = {}, {} {}, {} {}, col0 = {}", tidx, tScS_t2r[0][0], tScS_t2r[0][1], tScS_t2r[1][0], tScS_t2r[1][1], col0)
398
+ if const_expr(mask_seqlen):
399
+ # If col is beyond the column limit, we want to mask out the entire
400
+ # column, by setting row limit to be self.tile_m.
401
+ if t0ScS_t2r[0][COL] >= seqlenk_col_limit:
402
+ row_limit_top = self.tile_m
403
+ r2p = True
404
+ if const_expr(not r2p):
405
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
406
+ acc_S[i] = (
407
+ -cutlass.Float32.inf if t0ScS_t2r[i][ROW] < row_limit_top else acc_S[i]
408
+ )
409
+ else:
410
+ num_rep = cute.size(tScS_t2r, mode=[0]) # 16 or 32
411
+ mask_r2p_transposed(acc_S, row_limit_top, num_rep)
412
+ else:
413
+ assert False, "Local masking isn't supported yet"
build/torch-universal/mask_definitions.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable, Optional
2
+
3
+ import random
4
+ import math
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+ import torch
9
+
10
+ from . import utils
11
+
12
+
13
+ MaskModCallable = Optional[
14
+ Callable[
15
+ [
16
+ "cute.TensorSSA",
17
+ "cute.TensorSSA",
18
+ "cute.TensorSSA",
19
+ "cute.TensorSSA",
20
+ "Optional[list]",
21
+ ],
22
+ "cute.TensorSSA",
23
+ ]
24
+ ]
25
+
26
+
27
+ # Flex Attention mask functions (PyTorch signatures for reference implementation)
28
+ def get_flex_causal_mask(offset: int):
29
+ def _flex_causal_mask(b, h, q_idx, kv_idx):
30
+ return kv_idx <= q_idx + offset
31
+
32
+ return _flex_causal_mask
33
+
34
+
35
+ def get_flex_block_causal_mask(offset: int):
36
+ def _flex_block_causal_mask(b, h, q_idx, kv_idx):
37
+ return kv_idx <= q_idx + offset
38
+
39
+ return _flex_block_causal_mask
40
+
41
+
42
+ def get_flex_sliding_window_mask(window_left: int, window_right: int, offset: int):
43
+ def _flex_sliding_window_mask(b, h, q_idx, kv_idx):
44
+ center = q_idx + offset
45
+ lower = center - window_left
46
+ upper = center + window_right
47
+ return (kv_idx >= lower) & (kv_idx <= upper)
48
+
49
+ return _flex_sliding_window_mask
50
+
51
+
52
+ def flex_block_diagonal_mask(b, h, q_idx, kv_idx):
53
+ block_size = 64
54
+ return (q_idx // block_size) == (kv_idx // block_size)
55
+
56
+
57
+ def flex_mini_causal_mask(b, h, q_idx, kv_idx):
58
+ return (q_idx % 128) >= (kv_idx % 128)
59
+
60
+
61
+ def flex_document_mask(b, h, q_idx, kv_idx, doc_id):
62
+ return doc_id[b, h, q_idx] == doc_id[b, h, kv_idx]
63
+
64
+
65
+ # CuTe versions for kernel compilation
66
+ def get_cute_causal_mask(offset: int):
67
+ @cute.jit
68
+ def _cute_causal_mask(
69
+ batch: cute.TensorSSA,
70
+ head: cute.TensorSSA,
71
+ m_idx: cute.TensorSSA,
72
+ n_idx: cute.TensorSSA,
73
+ aux_tensors: None,
74
+ ) -> cute.TensorSSA:
75
+ offset_ssa = utils.scalar_to_ssa(offset, cutlass.Int32)
76
+ return n_idx <= (m_idx + offset_ssa)
77
+
78
+ return _cute_causal_mask
79
+
80
+
81
+ def get_cute_block_causal_mask(offset: int):
82
+ @cute.jit
83
+ def _cute_block_causal_mask(
84
+ batch: cute.TensorSSA,
85
+ head: cute.TensorSSA,
86
+ m_idx: cute.TensorSSA,
87
+ n_idx: cute.TensorSSA,
88
+ aux_tensors: None,
89
+ ) -> cute.TensorSSA:
90
+ offset_ssa = utils.scalar_to_ssa(offset, cutlass.Int32)
91
+ return n_idx <= (m_idx + offset_ssa)
92
+
93
+ return _cute_block_causal_mask
94
+
95
+
96
+ def get_cute_sliding_window_mask(window_left: int, window_right: int, offset: int):
97
+ @cute.jit
98
+ def _cute_sliding_window_mask(
99
+ batch: cute.TensorSSA,
100
+ head: cute.TensorSSA,
101
+ m_idx: cute.TensorSSA,
102
+ n_idx: cute.TensorSSA,
103
+ aux_tensors,
104
+ ) -> cute.TensorSSA:
105
+ offset_ssa = utils.scalar_to_ssa(offset, cutlass.Int32)
106
+ window_left_ssa = utils.scalar_to_ssa(window_left, cutlass.Int32)
107
+ window_right_ssa = utils.scalar_to_ssa(window_right, cutlass.Int32)
108
+ center = m_idx + offset_ssa
109
+ lower = center - window_left_ssa
110
+ upper = center + window_right_ssa
111
+ return (n_idx >= lower) & (n_idx <= upper)
112
+
113
+ return _cute_sliding_window_mask
114
+
115
+
116
+ @cute.jit
117
+ def cute_document_mask(
118
+ batch: cute.TensorSSA,
119
+ head: cute.TensorSSA,
120
+ m_idx: cute.TensorSSA,
121
+ n_idx: cute.TensorSSA,
122
+ aux_tensors: list,
123
+ ) -> cute.TensorSSA:
124
+ doc_id = aux_tensors[0]
125
+ m_doc = utils.scalar_to_ssa(doc_id[batch[0], head[0], m_idx[0]], cutlass.Int32)
126
+ n_doc = utils.scalar_to_ssa(doc_id[batch[0], head[0], n_idx[0]], cutlass.Int32)
127
+ return m_doc == n_doc
128
+
129
+
130
+ @cute.jit
131
+ def cute_block_diagonal_mask(
132
+ batch: cute.TensorSSA,
133
+ head: cute.TensorSSA,
134
+ m_idx: cute.TensorSSA,
135
+ n_idx: cute.TensorSSA,
136
+ aux_tensors,
137
+ ) -> cute.TensorSSA:
138
+ block_size_ssa = utils.scalar_to_ssa(64, cutlass.Int32)
139
+ return (m_idx // block_size_ssa) == (n_idx // block_size_ssa)
140
+
141
+
142
+ @cute.jit
143
+ def cute_mini_causal_mask(
144
+ batch: cute.TensorSSA,
145
+ head: cute.TensorSSA,
146
+ m_idx: cute.TensorSSA,
147
+ n_idx: cute.TensorSSA,
148
+ aux_tensors,
149
+ ) -> cute.TensorSSA:
150
+ tile_size_ssa = utils.scalar_to_ssa(128, cutlass.Int32)
151
+ m_mod = m_idx % tile_size_ssa
152
+ n_mod = n_idx % tile_size_ssa
153
+ return m_mod >= n_mod
154
+
155
+
156
+ def random_doc_id_tensor(nheads, batch, seqlen_q, device="cpu"):
157
+ doc_ids_tensor = torch.zeros(batch, nheads, seqlen_q, dtype=torch.int32, device=device)
158
+ for b in range(batch):
159
+ for h in range(nheads):
160
+ N = seqlen_q
161
+ max_segments = max(1, math.ceil(math.sqrt(max(N // 4, 1))))
162
+ n = random.randint(1, max_segments)
163
+ n = min(n, N)
164
+ cuts = sorted(random.sample(range(1, N), n - 1))
165
+ lengths = [b - a for a, b in zip((0, *cuts), (*cuts, N))]
166
+
167
+ doc_ids = []
168
+ for i, length in enumerate(lengths):
169
+ doc_ids += [i for _ in range(length)]
170
+
171
+ doc_ids_tensor[b, h, :] = torch.tensor(doc_ids, dtype=torch.int32, device=device)
172
+ return doc_ids_tensor
173
+
174
+
175
+ STATIC_MASKS = {
176
+ "block_diagonal": (cute_block_diagonal_mask, flex_block_diagonal_mask),
177
+ "mini_causal": (cute_mini_causal_mask, flex_mini_causal_mask),
178
+ "document": (cute_document_mask, flex_document_mask),
179
+ }
180
+
181
+ PARAMETERIZED_MASK_FACTORIES = {
182
+ "causal": (get_cute_causal_mask, get_flex_causal_mask),
183
+ "block_causal": (get_cute_block_causal_mask, get_flex_block_causal_mask),
184
+ "sliding_window": (get_cute_sliding_window_mask, get_flex_sliding_window_mask),
185
+ }
186
+
187
+
188
+ def get_mask_pair(mask_name, seqlen_q=None, seqlen_k=None, window_size=None):
189
+ """Get (cute_mask, flex_mask) pair for the given mask name.
190
+
191
+ For static masks, seqlen info is not needed.
192
+ For parameterized masks, seqlen_q and seqlen_k are required.
193
+ """
194
+ if mask_name in STATIC_MASKS:
195
+ return STATIC_MASKS[mask_name]
196
+
197
+ if mask_name not in PARAMETERIZED_MASK_FACTORIES:
198
+ raise ValueError(f"Unknown mask: {mask_name}")
199
+
200
+ if seqlen_q is None or seqlen_k is None:
201
+ raise ValueError(f"Parameterized mask '{mask_name}' requires seqlen_q and seqlen_k")
202
+
203
+ cute_factory, flex_factory = PARAMETERIZED_MASK_FACTORIES[mask_name]
204
+ offset = seqlen_k - seqlen_q
205
+
206
+ if mask_name == "sliding_window":
207
+ if window_size is None:
208
+ raise ValueError("sliding_window mask requires window_size parameter")
209
+ cute_mask = cute_factory(window_size, window_size, offset)
210
+ flex_mask = flex_factory(window_size, window_size, offset)
211
+ else:
212
+ cute_mask = cute_factory(offset)
213
+ flex_mask = flex_factory(offset)
214
+
215
+ return cute_mask, flex_mask
216
+
217
+
218
+ if __name__ == "__main__":
219
+ doc_ids = random_doc_id_tensor(1, 2, 128)
220
+ print(f"{doc_ids = }")
build/torch-universal/metadata.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"python-depends":["nvidia-cutlass-dsl"]}
build/torch-universal/mma_sm100_desc.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ # Ported Cutlass code from C++ to Python:
3
+ # https://github.com/NVIDIA/cutlass/blob/main/include/cute/arch/mma_sm100_desc.hpp
4
+ # https://github.com/NVIDIA/cutlass/blob/main/include/cute/atom/mma_traits_sm100.hpp
5
+
6
+ from enum import IntEnum
7
+
8
+ import cutlass
9
+ import cutlass.cute as cute
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Enumerations that match the HW encodings (values MUST stay identical)
13
+ # ---------------------------------------------------------------------------
14
+
15
+
16
+ class Major(IntEnum): # matrix “layout” in the ISA docs
17
+ K = 0
18
+ MN = 1
19
+
20
+
21
+ class ScaleIn(IntEnum): # negate flags
22
+ One = 0
23
+ Neg = 1
24
+
25
+
26
+ class Saturate(IntEnum):
27
+ False_ = 0
28
+ True_ = 1
29
+
30
+
31
+ class CFormat(IntEnum): # 2-bit field (bits 4-5)
32
+ F16 = 0
33
+ F32 = 1
34
+ S32 = 2
35
+
36
+
37
+ class F16F32Format(IntEnum): # 3-bit field (A/B element type)
38
+ F16 = 0
39
+ BF16 = 1
40
+ TF32 = 2
41
+
42
+
43
+ class S8Format(IntEnum):
44
+ UINT8 = 0
45
+ INT8 = 1
46
+
47
+
48
+ class MXF8F6F4Format(IntEnum):
49
+ E4M3 = 0
50
+ E5M2 = 1
51
+ E2M3 = 3
52
+ E3M2 = 4
53
+ E2M1 = 5
54
+
55
+
56
+ class MaxShift(IntEnum):
57
+ NoShift = 0
58
+ MaxShift8 = 1
59
+ MaxShift16 = 2
60
+ MaxShift32 = 3
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # CUTLASS-type → encoding helpers
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ def to_UMMA_format(cutlass_type) -> int:
69
+ """
70
+ Map a CUTLASS scalar class to the 3-bit encoding for Matrix A/B.
71
+ """
72
+ if cutlass_type is cutlass.Int8:
73
+ return S8Format.INT8
74
+ # Unsigned 8-bit (if available in your CUTLASS build)
75
+ if cutlass_type is cutlass.Uint8:
76
+ return S8Format.UINT8
77
+ # FP-16 / BF-16
78
+ if cutlass_type is cutlass.Float16:
79
+ return F16F32Format.F16
80
+ if cutlass_type is cutlass.BFloat16:
81
+ return F16F32Format.BF16
82
+ # TensorFloat-32 (8-bit exponent, 10-bit mantissa packed in 19 bits)
83
+ if cutlass_type is cutlass.TFloat32:
84
+ return F16F32Format.TF32
85
+ # Float-8 / Float-6 / Float-4 – add whenever CUTLASS exposes them
86
+ if cutlass_type is cutlass.FloatE4M3FN:
87
+ return MXF8F6F4Format.E4M3
88
+ if cutlass_type is cutlass.FloatE5M2:
89
+ return MXF8F6F4Format.E5M2
90
+ raise TypeError(f"Unsupported CUTLASS scalar type for A/B: {cutlass_type!r}")
91
+
92
+
93
+ def to_C_format(cutlass_type) -> int:
94
+ """
95
+ Map a CUTLASS scalar class to the 2-bit accumulator encoding.
96
+ """
97
+ if cutlass_type is cutlass.Float16:
98
+ return CFormat.F16
99
+ if cutlass_type is cutlass.Float32:
100
+ return CFormat.F32
101
+ if cutlass_type is cutlass.Int32:
102
+ return CFormat.S32
103
+ raise TypeError(f"Unsupported CUTLASS scalar type for accumulator: {cutlass_type!r}")
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # The constructor – accepts only CUTLASS scalar classes
108
+ # ---------------------------------------------------------------------------
109
+
110
+
111
+ def make_instr_desc(
112
+ a_type, # CUTLASS scalar class, e.g. cutlass.Int8
113
+ b_type,
114
+ c_type,
115
+ M: int, # 64, 128 or 256
116
+ N: int, # 8 … 256 (multiple of 8)
117
+ a_major: Major,
118
+ b_major: Major,
119
+ a_neg: ScaleIn = ScaleIn.One,
120
+ b_neg: ScaleIn = ScaleIn.One,
121
+ c_sat: Saturate = Saturate.False_,
122
+ is_sparse: bool = False,
123
+ max_shift: MaxShift = MaxShift.NoShift,
124
+ ) -> int:
125
+ """
126
+ Build the 32-bit instruction descriptor for Blackwell MMA.
127
+ All matrix/accumulator **types must be CUTLASS scalar classes** –
128
+ passing integers is forbidden.
129
+ """
130
+ # --- encode element formats -------------------------------------------------
131
+ a_fmt = int(to_UMMA_format(a_type))
132
+ b_fmt = int(to_UMMA_format(b_type))
133
+ c_fmt = int(to_C_format(c_type))
134
+
135
+ # --- range checks on M/N -----------------------------------------------------
136
+ if M not in (64, 128, 256):
137
+ raise ValueError("M must be 64, 128 or 256")
138
+ if N < 8 or N > 256 or (N & 7):
139
+ raise ValueError("N must be a multiple of 8 in the range 8…256")
140
+
141
+ m_dim = M >> 4 # 5-bit field
142
+ n_dim = N >> 3 # 6-bit field
143
+
144
+ # fmt: off
145
+ # --- pack the bit-fields -----------------------------------------------------
146
+ desc = 0
147
+ desc |= (0 & 0x3) << 0 # sparse_id2 (always 0 here)
148
+ desc |= (int(is_sparse) & 0x1) << 2 # sparse_flag
149
+ desc |= (int(c_sat) & 0x1) << 3 # saturate
150
+ desc |= (c_fmt & 0x3) << 4 # c_format
151
+ desc |= (a_fmt & 0x7) << 7 # a_format
152
+ desc |= (b_fmt & 0x7) << 10 # b_format
153
+ desc |= (int(a_neg) & 0x1) << 13 # a_negate
154
+ desc |= (int(b_neg) & 0x1) << 14 # b_negate
155
+ desc |= (int(a_major) & 0x1) << 15 # a_major
156
+ desc |= (int(b_major) & 0x1) << 16 # b_major
157
+ desc |= (n_dim & 0x3F) << 17 # n_dim (6 bits)
158
+ desc |= (m_dim & 0x1F) << 24 # m_dim (5 bits)
159
+ desc |= (int(max_shift) & 0x3) << 30 # max_shift (2 bits)
160
+ # fmt: on
161
+
162
+ return desc & 0xFFFF_FFFF # ensure 32-bit result
163
+
164
+
165
+ def mma_op_to_idesc(op: cute.nvgpu.tcgen05.mma.MmaOp):
166
+ return make_instr_desc(
167
+ op.a_dtype,
168
+ op.b_dtype,
169
+ op.acc_dtype,
170
+ op.shape_mnk[0],
171
+ op.shape_mnk[1],
172
+ Major.K if op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K else Major.MN,
173
+ Major.K if op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K else Major.MN,
174
+ )
175
+
176
+
177
+ class LayoutType(IntEnum): # occupies the top-3 bits [61:64)
178
+ SWIZZLE_NONE = 0 # (a.k.a. “INTERLEAVE” in older docs)
179
+ SWIZZLE_128B_BASE32B = 1
180
+ SWIZZLE_128B = 2
181
+ SWIZZLE_64B = 4
182
+ SWIZZLE_32B = 6
183
+ # values 3,5,7 are reserved / illegal for UMMA
184
+
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Helpers – figure out the SWIZZLE_* family from the tensor layout
188
+ # ---------------------------------------------------------------------------
189
+
190
+
191
+ def _layout_type(swizzle: cute.Swizzle) -> LayoutType:
192
+ # No idea what the right way to get B, M, S is – so we're just parsing it from the __str__
193
+ # Swizzle string has the form "S<B,M,S>"
194
+ swz_str = str(swizzle)
195
+ inside = swz_str[swz_str.index("<") + 1 : swz_str.index(">")] # '3,4,3'
196
+ B, M, S = [int(x) for x in inside.split(",")] # [3, 4, 3]
197
+
198
+ if M == 4: # Swizzle<*,4,3>
199
+ if S != 3:
200
+ raise ValueError("Unexpected swizzle shift – want S==3 for M==4")
201
+ return {
202
+ 0: LayoutType.SWIZZLE_NONE,
203
+ 1: LayoutType.SWIZZLE_32B,
204
+ 2: LayoutType.SWIZZLE_64B,
205
+ 3: LayoutType.SWIZZLE_128B,
206
+ }[B] # KeyError ⇒ invalid B→ raise
207
+ if M == 5: # Swizzle<2,5,2> (the only legal triple for M==5)
208
+ if (B, S) != (2, 2):
209
+ raise ValueError("Only Swizzle<2,5,2> supported for 128B_BASE32B")
210
+ return LayoutType.SWIZZLE_128B_BASE32B
211
+
212
+ # Any other (M,B,S) triple is not a UMMA-legal shared-memory layout
213
+ raise ValueError("Unsupported swizzle triple for UMMA smem descriptor")
214
+
215
+
216
+ def make_smem_desc_base(layout: cute.Layout, swizzle: cute.Swizzle, major: Major) -> int:
217
+ """
218
+ Convert a 2-D *shared-memory* Cute layout into the Blackwell 64-bit
219
+ smem-descriptor, without the smem start address.
220
+ layout must correspond to layout of an uint128 tensor.
221
+ """
222
+ # ------------------------------------------------------------------ meta
223
+ layout_type = _layout_type(swizzle) # resolve SWIZZLE_* family
224
+
225
+ VERSION = 1 # bits 46–47
226
+ LBO_MODE = 0 # bit 52
227
+ BASE_OFFSET = 0 # bits 49–51 (CUTLASS always 0)
228
+
229
+ # ---------------------------------------------------------- strides (units: uint128_t = 16 B)
230
+ swizzle_atom_mn_size = {
231
+ LayoutType.SWIZZLE_NONE: 1,
232
+ LayoutType.SWIZZLE_32B: 2,
233
+ LayoutType.SWIZZLE_64B: 4,
234
+ LayoutType.SWIZZLE_128B: 8,
235
+ LayoutType.SWIZZLE_128B_BASE32B: 8,
236
+ }[layout_type]
237
+
238
+ if major is Major.MN:
239
+ swizzle_atom_k_size = 4 if layout_type is LayoutType.SWIZZLE_128B_BASE32B else 8
240
+ canonical_layout = cute.logical_divide(layout, (swizzle_atom_mn_size, swizzle_atom_k_size))
241
+ if not cute.is_congruent(canonical_layout, ((1, 1), (1, 1))):
242
+ raise ValueError("Not a canonical UMMA_MN Layout: Expected profile failure.")
243
+ stride_00 = canonical_layout.stride[0][0]
244
+ if layout_type is not LayoutType.SWIZZLE_NONE and stride_00 != 1:
245
+ raise ValueError("Not a canonical UMMA_MN Layout: Expected stride failure.")
246
+ stride_10 = canonical_layout.stride[1][0]
247
+ if stride_10 != swizzle_atom_mn_size:
248
+ raise ValueError("Not a canonical UMMA_MN Layout: Expected stride failure.")
249
+ stride_01, stride_11 = canonical_layout.stride[0][1], canonical_layout.stride[1][1]
250
+ if layout_type is LayoutType.SWIZZLE_NONE:
251
+ stride_byte_offset, leading_byte_offset = stride_01, stride_11
252
+ else:
253
+ stride_byte_offset, leading_byte_offset = stride_11, stride_01
254
+ else:
255
+ if layout_type == LayoutType.SWIZZLE_128B_BASE32B:
256
+ raise ValueError("SWIZZLE_128B_BASE32B is invalid for Major-K")
257
+ if not cute.size(layout.shape[0]) % 8 == 0:
258
+ raise ValueError("Not a canonical UMMA_K Layout: Expected MN-size multiple of 8.")
259
+ canonical_layout = cute.logical_divide(layout, (8, 2))
260
+ if not cute.is_congruent(canonical_layout, ((1, 1), (1, 1))):
261
+ raise ValueError("Not a canonical UMMA_K Layout: Expected profile failure.")
262
+ stride_00 = canonical_layout.stride[0][0]
263
+ if stride_00 != swizzle_atom_mn_size:
264
+ raise ValueError("Not a canonical UMMA_K Layout: Expected stride failure.")
265
+ stride_10 = canonical_layout.stride[1][0]
266
+ if layout_type is not LayoutType.SWIZZLE_NONE and stride_10 != 1:
267
+ raise ValueError("Not a canonical UMMA_K Layout: Expected stride failure.")
268
+ stride_01 = canonical_layout.stride[0][1]
269
+ stride_byte_offset, leading_byte_offset = stride_01, stride_10
270
+
271
+ # ------------------------------------------------------------------ pack
272
+ desc = 0
273
+ # leading_byte_offset_ [16:30)
274
+ desc |= (leading_byte_offset & 0x3FFF) << 16
275
+ # stride_byte_offset_ [32:46)
276
+ desc |= (stride_byte_offset & 0x3FFF) << 32
277
+ # version_ [46:48)
278
+ desc |= (VERSION & 0x3) << 46
279
+ # base_offset_ [49:52)
280
+ desc |= (BASE_OFFSET & 0x7) << 49
281
+ # lbo_mode_ [52:53)
282
+ desc |= (LBO_MODE & 0x1) << 52
283
+ # layout_type_ [61:64)
284
+ desc |= (int(layout_type) & 0x7) << 61
285
+
286
+ return desc & 0xFFFF_FFFF_FFFF_FFFF # force 64-bit width
287
+
288
+
289
+ def make_smem_desc_start_addr(start_addr: cute.Pointer) -> cutlass.Int32:
290
+ # 14 bits, remove 4 LSB (bits 0-13 in desc)
291
+ return (start_addr.toint() & 0x3FFFF) >> 4
build/torch-universal/named_barrier.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+
3
+ import enum
4
+
5
+
6
+ class NamedBarrierFwd(enum.IntEnum):
7
+ Epilogue = enum.auto() # starts from 1 as barrier 0 is reserved for sync_threads()
8
+ WarpSchedulerWG1 = enum.auto()
9
+ WarpSchedulerWG2 = enum.auto()
10
+ WarpSchedulerWG3 = enum.auto()
11
+ PFull = enum.auto()
12
+ PEmpty = enum.auto()
13
+
14
+
15
+ class NamedBarrierBwd(enum.IntEnum):
16
+ Epilogue = enum.auto()
17
+ WarpSchedulerWG1 = enum.auto()
18
+ WarpSchedulerWG2 = enum.auto()
19
+ WarpSchedulerWG3 = enum.auto()
20
+ PdS = enum.auto()
21
+ dQFullWG0 = enum.auto()
22
+ dQFullWG1 = enum.auto()
23
+ dQEmptyWG0 = enum.auto()
24
+ dQEmptyWG1 = enum.auto()
25
+
26
+
27
+ class NamedBarrierBwdSm100(enum.IntEnum):
28
+ EpilogueWG1 = enum.auto()
29
+ EpilogueWG2 = enum.auto()
30
+ Compute = enum.auto()
31
+ dQaccReduce = enum.auto()
build/torch-universal/pack_gqa.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import math
4
+ import operator
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+
9
+ from . import utils
10
+
11
+
12
+ class PackGQA:
13
+ def __init__(
14
+ self,
15
+ m_block_size: cutlass.Constexpr[int],
16
+ head_dim_padded: cutlass.Constexpr[int],
17
+ check_hdim_oob: cutlass.Constexpr[bool],
18
+ qhead_per_kvhead: cutlass.Constexpr[bool],
19
+ ):
20
+ self.m_block_size = m_block_size
21
+ self.head_dim_padded = head_dim_padded
22
+ self.check_hdim_oob = check_hdim_oob
23
+ self.qhead_per_kvhead = qhead_per_kvhead
24
+
25
+ @cute.jit
26
+ def compute_ptr(
27
+ self,
28
+ tensor: cute.Tensor,
29
+ cRows: cute.Tensor,
30
+ tidx: cutlass.Int32,
31
+ block: cutlass.Int32,
32
+ threads_per_row: cutlass.Constexpr[int],
33
+ num_threads: cutlass.Constexpr[int],
34
+ ):
35
+ num_ptr_per_thread = cute.ceil_div(cute.size(cRows), threads_per_row)
36
+ tPrPtr = cute.make_fragment(num_ptr_per_thread, cutlass.Int64)
37
+ for i in cutlass.range_constexpr(num_ptr_per_thread):
38
+ row = i * num_threads + cRows[tidx % threads_per_row][0]
39
+ idx = block * self.m_block_size + row
40
+ m_idx = idx // self.qhead_per_kvhead
41
+ h_idx = idx - m_idx * self.qhead_per_kvhead
42
+ tPrPtr[i] = utils.elem_pointer(tensor, ((h_idx, m_idx),)).toint()
43
+ return tPrPtr
44
+
45
+ @cute.jit
46
+ def load_Q(
47
+ self,
48
+ mQ: cute.Tensor, # ((qhead_per_kvhead, seqlen_q), headdim)
49
+ sQ: cute.Tensor, # (m_block_size, head_dim_padded)
50
+ gmem_tiled_copy: cute.TiledCopy,
51
+ tidx: cutlass.Int32,
52
+ block: cutlass.Int32,
53
+ seqlen: cutlass.Int32,
54
+ ):
55
+ gmem_thr_copy = gmem_tiled_copy.get_slice(tidx)
56
+ cQ = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
57
+ tQsQ = gmem_thr_copy.partition_D(sQ)
58
+ tQcQ = gmem_thr_copy.partition_S(cQ)
59
+ t0QcQ = gmem_thr_copy.get_slice(0).partition_S(cQ)
60
+ tQpQ = utils.predicate_k(tQcQ, limit=mQ.shape[1])
61
+ tQcQ_row = tQcQ[0, None, 0]
62
+ threads_per_row = gmem_tiled_copy.layout_tv_tiled.shape[0][0]
63
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, "threads_per_row must divide WARP_SIZE"
64
+ num_threads = gmem_tiled_copy.size
65
+ tPrQPtr = self.compute_ptr(mQ[None, 0], tQcQ_row, tidx, block, threads_per_row, num_threads)
66
+ for m in cutlass.range_constexpr(cute.size(tQsQ.shape[1])):
67
+ q_ptr_i64 = utils.shuffle_sync(
68
+ tPrQPtr[m // threads_per_row], m % threads_per_row, width=threads_per_row
69
+ )
70
+ q_gmem_ptr = cute.make_ptr(
71
+ mQ.element_type, q_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
72
+ )
73
+ if (
74
+ t0QcQ[0, m, 0][0]
75
+ < seqlen * self.qhead_per_kvhead - block * self.m_block_size - tQcQ_row[0][0]
76
+ ):
77
+ mQ_cur = cute.make_tensor(q_gmem_ptr, (self.head_dim_padded,))
78
+ elems_per_load = cute.size(tQsQ.shape[0][0])
79
+ mQ_cur_copy = cute.tiled_divide(mQ_cur, (elems_per_load,))
80
+ for k in cutlass.range_constexpr(cute.size(tQsQ.shape[2])):
81
+ ki = tQcQ[0, 0, k][1] // elems_per_load
82
+ cute.copy(
83
+ gmem_thr_copy,
84
+ mQ_cur_copy[None, ki],
85
+ tQsQ[None, m, k],
86
+ pred=tQpQ[None, m, k] if cutlass.const_expr(self.check_hdim_oob) else None,
87
+ )
88
+ # We don't need to clear the sQ smem tiles since we'll only write out the valid outputs
89
+
90
+ @cute.jit
91
+ def store_LSE(
92
+ self,
93
+ mLSE: cute.Tensor, # (qhead_per_kvhead, seqlen_q)
94
+ tLSErLSE: cute.Tensor, # (m_block_size, head_dim_padded)
95
+ tiled_mma: cute.TiledMma,
96
+ tidx: cutlass.Int32,
97
+ block: cutlass.Int32,
98
+ seqlen: cutlass.Int32,
99
+ ):
100
+ thr_mma = tiled_mma.get_slice(tidx)
101
+ caccO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
102
+ taccOcO = thr_mma.partition_C(caccO)
103
+ taccOcO_row = utils.make_acc_tensor_mn_view(taccOcO)[None, 0]
104
+ assert cute.size(tLSErLSE) == cute.size(taccOcO_row)
105
+ threads_per_row = tiled_mma.tv_layout_C.shape[0][0]
106
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, "threads_per_row must divide WARP_SIZE"
107
+ assert cute.size(tLSErLSE) <= threads_per_row
108
+ num_threads = tiled_mma.size
109
+ tPrLSEPtr = self.compute_ptr(mLSE, taccOcO_row, tidx, block, threads_per_row, num_threads)
110
+ for m in cutlass.range_constexpr(cute.size(tLSErLSE)):
111
+ lse_ptr_i64 = utils.shuffle_sync(
112
+ tPrLSEPtr[m // threads_per_row],
113
+ m % threads_per_row,
114
+ width=threads_per_row,
115
+ )
116
+ lse_gmem_ptr = cute.make_ptr(
117
+ mLSE.element_type, lse_ptr_i64, cute.AddressSpace.gmem, assumed_align=4
118
+ )
119
+ row = block * self.m_block_size + taccOcO_row[m][0]
120
+ # Only the thread corresponding to column 0 writes out the lse to gmem
121
+ if taccOcO[0][1] == 0 and row < seqlen * self.qhead_per_kvhead:
122
+ mLSE_copy = cute.make_tensor(lse_gmem_ptr, (1,))
123
+ mLSE_copy[0] = tLSErLSE[m]
124
+
125
+ @cute.jit
126
+ def store_O(
127
+ self,
128
+ mO: cute.Tensor, # ((qhead_per_kvhead, seqlen_q), headdim)
129
+ tOrO: cute.Tensor, # (m_block_size, head_dim_padded) split across threads according to gmem_tiled_copy
130
+ gmem_tiled_copy: cute.TiledCopy,
131
+ tidx: cutlass.Int32,
132
+ block: cutlass.Int32,
133
+ seqlen: cutlass.Int32,
134
+ ):
135
+ gmem_thr_copy = gmem_tiled_copy.get_slice(tidx)
136
+ cO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
137
+ tOcO = gmem_thr_copy.partition_S(cO)
138
+ t0OcO = gmem_thr_copy.get_slice(0).partition_S(cO)
139
+ tOpO = utils.predicate_k(tOcO, limit=mO.shape[1])
140
+ tOcO_row = tOcO[0, None, 0]
141
+ threads_per_row = gmem_tiled_copy.layout_tv_tiled.shape[0][0]
142
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, "threads_per_row must divide WARP_SIZE"
143
+ num_threads = gmem_tiled_copy.size
144
+ tPrOPtr = self.compute_ptr(mO[None, 0], tOcO_row, tidx, block, threads_per_row, num_threads)
145
+ for m in cutlass.range_constexpr(cute.size(tOrO.shape[1])):
146
+ o_ptr_i64 = utils.shuffle_sync(
147
+ tPrOPtr[m // threads_per_row], m % threads_per_row, width=threads_per_row
148
+ )
149
+ o_gmem_ptr = cute.make_ptr(
150
+ mO.element_type, o_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
151
+ )
152
+ if (
153
+ t0OcO[0, m, 0][0]
154
+ < seqlen * self.qhead_per_kvhead - block * self.m_block_size - tOcO_row[0][0]
155
+ ):
156
+ mO_cur = cute.make_tensor(o_gmem_ptr, (self.head_dim_padded,))
157
+ elems_per_load = cute.size(tOrO.shape[0][0])
158
+ mO_cur_copy = cute.tiled_divide(mO_cur, (elems_per_load,))
159
+ for k in cutlass.range_constexpr(cute.size(tOrO.shape[2])):
160
+ ki = tOcO[0, 0, k][1] // elems_per_load
161
+ cute.copy(
162
+ gmem_thr_copy,
163
+ tOrO[None, m, k],
164
+ mO_cur_copy[None, ki],
165
+ pred=tOpO[None, m, k] if cutlass.const_expr(self.check_hdim_oob) else None,
166
+ )
build/torch-universal/pipeline.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ # import math
4
+ from typing import Optional
5
+ from dataclasses import dataclass
6
+
7
+ import cutlass
8
+ import cutlass.cute as cute
9
+ from cutlass import Boolean, Int32, const_expr
10
+ from cutlass.cutlass_dsl import if_generate
11
+ from cutlass.pipeline import PipelineAsync, PipelineState, Agent, CooperativeGroup
12
+ from cutlass.pipeline import PipelineUserType, PipelineOp
13
+ from cutlass.pipeline import PipelineTmaAsync as PipelineTmaAsyncOg
14
+ from cutlass.pipeline import PipelineTmaUmma as PipelineTmaUmmaOg
15
+
16
+
17
+ # We deviate from cute-dsl implementation to use cute.arch.cluster_arrive_relaxed
18
+ def pipeline_init_wait(cta_layout_vmnk: Optional[cute.Layout] = None):
19
+ """
20
+ Fences the mbarrier init and syncs the threadblock or cluster
21
+ """
22
+ cute.arch.mbarrier_init_fence()
23
+
24
+ if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
25
+ # If not using clusters, sync the threadblock
26
+ _sync(Agent.ThreadBlock)
27
+ else:
28
+ # If using clusters, sync the cluster
29
+ _sync(Agent.ThreadBlockCluster)
30
+
31
+
32
+ def _sync(group: Agent):
33
+ """
34
+ Syncs all threads within an agent.
35
+ """
36
+ if group is Agent.Thread:
37
+ raise NotImplementedError("Error: Not supported.")
38
+ elif group is Agent.ThreadBlock:
39
+ cute.arch.sync_threads()
40
+ elif group is Agent.ThreadBlockCluster:
41
+ cute.arch.cluster_arrive_relaxed()
42
+ cute.arch.cluster_wait()
43
+ else:
44
+ assert False, (
45
+ "Error: No explicit sync instruction exists. Please use barriers (named / mbarrier) instead."
46
+ )
47
+
48
+
49
+ class PipelineStateSimple:
50
+ """
51
+ Pipeline state contains an index and phase bit corresponding to the current position in the circular buffer.
52
+ Use a single Int32 to store both the index and phase bit, then we use divmod to get the
53
+ index and phase. If stages is a power of 2, divmod turns into bit twiddling.
54
+ """
55
+
56
+ def __init__(self, stages: int, phase_index: Int32):
57
+ # assert stages < 2**16
58
+ # self._log_stages = int(math.log2(stages))
59
+ # assert 1 << self._log_stages == stages, "Number of stages must be a power of 2."
60
+ self._stages = stages
61
+ self._phase_index = phase_index
62
+
63
+ def clone(self) -> "PipelineStateSimple":
64
+ return PipelineStateSimple(self.stages, self._phase_index)
65
+
66
+ @property
67
+ def stages(self) -> int:
68
+ # return 1 << self._log_stages
69
+ return self._stages
70
+
71
+ @property
72
+ def index(self) -> Int32:
73
+ # return self._phase_index & 0xFFFF
74
+ # return self._phase_index & ((1 << self._log_stages) - 1)
75
+ if const_expr(self._stages == 1):
76
+ return Int32(0)
77
+ else:
78
+ return self._phase_index % self._stages
79
+
80
+ @property
81
+ def phase(self) -> Int32:
82
+ # return self._phase_index >> 16
83
+ # PTX docs say that the phase parity needs to be 0 or 1, so by right we need to
84
+ # take modulo 2. But in practice just passing the phase in without modulo works fine.
85
+ # return (self._phase_index >> self._log_stages) % 2
86
+ # return self._phase_index >> self._log_stages
87
+ if const_expr(self._stages == 1):
88
+ return self._phase_index
89
+ else:
90
+ return self._phase_index // self._stages
91
+
92
+ def advance(self):
93
+ if const_expr(self._stages == 1):
94
+ self._phase_index ^= 1
95
+ else:
96
+ self._phase_index += 1
97
+
98
+ # def then_body(phase_index):
99
+ # # XOR the phase bit and set the index to 0
100
+ # return (phase_index & 0xFFFF0000) ^ (1 << 16)
101
+
102
+ # def else_body(phase_index):
103
+ # return phase_index
104
+
105
+ # self._phase_index = if_generate(
106
+ # (self._phase_index & 0xFFFF) == self.stages,
107
+ # then_body,
108
+ # else_body,
109
+ # [self._phase_index],
110
+ # [Int32],
111
+ # )
112
+
113
+ def __extract_mlir_values__(self):
114
+ phase_index = self._phase_index
115
+ return [phase_index.ir_value()]
116
+
117
+ def __new_from_mlir_values__(self, values):
118
+ return PipelineStateSimple(self.stages, Int32(values[0]))
119
+
120
+
121
+ def make_pipeline_state(type: PipelineUserType, stages: int):
122
+ """
123
+ Creates a pipeline state. Producers are assumed to start with an empty buffer and have a flipped phase bit of 1.
124
+ """
125
+ if type is PipelineUserType.Producer:
126
+ # return PipelineStateSimple(stages, Int32(1 << 16))
127
+ return PipelineStateSimple(stages, Int32(stages))
128
+ elif type is PipelineUserType.Consumer:
129
+ return PipelineStateSimple(stages, Int32(0))
130
+ else:
131
+ assert False, "Error: invalid PipelineUserType specified for make_pipeline_state."
132
+
133
+
134
+ @dataclass(frozen=True)
135
+ class PipelineTmaAsync(PipelineTmaAsyncOg):
136
+ """
137
+ If size(ClusterShape) == 1, PipelineTmaAsync has all threads
138
+ signaling the barrier during consumer_release. This causes a perf regression in FA3
139
+ forward pass (especially hdim 128 causal). We instead implement a version of
140
+ PipelineTmaAsync where only 1 out of 128 threads signals the barrier.
141
+
142
+ Assumptions:
143
+ (1) num_consumers % NumThreadsPerWarpGroup == 0
144
+ (2) all 128 threads in the warp group are sync'ed right before calling consumer_release
145
+ """
146
+
147
+ @staticmethod
148
+ def create(
149
+ *,
150
+ num_stages: int,
151
+ producer_group: CooperativeGroup,
152
+ consumer_group: CooperativeGroup,
153
+ tx_count: int,
154
+ barrier_storage: cute.Pointer = None,
155
+ cta_layout_vmnk: Optional[cute.Layout] = None,
156
+ tidx: Optional[Int32] = None,
157
+ mcast_mode_mn: tuple[int, int] = (1, 1),
158
+ init_wait: cutlass.Constexpr[bool] = True,
159
+ ):
160
+ """
161
+ This helper function computes any necessary attributes and returns an instance of PipelineTmaAsync.
162
+ :param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
163
+ :type barrier_storage: cute.Pointer
164
+ :param num_stages: Number of buffer stages for this pipeline
165
+ :type num_stages: Int32
166
+ :param producer_group: `CooperativeGroup` for the producer agent
167
+ :type producer_group: CooperativeGroup
168
+ :param consumer_group: `CooperativeGroup` for the consumer agent
169
+ :type consumer_group: CooperativeGroup
170
+ :param tx_count: Number of bytes expected to be written to the transaction barrier for one stage
171
+ :type tx_count: int
172
+ :param cta_layout_vmnk: Layout of the cluster shape
173
+ :type cta_layout_vmnk: cute.Layout | None
174
+ :param tidx: thread index to consumer async threads
175
+ :type tidx: Int32 | None
176
+ :param mcast_mode_mn: Tuple of two integers, specifying whether mcast is enabled for the m and n modes. At least one of the two integers must be 1.
177
+ :type mcast_mode_mn: tuple[int, int]
178
+ """
179
+ if not isinstance(barrier_storage, cute.Pointer):
180
+ raise ValueError(
181
+ f"Expected barrier_storage to be a cute.Pointer, but got {type(barrier_storage)}"
182
+ )
183
+
184
+ producer_type = PipelineOp.TmaLoad
185
+ consumer_type = PipelineOp.AsyncThread
186
+
187
+ producer = (producer_type, producer_group)
188
+ consumer = (consumer_type, consumer_group)
189
+
190
+ sync_object_full = PipelineAsync._make_sync_object(
191
+ barrier_storage.align(min_align=8), num_stages, producer, tx_count
192
+ )
193
+ sync_object_empty = PipelineAsync._make_sync_object(
194
+ barrier_storage.align(min_align=8) + num_stages, num_stages, consumer
195
+ )
196
+ if tidx is None:
197
+ tidx, _, _ = cute.arch.thread_idx()
198
+ if cta_layout_vmnk is None:
199
+ cta_layout_vmnk = cute.make_layout((1, 1, 1, 1))
200
+ if const_expr(cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1):
201
+ dst_rank = None
202
+ is_signalling_thread = tidx % 128 == 0
203
+ else:
204
+ (
205
+ dst_rank,
206
+ is_signalling_thread,
207
+ ) = PipelineTmaAsync.init_empty_barrier_arrive_signal(
208
+ cta_layout_vmnk, tidx, mcast_mode_mn
209
+ )
210
+
211
+ producer_mask = None
212
+
213
+ if const_expr(init_wait):
214
+ pipeline_init_wait()
215
+
216
+ return PipelineTmaAsync(
217
+ sync_object_full,
218
+ sync_object_empty,
219
+ num_stages,
220
+ producer_mask,
221
+ dst_rank,
222
+ is_signalling_thread,
223
+ )
224
+
225
+ def producer_acquire(
226
+ self,
227
+ state: PipelineState,
228
+ try_acquire_token: Optional[Boolean] = None,
229
+ extra_tx_count: int = 0,
230
+ ):
231
+ """
232
+ TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
233
+ """
234
+ if_generate(
235
+ try_acquire_token is None or try_acquire_token == 0,
236
+ lambda: self.sync_object_empty.wait(state.index, state.phase),
237
+ )
238
+ if const_expr(extra_tx_count == 0):
239
+ self.sync_object_full.arrive(state.index, self.producer_mask)
240
+ else:
241
+ tx_count = self.sync_object_full.tx_count + extra_tx_count
242
+ self.sync_object_full.arrive_and_expect_tx(state.index, tx_count)
243
+
244
+ def consumer_release(self, state: PipelineState):
245
+ """
246
+ TMA consumer release conditionally signals the empty buffer to the producer.
247
+ """
248
+ # Only 1 thread per warp group signals the empty buffer.
249
+ if self.consumer_mask is None: # No cluster, 1 thread per warp group to signal
250
+ if_generate(
251
+ cute.arch.thread_idx()[0] % 128 == 0,
252
+ lambda: self.sync_object_empty.arrive(state.index, self.consumer_mask),
253
+ )
254
+ else:
255
+ if_generate(
256
+ self.is_signalling_thread,
257
+ lambda: self.sync_object_empty.arrive(state.index, self.consumer_mask),
258
+ )
259
+
260
+
261
+ @dataclass(frozen=True)
262
+ class PipelineTmaUmma(PipelineTmaUmmaOg):
263
+ @staticmethod
264
+ def create(
265
+ *,
266
+ num_stages: int,
267
+ producer_group: CooperativeGroup,
268
+ consumer_group: CooperativeGroup,
269
+ tx_count: int,
270
+ barrier_storage: cute.Pointer = None,
271
+ cta_layout_vmnk: Optional[cute.Layout] = None,
272
+ mcast_mode_mn: tuple[int, int] = (1, 1),
273
+ init_wait: cutlass.Constexpr[bool] = True,
274
+ ):
275
+ """
276
+ This helper function computes any necessary attributes and returns an instance of PipelineTmaUmma.
277
+ :param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
278
+ :type barrier_storage: cute.Pointer
279
+ :param num_stages: Number of buffer stages for this pipeline
280
+ :type num_stages: Int32
281
+ :param producer_group: `CooperativeGroup` for the producer agent
282
+ :type producer_group: CooperativeGroup
283
+ :param consumer_group: `CooperativeGroup` for the consumer agent
284
+ :type consumer_group: CooperativeGroup
285
+ :param tx_count: Number of bytes expected to be written to the transaction barrier for one stage
286
+ :type tx_count: int
287
+ :param cta_layout_vmnk: Layout of the cluster shape
288
+ :type cta_layout_vmnk: cute.Layout | None
289
+ :param mcast_mode_mn: Tuple of two integers, specifying whether mcast is enabled for the m and n modes. At least one of the two integers must be 1.
290
+ :type mcast_mode_mn: tuple[int, int]
291
+ """
292
+ if not isinstance(barrier_storage, cute.Pointer):
293
+ raise ValueError(
294
+ f"Expected barrier_storage to be a cute.Pointer, but got {type(barrier_storage)}"
295
+ )
296
+
297
+ producer_type = PipelineOp.TmaLoad
298
+ consumer_type = PipelineOp.TCGen05Mma
299
+
300
+ producer = (producer_type, producer_group)
301
+ consumer = (consumer_type, consumer_group)
302
+
303
+ sync_object_full = PipelineAsync._make_sync_object(
304
+ barrier_storage.align(min_align=8), num_stages, producer, tx_count
305
+ )
306
+ sync_object_empty = PipelineAsync._make_sync_object(
307
+ barrier_storage.align(min_align=8) + num_stages, num_stages, consumer
308
+ )
309
+
310
+ if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
311
+ # No mcast mask if not using clusters
312
+ producer_mask = None
313
+ # All threadblocks are leaders if not using clusters
314
+ is_leader_cta = True
315
+ else:
316
+ producer_mask = PipelineTmaUmma._compute_mcast_arrival_mask(
317
+ cta_layout_vmnk, mcast_mode_mn
318
+ )
319
+ is_leader_cta = PipelineTmaUmma._compute_is_leader_cta(cta_layout_vmnk)
320
+
321
+ cta_group = (
322
+ cute.nvgpu.tcgen05.CtaGroup.ONE
323
+ if cta_layout_vmnk is None or cute.size(cta_layout_vmnk, mode=[0]) == 1
324
+ else cute.nvgpu.tcgen05.CtaGroup.TWO
325
+ )
326
+
327
+ consumer_mask = producer_mask
328
+
329
+ if const_expr(init_wait):
330
+ pipeline_init_wait(cta_layout_vmnk)
331
+
332
+ return PipelineTmaUmma(
333
+ sync_object_full,
334
+ sync_object_empty,
335
+ num_stages,
336
+ producer_mask,
337
+ consumer_mask,
338
+ is_leader_cta,
339
+ cta_group,
340
+ )
341
+
342
+ def producer_acquire(
343
+ self,
344
+ state: PipelineState,
345
+ try_acquire_token: Optional[Boolean] = None,
346
+ extra_tx_count: int = 0,
347
+ ):
348
+ """
349
+ TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
350
+ """
351
+ if_generate(
352
+ try_acquire_token is None or try_acquire_token == 0,
353
+ lambda: self.sync_object_empty.wait(state.index, state.phase),
354
+ )
355
+ if const_expr(extra_tx_count == 0):
356
+ if_generate(
357
+ self.is_leader_cta,
358
+ lambda: self.sync_object_full.arrive(state.index, self.producer_mask),
359
+ )
360
+ else:
361
+ tx_count = self.sync_object_full.tx_count + extra_tx_count
362
+ if_generate(
363
+ self.is_leader_cta,
364
+ lambda: self.sync_object_full.arrive_and_expect_tx(state.index, tx_count),
365
+ )
build/torch-universal/seqlen_info.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from dataclasses import dataclass
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+ from cutlass import Int32, const_expr
7
+
8
+ """
9
+ This consolidates all the info related to sequence length. This is so that we can do all
10
+ the gmem reads once at the beginning of each tile, rather than having to repeat these reads
11
+ to compute various things like n_block_min, n_block_max, etc.
12
+ """
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class SeqlenInfo:
17
+ offset: cutlass.Int32
18
+ seqlen: cutlass.Int32
19
+
20
+ @staticmethod
21
+ def create(
22
+ batch_idx: cutlass.Int32,
23
+ seqlen_static: cutlass.Int32,
24
+ cu_seqlens: Optional[cute.Tensor] = None,
25
+ seqused: Optional[cute.Tensor] = None,
26
+ ):
27
+ offset = 0 if const_expr(cu_seqlens is None) else cu_seqlens[batch_idx]
28
+ if const_expr(seqused is not None):
29
+ seqlen = seqused[batch_idx]
30
+ elif const_expr(cu_seqlens is not None):
31
+ seqlen = cu_seqlens[batch_idx + 1] - cu_seqlens[batch_idx]
32
+ else:
33
+ seqlen = seqlen_static
34
+ return SeqlenInfo(offset, seqlen)
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class SeqlenInfoQK:
39
+ offset_q: cutlass.Int32
40
+ offset_k: cutlass.Int32
41
+ seqlen_q: cutlass.Int32
42
+ seqlen_k: cutlass.Int32
43
+ has_cu_seqlens_q: cutlass.Constexpr[bool]
44
+ has_cu_seqlens_k: cutlass.Constexpr[bool]
45
+
46
+ @staticmethod
47
+ def create(
48
+ batch_idx: cutlass.Int32,
49
+ seqlen_q_static: cutlass.Int32,
50
+ seqlen_k_static: cutlass.Int32,
51
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
52
+ mCuSeqlensK: Optional[cute.Tensor] = None,
53
+ mSeqUsedQ: Optional[cute.Tensor] = None,
54
+ mSeqUsedK: Optional[cute.Tensor] = None,
55
+ ):
56
+ offset_q = 0 if const_expr(mCuSeqlensQ is None) else mCuSeqlensQ[batch_idx]
57
+ offset_k = 0 if const_expr(mCuSeqlensK is None) else mCuSeqlensK[batch_idx]
58
+ if const_expr(mSeqUsedQ is not None):
59
+ seqlen_q = mSeqUsedQ[batch_idx]
60
+ else:
61
+ seqlen_q = (
62
+ seqlen_q_static
63
+ if const_expr(mCuSeqlensQ is None)
64
+ else mCuSeqlensQ[batch_idx + 1] - offset_q
65
+ )
66
+ if const_expr(mSeqUsedK is not None):
67
+ seqlen_k = mSeqUsedK[batch_idx]
68
+ else:
69
+ seqlen_k = (
70
+ seqlen_k_static
71
+ if const_expr(mCuSeqlensK is None)
72
+ else mCuSeqlensK[batch_idx + 1] - offset_k
73
+ )
74
+ has_cu_seqlens_q: int = mCuSeqlensQ is not None
75
+ has_cu_seqlens_k: int = mCuSeqlensK is not None
76
+ return SeqlenInfoQK(
77
+ offset_q, offset_k, seqlen_q, seqlen_k, has_cu_seqlens_q, has_cu_seqlens_k
78
+ )
79
+
80
+ def offset_batch_Q(self, mQ: cute.Tensor, batch_idx: Int32, dim: int) -> cute.Tensor:
81
+ """Seqlen must be the first dimension of mQ"""
82
+ if const_expr(not self.has_cu_seqlens_q):
83
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mQ) - 1 - dim)
84
+ return mQ[idx]
85
+ else:
86
+ offset = (
87
+ self.offset_q if const_expr(cute.rank(mQ.shape[0]) == 1) else (0, self.offset_q)
88
+ )
89
+ idx = (offset,) + (0,) * (cute.rank(mQ) - 1)
90
+ return cute.domain_offset(idx, mQ)
91
+
92
+ def offset_batch_K(self, mK: cute.Tensor, batch_idx: Int32, dim: int) -> cute.Tensor:
93
+ """Seqlen must be the first dimension of mK"""
94
+ if const_expr(not self.has_cu_seqlens_k):
95
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mK) - 1 - dim)
96
+ return mK[idx]
97
+ else:
98
+ idx = (self.offset_k,) + (0,) * (cute.rank(mK) - 1)
99
+ return cute.domain_offset(idx, mK)
build/torch-universal/softmax.py ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import math
4
+ import operator
5
+ from typing import Tuple
6
+ from dataclasses import dataclass
7
+
8
+ import cutlass
9
+ import cutlass.cute as cute
10
+ from cutlass import Float32
11
+
12
+ from . import utils
13
+ from .cute_dsl_utils import ParamsBase
14
+
15
+
16
+ @dataclass
17
+ class Softmax(ParamsBase):
18
+ scale_log2: Float32
19
+ num_rows: cutlass.Constexpr[int]
20
+ row_max: cute.Tensor
21
+ row_sum: cute.Tensor
22
+ arch: cutlass.Constexpr[int] = 80
23
+ softmax_scale: Float32 | None = None
24
+
25
+ @staticmethod
26
+ def create(
27
+ scale_log2: Float32,
28
+ num_rows: cutlass.Constexpr[int],
29
+ arch: cutlass.Constexpr[int] = 80,
30
+ softmax_scale: Float32 | None = None,
31
+ ):
32
+ row_max = cute.make_fragment(num_rows, Float32)
33
+ row_sum = cute.make_fragment(num_rows, Float32)
34
+ return Softmax(scale_log2, num_rows, row_max, row_sum, arch, softmax_scale)
35
+
36
+ def reset(self) -> None:
37
+ self.row_max.fill(-Float32.inf)
38
+ self.row_sum.fill(0.0)
39
+
40
+ def _compute_row_max(
41
+ self, acc_S_row: cute.TensorSSA, init_val: float | Float32 | None = None
42
+ ) -> Float32:
43
+ return utils.fmax_reduce(acc_S_row, init_val, arch=self.arch)
44
+
45
+ def _compute_row_sum(
46
+ self, acc_S_row_exp: cute.TensorSSA, init_val: float | Float32 | None = None
47
+ ) -> Float32:
48
+ return utils.fadd_reduce(acc_S_row_exp, init_val, arch=self.arch)
49
+
50
+ @cute.jit
51
+ def online_softmax(
52
+ self,
53
+ acc_S: cute.Tensor,
54
+ is_first: cutlass.Constexpr[bool] = False,
55
+ check_inf: cutlass.Constexpr[bool] = True,
56
+ ) -> cute.Tensor:
57
+ """Apply online softmax and return the row_scale to rescale O.
58
+
59
+ :param acc_S: acc_S tensor
60
+ :type acc_S: cute.Tensor
61
+ :param is_first: is first n_block
62
+ :type is_first: cutlass.Constexpr
63
+ """
64
+ # Change acc_S to M,N layout view.
65
+ acc_S_mn = utils.make_acc_tensor_mn_view(acc_S)
66
+ row_scale = cute.make_fragment_like(self.row_max, Float32)
67
+
68
+ row_max = self.row_max
69
+ row_sum = self.row_sum
70
+ scale_log2 = self.scale_log2
71
+ arch = self.arch
72
+
73
+ # Each iteration processes one row of acc_S
74
+ for r in cutlass.range(cute.size(row_max), unroll_full=True):
75
+ acc_S_row = acc_S_mn[r, None].load() # (n_block_size)
76
+
77
+ row_max_cur = utils.fmax_reduce(
78
+ acc_S_row,
79
+ init_val=row_max[r] if cutlass.const_expr(not is_first) else None,
80
+ arch=arch,
81
+ )
82
+
83
+ row_max_cur = utils.warp_reduce(row_max_cur, cute.arch.fmax, width=4)
84
+ if cutlass.const_expr(check_inf):
85
+ row_max_cur = 0.0 if row_max_cur == -Float32.inf else row_max_cur
86
+
87
+ if cutlass.const_expr(is_first):
88
+ row_max_cur_scaled = row_max_cur * scale_log2
89
+ acc_S_row_exp = utils.exp2f(acc_S_row * scale_log2 - row_max_cur_scaled)
90
+
91
+ acc_S_row_sum = utils.fadd_reduce(acc_S_row_exp, init_val=None, arch=arch)
92
+ row_scale[r] = 1.0
93
+ else:
94
+ row_max_prev = row_max[r]
95
+ row_max_cur_scaled = row_max_cur * scale_log2
96
+ acc_S_row_exp = utils.exp2f(acc_S_row * scale_log2 - row_max_cur_scaled)
97
+ # row_scale[r] = utils.exp2f(row_max_prev * self.scale_log2 - row_max_cur_scaled)
98
+ row_scale[r] = utils.exp2f((row_max_prev - row_max_cur) * scale_log2)
99
+
100
+ acc_S_row_sum = utils.fadd_reduce(
101
+ acc_S_row_exp, init_val=row_sum[r] * row_scale[r], arch=arch
102
+ )
103
+
104
+ row_max[r] = row_max_cur
105
+ row_sum[r] = acc_S_row_sum
106
+ acc_S_mn[r, None].store(acc_S_row_exp)
107
+
108
+ return row_scale
109
+
110
+ @cute.jit
111
+ def finalize(
112
+ self, final_scale: Float32 = 1.0, sink_val: Float32 | cute.Tensor | None = None
113
+ ) -> cute.Tensor:
114
+ """Finalize the online softmax by computing the scale and logsumexp."""
115
+ if cutlass.const_expr(sink_val is not None and isinstance(sink_val, cute.Tensor)):
116
+ assert cute.size(sink_val) == cute.size(self.row_sum)
117
+ row_sum = self.row_sum
118
+ row_max = self.row_max
119
+ scale_log2 = self.scale_log2
120
+
121
+ # quad reduction for row_sum as we didn't do it during each iteration of online softmax
122
+ row_sum.store(utils.warp_reduce(row_sum.load(), operator.add, width=4))
123
+ row_scale = cute.make_fragment_like(row_max, Float32)
124
+
125
+ for r in cutlass.range(cute.size(row_sum), unroll_full=True):
126
+ if cutlass.const_expr(sink_val is not None):
127
+ sink_val_cur = sink_val if not isinstance(sink_val, cute.Tensor) else sink_val[r]
128
+ LOG2_E = math.log2(math.e)
129
+ row_sum[r] += utils.exp2f(sink_val_cur * LOG2_E - row_max[r] * scale_log2)
130
+
131
+ # if row_sum is zero or nan, set acc_O_mn_row to 1.0
132
+ acc_O_mn_row_is_zero_or_nan = row_sum[r] == 0.0 or row_sum[r] != row_sum[r]
133
+ row_scale[r] = (
134
+ cute.arch.rcp_approx(row_sum[r] if not acc_O_mn_row_is_zero_or_nan else 1.0)
135
+ ) * final_scale
136
+ row_sum_cur = row_sum[r]
137
+ LN2 = math.log(2.0)
138
+ row_sum[r] = (
139
+ (row_max[r] * scale_log2 + utils.log2f(row_sum_cur)) * LN2
140
+ if not acc_O_mn_row_is_zero_or_nan
141
+ else -Float32.inf
142
+ )
143
+ return row_scale
144
+
145
+ @cute.jit
146
+ def rescale_O(self, acc_O: cute.Tensor, row_scale: cute.Tensor) -> None:
147
+ """Scale each row of acc_O by the given scale tensor.
148
+ :param acc_O: input tensor
149
+ :type acc_O: cute.Tensor
150
+ :param row_scale: row_scale tensor
151
+ :type row_scale: cute.Tensor
152
+ """
153
+ acc_O_mn = utils.make_acc_tensor_mn_view(acc_O)
154
+ assert cute.size(row_scale) == cute.size(acc_O_mn, mode=[0])
155
+ for r in cutlass.range(cute.size(row_scale), unroll_full=True):
156
+ acc_O_mn[r, None].store(acc_O_mn[r, None].load() * row_scale[r])
157
+
158
+
159
+ @dataclass
160
+ class SoftmaxSm100(Softmax):
161
+ rescale_threshold: cutlass.Constexpr[float] = 0.0
162
+
163
+ @staticmethod
164
+ def create(
165
+ scale_log2: Float32,
166
+ rescale_threshold: cutlass.Constexpr[float] = 0.0,
167
+ softmax_scale: Float32 | None = None,
168
+ ):
169
+ num_rows = 1
170
+ arch = 100
171
+ row_max = cute.make_fragment(num_rows, Float32)
172
+ row_sum = cute.make_fragment(num_rows, Float32)
173
+ return SoftmaxSm100(
174
+ scale_log2,
175
+ num_rows,
176
+ row_max,
177
+ row_sum,
178
+ arch,
179
+ softmax_scale,
180
+ rescale_threshold=rescale_threshold,
181
+ )
182
+
183
+ @cute.jit
184
+ def update_row_max(self, acc_S_row: cute.TensorSSA, is_first: int) -> Tuple[Float32, Float32]:
185
+ if cutlass.const_expr(is_first):
186
+ row_max_new = self._compute_row_max(acc_S_row)
187
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
188
+ acc_scale = 0.0
189
+ else:
190
+ row_max_old = self.row_max[0]
191
+ row_max_new = self._compute_row_max(acc_S_row, init_val=row_max_old)
192
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
193
+ acc_scale_ = (row_max_old - row_max_safe) * self.scale_log2
194
+ acc_scale = utils.exp2f(acc_scale_)
195
+ if cutlass.const_expr(self.rescale_threshold > 0.0):
196
+ if acc_scale_ >= -self.rescale_threshold:
197
+ row_max_new = row_max_old
198
+ row_max_safe = row_max_old
199
+ acc_scale = 1.0
200
+ self.row_max[0] = row_max_new
201
+ return row_max_safe, acc_scale
202
+
203
+ def update_row_sum(
204
+ self, acc_S_row_exp: cute.TensorSSA, row_scale: Float32, is_first: int = False
205
+ ) -> None:
206
+ init_val = self.row_sum[0] * row_scale if cutlass.const_expr(not is_first) else None
207
+ # self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=self.row_sum[0] * row_scale)
208
+ self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=init_val)
209
+ # tmp = self._compute_row_sum(acc_S_row_exp)
210
+ # self.row_sum[0] = self.row_sum[0] * row_scale + tmp
211
+
212
+ @cute.jit
213
+ def scale_subtract_rowmax(
214
+ self,
215
+ acc_S_row: cute.Tensor,
216
+ row_max: Float32,
217
+ ):
218
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
219
+ row_max_scaled = row_max * self.scale_log2
220
+ for i in cutlass.range(0, cute.size(acc_S_row.shape), 2, unroll_full=True):
221
+ acc_S_row[i], acc_S_row[i + 1] = utils.fma_packed_f32x2(
222
+ (acc_S_row[i], acc_S_row[i + 1]),
223
+ (self.scale_log2, self.scale_log2),
224
+ (-row_max_scaled, -row_max_scaled),
225
+ )
226
+
227
+ @cute.jit
228
+ def apply_exp2_convert(
229
+ self,
230
+ acc_S_row: cute.Tensor,
231
+ acc_S_row_converted: cute.Tensor,
232
+ e2e: cutlass.Constexpr[bool] = False,
233
+ e2e_freq: cutlass.Constexpr[int] = 16,
234
+ e2e_res: cutlass.Constexpr[int] = 4,
235
+ e2e_frg_limit: cutlass.Constexpr[int] = 1,
236
+ ):
237
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
238
+ frg_tile = 32
239
+ assert frg_tile % 2 == 0
240
+ frg_cnt = cute.size(acc_S_row) // frg_tile
241
+ assert cute.size(acc_S_row) % frg_tile == 0
242
+ acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
243
+ acc_S_row_converted_frg = cute.logical_divide(
244
+ acc_S_row_converted, cute.make_layout(frg_tile)
245
+ )
246
+ for j in cutlass.range_constexpr(frg_cnt):
247
+ for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
248
+ # acc_S_row_frg[k, j] = utils.exp2f(acc_S_row_frg[k, j])
249
+ # acc_S_row_frg[k + 1, j] = utils.exp2f(acc_S_row_frg[k + 1, j])
250
+ if cutlass.const_expr(not e2e):
251
+ acc_S_row_frg[k, j] = cute.arch.exp2(acc_S_row_frg[k, j])
252
+ acc_S_row_frg[k + 1, j] = cute.arch.exp2(acc_S_row_frg[k + 1, j])
253
+ else:
254
+ if cutlass.const_expr(
255
+ k % e2e_freq < e2e_freq - e2e_res or j >= frg_cnt - e2e_frg_limit
256
+ ):
257
+ acc_S_row_frg[k, j] = cute.arch.exp2(acc_S_row_frg[k, j])
258
+ acc_S_row_frg[k + 1, j] = cute.arch.exp2(acc_S_row_frg[k + 1, j])
259
+ else:
260
+ # acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = utils.e2e_asm2(acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j])
261
+ acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = utils.ex2_emulation_2(
262
+ acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]
263
+ )
264
+ acc_S_row_converted_frg[None, j].store(
265
+ acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
266
+ )
267
+
268
+ @cute.jit
269
+ def scale_apply_exp2_convert(
270
+ self,
271
+ acc_S_row: cute.Tensor,
272
+ row_max: Float32,
273
+ acc_S_row_converted: cute.Tensor,
274
+ ):
275
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
276
+ minus_row_max_scaled = -row_max * self.scale_log2
277
+ for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
278
+ acc_S_row[i], acc_S_row[i + 1] = utils.fma_packed_f32x2(
279
+ (acc_S_row[i], acc_S_row[i + 1]),
280
+ (self.scale_log2, self.scale_log2),
281
+ (minus_row_max_scaled, minus_row_max_scaled),
282
+ )
283
+
284
+ # for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
285
+ # acc_S_row[i], acc_S_row[i + 1] = utils.fma_packed_f32x2(
286
+ # (acc_S_row[i], acc_S_row[i + 1]),
287
+ # (self.scale_log2, self.scale_log2),
288
+ # (minus_row_max_scaled, minus_row_max_scaled),
289
+ # )
290
+ # acc_S_row[i] = cute.arch.exp2(acc_S_row[i])
291
+ # acc_S_row[i + 1] = cute.arch.exp2(acc_S_row[i + 1])
292
+
293
+ frg_tile = 32
294
+ assert frg_tile % 2 == 0
295
+ frg_cnt = cute.size(acc_S_row) // frg_tile
296
+ assert cute.size(acc_S_row) % frg_tile == 0
297
+ acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
298
+ acc_S_row_converted_frg = cute.logical_divide(
299
+ acc_S_row_converted, cute.make_layout(frg_tile)
300
+ )
301
+ for j in cutlass.range_constexpr(frg_cnt):
302
+ for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
303
+ # acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = (
304
+ # utils.fma_packed_f32x2(
305
+ # (acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]),
306
+ # (self.scale_log2, self.scale_log2),
307
+ # (minus_row_max_scaled, minus_row_max_scaled),
308
+ # )
309
+ # )
310
+ # acc_S_row_frg[k, j] = utils.exp2f(acc_S_row_frg[k, j])
311
+ # acc_S_row_frg[k + 1, j] = utils.exp2f(acc_S_row_frg[k + 1, j])
312
+ acc_S_row_frg[k, j] = cute.arch.exp2(acc_S_row_frg[k, j])
313
+ acc_S_row_frg[k + 1, j] = cute.arch.exp2(acc_S_row_frg[k + 1, j])
314
+ acc_S_row_converted_frg[None, j].store(
315
+ acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
316
+ )
317
+
318
+
319
+ @cute.jit
320
+ def floor_if_packed(
321
+ q_idx,
322
+ qhead_per_kvhead: cutlass.Constexpr[int],
323
+ ) -> cute.Tensor:
324
+ """Convert q_idx to packed format for Pack-GQA."""
325
+ if cutlass.const_expr(qhead_per_kvhead == 1):
326
+ return q_idx
327
+ return q_idx // qhead_per_kvhead
328
+
329
+
330
+ @cute.jit
331
+ def apply_score_mod_inner(
332
+ score_tensor,
333
+ index_tensor,
334
+ score_mod: cutlass.Constexpr,
335
+ batch_idx,
336
+ head_idx,
337
+ softmax_scale,
338
+ vec_size: cutlass.Constexpr,
339
+ qk_acc_dtype: cutlass.Constexpr,
340
+ aux_tensors,
341
+ fastdiv_mods,
342
+ constant_q_idx: cutlass.Constexpr,
343
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
344
+ ):
345
+ """Shared implementation for applying score modification.
346
+
347
+ Args:
348
+ score_tensor: The scores to modify (acc_S for flash_fwd, tSrS_t2r for sm100)
349
+ index_tensor: Index positions (tScS for flash_fwd, tScS_t2r for sm100)
350
+ score_mod: The score modification function to apply
351
+ batch_idx: Batch index
352
+ head_idx: Head index
353
+ softmax_scale: Scale to apply
354
+ vec_size: Vector size for processing elements
355
+ qk_acc_dtype: Data type for accumulator
356
+ aux_tensors: Optional aux_tensors for FlexAttention
357
+ fastdiv_mods: Tuple of (seqlen_q_divmod, seqlen_k_divmod) for wrapping
358
+ constant_q_idx: If provided, use this constant for all q_idx values
359
+ If None, compute q_idx per-element
360
+ qhead_per_kvhead_packgqa: Pack-GQA replication factor. Divide q_idx by this
361
+ when greater than 1 so score mods see logical heads.
362
+ """
363
+ n_vals = cutlass.const_expr(cute.size(score_tensor.shape))
364
+ score_vec = cute.make_fragment(vec_size, qk_acc_dtype)
365
+ kv_idx_vec = cute.make_fragment(vec_size, cutlass.Int32)
366
+
367
+ # SSA values for batch (constant across all elements)
368
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32).broadcast_to((vec_size,))
369
+
370
+ # Handle q_idx based on whether it's constant
371
+ q_idx_vec = cute.make_fragment(vec_size, cutlass.Int32)
372
+
373
+ # For Pack-GQA with non-constant q_idx, we need per-element head indices
374
+ # since a thread my process multiple query head indices
375
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
376
+ head_idx_vec = cute.make_fragment(vec_size, cutlass.Int32)
377
+
378
+ for i in cutlass.range(0, n_vals, vec_size, unroll_full=True):
379
+ for j in cutlass.range(vec_size, unroll_full=True):
380
+ score_vec[j] = score_tensor[i + j] * softmax_scale
381
+
382
+ # Extract head offset from packed q_idx for Pack-GQA
383
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
384
+ q_idx_packed = index_tensor[i + j][0]
385
+ # Building up the logical q_head idx: final_q_head = kv_head * qhead_per_kvhead + (q_physical % qhead_per_kvhead)
386
+ q_idx_logical = q_idx_packed // qhead_per_kvhead
387
+ head_offset = q_idx_packed - q_idx_logical * qhead_per_kvhead
388
+ head_idx_vec[j] = head_idx * qhead_per_kvhead + head_offset
389
+
390
+ # If we will do loads we mod, in order to not read OOB
391
+ if cutlass.const_expr(aux_tensors is not None and fastdiv_mods is not None):
392
+ if cutlass.const_expr(constant_q_idx is None):
393
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
394
+ q_idx_floored = floor_if_packed(index_tensor[i + j][0], qhead_per_kvhead)
395
+ _, q_idx_wrapped = seqlen_q_divmod.divmod(q_idx_floored)
396
+ q_idx_vec[j] = q_idx_wrapped
397
+ else:
398
+ _, seqlen_k_divmod = fastdiv_mods
399
+
400
+ _, kv_idx_wrapped = seqlen_k_divmod.divmod(index_tensor[i + j][1])
401
+ kv_idx_vec[j] = kv_idx_wrapped
402
+ else:
403
+ # No bounds checking - direct indexing
404
+ if constant_q_idx is None:
405
+ q_idx_vec[j] = floor_if_packed(index_tensor[i + j][0], qhead_per_kvhead)
406
+ kv_idx_vec[j] = index_tensor[i + j][1]
407
+
408
+ # Convert to SSA for score_mod call
409
+ score_ssa = score_vec.load()
410
+ kv_idx_ssa = kv_idx_vec.load()
411
+ if cutlass.const_expr(constant_q_idx is None):
412
+ q_idx_ssa = q_idx_vec.load()
413
+ else:
414
+ # NB we do not apply Pack-GQA division here, as constant_q_idx is assumed to already be logical
415
+ q_idx_const = constant_q_idx
416
+ q_idx_ssa = utils.scalar_to_ssa(q_idx_const, cutlass.Int32).broadcast_to((vec_size,))
417
+
418
+ # Compute head_idx_ssa: per-element for Pack-GQA with non-constant q_idx, constant otherwise
419
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
420
+ head_idx_ssa = head_idx_vec.load()
421
+ else:
422
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32).broadcast_to((vec_size,))
423
+
424
+ aux_args = []
425
+ if cutlass.const_expr(aux_tensors is not None):
426
+ aux_args = aux_tensors
427
+
428
+ post_mod_scores = score_mod(
429
+ score_ssa,
430
+ batch_idx_ssa,
431
+ head_idx_ssa,
432
+ q_idx=q_idx_ssa,
433
+ kv_idx=kv_idx_ssa,
434
+ aux_tensors=aux_args,
435
+ )
436
+
437
+ # Write back modified scores
438
+ score_vec.store(post_mod_scores)
439
+ for j in cutlass.range(vec_size, unroll_full=True):
440
+ score_tensor[i + j] = score_vec[j]
build/torch-universal/testing.py ADDED
@@ -0,0 +1,404 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Optional
3
+
4
+ import torch
5
+ import torch.nn.functional as F
6
+ from einops import rearrange, repeat
7
+
8
+
9
+ class IndexFirstAxis(torch.autograd.Function):
10
+ @staticmethod
11
+ def forward(ctx, input, indices):
12
+ ctx.save_for_backward(indices)
13
+ assert input.ndim >= 2
14
+ ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
15
+ second_dim = other_shape.numel()
16
+ return torch.gather(
17
+ rearrange(input, "b ... -> b (...)"),
18
+ 0,
19
+ repeat(indices, "z -> z d", d=second_dim),
20
+ ).reshape(-1, *other_shape)
21
+
22
+ @staticmethod
23
+ def backward(ctx, grad_output):
24
+ (indices,) = ctx.saved_tensors
25
+ assert grad_output.ndim >= 2
26
+ other_shape = grad_output.shape[1:]
27
+ grad_output = rearrange(grad_output, "b ... -> b (...)")
28
+ grad_input = torch.zeros(
29
+ [ctx.first_axis_dim, grad_output.shape[1]],
30
+ device=grad_output.device,
31
+ dtype=grad_output.dtype,
32
+ )
33
+ grad_input.scatter_(0, repeat(indices, "z -> z d", d=grad_output.shape[1]), grad_output)
34
+ return grad_input.reshape(ctx.first_axis_dim, *other_shape), None
35
+
36
+
37
+ index_first_axis = IndexFirstAxis.apply
38
+
39
+
40
+ class IndexPutFirstAxis(torch.autograd.Function):
41
+ @staticmethod
42
+ def forward(ctx, values, indices, first_axis_dim):
43
+ ctx.save_for_backward(indices)
44
+ assert indices.ndim == 1
45
+ assert values.ndim >= 2
46
+ output = torch.zeros(
47
+ first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
48
+ )
49
+ output[indices] = values
50
+ return output
51
+
52
+ @staticmethod
53
+ def backward(ctx, grad_output):
54
+ (indices,) = ctx.saved_tensors
55
+ grad_values = grad_output[indices]
56
+ return grad_values, None, None
57
+
58
+
59
+ index_put_first_axis = IndexPutFirstAxis.apply
60
+
61
+
62
+ def unpad_input(hidden_states, attention_mask, unused_mask=None):
63
+ all_masks = (attention_mask + unused_mask) if unused_mask is not None else attention_mask
64
+ seqlens_in_batch = all_masks.sum(dim=-1, dtype=torch.int32)
65
+ used_seqlens_in_batch = attention_mask.sum(dim=-1, dtype=torch.int32)
66
+ indices = torch.nonzero(all_masks.flatten(), as_tuple=False).flatten()
67
+ max_seqlen_in_batch = seqlens_in_batch.max().item()
68
+ cu_seqlens = F.pad(torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0))
69
+ return (
70
+ index_first_axis(rearrange(hidden_states, "b s ... -> (b s) ..."), indices),
71
+ indices,
72
+ cu_seqlens,
73
+ max_seqlen_in_batch,
74
+ used_seqlens_in_batch,
75
+ )
76
+
77
+
78
+ def pad_input(hidden_states, indices, batch, seqlen):
79
+ output = index_put_first_axis(hidden_states, indices, batch * seqlen)
80
+ return rearrange(output, "(b s) ... -> b s ...", b=batch)
81
+
82
+
83
+ def generate_random_padding_mask(max_seqlen, batch_size, device, mode="random", zero_lengths=False):
84
+ assert mode in ["full", "random", "third"]
85
+ if mode == "full":
86
+ lengths = torch.full((batch_size, 1), max_seqlen, device=device, dtype=torch.int32)
87
+ elif mode == "random":
88
+ lengths = torch.randint(
89
+ max(0 if zero_lengths else 1, max_seqlen - 20),
90
+ max_seqlen + 1,
91
+ (batch_size, 1),
92
+ device=device,
93
+ )
94
+ else:
95
+ lengths = torch.randint(max_seqlen // 3, max_seqlen + 1, (batch_size, 1), device=device)
96
+
97
+ if zero_lengths:
98
+ for i in range(batch_size):
99
+ if i % 5 == 0:
100
+ lengths[i] = 0
101
+ lengths[-1] = 0
102
+ padding_mask = repeat(torch.arange(max_seqlen, device=device), "s -> b s", b=batch_size) < lengths
103
+ return padding_mask
104
+
105
+
106
+ def generate_qkv(
107
+ q,
108
+ k,
109
+ v,
110
+ query_padding_mask=None,
111
+ key_padding_mask=None,
112
+ qv=None,
113
+ kvpacked=False,
114
+ qkvpacked=False,
115
+ query_unused_mask=None,
116
+ key_unused_mask=None,
117
+ ):
118
+ assert not (kvpacked and qkvpacked)
119
+ batch_size, seqlen_q, nheads, d = q.shape
120
+ d_v = v.shape[-1]
121
+ _, seqlen_k, nheads_k, _ = k.shape
122
+ assert k.shape == (batch_size, seqlen_k, nheads_k, d)
123
+ assert v.shape == (batch_size, seqlen_k, nheads_k, d_v)
124
+ if query_unused_mask is not None or key_unused_mask is not None:
125
+ assert not kvpacked
126
+ assert not qkvpacked
127
+
128
+ if query_padding_mask is not None:
129
+ q_unpad, indices_q, cu_seqlens_q, max_seqlen_q, seqused_q = unpad_input(
130
+ q, query_padding_mask, query_unused_mask
131
+ )
132
+ output_pad_fn = lambda output_unpad: pad_input(output_unpad, indices_q, batch_size, seqlen_q)
133
+ qv_unpad = rearrange(qv, "b s ... -> (b s) ...")[indices_q] if qv is not None else None
134
+ else:
135
+ q_unpad = rearrange(q, "b s h d -> (b s) h d")
136
+ cu_seqlens_q = torch.arange(
137
+ 0, (batch_size + 1) * seqlen_q, step=seqlen_q, dtype=torch.int32, device=q_unpad.device
138
+ )
139
+ seqused_q = None
140
+ max_seqlen_q = seqlen_q
141
+ output_pad_fn = lambda output_unpad: rearrange(output_unpad, "(b s) h d -> b s h d", b=batch_size)
142
+ qv_unpad = rearrange(qv, "b s ... -> (b s) ...") if qv is not None else None
143
+
144
+ if key_padding_mask is not None:
145
+ k_unpad, indices_k, cu_seqlens_k, max_seqlen_k, seqused_k = unpad_input(
146
+ k, key_padding_mask, key_unused_mask
147
+ )
148
+ v_unpad, *_ = unpad_input(v, key_padding_mask, key_unused_mask)
149
+ else:
150
+ k_unpad = rearrange(k, "b s h d -> (b s) h d")
151
+ v_unpad = rearrange(v, "b s h d -> (b s) h d")
152
+ cu_seqlens_k = torch.arange(
153
+ 0, (batch_size + 1) * seqlen_k, step=seqlen_k, dtype=torch.int32, device=k_unpad.device
154
+ )
155
+ seqused_k = None
156
+ max_seqlen_k = seqlen_k
157
+
158
+ if qkvpacked:
159
+ assert (query_padding_mask == key_padding_mask).all()
160
+ assert nheads == nheads_k
161
+ qkv_unpad = torch.stack([q_unpad, k_unpad, v_unpad], dim=1)
162
+ qkv = torch.stack([q, k, v], dim=2)
163
+ if query_padding_mask is not None:
164
+ dqkv_pad_fn = lambda dqkv_unpad: pad_input(dqkv_unpad, indices_q, batch_size, seqlen_q)
165
+ else:
166
+ dqkv_pad_fn = lambda dqkv_unpad: rearrange(
167
+ dqkv_unpad, "(b s) t h d -> b s t h d", b=batch_size
168
+ )
169
+ return (
170
+ qkv_unpad.detach().requires_grad_(),
171
+ cu_seqlens_q,
172
+ max_seqlen_q,
173
+ qkv.detach().requires_grad_(),
174
+ output_pad_fn,
175
+ dqkv_pad_fn,
176
+ )
177
+ elif kvpacked:
178
+ kv_unpad = torch.stack([k_unpad, v_unpad], dim=1)
179
+ kv = torch.stack([k, v], dim=2)
180
+ dq_pad_fn = output_pad_fn
181
+ if key_padding_mask is not None:
182
+ dkv_pad_fn = lambda dkv_unpad: pad_input(dkv_unpad, indices_k, batch_size, seqlen_k)
183
+ else:
184
+ dkv_pad_fn = lambda dkv_unpad: rearrange(
185
+ dkv_unpad, "(b s) t h d -> b s t h d", b=batch_size
186
+ )
187
+ return (
188
+ q_unpad.detach().requires_grad_(),
189
+ kv_unpad.detach().requires_grad_(),
190
+ cu_seqlens_q,
191
+ cu_seqlens_k,
192
+ max_seqlen_q,
193
+ max_seqlen_k,
194
+ q.detach().requires_grad_(),
195
+ kv.detach().requires_grad_(),
196
+ output_pad_fn,
197
+ dq_pad_fn,
198
+ dkv_pad_fn,
199
+ )
200
+ else:
201
+ dq_pad_fn = output_pad_fn
202
+ if key_padding_mask is not None:
203
+ dk_pad_fn = lambda dk_unpad: pad_input(dk_unpad, indices_k, batch_size, seqlen_k)
204
+ else:
205
+ dk_pad_fn = lambda dk_unpad: rearrange(dk_unpad, "(b s) h d -> b s h d", b=batch_size)
206
+ return (
207
+ q_unpad.detach().requires_grad_(),
208
+ k_unpad.detach().requires_grad_(),
209
+ v_unpad.detach().requires_grad_(),
210
+ qv_unpad.detach() if qv is not None else None,
211
+ cu_seqlens_q,
212
+ cu_seqlens_k,
213
+ seqused_q,
214
+ seqused_k,
215
+ max_seqlen_q,
216
+ max_seqlen_k,
217
+ q.detach().requires_grad_(),
218
+ k.detach().requires_grad_(),
219
+ v.detach().requires_grad_(),
220
+ qv.detach() if qv is not None else None,
221
+ output_pad_fn,
222
+ dq_pad_fn,
223
+ dk_pad_fn,
224
+ )
225
+
226
+
227
+ def construct_local_mask(
228
+ seqlen_q,
229
+ seqlen_k,
230
+ window_size=(None, None),
231
+ sink_token_length=0,
232
+ query_padding_mask=None,
233
+ key_padding_mask=None,
234
+ key_leftpad=None,
235
+ device=None,
236
+ ):
237
+ row_idx = rearrange(torch.arange(seqlen_q, device=device, dtype=torch.long), "s -> s 1")
238
+ col_idx = torch.arange(seqlen_k, device=device, dtype=torch.long)
239
+ if key_leftpad is not None:
240
+ key_leftpad = rearrange(key_leftpad, "b -> b 1 1 1")
241
+ col_idx = repeat(col_idx, "s -> b 1 1 s", b=key_leftpad.shape[0])
242
+ col_idx = torch.where(col_idx >= key_leftpad, col_idx - key_leftpad, 2**32)
243
+ sk = (
244
+ seqlen_k
245
+ if key_padding_mask is None
246
+ else rearrange(key_padding_mask.sum(-1), "b -> b 1 1 1")
247
+ )
248
+ sq = (
249
+ seqlen_q
250
+ if query_padding_mask is None
251
+ else rearrange(query_padding_mask.sum(-1), "b -> b 1 1 1")
252
+ )
253
+ if window_size[0] is None:
254
+ return col_idx > row_idx + sk - sq + window_size[1]
255
+ else:
256
+ sk = torch.full_like(col_idx, seqlen_k) if key_padding_mask is None else sk
257
+ return torch.logical_or(
258
+ col_idx > torch.minimum(row_idx + sk - sq + window_size[1], sk),
259
+ torch.logical_and(col_idx < row_idx + sk - sq - window_size[0], col_idx >= sink_token_length),
260
+ )
261
+
262
+
263
+ def construct_chunk_mask(
264
+ seqlen_q,
265
+ seqlen_k,
266
+ attention_chunk,
267
+ query_padding_mask=None,
268
+ key_padding_mask=None,
269
+ key_leftpad=None,
270
+ device=None,
271
+ ):
272
+ row_idx = rearrange(torch.arange(seqlen_q, device=device, dtype=torch.long), "s -> s 1")
273
+ col_idx = torch.arange(seqlen_k, device=device, dtype=torch.long)
274
+ if key_leftpad is not None:
275
+ key_leftpad = rearrange(key_leftpad, "b -> b 1 1 1")
276
+ col_idx = repeat(col_idx, "s -> b 1 1 s", b=key_leftpad.shape[0])
277
+ col_idx = torch.where(col_idx >= key_leftpad, col_idx - key_leftpad, 2**32)
278
+ sk = (
279
+ seqlen_k
280
+ if key_padding_mask is None
281
+ else rearrange(key_padding_mask.sum(-1), "b -> b 1 1 1")
282
+ )
283
+ sq = (
284
+ seqlen_q
285
+ if query_padding_mask is None
286
+ else rearrange(query_padding_mask.sum(-1), "b -> b 1 1 1")
287
+ )
288
+ sk = torch.full_like(col_idx, seqlen_k) if key_padding_mask is None else sk
289
+ col_limit_left_chunk = row_idx + sk - sq - (row_idx + sk - sq) % attention_chunk
290
+ return torch.logical_or(
291
+ col_idx < col_limit_left_chunk, col_idx >= col_limit_left_chunk + attention_chunk
292
+ )
293
+
294
+
295
+ def attention_ref(
296
+ q,
297
+ k,
298
+ v,
299
+ query_padding_mask=None,
300
+ key_padding_mask=None,
301
+ key_leftpad=None,
302
+ attn_bias=None,
303
+ dropout_p=0.0,
304
+ dropout_mask=None,
305
+ causal=False,
306
+ qv=None,
307
+ q_descale=None,
308
+ k_descale=None,
309
+ v_descale=None,
310
+ window_size=(None, None),
311
+ attention_chunk=0,
312
+ sink_token_length=0,
313
+ learnable_sink: Optional[torch.Tensor] = None,
314
+ softcap=0.0,
315
+ upcast=True,
316
+ reorder_ops=False,
317
+ intermediate_dtype=None,
318
+ ):
319
+ if causal:
320
+ window_size = (window_size[0], 0)
321
+ dtype_og = q.dtype
322
+ if upcast:
323
+ q, k, v = q.float(), k.float(), v.float()
324
+ qv = qv.float() if qv is not None else None
325
+ if q_descale is not None:
326
+ q_descale = repeat(q_descale, "b h -> b 1 (h g) 1", g=q.shape[2] // k.shape[2])
327
+ q = (q.float() * q_descale).to(q.dtype)
328
+ qv = (qv.float() * q_descale).to(qv.dtype) if qv is not None else None
329
+ if k_descale is not None:
330
+ k = (k.float() * rearrange(k_descale, "b h -> b 1 h 1")).to(dtype=k.dtype)
331
+ if v_descale is not None:
332
+ v = (v.float() * rearrange(v_descale, "b h -> b 1 h 1")).to(dtype=v.dtype)
333
+ seqlen_q, seqlen_k = q.shape[1], k.shape[1]
334
+ k = repeat(k, "b s h d -> b s (h g) d", g=q.shape[2] // k.shape[2])
335
+ v = repeat(v, "b s h d -> b s (h g) d", g=q.shape[2] // v.shape[2])
336
+ d = q.shape[-1]
337
+ dv = v.shape[-1]
338
+ softmax_scale = 1.0 / math.sqrt(d if qv is None else d + dv)
339
+ if not reorder_ops:
340
+ scores = torch.einsum("bthd,bshd->bhts", q * softmax_scale, k)
341
+ else:
342
+ scores = torch.einsum("bthd,bshd->bhts", q, k * softmax_scale)
343
+ if qv is not None:
344
+ scores = scores + torch.einsum("bthd,bshd->bhts", qv * softmax_scale, v)
345
+ if softcap > 0:
346
+ scores = torch.tanh(scores / softcap) * softcap
347
+ if key_padding_mask is not None:
348
+ scores.masked_fill_(rearrange(~key_padding_mask, "b s -> b 1 1 s"), float("-inf"))
349
+ local_mask = None
350
+ if window_size[0] is not None or window_size[1] is not None:
351
+ local_mask = construct_local_mask(
352
+ seqlen_q,
353
+ seqlen_k,
354
+ window_size,
355
+ sink_token_length,
356
+ query_padding_mask,
357
+ key_padding_mask,
358
+ key_leftpad=key_leftpad,
359
+ device=q.device,
360
+ )
361
+ if attention_chunk > 0:
362
+ chunk_mask = construct_chunk_mask(
363
+ seqlen_q,
364
+ seqlen_k,
365
+ attention_chunk,
366
+ query_padding_mask,
367
+ key_padding_mask,
368
+ key_leftpad=key_leftpad,
369
+ device=q.device,
370
+ )
371
+ local_mask = torch.logical_or(local_mask, chunk_mask) if local_mask is not None else chunk_mask
372
+ if local_mask is not None:
373
+ scores.masked_fill_(local_mask, float("-inf"))
374
+ if attn_bias is not None:
375
+ scores = scores + attn_bias
376
+ if learnable_sink is None:
377
+ attention = torch.softmax(scores, dim=-1).to(v.dtype)
378
+ else:
379
+ scores_fp32 = scores.to(torch.float32)
380
+ logits_max = torch.amax(scores_fp32, dim=-1, keepdim=True)
381
+ learnable_sink = rearrange(learnable_sink, "h -> h 1 1")
382
+ logits_or_sinks_max = torch.maximum(learnable_sink, logits_max)
383
+ unnormalized_scores = torch.exp(scores_fp32 - logits_or_sinks_max)
384
+ normalizer = unnormalized_scores.sum(dim=-1, keepdim=True) + torch.exp(
385
+ learnable_sink - logits_or_sinks_max
386
+ )
387
+ attention = (unnormalized_scores / normalizer).to(v.dtype)
388
+ if query_padding_mask is not None:
389
+ attention = attention.masked_fill(rearrange(~query_padding_mask, "b s -> b 1 s 1"), 0.0)
390
+ if key_padding_mask is not None:
391
+ attention = attention.masked_fill(rearrange(~key_padding_mask, "b s -> b 1 1 s"), 0.0)
392
+ if local_mask is not None:
393
+ attention = attention.masked_fill(torch.all(local_mask, dim=-1, keepdim=True), 0.0)
394
+ dropout_scaling = 1.0 / (1 - dropout_p)
395
+ if dropout_mask is not None:
396
+ attention_drop = attention.masked_fill(~dropout_mask, 0.0)
397
+ else:
398
+ attention_drop = attention
399
+ if intermediate_dtype is not None:
400
+ attention_drop = attention_drop.to(intermediate_dtype).to(attention_drop.dtype)
401
+ output = torch.einsum("bhts,bshd->bthd", attention_drop, v * dropout_scaling)
402
+ if query_padding_mask is not None:
403
+ output.masked_fill_(rearrange(~query_padding_mask, "b s -> b s 1 1"), 0.0)
404
+ return output.to(dtype=dtype_og), attention.to(dtype=dtype_og)
build/torch-universal/tile_scheduler.py ADDED
@@ -0,0 +1,695 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Optional, Tuple
4
+ from dataclasses import dataclass, fields
5
+
6
+ try:
7
+ from typing import override
8
+ except ImportError: # Python < 3.12
9
+ from typing_extensions import override
10
+
11
+ import cutlass
12
+ from cutlass._mlir import ir
13
+ import cutlass.cute as cute
14
+ from cutlass import Int32, const_expr
15
+
16
+ from . import utils
17
+ from .fast_math import FastDivmod, clz
18
+
19
+
20
+ class WorkTileInfo(cutlass.utils.WorkTileInfo):
21
+ """Altered WorkTileInfo which includes four axes: (block, head, batch, split)"""
22
+
23
+ @override
24
+ def __new_from_mlir_values__(self, values: list[ir.Value]) -> "WorkTileInfo":
25
+ assert len(values) == 5
26
+ new_tile_idx = cutlass.new_from_mlir_values(self._tile_idx, values[:-1])
27
+ new_is_valid_tile = cutlass.new_from_mlir_values(self._is_valid_tile, [values[-1]])
28
+ return WorkTileInfo(new_tile_idx, new_is_valid_tile)
29
+
30
+
31
+ @dataclass
32
+ class ParamsBase:
33
+ def __extract_mlir_values__(self):
34
+ all_fields = [getattr(self, field.name) for field in fields(self)]
35
+ non_constexpr_fields = [f for f in all_fields if not isinstance(f, cutlass.Constexpr)]
36
+ values, self._values_pos = [], []
37
+ for obj in non_constexpr_fields:
38
+ obj_values = cutlass.extract_mlir_values(obj)
39
+ values += obj_values
40
+ self._values_pos.append(len(obj_values))
41
+ return values
42
+
43
+ def __new_from_mlir_values__(self, values):
44
+ all_fields = {field.name: getattr(self, field.name) for field in fields(self)}
45
+ constexpr_fields = {n: f for n, f in all_fields.items() if isinstance(f, cutlass.Constexpr)}
46
+ non_constexpr_fields = {
47
+ n: f for n, f in all_fields.items() if not isinstance(f, cutlass.Constexpr)
48
+ }
49
+ for (name, field), n_items in zip(non_constexpr_fields.items(), self._values_pos):
50
+ non_constexpr_fields[name] = cutlass.new_from_mlir_values(field, values[:n_items])
51
+ values = values[n_items:]
52
+ return self.__class__(**non_constexpr_fields, **constexpr_fields)
53
+
54
+
55
+ @dataclass
56
+ class TileSchedulerArguments(ParamsBase):
57
+ num_block: Int32
58
+ num_head: Int32
59
+ num_batch: Int32
60
+ num_splits: Int32
61
+ seqlen_k: Int32
62
+ headdim: Int32
63
+ headdim_v: Int32
64
+ total_q: Int32
65
+ tile_shape_mn: cutlass.Constexpr[Tuple[int, int]]
66
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
67
+ mCuSeqlensQ: Optional[cute.Tensor] = None
68
+ mSeqUsedQ: Optional[cute.Tensor] = None
69
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
70
+ element_size: cutlass.Constexpr[int] = 2
71
+ is_persistent: cutlass.Constexpr[bool] = False
72
+ lpt: cutlass.Constexpr[bool] = False
73
+ is_split_kv: cutlass.Constexpr[bool] = False
74
+
75
+
76
+ class SingleTileScheduler:
77
+ @dataclass
78
+ class Params(ParamsBase):
79
+ num_block: Int32
80
+ num_head: Int32
81
+ num_batch: Int32
82
+ num_splits: Int32
83
+ num_splits_divmod: FastDivmod
84
+ is_split_kv: cutlass.Constexpr[bool] = False
85
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
86
+
87
+ @staticmethod
88
+ def create(
89
+ args: TileSchedulerArguments, *, loc=None, ip=None
90
+ ) -> "SingleTileScheduler.Params":
91
+ return SingleTileScheduler.Params(
92
+ args.num_block,
93
+ args.num_head,
94
+ args.num_batch,
95
+ args.num_splits,
96
+ FastDivmod.create(args.num_splits),
97
+ args.is_split_kv,
98
+ args.cluster_shape_mn,
99
+ )
100
+
101
+ def __init__(self, params: Params, blk_coord: cute.Coord, *, loc=None, ip=None):
102
+ self.params = params
103
+ self._blk_coord = blk_coord
104
+ self._is_first_block = True
105
+ self._loc = loc
106
+ self._ip = ip
107
+
108
+ @staticmethod
109
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
110
+ return SingleTileScheduler.Params.create(args, loc=loc, ip=ip)
111
+
112
+ @staticmethod
113
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileScheduler":
114
+ blk_coord = cute.arch.block_idx()
115
+ return SingleTileScheduler(params, blk_coord, loc=loc, ip=ip)
116
+
117
+ # called by host
118
+ @staticmethod
119
+ def get_grid_shape(
120
+ params: Params,
121
+ *,
122
+ loc=None,
123
+ ip=None,
124
+ ) -> Tuple[Int32, Int32, Int32]:
125
+ # TODO: this hard-codes the fact that we only use cluster = (1, 1) or (2, 1)
126
+ assert params.cluster_shape_mn[1] == 1, "Only cluster_shape_mn[1] == 1 is supported"
127
+ return (
128
+ cute.round_up(params.num_block, params.cluster_shape_mn[0]),
129
+ params.num_head * params.num_splits,
130
+ params.num_batch,
131
+ )
132
+
133
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
134
+ block_idx, head_idx, batch_idx = self._blk_coord
135
+ if const_expr(self.params.is_split_kv):
136
+ head_idx, split_idx = self.params.num_splits_divmod.divmod(head_idx)
137
+ else:
138
+ split_idx = Int32(0)
139
+ return WorkTileInfo(
140
+ (block_idx, head_idx, batch_idx, split_idx),
141
+ self._is_first_block,
142
+ )
143
+
144
+ def initial_work_tile_info(self, *, loc=None, ip=None):
145
+ return self.get_current_work(loc=loc, ip=ip)
146
+
147
+ def prefetch_next_work(self, *, loc=None, ip=None):
148
+ pass
149
+
150
+ def advance_to_next_work(self, *, loc=None, ip=None):
151
+ self._is_first_block = False
152
+
153
+ def __extract_mlir_values__(self):
154
+ values, self._values_pos = [], []
155
+ for obj in [self.params, self._blk_coord]:
156
+ obj_values = cutlass.extract_mlir_values(obj)
157
+ values += obj_values
158
+ self._values_pos.append(len(obj_values))
159
+ return values
160
+
161
+ def __new_from_mlir_values__(self, values):
162
+ obj_list = []
163
+ for obj, n_items in zip([self.params, self._blk_coord], self._values_pos):
164
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
165
+ values = values[n_items:]
166
+ return SingleTileScheduler(*(tuple(obj_list)), loc=self._loc)
167
+
168
+
169
+ class StaticPersistentTileScheduler:
170
+ @dataclass
171
+ class Params(ParamsBase):
172
+ num_block_divmod: FastDivmod
173
+ num_head_divmod: FastDivmod
174
+ total_blocks: Int32
175
+
176
+ @staticmethod
177
+ def create(
178
+ args: TileSchedulerArguments, *, loc=None, ip=None
179
+ ) -> "StaticPersistentTileScheduler.Params":
180
+ total_blocks = args.num_block * args.num_head * args.num_batch
181
+ return StaticPersistentTileScheduler.Params(
182
+ FastDivmod.create(args.num_block), FastDivmod.create(args.num_head), total_blocks
183
+ )
184
+
185
+ def __init__(self, params: Params, tile_idx: Int32, *, loc=None, ip=None):
186
+ self.params = params
187
+ self._tile_idx = tile_idx
188
+ self._loc = loc
189
+ self._ip = ip
190
+
191
+ @staticmethod
192
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
193
+ return StaticPersistentTileScheduler.Params.create(args, loc=loc, ip=ip)
194
+
195
+ @staticmethod
196
+ def create(params: Params, *, loc=None, ip=None) -> "StaticPersistentTileScheduler":
197
+ tile_idx = cute.arch.block_idx()[0]
198
+ return StaticPersistentTileScheduler(params, tile_idx, loc=loc, ip=ip)
199
+
200
+ # called by host
201
+ @staticmethod
202
+ def get_grid_shape(
203
+ params: Params,
204
+ *,
205
+ loc=None,
206
+ ip=None,
207
+ ) -> Tuple[Int32, Int32, Int32]:
208
+ hardware_info = cutlass.utils.HardwareInfo()
209
+ sm_count = hardware_info.get_device_multiprocessor_count()
210
+ return (cutlass.min(sm_count, params.total_blocks), Int32(1), Int32(1))
211
+
212
+ # @cute.jit
213
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
214
+ hn_idx, block_idx = self.params.num_block_divmod.divmod(self._tile_idx)
215
+ batch_idx, head_idx = self.params.num_head_divmod.divmod(hn_idx)
216
+ is_valid = self._tile_idx < self.params.total_blocks
217
+ # if cute.arch.thread_idx()[0] == 0:
218
+ # cute.printf("TileScheduler: tile_idx=%d, hn_idx=%d, block_idx=%d, batch_idx=%d, head_idx=%d, is_valid=%d", self._tile_idx, hn_idx, block_idx, batch_idx, head_idx, is_valid)
219
+ return WorkTileInfo(
220
+ (Int32(block_idx), Int32(head_idx), Int32(batch_idx), Int32(0)), is_valid
221
+ )
222
+
223
+ def initial_work_tile_info(self, *, loc=None, ip=None):
224
+ return self.get_current_work(loc=loc, ip=ip)
225
+
226
+ def prefetch_next_work(self, *, loc=None, ip=None):
227
+ pass
228
+
229
+ def advance_to_next_work(self, *, loc=None, ip=None):
230
+ self._tile_idx += cute.arch.grid_dim()[0]
231
+
232
+ def __extract_mlir_values__(self):
233
+ values, self._values_pos = [], []
234
+ for obj in [self.params, self._tile_idx]:
235
+ obj_values = cutlass.extract_mlir_values(obj)
236
+ values += obj_values
237
+ self._values_pos.append(len(obj_values))
238
+ return values
239
+
240
+ def __new_from_mlir_values__(self, values):
241
+ obj_list = []
242
+ for obj, n_items in zip(
243
+ [self.params, self._tile_idx],
244
+ self._values_pos,
245
+ ):
246
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
247
+ values = values[n_items:]
248
+ return StaticPersistentTileScheduler(*(tuple(obj_list)), loc=self._loc)
249
+
250
+
251
+ class SingleTileLPTScheduler:
252
+ @dataclass
253
+ class Params(ParamsBase):
254
+ total_blocks: Int32
255
+ num_splits: Int32
256
+ num_block_divmod: FastDivmod
257
+ num_head_divmod: FastDivmod
258
+ l2_minor_divmod: FastDivmod
259
+ l2_major_divmod: FastDivmod
260
+ l2_minor_residual_divmod: FastDivmod
261
+ num_hb_quotient: Int32
262
+ is_split_kv: cutlass.Constexpr[bool] = False
263
+
264
+ @staticmethod
265
+ @cute.jit
266
+ def create(
267
+ args: TileSchedulerArguments, *, loc=None, ip=None
268
+ ) -> "SingleTileLPTScheduler.Params":
269
+ # cute.printf(args.num_block, args.num_head, args.num_batch, args.seqlen_k, args.headdim, args.headdim_v, args.total_q, args.tile_shape_mn, args.qhead_per_kvhead_packgqa, args.element_size)
270
+ size_one_kv_head = args.seqlen_k * (args.headdim + args.headdim_v) * args.element_size
271
+ size_one_head = size_one_kv_head
272
+ size_l2 = 50 * 1024 * 1024 # 40 MB for K & V
273
+ # Swizzle is the size of each "section". Round swizzle to a power of 2
274
+ # Need to be careful about the case where only one head will fit
275
+ # swizzle is how many heads can fit in L2
276
+ # swizzle = 1 if size_l2 < size_one_head else (size_l2 // size_one_head)
277
+ # Seems faster if swizzle if a power of 2
278
+ log2_floor = lambda n: 31 - clz(n)
279
+ swizzle = 1 if size_l2 < size_one_head else (1 << log2_floor(size_l2 // size_one_head))
280
+ # swizzle = 1 if size_l2 < size_one_head else (size_l2 // size_one_head)
281
+ # If we're in the last section (called residual), we don't want to divide by
282
+ # swizzle. Instead we want to divide by the remainder.
283
+ num_hb_quotient = (args.num_head * args.num_batch) // swizzle
284
+ num_hb_remainder = (args.num_head * args.num_batch) % swizzle
285
+ return SingleTileLPTScheduler.Params(
286
+ total_blocks=args.num_block * args.num_head * args.num_batch,
287
+ num_block_divmod=FastDivmod.create(args.num_block),
288
+ num_head_divmod=FastDivmod.create(args.num_head),
289
+ l2_minor_divmod=FastDivmod.create(swizzle),
290
+ l2_major_divmod=FastDivmod.create(swizzle * args.num_block),
291
+ l2_minor_residual_divmod=FastDivmod.create(
292
+ max(num_hb_remainder, 1)
293
+ ), # don't divide by 0
294
+ num_hb_quotient=Int32(num_hb_quotient),
295
+ num_splits=args.num_splits,
296
+ is_split_kv=args.is_split_kv,
297
+ )
298
+
299
+ def __init__(self, params: Params, tile_idx: Int32, split_idx: Int32, *, loc=None, ip=None):
300
+ self.params = params
301
+ self._tile_idx = tile_idx
302
+ self._split_idx = split_idx
303
+ self._loc = loc
304
+ self._ip = ip
305
+
306
+ @staticmethod
307
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
308
+ return SingleTileLPTScheduler.Params.create(args, loc=loc, ip=ip)
309
+
310
+ @staticmethod
311
+ @cute.jit
312
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileLPTScheduler":
313
+ tile_idx, split_idx, _ = cute.arch.block_idx()
314
+ return SingleTileLPTScheduler(params, tile_idx, split_idx, loc=loc, ip=ip)
315
+
316
+ # called by host
317
+ @staticmethod
318
+ def get_grid_shape(
319
+ params: Params,
320
+ *,
321
+ loc=None,
322
+ ip=None,
323
+ ) -> Tuple[Int32, Int32, Int32]:
324
+ return (params.total_blocks, params.num_splits, Int32(1))
325
+
326
+ @cute.jit
327
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
328
+ params = self.params
329
+ # Implement LPT scheduling coordinate calculation
330
+ bidhb, l2_mod = params.l2_major_divmod.divmod(self._tile_idx)
331
+ # If we're in the last section (called residual), we don't want to divide by
332
+ # swizzle. Instead we want to divide by the remainder.
333
+ block, bidhb_residual = 0, 0
334
+ if bidhb < params.num_hb_quotient:
335
+ block, bidhb_residual = params.l2_minor_divmod.divmod(l2_mod)
336
+ else:
337
+ block, bidhb_residual = params.l2_minor_residual_divmod.divmod(l2_mod)
338
+ bidhb_actual = bidhb * params.l2_minor_divmod.divisor + bidhb_residual
339
+ batch_idx, head_idx = params.num_head_divmod.divmod(bidhb_actual)
340
+ # Longest-processing-time-first
341
+ block = params.num_block_divmod.divisor - 1 - block
342
+ is_valid = self._tile_idx < params.total_blocks
343
+ return WorkTileInfo(
344
+ (Int32(block), Int32(head_idx), Int32(batch_idx), Int32(self._split_idx)), is_valid
345
+ )
346
+
347
+ def initial_work_tile_info(self, *, loc=None, ip=None):
348
+ return self.get_current_work(loc=loc, ip=ip)
349
+
350
+ def prefetch_next_work(self, *, loc=None, ip=None):
351
+ pass
352
+
353
+ def advance_to_next_work(self, *, loc=None, ip=None):
354
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
355
+ self._tile_idx = self.params.total_blocks
356
+
357
+ def __extract_mlir_values__(self):
358
+ values, self._values_pos = [], []
359
+ for obj in [self.params, self._tile_idx, self._split_idx]:
360
+ obj_values = cutlass.extract_mlir_values(obj)
361
+ values += obj_values
362
+ self._values_pos.append(len(obj_values))
363
+ return values
364
+
365
+ def __new_from_mlir_values__(self, values):
366
+ obj_list = []
367
+ for obj, n_items in zip([self.params, self._tile_idx, self._split_idx], self._values_pos):
368
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
369
+ values = values[n_items:]
370
+ return self.__class__(*(tuple(obj_list)), loc=self._loc)
371
+
372
+
373
+ class SingleTileLPTBwdScheduler:
374
+ @dataclass
375
+ class Params(ParamsBase):
376
+ total_blocks: Int32
377
+ num_head_divmod: FastDivmod
378
+ l2_minor_divmod: FastDivmod
379
+ l2_major_divmod: FastDivmod
380
+ l2_minor_residual_divmod: FastDivmod
381
+ num_hb_quotient: Int32
382
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
383
+
384
+ @staticmethod
385
+ @cute.jit
386
+ def create(
387
+ args: TileSchedulerArguments, *, loc=None, ip=None
388
+ ) -> "SingleTileLPTBwdScheduler.Params":
389
+ swizzle = 8
390
+ # If we're in the last section (called residual), we don't want to divide by
391
+ # swizzle. Instead we want to divide by the remainder.
392
+ num_hb_quotient = (args.num_head * args.num_batch) // swizzle
393
+ num_hb_remainder = (args.num_head * args.num_batch) % swizzle
394
+ num_block = cute.ceil_div(args.num_block, args.cluster_shape_mn[0])
395
+ return SingleTileLPTBwdScheduler.Params(
396
+ total_blocks=(num_block * args.cluster_shape_mn[0])
397
+ * args.num_head
398
+ * args.num_batch,
399
+ num_head_divmod=FastDivmod.create(args.num_head),
400
+ l2_minor_divmod=FastDivmod.create(swizzle),
401
+ l2_major_divmod=FastDivmod.create(swizzle * num_block),
402
+ l2_minor_residual_divmod=FastDivmod.create(
403
+ max(num_hb_remainder, 1)
404
+ ), # don't divide by 0
405
+ num_hb_quotient=Int32(num_hb_quotient),
406
+ cluster_shape_mn=args.cluster_shape_mn,
407
+ )
408
+
409
+ def __init__(self, params: Params, tile_idx: Int32, *, loc=None, ip=None):
410
+ self.params = params
411
+ self._tile_idx = tile_idx
412
+ self._loc = loc
413
+ self._ip = ip
414
+
415
+ @staticmethod
416
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
417
+ return SingleTileLPTBwdScheduler.Params.create(args, loc=loc, ip=ip)
418
+
419
+ @staticmethod
420
+ @cute.jit
421
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileLPTBwdScheduler":
422
+ tile_idx = cute.arch.block_idx()[0]
423
+ return SingleTileLPTBwdScheduler(params, tile_idx, loc=loc, ip=ip)
424
+
425
+ # called by host
426
+ @staticmethod
427
+ def get_grid_shape(
428
+ params: Params,
429
+ *,
430
+ loc=None,
431
+ ip=None,
432
+ ) -> Tuple[Int32, Int32, Int32]:
433
+ return (params.total_blocks, Int32(1), Int32(1))
434
+
435
+ @cute.jit
436
+ def get_current_work(self, *, loc=None, ip=None) -> cutlass.utils.WorkTileInfo:
437
+ cluster_idx = self._tile_idx // self.params.cluster_shape_mn[0]
438
+ params = self.params
439
+ # Implement LPT scheduling coordinate calculation
440
+ bidhb, l2_mod = params.l2_major_divmod.divmod(cluster_idx)
441
+ # If we're in the last section (called residual), we don't want to divide by
442
+ # swizzle. Instead we want to divide by the remainder.
443
+ block, bidhb_residual = 0, 0
444
+ if bidhb < params.num_hb_quotient:
445
+ block, bidhb_residual = params.l2_minor_divmod.divmod(l2_mod)
446
+ else:
447
+ block, bidhb_residual = params.l2_minor_residual_divmod.divmod(l2_mod)
448
+ bidhb_actual = bidhb * params.l2_minor_divmod.divisor + bidhb_residual
449
+ batch_idx, head_idx = params.num_head_divmod.divmod(bidhb_actual)
450
+ is_valid = self._tile_idx < params.total_blocks
451
+ bidx_in_cluster = cute.arch.block_in_cluster_idx()
452
+ block = block * params.cluster_shape_mn[0] + bidx_in_cluster[0]
453
+ return WorkTileInfo((Int32(block), Int32(head_idx), Int32(batch_idx), Int32(0)), is_valid)
454
+
455
+ def initial_work_tile_info(self, *, loc=None, ip=None):
456
+ return self.get_current_work(loc=loc, ip=ip)
457
+
458
+ def prefetch_next_work(self, *, loc=None, ip=None):
459
+ pass
460
+
461
+ def advance_to_next_work(self, *, loc=None, ip=None):
462
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
463
+ self._tile_idx = self.params.total_blocks
464
+
465
+ def __extract_mlir_values__(self):
466
+ values, self._values_pos = [], []
467
+ for obj in [self.params, self._tile_idx]:
468
+ obj_values = cutlass.extract_mlir_values(obj)
469
+ values += obj_values
470
+ self._values_pos.append(len(obj_values))
471
+ return values
472
+
473
+ def __new_from_mlir_values__(self, values):
474
+ obj_list = []
475
+ for obj, n_items in zip([self.params, self._tile_idx], self._values_pos):
476
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
477
+ values = values[n_items:]
478
+ return self.__class__(*(tuple(obj_list)), loc=self._loc)
479
+
480
+
481
+ class SingleTileVarlenScheduler:
482
+ @dataclass
483
+ class Params(ParamsBase):
484
+ num_head: Int32
485
+ num_batch: Int32
486
+ total_q: Int32
487
+ num_splits: Int32
488
+ max_kvblock_in_l2: Int32
489
+ tile_shape_mn: cutlass.Constexpr[Tuple[int, int]]
490
+ mCuSeqlensQ: Optional[cute.Tensor] = None
491
+ mSeqUsedQ: Optional[cute.Tensor] = None
492
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
493
+ lpt: cutlass.Constexpr[bool] = False
494
+ is_split_kv: cutlass.Constexpr[bool] = False
495
+
496
+ @staticmethod
497
+ @cute.jit
498
+ def create(
499
+ args: TileSchedulerArguments, *, loc=None, ip=None
500
+ ) -> "SingleTileVarlenScheduler.Params":
501
+ size_l2 = 50 * 1024 * 1024 # 50 MB for K & V
502
+ max_kvblock_in_l2 = size_l2 // (
503
+ (args.headdim + args.headdim_v) * args.element_size * args.tile_shape_mn[1]
504
+ )
505
+ assert args.mCuSeqlensQ is not None or args.mSeqUsedQ is not None, (
506
+ "At least one of mCuSeqlensQ or mSeqUsedQ must be provided"
507
+ )
508
+ return SingleTileVarlenScheduler.Params(
509
+ num_head=args.num_head,
510
+ num_batch=args.num_batch,
511
+ total_q=args.total_q,
512
+ num_splits=args.num_splits,
513
+ max_kvblock_in_l2=max_kvblock_in_l2,
514
+ tile_shape_mn=args.tile_shape_mn,
515
+ mCuSeqlensQ=args.mCuSeqlensQ,
516
+ mSeqUsedQ=args.mSeqUsedQ,
517
+ qhead_per_kvhead_packgqa=args.qhead_per_kvhead_packgqa,
518
+ lpt=args.lpt,
519
+ is_split_kv=args.is_split_kv,
520
+ )
521
+
522
+ def __init__(self, params: Params, tile_idx: Int32, split_idx: Int32, *, loc=None, ip=None):
523
+ self.params = params
524
+ self._tile_idx = tile_idx
525
+ self._split_idx = split_idx
526
+ self._is_first_block = True
527
+ self._loc = loc
528
+ self._ip = ip
529
+
530
+ @staticmethod
531
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
532
+ return SingleTileVarlenScheduler.Params.create(args, loc=loc, ip=ip)
533
+
534
+ @staticmethod
535
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileVarlenScheduler":
536
+ tile_idx, split_idx, _ = cute.arch.block_idx()
537
+ return SingleTileVarlenScheduler(params, tile_idx, split_idx, loc=loc, ip=ip)
538
+
539
+ # called by host
540
+ @staticmethod
541
+ def get_grid_shape(
542
+ params: Params,
543
+ *,
544
+ loc=None,
545
+ ip=None,
546
+ ) -> Tuple[Int32, Int32, Int32]:
547
+ total_blocks_max = (
548
+ params.total_q + params.num_batch * (params.tile_shape_mn[0] - 1)
549
+ ) // params.tile_shape_mn[0]
550
+ return (total_blocks_max * params.num_head, params.num_splits, Int32(1))
551
+
552
+ @cute.jit
553
+ def _get_num_m_blocks(self, lane: Int32, bidb_start: Int32) -> Int32:
554
+ params = self.params
555
+ batch_idx = lane + bidb_start
556
+ if cutlass.const_expr(params.mSeqUsedQ is not None):
557
+ seqlen = Int32(0)
558
+ if batch_idx < params.num_batch:
559
+ seqlen = params.mSeqUsedQ[batch_idx]
560
+ else:
561
+ assert params.mCuSeqlensQ is not None
562
+ cur_cu_seqlen = Int32(0)
563
+ if batch_idx <= params.num_batch:
564
+ cur_cu_seqlen = params.mCuSeqlensQ[batch_idx]
565
+ next_cu_seqlen = cute.arch.shuffle_sync_down(cur_cu_seqlen, offset=1)
566
+ seqlen = next_cu_seqlen - cur_cu_seqlen
567
+ if cutlass.const_expr(params.qhead_per_kvhead_packgqa > 1):
568
+ seqlen *= params.qhead_per_kvhead_packgqa
569
+ return (
570
+ cute.ceil_div(seqlen, params.tile_shape_mn[0])
571
+ if batch_idx < params.num_batch and lane < cute.arch.WARP_SIZE - 1
572
+ else Int32(0)
573
+ )
574
+
575
+ @cute.jit
576
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
577
+ params = self.params
578
+ lane_idx = cute.arch.lane_idx()
579
+ num_m_blocks = self._get_num_m_blocks(lane_idx, bidb_start=0)
580
+ num_m_blocks_cumulative = utils.warp_prefix_sum(num_m_blocks, lane_idx)
581
+ # Total number of blocks for the next 31 batches
582
+ m_blocks_in_group = cute.arch.shuffle_sync(num_m_blocks_cumulative, cute.arch.WARP_SIZE - 1)
583
+ # Same for all lanes
584
+ group_end_tile = m_blocks_in_group * params.num_head
585
+ # if cute.arch.thread_idx()[0] == 128 + 31: cute.printf("SingleTileVarlenScheduler: tile_idx=%d, group_end_tile = %d, num_m_blocks=%d, num_m_blocks_cumulative = %d, m_blocks_in_group = %d", self._tile_idx, group_end_tile, num_m_blocks, num_m_blocks_cumulative, m_blocks_in_group)
586
+ block, head_idx, batch_idx = Int32(0), Int32(0), Int32(0)
587
+ next_tile_idx = self._tile_idx
588
+ while group_end_tile <= next_tile_idx:
589
+ batch_idx += cute.arch.WARP_SIZE - 1
590
+ if batch_idx >= params.num_batch:
591
+ batch_idx = Int32(params.num_batch)
592
+ group_end_tile = next_tile_idx + 1
593
+ else:
594
+ num_m_blocks = self._get_num_m_blocks(lane_idx, bidb_start=batch_idx)
595
+ num_m_blocks_cumulative = utils.warp_prefix_sum(num_m_blocks, lane_idx)
596
+ m_blocks_in_group = cute.arch.shuffle_sync(
597
+ num_m_blocks_cumulative, cute.arch.WARP_SIZE - 1
598
+ )
599
+ group_end_tile += m_blocks_in_group * params.num_head
600
+ is_valid = False
601
+ if batch_idx >= params.num_batch:
602
+ block, head_idx, batch_idx = Int32(0), Int32(0), Int32(params.num_batch)
603
+ else:
604
+ group_start_tile = group_end_tile - m_blocks_in_group * params.num_head
605
+ # if cute.arch.thread_idx()[0] == 128 + 31: cute.printf("SingleTileVarlenScheduler: tile_idx=%d, group_end_tile = %d, num_m_blocks=%d, batch_idx = %d", self._tile_idx, group_end_tile, num_m_blocks, batch_idx)
606
+ # The next problem to process is the first one that does not have ending tile position
607
+ # that is greater than or equal to tile index.
608
+ batch_idx_in_group = cute.arch.popc(
609
+ cute.arch.vote_ballot_sync(
610
+ group_start_tile + num_m_blocks_cumulative * params.num_head <= next_tile_idx
611
+ )
612
+ )
613
+ batch_idx += batch_idx_in_group
614
+ num_m_blocks_prev_lane = (
615
+ 0
616
+ if batch_idx_in_group == 0
617
+ else cute.arch.shuffle_sync(num_m_blocks_cumulative, batch_idx_in_group - 1)
618
+ )
619
+ num_m_blocks = cute.arch.shuffle_sync(num_m_blocks, batch_idx_in_group)
620
+ mh_block = next_tile_idx - group_start_tile - num_m_blocks_prev_lane * params.num_head
621
+ if cutlass.const_expr(params.lpt):
622
+ # This is a version of the SingleTileLPTScheduler, complicated by the fact that
623
+ # the seqlen can vary per batch.
624
+ # TODO: is there any case where num_m_blocks is 0?
625
+ # TODO: by right we should read the seqlen_kv but we're assuming seqlen_q == seqlen_k here
626
+ num_n_blocks = (
627
+ num_m_blocks
628
+ * params.tile_shape_mn[0]
629
+ // params.qhead_per_kvhead_packgqa
630
+ // params.tile_shape_mn[1]
631
+ )
632
+ # nheads_in_l2 = min(max(self.max_kvblock_in_l2 // num_n_blocks, 1), self.num_head)
633
+ # Seems faster to have this be a power of 2
634
+ nheads_in_l2 = (
635
+ 16
636
+ if num_n_blocks * 16 <= params.max_kvblock_in_l2
637
+ else (
638
+ 8
639
+ if num_n_blocks * 8 <= params.max_kvblock_in_l2
640
+ else (
641
+ 4
642
+ if num_n_blocks * 4 <= params.max_kvblock_in_l2
643
+ else (2 if num_n_blocks * 2 <= params.max_kvblock_in_l2 else 1)
644
+ )
645
+ )
646
+ )
647
+ nheads_in_l2 = min(nheads_in_l2, params.num_head)
648
+ mh_in_l2 = nheads_in_l2 * num_m_blocks
649
+ section_idx = mh_block // mh_in_l2
650
+ l2_mod = mh_block - section_idx * mh_in_l2
651
+ # Deal with tail section
652
+ nheads_in_this_section = (
653
+ nheads_in_l2
654
+ if nheads_in_l2 * (section_idx + 1) <= params.num_head
655
+ else params.num_head - section_idx * nheads_in_l2
656
+ )
657
+ block = l2_mod // nheads_in_this_section
658
+ head_idx_residual = l2_mod - block * nheads_in_this_section
659
+ head_idx = section_idx * nheads_in_l2 + head_idx_residual
660
+ block = num_m_blocks - 1 - block
661
+ else:
662
+ head_idx = mh_block // num_m_blocks
663
+ block = mh_block - head_idx * num_m_blocks
664
+ is_valid = self._is_first_block and batch_idx < params.num_batch
665
+ # if cute.arch.thread_idx()[0] == 128: cute.printf("SingleTileVarlenScheduler: tile_idx=%d, batch_idx=%d, head_idx=%d, block=%d, is_valid = %d", self._tile_idx, batch_idx, head_idx, block, is_valid)
666
+ split_idx = self._split_idx if const_expr(params.is_split_kv) else Int32(0)
667
+ return WorkTileInfo((Int32(block), Int32(head_idx), Int32(batch_idx), split_idx), is_valid)
668
+
669
+ def initial_work_tile_info(self, *, loc=None, ip=None):
670
+ return self.get_current_work(loc=loc, ip=ip)
671
+
672
+ def prefetch_next_work(self, *, loc=None, ip=None):
673
+ pass
674
+
675
+ def advance_to_next_work(self, *, loc=None, ip=None):
676
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
677
+ self._is_first_block = False
678
+
679
+ def __extract_mlir_values__(self):
680
+ values, self._values_pos = [], []
681
+ for obj in [self.params, self._tile_idx, self._split_idx]:
682
+ obj_values = cutlass.extract_mlir_values(obj)
683
+ values += obj_values
684
+ self._values_pos.append(len(obj_values))
685
+ return values
686
+
687
+ def __new_from_mlir_values__(self, values):
688
+ obj_list = []
689
+ for obj, n_items in zip(
690
+ [self.params, self._tile_idx, self._split_idx],
691
+ self._values_pos,
692
+ ):
693
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
694
+ values = values[n_items:]
695
+ return SingleTileVarlenScheduler(*(tuple(obj_list)), loc=self._loc)
build/torch-universal/utils.py ADDED
@@ -0,0 +1,788 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import math
4
+ import hashlib
5
+ import inspect
6
+ import re
7
+ from typing import Type, Callable, Optional, Tuple, overload
8
+ from functools import partial
9
+
10
+ import cutlass
11
+ import cutlass.cute as cute
12
+
13
+ from cutlass import Float32, Int32, const_expr
14
+ from cutlass.cutlass_dsl import T, dsl_user_op
15
+ from cutlass._mlir.dialects import nvvm, llvm
16
+ from cutlass.cute.runtime import from_dlpack
17
+
18
+
19
+ # cute.arch.{fma,mul,add}_packed_f32x2 uses RZ rounding mode by default
20
+ fma_packed_f32x2 = partial(cute.arch.fma_packed_f32x2, rnd=nvvm.RoundingModeKind.RN)
21
+ mul_packed_f32x2 = partial(cute.arch.mul_packed_f32x2, rnd=nvvm.RoundingModeKind.RN)
22
+ add_packed_f32x2 = partial(cute.arch.add_packed_f32x2, rnd=nvvm.RoundingModeKind.RN)
23
+ sub_packed_f32x2 = partial(
24
+ cute.arch.calc_packed_f32x2_op,
25
+ src_c=None,
26
+ calc_func=nvvm.sub_packed_f32x2,
27
+ rnd=nvvm.RoundingModeKind.RN
28
+ )
29
+
30
+ def hash_callable(func: Callable) -> str:
31
+ """Hash a callable based on the source code or bytecode and closure values."""
32
+ if hasattr(func, "__wrapped__"):
33
+ # cute.jit returns a wrapper whose repr/closure changes per compile; hash the undecorated function.
34
+ base_func = func.__wrapped__
35
+ func = base_func
36
+
37
+ try:
38
+ data = inspect.getsource(func).encode()
39
+ except (OSError, TypeError):
40
+ if hasattr(func, "__code__") and func.__code__ is not None:
41
+ data = func.__code__.co_code
42
+ else:
43
+ data = repr(func).encode()
44
+
45
+ hasher = hashlib.sha256(data)
46
+
47
+ if hasattr(func, "__closure__") and func.__closure__ is not None:
48
+ for idx, cell in enumerate(func.__closure__):
49
+ cell_value = cell.cell_contents
50
+ hasher.update(repr(cell_value).encode())
51
+
52
+ return hasher.hexdigest()
53
+
54
+
55
+ def create_softcap_scoremod(softcap_val):
56
+ inv_softcap = 1.0 / softcap_val
57
+
58
+ @cute.jit
59
+ def scoremod_premask_fn(acc_S_SSA, batch_idx, head_idx, q_idx, kv_idx, buffers):
60
+ scores = acc_S_SSA * inv_softcap
61
+ return scores * cute.math.tanh(scores, fastmath=True)
62
+
63
+ return scoremod_premask_fn
64
+
65
+ def convert_from_dlpack(x, leading_dim, alignment=16, divisibility=1) -> cute.Tensor:
66
+ return (
67
+ from_dlpack(x, assumed_align=alignment)
68
+ .mark_layout_dynamic(leading_dim=leading_dim)
69
+ .mark_compact_shape_dynamic(
70
+ mode=leading_dim, stride_order=x.dim_order(), divisibility=divisibility
71
+ )
72
+ )
73
+
74
+
75
+ def make_tiled_copy_A(
76
+ copy_atom: cute.CopyAtom, tiled_mma: cute.TiledMma, swapAB: cutlass.Constexpr[bool] = False
77
+ ) -> cute.TiledCopy:
78
+ if const_expr(swapAB):
79
+ return cute.make_tiled_copy_B(copy_atom, tiled_mma)
80
+ else:
81
+ return cute.make_tiled_copy_A(copy_atom, tiled_mma)
82
+
83
+
84
+ def make_tiled_copy_B(
85
+ copy_atom: cute.CopyAtom, tiled_mma: cute.TiledMma, swapAB: cutlass.Constexpr[bool] = False
86
+ ) -> cute.TiledCopy:
87
+ if const_expr(swapAB):
88
+ return cute.make_tiled_copy_A(copy_atom, tiled_mma)
89
+ else:
90
+ return cute.make_tiled_copy_B(copy_atom, tiled_mma)
91
+
92
+
93
+ def mma_make_fragment_A(
94
+ smem: cute.Tensor, thr_mma: cute.core.ThrMma, swapAB: cutlass.Constexpr[bool] = False
95
+ ) -> cute.Tensor:
96
+ if const_expr(swapAB):
97
+ return mma_make_fragment_B(smem, thr_mma)
98
+ else:
99
+ return thr_mma.make_fragment_A(thr_mma.partition_A(smem))
100
+
101
+
102
+ def mma_make_fragment_B(
103
+ smem: cute.Tensor, thr_mma: cute.core.ThrMma, swapAB: cutlass.Constexpr[bool] = False
104
+ ) -> cute.Tensor:
105
+ if const_expr(swapAB):
106
+ return mma_make_fragment_A(smem, thr_mma)
107
+ else:
108
+ return thr_mma.make_fragment_B(thr_mma.partition_B(smem))
109
+
110
+
111
+ def get_smem_store_atom(
112
+ arch: cutlass.Constexpr[int], element_type: Type[cute.Numeric], transpose: bool = False
113
+ ) -> cute.CopyAtom:
114
+ if const_expr(arch < 90 or element_type.width != 16):
115
+ return cute.make_copy_atom(
116
+ cute.nvgpu.CopyUniversalOp(),
117
+ element_type,
118
+ num_bits_per_copy=2 * element_type.width,
119
+ )
120
+ else:
121
+ return cute.make_copy_atom(
122
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=transpose, num_matrices=4),
123
+ element_type,
124
+ )
125
+
126
+
127
+ @cute.jit
128
+ def warp_reduce(
129
+ val: cute.TensorSSA | cute.Numeric,
130
+ op: Callable,
131
+ width: cutlass.Constexpr[int] = cute.arch.WARP_SIZE,
132
+ ) -> cute.TensorSSA | cute.Numeric:
133
+ if const_expr(isinstance(val, cute.TensorSSA)):
134
+ res = cute.make_fragment(val.shape, val.dtype)
135
+ res.store(val)
136
+ for i in cutlass.range_constexpr(cute.size(val.shape)):
137
+ res[i] = warp_reduce(res[i], op, width)
138
+ return res.load()
139
+ else:
140
+ for i in cutlass.range_constexpr(int(math.log2(width))):
141
+ val = op(val, cute.arch.shuffle_sync_bfly(val, offset=1 << i))
142
+ return val
143
+
144
+
145
+ def convert_layout_acc_mn(acc_layout: cute.Layout, transpose: bool = False) -> cute.Layout:
146
+ """
147
+ For Sm80, convert ((2, 2), MMA_M, MMA_N, ...) to ((2, MMA_M), (2, MMA_N), ...).
148
+ For Sm90, convert ((2, 2, V), MMA_M, MMA_N, ...) to ((2, MMA_M), (2, V, MMA_N), ...).
149
+ """
150
+ acc_layout_col_major = cute.make_layout(acc_layout.shape)
151
+ shape = (
152
+ (acc_layout_col_major.shape[0][1], acc_layout_col_major.shape[1]), # MMA_M
153
+ (
154
+ acc_layout_col_major.shape[0][0],
155
+ *acc_layout_col_major.shape[0][2:],
156
+ acc_layout_col_major.shape[2],
157
+ ), # MMA_N
158
+ *acc_layout_col_major.shape[3:],
159
+ )
160
+ stride = (
161
+ (acc_layout_col_major.stride[0][1], acc_layout_col_major.stride[1]), # MMA_M
162
+ (
163
+ acc_layout_col_major.stride[0][0],
164
+ *acc_layout_col_major.stride[0][2:],
165
+ acc_layout_col_major.stride[2],
166
+ ), # MMA_N
167
+ *acc_layout_col_major.stride[3:],
168
+ )
169
+ if const_expr(transpose):
170
+ shape = (shape[1], shape[0], *shape[2:])
171
+ stride = (stride[1], stride[0], *stride[2:])
172
+ acc_layout_mn = cute.make_layout(shape, stride=stride)
173
+ return cute.composition(acc_layout, acc_layout_mn)
174
+
175
+
176
+ def make_acc_tensor_mn_view(acc: cute.Tensor, transpose: bool = False) -> cute.Tensor:
177
+ return cute.make_tensor(acc.iterator, convert_layout_acc_mn(acc.layout, transpose=transpose))
178
+
179
+
180
+ @cute.jit
181
+ def convert_layout_acc_frgA(acc_layout: cute.Layout) -> cute.Layout:
182
+ # For back to back gemm, convert layout of acc0 to gemm 1 accept layout.
183
+ # For Sm80, as the mma instruction shape is 16x8x16, we need to convert from (4, MMA_M, MMA_N) to ((4, 2), MMA_M, MMA_N / 2)
184
+ # For Sm90, FP16/BF16, convert acc_layout from ((2, 2, N / 8), MMA_M, MMA_N) to ((2, 2, 2), MMA_M, (N / 16, MMA_N))
185
+ # TODO: Sm90 FP8
186
+ if const_expr(cute.rank(acc_layout.shape[0]) == 3): # Sm90
187
+ l = cute.logical_divide(
188
+ acc_layout, ((None, None, 2), None, None)
189
+ ) # ((2, 2, (2, N / 16)), MMA_M, MMA_N)
190
+ rA_mma_view = cute.make_layout(
191
+ (
192
+ (l.shape[0][0], l.shape[0][1], l.shape[0][2][0]),
193
+ l.shape[1],
194
+ (l.shape[0][2][1], l.shape[2]),
195
+ ),
196
+ stride=(
197
+ (l.stride[0][0], l.stride[0][1], l.stride[0][2][0]),
198
+ l.stride[1],
199
+ (l.stride[0][2][1], l.stride[2]),
200
+ ),
201
+ )
202
+ else: # Sm80
203
+ # (4, MMA_M, MMA_N) -> (4, MMA_M, (2, MMA_N / 2))
204
+ l = cute.logical_divide(acc_layout, (None, None, 2))
205
+ rA_mma_view = cute.make_layout(
206
+ (
207
+ (l.shape[0], l.shape[2][0]),
208
+ l.shape[1],
209
+ l.shape[2][1],
210
+ ),
211
+ stride=(
212
+ (l.stride[0], l.stride[2][0]),
213
+ l.stride[1],
214
+ l.stride[2][1],
215
+ ),
216
+ )
217
+ return rA_mma_view
218
+
219
+
220
+ def make_acc_tensor_frgA_view(acc: cute.Tensor) -> cute.Tensor:
221
+ return cute.make_tensor(acc.iterator, convert_layout_acc_frgA(acc.layout))
222
+
223
+
224
+ def select(a: cute.Tensor, mode: list[int]) -> cute.Tensor:
225
+ return cute.make_tensor(a.iterator, cute.select(a.layout, mode))
226
+
227
+
228
+ def transpose_view(a: cute.Tensor) -> cute.Tensor:
229
+ """Transpose the first two dimensions of a tensor on smem."""
230
+ shape = (a.shape[1], a.shape[0], *a.shape[2:])
231
+ order = (1, 0, *range(2, cute.rank(a)))
232
+ return cute.composition(a, cute.make_ordered_layout(shape, order=order))
233
+ # stride = (a.layout.stride[1], a.layout.stride[0], *a.layout.stride[2:])
234
+ # return cute.make_tensor(a.iterator, cute.make_layout(shape, stride=stride))
235
+
236
+
237
+ def parse_swizzle_from_pointer(ptr: cute.Pointer) -> cute.Swizzle:
238
+ """Extract swizzle parameters from a pointer's swizzle_type.
239
+
240
+ The swizzle_type string has the form '!cute.swizzle<"S<b,m,s>">' where
241
+ b, m, s are the swizzle parameters (bits, base, shift).
242
+
243
+ Returns:
244
+ A cute.Swizzle object constructed from the extracted parameters
245
+
246
+ Raises:
247
+ ValueError: If the swizzle_type string cannot be parsed
248
+ """
249
+ # Ideally there should be a better API to get swizzle parameters, but we'll just parse
250
+ # the string here.
251
+ swizzle_str = str(ptr.type.swizzle_type)
252
+ # Extract the inner part "S<b,m,s>"
253
+ match = re.search(r'S<(\d+),(\d+),(\d+)>', swizzle_str)
254
+ if match:
255
+ b, m, s = int(match.group(1)), int(match.group(2)), int(match.group(3))
256
+ return cute.make_swizzle(b, m, s)
257
+ else:
258
+ raise ValueError(f"Could not parse swizzle_type: {swizzle_str}")
259
+
260
+
261
+ @cute.jit
262
+ def exp2f(x: cute.TensorSSA | Float32) -> cute.TensorSSA | Float32:
263
+ """exp2f calculation for both vector and scalar.
264
+ :param x: input value
265
+ :type x: cute.TensorSSA or Float32
266
+ :return: exp2 value
267
+ :rtype: cute.TensorSSA or Float32
268
+ """
269
+ if const_expr(isinstance(x, cute.TensorSSA)):
270
+ res = cute.make_fragment(x.shape, Float32)
271
+ res.store(x)
272
+ for i in cutlass.range_constexpr(cute.size(x.shape)):
273
+ res[i] = cute.arch.exp2(res[i])
274
+ return res.load()
275
+ else:
276
+ return cute.arch.exp2(x)
277
+
278
+
279
+ @dsl_user_op
280
+ def log2f(a: float | Float32, *, loc=None, ip=None) -> Float32:
281
+ return Float32(
282
+ llvm.inline_asm(
283
+ T.f32(),
284
+ [Float32(a).ir_value(loc=loc, ip=ip)],
285
+ "lg2.approx.ftz.f32 $0, $1;",
286
+ "=f,f",
287
+ has_side_effects=False,
288
+ is_align_stack=False,
289
+ asm_dialect=llvm.AsmDialect.AD_ATT,
290
+ )
291
+ )
292
+
293
+ @dsl_user_op
294
+ def logf(a: float | Float32, *, loc=None, ip=None) -> Float32:
295
+ return log2f(a, loc=loc, ip=ip) * math.log(2.0)
296
+
297
+
298
+ @dsl_user_op
299
+ def fmax(
300
+ a: float | Float32, b: float | Float32, c: float | Float32 | None = None, *, loc=None, ip=None
301
+ ) -> Float32:
302
+ return Float32(
303
+ nvvm.fmax(
304
+ T.f32(),
305
+ Float32(a).ir_value(loc=loc, ip=ip),
306
+ Float32(b).ir_value(loc=loc, ip=ip),
307
+ c=Float32(c).ir_value(loc=loc, ip=ip) if c is not None else None,
308
+ loc=loc,
309
+ ip=ip,
310
+ )
311
+ )
312
+
313
+
314
+ @cute.jit
315
+ def fmax_reduce(
316
+ x: cute.TensorSSA, init_val: float | Float32 | None = None, arch: cutlass.Constexpr[int] = 80
317
+ ) -> Float32:
318
+ if const_expr(arch < 100 or cute.size(x.shape) % 8 != 0):
319
+ # if const_expr(init_val is None):
320
+ # init_val = -cutlass.Float32.if
321
+ # return x.reduce(cute.ReductionOp.MAX, init_val, 0)
322
+ res = cute.make_fragment(x.shape, Float32)
323
+ res.store(x)
324
+ # local_max = [res[0], res[1]]
325
+ # for i in cutlass.range_constexpr(2, cute.size(x.shape), 2):
326
+ # local_max[0] = fmax(local_max[0], res[i + 0])
327
+ # local_max[1] = fmax(local_max[1], res[i + 1])
328
+ # local_max[0] = fmax(local_max[0], local_max[1])
329
+ # return local_max[0] if const_expr(init_val is None) else fmax(local_max[0], init_val)
330
+ local_max = [res[0], res[1], res[2], res[3]]
331
+ for i in cutlass.range_constexpr(4, cute.size(x.shape), 4):
332
+ local_max[0] = fmax(local_max[0], res[i + 0])
333
+ local_max[1] = fmax(local_max[1], res[i + 1])
334
+ local_max[2] = fmax(local_max[2], res[i + 2])
335
+ local_max[3] = fmax(local_max[3], res[i + 3])
336
+ local_max[0] = fmax(local_max[0], local_max[1])
337
+ local_max[2] = fmax(local_max[2], local_max[3])
338
+ local_max[0] = fmax(local_max[0], local_max[2])
339
+ return local_max[0] if const_expr(init_val is None) else fmax(local_max[0], init_val)
340
+ else:
341
+ # [2025-06-15] x.reduce only seems to use 50% 3-input max and 50% 2-input max
342
+ # We instead force the 3-input max.
343
+ res = cute.make_fragment(x.shape, Float32)
344
+ res.store(x)
345
+ local_max_0 = fmax(init_val, res[0], res[1]) if const_expr(init_val is not None) else fmax(res[0], res[1])
346
+ local_max = [
347
+ local_max_0,
348
+ fmax(res[2], res[3]),
349
+ fmax(res[4], res[5]),
350
+ fmax(res[6], res[7]),
351
+ ]
352
+ for i in cutlass.range_constexpr(8, cute.size(x.shape), 8):
353
+ local_max[0] = fmax(local_max[0], res[i], res[i + 1])
354
+ local_max[1] = fmax(local_max[1], res[i + 2], res[i + 3])
355
+ local_max[2] = fmax(local_max[2], res[i + 4], res[i + 5])
356
+ local_max[3] = fmax(local_max[3], res[i + 6], res[i + 7])
357
+ local_max[0] = fmax(local_max[0], local_max[1])
358
+ return fmax(local_max[0], local_max[2], local_max[3])
359
+
360
+
361
+ @cute.jit
362
+ def fadd_reduce(
363
+ x: cute.TensorSSA, init_val: float | Float32 | None = None, arch: cutlass.Constexpr[int] = 80
364
+ ) -> Float32:
365
+ if const_expr(arch < 100 or cute.size(x.shape) % 8 != 0):
366
+ if const_expr(init_val is None):
367
+ init_val = Float32.zero
368
+ return x.reduce(cute.ReductionOp.ADD, init_val, 0)
369
+ # res = cute.make_fragment(x.shape, Float32)
370
+ # res.store(x)
371
+ # local_sum = [res[0], res[1], res[2], res[3]]
372
+ # for i in cutlass.range_constexpr(4, cute.size(x.shape), 4):
373
+ # local_sum[0] += res[i + 0]
374
+ # local_sum[1] += res[i + 1]
375
+ # local_sum[2] += res[i + 2]
376
+ # local_sum[3] += res[i + 3]
377
+ # local_sum[0] += local_sum[1]
378
+ # local_sum[2] += local_sum[3]
379
+ # local_sum[0] += local_sum[2]
380
+ # return local_sum[0] if const_expr(init_val is None) else local_sum[0] + init_val
381
+ else:
382
+ res = cute.make_fragment(x.shape, Float32)
383
+ res.store(x)
384
+ local_sum_0 = (
385
+ add_packed_f32x2((init_val, 0.0), (res[0], res[1]))
386
+ # add_packed_f32x2((init_val / 2, init_val / 2), (res[0], res[1]))
387
+ if const_expr(init_val is not None)
388
+ else (res[0], res[1])
389
+ )
390
+ local_sum = [local_sum_0, (res[2], res[3]), (res[4], res[5]), (res[6], res[7])]
391
+ for i in cutlass.range_constexpr(8, cute.size(x.shape), 8):
392
+ local_sum[0] = add_packed_f32x2(local_sum[0], (res[i + 0], res[i + 1]))
393
+ local_sum[1] = add_packed_f32x2(local_sum[1], (res[i + 2], res[i + 3]))
394
+ local_sum[2] = add_packed_f32x2(local_sum[2], (res[i + 4], res[i + 5]))
395
+ local_sum[3] = add_packed_f32x2(local_sum[3], (res[i + 6], res[i + 7]))
396
+ local_sum[0] = add_packed_f32x2(local_sum[0], local_sum[1])
397
+ local_sum[2] = add_packed_f32x2(local_sum[2], local_sum[3])
398
+ local_sum[0] = add_packed_f32x2(local_sum[0], local_sum[2])
399
+ return local_sum[0][0] + local_sum[0][1]
400
+
401
+
402
+ @dsl_user_op
403
+ def atomic_add_fp32(a: float | Float32, gmem_ptr: cute.Pointer, *, loc=None, ip=None) -> None:
404
+ # gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
405
+ # # cache_hint = cutlass.Int64(0x12F0000000000000)
406
+ # llvm.inline_asm(
407
+ # None,
408
+ # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip)],
409
+ # # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip), cache_hint.ir_value()],
410
+ # "red.global.add.f32 [$0], $1;",
411
+ # # "red.global.add.L2::cache_hint.f32 [$0], $1, 0x12F0000000000000;",
412
+ # # "red.global.add.L2::cache_hint.f32 [$0], $1, $2;",
413
+ # "l,f",
414
+ # # "l,f,l",
415
+ # has_side_effects=True,
416
+ # is_align_stack=False,
417
+ # asm_dialect=llvm.AsmDialect.AD_ATT,
418
+ # )
419
+ nvvm.atomicrmw(
420
+ res=T.f32(), op=nvvm.AtomicOpKind.FADD, ptr=gmem_ptr.llvm_ptr, a=Float32(a).ir_value()
421
+ )
422
+
423
+
424
+ @dsl_user_op
425
+ def elem_pointer(x: cute.Tensor, coord: cute.Coord, *, loc=None, ip=None) -> cute.Pointer:
426
+ return x.iterator + cute.crd2idx(coord, x.layout, loc=loc, ip=ip)
427
+
428
+
429
+ @dsl_user_op
430
+ def elem_pointer_i64(x: cute.Tensor, coord: cute.Coord, *, loc=None, ip=None) -> cute.Pointer:
431
+ flat_coord_i64 = tuple(cutlass.Int64(c) for c in cute.flatten(coord))
432
+ flat_stride = cute.flatten_to_tuple(x.stride)
433
+ assert len(flat_coord_i64) == len(flat_stride), "Coordinate and stride must have the same length"
434
+ offset = sum(c * s for c, s in zip(flat_coord_i64, flat_stride))
435
+ # HACK: we assume that applying the offset does not change the pointer alignment
436
+ byte_offset = offset * x.element_type.width // 8
437
+ return cute.make_ptr(
438
+ x.element_type,
439
+ x.iterator.toint() + byte_offset,
440
+ x.memspace,
441
+ assumed_align=x.iterator.alignment,
442
+ )
443
+
444
+
445
+ @cute.jit
446
+ def predicate_k(tAcA: cute.Tensor, limit: cutlass.Int32) -> cute.Tensor:
447
+ # Only compute predicates for the "k" dimension. For the mn dimension, we will use "if"
448
+ tApA = cute.make_fragment(
449
+ cute.make_layout(
450
+ (cute.size(tAcA, mode=[0, 1]), cute.size(tAcA, mode=[1]), cute.size(tAcA, mode=[2])),
451
+ stride=(cute.size(tAcA, mode=[2]), 0, 1),
452
+ ),
453
+ cutlass.Boolean,
454
+ )
455
+ for rest_v in cutlass.range_constexpr(tApA.shape[0]):
456
+ for rest_k in cutlass.range_constexpr(tApA.shape[2]):
457
+ tApA[rest_v, 0, rest_k] = cute.elem_less(tAcA[(0, rest_v), 0, rest_k][1], limit)
458
+ return tApA
459
+
460
+
461
+ def canonical_warp_group_idx(sync: bool = True) -> cutlass.Int32:
462
+ warp_group_idx = cute.arch.thread_idx()[0] // 128
463
+ if const_expr(sync):
464
+ warp_group_idx = cute.arch.make_warp_uniform(warp_group_idx)
465
+ return warp_group_idx
466
+
467
+
468
+ # @dsl_user_op
469
+ # def warp_vote_any_lt(a: float | Float32, b: float | Float32, *, loc=None, ip=None) -> cutlass.Boolean:
470
+ # mask = cutlass.Int32(-1)
471
+ # return cutlass.Boolean(
472
+ # llvm.inline_asm(
473
+ # T.i32(),
474
+ # [Float32(a).ir_value(loc=loc, ip=ip), Float32(b).ir_value(loc=loc, ip=ip), mask.ir_value(loc=loc, ip=ip)],
475
+ # ".pred p1, p2;\n"
476
+ # "setp.lt.f32 p1, $1, $2;\n"
477
+ # "vote.sync.any.pred p2, p1, $3;\n"
478
+ # "selp.u32 $0, 1, 0, p2;",
479
+ # # "selp.u32 $0, 1, 0, p1;",
480
+ # "=r,f,f,r",
481
+ # has_side_effects=False,
482
+ # is_align_stack=False,
483
+ # asm_dialect=llvm.AsmDialect.AD_ATT,
484
+ # )
485
+ # )
486
+
487
+
488
+ @cute.jit
489
+ def shuffle_sync(
490
+ value: cute.Numeric,
491
+ offset: cute.typing.Int,
492
+ width: cutlass.Constexpr[int] = cute.arch.WARP_SIZE,
493
+ ) -> cute.Numeric:
494
+ assert value.width % 32 == 0, "value type must be a multiple of 32 bits"
495
+ # 1 -> 0b11111, 2 -> 0b11110, 4 -> 0b11100, 8 -> 0b11000, 16 -> 0b10000, 32 -> 0b00000
496
+ mask = cute.arch.WARP_SIZE - width
497
+ clamp = cute.arch.WARP_SIZE - 1
498
+ mask_and_clamp = mask << 8 | clamp
499
+ val = cute.make_fragment(1, type(value))
500
+ val[0] = value
501
+ val_i32 = cute.recast_tensor(val, cutlass.Int32)
502
+ for i in cutlass.range_constexpr(cute.size(val_i32)):
503
+ val_i32[i] = cute.arch.shuffle_sync(val_i32[i], offset, mask_and_clamp=mask_and_clamp)
504
+ return val[0]
505
+
506
+
507
+ @dsl_user_op
508
+ def shr_u32(val: cutlass.Uint32, shift: cutlass.Uint32, *, loc=None, ip=None) -> cutlass.Uint32:
509
+ return cutlass.Uint32(
510
+ llvm.inline_asm(
511
+ T.i32(),
512
+ [cutlass.Uint32(val).ir_value(loc=loc, ip=ip), cutlass.Uint32(shift).ir_value(loc=loc, ip=ip)],
513
+ "shr.s32 $0, $1, $2;",
514
+ "=r,r,r",
515
+ has_side_effects=False,
516
+ is_align_stack=False,
517
+ asm_dialect=llvm.AsmDialect.AD_ATT,
518
+ )
519
+ )
520
+
521
+
522
+ @cute.jit
523
+ def warp_prefix_sum(val: cutlass.Int32, lane: Optional[cutlass.Int32] = None) -> cutlass.Int32:
524
+ if const_expr(lane is None):
525
+ lane = cute.arch.lane_idx()
526
+ # if cute.arch.thread_idx()[0] >= 128 and cute.arch.thread_idx()[0] < 128 + 32 and cute.arch.block_idx()[0] == 0: cute.printf("tidx = %d, val = %d", cute.arch.thread_idx()[0] % 32, val)
527
+ for i in cutlass.range_constexpr(int(math.log2(cute.arch.WARP_SIZE))):
528
+ offset = 1 << i
529
+ # Very important that we set mask_and_clamp to 0
530
+ partial_sum = cute.arch.shuffle_sync_up(val, offset=offset, mask_and_clamp=0)
531
+ if lane >= offset:
532
+ val += partial_sum
533
+ # if cute.arch.thread_idx()[0] >= 128 and cute.arch.thread_idx()[0] < 128 + 32 and cute.arch.block_idx()[0] == 0: cute.printf("tidx = %d, partial_sum = %d, val = %d", cute.arch.thread_idx()[0] % 32, partial_sum, val)
534
+ return val
535
+
536
+
537
+ @dsl_user_op
538
+ def cvt_f16x2_f32(a: float | Float32, b: float | Float32, to_dtype: Type, *, loc=None, ip=None) -> cutlass.Int32:
539
+ assert to_dtype in [cutlass.BFloat16, cutlass.Float16], "to_dtype must be BFloat16 or Float16"
540
+ return cutlass.Int32(
541
+ llvm.inline_asm(
542
+ T.i32(),
543
+ [Float32(a).ir_value(loc=loc, ip=ip), Float32(b).ir_value(loc=loc, ip=ip)],
544
+ f"cvt.rn.{'bf16x2' if to_dtype is cutlass.BFloat16 else 'f16x2'}.f32 $0, $2, $1;",
545
+ "=r,f,f",
546
+ has_side_effects=False,
547
+ is_align_stack=False,
548
+ asm_dialect=llvm.AsmDialect.AD_ATT,
549
+ )
550
+ )
551
+
552
+
553
+ @overload
554
+ def cvt_f16(src: cute.Tensor, dst: cute.Tensor) -> None: ...
555
+
556
+ @overload
557
+ def cvt_f16(src: cute.Tensor, dtype: Type[cute.Numeric]) -> cute.Tensor: ...
558
+
559
+ @cute.jit
560
+ def cvt_f16(src: cute.Tensor, dst_or_dtype):
561
+ """Convert Float32 tensor to Float16/BFloat16.
562
+
563
+ Args:
564
+ src: Source tensor with Float32 element type
565
+ dst_or_dtype: Either a destination tensor or a dtype (Float16/BFloat16)
566
+
567
+ Returns:
568
+ None if dst is a tensor, or a new tensor if dtype is provided
569
+ """
570
+ if const_expr(isinstance(dst_or_dtype, type)):
571
+ # dtype variant: create new tensor and call the tensor variant
572
+ dtype = dst_or_dtype
573
+ dst = cute.make_fragment(src.shape, dtype)
574
+ cvt_f16(src, dst)
575
+ return dst
576
+ else:
577
+ # tensor variant: write to dst
578
+ dst = dst_or_dtype
579
+ assert cute.size(dst.shape) == cute.size(src.shape), "dst and src must have the same size"
580
+ assert cute.size(src.shape) % 2 == 0, "src must have an even number of elements"
581
+ assert dst.element_type in [cutlass.BFloat16, cutlass.Float16], "dst must be BFloat16 or Float16"
582
+ assert src.element_type is Float32, "src must be Float32"
583
+ dst_i32 = cute.recast_tensor(dst, cutlass.Int32)
584
+ assert cute.size(dst_i32.shape) * 2 == cute.size(src.shape)
585
+ for i in cutlass.range_constexpr(cute.size(dst_i32)):
586
+ dst_i32[i] = cvt_f16x2_f32(src[2 * i], src[2 * i + 1], dst.element_type)
587
+
588
+
589
+ @cute.jit
590
+ @dsl_user_op
591
+ def evaluate_polynomial(x: Float32, poly: Tuple[Float32, ...], *, loc=None, ip=None) -> Float32:
592
+ deg = len(poly) - 1
593
+ out = poly[deg]
594
+ for i in cutlass.range_constexpr(deg - 1, -1, -1):
595
+ out = out * x + poly[i]
596
+ return out
597
+
598
+
599
+ @cute.jit
600
+ @dsl_user_op
601
+ def evaluate_polynomial_2(x: Float32, y: Float32, poly: Tuple[Float32, ...], *, loc=None, ip=None) -> Tuple[Float32, Float32]:
602
+ deg = len(poly) - 1
603
+ out = (poly[deg], poly[deg])
604
+ for i in cutlass.range_constexpr(deg - 1, -1, -1):
605
+ out = fma_packed_f32x2(out, (x, y), (poly[i], poly[i]))
606
+ return out
607
+
608
+
609
+ @dsl_user_op
610
+ def add_round_down(x: float | Float32, y: float | Float32, *, loc=None, ip=None) -> Float32:
611
+ # There's probably a way to call llvm or nvvm to do this instead of ptx
612
+ return cutlass.Float32(
613
+ llvm.inline_asm(
614
+ T.f32(),
615
+ [Float32(x).ir_value(loc=loc, ip=ip), Float32(y).ir_value(loc=loc, ip=ip)],
616
+ f"add.rm.ftz.f32 $0, $1, $2;",
617
+ "=f,f,f",
618
+ has_side_effects=False,
619
+ is_align_stack=False,
620
+ asm_dialect=llvm.AsmDialect.AD_ATT,
621
+ )
622
+ )
623
+
624
+
625
+ @dsl_user_op
626
+ def combine_int_frac_ex2(x_rounded: Float32, frac_ex2: Float32, *, loc=None, ip=None) -> Float32:
627
+ return cutlass.Float32(
628
+ llvm.inline_asm(
629
+ T.f32(),
630
+ [Float32(x_rounded).ir_value(loc=loc, ip=ip), Float32(frac_ex2).ir_value(loc=loc, ip=ip)],
631
+ "{\n\t"
632
+ ".reg .s32 x_rounded_i, frac_ex_i, x_rounded_e, out_i;\n\t"
633
+ "mov.b32 x_rounded_i, $1;\n\t"
634
+ "mov.b32 frac_ex_i, $2;\n\t"
635
+ "shl.b32 x_rounded_e, x_rounded_i, 23;\n\t"
636
+ # add.u32 generates IMAD instruction and add.s32 generates LEA instruction
637
+ # IMAD uses the FMA pipeline and LEA uses the ALU pipeline, afaik
638
+ "add.s32 out_i, x_rounded_e, frac_ex_i;\n\t"
639
+ "mov.b32 $0, out_i;\n\t"
640
+ "}\n",
641
+ "=f,f,f",
642
+ has_side_effects=False,
643
+ is_align_stack=False,
644
+ asm_dialect=llvm.AsmDialect.AD_ATT,
645
+ )
646
+ )
647
+
648
+
649
+ @dsl_user_op
650
+ def ex2_emulation(x: Float32, *, loc=None, ip=None) -> Float32:
651
+ # We assume x <= 127.0
652
+ poly_ex2_deg3 = (1.0, 0.695146143436431884765625, 0.227564394474029541015625, 0.077119089663028717041015625)
653
+ fp32_round_int = float(2**23 + 2**22)
654
+ x_clamped = cute.arch.fmax(x, -127.0)
655
+ # We want to round down here, so that the fractional part is in [0, 1)
656
+ x_rounded = add_round_down(x_clamped, fp32_round_int, loc=loc, ip=ip)
657
+ # The integer floor of x is now in the last 8 bits of x_rounded
658
+ # We assume the next 2 ops round to nearest even. The rounding mode is important.
659
+ x_rounded_back = x_rounded - fp32_round_int
660
+ x_frac = x_clamped - x_rounded_back
661
+ x_frac_ex2 = evaluate_polynomial(x_frac, poly_ex2_deg3, loc=loc, ip=ip)
662
+ return combine_int_frac_ex2(x_rounded, x_frac_ex2, loc=loc, ip=ip)
663
+
664
+
665
+ # TODO: check that the ex2_emulation_2 produces the same SASS as the ptx version
666
+ @dsl_user_op
667
+ def ex2_emulation_2(x: Float32, y: Float32, *, loc=None, ip=None) -> Tuple[Float32, Float32]:
668
+ # We assume x <= 127.0 and y <= 127.0
669
+ poly_ex2_deg3 = (1.0, 0.695146143436431884765625, 0.227564394474029541015625, 0.077119089663028717041015625)
670
+ fp32_round_int = float(2**23 + 2**22)
671
+ xy_clamped = (cute.arch.fmax(x, -127.0), cute.arch.fmax(y, -127.0))
672
+ # We want to round down here, so that the fractional part is in [0, 1)
673
+ xy_rounded = cute.arch.add_packed_f32x2(xy_clamped, (fp32_round_int, fp32_round_int), rnd=nvvm.RoundingModeKind.RM)
674
+ # The integer floor of x & y are now in the last 8 bits of xy_rounded
675
+ # We want the next 2 ops to round to nearest even. The rounding mode is important.
676
+ xy_rounded_back = sub_packed_f32x2(xy_rounded, (fp32_round_int, fp32_round_int))
677
+ xy_frac = sub_packed_f32x2(xy_clamped, xy_rounded_back)
678
+ xy_frac_ex2 = evaluate_polynomial_2(*xy_frac, poly_ex2_deg3, loc=loc, ip=ip)
679
+ x_out = combine_int_frac_ex2(xy_rounded[0], xy_frac_ex2[0], loc=loc, ip=ip)
680
+ y_out = combine_int_frac_ex2(xy_rounded[1], xy_frac_ex2[1], loc=loc, ip=ip)
681
+ return x_out, y_out
682
+
683
+
684
+ @dsl_user_op
685
+ def e2e_asm2(x: Float32, y: Float32, *, loc=None, ip=None) -> Tuple[Float32, Float32]:
686
+ out_f32x2 = llvm.inline_asm(
687
+ llvm.StructType.get_literal([T.f32(), T.f32()]),
688
+ [Float32(x).ir_value(loc=loc, ip=ip), Float32(y, loc=loc, ip=ip).ir_value()],
689
+ "{\n\t"
690
+ ".reg .f32 f1, f2, f3, f4, f5, f6, f7;\n\t"
691
+ ".reg .b64 l1, l2, l3, l4, l5, l6, l7, l8, l9, l10;\n\t"
692
+ ".reg .s32 r1, r2, r3, r4, r5, r6, r7, r8;\n\t"
693
+ "max.ftz.f32 f1, $2, 0fC2FE0000;\n\t"
694
+ "max.ftz.f32 f2, $3, 0fC2FE0000;\n\t"
695
+ "mov.b64 l1, {f1, f2};\n\t"
696
+ "mov.f32 f3, 0f4B400000;\n\t"
697
+ "mov.b64 l2, {f3, f3};\n\t"
698
+ "add.rm.ftz.f32x2 l7, l1, l2;\n\t"
699
+ "sub.rn.ftz.f32x2 l8, l7, l2;\n\t"
700
+ "sub.rn.ftz.f32x2 l9, l1, l8;\n\t"
701
+ "mov.f32 f7, 0f3D9DF09D;\n\t"
702
+ "mov.b64 l6, {f7, f7};\n\t"
703
+ "mov.f32 f6, 0f3E6906A4;\n\t"
704
+ "mov.b64 l5, {f6, f6};\n\t"
705
+ "mov.f32 f5, 0f3F31F519;\n\t"
706
+ "mov.b64 l4, {f5, f5};\n\t"
707
+ "mov.f32 f4, 0f3F800000;\n\t"
708
+ "mov.b64 l3, {f4, f4};\n\t"
709
+ "fma.rn.ftz.f32x2 l10, l9, l6, l5;\n\t"
710
+ "fma.rn.ftz.f32x2 l10, l10, l9, l4;\n\t"
711
+ "fma.rn.ftz.f32x2 l10, l10, l9, l3;\n\t"
712
+ "mov.b64 {r1, r2}, l7;\n\t"
713
+ "mov.b64 {r3, r4}, l10;\n\t"
714
+ "shl.b32 r5, r1, 23;\n\t"
715
+ "add.s32 r7, r5, r3;\n\t"
716
+ "shl.b32 r6, r2, 23;\n\t"
717
+ "add.s32 r8, r6, r4;\n\t"
718
+ "mov.b32 $0, r7;\n\t"
719
+ "mov.b32 $1, r8;\n\t"
720
+ "}\n",
721
+ "=r,=r,f,f",
722
+ has_side_effects=False,
723
+ is_align_stack=False,
724
+ asm_dialect=llvm.AsmDialect.AD_ATT,
725
+ )
726
+ out0 = Float32(llvm.extractvalue(T.f32(), out_f32x2, [0], loc=loc, ip=ip))
727
+ out1 = Float32(llvm.extractvalue(T.f32(), out_f32x2, [1], loc=loc, ip=ip))
728
+ return out0, out1
729
+ @dsl_user_op
730
+ def domain_offset_aligned(coord: cute.Coord, tensor: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
731
+ assert isinstance(tensor.iterator, cute.Pointer)
732
+ # We assume that applying the offset does not change the pointer alignment
733
+ new_ptr = cute.make_ptr(
734
+ tensor.element_type,
735
+ elem_pointer(tensor, coord).toint(),
736
+ tensor.memspace,
737
+ assumed_align=tensor.iterator.alignment,
738
+ )
739
+ return cute.make_tensor(new_ptr, tensor.layout)
740
+
741
+
742
+ @dsl_user_op
743
+ def domain_offset_i64(coord: cute.Coord, tensor: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
744
+ flat_coord_i64 = tuple(cutlass.Int64(c) for c in cute.flatten(coord))
745
+ flat_stride = cute.flatten_to_tuple(tensor.stride)
746
+ assert len(flat_coord_i64) == len(
747
+ flat_stride
748
+ ), "Coordinate and stride must have the same length"
749
+ offset = sum(c * s for c, s in zip(flat_coord_i64, flat_stride))
750
+ assert isinstance(tensor.iterator, cute.Pointer)
751
+ # HACK: we assume that applying the offset does not change the pointer alignment
752
+ new_ptr = cute.make_ptr(
753
+ tensor.element_type,
754
+ tensor.iterator.toint() + offset * tensor.element_type.width // 8,
755
+ tensor.memspace,
756
+ assumed_align=tensor.iterator.max_alignment,
757
+ )
758
+ return cute.make_tensor(new_ptr, tensor.layout)
759
+
760
+
761
+ @dsl_user_op
762
+ def coord_offset_i64(
763
+ tensor: cute.Tensor, idx: cute.typing.Int, dim: int, *, loc=None, ip=None
764
+ ) -> cute.Tensor:
765
+ offset = cutlass.Int64(idx) * cute.size(tensor.stride[dim])
766
+ assert isinstance(tensor.iterator, cute.Pointer)
767
+ # HACK: we assume that applying the offset does not change the pointer alignment
768
+ new_ptr = cute.make_ptr(
769
+ tensor.element_type,
770
+ tensor.iterator.toint() + offset * tensor.element_type.width // 8,
771
+ tensor.memspace,
772
+ assumed_align=tensor.iterator.max_alignment,
773
+ )
774
+ new_layout = cute.slice_(tensor.layout, (*[None] * dim, 0, *[None] * (cute.rank(tensor) - dim - 1)))
775
+ return cute.make_tensor(new_ptr, new_layout)
776
+
777
+
778
+ @cute.jit
779
+ def scalar_to_ssa(a: cute.Numeric, dtype) -> cute.TensorSSA:
780
+ """ Convert a scalar to a cute TensorSSA of shape (1,) and given dtype """
781
+ vec = cute.make_fragment(1, dtype)
782
+ vec[0] = a
783
+ return vec.load()
784
+
785
+
786
+ def ssa_to_scalar(val):
787
+ """ Could inline but nice for reflecting the above api """
788
+ return val[0]