kernels-bot commited on
Commit
abbad15
·
verified ·
1 Parent(s): 2667923

Uploaded using `kernel-builder`.

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +8 -3
  2. build/torch-cuda/__init__.py +5 -11
  3. build/torch-cuda/_ops.py +39 -3
  4. build/torch-cuda/_private_for_testing.py +16 -0
  5. build/torch-cuda/benchmark.py +0 -268
  6. build/torch-cuda/blackwell_helpers.py +48 -22
  7. build/torch-cuda/block_info.py +69 -8
  8. build/torch-cuda/block_sparse_utils.py +790 -312
  9. build/torch-cuda/block_sparsity.py +359 -73
  10. build/torch-cuda/cache_utils.py +9 -16
  11. build/torch-cuda/compute_block_sparsity.py +330 -157
  12. build/torch-cuda/cu_blocks_kernel.py +181 -0
  13. build/torch-cuda/cute_dsl_utils.py +155 -67
  14. build/torch-cuda/fa_logging.py +97 -0
  15. build/torch-cuda/flash_attn4/__init__.py +0 -26
  16. build/torch-cuda/flash_bwd.py +161 -79
  17. build/torch-cuda/flash_bwd_mla_dk_sm100.py +1185 -0
  18. build/torch-cuda/flash_bwd_mla_dq_dqv_sm100.py +1297 -0
  19. build/torch-cuda/flash_bwd_mla_sm100.py +2135 -0
  20. build/torch-cuda/flash_bwd_postprocess.py +133 -35
  21. build/torch-cuda/flash_bwd_preprocess.py +274 -164
  22. build/torch-cuda/flash_bwd_sm100.py +621 -259
  23. build/torch-cuda/flash_bwd_sm120.py +55 -0
  24. build/torch-cuda/flash_bwd_sm90.py +556 -192
  25. build/torch-cuda/flash_fwd.py +0 -0
  26. build/torch-cuda/flash_fwd_combine.py +356 -310
  27. build/torch-cuda/flash_fwd_mla_sm100.py +0 -0
  28. build/torch-cuda/flash_fwd_sm100.py +0 -0
  29. build/torch-cuda/flash_fwd_sm120.py +61 -0
  30. build/torch-cuda/flash_fwd_sm90.py +1550 -0
  31. build/torch-cuda/interface.py +0 -0
  32. build/torch-cuda/mask.py +1221 -163
  33. build/torch-cuda/metadata.json +86 -2
  34. build/torch-cuda/metadata.json.sigstore +1 -0
  35. build/torch-cuda/mma_sm100_desc.py +2 -2
  36. build/torch-cuda/named_barrier.py +31 -0
  37. build/torch-cuda/pack_gqa.py +111 -13
  38. build/torch-cuda/paged_kv.py +62 -28
  39. build/torch-cuda/pipeline.py +198 -236
  40. build/torch-cuda/prepare_scheduler.py +387 -0
  41. build/torch-cuda/quack/__init__.py +9 -0
  42. build/torch-cuda/quack/activation.py +0 -568
  43. build/torch-cuda/quack/copy_utils.py +529 -73
  44. build/torch-cuda/quack/cute_dsl_utils.py +55 -29
  45. build/torch-cuda/quack/dsl/__init__.py +12 -0
  46. build/torch-cuda/quack/dsl/cute_tensor_indexing.py +139 -0
  47. build/torch-cuda/quack/layout_utils.py +92 -4
  48. build/torch-cuda/quack/rounding.py +231 -0
  49. build/torch-cuda/quack/sm90_utils.py +33 -1
  50. build/torch-cuda/quack/utils.py +201 -0
README.md CHANGED
@@ -11,16 +11,21 @@ This is the repository card of kernels-community/flash-attn4 that has been pushe
11
  # make sure `kernels` is installed: `pip install -U kernels`
12
  from kernels import get_kernel
13
 
14
- kernel_module = get_kernel("kernels-community/flash-attn4", version=0)
15
- flash_attn_func = kernel_module.flash_attn_func
16
 
17
- flash_attn_func(...)
18
  ```
19
 
20
  ## Available functions
 
21
  - `flash_attn_func`
22
  - `flash_attn_varlen_func`
23
 
24
  ## Benchmarks
25
 
26
  No benchmark available yet.
 
 
 
 
 
11
  # make sure `kernels` is installed: `pip install -U kernels`
12
  from kernels import get_kernel
13
 
14
+ kernel_module = get_kernel("kernels-community/flash-attn4", version=1)
15
+ _private_for_testing = kernel_module._private_for_testing
16
 
17
+ _private_for_testing(...)
18
  ```
19
 
20
  ## Available functions
21
+ - `_private_for_testing`
22
  - `flash_attn_func`
23
  - `flash_attn_varlen_func`
24
 
25
  ## Benchmarks
26
 
27
  No benchmark available yet.
28
+
29
+ ## Source code
30
+
31
+ Source code of this kernel originally comes from https://github.com/Dao-AILab/flash-attention.git and it was repurposed for compatibility with `kernels`.
build/torch-cuda/__init__.py CHANGED
@@ -1,24 +1,18 @@
1
  """Flash Attention CUTE (CUDA Template Engine) implementation."""
2
 
3
- from importlib.metadata import PackageNotFoundError, version
4
-
5
- # Update when syncing again.
6
- __version__ = "4.0.0.beta4"
7
-
8
- import cutlass.cute as cute
9
 
10
  from .interface import (
11
  flash_attn_func,
12
  flash_attn_varlen_func,
13
  )
14
 
15
- from .cute_dsl_utils import cute_compile_patched
16
-
17
- # Patch cute.compile to optionally dump SASS
18
- cute.compile = cute_compile_patched
19
-
20
 
21
  __all__ = [
 
22
  "flash_attn_func",
23
  "flash_attn_varlen_func",
24
  ]
 
1
  """Flash Attention CUTE (CUDA Template Engine) implementation."""
2
 
3
+ __version__ = "4.0.0.beta30"
 
 
 
 
 
4
 
5
  from .interface import (
6
  flash_attn_func,
7
  flash_attn_varlen_func,
8
  )
9
 
10
+ # Internal symbols the tests need to reach. Not public API -- see the module
11
+ # docstring.
12
+ from . import _private_for_testing # noqa: F401
 
 
13
 
14
  __all__ = [
15
+ "_private_for_testing",
16
  "flash_attn_func",
17
  "flash_attn_varlen_func",
18
  ]
build/torch-cuda/_ops.py CHANGED
@@ -1,8 +1,44 @@
1
  import torch
2
- ops = torch.ops._flash_attn4_06b9ab8
3
 
4
- def add_op_namespace_prefix(op_name: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  """
6
  Prefix op by namespace.
7
  """
8
- return f"_flash_attn4_06b9ab8::{op_name}"
 
1
  import torch
 
2
 
3
+ def get_backend() -> str:
4
+ """Detect the backend by inspecting torch."""
5
+ import torch
6
+
7
+ if hasattr(torch.backends, "tpu"):
8
+ # torch_tpu sets torch.backends.tpu when it is imported (via
9
+ # torch's device-backend autoload), regardless of whether TPU
10
+ # hardware is present — analogous to torch.version.cuda being
11
+ # set on CUDA builds without a GPU.
12
+ return "tpu"
13
+ elif hasattr(torch, "neuron"):
14
+ # Needs to be sorted before specific Torch builds, since Neuron
15
+ # extension can be loaded into e.g. CUDA Torch builds.
16
+ return "neuron"
17
+ elif torch.version.cuda is not None:
18
+ return "cuda"
19
+ elif torch.version.hip is not None:
20
+ return "rocm"
21
+ elif torch.backends.mps.is_available():
22
+ return "metal"
23
+ elif hasattr(torch.version, "xpu") and torch.version.xpu is not None:
24
+ return "xpu"
25
+ else:
26
+ return "cpu"
27
+
28
+
29
+ def _find_ops_name() -> str:
30
+ kernel_name = "flash_attn4"
31
+ unique_id = "d8eaf3c"
32
+ backend = get_backend()
33
+ return f"_{kernel_name}_{backend}_{unique_id}"
34
+
35
+
36
+ _OPS_NAME = _find_ops_name()
37
+
38
+ ops = getattr(torch.ops, _OPS_NAME)
39
+
40
+ def add_op_namespace_prefix(op_name: str) -> str:
41
  """
42
  Prefix op by namespace.
43
  """
44
+ return f"{_OPS_NAME}::{op_name}"
build/torch-cuda/_private_for_testing.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Internal symbols reachable for the test suite.
2
+
3
+ Modules re-exported here are **not** public API. They carry no stability or
4
+ versioning guarantee and must not be used by downstream code -- they are
5
+ exposed only so that the tests can reach them through ``get_kernel``, which
6
+ loads the kernel under a generated module name.
7
+
8
+ Anything genuinely public belongs in ``__init__.py``'s ``__all__`` instead,
9
+ where changing it requires a kernel version bump.
10
+ """
11
+
12
+ from . import compute_block_sparsity # noqa: F401
13
+
14
+ __all__ = [
15
+ "compute_block_sparsity",
16
+ ]
build/torch-cuda/benchmark.py DELETED
@@ -1,268 +0,0 @@
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-cuda/blackwell_helpers.py CHANGED
@@ -10,6 +10,26 @@ from cutlass._mlir.dialects import llvm
10
  from . import mma_sm100_desc as sm100_desc
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  @cute.jit
14
  def gemm_w_idx(
15
  tiled_mma: cute.TiledMma,
@@ -108,6 +128,7 @@ def gemm_ptx(
108
  sA_layout = sA.layout if sA is not None else None
109
  sB_layout = sB.layout
110
  idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
 
111
  if const_expr(not is_ts):
112
  sA_swizzle = sA.iterator.type.swizzle_type
113
  smem_desc_base_a: int = const_expr(
@@ -177,7 +198,7 @@ def gemm_ptx(
177
  f"mov.b64 smem_desc_a, {{$1, {hex(smem_desc_a_hi)}}};\n\t"
178
  f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
179
  "setp.ne.b32 p, $3, 0;\n\t"
180
- f"tcgen05.mma.cta_group::1.kind::f16 [$0], smem_desc_a, smem_desc_b, idesc, p;\n\t"
181
  "}\n",
182
  "r,r,r,r",
183
  has_side_effects=True,
@@ -198,7 +219,7 @@ def gemm_ptx(
198
  ".reg .b64 smem_desc_b;\n\t"
199
  f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
200
  "setp.ne.b32 p, $3, 0;\n\t"
201
- f"tcgen05.mma.cta_group::1.kind::f16 [$0], [$1], smem_desc_b, {hex(idesc)}, p;\n\t"
202
  "}\n",
203
  "r,r,r,r",
204
  has_side_effects=True,
@@ -223,6 +244,7 @@ def gemm_ptx_loop(
223
  sA_layout = sA.layout if sA is not None else tCrA.layout
224
  sB_layout = sB.layout
225
  idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
 
226
  if const_expr(not is_ts):
227
  sA_swizzle = sA.iterator.type.swizzle_type
228
  smem_desc_base_a: int = const_expr(
@@ -310,14 +332,14 @@ def gemm_ptx_loop(
310
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
311
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
312
  "setp.ne.b32 p, $3, 0;\n\t"
313
- f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
314
  + "".join(
315
  (
316
  f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
317
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
318
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
319
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
320
- f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
321
  )
322
  for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
323
  )
@@ -351,14 +373,14 @@ def gemm_ptx_loop(
351
  f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
352
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
353
  "setp.ne.b32 p, $3, 0;\n\t"
354
- f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
355
  + "".join(
356
  (
357
  # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
358
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
359
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
360
  # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, 1;\n\t"
361
- f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
362
  )
363
  for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
364
  )
@@ -394,6 +416,7 @@ def gemm_ptx_partial(
394
  sA_layout = sA.layout if sA is not None else tCrA.layout
395
  sB_layout = sB.layout
396
  idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
 
397
  if const_expr(not is_ts):
398
  sA_swizzle = sA.iterator.type.swizzle_type
399
  smem_desc_base_a: int = const_expr(
@@ -477,7 +500,7 @@ def gemm_ptx_partial(
477
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo_start, smem_desc_a_hi}};\n\t"
478
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
479
  "setp.ne.b32 p, $2, 0;\n\t"
480
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
481
  + "".join(
482
  (
483
  # f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
@@ -486,7 +509,7 @@ def gemm_ptx_partial(
486
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
487
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
488
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
489
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
490
  )
491
  for k in range(1, cute.size(tCrA.shape[2]))
492
  )
@@ -554,7 +577,7 @@ def gemm_ptx_partial(
554
  f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
555
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
556
  "setp.ne.b32 p, $2, 0;\n\t"
557
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
558
  + "".join(
559
  (
560
  # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
@@ -562,7 +585,7 @@ def gemm_ptx_partial(
562
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
563
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
564
  # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, 1;\n\t"
565
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
566
  )
567
  for k in range(
568
  1,
@@ -575,7 +598,7 @@ def gemm_ptx_partial(
575
  (
576
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
577
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
578
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
579
  )
580
  for k in range(split_arrive_idx, cute.size(tCrA.shape[2]))
581
  )
@@ -613,6 +636,7 @@ def gemm_ptx_partial1(
613
  assert sA_layout is not None, "sA_layout must be provided when a_src is not TMEM"
614
  assert sA_swizzle is not None, "sA_swizzle must be provided when a_src is not TMEM"
615
  idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
 
616
  if const_expr(not is_ts):
617
  smem_desc_base_a: int = const_expr(
618
  sm100_desc.make_smem_desc_base(
@@ -706,14 +730,14 @@ def gemm_ptx_partial1(
706
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
707
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
708
  "setp.ne.b32 p, $4, 0;\n\t"
709
- 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"
710
  + "".join(
711
  (
712
  f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
713
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
714
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
715
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
716
- 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"
717
  )
718
  for k in range(1, cute.size(tCrA.shape[2]))
719
  )
@@ -751,13 +775,13 @@ def gemm_ptx_partial1(
751
  f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
752
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
753
  "setp.ne.b32 p, $3, 0;\n\t"
754
- 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"
755
  + "".join(
756
  (
757
  f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
758
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
759
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
760
- f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, {{$4, $5, $6, $7}}, 1;\n\t"
761
  )
762
  for k in range(1, cute.size(tCrA.shape[2]))
763
  )
@@ -783,6 +807,7 @@ def gemm_ptx_precomputed(
783
  mbar_phase: Optional[Int32] = None,
784
  zero_init: bool | Boolean = False,
785
  cta_group: int = 1,
 
786
  ) -> None:
787
  # acc_tmem_addr += acc_offset
788
  is_ts = const_expr(smem_desc_base_a is None)
@@ -842,7 +867,7 @@ def gemm_ptx_precomputed(
842
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo_start, smem_desc_a_hi}};\n\t"
843
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
844
  "setp.ne.b32 p, $2, 0;\n\t"
845
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
846
  + "".join(
847
  (
848
  # f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
@@ -851,7 +876,7 @@ def gemm_ptx_precomputed(
851
  f"add.s32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
852
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
853
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
854
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
855
  )
856
  for k in range(1, num_k_tile)
857
  )
@@ -911,7 +936,7 @@ def gemm_ptx_precomputed(
911
  f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
912
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
913
  "setp.ne.b32 p, $2, 0;\n\t"
914
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
915
  + "".join(
916
  (
917
  # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
@@ -919,7 +944,7 @@ def gemm_ptx_precomputed(
919
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
920
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
921
  # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, 1;\n\t"
922
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
923
  )
924
  for k in range(
925
  1,
@@ -933,7 +958,7 @@ def gemm_ptx_precomputed(
933
  # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
934
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
935
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
936
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
937
  )
938
  for k in range(num_k_tile // 4 * 3, num_k_tile)
939
  )
@@ -1019,6 +1044,7 @@ def gemm_ptx_precomputed_varname(
1019
  smem_offset: int,
1020
  zero_init: bool | Boolean = False,
1021
  cta_group: int = 1,
 
1022
  ) -> None:
1023
  is_ts = False
1024
  num_k_tile = cute.size(tCrB_layout.shape[2])
@@ -1067,7 +1093,7 @@ def gemm_ptx_precomputed_varname(
1067
  )
1068
  + "setp.ne.b32 p, $1, 0;\n\t"
1069
  # f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_0, smem_desc_b, idesc, {pred_str};\n\t"
1070
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_0, smem_desc_b_0, {idesc_var_name}, {pred_str};\n\t"
1071
  + "".join(
1072
  (
1073
  # f"mov.b64 {{smem_desc_a_lo, smem_desc_a_hi}}, {smem_var_name_prefix}_{k};\n\t"
@@ -1077,7 +1103,7 @@ def gemm_ptx_precomputed_varname(
1077
  # f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
1078
  # f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_{k}, smem_desc_b, idesc, 1;\n\t"
1079
  # f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_{k}, smem_desc_b, {idesc_var_name}, 1;\n\t"
1080
- f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_{k}, smem_desc_b_{k}, {idesc_var_name}, 1;\n\t"
1081
  )
1082
  for k in range(1, num_k_tile)
1083
  )
 
10
  from . import mma_sm100_desc as sm100_desc
11
 
12
 
13
+ def _tcgen05_mma_kind(op: cute.nvgpu.tcgen05.mma.MmaOp) -> str:
14
+ if isinstance(op, tcgen05.mma.MmaF16BF16Op):
15
+ return "f16"
16
+ if isinstance(op, tcgen05.mma.MmaTF32Op):
17
+ return "tf32"
18
+ if isinstance(op, tcgen05.mma.MmaI8Op):
19
+ return "i8"
20
+ # cutlass-dsl >=4.5.2 builds plain FP8 MMAs as MmaF8F6F4Op (make_trivial_tiled_mma's
21
+ # _F8F6F4_TYPES branch); <4.4.x returned the now-legacy MmaFP8Op. Both map to kind::f8f6f4.
22
+ if isinstance(op, (tcgen05.mma.MmaFP8Op, tcgen05.mma.MmaF8F6F4Op)):
23
+ return "f8f6f4"
24
+ if isinstance(op, tcgen05.mma.MmaMXF8Op):
25
+ return "mxf8f6f4"
26
+ if isinstance(op, tcgen05.mma.MmaMXF4Op):
27
+ return "mxf4"
28
+ if isinstance(op, tcgen05.mma.MmaMXF4NVF4Op):
29
+ return "mxf4nvf4"
30
+ raise TypeError(f"Unsupported tcgen05 MMA op kind: {type(op).__name__}")
31
+
32
+
33
  @cute.jit
34
  def gemm_w_idx(
35
  tiled_mma: cute.TiledMma,
 
128
  sA_layout = sA.layout if sA is not None else None
129
  sB_layout = sB.layout
130
  idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
131
+ kind = _tcgen05_mma_kind(op)
132
  if const_expr(not is_ts):
133
  sA_swizzle = sA.iterator.type.swizzle_type
134
  smem_desc_base_a: int = const_expr(
 
198
  f"mov.b64 smem_desc_a, {{$1, {hex(smem_desc_a_hi)}}};\n\t"
199
  f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
200
  "setp.ne.b32 p, $3, 0;\n\t"
201
+ f"tcgen05.mma.cta_group::1.kind::{kind} [$0], smem_desc_a, smem_desc_b, idesc, p;\n\t"
202
  "}\n",
203
  "r,r,r,r",
204
  has_side_effects=True,
 
219
  ".reg .b64 smem_desc_b;\n\t"
220
  f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
221
  "setp.ne.b32 p, $3, 0;\n\t"
222
+ f"tcgen05.mma.cta_group::1.kind::{kind} [$0], [$1], smem_desc_b, {hex(idesc)}, p;\n\t"
223
  "}\n",
224
  "r,r,r,r",
225
  has_side_effects=True,
 
244
  sA_layout = sA.layout if sA is not None else tCrA.layout
245
  sB_layout = sB.layout
246
  idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
247
+ kind = _tcgen05_mma_kind(op)
248
  if const_expr(not is_ts):
249
  sA_swizzle = sA.iterator.type.swizzle_type
250
  smem_desc_base_a: int = const_expr(
 
332
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
333
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
334
  "setp.ne.b32 p, $3, 0;\n\t"
335
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
336
  + "".join(
337
  (
338
  f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
339
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
340
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
341
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
342
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
343
  )
344
  for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
345
  )
 
373
  f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
374
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
375
  "setp.ne.b32 p, $3, 0;\n\t"
376
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
377
  + "".join(
378
  (
379
  # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
380
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
381
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
382
  # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, 1;\n\t"
383
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
384
  )
385
  for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
386
  )
 
416
  sA_layout = sA.layout if sA is not None else tCrA.layout
417
  sB_layout = sB.layout
418
  idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
419
+ kind = _tcgen05_mma_kind(op)
420
  if const_expr(not is_ts):
421
  sA_swizzle = sA.iterator.type.swizzle_type
422
  smem_desc_base_a: int = const_expr(
 
500
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo_start, smem_desc_a_hi}};\n\t"
501
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
502
  "setp.ne.b32 p, $2, 0;\n\t"
503
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
504
  + "".join(
505
  (
506
  # f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
 
509
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
510
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
511
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
512
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
513
  )
514
  for k in range(1, cute.size(tCrA.shape[2]))
515
  )
 
577
  f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
578
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
579
  "setp.ne.b32 p, $2, 0;\n\t"
580
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
581
  + "".join(
582
  (
583
  # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
 
585
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
586
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
587
  # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, 1;\n\t"
588
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
589
  )
590
  for k in range(
591
  1,
 
598
  (
599
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
600
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
601
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
602
  )
603
  for k in range(split_arrive_idx, cute.size(tCrA.shape[2]))
604
  )
 
636
  assert sA_layout is not None, "sA_layout must be provided when a_src is not TMEM"
637
  assert sA_swizzle is not None, "sA_swizzle must be provided when a_src is not TMEM"
638
  idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
639
+ kind = _tcgen05_mma_kind(op)
640
  if const_expr(not is_ts):
641
  smem_desc_base_a: int = const_expr(
642
  sm100_desc.make_smem_desc_base(
 
730
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
731
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
732
  "setp.ne.b32 p, $4, 0;\n\t"
733
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, {{$5, $6, $7, $8}}, {pred_str};\n\t"
734
  + "".join(
735
  (
736
  f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
737
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
738
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
739
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
740
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, {{$5, $6, $7, $8}}, 1;\n\t"
741
  )
742
  for k in range(1, cute.size(tCrA.shape[2]))
743
  )
 
775
  f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
776
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
777
  "setp.ne.b32 p, $3, 0;\n\t"
778
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], [tmem_a], smem_desc_b, idesc, {{$4, $5, $6, $7}}, {pred_str};\n\t"
779
  + "".join(
780
  (
781
  f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
782
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
783
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
784
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], [tmem_a], smem_desc_b, idesc, {{$4, $5, $6, $7}}, 1;\n\t"
785
  )
786
  for k in range(1, cute.size(tCrA.shape[2]))
787
  )
 
807
  mbar_phase: Optional[Int32] = None,
808
  zero_init: bool | Boolean = False,
809
  cta_group: int = 1,
810
+ kind: str = "f16",
811
  ) -> None:
812
  # acc_tmem_addr += acc_offset
813
  is_ts = const_expr(smem_desc_base_a is None)
 
867
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo_start, smem_desc_a_hi}};\n\t"
868
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
869
  "setp.ne.b32 p, $2, 0;\n\t"
870
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
871
  + "".join(
872
  (
873
  # f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
 
876
  f"add.s32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
877
  f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
878
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
879
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
880
  )
881
  for k in range(1, num_k_tile)
882
  )
 
936
  f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
937
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
938
  "setp.ne.b32 p, $2, 0;\n\t"
939
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
940
  + "".join(
941
  (
942
  # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
 
944
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
945
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
946
  # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, 1;\n\t"
947
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
948
  )
949
  for k in range(
950
  1,
 
958
  # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
959
  f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
960
  f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
961
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
962
  )
963
  for k in range(num_k_tile // 4 * 3, num_k_tile)
964
  )
 
1044
  smem_offset: int,
1045
  zero_init: bool | Boolean = False,
1046
  cta_group: int = 1,
1047
+ kind: str = "f16",
1048
  ) -> None:
1049
  is_ts = False
1050
  num_k_tile = cute.size(tCrB_layout.shape[2])
 
1093
  )
1094
  + "setp.ne.b32 p, $1, 0;\n\t"
1095
  # f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_0, smem_desc_b, idesc, {pred_str};\n\t"
1096
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], {smem_var_name_prefix}_0, smem_desc_b_0, {idesc_var_name}, {pred_str};\n\t"
1097
  + "".join(
1098
  (
1099
  # f"mov.b64 {{smem_desc_a_lo, smem_desc_a_hi}}, {smem_var_name_prefix}_{k};\n\t"
 
1103
  # f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
1104
  # f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_{k}, smem_desc_b, idesc, 1;\n\t"
1105
  # f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_{k}, smem_desc_b, {idesc_var_name}, 1;\n\t"
1106
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], {smem_var_name_prefix}_{k}, smem_desc_b_{k}, {idesc_var_name}, 1;\n\t"
1107
  )
1108
  for k in range(1, num_k_tile)
1109
  )
build/torch-cuda/block_info.py CHANGED
@@ -6,7 +6,7 @@ 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)
@@ -19,14 +19,18 @@ class BlockInfo:
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)):
@@ -45,11 +49,20 @@ class BlockInfo:
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
@@ -70,6 +83,37 @@ class BlockInfo:
70
  m_block_max = min(m_block_max, cute.ceil_div(m_idx_left, self.tile_m))
71
  return m_block_min, m_block_max
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  @cute.jit
74
  def get_n_block_min_causal_local_mask(
75
  self,
@@ -106,3 +150,20 @@ class BlockInfo:
106
  n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
107
  n_idx_left = n_idx - self.window_size_left
108
  return cutlass.max(n_block_min, cute.ceil_div(n_idx_left, self.tile_n))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import cutlass.cute as cute
7
  from cutlass import Int32, const_expr
8
 
9
+ from .seqlen_info import SeqlenInfoQK, SeqlenInfoQKNewK
10
 
11
 
12
  @dataclass(frozen=True)
 
19
  window_size_left: Optional[Int32] = None
20
  window_size_right: Optional[Int32] = None
21
  qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
22
+ num_splits: Int32 = 1
23
+ # If True, the scheduler packs num_splits into the top 16 bits of split_idx
24
+ pack_split_idx: cutlass.Constexpr[bool] = False
25
+ num_n_blocks_per_split: Optional[cutlass.Constexpr[Int32]] = None
26
 
27
  @cute.jit
28
  def get_n_block_min_max(
29
  self,
30
  seqlen_info: SeqlenInfoQK,
31
  m_block: Int32,
32
+ split_idx: Int32 = 0,
33
+ num_splits: Int32 = 1,
34
  ) -> Tuple[Int32, Int32]:
35
  n_block_max = cute.ceil_div(seqlen_info.seqlen_k, self.tile_n)
36
  if const_expr(self.is_causal or (self.is_local and self.window_size_right is not None)):
 
49
  n_idx_left = n_idx - self.window_size_left
50
  n_block_min = cutlass.max(n_idx_left // self.tile_n, 0)
51
  if cutlass.const_expr(self.is_split_kv):
52
+ if const_expr(self.pack_split_idx):
53
+ # Unpack num_splits from top 16 bits of split_idx (packed by scheduler)
54
+ num_splits = split_idx >> 16
55
+ split_idx = split_idx & 0xFFFF
56
+ else:
57
+ num_splits = self.num_splits
58
+ if const_expr(self.num_n_blocks_per_split is not None):
59
+ num_n_blocks_per_split = self.num_n_blocks_per_split
60
+ else:
61
+ num_n_blocks_per_split = (
62
+ Int32(0)
63
+ if n_block_max <= n_block_min
64
+ else (n_block_max - n_block_min + num_splits - 1) // num_splits
65
+ )
66
  n_block_min = n_block_min + split_idx * num_n_blocks_per_split
67
  n_block_max = cutlass.min(n_block_min + num_n_blocks_per_split, n_block_max)
68
  return n_block_min, n_block_max
 
83
  m_block_max = min(m_block_max, cute.ceil_div(m_idx_left, self.tile_m))
84
  return m_block_min, m_block_max
85
 
86
+ @cute.jit
87
+ def get_n_block_k_new_min_max(
88
+ self,
89
+ seqlen_info: SeqlenInfoQKNewK,
90
+ m_block: Int32,
91
+ split_idx: Int32 = 0,
92
+ num_splits: Int32 = 1,
93
+ ) -> Tuple[Int32, Int32]:
94
+ """Get the block range for new K tokens (append KV).
95
+
96
+ First computes the full n_block range via get_n_block_min_max, then maps
97
+ those blocks into the new-K index space by subtracting seqlen_k_og.
98
+ """
99
+ n_block_min, n_block_max = self.get_n_block_min_max(
100
+ seqlen_info,
101
+ m_block,
102
+ split_idx,
103
+ num_splits,
104
+ )
105
+ idx_k_new_min = cutlass.max(n_block_min * self.tile_n - seqlen_info.seqlen_k_og, 0)
106
+ idx_k_new_max = cutlass.min(
107
+ n_block_max * self.tile_n - seqlen_info.seqlen_k_og, seqlen_info.seqlen_k_new
108
+ )
109
+ n_block_new_min = idx_k_new_min // self.tile_n
110
+ n_block_new_max = (
111
+ cute.ceil_div(idx_k_new_max, self.tile_n)
112
+ if idx_k_new_max > idx_k_new_min
113
+ else n_block_new_min
114
+ )
115
+ return n_block_new_min, n_block_new_max
116
+
117
  @cute.jit
118
  def get_n_block_min_causal_local_mask(
119
  self,
 
150
  n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
151
  n_idx_left = n_idx - self.window_size_left
152
  return cutlass.max(n_block_min, cute.ceil_div(n_idx_left, self.tile_n))
153
+
154
+ @cute.jit
155
+ def get_n_block_max_for_m_block(
156
+ self,
157
+ seqlen_info: SeqlenInfoQK,
158
+ m_block: Int32,
159
+ ) -> Int32:
160
+ n_block_max = cute.ceil_div(seqlen_info.seqlen_k, self.tile_n)
161
+ if const_expr(self.is_causal or self.window_size_right is not None):
162
+ m_idx_max = (m_block + 1) * self.tile_m
163
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
164
+ m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
165
+ n_idx_right = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
166
+ if const_expr(self.window_size_right is not None):
167
+ n_idx_right += self.window_size_right
168
+ n_block_max = min(n_block_max, cute.ceil_div(n_idx_right, self.tile_n))
169
+ return n_block_max
build/torch-cuda/block_sparse_utils.py CHANGED
@@ -5,7 +5,7 @@ This module contains runtime execution functions for block-sparse attention kern
5
  These utilities are used by CUTE DSL kernels to produce and consume block-sparse loads.
6
  """
7
 
8
- from typing import Callable, Optional
9
  from functools import partial
10
  import math
11
  import cutlass
@@ -17,6 +17,80 @@ from .quack import copy_utils
17
  # Import data structures from block_sparsity
18
  from .block_sparsity import BlockSparseTensors
19
  from .named_barrier import NamedBarrierBwd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
 
22
  # NOTE [SM100 block-sparse empty tiles: mbarrier contract]
@@ -61,35 +135,51 @@ from .named_barrier import NamedBarrierBwd
61
  # to ack/advance, and arrives `mbar_P_full_O_rescaled` when MMA can proceed.
62
  #
63
  # Backward (SM100):
64
- # - Empty KV tile: for a given `n_block`, `total_m_block_cnt == 0` means no Q tiles contribute.
65
- # - Both the load and compute loops guard all pipeline work on `process_tile`, so empty tiles
66
- # skip producer/consumer operations entirely (no per-tile mbarrier phase handshake like forward).
 
 
 
 
67
  # - In the `not dKV_postprocess` path, dK/dV for empty KV tiles are explicitly written as zeros
68
  # even when `process_tile == False` (see `flash_bwd_sm100.py` `should_zero_dKV`).
69
 
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  @cute.jit
72
  def load_block_list(
73
  block_indices: cute.Tensor,
74
  block_count,
75
- load_q_with_first: cutlass.Constexpr,
76
  first_block_preloaded: cutlass.Constexpr,
77
  kv_producer_state,
78
- load_Q,
79
  load_K,
80
  load_V,
81
  pipeline_k,
82
  pipeline_v,
83
- use_tma_q: cutlass.Constexpr,
84
- tma_q_bytes: cutlass.Constexpr,
85
  intra_wg_overlap: cutlass.Constexpr,
 
86
  ):
87
- """Iterate over the sparse blocks and load K, V (and Q) into the pipeline.
88
- for the intra_wg_overlap case, we overlap the loads of K and V. And this
89
  means we need to pipeline the last V load from the partial block case,
90
  with the loads for the full blocks. Set first_block_preloaded when the
91
  caller has already issued the first K load for the list.
92
 
 
 
93
  Note:
94
  we iterate along the block_n indices in reverse.
95
 
@@ -98,44 +188,40 @@ def load_block_list(
98
 
99
  """
100
  if block_count > 0:
 
101
  if const_expr(not intra_wg_overlap):
102
- # Peel first iteration: the first block may need to load Q alongside K,
103
- # Parameters are already Constexpr, so no need to wrap in const_expr()
104
- n_block_first = block_indices[block_count - 1]
105
- extra_tx = tma_q_bytes if const_expr(load_q_with_first) and const_expr(use_tma_q) else 0
106
- pipeline_k.producer_acquire(kv_producer_state, extra_tx_count=extra_tx)
107
-
108
- if const_expr(load_q_with_first and use_tma_q):
109
- load_Q(tma_bar_ptr=pipeline_k.producer_get_barrier(kv_producer_state))
110
-
111
- load_K(src_idx=n_block_first, producer_state=kv_producer_state)
112
- pipeline_v.producer_acquire(kv_producer_state)
113
- load_V(src_idx=n_block_first, producer_state=kv_producer_state)
114
- kv_producer_state.advance()
115
-
116
- for offset in cutlass.range(1, block_count):
117
- n_block = block_indices[block_count - 1 - offset]
118
  pipeline_k.producer_acquire(kv_producer_state)
119
  load_K(src_idx=n_block, producer_state=kv_producer_state)
120
  pipeline_v.producer_acquire(kv_producer_state)
121
  load_V(src_idx=n_block, producer_state=kv_producer_state)
122
  kv_producer_state.advance()
123
  else:
124
- n_block_first = block_indices[block_count - 1]
 
 
 
 
125
  if const_expr(not first_block_preloaded):
126
- extra_tx = (
127
- tma_q_bytes if const_expr(load_q_with_first) and const_expr(use_tma_q) else 0
128
- )
129
- pipeline_k.producer_acquire(kv_producer_state, extra_tx_count=extra_tx)
130
-
131
- if const_expr(load_q_with_first and use_tma_q):
132
- load_Q(tma_bar_ptr=pipeline_k.producer_get_barrier(kv_producer_state))
133
-
134
  load_K(src_idx=n_block_first, producer_state=kv_producer_state)
135
 
136
- for idx in cutlass.range(block_count - 1, unroll=1):
137
- n_block_prev = block_indices[block_count - 1 - idx]
138
- n_block = block_indices[block_count - 2 - idx]
 
 
 
 
 
 
 
 
139
  kv_producer_state_prev = kv_producer_state.clone()
140
  kv_producer_state.advance()
141
  pipeline_k.producer_acquire(kv_producer_state)
@@ -153,10 +239,11 @@ def finish_overlap_v_load(
153
  load_V,
154
  pipeline_v,
155
  kv_producer_state,
 
156
  ):
157
  """Load the final V block after overlapped K/V loads."""
158
  if block_count > 0:
159
- n_block_last = block_indices[0]
160
  pipeline_v.producer_acquire(kv_producer_state)
161
  load_V(src_idx=n_block_last, producer_state=kv_producer_state)
162
  kv_producer_state.advance()
@@ -185,20 +272,21 @@ def produce_block_sparse_loads(
185
  batch_idx,
186
  head_idx,
187
  m_block,
 
188
  kv_producer_state,
189
- load_Q,
190
  load_K,
191
  load_V,
192
  pipeline_k,
193
  pipeline_v,
194
- use_tma_q: cutlass.Constexpr,
195
- tma_q_bytes: cutlass.Constexpr,
196
  intra_wg_overlap: cutlass.Constexpr,
197
  qhead_per_kvhead: cutlass.Constexpr[int] = 1,
198
  q_subtile_factor: cutlass.Constexpr[int] = 1,
 
199
  ):
200
  """Iterate over the mask and full block lists for a single tile.
201
 
 
 
202
  The masked (partial) list may leave the last V load pending when intra-warp-group
203
  overlap is enabled. The first full block must consume that pending V while
204
  issuing its own K load on the next pipeline stage.
@@ -211,40 +299,42 @@ def produce_block_sparse_loads(
211
  qhead_per_kvhead: Pack-GQA factor. When > 1, m_block is in packed space and
212
  must be converted to unpacked for sparse tensor indexing.
213
  """
214
-
215
- mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
216
-
 
217
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
218
 
219
- curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
220
- curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
221
-
222
- if const_expr(full_block_cnt is not None):
223
- curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
224
- curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
225
- else:
226
- curr_full_block_cnt = Int32(0)
227
- curr_full_block_idx = None
 
 
 
 
228
 
229
  mask_empty = curr_mask_block_cnt == 0
230
  full_empty = curr_full_block_cnt == 0
231
 
232
  if mask_empty:
233
- # No masked blocks: the full list owns the initial Q+K load.
234
  kv_producer_state = load_block_list(
235
  curr_full_block_idx,
236
  curr_full_block_cnt,
237
- load_q_with_first=True,
238
  first_block_preloaded=False,
239
  kv_producer_state=kv_producer_state,
240
- load_Q=load_Q,
241
  load_K=load_K,
242
  load_V=load_V,
243
  pipeline_k=pipeline_k,
244
  pipeline_v=pipeline_v,
245
- use_tma_q=use_tma_q,
246
- tma_q_bytes=tma_q_bytes,
247
  intra_wg_overlap=intra_wg_overlap,
 
248
  )
249
 
250
  if const_expr(intra_wg_overlap) and curr_full_block_cnt > 0:
@@ -254,24 +344,21 @@ def produce_block_sparse_loads(
254
  load_V,
255
  pipeline_v,
256
  kv_producer_state,
 
257
  )
258
  else:
259
- # Masked blocks present: load Q together with the first masked K so consumers can
260
- # start immediately. When overlap is disabled this fully drains the list.
261
  kv_producer_state = load_block_list(
262
  curr_mask_block_idx,
263
  curr_mask_block_cnt,
264
- load_q_with_first=True,
265
  first_block_preloaded=False,
266
  kv_producer_state=kv_producer_state,
267
- load_Q=load_Q,
268
  load_K=load_K,
269
  load_V=load_V,
270
  pipeline_k=pipeline_k,
271
  pipeline_v=pipeline_v,
272
- use_tma_q=use_tma_q,
273
- tma_q_bytes=tma_q_bytes,
274
  intra_wg_overlap=intra_wg_overlap,
 
275
  )
276
 
277
  if full_empty:
@@ -282,13 +369,18 @@ def produce_block_sparse_loads(
282
  load_V,
283
  pipeline_v,
284
  kv_producer_state,
 
285
  )
286
  else:
287
  if const_expr(intra_wg_overlap):
288
  # Bridge the masked list to the full list by overlapping the pending masked V
289
  # with the first full K load.
290
- n_block_mask_last = curr_mask_block_idx[0]
291
- n_block_full_first = curr_full_block_idx[curr_full_block_cnt - 1]
 
 
 
 
292
  kv_producer_state_prev = kv_producer_state.clone()
293
  kv_producer_state.advance()
294
  pipeline_k.producer_acquire(kv_producer_state)
@@ -299,17 +391,14 @@ def produce_block_sparse_loads(
299
  kv_producer_state = load_block_list(
300
  curr_full_block_idx,
301
  curr_full_block_cnt,
302
- load_q_with_first=False,
303
  first_block_preloaded=True,
304
  kv_producer_state=kv_producer_state,
305
- load_Q=load_Q,
306
  load_K=load_K,
307
  load_V=load_V,
308
  pipeline_k=pipeline_k,
309
  pipeline_v=pipeline_v,
310
- use_tma_q=use_tma_q,
311
- tma_q_bytes=tma_q_bytes,
312
  intra_wg_overlap=intra_wg_overlap,
 
313
  )
314
 
315
  kv_producer_state = finish_overlap_v_load(
@@ -318,24 +407,21 @@ def produce_block_sparse_loads(
318
  load_V,
319
  pipeline_v,
320
  kv_producer_state,
 
321
  )
322
  else:
323
- # Non-overlap path with both lists: run the full list normally (skipping the Q
324
- # reload because the masked list already issued it).
325
  kv_producer_state = load_block_list(
326
  curr_full_block_idx,
327
  curr_full_block_cnt,
328
- load_q_with_first=False,
329
  first_block_preloaded=False,
330
  kv_producer_state=kv_producer_state,
331
- load_Q=load_Q,
332
  load_K=load_K,
333
  load_V=load_V,
334
  pipeline_k=pipeline_k,
335
  pipeline_v=pipeline_v,
336
- use_tma_q=use_tma_q,
337
- tma_q_bytes=tma_q_bytes,
338
  intra_wg_overlap=intra_wg_overlap,
 
339
  )
340
 
341
  return kv_producer_state
@@ -347,7 +433,7 @@ def consume_block_sparse_loads(
347
  batch_idx,
348
  head_idx,
349
  m_block,
350
- seqlen,
351
  kv_consumer_state,
352
  mma_pv_fn,
353
  mma_one_n_block,
@@ -373,15 +459,20 @@ def consume_block_sparse_loads(
373
  qhead_per_kvhead: Pack-GQA factor. When > 1, m_block is in packed space and
374
  must be converted to unpacked for sparse tensor indexing.
375
  """
376
-
377
- mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
378
-
379
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
380
 
381
- curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
382
- curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
383
- curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
384
- curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
 
 
 
 
 
 
 
 
385
 
386
  processed_any = curr_mask_block_cnt + curr_full_block_cnt > 0
387
 
@@ -462,7 +553,7 @@ def consume_block_sparse_loads(
462
  mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
463
  kv_consumer_state = process_first_half_block(
464
  n_block=mask_n_block,
465
- seqlen=seqlen,
466
  kv_consumer_state=kv_consumer_state,
467
  mask_fn=partial(
468
  mask_fn,
@@ -478,7 +569,7 @@ def consume_block_sparse_loads(
478
  kv_consumer_state = mma_one_n_block(
479
  kv_consumer_state,
480
  n_block=mask_n_block,
481
- seqlen=seqlen,
482
  mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
483
  mask_fn=partial(mask_fn, mask_mod=mask_mod, mask_seqlen=False),
484
  )
@@ -489,7 +580,7 @@ def consume_block_sparse_loads(
489
  if curr_mask_block_cnt == 0:
490
  kv_consumer_state = process_first_half_block(
491
  n_block=full_n_block,
492
- seqlen=seqlen,
493
  kv_consumer_state=kv_consumer_state,
494
  mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
495
  score_mod_fn=score_mod_fn,
@@ -499,7 +590,7 @@ def consume_block_sparse_loads(
499
  kv_consumer_state = mma_one_n_block(
500
  kv_consumer_state,
501
  n_block=full_n_block,
502
- seqlen=seqlen,
503
  mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
504
  mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
505
  )
@@ -509,7 +600,7 @@ def consume_block_sparse_loads(
509
  kv_consumer_state = mma_one_n_block(
510
  kv_consumer_state,
511
  n_block=full_n_block,
512
- seqlen=seqlen,
513
  mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
514
  mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=False),
515
  )
@@ -525,10 +616,20 @@ def consume_block_sparse_loads(
525
  return kv_consumer_state, O_should_accumulate, processed_any
526
 
527
 
 
 
 
 
 
 
 
 
 
528
  @cute.jit
529
  def load_block_list_sm100(
530
  block_indices: cute.Tensor,
531
- block_count,
 
532
  load_q_with_first: cutlass.Constexpr,
533
  q_stage: cutlass.Constexpr,
534
  kv_producer_state,
@@ -536,11 +637,16 @@ def load_block_list_sm100(
536
  load_K,
537
  load_V,
538
  pipeline_kv,
 
539
  ):
540
- """SM100 version of load_block_list (no intra_wg_overlap, no extra_tx_count)."""
541
- if block_count > 0:
 
 
 
 
542
  # First iteration: load Q alongside K if requested
543
- n_block_first = block_indices[block_count - 1]
544
 
545
  if const_expr(load_q_with_first):
546
  # SM100 loads Q0 and optionally Q1
@@ -556,8 +662,8 @@ def load_block_list_sm100(
556
  kv_producer_state.advance()
557
 
558
  # Remaining blocks
559
- for offset in cutlass.range(1, block_count):
560
- n_block = block_indices[block_count - 1 - offset]
561
  load_K(block=n_block, producer_state=kv_producer_state, page_idx=None)
562
  kv_producer_state.advance()
563
  load_V(block=n_block, producer_state=kv_producer_state, page_idx=None)
@@ -573,6 +679,9 @@ def produce_block_sparse_loads_sm100(
573
  batch_idx,
574
  head_idx,
575
  m_block,
 
 
 
576
  kv_producer_state,
577
  load_Q,
578
  load_K,
@@ -582,6 +691,7 @@ def produce_block_sparse_loads_sm100(
582
  q_producer_phase: Int32,
583
  qhead_per_kvhead: cutlass.Constexpr,
584
  q_subtile_factor: cutlass.Constexpr,
 
585
  ):
586
  """SM100 entry point for sparse block iteration.
587
 
@@ -594,20 +704,32 @@ def produce_block_sparse_loads_sm100(
594
  """
595
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
596
 
597
- mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
598
-
599
- curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
600
- curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
601
-
602
- if const_expr(full_block_cnt is not None):
603
- curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
604
- curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
605
- else:
606
- curr_full_block_cnt = Int32(0)
607
- curr_full_block_idx = None
 
 
608
 
609
- mask_empty = curr_mask_block_cnt == 0
610
- full_empty = curr_full_block_cnt == 0
 
 
 
 
 
 
 
 
 
 
611
 
612
  q_phase_flipped = False
613
 
@@ -615,7 +737,8 @@ def produce_block_sparse_loads_sm100(
615
  # No masked blocks: process full list with Q loading
616
  kv_producer_state = load_block_list_sm100(
617
  curr_full_block_idx,
618
- curr_full_block_cnt,
 
619
  load_q_with_first=True,
620
  q_stage=q_stage,
621
  kv_producer_state=kv_producer_state,
@@ -623,13 +746,15 @@ def produce_block_sparse_loads_sm100(
623
  load_K=load_K,
624
  load_V=load_V,
625
  pipeline_kv=pipeline_kv,
 
626
  )
627
  q_phase_flipped = not full_empty
628
  else:
629
  # Process masked blocks with Q loading
630
  kv_producer_state = load_block_list_sm100(
631
  curr_mask_block_idx,
632
- curr_mask_block_cnt,
 
633
  load_q_with_first=True,
634
  q_stage=q_stage,
635
  kv_producer_state=kv_producer_state,
@@ -637,6 +762,7 @@ def produce_block_sparse_loads_sm100(
637
  load_K=load_K,
638
  load_V=load_V,
639
  pipeline_kv=pipeline_kv,
 
640
  )
641
  q_phase_flipped = True
642
 
@@ -644,7 +770,8 @@ def produce_block_sparse_loads_sm100(
644
  # Process full blocks without Q loading
645
  kv_producer_state = load_block_list_sm100(
646
  curr_full_block_idx,
647
- curr_full_block_cnt,
 
648
  load_q_with_first=False,
649
  q_stage=q_stage,
650
  kv_producer_state=kv_producer_state,
@@ -652,6 +779,7 @@ def produce_block_sparse_loads_sm100(
652
  load_K=load_K,
653
  load_V=load_V,
654
  pipeline_kv=pipeline_kv,
 
655
  )
656
 
657
  if q_phase_flipped:
@@ -666,19 +794,39 @@ def get_total_block_count(
666
  batch_idx,
667
  head_idx,
668
  m_block,
 
 
669
  qhead_per_kvhead: cutlass.Constexpr,
670
  q_subtile_factor: cutlass.Constexpr,
 
 
671
  ):
672
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
674
- mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
675
- if const_expr(full_block_cnt is not None):
676
- return (
677
- mask_block_cnt[batch_idx, head_idx, m_block_sparse]
678
- + full_block_cnt[batch_idx, head_idx, m_block_sparse]
679
- )
680
- else:
681
- return mask_block_cnt[batch_idx, head_idx, m_block_sparse]
 
 
 
682
 
683
 
684
  @cute.jit
@@ -691,7 +839,7 @@ def handle_block_sparse_empty_tile_correction_sm100(
691
  is_split_kv: cutlass.Constexpr,
692
  learnable_sink,
693
  mLSE,
694
- seqlen,
695
  m_block: Int32,
696
  head_idx: Int32,
697
  batch_idx: Int32,
@@ -699,7 +847,7 @@ def handle_block_sparse_empty_tile_correction_sm100(
699
  sScale: cute.Tensor,
700
  stats: list,
701
  correction_epilogue: Callable,
702
- thr_mma_pv: cute.core.ThrMma,
703
  tOtO: cute.Tensor,
704
  sO: cute.Tensor,
705
  pipeline_sm_stats: cutlass.pipeline.PipelineAsync,
@@ -709,6 +857,8 @@ def handle_block_sparse_empty_tile_correction_sm100(
709
  o_corr_consumer_phase: Int32,
710
  corr_epi_producer_phase: Int32,
711
  softmax_scale_log2: Float32,
 
 
712
  mO_cur: Optional[cute.Tensor] = None,
713
  gO: Optional[cute.Tensor] = None,
714
  gmem_tiled_copy_O: Optional[cute.TiledCopy] = None,
@@ -748,10 +898,11 @@ def handle_block_sparse_empty_tile_correction_sm100(
748
  if sink_val != -Float32.inf and (const_expr(not is_split_kv) or split_idx == 0):
749
  if row_max_value == -Float32.inf:
750
  row_max_value = sink_val * (LOG2_E / softmax_scale_log2)
751
- row_sum_value = Float32(1.0)
752
  else:
753
  row_sum_value = row_sum_value + cute.math.exp2(
754
- sink_val * LOG2_E - row_max_value * softmax_scale_log2, fastmath=True
 
755
  )
756
  if tidx < m_block_size:
757
  scale_row_idx = tidx + stage * m_block_size
@@ -768,17 +919,19 @@ def handle_block_sparse_empty_tile_correction_sm100(
768
 
769
  if const_expr(gmem_tiled_copy_O is None):
770
  pipeline_o_epi.producer_acquire_w_index_phase(stage, corr_epi_producer_phase)
 
 
771
  correction_epilogue(
772
  thr_mma_pv,
773
  tOtO[None, None, None, stage],
774
  tidx,
775
  stage,
776
  m_block,
777
- seqlen.seqlen_q,
778
  Float32(0.0), # zero scale ensures empty tile writes zeros into staged outputs
779
  sO[None, None, stage],
780
  mO_cur,
781
- gO[None, None, stage],
782
  gmem_tiled_copy_O,
783
  )
784
  if const_expr(gmem_tiled_copy_O is None):
@@ -794,12 +947,103 @@ def handle_block_sparse_empty_tile_correction_sm100(
794
  )
795
 
796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
797
  @cute.jit
798
  def softmax_block_sparse_sm100(
799
  blocksparse_tensors: BlockSparseTensors,
800
  batch_idx,
801
  head_idx,
802
  m_block,
 
 
 
803
  softmax_step: Callable,
804
  mask_fn: Callable,
805
  mask_fn_none: Callable,
@@ -813,104 +1057,81 @@ def softmax_block_sparse_sm100(
813
  check_m_boundary: bool,
814
  qhead_per_kvhead: cutlass.Constexpr,
815
  q_subtile_factor: cutlass.Constexpr[int] = 1,
 
816
  ):
817
  warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % 4
818
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
819
 
820
- mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
821
-
822
- curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
823
- curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
824
-
825
- if const_expr(full_block_cnt is not None):
826
- curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
827
- curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
828
- else:
829
- curr_full_block_cnt = Int32(0)
830
- curr_full_block_idx = None
 
 
831
 
832
- total_block_cnt = curr_mask_block_cnt + curr_full_block_cnt
 
 
 
 
 
 
 
 
 
 
 
 
833
 
834
  if total_block_cnt == 0:
835
- # See NOTE [SM100 block-sparse empty tiles: mbarrier contract].
836
- # pipeline_sm_stats.producer_commit_w_index(stage_idx)
837
  sm_stats_barrier.arrive_w_index(index=stage_idx * 4 + warp_idx)
838
  else:
839
- if curr_mask_block_cnt > 0:
840
- mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
841
  (
842
  mma_si_consumer_phase,
843
  si_corr_producer_phase,
844
  s0_s1_sequence_phase,
845
- ) = softmax_step(
 
 
 
 
 
 
846
  mma_si_consumer_phase,
847
  si_corr_producer_phase,
848
  s0_s1_sequence_phase,
849
- mask_n_block,
850
- is_first=True,
851
- mask_fn=partial(mask_fn, mask_seqlen=True, check_q_boundary=check_m_boundary),
852
  )
853
- for i in cutlass.range(1, curr_mask_block_cnt):
854
- mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1 - i]
855
- (
856
- mma_si_consumer_phase,
857
- si_corr_producer_phase,
858
- s0_s1_sequence_phase,
859
- ) = softmax_step(
860
- mma_si_consumer_phase,
861
- si_corr_producer_phase,
862
- s0_s1_sequence_phase,
863
- mask_n_block,
864
- mask_fn=partial(mask_fn, mask_seqlen=False, check_q_boundary=check_m_boundary),
865
- )
866
 
867
- if curr_full_block_cnt > 0:
868
- full_n_block = curr_full_block_idx[curr_full_block_cnt - 1]
869
- if curr_mask_block_cnt == 0:
870
- (
871
- mma_si_consumer_phase,
872
- si_corr_producer_phase,
873
- s0_s1_sequence_phase,
874
- ) = softmax_step(
875
- mma_si_consumer_phase,
876
- si_corr_producer_phase,
877
- s0_s1_sequence_phase,
878
- full_n_block,
879
- is_first=True,
880
- mask_fn=partial(
881
- mask_fn_none, mask_seqlen=True, check_q_boundary=check_m_boundary
882
- ),
883
- )
884
- else:
885
- (
886
- mma_si_consumer_phase,
887
- si_corr_producer_phase,
888
- s0_s1_sequence_phase,
889
- ) = softmax_step(
890
- mma_si_consumer_phase,
891
- si_corr_producer_phase,
892
- s0_s1_sequence_phase,
893
- full_n_block,
894
- is_first=False,
895
- mask_fn=partial(
896
- mask_fn_none, mask_seqlen=False, check_q_boundary=check_m_boundary
897
- ),
898
- )
899
- for i in cutlass.range(1, curr_full_block_cnt):
900
- full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
901
- (
902
- mma_si_consumer_phase,
903
- si_corr_producer_phase,
904
- s0_s1_sequence_phase,
905
- ) = softmax_step(
906
- mma_si_consumer_phase,
907
- si_corr_producer_phase,
908
- s0_s1_sequence_phase,
909
- full_n_block,
910
- mask_fn=partial(
911
- mask_fn_none, mask_seqlen=False, check_q_boundary=check_m_boundary
912
- ),
913
- )
914
 
915
  return (
916
  mma_si_consumer_phase,
@@ -940,24 +1161,24 @@ def get_total_q_block_count_bwd(
940
  batch_idx,
941
  head_idx,
942
  n_block,
943
- subtile_factor: cutlass.Constexpr = 1,
944
  m_block_max: int = 0,
945
  ):
946
  """Count total tile iterations for given n_block (KV tile) in backward."""
947
- q_block_cnt, _, full_block_cnt, _ = blocksparse_tensors
948
  total = q_block_cnt[batch_idx, head_idx, n_block]
949
  if const_expr(full_block_cnt is not None):
950
  total = total + full_block_cnt[batch_idx, head_idx, n_block]
951
- return total * subtile_factor
952
 
953
 
954
  @cute.jit
955
- def produce_block_sparse_q_loads_bwd_sm100(
956
  blocksparse_tensors: BlockSparseTensors,
957
  batch_idx,
958
  head_idx,
959
  n_block,
960
- # Pipeline states (will be returned after advancing)
961
  producer_state_Q_LSE,
962
  producer_state_dO_dPsum,
963
  # Pipelines
@@ -983,14 +1204,20 @@ def produce_block_sparse_q_loads_bwd_sm100(
983
  should_load_Q: cutlass.Constexpr,
984
  should_load_dO: cutlass.Constexpr,
985
  # Subtiling factor and bounds
986
- subtile_factor: cutlass.Constexpr = 1,
987
  m_block_max: int = 0,
 
 
 
 
 
 
 
 
 
 
988
  ):
989
- """SM100 backward block sparse loading with subtiling.
990
-
991
- Returns updated (producer_state_Q_LSE, producer_state_dO_dPsum).
992
- First iteration loads K/V alongside Q/dO; subsequent iterations load only Q/dO.
993
- """
994
  (
995
  curr_q_cnt,
996
  curr_q_idx,
@@ -998,81 +1225,325 @@ def produce_block_sparse_q_loads_bwd_sm100(
998
  curr_full_idx,
999
  loop_count,
1000
  ) = get_block_sparse_iteration_info_bwd(
1001
- blocksparse_tensors, batch_idx, head_idx, n_block, subtile_factor, m_block_max
1002
  )
1003
-
1004
- for iter_idx in cutlass.range(loop_count, unroll=1):
1005
- m_block, _ = get_m_block_from_iter_bwd(
1006
- iter_idx,
1007
  curr_q_cnt,
1008
  curr_q_idx,
1009
  curr_full_cnt,
1010
  curr_full_idx,
1011
- subtile_factor,
1012
- m_block_max,
1013
  )
1014
- m_block_safe = m_block
1015
  if m_block_max > 0:
1016
- m_block_safe = cutlass.min(m_block, m_block_max - 1)
1017
-
1018
- if iter_idx == 0:
1019
- # First block: load K/V alongside Q/dO
1020
- if const_expr(should_load_Q):
1021
- pipeline_Q.producer_acquire(producer_state_Q_LSE, extra_tx_count=tma_copy_bytes_K)
1022
- load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q_LSE))
1023
- load_Q(m_block_safe, producer_state=producer_state_Q_LSE)
1024
- pipeline_Q.producer_commit(producer_state_Q_LSE)
1025
- pipeline_LSE.producer_acquire(producer_state_Q_LSE)
1026
- with cute.arch.elect_one():
1027
- copy_stats(
1028
- gLSE[None, m_block_safe],
1029
- sLSE[None, producer_state_Q_LSE.index],
1030
- mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_Q_LSE),
1031
- )
1032
- producer_state_Q_LSE.advance()
1033
- if const_expr(should_load_dO):
1034
- pipeline_dO.producer_acquire(
1035
- producer_state_dO_dPsum, extra_tx_count=tma_copy_bytes_V
1036
  )
1037
- load_V(tma_bar_ptr=pipeline_dO.producer_get_barrier(producer_state_dO_dPsum))
1038
- load_dO(m_block_safe, producer_state=producer_state_dO_dPsum)
1039
- pipeline_dO.producer_commit(producer_state_dO_dPsum)
1040
- pipeline_dPsum.producer_acquire(producer_state_dO_dPsum)
1041
- with cute.arch.elect_one():
1042
- copy_stats(
1043
- gdPsum[None, m_block_safe],
1044
- sdPsum[None, producer_state_dO_dPsum.index],
1045
- mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dO_dPsum),
1046
- )
1047
- producer_state_dO_dPsum.advance()
1048
- else:
1049
- # Subsequent blocks: just load Q/dO (K/V already loaded)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1050
  if const_expr(should_load_Q):
 
 
 
 
 
 
 
1051
  pipeline_Q.producer_acquire(producer_state_Q_LSE)
1052
- load_Q(m_block_safe, producer_state=producer_state_Q_LSE)
1053
  pipeline_Q.producer_commit(producer_state_Q_LSE)
 
1054
  pipeline_LSE.producer_acquire(producer_state_Q_LSE)
1055
  with cute.arch.elect_one():
1056
  copy_stats(
1057
- gLSE[None, m_block_safe],
1058
  sLSE[None, producer_state_Q_LSE.index],
1059
  mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_Q_LSE),
1060
  )
1061
  producer_state_Q_LSE.advance()
 
1062
  if const_expr(should_load_dO):
1063
- pipeline_dO.producer_acquire(producer_state_dO_dPsum)
1064
- load_dO(m_block_safe, producer_state=producer_state_dO_dPsum)
 
 
 
 
 
1065
  pipeline_dO.producer_commit(producer_state_dO_dPsum)
 
1066
  pipeline_dPsum.producer_acquire(producer_state_dO_dPsum)
1067
  with cute.arch.elect_one():
1068
  copy_stats(
1069
- gdPsum[None, m_block_safe],
1070
  sdPsum[None, producer_state_dO_dPsum.index],
1071
  mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dO_dPsum),
1072
  )
1073
  producer_state_dO_dPsum.advance()
 
 
 
 
 
 
 
 
 
1074
 
1075
- return producer_state_Q_LSE, producer_state_dO_dPsum
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1076
 
1077
 
1078
  @cute.jit
@@ -1081,14 +1552,14 @@ def get_block_sparse_iteration_info_bwd(
1081
  batch_idx,
1082
  head_idx,
1083
  n_block,
1084
- subtile_factor: cutlass.Constexpr = 1,
1085
  m_block_max: int = 0,
1086
  ):
1087
  """Extract block-sparse iteration info for backward pass.
1088
 
1089
  Returns (curr_q_cnt, curr_q_idx, curr_full_cnt, curr_full_idx, total_count).
1090
  """
1091
- q_cnt, q_idx, full_cnt, full_idx = blocksparse_tensors
1092
  curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1093
  curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1094
 
@@ -1102,7 +1573,7 @@ def get_block_sparse_iteration_info_bwd(
1102
  sparse_block_count = curr_q_cnt
1103
  if const_expr(full_cnt is not None):
1104
  sparse_block_count = sparse_block_count + curr_full_cnt
1105
- total_count = sparse_block_count * subtile_factor
1106
 
1107
  return curr_q_cnt, curr_q_idx, curr_full_cnt, curr_full_idx, total_count
1108
 
@@ -1114,7 +1585,7 @@ def get_m_block_from_iter_bwd(
1114
  curr_q_idx: cute.Tensor,
1115
  curr_full_cnt,
1116
  curr_full_idx: Optional[cute.Tensor],
1117
- subtile_factor: cutlass.Constexpr = 1,
1118
  m_block_max: int = 0,
1119
  ):
1120
  """Derive m_block index and is_full_block flag from iteration index.
@@ -1123,8 +1594,8 @@ def get_m_block_from_iter_bwd(
1123
  - m_block: The actual Q-tile block index
1124
  - is_full_block: True if this is a full block (no mask_mod needed)
1125
  """
1126
- sparse_iter_idx = iter_idx // subtile_factor
1127
- subtile_offset = iter_idx % subtile_factor
1128
 
1129
  sparse_m_block = Int32(0)
1130
  is_full_block = False
@@ -1137,7 +1608,7 @@ def get_m_block_from_iter_bwd(
1137
  else:
1138
  sparse_m_block = curr_q_idx[sparse_iter_idx]
1139
 
1140
- return sparse_m_block * subtile_factor + subtile_offset, is_full_block
1141
 
1142
 
1143
  @cute.jit
@@ -1165,6 +1636,13 @@ def _load_q_do_block_sm90(
1165
  else:
1166
  pipeline_Q.producer_acquire(producer_state_Q)
1167
  load_Q(m_block, producer_state=producer_state_Q)
 
 
 
 
 
 
 
1168
  load_LSE(m_block, producer_state=producer_state_Q)
1169
 
1170
  producer_state_dO_cur = (
@@ -1202,7 +1680,7 @@ def produce_block_sparse_q_loads_bwd_sm90(
1202
  tma_copy_bytes_K,
1203
  tma_copy_bytes_V,
1204
  Q_stage_eq_dO_stage: cutlass.Constexpr,
1205
- subtile_factor: cutlass.Constexpr,
1206
  m_block_max: int,
1207
  ):
1208
  """SM90 backward block sparse loading with separate partial/full loops.
@@ -1212,7 +1690,7 @@ def produce_block_sparse_q_loads_bwd_sm90(
1212
 
1213
  Returns updated (producer_state_Q, producer_state_dO).
1214
  """
1215
- q_cnt, q_idx, full_cnt, full_idx = blocksparse_tensors
1216
  curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1217
  curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1218
 
@@ -1225,10 +1703,10 @@ def produce_block_sparse_q_loads_bwd_sm90(
1225
 
1226
  kv_loaded = False
1227
 
1228
- for iter_idx in cutlass.range(curr_q_cnt * subtile_factor, unroll=1):
1229
- sparse_idx = iter_idx // subtile_factor
1230
- subtile_offset = iter_idx % subtile_factor
1231
- m_block = curr_q_idx[sparse_idx] * subtile_factor + subtile_offset
1232
 
1233
  if m_block < m_block_max:
1234
  producer_state_Q, producer_state_dO = _load_q_do_block_sm90(
@@ -1251,10 +1729,10 @@ def produce_block_sparse_q_loads_bwd_sm90(
1251
  kv_loaded = True
1252
 
1253
  if const_expr(full_cnt is not None):
1254
- for iter_idx in cutlass.range(curr_full_cnt * subtile_factor, unroll=1):
1255
- sparse_idx = iter_idx // subtile_factor
1256
- subtile_offset = iter_idx % subtile_factor
1257
- m_block = curr_full_idx[sparse_idx] * subtile_factor + subtile_offset
1258
 
1259
  if m_block < m_block_max:
1260
  producer_state_Q, producer_state_dO = _load_q_do_block_sm90(
@@ -1295,9 +1773,9 @@ def consume_block_sparse_mma_bwd_sm90(
1295
  thr_mma_SdP,
1296
  score_mod_fn=None,
1297
  score_mod_bwd_fn=None,
1298
- subtile_factor: cutlass.Constexpr = 1,
1299
  m_block_max: int = 0,
1300
- aux_tensors=None,
1301
  fastdiv_mods=(None, None),
1302
  ):
1303
  """SM90 backward block sparse MMA consumption with separate partial/full loops.
@@ -1307,7 +1785,7 @@ def consume_block_sparse_mma_bwd_sm90(
1307
 
1308
  Returns updated (consumer_state_Q, consumer_state_dO).
1309
  """
1310
- q_cnt, q_idx, full_cnt, full_idx = blocksparse_tensors
1311
  curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1312
  curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1313
 
@@ -1330,7 +1808,7 @@ def consume_block_sparse_mma_bwd_sm90(
1330
  mask_causal=is_causal,
1331
  mask_local=is_local,
1332
  mask_mod=mask_mod,
1333
- aux_tensors=aux_tensors,
1334
  fastdiv_mods=fastdiv_mods,
1335
  )
1336
 
@@ -1343,14 +1821,14 @@ def consume_block_sparse_mma_bwd_sm90(
1343
  mask_seqlen=True,
1344
  mask_causal=is_causal,
1345
  mask_local=is_local,
1346
- aux_tensors=aux_tensors,
1347
  fastdiv_mods=fastdiv_mods,
1348
  )
1349
 
1350
- for iter_idx in cutlass.range(curr_q_cnt * subtile_factor, unroll=1):
1351
- sparse_idx = iter_idx // subtile_factor
1352
- subtile_offset = iter_idx % subtile_factor
1353
- m_block = curr_q_idx[sparse_idx] * subtile_factor + subtile_offset
1354
 
1355
  if m_block < m_block_max:
1356
  consumer_state_Q, consumer_state_dO = mma_one_m_block_fn(
@@ -1365,10 +1843,10 @@ def consume_block_sparse_mma_bwd_sm90(
1365
  dKV_accumulate = True
1366
 
1367
  if const_expr(full_cnt is not None):
1368
- for iter_idx in cutlass.range(curr_full_cnt * subtile_factor, unroll=1):
1369
- sparse_idx = iter_idx // subtile_factor
1370
- subtile_offset = iter_idx % subtile_factor
1371
- m_block = curr_full_idx[sparse_idx] * subtile_factor + subtile_offset
1372
 
1373
  if m_block < m_block_max:
1374
  consumer_state_Q, consumer_state_dO = mma_one_m_block_fn(
@@ -1390,18 +1868,18 @@ def _store_one_dQaccum_sm90(
1390
  m_block,
1391
  sdQaccum: cute.Tensor,
1392
  gdQaccum: cute.Tensor,
1393
- num_mma_warp_groups: cutlass.Constexpr,
1394
  num_threads_per_warp_group: cutlass.Constexpr,
1395
  tma_copy_bytes_dQ,
1396
  ):
1397
  """Store dQaccum for a single m_block."""
1398
- for warp_group_idx in cutlass.range_constexpr(num_mma_warp_groups):
1399
- cute.arch.cp_async_bulk_wait_group(num_mma_warp_groups - 1 - warp_group_idx, read=True)
1400
  cute.arch.barrier_arrive(
1401
  barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1402
  number_of_threads=num_threads_per_warp_group + cute.arch.WARP_SIZE,
1403
  )
1404
- for warp_group_idx in cutlass.range_constexpr(num_mma_warp_groups):
1405
  cute.arch.barrier(
1406
  barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1407
  number_of_threads=num_threads_per_warp_group + cute.arch.WARP_SIZE,
@@ -1409,7 +1887,7 @@ def _store_one_dQaccum_sm90(
1409
  with cute.arch.elect_one():
1410
  copy_utils.cpasync_reduce_bulk_add_f32(
1411
  sdQaccum[None, warp_group_idx].iterator,
1412
- gdQaccum[None, warp_group_idx, m_block].iterator,
1413
  tma_copy_bytes_dQ,
1414
  )
1415
  cute.arch.cp_async_bulk_commit_group()
@@ -1423,9 +1901,9 @@ def dQaccum_store_block_sparse_bwd_sm90(
1423
  n_block,
1424
  sdQaccum: cute.Tensor,
1425
  gdQaccum: cute.Tensor,
1426
- subtile_factor: cutlass.Constexpr,
1427
  m_block_max: int,
1428
- num_mma_warp_groups: cutlass.Constexpr,
1429
  num_threads_per_warp_group: cutlass.Constexpr,
1430
  tma_copy_bytes_dQ,
1431
  ):
@@ -1433,7 +1911,7 @@ def dQaccum_store_block_sparse_bwd_sm90(
1433
 
1434
  Iterates partial blocks first, then full blocks, matching producer/consumer order.
1435
  """
1436
- q_cnt, q_idx, full_cnt, full_idx = blocksparse_tensors
1437
  curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1438
  curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1439
 
@@ -1444,33 +1922,33 @@ def dQaccum_store_block_sparse_bwd_sm90(
1444
  curr_full_cnt = Int32(0)
1445
  curr_full_idx = None
1446
 
1447
- for iter_idx in cutlass.range(curr_q_cnt * subtile_factor, unroll=1):
1448
- sparse_idx = iter_idx // subtile_factor
1449
- subtile_offset = iter_idx % subtile_factor
1450
- m_block = curr_q_idx[sparse_idx] * subtile_factor + subtile_offset
1451
 
1452
  if m_block < m_block_max:
1453
  _store_one_dQaccum_sm90(
1454
  m_block,
1455
  sdQaccum,
1456
  gdQaccum,
1457
- num_mma_warp_groups,
1458
  num_threads_per_warp_group,
1459
  tma_copy_bytes_dQ,
1460
  )
1461
 
1462
  if const_expr(full_cnt is not None):
1463
- for iter_idx in cutlass.range(curr_full_cnt * subtile_factor, unroll=1):
1464
- sparse_idx = iter_idx // subtile_factor
1465
- subtile_offset = iter_idx % subtile_factor
1466
- m_block = curr_full_idx[sparse_idx] * subtile_factor + subtile_offset
1467
 
1468
  if m_block < m_block_max:
1469
  _store_one_dQaccum_sm90(
1470
  m_block,
1471
  sdQaccum,
1472
  gdQaccum,
1473
- num_mma_warp_groups,
1474
  num_threads_per_warp_group,
1475
  tma_copy_bytes_dQ,
1476
  )
 
5
  These utilities are used by CUTE DSL kernels to produce and consume block-sparse loads.
6
  """
7
 
8
+ from typing import Callable, Optional, Tuple
9
  from functools import partial
10
  import math
11
  import cutlass
 
17
  # Import data structures from block_sparsity
18
  from .block_sparsity import BlockSparseTensors
19
  from .named_barrier import NamedBarrierBwd
20
+ from .seqlen_info import SeqlenInfoQK
21
+ from .utils import AuxData
22
+
23
+
24
+ @cute.jit
25
+ def _get_curr_blocksparse_tensors_varlen(
26
+ head_idx: cutlass.Int32,
27
+ m_block: cutlass.Int32,
28
+ blocksparse_tensors: BlockSparseTensors,
29
+ seqlen_info: SeqlenInfoQK,
30
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
31
+ ) -> Tuple[cutlass.Int32, cute.Tensor, cutlass.Int32, Optional[cute.Tensor]]:
32
+ """Varlen path: tensors are 2D [nheads, total_m_blocks] / [nheads, total_n_blocks]."""
33
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx, *_ = blocksparse_tensors
34
+ curr_m_block = seqlen_info.m_block_offset + m_block
35
+ sparse_num_n_blocks = (
36
+ seqlen_info.num_n_blocks
37
+ if const_expr(kv_subtile_factor == 1)
38
+ else (seqlen_info.num_n_blocks + kv_subtile_factor - 1) // kv_subtile_factor
39
+ )
40
+ batch_block_idx_offset = (
41
+ seqlen_info.block_idx_offset
42
+ if const_expr(seqlen_info.has_cu_block_idx_offsets)
43
+ else seqlen_info.m_block_offset * sparse_num_n_blocks
44
+ )
45
+ curr_block_idx_offset = batch_block_idx_offset + m_block * sparse_num_n_blocks
46
+ curr_mask_block_cnt = mask_block_cnt[head_idx, curr_m_block]
47
+ curr_mask_block_idx = cute.domain_offset(curr_block_idx_offset, mask_block_idx[head_idx, None])
48
+ if const_expr(full_block_cnt is not None):
49
+ curr_full_block_cnt = full_block_cnt[head_idx, curr_m_block]
50
+ curr_full_block_idx = cute.domain_offset(
51
+ curr_block_idx_offset, full_block_idx[head_idx, None]
52
+ )
53
+ else:
54
+ curr_full_block_cnt = Int32(0)
55
+ curr_full_block_idx = None
56
+ return (curr_mask_block_cnt, curr_mask_block_idx, curr_full_block_cnt, curr_full_block_idx)
57
+
58
+
59
+ @cute.jit
60
+ def _get_curr_blocksparse_tensors(
61
+ batch_idx: cutlass.Int32,
62
+ head_idx: cutlass.Int32,
63
+ m_block: cutlass.Int32,
64
+ blocksparse_tensors: BlockSparseTensors,
65
+ ) -> Tuple[cutlass.Int32, cute.Tensor, cutlass.Int32, Optional[cute.Tensor]]:
66
+ """Fixed-length path: tensors are 4D [batch, nheads, m_block, n_block]."""
67
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx, *_ = blocksparse_tensors
68
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block]
69
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block, None]
70
+ if const_expr(full_block_cnt is not None):
71
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block]
72
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block, None]
73
+ else:
74
+ curr_full_block_cnt = Int32(0)
75
+ curr_full_block_idx = None
76
+ return (curr_mask_block_cnt, curr_mask_block_idx, curr_full_block_cnt, curr_full_block_idx)
77
+
78
+
79
+ @cute.jit
80
+ def get_curr_blocksparse_tensors(
81
+ batch_idx: cutlass.Int32,
82
+ head_idx: cutlass.Int32,
83
+ m_block: cutlass.Int32,
84
+ blocksparse_tensors: BlockSparseTensors,
85
+ seqlen_info: SeqlenInfoQK,
86
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
87
+ ) -> Tuple[cutlass.Int32, cute.Tensor, cutlass.Int32, Optional[cute.Tensor]]:
88
+ """Extract head, m_block, and batch-local blocksparsity data from blocksparse_tensors"""
89
+ if const_expr(len(blocksparse_tensors.mask_block_cnt.shape) == 2):
90
+ return _get_curr_blocksparse_tensors_varlen(
91
+ head_idx, m_block, blocksparse_tensors, seqlen_info, kv_subtile_factor
92
+ )
93
+ return _get_curr_blocksparse_tensors(batch_idx, head_idx, m_block, blocksparse_tensors)
94
 
95
 
96
  # NOTE [SM100 block-sparse empty tiles: mbarrier contract]
 
135
  # to ack/advance, and arrives `mbar_P_full_O_rescaled` when MMA can proceed.
136
  #
137
  # Backward (SM100):
138
+ # - Empty KV tile: for a given sparse `n_block`, `loop_count == 0` means no Q tiles contribute.
139
+ # - Load helpers guard their prologue/mainloop/tail on `loop_count > 0`; MMA/softmax/relay use
140
+ # the same block-sparse count as `process_tile`. Empty tiles therefore skip both producer and
141
+ # consumer operations entirely, with no synthetic per-tile mbarrier handshake like forward.
142
+ # - For non-empty tiles, producer tails are part of the non-empty load contract: the load warp emits
143
+ # exactly the Q/dO/LSE/dPsum/Qt/Kt items that the MMA and softmax consumers wait on, then tails the
144
+ # advanced producer states. Empty tiles leave producer and consumer states unchanged for the next tile.
145
  # - In the `not dKV_postprocess` path, dK/dV for empty KV tiles are explicitly written as zeros
146
  # even when `process_tile == False` (see `flash_bwd_sm100.py` `should_zero_dKV`).
147
 
148
 
149
+ @cute.jit
150
+ def sparse_physical_n_block_forward(
151
+ block_indices: cute.Tensor,
152
+ offset,
153
+ kv_subtile_factor: cutlass.Constexpr[int],
154
+ ):
155
+ """Map forward physical N offsets to physical N tile indices."""
156
+ sparse_offset = offset // kv_subtile_factor
157
+ subtile_offset = offset - sparse_offset * kv_subtile_factor
158
+ coarse_block = block_indices[sparse_offset]
159
+ return coarse_block * kv_subtile_factor + subtile_offset
160
+
161
+
162
  @cute.jit
163
  def load_block_list(
164
  block_indices: cute.Tensor,
165
  block_count,
 
166
  first_block_preloaded: cutlass.Constexpr,
167
  kv_producer_state,
 
168
  load_K,
169
  load_V,
170
  pipeline_k,
171
  pipeline_v,
 
 
172
  intra_wg_overlap: cutlass.Constexpr,
173
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
174
  ):
175
+ """Iterate over the sparse blocks and load K, V into the pipeline.
176
+ For the intra_wg_overlap case, we overlap the loads of K and V. And this
177
  means we need to pipeline the last V load from the partial block case,
178
  with the loads for the full blocks. Set first_block_preloaded when the
179
  caller has already issued the first K load for the list.
180
 
181
+ Q is loaded separately on its own mbarrier before this function is called.
182
+
183
  Note:
184
  we iterate along the block_n indices in reverse.
185
 
 
188
 
189
  """
190
  if block_count > 0:
191
+ total_blocks = block_count * kv_subtile_factor
192
  if const_expr(not intra_wg_overlap):
193
+ for offset in cutlass.range(total_blocks):
194
+ n_block = sparse_physical_n_block_forward(
195
+ block_indices,
196
+ total_blocks - 1 - offset,
197
+ kv_subtile_factor,
198
+ )
 
 
 
 
 
 
 
 
 
 
199
  pipeline_k.producer_acquire(kv_producer_state)
200
  load_K(src_idx=n_block, producer_state=kv_producer_state)
201
  pipeline_v.producer_acquire(kv_producer_state)
202
  load_V(src_idx=n_block, producer_state=kv_producer_state)
203
  kv_producer_state.advance()
204
  else:
205
+ n_block_first = sparse_physical_n_block_forward(
206
+ block_indices,
207
+ total_blocks - 1,
208
+ kv_subtile_factor,
209
+ )
210
  if const_expr(not first_block_preloaded):
211
+ pipeline_k.producer_acquire(kv_producer_state)
 
 
 
 
 
 
 
212
  load_K(src_idx=n_block_first, producer_state=kv_producer_state)
213
 
214
+ for idx in cutlass.range(total_blocks - 1, unroll=1):
215
+ n_block_prev = sparse_physical_n_block_forward(
216
+ block_indices,
217
+ total_blocks - 1 - idx,
218
+ kv_subtile_factor,
219
+ )
220
+ n_block = sparse_physical_n_block_forward(
221
+ block_indices,
222
+ total_blocks - 1 - (idx + 1),
223
+ kv_subtile_factor,
224
+ )
225
  kv_producer_state_prev = kv_producer_state.clone()
226
  kv_producer_state.advance()
227
  pipeline_k.producer_acquire(kv_producer_state)
 
239
  load_V,
240
  pipeline_v,
241
  kv_producer_state,
242
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
243
  ):
244
  """Load the final V block after overlapped K/V loads."""
245
  if block_count > 0:
246
+ n_block_last = block_indices[0] * kv_subtile_factor
247
  pipeline_v.producer_acquire(kv_producer_state)
248
  load_V(src_idx=n_block_last, producer_state=kv_producer_state)
249
  kv_producer_state.advance()
 
272
  batch_idx,
273
  head_idx,
274
  m_block,
275
+ seqlen_info: SeqlenInfoQK,
276
  kv_producer_state,
 
277
  load_K,
278
  load_V,
279
  pipeline_k,
280
  pipeline_v,
 
 
281
  intra_wg_overlap: cutlass.Constexpr,
282
  qhead_per_kvhead: cutlass.Constexpr[int] = 1,
283
  q_subtile_factor: cutlass.Constexpr[int] = 1,
284
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
285
  ):
286
  """Iterate over the mask and full block lists for a single tile.
287
 
288
+ Q is loaded separately on its own mbarrier before this function is called.
289
+
290
  The masked (partial) list may leave the last V load pending when intra-warp-group
291
  overlap is enabled. The first full block must consume that pending V while
292
  issuing its own K load on the next pipeline stage.
 
299
  qhead_per_kvhead: Pack-GQA factor. When > 1, m_block is in packed space and
300
  must be converted to unpacked for sparse tensor indexing.
301
  """
302
+ # The SM90 consumer (consume_block_sparse_loads) has no kv_subtile support yet.
303
+ assert kv_subtile_factor == 1, (
304
+ "Coarse KV blocks (kv_subtile_factor > 1) are not supported on the SM90 forward path yet."
305
+ )
306
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
307
 
308
+ (
309
+ curr_mask_block_cnt,
310
+ curr_mask_block_idx,
311
+ curr_full_block_cnt,
312
+ curr_full_block_idx,
313
+ ) = get_curr_blocksparse_tensors(
314
+ batch_idx,
315
+ head_idx,
316
+ m_block_sparse,
317
+ blocksparse_tensors,
318
+ seqlen_info,
319
+ kv_subtile_factor,
320
+ )
321
 
322
  mask_empty = curr_mask_block_cnt == 0
323
  full_empty = curr_full_block_cnt == 0
324
 
325
  if mask_empty:
326
+ # No masked blocks: the full list owns the initial K load.
327
  kv_producer_state = load_block_list(
328
  curr_full_block_idx,
329
  curr_full_block_cnt,
 
330
  first_block_preloaded=False,
331
  kv_producer_state=kv_producer_state,
 
332
  load_K=load_K,
333
  load_V=load_V,
334
  pipeline_k=pipeline_k,
335
  pipeline_v=pipeline_v,
 
 
336
  intra_wg_overlap=intra_wg_overlap,
337
+ kv_subtile_factor=kv_subtile_factor,
338
  )
339
 
340
  if const_expr(intra_wg_overlap) and curr_full_block_cnt > 0:
 
344
  load_V,
345
  pipeline_v,
346
  kv_producer_state,
347
+ kv_subtile_factor=kv_subtile_factor,
348
  )
349
  else:
350
+ # Masked blocks present. When overlap is disabled this fully drains the list.
 
351
  kv_producer_state = load_block_list(
352
  curr_mask_block_idx,
353
  curr_mask_block_cnt,
 
354
  first_block_preloaded=False,
355
  kv_producer_state=kv_producer_state,
 
356
  load_K=load_K,
357
  load_V=load_V,
358
  pipeline_k=pipeline_k,
359
  pipeline_v=pipeline_v,
 
 
360
  intra_wg_overlap=intra_wg_overlap,
361
+ kv_subtile_factor=kv_subtile_factor,
362
  )
363
 
364
  if full_empty:
 
369
  load_V,
370
  pipeline_v,
371
  kv_producer_state,
372
+ kv_subtile_factor=kv_subtile_factor,
373
  )
374
  else:
375
  if const_expr(intra_wg_overlap):
376
  # Bridge the masked list to the full list by overlapping the pending masked V
377
  # with the first full K load.
378
+ n_block_mask_last = curr_mask_block_idx[0] * kv_subtile_factor
379
+ n_block_full_first = sparse_physical_n_block_forward(
380
+ curr_full_block_idx,
381
+ curr_full_block_cnt * kv_subtile_factor - 1,
382
+ kv_subtile_factor,
383
+ )
384
  kv_producer_state_prev = kv_producer_state.clone()
385
  kv_producer_state.advance()
386
  pipeline_k.producer_acquire(kv_producer_state)
 
391
  kv_producer_state = load_block_list(
392
  curr_full_block_idx,
393
  curr_full_block_cnt,
 
394
  first_block_preloaded=True,
395
  kv_producer_state=kv_producer_state,
 
396
  load_K=load_K,
397
  load_V=load_V,
398
  pipeline_k=pipeline_k,
399
  pipeline_v=pipeline_v,
 
 
400
  intra_wg_overlap=intra_wg_overlap,
401
+ kv_subtile_factor=kv_subtile_factor,
402
  )
403
 
404
  kv_producer_state = finish_overlap_v_load(
 
407
  load_V,
408
  pipeline_v,
409
  kv_producer_state,
410
+ kv_subtile_factor=kv_subtile_factor,
411
  )
412
  else:
413
+ # Non-overlap path with both lists: run the full list normally.
 
414
  kv_producer_state = load_block_list(
415
  curr_full_block_idx,
416
  curr_full_block_cnt,
 
417
  first_block_preloaded=False,
418
  kv_producer_state=kv_producer_state,
 
419
  load_K=load_K,
420
  load_V=load_V,
421
  pipeline_k=pipeline_k,
422
  pipeline_v=pipeline_v,
 
 
423
  intra_wg_overlap=intra_wg_overlap,
424
+ kv_subtile_factor=kv_subtile_factor,
425
  )
426
 
427
  return kv_producer_state
 
433
  batch_idx,
434
  head_idx,
435
  m_block,
436
+ seqlen_info,
437
  kv_consumer_state,
438
  mma_pv_fn,
439
  mma_one_n_block,
 
459
  qhead_per_kvhead: Pack-GQA factor. When > 1, m_block is in packed space and
460
  must be converted to unpacked for sparse tensor indexing.
461
  """
 
 
 
462
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
463
 
464
+ (
465
+ curr_mask_block_cnt,
466
+ curr_mask_block_idx,
467
+ curr_full_block_cnt,
468
+ curr_full_block_idx,
469
+ ) = get_curr_blocksparse_tensors(
470
+ batch_idx,
471
+ head_idx,
472
+ m_block_sparse,
473
+ blocksparse_tensors,
474
+ seqlen_info,
475
+ )
476
 
477
  processed_any = curr_mask_block_cnt + curr_full_block_cnt > 0
478
 
 
553
  mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
554
  kv_consumer_state = process_first_half_block(
555
  n_block=mask_n_block,
556
+ seqlen=seqlen_info,
557
  kv_consumer_state=kv_consumer_state,
558
  mask_fn=partial(
559
  mask_fn,
 
569
  kv_consumer_state = mma_one_n_block(
570
  kv_consumer_state,
571
  n_block=mask_n_block,
572
+ seqlen=seqlen_info,
573
  mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
574
  mask_fn=partial(mask_fn, mask_mod=mask_mod, mask_seqlen=False),
575
  )
 
580
  if curr_mask_block_cnt == 0:
581
  kv_consumer_state = process_first_half_block(
582
  n_block=full_n_block,
583
+ seqlen=seqlen_info,
584
  kv_consumer_state=kv_consumer_state,
585
  mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
586
  score_mod_fn=score_mod_fn,
 
590
  kv_consumer_state = mma_one_n_block(
591
  kv_consumer_state,
592
  n_block=full_n_block,
593
+ seqlen=seqlen_info,
594
  mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
595
  mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
596
  )
 
600
  kv_consumer_state = mma_one_n_block(
601
  kv_consumer_state,
602
  n_block=full_n_block,
603
+ seqlen=seqlen_info,
604
  mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
605
  mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=False),
606
  )
 
616
  return kv_consumer_state, O_should_accumulate, processed_any
617
 
618
 
619
+ @cute.jit
620
+ def split_block_range(block_count, split_idx: Int32, num_splits: Int32):
621
+ """Return the half-open block-list range assigned to one SplitKV partition."""
622
+ blocks_per_split = cute.ceil_div(block_count, num_splits)
623
+ block_begin = cutlass.min(split_idx * blocks_per_split, block_count)
624
+ block_end = cutlass.min(block_begin + blocks_per_split, block_count)
625
+ return block_begin, block_end
626
+
627
+
628
  @cute.jit
629
  def load_block_list_sm100(
630
  block_indices: cute.Tensor,
631
+ block_begin,
632
+ block_end,
633
  load_q_with_first: cutlass.Constexpr,
634
  q_stage: cutlass.Constexpr,
635
  kv_producer_state,
 
637
  load_K,
638
  load_V,
639
  pipeline_kv,
640
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
641
  ):
642
+ """SM100 sparse load loop over physical N tiles from coarse metadata."""
643
+ physical_n_block = partial(
644
+ sparse_physical_n_block_forward, block_indices, kv_subtile_factor=kv_subtile_factor
645
+ )
646
+ physical_block_count = block_end - block_begin
647
+ if physical_block_count > 0:
648
  # First iteration: load Q alongside K if requested
649
+ n_block_first = physical_n_block(block_end - 1)
650
 
651
  if const_expr(load_q_with_first):
652
  # SM100 loads Q0 and optionally Q1
 
662
  kv_producer_state.advance()
663
 
664
  # Remaining blocks
665
+ for offset in cutlass.range(1, physical_block_count):
666
+ n_block = physical_n_block(block_end - 1 - offset)
667
  load_K(block=n_block, producer_state=kv_producer_state, page_idx=None)
668
  kv_producer_state.advance()
669
  load_V(block=n_block, producer_state=kv_producer_state, page_idx=None)
 
679
  batch_idx,
680
  head_idx,
681
  m_block,
682
+ seqlen_info: SeqlenInfoQK,
683
+ split_idx: Int32,
684
+ num_splits: Int32,
685
  kv_producer_state,
686
  load_Q,
687
  load_K,
 
691
  q_producer_phase: Int32,
692
  qhead_per_kvhead: cutlass.Constexpr,
693
  q_subtile_factor: cutlass.Constexpr,
694
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
695
  ):
696
  """SM100 entry point for sparse block iteration.
697
 
 
704
  """
705
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
706
 
707
+ (
708
+ curr_mask_block_cnt,
709
+ curr_mask_block_idx,
710
+ curr_full_block_cnt,
711
+ curr_full_block_idx,
712
+ ) = get_curr_blocksparse_tensors(
713
+ batch_idx,
714
+ head_idx,
715
+ m_block_sparse,
716
+ blocksparse_tensors,
717
+ seqlen_info,
718
+ kv_subtile_factor,
719
+ )
720
 
721
+ mask_begin, mask_end = split_block_range(
722
+ curr_mask_block_cnt * kv_subtile_factor,
723
+ split_idx,
724
+ num_splits,
725
+ )
726
+ full_begin, full_end = split_block_range(
727
+ curr_full_block_cnt * kv_subtile_factor,
728
+ split_idx,
729
+ num_splits,
730
+ )
731
+ mask_empty = mask_begin == mask_end
732
+ full_empty = full_begin == full_end
733
 
734
  q_phase_flipped = False
735
 
 
737
  # No masked blocks: process full list with Q loading
738
  kv_producer_state = load_block_list_sm100(
739
  curr_full_block_idx,
740
+ full_begin,
741
+ full_end,
742
  load_q_with_first=True,
743
  q_stage=q_stage,
744
  kv_producer_state=kv_producer_state,
 
746
  load_K=load_K,
747
  load_V=load_V,
748
  pipeline_kv=pipeline_kv,
749
+ kv_subtile_factor=kv_subtile_factor,
750
  )
751
  q_phase_flipped = not full_empty
752
  else:
753
  # Process masked blocks with Q loading
754
  kv_producer_state = load_block_list_sm100(
755
  curr_mask_block_idx,
756
+ mask_begin,
757
+ mask_end,
758
  load_q_with_first=True,
759
  q_stage=q_stage,
760
  kv_producer_state=kv_producer_state,
 
762
  load_K=load_K,
763
  load_V=load_V,
764
  pipeline_kv=pipeline_kv,
765
+ kv_subtile_factor=kv_subtile_factor,
766
  )
767
  q_phase_flipped = True
768
 
 
770
  # Process full blocks without Q loading
771
  kv_producer_state = load_block_list_sm100(
772
  curr_full_block_idx,
773
+ full_begin,
774
+ full_end,
775
  load_q_with_first=False,
776
  q_stage=q_stage,
777
  kv_producer_state=kv_producer_state,
 
779
  load_K=load_K,
780
  load_V=load_V,
781
  pipeline_kv=pipeline_kv,
782
+ kv_subtile_factor=kv_subtile_factor,
783
  )
784
 
785
  if q_phase_flipped:
 
794
  batch_idx,
795
  head_idx,
796
  m_block,
797
+ split_idx: Int32,
798
+ num_splits: Int32,
799
  qhead_per_kvhead: cutlass.Constexpr,
800
  q_subtile_factor: cutlass.Constexpr,
801
+ seqlen_info: SeqlenInfoQK,
802
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
803
  ):
804
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
805
+ (
806
+ curr_mask_block_cnt,
807
+ _,
808
+ curr_full_block_cnt,
809
+ _,
810
+ ) = get_curr_blocksparse_tensors(
811
+ batch_idx,
812
+ head_idx,
813
+ m_block_sparse,
814
+ blocksparse_tensors,
815
+ seqlen_info,
816
+ kv_subtile_factor,
817
+ )
818
 
819
+ mask_begin, mask_end = split_block_range(
820
+ curr_mask_block_cnt * kv_subtile_factor,
821
+ split_idx,
822
+ num_splits,
823
+ )
824
+ full_begin, full_end = split_block_range(
825
+ curr_full_block_cnt * kv_subtile_factor,
826
+ split_idx,
827
+ num_splits,
828
+ )
829
+ return mask_end - mask_begin + full_end - full_begin
830
 
831
 
832
  @cute.jit
 
839
  is_split_kv: cutlass.Constexpr,
840
  learnable_sink,
841
  mLSE,
842
+ seqlen_info,
843
  m_block: Int32,
844
  head_idx: Int32,
845
  batch_idx: Int32,
 
847
  sScale: cute.Tensor,
848
  stats: list,
849
  correction_epilogue: Callable,
850
+ thr_mma_pv: cute.ThrMma,
851
  tOtO: cute.Tensor,
852
  sO: cute.Tensor,
853
  pipeline_sm_stats: cutlass.pipeline.PipelineAsync,
 
857
  o_corr_consumer_phase: Int32,
858
  corr_epi_producer_phase: Int32,
859
  softmax_scale_log2: Float32,
860
+ max_offset: Float32,
861
+ max_offset_scale: Float32,
862
  mO_cur: Optional[cute.Tensor] = None,
863
  gO: Optional[cute.Tensor] = None,
864
  gmem_tiled_copy_O: Optional[cute.TiledCopy] = None,
 
898
  if sink_val != -Float32.inf and (const_expr(not is_split_kv) or split_idx == 0):
899
  if row_max_value == -Float32.inf:
900
  row_max_value = sink_val * (LOG2_E / softmax_scale_log2)
901
+ row_sum_value = max_offset_scale
902
  else:
903
  row_sum_value = row_sum_value + cute.math.exp2(
904
+ sink_val * LOG2_E - row_max_value * softmax_scale_log2 + max_offset,
905
+ fastmath=True,
906
  )
907
  if tidx < m_block_size:
908
  scale_row_idx = tidx + stage * m_block_size
 
919
 
920
  if const_expr(gmem_tiled_copy_O is None):
921
  pipeline_o_epi.producer_acquire_w_index_phase(stage, corr_epi_producer_phase)
922
+
923
+ gO_stage = gO[None, None, stage] if const_expr(gO is not None) else None
924
  correction_epilogue(
925
  thr_mma_pv,
926
  tOtO[None, None, None, stage],
927
  tidx,
928
  stage,
929
  m_block,
930
+ seqlen_info.seqlen_q,
931
  Float32(0.0), # zero scale ensures empty tile writes zeros into staged outputs
932
  sO[None, None, stage],
933
  mO_cur,
934
+ gO_stage,
935
  gmem_tiled_copy_O,
936
  )
937
  if const_expr(gmem_tiled_copy_O is None):
 
947
  )
948
 
949
 
950
+ @cute.jit
951
+ def softmax_block_sparse_sm100_list(
952
+ block_indices: cute.Tensor,
953
+ block_end,
954
+ split_block_cnt,
955
+ kv_subtile_factor: cutlass.Constexpr[int],
956
+ softmax_step: Callable,
957
+ mask_fn_base: Callable,
958
+ mma_si_consumer_phase: Int32,
959
+ si_corr_producer_phase: Int32,
960
+ s0_s1_sequence_phase: Int32,
961
+ check_m_boundary: bool,
962
+ is_first_block: bool,
963
+ allow_unmasked_inner_blocks: cutlass.Constexpr[bool],
964
+ ):
965
+ """Run one reverse sparse list while masking only the first coarse KV fragment."""
966
+ mask_fn_seqlen = partial(mask_fn_base, mask_seqlen=True, check_q_boundary=check_m_boundary)
967
+ physical_n_block = partial(
968
+ sparse_physical_n_block_forward, block_indices, kv_subtile_factor=kv_subtile_factor
969
+ )
970
+
971
+ n_block = physical_n_block(block_end - 1)
972
+ if is_first_block:
973
+ (
974
+ mma_si_consumer_phase,
975
+ si_corr_producer_phase,
976
+ s0_s1_sequence_phase,
977
+ ) = softmax_step(
978
+ mma_si_consumer_phase,
979
+ si_corr_producer_phase,
980
+ s0_s1_sequence_phase,
981
+ n_block,
982
+ is_first=True,
983
+ mask_fn=mask_fn_seqlen,
984
+ )
985
+ else:
986
+ (
987
+ mma_si_consumer_phase,
988
+ si_corr_producer_phase,
989
+ s0_s1_sequence_phase,
990
+ ) = softmax_step(
991
+ mma_si_consumer_phase,
992
+ si_corr_producer_phase,
993
+ s0_s1_sequence_phase,
994
+ n_block,
995
+ is_first=False,
996
+ mask_fn=mask_fn_seqlen,
997
+ )
998
+
999
+ first_fragment_count = cutlass.min(
1000
+ split_block_cnt,
1001
+ (block_end - 1) % kv_subtile_factor + 1,
1002
+ )
1003
+ for j in cutlass.range_constexpr(1, kv_subtile_factor):
1004
+ if j < first_fragment_count:
1005
+ (
1006
+ mma_si_consumer_phase,
1007
+ si_corr_producer_phase,
1008
+ s0_s1_sequence_phase,
1009
+ ) = softmax_step(
1010
+ mma_si_consumer_phase,
1011
+ si_corr_producer_phase,
1012
+ s0_s1_sequence_phase,
1013
+ physical_n_block(block_end - 1 - j),
1014
+ mask_fn=mask_fn_seqlen,
1015
+ )
1016
+ for i in cutlass.range(first_fragment_count, split_block_cnt):
1017
+ (
1018
+ mma_si_consumer_phase,
1019
+ si_corr_producer_phase,
1020
+ s0_s1_sequence_phase,
1021
+ ) = softmax_step(
1022
+ mma_si_consumer_phase,
1023
+ si_corr_producer_phase,
1024
+ s0_s1_sequence_phase,
1025
+ physical_n_block(block_end - 1 - i),
1026
+ mask_fn=None
1027
+ if const_expr(allow_unmasked_inner_blocks and check_m_boundary is False)
1028
+ else partial(
1029
+ mask_fn_base,
1030
+ mask_seqlen=False,
1031
+ check_q_boundary=check_m_boundary,
1032
+ ),
1033
+ )
1034
+
1035
+ return mma_si_consumer_phase, si_corr_producer_phase, s0_s1_sequence_phase
1036
+
1037
+
1038
  @cute.jit
1039
  def softmax_block_sparse_sm100(
1040
  blocksparse_tensors: BlockSparseTensors,
1041
  batch_idx,
1042
  head_idx,
1043
  m_block,
1044
+ seqlen_info: SeqlenInfoQK,
1045
+ split_idx: Int32,
1046
+ num_splits: Int32,
1047
  softmax_step: Callable,
1048
  mask_fn: Callable,
1049
  mask_fn_none: Callable,
 
1057
  check_m_boundary: bool,
1058
  qhead_per_kvhead: cutlass.Constexpr,
1059
  q_subtile_factor: cutlass.Constexpr[int] = 1,
1060
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
1061
  ):
1062
  warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % 4
1063
  m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
1064
 
1065
+ (
1066
+ curr_mask_block_cnt,
1067
+ curr_mask_block_idx,
1068
+ curr_full_block_cnt,
1069
+ curr_full_block_idx,
1070
+ ) = get_curr_blocksparse_tensors(
1071
+ batch_idx,
1072
+ head_idx,
1073
+ m_block_sparse,
1074
+ blocksparse_tensors,
1075
+ seqlen_info,
1076
+ kv_subtile_factor,
1077
+ )
1078
 
1079
+ mask_begin, mask_end = split_block_range(
1080
+ curr_mask_block_cnt * kv_subtile_factor,
1081
+ split_idx,
1082
+ num_splits,
1083
+ )
1084
+ full_begin, full_end = split_block_range(
1085
+ curr_full_block_cnt * kv_subtile_factor,
1086
+ split_idx,
1087
+ num_splits,
1088
+ )
1089
+ split_mask_block_cnt = mask_end - mask_begin
1090
+ split_full_block_cnt = full_end - full_begin
1091
+ total_block_cnt = split_mask_block_cnt + split_full_block_cnt
1092
 
1093
  if total_block_cnt == 0:
 
 
1094
  sm_stats_barrier.arrive_w_index(index=stage_idx * 4 + warp_idx)
1095
  else:
1096
+ if split_mask_block_cnt > 0:
 
1097
  (
1098
  mma_si_consumer_phase,
1099
  si_corr_producer_phase,
1100
  s0_s1_sequence_phase,
1101
+ ) = softmax_block_sparse_sm100_list(
1102
+ curr_mask_block_idx,
1103
+ mask_end,
1104
+ split_mask_block_cnt,
1105
+ kv_subtile_factor,
1106
+ softmax_step,
1107
+ mask_fn,
1108
  mma_si_consumer_phase,
1109
  si_corr_producer_phase,
1110
  s0_s1_sequence_phase,
1111
+ check_m_boundary,
1112
+ True,
1113
+ False,
1114
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
1115
 
1116
+ if split_full_block_cnt > 0:
1117
+ (
1118
+ mma_si_consumer_phase,
1119
+ si_corr_producer_phase,
1120
+ s0_s1_sequence_phase,
1121
+ ) = softmax_block_sparse_sm100_list(
1122
+ curr_full_block_idx,
1123
+ full_end,
1124
+ split_full_block_cnt,
1125
+ kv_subtile_factor,
1126
+ softmax_step,
1127
+ mask_fn_none,
1128
+ mma_si_consumer_phase,
1129
+ si_corr_producer_phase,
1130
+ s0_s1_sequence_phase,
1131
+ check_m_boundary,
1132
+ split_mask_block_cnt == 0,
1133
+ True,
1134
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1135
 
1136
  return (
1137
  mma_si_consumer_phase,
 
1161
  batch_idx,
1162
  head_idx,
1163
  n_block,
1164
+ q_subtile_factor: cutlass.Constexpr = 1,
1165
  m_block_max: int = 0,
1166
  ):
1167
  """Count total tile iterations for given n_block (KV tile) in backward."""
1168
+ q_block_cnt, _, full_block_cnt, _, *_ = blocksparse_tensors
1169
  total = q_block_cnt[batch_idx, head_idx, n_block]
1170
  if const_expr(full_block_cnt is not None):
1171
  total = total + full_block_cnt[batch_idx, head_idx, n_block]
1172
+ return total * q_subtile_factor
1173
 
1174
 
1175
  @cute.jit
1176
+ def produce_block_sparse_q_loads_bwd_sm100_default(
1177
  blocksparse_tensors: BlockSparseTensors,
1178
  batch_idx,
1179
  head_idx,
1180
  n_block,
1181
+ # Pipeline states returned after advancing
1182
  producer_state_Q_LSE,
1183
  producer_state_dO_dPsum,
1184
  # Pipelines
 
1204
  should_load_Q: cutlass.Constexpr,
1205
  should_load_dO: cutlass.Constexpr,
1206
  # Subtiling factor and bounds
1207
+ q_subtile_factor: cutlass.Constexpr = 1,
1208
  m_block_max: int = 0,
1209
+ # Optional 2CTA state for hdim <= 128
1210
+ use_2cta_instrs: cutlass.Constexpr = False,
1211
+ producer_state_Qt=None,
1212
+ producer_state_Kt=None,
1213
+ pipeline_Qt=None,
1214
+ pipeline_Kt=None,
1215
+ load_Qt=None,
1216
+ load_Kt=None,
1217
+ load_dOt=None,
1218
+ tma_copy_bytes_dO=0,
1219
  ):
1220
+ """Produce SM100 backward block-sparse Q/dO loads for 1CTA and non-hdim192 2CTA."""
 
 
 
 
1221
  (
1222
  curr_q_cnt,
1223
  curr_q_idx,
 
1225
  curr_full_idx,
1226
  loop_count,
1227
  ) = get_block_sparse_iteration_info_bwd(
1228
+ blocksparse_tensors, batch_idx, head_idx, n_block, q_subtile_factor, m_block_max
1229
  )
1230
+ # 2 cta peels the first loop for Qt path; so we guard the whole block if loopcount == 0
1231
+ if loop_count > Int32(0):
1232
+ first_m_block, _ = get_m_block_from_iter_bwd(
1233
+ Int32(0),
1234
  curr_q_cnt,
1235
  curr_q_idx,
1236
  curr_full_cnt,
1237
  curr_full_idx,
1238
+ q_subtile_factor=q_subtile_factor,
1239
+ m_block_max=m_block_max,
1240
  )
1241
+ # with q_subtile > 1 we need to guard against fully OOB regions
1242
  if m_block_max > 0:
1243
+ first_m_block = cutlass.min(first_m_block, m_block_max - 1)
1244
+
1245
+ if const_expr(should_load_Q):
1246
+ pipeline_Q.producer_acquire(producer_state_Q_LSE, extra_tx_count=tma_copy_bytes_K)
1247
+ load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q_LSE))
1248
+ load_Q(first_m_block, producer_state=producer_state_Q_LSE)
1249
+ pipeline_Q.producer_commit(producer_state_Q_LSE)
1250
+
1251
+ pipeline_LSE.producer_acquire(producer_state_Q_LSE)
1252
+ with cute.arch.elect_one():
1253
+ copy_stats(
1254
+ gLSE[None, first_m_block],
1255
+ sLSE[None, producer_state_Q_LSE.index],
1256
+ mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_Q_LSE),
 
 
 
 
 
 
1257
  )
1258
+ producer_state_Q_LSE.advance()
1259
+
1260
+ if const_expr(should_load_dO):
1261
+ pipeline_dO.producer_acquire(
1262
+ producer_state_dO_dPsum,
1263
+ extra_tx_count=tma_copy_bytes_V + tma_copy_bytes_dO
1264
+ if const_expr(load_dOt is not None)
1265
+ else tma_copy_bytes_V,
1266
+ )
1267
+ load_V(tma_bar_ptr=pipeline_dO.producer_get_barrier(producer_state_dO_dPsum))
1268
+ load_dO(first_m_block, producer_state=producer_state_dO_dPsum)
1269
+ if const_expr(load_dOt is not None):
1270
+ load_dOt(first_m_block, producer_state=producer_state_dO_dPsum)
1271
+ pipeline_dO.producer_commit(producer_state_dO_dPsum)
1272
+
1273
+ pipeline_dPsum.producer_acquire(producer_state_dO_dPsum)
1274
+ with cute.arch.elect_one():
1275
+ copy_stats(
1276
+ gdPsum[None, first_m_block],
1277
+ sdPsum[None, producer_state_dO_dPsum.index],
1278
+ mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dO_dPsum),
1279
+ )
1280
+ producer_state_dO_dPsum.advance()
1281
+
1282
+ if const_expr(use_2cta_instrs):
1283
+ assert load_Kt is not None and pipeline_Kt is not None
1284
+ assert producer_state_Kt is not None
1285
+ pipeline_Kt.producer_acquire(producer_state_Kt)
1286
+ load_Kt(tma_bar_ptr=pipeline_Kt.producer_get_barrier(producer_state_Kt))
1287
+ pipeline_Kt.producer_commit(producer_state_Kt)
1288
+ producer_state_Kt.advance()
1289
+
1290
+ prev_m_block = first_m_block
1291
+ for iter_idx in cutlass.range(Int32(1), loop_count, unroll=1):
1292
+ m_block, _ = get_m_block_from_iter_bwd(
1293
+ iter_idx,
1294
+ curr_q_cnt,
1295
+ curr_q_idx,
1296
+ curr_full_cnt,
1297
+ curr_full_idx,
1298
+ q_subtile_factor=q_subtile_factor,
1299
+ m_block_max=m_block_max,
1300
+ )
1301
+ if m_block_max > 0:
1302
+ m_block = cutlass.min(m_block, m_block_max - 1)
1303
  if const_expr(should_load_Q):
1304
+ if const_expr(load_Qt is not None):
1305
+ assert pipeline_Qt is not None and producer_state_Qt is not None
1306
+ pipeline_Qt.producer_acquire(producer_state_Qt)
1307
+ load_Qt(prev_m_block, producer_state=producer_state_Qt)
1308
+ pipeline_Qt.producer_commit(producer_state_Qt)
1309
+ producer_state_Qt.advance()
1310
+
1311
  pipeline_Q.producer_acquire(producer_state_Q_LSE)
1312
+ load_Q(m_block, producer_state=producer_state_Q_LSE)
1313
  pipeline_Q.producer_commit(producer_state_Q_LSE)
1314
+
1315
  pipeline_LSE.producer_acquire(producer_state_Q_LSE)
1316
  with cute.arch.elect_one():
1317
  copy_stats(
1318
+ gLSE[None, m_block],
1319
  sLSE[None, producer_state_Q_LSE.index],
1320
  mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_Q_LSE),
1321
  )
1322
  producer_state_Q_LSE.advance()
1323
+
1324
  if const_expr(should_load_dO):
1325
+ pipeline_dO.producer_acquire(
1326
+ producer_state_dO_dPsum,
1327
+ extra_tx_count=tma_copy_bytes_dO if const_expr(load_dOt is not None) else 0,
1328
+ )
1329
+ load_dO(m_block, producer_state=producer_state_dO_dPsum)
1330
+ if const_expr(load_dOt is not None):
1331
+ load_dOt(m_block, producer_state=producer_state_dO_dPsum)
1332
  pipeline_dO.producer_commit(producer_state_dO_dPsum)
1333
+
1334
  pipeline_dPsum.producer_acquire(producer_state_dO_dPsum)
1335
  with cute.arch.elect_one():
1336
  copy_stats(
1337
+ gdPsum[None, m_block],
1338
  sdPsum[None, producer_state_dO_dPsum.index],
1339
  mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dO_dPsum),
1340
  )
1341
  producer_state_dO_dPsum.advance()
1342
+ prev_m_block = m_block
1343
+
1344
+ if const_expr(should_load_Q):
1345
+ if const_expr(load_Qt is not None):
1346
+ assert pipeline_Qt is not None and producer_state_Qt is not None
1347
+ pipeline_Qt.producer_acquire(producer_state_Qt)
1348
+ load_Qt(prev_m_block, producer_state=producer_state_Qt)
1349
+ pipeline_Qt.producer_commit(producer_state_Qt)
1350
+ producer_state_Qt.advance()
1351
 
1352
+ pipeline_Q.producer_tail(producer_state_Q_LSE.clone())
1353
+ pipeline_LSE.producer_tail(producer_state_Q_LSE)
1354
+ if const_expr(load_Qt is not None):
1355
+ pipeline_Qt.producer_tail(producer_state_Qt)
1356
+ if const_expr(should_load_dO):
1357
+ pipeline_dO.producer_tail(producer_state_dO_dPsum.clone())
1358
+ pipeline_dPsum.producer_tail(producer_state_dO_dPsum)
1359
+
1360
+ return producer_state_Q_LSE, producer_state_dO_dPsum, producer_state_Qt, producer_state_Kt
1361
+
1362
+
1363
+ @cute.jit
1364
+ def produce_block_sparse_q_loads_bwd_sm100_2cta_hdim192(
1365
+ blocksparse_tensors: BlockSparseTensors,
1366
+ batch_idx,
1367
+ head_idx,
1368
+ n_block,
1369
+ # Pipeline states returned after advancing
1370
+ producer_state_Q_Qt,
1371
+ producer_state_O_Ot,
1372
+ producer_state_LSE,
1373
+ producer_state_dPsum,
1374
+ # Pipelines
1375
+ pipeline_Q,
1376
+ pipeline_LSE,
1377
+ pipeline_dO,
1378
+ pipeline_dPsum,
1379
+ pipeline_Qt,
1380
+ # Load functions
1381
+ load_K,
1382
+ load_V,
1383
+ load_Q,
1384
+ load_dO,
1385
+ load_Qt,
1386
+ load_Kt,
1387
+ load_dOt,
1388
+ copy_stats,
1389
+ # Global tensors for LSE/dPsum
1390
+ gLSE,
1391
+ sLSE,
1392
+ gdPsum,
1393
+ sdPsum,
1394
+ # TMA copy bytes for extra_tx_count
1395
+ tma_copy_bytes_K,
1396
+ tma_copy_bytes_V,
1397
+ # Subtiling factor and bounds
1398
+ q_subtile_factor: cutlass.Constexpr = 1,
1399
+ m_block_max: int = 0,
1400
+ ):
1401
+ """Produce SM100 backward block-sparse Q/dO loads for the hdim192 2CTA schedule."""
1402
+ (
1403
+ curr_q_cnt,
1404
+ curr_q_idx,
1405
+ curr_full_cnt,
1406
+ curr_full_idx,
1407
+ loop_count,
1408
+ ) = get_block_sparse_iteration_info_bwd(
1409
+ blocksparse_tensors, batch_idx, head_idx, n_block, q_subtile_factor, m_block_max
1410
+ )
1411
+
1412
+ if loop_count > Int32(0):
1413
+ first_m_block, _ = get_m_block_from_iter_bwd(
1414
+ Int32(0),
1415
+ curr_q_cnt,
1416
+ curr_q_idx,
1417
+ curr_full_cnt,
1418
+ curr_full_idx,
1419
+ q_subtile_factor=q_subtile_factor,
1420
+ m_block_max=m_block_max,
1421
+ )
1422
+
1423
+ # with q_subtile > 1 we need to guard against fully OOB regions
1424
+ if m_block_max > 0:
1425
+ first_m_block = cutlass.min(first_m_block, m_block_max - 1)
1426
+
1427
+ # K & Q (for S)
1428
+ pipeline_Q.producer_acquire(
1429
+ producer_state_Q_Qt,
1430
+ extra_tx_count=tma_copy_bytes_K,
1431
+ )
1432
+ load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q_Qt))
1433
+ load_Q(first_m_block, producer_state=producer_state_Q_Qt)
1434
+ pipeline_Q.producer_commit(producer_state_Q_Qt)
1435
+ producer_state_Q_Qt.advance()
1436
+
1437
+ # LSE
1438
+ pipeline_LSE.producer_acquire(producer_state_LSE)
1439
+ with cute.arch.elect_one():
1440
+ copy_stats(
1441
+ gLSE[None, first_m_block],
1442
+ sLSE[None, producer_state_LSE.index],
1443
+ mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_LSE),
1444
+ )
1445
+ producer_state_LSE.advance()
1446
+
1447
+ # dOt + V, for dP.T = V @ dO.T
1448
+ pipeline_dO.producer_acquire(
1449
+ producer_state_O_Ot,
1450
+ extra_tx_count=tma_copy_bytes_V,
1451
+ )
1452
+ load_V(tma_bar_ptr=pipeline_dO.producer_get_barrier(producer_state_O_Ot))
1453
+ load_dOt(first_m_block, producer_state=producer_state_O_Ot)
1454
+ pipeline_dO.producer_commit(producer_state_O_Ot)
1455
+ producer_state_O_Ot.advance()
1456
+
1457
+ # dPsum
1458
+ pipeline_dPsum.producer_acquire(producer_state_dPsum)
1459
+ with cute.arch.elect_one():
1460
+ copy_stats(
1461
+ gdPsum[None, first_m_block],
1462
+ sdPsum[None, producer_state_dPsum.index],
1463
+ mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dPsum),
1464
+ )
1465
+ producer_state_dPsum.advance()
1466
+
1467
+ # Qt, for dK = dS.T @ Q
1468
+ pipeline_Qt.producer_acquire(
1469
+ producer_state_Q_Qt,
1470
+ extra_tx_count=tma_copy_bytes_K,
1471
+ )
1472
+ load_Qt(first_m_block, producer_state=producer_state_Q_Qt)
1473
+ load_Kt(tma_bar_ptr=pipeline_Qt.producer_get_barrier(producer_state_Q_Qt))
1474
+ pipeline_Qt.producer_commit(producer_state_Q_Qt)
1475
+ producer_state_Q_Qt.advance()
1476
+
1477
+ # dO, for dV = P.T @ dO
1478
+ pipeline_dO.producer_acquire(producer_state_O_Ot)
1479
+ load_dO(first_m_block, producer_state=producer_state_O_Ot)
1480
+ pipeline_dO.producer_commit(producer_state_O_Ot)
1481
+ producer_state_O_Ot.advance()
1482
+
1483
+ # 2CTA: [lse | Q | dOt | dPsum | Qt | dO]
1484
+ for iter_idx in cutlass.range(Int32(1), loop_count, unroll=1):
1485
+ m_block, _ = get_m_block_from_iter_bwd(
1486
+ iter_idx,
1487
+ curr_q_cnt,
1488
+ curr_q_idx,
1489
+ curr_full_cnt,
1490
+ curr_full_idx,
1491
+ q_subtile_factor=q_subtile_factor,
1492
+ m_block_max=m_block_max,
1493
+ )
1494
+ if m_block_max > 0:
1495
+ m_block = cutlass.min(m_block, m_block_max - 1)
1496
+
1497
+ # LSE
1498
+ pipeline_LSE.producer_acquire(producer_state_LSE)
1499
+ with cute.arch.elect_one():
1500
+ copy_stats(
1501
+ gLSE[None, m_block],
1502
+ sLSE[None, producer_state_LSE.index],
1503
+ mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_LSE),
1504
+ )
1505
+ producer_state_LSE.advance()
1506
+
1507
+ # Q
1508
+ pipeline_Q.producer_acquire(producer_state_Q_Qt)
1509
+ load_Q(m_block, producer_state=producer_state_Q_Qt)
1510
+ pipeline_Q.producer_commit(producer_state_Q_Qt)
1511
+ producer_state_Q_Qt.advance()
1512
+
1513
+ # dPsum
1514
+ pipeline_dPsum.producer_acquire(producer_state_dPsum)
1515
+ with cute.arch.elect_one():
1516
+ copy_stats(
1517
+ gdPsum[None, m_block],
1518
+ sdPsum[None, producer_state_dPsum.index],
1519
+ mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dPsum),
1520
+ )
1521
+ producer_state_dPsum.advance()
1522
+
1523
+ # dOt, for dP.T = V @ dO.T
1524
+ pipeline_dO.producer_acquire(producer_state_O_Ot)
1525
+ load_dOt(m_block, producer_state=producer_state_O_Ot)
1526
+ pipeline_dO.producer_commit(producer_state_O_Ot)
1527
+ producer_state_O_Ot.advance()
1528
+
1529
+ # Qt, for dK = dS.T @ Q
1530
+ pipeline_Qt.producer_acquire(producer_state_Q_Qt)
1531
+ load_Qt(m_block, producer_state=producer_state_Q_Qt)
1532
+ pipeline_Qt.producer_commit(producer_state_Q_Qt)
1533
+ producer_state_Q_Qt.advance()
1534
+
1535
+ # dO, for dV = P.T @ dO
1536
+ pipeline_dO.producer_acquire(producer_state_O_Ot)
1537
+ load_dO(m_block, producer_state=producer_state_O_Ot)
1538
+ pipeline_dO.producer_commit(producer_state_O_Ot)
1539
+ producer_state_O_Ot.advance()
1540
+
1541
+ pipeline_Q.producer_tail(producer_state_Q_Qt)
1542
+ pipeline_LSE.producer_tail(producer_state_LSE)
1543
+ pipeline_dO.producer_tail(producer_state_O_Ot)
1544
+ pipeline_dPsum.producer_tail(producer_state_dPsum)
1545
+
1546
+ return producer_state_Q_Qt, producer_state_O_Ot, producer_state_LSE, producer_state_dPsum
1547
 
1548
 
1549
  @cute.jit
 
1552
  batch_idx,
1553
  head_idx,
1554
  n_block,
1555
+ q_subtile_factor: cutlass.Constexpr = 1,
1556
  m_block_max: int = 0,
1557
  ):
1558
  """Extract block-sparse iteration info for backward pass.
1559
 
1560
  Returns (curr_q_cnt, curr_q_idx, curr_full_cnt, curr_full_idx, total_count).
1561
  """
1562
+ q_cnt, q_idx, full_cnt, full_idx, *_ = blocksparse_tensors
1563
  curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1564
  curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1565
 
 
1573
  sparse_block_count = curr_q_cnt
1574
  if const_expr(full_cnt is not None):
1575
  sparse_block_count = sparse_block_count + curr_full_cnt
1576
+ total_count = sparse_block_count * q_subtile_factor
1577
 
1578
  return curr_q_cnt, curr_q_idx, curr_full_cnt, curr_full_idx, total_count
1579
 
 
1585
  curr_q_idx: cute.Tensor,
1586
  curr_full_cnt,
1587
  curr_full_idx: Optional[cute.Tensor],
1588
+ q_subtile_factor: cutlass.Constexpr = 1,
1589
  m_block_max: int = 0,
1590
  ):
1591
  """Derive m_block index and is_full_block flag from iteration index.
 
1594
  - m_block: The actual Q-tile block index
1595
  - is_full_block: True if this is a full block (no mask_mod needed)
1596
  """
1597
+ sparse_iter_idx = iter_idx // q_subtile_factor
1598
+ subtile_offset = iter_idx % q_subtile_factor
1599
 
1600
  sparse_m_block = Int32(0)
1601
  is_full_block = False
 
1608
  else:
1609
  sparse_m_block = curr_q_idx[sparse_iter_idx]
1610
 
1611
+ return sparse_m_block * q_subtile_factor + subtile_offset, is_full_block
1612
 
1613
 
1614
  @cute.jit
 
1636
  else:
1637
  pipeline_Q.producer_acquire(producer_state_Q)
1638
  load_Q(m_block, producer_state=producer_state_Q)
1639
+ if load_kv:
1640
+ # Wait for bwd preprocess to finish writing LSE and dPsum, and zeroing dQaccum. Same
1641
+ # point in the load order as the dense arm's wait in flash_bwd_sm90.load(): after this
1642
+ # block's K/Q loads, before the first load that reads preprocess output.
1643
+ # load_kv is the tile's first-block marker -- the caller flips kv_loaded in the same
1644
+ # branch that loads a block -- so this runs once per tile, like the dense arm's.
1645
+ cute.arch.griddepcontrol_wait()
1646
  load_LSE(m_block, producer_state=producer_state_Q)
1647
 
1648
  producer_state_dO_cur = (
 
1680
  tma_copy_bytes_K,
1681
  tma_copy_bytes_V,
1682
  Q_stage_eq_dO_stage: cutlass.Constexpr,
1683
+ q_subtile_factor: cutlass.Constexpr,
1684
  m_block_max: int,
1685
  ):
1686
  """SM90 backward block sparse loading with separate partial/full loops.
 
1690
 
1691
  Returns updated (producer_state_Q, producer_state_dO).
1692
  """
1693
+ q_cnt, q_idx, full_cnt, full_idx, *_ = blocksparse_tensors
1694
  curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1695
  curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1696
 
 
1703
 
1704
  kv_loaded = False
1705
 
1706
+ for iter_idx in cutlass.range(curr_q_cnt * q_subtile_factor, unroll=1):
1707
+ sparse_idx = iter_idx // q_subtile_factor
1708
+ subtile_offset = iter_idx % q_subtile_factor
1709
+ m_block = curr_q_idx[sparse_idx] * q_subtile_factor + subtile_offset
1710
 
1711
  if m_block < m_block_max:
1712
  producer_state_Q, producer_state_dO = _load_q_do_block_sm90(
 
1729
  kv_loaded = True
1730
 
1731
  if const_expr(full_cnt is not None):
1732
+ for iter_idx in cutlass.range(curr_full_cnt * q_subtile_factor, unroll=1):
1733
+ sparse_idx = iter_idx // q_subtile_factor
1734
+ subtile_offset = iter_idx % q_subtile_factor
1735
+ m_block = curr_full_idx[sparse_idx] * q_subtile_factor + subtile_offset
1736
 
1737
  if m_block < m_block_max:
1738
  producer_state_Q, producer_state_dO = _load_q_do_block_sm90(
 
1773
  thr_mma_SdP,
1774
  score_mod_fn=None,
1775
  score_mod_bwd_fn=None,
1776
+ q_subtile_factor: cutlass.Constexpr = 1,
1777
  m_block_max: int = 0,
1778
+ aux_data: AuxData = AuxData(),
1779
  fastdiv_mods=(None, None),
1780
  ):
1781
  """SM90 backward block sparse MMA consumption with separate partial/full loops.
 
1785
 
1786
  Returns updated (consumer_state_Q, consumer_state_dO).
1787
  """
1788
+ q_cnt, q_idx, full_cnt, full_idx, *_ = blocksparse_tensors
1789
  curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1790
  curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1791
 
 
1808
  mask_causal=is_causal,
1809
  mask_local=is_local,
1810
  mask_mod=mask_mod,
1811
+ aux_data=aux_data,
1812
  fastdiv_mods=fastdiv_mods,
1813
  )
1814
 
 
1821
  mask_seqlen=True,
1822
  mask_causal=is_causal,
1823
  mask_local=is_local,
1824
+ aux_data=aux_data,
1825
  fastdiv_mods=fastdiv_mods,
1826
  )
1827
 
1828
+ for iter_idx in cutlass.range(curr_q_cnt * q_subtile_factor, unroll=1):
1829
+ sparse_idx = iter_idx // q_subtile_factor
1830
+ subtile_offset = iter_idx % q_subtile_factor
1831
+ m_block = curr_q_idx[sparse_idx] * q_subtile_factor + subtile_offset
1832
 
1833
  if m_block < m_block_max:
1834
  consumer_state_Q, consumer_state_dO = mma_one_m_block_fn(
 
1843
  dKV_accumulate = True
1844
 
1845
  if const_expr(full_cnt is not None):
1846
+ for iter_idx in cutlass.range(curr_full_cnt * q_subtile_factor, unroll=1):
1847
+ sparse_idx = iter_idx // q_subtile_factor
1848
+ subtile_offset = iter_idx % q_subtile_factor
1849
+ m_block = curr_full_idx[sparse_idx] * q_subtile_factor + subtile_offset
1850
 
1851
  if m_block < m_block_max:
1852
  consumer_state_Q, consumer_state_dO = mma_one_m_block_fn(
 
1868
  m_block,
1869
  sdQaccum: cute.Tensor,
1870
  gdQaccum: cute.Tensor,
1871
+ num_dQ_warp_groups: cutlass.Constexpr,
1872
  num_threads_per_warp_group: cutlass.Constexpr,
1873
  tma_copy_bytes_dQ,
1874
  ):
1875
  """Store dQaccum for a single m_block."""
1876
+ for warp_group_idx in cutlass.range_constexpr(num_dQ_warp_groups):
1877
+ cute.arch.cp_async_bulk_wait_group(num_dQ_warp_groups - 1 - warp_group_idx, read=True)
1878
  cute.arch.barrier_arrive(
1879
  barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1880
  number_of_threads=num_threads_per_warp_group + cute.arch.WARP_SIZE,
1881
  )
1882
+ for warp_group_idx in cutlass.range_constexpr(num_dQ_warp_groups):
1883
  cute.arch.barrier(
1884
  barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1885
  number_of_threads=num_threads_per_warp_group + cute.arch.WARP_SIZE,
 
1887
  with cute.arch.elect_one():
1888
  copy_utils.cpasync_reduce_bulk_add_f32(
1889
  sdQaccum[None, warp_group_idx].iterator,
1890
+ gdQaccum[(None, warp_group_idx), m_block].iterator,
1891
  tma_copy_bytes_dQ,
1892
  )
1893
  cute.arch.cp_async_bulk_commit_group()
 
1901
  n_block,
1902
  sdQaccum: cute.Tensor,
1903
  gdQaccum: cute.Tensor,
1904
+ q_subtile_factor: cutlass.Constexpr,
1905
  m_block_max: int,
1906
+ num_dQ_warp_groups: cutlass.Constexpr,
1907
  num_threads_per_warp_group: cutlass.Constexpr,
1908
  tma_copy_bytes_dQ,
1909
  ):
 
1911
 
1912
  Iterates partial blocks first, then full blocks, matching producer/consumer order.
1913
  """
1914
+ q_cnt, q_idx, full_cnt, full_idx, *_ = blocksparse_tensors
1915
  curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1916
  curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1917
 
 
1922
  curr_full_cnt = Int32(0)
1923
  curr_full_idx = None
1924
 
1925
+ for iter_idx in cutlass.range(curr_q_cnt * q_subtile_factor, unroll=1):
1926
+ sparse_idx = iter_idx // q_subtile_factor
1927
+ subtile_offset = iter_idx % q_subtile_factor
1928
+ m_block = curr_q_idx[sparse_idx] * q_subtile_factor + subtile_offset
1929
 
1930
  if m_block < m_block_max:
1931
  _store_one_dQaccum_sm90(
1932
  m_block,
1933
  sdQaccum,
1934
  gdQaccum,
1935
+ num_dQ_warp_groups,
1936
  num_threads_per_warp_group,
1937
  tma_copy_bytes_dQ,
1938
  )
1939
 
1940
  if const_expr(full_cnt is not None):
1941
+ for iter_idx in cutlass.range(curr_full_cnt * q_subtile_factor, unroll=1):
1942
+ sparse_idx = iter_idx // q_subtile_factor
1943
+ subtile_offset = iter_idx % q_subtile_factor
1944
+ m_block = curr_full_idx[sparse_idx] * q_subtile_factor + subtile_offset
1945
 
1946
  if m_block < m_block_max:
1947
  _store_one_dQaccum_sm90(
1948
  m_block,
1949
  sdQaccum,
1950
  gdQaccum,
1951
+ num_dQ_warp_groups,
1952
  num_threads_per_warp_group,
1953
  tma_copy_bytes_dQ,
1954
  )
build/torch-cuda/block_sparsity.py CHANGED
@@ -17,13 +17,23 @@ def ceildiv(a: int, b: int) -> int:
17
  class BlockSparseTensors(NamedTuple):
18
  mask_block_cnt: cute.Tensor
19
  mask_block_idx: cute.Tensor
20
- full_block_cnt: cute.Tensor | None
21
- full_block_idx: cute.Tensor | None
 
 
 
 
22
 
23
  def __new_from_mlir_values__(self, values):
24
- if len(values) == 2:
25
- values = (*values, None, None)
26
- return BlockSparseTensors(*values)
 
 
 
 
 
 
27
 
28
 
29
  class BlockSparseTensorsTorch(NamedTuple):
@@ -31,7 +41,184 @@ class BlockSparseTensorsTorch(NamedTuple):
31
  mask_block_idx: torch.Tensor
32
  full_block_cnt: torch.Tensor | None = None
33
  full_block_idx: torch.Tensor | None = None
 
 
34
  block_size: tuple[int, int] | None = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
 
37
  def _expand_sparsity_tensor(
@@ -61,8 +248,8 @@ def _check_and_expand_block(
61
  name: str,
62
  cnt: torch.Tensor | None,
63
  idx: torch.Tensor | None,
64
- expected_count_shape: Tuple[int, int, int],
65
- expected_index_shape: Tuple[int, int, int, int],
66
  context: str | None,
67
  hint: str | Callable[[], str] | None,
68
  ) -> Tuple[torch.Tensor | None, torch.Tensor | None]:
@@ -81,12 +268,37 @@ def _check_and_expand_block(
81
  expanded_cnt = _expand_sparsity_tensor(
82
  cnt, expected_count_shape, f"{name}_block_cnt", context, hint
83
  )
 
 
 
 
 
 
84
  expanded_idx = _expand_sparsity_tensor(
85
  idx, expected_index_shape, f"{name}_block_idx", context, hint
86
  )
87
  return expanded_cnt, expanded_idx
88
 
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  def get_block_sparse_expected_shapes(
91
  batch_size: int,
92
  num_head: int,
@@ -124,7 +336,7 @@ def infer_block_sparse_expected_shapes(
124
  Expectations:
125
  - mask_block_cnt is (B, H, M) and mask_block_idx is (B, H, M, N).
126
  - Batch/head dims may be 1 for broadcast, or match the requested sizes.
127
- - sparse_block_size_kv must match tile_n.
128
  - sparse_block_size_q must be a multiple of q_stage * tile_m.
129
  - If sparse_block_size_q is omitted and seqlen_q/num_m_blocks is ambiguous,
130
  the caller must provide block_size to disambiguate. TODO will make this required in a future PR.
@@ -133,24 +345,23 @@ def infer_block_sparse_expected_shapes(
133
  base_n_block = n_block_size
134
  if sparse_block_size_kv is None:
135
  sparse_block_size_kv = base_n_block
136
- if sparse_block_size_kv != base_n_block:
137
- raise ValueError(f"Block sparse tensors{context} require BLOCK_SIZE_KV={base_n_block}.")
 
 
138
  if tensors.mask_block_idx is None:
139
  raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
140
  num_m_blocks = tensors.mask_block_idx.shape[2]
141
 
142
  if sparse_block_size_q is None:
143
- min_block_size = ceildiv(seqlen_q, num_m_blocks)
144
- if num_m_blocks == 1:
145
- max_block_size = seqlen_q
146
- else:
147
- max_block_size = (seqlen_q - 1) // (num_m_blocks - 1)
148
- if max_block_size != min_block_size and base_m_block != 1:
149
  raise ValueError(
150
  f"Block sparse tensors{context} require explicit sparse_block_size[0] "
151
  f"to disambiguate block size for seqlen_q={seqlen_q} and num_m_blocks={num_m_blocks}."
152
  )
153
- sparse_block_size_q = min_block_size
 
154
 
155
  if sparse_block_size_q % base_m_block != 0:
156
  raise ValueError(
@@ -186,9 +397,11 @@ def infer_block_sparse_expected_shapes(
186
  raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
187
  if mask_block_cnt.shape[2] != mask_block_idx.shape[2]:
188
  raise ValueError(f"Block sparse tensors{context} must share the same m-block dimension.")
189
- if mask_block_idx.shape[3] != expected_n_blocks:
 
 
190
  raise ValueError(
191
- f"Block sparse tensors{context} n-block dimension must be {expected_n_blocks}."
192
  )
193
  if expected_m_blocks != num_m_blocks:
194
  raise ValueError(
@@ -206,17 +419,19 @@ def get_block_sparse_expected_shapes_bwd(
206
  seqlen_k: int,
207
  m_block_size: int,
208
  n_block_size: int,
209
- subtile_factor: int,
 
210
  ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
211
  """Return (expected_count_shape, expected_index_shape) for backward block sparse normalization.
212
 
213
  Backward uses Q-direction indexing (transposed from forward), where shapes are
214
  indexed by N-blocks first, then M-blocks. The sparse_block_size_q is determined
215
- by subtile_factor * m_block_size.
216
  """
217
- sparse_block_size_q = subtile_factor * m_block_size
 
218
  expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
219
- expected_n_blocks = ceildiv(seqlen_k, n_block_size)
220
  expected_count_shape = (batch_size, num_head, expected_n_blocks)
221
  expected_index_shape = (batch_size, num_head, expected_n_blocks, expected_m_blocks)
222
  return expected_count_shape, expected_index_shape
@@ -225,8 +440,8 @@ def get_block_sparse_expected_shapes_bwd(
225
  def normalize_block_sparse_tensors(
226
  tensors: BlockSparseTensorsTorch,
227
  *,
228
- expected_count_shape: Tuple[int, int, int],
229
- expected_index_shape: Tuple[int, int, int, int],
230
  context: str | None = None,
231
  hint: str | Callable[[], str] | None = None,
232
  ) -> BlockSparseTensorsTorch:
@@ -257,12 +472,39 @@ def normalize_block_sparse_tensors(
257
  if full_cnt is not None and mask_cnt.device != full_cnt.device:
258
  raise ValueError("All block sparse tensors must be on the same device")
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  return BlockSparseTensorsTorch(
261
  mask_block_cnt=mask_cnt,
262
  mask_block_idx=mask_idx,
263
  full_block_cnt=full_cnt,
264
  full_block_idx=full_idx,
 
 
265
  block_size=tensors.block_size,
 
 
 
266
  )
267
 
268
 
@@ -294,6 +536,8 @@ def get_block_sparse_broadcast_pattern(
294
  tensors.mask_block_idx,
295
  tensors.full_block_cnt,
296
  tensors.full_block_idx,
 
 
297
  ):
298
  if tensor is not None:
299
  patterns.append(get_broadcast_dims(tensor))
@@ -302,6 +546,15 @@ def get_block_sparse_broadcast_pattern(
302
  return tuple(patterns)
303
 
304
 
 
 
 
 
 
 
 
 
 
305
  def normalize_block_sparse_config(
306
  tensors: BlockSparseTensorsTorch,
307
  *,
@@ -311,40 +564,67 @@ def normalize_block_sparse_config(
311
  seqlen_k: int,
312
  block_size: tuple[int, int],
313
  q_stage: int,
314
- ) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None, int]:
 
 
 
 
 
 
 
 
315
  m_block_size, n_block_size = block_size
316
  if tensors.block_size is None:
317
- sparse_block_size_q, sparse_block_size_kv = q_stage * m_block_size, n_block_size
318
  else:
319
  sparse_block_size_q, sparse_block_size_kv = tensors.block_size
320
- if sparse_block_size_kv != n_block_size:
 
 
 
 
 
321
  raise ValueError(
322
  f"Block sparsity requires sparse_block_size[1]={n_block_size} to match tile_n."
323
  )
324
- expected_count_shape, expected_index_shape, q_subtile_factor = (
325
- infer_block_sparse_expected_shapes(
326
- tensors,
327
- batch_size=batch_size,
328
- num_head=num_head,
329
- seqlen_q=seqlen_q,
330
- seqlen_k=seqlen_k,
331
- m_block_size=m_block_size,
332
- n_block_size=n_block_size,
333
- q_stage=q_stage,
334
- context="forward",
335
- sparse_block_size_q=sparse_block_size_q,
336
- sparse_block_size_kv=sparse_block_size_kv,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  )
338
- )
339
  normalized_tensors = normalize_block_sparse_tensors(
340
  tensors,
341
  expected_count_shape=expected_count_shape,
342
  expected_index_shape=expected_index_shape,
343
  )
344
- return (
345
- normalized_tensors,
346
- get_block_sparse_broadcast_pattern(normalized_tensors),
347
- q_subtile_factor,
 
348
  )
349
 
350
 
@@ -356,21 +636,24 @@ def normalize_block_sparse_config_bwd(
356
  seqlen_q: int,
357
  seqlen_k: int,
358
  block_size: tuple[int, int],
359
- subtile_factor: int,
 
360
  ) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None]:
361
  m_block_size, n_block_size = block_size
362
  if tensors.block_size is None:
363
- sparse_block_size_q, sparse_block_size_kv = subtile_factor * m_block_size, n_block_size
364
  else:
365
  sparse_block_size_q, sparse_block_size_kv = tensors.block_size
366
- if sparse_block_size_q != subtile_factor * m_block_size:
367
  raise ValueError(
368
- f"Block sparsity expects sparse_block_size_q={subtile_factor * m_block_size} "
369
- f"for subtile_factor={subtile_factor}."
370
  )
371
- if sparse_block_size_kv != n_block_size:
 
372
  raise ValueError(
373
- f"Block sparsity expects sparse_block_size[1]={n_block_size} to match tile_n."
 
374
  )
375
  expected_count_shape, expected_index_shape = get_block_sparse_expected_shapes_bwd(
376
  batch_size,
@@ -379,7 +662,8 @@ def normalize_block_sparse_config_bwd(
379
  seqlen_k,
380
  m_block_size,
381
  n_block_size,
382
- subtile_factor,
 
383
  )
384
  normalized_tensors = normalize_block_sparse_tensors(
385
  tensors,
@@ -389,7 +673,7 @@ def normalize_block_sparse_config_bwd(
389
  hint=lambda: (
390
  f"Backward expects Q-direction block-sparse tensors (q_mask_cnt/q_mask_idx, "
391
  f"and optionally full_q_cnt/full_q_idx). Regenerate the backward BlockMask with "
392
- f"BLOCK_SIZE=({subtile_factor * m_block_size}, {n_block_size})."
393
  ),
394
  )
395
  return normalized_tensors, get_block_sparse_broadcast_pattern(normalized_tensors)
@@ -401,29 +685,27 @@ def to_cute_block_sparse_tensors(
401
  """Convert torch block sparsity tensors to CuTe tensors, optionally for tvm ffi"""
402
  if not is_block_sparsity_enabled(tensors):
403
  return None
404
- (
405
- mask_block_cnt,
406
- mask_block_idx,
407
- full_block_cnt,
408
- full_block_idx,
409
- *_,
410
- ) = tensors
411
-
412
- (
413
- mask_block_cnt_tensor,
414
- mask_block_idx_tensor,
415
- ) = [
416
  to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
417
- for t in (mask_block_cnt, mask_block_idx)
418
  ]
419
- (
420
- full_block_cnt_tensor,
421
- full_block_idx_tensor,
422
- ) = [
 
 
 
 
 
 
 
 
 
423
  to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
424
  if t is not None
425
  else None
426
- for t in (full_block_cnt, full_block_idx)
427
  ]
428
 
429
  return BlockSparseTensors(
@@ -431,6 +713,10 @@ def to_cute_block_sparse_tensors(
431
  mask_block_idx_tensor,
432
  full_block_cnt_tensor,
433
  full_block_idx_tensor,
 
 
 
 
434
  )
435
 
436
 
 
17
  class BlockSparseTensors(NamedTuple):
18
  mask_block_cnt: cute.Tensor
19
  mask_block_idx: cute.Tensor
20
+ full_block_cnt: cute.Tensor | None = None
21
+ full_block_idx: cute.Tensor | None = None
22
+ cu_total_m_blocks: cute.Tensor | None = None
23
+ cu_block_idx_offsets: cute.Tensor | None = None
24
+ dq_write_order: cute.Tensor | None = None
25
+ dq_write_order_full: cute.Tensor | None = None
26
 
27
  def __new_from_mlir_values__(self, values):
28
+ new_fields = []
29
+ idx = 0
30
+ for original in self:
31
+ if original is None:
32
+ new_fields.append(None)
33
+ else:
34
+ new_fields.append(values[idx])
35
+ idx += 1
36
+ return BlockSparseTensors(*new_fields)
37
 
38
 
39
  class BlockSparseTensorsTorch(NamedTuple):
 
41
  mask_block_idx: torch.Tensor
42
  full_block_cnt: torch.Tensor | None = None
43
  full_block_idx: torch.Tensor | None = None
44
+ cu_total_m_blocks: torch.Tensor | None = None
45
+ cu_block_idx_offsets: torch.Tensor | None = None
46
  block_size: tuple[int, int] | None = None
47
+ dq_write_order: torch.Tensor | None = None
48
+ dq_write_order_full: torch.Tensor | None = None
49
+ spt: bool | None = None
50
+
51
+
52
+ def _ordered_to_dense_simple(
53
+ num_blocks: torch.Tensor,
54
+ indices: torch.Tensor,
55
+ num_cols: int,
56
+ ) -> torch.Tensor:
57
+ """Convert ordered sparse representation to dense binary matrix.
58
+
59
+ Args:
60
+ num_blocks: [B, H, num_rows] count of valid entries per row
61
+ indices: [B, H, num_rows, max_entries] column indices (valid entries packed left)
62
+ num_cols: total number of columns
63
+
64
+ Returns:
65
+ dense: [B, H, num_rows, num_cols] binary int32 matrix
66
+ """
67
+ B, H, num_rows, max_entries = indices.shape
68
+ device = indices.device
69
+ dense = torch.zeros(B, H, num_rows, num_cols + 1, dtype=torch.int32, device=device)
70
+ col_range = torch.arange(max_entries, device=device)
71
+ valid = col_range[None, None, None, :] < num_blocks[:, :, :, None]
72
+ safe_indices = torch.where(valid, indices.long(), num_cols)
73
+ row_idx = torch.arange(num_rows, device=device)[None, None, :, None].expand_as(indices)
74
+ b_idx = torch.arange(B, device=device)[:, None, None, None].expand_as(indices)
75
+ h_idx = torch.arange(H, device=device)[None, :, None, None].expand_as(indices)
76
+ dense[b_idx, h_idx, row_idx, safe_indices] = 1
77
+ return dense[:, :, :, :num_cols]
78
+
79
+
80
+ def compute_dq_write_order(
81
+ fwd_mask_cnt: torch.Tensor,
82
+ fwd_mask_idx: torch.Tensor,
83
+ fwd_full_cnt: torch.Tensor | None,
84
+ fwd_full_idx: torch.Tensor | None,
85
+ bwd_mask_cnt: torch.Tensor,
86
+ bwd_mask_idx: torch.Tensor,
87
+ bwd_full_cnt: torch.Tensor | None,
88
+ bwd_full_idx: torch.Tensor | None,
89
+ spt: bool = False,
90
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
91
+ """Compute dQ write-order metadata for deterministic block-sparse backward.
92
+
93
+ For each (n_block, i) in the backward iteration, computes the semaphore
94
+ lock value: the rank of n_block in the combined (partial + full) sorted
95
+ contributor list for the target m_block.
96
+
97
+ Lock values are assigned in ascending n_block order (or descending if spt=True)
98
+ to guarantee deadlock-freedom with the CTA scheduling order.
99
+
100
+ Args:
101
+ fwd_mask_cnt: [B, H, num_m_blocks] partial contributor counts per m_block
102
+ fwd_mask_idx: [B, H, num_m_blocks, max_kv] partial contributor n_block indices (ascending)
103
+ fwd_full_cnt: [B, H, num_m_blocks] full contributor counts per m_block (optional)
104
+ fwd_full_idx: [B, H, num_m_blocks, max_kv] full contributor n_block indices (optional)
105
+ bwd_mask_cnt: [B, H, num_n_blocks] partial iteration counts per n_block
106
+ bwd_mask_idx: [B, H, num_n_blocks, max_q] partial iteration m_block indices
107
+ bwd_full_cnt: [B, H, num_n_blocks] full iteration counts per n_block (optional)
108
+ bwd_full_idx: [B, H, num_n_blocks, max_q] full iteration m_block indices (optional)
109
+ spt: if True, reverse ordering (highest n_block gets lock_value=0)
110
+
111
+ Returns:
112
+ (dq_write_order, dq_write_order_full): tensors parallel to bwd_mask_idx
113
+ and bwd_full_idx respectively, containing lock values.
114
+ """
115
+ device = fwd_mask_idx.device
116
+ B, H, num_m, max_kv_partial = fwd_mask_idx.shape
117
+ _, _, num_n, max_q_partial = bwd_mask_idx.shape
118
+
119
+ has_full = fwd_full_cnt is not None and fwd_full_idx is not None
120
+
121
+ dense_partial = _ordered_to_dense_simple(fwd_mask_cnt, fwd_mask_idx, num_n)
122
+ if has_full:
123
+ dense_full = _ordered_to_dense_simple(fwd_full_cnt, fwd_full_idx, num_n)
124
+ dense = (dense_partial + dense_full).clamp(max=1)
125
+ else:
126
+ dense = dense_partial
127
+
128
+ cumsum = dense.cumsum(dim=-1)
129
+ rank_table = (cumsum - dense).to(torch.int32)
130
+
131
+ if spt:
132
+ total_per_m = cumsum[:, :, :, -1:]
133
+ rank_table = (total_per_m - 1 - rank_table).to(torch.int32)
134
+
135
+ def _gather_write_order(bwd_idx, bwd_cnt):
136
+ b_i = torch.arange(B, device=device)[:, None, None, None].expand_as(bwd_idx)
137
+ h_i = torch.arange(H, device=device)[None, :, None, None].expand_as(bwd_idx)
138
+ n_i = torch.arange(bwd_idx.shape[2], device=device)[None, None, :, None].expand_as(bwd_idx)
139
+ m_vals = bwd_idx.long().clamp(0, num_m - 1)
140
+ return rank_table[b_i, h_i, m_vals, n_i].to(torch.int32)
141
+
142
+ dq_write_order = _gather_write_order(bwd_mask_idx, bwd_mask_cnt)
143
+
144
+ dq_write_order_full = None
145
+ if has_full and bwd_full_cnt is not None and bwd_full_idx is not None:
146
+ dq_write_order_full = _gather_write_order(bwd_full_idx, bwd_full_cnt)
147
+
148
+ return dq_write_order, dq_write_order_full
149
+
150
+
151
+ def compute_dq_write_order_from_block_mask(
152
+ block_mask,
153
+ spt: bool = False,
154
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
155
+ (
156
+ _seq_q,
157
+ _seq_k,
158
+ kv_mask_cnt,
159
+ kv_mask_idx,
160
+ full_kv_cnt,
161
+ full_kv_idx,
162
+ q_mask_cnt,
163
+ q_mask_idx,
164
+ full_q_cnt,
165
+ full_q_idx,
166
+ *_,
167
+ ) = block_mask.as_tuple()
168
+ return compute_dq_write_order(
169
+ kv_mask_cnt,
170
+ kv_mask_idx,
171
+ full_kv_cnt,
172
+ full_kv_idx,
173
+ q_mask_cnt,
174
+ q_mask_idx,
175
+ full_q_cnt,
176
+ full_q_idx,
177
+ spt=spt,
178
+ )
179
+
180
+
181
+ def get_sparse_q_block_size(
182
+ tensors: BlockSparseTensorsTorch | None,
183
+ seqlen_q: int,
184
+ ) -> int | None:
185
+ """Return the Q sparse block size, or None when sparsity is unset or ambiguous."""
186
+ if tensors is None:
187
+ return None
188
+ if tensors.block_size is not None:
189
+ return tensors.block_size[0]
190
+ num_m_blocks = tensors.mask_block_idx.shape[2]
191
+ min_block_size = ceildiv(seqlen_q, num_m_blocks)
192
+ max_block_size = seqlen_q if num_m_blocks == 1 else (seqlen_q - 1) // (num_m_blocks - 1)
193
+ if min_block_size != max_block_size:
194
+ return None
195
+ return min_block_size
196
+
197
+
198
+ def get_kv_subtile_factor(
199
+ block_sparse_tensors: BlockSparseTensorsTorch | None,
200
+ n_block_size: int,
201
+ ) -> int:
202
+ """Return the number of physical KV tiles covered by one sparse KV block."""
203
+ if block_sparse_tensors is None or block_sparse_tensors.block_size is None:
204
+ return 1
205
+ sparse_block_size_kv = block_sparse_tensors.block_size[1]
206
+ if sparse_block_size_kv % n_block_size != 0:
207
+ raise ValueError(
208
+ "Block sparsity expects sparse_block_size[1] "
209
+ f"to be a multiple of tile_n={n_block_size}; got {sparse_block_size_kv}."
210
+ )
211
+ return sparse_block_size_kv // n_block_size
212
+
213
+
214
+ def block_sparse_bwd_supports_2cta(
215
+ block_sparse_tensors: BlockSparseTensorsTorch | None,
216
+ n_block_size: int,
217
+ ) -> bool:
218
+ """Return whether sparse KV metadata constrains backward away from 2CTA."""
219
+ if block_sparse_tensors is None:
220
+ return True
221
+ return get_kv_subtile_factor(block_sparse_tensors, n_block_size) % 2 == 0
222
 
223
 
224
  def _expand_sparsity_tensor(
 
248
  name: str,
249
  cnt: torch.Tensor | None,
250
  idx: torch.Tensor | None,
251
+ expected_count_shape: Tuple[int, ...],
252
+ expected_index_shape: Tuple[int, ...],
253
  context: str | None,
254
  hint: str | Callable[[], str] | None,
255
  ) -> Tuple[torch.Tensor | None, torch.Tensor | None]:
 
268
  expanded_cnt = _expand_sparsity_tensor(
269
  cnt, expected_count_shape, f"{name}_block_cnt", context, hint
270
  )
271
+ # [Note] Allow Compact block sparse indices
272
+ # Allow the last dimension (n_blocks) of idx to be <= expected, since
273
+ # FA4 only accesses indices 0..cnt-1 per query tile. This enables compact
274
+ # index tensors that avoid O(N^2) memory at long sequence lengths.
275
+ if idx.ndim == 4 and idx.shape[3] <= expected_index_shape[3]:
276
+ expected_index_shape = (*expected_index_shape[:3], idx.shape[3])
277
  expanded_idx = _expand_sparsity_tensor(
278
  idx, expected_index_shape, f"{name}_block_idx", context, hint
279
  )
280
  return expanded_cnt, expanded_idx
281
 
282
 
283
+ def _check_and_expand_metadata_tensor(
284
+ name: str,
285
+ tensor: torch.Tensor | None,
286
+ expected_shape: Tuple[int, ...],
287
+ context: str | None,
288
+ hint: str | Callable[[], str] | None,
289
+ device: torch.device,
290
+ ) -> torch.Tensor | None:
291
+ if tensor is None:
292
+ return None
293
+ if tensor.dtype != torch.int32:
294
+ raise ValueError(f"{name} must have dtype torch.int32")
295
+ if tensor.device != device:
296
+ raise ValueError(f"{name} must be on the same device as block sparse tensors")
297
+ if not tensor.is_cuda:
298
+ raise ValueError(f"{name} must live on CUDA")
299
+ return _expand_sparsity_tensor(tensor, expected_shape, name, context, hint)
300
+
301
+
302
  def get_block_sparse_expected_shapes(
303
  batch_size: int,
304
  num_head: int,
 
336
  Expectations:
337
  - mask_block_cnt is (B, H, M) and mask_block_idx is (B, H, M, N).
338
  - Batch/head dims may be 1 for broadcast, or match the requested sizes.
339
+ - sparse_block_size_kv must be a multiple of tile_n.
340
  - sparse_block_size_q must be a multiple of q_stage * tile_m.
341
  - If sparse_block_size_q is omitted and seqlen_q/num_m_blocks is ambiguous,
342
  the caller must provide block_size to disambiguate. TODO will make this required in a future PR.
 
345
  base_n_block = n_block_size
346
  if sparse_block_size_kv is None:
347
  sparse_block_size_kv = base_n_block
348
+ if sparse_block_size_kv % base_n_block != 0:
349
+ raise ValueError(
350
+ f"Block sparse tensors{context} require BLOCK_SIZE_KV to be a multiple of {base_n_block}."
351
+ )
352
  if tensors.mask_block_idx is None:
353
  raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
354
  num_m_blocks = tensors.mask_block_idx.shape[2]
355
 
356
  if sparse_block_size_q is None:
357
+ sparse_block_size_q = get_sparse_q_block_size(tensors, seqlen_q)
358
+ if sparse_block_size_q is None and base_m_block != 1:
 
 
 
 
359
  raise ValueError(
360
  f"Block sparse tensors{context} require explicit sparse_block_size[0] "
361
  f"to disambiguate block size for seqlen_q={seqlen_q} and num_m_blocks={num_m_blocks}."
362
  )
363
+ if sparse_block_size_q is None:
364
+ sparse_block_size_q = ceildiv(seqlen_q, num_m_blocks)
365
 
366
  if sparse_block_size_q % base_m_block != 0:
367
  raise ValueError(
 
397
  raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
398
  if mask_block_cnt.shape[2] != mask_block_idx.shape[2]:
399
  raise ValueError(f"Block sparse tensors{context} must share the same m-block dimension.")
400
+ # [Note] Allow Compact block sparse indices: FA4 only accesses indices 0..cnt-1
401
+ # per query tile, so idx.shape[3] can be <= expected_n_blocks.
402
+ if mask_block_idx.shape[3] > expected_n_blocks:
403
  raise ValueError(
404
+ f"Block sparse tensors{context} n-block dimension must be <= {expected_n_blocks}."
405
  )
406
  if expected_m_blocks != num_m_blocks:
407
  raise ValueError(
 
419
  seqlen_k: int,
420
  m_block_size: int,
421
  n_block_size: int,
422
+ q_subtile_factor: int,
423
+ kv_subtile_factor: int = 1,
424
  ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
425
  """Return (expected_count_shape, expected_index_shape) for backward block sparse normalization.
426
 
427
  Backward uses Q-direction indexing (transposed from forward), where shapes are
428
  indexed by N-blocks first, then M-blocks. The sparse_block_size_q is determined
429
+ by q_subtile_factor * m_block_size.
430
  """
431
+ sparse_block_size_q = q_subtile_factor * m_block_size
432
+ sparse_block_size_kv = kv_subtile_factor * n_block_size
433
  expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
434
+ expected_n_blocks = ceildiv(seqlen_k, sparse_block_size_kv)
435
  expected_count_shape = (batch_size, num_head, expected_n_blocks)
436
  expected_index_shape = (batch_size, num_head, expected_n_blocks, expected_m_blocks)
437
  return expected_count_shape, expected_index_shape
 
440
  def normalize_block_sparse_tensors(
441
  tensors: BlockSparseTensorsTorch,
442
  *,
443
+ expected_count_shape: Tuple[int, ...],
444
+ expected_index_shape: Tuple[int, ...],
445
  context: str | None = None,
446
  hint: str | Callable[[], str] | None = None,
447
  ) -> BlockSparseTensorsTorch:
 
472
  if full_cnt is not None and mask_cnt.device != full_cnt.device:
473
  raise ValueError("All block sparse tensors must be on the same device")
474
 
475
+ dq_write_order = _check_and_expand_metadata_tensor(
476
+ "dq_write_order",
477
+ tensors.dq_write_order,
478
+ tuple(mask_idx.shape),
479
+ context,
480
+ hint,
481
+ mask_cnt.device,
482
+ )
483
+ dq_write_order_full = _check_and_expand_metadata_tensor(
484
+ "dq_write_order_full",
485
+ tensors.dq_write_order_full,
486
+ tuple(full_idx.shape) if full_idx is not None else expected_index_shape,
487
+ context,
488
+ hint,
489
+ mask_cnt.device,
490
+ )
491
+ spt = tensors.spt
492
+ if spt is not None and not isinstance(spt, bool):
493
+ raise ValueError("spt must be a bool when provided")
494
+ if spt is not None and dq_write_order is None:
495
+ raise ValueError("spt requires dq_write_order to be provided")
496
+
497
  return BlockSparseTensorsTorch(
498
  mask_block_cnt=mask_cnt,
499
  mask_block_idx=mask_idx,
500
  full_block_cnt=full_cnt,
501
  full_block_idx=full_idx,
502
+ cu_total_m_blocks=tensors.cu_total_m_blocks,
503
+ cu_block_idx_offsets=tensors.cu_block_idx_offsets,
504
  block_size=tensors.block_size,
505
+ dq_write_order=dq_write_order,
506
+ dq_write_order_full=dq_write_order_full,
507
+ spt=spt,
508
  )
509
 
510
 
 
536
  tensors.mask_block_idx,
537
  tensors.full_block_cnt,
538
  tensors.full_block_idx,
539
+ tensors.dq_write_order,
540
+ tensors.dq_write_order_full,
541
  ):
542
  if tensor is not None:
543
  patterns.append(get_broadcast_dims(tensor))
 
546
  return tuple(patterns)
547
 
548
 
549
+ class NormalizedBlockSparseConfig(NamedTuple):
550
+ """Result of validating and normalizing a user block-sparse config."""
551
+
552
+ tensors: BlockSparseTensorsTorch
553
+ broadcast_pattern: Tuple[Tuple[bool, ...], ...] | None
554
+ q_subtile_factor: int
555
+ kv_subtile_factor: int
556
+
557
+
558
  def normalize_block_sparse_config(
559
  tensors: BlockSparseTensorsTorch,
560
  *,
 
564
  seqlen_k: int,
565
  block_size: tuple[int, int],
566
  q_stage: int,
567
+ allow_kv_subtile: bool = False,
568
+ ) -> NormalizedBlockSparseConfig:
569
+ """Validate the block-sparse config, infer expected shapes, and normalize.
570
+
571
+ Handles both fixed-length (3D `[B, H, M]` / 4D `[B, H, M, N]`) and varlen
572
+ (2D `[H, total_m_blocks]` / `[H, total_n_blocks]`) layouts. Varlen is
573
+ detected by `tensors.cu_total_m_blocks is not None` and forces
574
+ `q_subtile_factor == 1` (TODO: potentially remove this restriction).
575
+ """
576
  m_block_size, n_block_size = block_size
577
  if tensors.block_size is None:
578
+ sparse_block_size_q, sparse_block_size_kv = None, n_block_size
579
  else:
580
  sparse_block_size_q, sparse_block_size_kv = tensors.block_size
581
+ if sparse_block_size_kv % n_block_size != 0:
582
+ raise ValueError(
583
+ f"Block sparsity requires sparse_block_size[1] to be a multiple of tile_n={n_block_size}."
584
+ )
585
+ kv_subtile_factor = sparse_block_size_kv // n_block_size
586
+ if kv_subtile_factor != 1 and not allow_kv_subtile:
587
  raise ValueError(
588
  f"Block sparsity requires sparse_block_size[1]={n_block_size} to match tile_n."
589
  )
590
+ if tensors.cu_total_m_blocks is not None:
591
+ base_m_block = q_stage * m_block_size
592
+ if sparse_block_size_q is not None and sparse_block_size_q != base_m_block:
593
+ raise ValueError(
594
+ f"Varlen block sparsity requires sparse_block_size[0]={base_m_block} "
595
+ f"(= q_stage * tile_m); got {sparse_block_size_q}."
596
+ )
597
+ total_m_blocks = tensors.mask_block_cnt.shape[-1]
598
+ total_n_blocks = tensors.mask_block_idx.shape[-1]
599
+ expected_count_shape = (num_head, total_m_blocks)
600
+ expected_index_shape = (num_head, total_n_blocks)
601
+ q_subtile_factor = 1
602
+ else:
603
+ expected_count_shape, expected_index_shape, q_subtile_factor = (
604
+ infer_block_sparse_expected_shapes(
605
+ tensors,
606
+ batch_size=batch_size,
607
+ num_head=num_head,
608
+ seqlen_q=seqlen_q,
609
+ seqlen_k=seqlen_k,
610
+ m_block_size=m_block_size,
611
+ n_block_size=n_block_size,
612
+ q_stage=q_stage,
613
+ context="forward",
614
+ sparse_block_size_q=sparse_block_size_q,
615
+ sparse_block_size_kv=sparse_block_size_kv,
616
+ )
617
  )
 
618
  normalized_tensors = normalize_block_sparse_tensors(
619
  tensors,
620
  expected_count_shape=expected_count_shape,
621
  expected_index_shape=expected_index_shape,
622
  )
623
+ return NormalizedBlockSparseConfig(
624
+ tensors=normalized_tensors,
625
+ broadcast_pattern=get_block_sparse_broadcast_pattern(normalized_tensors),
626
+ q_subtile_factor=q_subtile_factor,
627
+ kv_subtile_factor=kv_subtile_factor,
628
  )
629
 
630
 
 
636
  seqlen_q: int,
637
  seqlen_k: int,
638
  block_size: tuple[int, int],
639
+ q_subtile_factor: int,
640
+ kv_subtile_factor: int = 1,
641
  ) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None]:
642
  m_block_size, n_block_size = block_size
643
  if tensors.block_size is None:
644
+ sparse_block_size_q, sparse_block_size_kv = q_subtile_factor * m_block_size, n_block_size
645
  else:
646
  sparse_block_size_q, sparse_block_size_kv = tensors.block_size
647
+ if sparse_block_size_q != q_subtile_factor * m_block_size:
648
  raise ValueError(
649
+ f"Block sparsity expects sparse_block_size_q={q_subtile_factor * m_block_size} "
650
+ f"for q_subtile_factor={q_subtile_factor}."
651
  )
652
+ expected_sparse_block_size_kv = kv_subtile_factor * n_block_size
653
+ if sparse_block_size_kv != expected_sparse_block_size_kv:
654
  raise ValueError(
655
+ f"Block sparsity expects sparse_block_size[1]={expected_sparse_block_size_kv} "
656
+ f"for kv_subtile_factor={kv_subtile_factor}."
657
  )
658
  expected_count_shape, expected_index_shape = get_block_sparse_expected_shapes_bwd(
659
  batch_size,
 
662
  seqlen_k,
663
  m_block_size,
664
  n_block_size,
665
+ q_subtile_factor,
666
+ kv_subtile_factor,
667
  )
668
  normalized_tensors = normalize_block_sparse_tensors(
669
  tensors,
 
673
  hint=lambda: (
674
  f"Backward expects Q-direction block-sparse tensors (q_mask_cnt/q_mask_idx, "
675
  f"and optionally full_q_cnt/full_q_idx). Regenerate the backward BlockMask with "
676
+ f"BLOCK_SIZE=({q_subtile_factor * m_block_size}, {expected_sparse_block_size_kv})."
677
  ),
678
  )
679
  return normalized_tensors, get_block_sparse_broadcast_pattern(normalized_tensors)
 
685
  """Convert torch block sparsity tensors to CuTe tensors, optionally for tvm ffi"""
686
  if not is_block_sparsity_enabled(tensors):
687
  return None
688
+ mask_block_cnt_tensor, mask_block_idx_tensor = [
 
 
 
 
 
 
 
 
 
 
 
689
  to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
690
+ for t in (tensors.mask_block_cnt, tensors.mask_block_idx)
691
  ]
692
+ full_block_cnt_tensor, full_block_idx_tensor = [
693
+ to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
694
+ if t is not None
695
+ else None
696
+ for t in (tensors.full_block_cnt, tensors.full_block_idx)
697
+ ]
698
+ cu_total_m_blocks_tensor, cu_block_idx_offsets_tensor = [
699
+ to_cute_tensor(t, assumed_align=4, leading_dim=0, enable_tvm_ffi=enable_tvm_ffi)
700
+ if t is not None
701
+ else None
702
+ for t in (tensors.cu_total_m_blocks, tensors.cu_block_idx_offsets)
703
+ ]
704
+ dq_write_order_tensor, dq_write_order_full_tensor = [
705
  to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
706
  if t is not None
707
  else None
708
+ for t in (tensors.dq_write_order, tensors.dq_write_order_full)
709
  ]
710
 
711
  return BlockSparseTensors(
 
713
  mask_block_idx_tensor,
714
  full_block_cnt_tensor,
715
  full_block_idx_tensor,
716
+ cu_total_m_blocks_tensor,
717
+ cu_block_idx_offsets_tensor,
718
+ dq_write_order_tensor,
719
+ dq_write_order_full_tensor,
720
  )
721
 
722
 
build/torch-cuda/cache_utils.py CHANGED
@@ -1,7 +1,6 @@
1
  # Manage Ahead-of-Time (AOT) compiled kernels
2
  import fcntl
3
  import hashlib
4
- import logging
5
  import os
6
  import pickle
7
  import sys
@@ -18,6 +17,7 @@ import cutlass
18
  import cutlass.cute as cute
19
  import tvm_ffi
20
  from cutlass.cutlass_dsl import JitCompiledFunction
 
21
 
22
  # Pre-load cute DSL runtime libraries with RTLD_GLOBAL so that their symbols
23
  # (e.g. _cudaLibraryLoadData) are visible to .so modules loaded later via dlopen.
@@ -30,13 +30,6 @@ for _lib_path in cute.runtime.find_runtime_libraries(enable_tvm_ffi=False):
30
  CompileKeyType: TypeAlias = tuple[Hashable, ...]
31
  CallableFunction: TypeAlias = JitCompiledFunction | tvm_ffi.Function
32
 
33
- logger = logging.getLogger(__name__)
34
- _handler = logging.StreamHandler()
35
- _handler.setFormatter(logging.Formatter("%(asctime)s.%(msecs)03d %(levelname)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S"))
36
- logger.addHandler(_handler)
37
- logger.setLevel(logging.DEBUG)
38
-
39
-
40
  # Enable cache via `FLASH_ATTENTION_CUTE_DSL_CACHE_ENABLED=1`
41
  CUTE_DSL_CACHE_ENABLED: bool = os.getenv("FLASH_ATTENTION_CUTE_DSL_CACHE_ENABLED", "0") == "1"
42
 
@@ -222,13 +215,13 @@ class JITPersistentCache(JITCache):
222
  label=sha256_hex,
223
  ):
224
  if obj_path.exists():
225
- logger.debug("Loading compiled function from disk: %s", obj_path)
226
  m = cute.runtime.load_module(str(obj_path), enable_tvm_ffi=True)
227
  fn = getattr(m, self.EXPORT_FUNCTION_PREFIX)
228
  JITCache.__setitem__(self, key, fn)
229
  return True
230
  else:
231
- logger.debug("Cache miss on disk for key hash %s", sha256_hex)
232
  return False
233
 
234
  def _try_export_to_storage(self, key: CompileKeyType, fn: JitCompiledFunction) -> None:
@@ -243,14 +236,14 @@ class JITPersistentCache(JITCache):
243
  obj_path = self.cache_path / f"{sha256_hex}.o"
244
  if obj_path.exists():
245
  # Another process already exported.
246
- logger.debug("Skipping export, already on disk: %s", obj_path)
247
  return
248
- logger.debug("Exporting compiled function to disk: %s", obj_path)
249
  fn.export_to_c(
250
  object_file_path=str(obj_path),
251
  function_name=self.EXPORT_FUNCTION_PREFIX,
252
  )
253
- logger.debug("Successfully exported compiled function to disk: %s", obj_path)
254
 
255
  def _key_to_hash(self, key: CompileKeyType) -> str:
256
  return hashlib.sha256(pickle.dumps(key)).hexdigest()
@@ -262,7 +255,7 @@ class JITPersistentCache(JITCache):
262
  """
263
  Not only clear the in-memory cache. Also purge persistent compilation cache.
264
  """
265
- logger.debug("Clearing persistent cache at %s", self.cache_path)
266
  super().clear()
267
  for child in self.cache_path.iterdir():
268
  child.unlink()
@@ -281,8 +274,8 @@ def get_jit_cache(name: str | None = None) -> JITCache:
281
  path = get_cache_path() / _compute_source_fingerprint()
282
  if name:
283
  path = path / name
284
- logger.debug("Creating persistent JIT cache at %s", path)
285
  return JITPersistentCache(path)
286
  else:
287
- logger.debug("Persistent cache disabled, using in-memory JIT cache")
288
  return JITCache()
 
1
  # Manage Ahead-of-Time (AOT) compiled kernels
2
  import fcntl
3
  import hashlib
 
4
  import os
5
  import pickle
6
  import sys
 
17
  import cutlass.cute as cute
18
  import tvm_ffi
19
  from cutlass.cutlass_dsl import JitCompiledFunction
20
+ from .fa_logging import fa_log
21
 
22
  # Pre-load cute DSL runtime libraries with RTLD_GLOBAL so that their symbols
23
  # (e.g. _cudaLibraryLoadData) are visible to .so modules loaded later via dlopen.
 
30
  CompileKeyType: TypeAlias = tuple[Hashable, ...]
31
  CallableFunction: TypeAlias = JitCompiledFunction | tvm_ffi.Function
32
 
 
 
 
 
 
 
 
33
  # Enable cache via `FLASH_ATTENTION_CUTE_DSL_CACHE_ENABLED=1`
34
  CUTE_DSL_CACHE_ENABLED: bool = os.getenv("FLASH_ATTENTION_CUTE_DSL_CACHE_ENABLED", "0") == "1"
35
 
 
215
  label=sha256_hex,
216
  ):
217
  if obj_path.exists():
218
+ fa_log(1, f"Loading compiled function from disk: {obj_path}")
219
  m = cute.runtime.load_module(str(obj_path), enable_tvm_ffi=True)
220
  fn = getattr(m, self.EXPORT_FUNCTION_PREFIX)
221
  JITCache.__setitem__(self, key, fn)
222
  return True
223
  else:
224
+ fa_log(1, f"Cache miss on disk for key hash {sha256_hex}")
225
  return False
226
 
227
  def _try_export_to_storage(self, key: CompileKeyType, fn: JitCompiledFunction) -> None:
 
236
  obj_path = self.cache_path / f"{sha256_hex}.o"
237
  if obj_path.exists():
238
  # Another process already exported.
239
+ fa_log(1, f"Skipping export, already on disk: {obj_path}")
240
  return
241
+ fa_log(1, f"Exporting compiled function to disk: {obj_path}")
242
  fn.export_to_c(
243
  object_file_path=str(obj_path),
244
  function_name=self.EXPORT_FUNCTION_PREFIX,
245
  )
246
+ fa_log(1, f"Successfully exported compiled function to disk: {obj_path}")
247
 
248
  def _key_to_hash(self, key: CompileKeyType) -> str:
249
  return hashlib.sha256(pickle.dumps(key)).hexdigest()
 
255
  """
256
  Not only clear the in-memory cache. Also purge persistent compilation cache.
257
  """
258
+ fa_log(1, f"Clearing persistent cache at {self.cache_path}")
259
  super().clear()
260
  for child in self.cache_path.iterdir():
261
  child.unlink()
 
274
  path = get_cache_path() / _compute_source_fingerprint()
275
  if name:
276
  path = path / name
277
+ fa_log(1, f"Creating persistent JIT cache at {path}")
278
  return JITPersistentCache(path)
279
  else:
280
+ fa_log(1, "Persistent cache disabled, using in-memory JIT cache")
281
  return JITCache()
build/torch-cuda/compute_block_sparsity.py CHANGED
@@ -11,8 +11,22 @@ from .block_sparsity import (
11
  BlockSparseTensorsTorch,
12
  to_cute_block_sparse_tensors,
13
  )
14
- from .utils import hash_callable, scalar_to_ssa, ssa_to_scalar
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  from .seqlen_info import SeqlenInfoQK
 
16
 
17
 
18
  class BlockSparsityKernel:
@@ -28,7 +42,6 @@ class BlockSparsityKernel:
28
 
29
  TODO:
30
  - optimize mask_mod evaluation
31
- - varlen support
32
  - transposed tensors for bwd pass
33
  """
34
 
@@ -52,18 +65,31 @@ class BlockSparsityKernel:
52
  blocksparse_tensors: BlockSparseTensors,
53
  seqlen_q: Int32,
54
  seqlen_k: Int32,
55
- aux_tensors: Optional[list] = None,
 
 
 
 
56
  ):
57
- self.mask_cnt, self.mask_idx, self.full_cnt, self.full_idx = blocksparse_tensors
 
 
 
 
58
 
59
  if const_expr(self.compute_full_blocks):
60
- assert self.full_cnt is not None and self.full_idx is not None, (
61
  "full block tensors must be provided when computing full blocks"
62
  )
63
-
64
- batch_size, num_heads, num_m_blocks, num_n_blocks = self.mask_idx.shape
65
- # launch 1 CTA per m block
66
- grid = [num_m_blocks, num_heads, batch_size]
 
 
 
 
 
67
 
68
  if const_expr(self.use_fast_sampling):
69
  num_threads = 5
@@ -72,46 +98,46 @@ class BlockSparsityKernel:
72
  num_threads = self.tile_mn[0]
73
  self.num_warps = (num_threads + 32 - 1) // 32
74
 
 
 
 
 
 
75
  self.kernel(
76
- self.mask_cnt,
77
- self.mask_idx,
78
- self.full_cnt,
79
- self.full_idx,
80
- num_n_blocks,
81
  seqlen_q,
82
  seqlen_k,
83
- aux_tensors,
 
 
 
 
 
 
 
84
  ).launch(grid=grid, block=[num_threads, 1, 1])
85
 
86
  @cute.kernel
87
  def kernel(
88
  self,
89
- mask_cnt: cute.Tensor,
90
- mask_idx: cute.Tensor,
91
- full_cnt: cute.Tensor,
92
- full_idx: cute.Tensor,
93
- num_n_blocks: Int32,
94
  seqlen_q: Int32,
95
  seqlen_k: Int32,
96
- aux_tensors: Optional[list] = None,
 
 
 
 
 
 
 
97
  ):
98
  tidx, _, _ = cute.arch.thread_idx()
99
  warp_idx = cute.arch.warp_idx()
100
  lane_id = cute.arch.lane_idx()
101
- m_block, head_idx, batch_idx = cute.arch.block_idx()
102
 
103
  ssa = partial(scalar_to_ssa, dtype=Int32)
104
 
105
- seqlen = SeqlenInfoQK.create(
106
- batch_idx,
107
- seqlen_q,
108
- seqlen_k,
109
- mCuSeqlensQ=None,
110
- mCuSeqlensK=None,
111
- mSeqUsedQ=None,
112
- mSeqUsedK=None,
113
- )
114
-
115
  @cute.struct
116
  class SharedStorage:
117
  reduction_buffer_smem: cute.struct.Align[
@@ -124,132 +150,157 @@ class BlockSparsityKernel:
124
  reduction_buffer = storage.reduction_buffer_smem.get_tensor(
125
  cute.make_layout((self.num_warps, 2))
126
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
  num_mask_blocks = Int32(0)
129
  num_full_blocks = Int32(0)
130
 
131
- for n_block in cutlass.range(num_n_blocks, unroll_full=True):
132
- m_base = m_block * self.tile_mn[0]
 
 
 
 
 
 
 
 
 
 
 
 
133
  n_base = n_block * self.tile_mn[1]
134
 
135
  if const_expr(self.use_fast_sampling):
136
- # Fast path: 5-point sampling (4 corners + center)
137
- # Clamps OOB indices to nearest in bounds.
138
- thread_result = Boolean(False)
139
- thread_is_valid = Boolean(False)
140
- q_idx = Int32(0)
141
- kv_idx = Int32(0)
 
 
 
 
 
 
142
 
143
- if tidx == 0:
144
- # Top-left corner (0, 0); always in bounds
145
- q_idx = m_base
146
- kv_idx = n_base
147
- elif tidx == 1:
148
- # Top-right corner
149
- q_idx = m_base
150
- kv_idx = cutlass.min(n_base + self.tile_mn[1] - 1, seqlen_k - 1)
151
- elif tidx == 2:
152
- # Bottom-left corner
153
- q_idx = cutlass.min(m_base + self.tile_mn[0] - 1, seqlen_q - 1)
154
- kv_idx = n_base
155
- elif tidx == 3:
156
- # Bottom-right corner
157
- q_idx = cutlass.min(m_base + self.tile_mn[0] - 1, seqlen_q - 1)
158
- kv_idx = cutlass.min(n_base + self.tile_mn[1] - 1, seqlen_k - 1)
159
  elif tidx == 4:
160
- # Center point
161
- q_idx = m_base + (cutlass.min(seqlen_q - m_base, self.tile_mn[0])) // 2
162
- kv_idx = n_base + (cutlass.min(seqlen_k - n_base, self.tile_mn[1])) // 2
163
- else:
164
- thread_is_valid = Boolean(False)
165
 
166
- # Check bounds and determine if this thread has a valid index pair
167
- if tidx < 5 and q_idx < seqlen_q and kv_idx < seqlen_k:
 
168
  thread_is_valid = Boolean(True)
169
- q_idx_ssa = ssa(q_idx)
170
- kv_idx_ssa = ssa(kv_idx)
171
  thread_result = ssa_to_scalar(
172
- self.mask_mod(
 
173
  ssa(batch_idx),
174
  ssa(head_idx),
175
- q_idx_ssa,
176
- kv_idx_ssa,
177
  seqlen,
178
- aux_tensors,
179
  )
180
  )
181
- else:
182
- thread_is_valid = Boolean(False)
183
 
184
- # Use vote_any_sync to see if any valid thread found unmasked or masked
185
- # Only count results from threads that checked valid indices
186
  has_unmasked = cute.arch.vote_any_sync(thread_result & thread_is_valid)
187
- has_masked = cute.arch.vote_any_sync((Boolean(not thread_result)) & thread_is_valid)
188
 
189
  else:
190
- # Full path: check all elements in the block
191
- # Track if this thread's row has any masked or unmasked elements
192
  thread_has_unmasked = Boolean(False)
193
  thread_has_masked = Boolean(False)
194
- thread_is_valid = Boolean(False)
195
-
196
- # Each thread handles 1 row
197
- q_idx = m_base + tidx
198
  kv_idx = Int32(0)
199
- if tidx < self.tile_mn[0] and q_idx < seqlen_q:
200
- thread_is_valid = Boolean(True)
201
- q_idx_ssa = ssa(q_idx)
202
 
203
- # Loop over all columns in this row
204
- for c in cutlass.range(self.tile_mn[1], unroll_full=True):
205
- kv_idx = n_base + c
206
- kv_idx_ssa = ssa(kv_idx)
207
-
208
- # Only check elements within valid sequence bounds
209
- if kv_idx < seqlen_k:
210
- # Direct scalar call
211
  mask_val = ssa_to_scalar(
212
- self.mask_mod(
 
213
  ssa(batch_idx),
214
  ssa(head_idx),
215
- q_idx_ssa,
216
- kv_idx_ssa,
217
  seqlen,
218
- aux_tensors,
219
  )
220
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
- # Update tracking flags
223
- if mask_val:
224
- thread_has_unmasked = Boolean(True)
225
- else:
226
- thread_has_masked = Boolean(True)
227
-
228
- # Block-level reduction to combine results across all threads
229
- # Only count votes from threads that checked valid indices
230
- warp_has_unmasked_mask = cute.arch.vote_any_sync(
231
- thread_has_unmasked & thread_is_valid
232
- )
233
- warp_has_masked_mask = cute.arch.vote_any_sync(thread_has_masked & thread_is_valid)
234
-
235
- # lane 0 writes the ballot mask to shared memory
236
- lane_id = tidx % 32
237
  if lane_id == 0:
238
- # Store as Int8
239
- reduction_buffer[warp_idx, 0] = Int8(1) if warp_has_unmasked_mask else Int8(0)
240
- reduction_buffer[warp_idx, 1] = Int8(1) if warp_has_masked_mask else Int8(0)
241
-
242
  cute.arch.sync_threads()
243
 
244
- # Thread 0 ORs all warp results together
245
  has_unmasked = Boolean(False)
246
  has_masked = Boolean(False)
247
- if tidx == 0:
248
- for w in cutlass.range(self.num_warps):
249
- if reduction_buffer[w, 0]:
250
- has_unmasked = Boolean(True)
251
- if reduction_buffer[w, 1]:
252
- has_masked = Boolean(True)
 
 
253
 
254
  # Only thread 0 updates the output arrays (common to both paths)
255
  if tidx == 0:
@@ -261,17 +312,23 @@ class BlockSparsityKernel:
261
  is_full = Boolean(has_unmasked and (not has_masked))
262
 
263
  if is_partial:
264
- mask_idx[batch_idx, head_idx, m_block, num_mask_blocks] = n_block
265
  num_mask_blocks += 1
266
  elif is_full and const_expr(self.compute_full_blocks):
267
- full_idx[batch_idx, head_idx, m_block, num_full_blocks] = n_block
268
  num_full_blocks += 1
269
 
270
  # Only thread 0 writes back the counts
271
  if tidx == 0:
272
- mask_cnt[batch_idx, head_idx, m_block] = num_mask_blocks
273
- if const_expr(self.compute_full_blocks):
274
- full_cnt[batch_idx, head_idx, m_block] = num_full_blocks
 
 
 
 
 
 
275
 
276
 
277
  def compute_block_sparsity(
@@ -282,11 +339,18 @@ def compute_block_sparsity(
282
  seqlen_q,
283
  seqlen_k,
284
  mask_mod: Callable,
285
- aux_tensors: Optional[list], # list[cute.Tensor]
286
  device,
 
 
 
 
 
 
 
287
  compute_full_blocks: bool = True,
288
  use_fast_sampling: bool = False,
289
- ) -> Tuple[BlockSparseTensors, BlockSparseTensorsTorch]:
290
  """
291
  Computes block sparsity for a given `mask_mod`.
292
 
@@ -300,59 +364,145 @@ def compute_block_sparsity(
300
  mask_mod: The `mask_mod` callable to use.
301
  aux_tensors: A list of auxiliary tensors.
302
  device: The device to use.
 
 
 
 
 
 
 
303
  compute_full_blocks: Whether to compute full blocks. If False, only partially-masked blocks are computed.
304
  use_fast_sampling: Whether to use 5-point sampling (4 corners + center). This is much faster, but only suitable for masks where this check is sufficient.
305
 
306
  Returns:
307
- A tuple of `BlockSparseTensors` and `BlockSparseTensorsTorch`.
308
  """
309
- # Check if mask_mod is marked as suitable for 5-point fast sampling
 
 
310
  use_fast_sampling = getattr(mask_mod, "use_fast_sampling", use_fast_sampling)
311
 
312
  num_m_blocks = (seqlen_q + tile_m - 1) // tile_m
313
  num_n_blocks = (seqlen_k + tile_n - 1) // tile_n
314
 
315
- mask_block_cnt = torch.zeros(
316
- (batch_size, num_heads, num_m_blocks), device=device, dtype=torch.int32
317
- )
318
- mask_block_idx = torch.zeros(
319
- (batch_size, num_heads, num_m_blocks, num_n_blocks), device=device, dtype=torch.int32
320
- )
321
- full_block_cnt = (
322
- torch.zeros((batch_size, num_heads, num_m_blocks), device=device, dtype=torch.int32)
323
- if compute_full_blocks
324
- else None
325
- )
326
- full_block_idx = (
327
- torch.zeros(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  (batch_size, num_heads, num_m_blocks, num_n_blocks), device=device, dtype=torch.int32
329
  )
330
- if compute_full_blocks
331
- else None
332
- )
 
 
 
 
 
 
 
 
 
 
 
333
 
334
  blocksparse_tensors_torch = BlockSparseTensorsTorch(
335
  mask_block_cnt=mask_block_cnt,
336
  mask_block_idx=mask_block_idx,
337
  full_block_cnt=full_block_cnt,
338
  full_block_idx=full_block_idx,
 
 
339
  block_size=(tile_m, tile_n),
340
  )
341
 
342
  mask_mod_hash = hash_callable(mask_mod)
343
- blocksparse_tensors = to_cute_block_sparse_tensors(
344
- blocksparse_tensors_torch, enable_tvm_ffi=True
345
- )
 
 
346
 
347
  compile_key = (
348
  tile_m,
349
  tile_n,
350
  mask_mod_hash,
 
 
351
  compute_full_blocks,
 
 
 
 
352
  aux_tensors is not None,
353
  use_fast_sampling,
354
  )
355
  if compile_key not in compute_block_sparsity.compile_cache:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  kernel = BlockSparsityKernel(
357
  mask_mod,
358
  tile_mn=(tile_m, tile_n),
@@ -362,17 +512,40 @@ def compute_block_sparsity(
362
  )
363
 
364
  compute_block_sparsity.compile_cache[compile_key] = cute.compile(
365
- kernel, blocksparse_tensors, seqlen_q, seqlen_k, aux_tensors, options="--enable-tvm-ffi"
 
 
 
 
 
 
 
 
 
366
  )
367
 
368
- compute_block_sparsity.compile_cache[compile_key](
369
- blocksparse_tensors_torch[:4],
370
- seqlen_q,
371
- seqlen_k,
372
- aux_tensors,
373
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
- return blocksparse_tensors, blocksparse_tensors_torch
376
 
377
 
378
  compute_block_sparsity.compile_cache = {}
 
11
  BlockSparseTensorsTorch,
12
  to_cute_block_sparse_tensors,
13
  )
14
+ from .block_sparse_utils import get_curr_blocksparse_tensors
15
+ from .testing import is_fake_mode
16
+ from .cute_dsl_utils import (
17
+ get_aux_tensor_metadata,
18
+ to_cute_aux_tensor,
19
+ to_cute_tensor,
20
+ )
21
+ from .utils import (
22
+ get_batch_from_cu_tensor,
23
+ hash_callable,
24
+ scalar_to_ssa,
25
+ ssa_to_scalar,
26
+ )
27
+ from .mask import call_mask_mod
28
  from .seqlen_info import SeqlenInfoQK
29
+ from .utils import AuxData
30
 
31
 
32
  class BlockSparsityKernel:
 
42
 
43
  TODO:
44
  - optimize mask_mod evaluation
 
45
  - transposed tensors for bwd pass
46
  """
47
 
 
65
  blocksparse_tensors: BlockSparseTensors,
66
  seqlen_q: Int32,
67
  seqlen_k: Int32,
68
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
69
+ mCuSeqlensK: Optional[cute.Tensor] = None,
70
+ mSeqUsedQ: Optional[cute.Tensor] = None,
71
+ mSeqUsedK: Optional[cute.Tensor] = None,
72
+ aux_data: AuxData = AuxData(),
73
  ):
74
+ mask_cnt, mask_idx, full_cnt, full_idx, mCuTotalMBlocks, mCuBlockIdxOffsets, *_ = (
75
+ blocksparse_tensors
76
+ )
77
+
78
+ self.is_varlen_q = const_expr(mCuSeqlensQ is not None)
79
 
80
  if const_expr(self.compute_full_blocks):
81
+ assert full_cnt is not None and full_idx is not None, (
82
  "full block tensors must be provided when computing full blocks"
83
  )
84
+ if const_expr(not self.is_varlen_q):
85
+ batch_size, num_heads, num_m_blocks, _ = mask_idx.shape
86
+ total_m_blocks = batch_size * num_m_blocks
87
+ else:
88
+ assert const_expr(mCuTotalMBlocks is not None), (
89
+ "mCuTotalMBlocks must be provided when varlen q"
90
+ )
91
+ num_heads, total_m_blocks = mask_cnt.shape # num_m_blocks is total_m_blocks
92
+ batch_size = mCuSeqlensQ.shape[0] - 1
93
 
94
  if const_expr(self.use_fast_sampling):
95
  num_threads = 5
 
98
  num_threads = self.tile_mn[0]
99
  self.num_warps = (num_threads + 32 - 1) // 32
100
 
101
+ if const_expr(not self.is_varlen_q):
102
+ grid = [num_m_blocks, num_heads, batch_size]
103
+ else:
104
+ grid = [total_m_blocks, num_heads, 1]
105
+
106
  self.kernel(
107
+ blocksparse_tensors,
 
 
 
 
108
  seqlen_q,
109
  seqlen_k,
110
+ batch_size,
111
+ mCuSeqlensQ,
112
+ mCuSeqlensK,
113
+ mSeqUsedQ,
114
+ mSeqUsedK,
115
+ mCuTotalMBlocks,
116
+ mCuBlockIdxOffsets,
117
+ aux_data,
118
  ).launch(grid=grid, block=[num_threads, 1, 1])
119
 
120
  @cute.kernel
121
  def kernel(
122
  self,
123
+ blocksparse_tensors: BlockSparseTensors,
 
 
 
 
124
  seqlen_q: Int32,
125
  seqlen_k: Int32,
126
+ batch_size: Int32,
127
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
128
+ mCuSeqlensK: Optional[cute.Tensor] = None,
129
+ mSeqUsedQ: Optional[cute.Tensor] = None,
130
+ mSeqUsedK: Optional[cute.Tensor] = None,
131
+ mCuTotalMBlocks: Optional[cute.Tensor] = None,
132
+ mCuBlockIdxOffsets: Optional[cute.Tensor] = None,
133
+ aux_data: AuxData = AuxData(),
134
  ):
135
  tidx, _, _ = cute.arch.thread_idx()
136
  warp_idx = cute.arch.warp_idx()
137
  lane_id = cute.arch.lane_idx()
 
138
 
139
  ssa = partial(scalar_to_ssa, dtype=Int32)
140
 
 
 
 
 
 
 
 
 
 
 
141
  @cute.struct
142
  class SharedStorage:
143
  reduction_buffer_smem: cute.struct.Align[
 
150
  reduction_buffer = storage.reduction_buffer_smem.get_tensor(
151
  cute.make_layout((self.num_warps, 2))
152
  )
153
+ SeqlenInfoCls = partial(
154
+ SeqlenInfoQK.create,
155
+ seqlen_q_static=seqlen_q,
156
+ seqlen_k_static=seqlen_k,
157
+ mCuSeqlensQ=mCuSeqlensQ,
158
+ mCuSeqlensK=mCuSeqlensK,
159
+ mSeqUsedQ=mSeqUsedQ,
160
+ mSeqUsedK=mSeqUsedK,
161
+ mCuTotalMBlocks=mCuTotalMBlocks,
162
+ mCuBlockIdxOffsets=mCuBlockIdxOffsets,
163
+ tile_m=self.tile_mn[0],
164
+ tile_n=self.tile_mn[1],
165
+ )
166
+
167
+ if const_expr(not self.is_varlen_q):
168
+ m_block, head_idx, batch_idx = cute.arch.block_idx()
169
+ else:
170
+ global_m_block, head_idx, _ = cute.arch.block_idx()
171
+ batch_idx = get_batch_from_cu_tensor(global_m_block, mCuTotalMBlocks)
172
+ m_block = global_m_block - mCuTotalMBlocks[batch_idx]
173
+
174
+ seqlen = SeqlenInfoCls(batch_idx)
175
+ seqlen_q = seqlen.seqlen_q
176
+ seqlen_k = seqlen.seqlen_k
177
+ global_m_block = seqlen.m_block_offset + m_block
178
+
179
+ num_n_blocks = (seqlen_k + self.tile_mn[1] - 1) // self.tile_mn[1]
180
+
181
+ _, curr_mask_idx, _, curr_full_idx = get_curr_blocksparse_tensors(
182
+ batch_idx, head_idx, m_block, blocksparse_tensors, seqlen
183
+ )
184
 
185
  num_mask_blocks = Int32(0)
186
  num_full_blocks = Int32(0)
187
 
188
+ m_base = m_block * self.tile_mn[0]
189
+ if const_expr(self.use_fast_sampling):
190
+ # Loop-invariant per-thread q_idx for the 5 sample points
191
+ # (tidx 0, 1: top corners; 2, 3: bottom corners; 4: center).
192
+ q_idx_sample = m_base
193
+ if tidx == 2 or tidx == 3:
194
+ q_idx_sample = cutlass.min(m_base + self.tile_mn[0] - 1, seqlen_q - 1)
195
+ elif tidx == 4:
196
+ q_idx_sample = m_base + cutlass.min(seqlen_q - m_base, self.tile_mn[0]) // 2
197
+ else:
198
+ q_idx_thread = m_base + tidx
199
+ thread_in_bounds = Boolean(tidx < self.tile_mn[0] and q_idx_thread < seqlen_q)
200
+
201
+ for n_block in cutlass.range(num_n_blocks):
202
  n_base = n_block * self.tile_mn[1]
203
 
204
  if const_expr(self.use_fast_sampling):
205
+ # 5-point sampling (4 corners + center). Interior n_blocks
206
+ # (n_base + tile_n <= seqlen_k) skip the OOB clamp on the right /
207
+ # center samples.
208
+ is_interior = (n_base + self.tile_mn[1]) <= seqlen_k
209
+ n_right = Int32(0)
210
+ n_mid = Int32(0)
211
+ if is_interior:
212
+ n_right = n_base + self.tile_mn[1] - 1
213
+ n_mid = n_base + self.tile_mn[1] // 2
214
+ else:
215
+ n_right = cutlass.min(n_base + self.tile_mn[1] - 1, seqlen_k - 1)
216
+ n_mid = n_base + cutlass.min(seqlen_k - n_base, self.tile_mn[1]) // 2
217
 
218
+ kv_idx = n_base
219
+ if tidx == 1 or tidx == 3:
220
+ kv_idx = n_right
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  elif tidx == 4:
222
+ kv_idx = n_mid
 
 
 
 
223
 
224
+ thread_result = Boolean(False)
225
+ thread_is_valid = Boolean(False)
226
+ if tidx < 5:
227
  thread_is_valid = Boolean(True)
 
 
228
  thread_result = ssa_to_scalar(
229
+ call_mask_mod(
230
+ self.mask_mod,
231
  ssa(batch_idx),
232
  ssa(head_idx),
233
+ ssa(q_idx_sample),
234
+ ssa(kv_idx),
235
  seqlen,
236
+ aux_data,
237
  )
238
  )
 
 
239
 
 
 
240
  has_unmasked = cute.arch.vote_any_sync(thread_result & thread_is_valid)
241
+ has_masked = cute.arch.vote_any_sync(Boolean(not thread_result) & thread_is_valid)
242
 
243
  else:
244
+ # Full path. Interior blocks (n_base + tile_n <= seqlen_k) drop the
245
+ # per-element bound check; the boundary block (at most one) keeps it.
246
  thread_has_unmasked = Boolean(False)
247
  thread_has_masked = Boolean(False)
 
 
 
 
248
  kv_idx = Int32(0)
249
+ is_interior = (n_base + self.tile_mn[1]) <= seqlen_k
 
 
250
 
251
+ if is_interior:
252
+ if thread_in_bounds:
253
+ for c in cutlass.range(self.tile_mn[1], unroll_full=True):
 
 
 
 
 
254
  mask_val = ssa_to_scalar(
255
+ call_mask_mod(
256
+ self.mask_mod,
257
  ssa(batch_idx),
258
  ssa(head_idx),
259
+ ssa(q_idx_thread),
260
+ ssa(n_base + c),
261
  seqlen,
262
+ aux_data,
263
  )
264
  )
265
+ thread_has_unmasked |= Boolean(mask_val)
266
+ thread_has_masked |= Boolean(not mask_val)
267
+ else:
268
+ if thread_in_bounds:
269
+ for c in cutlass.range(self.tile_mn[1], unroll_full=True):
270
+ kv_idx = n_base + c
271
+ if kv_idx < seqlen_k:
272
+ mask_val = ssa_to_scalar(
273
+ call_mask_mod(
274
+ self.mask_mod,
275
+ ssa(batch_idx),
276
+ ssa(head_idx),
277
+ ssa(q_idx_thread),
278
+ ssa(kv_idx),
279
+ seqlen,
280
+ aux_data,
281
+ )
282
+ )
283
+ thread_has_unmasked |= Boolean(mask_val)
284
+ thread_has_masked |= Boolean(not mask_val)
285
 
286
+ warp_unmasked = cute.arch.vote_any_sync(thread_has_unmasked & thread_in_bounds)
287
+ warp_masked = cute.arch.vote_any_sync(thread_has_masked & thread_in_bounds)
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  if lane_id == 0:
289
+ reduction_buffer[warp_idx, 0] = Int8(1) if warp_unmasked else Int8(0)
290
+ reduction_buffer[warp_idx, 1] = Int8(1) if warp_masked else Int8(0)
 
 
291
  cute.arch.sync_threads()
292
 
293
+ # Cross-warp OR via warp 0; thread 0 (lane 0 of warp 0) holds the result.
294
  has_unmasked = Boolean(False)
295
  has_masked = Boolean(False)
296
+ if warp_idx == 0:
297
+ lane_unmasked = Boolean(False)
298
+ lane_masked = Boolean(False)
299
+ if lane_id < self.num_warps:
300
+ lane_unmasked = reduction_buffer[lane_id, 0] != Int8(0)
301
+ lane_masked = reduction_buffer[lane_id, 1] != Int8(0)
302
+ has_unmasked = cute.arch.vote_any_sync(lane_unmasked)
303
+ has_masked = cute.arch.vote_any_sync(lane_masked)
304
 
305
  # Only thread 0 updates the output arrays (common to both paths)
306
  if tidx == 0:
 
312
  is_full = Boolean(has_unmasked and (not has_masked))
313
 
314
  if is_partial:
315
+ curr_mask_idx[num_mask_blocks] = n_block
316
  num_mask_blocks += 1
317
  elif is_full and const_expr(self.compute_full_blocks):
318
+ curr_full_idx[num_full_blocks] = n_block
319
  num_full_blocks += 1
320
 
321
  # Only thread 0 writes back the counts
322
  if tidx == 0:
323
+ mask_cnt, _, full_cnt, *_ = blocksparse_tensors
324
+ if const_expr(self.is_varlen_q):
325
+ mask_cnt[head_idx, global_m_block] = num_mask_blocks
326
+ if const_expr(self.compute_full_blocks):
327
+ full_cnt[head_idx, global_m_block] = num_full_blocks
328
+ else:
329
+ mask_cnt[batch_idx, head_idx, m_block] = num_mask_blocks
330
+ if const_expr(self.compute_full_blocks):
331
+ full_cnt[batch_idx, head_idx, m_block] = num_full_blocks
332
 
333
 
334
  def compute_block_sparsity(
 
339
  seqlen_q,
340
  seqlen_k,
341
  mask_mod: Callable,
342
+ aux_tensors: Optional[list],
343
  device,
344
+ aux_scalars: Optional[tuple] = None,
345
+ cu_seqlens_q: Optional[torch.Tensor] = None,
346
+ cu_seqlens_k: Optional[torch.Tensor] = None,
347
+ seqused_q: Optional[torch.Tensor] = None,
348
+ seqused_k: Optional[torch.Tensor] = None,
349
+ cu_total_m_blocks: Optional[torch.Tensor] = None,
350
+ cu_block_idx_offsets: Optional[torch.Tensor] = None,
351
  compute_full_blocks: bool = True,
352
  use_fast_sampling: bool = False,
353
+ ) -> BlockSparseTensorsTorch:
354
  """
355
  Computes block sparsity for a given `mask_mod`.
356
 
 
364
  mask_mod: The `mask_mod` callable to use.
365
  aux_tensors: A list of auxiliary tensors.
366
  device: The device to use.
367
+ cu_seqlens_q: Cumulative q sequence lengths for varlen
368
+ cu_seqlens_k: Cumulative k sequence lengths for varlen
369
+ seqused_q: Per-batch effective q sequence lengths
370
+ seqused_k: Per-batch effective k sequence lengths
371
+ cu_total_m_blocks: Cumulative total m blocks tensor for varlen q
372
+ cu_block_idx_offsets: Cumulative offsets into the packed mask_block_idx /
373
+ full_block_idx tensors per batch (== cumsum of M_b * N_b).
374
  compute_full_blocks: Whether to compute full blocks. If False, only partially-masked blocks are computed.
375
  use_fast_sampling: Whether to use 5-point sampling (4 corners + center). This is much faster, but only suitable for masks where this check is sufficient.
376
 
377
  Returns:
378
+ BlockSparseTensorsTorch
379
  """
380
+ aux_scalars = tuple(aux_scalars) if aux_scalars else None
381
+
382
+ # Check if mask_mod is marked as suitable for 5-point sampling
383
  use_fast_sampling = getattr(mask_mod, "use_fast_sampling", use_fast_sampling)
384
 
385
  num_m_blocks = (seqlen_q + tile_m - 1) // tile_m
386
  num_n_blocks = (seqlen_k + tile_n - 1) // tile_n
387
 
388
+ if cu_seqlens_q is not None:
389
+ assert cu_total_m_blocks is not None, "total m blocks must be provided when varlen q"
390
+ total_m_blocks = cu_total_m_blocks[-1].item()
391
+ if cu_block_idx_offsets is None and (cu_seqlens_k is not None or seqused_k is not None):
392
+ # Derive cu_block_idx_offsets from per-batch K seqlens.
393
+ cu_block_idx_offsets_list = [0]
394
+ for batch_idx in range(batch_size):
395
+ batch_seqlen_q = cu_seqlens_q[batch_idx + 1].item() - cu_seqlens_q[batch_idx].item()
396
+ if cu_seqlens_k is not None:
397
+ batch_seqlen_k = (
398
+ cu_seqlens_k[batch_idx + 1].item() - cu_seqlens_k[batch_idx].item()
399
+ )
400
+ else:
401
+ batch_seqlen_k = seqused_k[batch_idx].item()
402
+ num_m_blocks_batch = (batch_seqlen_q + tile_m - 1) // tile_m
403
+ num_n_blocks_batch = (batch_seqlen_k + tile_n - 1) // tile_n
404
+ cu_block_idx_offsets_list.append(
405
+ cu_block_idx_offsets_list[-1] + num_m_blocks_batch * num_n_blocks_batch
406
+ )
407
+ cu_block_idx_offsets = torch.tensor(
408
+ cu_block_idx_offsets_list, dtype=torch.int32, device=device
409
+ )
410
+ if cu_block_idx_offsets is not None:
411
+ total_n_blocks = cu_block_idx_offsets[-1].item()
412
+ else:
413
+ # Uniform-K varlen-Q: every batch has the same K seqlen.
414
+ total_n_blocks = total_m_blocks * num_n_blocks
415
+
416
+ mask_block_cnt = torch.zeros((num_heads, total_m_blocks), device=device, dtype=torch.int32)
417
+ mask_block_idx = torch.zeros((num_heads, total_n_blocks), device=device, dtype=torch.int32)
418
+ full_block_cnt = (
419
+ torch.zeros((num_heads, total_m_blocks), device=device, dtype=torch.int32)
420
+ if compute_full_blocks
421
+ else None
422
+ )
423
+ full_block_idx = (
424
+ torch.zeros((num_heads, total_n_blocks), device=device, dtype=torch.int32)
425
+ if compute_full_blocks
426
+ else None
427
+ )
428
+ else:
429
+ total_m_blocks = batch_size * num_m_blocks
430
+ total_n_blocks = batch_size * num_m_blocks * num_n_blocks
431
+
432
+ mask_block_cnt = torch.zeros(
433
+ (batch_size, num_heads, num_m_blocks), device=device, dtype=torch.int32
434
+ )
435
+ mask_block_idx = torch.zeros(
436
  (batch_size, num_heads, num_m_blocks, num_n_blocks), device=device, dtype=torch.int32
437
  )
438
+ full_block_cnt = (
439
+ torch.zeros((batch_size, num_heads, num_m_blocks), device=device, dtype=torch.int32)
440
+ if compute_full_blocks
441
+ else None
442
+ )
443
+ full_block_idx = (
444
+ torch.zeros(
445
+ (batch_size, num_heads, num_m_blocks, num_n_blocks),
446
+ device=device,
447
+ dtype=torch.int32,
448
+ )
449
+ if compute_full_blocks
450
+ else None
451
+ )
452
 
453
  blocksparse_tensors_torch = BlockSparseTensorsTorch(
454
  mask_block_cnt=mask_block_cnt,
455
  mask_block_idx=mask_block_idx,
456
  full_block_cnt=full_block_cnt,
457
  full_block_idx=full_block_idx,
458
+ cu_total_m_blocks=cu_total_m_blocks,
459
+ cu_block_idx_offsets=cu_block_idx_offsets,
460
  block_size=(tile_m, tile_n),
461
  )
462
 
463
  mask_mod_hash = hash_callable(mask_mod)
464
+ if aux_tensors is not None:
465
+ aux_tensor_metadata = get_aux_tensor_metadata(aux_tensors)
466
+ else:
467
+ aux_tensor_metadata = None
468
+ aux_scalar_metadata = tuple(type(s) for s in aux_scalars) if aux_scalars is not None else None
469
 
470
  compile_key = (
471
  tile_m,
472
  tile_n,
473
  mask_mod_hash,
474
+ aux_tensor_metadata,
475
+ aux_scalar_metadata,
476
  compute_full_blocks,
477
+ cu_seqlens_q is None,
478
+ cu_seqlens_k is None,
479
+ seqused_q is None,
480
+ seqused_k is None,
481
  aux_tensors is not None,
482
  use_fast_sampling,
483
  )
484
  if compile_key not in compute_block_sparsity.compile_cache:
485
+ (
486
+ cu_seqlens_q_tensor,
487
+ cu_seqlens_k_tensor,
488
+ seqused_q_tensor,
489
+ seqused_k_tensor,
490
+ ) = [
491
+ to_cute_tensor(t, assumed_align=4, leading_dim=0) if t is not None else None
492
+ for t in (
493
+ cu_seqlens_q,
494
+ cu_seqlens_k,
495
+ seqused_q,
496
+ seqused_k,
497
+ )
498
+ ]
499
+ blocksparse_tensors = to_cute_block_sparse_tensors(
500
+ blocksparse_tensors_torch, enable_tvm_ffi=True
501
+ )
502
+ if aux_tensors is not None:
503
+ cute_aux_tensors = [to_cute_aux_tensor(buf) for buf in aux_tensors]
504
+ else:
505
+ cute_aux_tensors = None
506
  kernel = BlockSparsityKernel(
507
  mask_mod,
508
  tile_mn=(tile_m, tile_n),
 
512
  )
513
 
514
  compute_block_sparsity.compile_cache[compile_key] = cute.compile(
515
+ kernel,
516
+ blocksparse_tensors,
517
+ seqlen_q,
518
+ seqlen_k,
519
+ cu_seqlens_q_tensor,
520
+ cu_seqlens_k_tensor,
521
+ seqused_q_tensor,
522
+ seqused_k_tensor,
523
+ AuxData(cute_aux_tensors, aux_scalars),
524
+ options="--enable-tvm-ffi",
525
  )
526
 
527
+ if not is_fake_mode():
528
+ compute_block_sparsity.compile_cache[compile_key](
529
+ (
530
+ blocksparse_tensors_torch.mask_block_cnt,
531
+ blocksparse_tensors_torch.mask_block_idx,
532
+ blocksparse_tensors_torch.full_block_cnt,
533
+ blocksparse_tensors_torch.full_block_idx,
534
+ blocksparse_tensors_torch.cu_total_m_blocks,
535
+ blocksparse_tensors_torch.cu_block_idx_offsets,
536
+ blocksparse_tensors_torch.dq_write_order,
537
+ blocksparse_tensors_torch.dq_write_order_full,
538
+ ),
539
+ seqlen_q,
540
+ seqlen_k,
541
+ cu_seqlens_q,
542
+ cu_seqlens_k,
543
+ seqused_q,
544
+ seqused_k,
545
+ AuxData(aux_tensors, aux_scalars),
546
+ )
547
 
548
+ return blocksparse_tensors_torch
549
 
550
 
551
  compute_block_sparsity.compile_cache = {}
build/torch-cuda/cu_blocks_kernel.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Callable, Optional
2
+
3
+ import cuda.bindings.driver as cuda
4
+
5
+ import cutlass
6
+ import cutlass.cute as cute
7
+ from cutlass import Int32, const_expr
8
+
9
+ from .utils import get_batch_from_cu_tensor
10
+
11
+
12
+ class CuSeqlensToBlocksKernel:
13
+ def __init__(
14
+ self,
15
+ tile: int = 128,
16
+ num_threads: int = 1024,
17
+ seqlen_q_multiplier: int = 1,
18
+ ):
19
+ self.tile = tile
20
+ self.num_threads = num_threads
21
+ assert num_threads % 32 == 0
22
+ self.num_warps = num_threads // cute.arch.WARP_SIZE
23
+ self.seqlen_q_multiplier = seqlen_q_multiplier
24
+
25
+ @cute.jit
26
+ def __call__(
27
+ self,
28
+ mCuBlocks: cute.Tensor,
29
+ mCuSplitsBlocks: Optional[cute.Tensor],
30
+ mCuSeqlens: Optional[cute.Tensor],
31
+ mSeqUsed: Optional[cute.Tensor] = None,
32
+ mNumSplits: Optional[cute.Tensor] = None,
33
+ mVirtualBatchIdx: Optional[cute.Tensor] = None,
34
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
35
+ stream: cuda.CUstream = None,
36
+ ):
37
+ assert const_expr((mNumSplits is None) == (mCuSplitsBlocks is None))
38
+ assert const_expr(mCuSeqlens is not None or mSeqUsed is not None)
39
+
40
+ @cute.struct
41
+ class SharedStorage:
42
+ warp_block_count: cute.struct.MemRange[Int32, self.num_warps]
43
+ warp_split_count: cute.struct.MemRange[Int32, self.num_warps]
44
+
45
+ self.kernel(
46
+ mCuBlocks,
47
+ mCuSplitsBlocks,
48
+ mCuSeqlens,
49
+ mSeqUsed,
50
+ mNumSplits,
51
+ mVirtualBatchIdx,
52
+ SharedStorage,
53
+ ).launch(
54
+ grid=[1, 1, 1],
55
+ block=[self.num_threads, 1, 1],
56
+ stream=stream,
57
+ )
58
+
59
+ @cute.kernel
60
+ def kernel(
61
+ self,
62
+ mCuBlocks: cute.Tensor,
63
+ mCuSplitsBlocks: Optional[cute.Tensor],
64
+ mCuSeqlens: Optional[cute.Tensor],
65
+ mSeqUsed: Optional[cute.Tensor],
66
+ mNumSplits: Optional[cute.Tensor],
67
+ mVirtualBatchIdx: Optional[cute.Tensor],
68
+ SharedStorage: cutlass.Constexpr[Callable],
69
+ ):
70
+ has_splits = mNumSplits is not None
71
+ batch_size = mCuBlocks.shape[0] - 1
72
+ tidx = cute.arch.thread_idx()[0]
73
+ lane_idx = cute.arch.lane_idx()
74
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
75
+
76
+ smem = cutlass.utils.SmemAllocator()
77
+ storage = smem.allocate(SharedStorage)
78
+ warp_block_count = storage.warp_block_count.get_tensor(cute.make_layout(self.num_warps))
79
+ warp_split_count = storage.warp_split_count.get_tensor(cute.make_layout(self.num_warps))
80
+
81
+ if tidx == 0:
82
+ mCuBlocks[0] = 0
83
+ if const_expr(has_splits):
84
+ mCuSplitsBlocks[0] = 0
85
+
86
+ # Process the batch in chunks of num_threads, carrying the running totals in the bases.
87
+ base = Int32(0)
88
+ base_splits = Int32(0)
89
+ num_chunks = (batch_size + self.num_threads - 1) // self.num_threads
90
+ for chunk in cutlass.range(num_chunks):
91
+ batch_idx = chunk * self.num_threads + tidx
92
+
93
+ seqlen = Int32(0)
94
+ batch_splits = Int32(0)
95
+ if batch_idx < batch_size:
96
+ if const_expr(mVirtualBatchIdx is not None):
97
+ batch_idx = Int32(mVirtualBatchIdx[batch_idx])
98
+ if const_expr(mSeqUsed is not None):
99
+ seqlen = mSeqUsed[batch_idx]
100
+ else:
101
+ seqlen = mCuSeqlens[batch_idx + 1] - mCuSeqlens[batch_idx]
102
+ if const_expr(has_splits):
103
+ batch_splits = mNumSplits[batch_idx]
104
+ seqlen *= self.seqlen_q_multiplier
105
+ num_blocks = (seqlen + self.tile - 1) // self.tile
106
+ num_split_blocks = num_blocks * batch_splits
107
+
108
+ total_blocks_for_batch = num_blocks
109
+ total_split_blocks_for_batch = num_split_blocks
110
+
111
+ for delta in (1, 2, 4, 8, 16):
112
+ other = cute.arch.shuffle_sync_up(total_blocks_for_batch, delta, mask_and_clamp=0)
113
+ if const_expr(has_splits):
114
+ other_splits = cute.arch.shuffle_sync_up(
115
+ total_split_blocks_for_batch, delta, mask_and_clamp=0
116
+ )
117
+ if lane_idx >= delta:
118
+ total_split_blocks_for_batch += other_splits
119
+ if lane_idx >= delta:
120
+ total_blocks_for_batch += other
121
+
122
+ if lane_idx == 31:
123
+ warp_block_count[warp_idx] = total_blocks_for_batch
124
+ if const_expr(has_splits):
125
+ warp_split_count[warp_idx] = total_split_blocks_for_batch
126
+
127
+ cute.arch.sync_threads()
128
+
129
+ total_blocks_for_batch += base
130
+ total_split_blocks_for_batch += base_splits
131
+
132
+ for idx in cutlass.range(warp_idx):
133
+ total_blocks_for_batch += warp_block_count[idx]
134
+ if const_expr(has_splits):
135
+ total_split_blocks_for_batch += warp_split_count[idx]
136
+
137
+ if batch_idx < batch_size:
138
+ mCuBlocks[chunk * self.num_threads + tidx + 1] = total_blocks_for_batch
139
+ if const_expr(has_splits):
140
+ mCuSplitsBlocks[chunk * self.num_threads + tidx + 1] = (
141
+ total_split_blocks_for_batch
142
+ )
143
+
144
+ for idx in cutlass.range(self.num_warps):
145
+ base += warp_block_count[idx]
146
+ if const_expr(has_splits):
147
+ base_splits += warp_split_count[idx]
148
+ # warp_block_count / warp_split_count are reused by the next chunk.
149
+ cute.arch.sync_threads()
150
+
151
+
152
+ class CuBlocksToBatchKernel:
153
+ """Inverts a cumulative block count into a flat block -> batch lookup."""
154
+
155
+ def __init__(self, num_threads: int = 128):
156
+ self.num_threads = num_threads
157
+
158
+ @cute.jit
159
+ def __call__(
160
+ self,
161
+ mCuTotalBlocks: cute.Tensor,
162
+ mBlocksToBatchIdx: cute.Tensor,
163
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
164
+ stream: cuda.CUstream = None,
165
+ ):
166
+ num_blocks = mBlocksToBatchIdx.shape[0]
167
+ self.kernel(mCuTotalBlocks, mBlocksToBatchIdx).launch(
168
+ grid=[(num_blocks + self.num_threads - 1) // self.num_threads, 1, 1],
169
+ block=[self.num_threads, 1, 1],
170
+ stream=stream,
171
+ )
172
+
173
+ @cute.kernel
174
+ def kernel(
175
+ self,
176
+ mCuTotalBlocks: cute.Tensor,
177
+ mBlocksToBatchIdx: cute.Tensor,
178
+ ):
179
+ block_idx = cute.arch.block_idx()[0] * self.num_threads + cute.arch.thread_idx()[0]
180
+ if block_idx < mBlocksToBatchIdx.shape[0]:
181
+ mBlocksToBatchIdx[block_idx] = get_batch_from_cu_tensor(block_idx, mCuTotalBlocks)
build/torch-cuda/cute_dsl_utils.py CHANGED
@@ -1,12 +1,11 @@
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
@@ -15,7 +14,6 @@ except ImportError:
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
  from cutlass.cute.runtime import from_dlpack
21
 
@@ -30,6 +28,8 @@ torch2cute_dtype_map = {
30
  torch.float16: cutlass.Float16,
31
  torch.bfloat16: cutlass.BFloat16,
32
  torch.float32: cutlass.Float32,
 
 
33
  }
34
 
35
 
@@ -43,61 +43,73 @@ 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 ArgumentsBase(JitArgument):
48
- def __c_pointers__(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
- c_ptrs = []
52
- for obj in non_constexpr_fields:
53
- if hasattr(obj, "__c_pointers__"):
54
- c_ptrs.extend(obj.__c_pointers__())
55
- return c_ptrs
56
-
57
- def __get_mlir_types__(self):
58
- all_fields = [getattr(self, field.name) for field in fields(self)]
59
- non_constexpr_fields = [f for f in all_fields if not isinstance(f, StaticTypes)]
60
- types, self._values_pos = [], []
61
- for obj in non_constexpr_fields:
62
- if hasattr(obj, "__get_mlir_types__"):
63
- obj_types = obj.__get_mlir_types__()
64
- types.extend(obj_types)
65
- self._values_pos.append(len(obj_types))
66
- else:
67
- self._values_pos.append(0)
68
- return types
69
-
70
- def __new_from_mlir_values__(self, values):
71
- all_fields = {field.name: getattr(self, field.name) for field in fields(self)}
72
- constexpr_fields = {n: f for n, f in all_fields.items() if isinstance(f, StaticTypes)}
73
- non_constexpr_fields = {
74
- n: f for n, f in all_fields.items() if not isinstance(f, StaticTypes)
75
- }
76
- for (name, field), n_items in zip(non_constexpr_fields.items(), self._values_pos):
77
- non_constexpr_fields[name] = cutlass.new_from_mlir_values(field, values[:n_items])
78
- values = values[n_items:]
79
- return self.__class__(**non_constexpr_fields, **constexpr_fields)
80
-
81
-
82
- def load_cubin_module_data_patched(cubin_data, filepath):
83
- pathlib.Path(filepath).write_bytes(cubin_data)
84
- return load_cubin_module_data_og(cubin_data)
85
-
86
-
87
- def cute_compile_patched(*args, **kwargs):
88
- """A patched version of cute.compile that dump the SASS to a file if CUTE_CUBIN_PATH is set."""
89
- cubin_path = os.getenv("CUTE_CUBIN_PATH", None)
90
- if cubin_path is not None:
91
- cutlass.base_dsl.runtime.cuda.load_cubin_module_data = partial(
92
- load_cubin_module_data_patched, filepath=cubin_path
 
 
 
93
  )
94
- output = cute_compile_og(*args, **kwargs)
95
- if cubin_path is not None:
96
- cutlass.base_dsl.runtime.cuda.load_cubin_module_data = load_cubin_module_data_og
97
- if extract is not None:
98
- sass = extract(cubin_path, None)
99
- pathlib.Path(cubin_path).with_suffix(".annotated.sass").write_text(sass)
100
- return output
 
 
 
 
 
 
 
 
 
101
 
102
 
103
  def assume_strides_aligned(t):
@@ -120,7 +132,22 @@ def assume_tensor_aligned(t):
120
 
121
  def to_cute_tensor(t, assumed_align=16, leading_dim=-1, fully_dynamic=False, enable_tvm_ffi=True):
122
  """Convert torch tensor to cute tensor for TVM FFI. leading_dim=-1 defaults to t.ndim-1."""
123
- tensor = from_dlpack(t.detach(), assumed_align=assumed_align, enable_tvm_ffi=enable_tvm_ffi)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  if fully_dynamic:
125
  return tensor.mark_layout_dynamic()
126
  if leading_dim == -1:
@@ -146,15 +173,37 @@ def to_cute_aux_tensor(t, enable_tvm_ffi=True):
146
  )
147
 
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  def get_aux_tensor_metadata(aux_tensors):
150
- return tuple(
151
- (
152
- getattr(t, "__assumed_align__", 0),
153
- getattr(t, "__leading_dim__", -1),
154
- hasattr(t, "__leading_dim__"),
 
 
155
  )
156
- for t in aux_tensors
157
- )
158
 
159
 
160
  def get_broadcast_dims(tensor: torch.Tensor) -> Tuple[bool, ...]:
@@ -164,4 +213,43 @@ def get_broadcast_dims(tensor: torch.Tensor) -> Tuple[bool, ...]:
164
  stride=0 as static, meaning kernels compiled with different broadcast
165
  patterns are not interchangeable.
166
  """
167
- return tuple(s == 0 for s in tensor.stride())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Copyright (c) 2025, Tri Dao.
2
 
3
  import os
 
4
  from typing import Tuple
5
+ from functools import lru_cache
 
6
 
7
  import torch
8
+ from torch._subclasses.fake_tensor import FakeTensor
9
 
10
  try:
11
  from triton.tools.disasm import extract
 
14
 
15
  import cutlass
16
  import cutlass.cute as cute
 
17
  from cutlass.cutlass_dsl import NumericMeta
18
  from cutlass.cute.runtime import from_dlpack
19
 
 
28
  torch.float16: cutlass.Float16,
29
  torch.bfloat16: cutlass.BFloat16,
30
  torch.float32: cutlass.Float32,
31
+ torch.float8_e4m3fn: cutlass.Float8E4M3FN,
32
+ torch.float8_e5m2: cutlass.Float8E5M2,
33
  }
34
 
35
 
 
43
  return torch.cuda.get_device_capability(device)
44
 
45
 
46
+ @lru_cache
47
+ def _get_device_arch_and_num_sms(device_index: int) -> tuple[int, int]:
48
+ properties = torch.cuda.get_device_properties(device_index)
49
+ return properties.major * 10 + properties.minor, properties.multi_processor_count
50
+
51
+
52
+ def get_num_sms_for_selection(device_index: int, arch: int) -> int:
53
+ """Return the SM count of the matching local GPU or cross-compilation target."""
54
+ override = os.getenv("FLASH_ATTENTION_NUM_SMS")
55
+ if override is not None:
56
+ num_sms = int(override)
57
+ if num_sms <= 0:
58
+ raise ValueError("FLASH_ATTENTION_NUM_SMS must be positive")
59
+ return num_sms
60
+ if torch.cuda.is_available():
61
+ device_arch, num_sms = _get_device_arch_and_num_sms(device_index)
62
+ if device_arch == arch:
63
+ return num_sms
64
+ raise RuntimeError(
65
+ "Cannot determine the target GPU's SM count; set FLASH_ATTENTION_NUM_SMS "
66
+ "when cross-compiling without a matching local GPU"
67
+ )
68
+
69
+
70
+ def _has_aligned_pointer(tensor: torch.Tensor, align_bytes: int) -> bool:
71
+ address = (
72
+ tensor.storage_offset() * tensor.element_size()
73
+ if isinstance(tensor, FakeTensor)
74
+ else tensor.data_ptr()
75
+ )
76
+ return address % align_bytes == 0
77
+
78
+
79
+ def _is_aligned_layout(tensor: torch.Tensor, align_bytes: int) -> bool:
80
+ """Return whether a tensor satisfies the pointer and stride ABI kernels assume."""
81
+ if tensor.stride(-1) != 1 or not _has_aligned_pointer(tensor, align_bytes):
82
+ return False
83
+ stride_alignment = max(1, align_bytes // tensor.element_size())
84
+ return all(stride == 0 or stride % stride_alignment == 0 for stride in tensor.stride()[:-1])
85
+
86
+
87
+ def maybe_contiguous(tensor: torch.Tensor | None, align_bytes: int = 16):
88
+ """Canonicalize inputs to the pointer and stride alignment kernels assume."""
89
+ if tensor is None:
90
+ return None
91
+ if tensor.is_contiguous():
92
+ return (
93
+ tensor
94
+ if _has_aligned_pointer(tensor, align_bytes)
95
+ else tensor.clone(memory_format=torch.contiguous_format)
96
  )
97
+ if not _has_aligned_pointer(tensor, align_bytes):
98
+ return tensor.clone(memory_format=torch.contiguous_format)
99
+ return tensor if _is_aligned_layout(tensor, align_bytes) else tensor.contiguous()
100
+
101
+
102
+ def validate_output_layout(tensor: torch.Tensor, name: str, align_bytes: int) -> None:
103
+ """Validate a caller-provided output or SplitKV workspace."""
104
+ assert 0 not in tensor.stride(), f"{name} must not have broadcast dimensions"
105
+ if tensor.is_contiguous():
106
+ assert _has_aligned_pointer(tensor, align_bytes), (
107
+ f"{name} must have aligned strides and a contiguous last dimension"
108
+ )
109
+ return
110
+ assert _is_aligned_layout(tensor, align_bytes), (
111
+ f"{name} must have aligned strides and a contiguous last dimension"
112
+ )
113
 
114
 
115
  def assume_strides_aligned(t):
 
132
 
133
  def to_cute_tensor(t, assumed_align=16, leading_dim=-1, fully_dynamic=False, enable_tvm_ffi=True):
134
  """Convert torch tensor to cute tensor for TVM FFI. leading_dim=-1 defaults to t.ndim-1."""
135
+ if t is None:
136
+ return None
137
+ # NOTE: torch 2.9.1 doesn't support fp8 via DLPack but 2.11.0 nightly does
138
+ # currently export raw bytes as uint8 and tell cutlass correct type
139
+ # can directly export as fp8 when torch supports it
140
+ if t.dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
141
+ tensor = from_dlpack(
142
+ t.view(torch.uint8).detach(),
143
+ assumed_align=assumed_align,
144
+ enable_tvm_ffi=enable_tvm_ffi,
145
+ )
146
+ tensor.element_type = (
147
+ cutlass.Float8E4M3FN if t.dtype == torch.float8_e4m3fn else cutlass.Float8E5M2
148
+ )
149
+ else:
150
+ tensor = from_dlpack(t.detach(), assumed_align=assumed_align, enable_tvm_ffi=enable_tvm_ffi)
151
  if fully_dynamic:
152
  return tensor.mark_layout_dynamic()
153
  if leading_dim == -1:
 
173
  )
174
 
175
 
176
+ def _resolve_aux_leading_dim(tensor: torch.Tensor) -> int | None:
177
+ """Pick the mode CuTe keeps as a static stride-1 leading dimension."""
178
+ leading_dim = getattr(tensor, "__leading_dim__", None)
179
+ if leading_dim is not None:
180
+ if tensor.ndim == 0:
181
+ raise ValueError("Scalar aux tensors cannot declare __leading_dim__")
182
+ leading_dim %= tensor.ndim
183
+ if tensor.stride(leading_dim) != 1:
184
+ raise ValueError("Aux tensor __leading_dim__ must identify a stride-1 dimension")
185
+ return leading_dim
186
+
187
+ unit_stride_dims = [dim for dim, stride in enumerate(tensor.stride()) if stride == 1]
188
+ if len(unit_stride_dims) <= 1:
189
+ return unit_stride_dims[0] if unit_stride_dims else None
190
+ nontrivial_dims = [dim for dim in unit_stride_dims if tensor.shape[dim] > 1]
191
+ if len(nontrivial_dims) != 1:
192
+ raise ValueError("Aux tensor layout has no unique stride-1 leading dimension")
193
+ return nontrivial_dims[0]
194
+
195
+
196
  def get_aux_tensor_metadata(aux_tensors):
197
+ """Return the static aux-tensor ABI facts that must key the compile cache."""
198
+ metadata = []
199
+ for tensor in aux_tensors:
200
+ leading_dim = _resolve_aux_leading_dim(tensor)
201
+ static_strides = tuple(
202
+ 0 if stride == 0 else 1 if dim == leading_dim else None
203
+ for dim, stride in enumerate(tensor.stride())
204
  )
205
+ metadata.append((tensor.dtype, getattr(tensor, "__assumed_align__", None), static_strides))
206
+ return tuple(metadata)
207
 
208
 
209
  def get_broadcast_dims(tensor: torch.Tensor) -> Tuple[bool, ...]:
 
213
  stride=0 as static, meaning kernels compiled with different broadcast
214
  patterns are not interchangeable.
215
  """
216
+ strides = tensor.stride()
217
+ # Written this way for speed.
218
+ if 0 not in strides:
219
+ return (False,) * len(strides)
220
+ return tuple(stride == 0 for stride in strides)
221
+
222
+
223
+ # credit: monellz (https://github.com/NVIDIA/cutlass/issues/2658#issuecomment-3630564264)
224
+ def dump_kernel_attributes(compiled_kernel):
225
+ from cuda.bindings import driver
226
+ from cutlass.utils import HardwareInfo
227
+ import torch
228
+
229
+ device_id = torch.cuda.current_device()
230
+ hardware_info = HardwareInfo(device_id=device_id)
231
+ cubin_data = compiled_kernel.artifacts.CUBIN
232
+ assert cubin_data is not None, "cubin_data is None, need '--keep-cubin' option when compiling"
233
+ cuda_library = hardware_info._checkCudaErrors(
234
+ driver.cuLibraryLoadData(cubin_data, None, None, 0, None, None, 0)
235
+ )
236
+ kernels = hardware_info._checkCudaErrors(driver.cuLibraryEnumerateKernels(1, cuda_library))
237
+ kernel = hardware_info._checkCudaErrors(driver.cuKernelGetFunction(kernels[0]))
238
+ # more metrics: https://docs.nvidia.com/cuda/cuda-driver-api/group__CUDA__EXEC.html#group__CUDA__EXEC_1g5e92a1b0d8d1b82cb00dcfb2de15961b
239
+ local_size_bytes = hardware_info._checkCudaErrors(
240
+ driver.cuFuncGetAttribute(
241
+ driver.CUfunction_attribute.CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES,
242
+ kernel,
243
+ )
244
+ )
245
+ num_regs = hardware_info._checkCudaErrors(
246
+ driver.cuFuncGetAttribute(
247
+ driver.CUfunction_attribute.CU_FUNC_ATTRIBUTE_NUM_REGS,
248
+ kernel,
249
+ )
250
+ )
251
+
252
+ print("--- Kernel Info ---")
253
+ print(f"local_size_bytes: {local_size_bytes}")
254
+ print(f"num_regs: {num_regs}")
255
+ print("--- End Kernel Info ---")
build/torch-cuda/fa_logging.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ """Unified FlashAttention logging controlled by a single ``FA_LOG_LEVEL`` env var.
4
+
5
+ Host-side messages go through Python ``logging`` (logger name ``flash_attn``).
6
+ A default ``StreamHandler`` is attached automatically when ``FA_LOG_LEVEL >= 1``
7
+ so that standalone scripts get output without extra setup; applications that
8
+ configure their own logging can remove or replace it via the standard API.
9
+
10
+ FA_LOG_LEVEL mapping::
11
+
12
+ 0 off nothing logged
13
+ 1 host host-side summaries only (no kernel printf)
14
+ 2 kernel host + curated kernel traces
15
+ 3 max host + all kernel traces (noisy, perf hit)
16
+
17
+ Set via environment variable::
18
+
19
+ FA_LOG_LEVEL=1 python train.py
20
+
21
+ Device-side ``cute.printf`` calls are compile-time eliminated via
22
+ ``cutlass.const_expr`` when the log level is below the callsite threshold,
23
+ so there is zero performance cost when device logging is off.
24
+ Changing the log level after kernel compilation requires a recompile
25
+ (the level participates in the forward compile key).
26
+ """
27
+
28
+ import logging
29
+ import os
30
+ import sys
31
+
32
+ import cutlass.cute as cute
33
+ from cutlass import const_expr
34
+
35
+ _LOG_LEVEL_NAMES = {"off": 0, "host": 1, "kernel": 2, "max": 3}
36
+
37
+
38
+ def _parse_log_level(raw: str) -> int:
39
+ if raw in _LOG_LEVEL_NAMES:
40
+ return _LOG_LEVEL_NAMES[raw]
41
+ try:
42
+ level = int(raw)
43
+ except ValueError:
44
+ return 0
45
+ return max(0, min(level, 3))
46
+
47
+
48
+ _fa_log_level: int = _parse_log_level(os.environ.get("FA_LOG_LEVEL", "0"))
49
+
50
+ _logger = logging.getLogger("flash_attn")
51
+ _logger.addHandler(logging.NullHandler())
52
+ _default_handler: logging.Handler | None = None
53
+
54
+
55
+ def _configure_default_handler() -> None:
56
+ global _default_handler
57
+ if _fa_log_level >= 1:
58
+ if _default_handler is None:
59
+ _default_handler = logging.StreamHandler(sys.stdout)
60
+ _default_handler.setFormatter(logging.Formatter("[FA] %(message)s"))
61
+ _logger.addHandler(_default_handler)
62
+ _logger.setLevel(logging.DEBUG)
63
+ else:
64
+ if _default_handler is not None:
65
+ _logger.removeHandler(_default_handler)
66
+ _default_handler = None
67
+ _logger.setLevel(logging.WARNING)
68
+
69
+
70
+ _configure_default_handler()
71
+
72
+
73
+ def get_fa_log_level() -> int:
74
+ return _fa_log_level
75
+
76
+
77
+ def set_fa_log_level(level: int | str) -> None:
78
+ """Set the FA log level programmatically.
79
+
80
+ Host logging takes effect immediately. Device logging changes only
81
+ affect kernels compiled after this call (new compile-key selection).
82
+ """
83
+ global _fa_log_level
84
+ if isinstance(level, str):
85
+ level = _parse_log_level(level)
86
+ _fa_log_level = max(0, min(int(level), 3))
87
+ _configure_default_handler()
88
+
89
+
90
+ def fa_log(level: int, msg: str):
91
+ if _fa_log_level >= level:
92
+ _logger.info(msg)
93
+
94
+
95
+ def fa_printf(level: int, fmt, *args):
96
+ if const_expr(_fa_log_level >= level):
97
+ cute.printf(fmt, *args)
build/torch-cuda/flash_attn4/__init__.py DELETED
@@ -1,26 +0,0 @@
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-cuda/flash_bwd.py CHANGED
@@ -11,7 +11,7 @@ import cuda.bindings.driver as cuda
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 .quack import layout_utils
@@ -19,9 +19,13 @@ from . import ampere_helpers as sm80_utils
19
  from .cute_dsl_utils import assume_tensor_aligned
20
  from . import utils
21
  from .mask import AttentionMask
 
22
  from .seqlen_info import SeqlenInfoQK
 
23
  from .quack.cute_dsl_utils import ParamsBase
24
  from .tile_scheduler import SingleTileScheduler, SingleTileVarlenScheduler, TileSchedulerArguments
 
 
25
 
26
 
27
  class FlashAttentionBackwardSm80:
@@ -38,6 +42,7 @@ class FlashAttentionBackwardSm80:
38
  num_threads: int = 256,
39
  pack_gqa: bool = False,
40
  is_causal: bool = False,
 
41
  SdP_swapAB: bool = False,
42
  dKV_swapAB: bool = False,
43
  dQ_swapAB: bool = False,
@@ -45,6 +50,8 @@ class FlashAttentionBackwardSm80:
45
  AtomLayoutNdKV: int = 8,
46
  AtomLayoutMdQ: int = 1,
47
  V_in_regs: bool = False,
 
 
48
  ):
49
  """Initializes the configuration for a flash attention v2 kernel.
50
 
@@ -62,7 +69,8 @@ class FlashAttentionBackwardSm80:
62
  :param is_causal: is causal
63
  """
64
  self.dtype = dtype
65
- # padding head_dim to a multiple of 16 as k_block_size
 
66
  hdim_multiple_of = 32
67
  self.head_dim_padded = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
68
  head_dim_v = head_dim_v if head_dim_v is not None else head_dim
@@ -77,6 +85,7 @@ class FlashAttentionBackwardSm80:
77
  self.num_threads = num_threads
78
  self.pack_gqa = pack_gqa
79
  self.is_causal = is_causal
 
80
  self.num_stages_Q = num_stages_Q
81
  self.num_stages_dO = num_stages_dO
82
  self.SdP_swapAB = SdP_swapAB
@@ -89,6 +98,8 @@ class FlashAttentionBackwardSm80:
89
  self.Mma_dKV_is_RS = AtomLayoutMSdP == 1 and AtomLayoutNdKV == num_mma_warps and SdP_swapAB and not dKV_swapAB
90
  self.V_in_regs = V_in_regs
91
  self.share_QV_smem = V_in_regs
 
 
92
 
93
  @staticmethod
94
  def can_implement(
@@ -161,13 +172,13 @@ class FlashAttentionBackwardSm80:
161
  else:
162
  if cutlass.const_expr(not (mdK_type == mdV_type == cutlass.Float32)):
163
  raise TypeError("mdKaccum and mdVaccum tensors must have the data type Float32")
164
- if cutlass.const_expr(not mQ_type in [cutlass.Float16, cutlass.BFloat16]):
165
  raise TypeError("Only Float16 or BFloat16 is supported")
166
- if cutlass.const_expr(not mLSE_type in [cutlass.Float32]):
167
  raise TypeError("LSE tensor must be Float32")
168
- if cutlass.const_expr(not mdPsum_type in [cutlass.Float32]):
169
  raise TypeError("dPsum tensor must be Float32")
170
- if cutlass.const_expr(not mdQaccum_type in [cutlass.Float32]):
171
  raise TypeError("dQaccum tensor must be Float32")
172
  if cutlass.const_expr(mCuSeqlensQ_type not in [None, cutlass.Int32]):
173
  raise TypeError("cuSeqlensQ tensor must be Int32")
@@ -372,17 +383,24 @@ class FlashAttentionBackwardSm80:
372
  mdK: cute.Tensor,
373
  mdV: cute.Tensor,
374
  softmax_scale: cutlass.Float32,
375
- stream: cuda.CUstream,
376
  mCuSeqlensQ: Optional[cute.Tensor] = None,
377
  mCuSeqlensK: Optional[cute.Tensor] = None,
378
  mSeqUsedQ: Optional[cute.Tensor] = None,
379
  mSeqUsedK: Optional[cute.Tensor] = None,
380
- softcap: Float32 | float | None = None,
381
  window_size_left: Int32 | int | None = None,
382
  window_size_right: Int32 | int | None = None,
383
  mdQ_semaphore: Optional[cute.Tensor] = None,
 
 
 
 
 
 
 
384
  ):
385
- assert mdQ_semaphore is None, "semaphore not supported yet"
 
 
386
  # Get the data type and check if it is fp16 or bf16
387
  self._check_type(*(t.element_type if t is not None else None
388
  for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV, mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK)))
@@ -417,12 +435,15 @@ class FlashAttentionBackwardSm80:
417
  qhead_per_kvhead_packgqa=self.qhead_per_kvhead if cutlass.const_expr(self.pack_gqa) else 1,
418
  mCuSeqlensQ=mCuSeqlensK,
419
  mSeqUsedQ=mSeqUsedK,
 
420
  )
421
 
422
  tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
423
  grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
424
 
425
- softmax_scale_log2 = softmax_scale * math.log2(math.e)
 
 
426
  self.kernel(
427
  mQ,
428
  mK,
@@ -439,6 +460,8 @@ class FlashAttentionBackwardSm80:
439
  mSeqUsedK,
440
  softmax_scale,
441
  softmax_scale_log2,
 
 
442
  self.sQ_layout,
443
  self.sK_layout,
444
  self.sV_layout,
@@ -458,6 +481,7 @@ class FlashAttentionBackwardSm80:
458
  SharedStorage,
459
  tile_sched_params,
460
  TileScheduler,
 
461
  ).launch(
462
  grid=grid_dim,
463
  block=[self.num_threads, 1, 1],
@@ -483,6 +507,8 @@ class FlashAttentionBackwardSm80:
483
  mSeqUsedK: Optional[cute.Tensor],
484
  softmax_scale: cutlass.Float32,
485
  softmax_scale_log2: cutlass.Float32,
 
 
486
  sQ_layout: cute.ComposedLayout,
487
  sK_layout: cute.ComposedLayout,
488
  sV_layout: cute.ComposedLayout,
@@ -502,6 +528,7 @@ class FlashAttentionBackwardSm80:
502
  SharedStorage: cutlass.Constexpr,
503
  tile_sched_params: ParamsBase,
504
  TileScheduler: cutlass.Constexpr[Callable],
 
505
  ):
506
  # Thread index, block index
507
  tidx, _, _ = cute.arch.thread_idx()
@@ -512,15 +539,28 @@ class FlashAttentionBackwardSm80:
512
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
513
 
514
  if work_tile.is_valid_tile:
515
- seqlen = SeqlenInfoQK.create(batch_idx, mQ.shape[1], mK.shape[1], mCuSeqlensQ=mCuSeqlensQ, mCuSeqlensK=mCuSeqlensK, mSeqUsedQ=mSeqUsedQ, mSeqUsedK=mSeqUsedK)
516
-
517
- m_block_max = cute.ceil_div(seqlen.seqlen_q, self.m_block_size)
518
- m_block_min = 0
519
- if cutlass.const_expr(self.is_causal):
520
- m_block_min = max(
521
- (n_block * self.n_block_size + seqlen.seqlen_q - seqlen.seqlen_k) // self.m_block_size,
522
- m_block_min,
523
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  # TODO: return early if m_block_max == 0
525
 
526
  # ///////////////////////////////////////////////////////////////////////////////
@@ -538,7 +578,7 @@ class FlashAttentionBackwardSm80:
538
  mdPsum_cur = mdPsum[batch_idx, head_idx, None]
539
  mdQaccum_cur = mdQaccum[batch_idx, head_idx, None]
540
  else:
541
- padded_offset_q = seqlen.offset_q + batch_idx * self.m_block_size
542
  mQ_cur = cute.domain_offset((seqlen.offset_q, 0), mQ[None, head_idx, None])
543
  mLSE_cur = cute.domain_offset((padded_offset_q,), mLSE[head_idx, None])
544
  mdO_cur = cute.domain_offset((seqlen.offset_q, 0), mdO[None, head_idx, None])
@@ -615,8 +655,8 @@ class FlashAttentionBackwardSm80:
615
  thr_mma_dq = tiled_mma_dq.get_slice(tidx)
616
  acc_shape_dK = thr_mma_dkv.partition_shape_C((self.n_block_size, self.head_dim_padded))
617
  acc_shape_dV = thr_mma_dkv.partition_shape_C((self.n_block_size, self.head_dim_v_padded))
618
- acc_dK = cute.make_fragment(acc_shape_dK, cutlass.Float32)
619
- acc_dV = cute.make_fragment(acc_shape_dV, cutlass.Float32)
620
  acc_dK.fill(0.0)
621
  acc_dV.fill(0.0)
622
 
@@ -755,63 +795,73 @@ class FlashAttentionBackwardSm80:
755
  smem_copy_params=smem_copy_params, gmem_copy_params=gmem_copy_params,
756
  load_Q_LSE=load_Q_LSE, load_dO_dPsum=load_dO_dPsum,
757
  m_block_max=m_block_max,
 
758
  softmax_scale_log2=softmax_scale_log2,
 
759
  )
760
 
761
- # ///////////////////////////////////////////////////////////////////////////////
762
- # Prologue
763
- # ///////////////////////////////////////////////////////////////////////////////
764
- # Start async loads of the last mn-tile, where we take care of the mn residue
765
- self.load_V(gmem_thr_copy_VdO, tVgV, tVsV, n_block, seqlen=seqlen.seqlen_k,
766
- headdim=d_head_v)
767
- if cutlass.const_expr(self.V_in_regs):
768
- cute.arch.cp_async_commit_group()
769
- self.load_K(gmem_thr_copy_QK, tKgK, tKsK, n_block, seqlen=seqlen.seqlen_k,
770
- headdim=d_head)
771
- cute.arch.cp_async_commit_group()
772
-
773
- if cutlass.const_expr(self.V_in_regs):
774
- cute.arch.cp_async_wait_group(1)
775
- cute.arch.barrier()
776
- tdPrV_copy_view = smem_thr_copy_KV.retile(tdPrV)
777
- cute.copy(smem_thr_copy_KV, tdPsV, tdPrV_copy_view)
778
- # Sync to avoid loading Q to smem_q, which overlaps with smem_v
779
- cute.arch.barrier()
780
-
781
- m_block = m_block_min
782
- assert self.num_stages_Q >= self.num_stages_dO
783
- for stage in cutlass.range_constexpr(self.num_stages_Q):
784
- if cutlass.const_expr(self.num_stages_Q == 1 or stage < self.num_stages_Q - 1):
785
- if stage == 0 or m_block + stage < m_block_max:
786
- load_Q_LSE(m_block + stage, smem_pipe_write_q=stage)
787
- cute.arch.cp_async_commit_group()
788
- if cutlass.const_expr(stage < self.num_stages_dO):
789
- if stage == 0 or m_block + stage < m_block_max:
790
- load_dO_dPsum(m_block + stage, smem_pipe_write_q=stage)
791
  cute.arch.cp_async_commit_group()
 
 
 
792
 
793
- # ///////////////////////////////////////////////////////////////////////////////
794
- # Mainloop
795
- # ///////////////////////////////////////////////////////////////////////////////
796
- # Start processing of the first n-block.
797
- mask = AttentionMask(self.m_block_size, self.n_block_size, seqlen.seqlen_q, seqlen.seqlen_k)
798
- mask_fn = partial(
799
- mask.apply_mask, n_block=n_block, thr_mma=thr_mma_sdp,
800
- mask_seqlen=True, mask_causal=self.is_causal
801
- )
802
- smem_pipe_read_q = cutlass.Int32(0)
803
- smem_pipe_read_do = cutlass.Int32(0)
804
- smem_pipe_write_q = cutlass.Int32(self.num_stages_Q - 1)
805
- smem_pipe_write_do = cutlass.Int32(0)
806
- for m_tile in cutlass.range(m_block_min, m_block_max, unroll=1):
807
- compute_one_m_block(
808
- m_tile, smem_pipe_read_q, smem_pipe_read_do, smem_pipe_write_q, smem_pipe_write_do,
809
- mask_fn=mask_fn,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
810
  )
811
- smem_pipe_read_q = self.advance_pipeline(smem_pipe_read_q, self.num_stages_Q)
812
- smem_pipe_read_do = self.advance_pipeline(smem_pipe_read_do, self.num_stages_dO)
813
- smem_pipe_write_q = self.advance_pipeline(smem_pipe_write_q, self.num_stages_Q)
814
- smem_pipe_write_do = self.advance_pipeline(smem_pipe_write_do, self.num_stages_dO)
 
 
 
 
 
 
 
 
 
815
 
816
  # ///////////////////////////////////////////////////////////////////////////////
817
  # Epilogue
@@ -842,7 +892,9 @@ class FlashAttentionBackwardSm80:
842
  load_Q_LSE: Callable,
843
  load_dO_dPsum: Callable,
844
  m_block_max: cutlass.Int32,
 
845
  softmax_scale_log2: cutlass.Float32,
 
846
  mask_fn: Optional[Callable] = None,
847
  ):
848
  def load_Q_next():
@@ -860,7 +912,7 @@ class FlashAttentionBackwardSm80:
860
  acc_shape_SdP = mma_params.thr_mma_sdp.partition_shape_C(
861
  (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)
862
  )
863
- acc_S = cute.make_fragment(acc_shape_SdP, cutlass.Float32)
864
  acc_S.fill(0.0)
865
  cute.arch.cp_async_wait_group(1 if cutlass.const_expr(self.num_stages_Q > 1) else 0)
866
  cute.arch.barrier()
@@ -871,13 +923,30 @@ class FlashAttentionBackwardSm80:
871
  smem_copy_params.smem_thr_copy_QdO, smem_copy_params.smem_thr_copy_KV,
872
  swap_AB=self.SdP_swapAB,
873
  )
 
 
874
  tLSErLSE = cute.make_fragment_like(smem_copy_params.tSsLSEMma[None, 0])
875
  cute.autovec_copy(
876
  smem_copy_params.tSsLSEMma[None, smem_pipe_read_q if cutlass.const_expr(self.num_stages_Q > 1) else 0], tLSErLSE
877
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
878
  if cutlass.const_expr(mask_fn is not None):
879
  mask_fn(acc_S, m_block=m_block)
880
- acc_S_mn = layout_utils.reshape_acc_to_mn(acc_S)
881
  bidx = 0
882
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_S_mn)
883
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == 1: cute.print_tensor(tLSErLSE)
@@ -887,7 +956,7 @@ class FlashAttentionBackwardSm80:
887
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_S_mn)
888
 
889
  # MMA dP
890
- acc_dP = cute.make_fragment(acc_shape_SdP, cutlass.Float32)
891
  acc_dP.fill(0.0)
892
  cute.arch.cp_async_wait_group(1 if cutlass.const_expr(self.num_stages_dO > 1) else 0)
893
  cute.arch.barrier()
@@ -907,7 +976,20 @@ class FlashAttentionBackwardSm80:
907
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dP_mn)
908
  assert cute.size(acc_dP_mn, mode=[0]) == cute.size(tLSErdPsum)
909
  for r in cutlass.range(cute.size(acc_dP_mn, mode=[0]), unroll_full=True):
910
- acc_dP_mn[r, None].store(acc_S_mn[r, None].load() * (acc_dP_mn[r, None].load() - tLSErdPsum[r]))
 
 
 
 
 
 
 
 
 
 
 
 
 
911
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dP_mn)
912
  rP = cute.make_fragment_like(acc_S, self.dtype)
913
  rP.store(acc_S.load().to(self.dtype))
@@ -944,7 +1026,7 @@ class FlashAttentionBackwardSm80:
944
  acc_shape_dQ = mma_params.thr_mma_dq.partition_shape_C(
945
  (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)
946
  )
947
- acc_dQ = cute.make_fragment(acc_shape_dQ, cutlass.Float32)
948
  acc_dQ.fill(0.0)
949
  sm80_utils.gemm(
950
  mma_params.thr_mma_dq, acc_dQ, mma_params.tdQrdS, mma_params.tdQrK,
@@ -968,7 +1050,7 @@ class FlashAttentionBackwardSm80:
968
 
969
  # MMA dK
970
  if cutlass.const_expr(self.Mma_dKV_is_RS):
971
- tdVrP = layout_utils.reshape_acc_to_frgA(rdS)
972
  else:
973
  tdKrdS = mma_params.tdKrdS
974
  sm80_utils.gemm(
 
11
  import cutlass
12
  import cutlass.cute as cute
13
  from cutlass.cute.nvgpu import cpasync, warp
14
+ from cutlass import Int32
15
  import cutlass.utils as utils_basic
16
 
17
  from .quack import layout_utils
 
19
  from .cute_dsl_utils import assume_tensor_aligned
20
  from . import utils
21
  from .mask import AttentionMask
22
+ from .softmax import call_score_mod, call_score_mod_bwd
23
  from .seqlen_info import SeqlenInfoQK
24
+ from .block_info import BlockInfo
25
  from .quack.cute_dsl_utils import ParamsBase
26
  from .tile_scheduler import SingleTileScheduler, SingleTileVarlenScheduler, TileSchedulerArguments
27
+ from .block_sparsity import BlockSparseTensors
28
+ from .utils import AuxData
29
 
30
 
31
  class FlashAttentionBackwardSm80:
 
42
  num_threads: int = 256,
43
  pack_gqa: bool = False,
44
  is_causal: bool = False,
45
+ is_local: bool = False,
46
  SdP_swapAB: bool = False,
47
  dKV_swapAB: bool = False,
48
  dQ_swapAB: bool = False,
 
50
  AtomLayoutNdKV: int = 8,
51
  AtomLayoutMdQ: int = 1,
52
  V_in_regs: bool = False,
53
+ score_mod: cutlass.Constexpr | None = None,
54
+ score_mod_bwd: cutlass.Constexpr | None = None,
55
  ):
56
  """Initializes the configuration for a flash attention v2 kernel.
57
 
 
69
  :param is_causal: is causal
70
  """
71
  self.dtype = dtype
72
+ # padding head_dim to a multiple of 32 (stricter than fwd's 16) due to
73
+ # backward kernel register layout requirements for dQ/dK/dV accumulation
74
  hdim_multiple_of = 32
75
  self.head_dim_padded = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
76
  head_dim_v = head_dim_v if head_dim_v is not None else head_dim
 
85
  self.num_threads = num_threads
86
  self.pack_gqa = pack_gqa
87
  self.is_causal = is_causal
88
+ self.is_local = is_local
89
  self.num_stages_Q = num_stages_Q
90
  self.num_stages_dO = num_stages_dO
91
  self.SdP_swapAB = SdP_swapAB
 
98
  self.Mma_dKV_is_RS = AtomLayoutMSdP == 1 and AtomLayoutNdKV == num_mma_warps and SdP_swapAB and not dKV_swapAB
99
  self.V_in_regs = V_in_regs
100
  self.share_QV_smem = V_in_regs
101
+ self.score_mod = score_mod
102
+ self.score_mod_bwd = score_mod_bwd
103
 
104
  @staticmethod
105
  def can_implement(
 
172
  else:
173
  if cutlass.const_expr(not (mdK_type == mdV_type == cutlass.Float32)):
174
  raise TypeError("mdKaccum and mdVaccum tensors must have the data type Float32")
175
+ if cutlass.const_expr(mQ_type not in [cutlass.Float16, cutlass.BFloat16]):
176
  raise TypeError("Only Float16 or BFloat16 is supported")
177
+ if cutlass.const_expr(mLSE_type not in [cutlass.Float32]):
178
  raise TypeError("LSE tensor must be Float32")
179
+ if cutlass.const_expr(mdPsum_type not in [cutlass.Float32]):
180
  raise TypeError("dPsum tensor must be Float32")
181
+ if cutlass.const_expr(mdQaccum_type not in [cutlass.Float32]):
182
  raise TypeError("dQaccum tensor must be Float32")
183
  if cutlass.const_expr(mCuSeqlensQ_type not in [None, cutlass.Int32]):
184
  raise TypeError("cuSeqlensQ tensor must be Int32")
 
383
  mdK: cute.Tensor,
384
  mdV: cute.Tensor,
385
  softmax_scale: cutlass.Float32,
 
386
  mCuSeqlensQ: Optional[cute.Tensor] = None,
387
  mCuSeqlensK: Optional[cute.Tensor] = None,
388
  mSeqUsedQ: Optional[cute.Tensor] = None,
389
  mSeqUsedK: Optional[cute.Tensor] = None,
 
390
  window_size_left: Int32 | int | None = None,
391
  window_size_right: Int32 | int | None = None,
392
  mdQ_semaphore: Optional[cute.Tensor] = None,
393
+ mdK_semaphore: Optional[cute.Tensor] = None,
394
+ mdV_semaphore: Optional[cute.Tensor] = None,
395
+ aux_data: AuxData = AuxData(),
396
+ blocksparse_tensors: Optional[BlockSparseTensors] = None,
397
+ mCuTotalMBlocks: Optional[cute.Tensor] = None,
398
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
399
+ stream: cuda.CUstream = None,
400
  ):
401
+ assert mdQ_semaphore is None and mdK_semaphore is None and mdV_semaphore is None, (
402
+ "determinism not supported yet for Sm80"
403
+ )
404
  # Get the data type and check if it is fp16 or bf16
405
  self._check_type(*(t.element_type if t is not None else None
406
  for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV, mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK)))
 
435
  qhead_per_kvhead_packgqa=self.qhead_per_kvhead if cutlass.const_expr(self.pack_gqa) else 1,
436
  mCuSeqlensQ=mCuSeqlensK,
437
  mSeqUsedQ=mSeqUsedK,
438
+ cu_total_m_blocks_ptr=mCuTotalMBlocks,
439
  )
440
 
441
  tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
442
  grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
443
 
444
+ softmax_scale_log2, _ = utils.compute_softmax_scale_log2(
445
+ softmax_scale, self.score_mod
446
+ )
447
  self.kernel(
448
  mQ,
449
  mK,
 
460
  mSeqUsedK,
461
  softmax_scale,
462
  softmax_scale_log2,
463
+ window_size_left,
464
+ window_size_right,
465
  self.sQ_layout,
466
  self.sK_layout,
467
  self.sV_layout,
 
481
  SharedStorage,
482
  tile_sched_params,
483
  TileScheduler,
484
+ aux_data,
485
  ).launch(
486
  grid=grid_dim,
487
  block=[self.num_threads, 1, 1],
 
507
  mSeqUsedK: Optional[cute.Tensor],
508
  softmax_scale: cutlass.Float32,
509
  softmax_scale_log2: cutlass.Float32,
510
+ window_size_left: Optional[Int32],
511
+ window_size_right: Optional[Int32],
512
  sQ_layout: cute.ComposedLayout,
513
  sK_layout: cute.ComposedLayout,
514
  sV_layout: cute.ComposedLayout,
 
528
  SharedStorage: cutlass.Constexpr,
529
  tile_sched_params: ParamsBase,
530
  TileScheduler: cutlass.Constexpr[Callable],
531
+ aux_data: AuxData = AuxData(),
532
  ):
533
  # Thread index, block index
534
  tidx, _, _ = cute.arch.thread_idx()
 
539
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
540
 
541
  if work_tile.is_valid_tile:
542
+ seqlen = SeqlenInfoQK.create(
543
+ batch_idx,
544
+ mQ.shape[1],
545
+ mK.shape[1],
546
+ mCuSeqlensQ=mCuSeqlensQ,
547
+ mCuSeqlensK=mCuSeqlensK,
548
+ mSeqUsedQ=mSeqUsedQ,
549
+ mSeqUsedK=mSeqUsedK,
550
+ tile_m=self.m_block_size,
551
+ tile_n=self.n_block_size,
552
+ )
553
+
554
+ block_info = BlockInfo(
555
+ self.m_block_size,
556
+ self.n_block_size,
557
+ self.is_causal,
558
+ self.is_local,
559
+ False,
560
+ window_size_left,
561
+ window_size_right,
562
+ )
563
+ m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
564
  # TODO: return early if m_block_max == 0
565
 
566
  # ///////////////////////////////////////////////////////////////////////////////
 
578
  mdPsum_cur = mdPsum[batch_idx, head_idx, None]
579
  mdQaccum_cur = mdQaccum[batch_idx, head_idx, None]
580
  else:
581
+ padded_offset_q = seqlen.padded_offset_q
582
  mQ_cur = cute.domain_offset((seqlen.offset_q, 0), mQ[None, head_idx, None])
583
  mLSE_cur = cute.domain_offset((padded_offset_q,), mLSE[head_idx, None])
584
  mdO_cur = cute.domain_offset((seqlen.offset_q, 0), mdO[None, head_idx, None])
 
655
  thr_mma_dq = tiled_mma_dq.get_slice(tidx)
656
  acc_shape_dK = thr_mma_dkv.partition_shape_C((self.n_block_size, self.head_dim_padded))
657
  acc_shape_dV = thr_mma_dkv.partition_shape_C((self.n_block_size, self.head_dim_v_padded))
658
+ acc_dK = cute.make_rmem_tensor(acc_shape_dK, cutlass.Float32)
659
+ acc_dV = cute.make_rmem_tensor(acc_shape_dV, cutlass.Float32)
660
  acc_dK.fill(0.0)
661
  acc_dV.fill(0.0)
662
 
 
795
  smem_copy_params=smem_copy_params, gmem_copy_params=gmem_copy_params,
796
  load_Q_LSE=load_Q_LSE, load_dO_dPsum=load_dO_dPsum,
797
  m_block_max=m_block_max,
798
+ softmax_scale=softmax_scale,
799
  softmax_scale_log2=softmax_scale_log2,
800
+ aux_data=aux_data,
801
  )
802
 
803
+ if m_block_min < m_block_max:
804
+ # ///////////////////////////////////////////////////////////////////////////////
805
+ # Prologue
806
+ # ///////////////////////////////////////////////////////////////////////////////
807
+ # Start async loads of the last mn-tile, where we take care of the mn residue
808
+ self.load_V(gmem_thr_copy_VdO, tVgV, tVsV, n_block, seqlen=seqlen.seqlen_k,
809
+ headdim=d_head_v)
810
+ if cutlass.const_expr(self.V_in_regs):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
  cute.arch.cp_async_commit_group()
812
+ self.load_K(gmem_thr_copy_QK, tKgK, tKsK, n_block, seqlen=seqlen.seqlen_k,
813
+ headdim=d_head)
814
+ cute.arch.cp_async_commit_group()
815
 
816
+ if cutlass.const_expr(self.V_in_regs):
817
+ cute.arch.cp_async_wait_group(1)
818
+ cute.arch.barrier()
819
+ tdPrV_copy_view = smem_thr_copy_KV.retile(tdPrV)
820
+ cute.copy(smem_thr_copy_KV, tdPsV, tdPrV_copy_view)
821
+ # Sync to avoid loading Q to smem_q, which overlaps with smem_v
822
+ cute.arch.barrier()
823
+
824
+ m_block = m_block_min
825
+ assert self.num_stages_Q >= self.num_stages_dO
826
+ for stage in cutlass.range_constexpr(self.num_stages_Q):
827
+ if cutlass.const_expr(self.num_stages_Q == 1 or stage < self.num_stages_Q - 1):
828
+ if stage == 0 or m_block + stage < m_block_max:
829
+ load_Q_LSE(m_block + stage, smem_pipe_write_q=stage)
830
+ cute.arch.cp_async_commit_group()
831
+ if cutlass.const_expr(stage < self.num_stages_dO):
832
+ if stage == 0 or m_block + stage < m_block_max:
833
+ load_dO_dPsum(m_block + stage, smem_pipe_write_q=stage)
834
+ cute.arch.cp_async_commit_group()
835
+
836
+ # ///////////////////////////////////////////////////////////////////////////////
837
+ # Mainloop
838
+ # ///////////////////////////////////////////////////////////////////////////////
839
+ # Start processing of the first n-block.
840
+ mask = AttentionMask(
841
+ self.m_block_size,
842
+ self.n_block_size,
843
+ seqlen,
844
+ window_size_left,
845
+ window_size_right,
846
+ )
847
+ mask_fn = partial(
848
+ mask.apply_mask, n_block=n_block, thr_mma=thr_mma_sdp,
849
+ batch_idx=batch_idx, head_idx=head_idx,
850
+ mask_seqlen=True, mask_causal=self.is_causal, mask_local=self.is_local
851
  )
852
+ smem_pipe_read_q = cutlass.Int32(0)
853
+ smem_pipe_read_do = cutlass.Int32(0)
854
+ smem_pipe_write_q = cutlass.Int32(self.num_stages_Q - 1)
855
+ smem_pipe_write_do = cutlass.Int32(0)
856
+ for m_tile in cutlass.range(m_block_min, m_block_max, unroll=1):
857
+ compute_one_m_block(
858
+ m_tile, smem_pipe_read_q, smem_pipe_read_do, smem_pipe_write_q, smem_pipe_write_do,
859
+ mask_fn=mask_fn,
860
+ )
861
+ smem_pipe_read_q = self.advance_pipeline(smem_pipe_read_q, self.num_stages_Q)
862
+ smem_pipe_read_do = self.advance_pipeline(smem_pipe_read_do, self.num_stages_dO)
863
+ smem_pipe_write_q = self.advance_pipeline(smem_pipe_write_q, self.num_stages_Q)
864
+ smem_pipe_write_do = self.advance_pipeline(smem_pipe_write_do, self.num_stages_dO)
865
 
866
  # ///////////////////////////////////////////////////////////////////////////////
867
  # Epilogue
 
892
  load_Q_LSE: Callable,
893
  load_dO_dPsum: Callable,
894
  m_block_max: cutlass.Int32,
895
+ softmax_scale: cutlass.Float32,
896
  softmax_scale_log2: cutlass.Float32,
897
+ aux_data: AuxData = AuxData(),
898
  mask_fn: Optional[Callable] = None,
899
  ):
900
  def load_Q_next():
 
912
  acc_shape_SdP = mma_params.thr_mma_sdp.partition_shape_C(
913
  (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)
914
  )
915
+ acc_S = cute.make_rmem_tensor(acc_shape_SdP, cutlass.Float32)
916
  acc_S.fill(0.0)
917
  cute.arch.cp_async_wait_group(1 if cutlass.const_expr(self.num_stages_Q > 1) else 0)
918
  cute.arch.barrier()
 
923
  smem_copy_params.smem_thr_copy_QdO, smem_copy_params.smem_thr_copy_KV,
924
  swap_AB=self.SdP_swapAB,
925
  )
926
+ acc_S_pre = cute.make_fragment_like(acc_S)
927
+ acc_S_pre.store(acc_S.load())
928
  tLSErLSE = cute.make_fragment_like(smem_copy_params.tSsLSEMma[None, 0])
929
  cute.autovec_copy(
930
  smem_copy_params.tSsLSEMma[None, smem_pipe_read_q if cutlass.const_expr(self.num_stages_Q > 1) else 0], tLSErLSE
931
  )
932
+ acc_S_mn = layout_utils.reshape_acc_to_mn(acc_S)
933
+ acc_S_pre_mn = layout_utils.reshape_acc_to_mn(acc_S_pre)
934
+ if cutlass.const_expr(self.score_mod is not None):
935
+ for r in cutlass.range(cute.size(acc_S_mn, mode=[0]), unroll_full=True):
936
+ acc_S_mn[r, None].store(
937
+ call_score_mod(
938
+ self.score_mod,
939
+ acc_S_mn[r, None].load() * softmax_scale,
940
+ 0,
941
+ 0,
942
+ 0,
943
+ 0,
944
+ None,
945
+ aux_data,
946
+ )
947
+ )
948
  if cutlass.const_expr(mask_fn is not None):
949
  mask_fn(acc_S, m_block=m_block)
 
950
  bidx = 0
951
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_S_mn)
952
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == 1: cute.print_tensor(tLSErLSE)
 
956
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_S_mn)
957
 
958
  # MMA dP
959
+ acc_dP = cute.make_rmem_tensor(acc_shape_SdP, cutlass.Float32)
960
  acc_dP.fill(0.0)
961
  cute.arch.cp_async_wait_group(1 if cutlass.const_expr(self.num_stages_dO > 1) else 0)
962
  cute.arch.barrier()
 
976
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dP_mn)
977
  assert cute.size(acc_dP_mn, mode=[0]) == cute.size(tLSErdPsum)
978
  for r in cutlass.range(cute.size(acc_dP_mn, mode=[0]), unroll_full=True):
979
+ grad_val = acc_S_mn[r, None].load() * (acc_dP_mn[r, None].load() - tLSErdPsum[r])
980
+ if cutlass.const_expr(self.score_mod_bwd is not None):
981
+ grad_val = call_score_mod_bwd(
982
+ self.score_mod_bwd,
983
+ grad_val,
984
+ acc_S_pre_mn[r, None].load() * softmax_scale,
985
+ 0,
986
+ 0,
987
+ 0,
988
+ 0,
989
+ None,
990
+ aux_data,
991
+ )
992
+ acc_dP_mn[r, None].store(grad_val)
993
  # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dP_mn)
994
  rP = cute.make_fragment_like(acc_S, self.dtype)
995
  rP.store(acc_S.load().to(self.dtype))
 
1026
  acc_shape_dQ = mma_params.thr_mma_dq.partition_shape_C(
1027
  (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)
1028
  )
1029
+ acc_dQ = cute.make_rmem_tensor(acc_shape_dQ, cutlass.Float32)
1030
  acc_dQ.fill(0.0)
1031
  sm80_utils.gemm(
1032
  mma_params.thr_mma_dq, acc_dQ, mma_params.tdQrdS, mma_params.tdQrK,
 
1050
 
1051
  # MMA dK
1052
  if cutlass.const_expr(self.Mma_dKV_is_RS):
1053
+ tdKrdS = layout_utils.reshape_acc_to_frgA(rdS)
1054
  else:
1055
  tdKrdS = mma_params.tdKrdS
1056
  sm80_utils.gemm(
build/torch-cuda/flash_bwd_mla_dk_sm100.py ADDED
@@ -0,0 +1,1185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Modified from CUTLASS example file, original copyright:
2
+ # Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ # SPDX-License-Identifier: BSD-3-Clause
4
+
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+
8
+ # 1. Redistributions of source code must retain the above copyright notice, this
9
+ # list of conditions and the following disclaimer.
10
+
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+
15
+ # 3. Neither the name of the copyright holder nor the names of its
16
+ # contributors may be used to endorse or promote products derived from
17
+ # this software without specific prior written permission.
18
+
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+
30
+ from typing import Callable, Optional, Tuple, Union
31
+ import cuda.bindings.driver as cuda
32
+
33
+ import cutlass
34
+ from cutlass import Int32, const_expr
35
+ from cutlass.cutlass_dsl import dsl_user_op
36
+ import cutlass.cute as cute
37
+ import cutlass.cute.testing as testing
38
+ import cutlass.utils as utils
39
+ from cutlass.utils.gemm.sm100 import (
40
+ transform_partitioned_tensor_layout,
41
+ epilogue_smem_copy_and_partition,
42
+ epilogue_tmem_copy_and_partition,
43
+ )
44
+ import cutlass.pipeline as pipeline
45
+ from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait
46
+ from cutlass.cute.nvgpu import cpasync, tcgen05
47
+
48
+ from .utils import get_batch_from_cu_tensor
49
+
50
+
51
+ @dsl_user_op
52
+ def elem_pointer(x: cute.Tensor, coord, *, loc=None, ip=None) -> cute.Pointer:
53
+ """
54
+ Get a pointer to an element at the specified coordinate in a tensor.
55
+
56
+ Args:
57
+ x: The tensor (typically a shared memory tensor)
58
+ coord: The coordinate tuple, can be hierarchical like (row, (col, cluster_idx))
59
+
60
+ Returns:
61
+ Pointer to the element at the specified coordinate
62
+ """
63
+ return x.iterator + cute.crd2idx(coord, x.layout, loc=loc, ip=ip)
64
+
65
+
66
+ class dKGemmKernel:
67
+ def __init__(
68
+ self,
69
+ topk: int,
70
+ heads: int,
71
+ dim: int,
72
+ varlen: bool,
73
+ ):
74
+ self.varlen = varlen
75
+ self.topk = topk
76
+ self.heads = heads
77
+ self.dim = dim
78
+ # A operand dS'^T: (total_q, heads, topk), topk-major
79
+ self.ab_dtype = cutlass.BFloat16
80
+ self.a_major_mode = cute.nvgpu.OperandMajorMode.MN
81
+ # B operand Q: (total_q, heads, dim), dim-major
82
+ self.b_major_mode = cute.nvgpu.OperandMajorMode.MN
83
+ # Index operand I: (total_q, seqlen_k)
84
+ self.idx_dtype = cutlass.Int32
85
+
86
+ # Output dKaccum: (total_q, seqlen_k, dim), dim-major
87
+ self.c_dtype = cutlass.Float32
88
+ self.c_layout = utils.LayoutEnum.ROW_MAJOR
89
+
90
+ self.acc_dtype = cutlass.Float32
91
+
92
+ if self.topk % 256 == 1:
93
+ # The kernel schedule requires 2CTA instructions with this tile shape
94
+ self.cluster_shape_mn = (2, 1)
95
+ self.use_2cta_instrs = True
96
+ else:
97
+ self.cluster_shape_mn = (1, 1)
98
+ self.use_2cta_instrs = False
99
+ self.cta_group = tcgen05.CtaGroup.TWO if self.use_2cta_instrs else tcgen05.CtaGroup.ONE
100
+ self.atom_thr_size = 2 if self.use_2cta_instrs else 1
101
+ self.cta_tile_shape_mnk = (128, 64, 1)
102
+ self.mma_tiler_dK = (
103
+ self.cta_tile_shape_mnk[0] * self.atom_thr_size,
104
+ self.cta_tile_shape_mnk[1],
105
+ 1,
106
+ )
107
+ self.arch = "sm_100"
108
+
109
+ self.occupancy = 1
110
+ # Set specialized warp ids
111
+ self.epilogue_warp_id = [0, 1, 2, 3]
112
+ self.mma_warp_id = 4
113
+ self.tma_warp_id = 5
114
+ self.sched_warp_id = 6
115
+ self.threads_per_cta = 32 * len(
116
+ [
117
+ self.mma_warp_id,
118
+ self.tma_warp_id,
119
+ self.sched_warp_id,
120
+ *self.epilogue_warp_id,
121
+ ]
122
+ )
123
+ # Set barrier id for cta sync, epilogue sync and tmem ptr sync
124
+ self.epilog_sync_bar_id = 1
125
+ self.tmem_alloc_sync_bar_id = 2
126
+ self.I_load_bar_id = 3
127
+
128
+ @cute.jit
129
+ def __call__(
130
+ self,
131
+ dS: cute.Tensor, # (batch, seqlen_q, heads, topk) or (total_q, heads, topk)
132
+ I: cute.Tensor, # (batch, seqlen_q, heads, topk) or (total_q, heads, topk)
133
+ Q: cute.Tensor, # (batch, seqlen_q, heads, dim) or (total_q, heads, dim)
134
+ dKaccum: cute.Tensor, # (batch, seqlen_k, dim) or (total_k, dim)
135
+ cuSeqlensQ: Optional[cute.Tensor], # (batch + 1,)
136
+ cuSeqlensK: Optional[cute.Tensor], # (batch + 1,)
137
+ stream: cuda.CUstream,
138
+ ):
139
+ if const_expr(self.ab_dtype != dS.element_type):
140
+ raise TypeError(f"Type must match: {self.ab_dtype} != {dS.element_type}")
141
+ if const_expr(self.ab_dtype != Q.element_type):
142
+ raise TypeError(f"Type must match: {self.ab_dtype} != {Q.element_type}")
143
+ if const_expr(self.c_dtype != dKaccum.element_type):
144
+ raise TypeError(f"Type must match: {self.c_dtype} != {dKaccum.element_type}")
145
+
146
+ if const_expr(self.varlen):
147
+ assert cuSeqlensQ is not None
148
+ assert cuSeqlensK is not None
149
+
150
+ # For non-varlen, group batch and seqlen modes into token mode
151
+ if const_expr(not self.varlen):
152
+ batch_divmod = cute.FastDivmodDivisor(dS.shape[0])
153
+ dS = cute.group_modes(dS, 0, 2)
154
+ I = cute.group_modes(I, 0, 2)
155
+ Q = cute.group_modes(Q, 0, 2)
156
+ dKaccum = cute.group_modes(dKaccum, 0, 2)
157
+ else:
158
+ batch_divmod = cute.FastDivmodDivisor(0)
159
+
160
+ # Permute everything to (_, heads, tokens) for MMA
161
+ dS_mkl = cute.make_tensor(dS.iterator, cute.select(dS.layout, [2, 1, 0]))
162
+ I_ml = cute.make_tensor(I.iterator, cute.select(I.layout, [1, 0]))
163
+ Q_nkl = cute.make_tensor(Q.iterator, cute.select(Q.layout, [2, 1, 0]))
164
+ dKaccum_nl = cute.make_tensor(dKaccum.iterator, cute.select(dKaccum.layout, [1, 0]))
165
+
166
+ # Configure tiled mma
167
+ self.tiled_mma_dK = utils.sm100.make_trivial_tiled_mma(
168
+ self.ab_dtype,
169
+ self.ab_dtype,
170
+ self.a_major_mode,
171
+ self.b_major_mode,
172
+ self.acc_dtype,
173
+ self.cta_group,
174
+ self.mma_tiler_dK[:2],
175
+ )
176
+
177
+ # Compute mma/cluster/tile shapes
178
+ mma_inst_shape_k = cute.size(self.tiled_mma_dK.shape_mnk, mode=[2])
179
+ mma_inst_tile_k = 4 if self.heads == 64 else 8
180
+ self.mma_tiler_dK = (
181
+ self.mma_tiler_dK[0],
182
+ self.mma_tiler_dK[1],
183
+ mma_inst_shape_k * mma_inst_tile_k,
184
+ )
185
+ self.cta_tile_shape_mnk_dK = (
186
+ self.mma_tiler_dK[0] // self.atom_thr_size,
187
+ self.mma_tiler_dK[1],
188
+ self.mma_tiler_dK[2],
189
+ )
190
+
191
+ # Compute cluster layout
192
+ self.cluster_layout_vmnk = cute.tiled_divide(
193
+ cute.make_layout((*self.cluster_shape_mn, 1)),
194
+ (self.atom_thr_size,),
195
+ )
196
+
197
+ # Compute number of multicast CTAs for A/B
198
+ self.num_mcast_ctas_a = cute.size(self.cluster_layout_vmnk.shape[2])
199
+ self.num_mcast_ctas_b = cute.size(self.cluster_layout_vmnk.shape[1])
200
+ self.is_a_mcast = self.num_mcast_ctas_a > 1
201
+ self.is_b_mcast = self.num_mcast_ctas_b > 1
202
+
203
+ # Compute epilogue subtile shape for TMA store
204
+ # self.epi_tile_dK = utils.sm100.compute_epilogue_tile_shape(
205
+ # self.cta_tile_shape_mnk_dK,
206
+ # self.use_2cta_instrs,
207
+ # self.c_layout,
208
+ # self.c_dtype,
209
+ # )
210
+ self.epi_tile_dK = (cute.make_layout(128), cute.make_layout(32))
211
+ self.epi_tile_dK_width = cute.size(self.epi_tile_dK[1].shape)
212
+
213
+ self.dK_smem_layout = utils.sm100.make_smem_layout_epi(
214
+ self.c_dtype, self.c_layout, self.epi_tile_dK, 1
215
+ )
216
+
217
+ smem_capacity = utils.get_smem_capacity_in_bytes()
218
+
219
+ # TMEM pipeline stages
220
+ # 64 TMEM columns per stage, 8 stages can fit in TMEM
221
+ self.num_acc_stage = 8
222
+ self.num_tmem_alloc_cols = 512
223
+
224
+ # SMEM pipeline stages
225
+ # TMA load Q (persists for the whole token)
226
+ self.num_Q_stage = 1
227
+ assert const_expr(self.mma_tiler_dK[1] * self.num_Q_stage == self.dim)
228
+ Q_smem_layout_stage_one = utils.sm100.make_smem_layout_b(
229
+ self.tiled_mma_dK, self.mma_tiler_dK, self.ab_dtype, 1
230
+ )
231
+ Q_bytes = cute.size_in_bytes(self.ab_dtype, Q_smem_layout_stage_one) * self.num_Q_stage
232
+
233
+ # TMA store-reduce dK
234
+ self.num_c_stage = 2
235
+ dK_bytes_per_stage = cute.size_in_bytes(self.c_dtype, self.dK_smem_layout)
236
+ dK_bytes = dK_bytes_per_stage * self.num_c_stage
237
+
238
+ # cp.async load I
239
+ self.num_I_stage = 2
240
+ self.I_smem_layout_staged = cute.make_layout(
241
+ (self.mma_tiler_dK[0] // self.atom_thr_size, self.num_I_stage)
242
+ )
243
+ I_bytes = cute.size_in_bytes(self.idx_dtype, self.I_smem_layout_staged)
244
+
245
+ # TMA load dS
246
+ dS_smem_layout_stage_one = utils.sm100.make_smem_layout_a(
247
+ self.tiled_mma_dK, self.mma_tiler_dK, self.ab_dtype, 1
248
+ )
249
+ dS_bytes_per_stage = cute.size_in_bytes(self.ab_dtype, dS_smem_layout_stage_one)
250
+
251
+ mbar_helpers_bytes = 1024
252
+
253
+ # Increase dS stages to fill SMEM
254
+ self.num_dS_stage = (
255
+ smem_capacity // self.occupancy - (mbar_helpers_bytes + dK_bytes + Q_bytes + I_bytes)
256
+ ) // dS_bytes_per_stage
257
+ dS_bytes = self.num_dS_stage * dS_bytes_per_stage
258
+
259
+ # Increase dK stages to fill remainder
260
+ self.num_c_stage += (
261
+ smem_capacity // self.occupancy
262
+ - (mbar_helpers_bytes + dK_bytes + dS_bytes + Q_bytes + I_bytes)
263
+ ) // dK_bytes_per_stage
264
+
265
+ # Increase I stages to fill remainder
266
+
267
+ # Compute shared memory layout
268
+ self.dS_smem_layout_staged = utils.sm100.make_smem_layout_a(
269
+ self.tiled_mma_dK, self.mma_tiler_dK, self.ab_dtype, self.num_dS_stage
270
+ )
271
+ self.Q_smem_layout_staged = utils.sm100.make_smem_layout_b(
272
+ self.tiled_mma_dK, self.mma_tiler_dK, self.ab_dtype, self.num_Q_stage
273
+ )
274
+ self.dK_smem_layout_staged = utils.sm100.make_smem_layout_epi(
275
+ self.c_dtype, self.c_layout, self.epi_tile_dK, self.num_c_stage
276
+ )
277
+
278
+ # TMA load for dS
279
+ dS_op = utils.sm100.cluster_shape_to_tma_atom_A(
280
+ self.cluster_shape_mn, self.tiled_mma_dK.thr_id
281
+ )
282
+ self.dS_smem_layout = cute.slice_(self.dS_smem_layout_staged, (None, None, None, 0))
283
+ tma_atom_dS, tma_tensor_dS = cute.nvgpu.make_tiled_tma_atom_A(
284
+ dS_op,
285
+ dS_mkl,
286
+ self.dS_smem_layout,
287
+ self.mma_tiler_dK,
288
+ self.tiled_mma_dK,
289
+ self.cluster_layout_vmnk.shape,
290
+ )
291
+
292
+ # TMA load for Q
293
+ Q_op = utils.sm100.cluster_shape_to_tma_atom_B(
294
+ self.cluster_shape_mn, self.tiled_mma_dK.thr_id
295
+ )
296
+ self.Q_smem_layout = cute.slice_(self.Q_smem_layout_staged, (None, None, None, 0))
297
+ tma_atom_Q, tma_tensor_Q = cute.nvgpu.make_tiled_tma_atom_B(
298
+ Q_op,
299
+ Q_nkl,
300
+ self.Q_smem_layout,
301
+ self.mma_tiler_dK,
302
+ self.tiled_mma_dK,
303
+ self.cluster_layout_vmnk.shape,
304
+ )
305
+ self.Q_load_bytes = (
306
+ cute.size_in_bytes(self.ab_dtype, self.Q_smem_layout) * self.atom_thr_size
307
+ )
308
+
309
+ self.dS_load_bytes = (
310
+ cute.size_in_bytes(self.ab_dtype, self.dS_smem_layout) * self.atom_thr_size
311
+ )
312
+
313
+ # coalesced store for dKaccum: 1 warp copies 1 epi tile row
314
+ vector_width_dK = self.epi_tile_dK_width * self.c_dtype.width // cute.arch.WARP_SIZE
315
+ copy_atom_dK = cute.make_copy_atom(
316
+ cute.nvgpu.CopyUniversalOp(),
317
+ self.c_dtype,
318
+ num_bits_per_copy=vector_width_dK,
319
+ )
320
+ vector_elts_dK = vector_width_dK // self.c_dtype.width
321
+ thr_layout_dK = cute.make_layout((cute.arch.WARP_SIZE,))
322
+ val_layout_dK = cute.make_layout((vector_elts_dK,))
323
+ tiled_copy_dK = cute.make_tiled_copy_tv(copy_atom_dK, thr_layout_dK, val_layout_dK)
324
+
325
+ # cp.async load I: 1 warp copies 32 values, 4 warps copy 128 values
326
+ copy_atom_I = cute.make_copy_atom(
327
+ cute.nvgpu.cpasync.CopyG2SOp(),
328
+ self.idx_dtype,
329
+ num_bits_per_copy=32,
330
+ )
331
+ vI = cute.make_layout((1,))
332
+ tI = cute.make_layout((128,))
333
+ tiled_copy_I = cute.make_tiled_copy_tv(copy_atom_I, tI, vI)
334
+
335
+ # Setup clc stage by default
336
+ self.num_clc_stage = 1
337
+ assert self.num_clc_stage == 1, "Only single-stage CLC pipeline is supported"
338
+
339
+ # Response size is 4B * 4 elements
340
+ self.num_clc_response_bytes = 16
341
+
342
+ # Compute grid size and set up tile scheduler
343
+ total_q = cute.size(dS_mkl.shape[2])
344
+ cluster_shape_mnl = (*self.cluster_shape_mn, 1)
345
+ num_ctas_mnl = (self.cluster_shape_mn[0] * total_q, self.cluster_shape_mn[1], 1)
346
+ self.tile_sched_params = utils.ClcDynamicPersistentTileSchedulerParams(
347
+ num_ctas_mnl, cluster_shape_mnl
348
+ )
349
+ grid = utils.ClcDynamicPersistentTileScheduler.get_grid_shape(self.tile_sched_params)
350
+
351
+ # Define shared storage for kernel
352
+ buffer_align_bytes = 1024
353
+
354
+ @cute.struct
355
+ class SharedStorage:
356
+ dS_full_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_dS_stage * 2]
357
+ Q_full_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_Q_stage * 2]
358
+ acc_full_mbar_ptr: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage * 2]
359
+ tmem_dealloc_mbar: cutlass.Int64
360
+ tmem_holding_buf: cutlass.Int32
361
+ clc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2]
362
+ clc_response: cute.struct.MemRange[cutlass.Int32, 4]
363
+ sI: cute.struct.Align[
364
+ cute.struct.MemRange[self.idx_dtype, cute.cosize(self.I_smem_layout_staged)],
365
+ buffer_align_bytes,
366
+ ]
367
+ sdS: cute.struct.Align[
368
+ cute.struct.MemRange[self.ab_dtype, cute.cosize(self.dS_smem_layout_staged)],
369
+ buffer_align_bytes,
370
+ ]
371
+ sQ: cute.struct.Align[
372
+ cute.struct.MemRange[self.ab_dtype, cute.cosize(self.Q_smem_layout_staged)],
373
+ buffer_align_bytes,
374
+ ]
375
+ sdK: cute.struct.Align[
376
+ cute.struct.MemRange[self.c_dtype, cute.cosize(self.dK_smem_layout_staged)],
377
+ buffer_align_bytes,
378
+ ]
379
+
380
+ # Launch the kernel synchronously
381
+ self.kernel(
382
+ self.tiled_mma_dK,
383
+ tma_atom_dS,
384
+ tma_tensor_dS,
385
+ tma_atom_Q,
386
+ tma_tensor_Q,
387
+ tiled_copy_dK,
388
+ dKaccum_nl,
389
+ tiled_copy_I,
390
+ I_ml,
391
+ batch_divmod,
392
+ cuSeqlensQ,
393
+ cuSeqlensK,
394
+ self.cluster_layout_vmnk,
395
+ self.dS_smem_layout_staged,
396
+ self.Q_smem_layout_staged,
397
+ self.dK_smem_layout_staged,
398
+ self.I_smem_layout_staged,
399
+ self.epi_tile_dK,
400
+ self.tile_sched_params,
401
+ SharedStorage,
402
+ ).launch(
403
+ grid=grid,
404
+ block=[self.threads_per_cta, 1, 1],
405
+ cluster=(*self.cluster_shape_mn, 1),
406
+ stream=stream,
407
+ )
408
+
409
+ # GPU device kernel
410
+ @cute.kernel
411
+ def kernel(
412
+ self,
413
+ tiled_mma_dK: cute.TiledMma,
414
+ tma_atom_dS: cute.CopyAtom,
415
+ mdS_mkl: cute.Tensor,
416
+ tma_atom_Q: Optional[cute.CopyAtom],
417
+ mQ_nkl: Optional[cute.Tensor],
418
+ tiled_copy_dK: cute.TiledCopy,
419
+ mdKaccum_nl: Optional[cute.Tensor],
420
+ tiled_copy_I: cute.TiledCopy,
421
+ mI_ml: cute.Tensor,
422
+ batch_divmod: cute.FastDivmodDivisor,
423
+ cuSeqlensQ: Optional[cute.Tensor],
424
+ cuSeqlensK: Optional[cute.Tensor],
425
+ cluster_layout_vmnk: cute.Layout,
426
+ dS_smem_layout_staged: cute.ComposedLayout,
427
+ Q_smem_layout_staged: Union[cute.Layout, cute.ComposedLayout],
428
+ dK_smem_layout_staged: Union[cute.ComposedLayout, cute.Layout],
429
+ I_smem_layout_staged: Union[cute.Layout, cute.ComposedLayout],
430
+ epi_tile_dK: cute.Tile,
431
+ tile_sched_params: utils.ClcDynamicPersistentTileSchedulerParams,
432
+ SharedStorage: cutlass.Constexpr[Callable],
433
+ ):
434
+ warp_idx = cute.arch.warp_idx()
435
+ warp_idx = cute.arch.make_warp_uniform(warp_idx)
436
+
437
+ #
438
+ # Prefetch tma desc
439
+ #
440
+ if warp_idx == self.tma_warp_id:
441
+ cpasync.prefetch_descriptor(tma_atom_dS)
442
+ cpasync.prefetch_descriptor(tma_atom_Q)
443
+
444
+ #
445
+ # Setup cta/thread coordinates
446
+ #
447
+ # Coords inside cluster
448
+ bidx, bidy, bidz = cute.arch.block_idx()
449
+ mma_tile_coord_v = bidx % self.atom_thr_size
450
+ is_leader_cta = mma_tile_coord_v == 0
451
+ cta_rank_in_cluster = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
452
+ is_first_cta_in_cluster = cta_rank_in_cluster == 0
453
+ block_in_cluster_coord_vmnk = cluster_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
454
+ # Coord inside cta
455
+ tidx, _, _ = cute.arch.thread_idx()
456
+
457
+ #
458
+ # Alloc and init: a+b full/empty, accumulator full/empty, tensor memory dealloc barrier
459
+ #
460
+
461
+ smem = utils.SmemAllocator()
462
+ storage = smem.allocate(SharedStorage)
463
+
464
+ # Initialize mainloop ab_pipeline (barrier) and states
465
+ dS_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
466
+ num_tma_producer = self.num_mcast_ctas_a + self.num_mcast_ctas_b - 1
467
+ dS_pipeline_consumer_group = pipeline.CooperativeGroup(
468
+ pipeline.Agent.Thread, num_tma_producer
469
+ )
470
+ dS_producer, dS_consumer = pipeline.PipelineTmaUmma.create(
471
+ barrier_storage=storage.dS_full_mbar_ptr.data_ptr(),
472
+ num_stages=self.num_dS_stage,
473
+ producer_group=dS_pipeline_producer_group,
474
+ consumer_group=dS_pipeline_consumer_group,
475
+ tx_count=self.dS_load_bytes,
476
+ cta_layout_vmnk=cluster_layout_vmnk,
477
+ defer_sync=True,
478
+ ).make_participants()
479
+
480
+ Q_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
481
+ Q_pipeline_consumer_group = pipeline.CooperativeGroup(
482
+ pipeline.Agent.Thread, num_tma_producer
483
+ )
484
+ Q_pipeline = pipeline.PipelineTmaUmma.create(
485
+ barrier_storage=storage.Q_full_mbar_ptr.data_ptr(),
486
+ num_stages=self.num_Q_stage,
487
+ producer_group=Q_pipeline_producer_group,
488
+ consumer_group=Q_pipeline_consumer_group,
489
+ tx_count=self.Q_load_bytes,
490
+ cta_layout_vmnk=cluster_layout_vmnk,
491
+ defer_sync=True,
492
+ )
493
+
494
+ # Initialize acc_pipeline (barrier) and states
495
+ acc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
496
+ num_acc_consumer_threads = len(self.epilogue_warp_id) * (2 if self.use_2cta_instrs else 1)
497
+ acc_pipeline_consumer_group = pipeline.CooperativeGroup(
498
+ pipeline.Agent.Thread, num_acc_consumer_threads
499
+ )
500
+ acc_pipeline = pipeline.PipelineUmmaAsync.create(
501
+ barrier_storage=storage.acc_full_mbar_ptr.data_ptr(),
502
+ num_stages=self.num_acc_stage,
503
+ producer_group=acc_pipeline_producer_group,
504
+ consumer_group=acc_pipeline_consumer_group,
505
+ cta_layout_vmnk=cluster_layout_vmnk,
506
+ defer_sync=True,
507
+ )
508
+
509
+ # Initialize clc_pipeline (barrier) and states
510
+ clc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
511
+ cluster_size = cute.size(self.cluster_shape_mn)
512
+ num_clc_consumer_threads = 32 * (1 + cluster_size * (1 + len(self.epilogue_warp_id) + 1))
513
+ clc_pipeline_consumer_group = pipeline.CooperativeGroup(
514
+ pipeline.Agent.Thread, num_clc_consumer_threads
515
+ )
516
+ clc_pipeline = pipeline.PipelineClcFetchAsync.create(
517
+ barrier_storage=storage.clc_mbar_ptr.data_ptr(),
518
+ num_stages=self.num_clc_stage,
519
+ producer_group=clc_pipeline_producer_group,
520
+ consumer_group=clc_pipeline_consumer_group,
521
+ tx_count=self.num_clc_response_bytes,
522
+ cta_layout_vmnk=cluster_layout_vmnk,
523
+ defer_sync=True,
524
+ )
525
+
526
+ tmem_alloc_barrier = pipeline.NamedBarrier(
527
+ barrier_id=self.tmem_alloc_sync_bar_id,
528
+ num_threads=32 * len((self.mma_warp_id, *self.epilogue_warp_id)),
529
+ )
530
+ # Tensor memory dealloc barrier init
531
+ tmem = utils.TmemAllocator(
532
+ storage.tmem_holding_buf.ptr,
533
+ barrier_for_retrieve=tmem_alloc_barrier,
534
+ allocator_warp_id=self.epilogue_warp_id[0],
535
+ is_two_cta=self.use_2cta_instrs,
536
+ two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
537
+ )
538
+
539
+ # Cluster arrive after barrier init
540
+ pipeline_init_arrive(cluster_shape_mn=cluster_layout_vmnk, is_relaxed=True)
541
+
542
+ # Initial clc response pointer
543
+ clc_response_ptr = storage.clc_response.data_ptr()
544
+
545
+ clc_consumer_state = pipeline.make_pipeline_state(
546
+ pipeline.PipelineUserType.Consumer, self.num_clc_stage
547
+ )
548
+
549
+ #
550
+ # Setup smem tensor A/B/C
551
+ #
552
+ # (MMA, MMA_M, MMA_K, STAGE)
553
+ sdS = storage.sdS.get_tensor(
554
+ dS_smem_layout_staged.outer,
555
+ swizzle=dS_smem_layout_staged.inner,
556
+ )
557
+ # (MMA_M, STAGE)
558
+ sI = storage.sI.get_tensor(I_smem_layout_staged)
559
+
560
+ sQ = storage.sQ.get_tensor(
561
+ Q_smem_layout_staged.outer,
562
+ swizzle=Q_smem_layout_staged.inner,
563
+ )
564
+ sdK = storage.sdK.get_tensor(
565
+ dK_smem_layout_staged.outer,
566
+ swizzle=dK_smem_layout_staged.inner,
567
+ )
568
+
569
+ #
570
+ # Compute multicast mask for A/B buffer full
571
+ #
572
+ a_full_mcast_mask = None
573
+ b_full_mcast_mask = None
574
+ if const_expr(self.is_a_mcast or self.is_b_mcast or self.use_2cta_instrs):
575
+ a_full_mcast_mask = cpasync.create_tma_multicast_mask(
576
+ cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=2
577
+ )
578
+ b_full_mcast_mask = cpasync.create_tma_multicast_mask(
579
+ cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=1
580
+ )
581
+
582
+ #
583
+ # Local_tile partition global tensors
584
+ #
585
+ # (bM, bK, RestM, RestK, RestL)
586
+ gdS_mkl = cute.local_tile(
587
+ mdS_mkl, cute.slice_(self.mma_tiler_dK, (None, 0, None)), (None, None, None)
588
+ )
589
+ # (bM, RestM, RestL)
590
+ gI_ml = cute.local_tile(mI_ml, cute.slice_(self.mma_tiler_dK, (None, 0, 0)), (None, None))
591
+ # (bN, bK, RestN, RestK, RestL)
592
+ gQ_nkl = cute.local_tile(
593
+ mQ_nkl,
594
+ cute.slice_(self.mma_tiler_dK, (0, None, None)),
595
+ (None, None, None),
596
+ )
597
+ n_tile_cnt = cute.size(gQ_nkl, mode=[2])
598
+ # (bN, RestN, RestL)
599
+ gdKaccum_nl = cute.local_tile(
600
+ mdKaccum_nl,
601
+ cute.slice_(self.mma_tiler_dK, (0, None, 0)),
602
+ (None, None),
603
+ )
604
+
605
+ m_tile_cnt = cute.size(gdS_mkl, mode=[2])
606
+ k_tile_cnt = cute.size(gdS_mkl, mode=[3])
607
+ assert const_expr(k_tile_cnt == 1)
608
+
609
+ #
610
+ # Partition global tensor for TiledMMA_A/B/C
611
+ #
612
+ thr_mma_dK = tiled_mma_dK.get_slice(mma_tile_coord_v)
613
+ # (MMA, MMA_N, MMA_K, RestN, RestK, RestL)
614
+ tCgQ = thr_mma_dK.partition_B(gQ_nkl)
615
+
616
+ # (MMA, MMA_M, MMA_N, RestM, RestN, RestL)
617
+ tCgdKaccum_fake = thr_mma_dK.partition_C(
618
+ cute.make_identity_tensor(
619
+ cute.append(cute.slice_(self.mma_tiler_dK, (None, None, 0)), 1, up_to_rank=5)
620
+ )
621
+ )
622
+
623
+ # (MMA, MMA_M, MMA_K, RestM, RestK, RestL)
624
+ tCgdS = thr_mma_dK.partition_A(gdS_mkl)
625
+
626
+ #
627
+ # Partition global/shared tensor for TMA load A/B
628
+ #
629
+ # TMA load A partition_S/D
630
+ a_cta_layout = cute.make_layout(cute.slice_(cluster_layout_vmnk, (0, 0, None, 0)).shape)
631
+ # ((atom_v, rest_v), STAGE)
632
+ # ((atom_v, rest_v), RestM, RestK, RestL)
633
+ tAsdS, tAgdS = cpasync.tma_partition(
634
+ tma_atom_dS,
635
+ block_in_cluster_coord_vmnk[2],
636
+ a_cta_layout,
637
+ cute.group_modes(sdS, 0, 3),
638
+ cute.group_modes(tCgdS, 0, 3),
639
+ )
640
+ # TMA load B partition_S/D
641
+ b_cta_layout = cute.make_layout(cute.slice_(cluster_layout_vmnk, (0, None, 0, 0)).shape)
642
+ # ((atom_v, rest_v), RestM, RestK, RestL)
643
+ tBsQ, tBgQ = cpasync.tma_partition(
644
+ tma_atom_Q,
645
+ block_in_cluster_coord_vmnk[1],
646
+ b_cta_layout,
647
+ cute.group_modes(sQ, 0, 3),
648
+ cute.group_modes(tCgQ, 0, 3),
649
+ )
650
+
651
+ #
652
+ # Partition shared/tensor memory tensor for TiledMMA_A/B/C
653
+ #
654
+ # (MMA, MMA_M, MMA_K, STAGE)
655
+ tCrdS = tiled_mma_dK.make_fragment_A(sdS)
656
+ # (MMA, MMA_N, MMA_K, STAGE)
657
+ tCrQ = tiled_mma_dK.make_fragment_B(sQ)
658
+ # (MMA, MMA_M, MMA_N)
659
+ acc_shape_dK = tiled_mma_dK.partition_shape_C(self.mma_tiler_dK[:2])
660
+ # (MMA, MMA_M, MMA_N, STAGE)
661
+ tCtdK_fake = tiled_mma_dK.make_fragment_C(cute.append(acc_shape_dK, self.num_acc_stage))
662
+
663
+ #
664
+ # Cluster wait before tensor memory alloc
665
+ #
666
+ pipeline_init_wait(cluster_shape_mn=cluster_layout_vmnk)
667
+
668
+ #
669
+ # Construct the scheduler
670
+ #
671
+ tile_sched = utils.ClcDynamicPersistentTileScheduler.create(
672
+ tile_sched_params,
673
+ cute.arch.block_idx(),
674
+ cute.arch.grid_dim(),
675
+ clc_response_ptr,
676
+ )
677
+ work_tile = tile_sched.initial_work_tile_info()
678
+
679
+ #
680
+ # Specialized TMA load warp
681
+ #
682
+
683
+ if warp_idx == self.tma_warp_id:
684
+ Q_producer_state = pipeline.make_pipeline_state(
685
+ pipeline.PipelineUserType.Producer, self.num_Q_stage
686
+ )
687
+ #
688
+ # Persistent tile scheduling loop
689
+ #
690
+
691
+ dS_producer.reset()
692
+ peek_dS_empty_status = dS_producer.try_acquire()
693
+
694
+ while work_tile.is_valid_tile:
695
+ # Get tile coord from tile scheduler
696
+ cur_tile_coord = work_tile.tile_idx
697
+ total_q_coord = cur_tile_coord[0] // self.cluster_shape_mn[0]
698
+
699
+ #
700
+ # Slice to per mma tile index
701
+ #
702
+ # ((atom_v, rest_v), RestM, RestK)
703
+ tAgdS_slice = tAgdS[(None, None, None, total_q_coord)]
704
+ # ((atom_v, rest_v), RestN, RestK)
705
+ tBgQ_slice = tBgQ[(None, None, None, total_q_coord)]
706
+
707
+ #
708
+ # Tma load loop -- fully unrolled as all sizes are static
709
+ #
710
+ for m_tile in cutlass.range_constexpr(m_tile_cnt):
711
+ # Conditionally wait for AB buffer empty
712
+ dS_handle = dS_producer.acquire_and_advance(peek_dS_empty_status)
713
+
714
+ # TMA load dS
715
+ cute.copy(
716
+ tma_atom_dS,
717
+ tAgdS_slice[(None, m_tile, 0)],
718
+ tAsdS[(None, dS_handle.index)],
719
+ tma_bar_ptr=dS_handle.barrier,
720
+ mcast_mask=a_full_mcast_mask,
721
+ )
722
+
723
+ peek_dS_empty_status = dS_producer.try_acquire()
724
+
725
+ if m_tile == 0:
726
+ # TMA load Q only on first m-tile
727
+ for n_tile in cutlass.range_constexpr(n_tile_cnt):
728
+ Q_pipeline.producer_acquire(Q_producer_state)
729
+ Q_load_barrier = Q_pipeline.producer_get_barrier(Q_producer_state)
730
+ cute.copy(
731
+ tma_atom_Q,
732
+ tBgQ_slice[(None, n_tile, 0)],
733
+ tBsQ[(None, n_tile)],
734
+ tma_bar_ptr=Q_load_barrier,
735
+ mcast_mask=b_full_mcast_mask,
736
+ )
737
+ Q_pipeline.producer_commit(Q_producer_state)
738
+ Q_producer_state.advance()
739
+
740
+ #
741
+ # Advance to next tile
742
+ #
743
+ clc_pipeline.consumer_wait(clc_consumer_state)
744
+ work_tile = tile_sched.get_current_work()
745
+ clc_pipeline.consumer_release(clc_consumer_state)
746
+ clc_consumer_state.advance()
747
+ #
748
+ # Wait A/B buffer empty
749
+ #
750
+ dS_producer.tail()
751
+ Q_pipeline.producer_tail(Q_producer_state)
752
+
753
+ #
754
+ # Sched warp
755
+ #
756
+ elif warp_idx == self.sched_warp_id and is_first_cta_in_cluster:
757
+ #
758
+ # Persistent tile scheduling loop
759
+ #
760
+ clc_producer_state = pipeline.make_pipeline_state(
761
+ pipeline.PipelineUserType.ProducerConsumer, self.num_clc_stage
762
+ )
763
+
764
+ while work_tile.is_valid_tile:
765
+ #
766
+ # Advance to next tile
767
+ #
768
+ clc_pipeline.producer_acquire(clc_producer_state)
769
+ mbarrier_addr = clc_pipeline.producer_get_barrier(clc_producer_state)
770
+ tile_sched.advance_to_next_work(mbarrier_addr)
771
+ clc_producer_state.advance()
772
+
773
+ clc_pipeline.consumer_wait(clc_consumer_state)
774
+ work_tile = tile_sched.get_current_work()
775
+ clc_pipeline.consumer_release(clc_consumer_state)
776
+ clc_consumer_state.advance()
777
+ clc_pipeline.producer_tail(clc_producer_state)
778
+
779
+ #
780
+ # Specialized MMA warp
781
+ #
782
+ elif warp_idx == self.mma_warp_id:
783
+ #
784
+ # Retrieving tensor memory ptr and make accumulator tensor
785
+ #
786
+ tmem.wait_for_alloc()
787
+ tmem_ptr = tmem.retrieve_ptr(self.acc_dtype)
788
+ # (MMA, MMA_M, MMA_N, STAGE)
789
+ tCtdK_base = cute.make_tensor(tmem_ptr, tCtdK_fake.layout)
790
+
791
+ #
792
+ # Persistent tile scheduling loop
793
+ #
794
+ acc_producer_state = pipeline.make_pipeline_state(
795
+ pipeline.PipelineUserType.Producer, self.num_acc_stage
796
+ )
797
+ Q_consumer_state = pipeline.make_pipeline_state(
798
+ pipeline.PipelineUserType.Consumer, self.num_Q_stage
799
+ )
800
+
801
+ # Peek (try_wait) AB buffer full for k_tile = 0
802
+ dS_consumer.reset()
803
+ peek_dS_full_status = cutlass.Boolean(1)
804
+ if is_leader_cta:
805
+ peek_dS_full_status = dS_consumer.try_wait()
806
+
807
+ while work_tile.is_valid_tile:
808
+ # Get tile coord from tile scheduler
809
+ cur_tile_coord = work_tile.tile_idx
810
+ total_q_coord = cur_tile_coord[0] // self.cluster_shape_mn[0]
811
+
812
+ #
813
+ # Mma mainloop
814
+ #
815
+ Q_consumer_state_wait = Q_consumer_state.clone()
816
+ for m_tile in cutlass.range(m_tile_cnt):
817
+ if is_leader_cta:
818
+ # Conditionally wait for AB buffer full
819
+ dS_handle = dS_consumer.wait_and_advance(peek_dS_full_status)
820
+ for n_tile in cutlass.range_constexpr(n_tile_cnt):
821
+ tCtdK = tCtdK_base[(None, None, None, acc_producer_state.index)]
822
+ tiled_mma_dK.set(tcgen05.Field.ACCUMULATE, False)
823
+ acc_pipeline.producer_acquire(acc_producer_state)
824
+
825
+ if m_tile == 0:
826
+ Q_pipeline.consumer_wait(Q_consumer_state_wait)
827
+ Q_consumer_state_wait.advance()
828
+
829
+ num_kblocks = cute.size(tCrdS, mode=[2])
830
+ for kblk_idx in cutlass.range(num_kblocks, unroll_full=True):
831
+ cute.gemm(
832
+ tiled_mma_dK,
833
+ tCtdK,
834
+ tCrdS[(None, None, kblk_idx, dS_handle.index)],
835
+ tCrQ[(None, None, kblk_idx, n_tile)],
836
+ tCtdK,
837
+ )
838
+ # Enable accumulate on tCtdK after first kblock
839
+ tiled_mma_dK.set(tcgen05.Field.ACCUMULATE, True)
840
+
841
+ if m_tile == m_tile_cnt - 1:
842
+ Q_pipeline.consumer_release(Q_consumer_state)
843
+ Q_consumer_state.advance()
844
+
845
+ acc_pipeline.producer_commit(acc_producer_state)
846
+ acc_producer_state.advance()
847
+ # Async arrive AB buffer empty
848
+ dS_handle.release()
849
+
850
+ peek_dS_full_status = dS_consumer.try_wait()
851
+ else:
852
+ for n_tile in cutlass.range_constexpr(n_tile_cnt):
853
+ acc_producer_state.advance()
854
+
855
+ #
856
+ # Advance to next tile
857
+ #
858
+ clc_pipeline.consumer_wait(clc_consumer_state)
859
+ work_tile = tile_sched.get_current_work()
860
+ clc_pipeline.consumer_release(clc_consumer_state)
861
+ clc_consumer_state.advance()
862
+ #
863
+ # Wait for accumulator buffer empty
864
+ #
865
+ acc_pipeline.producer_tail(acc_producer_state)
866
+
867
+ #
868
+ # Specialized epilogue warps
869
+ #
870
+ elif warp_idx < self.mma_warp_id:
871
+ #
872
+ # Alloc tensor memory buffer
873
+ #
874
+ tmem.allocate(self.num_tmem_alloc_cols)
875
+
876
+ #
877
+ # Retrieving tensor memory ptr and make accumulator tensor
878
+ #
879
+ tmem.wait_for_alloc()
880
+ tmem_ptr = tmem.retrieve_ptr(self.acc_dtype)
881
+ # (MMA, MMA_M, MMA_N, STAGE)
882
+ tCtdK_base = cute.make_tensor(tmem_ptr, tCtdK_fake.layout)
883
+
884
+ # Both gemms share accumulator and TMA store pipelines
885
+ acc_consumer_state = pipeline.make_pipeline_state(
886
+ pipeline.PipelineUserType.Consumer, self.num_acc_stage
887
+ )
888
+ c_producer_group = pipeline.CooperativeGroup(
889
+ pipeline.Agent.Thread,
890
+ 32 * len(self.epilogue_warp_id),
891
+ )
892
+
893
+ I_load_barrier = pipeline.NamedBarrier(
894
+ barrier_id=self.I_load_bar_id,
895
+ num_threads=32 * len(self.epilogue_warp_id),
896
+ )
897
+
898
+ # (EPI_TOPK, REST_TOPK, TOKENS)
899
+ gI_tile = cute.local_tile(
900
+ gI_ml,
901
+ (epi_tile_dK[0],),
902
+ (0 if is_leader_cta else 1, None, None),
903
+ )
904
+
905
+ thr_copy_I = tiled_copy_I.get_slice(tidx)
906
+ # (COPY_ATOM, EPI_TOPK, REST_TOPK, TOKENS)
907
+ tIgI = thr_copy_I.partition_S(gI_tile)
908
+ # (COPY_ATOM, EPI_TOPK, STAGE)
909
+ tIsI = thr_copy_I.partition_D(sI)
910
+
911
+ num_subtiles_executed = 0
912
+ while work_tile.is_valid_tile:
913
+ # Get tile coord from tile scheduler
914
+ cur_tile_coord = work_tile.tile_idx
915
+ token = cutlass.Int64(cur_tile_coord[0]) // self.cluster_shape_mn[0]
916
+
917
+ # Get batch index from token_coord and cuSeqlensQ
918
+ # Get seqlen_k offset from cuSeqlensK
919
+ if const_expr(self.varlen):
920
+ batch = get_batch_from_cu_tensor(token, cuSeqlensQ)
921
+ seqlen_k_offset = cuSeqlensK[batch]
922
+ else:
923
+ _, batch = divmod(token, batch_divmod)
924
+ seqlen_k_offset = Int32(0) # unused
925
+
926
+ sI_read_stage = 0
927
+ sI_write_stage = 0
928
+
929
+ # Prefetch load I
930
+ I_load_barrier.arrive_and_wait()
931
+ for m_tile in cutlass.range_constexpr(min(m_tile_cnt, self.num_I_stage - 1)):
932
+ cute.copy(
933
+ tiled_copy_I,
934
+ tIgI[(None, None, m_tile, token)],
935
+ tIsI[(None, None, sI_write_stage)],
936
+ )
937
+ cute.arch.cp_async_commit_group()
938
+ sI_write_stage = (sI_write_stage + 1) % self.num_I_stage
939
+
940
+ for m_tile in cutlass.range(m_tile_cnt):
941
+ I_load_barrier.arrive_and_wait()
942
+ # cp.async load I
943
+ if m_tile < m_tile_cnt - 1:
944
+ cute.copy(
945
+ tiled_copy_I,
946
+ tIgI[(None, None, m_tile + 1, token)],
947
+ tIsI[(None, None, sI_write_stage)],
948
+ )
949
+ cute.arch.cp_async_commit_group()
950
+ sI_write_stage = (sI_write_stage + 1) % self.num_I_stage
951
+ cute.arch.cp_async_wait_group(self.num_I_stage - 1)
952
+ I_load_barrier.arrive_and_wait()
953
+
954
+ sI_tile = sI[(None, sI_read_stage)]
955
+
956
+ for n_tile in cutlass.range_constexpr(n_tile_cnt):
957
+ # (MMA, MMA_M, MMA_N, RestM, RestN, RestL)
958
+ mma_tile_coord_mnl = (
959
+ m_tile,
960
+ n_tile,
961
+ token,
962
+ )
963
+ acc_consumer_state = self.epilogue_scatter_reduce(
964
+ tidx,
965
+ 0 if is_leader_cta else 1,
966
+ tiled_copy_dK,
967
+ tCtdK_base,
968
+ sdK,
969
+ sI_tile,
970
+ gdKaccum_nl,
971
+ tCgdKaccum_fake,
972
+ batch,
973
+ seqlen_k_offset,
974
+ epi_tile_dK,
975
+ num_subtiles_executed,
976
+ mma_tile_coord_mnl,
977
+ acc_consumer_state,
978
+ acc_pipeline,
979
+ )
980
+ num_subtiles_executed += self.mma_tiler_dK[1] // cute.size(epi_tile_dK[1])
981
+ # Advance I consumer pipeline
982
+ sI_read_stage = (sI_read_stage + 1) % self.num_I_stage
983
+
984
+ cute.arch.cp_async_wait_group(0)
985
+ #
986
+ # Advance to next tile
987
+ #
988
+ clc_pipeline.consumer_wait(clc_consumer_state)
989
+ work_tile = tile_sched.get_current_work()
990
+ clc_pipeline.consumer_release(clc_consumer_state)
991
+ clc_consumer_state.advance()
992
+
993
+ # # Wait for C store complete
994
+ # dK_pipeline.producer_tail()
995
+ #
996
+ # Dealloc the tensor memory buffer
997
+ #
998
+ tmem.relinquish_alloc_permit()
999
+ tmem.free(tmem_ptr)
1000
+
1001
+ @cute.jit
1002
+ def epilogue_scatter_reduce(
1003
+ self,
1004
+ epi_tidx: Int32,
1005
+ cta_idx: Int32,
1006
+ tiled_copy_c: cute.CopyAtom,
1007
+ tCtAcc_base: cute.Tensor,
1008
+ sC: cute.Tensor,
1009
+ sI_tile: cute.Tensor,
1010
+ gC_base: cute.Tensor,
1011
+ tCgC_fake: cute.Tensor,
1012
+ batch: Int32,
1013
+ seqlen_k_offset: Int32,
1014
+ epi_tile: cute.Tile,
1015
+ num_subtiles_executed: Int32,
1016
+ mma_tile_coord_mnl: Tuple[Int32, Int32, cutlass.Int64],
1017
+ acc_consumer_state: pipeline.PipelineState,
1018
+ acc_pipeline: pipeline.PipelineAsync,
1019
+ ) -> pipeline.PipelineState:
1020
+ warp_idx = cute.arch.make_warp_uniform(epi_tidx // 32)
1021
+
1022
+ # Layout transformation for tCgC_base
1023
+ # ((MMA_ATOM_M, MMA_ATOM_N), MMA_M, MMA_N, REST_M, REST_N, REST_L)
1024
+ # -> ((MMA_ATOM_M, MMA_M), (MMA_ATOM_N, MMA_N), REST_M, REST_N, REST_L)
1025
+ tCgC_fake = transform_partitioned_tensor_layout(tCgC_fake)
1026
+
1027
+ # Layout transformation for tCtAcc_base
1028
+ # ((MMA_ATOM_M, MMA_ATOM_N), MMA_M, MMA_N, STAGE)
1029
+ # -> ((MMA_ATOM_M, MMA_M), (MMA_ATOM_N, MMA_N), STAGE)
1030
+ tCtAcc = transform_partitioned_tensor_layout(tCtAcc_base)
1031
+
1032
+ tiled_copy_t2r, tTR_tAcc_base, tTR_rAcc = epilogue_tmem_copy_and_partition(
1033
+ self,
1034
+ epi_tidx,
1035
+ tCtAcc,
1036
+ tCgC_fake,
1037
+ epi_tile,
1038
+ self.use_2cta_instrs,
1039
+ )
1040
+
1041
+ tTR_rC = cute.make_rmem_tensor(tTR_rAcc.shape, self.c_dtype)
1042
+ tiled_copy_r2s, tRS_rC, tRS_sC = epilogue_smem_copy_and_partition(
1043
+ self, tiled_copy_t2r, tTR_rC, epi_tidx, sC
1044
+ )
1045
+
1046
+ # (EPI_TILE_N, EPI_N, RestN, RestL)
1047
+ gC_epi = cute.flat_divide(gC_base, (epi_tile[1],))
1048
+ # (EPI_TILE_N, EPI_N, SEQLEN_K)
1049
+ gC_epi = gC_epi[None, None, mma_tile_coord_mnl[1], None]
1050
+ # (EPI_TILE_N, MMA_M, STAGE)
1051
+ sC_epi = cute.make_tensor(
1052
+ sC.iterator,
1053
+ cute.select(sC.layout, [1, 0, 2]),
1054
+ # swizzle=sC.layout.inner,
1055
+ )
1056
+
1057
+ thr_copy_c = tiled_copy_c.get_slice(epi_tidx % 32)
1058
+ # (COPY_ATOM_N, COPY_N, MMA_M, STAGE)
1059
+ tCsC = thr_copy_c.partition_S(sC_epi)
1060
+ # (COPY_ATOM_N, COPY_N)
1061
+ tCrC = cute.make_fragment_like(tCsC[None, None, 0, 0])
1062
+ # (COPY_ATOM_N, COPY_N, EPI_N, SEQLEN_K)
1063
+ tCgC = thr_copy_c.partition_D(gC_epi)
1064
+
1065
+ # Set tensor memory buffer for current tile
1066
+ # (T2R, T2R_M, T2R_N, EPI_M, EPI_M)
1067
+ tTR_tAcc = tTR_tAcc_base[(None, None, None, None, None, acc_consumer_state.index)]
1068
+
1069
+ #
1070
+ # Wait for accumulator buffer full
1071
+ #
1072
+ acc_pipeline.consumer_wait(acc_consumer_state)
1073
+
1074
+ tTR_tAcc = cute.group_modes(tTR_tAcc, 3, cute.rank(tTR_tAcc))
1075
+
1076
+ epilog_sync_barrier = pipeline.NamedBarrier(
1077
+ barrier_id=self.epilog_sync_bar_id,
1078
+ num_threads=32 * len(self.epilogue_warp_id),
1079
+ )
1080
+
1081
+ #
1082
+ # Store accumulator to global memory in subtiles
1083
+ #
1084
+ subtile_cnt = cute.size(tTR_tAcc.shape, mode=[3])
1085
+ for subtile_idx in range(subtile_cnt):
1086
+ #
1087
+ # Load accumulator from tensor memory buffer to register
1088
+ #
1089
+ tTR_tAcc_mn = tTR_tAcc[(None, None, None, subtile_idx)]
1090
+ cute.copy(tiled_copy_t2r, tTR_tAcc_mn, tTR_rAcc)
1091
+
1092
+ #
1093
+ # Convert to C type
1094
+ #
1095
+ acc_vec = tiled_copy_r2s.retile(tTR_rAcc).load()
1096
+ acc_vec = acc_vec.to(self.c_dtype)
1097
+ tRS_rC.store(acc_vec)
1098
+
1099
+ #
1100
+ # Store C to shared memory
1101
+ #
1102
+ c_buffer = (num_subtiles_executed + subtile_idx) % self.num_c_stage
1103
+ cute.copy(tiled_copy_r2s, tRS_rC, tRS_sC[(None, None, None, c_buffer)])
1104
+
1105
+ # Fence and barrier to make sure shared memory store is visible to TMA store
1106
+ cute.arch.fence_proxy("async.shared", space="cta")
1107
+ epilog_sync_barrier.arrive_and_wait()
1108
+
1109
+ #
1110
+ # TMA store C to global memory (issued by lane 0 from all epi warps)
1111
+ #
1112
+ for topk_idx_in_warp in cutlass.range(32):
1113
+ topk_idx = topk_idx_in_warp + warp_idx * 32
1114
+ seqlen_k_idx_in_batch = sI_tile[topk_idx]
1115
+ if const_expr(self.varlen):
1116
+ seqlen_k_idx = seqlen_k_idx_in_batch + seqlen_k_offset
1117
+ else:
1118
+ seqlen_k_idx = (batch, seqlen_k_idx_in_batch)
1119
+ cute.copy(tiled_copy_c, tCsC[(None, None, topk_idx, c_buffer)], tCrC)
1120
+ # Skip -1 sentinel slots (invalid top-k entries)
1121
+ if seqlen_k_idx_in_batch >= 0:
1122
+ for j in cutlass.range_constexpr(cute.size(tCrC, mode=[1])):
1123
+ for i in cutlass.range_constexpr(cute.size(tCrC, mode=[0])):
1124
+ ptr = elem_pointer(tCgC, (i, j, subtile_idx, seqlen_k_idx))
1125
+ cute.arch.atomic_add(
1126
+ ptr=ptr,
1127
+ val=tCrC[i, j],
1128
+ )
1129
+ epilog_sync_barrier.arrive_and_wait()
1130
+
1131
+ epilog_sync_barrier.arrive_and_wait()
1132
+
1133
+ #
1134
+ # Async arrive accumulator buffer empty
1135
+ #
1136
+ with cute.arch.elect_one():
1137
+ acc_pipeline.consumer_release(acc_consumer_state)
1138
+ acc_consumer_state.advance()
1139
+ return acc_consumer_state
1140
+
1141
+ def check_can_implement(self):
1142
+ """Check if parameters are valid.
1143
+
1144
+ :raises testing.CantImplementError: If the mma tiler, cluster shape, or alignments are invalid
1145
+ """
1146
+ if self.dim != 64:
1147
+ raise testing.CantImplementError(f"Only dim = 64 supported for dK gemm, got {self.dim}")
1148
+ # Check valid MMA tile shape and cluster shape
1149
+ if not (
1150
+ (not self.use_2cta_instrs and self.mma_tiler_dK[0] in [64, 128])
1151
+ or (self.use_2cta_instrs and self.mma_tiler_dK[0] in [128, 256])
1152
+ ):
1153
+ raise testing.CantImplementError(
1154
+ f"Invalid mma tiler & use_2cta_instrs: {self.mma_tiler_dK}, {self.use_2cta_instrs}"
1155
+ )
1156
+ if self.mma_tiler_dK[1] not in range(32, 257, 32):
1157
+ raise testing.CantImplementError(f"Invalid mma tiler N: {self.mma_tiler_dK[1]}")
1158
+ # Skip illegal cluster shape
1159
+ if self.cluster_shape_mn[0] % (2 if self.use_2cta_instrs else 1) != 0:
1160
+ raise testing.CantImplementError(f"Invalid cluster shape M: {self.cluster_shape_mn[0]}")
1161
+ # Skip invalid cluster shape
1162
+ is_power_of_2 = lambda x: x > 0 and (x & (x - 1)) == 0
1163
+ if (
1164
+ self.cluster_shape_mn[0] * self.cluster_shape_mn[1] > 16
1165
+ or self.cluster_shape_mn[0] <= 0
1166
+ or self.cluster_shape_mn[1] <= 0
1167
+ or not is_power_of_2(self.cluster_shape_mn[0])
1168
+ or not is_power_of_2(self.cluster_shape_mn[1])
1169
+ ):
1170
+ raise testing.CantImplementError(f"Invalid cluster shape: {self.cluster_shape_mn}")
1171
+
1172
+ # Check that all tensors are 16B aligned for TMA
1173
+
1174
+ def check_contiguous_16B_alignment(dtype, num_major_elements):
1175
+ num_contiguous_elements = 16 * 8 // dtype.width
1176
+ return num_major_elements % num_contiguous_elements == 0
1177
+
1178
+ if (
1179
+ not check_contiguous_16B_alignment(self.ab_dtype, self.topk)
1180
+ or not check_contiguous_16B_alignment(self.ab_dtype, self.dim)
1181
+ or not check_contiguous_16B_alignment(self.c_dtype, self.dim)
1182
+ ):
1183
+ raise testing.CantImplementError(
1184
+ f"Invalid tensor alignment: {self.ab_dtype=}, {self.c_dtype=}, {self.topk=}, {self.dim=}"
1185
+ )
build/torch-cuda/flash_bwd_mla_dq_dqv_sm100.py ADDED
@@ -0,0 +1,1297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026, Colfax International.
2
+
3
+ """
4
+ CuTe DSL implementation of dQ+dQv gemm for DSA backward.
5
+ Performs both dQ = dS @ K and dQv = dS @ V, where K and V are
6
+ gathered according to index tensor mIdxTopK.
7
+
8
+ This uses MQA with 128 heads.
9
+
10
+ Inputs:
11
+ - dS: [batch, seqlen_q, nheads, top_k] or [total_q, nheads, top_k]
12
+ - K: [batch, seqlen_k, hdim] or [total_k, hdim]
13
+ - V: [batch, seqlen_k, hdim_v] or [total_k, hdim_v]
14
+ - IdxTopK: [batch, seqlen_q, top_k] or [total_q, top_k]
15
+
16
+ Outputs:
17
+ - dQ: [batch, seqlen_q, nheads, hdim] or [total_q, nheads, hdim]
18
+ - dQv: [batch, seqlen_q, nheads, hdim_v] or [total_q, nheads, hdim_v]
19
+
20
+ All sizes are known at compile time except seqlen_q, which is the batch dimension.
21
+ Representative numbers are:
22
+ - nheads = 128
23
+ - hdim = 64
24
+ - hdim_v = 512
25
+ - top_k = 2048
26
+
27
+ We launch a cluster of shape (1, 2) with mma tile 128x256, so that one cluster
28
+ covers the full dQv mma. dS is loaded via TMA and multicast across the CTAs.
29
+ Cluster 0 also performs the dQ mma with tile size 128x64.
30
+
31
+ K and V are loaded via CpAsync, with logic according to CpasyncGatherKVManager.
32
+ """
33
+
34
+ from functools import partial
35
+ from typing import Optional, Tuple, Type
36
+
37
+ import cuda.bindings.driver as cuda
38
+ import cutlass
39
+ import cutlass.cute as cute
40
+ import cutlass.pipeline as pipeline
41
+ import cutlass.utils as utils
42
+ from cutlass import Int32, const_expr
43
+ from cutlass.cute import FastDivmodDivisor
44
+ from cutlass.cute.nvgpu import cpasync, tcgen05
45
+ from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait
46
+
47
+ from .topk_gather_kv import CpasyncGatherKVManager
48
+ from .utils import get_batch_from_cu_tensor
49
+
50
+
51
+ class dQdQvGemmKernel:
52
+ def __init__(
53
+ self,
54
+ acc_dtype: Type[cutlass.Numeric],
55
+ nheads: int,
56
+ head_dim_k: Optional[int],
57
+ head_dim_v: int,
58
+ top_k: int,
59
+ ):
60
+ self.acc_dtype: Type[cutlass.Numeric] = acc_dtype
61
+ self.nheads = nheads
62
+ assert self.nheads == 128, (
63
+ "only 128 heads supported; will expand to include 64 heads in a future PR."
64
+ )
65
+ self.head_dim_k = head_dim_k or 0 # when head_dim_k not provided, dQ is not computed
66
+ self.head_dim_v = head_dim_v
67
+ self.top_k = top_k
68
+ self.tile_k = 128
69
+
70
+ self.cluster_shape_mn = (1, 2)
71
+ self.mma_tiler_dQ = (self.nheads, self.head_dim_k, self.tile_k)
72
+ self.mma_tiler_dQv = (self.nheads, self.head_dim_v // 2, self.tile_k)
73
+ self.num_mainloop_iters = self.top_k // self.tile_k
74
+ self.arch = "sm_100"
75
+
76
+ self.cta_group = tcgen05.CtaGroup.ONE
77
+
78
+ self.occupancy = 1
79
+ self.threads_per_warp = cute.arch.WARP_SIZE
80
+
81
+ # ---- Set specialized warp ids ----
82
+ self.epilogue_warp_ids = (0, 1, 2, 3)
83
+ self.kv_load_warp_ids = (4, 5, 6, 7)
84
+ self.mma_warp_id = 8
85
+ self.tma_warp_id = 9
86
+ self.sched_warp_id = 10
87
+ self.threads_per_cta = 32 * len(
88
+ (
89
+ self.mma_warp_id,
90
+ self.tma_warp_id,
91
+ self.sched_warp_id,
92
+ *self.epilogue_warp_ids,
93
+ *self.kv_load_warp_ids,
94
+ )
95
+ )
96
+ # ---- Set barrier id for cta sync, epilogue sync and tmem ptr sync ----
97
+ self.epilog_sync_bar_id = 1
98
+ self.tmem_alloc_sync_bar_id = 2
99
+ self.tmem_dealloc_sync_bar_id = 3
100
+ self.kv_load_sync_bar_id = 4
101
+
102
+ self.epilog_sync_barrier = pipeline.NamedBarrier(
103
+ barrier_id=self.epilog_sync_bar_id,
104
+ num_threads=self.threads_per_warp * len(self.epilogue_warp_ids),
105
+ )
106
+ self.kv_load_sync_barrier = pipeline.NamedBarrier(
107
+ barrier_id=self.kv_load_sync_bar_id,
108
+ num_threads=self.threads_per_warp * len(self.kv_load_warp_ids),
109
+ )
110
+
111
+ self.is_persistent = False
112
+
113
+ # ---- pipeline stages ---- TODO: tune these
114
+ self.num_stages_dS = 2
115
+ self.num_stages_KV = 2
116
+ self.num_stages_acc = 1
117
+ self.num_stages_clc = 1
118
+
119
+ # ---- register allocation ----
120
+ self.num_regs_KV = 224
121
+ self.num_regs_epi = 128
122
+ self.num_regs_other = 112
123
+
124
+ @cute.jit
125
+ def __call__(
126
+ self,
127
+ mdS: cute.Tensor,
128
+ mK: Optional[cute.Tensor],
129
+ mV: cute.Tensor,
130
+ mdQ: cute.Tensor,
131
+ mdQv: cute.Tensor,
132
+ mIdxTopK: cute.Tensor,
133
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
134
+ mCuSeqlensK: Optional[cute.Tensor] = None,
135
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
136
+ stream: cuda.CUstream = None,
137
+ ):
138
+ self.compute_dQ = const_expr(mK is not None)
139
+
140
+ # ---- dtype info ----
141
+ self.ds_dtype: Type[cutlass.Numeric] = mdS.element_type
142
+ self.kv_dtype: Type[cutlass.Numeric] = mV.element_type
143
+ self.dq_dtype: Type[cutlass.Numeric] = mdQv.element_type
144
+ if const_expr(self.compute_dQ):
145
+ assert self.kv_dtype == mV.element_type
146
+ assert self.dq_dtype == mdQ.element_type
147
+
148
+ varlen_q = const_expr(mCuSeqlensQ is not None)
149
+ varlen_k = const_expr(mCuSeqlensK is not None)
150
+
151
+ # ------------------------------------------------------------------ #
152
+ # Reshape GMEM layouts for static strides #
153
+ # ------------------------------------------------------------------ #
154
+ seqlen_q = Int32(0) if const_expr(varlen_q) else mdS.shape[1]
155
+ seqlen_q_divmod = FastDivmodDivisor(seqlen_q)
156
+ seqlen_k = Int32(0) if const_expr(varlen_k) else mV.shape[1]
157
+
158
+ # ---- group batch and seqlen modes in nonvarlen case ----
159
+ def group_batch_seqlen(t: cute.Tensor, varlen: bool) -> cute.Tensor:
160
+ if const_expr(not varlen):
161
+ t = cute.make_tensor(
162
+ t.iterator,
163
+ cute.make_layout(
164
+ (t.shape[1], t.shape[0], *t.shape[2:]),
165
+ stride=(t.stride[1], t.stride[0], *t.stride[2:]),
166
+ ),
167
+ )
168
+ t = cute.group_modes(t, 0, 2)
169
+ return t
170
+
171
+ mdS = group_batch_seqlen(mdS, varlen_q)
172
+ mdQv = group_batch_seqlen(mdQv, varlen_q)
173
+ mV = group_batch_seqlen(mV, varlen_k)
174
+ mIdxTopK = group_batch_seqlen(mIdxTopK, varlen_q)
175
+ if const_expr(self.compute_dQ):
176
+ mdQ = group_batch_seqlen(mdQ, varlen_q)
177
+ mK = group_batch_seqlen(mK, varlen_k)
178
+
179
+ # ---- transpose and make static modes static ----
180
+ def static_reshape(t: cute.Tensor, *static_shapes) -> cute.Tensor:
181
+ static_modes = range(1, len(t.shape))
182
+ return cute.make_tensor(
183
+ t.iterator,
184
+ cute.make_layout(
185
+ (*static_shapes, t.shape[0]),
186
+ stride=(*(t.stride[i] for i in static_modes), t.stride[0]),
187
+ ),
188
+ )
189
+
190
+ mdS = static_reshape(mdS, self.nheads, self.top_k)
191
+ mdQv = static_reshape(mdQv, self.nheads, self.head_dim_v)
192
+ mV = static_reshape(mV, self.head_dim_v)
193
+ mIdxTopK = static_reshape(mIdxTopK, self.top_k)
194
+ if const_expr(self.compute_dQ):
195
+ mdQ = static_reshape(mdQ, self.nheads, self.head_dim_k)
196
+ mK = static_reshape(mK, self.head_dim_k)
197
+
198
+ # ---- layout info ----
199
+ self.ds_major_mode = utils.LayoutEnum.from_tensor(mdS).mma_major_mode()
200
+ self.kv_major_mode = utils.LayoutEnum.from_tensor(mV).mma_major_mode()
201
+ self.dq_layout = utils.LayoutEnum.from_tensor(mdQv)
202
+ if const_expr(self.compute_dQ):
203
+ assert self.dq_layout == utils.LayoutEnum.from_tensor(mdQ)
204
+
205
+ # ------------------------------------------------------------------ #
206
+ # Setup attributes that depend on kernel inputs #
207
+ # ------------------------------------------------------------------ #
208
+ tiled_mma_v = utils.sm100.make_trivial_tiled_mma(
209
+ self.ds_dtype,
210
+ self.ds_major_mode,
211
+ self.kv_major_mode,
212
+ self.acc_dtype,
213
+ self.cta_group,
214
+ self.mma_tiler_dQv[:2],
215
+ )
216
+ if const_expr(self.compute_dQ):
217
+ tiled_mma_k = utils.sm100.make_trivial_tiled_mma(
218
+ self.ds_dtype,
219
+ self.ds_major_mode,
220
+ self.kv_major_mode,
221
+ self.acc_dtype,
222
+ self.cta_group,
223
+ self.mma_tiler_dQ[:2],
224
+ )
225
+
226
+ self.cta_tile_shape_dQv = (
227
+ self.mma_tiler_dQv[0],
228
+ self.mma_tiler_dQv[1],
229
+ self.mma_tiler_dQv[2],
230
+ )
231
+
232
+ # ---- Compute cluster layout ----
233
+ self.cluster_layout_vmnk = cute.tiled_divide(
234
+ cute.make_layout((*self.cluster_shape_mn, 1)),
235
+ (tiled_mma_v.thr_id.shape,),
236
+ )
237
+
238
+ # ---- Compute number of multicast CTAs for A/B ----
239
+ self.num_mcast_ctas_a = cute.size(self.cluster_layout_vmnk.shape[2])
240
+
241
+ # ---- Compute epi tiles for dQ/dQv ----
242
+ self.epi_tile_dQv = utils.sm100.compute_epilogue_tile_shape(
243
+ self.cta_tile_shape_dQv,
244
+ False, # use_2cta_instrs
245
+ self.dq_layout,
246
+ self.dq_dtype,
247
+ )
248
+ self.epi_tile_dQ = None
249
+ if const_expr(self.compute_dQ):
250
+ self.epi_tile_dQ = utils.sm100.compute_epilogue_tile_shape(
251
+ self.mma_tiler_dQ,
252
+ False,
253
+ self.dq_layout,
254
+ self.dq_dtype,
255
+ )
256
+
257
+ # ---- Device-specific attributes ----
258
+ self.smem_capacity = utils.get_smem_capacity_in_bytes()
259
+
260
+ self.num_tmem_alloc_cols = 512
261
+
262
+ # ------------------------------------------------------------------ #
263
+ # Make SMEM layouts #
264
+ # ------------------------------------------------------------------ #
265
+ sdS_layout = utils.sm100.make_smem_layout_a(
266
+ tiled_mma_v,
267
+ self.mma_tiler_dQ,
268
+ self.ds_dtype,
269
+ self.num_stages_dS,
270
+ )
271
+ sV_layout = utils.sm100.make_smem_layout_b(
272
+ tiled_mma_v,
273
+ self.mma_tiler_dQv,
274
+ self.kv_dtype,
275
+ self.num_stages_KV,
276
+ )
277
+ sdQv_layout = utils.sm100.make_smem_layout_epi(
278
+ self.dq_dtype,
279
+ self.dq_layout,
280
+ self.epi_tile_dQv,
281
+ self.num_stages_acc,
282
+ )
283
+ sK_layout, sdQ_layout = None, None
284
+ if const_expr(self.compute_dQ):
285
+ sK_layout = utils.sm100.make_smem_layout_b(
286
+ tiled_mma_k,
287
+ self.mma_tiler_dQ,
288
+ self.kv_dtype,
289
+ self.num_stages_KV,
290
+ )
291
+ sdQ_layout = utils.sm100.make_smem_layout_epi(
292
+ self.dq_dtype,
293
+ self.dq_layout,
294
+ self.epi_tile_dQ,
295
+ self.num_stages_acc,
296
+ )
297
+
298
+ # ------------------------------------------------------------------ #
299
+ # Set up TMA load/stores #
300
+ # ------------------------------------------------------------------ #
301
+ atom_thr_size = cute.size(tiled_mma_v.thr_id.shape)
302
+
303
+ # ---- Setup TMA load for dS ----
304
+ dS_op = utils.sm100.cluster_shape_to_tma_atom_A(self.cluster_shape_mn, tiled_mma_v.thr_id)
305
+ dS_smem_layout = cute.slice_(sdS_layout, (None, None, None, 0))
306
+ tma_atom_dS, tma_tensor_dS = cute.nvgpu.make_tiled_tma_atom_A(
307
+ dS_op,
308
+ mdS,
309
+ dS_smem_layout,
310
+ self.mma_tiler_dQv,
311
+ tiled_mma_v,
312
+ self.cluster_layout_vmnk.shape,
313
+ )
314
+
315
+ dS_copy_size = cute.size_in_bytes(self.ds_dtype, dS_smem_layout)
316
+ self.num_tma_load_bytes = dS_copy_size * atom_thr_size
317
+
318
+ # ---- Setup TMA store for dQ and dQV ----
319
+ dQv_epi_smem_layout = cute.select(sdQv_layout, mode=[0, 1])
320
+ tma_atom_dQv, tma_tensor_dQv = cpasync.make_tiled_tma_atom(
321
+ cpasync.CopyBulkTensorTileS2GOp(), mdQv, dQv_epi_smem_layout, self.epi_tile_dQv
322
+ )
323
+ tma_atom_dQ, tma_tensor_dQ = None, None
324
+ if const_expr(self.compute_dQ):
325
+ dQ_epi_smem_layout = cute.select(sdQ_layout, mode=[0, 1])
326
+ tma_atom_dQ, tma_tensor_dQ = cpasync.make_tiled_tma_atom(
327
+ cpasync.CopyBulkTensorTileS2GOp(), mdQ, dQ_epi_smem_layout, self.epi_tile_dQ
328
+ )
329
+
330
+ # ------------------------------------------------------------------ #
331
+ # Set up shared storage for SMEM #
332
+ # ------------------------------------------------------------------ #
333
+
334
+ self.buffer_align_bytes = 1024
335
+
336
+ sdS_size = cute.cosize(sdS_layout)
337
+ sK_size = cute.cosize(sK_layout) if const_expr(self.compute_dQ) else 0
338
+ sV_size = cute.cosize(sV_layout)
339
+ sdQ_size = cute.cosize(sdQ_layout) if const_expr(self.compute_dQ) else 0
340
+ sdQv_size = cute.cosize(sdQv_layout)
341
+ assert sdQ_size <= sK_size, f"require {sdQ_size=} <= {sK_size=}"
342
+ assert sdQv_size <= sV_size, f"require {sdQv_size=} <= {sV_size=}"
343
+
344
+ self.overlap_kv_epi = self.compute_dQ
345
+ if const_expr(self.overlap_kv_epi):
346
+ sdQ_size = 0
347
+ sdQv_size = 0
348
+
349
+ @cute.struct
350
+ class SharedStorage:
351
+ mbar_ptr_dS: cute.struct.MemRange[cutlass.Int64, self.num_stages_dS * 2]
352
+ mbar_ptr_KV: cute.struct.MemRange[cutlass.Int64, self.num_stages_KV * 2]
353
+ mbar_ptr_dQ_dQv: cute.struct.MemRange[cutlass.Int64, self.num_stages_acc * 2]
354
+ mbar_ptr_KV_cpasync: cute.struct.MemRange[cutlass.Int64, self.num_stages_KV * 2]
355
+ mbar_ptr_load_kv_epi: cute.struct.MemRange[cutlass.Int64, 2]
356
+ # Tmem holding buffer
357
+ tmem_dealloc_mbar: cutlass.Int64
358
+ tmem_holding_buf: cutlass.Int32
359
+ # Clc pointers
360
+ clc_ptr: cute.struct.Align[
361
+ cute.struct.MemRange[cutlass.Int64, self.num_stages_clc * 2], 16
362
+ ]
363
+ clc_response_ptr: cute.struct.Align[cute.struct.MemRange[cutlass.Int32, 4], 16]
364
+ # Smem tensors
365
+ sdS: cute.struct.Align[
366
+ cute.struct.MemRange[self.ds_dtype, sdS_size],
367
+ self.buffer_align_bytes,
368
+ ]
369
+ sK: cute.struct.Align[
370
+ cute.struct.MemRange[self.kv_dtype, sK_size],
371
+ self.buffer_align_bytes,
372
+ ]
373
+ sV: cute.struct.Align[
374
+ cute.struct.MemRange[self.kv_dtype, sV_size],
375
+ self.buffer_align_bytes,
376
+ ]
377
+ sdQ: cute.struct.Align[
378
+ cute.struct.MemRange[self.dq_dtype, sdQ_size],
379
+ self.buffer_align_bytes,
380
+ ]
381
+ sdQv: cute.struct.Align[
382
+ cute.struct.MemRange[self.dq_dtype, sdQv_size],
383
+ self.buffer_align_bytes,
384
+ ]
385
+
386
+ self.shared_storage = SharedStorage
387
+
388
+ # ---- Compute grid size ----
389
+ self.tile_sched_params, grid = self._compute_grid(
390
+ mdQv,
391
+ self.cta_tile_shape_dQv,
392
+ self.cluster_shape_mn,
393
+ )
394
+ self.num_clc_response_bytes = 16
395
+ # permute grid to conform to grid_dim_z <= 65536 constraint;
396
+ # this is undone in the kernel
397
+ grid = (grid[2], grid[1], grid[0])
398
+
399
+ # cute.printf("dQ/dQv grid: {}", grid)
400
+ # print("dQ/dQv SMEM: ", self.shared_storage.size_in_bytes())
401
+ # ---- Launch the kernel synchronously ----
402
+ self.kernel(
403
+ tiled_mma_k if const_expr(self.compute_dQ) else None,
404
+ tiled_mma_v,
405
+ tma_atom_dS,
406
+ tma_tensor_dS,
407
+ mK,
408
+ mV,
409
+ tma_atom_dQ,
410
+ tma_tensor_dQ,
411
+ tma_atom_dQv,
412
+ tma_tensor_dQv,
413
+ mIdxTopK,
414
+ mCuSeqlensQ,
415
+ mCuSeqlensK,
416
+ seqlen_q_divmod,
417
+ self.cluster_layout_vmnk,
418
+ sdS_layout,
419
+ sK_layout,
420
+ sV_layout,
421
+ sdQ_layout,
422
+ sdQv_layout,
423
+ self.epi_tile_dQ,
424
+ self.epi_tile_dQv,
425
+ self.tile_sched_params,
426
+ seqlen_k,
427
+ ).launch(
428
+ grid=grid,
429
+ block=[self.threads_per_cta, 1, 1],
430
+ cluster=(*self.cluster_shape_mn, 1),
431
+ smem=self.shared_storage.size_in_bytes(),
432
+ stream=stream,
433
+ )
434
+
435
+ # GPU device kernel
436
+ @cute.kernel
437
+ def kernel(
438
+ self,
439
+ tiled_mma_k: Optional[cute.TiledMma],
440
+ tiled_mma_v: cute.TiledMma,
441
+ tma_atom_dS: cute.CopyAtom,
442
+ mdS: cute.Tensor,
443
+ mK: Optional[cute.Tensor],
444
+ mV: cute.Tensor,
445
+ tma_atom_dQ: Optional[cute.CopyAtom],
446
+ mdQ: Optional[cute.Tensor],
447
+ tma_atom_dQv: cute.CopyAtom,
448
+ mdQv: cute.Tensor,
449
+ mIdxTopK: cute.Tensor,
450
+ mCuSeqlensQ: cute.Tensor,
451
+ mCuSeqlensK: cute.Tensor,
452
+ seqlen_q_divmod: FastDivmodDivisor,
453
+ cluster_layout_vmnk: cute.Layout,
454
+ sdS_layout: cute.ComposedLayout,
455
+ sK_layout: Optional[cute.ComposedLayout],
456
+ sV_layout: cute.ComposedLayout,
457
+ sdQ_layout: Optional[cute.ComposedLayout],
458
+ sdQv_layout: cute.ComposedLayout,
459
+ epi_tile_dQ: Optional[cute.Tile],
460
+ epi_tile_dQv: cute.Tile,
461
+ tile_sched_params: utils.ClcDynamicPersistentTileSchedulerParams,
462
+ seqlen_k_static: Int32,
463
+ ):
464
+ """
465
+ GPU device kernel performing the Persistent batched GEMM computation.
466
+ """
467
+ warp_idx = cute.arch.warp_idx()
468
+ warp_idx = cute.arch.make_warp_uniform(warp_idx)
469
+
470
+ # ------------------------------------------------------------------ #
471
+ # Prefetch TMA descriptors #
472
+ # ------------------------------------------------------------------ #
473
+ if warp_idx == self.tma_warp_id:
474
+ cpasync.prefetch_descriptor(tma_atom_dS)
475
+ cpasync.prefetch_descriptor(tma_atom_dQv)
476
+ if const_expr(self.compute_dQ):
477
+ cpasync.prefetch_descriptor(tma_atom_dQ)
478
+
479
+ # ------------------------------------------------------------------ #
480
+ # Cluster coordinates #
481
+ # ------------------------------------------------------------------ #
482
+ bidx, bidy, bidz = cute.arch.block_idx()
483
+ gridx, gridy, gridz = cute.arch.grid_dim()
484
+ mma_v_tile_coord_v = bidx % cute.size(tiled_mma_v.thr_id.shape)
485
+ if const_expr(self.compute_dQ):
486
+ mma_k_tile_coord_v = bidx % cute.size(tiled_mma_k.thr_id.shape)
487
+ cta_rank_in_cluster = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
488
+ block_in_cluster_coord_vmnk = cluster_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
489
+ is_first_cta = cta_rank_in_cluster == 0
490
+ tidx, _, _ = cute.arch.thread_idx()
491
+
492
+ # ------------------------------------------------------------------ #
493
+ # Shared storage allocation #
494
+ # ------------------------------------------------------------------ #
495
+ smem = utils.SmemAllocator()
496
+ storage = smem.allocate(self.shared_storage)
497
+
498
+ # ------------------------------------------------------------------ #
499
+ # Initialize pipelines #
500
+ # ------------------------------------------------------------------ #
501
+ ThreadCooperativeGroup = partial(pipeline.CooperativeGroup, pipeline.Agent.Thread)
502
+ kv_commit_group = ThreadCooperativeGroup(1)
503
+ clc_producer_group = ThreadCooperativeGroup(1)
504
+ num_clc_consumer_threads = 32 * (
505
+ 1 # sched warp on CTA0 only
506
+ + cute.size(self.cluster_shape_mn)
507
+ * (1 + len(self.epilogue_warp_ids) + len(self.kv_load_warp_ids) + 1)
508
+ # tma + epi + kv_load + mma, on BOTH CTAs
509
+ )
510
+ clc_consumer_group = ThreadCooperativeGroup(num_clc_consumer_threads)
511
+ mma_warp = ThreadCooperativeGroup(1)
512
+ tma_warp = ThreadCooperativeGroup(cute.size(self.cluster_shape_mn))
513
+ tma_warp_local = ThreadCooperativeGroup(1)
514
+ epilogue_warps = ThreadCooperativeGroup(len(self.epilogue_warp_ids))
515
+ load_warps = ThreadCooperativeGroup(len(self.kv_load_warp_ids))
516
+
517
+ pipeline_dS = pipeline.PipelineTmaUmma.create(
518
+ barrier_storage=storage.mbar_ptr_dS.data_ptr(),
519
+ num_stages=self.num_stages_dS,
520
+ producer_group=mma_warp,
521
+ consumer_group=tma_warp,
522
+ tx_count=self.num_tma_load_bytes,
523
+ cta_layout_vmnk=cluster_layout_vmnk,
524
+ defer_sync=True,
525
+ )
526
+
527
+ pipeline_KV = pipeline.PipelineAsyncUmma.create(
528
+ barrier_storage=storage.mbar_ptr_KV.data_ptr(),
529
+ num_stages=self.num_stages_KV,
530
+ producer_group=kv_commit_group,
531
+ consumer_group=mma_warp,
532
+ defer_sync=True,
533
+ )
534
+
535
+ pipeline_dQ_dQv = pipeline.PipelineUmmaAsync.create(
536
+ barrier_storage=storage.mbar_ptr_dQ_dQv.data_ptr(),
537
+ num_stages=self.num_stages_acc,
538
+ producer_group=mma_warp,
539
+ consumer_group=epilogue_warps,
540
+ defer_sync=True,
541
+ )
542
+
543
+ pipeline_clc = pipeline.PipelineClcFetchAsync.create(
544
+ barrier_storage=storage.clc_ptr.data_ptr(),
545
+ num_stages=self.num_stages_clc,
546
+ producer_group=clc_producer_group,
547
+ consumer_group=clc_consumer_group,
548
+ tx_count=self.num_clc_response_bytes,
549
+ cta_layout_vmnk=cluster_layout_vmnk,
550
+ defer_sync=True,
551
+ )
552
+
553
+ pipeline_load_kv_epi = None
554
+ if const_expr(self.overlap_kv_epi):
555
+ pipeline_load_kv_epi = pipeline.PipelineAsync.create(
556
+ barrier_storage=storage.mbar_ptr_load_kv_epi.data_ptr(),
557
+ num_stages=1,
558
+ producer_group=epilogue_warps,
559
+ consumer_group=load_warps,
560
+ defer_sync=True,
561
+ )
562
+
563
+ # ------------------------------------------------------------------ #
564
+ # TMEM Allocation #
565
+ # ------------------------------------------------------------------ #
566
+ tmem_alloc_barrier = pipeline.NamedBarrier(
567
+ barrier_id=self.tmem_alloc_sync_bar_id,
568
+ num_threads=32 * len((self.mma_warp_id, *self.epilogue_warp_ids)),
569
+ )
570
+ # ---- Tensor memory dealloc barrier init ----
571
+ tmem = utils.TmemAllocator(
572
+ storage.tmem_holding_buf.ptr,
573
+ barrier_for_retrieve=tmem_alloc_barrier,
574
+ allocator_warp_id=self.epilogue_warp_ids[0],
575
+ is_two_cta=False,
576
+ two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
577
+ )
578
+
579
+ # ---- Cluster arrive after barrier init ----
580
+ pipeline_init_arrive(cluster_shape_mn=cluster_layout_vmnk, is_relaxed=True)
581
+
582
+ # ---- Initial clc response pointer ----
583
+ clc_response_ptr = storage.clc_response_ptr.data_ptr()
584
+
585
+ clc_consumer_state = pipeline.make_pipeline_state(
586
+ pipeline.PipelineUserType.Consumer, self.num_stages_clc
587
+ )
588
+
589
+ # ------------------------------------------------------------------ #
590
+ # SMEM tensors #
591
+ # ------------------------------------------------------------------ #
592
+ # (MMA, MMA_M, MMA_K, STAGE)
593
+ sdS = storage.sdS.get_tensor(sdS_layout.outer, swizzle=sdS_layout.inner)
594
+ if const_expr(self.compute_dQ):
595
+ sK = storage.sK.get_tensor(sK_layout.outer, swizzle=sK_layout.inner)
596
+ sV = storage.sV.get_tensor(sV_layout.outer, swizzle=sV_layout.inner)
597
+ # sdQ = storage.sdQ.get_tensor(sdQ_layout.outer, swizzle=sdQ_layout.inner)
598
+ if const_expr(self.compute_dQ):
599
+ sdQ = cute.make_tensor(
600
+ cute.recast_ptr(sK.iterator, sdQ_layout.inner, self.dq_dtype), sdQ_layout.outer
601
+ )
602
+ if const_expr(not self.compute_dQ):
603
+ sdQv = storage.sdQv.get_tensor(sdQv_layout.outer, swizzle=sdQv_layout.inner)
604
+ else:
605
+ sdQv = cute.make_tensor(
606
+ cute.recast_ptr(sV.iterator, sdQv_layout.inner, self.dq_dtype), sdQv_layout.outer
607
+ )
608
+
609
+ dS_full_mcast_mask = cpasync.create_tma_multicast_mask(
610
+ cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=2
611
+ )
612
+
613
+ # ------------------------------------------------------------------ #
614
+ # Global tile partitioning #
615
+ # ------------------------------------------------------------------ #
616
+ # (bM, bK, RestM, RestK, RestL)
617
+ gdS = cute.local_tile(
618
+ mdS, cute.slice_(self.mma_tiler_dQv, (None, 0, None)), (None, None, None)
619
+ )
620
+ # (bM, bN, RestM, RestN, RestL)
621
+ if const_expr(self.compute_dQ):
622
+ gdQ = cute.local_tile(
623
+ mdQ, cute.slice_(self.mma_tiler_dQ, (None, None, 0)), (None, None, None)
624
+ )
625
+ gdQv = cute.local_tile(
626
+ mdQv, cute.slice_(self.mma_tiler_dQv, (None, None, 0)), (None, None, None)
627
+ )
628
+ k_tile_cnt = cute.size(gdS, mode=[3])
629
+
630
+ # ------------------------------------------------------------------ #
631
+ # TiledMMA partitioning #
632
+ # ------------------------------------------------------------------ #
633
+ thr_mma_v = tiled_mma_v.get_slice(mma_v_tile_coord_v)
634
+ # (MMA, MMA_M, MMA_K, RestM, RestK, RestL)
635
+ tdQvgdS = thr_mma_v.partition_A(gdS)
636
+ # (MMA, MMA_M, MMA_N, RestM, RestN, RestL)
637
+ tdQvgdQv = thr_mma_v.partition_C(gdQv)
638
+ if const_expr(self.compute_dQ):
639
+ thr_mma_k = tiled_mma_k.get_slice(mma_k_tile_coord_v)
640
+ tdQgdQ = thr_mma_k.partition_C(gdQ)
641
+
642
+ # ------------------------------------------------------------------ #
643
+ # TMA partition for dS #
644
+ # ------------------------------------------------------------------ #
645
+ dS_cta_layout = cute.make_layout(cute.slice_(cluster_layout_vmnk, (0, 0, None, 0)).shape)
646
+ # ((atom_v, rest_v), STAGE)
647
+ # ((atom_v, rest_v), RestM, RestK, RestL)
648
+ tdSsdS, tdSgdS = cpasync.tma_partition(
649
+ tma_atom_dS,
650
+ block_in_cluster_coord_vmnk[2],
651
+ dS_cta_layout,
652
+ cute.group_modes(sdS, 0, 3),
653
+ cute.group_modes(tdQvgdS, 0, 3),
654
+ )
655
+
656
+ # ------------------------------------------------------------------ #
657
+ # MMA fragments #
658
+ # ------------------------------------------------------------------ #
659
+ # (MMA, MMA_M, MMA_K, STAGE)
660
+ tdQvrdS = tiled_mma_v.make_fragment_A(sdS)
661
+ # (MMA, MMA_N, MMA_K, STAGE)
662
+ tdQvrV = tiled_mma_v.make_fragment_B(sV)
663
+ # (MMA, MMA_M, MMA_N)
664
+ acc_v_shape = tiled_mma_v.partition_shape_C(self.mma_tiler_dQv[:2])
665
+ # (MMA, MMA_M, MMA_N, STAGE)
666
+ tdQvtAcc_fake = tiled_mma_v.make_fragment_C(cute.append(acc_v_shape, self.num_stages_acc))
667
+ if const_expr(self.compute_dQ):
668
+ # (MMA, MMA_N, MMA_K, STAGE)
669
+ tdQrK = tiled_mma_k.make_fragment_B(sK)
670
+ # (MMA, MMA_M, MMA_N)
671
+ acc_k_shape = tiled_mma_k.partition_shape_C(self.mma_tiler_dQ[:2])
672
+ # (MMA, MMA_M, MMA_N, STAGE)
673
+ tdQtAcc_fake = tiled_mma_k.make_fragment_C(
674
+ cute.append(acc_k_shape, self.num_stages_acc)
675
+ )
676
+
677
+ # ------------------------------------------------------------------ #
678
+ # Cluster wait before tensor memory alloc #
679
+ # ------------------------------------------------------------------ #
680
+ pipeline_init_wait(cluster_shape_mn=cluster_layout_vmnk)
681
+
682
+ # ------------------------------------------------------------------ #
683
+ # Tile Scheduler #
684
+ # ------------------------------------------------------------------ #
685
+ tile_sched = utils.ClcDynamicPersistentTileScheduler.create(
686
+ tile_sched_params,
687
+ cute.arch.block_idx(),
688
+ cute.arch.grid_dim(),
689
+ clc_response_ptr,
690
+ )
691
+ work_tile = tile_sched.initial_work_tile_info()
692
+
693
+ # ------------------------------------------------------------------ #
694
+ # TMA load warp #
695
+ # ------------------------------------------------------------------ #
696
+ if warp_idx == self.tma_warp_id:
697
+ cute.arch.setmaxregister_decrease(self.num_regs_other)
698
+
699
+ producer_state_dS = pipeline.make_pipeline_state(
700
+ pipeline.PipelineUserType.Producer, stages=self.num_stages_dS
701
+ )
702
+
703
+ while work_tile.is_valid_tile:
704
+ # ---- Get tile coord from tile scheduler ----
705
+ token, cta, _ = work_tile.tile_idx
706
+
707
+ # ((atom_v, rest_v), RestK)
708
+ tdSgdS_slice = tdSgdS[(None, 0, None, token)]
709
+
710
+ # ---- mainloop ----
711
+ for k_tile in cutlass.range(0, k_tile_cnt, 1, unroll=1):
712
+ pipeline_dS.producer_acquire(producer_state_dS)
713
+ index_dS = producer_state_dS.index
714
+
715
+ # ---- TMA load dS ----
716
+ cute.copy(
717
+ tma_atom_dS,
718
+ tdSgdS_slice[(None, k_tile)],
719
+ tdSsdS[(None, index_dS)],
720
+ tma_bar_ptr=pipeline_dS.producer_get_barrier(producer_state_dS),
721
+ mcast_mask=dS_full_mcast_mask,
722
+ )
723
+
724
+ producer_state_dS.advance()
725
+
726
+ # ---- Advance to next tile ----
727
+ pipeline_clc.consumer_wait(clc_consumer_state)
728
+ work_tile = tile_sched.get_current_work()
729
+ pipeline_clc.consumer_release(clc_consumer_state)
730
+ clc_consumer_state.advance()
731
+
732
+ # ---- Wait dS buffer empty ----
733
+ pipeline_dS.producer_tail(producer_state_dS)
734
+
735
+ # ------------------------------------------------------------------ #
736
+ # Clc Scheduler warp #
737
+ # ------------------------------------------------------------------ #
738
+ if warp_idx == self.sched_warp_id and is_first_cta:
739
+ cute.arch.setmaxregister_decrease(self.num_regs_other)
740
+ clc_producer_state = pipeline.make_pipeline_state(
741
+ pipeline.PipelineUserType.ProducerConsumer, self.num_stages_clc
742
+ )
743
+
744
+ while work_tile.is_valid_tile:
745
+ pipeline_clc.producer_acquire(clc_producer_state)
746
+ mbar_addr = pipeline_clc.producer_get_barrier(clc_producer_state)
747
+ tile_sched.advance_to_next_work(mbar_addr)
748
+ clc_producer_state.advance()
749
+
750
+ pipeline_clc.consumer_wait(clc_consumer_state)
751
+ work_tile = tile_sched.get_current_work()
752
+ pipeline_clc.consumer_release(clc_consumer_state)
753
+ clc_consumer_state.advance()
754
+
755
+ pipeline_clc.producer_tail(clc_producer_state)
756
+
757
+ # ------------------------------------------------------------------ #
758
+ # CpAsync KV load warps #
759
+ # ------------------------------------------------------------------ #
760
+ if warp_idx >= self.kv_load_warp_ids[0] and warp_idx <= self.kv_load_warp_ids[-1]:
761
+ cute.arch.setmaxregister_increase(self.num_regs_KV)
762
+ find_batch = partial(
763
+ self.find_batch_from_q, seqlen_q_divmod=seqlen_q_divmod, mCuSeqlensQ=mCuSeqlensQ
764
+ )
765
+
766
+ load_epi_consumer_state = pipeline.make_pipeline_state(
767
+ pipeline.PipelineUserType.Consumer, 1
768
+ )
769
+ producer_state_KV = pipeline.make_pipeline_state(
770
+ pipeline.PipelineUserType.Producer, stages=self.num_stages_KV
771
+ )
772
+
773
+ kv_tidx = tidx % (len(self.kv_load_warp_ids) * self.threads_per_warp)
774
+ kv_warp_idx = warp_idx % len(self.kv_load_warp_ids)
775
+
776
+ mV_cta = cute.domain_offset((cta_rank_in_cluster * (self.head_dim_v // 2), 0), mV)
777
+
778
+ while work_tile.is_valid_tile:
779
+ # ---- Get tile coord from tile scheduler ----
780
+ token, cta, _ = work_tile.tile_idx
781
+
782
+ batch_idx = find_batch(token)
783
+ k_batch_offset = (
784
+ mCuSeqlensK[batch_idx] if const_expr(mCuSeqlensK is not None) else Int32(0)
785
+ )
786
+ seqlen_k = (
787
+ mCuSeqlensK[batch_idx + 1] - k_batch_offset
788
+ if const_expr(mCuSeqlensK is not None)
789
+ else seqlen_k_static
790
+ )
791
+ if const_expr(mCuSeqlensK is not None):
792
+ if const_expr(self.compute_dQ):
793
+ mK_cur = cute.domain_offset((0, k_batch_offset), mK)[None, None]
794
+ mV_cur = cute.domain_offset((0, k_batch_offset), mV_cta)[None, None]
795
+ else:
796
+ if const_expr(self.compute_dQ):
797
+ mK_cur = cute.domain_offset((0, (0, batch_idx)), mK)[None, None]
798
+ mV_cur = cute.domain_offset((0, (0, batch_idx)), mV_cta)[None, None]
799
+ mIdxTopK_cur = mIdxTopK[None, token]
800
+
801
+ cpasync_gather_kv_manager = CpasyncGatherKVManager.create(
802
+ mIdxTopK_cur,
803
+ 0,
804
+ kv_tidx,
805
+ kv_warp_idx,
806
+ self.top_k,
807
+ seqlen_k,
808
+ self.mma_tiler_dQv[2],
809
+ self.head_dim_k,
810
+ self.mma_tiler_dQv[1],
811
+ 1,
812
+ len(self.kv_load_warp_ids) * self.threads_per_warp,
813
+ mV.element_type,
814
+ 1,
815
+ )
816
+
817
+ # ---- K/V load mainloop ----
818
+ for k_tile in cutlass.range_constexpr(k_tile_cnt):
819
+ # ---- Load top-k index tensor ----
820
+ cpasync_gather_kv_manager.load_index_topk(k_tile, transpose=True)
821
+
822
+ stage = producer_state_KV.index
823
+ pipeline_KV.producer_acquire(producer_state_KV)
824
+
825
+ # ---- Load V (and optionally load K) ----
826
+ cpasync_gather_kv_manager.load_X(mV_cur, sV[None, None, None, stage], True, "V")
827
+ if const_expr(self.compute_dQ):
828
+ if is_first_cta:
829
+ cpasync_gather_kv_manager.load_X(
830
+ mK_cur, sK[None, None, None, stage], True, "K"
831
+ )
832
+
833
+ cute.arch.cp_async_commit_group()
834
+ cute.arch.cp_async_wait_group(0)
835
+ self.kv_load_sync_barrier.arrive_and_wait()
836
+
837
+ cute.arch.fence_proxy("async.shared", space="cta")
838
+ if kv_warp_idx == 0:
839
+ with cute.arch.elect_one():
840
+ pipeline_KV.producer_commit(producer_state_KV)
841
+ producer_state_KV.advance()
842
+
843
+ if const_expr(self.overlap_kv_epi):
844
+ pipeline_load_kv_epi.consumer_wait(load_epi_consumer_state)
845
+ with cute.arch.elect_one():
846
+ pipeline_load_kv_epi.consumer_release(load_epi_consumer_state)
847
+ load_epi_consumer_state.advance()
848
+
849
+ # ---- Advance to next tile ----
850
+ pipeline_clc.consumer_wait(clc_consumer_state)
851
+ work_tile = tile_sched.get_current_work()
852
+ pipeline_clc.consumer_release(clc_consumer_state)
853
+ clc_consumer_state.advance()
854
+
855
+ pipeline_KV.producer_tail(producer_state_KV)
856
+
857
+ # ------------------------------------------------------------------ #
858
+ # MMA warp #
859
+ # ------------------------------------------------------------------ #
860
+ if warp_idx == self.mma_warp_id:
861
+ cute.arch.setmaxregister_decrease(self.num_regs_other)
862
+ # --- Retrieve TMEM ptr and make accumulator tensors
863
+ tmem.wait_for_alloc()
864
+ tmem_ptr = tmem.retrieve_ptr(self.acc_dtype)
865
+ # (MMA, MMA_M, MMA_N, STAGE)
866
+ if const_expr(self.compute_dQ):
867
+ tdQtAcc_base = cute.make_tensor(tmem_ptr, tdQtAcc_fake.layout)
868
+ tdQvtAcc_ptr = tmem_ptr + (
869
+ tcgen05.find_tmem_tensor_col_offset(tdQtAcc_base)
870
+ if const_expr(self.compute_dQ)
871
+ else 0
872
+ )
873
+ tdQvtAcc_base = cute.make_tensor(tdQvtAcc_ptr, tdQvtAcc_fake.layout)
874
+
875
+ consumer_state_dS = pipeline.make_pipeline_state(
876
+ pipeline.PipelineUserType.Consumer, stages=self.num_stages_dS
877
+ )
878
+ consumer_state_KV = pipeline.make_pipeline_state(
879
+ pipeline.PipelineUserType.Consumer, stages=self.num_stages_KV
880
+ )
881
+ producer_state_dQ_dQv = pipeline.make_pipeline_state(
882
+ pipeline.PipelineUserType.Producer, self.num_stages_acc
883
+ )
884
+
885
+ while work_tile.is_valid_tile:
886
+ # ---- Get tile coord from tile scheduler ----
887
+ token, cta, _ = work_tile.tile_idx
888
+
889
+ # ---- Set tensor memory buffer for current tile ----
890
+ # (MMA, MMA_M, MMA_N)
891
+ tdQvtAcc = tdQvtAcc_base[(None, None, None, producer_state_dQ_dQv.index)]
892
+ if const_expr(self.compute_dQ):
893
+ tdQtAcc = tdQtAcc_base[(None, None, None, producer_state_dQ_dQv.index)]
894
+
895
+ # ---- Wait for accumulator buffer empty ----
896
+ pipeline_dQ_dQv.producer_acquire(producer_state_dQ_dQv)
897
+
898
+ # ---- Reset the ACCUMULATE field for each tile ----
899
+ tiled_mma_v.set(tcgen05.Field.ACCUMULATE, False)
900
+ if const_expr(self.compute_dQ):
901
+ tiled_mma_k.set(tcgen05.Field.ACCUMULATE, False)
902
+
903
+ # ---- Mma mainloop ----
904
+ for k_tile in cutlass.range_constexpr(k_tile_cnt):
905
+ pipeline_dS.consumer_wait(consumer_state_dS)
906
+ pipeline_KV.consumer_wait(consumer_state_KV)
907
+ dS_stage = consumer_state_dS.index
908
+ KV_stage = consumer_state_KV.index
909
+
910
+ num_kblocks = cute.size(tdQvrdS, mode=[2])
911
+ for kblk_idx in cutlass.range(num_kblocks, unroll_full=True):
912
+ # dQv += dS @ V
913
+ cute.gemm(
914
+ tiled_mma_v,
915
+ tdQvtAcc,
916
+ tdQvrdS[(None, None, kblk_idx, dS_stage)],
917
+ tdQvrV[(None, None, kblk_idx, KV_stage)],
918
+ tdQvtAcc,
919
+ )
920
+ # Enable accumulate on tdQvtAcc after first kblock
921
+ tiled_mma_v.set(tcgen05.Field.ACCUMULATE, True)
922
+
923
+ if const_expr(self.compute_dQ):
924
+ if is_first_cta:
925
+ # dQ += dS @ K
926
+ cute.gemm(
927
+ tiled_mma_k,
928
+ tdQtAcc,
929
+ tdQvrdS[(None, None, kblk_idx, dS_stage)],
930
+ tdQrK[(None, None, kblk_idx, KV_stage)],
931
+ tdQtAcc,
932
+ )
933
+ # Enable accumulate on tdQtAcc after first kblock
934
+ tiled_mma_k.set(tcgen05.Field.ACCUMULATE, True)
935
+
936
+ pipeline_dS.consumer_release(consumer_state_dS)
937
+ pipeline_KV.consumer_release(consumer_state_KV)
938
+ consumer_state_dS.advance()
939
+ consumer_state_KV.advance()
940
+
941
+ pipeline_dQ_dQv.producer_commit(producer_state_dQ_dQv)
942
+ producer_state_dQ_dQv.advance()
943
+
944
+ # ---- Advance to next tile ----
945
+ pipeline_clc.consumer_wait(clc_consumer_state)
946
+ work_tile = tile_sched.get_current_work()
947
+ pipeline_clc.consumer_release(clc_consumer_state)
948
+ clc_consumer_state.advance()
949
+
950
+ pipeline_dQ_dQv.producer_tail(producer_state_dQ_dQv)
951
+
952
+ # ------------------------------------------------------------------ #
953
+ # Epilogue warps #
954
+ # ------------------------------------------------------------------ #
955
+ if warp_idx >= self.epilogue_warp_ids[0] and warp_idx <= self.epilogue_warp_ids[-1]:
956
+ cute.arch.setmaxregister_increase(self.num_regs_epi)
957
+ # ---- Alloc tensor memory buffer ----
958
+ tmem.allocate(self.num_tmem_alloc_cols)
959
+
960
+ # ---- Retrieving tensor memory ptr and make accumulator tensor ----
961
+ tmem.wait_for_alloc()
962
+ tmem_ptr = tmem.retrieve_ptr(self.acc_dtype)
963
+ # (MMA, MMA_M, MMA_N, STAGE)
964
+ if const_expr(self.compute_dQ):
965
+ tdQtAcc_base = cute.make_tensor(tmem_ptr, tdQtAcc_fake.layout)
966
+ tdQvtAcc_ptr = tmem_ptr + (
967
+ tcgen05.find_tmem_tensor_col_offset(tdQtAcc_base)
968
+ if const_expr(self.compute_dQ)
969
+ else 0
970
+ )
971
+ tdQvtAcc_base = cute.make_tensor(tdQvtAcc_ptr, tdQvtAcc_fake.layout)
972
+
973
+ epi_idx = tidx
974
+ # print(f"tdQvtAcc_base.layout = {tdQvtAcc_base.layout}")
975
+ # ---- TMEM -> RMEM -> SMEM -> GMEM copies + partitions ----
976
+ tiled_copy_dQv_t2r, tTR_dQvtAcc_base, tTR_dQvrAcc = (
977
+ self.epilogue_tmem_copy_and_partition(
978
+ epi_idx,
979
+ tdQvtAcc_base,
980
+ tdQvgdQv,
981
+ epi_tile_dQv,
982
+ self.mma_tiler_dQv,
983
+ self.dq_layout,
984
+ self.dq_dtype,
985
+ )
986
+ )
987
+ # print(f"tTR_tdQvtAcc_base.layout = {tTR_dQvtAcc_base.layout}")
988
+ tTR_rdQv = cute.make_rmem_tensor(tTR_dQvrAcc.shape, self.dq_dtype)
989
+ (
990
+ tiled_copy_dQv_r2s,
991
+ tRS_rdQv,
992
+ tRS_sdQv,
993
+ ) = self.epilogue_smem_copy_and_partition(
994
+ self.dq_layout,
995
+ self.dq_dtype,
996
+ tiled_copy_dQv_t2r,
997
+ tTR_rdQv,
998
+ epi_idx,
999
+ sdQv,
1000
+ )
1001
+ bSG_sdQv, bSG_gdQv_partitioned = self.epilogue_gmem_copy_and_partition(
1002
+ tma_atom_dQv,
1003
+ tdQvgdQv,
1004
+ epi_tile_dQv,
1005
+ sdQv,
1006
+ )
1007
+ if const_expr(self.compute_dQ):
1008
+ (tiled_copy_dQ_t2r, tTR_dQtAcc_base, tTR_dQrAcc) = (
1009
+ self.epilogue_tmem_copy_and_partition(
1010
+ epi_idx,
1011
+ tdQtAcc_base,
1012
+ tdQgdQ,
1013
+ epi_tile_dQ,
1014
+ self.mma_tiler_dQ,
1015
+ self.dq_layout,
1016
+ self.dq_dtype,
1017
+ )
1018
+ )
1019
+
1020
+ tTR_rdQ = cute.make_rmem_tensor(tTR_dQrAcc.shape, self.dq_dtype)
1021
+ (
1022
+ tiled_copy_dQ_r2s,
1023
+ tRS_rdQ,
1024
+ tRS_sdQ,
1025
+ ) = self.epilogue_smem_copy_and_partition(
1026
+ self.dq_layout,
1027
+ self.dq_dtype,
1028
+ tiled_copy_dQ_t2r,
1029
+ tTR_rdQ,
1030
+ epi_idx,
1031
+ sdQ,
1032
+ )
1033
+ bSG_sdQ, bSG_gdQ_partitioned = self.epilogue_gmem_copy_and_partition(
1034
+ tma_atom_dQ,
1035
+ tdQgdQ,
1036
+ epi_tile_dQ,
1037
+ sdQ,
1038
+ )
1039
+
1040
+ acc_consumer_state = pipeline.make_pipeline_state(
1041
+ pipeline.PipelineUserType.Consumer, self.num_stages_acc
1042
+ )
1043
+ load_epi_producer_state = pipeline.make_pipeline_state(
1044
+ pipeline.PipelineUserType.Producer, 1
1045
+ )
1046
+
1047
+ epi_producer_group = pipeline.CooperativeGroup(
1048
+ pipeline.Agent.Thread,
1049
+ cute.arch.WARP_SIZE,
1050
+ )
1051
+ pipeline_epi = pipeline.PipelineTmaStore.create(
1052
+ num_stages=self.num_stages_acc,
1053
+ producer_group=epi_producer_group,
1054
+ )
1055
+
1056
+ # ---- Persistent tile scheduling loop for epilogue ----
1057
+ while work_tile.is_valid_tile:
1058
+ # ---- Get current work tile ----
1059
+ token, cta, bid_x = work_tile.tile_idx
1060
+
1061
+ bSG_gdQv = bSG_gdQv_partitioned[
1062
+ (None, None, None, bid_x, cta_rank_in_cluster, token)
1063
+ ]
1064
+ tTR_dQvtAcc = tTR_dQvtAcc_base[
1065
+ (None, None, None, None, None, acc_consumer_state.index)
1066
+ ]
1067
+ tTR_dQvtAcc = cute.group_modes(tTR_dQvtAcc, 3, cute.rank(tTR_dQvtAcc))
1068
+ bSG_gdQv = cute.group_modes(bSG_gdQv, 1, cute.rank(bSG_gdQv))
1069
+
1070
+ subtile_cnt_v = cute.size(tTR_dQvtAcc.shape, mode=[3])
1071
+ epi_subtile_counter_v = 0
1072
+
1073
+ if const_expr(self.compute_dQ):
1074
+ bSG_gdQ = bSG_gdQ_partitioned[(None, None, None, bid_x, cta, token)]
1075
+ tTR_dQtAcc = tTR_dQtAcc_base[
1076
+ (None, None, None, None, None, acc_consumer_state.index)
1077
+ ]
1078
+ tTR_dQtAcc = cute.group_modes(tTR_dQtAcc, 3, cute.rank(tTR_dQtAcc))
1079
+ bSG_gdQ = cute.group_modes(bSG_gdQ, 1, cute.rank(bSG_gdQ))
1080
+
1081
+ subtile_cnt_k = cute.size(tTR_dQtAcc.shape, mode=[3])
1082
+ epi_subtile_counter_k = 0
1083
+
1084
+ pipeline_dQ_dQv.consumer_wait(acc_consumer_state)
1085
+
1086
+ for subtile_idx in cutlass.range(subtile_cnt_v, unroll_full=True):
1087
+ store_dQ = (
1088
+ const_expr(self.compute_dQ)
1089
+ and is_first_cta
1090
+ and (subtile_idx < subtile_cnt_k)
1091
+ )
1092
+
1093
+ if not store_dQ:
1094
+ tTR_dQvtAcc_mn = tTR_dQvtAcc[(None, None, None, subtile_idx)]
1095
+ cute.copy(tiled_copy_dQv_t2r, tTR_dQvtAcc_mn, tTR_dQvrAcc)
1096
+ cute.arch.fence_view_async_tmem_load()
1097
+
1098
+ # convert to output dtype
1099
+ tRS_rdQv.store(
1100
+ tiled_copy_dQv_r2s.retile(tTR_dQvrAcc).load().to(self.dq_dtype)
1101
+ )
1102
+
1103
+ epi_buffer = epi_subtile_counter_v % self.num_stages_acc
1104
+ cute.copy(
1105
+ tiled_copy_dQv_r2s, tRS_rdQv, tRS_sdQv[(None, None, None, epi_buffer)]
1106
+ )
1107
+ cute.arch.fence_proxy("async.shared", space="cta")
1108
+ self.epilog_sync_barrier.arrive_and_wait()
1109
+
1110
+ if warp_idx == self.epilogue_warp_ids[0]:
1111
+ cute.copy(
1112
+ tma_atom_dQv,
1113
+ bSG_sdQv[(None, epi_buffer)],
1114
+ bSG_gdQv[(None, subtile_idx)],
1115
+ )
1116
+ pipeline_epi.producer_commit()
1117
+ pipeline_epi.producer_acquire()
1118
+
1119
+ self.epilog_sync_barrier.arrive_and_wait()
1120
+ epi_subtile_counter_v += 1
1121
+ elif const_expr(self.compute_dQ):
1122
+ tTR_dQtAcc_mn = tTR_dQtAcc[(None, None, None, subtile_idx)]
1123
+ tTR_dQvtAcc_mn = tTR_dQvtAcc[(None, None, None, subtile_idx)]
1124
+ cute.copy(tiled_copy_dQ_t2r, tTR_dQtAcc_mn, tTR_dQrAcc)
1125
+ cute.copy(tiled_copy_dQv_t2r, tTR_dQvtAcc_mn, tTR_dQvrAcc)
1126
+
1127
+ # convert to output dtype
1128
+ tRS_rdQ.store(tiled_copy_dQ_r2s.retile(tTR_dQrAcc).load().to(self.dq_dtype))
1129
+ tRS_rdQv.store(
1130
+ tiled_copy_dQv_r2s.retile(tTR_dQvrAcc).load().to(self.dq_dtype)
1131
+ )
1132
+
1133
+ epi_buffer = epi_subtile_counter_v % self.num_stages_acc
1134
+ epi_buffer_k = epi_subtile_counter_k % self.num_stages_acc
1135
+ cute.copy(
1136
+ tiled_copy_dQv_r2s, tRS_rdQv, tRS_sdQv[(None, None, None, epi_buffer)]
1137
+ )
1138
+ cute.copy(
1139
+ tiled_copy_dQ_r2s, tRS_rdQ, tRS_sdQ[(None, None, None, epi_buffer_k)]
1140
+ )
1141
+ cute.arch.fence_proxy("async.shared", space="cta")
1142
+ self.epilog_sync_barrier.arrive_and_wait()
1143
+
1144
+ if warp_idx == self.epilogue_warp_ids[0]:
1145
+ cute.copy(
1146
+ tma_atom_dQv,
1147
+ bSG_sdQv[(None, epi_buffer)],
1148
+ bSG_gdQv[(None, subtile_idx)],
1149
+ )
1150
+ cute.copy(
1151
+ tma_atom_dQ,
1152
+ bSG_sdQ[(None, epi_buffer_k)],
1153
+ bSG_gdQ[(None, subtile_idx)],
1154
+ )
1155
+ pipeline_epi.producer_commit()
1156
+ pipeline_epi.producer_acquire()
1157
+
1158
+ self.epilog_sync_barrier.arrive_and_wait()
1159
+ epi_subtile_counter_v += 1
1160
+ epi_subtile_counter_k += 1
1161
+
1162
+ if const_expr(self.overlap_kv_epi):
1163
+ pipeline_load_kv_epi.producer_acquire(load_epi_producer_state)
1164
+ with cute.arch.elect_one():
1165
+ pipeline_load_kv_epi.producer_commit(load_epi_producer_state)
1166
+ load_epi_producer_state.advance()
1167
+
1168
+ with cute.arch.elect_one():
1169
+ pipeline_dQ_dQv.consumer_release(acc_consumer_state)
1170
+ acc_consumer_state.advance()
1171
+
1172
+ # ---- Advance to next tile ----
1173
+ pipeline_clc.consumer_wait(clc_consumer_state)
1174
+ work_tile = tile_sched.get_current_work()
1175
+ pipeline_clc.consumer_release(clc_consumer_state)
1176
+ clc_consumer_state.advance()
1177
+
1178
+ # ---- Dealloc the tensor memory buffer ----
1179
+ tmem.relinquish_alloc_permit()
1180
+ self.epilog_sync_barrier.arrive_and_wait()
1181
+ tmem.free(tmem_ptr)
1182
+ pipeline_epi.producer_tail()
1183
+
1184
+ def epilogue_tmem_copy_and_partition(
1185
+ self,
1186
+ tidx: cutlass.Int32,
1187
+ tAcc: cute.Tensor,
1188
+ gC_mnl: cute.Tensor,
1189
+ epi_tile: cute.Tile,
1190
+ mma_tiler_mnk,
1191
+ c_layout,
1192
+ c_dtype,
1193
+ ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]:
1194
+ copy_atom_t2r = utils.sm100.get_tmem_load_op(
1195
+ mma_tiler_mnk,
1196
+ c_layout,
1197
+ c_dtype,
1198
+ self.acc_dtype,
1199
+ epi_tile,
1200
+ use_2cta_instrs=False,
1201
+ )
1202
+
1203
+ # (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, STAGE)
1204
+ tAcc_epi = cute.flat_divide(tAcc[((None, None), 0, 0, None)], epi_tile)
1205
+
1206
+ # (EPI_TILE_M, EPI_TILE_N)
1207
+ tiled_copy_t2r = tcgen05.make_tmem_copy(copy_atom_t2r, tAcc_epi[(None, None, 0, 0, 0)])
1208
+ thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
1209
+
1210
+ # (T2R, T2R_M, T2R_N, EPI_M, EPI_N, STAGE)
1211
+ tTR_tAcc = thr_copy_t2r.partition_S(tAcc_epi)
1212
+
1213
+ # (EPI_TILE_M, EPI_TILE_N, EPI_M, EPI_N, RestM, RestN, RestL)
1214
+ gC_mnl_epi = cute.flat_divide(gC_mnl[((None, None), 0, 0, None, None, None)], epi_tile)
1215
+ # (T2R, T2R_M, T2R_N, EPI_M, EPI_N, RestM, RestN, RestL)
1216
+ tTR_gC = thr_copy_t2r.partition_D(gC_mnl_epi)
1217
+
1218
+ # (T2R, T2R_M, T2R_N)
1219
+ rAcc_shape = tTR_gC[(None, None, None, 0, 0, 0, 0, 0)].shape
1220
+ tTR_rAcc = cute.make_rmem_tensor(rAcc_shape, self.acc_dtype)
1221
+
1222
+ return (tiled_copy_t2r, tTR_tAcc, tTR_rAcc)
1223
+
1224
+ def epilogue_smem_copy_and_partition(
1225
+ self,
1226
+ c_layout,
1227
+ c_dtype: Type[cutlass.Numeric],
1228
+ tiled_copy_t2r: cute.TiledCopy,
1229
+ tTR_rC: cute.Tensor,
1230
+ tidx: cutlass.Int32,
1231
+ sC: cute.Tensor,
1232
+ ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]:
1233
+ copy_atom_r2s = utils.sm100.get_smem_store_op(
1234
+ c_layout, c_dtype, self.acc_dtype, tiled_copy_t2r
1235
+ )
1236
+ tiled_copy_r2s = cute.make_tiled_copy_D(copy_atom_r2s, tiled_copy_t2r)
1237
+ # (R2S, R2S_M, R2S_N, PIPE_D)
1238
+ thr_copy_r2s = tiled_copy_r2s.get_slice(tidx)
1239
+ tRS_sC = thr_copy_r2s.partition_D(sC)
1240
+ # (R2S, R2S_M, R2S_N)
1241
+ tRS_rC = tiled_copy_r2s.retile(tTR_rC)
1242
+ return tiled_copy_r2s, tRS_rC, tRS_sC
1243
+
1244
+ def epilogue_gmem_copy_and_partition(
1245
+ self,
1246
+ tma_atom,
1247
+ gC,
1248
+ epi_tile,
1249
+ sC,
1250
+ ) -> Tuple[cute.Tensor, cute.Tensor]:
1251
+ gC_epi = cute.flat_divide(gC[((None, None), 0, 0, None, None, None)], epi_tile)
1252
+
1253
+ sC_for_tma_partition = cute.group_modes(sC, 0, 2)
1254
+
1255
+ gC_for_tma_partition = cute.group_modes(gC_epi, 0, 2)
1256
+
1257
+ bSG_sC, bSG_gC = cpasync.tma_partition(
1258
+ tma_atom,
1259
+ 0,
1260
+ cute.make_layout(1),
1261
+ sC_for_tma_partition,
1262
+ gC_for_tma_partition,
1263
+ )
1264
+
1265
+ return bSG_sC, bSG_gC
1266
+
1267
+ @cute.jit
1268
+ def find_batch_from_q(
1269
+ self,
1270
+ token: Int32,
1271
+ seqlen_q_divmod: FastDivmodDivisor,
1272
+ mCuSeqlensQ: Optional[cute.Tensor],
1273
+ ) -> Int32:
1274
+ """Find batch index from q token (binary search for varlen, divmod otherwise)"""
1275
+ if const_expr(mCuSeqlensQ is not None):
1276
+ return get_batch_from_cu_tensor(token, mCuSeqlensQ)
1277
+ else:
1278
+ batch, _ = divmod(token, seqlen_q_divmod)
1279
+ return batch
1280
+
1281
+ @staticmethod
1282
+ def _compute_grid(c, cta_tile_shape_mnk, cluster_shape_mn):
1283
+ c_shape = cute.slice_(cta_tile_shape_mnk, (None, None, 0))
1284
+ gc = cute.zipped_divide(c, tiler=c_shape)
1285
+ c_logical_shape = gc[(0, (None, None, None))].shape
1286
+
1287
+ num_ctas_mnl = (
1288
+ cute.size(c_logical_shape[0]),
1289
+ cute.size(c_logical_shape[1]),
1290
+ cute.size(c_logical_shape[2]),
1291
+ )
1292
+
1293
+ tile_sched_params = utils.ClcDynamicPersistentTileSchedulerParams(
1294
+ num_ctas_mnl, (*cluster_shape_mn, 1)
1295
+ )
1296
+ grid = utils.ClcDynamicPersistentTileScheduler.get_grid_shape(tile_sched_params)
1297
+ return tile_sched_params, grid
build/torch-cuda/flash_bwd_mla_sm100.py ADDED
@@ -0,0 +1,2135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026, Colfax International.
2
+
3
+ import math
4
+ from functools import partial
5
+ from typing import Callable, Optional
6
+
7
+ import cuda.bindings.driver as cuda
8
+
9
+ import cutlass
10
+ import cutlass.cute as cute
11
+ from cutlass import Float32, Int64, Int32, Boolean, const_expr
12
+ import cutlass.pipeline as pipeline
13
+ from cutlass.cute.nvgpu import cpasync, tcgen05
14
+ import cutlass.utils.blackwell_helpers as sm100_utils
15
+ from cutlass.utils import ClcDynamicPersistentTileScheduler
16
+
17
+ from .quack import copy_utils, layout_utils
18
+
19
+ from .pack_gqa import pack_gqa_layout
20
+ from .seqlen_info import SeqlenInfoQK
21
+ from .block_info import BlockInfo
22
+ from . import blackwell_helpers as fa_sm100_utils
23
+ from .tile_scheduler import (
24
+ SchedulerState,
25
+ SchedulingMode,
26
+ TileSchedulerArguments,
27
+ TileSchedulerProtocol,
28
+ SingleTileScheduler,
29
+ SingleTileLPTScheduler,
30
+ SingleTileVarlenScheduler,
31
+ ParamsBase,
32
+ )
33
+ from .fa_logging import fa_log, fa_printf
34
+ from .utils import smid, elem_pointer, get_batch_from_cu_tensor
35
+ from .copy_utils import tiled_copy_2d, atomic_add_fp32x4
36
+
37
+ from .topk_gather_kv import CpasyncGatherKVManager
38
+
39
+
40
+ from .named_barrier import NamedBarrierBwdSm100_MLA2CTA
41
+
42
+
43
+ class FlashAttentionSparseMLABackwardSm100:
44
+ def __init__(
45
+ self,
46
+ is_causal: bool = False,
47
+ topk_length: int = 2048,
48
+ qhead_per_kvhead: int = 1,
49
+ nheads_kv: int = 1,
50
+ hdim: int = 64,
51
+ hdimv: int = 512,
52
+ has_seqused_q: bool = False,
53
+ disable_bitmask: bool = False,
54
+ use_clc_scheduler: bool = True,
55
+ ):
56
+ use_cpasync_load_KV = True
57
+ self.is_causal = is_causal
58
+ self.is_local = False
59
+ self.pack_gqa = True
60
+ self.qhead_per_kvhead = qhead_per_kvhead
61
+ self.nheads_kv = nheads_kv
62
+ self.has_seqused_q = has_seqused_q
63
+ self.use_tma_O = True
64
+ self.use_cpasync_load_KV = True
65
+ self.use_tma_KV = False
66
+ self.topk_length = topk_length
67
+ self.is_topk_gather = True
68
+ assert qhead_per_kvhead == 128 or qhead_per_kvhead == 64
69
+
70
+ # user-provided option if topk indices guaranteed in bounds
71
+ self.disable_bitmask = disable_bitmask
72
+
73
+ # ==== tile scheduler ====
74
+ self.static_persistent = False
75
+ self.use_clc_scheduler = use_clc_scheduler
76
+ self.sched_stages = 1
77
+ self.scheduling_mode = (
78
+ SchedulingMode.CLC if self.use_clc_scheduler else SchedulingMode.STATIC
79
+ )
80
+
81
+ if const_expr(has_seqused_q):
82
+ self.TileScheduler = SingleTileVarlenScheduler
83
+ elif self.use_clc_scheduler:
84
+ self.TileScheduler = SingleTileLPTScheduler
85
+ else:
86
+ self.TileScheduler = SingleTileScheduler
87
+
88
+ fa_log(
89
+ 1,
90
+ f"TileScheduler={self.TileScheduler.__name__}, scheduling_mode={self.scheduling_mode.name}",
91
+ )
92
+
93
+ # ==== thread info ====
94
+ self.num_softmax_threads = 128
95
+ self.num_epilogue_threads = 128
96
+ self.num_load_threads = 32
97
+ self.num_mma_threads = 32
98
+ self.num_empty_threads = 0
99
+ self.num_relay_threads = 32
100
+ self.num_cpasync_load_threads = 128
101
+ self.num_threads = 512
102
+ self.num_warps = self.num_threads // 32
103
+ self.softmax_warp_indices = (0, 1, 2, 3)
104
+ self.epilogue_warp_indices = (4, 5, 6, 7)
105
+ self.load_warp_id = 8
106
+ self.mma_warp_id = 9
107
+ self.clc_scheduler_warp_id = 10
108
+ self.relay_warp_id = 11
109
+ self.cpasync_load_warp_indices = (12, 13, 14, 15)
110
+ self.empty_warp_ids = ()
111
+
112
+ # ==== register usage ====
113
+ assert self.num_warps == 16
114
+
115
+ self.num_regs_load = 128
116
+ self.num_regs_mma = 128
117
+ self.num_regs_softmax = 128
118
+ self.num_regs_epilogue = 128
119
+ self.num_regs_cpasync = 128
120
+ self.num_regs_other = 128
121
+
122
+ # self.num_regs_load = 128 - 32
123
+ # self.num_regs_mma = 128 - 32
124
+ # self.num_regs_softmax = 128 + 32
125
+ # self.num_regs_epilogue = 128 + 32
126
+ # self.num_regs_cpasync = 128 - 32
127
+ # self.num_regs_other = 48
128
+
129
+ self.num_regs_per_thread = 128
130
+ self.num_regs_total = 512
131
+
132
+ assert (
133
+ self.num_regs_mma
134
+ + self.num_regs_softmax
135
+ + self.num_regs_epilogue
136
+ + self.num_regs_cpasync
137
+ <= self.num_regs_total
138
+ )
139
+
140
+ # ==== 2cta info ====
141
+ self.use_2cta_instrs = True
142
+ self.cta_group = tcgen05.CtaGroup.TWO
143
+ self.cta_group_size = 2
144
+ self.cluster_shape_mn = (2, 1)
145
+ self.cluster_shape_mnk = (2, 1, 1)
146
+
147
+ # ==== problem shape info ====
148
+ self.hdim = hdim # ignored
149
+ self.hdimv = hdimv
150
+ self.tile_m = qhead_per_kvhead
151
+ self.tile_n = 64
152
+ self.cta_tiler_mn = (self.tile_m // self.cta_group_size, self.tile_n)
153
+ self.cluster_tile_n = self.cta_group_size * self.tile_n
154
+ self.num_hdimv_splits = 2 # split hdimv in half for our Qv @ V^T and P @ V mmas.
155
+
156
+ self.tile_P = (self.tile_m, self.tile_n)
157
+ self.tile_Pt = (self.tile_n, self.tile_m)
158
+ self.tile_dS = (self.tile_m, self.tile_n)
159
+ self.tile_dSt = (self.tile_n, self.tile_m)
160
+ self.tile_dV = (self.tile_n, 32)
161
+
162
+ # ==== MMA info ====
163
+ # dP.T = V @ dO.T , N x M x dv
164
+ # dV += P.T @ dO , N x dv x M
165
+ # dV += dS.T @ Qv , N x dv x M
166
+ self.mma_tiler_VdO = (
167
+ self.cluster_tile_n,
168
+ self.tile_m,
169
+ self.hdimv // self.num_hdimv_splits,
170
+ )
171
+ self.mma_tiler_PtdOt = (
172
+ self.cluster_tile_n,
173
+ self.hdimv // self.num_hdimv_splits,
174
+ self.tile_m,
175
+ )
176
+ self.mma_tiler_dStQvt = (
177
+ self.cluster_tile_n,
178
+ self.hdimv // self.num_hdimv_splits,
179
+ self.tile_m,
180
+ )
181
+ # note: store P.T, dS.T as tile_n major (i.e., as P and dS)
182
+ self.major_mode_V = tcgen05.OperandMajorMode.K
183
+ self.major_mode_dO = tcgen05.OperandMajorMode.K
184
+ self.major_mode_Pt = tcgen05.OperandMajorMode.MN
185
+ self.major_mode_dOt = tcgen05.OperandMajorMode.MN
186
+ self.major_mode_dSt = tcgen05.OperandMajorMode.MN
187
+ self.major_mode_Qvt = tcgen05.OperandMajorMode.MN
188
+ self.operand_source_V = tcgen05.OperandSource.SMEM
189
+ self.operand_source_Pt = tcgen05.OperandSource.SMEM
190
+ self.operand_source_dSt = tcgen05.OperandSource.SMEM
191
+
192
+ # ==== pipeline info ====
193
+ # stationary: dOi
194
+ # mainloop:
195
+ # *) P, scaleP => Pt
196
+ # *) dSt
197
+ # *) Vi, i = {0, 1}
198
+ # *) dOti => Qvi => dVi, i = {0, 1}
199
+
200
+ # redundant names for ease-of-use
201
+ self.num_stages_V = 2
202
+ self.num_stages_dO = 2
203
+ self.num_stages_P = 1
204
+ self.num_stages_Pt = 1
205
+ self.num_stages_dS = 1
206
+ self.num_stages_dSt = 1
207
+ self.num_stages_dOt = 2
208
+ self.num_stages_Qv = 2
209
+ self.num_stages_Qvt = 2
210
+
211
+ self.num_stages_dP = 1
212
+ self.num_stages_dPt = 1
213
+ self.num_stages_dV = 2 # == hdimv splits, for Umma <-> Async
214
+ self.num_epi_stages_dV = 8 # == 2 splits x 4 slots/split
215
+
216
+ self.num_stages_scaleP = 1
217
+ self.num_stages_dPsum = 1
218
+
219
+ # ==== dtype info ====
220
+ self.dtype_acc = Float32
221
+
222
+ # ==== TMEM info ====
223
+ SM100_TMEM_CAPACITY_COLUMNS = 512
224
+ self.tmem_alloc_cols = SM100_TMEM_CAPACITY_COLUMNS
225
+ self.tmem_cols_dP = self.tile_m // self.cta_group_size
226
+ self.tmem_cols_dVi = (self.hdimv // self.num_hdimv_splits) // self.cta_group_size
227
+ self.tmem_offset_dV0 = 0
228
+ self.tmem_offset_dV1 = self.tmem_offset_dV0 + self.tmem_cols_dVi
229
+ self.tmem_offsets_dV = [self.tmem_offset_dV0, self.tmem_offset_dV1]
230
+ self.tmem_offset_dP = self.tmem_offset_dV1 + self.tmem_cols_dVi
231
+ self.total_tmem = self.tmem_offset_dP + self.tmem_cols_dP
232
+ assert self.total_tmem <= self.tmem_alloc_cols, (
233
+ f"Total TMEM columns allocated {self.total_tmem} exceeds capacity {self.tmem_alloc_cols}"
234
+ )
235
+
236
+ def _get_shared_storage_cls(self):
237
+ self.buffer_align_bytes = 1024
238
+
239
+ def smem_struct_align(dtype, staged_layout):
240
+ return cute.struct.Align[
241
+ cute.struct.MemRange[dtype, cute.cosize(staged_layout)],
242
+ self.buffer_align_bytes,
243
+ ]
244
+
245
+ def mbar_struct(num_stages):
246
+ return cute.struct.MemRange[Int64, 2 * num_stages]
247
+
248
+ # sV, sdO, sP = sPt, sdSt = sdS, sdOt = sQvt = sdV
249
+ (
250
+ sV_struct,
251
+ sdO_struct,
252
+ sP_struct,
253
+ sdS_struct,
254
+ sQvt_struct,
255
+ sScaleP_struct,
256
+ sdPsum_struct,
257
+ ) = (
258
+ smem_struct_align(dtype, layout)
259
+ for dtype, layout in [
260
+ (self.dtype, self.sV_layout_staged),
261
+ (self.dtype, self.sdO_layout_staged),
262
+ (self.dtype, self.sPt_layout_staged),
263
+ (self.dtype, self.sdSt_layout_staged),
264
+ (self.dtype, self.sQvt_layout_staged),
265
+ (self.dtype_scale, self.sScaleP_layout_staged),
266
+ (self.dtype_scale, self.sdPsum_layout_staged),
267
+ ]
268
+ )
269
+
270
+ (
271
+ mbar_ptr_V_struct, # load V
272
+ mbar_ptr_dO_struct, # load dO
273
+ mbar_ptr_dOt_Qvt_struct, # load dOt => Qvt
274
+ mbar_ptr_dSt_struct, # store dS
275
+ mbar_ptr_P_struct, # load P
276
+ mbar_ptr_Pt_struct, # store Pt
277
+ mbar_ptr_dPt_struct, # dP mma
278
+ mbar_ptr_dV_struct, # dV mma
279
+ mbar_ptr_scaleP_struct, # load scaleP
280
+ mbar_ptr_dPsum_struct, # load dPsum
281
+ ) = (
282
+ mbar_struct(n)
283
+ for n in [
284
+ self.num_stages_V,
285
+ self.num_stages_dO,
286
+ self.num_stages_Qvt,
287
+ self.num_stages_dSt,
288
+ self.num_stages_P,
289
+ self.num_stages_Pt,
290
+ self.num_stages_dPt,
291
+ self.num_stages_dV,
292
+ self.num_stages_scaleP,
293
+ self.num_stages_dPsum,
294
+ ]
295
+ )
296
+ tmem_dealloc_mbar_struct = Int64
297
+ tmem_holding_buf_struct = Int32
298
+
299
+ self.sched_stages = 1
300
+ clc_response_size = self.sched_stages * 4 if self.use_clc_scheduler else 0
301
+ clc_mbar_size = self.sched_stages * 2 if self.use_clc_scheduler else 0
302
+
303
+ @cute.struct
304
+ class SharedStorage:
305
+ mbar_ptr_V: mbar_ptr_V_struct
306
+ mbar_ptr_V_cpasync: mbar_ptr_V_struct
307
+ mbar_ptr_dO: mbar_ptr_dO_struct
308
+ mbar_ptr_dOt_Qvt: mbar_ptr_dOt_Qvt_struct
309
+ mbar_ptr_P: mbar_ptr_P_struct
310
+ mbar_ptr_Pt: mbar_ptr_Pt_struct
311
+ mbar_ptr_dSt: mbar_ptr_dSt_struct
312
+ mbar_ptr_dPt: mbar_ptr_dPt_struct
313
+ mbar_ptr_dV: mbar_ptr_dV_struct
314
+ mbar_ptr_dV_epi: mbar_ptr_dV_struct
315
+ mbar_ptr_scaleP: mbar_ptr_scaleP_struct
316
+ mbar_ptr_dPsum: mbar_ptr_dPsum_struct
317
+ tmem_dealloc_mbar: tmem_dealloc_mbar_struct
318
+ tmem_holding_buf: tmem_holding_buf_struct
319
+ clc_mbar_ptr: cute.struct.MemRange[cutlass.Int64, clc_mbar_size]
320
+ clc_response: cute.struct.MemRange[Int32, clc_response_size]
321
+
322
+ sScaleP: sScaleP_struct
323
+ sdPsum: sdPsum_struct
324
+ sV: sV_struct
325
+ sdO: sdO_struct
326
+ sP: sP_struct
327
+ sdS: sdS_struct
328
+ sQv: sQvt_struct
329
+
330
+ # print("smem bytes = ", SharedStorage.size_in_bytes())
331
+
332
+ return SharedStorage
333
+
334
+ # fmt: off
335
+ @cute.jit
336
+ def __call__(
337
+ self,
338
+ mdO: cute.Tensor, # (b, s_q, h, dv) or (total_q, h, dv) if there is cu_seqlens_q
339
+ mV: cute.Tensor, # (b_k, s_k, h_k, dv) or (total_k, h_k, dv) if there is cu_seqlens_k
340
+ mQv: cute.Tensor, # == mdO
341
+ mP: cute.Tensor, # (b, s_q, h, topk) or (total_q, h, topk)
342
+ mdV: cute.Tensor, # == mV
343
+ mdS: cute.Tensor, # == mP
344
+ mIndexTopk: cute.Tensor, # (b, s_q, topk) or (total_q, topk) if there is cu_seqlens_q
345
+ softmax_scale: Float32,
346
+ mScaleP: Optional[cute.Tensor] = None, # (b, s_q, topk//128, h) or (total_q, topk//128, h)
347
+ mdPsum: Optional[cute.Tensor] = None, # (b, s_q, h) or (total_q, h) if there is cu_seqlens_q
348
+ mCuSeqlensQ: Optional[cute.Tensor] = None, # (b + 1)
349
+ mCuSeqlensK: Optional[cute.Tensor] = None, # (b + 1)
350
+ mSeqUsedQ: Optional[cute.Tensor] = None, # (b)
351
+ mSeqUsedK: Optional[cute.Tensor] = None, # (b)
352
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
353
+ stream: cuda.CUstream = None,
354
+ ):
355
+ # fmt: on
356
+ # ==== dtype info ====
357
+ self.dtype = mdO.element_type
358
+ self.dtype_dV = mdV.element_type
359
+ self.dtype_scale = Float32
360
+ self.dtype_index = mIndexTopk.element_type
361
+ assert self.dtype.width == 16
362
+ assert self.dtype_dV.width == 32
363
+ assert self.dtype_index == Int32
364
+ if const_expr(mScaleP is not None):
365
+ assert mScaleP.element_type == self.dtype_scale
366
+ if const_expr(mdPsum is not None):
367
+ assert mdPsum.element_type == self.dtype_scale
368
+
369
+ # ==== Prepare Tensors ====
370
+ new_stride = lambda mX: (
371
+ *(cute.assume(s, divby=128 // mX.element_type.width) for s in mX.stride[:-1]),
372
+ mX.stride[-1],
373
+ )
374
+ mQv, mV, mdV, mdO, mP, mdS, mScaleP, mdPsum = [
375
+ cute.make_tensor(mX.iterator, cute.make_layout(mX.shape, stride=new_stride(mX)))
376
+ if mX is not None
377
+ else None
378
+ for mX in (mQv, mV, mdV, mdO, mP, mdS, mScaleP, mdPsum)
379
+ ]
380
+ # (b, s, h, d) -> (s, d, h, b) or
381
+ # (total, h, d) -> (total, d, h)
382
+ QO_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 2, 1]
383
+ KV_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensK is None) else [0, 2, 1]
384
+ mQv, mdO, mP, mdS = [
385
+ cute.make_tensor(mX.iterator, cute.select(mX.layout, mode=QO_layout_transpose))
386
+ if mX is not None
387
+ else None
388
+ for mX in (mQv, mdO, mP, mdS)
389
+ ]
390
+ mV, mdV = [
391
+ cute.make_tensor(mX.iterator, cute.select(mX.layout, mode=KV_layout_transpose))
392
+ if mX is not None
393
+ else None
394
+ for mX in (mV, mdV)
395
+ ]
396
+
397
+ # (b, s, topk//128, h) -> (s, topk//128, h, b) or
398
+ # (total, topk//128, h) -> (total, topk//128, h)
399
+ ScaleP_layout_transpose = [1, 2, 3, 0] if const_expr(mCuSeqlensQ is None) else [0, 1, 2]
400
+ mScaleP = cute.make_tensor(
401
+ mScaleP.iterator, cute.select(mScaleP.layout, mode=ScaleP_layout_transpose)
402
+ )
403
+
404
+ # (b, s, h) -> (s, h, b) or
405
+ # (total, h) -> (total, h)
406
+ dPsum_layout_transpose = [1, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 1]
407
+ mdPsum = cute.make_tensor(
408
+ mdPsum.iterator, cute.select(mdPsum.layout, mode=dPsum_layout_transpose)
409
+ )
410
+
411
+ # (b, s_q, topk) -> (topk, s_q, b) or (total_q, topk) -> (topk, total_q)
412
+ topk_layout_transpose = [2, 1, 0] if const_expr(mCuSeqlensQ is None) else [1, 0]
413
+ mIndexTopk = cute.make_tensor(
414
+ mIndexTopk.iterator, cute.select(mIndexTopk.layout, mode=topk_layout_transpose)
415
+ )
416
+ topk_length_dynamic = mIndexTopk.shape[0]
417
+
418
+ if const_expr(self.pack_gqa):
419
+ mQv, mdO, mP, mdS, mScaleP = [
420
+ pack_gqa_layout(mX, self.qhead_per_kvhead, self.nheads_kv, head_idx=2)
421
+ if mX is not None
422
+ else None
423
+ for mX in (mQv, mdO, mP, mdS, mScaleP)
424
+ ]
425
+ if const_expr(mdPsum is not None):
426
+ mdPsum = pack_gqa_layout(mdPsum, self.qhead_per_kvhead, self.nheads_kv, head_idx=1)
427
+
428
+ # ((h/h_k, s_q), dv, h_k, b) -> (dv, (h/h_k, s_q), h_k, b)
429
+ # or ((h/h_k, total_q), dv, h_k) -> (dv, (h/h_k, total_q), h_k)
430
+ mma_operand_layout_transpose = (
431
+ [1, 0, 2, 3] if const_expr(mCuSeqlensQ is None) else [1, 0, 2]
432
+ )
433
+ mQvt, mdOt = [
434
+ cute.make_tensor(mX.iterator, cute.select(mX.layout, mode=mma_operand_layout_transpose))
435
+ for mX in (mQv, mdO)
436
+ ]
437
+
438
+ # fmt: off
439
+ # ==== Prepare MMAs ====
440
+ # (local_var, dtype_a, major_a, major_b, mma_tiler, operand_source_a)
441
+ _mma_specs = [
442
+ ("tiled_mma_VdO", self.dtype, self.major_mode_V, self.major_mode_dO, self.mma_tiler_VdO, self.operand_source_V),
443
+ ("tiled_mma_PtdOt", self.dtype, self.major_mode_Pt, self.major_mode_dOt, self.mma_tiler_PtdOt, self.operand_source_Pt),
444
+ ("tiled_mma_dStQvt", self.dtype, self.major_mode_dSt, self.major_mode_Qvt, self.mma_tiler_dStQvt, self.operand_source_dSt),
445
+ ]
446
+ tiled_mma_VdO, tiled_mma_PtdOt, tiled_mma_dStQvt = (
447
+ sm100_utils.make_trivial_tiled_mma(
448
+ dtype_a, major_a, major_b, self.dtype_acc, self.cta_group, mma_tiler[:2], operand_source_a,
449
+ )
450
+ for _, dtype_a, major_a, major_b, mma_tiler, operand_source_a in _mma_specs
451
+ )
452
+
453
+ # ==== Prepare SMEM layouts and TMAs ====
454
+ # (attr, make_fn, tiled_mma, mma_tiler, dtype, num_stages)
455
+ _smem_layout_specs = [
456
+ ("sV_layout", sm100_utils.make_smem_layout_a, tiled_mma_VdO, self.mma_tiler_VdO, self.dtype, self.num_stages_V),
457
+ ("sdO_layout", sm100_utils.make_smem_layout_b, tiled_mma_VdO, self.mma_tiler_VdO, self.dtype, self.num_stages_dO),
458
+ ("sPt_layout", sm100_utils.make_smem_layout_a, tiled_mma_PtdOt, self.mma_tiler_PtdOt, self.dtype, self.num_stages_Pt),
459
+ ("sdOt_layout", sm100_utils.make_smem_layout_b, tiled_mma_PtdOt, self.mma_tiler_PtdOt, self.dtype, self.num_stages_dOt),
460
+ ("sdSt_layout", sm100_utils.make_smem_layout_a, tiled_mma_dStQvt, self.mma_tiler_dStQvt, self.dtype, self.num_stages_dSt),
461
+ ("sQvt_layout", sm100_utils.make_smem_layout_b, tiled_mma_dStQvt, self.mma_tiler_dStQvt, self.dtype, self.num_stages_Qvt),
462
+ ]
463
+ for attr, make_fn, tiled_mma, mma_tiler, dtype, num_stages in _smem_layout_specs:
464
+ ab_kwarg = "a_dtype" if make_fn is sm100_utils.make_smem_layout_a else "b_dtype"
465
+ staged = make_fn(
466
+ tiled_mma=tiled_mma,
467
+ mma_tiler_mnk=mma_tiler,
468
+ num_stages=num_stages,
469
+ **{ab_kwarg: dtype},
470
+ )
471
+ setattr(self, f"{attr}_staged", staged)
472
+ setattr(self, attr, cute.select(staged, mode=[0, 1, 2]))
473
+
474
+ # Prepare additional SMEM load layouts
475
+ self.P_layout_major = cutlass.utils.LayoutEnum.from_tensor(mP)
476
+ self.sP_layout_staged = sm100_utils.make_smem_layout_epi(
477
+ self.dtype, self.P_layout_major, self.tile_P, self.num_stages_P
478
+ )
479
+ self.sP_layout = cute.select(self.sP_layout_staged, mode=[0, 1])
480
+ self.sScaleP_layout_staged = cute.make_layout((self.tile_m, self.num_stages_scaleP))
481
+ self.sScaleP_layout = cute.select(self.sScaleP_layout_staged, mode=[0])
482
+ self.sdPsum_layout_staged = cute.make_layout((self.tile_m, self.num_stages_dPsum))
483
+ self.sdPsum_layout = cute.select(self.sdPsum_layout_staged, mode=[0])
484
+
485
+ # ==== TMA load ====
486
+ for attr, dtype, layout in [
487
+ ("tma_copy_bytes_V", self.dtype, self.sV_layout),
488
+ ("tma_copy_bytes_dO", self.dtype, self.sdO_layout),
489
+ ("tma_copy_bytes_dOt", self.dtype, self.sdOt_layout),
490
+ ("tma_copy_bytes_Qvt", self.dtype, self.sQvt_layout),
491
+ ]:
492
+ setattr(self, attr, cute.size_in_bytes(dtype, layout) * self.cta_group_size)
493
+
494
+ assert self.tma_copy_bytes_dOt == self.tma_copy_bytes_Qvt
495
+ self.tma_copy_bytes_P = cute.size_in_bytes(self.dtype, self.sP_layout)
496
+ self.tma_copy_bytes_scaleP = cute.size_in_bytes(self.dtype_scale, self.sScaleP_layout)
497
+ self.tma_copy_bytes_dPsum = cute.size_in_bytes(self.dtype_scale, self.sdPsum_layout)
498
+
499
+ tma_load_op = cpasync.CopyBulkTensorTileG2SOp(self.cta_group)
500
+ cta_layout_vmnk = cute.tiled_divide(
501
+ cute.make_layout(self.cluster_shape_mnk), (tiled_mma_VdO.thr_id.shape,)
502
+ )
503
+ cta_shape = cta_layout_vmnk.shape
504
+
505
+ def make_tma(make_fn, mX, smem_layout, mma_tiler, tiled_mma):
506
+ return make_fn(tma_load_op, mX, smem_layout, mma_tiler, tiled_mma, cta_shape)
507
+
508
+ A, B = cute.nvgpu.make_tiled_tma_atom_A, cute.nvgpu.make_tiled_tma_atom_B
509
+
510
+ # (atom_name, tensor_name, make_fn, m, smem_layout, mma_tiler, tiled_mma)
511
+ _tma_specs = [
512
+ ("tma_atom_dO", "tma_tensor_dO", B, mdO, self.sdO_layout, self.mma_tiler_VdO, tiled_mma_VdO),
513
+ ("tma_atom_dOt", "tma_tensor_dOt", B, mdOt, self.sdOt_layout, self.mma_tiler_PtdOt, tiled_mma_PtdOt),
514
+ ("tma_atom_Qvt", "tma_tensor_Qvt", B, mQvt, self.sQvt_layout, self.mma_tiler_dStQvt, tiled_mma_dStQvt),
515
+ ]
516
+ _tmas = {}
517
+ for atom_name, tensor_name, make_fn, m, smem_layout, mma_tiler, tiled_mma in _tma_specs:
518
+ _tmas[atom_name], _tmas[tensor_name] = (
519
+ make_tma(make_fn, m, smem_layout, mma_tiler, tiled_mma)
520
+ )
521
+
522
+ (tma_atom_dO, tma_tensor_dO,
523
+ tma_atom_dOt, tma_tensor_dOt,
524
+ tma_atom_Qvt, tma_tensor_Qvt) = _tmas.values()
525
+
526
+ # Make TMA load for P separately
527
+ tma_atom_P, tma_tensor_P = cute.nvgpu.cpasync.make_tiled_tma_atom(
528
+ cpasync.CopyBulkTensorTileG2SOp(),
529
+ mP,
530
+ self.sP_layout,
531
+ self.tile_P,
532
+ )
533
+
534
+ # ==== TMA store ====
535
+ tma_store_op = cpasync.CopyBulkTensorTileS2GOp()
536
+
537
+ self.dS_layout_major = cutlass.utils.LayoutEnum.from_tensor(mdS)
538
+ self.dV_layout_major = cutlass.utils.LayoutEnum.from_tensor(mdV)
539
+ # (tile_m, tile_n, dS_stages) = (nheads, 64, dS_stage)
540
+ sdS_layout_staged = sm100_utils.make_smem_layout_epi(
541
+ self.dtype, self.dS_layout_major, self.tile_dS, self.num_stages_dSt
542
+ )
543
+ # (tile_n, 32, dV_epi_stages) = (64, 32, 4 x 2)
544
+ sdV_layout_staged = sm100_utils.make_smem_layout_epi(
545
+ self.dtype_dV, self.dV_layout_major, self.tile_dV, self.num_epi_stages_dV
546
+ )
547
+ tma_atom_dS, tma_tensor_dS = cpasync.make_tiled_tma_atom(
548
+ tma_store_op, mdS, cute.select(sdS_layout_staged, mode=[0, 1]), self.tile_dS
549
+ )
550
+ # fmt: on
551
+
552
+ # ==== Allocate shared memory ====
553
+ SharedStorage = self._get_shared_storage_cls()
554
+
555
+ # ==== Tile scheduler ====
556
+ TileScheduler = self.TileScheduler
557
+
558
+ fa_printf(1, "mdO = {}", mdO.layout)
559
+ batch_size_for_sched = cute.size(mdO.shape[3]) if const_expr(mCuSeqlensQ is None) else 1
560
+ tile_sched_args = TileSchedulerArguments(
561
+ num_block=cute.ceil_div(cute.size(mdO.shape[0]), self.tile_m),
562
+ num_head=cute.size(mdO.shape[2]),
563
+ num_batch=batch_size_for_sched,
564
+ num_splits=1,
565
+ seqlen_k=cute.size(mV.shape[0]),
566
+ headdim=self.hdim,
567
+ headdim_v=self.hdimv,
568
+ total_q=cute.size(mdO.shape[0])
569
+ if const_expr(mCuSeqlensQ is not None)
570
+ else cute.size(mdO.shape[0]) * cute.size(mdO.shape[3]),
571
+ tile_shape_mn=self.cta_tiler_mn,
572
+ mCuSeqlensQ=mCuSeqlensQ,
573
+ mSeqUsedQ=mSeqUsedQ,
574
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
575
+ element_size=self.dtype.width // 8,
576
+ is_persistent=self.static_persistent,
577
+ lpt=False,
578
+ is_split_kv=False,
579
+ cluster_shape_mn=self.cluster_shape_mn,
580
+ use_cluster_idx=True,
581
+ )
582
+ tile_sched_params = TileScheduler.to_underlying_arguments(
583
+ tile_sched_args, scheduling_mode=self.scheduling_mode
584
+ )
585
+ self.tile_scheduler_cls = TileScheduler
586
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
587
+ fa_printf(1, "grid = {}", grid_dim)
588
+
589
+ # ==== Named Barrier ====
590
+ self.cpasync_barrier = cutlass.pipeline.NamedBarrier(
591
+ barrier_id=int(NamedBarrierBwdSm100_MLA2CTA.Cpasync),
592
+ num_threads=self.num_cpasync_load_threads,
593
+ )
594
+ self.softmax_barrier = cutlass.pipeline.NamedBarrier(
595
+ barrier_id=int(NamedBarrierBwdSm100_MLA2CTA.Softmax),
596
+ num_threads=self.num_softmax_threads,
597
+ )
598
+ self.epi_barrier = cutlass.pipeline.NamedBarrier(
599
+ barrier_id=int(NamedBarrierBwdSm100_MLA2CTA.Epilogue),
600
+ num_threads=self.num_epilogue_threads,
601
+ )
602
+
603
+ LOG2_E = math.log2(math.e)
604
+ softmax_scale_log2 = softmax_scale * LOG2_E
605
+
606
+ # ==== Launch kernel ====
607
+ block_dim = (self.num_threads, 1, 1)
608
+ self.kernel(
609
+ mV,
610
+ mdV,
611
+ tma_tensor_dO,
612
+ tma_tensor_dOt,
613
+ tma_tensor_Qvt,
614
+ tma_tensor_P,
615
+ tma_tensor_dS,
616
+ mScaleP,
617
+ mdPsum,
618
+ mCuSeqlensQ,
619
+ mCuSeqlensK,
620
+ mSeqUsedQ,
621
+ mSeqUsedK,
622
+ mIndexTopk,
623
+ tma_atom_dO,
624
+ tma_atom_dOt,
625
+ tma_atom_Qvt,
626
+ tma_atom_P,
627
+ tma_atom_dS,
628
+ self.sV_layout_staged,
629
+ self.sdO_layout_staged,
630
+ self.sdOt_layout_staged,
631
+ self.sQvt_layout_staged,
632
+ self.sP_layout_staged, # load P
633
+ sdS_layout_staged, # store dS
634
+ sdV_layout_staged,
635
+ self.sPt_layout_staged, # mma Pt
636
+ self.sdSt_layout_staged, # mma dSt
637
+ self.sScaleP_layout_staged,
638
+ self.sdPsum_layout_staged,
639
+ tiled_mma_VdO,
640
+ tiled_mma_PtdOt,
641
+ tiled_mma_dStQvt,
642
+ softmax_scale,
643
+ softmax_scale_log2,
644
+ topk_length_dynamic,
645
+ tile_sched_params,
646
+ SharedStorage,
647
+ ).launch(
648
+ grid=grid_dim,
649
+ block=block_dim,
650
+ cluster=self.cluster_shape_mnk,
651
+ smem=SharedStorage.size_in_bytes(),
652
+ stream=stream,
653
+ )
654
+
655
+ @cute.kernel
656
+ def kernel(
657
+ self,
658
+ mV: cute.Tensor,
659
+ mdV: cute.Tensor,
660
+ mdO: cute.Tensor,
661
+ mdOt: cute.Tensor,
662
+ mQvt: cute.Tensor,
663
+ mP: cute.Tensor,
664
+ mdS: cute.Tensor,
665
+ mScaleP: Optional[cute.Tensor],
666
+ mdPsum: Optional[cute.Tensor],
667
+ mCuSeqlensQ: Optional[cute.Tensor],
668
+ mCuSeqlensK: Optional[cute.Tensor],
669
+ mSeqUsedQ: Optional[cute.Tensor],
670
+ mSeqUsedK: Optional[cute.Tensor],
671
+ mIndexTopk: Optional[cute.Tensor],
672
+ tma_atom_dO: cute.CopyAtom,
673
+ tma_atom_dOt: cute.CopyAtom,
674
+ tma_atom_Qvt: cute.CopyAtom,
675
+ tma_atom_P: cute.CopyAtom,
676
+ tma_atom_dS: cute.CopyAtom,
677
+ sV_layout_staged: cute.ComposedLayout,
678
+ sdO_layout_staged: cute.ComposedLayout,
679
+ sdOt_layout_staged: cute.ComposedLayout,
680
+ sQvt_layout_staged: cute.ComposedLayout,
681
+ sP_layout_staged: cute.ComposedLayout,
682
+ sdS_layout_staged: cute.ComposedLayout,
683
+ sdV_layout_staged: cute.ComposedLayout,
684
+ sPt_layout_staged: cute.ComposedLayout,
685
+ sdSt_layout_staged: cute.ComposedLayout,
686
+ sScaleP_layout_staged: cute.Layout,
687
+ sdPsum_layout_staged: cute.Layout,
688
+ tiled_mma_VdO: cute.TiledMma,
689
+ tiled_mma_PtdOt: cute.TiledMma,
690
+ tiled_mma_dStQvt: cute.TiledMma,
691
+ softmax_scale: Float32,
692
+ softmax_scale_log2: Float32,
693
+ topk_length_dynamic: Optional[Int32],
694
+ tile_sched_params: ParamsBase,
695
+ SharedStorage: cutlass.Constexpr[Callable],
696
+ ):
697
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
698
+ cta_layout_vmnk = cute.tiled_divide(
699
+ cute.make_layout(self.cluster_shape_mnk), (tiled_mma_VdO.thr_id.shape,)
700
+ )
701
+ mma_tile_coord_v = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
702
+ is_leader_cta = mma_tile_coord_v == 0
703
+
704
+ # ==== Allocate SMEM ====
705
+ smem = cutlass.utils.SmemAllocator()
706
+ storage = smem.allocate(SharedStorage)
707
+
708
+ # ==== Prepare TMEM allocator ====
709
+ tmem_alloc_barrier = pipeline.NamedBarrier(
710
+ barrier_id=int(NamedBarrierBwdSm100_MLA2CTA.TmemPtr),
711
+ num_threads=self.num_mma_threads + self.num_softmax_threads + self.num_epilogue_threads,
712
+ )
713
+ tmem = cutlass.utils.TmemAllocator(
714
+ storage.tmem_holding_buf.ptr,
715
+ barrier_for_retrieve=tmem_alloc_barrier,
716
+ allocator_warp_id=self.mma_warp_id,
717
+ is_two_cta=self.use_2cta_instrs,
718
+ two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
719
+ )
720
+
721
+ # ==== Prefetch TMA descriptors ====
722
+ if warp_idx == self.load_warp_id:
723
+ cpasync.prefetch_descriptor(tma_atom_dO)
724
+ cpasync.prefetch_descriptor(tma_atom_dOt)
725
+ cpasync.prefetch_descriptor(tma_atom_Qvt)
726
+ cpasync.prefetch_descriptor(tma_atom_P)
727
+ cpasync.prefetch_descriptor(tma_atom_dS)
728
+
729
+ # ==== Construct pipelines ====
730
+ tma_warp = pipeline.CooperativeGroup(pipeline.Agent.Thread, 1)
731
+ mma_warp = pipeline.CooperativeGroup(pipeline.Agent.Thread, 1)
732
+ sm_warps = pipeline.CooperativeGroup(pipeline.Agent.Thread, self.num_softmax_threads // 32)
733
+ store_warp = pipeline.CooperativeGroup(pipeline.Agent.Thread, 1)
734
+ sm_threads = pipeline.CooperativeGroup(pipeline.Agent.Thread, self.num_softmax_threads)
735
+ epi_threads = pipeline.CooperativeGroup(pipeline.Agent.Thread, self.num_epilogue_threads)
736
+ sm_threads_cluster = pipeline.CooperativeGroup(
737
+ pipeline.Agent.Thread, self.num_softmax_threads * self.cta_group_size
738
+ )
739
+ epi_threads_cluster = pipeline.CooperativeGroup(
740
+ pipeline.Agent.Thread, self.num_epilogue_threads * self.cta_group_size
741
+ )
742
+ cpasync_load_threads = pipeline.CooperativeGroup(
743
+ pipeline.Agent.Thread, self.num_cpasync_load_threads
744
+ )
745
+ relay_warps_cluster = pipeline.CooperativeGroup(pipeline.Agent.Thread, self.cta_group_size)
746
+ relay_threads = pipeline.CooperativeGroup(pipeline.Agent.Thread, self.num_relay_threads)
747
+
748
+ TmaUmma = pipeline.PipelineTmaUmma
749
+ TmaAsync = pipeline.PipelineTmaAsync
750
+ AsyncUmma = pipeline.PipelineAsyncUmma
751
+ UmmaAsync = pipeline.PipelineUmmaAsync
752
+ Async = pipeline.PipelineAsync
753
+
754
+ def make_pipeline(cls, mbar_ptr, num_stages, producer, consumer, tx_count=None):
755
+ return cls.create(
756
+ barrier_storage=mbar_ptr.data_ptr(),
757
+ num_stages=num_stages,
758
+ producer_group=producer,
759
+ consumer_group=consumer,
760
+ defer_sync=True,
761
+ **(
762
+ {"cta_layout_vmnk": cta_layout_vmnk}
763
+ if cls is not Async and cls is not TmaAsync
764
+ else {}
765
+ ),
766
+ **({"tx_count": tx_count} if tx_count is not None else {}),
767
+ )
768
+
769
+ # Unconditional pipelines
770
+ # fmt: off
771
+ # TmaUmma: dO, dOt & Qvt
772
+ pipeline_dO = make_pipeline(TmaUmma, storage.mbar_ptr_dO, self.num_stages_dO, tma_warp, mma_warp, self.tma_copy_bytes_dO)
773
+ pipeline_dOt_Qvt = make_pipeline(TmaUmma, storage.mbar_ptr_dOt_Qvt, self.num_stages_Qvt, tma_warp, mma_warp, self.tma_copy_bytes_dOt)
774
+ # TmaAsync: P, scaleP
775
+ pipeline_P = make_pipeline(TmaAsync, storage.mbar_ptr_P, self.num_stages_P, tma_warp, sm_warps, self.tma_copy_bytes_P)
776
+ pipeline_scaleP = make_pipeline(TmaAsync, storage.mbar_ptr_scaleP, self.num_stages_scaleP, tma_warp, sm_warps, self.tma_copy_bytes_scaleP)
777
+ pipeline_dPsum = make_pipeline(TmaAsync, storage.mbar_ptr_dPsum, self.num_stages_dPsum, tma_warp, sm_warps, self.tma_copy_bytes_dPsum)
778
+ # AsyncUmma: Pt => dV mma, dSt => dV mma
779
+ pipeline_Pt = make_pipeline(AsyncUmma, storage.mbar_ptr_Pt, self.num_stages_Pt, sm_threads_cluster, mma_warp)
780
+ pipeline_dSt = make_pipeline(AsyncUmma, storage.mbar_ptr_dSt, self.num_stages_dSt, sm_threads_cluster, mma_warp)
781
+ # UmmaAsync: dPt, dV
782
+ pipeline_dPt = make_pipeline(UmmaAsync, storage.mbar_ptr_dPt, self.num_stages_dPt, mma_warp, sm_threads_cluster)
783
+ pipeline_dV = make_pipeline(UmmaAsync, storage.mbar_ptr_dV, self.num_stages_dV, mma_warp, epi_threads_cluster)
784
+ # Async: dV_epi
785
+ pipeline_dV_epi = make_pipeline(Async, storage.mbar_ptr_dV_epi, self.num_stages_dV, tma_warp, store_warp)
786
+
787
+ pipeline_V = make_pipeline(AsyncUmma, storage.mbar_ptr_V, self.num_stages_V, relay_warps_cluster, mma_warp)
788
+ pipeline_V_cpasync = make_pipeline(Async, storage.mbar_ptr_V_cpasync, self.num_stages_V, cpasync_load_threads, relay_threads)
789
+ # fmt: on
790
+
791
+ pipeline.pipeline_init_arrive(cluster_shape_mn=cta_layout_vmnk, is_relaxed=True)
792
+
793
+ # ==== Get SMEM tensors ====
794
+ # fmt: off
795
+ sV, sdO, sP, sPt, sdOt, sdS, sdSt, sQvt = (
796
+ store.get_tensor(layout.outer, swizzle=layout.inner)
797
+ for store, layout in [
798
+ (storage.sV, sV_layout_staged),
799
+ (storage.sdO, sdO_layout_staged),
800
+ (storage.sP, sP_layout_staged), # P & Pt overlap
801
+ (storage.sP, sPt_layout_staged), # P & Pt overlap
802
+ (storage.sQv, sdOt_layout_staged), # {dOt, Qvt, dV} overlap
803
+ (storage.sdS, sdS_layout_staged), # dS & dSt overlap
804
+ (storage.sdS, sdSt_layout_staged), # dS & dSt overlap
805
+ (storage.sQv, sQvt_layout_staged), # {dOt, Qvt, dV} overlap
806
+ ]
807
+ )
808
+ sdV = cute.make_tensor(
809
+ cute.recast_ptr(sdOt.iterator, sdV_layout_staged.inner, self.dtype_acc), sdV_layout_staged.outer
810
+ )
811
+ assert cute.cosize(sdV) * self.dtype_acc.width // self.dtype.width == cute.cosize(sdOt)
812
+
813
+ sScaleP = storage.sScaleP.get_tensor(sScaleP_layout_staged)
814
+ sdPsum = storage.sdPsum.get_tensor(sdPsum_layout_staged)
815
+ # fmt: on
816
+
817
+ # ==== Get thread MMAs and accumulator fragments ====
818
+ thr_mma_VdO = tiled_mma_VdO.get_slice(mma_tile_coord_v)
819
+ thr_mma_PtdOt = tiled_mma_PtdOt.get_slice(mma_tile_coord_v)
820
+ thr_mma_dStQvt = tiled_mma_dStQvt.get_slice(mma_tile_coord_v)
821
+
822
+ acc_shape_dPt = thr_mma_VdO.partition_shape_C(self.mma_tiler_VdO[:2])
823
+ acc_shape_dVi = thr_mma_PtdOt.partition_shape_C(self.mma_tiler_PtdOt[:2])
824
+ tdPtdP_fake = thr_mma_VdO.make_fragment_C(acc_shape_dPt)
825
+ tdVtdV0_fake = thr_mma_PtdOt.make_fragment_C(acc_shape_dVi)
826
+ tdVtdV1_fake = thr_mma_PtdOt.make_fragment_C(acc_shape_dVi)
827
+ # tdPtdP = cute.make_tensor(tdPtdP.iterator + self.tmem_offset_dP, tdPtdP.layout)
828
+ # tdVtdV0 = cute.make_tensor(tdVtdV0.iterator + self.tmem_offset_dV0, tdVtdV0.layout)
829
+ # tdVtdV1 = cute.make_tensor(tdVtdV1.iterator + self.tmem_offset_dV1, tdVtdV1.layout)
830
+
831
+ block_info = BlockInfo(
832
+ self.tile_m * self.cta_group_size,
833
+ self.tile_n,
834
+ is_causal=self.is_causal,
835
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
836
+ )
837
+ SeqlenInfoCls = partial(
838
+ SeqlenInfoQK.create,
839
+ seqlen_q_static=mdO.shape[0] if const_expr(not self.pack_gqa) else mdO.shape[0][1],
840
+ seqlen_k_static=mV.shape[0],
841
+ tile_m=self.tile_m,
842
+ tile_n=self.tile_n,
843
+ mCuSeqlensQ=mCuSeqlensQ,
844
+ mCuSeqlensK=mCuSeqlensK,
845
+ mSeqUsedQ=mSeqUsedQ,
846
+ mSeqUsedK=mSeqUsedK,
847
+ )
848
+
849
+ if const_expr(self.use_clc_scheduler):
850
+ clc_response_ptr = storage.clc_response.data_ptr()
851
+ clc_mbar_ptr = storage.clc_mbar_ptr.data_ptr()
852
+
853
+ clc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
854
+ num_clc_consumer_warps_per_cta = self.num_threads // cute.arch.WARP_SIZE
855
+ num_clc_consumer_warps = num_clc_consumer_warps_per_cta * self.cta_group_size
856
+ clc_pipeline_consumer_group = pipeline.CooperativeGroup(
857
+ pipeline.Agent.Thread, cute.arch.WARP_SIZE * num_clc_consumer_warps
858
+ )
859
+ sched_ctx = SchedulerState.create_clc(
860
+ hw_scheduler=ClcDynamicPersistentTileScheduler.create(
861
+ self.tile_scheduler_cls.clc_problem_shape(tile_sched_params),
862
+ cute.arch.block_idx(),
863
+ cute.arch.grid_dim(),
864
+ clc_response_ptr,
865
+ ),
866
+ pipeline=pipeline.PipelineClcFetchAsync.create(
867
+ barrier_storage=clc_mbar_ptr,
868
+ num_stages=self.sched_stages,
869
+ producer_group=clc_pipeline_producer_group,
870
+ consumer_group=clc_pipeline_consumer_group,
871
+ tx_count=16,
872
+ cta_layout_vmnk=cta_layout_vmnk,
873
+ ),
874
+ consumer_state=pipeline.make_pipeline_state(
875
+ pipeline.PipelineUserType.Consumer, self.sched_stages
876
+ ),
877
+ producer_state=pipeline.make_pipeline_state(
878
+ pipeline.PipelineUserType.Producer, self.sched_stages
879
+ ),
880
+ )
881
+ tile_scheduler = self.tile_scheduler_cls.create(tile_sched_params, ctx=sched_ctx)
882
+ else:
883
+ tile_scheduler = self.tile_scheduler_cls.create(tile_sched_params)
884
+ assert isinstance(tile_scheduler, TileSchedulerProtocol), (
885
+ f"tile_scheduler is not a TileSchedulerProtocol: {type(tile_scheduler)}"
886
+ )
887
+
888
+ pipeline.pipeline_init_wait(cluster_shape_mn=cta_layout_vmnk)
889
+
890
+ if const_expr(self.use_clc_scheduler):
891
+ if warp_idx == self.clc_scheduler_warp_id:
892
+ cute.arch.setmaxregister_decrease(self.num_regs_other)
893
+ if is_leader_cta:
894
+ self.clc_scheduler_warp(tile_scheduler)
895
+ else:
896
+ self.empty_warp(tile_scheduler)
897
+ for i in cutlass.range_constexpr(len(self.empty_warp_ids)):
898
+ if warp_idx == self.empty_warp_ids[i] and warp_idx != self.clc_scheduler_warp_id:
899
+ cute.arch.setmaxregister_decrease(self.num_regs_other)
900
+ self.empty_warp(tile_scheduler)
901
+ else:
902
+ for i in cutlass.range_constexpr(len(self.empty_warp_ids)):
903
+ if warp_idx == self.empty_warp_ids[i]:
904
+ cute.arch.setmaxregister_decrease(self.num_regs_other)
905
+
906
+ if const_expr(self.use_cpasync_load_KV):
907
+ if warp_idx == self.relay_warp_id:
908
+ if const_expr(self.num_regs_load < self.num_regs_per_thread):
909
+ cute.arch.setmaxregister_decrease(self.num_regs_load)
910
+ self.relay(
911
+ pipeline_V,
912
+ pipeline_V_cpasync,
913
+ topk_length_dynamic,
914
+ block_info,
915
+ SeqlenInfoCls,
916
+ tile_scheduler=tile_scheduler,
917
+ )
918
+
919
+ if warp_idx in self.cpasync_load_warp_indices:
920
+ if const_expr(self.num_regs_cpasync < self.num_regs_per_thread):
921
+ cute.arch.setmaxregister_decrease(self.num_regs_cpasync)
922
+ self.load_cpasync(
923
+ mIndexTopk,
924
+ mV,
925
+ sV,
926
+ pipeline_V,
927
+ pipeline_V_cpasync,
928
+ topk_length_dynamic,
929
+ block_info,
930
+ SeqlenInfoCls,
931
+ mCuSeqlensQ,
932
+ tile_scheduler=tile_scheduler,
933
+ )
934
+
935
+ if warp_idx == self.load_warp_id:
936
+ if const_expr(self.num_regs_load < self.num_regs_per_thread):
937
+ cute.arch.setmaxregister_decrease(self.num_regs_load)
938
+ self.load(
939
+ mdO,
940
+ mP,
941
+ mdOt,
942
+ mQvt,
943
+ mScaleP,
944
+ mdPsum,
945
+ sdO,
946
+ sP,
947
+ sdOt,
948
+ sQvt,
949
+ sScaleP,
950
+ sdPsum,
951
+ tma_atom_dO,
952
+ tma_atom_P,
953
+ tma_atom_dOt,
954
+ tma_atom_Qvt,
955
+ pipeline_dO,
956
+ pipeline_P,
957
+ pipeline_dOt_Qvt,
958
+ pipeline_Pt,
959
+ pipeline_dV_epi,
960
+ pipeline_scaleP,
961
+ pipeline_dPsum,
962
+ thr_mma_VdO,
963
+ thr_mma_PtdOt,
964
+ thr_mma_dStQvt,
965
+ topk_length_dynamic,
966
+ block_info,
967
+ SeqlenInfoCls,
968
+ mCuSeqlensQ,
969
+ tile_scheduler=tile_scheduler,
970
+ )
971
+
972
+ if warp_idx == self.mma_warp_id:
973
+ if const_expr(self.num_regs_mma < self.num_regs_per_thread):
974
+ cute.arch.setmaxregister_decrease(self.num_regs_mma)
975
+ # ==== Allocate TMEM ====
976
+ tmem.allocate(self.tmem_alloc_cols)
977
+ tmem.wait_for_alloc()
978
+ tmem_ptr = tmem.retrieve_ptr(self.dtype_acc)
979
+ tdPtdP = cute.make_tensor(tmem_ptr + self.tmem_offset_dP, tdPtdP_fake.layout)
980
+ tdVtdV0 = cute.make_tensor(tmem_ptr + self.tmem_offset_dV0, tdVtdV0_fake.layout)
981
+ tdVtdV1 = cute.make_tensor(tmem_ptr + self.tmem_offset_dV1, tdVtdV1_fake.layout)
982
+ self.mma(
983
+ sV,
984
+ sdO,
985
+ sPt,
986
+ sdOt,
987
+ sdSt,
988
+ sQvt,
989
+ tdPtdP,
990
+ tdVtdV0,
991
+ tdVtdV1,
992
+ tiled_mma_VdO,
993
+ tiled_mma_PtdOt,
994
+ tiled_mma_dStQvt,
995
+ pipeline_V,
996
+ pipeline_dO,
997
+ pipeline_dPt,
998
+ pipeline_Pt,
999
+ pipeline_dOt_Qvt,
1000
+ pipeline_dSt,
1001
+ pipeline_dV,
1002
+ is_leader_cta,
1003
+ topk_length_dynamic,
1004
+ block_info,
1005
+ SeqlenInfoCls,
1006
+ mCuSeqlensQ,
1007
+ tile_scheduler=tile_scheduler,
1008
+ )
1009
+ tmem.relinquish_alloc_permit()
1010
+ tmem_alloc_barrier.arrive_and_wait()
1011
+ tmem.free(tmem_ptr)
1012
+
1013
+ if warp_idx in self.softmax_warp_indices:
1014
+ cute.arch.setmaxregister_increase(self.num_regs_softmax)
1015
+ tmem.wait_for_alloc()
1016
+ tmem_ptr = tmem.retrieve_ptr(self.dtype_acc)
1017
+ tdPtdP = cute.make_tensor(tmem_ptr + self.tmem_offset_dP, tdPtdP_fake.layout)
1018
+ self.compute_loop(
1019
+ softmax_scale,
1020
+ softmax_scale_log2,
1021
+ thr_mma_VdO,
1022
+ tdPtdP,
1023
+ sP,
1024
+ sdS,
1025
+ sScaleP,
1026
+ sdPsum,
1027
+ mdS,
1028
+ tma_atom_dS,
1029
+ pipeline_P,
1030
+ pipeline_Pt,
1031
+ pipeline_dPt,
1032
+ pipeline_dSt,
1033
+ pipeline_scaleP,
1034
+ pipeline_dPsum,
1035
+ topk_length_dynamic,
1036
+ block_info,
1037
+ SeqlenInfoCls,
1038
+ mCuSeqlensQ,
1039
+ tile_scheduler=tile_scheduler,
1040
+ )
1041
+ tmem_alloc_barrier.arrive()
1042
+
1043
+ if warp_idx in self.epilogue_warp_indices:
1044
+ if const_expr(self.num_regs_epilogue < self.num_regs_per_thread):
1045
+ cute.arch.setmaxregister_decrease(self.num_regs_epilogue)
1046
+ elif const_expr(self.num_regs_epilogue > self.num_regs_per_thread):
1047
+ cute.arch.setmaxregister_increase(self.num_regs_epilogue)
1048
+
1049
+ tmem.wait_for_alloc()
1050
+ tmem_ptr = tmem.retrieve_ptr(self.dtype_acc)
1051
+ tdVtdV0 = cute.make_tensor(tmem_ptr + self.tmem_offset_dV0, tdVtdV0_fake.layout)
1052
+ tdVtdV1 = cute.make_tensor(tmem_ptr + self.tmem_offset_dV1, tdVtdV1_fake.layout)
1053
+ self.dVacc_store(
1054
+ mIndexTopk,
1055
+ mdV,
1056
+ sdV,
1057
+ tdVtdV0,
1058
+ tdVtdV1,
1059
+ thr_mma_PtdOt,
1060
+ pipeline_dV,
1061
+ pipeline_dV_epi,
1062
+ topk_length_dynamic,
1063
+ block_info,
1064
+ SeqlenInfoCls,
1065
+ mCuSeqlensQ,
1066
+ tile_scheduler=tile_scheduler,
1067
+ )
1068
+ tmem_alloc_barrier.arrive()
1069
+
1070
+ @cute.jit
1071
+ def clc_scheduler_warp(
1072
+ self,
1073
+ tile_scheduler: TileSchedulerProtocol,
1074
+ ):
1075
+ work_tile = tile_scheduler.initial_work_tile_info()
1076
+ while work_tile.is_valid_tile:
1077
+ tile_scheduler.prefetch_next_work()
1078
+ work_tile = tile_scheduler.advance_to_next_work()
1079
+ cta_m_block, head_idx, batch_idx, _ = work_tile.tile_idx
1080
+ if cute.arch.thread_idx()[0] == self.clc_scheduler_warp_id * cute.arch.WARP_SIZE:
1081
+ fa_printf(
1082
+ 3,
1083
+ "[CLC] query sm={} cta={} (m_blk={},h={},b={},s={}) valid={}\n",
1084
+ smid(),
1085
+ cute.arch.block_idx()[0],
1086
+ work_tile.tile_idx[0],
1087
+ work_tile.tile_idx[1],
1088
+ work_tile.tile_idx[2],
1089
+ work_tile.tile_idx[3],
1090
+ work_tile.is_valid_tile,
1091
+ )
1092
+ tile_scheduler.producer_tail()
1093
+
1094
+ @cute.jit
1095
+ def empty_warp(
1096
+ self,
1097
+ tile_scheduler: TileSchedulerProtocol,
1098
+ ):
1099
+ work_tile = tile_scheduler.initial_work_tile_info()
1100
+ while work_tile.is_valid_tile:
1101
+ work_tile = tile_scheduler.advance_to_next_work()
1102
+
1103
+ @cute.jit
1104
+ def relay(
1105
+ self,
1106
+ pipeline_V: pipeline.PipelineAsyncUmma,
1107
+ pipeline_V_cpasync: pipeline.PipelineAsync,
1108
+ topk_length_dynamic: Optional[Int32],
1109
+ block_info: BlockInfo,
1110
+ SeqlenInfoCls: Callable,
1111
+ tile_scheduler: TileSchedulerProtocol,
1112
+ ):
1113
+ # ==== Make pipeline states ====
1114
+ # pipeline_V producer
1115
+ # pipeline_V_cpasync consumer
1116
+ producer_state_V = pipeline.make_pipeline_state(
1117
+ pipeline.PipelineUserType.Producer, stages=self.num_stages_V
1118
+ )
1119
+ consumer_state_V = pipeline.make_pipeline_state(
1120
+ pipeline.PipelineUserType.Consumer, stages=self.num_stages_V
1121
+ )
1122
+ relay_V_fn = partial(self.relay_inner, pipeline_V_cpasync, pipeline_V)
1123
+
1124
+ work_tile = tile_scheduler.initial_work_tile_info()
1125
+ while work_tile.is_valid_tile:
1126
+ # m_block, head_idx, batch_idx, _ = work_tile.tile_idx
1127
+ # seqlen = SeqlenInfoCls(batch_idx)
1128
+
1129
+ num_n_block_groups = self.topk_length // self.cluster_tile_n
1130
+ # num_n_block_groups = topk_length_dynamic // self.cluster_tile_n
1131
+
1132
+ # ==== Mainloop ====
1133
+ for _ in cutlass.range(num_n_block_groups, unroll=1):
1134
+ for _ in cutlass.range_constexpr(self.num_hdimv_splits):
1135
+ consumer_state_V, producer_state_V = relay_V_fn(
1136
+ consumer_state_V, producer_state_V
1137
+ )
1138
+
1139
+ # Advance to next tile
1140
+ work_tile = tile_scheduler.advance_to_next_work()
1141
+
1142
+ pipeline_V.producer_tail(producer_state_V)
1143
+
1144
+ @cute.jit
1145
+ def relay_inner(
1146
+ self,
1147
+ pipeline_cpasync: pipeline.PipelineAsync,
1148
+ pipeline_mma: pipeline.PipelineAsyncUmma,
1149
+ consumer_state: pipeline.PipelineState,
1150
+ producer_state: pipeline.PipelineState,
1151
+ ):
1152
+ pipeline_cpasync.consumer_wait(consumer_state)
1153
+ with cute.arch.elect_one():
1154
+ pipeline_mma.producer_commit(producer_state)
1155
+ consumer_state.advance()
1156
+ producer_state.advance()
1157
+ return consumer_state, producer_state
1158
+
1159
+ @cute.jit
1160
+ def load_cpasync(
1161
+ self,
1162
+ mIndexTopk: cute.Tensor,
1163
+ mV: cute.Tensor,
1164
+ sV: cute.Tensor,
1165
+ pipeline_V: pipeline.PipelineAsyncUmma,
1166
+ pipeline_V_cpasync: pipeline.PipelineAsync,
1167
+ topk_length_dynamic: Optional[Int32],
1168
+ block_info: BlockInfo,
1169
+ SeqlenInfoCls: Callable,
1170
+ mCuSeqlensQ: Optional[cute.Tensor],
1171
+ tile_scheduler: TileSchedulerProtocol,
1172
+ ):
1173
+ # ==== cpasync load warpgroup ====
1174
+ # Description: loads tiles of V from gmem to smem using cpasync
1175
+ # produces: V
1176
+ # consumes: -
1177
+
1178
+ cta_rank_in_cluster = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
1179
+ tidx = cute.arch.thread_idx()[0] % self.num_cpasync_load_threads
1180
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % (
1181
+ self.num_cpasync_load_threads // 32
1182
+ )
1183
+
1184
+ # ==== Make pipeline states ====
1185
+ # producer: acquire PipelineAsyncUmma <- mma
1186
+ # producer: commit PipelineAsync -> relay
1187
+ producer_state_V = pipeline.make_pipeline_state(
1188
+ pipeline.PipelineUserType.Producer, stages=self.num_stages_V
1189
+ )
1190
+
1191
+ work_tile = tile_scheduler.initial_work_tile_info()
1192
+ while work_tile.is_valid_tile:
1193
+ m_block, head_idx, batch_idx, _ = work_tile.tile_idx
1194
+ if const_expr(mCuSeqlensQ is not None):
1195
+ batch_idx = get_batch_from_cu_tensor(m_block, mCuSeqlensQ)
1196
+ seqlen = SeqlenInfoCls(batch_idx)
1197
+
1198
+ num_n_block_groups = self.topk_length // self.cluster_tile_n
1199
+ # num_n_block_groups = topk_length_dynamic // self.cluster_tile_n
1200
+
1201
+ if const_expr(seqlen.has_cu_seqlens_q):
1202
+ # m_block means absolute m_idx
1203
+ mIndexTopk_cur = mIndexTopk[None, m_block]
1204
+ else:
1205
+ mIndexTopk_cur = mIndexTopk[None, m_block, batch_idx]
1206
+
1207
+ if const_expr(self.is_causal):
1208
+ m_local_idx = (
1209
+ m_block - seqlen.offset_q if const_expr(seqlen.has_cu_seqlens_q) else m_block
1210
+ )
1211
+ seqlen_k_limit = m_local_idx + 1 + seqlen.seqlen_k - seqlen.seqlen_q
1212
+ else:
1213
+ seqlen_k_limit = seqlen.seqlen_k
1214
+ cpasync_gather_kv_manager = CpasyncGatherKVManager.create(
1215
+ mIndexTopk_cur,
1216
+ cta_rank_in_cluster,
1217
+ tidx,
1218
+ warp_idx,
1219
+ self.topk_length,
1220
+ seqlen_k_limit,
1221
+ self.cluster_tile_n,
1222
+ self.hdim,
1223
+ self.hdimv,
1224
+ self.num_hdimv_splits,
1225
+ self.num_cpasync_load_threads,
1226
+ mV.element_type,
1227
+ self.cta_group_size,
1228
+ self.cpasync_barrier,
1229
+ self.disable_bitmask,
1230
+ )
1231
+
1232
+ # (seqlen_k, hdimv)
1233
+ mV_cur = seqlen.offset_batch_K(mV, batch_idx, dim=3)[None, None, head_idx]
1234
+
1235
+ load_V = partial(
1236
+ self.cpasync_gather_load_KV,
1237
+ cpasync_gather_kv_manager,
1238
+ pipeline_V,
1239
+ pipeline_V_cpasync,
1240
+ sV,
1241
+ False,
1242
+ "V",
1243
+ mV_cur,
1244
+ )
1245
+
1246
+ # ==== Mainloop ====
1247
+ for n_block_group in cutlass.range(num_n_block_groups, unroll=1):
1248
+ cpasync_gather_kv_manager.load_index_topk(n_block_group, transpose=False)
1249
+ for split in cutlass.range_constexpr(self.num_hdimv_splits):
1250
+ producer_state_V = load_V(producer_state_V, d_offset=split * self.hdimv // 2)
1251
+
1252
+ # Advance to next tile
1253
+ work_tile = tile_scheduler.advance_to_next_work()
1254
+
1255
+ pipeline_V.producer_tail(producer_state_V)
1256
+
1257
+ @cute.jit
1258
+ def cpasync_gather_load_KV(
1259
+ self,
1260
+ cpasync_gather_kv_manager: CpasyncGatherKVManager,
1261
+ pipeline_mma: pipeline.PipelineAsyncUmma,
1262
+ pipeline_cpasync: pipeline.PipelineAsync,
1263
+ sX: cute.Tensor,
1264
+ transpose: bool,
1265
+ K_or_V: str,
1266
+ mX: cute.Tensor,
1267
+ producer_state: pipeline.PipelineState,
1268
+ d_offset: int = 0,
1269
+ ):
1270
+ stage = producer_state.index
1271
+ pipeline_mma.producer_acquire(producer_state)
1272
+ cpasync_gather_kv_manager.load_X(
1273
+ mX, sX[None, None, None, stage], transpose, K_or_V, d_offset
1274
+ )
1275
+ cute.arch.cp_async_commit_group()
1276
+ pipeline_cpasync.sync_object_full.arrive_cp_async_mbarrier(stage)
1277
+ producer_state.advance()
1278
+ return producer_state
1279
+
1280
+ @cute.jit
1281
+ def load(
1282
+ self,
1283
+ mdO: cute.Tensor,
1284
+ mP: cute.Tensor,
1285
+ mdOt: cute.Tensor,
1286
+ mQvt: cute.Tensor,
1287
+ mScaleP: Optional[cute.Tensor],
1288
+ mdPsum: Optional[cute.Tensor],
1289
+ sdO: cute.Tensor,
1290
+ sP: cute.Tensor,
1291
+ sdOt: cute.Tensor,
1292
+ sQvt: cute.Tensor,
1293
+ sScaleP: cute.Tensor,
1294
+ sdPsum: cute.Tensor,
1295
+ tma_atom_dO: cute.CopyAtom,
1296
+ tma_atom_P: cute.CopyAtom,
1297
+ tma_atom_dOt: cute.CopyAtom,
1298
+ tma_atom_Qvt: cute.CopyAtom,
1299
+ pipeline_dO: pipeline.PipelineAsync, # TmaUmma
1300
+ pipeline_P: pipeline.PipelineAsync, # TmaAsync
1301
+ pipeline_dOt_Qvt: pipeline.PipelineAsync, # TmaUmma
1302
+ pipeline_Pt: pipeline.PipelineAsync, # AsyncUmma
1303
+ pipeline_dV_epi: pipeline.PipelineAsync, # Async
1304
+ pipeline_scaleP: pipeline.PipelineAsync, # TmaAsync
1305
+ pipeline_dPsum: pipeline.PipelineAsync, # TmaAsync
1306
+ thr_mma_VdO: cute.ThrMma,
1307
+ thr_mma_PtdOt: cute.ThrMma,
1308
+ thr_mma_dStQvt: cute.ThrMma,
1309
+ topk_length_dynamic: Optional[Int32],
1310
+ block_info: BlockInfo,
1311
+ SeqlenInfoCls: Callable,
1312
+ mCuSeqlensQ: Optional[cute.Tensor],
1313
+ tile_scheduler: TileSchedulerProtocol,
1314
+ ):
1315
+ # ==== Load warp ====
1316
+ # Description: loads tiles of dO, P, dOt, Qvt from gmem to smem using TMA
1317
+ # produces: dO, P, dOt, Qvt
1318
+ # consumes: -
1319
+ cta_rank_in_cluster = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
1320
+ lane_idx = cute.arch.lane_idx()
1321
+
1322
+ # ==== Make pipeline states ====
1323
+ Producer = pipeline.PipelineUserType.Producer
1324
+ producer_state_dO = pipeline.make_pipeline_state(Producer, stages=self.num_stages_dO)
1325
+ producer_state_P = pipeline.make_pipeline_state(Producer, stages=self.num_stages_P)
1326
+ producer_state_dOt_Qvt = pipeline.make_pipeline_state(Producer, stages=self.num_stages_dOt)
1327
+ producer_state_dV_epi = pipeline.make_pipeline_state(Producer, stages=self.num_stages_dV)
1328
+ producer_state_scaleP = pipeline.make_pipeline_state(
1329
+ Producer, stages=self.num_stages_scaleP
1330
+ )
1331
+ producer_state_dPsum = pipeline.make_pipeline_state(Producer, stages=self.num_stages_dPsum)
1332
+
1333
+ copy_atom_stats = cute.make_copy_atom(cpasync.CopyBulkG2SOp(), Float32)
1334
+
1335
+ work_tile = tile_scheduler.initial_work_tile_info()
1336
+ while work_tile.is_valid_tile:
1337
+ m_block, head_idx, batch_idx, _ = work_tile.tile_idx
1338
+ if const_expr(mCuSeqlensQ is not None):
1339
+ batch_idx = get_batch_from_cu_tensor(m_block, mCuSeqlensQ)
1340
+ seqlen = SeqlenInfoCls(batch_idx)
1341
+ if const_expr(mCuSeqlensQ is not None):
1342
+ m_block -= seqlen.offset_q
1343
+ num_n_block_groups = self.topk_length // self.cluster_tile_n
1344
+ # num_n_block_groups = topk_length_dynamic // self.cluster_tile_n
1345
+
1346
+ # ==== Partition GMEM tensors ====
1347
+ # (seqlen_q, topk or hdimv)
1348
+ mP_cur = seqlen.offset_batch_Q(mP, batch_idx, dim=3)[None, None, head_idx]
1349
+ mdO_cur = seqlen.offset_batch_Q(mdO, batch_idx, dim=3)[None, None, head_idx]
1350
+
1351
+ # (hdimv, seqlen_q)
1352
+ offset = (
1353
+ (0, seqlen.offset_q) if const_expr(not self.pack_gqa) else (0, (0, seqlen.offset_q))
1354
+ )
1355
+ if const_expr(not seqlen.has_cu_seqlens_q):
1356
+ mdOt_cur = mdOt[None, None, head_idx, batch_idx]
1357
+ mQvt_cur = mQvt[None, None, head_idx, batch_idx]
1358
+ else:
1359
+ mdOt_cur = cute.domain_offset(offset, mdOt[None, None, head_idx])
1360
+ mQvt_cur = cute.domain_offset(offset, mQvt[None, None, head_idx])
1361
+
1362
+ gScaleP = None
1363
+ if const_expr(mScaleP is not None):
1364
+ mScaleP_cur = seqlen.offset_batch_Q(mScaleP, batch_idx, dim=3)[None, None, head_idx]
1365
+ # (tile_m, topk//128)
1366
+ gScaleP = cute.local_tile(mScaleP_cur, (self.tile_m,), (m_block, None))
1367
+ gdPsum = None
1368
+ if const_expr(mdPsum is not None):
1369
+ mdPsum_cur = seqlen.offset_batch_Q(mdPsum, batch_idx, dim=2)[None, head_idx]
1370
+ # (tile_m)
1371
+ gdPsum = cute.local_tile(mdPsum_cur, (self.tile_m,), (m_block,))
1372
+
1373
+ # (tile_m, tile_n, n_blocks)
1374
+ gP = cute.local_tile(
1375
+ mP_cur,
1376
+ (self.tile_m, self.tile_n),
1377
+ (m_block, None),
1378
+ )
1379
+ # (tile_m, hdimv//2, 2)
1380
+ gdO = cute.local_tile(
1381
+ mdO_cur,
1382
+ (self.mma_tiler_VdO[1], self.mma_tiler_VdO[2]),
1383
+ (m_block, None),
1384
+ )
1385
+ # (hdimv//2, tile_m, 2)
1386
+ gdOt = cute.local_tile(
1387
+ mdOt_cur,
1388
+ (self.mma_tiler_PtdOt[1], self.mma_tiler_PtdOt[2]),
1389
+ (None, m_block),
1390
+ )
1391
+ gQvt = cute.local_tile(
1392
+ mQvt_cur,
1393
+ (self.mma_tiler_dStQvt[1], self.mma_tiler_dStQvt[2]),
1394
+ (None, m_block),
1395
+ )
1396
+
1397
+ tdPgdO = thr_mma_VdO.partition_B(gdO)
1398
+ tdVgdOt = thr_mma_PtdOt.partition_B(gdOt)
1399
+ tdVgQvt = thr_mma_dStQvt.partition_B(gQvt)
1400
+
1401
+ # (V, REST)
1402
+ tPsP, tPgP = cpasync.tma_partition(
1403
+ atom=tma_atom_P,
1404
+ cta_coord=0,
1405
+ cta_layout=cute.make_layout(1),
1406
+ smem_tensor=cute.group_modes(sP, 0, 2),
1407
+ gmem_tensor=cute.group_modes(gP, 0, 2),
1408
+ )
1409
+ tdOsdO, tdOgdO = cpasync.tma_partition(
1410
+ atom=tma_atom_dO,
1411
+ cta_coord=0,
1412
+ cta_layout=cute.make_layout(1),
1413
+ smem_tensor=cute.group_modes(sdO, 0, 3),
1414
+ gmem_tensor=cute.group_modes(tdPgdO, 0, 3),
1415
+ )
1416
+ tdOtsdOt, tdOtgdOt = cpasync.tma_partition(
1417
+ atom=tma_atom_dOt,
1418
+ cta_coord=0,
1419
+ cta_layout=cute.make_layout(1),
1420
+ smem_tensor=cute.group_modes(sdOt, 0, 3),
1421
+ gmem_tensor=cute.group_modes(tdVgdOt, 0, 3),
1422
+ )
1423
+ tQvtsQvt, tQvtgQvt = cpasync.tma_partition(
1424
+ atom=tma_atom_Qvt,
1425
+ cta_coord=0,
1426
+ cta_layout=cute.make_layout(1),
1427
+ smem_tensor=cute.group_modes(sQvt, 0, 3),
1428
+ gmem_tensor=cute.group_modes(tdVgQvt, 0, 3),
1429
+ )
1430
+
1431
+ load_P = partial(self.load_inner, tma_atom_P, tPgP, tPsP, pipeline_P)
1432
+ load_dO = partial(self.load_inner, tma_atom_dO, tdOgdO, tdOsdO, pipeline_dO)
1433
+ load_dOt = partial(self.load_inner, tma_atom_dOt, tdOtgdOt, tdOtsdOt, pipeline_dOt_Qvt)
1434
+ load_Qvt = partial(self.load_inner, tma_atom_Qvt, tQvtgQvt, tQvtsQvt, pipeline_dOt_Qvt)
1435
+ load_scaleP = partial(
1436
+ self.load_inner, copy_atom_stats, gScaleP, sScaleP, pipeline_scaleP, bulk_copy=True
1437
+ )
1438
+ load_dPsum = partial(
1439
+ self.load_inner, copy_atom_stats, gdPsum, sdPsum, pipeline_dPsum, bulk_copy=True
1440
+ )
1441
+
1442
+ # ==== Load stationary operands ====
1443
+ for split in cutlass.range_constexpr(self.num_hdimv_splits):
1444
+ producer_state_dO = load_dO(producer_state_dO, block=split)
1445
+
1446
+ producer_state_dPsum = load_dPsum(producer_state_dPsum)
1447
+
1448
+ # ==== Mainloop ====
1449
+ for n_block_group in cutlass.range(num_n_block_groups, unroll=1):
1450
+ n_block = 2 * n_block_group + cta_rank_in_cluster
1451
+ # load ScaleP
1452
+ if const_expr(mScaleP is not None):
1453
+ producer_state_scaleP = load_scaleP(producer_state_scaleP, block=n_block_group)
1454
+ pipeline_Pt.producer_acquire(producer_state_P)
1455
+ producer_state_P = load_P(producer_state_P, block=n_block)
1456
+ for split in cutlass.range_constexpr(self.num_hdimv_splits):
1457
+ pipeline_dV_epi.producer_acquire(producer_state_dV_epi)
1458
+ producer_state_dV_epi.advance()
1459
+ producer_state_dOt_Qvt = load_dOt(producer_state_dOt_Qvt, block=split)
1460
+ for split in cutlass.range_constexpr(self.num_hdimv_splits):
1461
+ producer_state_dOt_Qvt = load_Qvt(producer_state_dOt_Qvt, block=split)
1462
+
1463
+ # Advance to next tile
1464
+ work_tile = tile_scheduler.advance_to_next_work()
1465
+
1466
+ pipeline_P.producer_tail(producer_state_P)
1467
+ pipeline_dO.producer_tail(producer_state_dO)
1468
+ pipeline_dOt_Qvt.producer_tail(producer_state_dOt_Qvt)
1469
+
1470
+ @cute.jit
1471
+ def load_inner(
1472
+ self,
1473
+ copy_atom: cute.CopyAtom,
1474
+ tXgX: cute.Tensor,
1475
+ tXsX: cute.Tensor,
1476
+ load_pipeline: pipeline.PipelineAsync,
1477
+ producer_state: pipeline.PipelineState,
1478
+ block: Optional[Int32] = None,
1479
+ bulk_copy: bool = False,
1480
+ ):
1481
+ if const_expr(block is not None):
1482
+ tXgX = tXgX[(None, block)]
1483
+ if const_expr(cute.rank(tXsX) != 1):
1484
+ assert cute.rank(tXsX) == 2, f"wrong rank for tXsX, got {cute.rank(tXsX)}"
1485
+ stage = producer_state.index
1486
+ tXsX = tXsX[(None, stage)]
1487
+
1488
+ load_pipeline.producer_acquire(producer_state)
1489
+ mbar_ptr = load_pipeline.producer_get_barrier(producer_state)
1490
+ if const_expr(bulk_copy):
1491
+ with cute.arch.elect_one():
1492
+ cute.copy(copy_atom, tXgX, tXsX, mbar_ptr=mbar_ptr)
1493
+ else:
1494
+ cute.copy(copy_atom, tXgX, tXsX, tma_bar_ptr=mbar_ptr)
1495
+ producer_state.advance()
1496
+ return producer_state
1497
+
1498
+ @cute.jit
1499
+ def mma(
1500
+ self,
1501
+ sV: cute.Tensor,
1502
+ sdO: cute.Tensor,
1503
+ sPt: cute.Tensor,
1504
+ sdOt: cute.Tensor,
1505
+ sdSt: cute.Tensor,
1506
+ sQvt: cute.Tensor,
1507
+ tdPtdP: cute.Tensor,
1508
+ tdVtdV0: cute.Tensor,
1509
+ tdVtdV1: cute.Tensor,
1510
+ tiled_mma_VdO: cute.TiledMma,
1511
+ tiled_mma_PtdOt: cute.TiledMma,
1512
+ tiled_mma_dStQvt: cute.TiledMma,
1513
+ pipeline_V: pipeline.PipelineAsync, # AsyncUmma
1514
+ pipeline_dO: pipeline.PipelineAsync, # TmaUmma
1515
+ pipeline_dPt: pipeline.PipelineAsync, # UmmaAsync
1516
+ pipeline_Pt: pipeline.PipelineAsync, # AsyncUmma
1517
+ pipeline_dOt_Qvt: pipeline.PipelineAsync, # TmaUmma
1518
+ pipeline_dSt: pipeline.PipelineAsync, # AsyncUmma
1519
+ pipeline_dV: pipeline.PipelineAsync, # UmmaAsync
1520
+ is_leader_cta: Boolean,
1521
+ topk_length_dynamic: Optional[Int32],
1522
+ block_info: BlockInfo,
1523
+ SeqlenInfoCls: Callable,
1524
+ mCuSeqlensQ: Optional[cute.Tensor],
1525
+ tile_scheduler: TileSchedulerProtocol,
1526
+ ):
1527
+ # ==== mma warp ====
1528
+ # Description: Computes dP = V @ dO^T, dV = P^T @ dO, and dV += dS^T @ Qv
1529
+ # i.e. dP = gemm(V, dO), dV += gemm(P.T, dO.T), dV += gemm(dS.T, Qv.T)
1530
+ # Produces: dP, dV
1531
+ # Consumes: V, dO, P.T, dO.T, dS.T, Qv.T
1532
+ lane_idx = cute.arch.lane_idx()
1533
+
1534
+ tdVtdVs = [tdVtdV0, tdVtdV1]
1535
+
1536
+ # Set accumulate = True for dS^T@Qv since we are accumulating on the P^T@dO result
1537
+ tiled_mma_dStQvt.set(tcgen05.Field.ACCUMULATE, True)
1538
+
1539
+ # Operands for dP=V@dO^T
1540
+ tdPrV = tiled_mma_VdO.make_fragment_A(sV)
1541
+ tdPrdO = tiled_mma_VdO.make_fragment_B(sdO)
1542
+
1543
+ # Operands for dVi=P^T@dOi
1544
+ tdVrPt = tiled_mma_PtdOt.make_fragment_A(sPt)
1545
+ tdVrdOt = tiled_mma_PtdOt.make_fragment_B(sdOt)
1546
+
1547
+ # Operands for dVi+=dS^T@Qvi
1548
+ tdVrdSt = tiled_mma_dStQvt.make_fragment_A(sdSt)
1549
+ tdVrQvt = tiled_mma_dStQvt.make_fragment_B(sQvt)
1550
+
1551
+ use_ptx_gemm_VdO = False
1552
+ use_ptx_gemm_PtdOt = False
1553
+ use_ptx_gemm_dStQvt = False
1554
+
1555
+ # GEMM functions
1556
+ if const_expr(use_ptx_gemm_VdO):
1557
+ gemm_VdO = partial(
1558
+ fa_sm100_utils.gemm_ptx_partial,
1559
+ tiled_mma_VdO.op,
1560
+ self.tmem_offset_dP,
1561
+ zero_init=True,
1562
+ cta_group=self.cta_group_size,
1563
+ )
1564
+ else:
1565
+ gemm_VdO = partial(
1566
+ fa_sm100_utils.gemm,
1567
+ tiled_mma_VdO,
1568
+ tdPtdP,
1569
+ )
1570
+ if const_expr(use_ptx_gemm_PtdOt):
1571
+ gemm_PtdOt = [
1572
+ partial(
1573
+ fa_sm100_utils.gemm_ptx_partial,
1574
+ tiled_mma_PtdOt.op,
1575
+ self.tmem_offsets_dV[split],
1576
+ zero_init=True,
1577
+ cta_group=self.cta_group_size,
1578
+ )
1579
+ for split in range(self.num_hdimv_splits)
1580
+ ]
1581
+ else:
1582
+ gemm_PtdOt = [
1583
+ partial(
1584
+ fa_sm100_utils.gemm,
1585
+ tiled_mma_PtdOt,
1586
+ tdVtdVs[split],
1587
+ zero_init=True,
1588
+ )
1589
+ for split in range(self.num_hdimv_splits)
1590
+ ]
1591
+ if const_expr(use_ptx_gemm_dStQvt):
1592
+ gemm_dStQvt = [
1593
+ partial(
1594
+ fa_sm100_utils.gemm_ptx_partial,
1595
+ tiled_mma_dStQvt.op,
1596
+ self.tmem_offsets_dV[split],
1597
+ zero_init=False,
1598
+ cta_group=self.cta_group_size,
1599
+ )
1600
+ for split in range(self.num_hdimv_splits)
1601
+ ]
1602
+ else:
1603
+ gemm_dStQvt = [
1604
+ partial(
1605
+ fa_sm100_utils.gemm,
1606
+ tiled_mma_dStQvt,
1607
+ tdVtdVs[split],
1608
+ zero_init=False,
1609
+ )
1610
+ for split in range(self.num_hdimv_splits)
1611
+ ]
1612
+
1613
+ Consumer, Producer = pipeline.PipelineUserType.Consumer, pipeline.PipelineUserType.Producer
1614
+ consumer_state_V = pipeline.make_pipeline_state(Consumer, stages=self.num_stages_V)
1615
+ consumer_state_dO = pipeline.make_pipeline_state(Consumer, stages=self.num_stages_dO)
1616
+ consumer_state_Pt = pipeline.make_pipeline_state(Consumer, stages=self.num_stages_Pt)
1617
+ consumer_state_dOt_Qvt = pipeline.make_pipeline_state(Consumer, stages=self.num_stages_dOt)
1618
+ consumer_state_dSt = pipeline.make_pipeline_state(Consumer, stages=self.num_stages_dSt)
1619
+ producer_state_dPt = pipeline.make_pipeline_state(Producer, stages=self.num_stages_dPt)
1620
+ producer_state_dV = pipeline.make_pipeline_state(Producer, stages=self.num_stages_dV)
1621
+
1622
+ mma_VdO = partial(
1623
+ self.mma_inner, gemm_VdO, pipeline_V, tdPrV, sV, tdPrdO, sdO, swap_AB_stage=True, use_ptx=use_ptx_gemm_VdO
1624
+ )
1625
+ mma_PtdOt = partial(
1626
+ self.mma_inner, gemm_PtdOt, pipeline_dOt_Qvt, tdVrPt, sPt, tdVrdOt, sdOt, use_ptx=use_ptx_gemm_PtdOt
1627
+ )
1628
+ mma_dStQvt = partial(
1629
+ self.mma_inner, gemm_dStQvt, pipeline_dOt_Qvt, tdVrdSt, sdSt, tdVrQvt, sQvt, use_ptx=use_ptx_gemm_dStQvt
1630
+ )
1631
+
1632
+ work_tile = tile_scheduler.initial_work_tile_info()
1633
+ while work_tile.is_valid_tile:
1634
+ # m_block, head_idx, batch_idx, _ = work_tile.tile_idx
1635
+ # if const_expr(mCuSeqlensQ is not None):
1636
+ # batch_idx = get_batch_from_cu_tensor(m_block, mCuSeqlensQ)
1637
+ # seqlen = SeqlenInfoCls(batch_idx)
1638
+ # num_n_block_groups = self.topk_length // self.cluster_tile_n
1639
+ num_n_block_groups = topk_length_dynamic // self.cluster_tile_n
1640
+
1641
+ if is_leader_cta:
1642
+ # ==== Prologue ====
1643
+ consumer_wait_state_dO = consumer_state_dO.clone()
1644
+ for split in cutlass.range_constexpr(self.num_hdimv_splits):
1645
+ pipeline_dO.consumer_wait(consumer_wait_state_dO)
1646
+ consumer_wait_state_dO.advance()
1647
+
1648
+ # ==== Mainloop ====
1649
+ for _ in cutlass.range(num_n_block_groups, unroll=1):
1650
+ # 1. dP = V @ dO^T
1651
+ # mma inner waits for V
1652
+ pipeline_dPt.producer_acquire(producer_state_dPt)
1653
+ for split in cutlass.range_constexpr(self.num_hdimv_splits):
1654
+ consumer_state_V = mma_VdO(
1655
+ consumer_state_V, a_stage=split, zero_init=split == 0
1656
+ )
1657
+ pipeline_dPt.producer_commit(producer_state_dPt)
1658
+ producer_state_dPt.advance()
1659
+
1660
+ # 2. dV = P^T @ dO
1661
+ # mma inner waits for dOt
1662
+ pipeline_Pt.consumer_wait(consumer_state_Pt)
1663
+ producer_acquire_state_dV = producer_state_dV.clone()
1664
+ for split in cutlass.range_constexpr(self.num_hdimv_splits):
1665
+ pipeline_dV.producer_acquire(producer_acquire_state_dV)
1666
+ producer_acquire_state_dV.advance()
1667
+ consumer_state_dOt_Qvt = mma_PtdOt(consumer_state_dOt_Qvt, acc_stage=split)
1668
+ pipeline_Pt.consumer_release(consumer_state_Pt)
1669
+ consumer_state_Pt.advance()
1670
+
1671
+ # 3. dV += dS^T @ Qv
1672
+ # mma inner waits for Qvt
1673
+ pipeline_dSt.consumer_wait(consumer_state_dSt)
1674
+ for split in cutlass.range_constexpr(self.num_hdimv_splits):
1675
+ consumer_state_dOt_Qvt = mma_dStQvt(consumer_state_dOt_Qvt, acc_stage=split)
1676
+ pipeline_dV.producer_commit(producer_state_dV)
1677
+ producer_state_dV.advance()
1678
+ pipeline_dSt.consumer_release(consumer_state_dSt)
1679
+ consumer_state_dSt.advance()
1680
+
1681
+ # ==== Epilogue ====
1682
+ for _ in cutlass.range_constexpr(self.num_hdimv_splits):
1683
+ pipeline_dO.consumer_release(consumer_state_dO)
1684
+ consumer_state_dO.advance()
1685
+
1686
+ # Advance to next tile
1687
+ work_tile = tile_scheduler.advance_to_next_work()
1688
+
1689
+ pipeline_dPt.producer_tail(producer_state_dPt)
1690
+ pipeline_dV.producer_tail(producer_state_dV)
1691
+
1692
+ @cute.jit
1693
+ def mma_inner(
1694
+ self,
1695
+ gemm,
1696
+ load_pipeline,
1697
+ tCrA,
1698
+ sA,
1699
+ tCrB,
1700
+ sB,
1701
+ consumer_state: pipeline.PipelineState,
1702
+ acc_stage: Optional[Int32] = None,
1703
+ a_stage: Int32 = 0,
1704
+ zero_init: Optional[bool] = None,
1705
+ swap_AB_stage: bool = False,
1706
+ use_ptx: bool = True,
1707
+ ):
1708
+ if const_expr(acc_stage is not None):
1709
+ gemm = gemm[acc_stage]
1710
+
1711
+ smem_stage = consumer_state.index
1712
+
1713
+ if const_expr(not swap_AB_stage):
1714
+ a_stage = a_stage
1715
+ b_stage = smem_stage
1716
+ else:
1717
+ a_stage = smem_stage
1718
+ b_stage = a_stage
1719
+
1720
+ tCrA_cur = tCrA[None, None, None, a_stage]
1721
+ sA_cur = sA[None, None, None, a_stage]
1722
+ tCrB_cur = tCrB[None, None, None, b_stage]
1723
+ sB_cur = sB[None, None, None, b_stage]
1724
+
1725
+ kwargs = dict(tCrA=tCrA_cur, tCrB=tCrB_cur)
1726
+ if const_expr(use_ptx):
1727
+ kwargs |= dict(sA=sA_cur, sB=sB_cur)
1728
+ if const_expr(zero_init is not None):
1729
+ kwargs["zero_init"] = zero_init
1730
+
1731
+ load_pipeline.consumer_wait(consumer_state)
1732
+ gemm(**kwargs)
1733
+ load_pipeline.consumer_release(consumer_state)
1734
+ consumer_state.advance()
1735
+ return consumer_state
1736
+
1737
+ @cute.jit
1738
+ def compute_loop(
1739
+ self,
1740
+ softmax_scale: Float32,
1741
+ softmax_scale_log2: Float32,
1742
+ thr_mma_VdO: cute.ThrMma,
1743
+ tdPtdP: cute.Tensor,
1744
+ sP: cute.Tensor,
1745
+ sdS: cute.Tensor,
1746
+ sScaleP: cute.Tensor,
1747
+ sdPsum: cute.Tensor,
1748
+ mdS: cute.Tensor,
1749
+ tma_atom_dS: cute.CopyAtom,
1750
+ pipeline_P: pipeline.PipelineAsync, # TmaAsync
1751
+ pipeline_Pt: pipeline.PipelineAsync, # AsyncUmma
1752
+ pipeline_dPt: pipeline.PipelineAsync, # UmmaAsync
1753
+ pipeline_dSt: pipeline.PipelineAsync, # AsyncUmma
1754
+ pipeline_scaleP: pipeline.PipelineAsync, # TmaAsync
1755
+ pipeline_dPsum: pipeline.PipelineAsync, # TmaAsync
1756
+ topk_length_dynamic: Optional[Int32],
1757
+ block_info: BlockInfo,
1758
+ SeqlenInfoCls: Callable,
1759
+ mCuSeqlensQ: Optional[cute.Tensor],
1760
+ tile_scheduler: TileSchedulerProtocol,
1761
+ ):
1762
+ tidx = cute.arch.thread_idx()[0] % self.num_softmax_threads
1763
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % (
1764
+ self.num_softmax_threads // 32
1765
+ )
1766
+ cta_rank_in_cluster = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
1767
+ leader_warp = warp_idx == 0
1768
+
1769
+ # 256b // 32 = 8 values, 128 mqa // 2 => tmem_rep = 8
1770
+ tmem_rep = self.tile_m // self.cta_group_size // 8
1771
+ copy_atom_t2r = cute.make_copy_atom(
1772
+ tcgen05.copy.Ld16x256bOp(tcgen05.copy.Repetition(tmem_rep)),
1773
+ self.dtype_acc,
1774
+ )
1775
+ # ((64,(64,2)),1,1):((65536,(1,4194304 = 65536*64)),0,0)
1776
+ tdPtdP = tdPtdP[(None, None), 0, 0]
1777
+ tiled_copy_t2r = tcgen05.make_tmem_copy(copy_atom_t2r, tdPtdP)
1778
+ thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
1779
+ # (T2R, T2R_M, T2R_N)
1780
+ # (((64,16),1),2,1):(((1,65536),0),1048576,0)>, 1048576/65536 = 16
1781
+ tdPtdP_t2r = thr_copy_t2r.partition_S(tdPtdP)
1782
+
1783
+ cdP = cute.make_identity_tensor(self.mma_tiler_VdO[:2]) # (128, 128)
1784
+ tdPcdP = thr_mma_VdO.partition_C(cdP)[(None, None), 0, 0] # (64,128):(1@0,1@1)
1785
+ # (((2,2,8),1),2,1):(((1@1,8@0,8@1),0),16@0,0)
1786
+ tdPcdP_t2r = thr_copy_t2r.partition_D(tdPcdP)
1787
+ assert tdPcdP_t2r.shape[0][1] == 1, f"unexpected tdPcdP_t2r shape, got {tdPcdP_t2r.shape}"
1788
+
1789
+ smem_load_op = cute.nvgpu.warp.LdMatrix8x8x16bOp(True, 4) # ldsm x num_matrices = ldsm x 4
1790
+ smem_store_op = cute.nvgpu.warp.StMatrix8x8x16bOp(True, 4) # stsm x num_matrices = stsm x 4
1791
+ smem_load_atom = cute.make_copy_atom(smem_load_op, self.dtype)
1792
+ smem_store_atom = cute.make_copy_atom(smem_store_op, self.dtype)
1793
+ tiled_copy_r2s = cute.make_tiled_copy_D(smem_store_atom, tiled_copy_t2r)
1794
+ tiled_copy_s2r = cute.make_tiled_copy_D(smem_load_atom, tiled_copy_t2r)
1795
+ thr_copy_r2s = tiled_copy_r2s.get_slice(tidx)
1796
+ thr_copy_s2r = tiled_copy_s2r.get_slice(tidx)
1797
+
1798
+ sPt_load_layout = cute.make_ordered_layout(
1799
+ self.tile_Pt + (self.num_stages_P,), order=(1, 0, 2)
1800
+ )
1801
+ # (tile_n, tile_m, stages_P)
1802
+ sPt = cute.composition(sP, sPt_load_layout)
1803
+ sdSt = cute.composition(sdS, sPt_load_layout)
1804
+
1805
+ # (R2S, R2S_M, R2S_N, PIPE_D)
1806
+ # ((8,4),2,1,1):((1,1024),16,0,0)
1807
+ tSR_sPt = thr_copy_s2r.partition_S(sPt)
1808
+ tRS_sdSt = thr_copy_r2s.partition_D(sdSt)
1809
+
1810
+ # ((2,2),(2,8,1),stage):((0,0),(1,8,0),_)
1811
+ tPsScaleP_nm = self.broadcast_tensor_nm_view(sScaleP, thr_mma_VdO, thr_copy_t2r)
1812
+ tdPsdPsum_nm = self.broadcast_tensor_nm_view(sdPsum, thr_mma_VdO, thr_copy_t2r)
1813
+
1814
+ Consumer, Producer = pipeline.PipelineUserType.Consumer, pipeline.PipelineUserType.Producer
1815
+
1816
+ consumer_state_P = pipeline.make_pipeline_state(Consumer, stages=self.num_stages_P)
1817
+ consumer_state_dPt = pipeline.make_pipeline_state(Consumer, stages=1)
1818
+ consumer_state_scaleP = pipeline.make_pipeline_state(
1819
+ Consumer, stages=self.num_stages_scaleP
1820
+ )
1821
+ consumer_state_dPsum = pipeline.make_pipeline_state(Consumer, stages=self.num_stages_dPsum)
1822
+
1823
+ producer_state_Pt = pipeline.make_pipeline_state(Producer, stages=self.num_stages_Pt)
1824
+ producer_state_dSt = pipeline.make_pipeline_state(Producer, stages=self.num_stages_dSt)
1825
+
1826
+ work_tile = tile_scheduler.initial_work_tile_info()
1827
+ while work_tile.is_valid_tile:
1828
+ m_block, head_idx, batch_idx, _ = work_tile.tile_idx
1829
+ if const_expr(mCuSeqlensQ is not None):
1830
+ batch_idx = get_batch_from_cu_tensor(m_block, mCuSeqlensQ)
1831
+ seqlen = SeqlenInfoCls(batch_idx)
1832
+ if const_expr(mCuSeqlensQ is not None):
1833
+ m_block -= seqlen.offset_q
1834
+ num_n_block_groups = self.topk_length // self.cluster_tile_n
1835
+ # num_n_block_groups = topk_length_dynamic // self.cluster_tile_n
1836
+
1837
+ mdS_cur = seqlen.offset_batch_Q(mdS, batch_idx, dim=3)[None, None, head_idx]
1838
+
1839
+ gdS = cute.local_tile(mdS_cur, (self.tile_m, self.tile_n), (m_block, None))
1840
+ store_dS, _, _ = copy_utils.tma_get_copy_fn(
1841
+ tma_atom_dS,
1842
+ 0,
1843
+ cute.make_layout(1),
1844
+ sdS,
1845
+ gdS,
1846
+ )
1847
+
1848
+ pipeline_dPsum.consumer_wait(consumer_state_dPsum)
1849
+
1850
+ tdPsdPsum_cur = tdPsdPsum_nm[0, None, consumer_state_dPsum.index]
1851
+ tdPrdPsum_cur_f32 = cute.make_rmem_tensor(tdPsdPsum_cur.shape, dtype=self.dtype_scale)
1852
+ cute.autovec_copy(tdPsdPsum_cur, tdPrdPsum_cur_f32)
1853
+
1854
+ for n_block_group in cutlass.range(num_n_block_groups, unroll=1):
1855
+ n_block = 2 * n_block_group + cta_rank_in_cluster
1856
+
1857
+ pipeline_P.consumer_wait(consumer_state_P)
1858
+ # todo: ablate wait -> try_wait
1859
+ pipeline_scaleP.consumer_wait(consumer_state_scaleP)
1860
+
1861
+ # (((2,2,8),1),2,1):(((1,2,4),0),32,0)
1862
+ rPt = cute.make_rmem_tensor(tdPcdP_t2r.shape, self.dtype)
1863
+ # (S2R, S2R_M, S2R_N)
1864
+ rPt_copy_view = tiled_copy_s2r.retile(rPt)
1865
+ tSR_sPt_cur = tSR_sPt[None, None, None, consumer_state_P.index]
1866
+ cute.copy(tiled_copy_s2r, tSR_sPt_cur, rPt_copy_view)
1867
+
1868
+ # ((2,2),(2,8,1)):((2,32),(1,4,0))
1869
+ rP_nm = layout_utils.reshape_acc_to_mn(rPt[(None, 0), None, None])
1870
+
1871
+ tPsScaleP_cur = tPsScaleP_nm[0, None, consumer_state_scaleP.index]
1872
+ tPrScaleP_cur_f32 = cute.make_rmem_tensor(
1873
+ tPsScaleP_cur.shape, dtype=self.dtype_scale
1874
+ )
1875
+ tPrScaleP_cur = cute.make_rmem_tensor(tPsScaleP_cur.shape, dtype=self.dtype)
1876
+ cute.autovec_copy(tPsScaleP_cur, tPrScaleP_cur_f32)
1877
+ tPrScaleP_cur.store(tPrScaleP_cur_f32.load().to(self.dtype))
1878
+
1879
+ # scale P
1880
+ for n in cutlass.range_constexpr(cute.size(rP_nm.shape[0])):
1881
+ rP_cur = rP_nm[n, None]
1882
+ rP_cur.store(rP_cur.load() * tPrScaleP_cur.load())
1883
+ cute.arch.sync_warp()
1884
+
1885
+ cute.copy(tiled_copy_r2s, rPt_copy_view, tSR_sPt_cur)
1886
+ cute.arch.fence_view_async_shared()
1887
+ self.softmax_barrier.arrive_and_wait()
1888
+
1889
+ pipeline_scaleP.consumer_release(consumer_state_scaleP)
1890
+ consumer_state_scaleP.advance()
1891
+
1892
+ pipeline_Pt.producer_commit(producer_state_Pt)
1893
+ producer_state_Pt.advance()
1894
+
1895
+ # note: mma also signals Pt free, signal acquired in tma warp
1896
+ pipeline_P.consumer_release(consumer_state_P)
1897
+ consumer_state_P.advance()
1898
+
1899
+ pipeline_dPt.consumer_wait(consumer_state_dPt)
1900
+
1901
+ # (((2,2,8),1),2,1):(((1,2,4),0),32,0)
1902
+ tdPrdP_t2r = cute.make_rmem_tensor(tdPcdP_t2r.shape, self.dtype_acc)
1903
+ cute.copy(tiled_copy_t2r, tdPtdP_t2r, tdPrdP_t2r)
1904
+ cute.arch.fence_view_async_tmem_load()
1905
+ self.softmax_barrier.arrive_and_wait()
1906
+
1907
+ pipeline_dPt.consumer_release(consumer_state_dPt)
1908
+ consumer_state_dPt.advance()
1909
+
1910
+ # dS = P o (dP - dPsum)
1911
+ rdP_nm = layout_utils.reshape_acc_to_mn(tdPrdP_t2r[(None, 0), None, None])
1912
+ for n in cutlass.range_constexpr(cute.size(rdP_nm.shape[0])):
1913
+ rdP_cur = rdP_nm[n, None]
1914
+ rdP_cur.store(rdP_cur.load() - tdPrdPsum_cur_f32.load())
1915
+
1916
+ rPt.store(rPt.load() * (tdPrdP_t2r.load() * softmax_scale).to(self.dtype))
1917
+
1918
+ # wait for tma store to free dSt buffer
1919
+ if leader_warp:
1920
+ cute.arch.cp_async_bulk_wait_group(1 - self.num_stages_dSt, read=True)
1921
+ self.softmax_barrier.arrive_and_wait()
1922
+
1923
+ # note: dS guaranteed free as mma operand
1924
+ pipeline_dSt.producer_acquire(producer_state_dSt)
1925
+
1926
+ tRS_sdSt_cur = tRS_sdSt[None, None, None, producer_state_dSt.index]
1927
+ cute.copy(tiled_copy_r2s, rPt_copy_view, tRS_sdSt_cur)
1928
+
1929
+ cute.arch.fence_view_async_shared()
1930
+ self.softmax_barrier.arrive_and_wait()
1931
+ pipeline_dSt.producer_commit(producer_state_dSt)
1932
+
1933
+ # tma store
1934
+ if leader_warp:
1935
+ store_dS(src_idx=producer_state_dSt.index, dst_idx=n_block)
1936
+ cute.arch.cp_async_bulk_commit_group()
1937
+
1938
+ producer_state_dSt.advance()
1939
+
1940
+ pipeline_dPsum.consumer_release(consumer_state_dPsum)
1941
+ consumer_state_dPsum.advance()
1942
+
1943
+ # Advance to next tile
1944
+ work_tile = tile_scheduler.advance_to_next_work()
1945
+
1946
+ # producer tails
1947
+
1948
+ @cute.jit
1949
+ def broadcast_tensor_nm_view(
1950
+ self,
1951
+ sX: cute.Tensor, # (tile_m, num_stages)
1952
+ thr_mma: cute.ThrMma,
1953
+ thr_copy_t2r: cute.ThrCopy,
1954
+ ):
1955
+ assert cute.size(sX.shape[0]) == self.tile_m
1956
+ num_stages = sX.shape[1] if const_expr(cute.rank(sX) > 1) else 1
1957
+ sX_2D_cluster = cute.make_tensor(
1958
+ sX.iterator,
1959
+ cute.make_layout(
1960
+ (self.tile_m, self.cluster_tile_n, num_stages),
1961
+ stride=(1, 0, self.tile_m),
1962
+ ),
1963
+ )
1964
+ sXt_2D_cluster = layout_utils.transpose_view(sX_2D_cluster)
1965
+ sXt_2D = thr_mma.partition_C(sXt_2D_cluster)[(None, None), 0, 0, None]
1966
+ tXsXt_2D = thr_copy_t2r.partition_D(sXt_2D)[(None, 0), None, None, None]
1967
+ tXsXt_nm = layout_utils.make_acc_tensor_mn_view(tXsXt_2D)
1968
+ return tXsXt_nm
1969
+
1970
+ @cute.jit
1971
+ def dVacc_store(
1972
+ self,
1973
+ mIndexTopk: cute.Tensor,
1974
+ mdV: cute.Tensor,
1975
+ sdV: cute.Tensor,
1976
+ tdVtdV0: cute.Tensor,
1977
+ tdVtdV1: cute.Tensor,
1978
+ thr_mma_PtdOt: cute.ThrMma,
1979
+ pipeline_dV: pipeline.PipelineAsync, # UmmaAsync
1980
+ pipeline_dV_epi: pipeline.PipelineAsync, # Async
1981
+ topk_length_dynamic: Optional[Int32],
1982
+ block_info: BlockInfo,
1983
+ SeqlenInfoCls: Callable,
1984
+ mCuSeqlensQ: Optional[cute.Tensor],
1985
+ tile_scheduler: TileSchedulerProtocol,
1986
+ ):
1987
+ # ==== dVaccum store warpgroup ====
1988
+ # produces: -
1989
+ # consumes: dV
1990
+
1991
+ tdVtdV0 = tdVtdV0[(None, None), 0, 0]
1992
+ tdVtdV1 = tdVtdV1[(None, None), 0, 0]
1993
+
1994
+ num_epi_warps = self.num_epilogue_threads // 32
1995
+ cta_rank_in_cluster = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
1996
+ tidx = cute.arch.thread_idx()[0] % self.num_epilogue_threads
1997
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % num_epi_warps
1998
+ leader_warp = warp_idx == 0
1999
+ wg_half = warp_idx // 2
2000
+
2001
+ consumer_state_dV = pipeline.make_pipeline_state(
2002
+ pipeline.PipelineUserType.Consumer, stages=self.num_stages_dV
2003
+ )
2004
+
2005
+ copy_atom_t2r = cute.make_copy_atom(
2006
+ tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(32)),
2007
+ self.dtype_acc,
2008
+ )
2009
+ tiled_copy_t2r = tcgen05.make_tmem_copy(copy_atom_t2r, tdVtdV0)
2010
+ thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
2011
+ tdVtdV0_t2r = thr_copy_t2r.partition_S(tdVtdV0)
2012
+ tdVtdV1_t2r = thr_copy_t2r.partition_S(tdVtdV1)
2013
+ tdVtdVs_t2r = [tdVtdV0_t2r, tdVtdV1_t2r]
2014
+
2015
+ cdVmma = cute.make_identity_tensor(self.mma_tiler_PtdOt[:2])
2016
+ tdVcdVmma = thr_mma_PtdOt.partition_C(cdVmma)[(None, None), 0, 0]
2017
+ tdVcdVmma_t2r = thr_copy_t2r.partition_D(tdVcdVmma)
2018
+
2019
+ # 64 threads x 4 values to tile over tile_dV = (64, 32)
2020
+ tiled_copy_r2s = tiled_copy_2d(self.dtype_acc, 4, 64)
2021
+ thr_copy_r2s = tiled_copy_r2s.get_slice(tidx % 64)
2022
+
2023
+ # ((4,1),1,8,(1,8)):((1,0),0,4,(0,2048))
2024
+ tRS_sdV = thr_copy_r2s.partition_D(sdV)
2025
+
2026
+ tiled_copy_s2r = copy_utils.tiled_copy_2d(self.dtype_acc, 8, self.num_epilogue_threads, 4)
2027
+ thr_copy_s2r = tiled_copy_s2r.get_slice(tidx)
2028
+ # (V, M, N, STAGE)
2029
+ tSR_sdV = thr_copy_s2r.partition_S(sdV)
2030
+
2031
+ cdV = cute.make_identity_tensor(cute.product_each(sdV.shape[:2]))
2032
+ # (V, M, N)
2033
+ tdVcdV = thr_copy_s2r.partition_S(cdV)
2034
+
2035
+ gmem_rows_per_thread = cute.size(tSR_sdV.shape[1])
2036
+
2037
+ work_tile = tile_scheduler.initial_work_tile_info()
2038
+ while work_tile.is_valid_tile:
2039
+ m_block, head_idx, batch_idx, _ = work_tile.tile_idx
2040
+ if const_expr(mCuSeqlensQ is not None):
2041
+ batch_idx = get_batch_from_cu_tensor(m_block, mCuSeqlensQ)
2042
+ seqlen = SeqlenInfoCls(batch_idx)
2043
+ num_n_block_groups = self.topk_length // self.cluster_tile_n
2044
+ # num_n_block_groups = topk_length_dynamic // self.cluster_tile_n
2045
+
2046
+ # (seqlen_k, hdimv)
2047
+ mdV_cur = seqlen.offset_batch_K(mdV, batch_idx, dim=3)[None, None, head_idx]
2048
+
2049
+ # (topk, dv)
2050
+ if const_expr(seqlen.has_cu_seqlens_q):
2051
+ # m_block means absolute m_idx
2052
+ mIndexTopk_cur = mIndexTopk[None, m_block]
2053
+ else:
2054
+ mIndexTopk_cur = mIndexTopk[None, m_block, batch_idx]
2055
+
2056
+ # ==== Mainloop ====
2057
+ for n_block_group in cutlass.range(num_n_block_groups, unroll=1):
2058
+ n_block = 2 * n_block_group + cta_rank_in_cluster
2059
+
2060
+ rIdxTopK = cute.make_rmem_tensor((gmem_rows_per_thread,), dtype=self.dtype_index)
2061
+ for j in cutlass.range_constexpr(gmem_rows_per_thread):
2062
+ n_idx = n_block * self.tile_n + tdVcdV[0, j, 0][0]
2063
+ rIdxTopK[j] = mIndexTopk_cur[n_idx]
2064
+
2065
+ for split in cutlass.range_constexpr(self.num_hdimv_splits):
2066
+ tdVtdV_t2r = tdVtdVs_t2r[split]
2067
+
2068
+ pipeline_dV.consumer_wait(consumer_state_dV)
2069
+
2070
+ # TODO: record meaning of hard-coded values
2071
+ num_cols_per_store = self.tile_dV[1] * 2
2072
+ num_epi_subtiles = (self.hdimv // self.num_hdimv_splits) // num_cols_per_store
2073
+ assert num_cols_per_store == 64
2074
+ assert num_epi_subtiles == 4
2075
+ assert cute.size(tdVtdV_t2r.shape[2]) == num_epi_subtiles
2076
+
2077
+ tdVrdV_cur_shape = tdVcdVmma_t2r[None, None, 0].shape
2078
+ tRS_rdV_cur_shape = tRS_sdV[None, None, None, 0].shape
2079
+ assert cute.size(tdVrdV_cur_shape) == cute.size(tRS_rdV_cur_shape)
2080
+
2081
+ tdVrdV_out_shape = tSR_sdV[None, None, None, 0].shape + (2,)
2082
+
2083
+ for i in cutlass.range_constexpr(num_epi_subtiles):
2084
+ tdVrdV_cur = cute.make_rmem_tensor(tdVrdV_cur_shape, self.dtype_acc)
2085
+ cute.copy(tiled_copy_t2r, tdVtdV_t2r[None, None, i], tdVrdV_cur)
2086
+
2087
+ tRS_rdV_cur = cute.make_tensor(tdVrdV_cur.iterator, tRS_rdV_cur_shape)
2088
+
2089
+ stage = 4 * split + 2 * wg_half + (i % 2)
2090
+ cute.copy(tiled_copy_r2s, tRS_rdV_cur, tRS_sdV[None, None, None, stage])
2091
+ cute.arch.fence_view_async_shared()
2092
+ self.epi_barrier.arrive_and_wait()
2093
+
2094
+ tSR_rdV = cute.make_rmem_tensor(tdVrdV_out_shape, dtype=self.dtype_acc)
2095
+
2096
+ for w in cutlass.range_constexpr(2):
2097
+ stage_out = 4 * split + 2 * w + (i % 2)
2098
+ cute.copy(
2099
+ tiled_copy_s2r,
2100
+ tSR_sdV[None, None, None, stage_out],
2101
+ tSR_rdV[None, None, None, w],
2102
+ )
2103
+
2104
+ for j in cutlass.range_constexpr(gmem_rows_per_thread):
2105
+ gmem_n_idx = rIdxTopK[j]
2106
+ # Skip -1 sentinel slots (invalid top-k entries)
2107
+ if gmem_n_idx >= 0:
2108
+ for w in cutlass.range_constexpr(2):
2109
+ dv_offset = (
2110
+ self.hdimv // self.num_hdimv_splits * split # 256 * split
2111
+ + (self.hdimv // self.num_hdimv_splits // 2) * w # 128 * w
2112
+ + 32 * i
2113
+ )
2114
+ dv_offset += tdVcdV[0, j, 0][1]
2115
+ gmem_coord = (gmem_n_idx, dv_offset)
2116
+ dV_gmem_ptr = elem_pointer(mdV_cur, gmem_coord)
2117
+
2118
+ a = tSR_rdV[0, j, 0, w]
2119
+ b = tSR_rdV[1, j, 0, w]
2120
+ c = tSR_rdV[2, j, 0, w]
2121
+ d = tSR_rdV[3, j, 0, w]
2122
+ atomic_add_fp32x4(a, b, c, d, dV_gmem_ptr)
2123
+
2124
+ cute.arch.fence_view_async_tmem_load()
2125
+ self.epi_barrier.arrive_and_wait()
2126
+ pipeline_dV.consumer_release(consumer_state_dV)
2127
+
2128
+ if leader_warp:
2129
+ with cute.arch.elect_one():
2130
+ pipeline_dV_epi.consumer_release(consumer_state_dV)
2131
+
2132
+ consumer_state_dV.advance()
2133
+
2134
+ # Advance to next tile
2135
+ work_tile = tile_scheduler.advance_to_next_work()
build/torch-cuda/flash_bwd_postprocess.py CHANGED
@@ -2,7 +2,8 @@
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
 
@@ -31,39 +32,49 @@ from .tile_scheduler import (
31
  )
32
 
33
 
 
 
 
 
 
 
 
 
 
 
34
  class FlashAttentionBackwardPostprocess:
35
  def __init__(
36
  self,
37
  dtype: Type[cutlass.Numeric],
38
  head_dim: int,
39
- arch: Literal[80, 90, 100],
40
  tile_m: int = 128,
41
  num_threads: int = 256,
42
  AtomLayoutMdQ: int = 1,
43
  dQ_swapAB: bool = False,
44
  use_2cta_instrs: bool = False,
45
  cluster_size: int = 1, # for varlen offsets
 
46
  ):
47
  """
48
  :param head_dim: head dimension
49
  :type head_dim: int
50
  :param tile_m: m block size
51
  :type tile_m: int
 
52
  """
53
  self.dtype = dtype
54
  self.tile_m = tile_m
55
- assert arch // 10 in [8, 9, 10, 11], (
56
- "Only Ampere (8.x), Hopper (9.x), and Blackwell (10.x, 11.x) are supported"
57
  )
58
  self.arch = arch
59
- # padding head_dim to a multiple of 32 as k_block_size
60
- hdim_multiple_of = 32
61
  self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
62
  self.check_hdim_oob = head_dim != self.tile_hdim
63
  self.num_threads = num_threads
64
  self.AtomLayoutMdQ = AtomLayoutMdQ
65
  self.dQ_swapAB = dQ_swapAB
66
- self.use_2cta_instrs = use_2cta_instrs and arch == 100 and head_dim != 64
67
  self.cluster_size = cluster_size
68
 
69
  @staticmethod
@@ -89,7 +100,7 @@ class FlashAttentionBackwardPostprocess:
89
  return True
90
 
91
  def _get_tiled_mma(self):
92
- if const_expr(self.arch == 80):
93
  num_mma_warps = self.num_threads // 32
94
  atom_layout_dQ = (
95
  (self.AtomLayoutMdQ, num_mma_warps // self.AtomLayoutMdQ, 1)
@@ -101,9 +112,9 @@ class FlashAttentionBackwardPostprocess:
101
  atom_layout_dQ,
102
  permutation_mnk=(atom_layout_dQ[0] * 16, atom_layout_dQ[1] * 16, 16),
103
  )
104
- elif const_expr(self.arch == 90):
105
- num_mma_warp_groups = self.num_threads // 128
106
- atom_layout_dQ = (self.AtomLayoutMdQ, num_mma_warp_groups // self.AtomLayoutMdQ)
107
  tiler_mn_dQ = (self.tile_m // atom_layout_dQ[0], self.tile_hdim // atom_layout_dQ[1])
108
  tiled_mma = sm90_utils_basic.make_trivial_tiled_mma(
109
  self.dtype,
@@ -125,7 +136,7 @@ class FlashAttentionBackwardPostprocess:
125
  cta_group,
126
  (self.tile_m, self.tile_hdim),
127
  )
128
- if const_expr(self.arch in [80, 90]):
129
  assert self.num_threads == tiled_mma.size
130
  return tiled_mma
131
 
@@ -148,22 +159,22 @@ class FlashAttentionBackwardPostprocess:
148
  cute.make_layout(self.num_threads),
149
  cute.make_layout(async_copy_elems_accum),
150
  )
151
- num_s2r_copy_elems = 1 if const_expr(self.arch == 80) else 4
152
- if const_expr(self.arch == 80):
153
  self.s2r_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
154
  Float32, self.num_threads, num_s2r_copy_elems
155
  )
156
  self.sdQaccum_layout = cute.make_layout(self.tile_m * self.tile_hdim)
157
- elif const_expr(self.arch == 90):
158
  num_threads_per_warp_group = 128
159
- num_mma_warp_groups = self.num_threads // 128
160
  self.s2r_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
161
  cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
162
- cute.make_layout((num_threads_per_warp_group, num_mma_warp_groups)), # thr_layout
163
  cute.make_layout(128 // Float32.width), # val_layout
164
  )
165
  self.sdQaccum_layout = cute.make_layout(
166
- (self.tile_m * self.tile_hdim // num_mma_warp_groups, num_mma_warp_groups)
167
  )
168
  else:
169
  self.dQ_reduce_ncol = 32
@@ -188,14 +199,18 @@ class FlashAttentionBackwardPostprocess:
188
  # then setting kBlockKSmem to 32 will cause "Static shape_div failure".
189
  # We want to treat it as 64 x 48, so kBlockKSmem should be 16.
190
  mma_shape_n = self.tiled_mma.get_tile_size(1)
191
- if const_expr(self.arch == 80):
192
  sdQ_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, mma_shape_n)
193
  self.sdQ_layout = cute.tile_to_shape(
194
  sdQ_layout_atom, (self.tile_m, self.tile_hdim), (0, 1)
195
  )
196
- elif const_expr(self.arch == 90):
 
197
  self.sdQ_layout = sm90_utils.make_smem_layout(
198
- self.dtype, LayoutEnum.ROW_MAJOR, (self.tile_m, self.tile_hdim)
 
 
 
199
  )
200
  else:
201
  # TODO: this is hard-coded for hdim 128
@@ -211,7 +226,10 @@ class FlashAttentionBackwardPostprocess:
211
  scale: cutlass.Float32,
212
  mCuSeqlensQ: Optional[cute.Tensor],
213
  mSeqUsedQ: Optional[cute.Tensor],
214
- stream: cuda.CUstream,
 
 
 
215
  ):
216
  # Get the data type and check if it is fp16 or bf16
217
  if const_expr(mdQ.element_type not in [cutlass.Float16, cutlass.BFloat16]):
@@ -219,6 +237,19 @@ class FlashAttentionBackwardPostprocess:
219
  if const_expr(mdQaccum is not None):
220
  if const_expr(mdQaccum.element_type not in [cutlass.Float32]):
221
  raise TypeError("dQaccum tensor must be Float32")
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  mdQaccum, mdQ = [assume_tensor_aligned(t) for t in (mdQaccum, mdQ)]
224
 
@@ -253,6 +284,7 @@ class FlashAttentionBackwardPostprocess:
253
  tile_shape_mn=(self.tile_m, 1),
254
  mCuSeqlensQ=mCuSeqlensQ,
255
  mSeqUsedQ=mSeqUsedQ,
 
256
  )
257
 
258
  tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
@@ -264,6 +296,7 @@ class FlashAttentionBackwardPostprocess:
264
  mdQ,
265
  mCuSeqlensQ,
266
  mSeqUsedQ,
 
267
  scale,
268
  self.tiled_mma,
269
  self.dQ_swapAB,
@@ -288,6 +321,7 @@ class FlashAttentionBackwardPostprocess:
288
  mdQ: cute.Tensor,
289
  mCuSeqlensQ: Optional[cute.Tensor],
290
  mSeqUsedQ: Optional[cute.Tensor],
 
291
  scale: cutlass.Float32,
292
  tiled_mma: cute.TiledMma,
293
  dQ_swapAB: cutlass.Constexpr,
@@ -305,7 +339,7 @@ class FlashAttentionBackwardPostprocess:
305
  smem = cutlass.utils.SmemAllocator()
306
  sdQaccum = smem.allocate_tensor(cutlass.Float32, sdQaccum_layout, byte_alignment=1024)
307
  sdQaccum_flat = cute.make_tensor(sdQaccum.iterator, cute.make_layout(cute.size(sdQaccum)))
308
- if const_expr(self.arch in [80, 90]):
309
  sdQ = cute.make_tensor(cute.recast_ptr(sdQaccum.iterator, dtype=self.dtype), sdQ_layout)
310
  else:
311
  # extra stage dimension
@@ -323,6 +357,73 @@ class FlashAttentionBackwardPostprocess:
323
 
324
  m_block, head_idx, batch_idx, _ = work_tile.tile_idx
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  if work_tile.is_valid_tile:
327
  # ///////////////////////////////////////////////////////////////////////////////
328
  # Get the appropriate tiles for this thread block.
@@ -343,10 +444,7 @@ class FlashAttentionBackwardPostprocess:
343
  mdQaccum_cur = mdQaccum[batch_idx, head_idx, None]
344
  head_dim = mdQ.shape[3]
345
  else:
346
- if cutlass.const_expr(self.arch >= 90):
347
- padded_offset_q = seqlen.padded_offset_q
348
- else:
349
- padded_offset_q = seqlen.offset_q + batch_idx * self.tile_m
350
  mdQ_cur = cute.domain_offset((seqlen.offset_q, 0), mdQ[None, head_idx, None])
351
  mdQaccum_cur = cute.domain_offset(
352
  (padded_offset_q * self.tile_hdim,), mdQaccum[head_idx, None]
@@ -371,7 +469,7 @@ class FlashAttentionBackwardPostprocess:
371
  seqlen_q = seqlen.seqlen_q
372
  seqlen_q_rounded = cute.round_up(seqlen_q, self.tile_m)
373
 
374
- if const_expr(self.arch == 100 and self.use_2cta_instrs):
375
  # 2-CTA: remap dQaccum layout into TMEM view before writing sdQ
376
  num_reduce_threads = self.num_threads
377
  thr_mma_dsk = tiled_mma.get_slice(tidx)
@@ -394,7 +492,7 @@ class FlashAttentionBackwardPostprocess:
394
  g2s_thr_copy = tiled_copy_accum.get_slice(tidx)
395
 
396
  # S -> R
397
- tdQrdQ_fp32 = cute.make_fragment(tdQrdQ.shape, cutlass.Float32)
398
  tdQrdQ_s2r = cute.make_tensor(tdQrdQ_fp32.iterator, tdQrdQ_fp32.shape)
399
 
400
  smem_copy_atom = sm100_utils_basic.get_smem_store_op(
@@ -406,7 +504,7 @@ class FlashAttentionBackwardPostprocess:
406
  tiler_mn=tiled_tmem_ld.tiler_mn,
407
  )
408
  tdQsdQ_r2s = thr_tmem_ld.partition_D(thr_mma_dsk.partition_C(sdQ))
409
- tdQrdQ_r2s = cute.make_fragment(tdQsdQ_r2s.shape, self.dtype)
410
 
411
  num_stages = cute.size(tdQrdQ_fp32, mode=[1])
412
  stage_stride = self.dQ_reduce_ncol
@@ -502,11 +600,11 @@ class FlashAttentionBackwardPostprocess:
502
  tile_shape = (self.tile_m, self.tile_hdim)
503
  acc = None
504
  tiled_copy_t2r = None
505
- if const_expr(self.arch in [80, 90]):
506
  acc_shape = tiled_mma.partition_shape_C(
507
  tile_shape if const_expr(not dQ_swapAB) else tile_shape[::-1]
508
  )
509
- acc = cute.make_fragment(acc_shape, cutlass.Float32)
510
  assert cute.size(acc) == cute.size(tdQsdQaccum)
511
  else:
512
  thr_mma = tiled_mma.get_slice(0) # 1-CTA
@@ -522,7 +620,7 @@ class FlashAttentionBackwardPostprocess:
522
  tiled_copy_t2r = tcgen05.make_tmem_copy(tmem_load_atom, tdQtdQ)
523
  thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
524
  tdQrdQ_t2r_shape = thr_copy_t2r.partition_D(tdQcdQ).shape
525
- acc = cute.make_fragment(tdQrdQ_t2r_shape, Float32)
526
  tdQrdQaccum = cute.make_tensor(acc.iterator, cute.make_layout(tdQsdQaccum.shape))
527
  cute.autovec_copy(tdQsdQaccum, tdQrdQaccum)
528
  # Convert tdQrdQaccum from fp32 to fp16/bf16
@@ -531,7 +629,7 @@ class FlashAttentionBackwardPostprocess:
531
 
532
  # Step 3: Copy dQ from register to smem
533
  cute.arch.barrier() # make sure all threads have finished loading dQaccum
534
- if const_expr(self.arch in [80, 90]):
535
  copy_atom_r2s_dQ = utils.get_smem_store_atom(
536
  self.arch, self.dtype, transpose=self.dQ_swapAB
537
  )
@@ -553,7 +651,7 @@ class FlashAttentionBackwardPostprocess:
553
  )
554
  thr_copy_r2s_dQ = tiled_copy_r2s_dQ.get_slice(tidx)
555
  cdQ = cute.make_identity_tensor((self.tile_m, self.tile_hdim))
556
- if const_expr(self.arch in [80, 90]):
557
  taccdQrdQ = thr_copy_r2s_dQ.retile(rdQ)
558
  else:
559
  taccdQcdQ_shape = thr_copy_r2s_dQ.partition_S(cdQ).shape
 
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
+ import operator
6
+ from typing import Callable, NamedTuple, Optional, Type
7
 
8
  import cuda.bindings.driver as cuda
9
 
 
32
  )
33
 
34
 
35
+ class LearnableSinkBwdTensors(NamedTuple):
36
+ dpsum: cute.Tensor
37
+ lse: cute.Tensor
38
+ sink: cute.Tensor
39
+ dsink: cute.Tensor
40
+
41
+ def __new_from_mlir_values__(self, values):
42
+ return LearnableSinkBwdTensors(*values)
43
+
44
+
45
  class FlashAttentionBackwardPostprocess:
46
  def __init__(
47
  self,
48
  dtype: Type[cutlass.Numeric],
49
  head_dim: int,
50
+ arch: int,
51
  tile_m: int = 128,
52
  num_threads: int = 256,
53
  AtomLayoutMdQ: int = 1,
54
  dQ_swapAB: bool = False,
55
  use_2cta_instrs: bool = False,
56
  cluster_size: int = 1, # for varlen offsets
57
+ hdim_multiple_of: int = 32,
58
  ):
59
  """
60
  :param head_dim: head dimension
61
  :type head_dim: int
62
  :param tile_m: m block size
63
  :type tile_m: int
64
+ :param hdim_multiple_of: accumulator alignment shared with the main backward kernel.
65
  """
66
  self.dtype = dtype
67
  self.tile_m = tile_m
68
+ assert arch // 10 in [8, 9, 10, 11, 12], (
69
+ "Only Ampere (8.x), Hopper (9.x), and Blackwell (10.x, 11.x, 12.x) are supported"
70
  )
71
  self.arch = arch
 
 
72
  self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
73
  self.check_hdim_oob = head_dim != self.tile_hdim
74
  self.num_threads = num_threads
75
  self.AtomLayoutMdQ = AtomLayoutMdQ
76
  self.dQ_swapAB = dQ_swapAB
77
+ self.use_2cta_instrs = use_2cta_instrs and arch // 10 in [10, 11] and head_dim != 64
78
  self.cluster_size = cluster_size
79
 
80
  @staticmethod
 
100
  return True
101
 
102
  def _get_tiled_mma(self):
103
+ if const_expr(self.arch // 10 in [8, 12]):
104
  num_mma_warps = self.num_threads // 32
105
  atom_layout_dQ = (
106
  (self.AtomLayoutMdQ, num_mma_warps // self.AtomLayoutMdQ, 1)
 
112
  atom_layout_dQ,
113
  permutation_mnk=(atom_layout_dQ[0] * 16, atom_layout_dQ[1] * 16, 16),
114
  )
115
+ elif const_expr(self.arch // 10 == 9):
116
+ num_wg_mma = self.num_threads // 128
117
+ atom_layout_dQ = (self.AtomLayoutMdQ, num_wg_mma // self.AtomLayoutMdQ)
118
  tiler_mn_dQ = (self.tile_m // atom_layout_dQ[0], self.tile_hdim // atom_layout_dQ[1])
119
  tiled_mma = sm90_utils_basic.make_trivial_tiled_mma(
120
  self.dtype,
 
136
  cta_group,
137
  (self.tile_m, self.tile_hdim),
138
  )
139
+ if const_expr(self.arch // 10 in [8, 9, 12]):
140
  assert self.num_threads == tiled_mma.size
141
  return tiled_mma
142
 
 
159
  cute.make_layout(self.num_threads),
160
  cute.make_layout(async_copy_elems_accum),
161
  )
162
+ num_s2r_copy_elems = 1 if const_expr(self.arch // 10 in [8, 12]) else 4
163
+ if const_expr(self.arch // 10 in [8, 12]):
164
  self.s2r_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
165
  Float32, self.num_threads, num_s2r_copy_elems
166
  )
167
  self.sdQaccum_layout = cute.make_layout(self.tile_m * self.tile_hdim)
168
+ elif const_expr(self.arch // 10 == 9):
169
  num_threads_per_warp_group = 128
170
+ num_wg_mma = self.num_threads // 128
171
  self.s2r_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
172
  cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
173
+ cute.make_layout((num_threads_per_warp_group, num_wg_mma)), # thr_layout
174
  cute.make_layout(128 // Float32.width), # val_layout
175
  )
176
  self.sdQaccum_layout = cute.make_layout(
177
+ (self.tile_m * self.tile_hdim // num_wg_mma, num_wg_mma)
178
  )
179
  else:
180
  self.dQ_reduce_ncol = 32
 
199
  # then setting kBlockKSmem to 32 will cause "Static shape_div failure".
200
  # We want to treat it as 64 x 48, so kBlockKSmem should be 16.
201
  mma_shape_n = self.tiled_mma.get_tile_size(1)
202
+ if const_expr(self.arch // 10 in [8, 12]):
203
  sdQ_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, mma_shape_n)
204
  self.sdQ_layout = cute.tile_to_shape(
205
  sdQ_layout_atom, (self.tile_m, self.tile_hdim), (0, 1)
206
  )
207
+ elif const_expr(self.arch // 10 == 9):
208
+ wg_d_dQ = num_wg_mma // self.AtomLayoutMdQ
209
  self.sdQ_layout = sm90_utils.make_smem_layout(
210
+ self.dtype,
211
+ LayoutEnum.ROW_MAJOR,
212
+ (self.tile_m, self.tile_hdim),
213
+ major_mode_size=self.tile_hdim // wg_d_dQ,
214
  )
215
  else:
216
  # TODO: this is hard-coded for hdim 128
 
226
  scale: cutlass.Float32,
227
  mCuSeqlensQ: Optional[cute.Tensor],
228
  mSeqUsedQ: Optional[cute.Tensor],
229
+ sink_tensors: LearnableSinkBwdTensors | None,
230
+ mCuTotalMBlocks: Optional[cute.Tensor] = None,
231
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
232
+ stream: cuda.CUstream = None,
233
  ):
234
  # Get the data type and check if it is fp16 or bf16
235
  if const_expr(mdQ.element_type not in [cutlass.Float16, cutlass.BFloat16]):
 
237
  if const_expr(mdQaccum is not None):
238
  if const_expr(mdQaccum.element_type not in [cutlass.Float32]):
239
  raise TypeError("dQaccum tensor must be Float32")
240
+ if const_expr(sink_tensors is not None):
241
+ mdPsum, mLSE, mLearnableSink, mdSink = sink_tensors
242
+ if const_expr(
243
+ mLearnableSink.element_type
244
+ not in [cutlass.Float16, cutlass.BFloat16, cutlass.Float32]
245
+ ):
246
+ raise TypeError("Learnable sink tensor must be Float16, BFloat16, or Float32")
247
+ if const_expr(mdPsum.element_type not in [cutlass.Float32]):
248
+ raise TypeError("dPsum must be Float32")
249
+ if const_expr(mLSE.element_type not in [cutlass.Float32]):
250
+ raise TypeError("LSE must be Float32")
251
+ if const_expr(mdSink.element_type != mLearnableSink.element_type):
252
+ raise TypeError("dSink must have the learnable sink dtype")
253
 
254
  mdQaccum, mdQ = [assume_tensor_aligned(t) for t in (mdQaccum, mdQ)]
255
 
 
284
  tile_shape_mn=(self.tile_m, 1),
285
  mCuSeqlensQ=mCuSeqlensQ,
286
  mSeqUsedQ=mSeqUsedQ,
287
+ cu_total_m_blocks_ptr=mCuTotalMBlocks,
288
  )
289
 
290
  tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
 
296
  mdQ,
297
  mCuSeqlensQ,
298
  mSeqUsedQ,
299
+ sink_tensors,
300
  scale,
301
  self.tiled_mma,
302
  self.dQ_swapAB,
 
321
  mdQ: cute.Tensor,
322
  mCuSeqlensQ: Optional[cute.Tensor],
323
  mSeqUsedQ: Optional[cute.Tensor],
324
+ sink_tensors: LearnableSinkBwdTensors | None,
325
  scale: cutlass.Float32,
326
  tiled_mma: cute.TiledMma,
327
  dQ_swapAB: cutlass.Constexpr,
 
339
  smem = cutlass.utils.SmemAllocator()
340
  sdQaccum = smem.allocate_tensor(cutlass.Float32, sdQaccum_layout, byte_alignment=1024)
341
  sdQaccum_flat = cute.make_tensor(sdQaccum.iterator, cute.make_layout(cute.size(sdQaccum)))
342
+ if const_expr(self.arch // 10 in [8, 9, 12]):
343
  sdQ = cute.make_tensor(cute.recast_ptr(sdQaccum.iterator, dtype=self.dtype), sdQ_layout)
344
  else:
345
  # extra stage dimension
 
357
 
358
  m_block, head_idx, batch_idx, _ = work_tile.tile_idx
359
 
360
+ # Reuse one existing dQ postprocess CTA per head to reduce dSink from
361
+ # the per-row dPsum and LSE written by backward preprocess. This avoids
362
+ # both a global atomic accumulator and a separate zero-initialization.
363
+ if const_expr(sink_tensors is not None):
364
+ mdPsum, mLSE, mLearnableSink, mdSink = sink_tensors
365
+ block_x, block_y, block_z = cute.arch.block_idx()
366
+ sink_head_idx = head_idx if const_expr(mCuSeqlensQ is None) else block_x
367
+ # Varlen uses block_x to select one CTA per head. block_y and block_z
368
+ # are currently always zero, but check them defensively.
369
+ reduce_sink = (
370
+ m_block == 0 and batch_idx == 0
371
+ if const_expr(mCuSeqlensQ is None)
372
+ else block_x < mdSink.shape[0] and block_y == 0 and block_z == 0
373
+ )
374
+ if reduce_sink:
375
+ sink_sum = Float32(0.0)
376
+ num_batch = (
377
+ mdQ.shape[0] if const_expr(mCuSeqlensQ is None) else mCuSeqlensQ.shape[0] - 1
378
+ )
379
+ sink_val = Float32(mLearnableSink[sink_head_idx])
380
+ sink_batch = 0
381
+ while sink_batch < num_batch:
382
+ sink_seqlen = SeqlenInfoQK.create(
383
+ sink_batch,
384
+ mdQ.shape[1],
385
+ 0,
386
+ mCuSeqlensQ=mCuSeqlensQ,
387
+ mSeqUsedQ=mSeqUsedQ,
388
+ tile_m=self.tile_m * self.cluster_size,
389
+ )
390
+ sink_row = tidx
391
+ while sink_row < sink_seqlen.seqlen_q:
392
+ if const_expr(mCuSeqlensQ is None):
393
+ dpsum_val = mdPsum[sink_batch, sink_head_idx, sink_row]
394
+ lse_val = mLSE[sink_batch, sink_head_idx, sink_row]
395
+ else:
396
+ dpsum_val = mdPsum[
397
+ sink_head_idx, sink_seqlen.padded_offset_q + sink_row
398
+ ]
399
+ lse_val = mLSE[sink_head_idx, sink_seqlen.offset_q + sink_row]
400
+ lse_val = Float32(lse_val)
401
+ sink_prob = (
402
+ Float32(1.0)
403
+ if lse_val == -Float32.inf
404
+ else cute.math.exp2(
405
+ (sink_val - lse_val) * utils.LOG2_E,
406
+ fastmath=True,
407
+ )
408
+ )
409
+ sink_sum += -sink_prob * Float32(dpsum_val)
410
+ sink_row += self.num_threads
411
+ sink_batch += 1
412
+
413
+ sink_sum = utils.warp_reduce(sink_sum, operator.add)
414
+ lane_idx = cute.arch.lane_idx()
415
+ warp_idx = tidx // cute.arch.WARP_SIZE
416
+ num_warps = self.num_threads // cute.arch.WARP_SIZE
417
+ if lane_idx == 0:
418
+ sdQaccum_flat[warp_idx] = sink_sum
419
+ cute.arch.sync_threads()
420
+ if warp_idx == 0:
421
+ sink_sum = sdQaccum_flat[lane_idx] if lane_idx < num_warps else Float32(0.0)
422
+ sink_sum = utils.warp_reduce(sink_sum, operator.add)
423
+ if lane_idx == 0:
424
+ mdSink[sink_head_idx] = sink_sum.to(mdSink.element_type)
425
+ cute.arch.sync_threads()
426
+
427
  if work_tile.is_valid_tile:
428
  # ///////////////////////////////////////////////////////////////////////////////
429
  # Get the appropriate tiles for this thread block.
 
444
  mdQaccum_cur = mdQaccum[batch_idx, head_idx, None]
445
  head_dim = mdQ.shape[3]
446
  else:
447
+ padded_offset_q = seqlen.padded_offset_q
 
 
 
448
  mdQ_cur = cute.domain_offset((seqlen.offset_q, 0), mdQ[None, head_idx, None])
449
  mdQaccum_cur = cute.domain_offset(
450
  (padded_offset_q * self.tile_hdim,), mdQaccum[head_idx, None]
 
469
  seqlen_q = seqlen.seqlen_q
470
  seqlen_q_rounded = cute.round_up(seqlen_q, self.tile_m)
471
 
472
+ if const_expr(self.arch // 10 in [10, 11] and self.use_2cta_instrs):
473
  # 2-CTA: remap dQaccum layout into TMEM view before writing sdQ
474
  num_reduce_threads = self.num_threads
475
  thr_mma_dsk = tiled_mma.get_slice(tidx)
 
492
  g2s_thr_copy = tiled_copy_accum.get_slice(tidx)
493
 
494
  # S -> R
495
+ tdQrdQ_fp32 = cute.make_rmem_tensor(tdQrdQ.shape, cutlass.Float32)
496
  tdQrdQ_s2r = cute.make_tensor(tdQrdQ_fp32.iterator, tdQrdQ_fp32.shape)
497
 
498
  smem_copy_atom = sm100_utils_basic.get_smem_store_op(
 
504
  tiler_mn=tiled_tmem_ld.tiler_mn,
505
  )
506
  tdQsdQ_r2s = thr_tmem_ld.partition_D(thr_mma_dsk.partition_C(sdQ))
507
+ tdQrdQ_r2s = cute.make_rmem_tensor(tdQsdQ_r2s.shape, self.dtype)
508
 
509
  num_stages = cute.size(tdQrdQ_fp32, mode=[1])
510
  stage_stride = self.dQ_reduce_ncol
 
600
  tile_shape = (self.tile_m, self.tile_hdim)
601
  acc = None
602
  tiled_copy_t2r = None
603
+ if const_expr(self.arch // 10 in [8, 9, 12]):
604
  acc_shape = tiled_mma.partition_shape_C(
605
  tile_shape if const_expr(not dQ_swapAB) else tile_shape[::-1]
606
  )
607
+ acc = cute.make_rmem_tensor(acc_shape, cutlass.Float32)
608
  assert cute.size(acc) == cute.size(tdQsdQaccum)
609
  else:
610
  thr_mma = tiled_mma.get_slice(0) # 1-CTA
 
620
  tiled_copy_t2r = tcgen05.make_tmem_copy(tmem_load_atom, tdQtdQ)
621
  thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
622
  tdQrdQ_t2r_shape = thr_copy_t2r.partition_D(tdQcdQ).shape
623
+ acc = cute.make_rmem_tensor(tdQrdQ_t2r_shape, Float32)
624
  tdQrdQaccum = cute.make_tensor(acc.iterator, cute.make_layout(tdQsdQaccum.shape))
625
  cute.autovec_copy(tdQsdQaccum, tdQrdQaccum)
626
  # Convert tdQrdQaccum from fp32 to fp16/bf16
 
629
 
630
  # Step 3: Copy dQ from register to smem
631
  cute.arch.barrier() # make sure all threads have finished loading dQaccum
632
+ if const_expr(self.arch // 10 in [8, 9, 12]):
633
  copy_atom_r2s_dQ = utils.get_smem_store_atom(
634
  self.arch, self.dtype, transpose=self.dQ_swapAB
635
  )
 
651
  )
652
  thr_copy_r2s_dQ = tiled_copy_r2s_dQ.get_slice(tidx)
653
  cdQ = cute.make_identity_tensor((self.tile_m, self.tile_hdim))
654
+ if const_expr(self.arch // 10 in [8, 9, 12]):
655
  taccdQrdQ = thr_copy_r2s_dQ.retile(rdQ)
656
  else:
657
  taccdQcdQ_shape = thr_copy_r2s_dQ.partition_S(cdQ).shape
build/torch-cuda/flash_bwd_preprocess.py CHANGED
@@ -1,27 +1,38 @@
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, Literal
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 .quack import copy_utils
15
 
16
  from . import utils
17
- from .cute_dsl_utils import assume_tensor_aligned
18
- from .seqlen_info import SeqlenInfoQK
19
  from .quack.cute_dsl_utils import ParamsBase
20
  from .tile_scheduler import (
21
  SingleTileScheduler,
22
  SingleTileVarlenScheduler,
23
  TileSchedulerArguments,
24
  )
 
25
 
26
 
27
  class FlashAttentionBackwardPreprocess:
@@ -30,9 +41,14 @@ class FlashAttentionBackwardPreprocess:
30
  dtype: Type[cutlass.Numeric],
31
  head_dim: int,
32
  head_dim_v: int,
33
- arch: Literal[80, 90, 100],
34
- m_block_size: int = 128,
35
- num_threads: int = 128,
 
 
 
 
 
36
  ):
37
  """
38
  All contiguous dimensions must be at least 16 bytes aligned which indicates the head dimension
@@ -40,31 +56,35 @@ class FlashAttentionBackwardPreprocess:
40
 
41
  :param head_dim: head dimension
42
  :type head_dim: int
43
- :param m_block_size: m block size
44
- :type m_block_size: int
45
  :param num_threads: number of threads
46
  :type num_threads: int
 
47
  """
 
48
  self.dtype = dtype
49
- self.m_block_size = m_block_size
50
- self.arch = arch
51
- # padding head_dim to a multiple of 32 as k_block_size
52
- hdim_multiple_of = 32
53
  self.head_dim_padded = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
54
  self.head_dim_v_padded = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
55
  self.check_hdim_v_oob = head_dim_v != self.head_dim_v_padded
56
  self.num_threads = num_threads
 
 
 
 
 
57
 
58
  @staticmethod
59
- def can_implement(dtype, head_dim, m_block_size, num_threads) -> bool:
60
  """Check if the kernel can be implemented with the given parameters.
61
 
62
  :param dtype: data type
63
  :type dtype: cutlass.Numeric
64
  :param head_dim: head dimension
65
  :type head_dim: int
66
- :param m_block_size: m block size
67
- :type m_block_size: int
68
  :param num_threads: number of threads
69
  :type num_threads: int
70
 
@@ -77,7 +97,7 @@ class FlashAttentionBackwardPreprocess:
77
  return False
78
  if num_threads % 32 != 0:
79
  return False
80
- if num_threads < m_block_size: # For multiplying lse with log2
81
  return False
82
  return True
83
 
@@ -105,7 +125,7 @@ class FlashAttentionBackwardPreprocess:
105
  universal_copy_bits = 128
106
  num_copy_elems_dQaccum = universal_copy_bits // Float32.width
107
  assert (
108
- self.m_block_size * self.head_dim_padded // num_copy_elems_dQaccum
109
  ) % self.num_threads == 0
110
  self.gmem_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
111
  Float32, self.num_threads, num_copy_elems_dQaccum
@@ -114,72 +134,147 @@ class FlashAttentionBackwardPreprocess:
114
  @cute.jit
115
  def __call__(
116
  self,
117
- mO: cute.Tensor,
118
- mdO: cute.Tensor,
119
- mdPsum: cute.Tensor,
120
- mLSE: Optional[cute.Tensor],
121
- mLSElog2: Optional[cute.Tensor],
 
122
  mdQaccum: Optional[cute.Tensor],
123
- mCuSeqlensQ: Optional[cute.Tensor],
124
- mSeqUsedQ: Optional[cute.Tensor],
125
- stream: cuda.CUstream,
 
 
 
 
 
 
126
  ):
127
  # Get the data type and check if it is fp16 or bf16
128
- if cutlass.const_expr(not (mO.element_type == mdO.element_type)):
129
  raise TypeError("All tensors must have the same data type")
130
- if cutlass.const_expr(mO.element_type not in [cutlass.Float16, cutlass.BFloat16]):
131
  raise TypeError("Only Float16 or BFloat16 is supported")
132
- if cutlass.const_expr(mdPsum.element_type not in [Float32]):
133
- raise TypeError("dPsum tensor must be Float32")
134
- if cutlass.const_expr(mdQaccum is not None):
135
- if cutlass.const_expr(mdQaccum.element_type not in [Float32]):
 
 
 
136
  raise TypeError("dQaccum tensor must be Float32")
137
- if cutlass.const_expr(mLSE is not None):
138
- assert mLSElog2 is not None, "If mLSE is provided, mLSElog2 must also be provided"
139
- if cutlass.const_expr(mLSE.element_type not in [Float32]):
140
  raise TypeError("LSE tensor must be Float32")
141
- if cutlass.const_expr(mLSElog2.element_type not in [Float32]):
 
142
  raise TypeError("LSElog2 tensor must be Float32")
143
-
144
- mO, mdO, mdQaccum = [assume_tensor_aligned(t) for t in (mO, mdO, mdQaccum)]
 
 
 
 
 
 
 
 
 
145
 
146
  self._setup_attributes()
147
 
148
- if cutlass.const_expr(mCuSeqlensQ is not None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  TileScheduler = SingleTileVarlenScheduler
150
- num_head = mO.shape[1]
151
  num_batch = mCuSeqlensQ.shape[0] - 1
152
  else:
153
  TileScheduler = SingleTileScheduler
154
  num_head = mO.shape[2]
155
- num_batch = mO.shape[0]
156
 
157
  tile_sched_args = TileSchedulerArguments(
158
- num_block=cute.ceil_div(mO.shape[1], self.m_block_size),
159
  num_head=num_head,
160
  num_batch=num_batch,
161
  num_splits=1,
162
  seqlen_k=0,
163
  headdim=0,
164
- headdim_v=mO.shape[2],
165
- total_q=mO.shape[0],
166
- tile_shape_mn=(self.m_block_size, 1),
 
 
167
  mCuSeqlensQ=mCuSeqlensQ,
168
  mSeqUsedQ=mSeqUsedQ,
 
 
169
  )
170
 
171
  tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
172
  grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
173
 
 
 
 
174
  self.kernel(
175
  mO,
176
  mdO,
177
- mdPsum,
178
  mLSE,
179
  mLSElog2,
180
  mdQaccum,
181
  mCuSeqlensQ,
182
  mSeqUsedQ,
 
 
 
 
183
  self.gmem_tiled_copy_O,
184
  self.gmem_tiled_copy_dQaccum,
185
  tile_sched_params,
@@ -188,6 +283,7 @@ class FlashAttentionBackwardPreprocess:
188
  grid=grid_dim,
189
  block=[self.num_threads, 1, 1],
190
  stream=stream,
 
191
  )
192
 
193
  @cute.kernel
@@ -195,12 +291,16 @@ class FlashAttentionBackwardPreprocess:
195
  self,
196
  mO: cute.Tensor,
197
  mdO: cute.Tensor,
198
- mdPsum: cute.Tensor,
199
  mLSE: Optional[cute.Tensor],
200
  mLSElog2: Optional[cute.Tensor],
201
  mdQaccum: Optional[cute.Tensor],
202
  mCuSeqlensQ: Optional[cute.Tensor],
203
  mSeqUsedQ: Optional[cute.Tensor],
 
 
 
 
204
  gmem_tiled_copy_O: cute.TiledCopy,
205
  gmem_tiled_copy_dQaccum: cute.TiledCopy,
206
  tile_sched_params: ParamsBase,
@@ -213,149 +313,159 @@ class FlashAttentionBackwardPreprocess:
213
  work_tile = tile_scheduler.initial_work_tile_info()
214
  m_block, head_idx, batch_idx, _ = work_tile.tile_idx
215
 
 
 
 
 
 
 
 
 
216
  if work_tile.is_valid_tile:
217
  # ///////////////////////////////////////////////////////////////////////////////
218
  # Get the appropriate tiles for this thread block.
219
  # ///////////////////////////////////////////////////////////////////////////////
220
- seqlen = SeqlenInfoQK.create(
221
- batch_idx,
222
- mO.shape[1],
223
- 0,
224
- mCuSeqlensQ=mCuSeqlensQ,
225
- mCuSeqlensK=None,
226
- mSeqUsedQ=mSeqUsedQ,
227
- mSeqUsedK=None,
 
 
 
 
 
 
 
 
228
  )
 
 
229
 
230
- if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
231
- mO_cur = mO[batch_idx, None, head_idx, None]
232
- mdO_cur = mdO[batch_idx, None, head_idx, None]
233
- mdPsum_cur = mdPsum[batch_idx, head_idx, None]
234
- headdim_v = mO.shape[3]
235
- else:
236
- mO_cur = cute.domain_offset((seqlen.offset_q, 0), mO[None, head_idx, None])
237
- mdO_cur = cute.domain_offset((seqlen.offset_q, 0), mdO[None, head_idx, None])
238
-
239
- padded_offset_q = seqlen.offset_q + batch_idx * self.m_block_size
240
- if cutlass.const_expr(self.arch >= 90):
241
- padded_offset_q = padded_offset_q // self.m_block_size * self.m_block_size
242
- mdPsum_cur = cute.domain_offset((padded_offset_q,), mdPsum[head_idx, None])
243
- headdim_v = mO.shape[2]
244
-
245
- blkOdO_shape = (self.m_block_size, self.head_dim_v_padded)
246
- # (m_block_size, head_dim_v)
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_v_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_idx, head_idx, None]
272
- else:
273
- mLSE_cur = cute.domain_offset((seqlen.offset_q,), mLSE[head_idx, 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_v_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_v_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_idx, head_idx, None]
327
- else:
328
- mdQaccum_cur = cute.domain_offset(
329
- (padded_offset_q * self.head_dim_padded,), mdQaccum[head_idx, None]
330
- )
331
-
332
- # HACK: Compiler doesn't seem to recognize that padding
333
- # by padded_offset_q * self.head_dim_padded keeps alignment
334
- # since statically divisible by 4
335
-
336
- mdQaccum_cur_ptr = cute.make_ptr(
337
- dtype=mdQaccum_cur.element_type,
338
- value=mdQaccum_cur.iterator.toint(),
339
- mem_space=mdQaccum_cur.iterator.memspace,
340
- assumed_align=mdQaccum.iterator.alignment,
341
- )
342
- mdQaccum_cur = cute.make_tensor(mdQaccum_cur_ptr, mdQaccum_cur.layout)
343
-
344
- blkdQaccum_shape = (self.m_block_size * self.head_dim_padded,)
345
  gdQaccum = cute.local_tile(mdQaccum_cur, blkdQaccum_shape, (m_block,))
346
  gmem_thr_copy_dQaccum = gmem_tiled_copy_dQaccum.get_slice(tidx)
347
  tdQgdQaccum = gmem_thr_copy_dQaccum.partition_S(gdQaccum)
348
- zero = cute.make_fragment_like(tdQgdQaccum)
349
  zero.fill(0.0)
350
  cute.copy(gmem_tiled_copy_dQaccum, zero, tdQgdQaccum)
351
 
352
- if cutlass.const_expr(mLSE is not None):
353
- if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
354
- mLSElog2_cur = mLSElog2[batch_idx, head_idx, None]
355
- else:
356
- mLSElog2_cur = cute.domain_offset((padded_offset_q,), mLSElog2[head_idx, None])
357
-
358
- gLSElog2 = cute.local_tile(mLSElog2_cur, (self.m_block_size,), (m_block,))
359
  LOG2_E = math.log2(math.e)
360
- if tidx < seqlen_q_rounded - m_block * self.m_block_size:
361
- gLSElog2[tidx] = lse * LOG2_E if lse != -Float32.inf else 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ #
5
+ # Computes D_i = (dO_i * O_i).sum(dim=-1), optionally adjusted for LSE gradient:
6
+ # D'_i = D_i - dLSE_i
7
+ # This works because in the backward pass:
8
+ # dS_ij = P_ij * (dP_ij - D_i) [standard]
9
+ # When LSE is differentiable, d(loss)/d(S_ij) gets an extra term dLSE_i * P_ij
10
+ # (since d(LSE_i)/d(S_ij) = P_ij), giving:
11
+ # dS_ij = P_ij * (dP_ij - D_i) + dLSE_i * P_ij
12
+ # = P_ij * (dP_ij - (D_i - dLSE_i))
13
+ # So the main backward kernel is unchanged; we just replace D with D' = D - dLSE here.
14
  import math
15
  import operator
16
+ from typing import Callable, Type, Optional
17
 
18
  import cuda.bindings.driver as cuda
19
 
20
  import cutlass
21
  import cutlass.cute as cute
22
+ from cutlass import Float32, const_expr
23
+ from cutlass.cutlass_dsl import Arch, BaseDSL
24
 
25
+ from .quack import copy_utils, layout_utils
26
 
27
  from . import utils
28
+ from .seqlen_info import SeqlenInfo
 
29
  from .quack.cute_dsl_utils import ParamsBase
30
  from .tile_scheduler import (
31
  SingleTileScheduler,
32
  SingleTileVarlenScheduler,
33
  TileSchedulerArguments,
34
  )
35
+ from .pack_gqa import pack_gqa_layout
36
 
37
 
38
  class FlashAttentionBackwardPreprocess:
 
41
  dtype: Type[cutlass.Numeric],
42
  head_dim: int,
43
  head_dim_v: int,
44
+ tile_m: int = 128,
45
+ num_threads: int = 256,
46
+ use_padded_offsets: bool = True,
47
+ nheads_major: bool = False,
48
+ pack_gqa: bool = False,
49
+ qhead_per_kvhead: int = 1,
50
+ nheads_kv: int = 1,
51
+ hdim_multiple_of: int = 32,
52
  ):
53
  """
54
  All contiguous dimensions must be at least 16 bytes aligned which indicates the head dimension
 
56
 
57
  :param head_dim: head dimension
58
  :type head_dim: int
59
+ :param tile_m: m block size
60
+ :type tile_m: int
61
  :param num_threads: number of threads
62
  :type num_threads: int
63
+ :param hdim_multiple_of: accumulator alignment shared with the main backward kernel.
64
  """
65
+ self.use_pdl = BaseDSL._get_dsl().get_arch_enum() >= Arch.sm_90a
66
  self.dtype = dtype
67
+ self.tile_m = tile_m
 
 
 
68
  self.head_dim_padded = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
69
  self.head_dim_v_padded = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
70
  self.check_hdim_v_oob = head_dim_v != self.head_dim_v_padded
71
  self.num_threads = num_threads
72
+ self.use_padded_offsets = use_padded_offsets
73
+ self.nheads_major = nheads_major
74
+ self.pack_gqa = pack_gqa
75
+ self.qhead_per_kvhead = qhead_per_kvhead
76
+ self.nheads_kv = nheads_kv
77
 
78
  @staticmethod
79
+ def can_implement(dtype, head_dim, tile_m, num_threads) -> bool:
80
  """Check if the kernel can be implemented with the given parameters.
81
 
82
  :param dtype: data type
83
  :type dtype: cutlass.Numeric
84
  :param head_dim: head dimension
85
  :type head_dim: int
86
+ :param tile_m: m block size
87
+ :type tile_m: int
88
  :param num_threads: number of threads
89
  :type num_threads: int
90
 
 
97
  return False
98
  if num_threads % 32 != 0:
99
  return False
100
+ if num_threads < tile_m: # For multiplying lse with log2
101
  return False
102
  return True
103
 
 
125
  universal_copy_bits = 128
126
  num_copy_elems_dQaccum = universal_copy_bits // Float32.width
127
  assert (
128
+ self.tile_m * self.head_dim_padded // num_copy_elems_dQaccum
129
  ) % self.num_threads == 0
130
  self.gmem_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
131
  Float32, self.num_threads, num_copy_elems_dQaccum
 
134
  @cute.jit
135
  def __call__(
136
  self,
137
+ mO: cute.Tensor, # (batch, seqlen, nheads, head_dim_v) or (total_q, nheads, head_dim_v)
138
+ mdO: cute.Tensor, # same shape as mO
139
+ mPdPsum: cute.Tensor, # (batch, nheads, seqlen_padded) or (nheads, total_q_padded)
140
+ mLSE: Optional[cute.Tensor], # (batch, nheads, seqlen) or (nheads, total_q)
141
+ mLSElog2: Optional[cute.Tensor], # same shape as mPdPsum
142
+ # (batch, nheads, seqlen_padded * head_dim_v) or (nheads, total_q_padded * head_dim_v)
143
  mdQaccum: Optional[cute.Tensor],
144
+ mCuSeqlensQ: Optional[cute.Tensor], # (batch + 1,)
145
+ mSeqUsedQ: Optional[cute.Tensor], # (batch,)
146
+ mdLSE: Optional[cute.Tensor], # (batch, nheads, seqlen) or (nheads, total_q)
147
+ mRowMax: Optional[cute.Tensor], # (b, s, n, h) or (t, n, h)
148
+ mScaleP: Optional[cute.Tensor], # == mRowMax
149
+ softmax_scale: Float32,
150
+ mCuTotalMBlocks: Optional[cute.Tensor] = None,
151
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
152
+ stream: cuda.CUstream = None,
153
  ):
154
  # Get the data type and check if it is fp16 or bf16
155
+ if const_expr(not (mO.element_type == mdO.element_type)):
156
  raise TypeError("All tensors must have the same data type")
157
+ if const_expr(mO.element_type not in [cutlass.Float16, cutlass.BFloat16]):
158
  raise TypeError("Only Float16 or BFloat16 is supported")
159
+ if const_expr(mPdPsum.element_type not in [Float32]):
160
+ raise TypeError("PdPsum tensor must be Float32")
161
+ if const_expr(mdQaccum is not None):
162
+ assert self.nheads_major is False
163
+ assert self.pack_gqa is False
164
+ assert self.use_padded_offsets is True
165
+ if const_expr(mdQaccum.element_type not in [Float32]):
166
  raise TypeError("dQaccum tensor must be Float32")
167
+ if const_expr(mLSE is not None):
168
+ if const_expr(mLSE.element_type not in [Float32]):
 
169
  raise TypeError("LSE tensor must be Float32")
170
+ if const_expr(mLSElog2 is not None):
171
+ if const_expr(mLSElog2.element_type not in [Float32]):
172
  raise TypeError("LSElog2 tensor must be Float32")
173
+ if const_expr(mdLSE is not None):
174
+ if const_expr(mdLSE.element_type not in [Float32]):
175
+ raise TypeError("dLSE tensor must be Float32")
176
+ if const_expr(mScaleP is not None):
177
+ assert self.nheads_major is True
178
+ assert self.pack_gqa is True
179
+ assert mRowMax is not None
180
+ if const_expr(mScaleP.element_type not in [Float32]):
181
+ raise TypeError("ScaleP tensor must be Float32")
182
+ if const_expr(mRowMax.element_type not in [Float32]):
183
+ raise TypeError("RowMax tensor must be Float32")
184
 
185
  self._setup_attributes()
186
 
187
+ # (b, s, h, d) -> (s, d, h, b) or
188
+ # (total, h, d) -> (total, d, h)
189
+ QO_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 2, 1]
190
+ mO, mdO = [
191
+ cute.make_tensor(mX.iterator, cute.select(mX.layout, mode=QO_layout_transpose))
192
+ for mX in (mO, mdO)
193
+ ]
194
+
195
+ if const_expr(not self.nheads_major):
196
+ # (batch, nheads, seqlen) -> (seqlen, nheads, batch) or
197
+ # (nheads, total_q) -> (total_q, nheads)
198
+ transpose = [2, 1, 0] if const_expr(mCuSeqlensQ is None) else [1, 0]
199
+ else:
200
+ # (batch, seqlen, nheads) -> (seqlen, nheads, batch) or
201
+ # (total_q, nheads) -> (total_q, nheads)
202
+ transpose = [1, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 1]
203
+ mPdPsum, mLSE, mLSElog2, mdLSE, mdQaccum = [
204
+ layout_utils.select(mX, transpose) if mX is not None else None
205
+ for mX in (mPdPsum, mLSE, mLSElog2, mdLSE, mdQaccum)
206
+ ]
207
+
208
+ # (b, s, n, h) => (s, n, h, b) or
209
+ # (total, n, h) == (total, n, h)
210
+ rowmax_layout_transpose = [1, 2, 3, 0] if const_expr(mCuSeqlensQ is None) else [0, 1, 2]
211
+ if const_expr(mRowMax is not None):
212
+ mRowMax = layout_utils.select(mRowMax, rowmax_layout_transpose)
213
+ if const_expr(mScaleP is not None):
214
+ mScaleP = layout_utils.select(mScaleP, rowmax_layout_transpose)
215
+
216
+ # pack gqa
217
+ if const_expr(self.pack_gqa):
218
+ mO, mdO, mRowMax, mScaleP = [
219
+ pack_gqa_layout(mX, self.qhead_per_kvhead, self.nheads_kv, head_idx=2)
220
+ if mX is not None
221
+ else None
222
+ for mX in (mO, mdO, mRowMax, mScaleP)
223
+ ]
224
+ mPdPsum, mLSE, mLSElog2, mdLSE = [
225
+ pack_gqa_layout(mX, self.qhead_per_kvhead, self.nheads_kv, head_idx=1)
226
+ if mX is not None
227
+ else None
228
+ for mX in (mPdPsum, mLSE, mLSElog2, mdLSE)
229
+ ]
230
+
231
+ # mO: (s, d, h, b) or (total, d, h)
232
+ if const_expr(mCuSeqlensQ is not None):
233
  TileScheduler = SingleTileVarlenScheduler
234
+ num_head = mO.shape[2]
235
  num_batch = mCuSeqlensQ.shape[0] - 1
236
  else:
237
  TileScheduler = SingleTileScheduler
238
  num_head = mO.shape[2]
239
+ num_batch = mO.shape[3]
240
 
241
  tile_sched_args = TileSchedulerArguments(
242
+ num_block=cute.ceil_div(mO.shape[0], self.tile_m),
243
  num_head=num_head,
244
  num_batch=num_batch,
245
  num_splits=1,
246
  seqlen_k=0,
247
  headdim=0,
248
+ headdim_v=mO.shape[1],
249
+ total_q=cute.size(mO.shape[0])
250
+ if const_expr(mCuSeqlensQ is not None)
251
+ else cute.size(mO.shape[0]) * cute.size(mO.shape[3]),
252
+ tile_shape_mn=(self.tile_m, 1),
253
  mCuSeqlensQ=mCuSeqlensQ,
254
  mSeqUsedQ=mSeqUsedQ,
255
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
256
+ cu_total_m_blocks_ptr=mCuTotalMBlocks,
257
  )
258
 
259
  tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
260
  grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
261
 
262
+ LOG2_E = math.log2(math.e)
263
+ softmax_scale_log2 = softmax_scale * LOG2_E
264
+
265
  self.kernel(
266
  mO,
267
  mdO,
268
+ mPdPsum,
269
  mLSE,
270
  mLSElog2,
271
  mdQaccum,
272
  mCuSeqlensQ,
273
  mSeqUsedQ,
274
+ mdLSE,
275
+ mRowMax,
276
+ mScaleP,
277
+ softmax_scale_log2,
278
  self.gmem_tiled_copy_O,
279
  self.gmem_tiled_copy_dQaccum,
280
  tile_sched_params,
 
283
  grid=grid_dim,
284
  block=[self.num_threads, 1, 1],
285
  stream=stream,
286
+ use_pdl=self.use_pdl,
287
  )
288
 
289
  @cute.kernel
 
291
  self,
292
  mO: cute.Tensor,
293
  mdO: cute.Tensor,
294
+ mPdPsum: cute.Tensor,
295
  mLSE: Optional[cute.Tensor],
296
  mLSElog2: Optional[cute.Tensor],
297
  mdQaccum: Optional[cute.Tensor],
298
  mCuSeqlensQ: Optional[cute.Tensor],
299
  mSeqUsedQ: Optional[cute.Tensor],
300
+ mdLSE: Optional[cute.Tensor],
301
+ mRowMax: Optional[cute.Tensor],
302
+ mScaleP: Optional[cute.Tensor],
303
+ softmax_scale_log2: Float32,
304
  gmem_tiled_copy_O: cute.TiledCopy,
305
  gmem_tiled_copy_dQaccum: cute.TiledCopy,
306
  tile_sched_params: ParamsBase,
 
313
  work_tile = tile_scheduler.initial_work_tile_info()
314
  m_block, head_idx, batch_idx, _ = work_tile.tile_idx
315
 
316
+ # This kernel is launched with use_pdl=True, so the GPU may start executing it in
317
+ # "prologue" mode while the previous stream kernel is still running. We must wait
318
+ # before touching any upstream GMEM outputs (mO, mdO, mLSE); otherwise we risk
319
+ # reading a partially-written dout, which silently corrupts dpsum = sum(O * dO) and
320
+ # propagates to dQ/dK via dS = P * (dP - dpsum).
321
+ if const_expr(self.use_pdl):
322
+ cute.arch.griddepcontrol_wait()
323
+
324
  if work_tile.is_valid_tile:
325
  # ///////////////////////////////////////////////////////////////////////////////
326
  # Get the appropriate tiles for this thread block.
327
  # ///////////////////////////////////////////////////////////////////////////////
328
+ seqlen_static = mO.shape[0] if const_expr(not self.pack_gqa) else mO.shape[0][1]
329
+ seqlen = SeqlenInfo.create(
330
+ batch_idx, seqlen_static, mCuSeqlensQ, mSeqUsedQ, tile=self.tile_m
331
+ )
332
+ # (seqlen, dv)
333
+ mO_cur, mdO_cur = [
334
+ seqlen.offset_batch(mX, batch_idx, dim=3)[None, None, head_idx] for mX in (mO, mdO)
335
+ ]
336
+ mPdPsum_cur = seqlen.offset_batch(
337
+ mPdPsum, batch_idx, dim=2, padded=self.use_padded_offsets
338
+ )[None, head_idx]
339
+ headdim_v = mO_cur.shape[1]
340
+ seqlen_q = (
341
+ seqlen.seqlen
342
+ if const_expr(not self.pack_gqa)
343
+ else seqlen.seqlen * self.qhead_per_kvhead
344
  )
345
+ seqlen_q_rounded = cute.round_up(seqlen_q, self.tile_m)
346
+ seqlen_limit = seqlen_q - m_block * self.tile_m
347
 
348
+ lse = None
349
+ if const_expr(mLSE is not None):
350
+ mLSE_cur = seqlen.offset_batch(mLSE, batch_idx, dim=2)[None, head_idx]
351
+ gLSE = cute.local_tile(mLSE_cur, (self.tile_m,), (m_block,))
352
+ lse = Float32.inf
353
+ if tidx < seqlen_limit:
354
+ lse = gLSE[tidx]
 
 
 
 
 
 
 
 
 
 
 
 
355
 
356
+ blk_shape = (self.tile_m, self.head_dim_v_padded)
357
+ gO = cute.local_tile(mO_cur, blk_shape, (m_block, 0))
358
+ gdO = cute.local_tile(mdO_cur, blk_shape, (m_block, 0))
359
  gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
360
  # (CPY_Atom, CPY_M, CPY_K)
361
  tOgO = gmem_thr_copy_O.partition_S(gO)
362
  tOgdO = gmem_thr_copy_O.partition_S(gdO)
363
+ cO = cute.make_identity_tensor(blk_shape)
 
 
 
 
 
 
364
  tOcO = gmem_thr_copy_O.partition_S(cO)
365
  t0OcO = gmem_thr_copy_O.get_slice(0).partition_S(cO)
366
+ tOpO = None
367
+ if const_expr(self.check_hdim_v_oob):
368
+ tOpO = copy_utils.predicate_k(tOcO, limit=headdim_v)
369
+ # Each copy uses the predicate sliced to the current m (see loop below).
370
+ copy = copy_utils.copy
371
+
372
+ tOrO = cute.make_rmem_tensor_like(tOgO)
373
+ tOrdO = cute.make_rmem_tensor_like(tOgdO)
374
+ if const_expr(self.check_hdim_v_oob):
375
+ tOrO.fill(0.0)
376
+ tOrdO.fill(0.0)
377
+ assert tOgO.shape == tOgdO.shape
 
 
 
 
 
 
 
 
 
 
378
  for m in cutlass.range(cute.size(tOrO.shape[1]), unroll_full=True):
379
+ # Instead of using tOcO, we using t0OcO and subtract the offset from the limit.
380
+ # This is bc the entries of t0OcO are known at compile time.
381
+ if t0OcO[0, m, 0][0] < seqlen_limit - tOcO[0][0]:
382
+ # The predicate carries a CPY_M mode (broadcast over m); slice it to the
383
+ # current m so its shape matches the m-sliced copy source. Otherwise the
384
+ # vectorized copy atom's predicate-shape verification fails when head_dim_v
385
+ # is not a multiple of the copy-atom width (e.g. 72, 104).
386
+ tOpO_cur = tOpO[None, m, None] if const_expr(tOpO is not None) else None
387
+ copy(tOgO[None, m, None], tOrO[None, m, None], pred=tOpO_cur)
388
+ copy(tOgdO[None, m, None], tOrdO[None, m, None], pred=tOpO_cur)
389
+ # O and dO loads are done; signal that the next kernel can start.
390
+ # Correctness is ensured by griddepcontrol_wait() in bwd_sm90 before it reads our outputs.
391
+ if const_expr(self.use_pdl):
392
+ cute.arch.griddepcontrol_launch_dependents()
 
 
 
 
 
393
  # Sum across the "k" dimension
394
+ pdpsum = (tOrO.load().to(Float32) * tOrdO.load().to(Float32)).reduce(
395
  cute.ReductionOp.ADD, init_val=0.0, reduction_profile=(0, None, 1)
396
  )
397
  threads_per_row = gmem_tiled_copy_O.layout_src_tv_tiled[0].shape[0]
398
  assert cute.arch.WARP_SIZE % threads_per_row == 0
399
+ pdpsum = utils.warp_reduce(pdpsum, operator.add, width=threads_per_row)
400
+ PdP_sum = cute.make_rmem_tensor(cute.size(tOrO, mode=[1]), Float32)
401
+ PdP_sum.store(pdpsum)
402
+
403
+ # If dLSE is provided, compute D' = D - dLSE (see module docstring for derivation).
404
+ gdLSE = None
405
+ if const_expr(mdLSE is not None):
406
+ mdLSE_cur = seqlen.offset_batch(mdLSE, batch_idx, dim=2)[None, head_idx]
407
+ gdLSE = cute.local_tile(mdLSE_cur, (self.tile_m,), (m_block,))
408
+
409
+ # Write PdPsum from rmem -> gmem
410
+ gPdPsum = cute.local_tile(mPdPsum_cur, (self.tile_m,), (m_block,))
411
+ # Only the thread corresponding to column 0 writes out the PdPsum to gmem
412
  if tOcO[0, 0, 0][1] == 0:
413
+ for m in cutlass.range(cute.size(PdP_sum), unroll_full=True):
414
  row = tOcO[0, m, 0][0]
415
+ PdPsum_val = 0.0
416
+ if row < seqlen_limit:
417
+ PdPsum_val = PdP_sum[m]
418
+ if const_expr(mdLSE is not None):
419
+ PdPsum_val -= gdLSE[row]
420
+ gPdPsum[row] = PdPsum_val
421
 
422
  # Clear dQaccum
423
+ if const_expr(mdQaccum is not None):
424
+ mdQaccum_cur = seqlen.offset_batch(
425
+ mdQaccum,
426
+ batch_idx,
427
+ dim=2,
428
+ padded=self.use_padded_offsets,
429
+ multiple=self.head_dim_padded,
430
+ )[None, head_idx]
431
+ blkdQaccum_shape = (self.tile_m * self.head_dim_padded,)
 
 
 
 
 
 
 
 
 
 
 
 
432
  gdQaccum = cute.local_tile(mdQaccum_cur, blkdQaccum_shape, (m_block,))
433
  gmem_thr_copy_dQaccum = gmem_tiled_copy_dQaccum.get_slice(tidx)
434
  tdQgdQaccum = gmem_thr_copy_dQaccum.partition_S(gdQaccum)
435
+ zero = cute.make_rmem_tensor_like(tdQgdQaccum)
436
  zero.fill(0.0)
437
  cute.copy(gmem_tiled_copy_dQaccum, zero, tdQgdQaccum)
438
 
439
+ LOG2_E = math.log2(math.e)
440
+ lse_log2 = lse * LOG2_E if lse != -Float32.inf else 0.0
441
+ if const_expr(mLSElog2 is not None):
442
+ mLSElog2_cur = seqlen.offset_batch(
443
+ mLSElog2, batch_idx, dim=2, padded=self.use_padded_offsets
444
+ )[None, head_idx]
445
+ gLSElog2 = cute.local_tile(mLSElog2_cur, (self.tile_m,), (m_block,))
446
  LOG2_E = math.log2(math.e)
447
+ if tidx < seqlen_q_rounded - m_block * self.tile_m:
448
+ gLSElog2[tidx] = lse_log2
449
+
450
+ if const_expr(mRowMax is not None):
451
+ assert mLSE is not None
452
+ # (s, n)
453
+ mRowMax_cur, mScaleP_cur = [
454
+ seqlen.offset_batch(mX, batch_idx, dim=3)[None, None, head_idx]
455
+ for mX in (mRowMax, mScaleP)
456
+ ]
457
+ # (tile_m, n)
458
+ gRowMax, gScaleP = [
459
+ cute.local_tile(mX, (self.tile_m,), (m_block, None))
460
+ for mX in (mRowMax_cur, mScaleP_cur)
461
+ ]
462
+
463
+ assert self.tile_m <= self.num_threads
464
+ if const_expr(self.tile_m == self.num_threads) or tidx < self.tile_m:
465
+ for n in cutlass.range(gRowMax.shape[1], unroll=4):
466
+ row_max = gRowMax[tidx, n]
467
+ scale = 0.0
468
+ if row_max != -Float32.inf and lse != -Float32.inf:
469
+ scale = softmax_scale_log2 * row_max - lse_log2
470
+ scale = cute.math.exp2(scale, fastmath=True)
471
+ gScaleP[tidx, n] = scale
build/torch-cuda/flash_bwd_sm100.py CHANGED
@@ -14,7 +14,6 @@ from cutlass.cute.nvgpu import cpasync, tcgen05
14
  import cutlass.utils.blackwell_helpers as sm100_utils_basic
15
  from cutlass.pipeline import PipelineAsync
16
 
17
- from .quack import activation
18
  from .quack import layout_utils
19
  from . import utils
20
  from .cute_dsl_utils import assume_tensor_aligned
@@ -36,14 +35,35 @@ from . import barrier
36
  from .named_barrier import NamedBarrierBwdSm100
37
  from .softmax import apply_score_mod_inner, apply_score_mod_bwd_inner
38
  from .block_sparsity import BlockSparseTensors
 
39
  from .block_sparse_utils import (
40
  get_total_q_block_count_bwd,
41
  get_block_sparse_iteration_info_bwd,
42
  get_m_block_from_iter_bwd,
43
- produce_block_sparse_q_loads_bwd_sm100,
 
44
  )
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  class FlashAttentionBackwardSm100:
48
  arch = 100
49
 
@@ -58,22 +78,28 @@ class FlashAttentionBackwardSm100:
58
  tile_n: int = 128,
59
  is_persistent: bool = False,
60
  deterministic: bool = False,
 
61
  cluster_size: int = 1,
62
  use_2cta_instrs: bool = False,
63
  score_mod: cutlass.Constexpr | None = None,
64
  score_mod_bwd: cutlass.Constexpr | None = None,
65
  mask_mod: cutlass.Constexpr | None = None,
66
  has_aux_tensors: cutlass.Constexpr = False,
67
- subtile_factor: cutlass.Constexpr[int] = 1,
 
68
  ):
69
- # padding head_dim to a multiple of 16 as k_block_size
70
- hdim_multiple_of = 16
 
 
71
  self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
72
  head_dim_v = head_dim_v if head_dim_v is not None else head_dim
73
  self.same_hdim_kv = head_dim == head_dim_v
74
  self.tile_hdimv = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
75
  self.check_hdim_oob = head_dim != self.tile_hdim
76
  self.check_hdim_v_oob = head_dim_v != self.tile_hdimv
 
 
77
 
78
  self.tile_m = tile_m
79
  self.tile_n = tile_n
@@ -81,14 +107,7 @@ class FlashAttentionBackwardSm100:
81
  assert self.tile_hdim <= 128 or (self.tile_hdim == 192 and self.tile_hdimv == 128)
82
  assert self.tile_hdimv <= 128
83
 
84
- self.use_2cta_instrs = bool(
85
- use_2cta_instrs
86
- and cluster_size == 2
87
- and not is_local
88
- and score_mod is None
89
- and score_mod_bwd is None
90
- and mask_mod is None
91
- )
92
  self.cta_group_size = 2 if self.use_2cta_instrs else 1
93
 
94
  assert self.tile_hdim != 192 or self.use_2cta_instrs, "Must use 2CTA for hdim 192"
@@ -111,19 +130,25 @@ class FlashAttentionBackwardSm100:
111
 
112
  assert cluster_size in (1, 2), "Only cluster_size=1 or 2 is supported"
113
  self.cluster_shape_mn = (cluster_size, 1)
 
 
 
114
  self.is_persistent = is_persistent
115
  self.is_causal = is_causal
116
  self.is_local = is_local
117
  self.qhead_per_kvhead = qhead_per_kvhead
118
  self.pack_gqa = False
119
  self.deterministic = deterministic
 
120
 
121
  # Score mod and mask mod support
122
  self.score_mod = score_mod
123
  self.score_mod_bwd = score_mod_bwd
124
  self.mask_mod = mask_mod
125
  self.has_aux_tensors = has_aux_tensors
126
- self.subtile_factor = subtile_factor
 
 
127
  # For score_mod, use vec_size=1 (like forward) to handle per-element indices
128
  if cutlass.const_expr(has_aux_tensors):
129
  self.vec_size: cutlass.Constexpr = 1
@@ -137,6 +162,16 @@ class FlashAttentionBackwardSm100:
137
  # Generally slower to use store dS in smem for dK, and doesn't work for 2cta
138
  self.use_smem_dS_for_mma_dK = False
139
 
 
 
 
 
 
 
 
 
 
 
140
  self.reduce_warp_ids = (0, 1, 2, 3)
141
  self.compute_warp_ids = (4, 5, 6, 7, 8, 9, 10, 11)
142
  self.mma_warp_id = 12
@@ -191,7 +226,6 @@ class FlashAttentionBackwardSm100:
191
  self.tmem_dQ_offset = 512 - self.tile_hdim // 2
192
  else:
193
  self.tmem_S_offset = 0
194
- self.tmem_P_offset = 0 # overlap with S
195
  self.tmem_dV_offset = self.tmem_S_offset + self.tile_n
196
  self.tmem_dP_offset = self.tmem_dV_offset + self.tile_hdimv
197
  self.tmem_dQ_offset = (
@@ -200,7 +234,14 @@ class FlashAttentionBackwardSm100:
200
  else self.tmem_dP_offset
201
  )
202
  self.tmem_dK_offset = self.tmem_dP_offset + self.tile_m
203
- self.tmem_dS_offset = self.tmem_dP_offset # overlap with dP
 
 
 
 
 
 
 
204
 
205
  if (not is_causal and not is_local) or deterministic:
206
  self.num_regs_reduce = 136 if self.use_2cta_instrs else 152
@@ -238,6 +279,7 @@ class FlashAttentionBackwardSm100:
238
  self.Q_stage = 1 if self.use_2cta_instrs else 2
239
  self.dO_stage = 1
240
  self.single_stage = 1
 
241
  # LSE_stage = Q_stage and dPsum_stage = dO_stage
242
  self.sdKVaccum_stage = 2
243
  # number of tma reduce adds per dQacc mma
@@ -453,20 +495,21 @@ class FlashAttentionBackwardSm100:
453
  mdK: cute.Tensor,
454
  mdV: cute.Tensor,
455
  softmax_scale: Float32,
456
- stream: cuda.CUstream,
457
  mCuSeqlensQ: Optional[cute.Tensor] = None,
458
  mCuSeqlensK: Optional[cute.Tensor] = None,
459
  mSeqUsedQ: Optional[cute.Tensor] = None,
460
  mSeqUsedK: Optional[cute.Tensor] = None,
461
- softcap: Float32 | float | None = None,
462
  window_size_left: Int32 | int | None = None,
463
  window_size_right: Int32 | int | None = None,
464
  mdQ_semaphore: Optional[cute.Tensor] = None,
465
  mdK_semaphore: Optional[cute.Tensor] = None,
466
  mdV_semaphore: Optional[cute.Tensor] = None,
467
- aux_tensors: Optional[list] = None,
468
  # Block-sparse tensors (Q direction - for iterating m_blocks per n_block):
469
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
 
 
 
470
  ):
471
  self.q_dtype = mQ.element_type
472
  self.k_dtype = mK.element_type
@@ -489,7 +532,9 @@ class FlashAttentionBackwardSm100:
489
  assert self.dk_dtype.width == 32, "Must accumulate dK in float precision for GQA"
490
  assert self.dv_dtype.width == 32, "Must accumulate dV in float precision for GQA"
491
 
492
- mdQaccum, mdK, mdV = [assume_tensor_aligned(t) for t in (mdQaccum, mdK, mdV)]
 
 
493
 
494
  # (b, s, n, h) --> (s, h, n, b) or (t, n, h) -> (t, h, n)
495
  QO_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 2, 1]
@@ -706,7 +751,11 @@ class FlashAttentionBackwardSm100:
706
  TileScheduler = SingleTileLPTBwdScheduler
707
  else:
708
  TileScheduler = SingleTileScheduler
709
- self.spt = (self.is_causal or self.is_local) and self.deterministic
 
 
 
 
710
  tile_sched_args = TileSchedulerArguments(
711
  cute.ceil_div(cute.size(mK.shape[0]), self.cta_tiler[0]), # num_blocks
712
  cute.size(mQ.shape[2]), # num_heads = num_query_heads
@@ -727,6 +776,7 @@ class FlashAttentionBackwardSm100:
727
  qhead_per_kvhead_packgqa=1, # pack_gqa disabled for bwd
728
  element_size=self.k_dtype.width // 8,
729
  is_persistent=self.is_persistent, # persistent mode not tested
 
730
  lpt=self.spt,
731
  head_swizzle=self.deterministic,
732
  )
@@ -782,7 +832,7 @@ class FlashAttentionBackwardSm100:
782
  cutlass.Int64, self.dQaccum_reduce_stage // 2
783
  ]
784
  tmem_holding_buf: Int32
785
- tmem_dealloc_mbar_ptr: cutlass.Int64
786
 
787
  # 2-CTA
788
  Qt_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.Q_stage]
@@ -852,6 +902,7 @@ class FlashAttentionBackwardSm100:
852
  S_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.single_stage]
853
  dP_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.single_stage]
854
  dS_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.single_stage]
 
855
  dKV_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.sdKVaccum_stage]
856
  dQ_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2]
857
  dQ_cluster_full_mbar_ptr: cute.struct.MemRange[
@@ -861,7 +912,7 @@ class FlashAttentionBackwardSm100:
861
  cutlass.Int64, self.dQaccum_reduce_stage // 2
862
  ]
863
  tmem_holding_buf: Int32
864
- tmem_dealloc_mbar_ptr: Int64
865
 
866
  sQ: cute.struct.Align[
867
  cute.struct.MemRange[cute.Uint8, sQ_alloc_bytes],
@@ -912,7 +963,7 @@ class FlashAttentionBackwardSm100:
912
  window_size_right = Int32(window_size_right)
913
 
914
  fastdiv_mods = None
915
- if const_expr(aux_tensors is not None):
916
  seqlen_q = cute.size(mQ.shape[0]) // (
917
  self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1
918
  )
@@ -921,21 +972,17 @@ class FlashAttentionBackwardSm100:
921
  seqlen_k_divmod = FastDivmodDivisor(seqlen_k)
922
  fastdiv_mods = (seqlen_q_divmod, seqlen_k_divmod)
923
  self.use_block_sparsity = cutlass.const_expr(blocksparse_tensors is not None)
924
-
925
- if const_expr(self.use_2cta_instrs):
926
- assert blocksparse_tensors is None, (
927
- "2-CTA mode does not support block sparsity. "
928
- "Please create kernel with use_2cta_instrs=False for block sparse attention."
929
- )
930
- assert window_size_left is None and window_size_right is None, (
931
- "2-CTA mode does not support window attention. "
932
- "Please create kernel with use_2cta_instrs=False for window attention."
933
  )
 
934
  # 2-CTA: 231424 and 1-CTA: 232448
935
  # print("SMEM: ", self.shared_storage.size_in_bytes())
936
- if const_expr(self.use_block_sparsity or aux_tensors is not None):
937
  assert all(x is None for x in (mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK)), (
938
- "Variable sequence length is not supported yet for blocksparse or aux tensors in bwd"
939
  )
940
 
941
  self.kernel(
@@ -997,7 +1044,7 @@ class FlashAttentionBackwardSm100:
997
  window_size_left,
998
  window_size_right,
999
  tile_sched_params,
1000
- aux_tensors,
1001
  fastdiv_mods,
1002
  blocksparse_tensors,
1003
  ).launch(
@@ -1009,6 +1056,16 @@ class FlashAttentionBackwardSm100:
1009
  min_blocks_per_mp=1,
1010
  )
1011
 
 
 
 
 
 
 
 
 
 
 
1012
  @cute.kernel
1013
  def kernel(
1014
  self,
@@ -1070,7 +1127,7 @@ class FlashAttentionBackwardSm100:
1070
  window_size_left: Optional[Int32],
1071
  window_size_right: Optional[Int32],
1072
  tile_sched_params: ParamsBase,
1073
- aux_tensors: Optional[list] = None,
1074
  fastdiv_mods=(None, None),
1075
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
1076
  ):
@@ -1146,11 +1203,11 @@ class FlashAttentionBackwardSm100:
1146
  * len((self.mma_warp_id, *self.compute_warp_ids, *self.reduce_warp_ids)),
1147
  )
1148
  tmem = cutlass.utils.TmemAllocator(
1149
- storage.tmem_holding_buf,
1150
  barrier_for_retrieve=tmem_alloc_barrier,
1151
  allocator_warp_id=self.mma_warp_id,
1152
  is_two_cta=self.use_2cta_instrs,
1153
- two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar_ptr,
1154
  )
1155
 
1156
  # UMMA producers and AsyncThread consumers
@@ -1160,6 +1217,7 @@ class FlashAttentionBackwardSm100:
1160
  pipeline_consumer_group_MMA_AsyncThread = cutlass.pipeline.CooperativeGroup(
1161
  cutlass.pipeline.Agent.Thread, len(self.compute_warp_ids) * self.cta_group_size
1162
  )
 
1163
  pipeline_S_P = cutlass.pipeline.PipelineUmmaAsync.create(
1164
  num_stages=1,
1165
  producer_group=pipeline_producer_group_MMA_AsyncThread,
@@ -1209,6 +1267,16 @@ class FlashAttentionBackwardSm100:
1209
  barrier_storage=storage.dS_mbar_ptr.data_ptr(),
1210
  cta_layout_vmnk=cluster_layout_vmnk,
1211
  )
 
 
 
 
 
 
 
 
 
 
1212
 
1213
  # TMA producer and UMMA consumers
1214
  pipeline_producer_group = cutlass.pipeline.CooperativeGroup(
@@ -1412,14 +1480,7 @@ class FlashAttentionBackwardSm100:
1412
  )
1413
  TileSchedulerCls = partial(self.tile_scheduler_cls.create, tile_sched_params)
1414
 
1415
- AttentionMaskCls = partial(
1416
- AttentionMask,
1417
- self.tile_m,
1418
- self.tile_n * self.cta_group_size,
1419
- swap_AB=True,
1420
- window_size_left=window_size_left,
1421
- window_size_right=window_size_right,
1422
- )
1423
  # EMPTY
1424
  # (15)
1425
  if warp_idx == self.empty_warp_id:
@@ -1440,6 +1501,7 @@ class FlashAttentionBackwardSm100:
1440
  block_info,
1441
  SeqlenInfoCls,
1442
  TileSchedulerCls,
 
1443
  )
1444
 
1445
  # LOAD
@@ -1536,6 +1598,7 @@ class FlashAttentionBackwardSm100:
1536
  pipeline_dKV,
1537
  pipeline_dP,
1538
  pipeline_dQ,
 
1539
  block_info,
1540
  SeqlenInfoCls,
1541
  TileSchedulerCls,
@@ -1574,6 +1637,7 @@ class FlashAttentionBackwardSm100:
1574
  pipeline_dS,
1575
  pipeline_dKV,
1576
  pipeline_dP,
 
1577
  dS_cluster_empty_mbar_ptr,
1578
  dS_cluster_full_mbar_ptr,
1579
  dQaccum_empty_mbar_ptr,
@@ -1592,7 +1656,7 @@ class FlashAttentionBackwardSm100:
1592
  tiled_copy_r2s_dKV,
1593
  mdK_semaphore,
1594
  mdV_semaphore,
1595
- aux_tensors,
1596
  fastdiv_mods,
1597
  blocksparse_tensors,
1598
  )
@@ -1631,6 +1695,7 @@ class FlashAttentionBackwardSm100:
1631
  block_info: BlockInfo,
1632
  SeqlenInfoCls: Callable,
1633
  TileSchedulerCls: Callable,
 
1634
  ):
1635
  cta_rank_in_cluster = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
1636
  dS_cluster_phase = Int32(0)
@@ -1648,9 +1713,20 @@ class FlashAttentionBackwardSm100:
1648
  process_tile = (
1649
  const_expr(not self.is_local and not self.is_varlen_q) or m_block_min < m_block_max
1650
  )
 
 
 
 
 
 
 
 
 
 
 
 
1651
 
1652
  if process_tile:
1653
- num_iters = m_block_max - m_block_min
1654
  for _ in cutlass.range(num_iters, unroll=1):
1655
  # Wait for dS_xchg from peer CTA
1656
  cute.arch.mbarrier_wait(dS_cluster_full_mbar_ptr, phase=dS_cluster_phase)
@@ -1668,11 +1744,11 @@ class FlashAttentionBackwardSm100:
1668
  @cute.jit
1669
  def load(
1670
  self,
1671
- thr_mma_S: cute.core.ThrMma,
1672
- thr_mma_dP: cute.core.ThrMma,
1673
- thr_mma_dV: cute.core.ThrMma,
1674
- thr_mma_dK: cute.core.ThrMma,
1675
- thr_mma_dQ: cute.core.ThrMma,
1676
  mQ: cute.Tensor,
1677
  mK: cute.Tensor,
1678
  mKt: Optional[cute.Tensor],
@@ -1756,6 +1832,7 @@ class FlashAttentionBackwardSm100:
1756
  )
1757
  head_idx_kv = head_idx // self.qhead_per_kvhead
1758
  n_block_cta_group = n_block // self.cta_group_size
 
1759
 
1760
  # GMEM tensors (varlen-aware)
1761
  mQ_cur = seqlen.offset_batch_Q(mQ, batch_idx, dim=3)[None, None, head_idx]
@@ -1835,6 +1912,7 @@ class FlashAttentionBackwardSm100:
1835
  single_stage=True,
1836
  )
1837
 
 
1838
  if const_expr(tma_atom_dOt is not None):
1839
  gdOt = cute.local_tile(
1840
  mdOt_cur, cute.select(self.mma_tiler_vdo, mode=[1, 2]), (None, 0)
@@ -1864,6 +1942,7 @@ class FlashAttentionBackwardSm100:
1864
  load_dO = copy_utils.tma_producer_copy_fn(load_dO, pipeline_dO)
1865
 
1866
  # (4) dK += dS.T @ Q (2-CTA: needs separate Qt load)
 
1867
  if const_expr(tma_atom_Qt is not None):
1868
  gQt = cute.local_tile(
1869
  mQt_cur, cute.select(self.mma_tiler_dsq, mode=[1, 2]), (0, None)
@@ -1880,6 +1959,7 @@ class FlashAttentionBackwardSm100:
1880
  load_Qt = copy_utils.tma_producer_copy_fn(load_Qt, pipeline_Qt)
1881
 
1882
  # (5) dQ = dS @ K
 
1883
  if const_expr(self.use_2cta_instrs):
1884
  gKt = cute.local_tile(
1885
  mKt_cur, cute.select(self.mma_tiler_dsk, mode=[1, 2]), (0, n_block_cta_group)
@@ -1904,59 +1984,18 @@ class FlashAttentionBackwardSm100:
1904
  # gdPsum = cute.logical_divide(gdPsum, (64,))[(None, block_in_cluster_coord_vmnk[1]), None]
1905
  # copy_stats = partial(cute.copy, copy_atom_stats, mcast_mask=q_do_mcast_mask)
1906
 
1907
- # some tiles might be empty due to block sparsity
1908
- if const_expr(self.use_block_sparsity):
1909
- total_m_block_cnt = get_total_q_block_count_bwd(
1910
- blocksparse_tensors,
1911
- batch_idx,
1912
- head_idx,
1913
- n_block,
1914
- subtile_factor=self.subtile_factor,
1915
- m_block_max=m_block_max,
1916
- )
1917
- process_tile = total_m_block_cnt > Int32(0)
1918
- else:
1919
  process_tile = (
1920
  const_expr(not self.is_local and not self.is_varlen_q)
1921
  or m_block_min < m_block_max
1922
  )
1923
 
1924
- if process_tile:
1925
- if const_expr(self.use_block_sparsity):
1926
- producer_state_Q_LSE, producer_state_dO_dPsum = (
1927
- produce_block_sparse_q_loads_bwd_sm100(
1928
- blocksparse_tensors,
1929
- batch_idx,
1930
- head_idx,
1931
- n_block,
1932
- producer_state_Q_LSE,
1933
- producer_state_dO_dPsum,
1934
- pipeline_Q,
1935
- pipeline_LSE,
1936
- pipeline_dO,
1937
- pipeline_dPsum,
1938
- load_K,
1939
- load_V,
1940
- load_Q,
1941
- load_dO,
1942
- copy_stats,
1943
- gLSE,
1944
- sLSE,
1945
- gdPsum,
1946
- sdPsum,
1947
- self.tma_copy_bytes["K"],
1948
- self.tma_copy_bytes["V"],
1949
- should_load_Q=should_load_Q,
1950
- should_load_dO=should_load_dO,
1951
- subtile_factor=self.subtile_factor,
1952
- m_block_max=m_block_max,
1953
- )
1954
- )
1955
- else:
1956
  first_m_block = m_block_min
1957
  if const_expr(self.use_2cta_instrs and self.tile_hdim == 192):
1958
  #### Prologue ####
1959
  assert should_load_Q and should_load_dO
 
1960
  # K & Q (for S)
1961
  pipeline_Q.producer_acquire(
1962
  producer_state_Q_Qt,
@@ -2060,6 +2099,10 @@ class FlashAttentionBackwardSm100:
2060
  pipeline_dO.producer_commit(producer_state_O_Ot)
2061
  producer_state_O_Ot.advance()
2062
 
 
 
 
 
2063
  else:
2064
  #### Prologue ####
2065
  if const_expr(should_load_Q):
@@ -2089,7 +2132,7 @@ class FlashAttentionBackwardSm100:
2089
  pipeline_dO.producer_acquire(
2090
  producer_state_dO_dPsum,
2091
  extra_tx_count=self.tma_copy_bytes["V"] + self.tma_copy_bytes["dO"]
2092
- if const_expr(tma_atom_dOt is not None)
2093
  else self.tma_copy_bytes["V"],
2094
  )
2095
  load_V(
@@ -2098,7 +2141,7 @@ class FlashAttentionBackwardSm100:
2098
  )
2099
  )
2100
  load_dO(first_m_block, producer_state=producer_state_dO_dPsum)
2101
- if const_expr(tma_atom_dOt is not None):
2102
  load_dOt(first_m_block, producer_state=producer_state_dO_dPsum)
2103
  pipeline_dO.producer_commit(producer_state_dO_dPsum)
2104
 
@@ -2122,7 +2165,7 @@ class FlashAttentionBackwardSm100:
2122
  #### Main Loop ####
2123
  for m_block in cutlass.range(m_block_min + 1, m_block_max, unroll=1):
2124
  if const_expr(should_load_Q):
2125
- if const_expr(tma_atom_Qt is not None):
2126
  pipeline_Qt.producer_acquire(producer_state_Qt)
2127
  load_Qt(m_block - 1, producer_state=producer_state_Qt)
2128
  pipeline_Qt.producer_commit(producer_state_Qt)
@@ -2149,11 +2192,11 @@ class FlashAttentionBackwardSm100:
2149
  pipeline_dO.producer_acquire(
2150
  producer_state_dO_dPsum,
2151
  extra_tx_count=self.tma_copy_bytes["dO"]
2152
- if const_expr(tma_atom_dOt is not None)
2153
  else 0,
2154
  )
2155
  load_dO(m_block, producer_state=producer_state_dO_dPsum)
2156
- if const_expr(tma_atom_dOt is not None):
2157
  load_dOt(m_block, producer_state=producer_state_dO_dPsum)
2158
  pipeline_dO.producer_commit(producer_state_dO_dPsum)
2159
 
@@ -2171,27 +2214,104 @@ class FlashAttentionBackwardSm100:
2171
 
2172
  #### Tail ####
2173
  if const_expr(should_load_Q):
2174
- if const_expr(tma_atom_Qt is not None):
2175
  pipeline_Qt.producer_acquire(producer_state_Qt)
2176
  load_Qt(m_block_max - 1, producer_state=producer_state_Qt)
2177
  pipeline_Qt.producer_commit(producer_state_Qt)
2178
  producer_state_Qt.advance()
2179
 
 
 
 
 
 
 
 
 
 
 
2180
  if const_expr(self.use_2cta_instrs and self.tile_hdim == 192):
2181
- pipeline_Q.producer_tail(producer_state_Q_Qt)
2182
- pipeline_LSE.producer_tail(producer_state_LSE)
2183
- pipeline_dO.producer_tail(producer_state_O_Ot)
2184
- pipeline_dPsum.producer_tail(producer_state_dPsum)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2185
  else:
2186
- if const_expr(should_load_Q):
2187
- pipeline_Q.producer_tail(producer_state_Q_LSE.clone())
2188
- pipeline_LSE.producer_tail(producer_state_Q_LSE)
2189
- if const_expr(tma_atom_Qt is not None):
2190
- pipeline_Qt.producer_tail(producer_state_Qt)
2191
- if const_expr(should_load_dO):
2192
- pipeline_dO.producer_tail(producer_state_dO_dPsum.clone())
2193
- pipeline_dPsum.producer_tail(producer_state_dO_dPsum)
2194
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2195
  tile_scheduler.prefetch_next_work()
2196
  tile_scheduler.advance_to_next_work()
2197
  work_tile = tile_scheduler.get_current_work()
@@ -2232,6 +2352,7 @@ class FlashAttentionBackwardSm100:
2232
  pipeline_dKV: PipelineAsync,
2233
  pipeline_dP: PipelineAsync,
2234
  pipeline_dQ: PipelineAsync,
 
2235
  block_info: BlockInfo,
2236
  SeqlenInfoCls: Callable,
2237
  TileSchedulerCls: Callable,
@@ -2343,6 +2464,10 @@ class FlashAttentionBackwardSm100:
2343
  )
2344
  producer_phase_acc = Int32(1) # For S & P, dP, dQ
2345
  producer_phase_dQ = Int32(1) # 2-CTA: separate phase for dQ pipeline
 
 
 
 
2346
  consumer_state_dS = cutlass.pipeline.make_pipeline_state(
2347
  cutlass.pipeline.PipelineUserType.Consumer, 1
2348
  )
@@ -2366,8 +2491,8 @@ class FlashAttentionBackwardSm100:
2366
  blocksparse_tensors,
2367
  batch_idx,
2368
  head_idx,
2369
- n_block,
2370
- subtile_factor=self.subtile_factor,
2371
  m_block_max=m_block_max,
2372
  )
2373
  process_tile = block_iter_count > Int32(0)
@@ -2392,7 +2517,7 @@ class FlashAttentionBackwardSm100:
2392
  # 4. dV = P.T @ dO
2393
  # 5. dQ = dS @ K
2394
 
2395
- main_loop_iters = m_block_max - m_block_min
2396
 
2397
  # empty waits
2398
  # pipeline_S_P.sync_object_empty.wait(0, producer_phase_acc)
@@ -2582,20 +2707,26 @@ class FlashAttentionBackwardSm100:
2582
  else:
2583
  if is_leader_cta and process_tile:
2584
  accumulate_dK = False
 
2585
  # -----------------------------------------------------------
2586
  ###### Prologue
2587
  # -----------------------------------------------------------
2588
- # 1. S = Q0 @ K.T
2589
- # 2. dP = V @ dOt.T
2590
- # 3. dV = P @ dO
2591
 
2592
- # 1) S = K @ Q
2593
  handle_Q = pipeline_Q_consumer.wait_and_advance()
2594
- pipeline_S_P.sync_object_empty.wait(0, producer_phase_acc)
 
 
 
2595
  mma_qk_fn(B_idx=handle_Q.index)
2596
  pipeline_S_P.sync_object_full.arrive(0, pipeline_S_P.producer_mask, cta_group)
 
 
2597
 
2598
- # 2) dP = V @ dOt.T
2599
  pipeline_dO.consumer_wait(consumer_state_dO)
2600
  pipeline_dP.sync_object_empty.wait(0, producer_phase_acc)
2601
  pipeline_dQ.sync_object_empty.wait(0, producer_phase_acc)
@@ -2604,19 +2735,22 @@ class FlashAttentionBackwardSm100:
2604
 
2605
  producer_phase_acc ^= 1
2606
  # 3) dV = P.T @ dO
2607
- pipeline_S_P.sync_object_empty.wait(0, producer_phase_acc)
2608
- mma_pdo_fn(B_idx=consumer_state_dO.index, zero_init=True)
2609
- pipeline_dO.consumer_release(consumer_state_dO)
2610
- consumer_state_dO.advance()
 
2611
 
2612
  # -----------------------------------------------------------
2613
  ###### MAIN LOOP
2614
  # -----------------------------------------------------------
2615
- # 1. S = K @ Q.T
2616
- # 2. dQ = dS @ K
2617
- # 3. dK = dS.T @ Q
2618
- # 4. dP = V @ dOt.T
2619
- # 5. dV = P.T @ dO
 
 
2620
 
2621
  # For block sparsity, we use block_iter_count; for dense, use m_block range
2622
  # MMA doesn't need actual m_block indices, just the iteration count
@@ -2630,10 +2764,25 @@ class FlashAttentionBackwardSm100:
2630
  for _ in cutlass.range(main_loop_iters, unroll=1):
2631
  # (1) S.T = K @ Q.T
2632
  handle_Q_next = pipeline_Q_consumer.wait_and_advance()
 
 
 
 
2633
  mma_qk_fn(B_idx=handle_Q_next.index)
2634
  pipeline_S_P.sync_object_full.arrive(
2635
  0, pipeline_S_P.producer_mask, cta_group
2636
  )
 
 
 
 
 
 
 
 
 
 
 
2637
 
2638
  # (2) dK += dS.T @ Q
2639
  pipeline_dS.consumer_wait(consumer_state_dS)
@@ -2647,22 +2796,34 @@ class FlashAttentionBackwardSm100:
2647
  pipeline_dS.consumer_release(consumer_state_dS)
2648
  consumer_state_dS.advance()
2649
 
2650
- # (4) dP = V @ dO.T
2651
  pipeline_dO.consumer_wait(consumer_state_dO)
2652
  pipeline_dQ.sync_object_empty.wait(0, producer_phase_acc)
2653
  mma_dov_fn(B_idx=consumer_state_dO.index)
2654
  pipeline_dP.sync_object_full.arrive(0, pipeline_dP.producer_mask, cta_group)
2655
 
2656
- # (5) dV += P.T @ dO
2657
  producer_phase_acc ^= 1
2658
- pipeline_S_P.sync_object_empty.wait(0, producer_phase_acc)
2659
- mma_pdo_fn(B_idx=consumer_state_dO.index, zero_init=False)
2660
- pipeline_dO.consumer_release(consumer_state_dO)
2661
- consumer_state_dO.advance()
 
2662
 
2663
  handle_Q = handle_Q_next
2664
 
2665
- pipeline_S_P.sync_object_full.arrive(0, pipeline_S_P.producer_mask, cta_group)
 
 
 
 
 
 
 
 
 
 
 
2666
 
2667
  # signal to the epilogue that dV is ready
2668
  # pipeline_dKV.producer_acquire(producer_state_dKV)
@@ -2753,13 +2914,17 @@ class FlashAttentionBackwardSm100:
2753
  n_block,
2754
  softmax_scale,
2755
  seqlen_info,
2756
- aux_tensors=None,
2757
  fastdiv_mods=(None, None),
2758
  ):
2759
  """Apply forward score modification for SM100 backward pass."""
2760
- # In bwd, S is computed as K @ Q.T so dimensions are (tile_n, tile_m)
2761
- cS = cute.make_identity_tensor((self.tile_n, self.tile_m))
2762
- cS = cute.domain_offset((n_block * self.tile_n, m_block * self.tile_m), cS)
 
 
 
 
2763
  tScS = thr_mma_S.partition_C(cS)
2764
  tScS_idx = thr_copy_t2r.partition_D(tScS)
2765
 
@@ -2772,7 +2937,7 @@ class FlashAttentionBackwardSm100:
2772
  softmax_scale,
2773
  self.vec_size,
2774
  self.qk_acc_dtype,
2775
- aux_tensors,
2776
  fastdiv_mods,
2777
  seqlen_info,
2778
  constant_q_idx=None,
@@ -2790,7 +2955,7 @@ class FlashAttentionBackwardSm100:
2790
  head_idx,
2791
  softmax_scale,
2792
  seqlen_info,
2793
- aux_tensors=None,
2794
  fastdiv_mods=(None, None),
2795
  ):
2796
  """Apply backward score modification (joint graph) for SM100."""
@@ -2804,7 +2969,7 @@ class FlashAttentionBackwardSm100:
2804
  softmax_scale,
2805
  self.vec_size,
2806
  self.qk_acc_dtype,
2807
- aux_tensors,
2808
  fastdiv_mods,
2809
  seqlen_info,
2810
  constant_q_idx=None,
@@ -2815,10 +2980,10 @@ class FlashAttentionBackwardSm100:
2815
  @cute.jit
2816
  def compute_loop(
2817
  self,
2818
- thr_mma_S: cute.core.ThrMma,
2819
- thr_mma_dP: cute.core.ThrMma,
2820
- thr_mma_dV: cute.core.ThrMma,
2821
- thr_mma_dK: cute.core.ThrMma,
2822
  tStS: cute.Tensor,
2823
  tdPtdP: cute.Tensor,
2824
  tdVtdV: cute.Tensor,
@@ -2835,6 +3000,7 @@ class FlashAttentionBackwardSm100:
2835
  pipeline_dS: PipelineAsync,
2836
  pipeline_dKV: PipelineAsync,
2837
  pipeline_dP: PipelineAsync,
 
2838
  dS_cluster_empty_mbar_ptr: cute.Pointer,
2839
  dS_cluster_full_mbar_ptr: cute.Pointer,
2840
  dQaccum_empty_mbar_ptr: cute.Pointer,
@@ -2853,7 +3019,7 @@ class FlashAttentionBackwardSm100:
2853
  tiled_copy_r2s_dKV: Optional[cute.TiledCopy],
2854
  mdK_semaphore: Optional[cute.Tensor],
2855
  mdV_semaphore: Optional[cute.Tensor],
2856
- aux_tensors: Optional[list] = None,
2857
  fastdiv_mods=(None, None),
2858
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
2859
  ):
@@ -2888,13 +3054,17 @@ class FlashAttentionBackwardSm100:
2888
 
2889
  tileP_f32_like = self.cta_tiler[1] // 32 * self.v_dtype.width
2890
  # tStS has shape ((128, 128), 1, 1), tStP has shape ((128, 64), 1, 1)
2891
- # tP overlap with tS
2892
  tStP = cute.composition(tStS, (cute.make_layout((self.tile_n, tileP_f32_like)), 1, 1))
2893
- tStP = cute.make_tensor(tStS.iterator, tStP.layout) # Otherwise the tmem address is wrong
 
 
 
2894
  tScS = thr_mma_S.partition_C(cute.make_identity_tensor(self.mma_tiler_kq[:2]))
2895
  tScP = cute.composition(tScS, (cute.make_layout((self.tile_n, tileP_f32_like)), 1, 1))
2896
- # tdS overlap with tdP
2897
  tdPtdS = cute.composition(tdPtdP, (cute.make_layout((self.tile_n, tileP_f32_like)), 1, 1))
 
 
 
2898
  tdPcdP = thr_mma_dP.partition_C(cute.make_identity_tensor(self.mma_tiler_vdo[:2]))
2899
  tdPcdS = cute.composition(tdPcdP, (cute.make_layout((self.tile_n, tileP_f32_like)), 1, 1))
2900
 
@@ -2953,6 +3123,12 @@ class FlashAttentionBackwardSm100:
2953
  cutlass.pipeline.PipelineUserType.Consumer, 1
2954
  )
2955
  # consumer_phase_S_P_dP = Int32(0)
 
 
 
 
 
 
2956
  producer_state_dS = pipeline.make_pipeline_state( # Our impl has shortcut for stage==1
2957
  cutlass.pipeline.PipelineUserType.Producer, 1
2958
  )
@@ -2971,31 +3147,58 @@ class FlashAttentionBackwardSm100:
2971
  while work_tile.is_valid_tile:
2972
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
2973
  seqlen = SeqlenInfoCls(batch_idx)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2974
  m_block_min, m_block_max = block_info.get_m_block_min_max(
2975
  seqlen, n_block // self.cluster_shape_mnk[0]
2976
  )
2977
  mask = AttentionMaskCls(seqlen)
2978
- n_block_for_cluster = n_block // self.cta_group_size
2979
  # TODO: condition mask_seqlen
2980
  mask_fn = partial(
2981
  mask.apply_mask_sm100_transposed,
2982
  tScS_t2r=tScS_t2r,
2983
  t0ScS_t2r=t0ScS_t2r,
2984
- n_block=n_block_for_cluster,
2985
  mask_seqlen=True,
2986
  mask_causal=self.is_causal,
2987
  mask_local=self.is_local,
2988
  mask_mod=self.mask_mod,
2989
  batch_idx=batch_idx,
2990
  head_idx=head_idx,
2991
- aux_tensors=aux_tensors,
2992
  fastdiv_mods=fastdiv_mods,
2993
  )
2994
 
2995
  # prefetch_LSE = not self.is_causal
2996
  prefetch_LSE = False
2997
- # some tiles might be empty due to block sparsity
 
 
 
 
 
 
 
 
2998
  if const_expr(self.use_block_sparsity):
 
2999
  (
3000
  curr_q_cnt,
3001
  curr_q_idx,
@@ -3006,22 +3209,19 @@ class FlashAttentionBackwardSm100:
3006
  blocksparse_tensors,
3007
  batch_idx,
3008
  head_idx,
3009
- n_block,
3010
- subtile_factor=self.subtile_factor,
3011
  m_block_max=m_block_max,
3012
  )
3013
  process_tile = loop_count > Int32(0)
3014
- else:
3015
- process_tile = (
3016
- const_expr(not self.is_local and not self.is_varlen_q)
3017
- or m_block_min < m_block_max
3018
- )
3019
- loop_count = m_block_max - m_block_min
3020
 
3021
  # Mainloop
3022
  # Block sparsity: iterate over sparse m_block count and derive actual m_block
3023
  # from Q_IDX/FULL_Q_IDX tensors. Dense: iterate m_block_min..m_block_max directly.
3024
  for iter_idx in cutlass.range(loop_count, unroll=1):
 
 
 
3025
  if const_expr(self.use_block_sparsity):
3026
  m_block, is_full_block = get_m_block_from_iter_bwd(
3027
  iter_idx,
@@ -3029,27 +3229,34 @@ class FlashAttentionBackwardSm100:
3029
  curr_q_idx,
3030
  curr_full_cnt,
3031
  curr_full_idx,
3032
- subtile_factor=self.subtile_factor,
3033
  m_block_max=m_block_max,
3034
  )
3035
  m_block_oob = m_block >= m_block_max
3036
- else:
3037
- m_block = m_block_min + iter_idx
3038
- m_block_oob = False
3039
- is_full_block = False
3040
  # Prefetch 1 stage of LSE
3041
  pipeline_LSE.consumer_wait(consumer_state_LSE)
3042
- tSrLSE_s2r = cute.make_fragment(tScS_t2r[None, 0, 0, 0].shape, Float32)
3043
  if const_expr(prefetch_LSE and not self.shuffle_LSE):
3044
  cute.autovec_copy(tSsLSE[None, 0, 0, 0, consumer_state_LSE.index], tSrLSE_s2r)
3045
 
3046
- pipeline_S_P.consumer_wait(consumer_state_S_P_dP)
 
 
 
3047
  # pipeline_S_P.sync_object_full.wait(0, consumer_phase_S_P_dP)
3048
  #### TMEM->RMEM (Load S from TMEM)
3049
- tSrS_t2r = cute.make_fragment(tScS_t2r.shape, Float32)
3050
  cute.copy(thr_copy_t2r, tStS_t2r, tSrS_t2r)
3051
 
3052
- if const_expr(self.tile_hdim == 192):
 
 
 
 
 
 
 
 
3053
  # Signal S tmem load completion using pipeline_S_P when hdim 192
3054
  # dP is overlapped with S
3055
  cute.arch.fence_view_async_tmem_load()
@@ -3080,7 +3287,7 @@ class FlashAttentionBackwardSm100:
3080
  n_block,
3081
  softmax_scale,
3082
  seqlen,
3083
- aux_tensors,
3084
  fastdiv_mods,
3085
  )
3086
 
@@ -3097,7 +3304,7 @@ class FlashAttentionBackwardSm100:
3097
  #### P = exp(S - LSE)
3098
  # ---------------------------------------------
3099
  lane_idx = cute.arch.lane_idx()
3100
- tSrP_r2t_f32 = cute.make_fragment(tScP_r2t.shape, Float32) # 64
3101
  tSrP_r2t = cute.recast_tensor(tSrP_r2t_f32, self.q_dtype)
3102
  for stage in cutlass.range_constexpr(num_stages):
3103
  tSrS_cur = tSrS_t2r[None, stage, 0, 0]
@@ -3125,10 +3332,14 @@ class FlashAttentionBackwardSm100:
3125
  tSrS_cur[2 * v + 1] = cute.math.exp2(tSrS_cur[2 * v + 1], fastmath=True)
3126
  utils.cvt_f16(tSrS_cur, tSrP_r2t[None, stage, 0, 0])
3127
  if const_expr(stage == 0):
3128
- cute.arch.fence_view_async_tmem_load()
3129
- # Without this barrier, we could have 1 warp writing to P in tmem while
3130
- # another warp is still reading S from tmem.
3131
- self.compute_sync_barrier.arrive_and_wait()
 
 
 
 
3132
  cute.copy(
3133
  thr_copy_r2t,
3134
  tSrP_r2t_f32[None, stage, None, None],
@@ -3137,12 +3348,22 @@ class FlashAttentionBackwardSm100:
3137
 
3138
  cute.arch.fence_view_async_tmem_store()
3139
  cute.arch.fence_view_async_shared()
3140
- self.compute_sync_barrier.arrive_and_wait()
3141
- if const_expr(not self.tile_hdim == 192):
 
 
 
 
 
 
 
 
3142
  # Signal tmem store P completion with pipeline_S_P
3143
  with cute.arch.elect_one():
3144
  pipeline_S_P.consumer_release(consumer_state_S_P_dP)
3145
  # pipeline_S_P.sync_object_empty.arrive(0, pipeline_S_P.consumer_mask)
 
 
3146
  pipeline_LSE.consumer_release(consumer_state_LSE)
3147
  consumer_state_LSE.advance()
3148
  # ---------------------------------------------
@@ -3157,10 +3378,12 @@ class FlashAttentionBackwardSm100:
3157
 
3158
  ##### dS.T = P.T * (dP.T - Psum)
3159
  for stage in cutlass.range_constexpr(num_stages):
3160
- tdPrdP_t2r = cute.make_fragment(tScS_t2r[None, 0, None, None].shape, Float32)
3161
  cute.copy(thr_copy_t2r, tdPtdP_t2r[None, stage, None, None], tdPrdP_t2r)
3162
  cute.arch.fence_view_async_tmem_load()
3163
- self.compute_sync_barrier.arrive_and_wait()
 
 
3164
  tdPrdP_cur = tdPrdP_t2r[None, 0, 0]
3165
  tSrS_cur = tSrS_t2r[None, stage, 0, 0]
3166
  tSsdPsum_cur = tSsdPsum[None, stage, 0, 0, consumer_state_dPsum.index]
@@ -3177,10 +3400,8 @@ class FlashAttentionBackwardSm100:
3177
  utils.shuffle_sync(tSrdPsum, offset=2 * v),
3178
  utils.shuffle_sync(tSrdPsum, offset=2 * v + 1),
3179
  )
3180
- tdPrdP_cur[2 * v], tdPrdP_cur[2 * v + 1] = (
3181
- activation.sub_packed_f32x2(
3182
- (tdPrdP_cur[2 * v], tdPrdP_cur[2 * v + 1]), dPsum_pair
3183
- )
3184
  )
3185
  tdPrdP_cur[2 * v], tdPrdP_cur[2 * v + 1] = cute.arch.mul_packed_f32x2(
3186
  (tSrS_cur[2 * v], tSrS_cur[2 * v + 1]),
@@ -3189,9 +3410,12 @@ class FlashAttentionBackwardSm100:
3189
 
3190
  if const_expr(self.score_mod_bwd is not None):
3191
  tSrS_pre_cur = tSrS_pre[None, stage, 0, 0]
3192
- cS_bwd = cute.make_identity_tensor((self.tile_n, self.tile_m))
 
 
3193
  cS_bwd = cute.domain_offset(
3194
- (n_block * self.tile_n, m_block * self.tile_m), cS_bwd
 
3195
  )
3196
  tScS_bwd = thr_mma_S.partition_C(cS_bwd)
3197
  tScS_idx_bwd = thr_copy_t2r.partition_D(tScS_bwd)
@@ -3204,7 +3428,7 @@ class FlashAttentionBackwardSm100:
3204
  head_idx,
3205
  softmax_scale,
3206
  seqlen,
3207
- aux_tensors,
3208
  fastdiv_mods,
3209
  )
3210
  # Zero out OOB positions (kv_idx >= seqlen_k) after score_mod_bwd
@@ -3252,7 +3476,13 @@ class FlashAttentionBackwardSm100:
3252
  cute.autovec_copy(tdPrdS_xchg, tRS_sdS_xchg[None, 0])
3253
 
3254
  cute.arch.fence_view_async_shared()
3255
- self.compute_sync_barrier.arrive_and_wait()
 
 
 
 
 
 
3256
  pipeline_dPsum.consumer_release(consumer_state_dPsum)
3257
  consumer_state_dPsum.advance()
3258
  # when 2cta hdim 128, pipeline_dS also signals S tmem load completion so is deferred
@@ -3413,12 +3643,67 @@ class FlashAttentionBackwardSm100:
3413
  tile_scheduler.advance_to_next_work()
3414
  work_tile = tile_scheduler.get_current_work()
3415
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3416
  @cute.jit
3417
  def dQacc_reduce(
3418
  self,
3419
  mdQaccum: cute.Tensor,
3420
  sdQaccum: cute.Tensor,
3421
- thr_mma_dQ: cute.core.ThrMma,
3422
  tdQtdQ: cute.Tensor,
3423
  pipeline_dQ: PipelineAsync,
3424
  dQaccum_empty_mbar_ptr: Optional[cute.Pointer],
@@ -3470,6 +3755,7 @@ class FlashAttentionBackwardSm100:
3470
  while work_tile.is_valid_tile:
3471
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
3472
  n_block_cta_group = n_block // self.cta_group_size # for 2cta
 
3473
  seqlen = SeqlenInfoCls(batch_idx)
3474
  m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block_cta_group)
3475
  if const_expr(not seqlen.has_cu_seqlens_q):
@@ -3485,13 +3771,43 @@ class FlashAttentionBackwardSm100:
3485
  )
3486
 
3487
  if const_expr(self.deterministic):
 
3488
  mdQ_semaphore_cur = mdQ_semaphore[None, None, head_idx, batch_idx]
3489
 
3490
- # delay_semaphore_release = self.is_causal and not self.tile_hdim == 192
3491
- delay_semaphore_release = not self.tile_hdim == 192
3492
 
3493
- # some tiles might be empty due to block sparsity
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3494
  if const_expr(self.use_block_sparsity):
 
3495
  (
3496
  curr_q_cnt,
3497
  curr_q_idx,
@@ -3502,22 +3818,30 @@ class FlashAttentionBackwardSm100:
3502
  blocksparse_tensors,
3503
  batch_idx,
3504
  head_idx,
3505
- n_block,
3506
- subtile_factor=self.subtile_factor,
3507
  m_block_max=m_block_max,
3508
  )
3509
  process_tile = loop_count > Int32(0)
3510
- else:
3511
- process_tile = (
3512
- const_expr(not self.is_local and not self.is_varlen_q)
3513
- or m_block_min < m_block_max
3514
- )
3515
- loop_count = m_block_max - m_block_min
 
 
 
 
 
 
3516
 
3517
  # dQacc_reduce mainloop
3518
  # Block sparsity: iterate over sparse m_block count and derive actual m_block
3519
  # from Q_IDX/FULL_Q_IDX tensors. Dense: iterate m_block_min..m_block_max directly.
3520
  for iter_idx in cutlass.range(loop_count, unroll=1):
 
 
3521
  if const_expr(self.use_block_sparsity):
3522
  m_block, _ = get_m_block_from_iter_bwd(
3523
  iter_idx,
@@ -3525,16 +3849,13 @@ class FlashAttentionBackwardSm100:
3525
  curr_q_idx,
3526
  curr_full_cnt,
3527
  curr_full_idx,
3528
- subtile_factor=self.subtile_factor,
3529
  m_block_max=m_block_max,
3530
  )
3531
- if m_block_max > 0:
3532
- m_block = cutlass.min(m_block, m_block_max - 1)
3533
- else:
3534
- m_block = m_block_min + iter_idx
3535
  pipeline_dQ.consumer_wait(dQ_consumer_state)
3536
  # TMEM -> RMEM
3537
- tdQrdQ_t2r = cute.make_fragment(tdQrdQ_t2r_shape, Float32)
3538
  cute.copy(thr_copy_t2r, tdQtdQ_t2r, tdQrdQ_t2r)
3539
  cute.arch.fence_view_async_tmem_load()
3540
  cute.arch.sync_warp()
@@ -3542,6 +3863,8 @@ class FlashAttentionBackwardSm100:
3542
  pipeline_dQ.consumer_release(dQ_consumer_state)
3543
  dQ_consumer_state.advance()
3544
 
 
 
3545
  gdQaccum_cur = gdQaccum[None, None, m_block]
3546
 
3547
  tdQrdQ_shape = (
@@ -3559,22 +3882,27 @@ class FlashAttentionBackwardSm100:
3559
  cute.arch.fence_view_async_shared()
3560
  # semaphore acquire
3561
  if const_expr(self.deterministic and stage == 0):
3562
- if const_expr(self.spt):
3563
- _, n_block_max_for_m_block = block_info.get_n_block_min_max(
3564
- seqlen, m_block
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3565
  )
3566
- lock_value = n_block_max_for_m_block - 1 - n_block_cta_group
3567
- else:
3568
- lock_value = n_block_cta_group
3569
- barrier.wait_eq(
3570
- mdQ_semaphore_cur[(m_block, None)].iterator,
3571
- tidx,
3572
- cta_rank_in_cluster,
3573
- lock_value,
3574
- )
3575
  self.reduce_sync_barrier.arrive_and_wait()
3576
  # Copy from shared memory to global memory
3577
- if is_tma_warp:
3578
  with cute.arch.elect_one():
3579
  copy_utils.cpasync_reduce_bulk_add_f32(
3580
  sdQaccum[None, smem_idx].iterator,
@@ -3583,20 +3911,12 @@ class FlashAttentionBackwardSm100:
3583
  )
3584
  cute.arch.cp_async_bulk_commit_group()
3585
  cute.arch.cp_async_bulk_wait_group(self.sdQaccum_stage - 1, read=read_flag)
 
 
 
3586
  self.reduce_sync_barrier.arrive_and_wait()
3587
  dQ_tma_store_producer_state.advance()
3588
- # Directly add to gmem, much slower
3589
- # tdQgdQ = thr_copy_dQaccum_r2s.partition_D(gdQaccum[None, stage, m_block])
3590
- # assert cute.size(tdQrdQ_r2s) == cute.size(tdQgdQ)
3591
- # for i in cutlass.range(cute.size(tdQrdQ_r2s) // 4, unroll_full=True):
3592
- # copy_utils.atomic_add_fp32x4(
3593
- # tdQrdQ_r2s[4 * i],
3594
- # tdQrdQ_r2s[4 * i + 1],
3595
- # tdQrdQ_r2s[4 * i + 2],
3596
- # tdQrdQ_r2s[4 * i + 3],
3597
- # utils.elem_pointer(tdQgdQ, 4 * i),
3598
- # )
3599
- # semaphore release for prior m_block
3600
  if const_expr(self.deterministic and stage == 0 and delay_semaphore_release):
3601
  if m_block > m_block_min:
3602
  barrier.arrive_inc(
@@ -3618,12 +3938,16 @@ class FlashAttentionBackwardSm100:
3618
  # NOTE: arrive_inc calls red_release which issues membar
3619
  if const_expr(self.deterministic and not delay_semaphore_release):
3620
  if const_expr(self.sdQaccum_stage > 1 and not self.tile_hdim == 192):
3621
- if is_tma_warp:
3622
  cute.arch.cp_async_bulk_wait_group(0, read=read_flag)
3623
  self.reduce_sync_barrier.arrive_and_wait()
3624
- barrier.arrive_inc(
3625
- mdQ_semaphore_cur[m_block, None].iterator, tidx, cta_rank_in_cluster, 1
3626
- )
 
 
 
 
3627
 
3628
  if process_tile:
3629
  if is_tma_warp:
@@ -3639,7 +3963,10 @@ class FlashAttentionBackwardSm100:
3639
  )
3640
 
3641
  if const_expr(
3642
- self.deterministic and not self.spt and block_info.window_size_left is not None
 
 
 
3643
  ):
3644
  m_block_global_max = cute.ceil_div(seqlen.seqlen_q, self.tile_m)
3645
  for m_block in cutlass.range(m_block_max, m_block_global_max, unroll=1):
@@ -3650,6 +3977,9 @@ class FlashAttentionBackwardSm100:
3650
  tile_scheduler.advance_to_next_work()
3651
  work_tile = tile_scheduler.get_current_work()
3652
 
 
 
 
3653
  @cute.jit
3654
  def epilogue_dKV(
3655
  self,
@@ -3659,8 +3989,8 @@ class FlashAttentionBackwardSm100:
3659
  head_idx: Int32,
3660
  n_block: Int32,
3661
  seqlen,
3662
- thr_mma_dV: cute.core.ThrMma,
3663
- thr_mma_dK: cute.core.ThrMma,
3664
  tdVtdV: cute.Tensor,
3665
  tdKtdK: cute.Tensor,
3666
  mdV: cute.Tensor,
@@ -3678,13 +4008,22 @@ class FlashAttentionBackwardSm100:
3678
  mdV_cur = seqlen.offset_batch_K(mdV, batch_idx, dim=3)[None, None, head_idx]
3679
  mdK_cur = seqlen.offset_batch_K(mdK, batch_idx, dim=3)[None, None, head_idx]
3680
 
3681
- tmem_load_atom = cute.make_copy_atom(
3682
- tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(16)), Float32
 
 
 
 
 
 
 
 
 
3683
  )
3684
  # dV
3685
  pipeline_dKV.consumer_wait(consumer_state_dKV)
3686
 
3687
- tiled_tmem_ld_dV = tcgen05.make_tmem_copy(tmem_load_atom, tdVtdV)
3688
  thr_tmem_ld_dV = tiled_tmem_ld_dV.get_slice(tidx)
3689
 
3690
  tdVtdV_t2r_p = thr_tmem_ld_dV.partition_S(tdVtdV)
@@ -3696,7 +4035,7 @@ class FlashAttentionBackwardSm100:
3696
 
3697
  tdVcdV_t2r_p = thr_tmem_ld_dV.partition_D(tdVcdV_tensor)
3698
  tdVcdV_t2r = self.split_wg(tdVcdV_t2r_p, wg_idx, num_wg)
3699
- tdVrdV_t2r = cute.make_fragment(tdVcdV_t2r.shape, Float32)
3700
 
3701
  cute.copy(thr_tmem_ld_dV, tdVtdV_t2r, tdVrdV_t2r)
3702
  cute.arch.fence_view_async_tmem_load()
@@ -3713,7 +4052,7 @@ class FlashAttentionBackwardSm100:
3713
  tiler_mn=tiled_tmem_ld_dV.tiler_mn,
3714
  )
3715
 
3716
- tdVrdV_r2s = cute.make_fragment(tdVrdV_t2r.shape, self.dv_dtype)
3717
  for i in cutlass.range_constexpr(cute.size(tdVrdV_t2r, mode=[1])):
3718
  dV_vec = tdVrdV_t2r[(None, i, 0, 0)].load()
3719
  tdVrdV_r2s[(None, i, 0, 0)].store(dV_vec.to(self.dv_dtype))
@@ -3725,8 +4064,11 @@ class FlashAttentionBackwardSm100:
3725
  tdVgdV_r2g_p = thr_tmem_ld_dV.partition_D(tdVgdV)
3726
  tdVgdV_r2g = self.split_wg(tdVgdV_r2g_p, wg_idx, num_wg)
3727
 
 
 
 
3728
  if tidx < seqlen.seqlen_k - self.tile_n * n_block:
3729
- cute.copy(tiled_gmem_store_dV, tdVrdV_r2s, tdVgdV_r2g)
3730
 
3731
  cute.arch.sync_warp()
3732
  with cute.arch.elect_one():
@@ -3736,7 +4078,7 @@ class FlashAttentionBackwardSm100:
3736
  # dK
3737
  pipeline_dKV.consumer_wait(consumer_state_dKV)
3738
 
3739
- tiled_tmem_ld_dK = tcgen05.make_tmem_copy(tmem_load_atom, tdKtdK)
3740
  thr_tmem_ld_dK = tiled_tmem_ld_dK.get_slice(tidx)
3741
 
3742
  tdKtdK_t2r_p = thr_tmem_ld_dK.partition_S(tdKtdK)
@@ -3748,7 +4090,7 @@ class FlashAttentionBackwardSm100:
3748
 
3749
  tdKcdK_t2r_p = thr_tmem_ld_dK.partition_D(tdKcdK_tensor)
3750
  tdKcdK_t2r = self.split_wg(tdKcdK_t2r_p, wg_idx, num_wg)
3751
- tdKrdK_t2r = cute.make_fragment(tdKcdK_t2r.shape, Float32)
3752
 
3753
  cute.copy(tiled_tmem_ld_dK, tdKtdK_t2r, tdKrdK_t2r)
3754
  cute.arch.fence_view_async_tmem_load()
@@ -3766,7 +4108,7 @@ class FlashAttentionBackwardSm100:
3766
  tiler_mn=tiled_tmem_ld_dK.tiler_mn,
3767
  )
3768
 
3769
- tdKrdK_r2s = cute.make_fragment(tdKrdK_t2r.shape, self.dk_dtype)
3770
 
3771
  for i in cutlass.range_constexpr(cute.size(tdKrdK_t2r, mode=[1])):
3772
  dK_vec = tdKrdK_t2r[(None, i, 0, 0)].load() * softmax_scale
@@ -3779,14 +4121,33 @@ class FlashAttentionBackwardSm100:
3779
  tdKgdK_r2g_p = thr_tmem_ld_dK.partition_D(tdKgdK)
3780
  tdKgdK_r2g = self.split_wg(tdKgdK_r2g_p, wg_idx, num_wg)
3781
 
 
 
 
3782
  if tidx < seqlen.seqlen_k - self.tile_n * n_block:
3783
- cute.copy(tiled_gmem_store_dK, tdKrdK_r2s, tdKgdK_r2g)
3784
 
3785
  cute.arch.sync_warp()
3786
  with cute.arch.elect_one():
3787
  pipeline_dKV.consumer_release(consumer_state_dKV)
3788
  return consumer_state_dKV
3789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3790
  @cute.jit
3791
  def epilogue_dK_or_dV_tma(
3792
  self,
@@ -3795,7 +4156,7 @@ class FlashAttentionBackwardSm100:
3795
  head_idx: Int32,
3796
  n_block: Int32,
3797
  seqlen,
3798
- thr_mma: cute.core.ThrMma,
3799
  tdKVtdKV: cute.Tensor,
3800
  mdKV: cute.Tensor,
3801
  sdKV: cute.Tensor,
@@ -3861,6 +4222,7 @@ class FlashAttentionBackwardSm100:
3861
 
3862
  deterministic_KV = self.deterministic and self.qhead_per_kvhead > 1
3863
  if const_expr(deterministic_KV):
 
3864
  mdKV_semaphore_cur = mdKV_semaphore[n_block, None, head_idx_kv, batch_idx]
3865
 
3866
  if const_expr(not self.dKV_postprocess):
@@ -3913,7 +4275,7 @@ class FlashAttentionBackwardSm100:
3913
  if const_expr(num_epi_stages > 1):
3914
  tdKVcdKV_t2r = tdKVcdKV_t2r[None, epi_stage]
3915
 
3916
- tdKVrdKV_t2r = cute.make_fragment(tdKVcdKV_t2r.shape, Float32)
3917
 
3918
  assert cute.size(tdKVrdKV_t2r) == cute.size(tdKVtdKV_t2r) // cute.arch.WARP_SIZE, (
3919
  "RMEM<->TMEM fragment size mismatch"
@@ -3929,7 +4291,7 @@ class FlashAttentionBackwardSm100:
3929
  tdKVrdKV_t2r[2 * i], tdKVrdKV_t2r[2 * i + 1] = cute.arch.mul_packed_f32x2(
3930
  (tdKVrdKV_t2r[2 * i], tdKVrdKV_t2r[2 * i + 1]), (scale, scale)
3931
  )
3932
- tdKVrdKV = cute.make_fragment(tdKVrdKV_t2r.shape, dtype) # (32 columns)
3933
  tdKVrdKV.store(tdKVrdKV_t2r.load().to(dtype))
3934
 
3935
  # RMEM -> SMEM -- copy, fence and barrier
 
14
  import cutlass.utils.blackwell_helpers as sm100_utils_basic
15
  from cutlass.pipeline import PipelineAsync
16
 
 
17
  from .quack import layout_utils
18
  from . import utils
19
  from .cute_dsl_utils import assume_tensor_aligned
 
35
  from .named_barrier import NamedBarrierBwdSm100
36
  from .softmax import apply_score_mod_inner, apply_score_mod_bwd_inner
37
  from .block_sparsity import BlockSparseTensors
38
+ from .utils import AuxData
39
  from .block_sparse_utils import (
40
  get_total_q_block_count_bwd,
41
  get_block_sparse_iteration_info_bwd,
42
  get_m_block_from_iter_bwd,
43
+ produce_block_sparse_q_loads_bwd_sm100_2cta_hdim192,
44
+ produce_block_sparse_q_loads_bwd_sm100_default,
45
  )
46
 
47
 
48
+ # NOTE [hdim64 dedicated P/dS TMEM slots]
49
+ # The 1-CTA hdim64 backward uses TMEM columns [0, 384), leaving room for two 64-column
50
+ # slots. With split_P_dS, P and dS live there instead of aliasing S and dP:
51
+ # * S is released as soon as the compute warps have loaded it, so the MMA warp issues
52
+ # QK_{t+1} while the softmax of tile t is still running; dV += P.T @ dO for tile t
53
+ # then trails QK_{t+1} and is gated by its own pipeline_P.
54
+ # * The compute-wide barriers that ordered the P-over-S and dS-over-dP overwrites are
55
+ # gone. They were cross-warp hazards: a warp's P (dS) columns overlap S (dP) lanes
56
+ # that another warp still has to load.
57
+ # * The remaining compute-wide barriers before pipeline arrivals become sync_warp: every
58
+ # mbarrier signalled from the compute warps expects one arrival per warp, so the
59
+ # mbarrier itself is the cross-warp join. This is 1-CTA only: at 2-CTA the barrier
60
+ # before the dS commit also orders every warp's sdS_xchg write ahead of the
61
+ # single-thread DSMEM copy to the peer CTA.
62
+ # The S handshake keeps its own phase/state (producer_phase_S, consumer_state_S) so its
63
+ # parity never depends on where the other pipelines flip. Perf evidence in PR #2804; the
64
+ # aliased layout is what every other shape runs.
65
+
66
+
67
  class FlashAttentionBackwardSm100:
68
  arch = 100
69
 
 
78
  tile_n: int = 128,
79
  is_persistent: bool = False,
80
  deterministic: bool = False,
81
+ spt: Optional[bool] = None,
82
  cluster_size: int = 1,
83
  use_2cta_instrs: bool = False,
84
  score_mod: cutlass.Constexpr | None = None,
85
  score_mod_bwd: cutlass.Constexpr | None = None,
86
  mask_mod: cutlass.Constexpr | None = None,
87
  has_aux_tensors: cutlass.Constexpr = False,
88
+ q_subtile_factor: cutlass.Constexpr[int] = 1,
89
+ kv_subtile_factor: cutlass.Constexpr[int] = 1,
90
  ):
91
+ # Pad head_dim to a multiple of 32 (k_block_size). Must stay a multiple of 32 so
92
+ # that the dQ accumulator reduce (dQ_reduce_ncol up to 32) tiles evenly and matches
93
+ # both the preprocess kernel and the interface's head_dim_rounded (also mult of 32).
94
+ hdim_multiple_of = 32
95
  self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
96
  head_dim_v = head_dim_v if head_dim_v is not None else head_dim
97
  self.same_hdim_kv = head_dim == head_dim_v
98
  self.tile_hdimv = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
99
  self.check_hdim_oob = head_dim != self.tile_hdim
100
  self.check_hdim_v_oob = head_dim_v != self.tile_hdimv
101
+ self.head_dim = head_dim
102
+ self.head_dim_v = head_dim_v
103
 
104
  self.tile_m = tile_m
105
  self.tile_n = tile_n
 
107
  assert self.tile_hdim <= 128 or (self.tile_hdim == 192 and self.tile_hdimv == 128)
108
  assert self.tile_hdimv <= 128
109
 
110
+ self.use_2cta_instrs = bool(use_2cta_instrs and cluster_size == 2)
 
 
 
 
 
 
 
111
  self.cta_group_size = 2 if self.use_2cta_instrs else 1
112
 
113
  assert self.tile_hdim != 192 or self.use_2cta_instrs, "Must use 2CTA for hdim 192"
 
130
 
131
  assert cluster_size in (1, 2), "Only cluster_size=1 or 2 is supported"
132
  self.cluster_shape_mn = (cluster_size, 1)
133
+ # The MMA warp's producer_phase_acc (shared by the S/P, dP and dQ empty waits in the
134
+ # aliased layout) is only consistent across one work tile per CTA.
135
+ assert not is_persistent, "persistent tile scheduling is not supported"
136
  self.is_persistent = is_persistent
137
  self.is_causal = is_causal
138
  self.is_local = is_local
139
  self.qhead_per_kvhead = qhead_per_kvhead
140
  self.pack_gqa = False
141
  self.deterministic = deterministic
142
+ self.spt_override = spt
143
 
144
  # Score mod and mask mod support
145
  self.score_mod = score_mod
146
  self.score_mod_bwd = score_mod_bwd
147
  self.mask_mod = mask_mod
148
  self.has_aux_tensors = has_aux_tensors
149
+ self.q_subtile_factor = q_subtile_factor
150
+ self.kv_subtile_factor = kv_subtile_factor
151
+ assert self.kv_subtile_factor == 1 or self.kv_subtile_factor % self.cta_group_size == 0
152
  # For score_mod, use vec_size=1 (like forward) to handle per-element indices
153
  if cutlass.const_expr(has_aux_tensors):
154
  self.vec_size: cutlass.Constexpr = 1
 
162
  # Generally slower to use store dS in smem for dK, and doesn't work for 2cta
163
  self.use_smem_dS_for_mma_dK = False
164
 
165
+ # See NOTE [hdim64 dedicated P/dS TMEM slots]
166
+ self.split_P_dS = (
167
+ cluster_size == 1
168
+ and self.tile_m == 128
169
+ and self.tile_n == 128
170
+ and self.tile_hdim == 64
171
+ and self.tile_hdimv == 64
172
+ and not self.use_smem_dS_for_mma_dK
173
+ )
174
+
175
  self.reduce_warp_ids = (0, 1, 2, 3)
176
  self.compute_warp_ids = (4, 5, 6, 7, 8, 9, 10, 11)
177
  self.mma_warp_id = 12
 
226
  self.tmem_dQ_offset = 512 - self.tile_hdim // 2
227
  else:
228
  self.tmem_S_offset = 0
 
229
  self.tmem_dV_offset = self.tmem_S_offset + self.tile_n
230
  self.tmem_dP_offset = self.tmem_dV_offset + self.tile_hdimv
231
  self.tmem_dQ_offset = (
 
234
  else self.tmem_dP_offset
235
  )
236
  self.tmem_dK_offset = self.tmem_dP_offset + self.tile_m
237
+ if self.split_P_dS:
238
+ # P/dS are packed 16-bit over tile_m columns -> tile_m // 2 fp32 columns
239
+ self.tmem_P_offset = self.tmem_dK_offset + self.tile_hdim
240
+ self.tmem_dS_offset = self.tmem_P_offset + self.tile_m // 2
241
+ assert self.tmem_dS_offset + self.tile_m // 2 <= self.tmem_alloc_cols
242
+ else:
243
+ self.tmem_P_offset = self.tmem_S_offset # overlap with S
244
+ self.tmem_dS_offset = self.tmem_dP_offset # overlap with dP
245
 
246
  if (not is_causal and not is_local) or deterministic:
247
  self.num_regs_reduce = 136 if self.use_2cta_instrs else 152
 
279
  self.Q_stage = 1 if self.use_2cta_instrs else 2
280
  self.dO_stage = 1
281
  self.single_stage = 1
282
+ self.P_stage = 1 if self.split_P_dS else 0 # dedicated P pipeline, else P aliases S
283
  # LSE_stage = Q_stage and dPsum_stage = dO_stage
284
  self.sdKVaccum_stage = 2
285
  # number of tma reduce adds per dQacc mma
 
495
  mdK: cute.Tensor,
496
  mdV: cute.Tensor,
497
  softmax_scale: Float32,
 
498
  mCuSeqlensQ: Optional[cute.Tensor] = None,
499
  mCuSeqlensK: Optional[cute.Tensor] = None,
500
  mSeqUsedQ: Optional[cute.Tensor] = None,
501
  mSeqUsedK: Optional[cute.Tensor] = None,
 
502
  window_size_left: Int32 | int | None = None,
503
  window_size_right: Int32 | int | None = None,
504
  mdQ_semaphore: Optional[cute.Tensor] = None,
505
  mdK_semaphore: Optional[cute.Tensor] = None,
506
  mdV_semaphore: Optional[cute.Tensor] = None,
507
+ aux_data: AuxData = AuxData(),
508
  # Block-sparse tensors (Q direction - for iterating m_blocks per n_block):
509
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
510
+ mCuTotalMBlocks: Optional[cute.Tensor] = None,
511
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
512
+ stream: cuda.CUstream = None,
513
  ):
514
  self.q_dtype = mQ.element_type
515
  self.k_dtype = mK.element_type
 
532
  assert self.dk_dtype.width == 32, "Must accumulate dK in float precision for GQA"
533
  assert self.dv_dtype.width == 32, "Must accumulate dV in float precision for GQA"
534
 
535
+ mdQaccum, mdK, mdV, mLSE, mdPsum = [
536
+ assume_tensor_aligned(t) for t in (mdQaccum, mdK, mdV, mLSE, mdPsum)
537
+ ]
538
 
539
  # (b, s, n, h) --> (s, h, n, b) or (t, n, h) -> (t, h, n)
540
  QO_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 2, 1]
 
751
  TileScheduler = SingleTileLPTBwdScheduler
752
  else:
753
  TileScheduler = SingleTileScheduler
754
+ if const_expr(self.spt_override is None):
755
+ self.spt = (self.is_causal or self.is_local) and self.deterministic
756
+ else:
757
+ assert self.spt_override is not None
758
+ self.spt = self.spt_override and self.deterministic
759
  tile_sched_args = TileSchedulerArguments(
760
  cute.ceil_div(cute.size(mK.shape[0]), self.cta_tiler[0]), # num_blocks
761
  cute.size(mQ.shape[2]), # num_heads = num_query_heads
 
776
  qhead_per_kvhead_packgqa=1, # pack_gqa disabled for bwd
777
  element_size=self.k_dtype.width // 8,
778
  is_persistent=self.is_persistent, # persistent mode not tested
779
+ cu_total_m_blocks_ptr=mCuTotalMBlocks,
780
  lpt=self.spt,
781
  head_swizzle=self.deterministic,
782
  )
 
832
  cutlass.Int64, self.dQaccum_reduce_stage // 2
833
  ]
834
  tmem_holding_buf: Int32
835
+ tmem_dealloc_mbar: cutlass.Int64
836
 
837
  # 2-CTA
838
  Qt_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.Q_stage]
 
902
  S_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.single_stage]
903
  dP_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.single_stage]
904
  dS_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.single_stage]
905
+ P_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.P_stage]
906
  dKV_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2 * self.sdKVaccum_stage]
907
  dQ_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2]
908
  dQ_cluster_full_mbar_ptr: cute.struct.MemRange[
 
912
  cutlass.Int64, self.dQaccum_reduce_stage // 2
913
  ]
914
  tmem_holding_buf: Int32
915
+ tmem_dealloc_mbar: Int64
916
 
917
  sQ: cute.struct.Align[
918
  cute.struct.MemRange[cute.Uint8, sQ_alloc_bytes],
 
963
  window_size_right = Int32(window_size_right)
964
 
965
  fastdiv_mods = None
966
+ if const_expr(aux_data.tensors is not None):
967
  seqlen_q = cute.size(mQ.shape[0]) // (
968
  self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1
969
  )
 
972
  seqlen_k_divmod = FastDivmodDivisor(seqlen_k)
973
  fastdiv_mods = (seqlen_q_divmod, seqlen_k_divmod)
974
  self.use_block_sparsity = cutlass.const_expr(blocksparse_tensors is not None)
975
+ if const_expr(self.use_block_sparsity and self.use_2cta_instrs):
976
+ # Both CTAs of a cluster must map to the same sparse KV column or they deadlock.
977
+ assert self.kv_subtile_factor % self.cta_group_size == 0, (
978
+ "2-CTA block-sparse backward requires kv_subtile_factor % cta_group_size == 0"
 
 
 
 
 
979
  )
980
+
981
  # 2-CTA: 231424 and 1-CTA: 232448
982
  # print("SMEM: ", self.shared_storage.size_in_bytes())
983
+ if const_expr(self.use_block_sparsity):
984
  assert all(x is None for x in (mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK)), (
985
+ "Variable sequence length is not supported yet for blocksparse in bwd"
986
  )
987
 
988
  self.kernel(
 
1044
  window_size_left,
1045
  window_size_right,
1046
  tile_sched_params,
1047
+ aux_data,
1048
  fastdiv_mods,
1049
  blocksparse_tensors,
1050
  ).launch(
 
1056
  min_blocks_per_mp=1,
1057
  )
1058
 
1059
+ def _generate_attention_mask_cls(self, window_size_left, window_size_right):
1060
+ return partial(
1061
+ AttentionMask,
1062
+ self.tile_m,
1063
+ self.tile_n * self.cta_group_size,
1064
+ swap_AB=True,
1065
+ window_size_left=window_size_left,
1066
+ window_size_right=window_size_right,
1067
+ )
1068
+
1069
  @cute.kernel
1070
  def kernel(
1071
  self,
 
1127
  window_size_left: Optional[Int32],
1128
  window_size_right: Optional[Int32],
1129
  tile_sched_params: ParamsBase,
1130
+ aux_data: AuxData = AuxData(),
1131
  fastdiv_mods=(None, None),
1132
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
1133
  ):
 
1203
  * len((self.mma_warp_id, *self.compute_warp_ids, *self.reduce_warp_ids)),
1204
  )
1205
  tmem = cutlass.utils.TmemAllocator(
1206
+ storage.tmem_holding_buf.ptr,
1207
  barrier_for_retrieve=tmem_alloc_barrier,
1208
  allocator_warp_id=self.mma_warp_id,
1209
  is_two_cta=self.use_2cta_instrs,
1210
+ two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar.ptr,
1211
  )
1212
 
1213
  # UMMA producers and AsyncThread consumers
 
1217
  pipeline_consumer_group_MMA_AsyncThread = cutlass.pipeline.CooperativeGroup(
1218
  cutlass.pipeline.Agent.Thread, len(self.compute_warp_ids) * self.cta_group_size
1219
  )
1220
+ # With split_P_dS, this pipeline guards only the S TMEM buffer (P has pipeline_P).
1221
  pipeline_S_P = cutlass.pipeline.PipelineUmmaAsync.create(
1222
  num_stages=1,
1223
  producer_group=pipeline_producer_group_MMA_AsyncThread,
 
1267
  barrier_storage=storage.dS_mbar_ptr.data_ptr(),
1268
  cta_layout_vmnk=cluster_layout_vmnk,
1269
  )
1270
+ if const_expr(self.split_P_dS):
1271
+ pipeline_P = cutlass.pipeline.PipelineAsyncUmma.create(
1272
+ num_stages=1,
1273
+ producer_group=pipeline_PdS_producer_group,
1274
+ consumer_group=pipeline_PdS_consumer_group,
1275
+ barrier_storage=storage.P_mbar_ptr.data_ptr(),
1276
+ cta_layout_vmnk=cluster_layout_vmnk,
1277
+ )
1278
+ else:
1279
+ pipeline_P = None
1280
 
1281
  # TMA producer and UMMA consumers
1282
  pipeline_producer_group = cutlass.pipeline.CooperativeGroup(
 
1480
  )
1481
  TileSchedulerCls = partial(self.tile_scheduler_cls.create, tile_sched_params)
1482
 
1483
+ AttentionMaskCls = self._generate_attention_mask_cls(window_size_left, window_size_right)
 
 
 
 
 
 
 
1484
  # EMPTY
1485
  # (15)
1486
  if warp_idx == self.empty_warp_id:
 
1501
  block_info,
1502
  SeqlenInfoCls,
1503
  TileSchedulerCls,
1504
+ blocksparse_tensors,
1505
  )
1506
 
1507
  # LOAD
 
1598
  pipeline_dKV,
1599
  pipeline_dP,
1600
  pipeline_dQ,
1601
+ pipeline_P,
1602
  block_info,
1603
  SeqlenInfoCls,
1604
  TileSchedulerCls,
 
1637
  pipeline_dS,
1638
  pipeline_dKV,
1639
  pipeline_dP,
1640
+ pipeline_P,
1641
  dS_cluster_empty_mbar_ptr,
1642
  dS_cluster_full_mbar_ptr,
1643
  dQaccum_empty_mbar_ptr,
 
1656
  tiled_copy_r2s_dKV,
1657
  mdK_semaphore,
1658
  mdV_semaphore,
1659
+ aux_data,
1660
  fastdiv_mods,
1661
  blocksparse_tensors,
1662
  )
 
1695
  block_info: BlockInfo,
1696
  SeqlenInfoCls: Callable,
1697
  TileSchedulerCls: Callable,
1698
+ blocksparse_tensors: Optional[BlockSparseTensors] = None,
1699
  ):
1700
  cta_rank_in_cluster = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
1701
  dS_cluster_phase = Int32(0)
 
1713
  process_tile = (
1714
  const_expr(not self.is_local and not self.is_varlen_q) or m_block_min < m_block_max
1715
  )
1716
+ num_iters = m_block_max - m_block_min
1717
+ if const_expr(self.use_block_sparsity):
1718
+ assert blocksparse_tensors is not None
1719
+ num_iters = get_total_q_block_count_bwd(
1720
+ blocksparse_tensors,
1721
+ batch_idx,
1722
+ head_idx,
1723
+ n_block // self.kv_subtile_factor,
1724
+ q_subtile_factor=self.q_subtile_factor,
1725
+ m_block_max=m_block_max,
1726
+ )
1727
+ process_tile = num_iters > Int32(0)
1728
 
1729
  if process_tile:
 
1730
  for _ in cutlass.range(num_iters, unroll=1):
1731
  # Wait for dS_xchg from peer CTA
1732
  cute.arch.mbarrier_wait(dS_cluster_full_mbar_ptr, phase=dS_cluster_phase)
 
1744
  @cute.jit
1745
  def load(
1746
  self,
1747
+ thr_mma_S: cute.ThrMma,
1748
+ thr_mma_dP: cute.ThrMma,
1749
+ thr_mma_dV: cute.ThrMma,
1750
+ thr_mma_dK: cute.ThrMma,
1751
+ thr_mma_dQ: cute.ThrMma,
1752
  mQ: cute.Tensor,
1753
  mK: cute.Tensor,
1754
  mKt: Optional[cute.Tensor],
 
1832
  )
1833
  head_idx_kv = head_idx // self.qhead_per_kvhead
1834
  n_block_cta_group = n_block // self.cta_group_size
1835
+ n_block_sparse = n_block // self.kv_subtile_factor
1836
 
1837
  # GMEM tensors (varlen-aware)
1838
  mQ_cur = seqlen.offset_batch_Q(mQ, batch_idx, dim=3)[None, None, head_idx]
 
1912
  single_stage=True,
1913
  )
1914
 
1915
+ load_dOt = None
1916
  if const_expr(tma_atom_dOt is not None):
1917
  gdOt = cute.local_tile(
1918
  mdOt_cur, cute.select(self.mma_tiler_vdo, mode=[1, 2]), (None, 0)
 
1942
  load_dO = copy_utils.tma_producer_copy_fn(load_dO, pipeline_dO)
1943
 
1944
  # (4) dK += dS.T @ Q (2-CTA: needs separate Qt load)
1945
+ load_Qt = None
1946
  if const_expr(tma_atom_Qt is not None):
1947
  gQt = cute.local_tile(
1948
  mQt_cur, cute.select(self.mma_tiler_dsq, mode=[1, 2]), (0, None)
 
1959
  load_Qt = copy_utils.tma_producer_copy_fn(load_Qt, pipeline_Qt)
1960
 
1961
  # (5) dQ = dS @ K
1962
+ load_Kt = None
1963
  if const_expr(self.use_2cta_instrs):
1964
  gKt = cute.local_tile(
1965
  mKt_cur, cute.select(self.mma_tiler_dsk, mode=[1, 2]), (0, n_block_cta_group)
 
1984
  # gdPsum = cute.logical_divide(gdPsum, (64,))[(None, block_in_cluster_coord_vmnk[1]), None]
1985
  # copy_stats = partial(cute.copy, copy_atom_stats, mcast_mask=q_do_mcast_mask)
1986
 
1987
+ if const_expr(not self.use_block_sparsity):
 
 
 
 
 
 
 
 
 
 
 
1988
  process_tile = (
1989
  const_expr(not self.is_local and not self.is_varlen_q)
1990
  or m_block_min < m_block_max
1991
  )
1992
 
1993
+ if process_tile:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1994
  first_m_block = m_block_min
1995
  if const_expr(self.use_2cta_instrs and self.tile_hdim == 192):
1996
  #### Prologue ####
1997
  assert should_load_Q and should_load_dO
1998
+ assert load_dOt is not None and load_Qt is not None
1999
  # K & Q (for S)
2000
  pipeline_Q.producer_acquire(
2001
  producer_state_Q_Qt,
 
2099
  pipeline_dO.producer_commit(producer_state_O_Ot)
2100
  producer_state_O_Ot.advance()
2101
 
2102
+ pipeline_Q.producer_tail(producer_state_Q_Qt)
2103
+ pipeline_LSE.producer_tail(producer_state_LSE)
2104
+ pipeline_dO.producer_tail(producer_state_O_Ot)
2105
+ pipeline_dPsum.producer_tail(producer_state_dPsum)
2106
  else:
2107
  #### Prologue ####
2108
  if const_expr(should_load_Q):
 
2132
  pipeline_dO.producer_acquire(
2133
  producer_state_dO_dPsum,
2134
  extra_tx_count=self.tma_copy_bytes["V"] + self.tma_copy_bytes["dO"]
2135
+ if const_expr(load_dOt is not None)
2136
  else self.tma_copy_bytes["V"],
2137
  )
2138
  load_V(
 
2141
  )
2142
  )
2143
  load_dO(first_m_block, producer_state=producer_state_dO_dPsum)
2144
+ if const_expr(load_dOt is not None):
2145
  load_dOt(first_m_block, producer_state=producer_state_dO_dPsum)
2146
  pipeline_dO.producer_commit(producer_state_dO_dPsum)
2147
 
 
2165
  #### Main Loop ####
2166
  for m_block in cutlass.range(m_block_min + 1, m_block_max, unroll=1):
2167
  if const_expr(should_load_Q):
2168
+ if const_expr(load_Qt is not None):
2169
  pipeline_Qt.producer_acquire(producer_state_Qt)
2170
  load_Qt(m_block - 1, producer_state=producer_state_Qt)
2171
  pipeline_Qt.producer_commit(producer_state_Qt)
 
2192
  pipeline_dO.producer_acquire(
2193
  producer_state_dO_dPsum,
2194
  extra_tx_count=self.tma_copy_bytes["dO"]
2195
+ if const_expr(load_dOt is not None)
2196
  else 0,
2197
  )
2198
  load_dO(m_block, producer_state=producer_state_dO_dPsum)
2199
+ if const_expr(load_dOt is not None):
2200
  load_dOt(m_block, producer_state=producer_state_dO_dPsum)
2201
  pipeline_dO.producer_commit(producer_state_dO_dPsum)
2202
 
 
2214
 
2215
  #### Tail ####
2216
  if const_expr(should_load_Q):
2217
+ if const_expr(load_Qt is not None):
2218
  pipeline_Qt.producer_acquire(producer_state_Qt)
2219
  load_Qt(m_block_max - 1, producer_state=producer_state_Qt)
2220
  pipeline_Qt.producer_commit(producer_state_Qt)
2221
  producer_state_Qt.advance()
2222
 
2223
+ pipeline_Q.producer_tail(producer_state_Q_LSE.clone())
2224
+ pipeline_LSE.producer_tail(producer_state_Q_LSE)
2225
+ if const_expr(load_Qt is not None):
2226
+ pipeline_Qt.producer_tail(producer_state_Qt)
2227
+ if const_expr(should_load_dO):
2228
+ pipeline_dO.producer_tail(producer_state_dO_dPsum.clone())
2229
+ pipeline_dPsum.producer_tail(producer_state_dO_dPsum)
2230
+
2231
+ else:
2232
+ assert blocksparse_tensors is not None
2233
  if const_expr(self.use_2cta_instrs and self.tile_hdim == 192):
2234
+ assert should_load_Q and should_load_dO
2235
+ assert load_dOt is not None and load_Qt is not None
2236
+ assert load_Kt is not None and pipeline_Qt is not None
2237
+ (
2238
+ producer_state_Q_Qt,
2239
+ producer_state_O_Ot,
2240
+ producer_state_LSE,
2241
+ producer_state_dPsum,
2242
+ ) = produce_block_sparse_q_loads_bwd_sm100_2cta_hdim192(
2243
+ blocksparse_tensors,
2244
+ batch_idx,
2245
+ head_idx,
2246
+ n_block_sparse,
2247
+ producer_state_Q_Qt,
2248
+ producer_state_O_Ot,
2249
+ producer_state_LSE,
2250
+ producer_state_dPsum,
2251
+ pipeline_Q,
2252
+ pipeline_LSE,
2253
+ pipeline_dO,
2254
+ pipeline_dPsum,
2255
+ pipeline_Qt,
2256
+ load_K,
2257
+ load_V,
2258
+ load_Q,
2259
+ load_dO,
2260
+ load_Qt,
2261
+ load_Kt,
2262
+ load_dOt,
2263
+ copy_stats,
2264
+ gLSE,
2265
+ sLSE,
2266
+ gdPsum,
2267
+ sdPsum,
2268
+ self.tma_copy_bytes["K"],
2269
+ self.tma_copy_bytes["V"],
2270
+ q_subtile_factor=self.q_subtile_factor,
2271
+ m_block_max=m_block_max,
2272
+ )
2273
  else:
2274
+ (
2275
+ producer_state_Q_LSE,
2276
+ producer_state_dO_dPsum,
2277
+ producer_state_Qt,
2278
+ producer_state_Kt,
2279
+ ) = produce_block_sparse_q_loads_bwd_sm100_default(
2280
+ blocksparse_tensors,
2281
+ batch_idx,
2282
+ head_idx,
2283
+ n_block_sparse,
2284
+ producer_state_Q_LSE,
2285
+ producer_state_dO_dPsum,
2286
+ pipeline_Q,
2287
+ pipeline_LSE,
2288
+ pipeline_dO,
2289
+ pipeline_dPsum,
2290
+ load_K,
2291
+ load_V,
2292
+ load_Q,
2293
+ load_dO,
2294
+ copy_stats,
2295
+ gLSE,
2296
+ sLSE,
2297
+ gdPsum,
2298
+ sdPsum,
2299
+ self.tma_copy_bytes["K"],
2300
+ self.tma_copy_bytes["V"],
2301
+ should_load_Q,
2302
+ should_load_dO,
2303
+ q_subtile_factor=self.q_subtile_factor,
2304
+ m_block_max=m_block_max,
2305
+ use_2cta_instrs=self.use_2cta_instrs,
2306
+ producer_state_Qt=producer_state_Qt,
2307
+ producer_state_Kt=producer_state_Kt,
2308
+ pipeline_Qt=pipeline_Qt,
2309
+ pipeline_Kt=pipeline_Kt,
2310
+ load_Qt=load_Qt,
2311
+ load_Kt=load_Kt,
2312
+ load_dOt=load_dOt,
2313
+ tma_copy_bytes_dO=self.tma_copy_bytes["dO"],
2314
+ )
2315
  tile_scheduler.prefetch_next_work()
2316
  tile_scheduler.advance_to_next_work()
2317
  work_tile = tile_scheduler.get_current_work()
 
2352
  pipeline_dKV: PipelineAsync,
2353
  pipeline_dP: PipelineAsync,
2354
  pipeline_dQ: PipelineAsync,
2355
+ pipeline_P: Optional[PipelineAsync],
2356
  block_info: BlockInfo,
2357
  SeqlenInfoCls: Callable,
2358
  TileSchedulerCls: Callable,
 
2464
  )
2465
  producer_phase_acc = Int32(1) # For S & P, dP, dQ
2466
  producer_phase_dQ = Int32(1) # 2-CTA: separate phase for dQ pipeline
2467
+ producer_phase_S = Int32(1) # split_P_dS: separate phase for the S handshake
2468
+ consumer_state_P = cutlass.pipeline.make_pipeline_state(
2469
+ cutlass.pipeline.PipelineUserType.Consumer, 1
2470
+ )
2471
  consumer_state_dS = cutlass.pipeline.make_pipeline_state(
2472
  cutlass.pipeline.PipelineUserType.Consumer, 1
2473
  )
 
2491
  blocksparse_tensors,
2492
  batch_idx,
2493
  head_idx,
2494
+ n_block // self.kv_subtile_factor,
2495
+ q_subtile_factor=self.q_subtile_factor,
2496
  m_block_max=m_block_max,
2497
  )
2498
  process_tile = block_iter_count > Int32(0)
 
2517
  # 4. dV = P.T @ dO
2518
  # 5. dQ = dS @ K
2519
 
2520
+ main_loop_iters = block_iter_count
2521
 
2522
  # empty waits
2523
  # pipeline_S_P.sync_object_empty.wait(0, producer_phase_acc)
 
2707
  else:
2708
  if is_leader_cta and process_tile:
2709
  accumulate_dK = False
2710
+ accumulate_dV = False
2711
  # -----------------------------------------------------------
2712
  ###### Prologue
2713
  # -----------------------------------------------------------
2714
+ # 1. S.T = K @ Q.T
2715
+ # 2. dP.T = V @ dO.T
2716
+ # 3. dV = P.T @ dO (split_P_dS: issued in the main loop, after QK_1)
2717
 
2718
+ # 1) S.T = K @ Q.T
2719
  handle_Q = pipeline_Q_consumer.wait_and_advance()
2720
+ if const_expr(self.split_P_dS):
2721
+ pipeline_S_P.sync_object_empty.wait(0, producer_phase_S)
2722
+ else:
2723
+ pipeline_S_P.sync_object_empty.wait(0, producer_phase_acc)
2724
  mma_qk_fn(B_idx=handle_Q.index)
2725
  pipeline_S_P.sync_object_full.arrive(0, pipeline_S_P.producer_mask, cta_group)
2726
+ if const_expr(self.split_P_dS):
2727
+ producer_phase_S ^= 1
2728
 
2729
+ # 2) dP.T = V @ dO.T
2730
  pipeline_dO.consumer_wait(consumer_state_dO)
2731
  pipeline_dP.sync_object_empty.wait(0, producer_phase_acc)
2732
  pipeline_dQ.sync_object_empty.wait(0, producer_phase_acc)
 
2735
 
2736
  producer_phase_acc ^= 1
2737
  # 3) dV = P.T @ dO
2738
+ if const_expr(not self.split_P_dS):
2739
+ pipeline_S_P.sync_object_empty.wait(0, producer_phase_acc)
2740
+ mma_pdo_fn(B_idx=consumer_state_dO.index, zero_init=True)
2741
+ pipeline_dO.consumer_release(consumer_state_dO)
2742
+ consumer_state_dO.advance()
2743
 
2744
  # -----------------------------------------------------------
2745
  ###### MAIN LOOP
2746
  # -----------------------------------------------------------
2747
+ # Per trip t (the CTA owns a KV tile, so S and dP come out transposed):
2748
+ # 1. S.T = K @ Q.T (tile t+1)
2749
+ # 1b. dV += P.T @ dO (tile t; split_P_dS only -- otherwise at 5)
2750
+ # 2. dK += dS.T @ Q
2751
+ # 3. dQ = dS @ K
2752
+ # 4. dP.T = V @ dO.T (tile t+1)
2753
+ # 5. dV += P.T @ dO (tile t+1; aliased layout only)
2754
 
2755
  # For block sparsity, we use block_iter_count; for dense, use m_block range
2756
  # MMA doesn't need actual m_block indices, just the iteration count
 
2764
  for _ in cutlass.range(main_loop_iters, unroll=1):
2765
  # (1) S.T = K @ Q.T
2766
  handle_Q_next = pipeline_Q_consumer.wait_and_advance()
2767
+ if const_expr(self.split_P_dS):
2768
+ # S empty means "S_t loaded", not "P_t stored", so this QK
2769
+ # overlaps the softmax of tile t.
2770
+ pipeline_S_P.sync_object_empty.wait(0, producer_phase_S)
2771
  mma_qk_fn(B_idx=handle_Q_next.index)
2772
  pipeline_S_P.sync_object_full.arrive(
2773
  0, pipeline_S_P.producer_mask, cta_group
2774
  )
2775
+ if const_expr(self.split_P_dS):
2776
+ producer_phase_S ^= 1
2777
+ # (1b) dV += P_t.T @ dO_t, issued after QK_{t+1} so QK never
2778
+ # waits behind the P store.
2779
+ pipeline_P.consumer_wait(consumer_state_P)
2780
+ mma_pdo_fn(B_idx=consumer_state_dO.index, zero_init=not accumulate_dV)
2781
+ accumulate_dV = True
2782
+ pipeline_P.consumer_release(consumer_state_P)
2783
+ consumer_state_P.advance()
2784
+ pipeline_dO.consumer_release(consumer_state_dO)
2785
+ consumer_state_dO.advance()
2786
 
2787
  # (2) dK += dS.T @ Q
2788
  pipeline_dS.consumer_wait(consumer_state_dS)
 
2796
  pipeline_dS.consumer_release(consumer_state_dS)
2797
  consumer_state_dS.advance()
2798
 
2799
+ # (4) dP.T = V @ dO.T
2800
  pipeline_dO.consumer_wait(consumer_state_dO)
2801
  pipeline_dQ.sync_object_empty.wait(0, producer_phase_acc)
2802
  mma_dov_fn(B_idx=consumer_state_dO.index)
2803
  pipeline_dP.sync_object_full.arrive(0, pipeline_dP.producer_mask, cta_group)
2804
 
2805
+ # (5) dV += P.T @ dO (split_P_dS: done at (1b) of the next trip)
2806
  producer_phase_acc ^= 1
2807
+ if const_expr(not self.split_P_dS):
2808
+ pipeline_S_P.sync_object_empty.wait(0, producer_phase_acc)
2809
+ mma_pdo_fn(B_idx=consumer_state_dO.index, zero_init=False)
2810
+ pipeline_dO.consumer_release(consumer_state_dO)
2811
+ consumer_state_dO.advance()
2812
 
2813
  handle_Q = handle_Q_next
2814
 
2815
+ if const_expr(self.split_P_dS):
2816
+ # the last tile's dV += P.T @ dO; there is no further QK to overlap
2817
+ pipeline_P.consumer_wait(consumer_state_P)
2818
+ mma_pdo_fn(B_idx=consumer_state_dO.index, zero_init=not accumulate_dV)
2819
+ pipeline_P.consumer_release(consumer_state_P)
2820
+ consumer_state_P.advance()
2821
+ pipeline_dO.consumer_release(consumer_state_dO)
2822
+ consumer_state_dO.advance()
2823
+ else:
2824
+ pipeline_S_P.sync_object_full.arrive(
2825
+ 0, pipeline_S_P.producer_mask, cta_group
2826
+ )
2827
 
2828
  # signal to the epilogue that dV is ready
2829
  # pipeline_dKV.producer_acquire(producer_state_dKV)
 
2914
  n_block,
2915
  softmax_scale,
2916
  seqlen_info,
2917
+ aux_data: AuxData = AuxData(),
2918
  fastdiv_mods=(None, None),
2919
  ):
2920
  """Apply forward score modification for SM100 backward pass."""
2921
+ # In bwd, S is computed as K @ Q.T so dimensions are (tile_n, tile_m).
2922
+ # With 2CTA, partition_C must see the full cluster tile so each CTA
2923
+ # gets its own half of the tile.
2924
+ cluster_tile_n = self.tile_n * self.cta_group_size
2925
+ cluster_n_block = n_block // self.cta_group_size
2926
+ cS = cute.make_identity_tensor((cluster_tile_n, self.tile_m))
2927
+ cS = cute.domain_offset((cluster_n_block * cluster_tile_n, m_block * self.tile_m), cS)
2928
  tScS = thr_mma_S.partition_C(cS)
2929
  tScS_idx = thr_copy_t2r.partition_D(tScS)
2930
 
 
2937
  softmax_scale,
2938
  self.vec_size,
2939
  self.qk_acc_dtype,
2940
+ aux_data,
2941
  fastdiv_mods,
2942
  seqlen_info,
2943
  constant_q_idx=None,
 
2955
  head_idx,
2956
  softmax_scale,
2957
  seqlen_info,
2958
+ aux_data: AuxData = AuxData(),
2959
  fastdiv_mods=(None, None),
2960
  ):
2961
  """Apply backward score modification (joint graph) for SM100."""
 
2969
  softmax_scale,
2970
  self.vec_size,
2971
  self.qk_acc_dtype,
2972
+ aux_data,
2973
  fastdiv_mods,
2974
  seqlen_info,
2975
  constant_q_idx=None,
 
2980
  @cute.jit
2981
  def compute_loop(
2982
  self,
2983
+ thr_mma_S: cute.ThrMma,
2984
+ thr_mma_dP: cute.ThrMma,
2985
+ thr_mma_dV: cute.ThrMma,
2986
+ thr_mma_dK: cute.ThrMma,
2987
  tStS: cute.Tensor,
2988
  tdPtdP: cute.Tensor,
2989
  tdVtdV: cute.Tensor,
 
3000
  pipeline_dS: PipelineAsync,
3001
  pipeline_dKV: PipelineAsync,
3002
  pipeline_dP: PipelineAsync,
3003
+ pipeline_P: Optional[PipelineAsync],
3004
  dS_cluster_empty_mbar_ptr: cute.Pointer,
3005
  dS_cluster_full_mbar_ptr: cute.Pointer,
3006
  dQaccum_empty_mbar_ptr: cute.Pointer,
 
3019
  tiled_copy_r2s_dKV: Optional[cute.TiledCopy],
3020
  mdK_semaphore: Optional[cute.Tensor],
3021
  mdV_semaphore: Optional[cute.Tensor],
3022
+ aux_data: AuxData = AuxData(),
3023
  fastdiv_mods=(None, None),
3024
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
3025
  ):
 
3054
 
3055
  tileP_f32_like = self.cta_tiler[1] // 32 * self.v_dtype.width
3056
  # tStS has shape ((128, 128), 1, 1), tStP has shape ((128, 64), 1, 1)
 
3057
  tStP = cute.composition(tStS, (cute.make_layout((self.tile_n, tileP_f32_like)), 1, 1))
3058
+ # Rebase the iterator explicitly (P may alias S), otherwise the tmem address is wrong
3059
+ tStP = cute.make_tensor(
3060
+ tStS.iterator + (self.tmem_P_offset - self.tmem_S_offset), tStP.layout
3061
+ )
3062
  tScS = thr_mma_S.partition_C(cute.make_identity_tensor(self.mma_tiler_kq[:2]))
3063
  tScP = cute.composition(tScS, (cute.make_layout((self.tile_n, tileP_f32_like)), 1, 1))
 
3064
  tdPtdS = cute.composition(tdPtdP, (cute.make_layout((self.tile_n, tileP_f32_like)), 1, 1))
3065
+ tdPtdS = cute.make_tensor(
3066
+ tdPtdP.iterator + (self.tmem_dS_offset - self.tmem_dP_offset), tdPtdS.layout
3067
+ )
3068
  tdPcdP = thr_mma_dP.partition_C(cute.make_identity_tensor(self.mma_tiler_vdo[:2]))
3069
  tdPcdS = cute.composition(tdPcdP, (cute.make_layout((self.tile_n, tileP_f32_like)), 1, 1))
3070
 
 
3123
  cutlass.pipeline.PipelineUserType.Consumer, 1
3124
  )
3125
  # consumer_phase_S_P_dP = Int32(0)
3126
+ consumer_state_S = pipeline.make_pipeline_state(
3127
+ cutlass.pipeline.PipelineUserType.Consumer, 1
3128
+ )
3129
+ producer_state_P = pipeline.make_pipeline_state(
3130
+ cutlass.pipeline.PipelineUserType.Producer, 1
3131
+ )
3132
  producer_state_dS = pipeline.make_pipeline_state( # Our impl has shortcut for stage==1
3133
  cutlass.pipeline.PipelineUserType.Producer, 1
3134
  )
 
3147
  while work_tile.is_valid_tile:
3148
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
3149
  seqlen = SeqlenInfoCls(batch_idx)
3150
+
3151
+ recompute_fastdiv_mods_q = const_expr(
3152
+ aux_data.tensors is not None and (seqlen.has_cu_seqlens_q or seqlen.has_seqused_q)
3153
+ )
3154
+ recompute_fastdiv_mods_k = const_expr(
3155
+ aux_data.tensors is not None and (seqlen.has_cu_seqlens_k or seqlen.has_seqused_k)
3156
+ )
3157
+
3158
+ if const_expr(fastdiv_mods is not None and fastdiv_mods[0] is not None):
3159
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
3160
+ fastdiv_mods = (
3161
+ seqlen_q_divmod
3162
+ if not recompute_fastdiv_mods_q
3163
+ else FastDivmodDivisor(seqlen.seqlen_q),
3164
+ seqlen_k_divmod
3165
+ if not recompute_fastdiv_mods_k
3166
+ else FastDivmodDivisor(seqlen.seqlen_k),
3167
+ )
3168
  m_block_min, m_block_max = block_info.get_m_block_min_max(
3169
  seqlen, n_block // self.cluster_shape_mnk[0]
3170
  )
3171
  mask = AttentionMaskCls(seqlen)
3172
+ cluster_n_block = n_block // self.cta_group_size
3173
  # TODO: condition mask_seqlen
3174
  mask_fn = partial(
3175
  mask.apply_mask_sm100_transposed,
3176
  tScS_t2r=tScS_t2r,
3177
  t0ScS_t2r=t0ScS_t2r,
3178
+ n_block=cluster_n_block,
3179
  mask_seqlen=True,
3180
  mask_causal=self.is_causal,
3181
  mask_local=self.is_local,
3182
  mask_mod=self.mask_mod,
3183
  batch_idx=batch_idx,
3184
  head_idx=head_idx,
3185
+ aux_data=aux_data,
3186
  fastdiv_mods=fastdiv_mods,
3187
  )
3188
 
3189
  # prefetch_LSE = not self.is_causal
3190
  prefetch_LSE = False
3191
+
3192
+ curr_q_cnt = Int32(0)
3193
+ curr_q_idx = None
3194
+ curr_full_cnt = Int32(0)
3195
+ curr_full_idx = None
3196
+ loop_count = m_block_max - m_block_min
3197
+ process_tile = (
3198
+ const_expr(not self.is_local and not self.is_varlen_q) or m_block_min < m_block_max
3199
+ )
3200
  if const_expr(self.use_block_sparsity):
3201
+ assert blocksparse_tensors is not None
3202
  (
3203
  curr_q_cnt,
3204
  curr_q_idx,
 
3209
  blocksparse_tensors,
3210
  batch_idx,
3211
  head_idx,
3212
+ n_block // self.kv_subtile_factor,
3213
+ q_subtile_factor=self.q_subtile_factor,
3214
  m_block_max=m_block_max,
3215
  )
3216
  process_tile = loop_count > Int32(0)
 
 
 
 
 
 
3217
 
3218
  # Mainloop
3219
  # Block sparsity: iterate over sparse m_block count and derive actual m_block
3220
  # from Q_IDX/FULL_Q_IDX tensors. Dense: iterate m_block_min..m_block_max directly.
3221
  for iter_idx in cutlass.range(loop_count, unroll=1):
3222
+ m_block = m_block_min + iter_idx
3223
+ m_block_oob = False
3224
+ is_full_block = False
3225
  if const_expr(self.use_block_sparsity):
3226
  m_block, is_full_block = get_m_block_from_iter_bwd(
3227
  iter_idx,
 
3229
  curr_q_idx,
3230
  curr_full_cnt,
3231
  curr_full_idx,
3232
+ q_subtile_factor=self.q_subtile_factor,
3233
  m_block_max=m_block_max,
3234
  )
3235
  m_block_oob = m_block >= m_block_max
 
 
 
 
3236
  # Prefetch 1 stage of LSE
3237
  pipeline_LSE.consumer_wait(consumer_state_LSE)
3238
+ tSrLSE_s2r = cute.make_rmem_tensor(tScS_t2r[None, 0, 0, 0].shape, Float32)
3239
  if const_expr(prefetch_LSE and not self.shuffle_LSE):
3240
  cute.autovec_copy(tSsLSE[None, 0, 0, 0, consumer_state_LSE.index], tSrLSE_s2r)
3241
 
3242
+ if const_expr(self.split_P_dS):
3243
+ pipeline_S_P.consumer_wait(consumer_state_S)
3244
+ else:
3245
+ pipeline_S_P.consumer_wait(consumer_state_S_P_dP)
3246
  # pipeline_S_P.sync_object_full.wait(0, consumer_phase_S_P_dP)
3247
  #### TMEM->RMEM (Load S from TMEM)
3248
+ tSrS_t2r = cute.make_rmem_tensor(tScS_t2r.shape, Float32)
3249
  cute.copy(thr_copy_t2r, tStS_t2r, tSrS_t2r)
3250
 
3251
+ if const_expr(self.split_P_dS):
3252
+ # S is in registers: release it before the softmax so the MMA warp can
3253
+ # issue the next QK into the slot
3254
+ cute.arch.fence_view_async_tmem_load()
3255
+ cute.arch.sync_warp()
3256
+ with cute.arch.elect_one():
3257
+ pipeline_S_P.consumer_release(consumer_state_S)
3258
+ consumer_state_S.advance()
3259
+ elif const_expr(self.tile_hdim == 192):
3260
  # Signal S tmem load completion using pipeline_S_P when hdim 192
3261
  # dP is overlapped with S
3262
  cute.arch.fence_view_async_tmem_load()
 
3287
  n_block,
3288
  softmax_scale,
3289
  seqlen,
3290
+ aux_data,
3291
  fastdiv_mods,
3292
  )
3293
 
 
3304
  #### P = exp(S - LSE)
3305
  # ---------------------------------------------
3306
  lane_idx = cute.arch.lane_idx()
3307
+ tSrP_r2t_f32 = cute.make_rmem_tensor(tScP_r2t.shape, Float32) # 64
3308
  tSrP_r2t = cute.recast_tensor(tSrP_r2t_f32, self.q_dtype)
3309
  for stage in cutlass.range_constexpr(num_stages):
3310
  tSrS_cur = tSrS_t2r[None, stage, 0, 0]
 
3332
  tSrS_cur[2 * v + 1] = cute.math.exp2(tSrS_cur[2 * v + 1], fastmath=True)
3333
  utils.cvt_f16(tSrS_cur, tSrP_r2t[None, stage, 0, 0])
3334
  if const_expr(stage == 0):
3335
+ if const_expr(self.split_P_dS):
3336
+ # wait until the previous pdo UMMA has consumed the P slot
3337
+ pipeline_P.producer_acquire(producer_state_P)
3338
+ else:
3339
+ cute.arch.fence_view_async_tmem_load()
3340
+ # P overwrites S lanes another warp may still be loading, so
3341
+ # every warp must have loaded S before any stores P.
3342
+ self.compute_sync_barrier.arrive_and_wait()
3343
  cute.copy(
3344
  thr_copy_r2t,
3345
  tSrP_r2t_f32[None, stage, None, None],
 
3348
 
3349
  cute.arch.fence_view_async_tmem_store()
3350
  cute.arch.fence_view_async_shared()
3351
+ # The P (or S_P) and LSE mbarriers expect one arrival per compute warp
3352
+ if const_expr(self.split_P_dS):
3353
+ cute.arch.sync_warp()
3354
+ else:
3355
+ self.compute_sync_barrier.arrive_and_wait()
3356
+ if const_expr(self.split_P_dS):
3357
+ with cute.arch.elect_one():
3358
+ pipeline_P.producer_commit(producer_state_P)
3359
+ producer_state_P.advance()
3360
+ elif const_expr(not self.tile_hdim == 192):
3361
  # Signal tmem store P completion with pipeline_S_P
3362
  with cute.arch.elect_one():
3363
  pipeline_S_P.consumer_release(consumer_state_S_P_dP)
3364
  # pipeline_S_P.sync_object_empty.arrive(0, pipeline_S_P.consumer_mask)
3365
+ # Normally we'd need syncwarp here since only 1 thread will signal in
3366
+ # consumer_release, but we already have the barrier or sync_warp before this
3367
  pipeline_LSE.consumer_release(consumer_state_LSE)
3368
  consumer_state_LSE.advance()
3369
  # ---------------------------------------------
 
3378
 
3379
  ##### dS.T = P.T * (dP.T - Psum)
3380
  for stage in cutlass.range_constexpr(num_stages):
3381
+ tdPrdP_t2r = cute.make_rmem_tensor(tScS_t2r[None, 0, None, None].shape, Float32)
3382
  cute.copy(thr_copy_t2r, tdPtdP_t2r[None, stage, None, None], tdPrdP_t2r)
3383
  cute.arch.fence_view_async_tmem_load()
3384
+ if const_expr(not self.split_P_dS):
3385
+ # dS overwrites dP with the same cross-warp lane sharing as P over S
3386
+ self.compute_sync_barrier.arrive_and_wait()
3387
  tdPrdP_cur = tdPrdP_t2r[None, 0, 0]
3388
  tSrS_cur = tSrS_t2r[None, stage, 0, 0]
3389
  tSsdPsum_cur = tSsdPsum[None, stage, 0, 0, consumer_state_dPsum.index]
 
3400
  utils.shuffle_sync(tSrdPsum, offset=2 * v),
3401
  utils.shuffle_sync(tSrdPsum, offset=2 * v + 1),
3402
  )
3403
+ tdPrdP_cur[2 * v], tdPrdP_cur[2 * v + 1] = cute.arch.sub_packed_f32x2(
3404
+ (tdPrdP_cur[2 * v], tdPrdP_cur[2 * v + 1]), dPsum_pair
 
 
3405
  )
3406
  tdPrdP_cur[2 * v], tdPrdP_cur[2 * v + 1] = cute.arch.mul_packed_f32x2(
3407
  (tSrS_cur[2 * v], tSrS_cur[2 * v + 1]),
 
3410
 
3411
  if const_expr(self.score_mod_bwd is not None):
3412
  tSrS_pre_cur = tSrS_pre[None, stage, 0, 0]
3413
+ cluster_tile_n = self.tile_n * self.cta_group_size
3414
+ cluster_n_block = n_block // self.cta_group_size
3415
+ cS_bwd = cute.make_identity_tensor((cluster_tile_n, self.tile_m))
3416
  cS_bwd = cute.domain_offset(
3417
+ (cluster_n_block * cluster_tile_n, m_block * self.tile_m),
3418
+ cS_bwd,
3419
  )
3420
  tScS_bwd = thr_mma_S.partition_C(cS_bwd)
3421
  tScS_idx_bwd = thr_copy_t2r.partition_D(tScS_bwd)
 
3428
  head_idx,
3429
  softmax_scale,
3430
  seqlen,
3431
+ aux_data,
3432
  fastdiv_mods,
3433
  )
3434
  # Zero out OOB positions (kv_idx >= seqlen_k) after score_mod_bwd
 
3476
  cute.autovec_copy(tdPrdS_xchg, tRS_sdS_xchg[None, 0])
3477
 
3478
  cute.arch.fence_view_async_shared()
3479
+ # The dS and dPsum mbarriers expect one arrival per compute warp
3480
+ if const_expr(self.split_P_dS):
3481
+ cute.arch.sync_warp()
3482
+ else:
3483
+ self.compute_sync_barrier.arrive_and_wait()
3484
+ # Normally we'd need syncwarp here since only 1 thread will signal in
3485
+ # consumer_release, but we already have the barrier or sync_warp before this
3486
  pipeline_dPsum.consumer_release(consumer_state_dPsum)
3487
  consumer_state_dPsum.advance()
3488
  # when 2cta hdim 128, pipeline_dS also signals S tmem load completion so is deferred
 
3643
  tile_scheduler.advance_to_next_work()
3644
  work_tile = tile_scheduler.get_current_work()
3645
 
3646
+ @cute.jit
3647
+ def _dq_semaphore_lock_value(
3648
+ self,
3649
+ iter_idx: Int32,
3650
+ curr_q_cnt: Int32,
3651
+ curr_dq_write_order: Optional[cute.Tensor],
3652
+ curr_dq_write_order_full: Optional[cute.Tensor],
3653
+ blocksparse_tensors: Optional[BlockSparseTensors],
3654
+ block_info: BlockInfo,
3655
+ seqlen,
3656
+ m_block: Int32,
3657
+ n_block: Int32,
3658
+ ) -> Int32:
3659
+ lock_value = n_block
3660
+ if const_expr(self.spt):
3661
+ n_block_max_for_m_block = block_info.get_n_block_max_for_m_block(seqlen, m_block)
3662
+ lock_value = n_block_max_for_m_block - 1 - n_block
3663
+ if const_expr(self.use_block_sparsity):
3664
+ assert blocksparse_tensors is not None
3665
+ if const_expr(blocksparse_tensors.dq_write_order is not None):
3666
+ sparse_iter = iter_idx // self.q_subtile_factor
3667
+ if sparse_iter < curr_q_cnt:
3668
+ assert curr_dq_write_order is not None
3669
+ lock_value = curr_dq_write_order[sparse_iter]
3670
+ else:
3671
+ assert curr_dq_write_order_full is not None
3672
+ lock_value = curr_dq_write_order_full[sparse_iter - curr_q_cnt]
3673
+ if const_expr(self.kv_subtile_factor > self.cta_group_size):
3674
+ groups_per_sparse_block = self.kv_subtile_factor // self.cta_group_size
3675
+ local_group = n_block % groups_per_sparse_block
3676
+ if const_expr(self.spt):
3677
+ # [NOTE] KV_subtile determ + spt
3678
+ # dq_write_order stores one rank per sparse block; each physical tile
3679
+ # derives its slot as rank * groups_per_sparse_block + (n_block % groups_per_sparse_block).
3680
+ # W/ kv_subtile the tail sparse column can have a physical tile that is
3681
+ # never scheduled; e.g. kv_tile = 128, seqlen = 1023, KV_Block = 384 -> 3
3682
+ # sparse blocks. The last block is covered in [768, 896), [896, 1024) and
3683
+ # then [1024, 1152) which no CTA ever runs. Since the highest tile gets
3684
+ # the lowest lock value under spt, we would hang!
3685
+ # In this case we locally reverse, [N+2, N+1, N*] where N* is not scheduled
3686
+ # -> [N+1, N, N+2*]. Ahh but won't the vacant N+2 slot stall the next sparse
3687
+ # block's CTAs? It will, so the writer holding N+1 bumps the increment by 2
3688
+ # instead of 1 (i.e. 1 + #unscheduled) :)
3689
+ total_groups = cute.ceil_div(
3690
+ seqlen.seqlen_k, self.tile_n * self.cta_group_size
3691
+ )
3692
+ groups_in_own_block = cutlass.min(
3693
+ groups_per_sparse_block,
3694
+ total_groups
3695
+ - (n_block // groups_per_sparse_block) * groups_per_sparse_block,
3696
+ )
3697
+ local_group = groups_in_own_block - 1 - local_group
3698
+ lock_value = lock_value * groups_per_sparse_block + local_group
3699
+ return lock_value
3700
+
3701
  @cute.jit
3702
  def dQacc_reduce(
3703
  self,
3704
  mdQaccum: cute.Tensor,
3705
  sdQaccum: cute.Tensor,
3706
+ thr_mma_dQ: cute.ThrMma,
3707
  tdQtdQ: cute.Tensor,
3708
  pipeline_dQ: PipelineAsync,
3709
  dQaccum_empty_mbar_ptr: Optional[cute.Pointer],
 
3755
  while work_tile.is_valid_tile:
3756
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
3757
  n_block_cta_group = n_block // self.cta_group_size # for 2cta
3758
+ n_block_sparse = n_block // self.kv_subtile_factor
3759
  seqlen = SeqlenInfoCls(batch_idx)
3760
  m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block_cta_group)
3761
  if const_expr(not seqlen.has_cu_seqlens_q):
 
3771
  )
3772
 
3773
  if const_expr(self.deterministic):
3774
+ assert mdQ_semaphore is not None
3775
  mdQ_semaphore_cur = mdQ_semaphore[None, None, head_idx, batch_idx]
3776
 
3777
+ delay_semaphore_release = not self.tile_hdim == 192 and not self.use_block_sparsity
 
3778
 
3779
+ dq_sem_release_inc = Int32(1)
3780
+ if const_expr(
3781
+ self.deterministic
3782
+ and self.use_block_sparsity
3783
+ and self.spt
3784
+ and self.kv_subtile_factor > self.cta_group_size
3785
+ ):
3786
+ # A truncated tail block's last writer releases the missing increments,
3787
+ # see: [NOTE] KV_subtile determ + spt
3788
+ groups_per_sparse_block = self.kv_subtile_factor // self.cta_group_size
3789
+ total_groups = cute.ceil_div(seqlen.seqlen_k, self.tile_n * self.cta_group_size)
3790
+ tail_sparse_block_idx = (total_groups - 1) // groups_per_sparse_block
3791
+ groups_in_tail = total_groups - tail_sparse_block_idx * groups_per_sparse_block
3792
+ is_tail_bridge_group = (
3793
+ n_block_cta_group // groups_per_sparse_block == tail_sparse_block_idx
3794
+ and n_block_cta_group % groups_per_sparse_block == 0
3795
+ )
3796
+ if is_tail_bridge_group:
3797
+ dq_sem_release_inc = Int32(1) + groups_per_sparse_block - groups_in_tail
3798
+
3799
+ curr_q_cnt = Int32(0)
3800
+ curr_q_idx = None
3801
+ curr_full_cnt = Int32(0)
3802
+ curr_full_idx = None
3803
+ curr_dq_write_order = None
3804
+ curr_dq_write_order_full = None
3805
+ loop_count = m_block_max - m_block_min
3806
+ process_tile = (
3807
+ const_expr(not self.is_local and not self.is_varlen_q) or m_block_min < m_block_max
3808
+ )
3809
  if const_expr(self.use_block_sparsity):
3810
+ assert blocksparse_tensors is not None
3811
  (
3812
  curr_q_cnt,
3813
  curr_q_idx,
 
3818
  blocksparse_tensors,
3819
  batch_idx,
3820
  head_idx,
3821
+ n_block_sparse,
3822
+ q_subtile_factor=self.q_subtile_factor,
3823
  m_block_max=m_block_max,
3824
  )
3825
  process_tile = loop_count > Int32(0)
3826
+ if const_expr(self.deterministic and self.use_block_sparsity):
3827
+ assert blocksparse_tensors is not None
3828
+ if const_expr(blocksparse_tensors.dq_write_order is not None):
3829
+ assert blocksparse_tensors.dq_write_order is not None
3830
+ curr_dq_write_order = blocksparse_tensors.dq_write_order[
3831
+ batch_idx, head_idx, n_block_sparse, None
3832
+ ]
3833
+ if const_expr(blocksparse_tensors.dq_write_order_full is not None):
3834
+ assert blocksparse_tensors.dq_write_order_full is not None
3835
+ curr_dq_write_order_full = blocksparse_tensors.dq_write_order_full[
3836
+ batch_idx, head_idx, n_block_sparse, None
3837
+ ]
3838
 
3839
  # dQacc_reduce mainloop
3840
  # Block sparsity: iterate over sparse m_block count and derive actual m_block
3841
  # from Q_IDX/FULL_Q_IDX tensors. Dense: iterate m_block_min..m_block_max directly.
3842
  for iter_idx in cutlass.range(loop_count, unroll=1):
3843
+ m_block = m_block_min + iter_idx
3844
+ m_block_oob_upper = False
3845
  if const_expr(self.use_block_sparsity):
3846
  m_block, _ = get_m_block_from_iter_bwd(
3847
  iter_idx,
 
3849
  curr_q_idx,
3850
  curr_full_cnt,
3851
  curr_full_idx,
3852
+ q_subtile_factor=self.q_subtile_factor,
3853
  m_block_max=m_block_max,
3854
  )
3855
+ m_block_oob_upper = m_block >= m_block_max
 
 
 
3856
  pipeline_dQ.consumer_wait(dQ_consumer_state)
3857
  # TMEM -> RMEM
3858
+ tdQrdQ_t2r = cute.make_rmem_tensor(tdQrdQ_t2r_shape, Float32)
3859
  cute.copy(thr_copy_t2r, tdQtdQ_t2r, tdQrdQ_t2r)
3860
  cute.arch.fence_view_async_tmem_load()
3861
  cute.arch.sync_warp()
 
3863
  pipeline_dQ.consumer_release(dQ_consumer_state)
3864
  dQ_consumer_state.advance()
3865
 
3866
+ if m_block_max > 0:
3867
+ m_block = cutlass.min(m_block, m_block_max - 1)
3868
  gdQaccum_cur = gdQaccum[None, None, m_block]
3869
 
3870
  tdQrdQ_shape = (
 
3882
  cute.arch.fence_view_async_shared()
3883
  # semaphore acquire
3884
  if const_expr(self.deterministic and stage == 0):
3885
+ if not m_block_oob_upper:
3886
+ lock_value = self._dq_semaphore_lock_value(
3887
+ iter_idx,
3888
+ curr_q_cnt,
3889
+ curr_dq_write_order,
3890
+ curr_dq_write_order_full,
3891
+ blocksparse_tensors,
3892
+ block_info,
3893
+ seqlen,
3894
+ m_block,
3895
+ n_block_cta_group,
3896
+ )
3897
+ barrier.wait_eq(
3898
+ mdQ_semaphore_cur[(m_block, None)].iterator,
3899
+ tidx,
3900
+ cta_rank_in_cluster,
3901
+ lock_value,
3902
  )
 
 
 
 
 
 
 
 
 
3903
  self.reduce_sync_barrier.arrive_and_wait()
3904
  # Copy from shared memory to global memory
3905
+ if is_tma_warp and not m_block_oob_upper:
3906
  with cute.arch.elect_one():
3907
  copy_utils.cpasync_reduce_bulk_add_f32(
3908
  sdQaccum[None, smem_idx].iterator,
 
3911
  )
3912
  cute.arch.cp_async_bulk_commit_group()
3913
  cute.arch.cp_async_bulk_wait_group(self.sdQaccum_stage - 1, read=read_flag)
3914
+ elif is_tma_warp:
3915
+ # Drain pending TMA stores so SMEM buffers are safe to reuse
3916
+ cute.arch.cp_async_bulk_wait_group(0, read=read_flag)
3917
  self.reduce_sync_barrier.arrive_and_wait()
3918
  dQ_tma_store_producer_state.advance()
3919
+
 
 
 
 
 
 
 
 
 
 
 
3920
  if const_expr(self.deterministic and stage == 0 and delay_semaphore_release):
3921
  if m_block > m_block_min:
3922
  barrier.arrive_inc(
 
3938
  # NOTE: arrive_inc calls red_release which issues membar
3939
  if const_expr(self.deterministic and not delay_semaphore_release):
3940
  if const_expr(self.sdQaccum_stage > 1 and not self.tile_hdim == 192):
3941
+ if is_tma_warp and not m_block_oob_upper:
3942
  cute.arch.cp_async_bulk_wait_group(0, read=read_flag)
3943
  self.reduce_sync_barrier.arrive_and_wait()
3944
+ if not m_block_oob_upper:
3945
+ barrier.arrive_inc(
3946
+ mdQ_semaphore_cur[m_block, None].iterator,
3947
+ tidx,
3948
+ cta_rank_in_cluster,
3949
+ dq_sem_release_inc,
3950
+ )
3951
 
3952
  if process_tile:
3953
  if is_tma_warp:
 
3963
  )
3964
 
3965
  if const_expr(
3966
+ self.deterministic
3967
+ and not self.spt
3968
+ and not self.use_block_sparsity
3969
+ and block_info.window_size_left is not None
3970
  ):
3971
  m_block_global_max = cute.ceil_div(seqlen.seqlen_q, self.tile_m)
3972
  for m_block in cutlass.range(m_block_max, m_block_global_max, unroll=1):
 
3977
  tile_scheduler.advance_to_next_work()
3978
  work_tile = tile_scheduler.get_current_work()
3979
 
3980
+ if const_expr(not self.deterministic):
3981
+ cute.arch.cp_async_bulk_wait_group(0, read=True)
3982
+
3983
  @cute.jit
3984
  def epilogue_dKV(
3985
  self,
 
3989
  head_idx: Int32,
3990
  n_block: Int32,
3991
  seqlen,
3992
+ thr_mma_dV: cute.ThrMma,
3993
+ thr_mma_dK: cute.ThrMma,
3994
  tdVtdV: cute.Tensor,
3995
  tdKtdK: cute.Tensor,
3996
  mdV: cute.Tensor,
 
4008
  mdV_cur = seqlen.offset_batch_K(mdV, batch_idx, dim=3)[None, None, head_idx]
4009
  mdK_cur = seqlen.offset_batch_K(mdK, batch_idx, dim=3)[None, None, head_idx]
4010
 
4011
+ # The gmem store below has no head-dim OOB protection from TMA, so when head_dim is
4012
+ # not a multiple of the tile we predicate each 128-bit store vector (8 bf16 columns).
4013
+ # The store's predicate granularity is one tmem-load repetition, so shrink the
4014
+ # repetition to 8 columns for that case; 16 keeps fewer tmem loads otherwise.
4015
+ tmem_load_atom_dV = cute.make_copy_atom(
4016
+ tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(8 if self.check_hdim_v_oob else 16)),
4017
+ Float32,
4018
+ )
4019
+ tmem_load_atom_dK = cute.make_copy_atom(
4020
+ tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(8 if self.check_hdim_oob else 16)),
4021
+ Float32,
4022
  )
4023
  # dV
4024
  pipeline_dKV.consumer_wait(consumer_state_dKV)
4025
 
4026
+ tiled_tmem_ld_dV = tcgen05.make_tmem_copy(tmem_load_atom_dV, tdVtdV)
4027
  thr_tmem_ld_dV = tiled_tmem_ld_dV.get_slice(tidx)
4028
 
4029
  tdVtdV_t2r_p = thr_tmem_ld_dV.partition_S(tdVtdV)
 
4035
 
4036
  tdVcdV_t2r_p = thr_tmem_ld_dV.partition_D(tdVcdV_tensor)
4037
  tdVcdV_t2r = self.split_wg(tdVcdV_t2r_p, wg_idx, num_wg)
4038
+ tdVrdV_t2r = cute.make_rmem_tensor(tdVcdV_t2r.shape, Float32)
4039
 
4040
  cute.copy(thr_tmem_ld_dV, tdVtdV_t2r, tdVrdV_t2r)
4041
  cute.arch.fence_view_async_tmem_load()
 
4052
  tiler_mn=tiled_tmem_ld_dV.tiler_mn,
4053
  )
4054
 
4055
+ tdVrdV_r2s = cute.make_rmem_tensor(tdVrdV_t2r.shape, self.dv_dtype)
4056
  for i in cutlass.range_constexpr(cute.size(tdVrdV_t2r, mode=[1])):
4057
  dV_vec = tdVrdV_t2r[(None, i, 0, 0)].load()
4058
  tdVrdV_r2s[(None, i, 0, 0)].store(dV_vec.to(self.dv_dtype))
 
4064
  tdVgdV_r2g_p = thr_tmem_ld_dV.partition_D(tdVgdV)
4065
  tdVgdV_r2g = self.split_wg(tdVgdV_r2g_p, wg_idx, num_wg)
4066
 
4067
+ tdVpdV = None
4068
+ if const_expr(self.check_hdim_v_oob):
4069
+ tdVpdV = self.predicate_hdim(tdVcdV_t2r, self.head_dim_v)
4070
  if tidx < seqlen.seqlen_k - self.tile_n * n_block:
4071
+ cute.copy(tiled_gmem_store_dV, tdVrdV_r2s, tdVgdV_r2g, pred=tdVpdV)
4072
 
4073
  cute.arch.sync_warp()
4074
  with cute.arch.elect_one():
 
4078
  # dK
4079
  pipeline_dKV.consumer_wait(consumer_state_dKV)
4080
 
4081
+ tiled_tmem_ld_dK = tcgen05.make_tmem_copy(tmem_load_atom_dK, tdKtdK)
4082
  thr_tmem_ld_dK = tiled_tmem_ld_dK.get_slice(tidx)
4083
 
4084
  tdKtdK_t2r_p = thr_tmem_ld_dK.partition_S(tdKtdK)
 
4090
 
4091
  tdKcdK_t2r_p = thr_tmem_ld_dK.partition_D(tdKcdK_tensor)
4092
  tdKcdK_t2r = self.split_wg(tdKcdK_t2r_p, wg_idx, num_wg)
4093
+ tdKrdK_t2r = cute.make_rmem_tensor(tdKcdK_t2r.shape, Float32)
4094
 
4095
  cute.copy(tiled_tmem_ld_dK, tdKtdK_t2r, tdKrdK_t2r)
4096
  cute.arch.fence_view_async_tmem_load()
 
4108
  tiler_mn=tiled_tmem_ld_dK.tiler_mn,
4109
  )
4110
 
4111
+ tdKrdK_r2s = cute.make_rmem_tensor(tdKrdK_t2r.shape, self.dk_dtype)
4112
 
4113
  for i in cutlass.range_constexpr(cute.size(tdKrdK_t2r, mode=[1])):
4114
  dK_vec = tdKrdK_t2r[(None, i, 0, 0)].load() * softmax_scale
 
4121
  tdKgdK_r2g_p = thr_tmem_ld_dK.partition_D(tdKgdK)
4122
  tdKgdK_r2g = self.split_wg(tdKgdK_r2g_p, wg_idx, num_wg)
4123
 
4124
+ tdKpdK = None
4125
+ if const_expr(self.check_hdim_oob):
4126
+ tdKpdK = self.predicate_hdim(tdKcdK_t2r, self.head_dim)
4127
  if tidx < seqlen.seqlen_k - self.tile_n * n_block:
4128
+ cute.copy(tiled_gmem_store_dK, tdKrdK_r2s, tdKgdK_r2g, pred=tdKpdK)
4129
 
4130
  cute.arch.sync_warp()
4131
  with cute.arch.elect_one():
4132
  pipeline_dKV.consumer_release(consumer_state_dKV)
4133
  return consumer_state_dKV
4134
 
4135
+ @cute.jit
4136
+ def predicate_hdim(self, tcoord: cute.Tensor, limit: cutlass.Constexpr[int]) -> cute.Tensor:
4137
+ """Per-store-vector head-dim predicate for the non-TMA dK/dV epilogue store.
4138
+
4139
+ ``tcoord`` is the (tile_n, hdim) identity tensor partitioned like the register data,
4140
+ shape ((rep, 1), chunks, 1, 1) with one contiguous ``rep``-wide column chunk per copy
4141
+ vector; the predicate drops the vector mode and marks each chunk whose first column
4142
+ is inside ``limit``. Valid because ``limit`` is a multiple of ``rep``.
4143
+ """
4144
+ tpred = cute.make_rmem_tensor(
4145
+ cute.make_layout((1, cute.size(tcoord, mode=[1]), 1, 1)), cutlass.Boolean
4146
+ )
4147
+ for i in cutlass.range_constexpr(cute.size(tcoord, mode=[1])):
4148
+ tpred[0, i, 0, 0] = tcoord[0, i, 0, 0][1] < limit
4149
+ return tpred
4150
+
4151
  @cute.jit
4152
  def epilogue_dK_or_dV_tma(
4153
  self,
 
4156
  head_idx: Int32,
4157
  n_block: Int32,
4158
  seqlen,
4159
+ thr_mma: cute.ThrMma,
4160
  tdKVtdKV: cute.Tensor,
4161
  mdKV: cute.Tensor,
4162
  sdKV: cute.Tensor,
 
4222
 
4223
  deterministic_KV = self.deterministic and self.qhead_per_kvhead > 1
4224
  if const_expr(deterministic_KV):
4225
+ assert mdKV_semaphore is not None
4226
  mdKV_semaphore_cur = mdKV_semaphore[n_block, None, head_idx_kv, batch_idx]
4227
 
4228
  if const_expr(not self.dKV_postprocess):
 
4275
  if const_expr(num_epi_stages > 1):
4276
  tdKVcdKV_t2r = tdKVcdKV_t2r[None, epi_stage]
4277
 
4278
+ tdKVrdKV_t2r = cute.make_rmem_tensor(tdKVcdKV_t2r.shape, Float32)
4279
 
4280
  assert cute.size(tdKVrdKV_t2r) == cute.size(tdKVtdKV_t2r) // cute.arch.WARP_SIZE, (
4281
  "RMEM<->TMEM fragment size mismatch"
 
4291
  tdKVrdKV_t2r[2 * i], tdKVrdKV_t2r[2 * i + 1] = cute.arch.mul_packed_f32x2(
4292
  (tdKVrdKV_t2r[2 * i], tdKVrdKV_t2r[2 * i + 1]), (scale, scale)
4293
  )
4294
+ tdKVrdKV = cute.make_rmem_tensor(tdKVrdKV_t2r.shape, dtype) # (32 columns)
4295
  tdKVrdKV.store(tdKVrdKV_t2r.load().to(dtype))
4296
 
4297
  # RMEM -> SMEM -- copy, fence and barrier
build/torch-cuda/flash_bwd_sm120.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # SM120 (Blackwell GeForce / DGX Spark) backward pass.
3
+ #
4
+ # SM120 uses the same SM80-era MMA instructions (mma.sync.aligned.m16n8k16) but has
5
+ # a smaller shared memory capacity (99 KB vs 163 KB on SM80). This module subclasses
6
+ # FlashAttentionBackwardSm80 and overrides the SMEM capacity check accordingly.
7
+
8
+ import cutlass
9
+ import cutlass.utils as utils_basic
10
+
11
+ from .flash_bwd import FlashAttentionBackwardSm80
12
+
13
+
14
+ class FlashAttentionBackwardSm120(FlashAttentionBackwardSm80):
15
+ @staticmethod
16
+ def can_implement(
17
+ dtype,
18
+ head_dim,
19
+ head_dim_v,
20
+ m_block_size,
21
+ n_block_size,
22
+ num_stages_Q,
23
+ num_stages_dO,
24
+ num_threads,
25
+ is_causal,
26
+ V_in_regs=False,
27
+ ) -> bool:
28
+ """Check if the kernel can be implemented on SM120.
29
+
30
+ Same logic as SM80 but uses SM120's shared memory capacity (99 KB).
31
+ """
32
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
33
+ return False
34
+ if head_dim % 8 != 0:
35
+ return False
36
+ if head_dim_v % 8 != 0:
37
+ return False
38
+ if n_block_size % 16 != 0:
39
+ return False
40
+ if num_threads % 32 != 0:
41
+ return False
42
+ # Shared memory usage: Q tile + dO tile + K tile + V tile
43
+ smem_usage_Q = m_block_size * head_dim * num_stages_Q * 2
44
+ smem_usage_dO = m_block_size * head_dim_v * num_stages_dO * 2
45
+ smem_usage_K = n_block_size * head_dim * 2
46
+ smem_usage_V = n_block_size * head_dim_v * 2
47
+ smem_usage_QV = (
48
+ (smem_usage_Q + smem_usage_V) if not V_in_regs else max(smem_usage_Q, smem_usage_V)
49
+ )
50
+ smem_usage = smem_usage_QV + smem_usage_dO + smem_usage_K
51
+ # SM120 has 99 KB shared memory (vs 163 KB on SM80)
52
+ smem_capacity = utils_basic.get_smem_capacity_in_bytes("sm_120")
53
+ if smem_usage > smem_capacity:
54
+ return False
55
+ return True
build/torch-cuda/flash_bwd_sm90.py CHANGED
@@ -24,10 +24,17 @@ from .seqlen_info import SeqlenInfoQK
24
  from .block_info import BlockInfo
25
  from . import pipeline
26
  from .quack.cute_dsl_utils import ParamsBase
27
- from .tile_scheduler import TileSchedulerArguments, SingleTileScheduler
 
 
 
 
 
 
28
  from .named_barrier import NamedBarrierBwd
29
  from .softmax import apply_score_mod_inner, apply_score_mod_bwd_inner
30
  from .block_sparsity import BlockSparseTensors
 
31
  from .block_sparse_utils import (
32
  get_total_q_block_count_bwd,
33
  produce_block_sparse_q_loads_bwd_sm90,
@@ -46,6 +53,8 @@ class FlashAttentionBackwardSm90:
46
  head_dim_v: Optional[int] = None,
47
  qhead_per_kvhead: int = 1,
48
  is_causal: bool = False,
 
 
49
  tile_m: int = 64,
50
  tile_n: int = 128,
51
  Q_stage: int = 2,
@@ -63,11 +72,12 @@ class FlashAttentionBackwardSm90:
63
  score_mod_bwd: cutlass.Constexpr | None = None,
64
  mask_mod: cutlass.Constexpr | None = None,
65
  has_aux_tensors: cutlass.Constexpr = False,
66
- subtile_factor: cutlass.Constexpr[int] = 1,
 
67
  ):
68
  self.dtype = dtype
69
- # padding head_dim to a multiple of 16 as k_block_size
70
- hdim_multiple_of = 16
71
  self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
72
  head_dim_v = head_dim_v if head_dim_v is not None else head_dim
73
  self.same_hdim_kv = head_dim == head_dim_v
@@ -77,7 +87,8 @@ class FlashAttentionBackwardSm90:
77
  self.check_hdim_v_oob = head_dim_v != self.tile_hdimv
78
  self.qhead_per_kvhead = qhead_per_kvhead
79
  self.is_causal = is_causal
80
- self.is_local = False
 
81
  self.tile_m = tile_m
82
  self.tile_n = tile_n
83
  self.num_threads = num_threads
@@ -92,23 +103,23 @@ class FlashAttentionBackwardSm90:
92
  self.AtomLayoutMSdP = AtomLayoutMSdP
93
  self.AtomLayoutNdKV = AtomLayoutNdKV
94
  self.AtomLayoutMdQ = AtomLayoutMdQ
95
- self.num_mma_warp_groups = (self.num_threads // 128) - 1
96
  self.mma_dkv_is_rs = (
97
  AtomLayoutMSdP == 1
98
- and AtomLayoutNdKV == self.num_mma_warp_groups
99
  and SdP_swapAB
100
  and not dKV_swapAB
101
  )
102
  self.V_in_regs = V_in_regs
 
103
  if qhead_per_kvhead > 1:
104
  assert self.same_hdim_kv, "GQA backward requires head_dim == head_dim_v"
105
- assert self.num_mma_warp_groups == 2, "GQA backward assumes 2 warp groups"
106
  # These are tuned for speed
107
  # Do we keep the LSE and dPsum in each thread, or split them across 8 threads that share
108
  # them and then shuffle to get the value whenever we need? This can reduce register
109
  # pressure when SdP_swapAB, where each thread needs to keep statistics for (kBlockM / 4)
110
  # rows. If !SdP_swapAB, each thread only needs to keep statistics for 2 rows.
111
- # TODO: impl these for hdim 64
112
  self.shuffle_LSE = self.SdP_swapAB and self.tile_hdim <= 64
113
  self.shuffle_dPsum = self.SdP_swapAB and self.tile_hdim <= 64
114
 
@@ -118,12 +129,18 @@ class FlashAttentionBackwardSm90:
118
  self.score_mod_bwd = score_mod_bwd
119
  self.mask_mod = mask_mod
120
  self.has_aux_tensors = has_aux_tensors
121
- self.subtile_factor = subtile_factor
122
  if cutlass.const_expr(has_aux_tensors):
123
  self.vec_size: cutlass.Constexpr = 1
124
  else:
125
  self.vec_size: cutlass.Constexpr = 4
126
  self.qk_acc_dtype = Float32
 
 
 
 
 
 
127
 
128
  @staticmethod
129
  def can_implement(
@@ -182,32 +199,64 @@ class FlashAttentionBackwardSm90:
182
  assert mQ_type == self.dtype
183
 
184
  def _setup_attributes(self):
185
- self.sQ_layout, self.sK_layout, self.sV_layout, self.sdO_layout, self.sPdS_layout = [
186
- sm90_utils.make_smem_layout(self.dtype, LayoutEnum.ROW_MAJOR, shape, stage)
187
- for shape, stage in [
188
- ((self.tile_m, self.tile_hdim), self.Q_stage),
189
- ((self.tile_n, self.tile_hdim), None),
190
- ((self.tile_n, self.tile_hdimv), None),
191
- ((self.tile_m, self.tile_hdimv), self.dO_stage),
192
- ((self.tile_m, self.tile_n), self.PdS_stage),
 
 
 
 
 
 
 
 
193
  ]
194
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  self.sdQaccum_layout = cute.make_layout(
196
- (self.tile_m * self.tile_hdim // self.num_mma_warp_groups, self.num_mma_warp_groups)
197
  )
198
  # dQaccum R->S
199
  self.r2s_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
200
  cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
201
  # thr_layout
202
- cute.make_layout((self.num_threads_per_warp_group, self.num_mma_warp_groups)),
203
  cute.make_layout(128 // Float32.width), # val_layout
204
  )
205
  # dKVaccum for GQA epilogue - reuses sV+sK memory recast as f32
206
  # TODO: assert that sVaccum and sKaccum don't overflow smem
207
 
208
  def _get_tiled_mma(self):
 
209
  # S = Q @ K.T, dP = dO @ V.T
210
- atom_layout_SdP = (self.AtomLayoutMSdP, self.num_mma_warp_groups // self.AtomLayoutMSdP)
211
  tiler_mn_SdP = (self.tile_m // atom_layout_SdP[0], self.tile_n // atom_layout_SdP[1])
212
  tiled_mma_SdP = sm90_utils_basic.make_trivial_tiled_mma(
213
  self.dtype,
@@ -215,12 +264,11 @@ class FlashAttentionBackwardSm90:
215
  warpgroup.OperandMajorMode.K,
216
  warpgroup.OperandMajorMode.K,
217
  Float32,
218
- atom_layout_mnk=(atom_layout_SdP if not self.SdP_swapAB else atom_layout_SdP[::-1])
219
- + (1,),
220
- tiler_mn=tiler_mn_SdP if not self.SdP_swapAB else tiler_mn_SdP[::-1],
221
  )
222
  # dV = P.T @ dO, dK = dS.T @ Q
223
- atom_layout_dKV = (self.AtomLayoutNdKV, self.num_mma_warp_groups // self.AtomLayoutNdKV)
224
  tiler_mn_dK = (self.tile_n // atom_layout_dKV[0], self.tile_hdim // atom_layout_dKV[1])
225
  tiler_mn_dV = (self.tile_n // atom_layout_dKV[0], self.tile_hdimv // atom_layout_dKV[1])
226
  tiled_mma_dK, tiled_mma_dV = [
@@ -232,9 +280,8 @@ class FlashAttentionBackwardSm90:
232
  else warpgroup.OperandMajorMode.K,
233
  warpgroup.OperandMajorMode.MN,
234
  Float32,
235
- atom_layout_mnk=(atom_layout_dKV if not self.dKV_swapAB else atom_layout_dKV[::-1])
236
- + (1,),
237
- tiler_mn=tiler_mn_d if not self.dKV_swapAB else tiler_mn_d[::-1],
238
  a_source=warpgroup.OperandSource.RMEM
239
  if self.mma_dkv_is_rs
240
  else warpgroup.OperandSource.SMEM,
@@ -242,7 +289,8 @@ class FlashAttentionBackwardSm90:
242
  for tiler_mn_d in (tiler_mn_dK, tiler_mn_dV)
243
  ]
244
  # dQ = dS @ K
245
- atom_layout_dQ = (self.AtomLayoutMdQ, self.num_mma_warp_groups // self.AtomLayoutMdQ)
 
246
  tiler_mn_dQ = (self.tile_m // atom_layout_dQ[0], self.tile_hdim // atom_layout_dQ[1])
247
  tiled_mma_dQ = sm90_utils_basic.make_trivial_tiled_mma(
248
  self.dtype,
@@ -250,8 +298,8 @@ class FlashAttentionBackwardSm90:
250
  warpgroup.OperandMajorMode.K if not self.dQ_swapAB else warpgroup.OperandMajorMode.MN,
251
  warpgroup.OperandMajorMode.MN if not self.dQ_swapAB else warpgroup.OperandMajorMode.K,
252
  Float32,
253
- atom_layout_mnk=(atom_layout_dQ if not self.dQ_swapAB else atom_layout_dQ[::-1]) + (1,),
254
- tiler_mn=tiler_mn_dQ if not self.dQ_swapAB else tiler_mn_dQ[::-1],
255
  )
256
  return tiled_mma_SdP, tiled_mma_dK, tiled_mma_dV, tiled_mma_dQ
257
 
@@ -305,23 +353,25 @@ class FlashAttentionBackwardSm90:
305
  mdK: cute.Tensor,
306
  mdV: cute.Tensor,
307
  softmax_scale: Float32,
308
- stream: cuda.CUstream,
309
  mCuSeqlensQ: Optional[cute.Tensor] = None,
310
  mCuSeqlensK: Optional[cute.Tensor] = None,
311
  mSeqUsedQ: Optional[cute.Tensor] = None,
312
  mSeqUsedK: Optional[cute.Tensor] = None,
313
- softcap: Float32 | float | None = None,
314
  window_size_left: Int32 | int | None = None,
315
  window_size_right: Int32 | int | None = None,
316
  mdQ_semaphore: Optional[cute.Tensor] = None,
317
  mdK_semaphore: Optional[cute.Tensor] = None,
318
  mdV_semaphore: Optional[cute.Tensor] = None,
319
- aux_tensors: Optional[list] = None,
320
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
 
 
 
321
  ):
322
- assert mdQ_semaphore is None and mdK_semaphore is None and mdV_semaphore is None, (
323
- "determinism not supported yet for Sm90"
324
- )
 
325
 
326
  self._check_type(
327
  *(
@@ -330,23 +380,45 @@ class FlashAttentionBackwardSm90:
330
  )
331
  )
332
 
 
 
333
  mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV = [
334
  assume_tensor_aligned(t) for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV)
335
  ]
336
 
337
- layout_transpose = [1, 3, 2, 0] # (b, s, n, h) --> (s, h, n, b)
338
- mQ, mK, mV, mdO = [layout_utils.select(t, layout_transpose) for t in (mQ, mK, mV, mdO)]
 
 
 
 
 
339
  if const_expr(self.qhead_per_kvhead == 1):
340
- mdK, mdV = [layout_utils.select(t, layout_transpose) for t in (mdK, mdV)]
341
  else:
342
- accum_transpose = [2, 1, 0] # (b, n, s*h) -> (s*h, n, b)
 
343
  mdK, mdV = [layout_utils.select(t, accum_transpose) for t in (mdK, mdV)]
344
- LSE_dPsum_dQaccum_transpose = [2, 1, 0] # (b, n, s) -> (s, n, b)
 
345
  mLSE, mdPsum, mdQaccum = [
346
  layout_utils.select(t, LSE_dPsum_dQaccum_transpose) for t in (mLSE, mdPsum, mdQaccum)
347
  ]
348
 
349
  tiled_mma_SdP, tiled_mma_dK, tiled_mma_dV, tiled_mma_dQ = self._get_tiled_mma()
 
 
 
 
 
 
 
 
 
 
 
 
 
350
 
351
  self.num_mma_threads = tiled_mma_SdP.size
352
  assert self.num_mma_threads + 128 == self.num_threads
@@ -354,10 +426,25 @@ class FlashAttentionBackwardSm90:
354
  self.num_threads_per_warp_group = 128
355
  self.num_producer_threads = 32
356
 
357
- self.num_mma_regs = 240
358
- self.num_producer_regs = 24
359
- # self.num_mma_regs = 232
360
- # self.num_producer_regs = 40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
  self._setup_attributes()
363
  SharedStorage = self._get_shared_storage_cls()
@@ -374,7 +461,7 @@ class FlashAttentionBackwardSm90:
374
  self.tma_copy_bytes["LSE"] = self.tile_m * Float32.width // 8
375
  self.tma_copy_bytes["dPsum"] = self.tile_m * Float32.width // 8
376
  self.tma_copy_bytes["dQ"] = (
377
- self.tile_m * self.tile_hdim * Float32.width // 8 // self.num_mma_warp_groups
378
  )
379
  self.tma_copy_bytes["dKacc"] = self.tile_n * self.tile_hdim * Float32.width // 8
380
  self.tma_copy_bytes["dVacc"] = self.tile_n * self.tile_hdimv * Float32.width // 8
@@ -404,38 +491,60 @@ class FlashAttentionBackwardSm90:
404
  (self.tile_m, self.tile_hdimv),
405
  )
406
  if const_expr(self.qhead_per_kvhead == 1):
 
 
 
 
 
 
 
 
 
 
407
  tma_atom_dK, tma_tensor_dK = cpasync.make_tiled_tma_atom(
408
  cpasync.CopyBulkTensorTileS2GOp(),
409
- mdK,
410
  cute.select(self.sK_layout, mode=[0, 1]),
411
  (self.tile_n, self.tile_hdim),
412
  )
413
  tma_atom_dV, tma_tensor_dV = cpasync.make_tiled_tma_atom(
414
  cpasync.CopyBulkTensorTileS2GOp(),
415
- mdV,
416
  cute.select(self.sV_layout, mode=[0, 1]),
417
  (self.tile_n, self.tile_hdimv),
418
  )
419
  else:
420
  tma_atom_dK = tma_atom_dV = tma_tensor_dK = tma_tensor_dV = None
421
 
422
- TileScheduler = SingleTileScheduler
 
 
 
 
 
 
423
  tile_sched_args = TileSchedulerArguments(
424
  cute.ceil_div(cute.size(mK.shape[0]), self.tile_n),
425
  cute.size(mQ.shape[2]),
426
- cute.size(mQ.shape[3]),
 
 
427
  1, # num_splits
428
- cute.size(mK.shape[0]),
429
- mQ.shape[1],
430
- mV.shape[1],
431
- total_q=cute.size(mQ.shape[0]) * cute.size(mQ.shape[3]),
432
- tile_shape_mn=(self.tile_m, self.tile_n),
433
- mCuSeqlensQ=None,
434
- mSeqUsedQ=None,
 
 
435
  qhead_per_kvhead_packgqa=1,
436
  element_size=self.dtype.width // 8,
437
  is_persistent=False,
438
- lpt=False,
 
 
439
  )
440
 
441
  tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
@@ -448,7 +557,7 @@ class FlashAttentionBackwardSm90:
448
  softmax_scale_log2 = LOG2_E
449
 
450
  fastdiv_mods = None
451
- if const_expr(aux_tensors is not None):
452
  seqlen_q = cute.size(mQ.shape[0])
453
  seqlen_k = cute.size(mK.shape[0])
454
  seqlen_q_divmod = FastDivmodDivisor(seqlen_q)
@@ -461,6 +570,11 @@ class FlashAttentionBackwardSm90:
461
 
462
  self.use_block_sparsity = cutlass.const_expr(blocksparse_tensors is not None)
463
 
 
 
 
 
 
464
  self.kernel(
465
  tma_tensor_Q,
466
  tma_tensor_K,
@@ -477,6 +591,10 @@ class FlashAttentionBackwardSm90:
477
  mLSE,
478
  mdPsum,
479
  mdQaccum,
 
 
 
 
480
  self.sQ_layout,
481
  self.sK_layout,
482
  self.sV_layout,
@@ -493,15 +611,21 @@ class FlashAttentionBackwardSm90:
493
  tile_sched_params,
494
  TileScheduler,
495
  SharedStorage,
496
- aux_tensors,
497
  fastdiv_mods,
498
  blocksparse_tensors,
499
  qhead_per_kvhead_divmod,
 
 
 
 
 
500
  ).launch(
501
  grid=grid_dim,
502
  block=[self.num_threads, 1, 1],
503
  stream=stream,
504
  min_blocks_per_mp=1,
 
505
  )
506
 
507
  @cute.kernel
@@ -522,6 +646,10 @@ class FlashAttentionBackwardSm90:
522
  mLSE: cute.Tensor,
523
  mdPsum: cute.Tensor,
524
  mdQaccum: cute.Tensor,
 
 
 
 
525
  sQ_layout: cute.ComposedLayout,
526
  sK_layout: cute.ComposedLayout,
527
  sV_layout: cute.ComposedLayout,
@@ -538,19 +666,23 @@ class FlashAttentionBackwardSm90:
538
  tile_sched_params: ParamsBase,
539
  TileScheduler: cutlass.Constexpr[Callable],
540
  SharedStorage: cutlass.Constexpr[Callable],
541
- aux_tensors: Optional[list] = None,
542
  fastdiv_mods=(None, None),
543
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
544
  qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
 
 
 
 
 
545
  ):
546
  warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
547
 
548
  # prefetch TMA descriptors
549
  if warp_idx == 0:
550
- cpasync.prefetch_descriptor(tma_atom_Q)
551
- cpasync.prefetch_descriptor(tma_atom_K)
552
- cpasync.prefetch_descriptor(tma_atom_V)
553
- cpasync.prefetch_descriptor(tma_atom_dO)
554
 
555
  smem = cutlass.utils.SmemAllocator()
556
  storage = smem.allocate(SharedStorage)
@@ -604,25 +736,27 @@ class FlashAttentionBackwardSm90:
604
  self.is_causal,
605
  self.is_local,
606
  False, # is_split_kv
607
- None,
608
- None,
609
  qhead_per_kvhead_packgqa=1,
610
  )
611
  SeqlenInfoCls = partial(
612
  SeqlenInfoQK.create,
613
  seqlen_q_static=mQ.shape[0],
614
  seqlen_k_static=mK.shape[0],
615
- mCuSeqlensQ=None,
616
- mCuSeqlensK=None,
617
- mSeqUsedQ=None,
618
- mSeqUsedK=None,
 
 
619
  )
620
  AttentionMaskCls = partial(
621
  AttentionMask,
622
  self.tile_m,
623
  self.tile_n,
624
- window_size_left=None,
625
- window_size_right=None,
626
  swap_AB=self.SdP_swapAB,
627
  )
628
  TileSchedulerCls = partial(TileScheduler.create, tile_sched_params)
@@ -663,18 +797,20 @@ class FlashAttentionBackwardSm90:
663
  TileSchedulerCls,
664
  SeqlenInfoCls,
665
  blocksparse_tensors,
 
666
  )
667
  else:
668
- cute.arch.setmaxregister_increase(self.num_mma_regs)
669
  tidx, _, _ = cute.arch.thread_idx()
670
  tidx = tidx - 128
671
- self.mma(
672
  tiled_mma_SdP,
673
  tiled_mma_dK,
674
  tiled_mma_dV,
675
  tiled_mma_dQ,
676
  mdK,
677
  mdV,
 
 
678
  mdQaccum,
679
  sQ,
680
  sK,
@@ -697,11 +833,24 @@ class FlashAttentionBackwardSm90:
697
  SeqlenInfoCls,
698
  AttentionMaskCls,
699
  TileSchedulerCls,
700
- aux_tensors,
701
  fastdiv_mods,
702
  blocksparse_tensors,
703
  qhead_per_kvhead_divmod,
704
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
705
 
706
  @cute.jit
707
  def load(
@@ -749,18 +898,22 @@ class FlashAttentionBackwardSm90:
749
  if const_expr(self.qhead_per_kvhead == 1)
750
  else head_idx // qhead_per_kvhead_divmod
751
  )
752
- mK_cur = mK[None, None, head_idx_kv, batch_idx]
 
753
  gK = cute.local_tile(mK_cur, (self.tile_n, self.tile_hdim), (n_block, 0))
754
- mV_cur = mV[None, None, head_idx_kv, batch_idx]
755
  gV = cute.local_tile(mV_cur, (self.tile_n, self.tile_hdimv), (n_block, 0))
756
 
757
- mQ_cur = mQ[None, None, head_idx, batch_idx]
 
 
 
 
 
 
 
758
  gQ = cute.local_tile(mQ_cur, (self.tile_m, self.tile_hdim), (None, 0))
759
- mdO_cur = mdO[None, None, head_idx, batch_idx]
760
  gdO = cute.local_tile(mdO_cur, (self.tile_m, self.tile_hdimv), (None, 0))
761
- mLSE_cur = mLSE[None, head_idx, batch_idx]
762
  gLSE = cute.local_tile(mLSE_cur, (self.tile_m,), (None,))
763
- mdPsum_cur = mdPsum[None, head_idx, batch_idx]
764
  gdPsum = cute.local_tile(mdPsum_cur, (self.tile_m,), (None,))
765
 
766
  load_K, _, _ = copy_utils.tma_get_copy_fn(
@@ -786,14 +939,17 @@ class FlashAttentionBackwardSm90:
786
 
787
  if const_expr(not self.use_block_sparsity):
788
  total_m_block_cnt = m_block_max - m_block_min
789
- process_tile = const_expr(not self.is_local) or m_block_min < m_block_max
 
 
 
790
  else:
791
  total_m_block_cnt = get_total_q_block_count_bwd(
792
  blocksparse_tensors,
793
  batch_idx,
794
  head_idx,
795
  n_block,
796
- subtile_factor=self.subtile_factor,
797
  m_block_max=m_block_max,
798
  )
799
  process_tile = total_m_block_cnt > Int32(0)
@@ -806,6 +962,8 @@ class FlashAttentionBackwardSm90:
806
  )
807
  load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q))
808
  load_Q(first_m_block, producer_state=producer_state_Q)
 
 
809
  load_LSE(first_m_block, producer_state=producer_state_Q)
810
  producer_state_dO_cur = (
811
  producer_state_dO
@@ -836,6 +994,8 @@ class FlashAttentionBackwardSm90:
836
  producer_state_Q.advance()
837
  producer_state_dO.advance()
838
  else:
 
 
839
  producer_state_Q, producer_state_dO = produce_block_sparse_q_loads_bwd_sm90(
840
  blocksparse_tensors,
841
  batch_idx,
@@ -854,7 +1014,7 @@ class FlashAttentionBackwardSm90:
854
  self.tma_copy_bytes["K"],
855
  self.tma_copy_bytes["V"],
856
  Q_stage_eq_dO_stage=(self.Q_stage == self.dO_stage),
857
- subtile_factor=self.subtile_factor,
858
  m_block_max=m_block_max,
859
  )
860
 
@@ -866,14 +1026,14 @@ class FlashAttentionBackwardSm90:
866
  def apply_score_mod(
867
  self,
868
  acc_S: cute.Tensor,
869
- thr_mma_SdP: cute.core.ThrMma,
870
  batch_idx,
871
  head_idx,
872
  m_block,
873
  n_block,
874
  softmax_scale,
875
  seqlen_info: SeqlenInfoQK,
876
- aux_tensors=None,
877
  fastdiv_mods=(None, None),
878
  ):
879
  # [NOTE] SdP_swapAB: swapAB transposes the tile, so use (n, m) indexing
@@ -897,7 +1057,7 @@ class FlashAttentionBackwardSm90:
897
  softmax_scale,
898
  self.vec_size,
899
  self.qk_acc_dtype,
900
- aux_tensors,
901
  fastdiv_mods,
902
  seqlen_info,
903
  constant_q_idx=None,
@@ -910,14 +1070,14 @@ class FlashAttentionBackwardSm90:
910
  self,
911
  grad_tensor: cute.Tensor,
912
  score_tensor: cute.Tensor,
913
- thr_mma_SdP: cute.core.ThrMma,
914
  batch_idx,
915
  head_idx,
916
  m_block,
917
  n_block,
918
  softmax_scale,
919
  seqlen_info: SeqlenInfoQK,
920
- aux_tensors=None,
921
  fastdiv_mods=(None, None),
922
  ):
923
  cS = cute.make_identity_tensor(
@@ -941,7 +1101,7 @@ class FlashAttentionBackwardSm90:
941
  softmax_scale,
942
  self.vec_size,
943
  self.qk_acc_dtype,
944
- aux_tensors,
945
  fastdiv_mods,
946
  seqlen_info,
947
  constant_q_idx=None,
@@ -958,6 +1118,8 @@ class FlashAttentionBackwardSm90:
958
  tiled_mma_dQ: cute.TiledMma,
959
  mdK: cute.Tensor,
960
  mdV: cute.Tensor,
 
 
961
  mdQaccum: cute.Tensor,
962
  sQ: cute.Tensor,
963
  sK: cute.Tensor,
@@ -980,20 +1142,24 @@ class FlashAttentionBackwardSm90:
980
  SeqlenInfoCls: Callable,
981
  AttentionMaskCls: Callable,
982
  TileSchedulerCls: Callable,
983
- aux_tensors: Optional[list] = None,
984
  fastdiv_mods=(None, None),
985
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
986
  qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
 
987
  ):
988
  warp_group_idx = cute.arch.make_warp_uniform(tidx // self.num_threads_per_warp_group)
989
  warp_group_thread_layout = cute.make_layout(
990
- self.num_mma_warp_groups, stride=self.num_threads_per_warp_group
991
  )
992
  thr_mma_SdP = tiled_mma_SdP.get_slice(tidx)
993
  wg_mma_SdP = tiled_mma_SdP.get_slice(warp_group_thread_layout(warp_group_idx))
994
  wg_mma_dK = tiled_mma_dK.get_slice(warp_group_thread_layout(warp_group_idx))
995
  wg_mma_dV = tiled_mma_dV.get_slice(warp_group_thread_layout(warp_group_idx))
996
- wg_mma_dQ = tiled_mma_dQ.get_slice(warp_group_thread_layout(warp_group_idx))
 
 
 
997
  # S = Q @ K.T
998
  shape_mnk_S = (self.tile_m, self.tile_n, self.tile_hdim)
999
  _, tSrQ, tSrK = sm90_utils.partition_fragment_ABC(
@@ -1039,23 +1205,41 @@ class FlashAttentionBackwardSm90:
1039
  # dQ = dS @ K
1040
  sKt = layout_utils.transpose_view(sK)
1041
  shape_mnk_dQ = (self.tile_m, self.tile_hdim, self.tile_n)
1042
- _, tdQrdS, tdQrKt = sm90_utils.partition_fragment_ABC(
1043
- wg_mma_dQ, shape_mnk_dQ, sdS, sKt, swap_AB=self.dQ_swapAB
1044
- )
1045
- mma_dsk_fn = partial(
1046
- gemm_zero_init, tiled_mma_dQ, shape_mnk_dQ[:2], tdQrdS, tdQrKt, swap_AB=self.dQ_swapAB
1047
- )
 
 
 
 
 
 
 
1048
 
1049
- # Smem copy atom tiling
1050
  copy_P_r2s = None
 
1051
  if const_expr(sP is not None):
1052
  sP_cpy = sP if const_expr(not self.SdP_swapAB) else sPt
1053
  copy_P_r2s, _, _ = copy_utils.get_smem_store_C(
1054
- tiled_mma_SdP, sP_cpy, tidx, self.arch, transpose=self.SdP_swapAB
 
 
 
 
 
1055
  )
1056
  sdS_cpy = sdS if const_expr(not self.SdP_swapAB) else sdSt
1057
  copy_dS_r2s, _, _ = copy_utils.get_smem_store_C(
1058
- tiled_mma_SdP, sdS_cpy, tidx, self.arch, transpose=self.SdP_swapAB
 
 
 
 
 
1059
  )
1060
 
1061
  tLSEsLSE = layout_utils.mma_partition_C_vec(
@@ -1064,27 +1248,25 @@ class FlashAttentionBackwardSm90:
1064
  tLSEsdPsum = layout_utils.mma_partition_C_vec(
1065
  sdPsum, thr_mma_SdP, expand_shape=self.tile_n, is_colvec=not self.SdP_swapAB
1066
  )
1067
-
1068
- smem_thr_copy_dQaccum = r2s_tiled_copy_dQaccum.get_slice(tidx)
1069
- tdQsdQaccum = smem_thr_copy_dQaccum.partition_D(sdQaccum)
 
 
 
 
 
 
 
 
 
 
 
 
1070
 
1071
  PdS_barrier = cutlass.pipeline.NamedBarrier(
1072
  barrier_id=int(NamedBarrierBwd.PdS), num_threads=self.num_mma_threads
1073
  )
1074
- score_mod_fn = partial(
1075
- self.apply_score_mod,
1076
- thr_mma_SdP=thr_mma_SdP,
1077
- softmax_scale=softmax_scale,
1078
- aux_tensors=aux_tensors,
1079
- fastdiv_mods=fastdiv_mods,
1080
- )
1081
- score_mod_bwd_fn = partial(
1082
- self.apply_score_mod_bwd,
1083
- thr_mma_SdP=thr_mma_SdP,
1084
- softmax_scale=softmax_scale,
1085
- aux_tensors=aux_tensors,
1086
- fastdiv_mods=fastdiv_mods,
1087
- )
1088
 
1089
  mma_one_m_block_all = partial(
1090
  self.mma_one_m_block,
@@ -1105,6 +1287,7 @@ class FlashAttentionBackwardSm90:
1105
  PdS_barrier=PdS_barrier,
1106
  # acc_dV=acc_dV,
1107
  # acc_dK=acc_dK,
 
1108
  )
1109
 
1110
  consumer_state_Q = cutlass.pipeline.make_pipeline_state(
@@ -1118,7 +1301,40 @@ class FlashAttentionBackwardSm90:
1118
  while work_tile.is_valid_tile:
1119
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
1120
  seqlen = SeqlenInfoCls(batch_idx)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1121
  mask = AttentionMaskCls(seqlen)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1122
  score_mod_fn_cur = partial(
1123
  score_mod_fn,
1124
  batch_idx=batch_idx,
@@ -1136,14 +1352,17 @@ class FlashAttentionBackwardSm90:
1136
  m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
1137
 
1138
  if const_expr(not self.use_block_sparsity):
1139
- process_tile = const_expr(not self.is_local) or m_block_min < m_block_max
 
 
 
1140
  else:
1141
  total_m_block_cnt = get_total_q_block_count_bwd(
1142
  blocksparse_tensors,
1143
  batch_idx,
1144
  head_idx,
1145
  n_block,
1146
- subtile_factor=self.subtile_factor,
1147
  m_block_max=m_block_max,
1148
  )
1149
  process_tile = total_m_block_cnt > Int32(0)
@@ -1160,7 +1379,7 @@ class FlashAttentionBackwardSm90:
1160
  mask_causal=self.is_causal,
1161
  mask_local=self.is_local,
1162
  mask_mod=self.mask_mod,
1163
- aux_tensors=aux_tensors,
1164
  fastdiv_mods=fastdiv_mods,
1165
  )
1166
  dKV_accumulate = False
@@ -1191,9 +1410,9 @@ class FlashAttentionBackwardSm90:
1191
  thr_mma_SdP=thr_mma_SdP,
1192
  score_mod_fn=score_mod_fn_cur,
1193
  score_mod_bwd_fn=score_mod_bwd_fn_cur,
1194
- subtile_factor=self.subtile_factor,
1195
  m_block_max=m_block_max,
1196
- aux_tensors=aux_tensors,
1197
  fastdiv_mods=fastdiv_mods,
1198
  )
1199
 
@@ -1216,10 +1435,12 @@ class FlashAttentionBackwardSm90:
1216
  head_idx,
1217
  batch_idx,
1218
  qhead_per_kvhead_divmod,
 
 
1219
  )
1220
  else:
1221
- # Block sparsity: KV tile with zero Q blocks produces no dK/dV; write zeros.
1222
- if const_expr(self.use_block_sparsity):
1223
  acc_dK.fill(0.0)
1224
  acc_dV.fill(0.0)
1225
  self.epilogue_dKV(
@@ -1239,6 +1460,8 @@ class FlashAttentionBackwardSm90:
1239
  head_idx,
1240
  batch_idx,
1241
  qhead_per_kvhead_divmod,
 
 
1242
  )
1243
 
1244
  tile_scheduler.advance_to_next_work()
@@ -1248,6 +1471,22 @@ class FlashAttentionBackwardSm90:
1248
  if warp_idx == 4:
1249
  cute.arch.cp_async_bulk_wait_group(0, read=True)
1250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1251
  @cute.jit
1252
  def mma_one_m_block(
1253
  self,
@@ -1266,16 +1505,17 @@ class FlashAttentionBackwardSm90:
1266
  pipeline_dO: cutlass.pipeline.PipelineAsync,
1267
  tLSEsLSE: cute.Tensor,
1268
  tLSEsdPsum: cute.Tensor,
1269
- tdQsdQaccum: cute.Tensor,
1270
  softmax_scale_log2: Float32,
1271
  PdS_barrier: cutlass.pipeline.NamedBarrier,
 
1272
  mask_fn: Optional[Callable] = None,
1273
  score_mod_fn: Optional[Callable] = None,
1274
  score_mod_bwd_fn: Optional[Callable] = None,
1275
  dKV_accumulate: Boolean = True,
1276
  ):
1277
  consumer_state_dO_cur = (
1278
- consumer_state_dO if const_expr(self.Q_stage == self.dO_stage) else consumer_state_Q
1279
  )
1280
  smem_idx_Q = consumer_state_Q.index
1281
  smem_idx_dO = consumer_state_dO_cur.index if const_expr(self.dO_stage > 1) else 0
@@ -1283,6 +1523,7 @@ class FlashAttentionBackwardSm90:
1283
  # (1) [GEMM 1] S = Q @ K^T
1284
  pipeline_Q.consumer_wait(consumer_state_Q, pipeline_Q.consumer_try_wait(consumer_state_Q))
1285
  acc_S = mma_qk_fn(A_idx=smem_idx_Q, wg_wait=-1)
 
1286
  tLSErLSE = copy_utils.load_s2r(tLSEsLSE[None, smem_idx_Q])
1287
  # (2) [GEMM 2] dP = dO @ V.T
1288
  pipeline_dO.consumer_wait(
@@ -1301,10 +1542,12 @@ class FlashAttentionBackwardSm90:
1301
  if cutlass.const_expr(mask_fn is not None):
1302
  mask_fn(acc_S, m_block=m_block)
1303
  acc_S_mn = layout_utils.reshape_acc_to_mn(acc_S, transpose=self.SdP_swapAB)
 
1304
  for r in cutlass.range_constexpr(cute.size(acc_S_mn, mode=[0])):
 
1305
  for c in cutlass.range(cute.size(acc_S_mn, mode=[1]), unroll_full=True):
1306
  acc_S_mn[r, c] = cute.math.exp2(
1307
- acc_S_mn[r, c] * softmax_scale_log2 - tLSErLSE[r], fastmath=True
1308
  )
1309
  tLSErdPsum = copy_utils.load_s2r(tLSEsdPsum[None, smem_idx_dO])
1310
 
@@ -1321,8 +1564,9 @@ class FlashAttentionBackwardSm90:
1321
  warpgroup.wait_group(0)
1322
  acc_dP_mn = layout_utils.reshape_acc_to_mn(acc_dP, transpose=self.SdP_swapAB)
1323
  for r in cutlass.range_constexpr(cute.size(acc_dP_mn, mode=[0])):
 
1324
  for c in cutlass.range(cute.size(acc_dP_mn, mode=[1]), unroll_full=True):
1325
- acc_dP_mn[r, c] = acc_S_mn[r, c] * (acc_dP_mn[r, c] - tLSErdPsum[r])
1326
 
1327
  if const_expr(self.score_mod_bwd is not None):
1328
  score_mod_bwd_fn(acc_dP, acc_S_pre, m_block=m_block)
@@ -1354,36 +1598,50 @@ class FlashAttentionBackwardSm90:
1354
  # smem fence to make sure sdS is written before it's read by WGMMA
1355
  cute.arch.fence_view_async_shared()
1356
  PdS_barrier.arrive_and_wait()
1357
- # (6) [GEMM 4] dQ = dS @ K
1358
- acc_dQ = mma_dsk_fn(A_idx=smem_idx_PdS, wg_wait=1)
1359
- # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dV)
1360
- pipeline_dO.consumer_release(consumer_state_dO_cur) # release dO as dV mma is done
1361
 
1362
- # (7) [GEMM 5] dK += dS.T @ Q
1363
- if const_expr(not self.mma_dkv_is_rs):
1364
- mma_dsq_fn(
1365
- A_idx=smem_idx_PdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1
1366
- )
1367
- else:
1368
- mma_dsq_fn(tCrA=tdKrdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1)
1369
- # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dQ)
1370
 
1371
- cute.arch.barrier(
1372
- barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1373
- number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1374
- )
1375
- tdQrdQaccum_flat = cute.make_tensor(acc_dQ.iterator, cute.make_layout(tdQsdQaccum.shape))
1376
- cute.autovec_copy(tdQrdQaccum_flat, tdQsdQaccum)
1377
- cute.arch.fence_view_async_shared()
1378
- cute.arch.barrier_arrive(
1379
- barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1380
- number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1381
- )
1382
 
1383
- warpgroup.wait_group(0)
1384
- # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dK)
1385
- pipeline_Q.consumer_release(consumer_state_Q)
1386
- # 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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1387
 
1388
  consumer_state_Q.advance()
1389
  consumer_state_dO.advance()
@@ -1408,6 +1666,8 @@ class FlashAttentionBackwardSm90:
1408
  head_idx: Int32,
1409
  batch_idx: Int32,
1410
  qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
 
 
1411
  ):
1412
  epi_barrier = cutlass.pipeline.NamedBarrier(
1413
  barrier_id=int(NamedBarrierBwd.Epilogue), num_threads=self.num_mma_threads
@@ -1415,8 +1675,12 @@ class FlashAttentionBackwardSm90:
1415
  warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
1416
 
1417
  if const_expr(self.qhead_per_kvhead == 1):
1418
- mdV_cur = mdV[None, None, head_idx, batch_idx]
1419
- mdK_cur = mdK[None, None, head_idx, batch_idx]
 
 
 
 
1420
  gdK = cute.local_tile(mdK_cur, (self.tile_n, self.tile_hdim), (n_block, 0))
1421
  gdV = cute.local_tile(mdV_cur, (self.tile_n, self.tile_hdimv), (n_block, 0))
1422
  store_dK, _, _ = copy_utils.tma_get_copy_fn(
@@ -1428,10 +1692,18 @@ class FlashAttentionBackwardSm90:
1428
  sdV = sV if const_expr(not self.dKV_swapAB) else layout_utils.transpose_view(sV)
1429
  sdK = sK if const_expr(not self.dKV_swapAB) else layout_utils.transpose_view(sK)
1430
  copy_dV_r2s, _, _ = copy_utils.get_smem_store_C(
1431
- tiled_mma_dV, sdV, tidx, self.arch, transpose=self.dKV_swapAB
 
 
 
 
1432
  )
1433
  copy_dK_r2s, _, _ = copy_utils.get_smem_store_C(
1434
- tiled_mma_dK, sdK, tidx, self.arch, transpose=self.dKV_swapAB
 
 
 
 
1435
  )
1436
  cute.arch.cp_async_bulk_wait_group(1, read=True)
1437
  epi_barrier.arrive_and_wait()
@@ -1450,15 +1722,26 @@ class FlashAttentionBackwardSm90:
1450
  store_dK()
1451
  cute.arch.cp_async_bulk_commit_group()
1452
  else:
1453
- sdKaccum_shape0 = self.tile_n * self.tile_hdim // self.num_mma_warp_groups
1454
- sdVaccum_shape0 = self.tile_n * self.tile_hdimv // self.num_mma_warp_groups
1455
- sdKaccum_layout = cute.make_layout((sdKaccum_shape0, self.num_mma_warp_groups))
1456
- sdVaccum_layout = cute.make_layout((sdVaccum_shape0, self.num_mma_warp_groups))
 
1457
  head_idx_kv = head_idx // qhead_per_kvhead_divmod
1458
- mdKaccum_cur = mdK[None, head_idx_kv, batch_idx]
 
 
 
 
 
 
 
 
 
 
 
1459
  gdKaccum_ = cute.local_tile(mdKaccum_cur, (self.tile_n * self.tile_hdim,), (n_block,))
1460
  gdKaccum = cute.flat_divide(gdKaccum_, (sdKaccum_shape0,))
1461
- mdVaccum_cur = mdV[None, head_idx_kv, batch_idx]
1462
  gdVaccum_ = cute.local_tile(mdVaccum_cur, (self.tile_n * self.tile_hdimv,), (n_block,))
1463
  gdVaccum = cute.flat_divide(gdVaccum_, (sdVaccum_shape0,))
1464
  # These two overlap each other
@@ -1467,14 +1750,17 @@ class FlashAttentionBackwardSm90:
1467
  sdVaccum = cute.make_tensor(sVaccum_ptr, sdVaccum_layout)
1468
  tiled_copy_dKVaccum_r2s = cute.make_tiled_copy_tv(
1469
  cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
1470
- cute.make_layout((self.num_threads_per_warp_group, self.num_mma_warp_groups)),
1471
  cute.make_layout(128 // Float32.width),
1472
  )
1473
  thr_copy_dKVaccum_r2s = tiled_copy_dKVaccum_r2s.get_slice(tidx)
1474
  tdKsdKaccum = thr_copy_dKVaccum_r2s.partition_D(sdKaccum)
1475
  tdVsdVaccum = thr_copy_dKVaccum_r2s.partition_D(sdVaccum)
1476
 
1477
- cute.arch.cp_async_bulk_wait_group(0, read=True)
 
 
 
1478
  epi_barrier.arrive_and_wait()
1479
  tdKrdKaccum_flat = cute.make_tensor(acc_dK.iterator, tdKsdKaccum.shape)
1480
  cute.autovec_copy(tdKrdKaccum_flat, tdKsdKaccum)
@@ -1482,15 +1768,18 @@ class FlashAttentionBackwardSm90:
1482
  epi_barrier.arrive_and_wait()
1483
  if warp_idx == 4:
1484
  with cute.arch.elect_one():
1485
- for wg_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
1486
  copy_utils.cpasync_reduce_bulk_add_f32(
1487
  sdKaccum[None, wg_idx].iterator,
1488
  gdKaccum[None, wg_idx].iterator,
1489
- self.tma_copy_bytes["dKacc"] // self.num_mma_warp_groups,
1490
  )
1491
  cute.arch.cp_async_bulk_commit_group()
1492
 
1493
- cute.arch.cp_async_bulk_wait_group(0, read=True)
 
 
 
1494
  epi_barrier.arrive_and_wait()
1495
  tdVrdVaccum_flat = cute.make_tensor(acc_dV.iterator, tdVsdVaccum.shape)
1496
  cute.autovec_copy(tdVrdVaccum_flat, tdVsdVaccum)
@@ -1498,13 +1787,16 @@ class FlashAttentionBackwardSm90:
1498
  epi_barrier.arrive_and_wait()
1499
  if warp_idx == 4:
1500
  with cute.arch.elect_one():
1501
- for wg_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
1502
  copy_utils.cpasync_reduce_bulk_add_f32(
1503
  sdVaccum[None, wg_idx].iterator,
1504
  gdVaccum[None, wg_idx].iterator,
1505
- self.tma_copy_bytes["dVacc"] // self.num_mma_warp_groups,
1506
  )
1507
  cute.arch.cp_async_bulk_commit_group()
 
 
 
1508
 
1509
  @cute.jit
1510
  def dQaccum_store(
@@ -1515,21 +1807,45 @@ class FlashAttentionBackwardSm90:
1515
  TileSchedulerCls: cutlass.Constexpr[Callable],
1516
  SeqlenInfoCls: cutlass.Constexpr[Callable],
1517
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
 
1518
  ):
 
 
 
 
 
1519
  tile_scheduler = TileSchedulerCls()
1520
  work_tile = tile_scheduler.initial_work_tile_info()
1521
  while work_tile.is_valid_tile:
1522
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
1523
  seqlen = SeqlenInfoCls(batch_idx)
1524
- mdQaccum_cur = mdQaccum[None, head_idx, batch_idx]
1525
- gdQaccum_ = cute.local_tile(mdQaccum_cur, (self.tile_m * self.tile_hdim,), (None,))
1526
- # (M * K / WG, WG, _)
1527
- gdQaccum = cute.flat_divide(
1528
- gdQaccum_, (self.tile_m * self.tile_hdim // self.num_mma_warp_groups,)
 
 
 
 
 
 
 
 
 
 
1529
  )
 
 
 
 
 
1530
  m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
1531
  if const_expr(not self.use_block_sparsity):
1532
- process_tile = const_expr(not self.is_local) or m_block_min < m_block_max
 
 
 
1533
  loop_count = m_block_max - m_block_min
1534
  else:
1535
  total_block_cnt = get_total_q_block_count_bwd(
@@ -1537,7 +1853,7 @@ class FlashAttentionBackwardSm90:
1537
  batch_idx,
1538
  head_idx,
1539
  n_block,
1540
- subtile_factor=self.subtile_factor,
1541
  m_block_max=m_block_max,
1542
  )
1543
  process_tile = total_block_cnt > Int32(0)
@@ -1548,17 +1864,36 @@ class FlashAttentionBackwardSm90:
1548
  m_block = m_block_min + iter_idx
1549
  m_block_safe = m_block
1550
 
1551
- for warp_group_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
1552
- cute.arch.cp_async_bulk_wait_group(
1553
- self.num_mma_warp_groups - 1 - warp_group_idx, read=True
1554
- )
 
 
 
1555
  cute.arch.barrier_arrive(
1556
  barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1557
  number_of_threads=self.num_threads_per_warp_group
1558
  + cute.arch.WARP_SIZE,
1559
  )
1560
 
1561
- for warp_group_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1562
  cute.arch.barrier(
1563
  barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1564
  number_of_threads=self.num_threads_per_warp_group
@@ -1567,11 +1902,24 @@ class FlashAttentionBackwardSm90:
1567
  with cute.arch.elect_one():
1568
  copy_utils.cpasync_reduce_bulk_add_f32(
1569
  sdQaccum[None, warp_group_idx].iterator,
1570
- gdQaccum[None, warp_group_idx, m_block_safe].iterator,
1571
  self.tma_copy_bytes["dQ"],
1572
  )
1573
  cute.arch.cp_async_bulk_commit_group()
 
 
 
 
 
 
 
 
 
 
1574
  else:
 
 
 
1575
  dQaccum_store_block_sparse_bwd_sm90(
1576
  blocksparse_tensors,
1577
  batch_idx,
@@ -1579,13 +1927,29 @@ class FlashAttentionBackwardSm90:
1579
  n_block,
1580
  sdQaccum,
1581
  gdQaccum,
1582
- subtile_factor=self.subtile_factor,
1583
  m_block_max=m_block_max,
1584
- num_mma_warp_groups=self.num_mma_warp_groups,
1585
  num_threads_per_warp_group=self.num_threads_per_warp_group,
1586
  tma_copy_bytes_dQ=self.tma_copy_bytes["dQ"],
1587
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1588
  tile_scheduler.advance_to_next_work()
1589
  work_tile = tile_scheduler.get_current_work()
1590
 
1591
- cute.arch.cp_async_bulk_wait_group(0, read=True)
 
 
24
  from .block_info import BlockInfo
25
  from . import pipeline
26
  from .quack.cute_dsl_utils import ParamsBase
27
+ from .tile_scheduler import (
28
+ TileSchedulerArguments,
29
+ SingleTileScheduler,
30
+ SingleTileLPTBwdScheduler,
31
+ SingleTileVarlenScheduler,
32
+ )
33
+ from . import barrier
34
  from .named_barrier import NamedBarrierBwd
35
  from .softmax import apply_score_mod_inner, apply_score_mod_bwd_inner
36
  from .block_sparsity import BlockSparseTensors
37
+ from .utils import AuxData
38
  from .block_sparse_utils import (
39
  get_total_q_block_count_bwd,
40
  produce_block_sparse_q_loads_bwd_sm90,
 
53
  head_dim_v: Optional[int] = None,
54
  qhead_per_kvhead: int = 1,
55
  is_causal: bool = False,
56
+ is_local: bool = False,
57
+ deterministic: bool = False,
58
  tile_m: int = 64,
59
  tile_n: int = 128,
60
  Q_stage: int = 2,
 
72
  score_mod_bwd: cutlass.Constexpr | None = None,
73
  mask_mod: cutlass.Constexpr | None = None,
74
  has_aux_tensors: cutlass.Constexpr = False,
75
+ q_subtile_factor: cutlass.Constexpr[int] = 1,
76
+ dQ_single_wg: bool = False,
77
  ):
78
  self.dtype = dtype
79
+ # Swapped dK/dV accumulators put head_dim on WGMMA's 64-row M axis.
80
+ hdim_multiple_of = 64 if dKV_swapAB else 16
81
  self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
82
  head_dim_v = head_dim_v if head_dim_v is not None else head_dim
83
  self.same_hdim_kv = head_dim == head_dim_v
 
87
  self.check_hdim_v_oob = head_dim_v != self.tile_hdimv
88
  self.qhead_per_kvhead = qhead_per_kvhead
89
  self.is_causal = is_causal
90
+ self.is_local = is_local
91
+ self.deterministic = deterministic
92
  self.tile_m = tile_m
93
  self.tile_n = tile_n
94
  self.num_threads = num_threads
 
103
  self.AtomLayoutMSdP = AtomLayoutMSdP
104
  self.AtomLayoutNdKV = AtomLayoutNdKV
105
  self.AtomLayoutMdQ = AtomLayoutMdQ
106
+ self.num_wg_mma = (self.num_threads // 128) - 1
107
  self.mma_dkv_is_rs = (
108
  AtomLayoutMSdP == 1
109
+ and AtomLayoutNdKV == self.num_wg_mma
110
  and SdP_swapAB
111
  and not dKV_swapAB
112
  )
113
  self.V_in_regs = V_in_regs
114
+ # May be overridden in __call__ for varlen inputs.
115
  if qhead_per_kvhead > 1:
116
  assert self.same_hdim_kv, "GQA backward requires head_dim == head_dim_v"
117
+ assert self.num_wg_mma == 2, "GQA backward assumes 2 warp groups"
118
  # These are tuned for speed
119
  # Do we keep the LSE and dPsum in each thread, or split them across 8 threads that share
120
  # them and then shuffle to get the value whenever we need? This can reduce register
121
  # pressure when SdP_swapAB, where each thread needs to keep statistics for (kBlockM / 4)
122
  # rows. If !SdP_swapAB, each thread only needs to keep statistics for 2 rows.
 
123
  self.shuffle_LSE = self.SdP_swapAB and self.tile_hdim <= 64
124
  self.shuffle_dPsum = self.SdP_swapAB and self.tile_hdim <= 64
125
 
 
129
  self.score_mod_bwd = score_mod_bwd
130
  self.mask_mod = mask_mod
131
  self.has_aux_tensors = has_aux_tensors
132
+ self.q_subtile_factor = q_subtile_factor
133
  if cutlass.const_expr(has_aux_tensors):
134
  self.vec_size: cutlass.Constexpr = 1
135
  else:
136
  self.vec_size: cutlass.Constexpr = 4
137
  self.qk_acc_dtype = Float32
138
+ # dQ_single_wg: WG0 computes the full dQ GEMM, WG1 skips it.
139
+ # Only valid for 2 MMA warp groups.
140
+ # Credit: Ben Spector
141
+ if dQ_single_wg:
142
+ assert self.num_wg_mma == 2, "dQ_single_wg only supports 2 warp groups"
143
+ self.num_wg_dQ = 1 if dQ_single_wg else self.num_wg_mma
144
 
145
  @staticmethod
146
  def can_implement(
 
199
  assert mQ_type == self.dtype
200
 
201
  def _setup_attributes(self):
202
+ # We need to accommodate both Q and Q^T (and dO and dO^T) in shared memory.
203
+ # Q & dO are used in the SdP Mma and Q^T and dO^T are used in the dKV Mma.
204
+ # The M dimension (tile_m) doesn't matter for the layout, only the K dimension
205
+ wg_d_dKV = self.num_wg_mma // self.AtomLayoutNdKV
206
+ self.sQ_layout, self.sdO_layout = [
207
+ # Need to set major_mode_size (mms) to accommodate Q and Q.T
208
+ sm90_utils.make_smem_layout(
209
+ self.dtype,
210
+ LayoutEnum.ROW_MAJOR,
211
+ shape,
212
+ stage,
213
+ major_mode_size=mms,
214
+ )
215
+ for shape, stage, mms in [
216
+ ((self.tile_m, self.tile_hdim), self.Q_stage, self.tile_hdim // wg_d_dKV),
217
+ ((self.tile_m, self.tile_hdimv), self.dO_stage, self.tile_hdim // wg_d_dKV),
218
  ]
219
  ]
220
+ wg_d_dQ = self.num_wg_dQ // self.AtomLayoutMdQ
221
+ # Accomodate both K and K.T
222
+ self.sK_layout = sm90_utils.make_smem_layout(
223
+ self.dtype,
224
+ LayoutEnum.ROW_MAJOR,
225
+ (self.tile_n, self.tile_hdim),
226
+ stage=None,
227
+ major_mode_size=self.tile_hdim // wg_d_dQ,
228
+ )
229
+ # There's only V, no V.T, so layout is normal
230
+ self.sV_layout = sm90_utils.make_smem_layout(
231
+ self.dtype, LayoutEnum.ROW_MAJOR, (self.tile_n, self.tile_hdimv), None
232
+ )
233
+ # Accomodate both S and S.T
234
+ wg_n_SdP = self.num_wg_mma // self.AtomLayoutMSdP
235
+ wg_n_dKV = self.AtomLayoutNdKV
236
+ self.sPdS_layout = sm90_utils.make_smem_layout(
237
+ self.dtype,
238
+ LayoutEnum.ROW_MAJOR,
239
+ (self.tile_m, self.tile_n),
240
+ stage=self.PdS_stage,
241
+ major_mode_size=math.gcd(self.tile_n // wg_n_SdP, self.tile_n // wg_n_dKV),
242
+ )
243
  self.sdQaccum_layout = cute.make_layout(
244
+ (self.tile_m * self.tile_hdim // self.num_wg_dQ, self.num_wg_dQ)
245
  )
246
  # dQaccum R->S
247
  self.r2s_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
248
  cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
249
  # thr_layout
250
+ cute.make_layout((self.num_threads_per_warp_group, self.num_wg_dQ)),
251
  cute.make_layout(128 // Float32.width), # val_layout
252
  )
253
  # dKVaccum for GQA epilogue - reuses sV+sK memory recast as f32
254
  # TODO: assert that sVaccum and sKaccum don't overflow smem
255
 
256
  def _get_tiled_mma(self):
257
+ maybe_swap_mn = lambda shape, swap: (shape[1], shape[0], *shape[2:]) if swap else shape
258
  # S = Q @ K.T, dP = dO @ V.T
259
+ atom_layout_SdP = (self.AtomLayoutMSdP, self.num_wg_mma // self.AtomLayoutMSdP, 1)
260
  tiler_mn_SdP = (self.tile_m // atom_layout_SdP[0], self.tile_n // atom_layout_SdP[1])
261
  tiled_mma_SdP = sm90_utils_basic.make_trivial_tiled_mma(
262
  self.dtype,
 
264
  warpgroup.OperandMajorMode.K,
265
  warpgroup.OperandMajorMode.K,
266
  Float32,
267
+ atom_layout_mnk=maybe_swap_mn(atom_layout_SdP, self.SdP_swapAB),
268
+ tiler_mn=(64, tiler_mn_SdP[1] if not self.SdP_swapAB else tiler_mn_SdP[0]),
 
269
  )
270
  # dV = P.T @ dO, dK = dS.T @ Q
271
+ atom_layout_dKV = (self.AtomLayoutNdKV, self.num_wg_mma // self.AtomLayoutNdKV, 1)
272
  tiler_mn_dK = (self.tile_n // atom_layout_dKV[0], self.tile_hdim // atom_layout_dKV[1])
273
  tiler_mn_dV = (self.tile_n // atom_layout_dKV[0], self.tile_hdimv // atom_layout_dKV[1])
274
  tiled_mma_dK, tiled_mma_dV = [
 
280
  else warpgroup.OperandMajorMode.K,
281
  warpgroup.OperandMajorMode.MN,
282
  Float32,
283
+ atom_layout_mnk=maybe_swap_mn(atom_layout_dKV, self.dKV_swapAB),
284
+ tiler_mn=(64, tiler_mn_d[1] if not self.dKV_swapAB else tiler_mn_d[0]),
 
285
  a_source=warpgroup.OperandSource.RMEM
286
  if self.mma_dkv_is_rs
287
  else warpgroup.OperandSource.SMEM,
 
289
  for tiler_mn_d in (tiler_mn_dK, tiler_mn_dV)
290
  ]
291
  # dQ = dS @ K
292
+ assert self.num_wg_dQ % self.AtomLayoutMdQ == 0
293
+ atom_layout_dQ = (self.AtomLayoutMdQ, self.num_wg_dQ // self.AtomLayoutMdQ, 1)
294
  tiler_mn_dQ = (self.tile_m // atom_layout_dQ[0], self.tile_hdim // atom_layout_dQ[1])
295
  tiled_mma_dQ = sm90_utils_basic.make_trivial_tiled_mma(
296
  self.dtype,
 
298
  warpgroup.OperandMajorMode.K if not self.dQ_swapAB else warpgroup.OperandMajorMode.MN,
299
  warpgroup.OperandMajorMode.MN if not self.dQ_swapAB else warpgroup.OperandMajorMode.K,
300
  Float32,
301
+ atom_layout_mnk=maybe_swap_mn(atom_layout_dQ, self.dQ_swapAB),
302
+ tiler_mn=(64, tiler_mn_dQ[1] if not self.dQ_swapAB else tiler_mn_dQ[0]),
303
  )
304
  return tiled_mma_SdP, tiled_mma_dK, tiled_mma_dV, tiled_mma_dQ
305
 
 
353
  mdK: cute.Tensor,
354
  mdV: cute.Tensor,
355
  softmax_scale: Float32,
 
356
  mCuSeqlensQ: Optional[cute.Tensor] = None,
357
  mCuSeqlensK: Optional[cute.Tensor] = None,
358
  mSeqUsedQ: Optional[cute.Tensor] = None,
359
  mSeqUsedK: Optional[cute.Tensor] = None,
 
360
  window_size_left: Int32 | int | None = None,
361
  window_size_right: Int32 | int | None = None,
362
  mdQ_semaphore: Optional[cute.Tensor] = None,
363
  mdK_semaphore: Optional[cute.Tensor] = None,
364
  mdV_semaphore: Optional[cute.Tensor] = None,
365
+ aux_data: AuxData = AuxData(),
366
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
367
+ mCuTotalMBlocks: Optional[cute.Tensor] = None,
368
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
369
+ stream: cuda.CUstream = None,
370
  ):
371
+ # For GQA (qhead_per_kvhead > 1), multiple Q heads accumulate into the same dK/dV,
372
+ # so we need the float32 accum path + postprocess.
373
+ # For varlen_k with qhead_per_kvhead == 1, we use ragged TMA tensors.
374
+ self.varlen_k = mCuSeqlensK is not None or mSeqUsedK is not None
375
 
376
  self._check_type(
377
  *(
 
380
  )
381
  )
382
 
383
+ self.is_varlen_q = mCuSeqlensQ is not None or mSeqUsedQ is not None
384
+
385
  mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV = [
386
  assume_tensor_aligned(t) for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV)
387
  ]
388
 
389
+ # Non-varlen inputs are (b, s, n, h), varlen inputs are (s, n, h).
390
+ # We convert both to a seqlen-major view with head-dim second.
391
+ # Each tensor may have different rank when Q is padded (seqused_q) but K/V are unpadded (cu_seqlens_k).
392
+ def _qkv_transpose(t):
393
+ return layout_utils.select(t, [1, 3, 2, 0] if cute.rank(t.shape) == 4 else [0, 2, 1])
394
+
395
+ mQ, mK, mV, mdO = [_qkv_transpose(t) for t in (mQ, mK, mV, mdO)]
396
  if const_expr(self.qhead_per_kvhead == 1):
397
+ mdK, mdV = [_qkv_transpose(t) for t in (mdK, mdV)]
398
  else:
399
+ # Accum tensors are (b, n, s*h) for non-varlen and (n, s*h) for varlen.
400
+ accum_transpose = [2, 1, 0] if cute.rank(mdK.shape) == 3 else [1, 0]
401
  mdK, mdV = [layout_utils.select(t, accum_transpose) for t in (mdK, mdV)]
402
+ # Non-varlen stats are (b, n, s), varlen stats are (n, s).
403
+ LSE_dPsum_dQaccum_transpose = [2, 1, 0] if cute.rank(mLSE.shape) == 3 else [1, 0]
404
  mLSE, mdPsum, mdQaccum = [
405
  layout_utils.select(t, LSE_dPsum_dQaccum_transpose) for t in (mLSE, mdPsum, mdQaccum)
406
  ]
407
 
408
  tiled_mma_SdP, tiled_mma_dK, tiled_mma_dV, tiled_mma_dQ = self._get_tiled_mma()
409
+ # (batch, num_head, num_m_blocks, cluster_size) -> (num_m_blocks, cluster_size, num_head, batch)
410
+ if const_expr(self.deterministic):
411
+ assert mdQ_semaphore is not None
412
+ mdQ_semaphore = layout_utils.select(mdQ_semaphore, mode=[2, 3, 1, 0])
413
+ if const_expr(self.deterministic and self.qhead_per_kvhead > 1):
414
+ assert mdK_semaphore is not None
415
+ assert mdV_semaphore is not None
416
+ mdK_semaphore, mdV_semaphore = [
417
+ layout_utils.select(t, mode=[2, 3, 1, 0]) for t in (mdK_semaphore, mdV_semaphore)
418
+ ]
419
+ else:
420
+ mdK_semaphore = None
421
+ mdV_semaphore = None
422
 
423
  self.num_mma_threads = tiled_mma_SdP.size
424
  assert self.num_mma_threads + 128 == self.num_threads
 
426
  self.num_threads_per_warp_group = 128
427
  self.num_producer_threads = 32
428
 
429
+ REG_LIMIT = 504 if self.num_wg_mma == 2 else 512
430
+ if const_expr(self.num_wg_mma == 2):
431
+ if const_expr(self.num_wg_dQ == 1):
432
+ self.num_mma_regs_wg0 = 256
433
+ self.num_mma_regs_wg1 = 224
434
+ else:
435
+ self.num_mma_regs_wg0 = 240
436
+ self.num_mma_regs_wg1 = 240
437
+ self.num_mma_regs = self.num_mma_regs_wg0 # for backward compat
438
+ self.num_producer_regs = 24
439
+ assert (
440
+ self.num_mma_regs_wg0 + self.num_mma_regs_wg1 + self.num_producer_regs <= REG_LIMIT
441
+ )
442
+ else: # 3 warp groups
443
+ self.num_mma_regs_wg0 = 160
444
+ self.num_mma_regs_wg1 = 160
445
+ self.num_mma_regs = 160
446
+ self.num_producer_regs = 32
447
+ assert self.num_mma_regs_wg0 * self.num_wg_mma + self.num_producer_regs <= REG_LIMIT
448
 
449
  self._setup_attributes()
450
  SharedStorage = self._get_shared_storage_cls()
 
461
  self.tma_copy_bytes["LSE"] = self.tile_m * Float32.width // 8
462
  self.tma_copy_bytes["dPsum"] = self.tile_m * Float32.width // 8
463
  self.tma_copy_bytes["dQ"] = (
464
+ self.tile_m * self.tile_hdim * Float32.width // 8 // self.num_wg_dQ
465
  )
466
  self.tma_copy_bytes["dKacc"] = self.tile_n * self.tile_hdim * Float32.width // 8
467
  self.tma_copy_bytes["dVacc"] = self.tile_n * self.tile_hdimv * Float32.width // 8
 
491
  (self.tile_m, self.tile_hdimv),
492
  )
493
  if const_expr(self.qhead_per_kvhead == 1):
494
+ mdK_tma = (
495
+ copy_utils.create_ragged_tensor_for_tma(mdK, ragged_dim=0, ptr_shift=True)
496
+ if self.varlen_k
497
+ else mdK
498
+ )
499
+ mdV_tma = (
500
+ copy_utils.create_ragged_tensor_for_tma(mdV, ragged_dim=0, ptr_shift=True)
501
+ if self.varlen_k
502
+ else mdV
503
+ )
504
  tma_atom_dK, tma_tensor_dK = cpasync.make_tiled_tma_atom(
505
  cpasync.CopyBulkTensorTileS2GOp(),
506
+ mdK_tma,
507
  cute.select(self.sK_layout, mode=[0, 1]),
508
  (self.tile_n, self.tile_hdim),
509
  )
510
  tma_atom_dV, tma_tensor_dV = cpasync.make_tiled_tma_atom(
511
  cpasync.CopyBulkTensorTileS2GOp(),
512
+ mdV_tma,
513
  cute.select(self.sV_layout, mode=[0, 1]),
514
  (self.tile_n, self.tile_hdimv),
515
  )
516
  else:
517
  tma_atom_dK = tma_atom_dV = tma_tensor_dK = tma_tensor_dV = None
518
 
519
+ if const_expr(mCuSeqlensK is not None or mSeqUsedK is not None):
520
+ TileScheduler = SingleTileVarlenScheduler
521
+ elif const_expr(self.deterministic):
522
+ TileScheduler = SingleTileLPTBwdScheduler
523
+ else:
524
+ TileScheduler = SingleTileScheduler
525
+ self.spt = (self.is_causal or self.is_local) and self.deterministic
526
  tile_sched_args = TileSchedulerArguments(
527
  cute.ceil_div(cute.size(mK.shape[0]), self.tile_n),
528
  cute.size(mQ.shape[2]),
529
+ cute.size(mK.shape[3])
530
+ if const_expr(mCuSeqlensK is None)
531
+ else cute.size(mCuSeqlensK.shape[0] - 1), # num_batch
532
  1, # num_splits
533
+ cute.size(mQ.shape[0]), # pass seqlen_q or total_q for seqlen_k
534
+ mQ.shape[1], # headdim
535
+ mV.shape[1], # headdim_v
536
+ total_q=cute.size(mK.shape[0])
537
+ if const_expr(mCuSeqlensK is not None)
538
+ else cute.size(mK.shape[0]) * cute.size(mK.shape[3]),
539
+ tile_shape_mn=(self.tile_n, self.tile_m), # Swapping the role of Q & K
540
+ mCuSeqlensQ=mCuSeqlensK,
541
+ mSeqUsedQ=mSeqUsedK,
542
  qhead_per_kvhead_packgqa=1,
543
  element_size=self.dtype.width // 8,
544
  is_persistent=False,
545
+ lpt=self.spt,
546
+ head_swizzle=self.deterministic,
547
+ cu_total_m_blocks_ptr=mCuTotalMBlocks,
548
  )
549
 
550
  tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
 
557
  softmax_scale_log2 = LOG2_E
558
 
559
  fastdiv_mods = None
560
+ if const_expr(aux_data.tensors is not None):
561
  seqlen_q = cute.size(mQ.shape[0])
562
  seqlen_k = cute.size(mK.shape[0])
563
  seqlen_q_divmod = FastDivmodDivisor(seqlen_q)
 
570
 
571
  self.use_block_sparsity = cutlass.const_expr(blocksparse_tensors is not None)
572
 
573
+ if const_expr(window_size_left is not None):
574
+ window_size_left = Int32(window_size_left)
575
+ if const_expr(window_size_right is not None):
576
+ window_size_right = Int32(window_size_right)
577
+
578
  self.kernel(
579
  tma_tensor_Q,
580
  tma_tensor_K,
 
591
  mLSE,
592
  mdPsum,
593
  mdQaccum,
594
+ mCuSeqlensQ,
595
+ mCuSeqlensK,
596
+ mSeqUsedQ,
597
+ mSeqUsedK,
598
  self.sQ_layout,
599
  self.sK_layout,
600
  self.sV_layout,
 
611
  tile_sched_params,
612
  TileScheduler,
613
  SharedStorage,
614
+ aux_data,
615
  fastdiv_mods,
616
  blocksparse_tensors,
617
  qhead_per_kvhead_divmod,
618
+ mdQ_semaphore,
619
+ mdK_semaphore,
620
+ mdV_semaphore,
621
+ window_size_left,
622
+ window_size_right,
623
  ).launch(
624
  grid=grid_dim,
625
  block=[self.num_threads, 1, 1],
626
  stream=stream,
627
  min_blocks_per_mp=1,
628
+ use_pdl=True,
629
  )
630
 
631
  @cute.kernel
 
646
  mLSE: cute.Tensor,
647
  mdPsum: cute.Tensor,
648
  mdQaccum: cute.Tensor,
649
+ mCuSeqlensQ: Optional[cute.Tensor],
650
+ mCuSeqlensK: Optional[cute.Tensor],
651
+ mSeqUsedQ: Optional[cute.Tensor],
652
+ mSeqUsedK: Optional[cute.Tensor],
653
  sQ_layout: cute.ComposedLayout,
654
  sK_layout: cute.ComposedLayout,
655
  sV_layout: cute.ComposedLayout,
 
666
  tile_sched_params: ParamsBase,
667
  TileScheduler: cutlass.Constexpr[Callable],
668
  SharedStorage: cutlass.Constexpr[Callable],
669
+ aux_data: AuxData = AuxData(),
670
  fastdiv_mods=(None, None),
671
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
672
  qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
673
+ mdQ_semaphore: Optional[cute.Tensor] = None,
674
+ mdK_semaphore: Optional[cute.Tensor] = None,
675
+ mdV_semaphore: Optional[cute.Tensor] = None,
676
+ window_size_left: Optional[Int32] = None,
677
+ window_size_right: Optional[Int32] = None,
678
  ):
679
  warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
680
 
681
  # prefetch TMA descriptors
682
  if warp_idx == 0:
683
+ for atom in [tma_atom_Q, tma_atom_K, tma_atom_V, tma_atom_dO, tma_atom_dK, tma_atom_dV]:
684
+ if const_expr(atom is not None):
685
+ cpasync.prefetch_descriptor(atom)
 
686
 
687
  smem = cutlass.utils.SmemAllocator()
688
  storage = smem.allocate(SharedStorage)
 
736
  self.is_causal,
737
  self.is_local,
738
  False, # is_split_kv
739
+ window_size_left,
740
+ window_size_right,
741
  qhead_per_kvhead_packgqa=1,
742
  )
743
  SeqlenInfoCls = partial(
744
  SeqlenInfoQK.create,
745
  seqlen_q_static=mQ.shape[0],
746
  seqlen_k_static=mK.shape[0],
747
+ mCuSeqlensQ=mCuSeqlensQ,
748
+ mCuSeqlensK=mCuSeqlensK,
749
+ mSeqUsedQ=mSeqUsedQ,
750
+ mSeqUsedK=mSeqUsedK,
751
+ tile_m=self.tile_m,
752
+ tile_n=self.tile_n,
753
  )
754
  AttentionMaskCls = partial(
755
  AttentionMask,
756
  self.tile_m,
757
  self.tile_n,
758
+ window_size_left=window_size_left,
759
+ window_size_right=window_size_right,
760
  swap_AB=self.SdP_swapAB,
761
  )
762
  TileSchedulerCls = partial(TileScheduler.create, tile_sched_params)
 
797
  TileSchedulerCls,
798
  SeqlenInfoCls,
799
  blocksparse_tensors,
800
+ mdQ_semaphore,
801
  )
802
  else:
 
803
  tidx, _, _ = cute.arch.thread_idx()
804
  tidx = tidx - 128
805
+ mma_args = (
806
  tiled_mma_SdP,
807
  tiled_mma_dK,
808
  tiled_mma_dV,
809
  tiled_mma_dQ,
810
  mdK,
811
  mdV,
812
+ mdK_semaphore,
813
+ mdV_semaphore,
814
  mdQaccum,
815
  sQ,
816
  sK,
 
833
  SeqlenInfoCls,
834
  AttentionMaskCls,
835
  TileSchedulerCls,
836
+ aux_data,
837
  fastdiv_mods,
838
  blocksparse_tensors,
839
  qhead_per_kvhead_divmod,
840
  )
841
+ if const_expr(self.num_wg_dQ == self.num_wg_mma):
842
+ # Both WGs compute dQ
843
+ cute.arch.setmaxregister_increase(self.num_mma_regs_wg0)
844
+ self.mma(*mma_args, is_dQ_wg=True)
845
+ else:
846
+ # WG0 computes dQ, WG1 skips it
847
+ warp_idx_in_mma = cute.arch.make_warp_uniform(cute.arch.warp_idx()) - 4
848
+ if warp_idx_in_mma < 4:
849
+ cute.arch.setmaxregister_increase(self.num_mma_regs_wg0)
850
+ self.mma(*mma_args, is_dQ_wg=True)
851
+ else:
852
+ cute.arch.setmaxregister_increase(self.num_mma_regs_wg1)
853
+ self.mma(*mma_args, is_dQ_wg=False)
854
 
855
  @cute.jit
856
  def load(
 
898
  if const_expr(self.qhead_per_kvhead == 1)
899
  else head_idx // qhead_per_kvhead_divmod
900
  )
901
+ mK_cur = seqlen.offset_batch_K(mK, batch_idx, dim=3)[None, None, head_idx_kv]
902
+ mV_cur = seqlen.offset_batch_K(mV, batch_idx, dim=3)[None, None, head_idx_kv]
903
  gK = cute.local_tile(mK_cur, (self.tile_n, self.tile_hdim), (n_block, 0))
 
904
  gV = cute.local_tile(mV_cur, (self.tile_n, self.tile_hdimv), (n_block, 0))
905
 
906
+ mQ_cur = seqlen.offset_batch_Q(mQ, batch_idx, dim=3)[None, None, head_idx]
907
+ mLSE_cur = seqlen.offset_batch_Q(mLSE, batch_idx, dim=2, padded=True)[
908
+ None, head_idx
909
+ ]
910
+ mdO_cur = seqlen.offset_batch_Q(mdO, batch_idx, dim=3)[None, None, head_idx]
911
+ mdPsum_cur = seqlen.offset_batch_Q(mdPsum, batch_idx, dim=2, padded=True)[
912
+ None, head_idx
913
+ ]
914
  gQ = cute.local_tile(mQ_cur, (self.tile_m, self.tile_hdim), (None, 0))
 
915
  gdO = cute.local_tile(mdO_cur, (self.tile_m, self.tile_hdimv), (None, 0))
 
916
  gLSE = cute.local_tile(mLSE_cur, (self.tile_m,), (None,))
 
917
  gdPsum = cute.local_tile(mdPsum_cur, (self.tile_m,), (None,))
918
 
919
  load_K, _, _ = copy_utils.tma_get_copy_fn(
 
939
 
940
  if const_expr(not self.use_block_sparsity):
941
  total_m_block_cnt = m_block_max - m_block_min
942
+ process_tile = (
943
+ const_expr(not self.is_local and not self.is_varlen_q)
944
+ or m_block_min < m_block_max
945
+ )
946
  else:
947
  total_m_block_cnt = get_total_q_block_count_bwd(
948
  blocksparse_tensors,
949
  batch_idx,
950
  head_idx,
951
  n_block,
952
+ q_subtile_factor=self.q_subtile_factor,
953
  m_block_max=m_block_max,
954
  )
955
  process_tile = total_m_block_cnt > Int32(0)
 
962
  )
963
  load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q))
964
  load_Q(first_m_block, producer_state=producer_state_Q)
965
+ # Wait for bwd preprocess to finish writing LSE and dPsum
966
+ cute.arch.griddepcontrol_wait()
967
  load_LSE(first_m_block, producer_state=producer_state_Q)
968
  producer_state_dO_cur = (
969
  producer_state_dO
 
994
  producer_state_Q.advance()
995
  producer_state_dO.advance()
996
  else:
997
+ # The wait for bwd preprocess that the dense arm above does inline sits
998
+ # in _load_q_do_block_sm90, at the same point in the load order.
999
  producer_state_Q, producer_state_dO = produce_block_sparse_q_loads_bwd_sm90(
1000
  blocksparse_tensors,
1001
  batch_idx,
 
1014
  self.tma_copy_bytes["K"],
1015
  self.tma_copy_bytes["V"],
1016
  Q_stage_eq_dO_stage=(self.Q_stage == self.dO_stage),
1017
+ q_subtile_factor=self.q_subtile_factor,
1018
  m_block_max=m_block_max,
1019
  )
1020
 
 
1026
  def apply_score_mod(
1027
  self,
1028
  acc_S: cute.Tensor,
1029
+ thr_mma_SdP: cute.ThrMma,
1030
  batch_idx,
1031
  head_idx,
1032
  m_block,
1033
  n_block,
1034
  softmax_scale,
1035
  seqlen_info: SeqlenInfoQK,
1036
+ aux_data: AuxData = AuxData(),
1037
  fastdiv_mods=(None, None),
1038
  ):
1039
  # [NOTE] SdP_swapAB: swapAB transposes the tile, so use (n, m) indexing
 
1057
  softmax_scale,
1058
  self.vec_size,
1059
  self.qk_acc_dtype,
1060
+ aux_data,
1061
  fastdiv_mods,
1062
  seqlen_info,
1063
  constant_q_idx=None,
 
1070
  self,
1071
  grad_tensor: cute.Tensor,
1072
  score_tensor: cute.Tensor,
1073
+ thr_mma_SdP: cute.ThrMma,
1074
  batch_idx,
1075
  head_idx,
1076
  m_block,
1077
  n_block,
1078
  softmax_scale,
1079
  seqlen_info: SeqlenInfoQK,
1080
+ aux_data: AuxData = AuxData(),
1081
  fastdiv_mods=(None, None),
1082
  ):
1083
  cS = cute.make_identity_tensor(
 
1101
  softmax_scale,
1102
  self.vec_size,
1103
  self.qk_acc_dtype,
1104
+ aux_data,
1105
  fastdiv_mods,
1106
  seqlen_info,
1107
  constant_q_idx=None,
 
1118
  tiled_mma_dQ: cute.TiledMma,
1119
  mdK: cute.Tensor,
1120
  mdV: cute.Tensor,
1121
+ mdK_semaphore: Optional[cute.Tensor],
1122
+ mdV_semaphore: Optional[cute.Tensor],
1123
  mdQaccum: cute.Tensor,
1124
  sQ: cute.Tensor,
1125
  sK: cute.Tensor,
 
1142
  SeqlenInfoCls: Callable,
1143
  AttentionMaskCls: Callable,
1144
  TileSchedulerCls: Callable,
1145
+ aux_data: AuxData = AuxData(),
1146
  fastdiv_mods=(None, None),
1147
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
1148
  qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
1149
+ is_dQ_wg: cutlass.Constexpr[bool] = True,
1150
  ):
1151
  warp_group_idx = cute.arch.make_warp_uniform(tidx // self.num_threads_per_warp_group)
1152
  warp_group_thread_layout = cute.make_layout(
1153
+ self.num_wg_mma, stride=self.num_threads_per_warp_group
1154
  )
1155
  thr_mma_SdP = tiled_mma_SdP.get_slice(tidx)
1156
  wg_mma_SdP = tiled_mma_SdP.get_slice(warp_group_thread_layout(warp_group_idx))
1157
  wg_mma_dK = tiled_mma_dK.get_slice(warp_group_thread_layout(warp_group_idx))
1158
  wg_mma_dV = tiled_mma_dV.get_slice(warp_group_thread_layout(warp_group_idx))
1159
+ wg_mma_dQ = None
1160
+ if const_expr(is_dQ_wg):
1161
+ wg_idx_dQ = warp_group_idx if const_expr(self.num_wg_dQ > 1) else 0
1162
+ wg_mma_dQ = tiled_mma_dQ.get_slice(warp_group_thread_layout(wg_idx_dQ))
1163
  # S = Q @ K.T
1164
  shape_mnk_S = (self.tile_m, self.tile_n, self.tile_hdim)
1165
  _, tSrQ, tSrK = sm90_utils.partition_fragment_ABC(
 
1205
  # dQ = dS @ K
1206
  sKt = layout_utils.transpose_view(sK)
1207
  shape_mnk_dQ = (self.tile_m, self.tile_hdim, self.tile_n)
1208
+ mma_dsk_fn = None
1209
+ if const_expr(is_dQ_wg):
1210
+ _, tdQrdS, tdQrKt = sm90_utils.partition_fragment_ABC(
1211
+ wg_mma_dQ, shape_mnk_dQ, sdS, sKt, swap_AB=self.dQ_swapAB
1212
+ )
1213
+ mma_dsk_fn = partial(
1214
+ gemm_zero_init,
1215
+ tiled_mma_dQ,
1216
+ shape_mnk_dQ[:2],
1217
+ tdQrdS,
1218
+ tdQrKt,
1219
+ swap_AB=self.dQ_swapAB,
1220
+ )
1221
 
1222
+ # Smem copy atom tiling for P/dS R2S
1223
  copy_P_r2s = None
1224
+ mms_PdS = self.tile_n // (self.num_wg_mma // self.AtomLayoutMSdP)
1225
  if const_expr(sP is not None):
1226
  sP_cpy = sP if const_expr(not self.SdP_swapAB) else sPt
1227
  copy_P_r2s, _, _ = copy_utils.get_smem_store_C(
1228
+ tiled_mma_SdP,
1229
+ sP_cpy,
1230
+ tidx,
1231
+ transpose=self.SdP_swapAB,
1232
+ position_independent=True,
1233
+ major_mode_size=mms_PdS,
1234
  )
1235
  sdS_cpy = sdS if const_expr(not self.SdP_swapAB) else sdSt
1236
  copy_dS_r2s, _, _ = copy_utils.get_smem_store_C(
1237
+ tiled_mma_SdP,
1238
+ sdS_cpy,
1239
+ tidx,
1240
+ transpose=self.SdP_swapAB,
1241
+ position_independent=True,
1242
+ major_mode_size=mms_PdS,
1243
  )
1244
 
1245
  tLSEsLSE = layout_utils.mma_partition_C_vec(
 
1248
  tLSEsdPsum = layout_utils.mma_partition_C_vec(
1249
  sdPsum, thr_mma_SdP, expand_shape=self.tile_n, is_colvec=not self.SdP_swapAB
1250
  )
1251
+ # When shuffle=True, rows are distributed across 8 quads (4 threads each) within a warp.
1252
+ # Each thread loads only ceil(num_rows/8) values;
1253
+ shfl_copy = copy_utils.tiled_copy_1d(sLSE.element_type, num_threads=8, num_copy_elems=2)
1254
+ if const_expr(self.shuffle_LSE):
1255
+ tLSEsLSE = shfl_copy.get_slice(cute.arch.lane_idx() // 4).partition_S(tLSEsLSE)
1256
+ # ((2, 1), 1, 2) -> (((2, 1), 1), 2)
1257
+ tLSEsLSE = cute.group_modes(tLSEsLSE, 0, 2)
1258
+ if const_expr(self.shuffle_dPsum):
1259
+ tLSEsdPsum = shfl_copy.get_slice(cute.arch.lane_idx() // 4).partition_S(tLSEsdPsum)
1260
+ tLSEsdPsum = cute.group_modes(tLSEsdPsum, 0, 2)
1261
+
1262
+ tdQsdQaccum = None
1263
+ if const_expr(is_dQ_wg):
1264
+ smem_thr_copy_dQaccum = r2s_tiled_copy_dQaccum.get_slice(tidx)
1265
+ tdQsdQaccum = smem_thr_copy_dQaccum.partition_D(sdQaccum)
1266
 
1267
  PdS_barrier = cutlass.pipeline.NamedBarrier(
1268
  barrier_id=int(NamedBarrierBwd.PdS), num_threads=self.num_mma_threads
1269
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1270
 
1271
  mma_one_m_block_all = partial(
1272
  self.mma_one_m_block,
 
1287
  PdS_barrier=PdS_barrier,
1288
  # acc_dV=acc_dV,
1289
  # acc_dK=acc_dK,
1290
+ is_dQ_wg=is_dQ_wg,
1291
  )
1292
 
1293
  consumer_state_Q = cutlass.pipeline.make_pipeline_state(
 
1301
  while work_tile.is_valid_tile:
1302
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
1303
  seqlen = SeqlenInfoCls(batch_idx)
1304
+
1305
+ recompute_fastdiv_mods_q = const_expr(
1306
+ aux_data.tensors is not None and (seqlen.has_cu_seqlens_q or seqlen.has_seqused_q)
1307
+ )
1308
+ recompute_fastdiv_mods_k = const_expr(
1309
+ aux_data.tensors is not None and (seqlen.has_cu_seqlens_k or seqlen.has_seqused_k)
1310
+ )
1311
+
1312
+ if const_expr(fastdiv_mods is not None and fastdiv_mods[0] is not None):
1313
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
1314
+ fastdiv_mods = (
1315
+ seqlen_q_divmod
1316
+ if not recompute_fastdiv_mods_q
1317
+ else FastDivmodDivisor(seqlen.seqlen_q),
1318
+ seqlen_k_divmod
1319
+ if not recompute_fastdiv_mods_k
1320
+ else FastDivmodDivisor(seqlen.seqlen_k),
1321
+ )
1322
+
1323
  mask = AttentionMaskCls(seqlen)
1324
+ score_mod_fn = partial(
1325
+ self.apply_score_mod,
1326
+ thr_mma_SdP=thr_mma_SdP,
1327
+ softmax_scale=softmax_scale,
1328
+ aux_data=aux_data,
1329
+ fastdiv_mods=fastdiv_mods,
1330
+ )
1331
+ score_mod_bwd_fn = partial(
1332
+ self.apply_score_mod_bwd,
1333
+ thr_mma_SdP=thr_mma_SdP,
1334
+ softmax_scale=softmax_scale,
1335
+ aux_data=aux_data,
1336
+ fastdiv_mods=fastdiv_mods,
1337
+ )
1338
  score_mod_fn_cur = partial(
1339
  score_mod_fn,
1340
  batch_idx=batch_idx,
 
1352
  m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
1353
 
1354
  if const_expr(not self.use_block_sparsity):
1355
+ process_tile = (
1356
+ const_expr(not self.is_local and not self.is_varlen_q)
1357
+ or m_block_min < m_block_max
1358
+ )
1359
  else:
1360
  total_m_block_cnt = get_total_q_block_count_bwd(
1361
  blocksparse_tensors,
1362
  batch_idx,
1363
  head_idx,
1364
  n_block,
1365
+ q_subtile_factor=self.q_subtile_factor,
1366
  m_block_max=m_block_max,
1367
  )
1368
  process_tile = total_m_block_cnt > Int32(0)
 
1379
  mask_causal=self.is_causal,
1380
  mask_local=self.is_local,
1381
  mask_mod=self.mask_mod,
1382
+ aux_data=aux_data,
1383
  fastdiv_mods=fastdiv_mods,
1384
  )
1385
  dKV_accumulate = False
 
1410
  thr_mma_SdP=thr_mma_SdP,
1411
  score_mod_fn=score_mod_fn_cur,
1412
  score_mod_bwd_fn=score_mod_bwd_fn_cur,
1413
+ q_subtile_factor=self.q_subtile_factor,
1414
  m_block_max=m_block_max,
1415
+ aux_data=aux_data,
1416
  fastdiv_mods=fastdiv_mods,
1417
  )
1418
 
 
1435
  head_idx,
1436
  batch_idx,
1437
  qhead_per_kvhead_divmod,
1438
+ mdK_semaphore,
1439
+ mdV_semaphore,
1440
  )
1441
  else:
1442
+ # KV tile with zero Q blocks produces no dK/dV; write zeros.
1443
+ if const_expr(self.use_block_sparsity or self.is_local or self.is_varlen_q):
1444
  acc_dK.fill(0.0)
1445
  acc_dV.fill(0.0)
1446
  self.epilogue_dKV(
 
1460
  head_idx,
1461
  batch_idx,
1462
  qhead_per_kvhead_divmod,
1463
+ mdK_semaphore,
1464
+ mdV_semaphore,
1465
  )
1466
 
1467
  tile_scheduler.advance_to_next_work()
 
1471
  if warp_idx == 4:
1472
  cute.arch.cp_async_bulk_wait_group(0, read=True)
1473
 
1474
+ @staticmethod
1475
+ @cute.jit
1476
+ def _get_stat(tSrS: cute.Tensor, row: Int32, lane: Int32, shuffle: bool) -> Float32:
1477
+ """Retrieve the statistic for a given accumulator row.
1478
+
1479
+ When shuffle=False, direct register indexing.
1480
+ When shuffle=True, warp shuffle from the thread group that holds the value.
1481
+ """
1482
+ if const_expr(not shuffle):
1483
+ return tSrS[row]
1484
+ # tSrS: (((2, 1), 1), 1)), distributed across 8 threads in the warp
1485
+ vecsize = cute.size(tSrS, mode=[0, 0]) # 2
1486
+ idx0, off, idx1 = cute.idx2crd(row, (vecsize, 8, cute.shape(tSrS, mode=[0, 1])))
1487
+ # register index: 0, 1, 0, 1, ..., 2, 3, 2, 3, ...
1488
+ return utils.shuffle_sync(tSrS[idx0 + idx1 * vecsize], offset=off * 4 + (lane % 4))
1489
+
1490
  @cute.jit
1491
  def mma_one_m_block(
1492
  self,
 
1505
  pipeline_dO: cutlass.pipeline.PipelineAsync,
1506
  tLSEsLSE: cute.Tensor,
1507
  tLSEsdPsum: cute.Tensor,
1508
+ tdQsdQaccum: Optional[cute.Tensor],
1509
  softmax_scale_log2: Float32,
1510
  PdS_barrier: cutlass.pipeline.NamedBarrier,
1511
+ is_dQ_wg: cutlass.Constexpr[bool] = True,
1512
  mask_fn: Optional[Callable] = None,
1513
  score_mod_fn: Optional[Callable] = None,
1514
  score_mod_bwd_fn: Optional[Callable] = None,
1515
  dKV_accumulate: Boolean = True,
1516
  ):
1517
  consumer_state_dO_cur = (
1518
+ consumer_state_Q if const_expr(self.Q_stage == self.dO_stage) else consumer_state_dO
1519
  )
1520
  smem_idx_Q = consumer_state_Q.index
1521
  smem_idx_dO = consumer_state_dO_cur.index if const_expr(self.dO_stage > 1) else 0
 
1523
  # (1) [GEMM 1] S = Q @ K^T
1524
  pipeline_Q.consumer_wait(consumer_state_Q, pipeline_Q.consumer_try_wait(consumer_state_Q))
1525
  acc_S = mma_qk_fn(A_idx=smem_idx_Q, wg_wait=-1)
1526
+ # If shuffle_LSE, OOB reads are OK since sLSE is already padded
1527
  tLSErLSE = copy_utils.load_s2r(tLSEsLSE[None, smem_idx_Q])
1528
  # (2) [GEMM 2] dP = dO @ V.T
1529
  pipeline_dO.consumer_wait(
 
1542
  if cutlass.const_expr(mask_fn is not None):
1543
  mask_fn(acc_S, m_block=m_block)
1544
  acc_S_mn = layout_utils.reshape_acc_to_mn(acc_S, transpose=self.SdP_swapAB)
1545
+ lane_idx = cute.arch.lane_idx()
1546
  for r in cutlass.range_constexpr(cute.size(acc_S_mn, mode=[0])):
1547
+ lse_val = self._get_stat(tLSErLSE, r, lane_idx, shuffle=self.shuffle_LSE)
1548
  for c in cutlass.range(cute.size(acc_S_mn, mode=[1]), unroll_full=True):
1549
  acc_S_mn[r, c] = cute.math.exp2(
1550
+ acc_S_mn[r, c] * softmax_scale_log2 - lse_val, fastmath=True
1551
  )
1552
  tLSErdPsum = copy_utils.load_s2r(tLSEsdPsum[None, smem_idx_dO])
1553
 
 
1564
  warpgroup.wait_group(0)
1565
  acc_dP_mn = layout_utils.reshape_acc_to_mn(acc_dP, transpose=self.SdP_swapAB)
1566
  for r in cutlass.range_constexpr(cute.size(acc_dP_mn, mode=[0])):
1567
+ dpsum_val = self._get_stat(tLSErdPsum, r, lane_idx, shuffle=self.shuffle_dPsum)
1568
  for c in cutlass.range(cute.size(acc_dP_mn, mode=[1]), unroll_full=True):
1569
+ acc_dP_mn[r, c] = acc_S_mn[r, c] * (acc_dP_mn[r, c] - dpsum_val)
1570
 
1571
  if const_expr(self.score_mod_bwd is not None):
1572
  score_mod_bwd_fn(acc_dP, acc_S_pre, m_block=m_block)
 
1598
  # smem fence to make sure sdS is written before it's read by WGMMA
1599
  cute.arch.fence_view_async_shared()
1600
  PdS_barrier.arrive_and_wait()
 
 
 
 
1601
 
1602
+ if const_expr(is_dQ_wg):
1603
+ # (6) [GEMM 4] dQ = dS @ K
1604
+ acc_dQ = mma_dsk_fn(A_idx=smem_idx_PdS, wg_wait=1)
1605
+ pipeline_dO.consumer_release(consumer_state_dO_cur) # release dO as dV mma is done
 
 
 
 
1606
 
1607
+ # (7) [GEMM 5] dK += dS.T @ Q
1608
+ if const_expr(not self.mma_dkv_is_rs):
1609
+ mma_dsq_fn(
1610
+ A_idx=smem_idx_PdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1
1611
+ )
1612
+ else:
1613
+ mma_dsq_fn(tCrA=tdKrdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1)
 
 
 
 
1614
 
1615
+ # dQ R2S: wait for dQaccum_store to free the smem buffer, then write dQ to smem
1616
+ # When dQ_single_wg, only WG0 enters here so warp_group_idx == 0
1617
+ cute.arch.barrier(
1618
+ barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1619
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1620
+ )
1621
+ tdQrdQaccum_flat = cute.make_tensor(
1622
+ acc_dQ.iterator, cute.make_layout(tdQsdQaccum.shape)
1623
+ )
1624
+ cute.autovec_copy(tdQrdQaccum_flat, tdQsdQaccum)
1625
+ cute.arch.fence_view_async_shared()
1626
+ cute.arch.barrier_arrive(
1627
+ barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1628
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1629
+ )
1630
+
1631
+ warpgroup.wait_group(0)
1632
+ pipeline_Q.consumer_release(consumer_state_Q)
1633
+ else:
1634
+ # dQ_single_wg: WG1 skips dQ, only does dV wait + dK
1635
+ # (7) [GEMM 5] dK += dS.T @ Q
1636
+ if const_expr(not self.mma_dkv_is_rs):
1637
+ mma_dsq_fn(
1638
+ A_idx=smem_idx_PdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1
1639
+ )
1640
+ else:
1641
+ mma_dsq_fn(tCrA=tdKrdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1)
1642
+ pipeline_dO.consumer_release(consumer_state_dO_cur)
1643
+ warpgroup.wait_group(0)
1644
+ pipeline_Q.consumer_release(consumer_state_Q)
1645
 
1646
  consumer_state_Q.advance()
1647
  consumer_state_dO.advance()
 
1666
  head_idx: Int32,
1667
  batch_idx: Int32,
1668
  qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
1669
+ mdK_semaphore: Optional[cute.Tensor] = None,
1670
+ mdV_semaphore: Optional[cute.Tensor] = None,
1671
  ):
1672
  epi_barrier = cutlass.pipeline.NamedBarrier(
1673
  barrier_id=int(NamedBarrierBwd.Epilogue), num_threads=self.num_mma_threads
 
1675
  warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
1676
 
1677
  if const_expr(self.qhead_per_kvhead == 1):
1678
+ mdK_cur = seqlen.offset_batch_K(mdK, batch_idx, dim=3, ragged=self.varlen_k)[
1679
+ None, None, head_idx
1680
+ ]
1681
+ mdV_cur = seqlen.offset_batch_K(mdV, batch_idx, dim=3, ragged=self.varlen_k)[
1682
+ None, None, head_idx
1683
+ ]
1684
  gdK = cute.local_tile(mdK_cur, (self.tile_n, self.tile_hdim), (n_block, 0))
1685
  gdV = cute.local_tile(mdV_cur, (self.tile_n, self.tile_hdimv), (n_block, 0))
1686
  store_dK, _, _ = copy_utils.tma_get_copy_fn(
 
1692
  sdV = sV if const_expr(not self.dKV_swapAB) else layout_utils.transpose_view(sV)
1693
  sdK = sK if const_expr(not self.dKV_swapAB) else layout_utils.transpose_view(sK)
1694
  copy_dV_r2s, _, _ = copy_utils.get_smem_store_C(
1695
+ tiled_mma_dV,
1696
+ sdV,
1697
+ tidx,
1698
+ transpose=self.dKV_swapAB,
1699
+ position_independent=True,
1700
  )
1701
  copy_dK_r2s, _, _ = copy_utils.get_smem_store_C(
1702
+ tiled_mma_dK,
1703
+ sdK,
1704
+ tidx,
1705
+ transpose=self.dKV_swapAB,
1706
+ position_independent=True,
1707
  )
1708
  cute.arch.cp_async_bulk_wait_group(1, read=True)
1709
  epi_barrier.arrive_and_wait()
 
1722
  store_dK()
1723
  cute.arch.cp_async_bulk_commit_group()
1724
  else:
1725
+ deterministic_KV = self.deterministic and self.qhead_per_kvhead > 1
1726
+ sdKaccum_shape0 = self.tile_n * self.tile_hdim // self.num_wg_mma
1727
+ sdVaccum_shape0 = self.tile_n * self.tile_hdimv // self.num_wg_mma
1728
+ sdKaccum_layout = cute.make_layout((sdKaccum_shape0, self.num_wg_mma))
1729
+ sdVaccum_layout = cute.make_layout((sdVaccum_shape0, self.num_wg_mma))
1730
  head_idx_kv = head_idx // qhead_per_kvhead_divmod
1731
+ if const_expr(deterministic_KV):
1732
+ assert mdK_semaphore is not None
1733
+ assert mdV_semaphore is not None
1734
+ mdK_semaphore_cur = mdK_semaphore[n_block, None, head_idx_kv, batch_idx]
1735
+ mdV_semaphore_cur = mdV_semaphore[n_block, None, head_idx_kv, batch_idx]
1736
+ lock_value = head_idx % self.qhead_per_kvhead
1737
+ mdKaccum_cur = seqlen.offset_batch_K(
1738
+ mdK, batch_idx, dim=2, padded=True, multiple=self.tile_hdim
1739
+ )[None, head_idx_kv]
1740
+ mdVaccum_cur = seqlen.offset_batch_K(
1741
+ mdV, batch_idx, dim=2, padded=True, multiple=self.tile_hdimv
1742
+ )[None, head_idx_kv]
1743
  gdKaccum_ = cute.local_tile(mdKaccum_cur, (self.tile_n * self.tile_hdim,), (n_block,))
1744
  gdKaccum = cute.flat_divide(gdKaccum_, (sdKaccum_shape0,))
 
1745
  gdVaccum_ = cute.local_tile(mdVaccum_cur, (self.tile_n * self.tile_hdimv,), (n_block,))
1746
  gdVaccum = cute.flat_divide(gdVaccum_, (sdVaccum_shape0,))
1747
  # These two overlap each other
 
1750
  sdVaccum = cute.make_tensor(sVaccum_ptr, sdVaccum_layout)
1751
  tiled_copy_dKVaccum_r2s = cute.make_tiled_copy_tv(
1752
  cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
1753
+ cute.make_layout((self.num_threads_per_warp_group, self.num_wg_mma)),
1754
  cute.make_layout(128 // Float32.width),
1755
  )
1756
  thr_copy_dKVaccum_r2s = tiled_copy_dKVaccum_r2s.get_slice(tidx)
1757
  tdKsdKaccum = thr_copy_dKVaccum_r2s.partition_D(sdKaccum)
1758
  tdVsdVaccum = thr_copy_dKVaccum_r2s.partition_D(sdVaccum)
1759
 
1760
+ read_flag = const_expr(not deterministic_KV)
1761
+ cute.arch.cp_async_bulk_wait_group(0, read=read_flag)
1762
+ if const_expr(deterministic_KV):
1763
+ barrier.wait_eq(mdK_semaphore_cur.iterator, tidx, 0, lock_value)
1764
  epi_barrier.arrive_and_wait()
1765
  tdKrdKaccum_flat = cute.make_tensor(acc_dK.iterator, tdKsdKaccum.shape)
1766
  cute.autovec_copy(tdKrdKaccum_flat, tdKsdKaccum)
 
1768
  epi_barrier.arrive_and_wait()
1769
  if warp_idx == 4:
1770
  with cute.arch.elect_one():
1771
+ for wg_idx in cutlass.range_constexpr(self.num_wg_mma):
1772
  copy_utils.cpasync_reduce_bulk_add_f32(
1773
  sdKaccum[None, wg_idx].iterator,
1774
  gdKaccum[None, wg_idx].iterator,
1775
+ self.tma_copy_bytes["dKacc"] // self.num_wg_mma,
1776
  )
1777
  cute.arch.cp_async_bulk_commit_group()
1778
 
1779
+ cute.arch.cp_async_bulk_wait_group(0, read=read_flag)
1780
+ if const_expr(deterministic_KV):
1781
+ barrier.arrive_inc(mdK_semaphore_cur.iterator, tidx, 0, 1)
1782
+ barrier.wait_eq(mdV_semaphore_cur.iterator, tidx, 0, lock_value)
1783
  epi_barrier.arrive_and_wait()
1784
  tdVrdVaccum_flat = cute.make_tensor(acc_dV.iterator, tdVsdVaccum.shape)
1785
  cute.autovec_copy(tdVrdVaccum_flat, tdVsdVaccum)
 
1787
  epi_barrier.arrive_and_wait()
1788
  if warp_idx == 4:
1789
  with cute.arch.elect_one():
1790
+ for wg_idx in cutlass.range_constexpr(self.num_wg_mma):
1791
  copy_utils.cpasync_reduce_bulk_add_f32(
1792
  sdVaccum[None, wg_idx].iterator,
1793
  gdVaccum[None, wg_idx].iterator,
1794
+ self.tma_copy_bytes["dVacc"] // self.num_wg_mma,
1795
  )
1796
  cute.arch.cp_async_bulk_commit_group()
1797
+ if const_expr(deterministic_KV):
1798
+ cute.arch.cp_async_bulk_wait_group(0, read=read_flag)
1799
+ barrier.arrive_inc(mdV_semaphore_cur.iterator, tidx, 0, 1)
1800
 
1801
  @cute.jit
1802
  def dQaccum_store(
 
1807
  TileSchedulerCls: cutlass.Constexpr[Callable],
1808
  SeqlenInfoCls: cutlass.Constexpr[Callable],
1809
  blocksparse_tensors: Optional[BlockSparseTensors] = None,
1810
+ mdQ_semaphore: Optional[cute.Tensor] = None,
1811
  ):
1812
+ tidx, _, _ = cute.arch.thread_idx()
1813
+ # warp-local thread index (dQaccum_store runs on warp 1, global tidx 32-63)
1814
+ warp_local_tidx = tidx % cute.arch.WARP_SIZE
1815
+ read_flag = const_expr(not self.deterministic)
1816
+
1817
  tile_scheduler = TileSchedulerCls()
1818
  work_tile = tile_scheduler.initial_work_tile_info()
1819
  while work_tile.is_valid_tile:
1820
  n_block, head_idx, batch_idx, _ = work_tile.tile_idx
1821
  seqlen = SeqlenInfoCls(batch_idx)
1822
+ if const_expr(not seqlen.has_cu_seqlens_q):
1823
+ mdQaccum_cur = mdQaccum[None, head_idx, batch_idx]
1824
+ else:
1825
+ mdQaccum_cur = cute.domain_offset(
1826
+ (seqlen.padded_offset_q * self.tile_hdim,), mdQaccum[None, head_idx]
1827
+ )
1828
+ # ((M * K / num_wg_dQ, num_wg_dQ), num_m_blocks)
1829
+ gdQaccum = cute.local_tile(
1830
+ mdQaccum_cur,
1831
+ (
1832
+ cute.make_layout(
1833
+ (self.tile_m * self.tile_hdim // self.num_wg_dQ, self.num_wg_dQ)
1834
+ ),
1835
+ ),
1836
+ (None,),
1837
  )
1838
+
1839
+ if const_expr(mdQ_semaphore is not None):
1840
+ # mdQ_semaphore is (num_m_blocks, cluster_size, num_head, batch) after transpose
1841
+ mdQ_semaphore_cur = mdQ_semaphore[None, None, head_idx, batch_idx]
1842
+
1843
  m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
1844
  if const_expr(not self.use_block_sparsity):
1845
+ process_tile = (
1846
+ const_expr(not self.is_local and not self.is_varlen_q)
1847
+ or m_block_min < m_block_max
1848
+ )
1849
  loop_count = m_block_max - m_block_min
1850
  else:
1851
  total_block_cnt = get_total_q_block_count_bwd(
 
1853
  batch_idx,
1854
  head_idx,
1855
  n_block,
1856
+ q_subtile_factor=self.q_subtile_factor,
1857
  m_block_max=m_block_max,
1858
  )
1859
  process_tile = total_block_cnt > Int32(0)
 
1864
  m_block = m_block_min + iter_idx
1865
  m_block_safe = m_block
1866
 
1867
+ num_dQ_chunks = self.num_wg_dQ
1868
+ for warp_group_idx in cutlass.range_constexpr(num_dQ_chunks):
1869
+ if const_expr(not self.deterministic):
1870
+ # If deterministic, we already waited at the end of the prev iter
1871
+ cute.arch.cp_async_bulk_wait_group(
1872
+ num_dQ_chunks - 1 - warp_group_idx, read=read_flag
1873
+ )
1874
  cute.arch.barrier_arrive(
1875
  barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1876
  number_of_threads=self.num_threads_per_warp_group
1877
  + cute.arch.WARP_SIZE,
1878
  )
1879
 
1880
+ # Semaphore acquire: wait for prior n_blocks to finish writing this m_block
1881
+ if const_expr(self.deterministic):
1882
+ if const_expr(self.spt):
1883
+ _, n_block_max_for_m_block = block_info.get_n_block_min_max(
1884
+ seqlen, m_block_safe
1885
+ )
1886
+ lock_value = n_block_max_for_m_block - 1 - n_block
1887
+ else:
1888
+ lock_value = n_block
1889
+ barrier.wait_eq(
1890
+ mdQ_semaphore_cur[(m_block_safe, None)].iterator,
1891
+ warp_local_tidx,
1892
+ 0, # flag_offset
1893
+ lock_value,
1894
+ )
1895
+
1896
+ for warp_group_idx in cutlass.range_constexpr(num_dQ_chunks):
1897
  cute.arch.barrier(
1898
  barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1899
  number_of_threads=self.num_threads_per_warp_group
 
1902
  with cute.arch.elect_one():
1903
  copy_utils.cpasync_reduce_bulk_add_f32(
1904
  sdQaccum[None, warp_group_idx].iterator,
1905
+ gdQaccum[(None, warp_group_idx), m_block_safe].iterator,
1906
  self.tma_copy_bytes["dQ"],
1907
  )
1908
  cute.arch.cp_async_bulk_commit_group()
1909
+
1910
+ # Semaphore release: signal that this n_block is done with this m_block
1911
+ if const_expr(self.deterministic):
1912
+ cute.arch.cp_async_bulk_wait_group(0, read=read_flag)
1913
+ barrier.arrive_inc(
1914
+ mdQ_semaphore_cur[(m_block_safe, None)].iterator,
1915
+ warp_local_tidx,
1916
+ 0, # flag_offset
1917
+ 1,
1918
+ )
1919
  else:
1920
+ assert not self.deterministic, (
1921
+ "Deterministic not implemented for block-sparse backward"
1922
+ )
1923
  dQaccum_store_block_sparse_bwd_sm90(
1924
  blocksparse_tensors,
1925
  batch_idx,
 
1927
  n_block,
1928
  sdQaccum,
1929
  gdQaccum,
1930
+ q_subtile_factor=self.q_subtile_factor,
1931
  m_block_max=m_block_max,
1932
+ num_dQ_warp_groups=self.num_wg_dQ,
1933
  num_threads_per_warp_group=self.num_threads_per_warp_group,
1934
  tma_copy_bytes_dQ=self.tma_copy_bytes["dQ"],
1935
  )
1936
+
1937
+ # For local masking + deterministic (non-spt): signal remaining m_blocks
1938
+ # that this n_block won't visit, so they don't deadlock waiting.
1939
+ if const_expr(
1940
+ self.deterministic and not self.spt and block_info.window_size_left is not None
1941
+ ):
1942
+ m_block_global_max = cute.ceil_div(seqlen.seqlen_q, self.tile_m)
1943
+ for m_block in cutlass.range(m_block_max, m_block_global_max, unroll=1):
1944
+ barrier.arrive_inc(
1945
+ mdQ_semaphore_cur[(m_block, None)].iterator,
1946
+ warp_local_tidx,
1947
+ 0, # flag_offset
1948
+ 1,
1949
+ )
1950
+
1951
  tile_scheduler.advance_to_next_work()
1952
  work_tile = tile_scheduler.get_current_work()
1953
 
1954
+ if const_expr(not self.deterministic):
1955
+ cute.arch.cp_async_bulk_wait_group(0, read=True)
build/torch-cuda/flash_fwd.py CHANGED
The diff for this file is too large to render. See raw diff
 
build/torch-cuda/flash_fwd_combine.py CHANGED
@@ -2,7 +2,7 @@
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
- from typing import Type, Optional
6
  from functools import partial
7
 
8
  import cuda.bindings.driver as cuda
@@ -10,11 +10,18 @@ import cuda.bindings.driver as cuda
10
  import cutlass
11
  import cutlass.cute as cute
12
  from cutlass.cute.nvgpu import cpasync
13
- from cutlass import Float32, Int32, const_expr
 
 
14
 
15
  from . import utils
16
  from .cute_dsl_utils import assume_tensor_aligned
17
  from .seqlen_info import SeqlenInfo
 
 
 
 
 
18
  from cutlass.cute import FastDivmodDivisor
19
 
20
 
@@ -24,7 +31,8 @@ class FlashAttentionForwardCombine:
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,
@@ -36,7 +44,8 @@ class FlashAttentionForwardCombine:
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
@@ -46,7 +55,8 @@ class FlashAttentionForwardCombine:
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
@@ -58,7 +68,7 @@ class FlashAttentionForwardCombine:
58
  dtype,
59
  dtype_partial,
60
  head_dim,
61
- m_block_size,
62
  k_block_size,
63
  log_max_splits,
64
  num_threads,
@@ -72,12 +82,12 @@ class FlashAttentionForwardCombine:
72
  return False
73
  if num_threads % 32 != 0:
74
  return False
75
- if m_block_size % 8 != 0:
76
  return False
77
  max_splits = 1 << log_max_splits
78
  if max_splits > 256:
79
  return False
80
- if (m_block_size * max_splits) % num_threads != 0:
81
  return False
82
  return True
83
 
@@ -124,15 +134,11 @@ class FlashAttentionForwardCombine:
124
  lse_copy_bits = Float32.width # 1 element per copy, width is in bits
125
  m_block_smem = (
126
  128
127
- if self.m_block_size % 128 == 0
128
  else (
129
  64
130
- if self.m_block_size % 64 == 0
131
- else (
132
- 32
133
- if self.m_block_size % 32 == 0
134
- else (16 if self.m_block_size % 16 == 0 else 8)
135
- )
136
  )
137
  )
138
  gmem_threads_per_row_lse = m_block_smem
@@ -183,12 +189,12 @@ class FlashAttentionForwardCombine:
183
  smem_lse_swizzle, 0, cute.make_ordered_layout((8, m_block_smem), order=(1, 0))
184
  )
185
  self.smem_layout_lse = cute.tile_to_shape(
186
- smem_layout_atom_lse, (self.max_splits, self.m_block_size), (0, 1)
187
  )
188
 
189
  # O partial shared memory layout (simple layout for pipeline stages)
190
  self.smem_layout_o = cute.make_ordered_layout(
191
- (self.m_block_size, self.k_block_size, self.stages), order=(1, 0, 2)
192
  )
193
 
194
  @cute.jit
@@ -201,7 +207,9 @@ class FlashAttentionForwardCombine:
201
  cu_seqlens: Optional[cute.Tensor] = None,
202
  seqused: Optional[cute.Tensor] = None,
203
  num_splits_dynamic_ptr: Optional[cute.Tensor] = None,
 
204
  semaphore_to_reset: Optional[cute.Tensor] = None,
 
205
  stream: cuda.CUstream = None,
206
  ):
207
  # Type checking
@@ -269,7 +277,7 @@ class FlashAttentionForwardCombine:
269
  sLSE: cute.struct.Align[
270
  cute.struct.MemRange[Float32, cute.cosize(self.smem_layout_lse)], 128
271
  ]
272
- sMaxValidSplit: cute.struct.Align[cute.struct.MemRange[Int32, self.m_block_size], 128]
273
  sO: cute.struct.Align[
274
  cute.struct.MemRange[self.dtype_partial, cute.cosize(self.smem_layout_o)], 128
275
  ]
@@ -289,11 +297,29 @@ class FlashAttentionForwardCombine:
289
  seqlen_divmod = FastDivmodDivisor(seqlen)
290
  head_divmod = FastDivmodDivisor(num_head)
291
 
292
- grid_dim = (
293
- cute.ceil_div(seqlen * num_head, self.m_block_size),
294
- cute.ceil_div(self.head_dim, self.k_block_size),
295
- batch_size,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  )
 
 
297
 
298
  self.kernel(
299
  mO_partial,
@@ -303,6 +329,7 @@ class FlashAttentionForwardCombine:
303
  cu_seqlens,
304
  seqused,
305
  num_splits_dynamic_ptr,
 
306
  semaphore_to_reset,
307
  SharedStorage,
308
  self.smem_layout_lse,
@@ -314,6 +341,8 @@ class FlashAttentionForwardCombine:
314
  seqlen_divmod,
315
  head_divmod,
316
  varlen,
 
 
317
  ).launch(
318
  grid=grid_dim,
319
  block=[self.num_threads, 1, 1],
@@ -331,6 +360,7 @@ class FlashAttentionForwardCombine:
331
  cu_seqlens: Optional[cute.Tensor],
332
  seqused: Optional[cute.Tensor],
333
  num_splits_dynamic_ptr: Optional[cute.Tensor],
 
334
  semaphore_to_reset: Optional[cute.Tensor],
335
  SharedStorage: cutlass.Constexpr,
336
  smem_layout_lse: cute.Layout | cute.ComposedLayout,
@@ -342,10 +372,21 @@ class FlashAttentionForwardCombine:
342
  seqlen_divmod: FastDivmodDivisor,
343
  head_divmod: FastDivmodDivisor,
344
  varlen: cutlass.Constexpr[bool],
 
 
345
  ):
346
  # Thread and block indices
347
  tidx, _, _ = cute.arch.thread_idx()
348
- m_block, k_block, batch_idx = cute.arch.block_idx()
 
 
 
 
 
 
 
 
 
349
 
350
  # ///////////////////////////////////////////////////////////////////////////////
351
  # Get shared memory buffer
@@ -353,310 +394,315 @@ class FlashAttentionForwardCombine:
353
  smem = cutlass.utils.SmemAllocator()
354
  storage = smem.allocate(SharedStorage)
355
  sLSE = storage.sLSE.get_tensor(smem_layout_lse)
356
- sMaxValidSplit = storage.sMaxValidSplit.get_tensor((self.m_block_size,))
357
  sO = storage.sO.get_tensor(smem_layout_o)
358
 
359
- # Handle semaphore reset
360
  if const_expr(semaphore_to_reset is not None):
 
361
  if (
362
  tidx == 0
363
- and m_block == cute.arch.grid_dim()[0] - 1
364
- and k_block == cute.arch.grid_dim()[1] - 1
365
- and batch_idx == cute.arch.grid_dim()[2] - 1
366
  ):
 
367
  semaphore_to_reset[0] = 0
368
 
369
- # Get number of splits
370
- num_splits = (
371
- num_splits_dynamic_ptr[batch_idx]
372
- if const_expr(num_splits_dynamic_ptr is not None)
373
- else mLSE_partial.shape[1]
374
- )
375
- # Handle variable length sequences using SeqlenInfo
376
- seqlen_info = SeqlenInfo.create(
377
- batch_idx=batch_idx,
378
- seqlen_static=mO_partial.shape[0],
379
- cu_seqlens=cu_seqlens,
380
- seqused=seqused,
381
- )
382
- seqlen, offset = seqlen_info.seqlen, seqlen_info.offset
383
-
384
- # Extract number of heads (head index will be determined dynamically)
385
- num_head = mO_partial.shape[3]
386
- max_idx = seqlen * num_head
387
-
388
- # Early exit for single split if dynamic
389
- if (const_expr(num_splits_dynamic_ptr is None) or num_splits > 1) and (
390
- const_expr(not varlen) or m_block * self.m_block_size < max_idx
391
- ):
392
- # ===============================
393
- # Step 1: Load LSE_partial from gmem to shared memory
394
- # ===============================
395
-
396
- if const_expr(cu_seqlens is None):
397
- mLSE_partial_cur = mLSE_partial[None, None, None, batch_idx]
398
- else:
399
- mLSE_partial_cur = cute.domain_offset((offset, 0, 0), mLSE_partial)
400
- mLSE_partial_copy = cute.tiled_divide(mLSE_partial_cur, (1,))
401
-
402
- gmem_thr_copy_LSE = gmem_tiled_copy_LSE.get_slice(tidx)
403
- tLSEsLSE = gmem_thr_copy_LSE.partition_D(sLSE)
404
-
405
- # Create identity tensor for coordinate tracking
406
- cLSE = cute.make_identity_tensor((self.max_splits, self.m_block_size))
407
- tLSEcLSE = gmem_thr_copy_LSE.partition_S(cLSE)
408
-
409
- # Load LSE partial values
410
- for m in cutlass.range(cute.size(tLSEcLSE, mode=[2]), unroll_full=True):
411
- mi = tLSEcLSE[0, 0, m][1] # Get m coordinate
412
- idx = m_block * self.m_block_size + mi
413
- if idx < max_idx:
414
- # Calculate actual sequence position and head using FastDivmodDivisor
415
- if const_expr(not varlen):
416
- head_idx, m_idx = divmod(idx, seqlen_divmod)
417
- else:
418
- head_idx = idx // seqlen
419
- m_idx = idx - head_idx * seqlen
420
- mLSE_partial_cur_copy = mLSE_partial_copy[None, m_idx, None, head_idx]
421
- for s in cutlass.range(cute.size(tLSEcLSE, mode=[1]), unroll_full=True):
422
- si = tLSEcLSE[0, s, 0][0] # Get split coordinate
423
- if si < num_splits:
424
- cute.copy(
425
- gmem_thr_copy_LSE,
426
- mLSE_partial_cur_copy[None, si],
427
- tLSEsLSE[None, s, m],
428
- )
429
- else:
430
- tLSEsLSE[None, s, m].fill(-Float32.inf)
431
- # Don't need to zero out the rest of the LSEs, as we will not write the output to gmem
432
- cute.arch.cp_async_commit_group()
433
-
434
- # ===============================
435
- # Step 2: Load O_partial for pipeline stages
436
- # ===============================
437
-
438
- gmem_thr_copy_O_partial = gmem_tiled_copy_O_partial.get_slice(tidx)
439
- cO = cute.make_identity_tensor((self.m_block_size, self.k_block_size))
440
- tOcO = gmem_thr_copy_O_partial.partition_D(cO)
441
- tOsO_partial = gmem_thr_copy_O_partial.partition_D(sO)
442
- if const_expr(cu_seqlens is None):
443
- mO_partial_cur = mO_partial[None, None, None, None, batch_idx]
444
- else:
445
- mO_partial_cur = cute.domain_offset((offset, 0, 0, 0), mO_partial)
446
-
447
- # Precompute these values to avoid recomputing them in the loop
448
- num_rows = const_expr(cute.size(tOcO, mode=[1]))
449
- tOmidx = cute.make_fragment(num_rows, cutlass.Int32)
450
- tOhidx = cute.make_fragment(num_rows, cutlass.Int32)
451
- tOrOptr = cute.make_fragment(num_rows, cutlass.Int64)
452
- for m in cutlass.range(num_rows, unroll_full=True):
453
- mi = tOcO[0, m, 0][0] # m coordinate
454
- idx = m_block * self.m_block_size + mi
455
- if const_expr(not varlen):
456
- tOhidx[m], tOmidx[m] = divmod(idx, seqlen_divmod)
457
- else:
458
- tOhidx[m] = idx // seqlen
459
- tOmidx[m] = idx - tOhidx[m] * seqlen
460
- tOrOptr[m] = utils.elem_pointer(
461
- mO_partial_cur, (tOmidx[m], k_block * self.k_block_size, 0, tOhidx[m])
462
- ).toint()
463
- if idx >= max_idx:
464
- tOhidx[m] = -1
465
-
466
- tOpO = cute.make_fragment(cute.size(tOcO, [2]), cutlass.Boolean)
467
- if const_expr(not self.is_even_k):
468
- for k in cutlass.range(cute.size(tOpO), unroll_full=True):
469
- tOpO[k] = tOcO[0, 0, k][1] < mO_partial.shape[1] - k_block * self.k_block_size
470
- # if cute.arch.thread_idx()[0] == 0 and k_block == 1: cute.print_tensor(tOpO)
471
-
472
- load_O_partial = partial(
473
- self.load_O_partial,
474
- gmem_tiled_copy_O_partial,
475
- tOrOptr,
476
- tOsO_partial,
477
- tOhidx,
478
- tOpO,
479
- tOcO,
480
- mO_partial_cur.layout,
481
  )
 
482
 
483
- # Load first few stages of O_partial
484
- for stage in cutlass.range(self.stages - 1, unroll_full=True):
485
- if stage < num_splits:
486
- load_O_partial(stage, stage)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
  cute.arch.cp_async_commit_group()
488
 
489
- # ===============================
490
- # Step 3: Load and transpose LSE from smem to registers
491
- # ===============================
492
-
493
- # Wait for LSE and initial O partial stages to complete
494
- cute.arch.cp_async_wait_group(self.stages - 1)
495
- cute.arch.sync_threads()
496
- # if cute.arch.thread_idx()[0] == 0:
497
- # # cute.print_tensor(sLSE)
498
- # for i in range(64):
499
- # cute.printf("sLSE[%d, 0] = %f", i, sLSE[i, 0])
500
- # cute.arch.sync_threads()
501
-
502
- s2r_thr_copy_LSE = s2r_tiled_copy_LSE.get_slice(tidx)
503
- ts2rsLSE = s2r_thr_copy_LSE.partition_S(sLSE)
504
- ts2rrLSE = cute.make_fragment_like(ts2rsLSE)
505
- cute.copy(s2r_tiled_copy_LSE, ts2rsLSE, ts2rrLSE)
506
-
507
- # ===============================
508
- # Step 4: Compute final LSE along split dimension
509
- # ===============================
510
-
511
- lse_sum = cute.make_fragment(cute.size(ts2rrLSE, mode=[2]), Float32)
512
- ts2rcLSE = s2r_thr_copy_LSE.partition_D(cLSE)
513
- # We compute the max valid split for each row to short-circuit the computation later
514
- max_valid_split = cute.make_fragment(cute.size(ts2rrLSE, mode=[2]), Int32)
515
- assert cute.size(ts2rrLSE, mode=[0]) == 1
516
- # Compute max, scales, and final LSE for each row
517
- for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
518
- # Find max LSE value across splits
519
- threads_per_col = const_expr(self.smem_threads_per_col_lse)
520
- lse_max = cute.arch.warp_reduction_max(
521
- ts2rrLSE[None, None, m]
522
- .load()
523
- .reduce(cute.ReductionOp.MAX, init_val=-Float32.inf, reduction_profile=0),
524
- threads_in_group=threads_per_col,
525
- )
526
- # if cute.arch.thread_idx()[0] == 0: cute.printf(lse_max)
527
- # Find max valid split index
528
- max_valid_idx = -1
529
- for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
530
- if ts2rrLSE[0, s, m] != -Float32.inf:
531
- max_valid_idx = ts2rcLSE[0, s, 0][0] # Get split coordinate
532
- # if cute.arch.thread_idx()[0] < 32: cute.printf(max_valid_idx)
533
- max_valid_split[m] = cute.arch.warp_reduction_max(
534
- max_valid_idx, threads_in_group=threads_per_col
535
- )
536
- # Compute exp scales and sum
537
- lse_max_cur = (
538
- 0.0 if lse_max == -Float32.inf else lse_max
539
- ) # In case all local LSEs are -inf
540
- LOG2_E = math.log2(math.e)
541
- lse_sum_cur = 0.0
542
- for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
543
- scale = cute.math.exp2(
544
- ts2rrLSE[0, s, m] * LOG2_E - (lse_max_cur * LOG2_E), fastmath=True
545
- )
546
- lse_sum_cur += scale
547
- ts2rrLSE[0, s, m] = scale # Store scale for later use
548
- lse_sum_cur = cute.arch.warp_reduction_sum(
549
- lse_sum_cur, threads_in_group=threads_per_col
550
- )
551
- lse_sum[m] = cute.math.log(lse_sum_cur, fastmath=True) + lse_max
552
- # Normalize scales
553
- inv_sum = (
554
- 0.0 if (lse_sum_cur == 0.0 or lse_sum_cur != lse_sum_cur) else 1.0 / lse_sum_cur
555
- )
556
- ts2rrLSE[None, None, m].store(ts2rrLSE[None, None, m].load() * inv_sum)
557
- # Store the scales exp(lse - lse_logsum) back to smem
558
- cute.copy(s2r_tiled_copy_LSE, ts2rrLSE, ts2rsLSE)
559
-
560
- # Store max valid split to smem
561
- for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
562
- if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
563
- mi = ts2rcLSE[0, 0, m][1]
564
- if mi < self.m_block_size:
565
- sMaxValidSplit[mi] = max_valid_split[m]
566
-
567
- # ===============================
568
- # Step 5: Store final LSE to gmem
569
- # ===============================
570
-
571
- if const_expr(mLSE is not None):
572
- if const_expr(cu_seqlens is None):
573
- mLSE_cur = mLSE[None, None, batch_idx]
574
- else:
575
- mLSE_cur = cute.domain_offset((offset, 0), mLSE)
576
- if k_block == 0: # Only first k_block writes LSE when mLSE is provided
577
- for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
578
- if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
579
- mi = ts2rcLSE[0, 0, m][1]
580
- idx = m_block * self.m_block_size + mi
581
- if idx < max_idx:
582
- if const_expr(not varlen):
583
- head_idx, m_idx = divmod(idx, seqlen_divmod)
584
- else:
585
- head_idx = idx // seqlen
586
- m_idx = idx - head_idx * seqlen
587
- mLSE_cur[m_idx, head_idx] = lse_sum[m]
588
-
589
- # ===============================
590
- # Step 6: Read O_partial and accumulate final O
591
- # ===============================
592
-
593
- cute.arch.sync_threads()
594
-
595
- # Get max valid split for this thread
596
- thr_max_valid_split = sMaxValidSplit[tOcO[0, 0, 0][0]]
597
- for m in cutlass.range(1, cute.size(tOcO, mode=[1])):
598
- thr_max_valid_split = max(thr_max_valid_split, sMaxValidSplit[tOcO[0, m, 0][0]])
599
-
600
- tOrO_partial = cute.make_fragment_like(tOsO_partial[None, None, None, 0])
601
- tOrO = cute.make_fragment_like(tOrO_partial, Float32)
602
- tOrO.fill(0.0)
603
-
604
- stage_load = self.stages - 1
605
- stage_compute = 0
606
-
607
- # Main accumulation loop
608
- for s in cutlass.range(thr_max_valid_split + 1, unroll=4):
609
- # Get scales for this split
610
- scale = cute.make_fragment(num_rows, Float32)
611
  for m in cutlass.range(num_rows, unroll_full=True):
612
- scale[m] = sLSE[s, tOcO[0, m, 0][0]] # Get scale from smem
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
 
614
- # Load next stage if needed
615
- split_to_load = s + self.stages - 1
616
- if split_to_load <= thr_max_valid_split:
617
- load_O_partial(split_to_load, stage_load)
618
- cute.arch.cp_async_commit_group()
619
- stage_load = 0 if stage_load == self.stages - 1 else stage_load + 1
 
 
 
620
 
621
- # Wait for the current stage to be ready
622
  cute.arch.cp_async_wait_group(self.stages - 1)
623
- # We don't need __syncthreads() because each thread is just reading its own data from smem
624
- # Copy from smem to registers
625
- cute.autovec_copy(tOsO_partial[None, None, None, stage_compute], tOrO_partial)
626
- stage_compute = 0 if stage_compute == self.stages - 1 else stage_compute + 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
 
628
- # Accumulate scaled partial results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
629
  for m in cutlass.range(num_rows, unroll_full=True):
630
- if tOhidx[m] >= 0 and scale[m] > 0.0:
631
- tOrO[None, m, None].store(
632
- tOrO[None, m, None].load()
633
- + scale[m] * tOrO_partial[None, m, None].load().to(Float32)
634
  )
635
-
636
- # ===============================
637
- # Step 7: Write final O to gmem
638
- # ===============================
639
-
640
- rO = cute.make_fragment_like(tOrO, self.dtype)
641
- rO.store(tOrO.load().to(self.dtype))
642
- if const_expr(cu_seqlens is None):
643
- mO_cur = mO[None, None, None, batch_idx]
644
- else:
645
- mO_cur = cute.domain_offset((offset, 0, 0), mO)
646
- mO_cur = utils.domain_offset_aligned((0, k_block * self.k_block_size, 0), mO_cur)
647
- elems_per_store = const_expr(cute.size(gmem_tiled_copy_O.layout_tv_tiled[1]))
648
- # mO_cur_copy = cute.tiled_divide(mO_cur, (1, elems_per_store,))
649
- gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
650
- # Write final results
651
- for m in cutlass.range(num_rows, unroll_full=True):
652
- if tOhidx[m] >= 0:
653
- mO_cur_copy = cute.tiled_divide(
654
- mO_cur[tOmidx[m], None, tOhidx[m]], (elems_per_store,)
655
- )
656
- for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
657
- k_idx = tOcO[0, 0, k][1] // elems_per_store
658
- if const_expr(self.is_even_k) or tOpO[k]:
659
- cute.copy(gmem_thr_copy_O, rO[None, m, k], mO_cur_copy[None, k_idx])
660
 
661
  @cute.jit
662
  def load_O_partial(
@@ -665,7 +711,7 @@ class FlashAttentionForwardCombine:
665
  tOrOptr: cute.Tensor,
666
  tOsO_partial: cute.Tensor,
667
  tOhidx: cute.Tensor,
668
- tOpO: cute.Tensor,
669
  tOcO: cute.Tensor,
670
  mO_cur_partial_layout: cute.Layout,
671
  split: Int32,
@@ -684,7 +730,7 @@ class FlashAttentionForwardCombine:
684
  mO_partial_cur_copy = cute.tiled_divide(mO_partial_cur, (elems_per_load,))
685
  for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
686
  k_idx = tOcO[0, 0, k][1] // elems_per_load
687
- if const_expr(self.is_even_k) or tOpO[k]:
688
  cute.copy(
689
  gmem_tiled_copy_O_partial,
690
  mO_partial_cur_copy[None, k_idx, split],
 
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
+ from typing import Callable, Type, Optional
6
  from functools import partial
7
 
8
  import cuda.bindings.driver as cuda
 
10
  import cutlass
11
  import cutlass.cute as cute
12
  from cutlass.cute.nvgpu import cpasync
13
+ from cutlass import Float32, Int32, Boolean, const_expr
14
+
15
+ from .quack.cute_dsl_utils import ParamsBase
16
 
17
  from . import utils
18
  from .cute_dsl_utils import assume_tensor_aligned
19
  from .seqlen_info import SeqlenInfo
20
+ from .tile_scheduler import (
21
+ SingleTileScheduler,
22
+ SingleTileVarlenScheduler,
23
+ TileSchedulerArguments,
24
+ )
25
  from cutlass.cute import FastDivmodDivisor
26
 
27
 
 
31
  dtype: Type[cutlass.Numeric],
32
  dtype_partial: Type[cutlass.Numeric],
33
  head_dim: int,
34
+ num_head: int,
35
+ tile_m: int = 8,
36
  k_block_size: int = 64,
37
  log_max_splits: int = 4,
38
  num_threads: int = 256,
 
44
  :param dtype: output data type
45
  :param dtype_partial: partial accumulation data type
46
  :param head_dim: head dimension
47
+ :param num_head: number of heads
48
+ :param tile_m: m block size
49
  :param k_block_size: k block size
50
  :param log_max_splits: log2 of maximum splits
51
  :param num_threads: number of threads
 
55
  self.dtype = dtype
56
  self.dtype_partial = dtype_partial
57
  self.head_dim = head_dim
58
+ self.num_head = num_head
59
+ self.tile_m = tile_m
60
  self.k_block_size = k_block_size
61
  self.max_splits = 1 << log_max_splits
62
  self.num_threads = num_threads
 
68
  dtype,
69
  dtype_partial,
70
  head_dim,
71
+ tile_m,
72
  k_block_size,
73
  log_max_splits,
74
  num_threads,
 
82
  return False
83
  if num_threads % 32 != 0:
84
  return False
85
+ if tile_m % 8 != 0:
86
  return False
87
  max_splits = 1 << log_max_splits
88
  if max_splits > 256:
89
  return False
90
+ if (tile_m * max_splits) % num_threads != 0:
91
  return False
92
  return True
93
 
 
134
  lse_copy_bits = Float32.width # 1 element per copy, width is in bits
135
  m_block_smem = (
136
  128
137
+ if self.tile_m % 128 == 0
138
  else (
139
  64
140
+ if self.tile_m % 64 == 0
141
+ else (32 if self.tile_m % 32 == 0 else (16 if self.tile_m % 16 == 0 else 8))
 
 
 
 
142
  )
143
  )
144
  gmem_threads_per_row_lse = m_block_smem
 
189
  smem_lse_swizzle, 0, cute.make_ordered_layout((8, m_block_smem), order=(1, 0))
190
  )
191
  self.smem_layout_lse = cute.tile_to_shape(
192
+ smem_layout_atom_lse, (self.max_splits, self.tile_m), (0, 1)
193
  )
194
 
195
  # O partial shared memory layout (simple layout for pipeline stages)
196
  self.smem_layout_o = cute.make_ordered_layout(
197
+ (self.tile_m, self.k_block_size, self.stages), order=(1, 0, 2)
198
  )
199
 
200
  @cute.jit
 
207
  cu_seqlens: Optional[cute.Tensor] = None,
208
  seqused: Optional[cute.Tensor] = None,
209
  num_splits_dynamic_ptr: Optional[cute.Tensor] = None,
210
+ virtual_batch_idx: Optional[cute.Tensor] = None,
211
  semaphore_to_reset: Optional[cute.Tensor] = None,
212
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
213
  stream: cuda.CUstream = None,
214
  ):
215
  # Type checking
 
277
  sLSE: cute.struct.Align[
278
  cute.struct.MemRange[Float32, cute.cosize(self.smem_layout_lse)], 128
279
  ]
280
+ sMaxValidSplit: cute.struct.Align[cute.struct.MemRange[Int32, self.tile_m], 128]
281
  sO: cute.struct.Align[
282
  cute.struct.MemRange[self.dtype_partial, cute.cosize(self.smem_layout_o)], 128
283
  ]
 
297
  seqlen_divmod = FastDivmodDivisor(seqlen)
298
  head_divmod = FastDivmodDivisor(num_head)
299
 
300
+ if const_expr(varlen):
301
+ TileScheduler = SingleTileVarlenScheduler
302
+ else:
303
+ TileScheduler = SingleTileScheduler
304
+ tile_sched_args = TileSchedulerArguments(
305
+ num_block=cute.ceil_div(seqlen * num_head, self.tile_m),
306
+ num_head=cute.ceil_div(self.head_dim, self.k_block_size),
307
+ num_batch=batch_size,
308
+ num_splits=1,
309
+ seqlen_k=1,
310
+ headdim=1,
311
+ headdim_v=1,
312
+ total_q=mO_partial.shape[0] * num_head
313
+ if const_expr(cu_seqlens is not None)
314
+ else seqlen * batch_size * num_head,
315
+ tile_shape_mn=(self.tile_m, self.tile_m),
316
+ mCuSeqlensQ=cu_seqlens,
317
+ mSeqUsedQ=seqused,
318
+ qhead_per_kvhead_packgqa=self.num_head,
319
+ virtual_batch_idx_ptr=virtual_batch_idx,
320
  )
321
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
322
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
323
 
324
  self.kernel(
325
  mO_partial,
 
329
  cu_seqlens,
330
  seqused,
331
  num_splits_dynamic_ptr,
332
+ virtual_batch_idx,
333
  semaphore_to_reset,
334
  SharedStorage,
335
  self.smem_layout_lse,
 
341
  seqlen_divmod,
342
  head_divmod,
343
  varlen,
344
+ tile_sched_params,
345
+ TileScheduler,
346
  ).launch(
347
  grid=grid_dim,
348
  block=[self.num_threads, 1, 1],
 
360
  cu_seqlens: Optional[cute.Tensor],
361
  seqused: Optional[cute.Tensor],
362
  num_splits_dynamic_ptr: Optional[cute.Tensor],
363
+ virtual_batch_idx: Optional[cute.Tensor],
364
  semaphore_to_reset: Optional[cute.Tensor],
365
  SharedStorage: cutlass.Constexpr,
366
  smem_layout_lse: cute.Layout | cute.ComposedLayout,
 
372
  seqlen_divmod: FastDivmodDivisor,
373
  head_divmod: FastDivmodDivisor,
374
  varlen: cutlass.Constexpr[bool],
375
+ tile_sched_params: ParamsBase,
376
+ TileScheduler: cutlass.Constexpr[Callable],
377
  ):
378
  # Thread and block indices
379
  tidx, _, _ = cute.arch.thread_idx()
380
+ tile_scheduler = TileScheduler.create(tile_sched_params)
381
+ work_tile = tile_scheduler.initial_work_tile_info()
382
+ m_block, k_block, maybe_virtual_batch, _ = work_tile.tile_idx
383
+
384
+ # Map virtual batch index to real batch index (for persistent tile schedulers)
385
+ batch_idx = (
386
+ virtual_batch_idx[maybe_virtual_batch]
387
+ if const_expr(virtual_batch_idx is not None and not varlen)
388
+ else maybe_virtual_batch
389
+ )
390
 
391
  # ///////////////////////////////////////////////////////////////////////////////
392
  # Get shared memory buffer
 
394
  smem = cutlass.utils.SmemAllocator()
395
  storage = smem.allocate(SharedStorage)
396
  sLSE = storage.sLSE.get_tensor(smem_layout_lse)
397
+ sMaxValidSplit = storage.sMaxValidSplit.get_tensor((self.tile_m,))
398
  sO = storage.sO.get_tensor(smem_layout_o)
399
 
400
+ # Handle semaphore reset — wait for dependent grids first
401
  if const_expr(semaphore_to_reset is not None):
402
+ bidx, bidy, bidz = cute.arch.block_idx()
403
  if (
404
  tidx == 0
405
+ and bidx == cute.arch.grid_dim()[0] - 1
406
+ and bidy == cute.arch.grid_dim()[1] - 1
407
+ and bidz == cute.arch.grid_dim()[2] - 1
408
  ):
409
+ cute.arch.griddepcontrol_wait()
410
  semaphore_to_reset[0] = 0
411
 
412
+ if work_tile.is_valid_tile:
413
+ # Get number of splits (use maybe_virtual_batch for per-batch-slot splits)
414
+ num_splits = (
415
+ num_splits_dynamic_ptr[maybe_virtual_batch]
416
+ if const_expr(num_splits_dynamic_ptr is not None)
417
+ else mLSE_partial.shape[1]
418
+ )
419
+ # Handle variable length sequences using SeqlenInfo
420
+ seqlen_info = SeqlenInfo.create(
421
+ batch_idx=batch_idx,
422
+ seqlen_static=mO_partial.shape[0],
423
+ cu_seqlens=cu_seqlens,
424
+ seqused=seqused,
425
+ # Don't need to pass in tile size since we won't use offset_padded
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  )
427
+ seqlen, offset = seqlen_info.seqlen, seqlen_info.offset
428
 
429
+ # Extract number of heads (head index will be determined dynamically)
430
+ num_head = mO_partial.shape[3]
431
+ max_idx = seqlen * num_head
432
+
433
+ # TODO: early exit for single split if dynamic — for now always merge so the
434
+ # num_splits_dynamic == 1 case still writes mO from mO_partial[0].
435
+ if (const_expr(num_splits_dynamic_ptr is None) or num_splits > 0) and (
436
+ const_expr(not varlen) or m_block * self.tile_m < max_idx
437
+ ):
438
+ # Wait for dependent grids (e.g., the main attention kernel that produces O_partial/LSE_partial)
439
+ cute.arch.griddepcontrol_wait()
440
+
441
+ # ===============================
442
+ # Step 1: Load LSE_partial from gmem to shared memory
443
+ # ===============================
444
+
445
+ mLSE_partial_cur = seqlen_info.offset_batch(mLSE_partial, batch_idx, dim=3)
446
+ mLSE_partial_copy = cute.tiled_divide(mLSE_partial_cur, (1,))
447
+ gmem_thr_copy_LSE = gmem_tiled_copy_LSE.get_slice(tidx)
448
+ tLSEsLSE = gmem_thr_copy_LSE.partition_D(sLSE)
449
+ # Create identity tensor for coordinate tracking
450
+ cLSE = cute.make_identity_tensor((self.max_splits, self.tile_m))
451
+ tLSEcLSE = gmem_thr_copy_LSE.partition_S(cLSE)
452
+
453
+ # Load LSE partial values
454
+ for m in cutlass.range(cute.size(tLSEcLSE, mode=[2]), unroll_full=True):
455
+ mi = tLSEcLSE[0, 0, m][1] # Get m coordinate
456
+ idx = m_block * self.tile_m + mi
457
+ if idx < max_idx:
458
+ # Calculate actual sequence position and head using FastDivmodDivisor
459
+ if const_expr(not varlen):
460
+ head_idx, m_idx = divmod(idx, seqlen_divmod)
461
+ else:
462
+ head_idx = idx // seqlen
463
+ m_idx = idx - head_idx * seqlen
464
+ mLSE_partial_cur_copy = mLSE_partial_copy[None, m_idx, None, head_idx]
465
+ for s in cutlass.range(cute.size(tLSEcLSE, mode=[1]), unroll_full=True):
466
+ si = tLSEcLSE[0, s, 0][0] # Get split coordinate
467
+ if si < num_splits:
468
+ cute.copy(
469
+ gmem_thr_copy_LSE,
470
+ mLSE_partial_cur_copy[None, si],
471
+ tLSEsLSE[None, s, m],
472
+ )
473
+ else:
474
+ tLSEsLSE[None, s, m].fill(-Float32.inf)
475
+ # Don't need to zero out the rest of the LSEs, as we will not write the output to gmem
476
  cute.arch.cp_async_commit_group()
477
 
478
+ # ===============================
479
+ # Step 2: Load O_partial for pipeline stages
480
+ # ===============================
481
+
482
+ gmem_thr_copy_O_partial = gmem_tiled_copy_O_partial.get_slice(tidx)
483
+ cO = cute.make_identity_tensor((self.tile_m, self.k_block_size))
484
+ tOcO = gmem_thr_copy_O_partial.partition_D(cO)
485
+ tOsO_partial = gmem_thr_copy_O_partial.partition_D(sO)
486
+ mO_partial_cur = seqlen_info.offset_batch(mO_partial, batch_idx, dim=4)
487
+
488
+ # Precompute these values to avoid recomputing them in the loop
489
+ num_rows = const_expr(cute.size(tOcO, mode=[1]))
490
+ tOmidx = cute.make_rmem_tensor(num_rows, cutlass.Int32)
491
+ tOhidx = cute.make_rmem_tensor(num_rows, cutlass.Int32)
492
+ tOrOptr = cute.make_rmem_tensor(num_rows, cutlass.Int64)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493
  for m in cutlass.range(num_rows, unroll_full=True):
494
+ mi = tOcO[0, m, 0][0] # m coordinate
495
+ idx = m_block * self.tile_m + mi
496
+ if const_expr(not varlen):
497
+ tOhidx[m], tOmidx[m] = divmod(idx, seqlen_divmod)
498
+ else:
499
+ tOhidx[m] = idx // seqlen
500
+ tOmidx[m] = idx - tOhidx[m] * seqlen
501
+ tOrOptr[m] = utils.elem_pointer(
502
+ mO_partial_cur, (tOmidx[m], k_block * self.k_block_size, 0, tOhidx[m])
503
+ ).toint()
504
+ if idx >= max_idx:
505
+ tOhidx[m] = -1
506
+
507
+ tOpO = None
508
+ if const_expr(not self.is_even_k):
509
+ tOpO = cute.make_rmem_tensor(cute.size(tOcO, mode=[2]), Boolean)
510
+ for k in cutlass.range(cute.size(tOpO), unroll_full=True):
511
+ tOpO[k] = (
512
+ tOcO[0, 0, k][1] < mO_partial.shape[1] - k_block * self.k_block_size
513
+ )
514
+ # if cute.arch.thread_idx()[0] == 0 and k_block == 1: cute.print_tensor(tOpO)
515
+
516
+ load_O_partial = partial(
517
+ self.load_O_partial,
518
+ gmem_tiled_copy_O_partial,
519
+ tOrOptr,
520
+ tOsO_partial,
521
+ tOhidx,
522
+ tOpO,
523
+ tOcO,
524
+ mO_partial_cur.layout,
525
+ )
526
 
527
+ # Load first few stages of O_partial
528
+ for stage in cutlass.range(self.stages - 1, unroll_full=True):
529
+ if stage < num_splits:
530
+ load_O_partial(stage, stage)
531
+ cute.arch.cp_async_commit_group()
532
+
533
+ # ===============================
534
+ # Step 3: Load and transpose LSE from smem to registers
535
+ # ===============================
536
 
537
+ # Wait for LSE and initial O partial stages to complete
538
  cute.arch.cp_async_wait_group(self.stages - 1)
539
+ cute.arch.sync_threads()
540
+ # if cute.arch.thread_idx()[0] == 0:
541
+ # # cute.print_tensor(sLSE)
542
+ # for i in range(64):
543
+ # cute.printf("sLSE[%d, 0] = %f", i, sLSE[i, 0])
544
+ # cute.arch.sync_threads()
545
+
546
+ s2r_thr_copy_LSE = s2r_tiled_copy_LSE.get_slice(tidx)
547
+ ts2rsLSE = s2r_thr_copy_LSE.partition_S(sLSE)
548
+ ts2rrLSE = cute.make_rmem_tensor_like(ts2rsLSE)
549
+ cute.copy(s2r_tiled_copy_LSE, ts2rsLSE, ts2rrLSE)
550
+
551
+ # ===============================
552
+ # Step 4: Compute final LSE along split dimension
553
+ # ===============================
554
+
555
+ lse_sum = cute.make_rmem_tensor(cute.size(ts2rrLSE, mode=[2]), Float32)
556
+ ts2rcLSE = s2r_thr_copy_LSE.partition_D(cLSE)
557
+ # We compute the max valid split for each row to short-circuit the computation later
558
+ max_valid_split = cute.make_rmem_tensor(cute.size(ts2rrLSE, mode=[2]), Int32)
559
+ assert cute.size(ts2rrLSE, mode=[0]) == 1
560
+ # Compute max, scales, and final LSE for each row
561
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
562
+ # Find max LSE value across splits
563
+ threads_per_col = const_expr(self.smem_threads_per_col_lse)
564
+ lse_max = cute.arch.warp_reduction_max(
565
+ ts2rrLSE[None, None, m]
566
+ .load()
567
+ .reduce(cute.ReductionOp.MAX, init_val=-Float32.inf, reduction_profile=0),
568
+ threads_in_group=threads_per_col,
569
+ )
570
+ # if cute.arch.thread_idx()[0] == 0: cute.printf(lse_max)
571
+ # Find max valid split index
572
+ max_valid_idx = -1
573
+ for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
574
+ if ts2rrLSE[0, s, m] != -Float32.inf:
575
+ max_valid_idx = ts2rcLSE[0, s, 0][0] # Get split coordinate
576
+ # if cute.arch.thread_idx()[0] < 32: cute.printf(max_valid_idx)
577
+ max_valid_split[m] = cute.arch.warp_reduction_max(
578
+ max_valid_idx, threads_in_group=threads_per_col
579
+ )
580
+ # Compute exp scales and sum
581
+ lse_max_cur = (
582
+ 0.0 if lse_max == -Float32.inf else lse_max
583
+ ) # In case all local LSEs are -inf
584
+ LOG2_E = math.log2(math.e)
585
+ lse_sum_cur = 0.0
586
+ for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
587
+ scale = cute.math.exp2(
588
+ ts2rrLSE[0, s, m] * LOG2_E - (lse_max_cur * LOG2_E), fastmath=True
589
+ )
590
+ lse_sum_cur += scale
591
+ ts2rrLSE[0, s, m] = scale # Store scale for later use
592
+ lse_sum_cur = cute.arch.warp_reduction_sum(
593
+ lse_sum_cur, threads_in_group=threads_per_col
594
+ )
595
+ lse_sum[m] = cute.math.log(lse_sum_cur, fastmath=True) + lse_max
596
+ # Normalize scales
597
+ inv_sum = (
598
+ 0.0
599
+ if (lse_sum_cur == 0.0 or lse_sum_cur != lse_sum_cur)
600
+ else 1.0 / lse_sum_cur
601
+ )
602
+ ts2rrLSE[None, None, m].store(ts2rrLSE[None, None, m].load() * inv_sum)
603
+ # Store the scales exp(lse - lse_logsum) back to smem
604
+ cute.copy(s2r_tiled_copy_LSE, ts2rrLSE, ts2rsLSE)
605
+
606
+ # Store max valid split to smem
607
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
608
+ if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
609
+ mi = ts2rcLSE[0, 0, m][1]
610
+ if mi < self.tile_m:
611
+ sMaxValidSplit[mi] = max_valid_split[m]
612
+
613
+ # ===============================
614
+ # Step 5: Store final LSE to gmem
615
+ # ===============================
616
+
617
+ if const_expr(mLSE is not None):
618
+ if const_expr(cu_seqlens is None):
619
+ mLSE_cur = mLSE[None, None, batch_idx]
620
+ else:
621
+ mLSE_cur = cute.domain_offset((offset, 0), mLSE)
622
+ if k_block == 0: # Only first k_block writes LSE when mLSE is provided
623
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
624
+ if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
625
+ mi = ts2rcLSE[0, 0, m][1]
626
+ idx = m_block * self.tile_m + mi
627
+ if idx < max_idx:
628
+ if const_expr(not varlen):
629
+ head_idx, m_idx = divmod(idx, seqlen_divmod)
630
+ else:
631
+ head_idx = idx // seqlen
632
+ m_idx = idx - head_idx * seqlen
633
+ mLSE_cur[m_idx, head_idx] = lse_sum[m]
634
+
635
+ # ===============================
636
+ # Step 6: Read O_partial and accumulate final O
637
+ # ===============================
638
+
639
+ cute.arch.sync_threads()
640
+
641
+ # Get max valid split for this thread
642
+ thr_max_valid_split = sMaxValidSplit[tOcO[0, 0, 0][0]]
643
+ for m in cutlass.range(1, cute.size(tOcO, mode=[1]), unroll_full=True):
644
+ thr_max_valid_split = max(thr_max_valid_split, sMaxValidSplit[tOcO[0, m, 0][0]])
645
+
646
+ tOrO_partial = cute.make_rmem_tensor_like(tOsO_partial[None, None, None, 0])
647
+ tOrO = cute.make_rmem_tensor_like(tOrO_partial, Float32)
648
+ tOrO.fill(0.0)
649
+
650
+ stage_load = self.stages - 1
651
+ stage_compute = 0
652
+
653
+ # Main accumulation loop
654
+ for s in cutlass.range(thr_max_valid_split + 1, unroll=4):
655
+ # Get scales for this split
656
+ scale = cute.make_rmem_tensor(num_rows, Float32)
657
+ for m in cutlass.range(num_rows, unroll_full=True):
658
+ scale[m] = sLSE[s, tOcO[0, m, 0][0]] # Get scale from smem
659
+
660
+ # Load next stage if needed
661
+ split_to_load = s + self.stages - 1
662
+ if split_to_load <= thr_max_valid_split:
663
+ load_O_partial(split_to_load, stage_load)
664
+ cute.arch.cp_async_commit_group()
665
+ stage_load = 0 if stage_load == self.stages - 1 else stage_load + 1
666
+
667
+ # Wait for the current stage to be ready
668
+ cute.arch.cp_async_wait_group(self.stages - 1)
669
+ # We don't need __syncthreads() because each thread is just reading its own data from smem
670
+ # Copy from smem to registers
671
+ cute.autovec_copy(tOsO_partial[None, None, None, stage_compute], tOrO_partial)
672
+ stage_compute = 0 if stage_compute == self.stages - 1 else stage_compute + 1
673
+
674
+ # Accumulate scaled partial results
675
+ for m in cutlass.range(num_rows, unroll_full=True):
676
+ if tOhidx[m] >= 0 and scale[m] > 0.0:
677
+ tOrO[None, m, None].store(
678
+ tOrO[None, m, None].load()
679
+ + scale[m] * tOrO_partial[None, m, None].load().to(Float32)
680
+ )
681
 
682
+ # ===============================
683
+ # Step 7: Write final O to gmem
684
+ # ===============================
685
+
686
+ rO = cute.make_rmem_tensor_like(tOrO, self.dtype)
687
+ rO.store(tOrO.load().to(self.dtype))
688
+ if const_expr(cu_seqlens is None):
689
+ mO_cur = mO[None, None, None, batch_idx]
690
+ else:
691
+ mO_cur = cute.domain_offset((offset, 0, 0), mO)
692
+ mO_cur = utils.domain_offset_aligned((0, k_block * self.k_block_size, 0), mO_cur)
693
+ elems_per_store = const_expr(cute.size(gmem_tiled_copy_O.layout_tv_tiled[1]))
694
+ # mO_cur_copy = cute.tiled_divide(mO_cur, (1, elems_per_store,))
695
+ gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
696
+ # Write final results
697
  for m in cutlass.range(num_rows, unroll_full=True):
698
+ if tOhidx[m] >= 0:
699
+ mO_cur_copy = cute.tiled_divide(
700
+ mO_cur[tOmidx[m], None, tOhidx[m]], (elems_per_store,)
 
701
  )
702
+ for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
703
+ k_idx = tOcO[0, 0, k][1] // elems_per_store
704
+ if const_expr(self.is_even_k) or tOpO[k]:
705
+ cute.copy(gmem_thr_copy_O, rO[None, m, k], mO_cur_copy[None, k_idx])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
 
707
  @cute.jit
708
  def load_O_partial(
 
711
  tOrOptr: cute.Tensor,
712
  tOsO_partial: cute.Tensor,
713
  tOhidx: cute.Tensor,
714
+ tOpO: Optional[cute.Tensor],
715
  tOcO: cute.Tensor,
716
  mO_cur_partial_layout: cute.Layout,
717
  split: Int32,
 
730
  mO_partial_cur_copy = cute.tiled_divide(mO_partial_cur, (elems_per_load,))
731
  for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
732
  k_idx = tOcO[0, 0, k][1] // elems_per_load
733
+ if const_expr(tOpO is None) or tOpO[k]:
734
  cute.copy(
735
  gmem_tiled_copy_O_partial,
736
  mO_partial_cur_copy[None, k_idx, split],
build/torch-cuda/flash_fwd_mla_sm100.py ADDED
The diff for this file is too large to render. See raw diff
 
build/torch-cuda/flash_fwd_sm100.py CHANGED
The diff for this file is too large to render. See raw diff
 
build/torch-cuda/flash_fwd_sm120.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # SM120 (Blackwell GeForce / DGX Spark) forward pass.
3
+ #
4
+ # SM120 uses the same SM80-era MMA instructions (mma.sync.aligned.m16n8k16) but has
5
+ # a smaller shared memory capacity (99 KB vs 163 KB on SM80). This module subclasses
6
+ # FlashAttentionForwardSm80 and overrides the SMEM capacity check accordingly.
7
+
8
+ import cutlass
9
+ import cutlass.utils as utils_basic
10
+ from cutlass.base_dsl.arch import Arch
11
+
12
+ from .flash_fwd import FlashAttentionForwardSm80
13
+
14
+
15
+ class FlashAttentionForwardSm120(FlashAttentionForwardSm80):
16
+ def __init__(self, *args, **kwargs):
17
+ """Force SM80 code paths while the DSL still targets the resident SM120 GPU."""
18
+ super().__init__(*args, **kwargs)
19
+ self.arch = Arch.sm_80
20
+
21
+ @staticmethod
22
+ def can_implement(
23
+ dtype,
24
+ head_dim,
25
+ head_dim_v,
26
+ tile_m,
27
+ tile_n,
28
+ num_stages,
29
+ num_threads,
30
+ is_causal,
31
+ Q_in_regs=False,
32
+ ) -> bool:
33
+ """Check if the kernel can be implemented on SM120.
34
+
35
+ Same logic as SM80 but uses SM120's shared memory capacity (99 KB).
36
+ """
37
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
38
+ return False
39
+ if head_dim % 8 != 0:
40
+ return False
41
+ if head_dim_v % 8 != 0:
42
+ return False
43
+ if tile_n % 16 != 0:
44
+ return False
45
+ if num_threads % 32 != 0:
46
+ return False
47
+ # Shared memory usage: Q tile + (K tile + V tile)
48
+ smem_usage_Q = tile_m * head_dim * 2
49
+ smem_usage_K = tile_n * head_dim * num_stages * 2
50
+ smem_usage_V = tile_n * head_dim_v * num_stages * 2
51
+ smem_usage_QV = (
52
+ (smem_usage_Q + smem_usage_V) if not Q_in_regs else max(smem_usage_Q, smem_usage_V)
53
+ )
54
+ smem_usage = smem_usage_QV + smem_usage_K
55
+ # SM120 has 99 KB shared memory (vs 163 KB on SM80)
56
+ smem_capacity = utils_basic.get_smem_capacity_in_bytes("sm_120")
57
+ if smem_usage > smem_capacity:
58
+ return False
59
+ if (tile_m * 2) % num_threads != 0:
60
+ return False
61
+ return True
build/torch-cuda/flash_fwd_sm90.py ADDED
@@ -0,0 +1,1550 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # SM90 (Hopper) forward pass for flash attention, extracted from flash_fwd.py.
3
+
4
+ from types import SimpleNamespace
5
+ from typing import Callable, Literal, Optional
6
+ from functools import partial
7
+
8
+ import cuda.bindings.driver as cuda
9
+
10
+ import cutlass
11
+ import cutlass.cute as cute
12
+ from cutlass import Float32, Int32, const_expr
13
+ from cutlass.cute.nvgpu import cpasync, warpgroup
14
+ from cutlass.utils import LayoutEnum
15
+ import cutlass.utils.hopper_helpers as sm90_utils_basic
16
+ from cutlass import pipeline
17
+ from cutlass.pipeline import pipeline_init_arrive, pipeline_init_wait
18
+ from cutlass.base_dsl.arch import Arch
19
+
20
+ from .quack import copy_utils
21
+ from .quack import layout_utils
22
+ from .quack import sm90_utils
23
+
24
+ from .cute_dsl_utils import assume_tensor_aligned
25
+ from . import utils
26
+ from .mask import AttentionMask
27
+ from .softmax import Softmax, apply_score_mod_inner
28
+ from .seqlen_info import SeqlenInfoQK
29
+ from .block_info import BlockInfo
30
+ from .block_sparsity import BlockSparseTensors
31
+ from .block_sparse_utils import (
32
+ produce_block_sparse_loads,
33
+ consume_block_sparse_loads,
34
+ )
35
+ from . import pipeline as pipeline_custom
36
+ from .pack_gqa import PackGQA, pack_gqa_layout, make_packgqa_tiled_tma_atom
37
+ from .paged_kv import PagedKVManager
38
+ from .named_barrier import NamedBarrierFwd
39
+ from .quack.cute_dsl_utils import ParamsBase
40
+ from .tile_scheduler import (
41
+ TileSchedulerArguments,
42
+ SingleTileScheduler,
43
+ SingleTileLPTScheduler,
44
+ SingleTileVarlenScheduler,
45
+ )
46
+ from cutlass.cute import FastDivmodDivisor
47
+
48
+ from .flash_fwd import FlashAttentionForwardBase
49
+ from .utils import AuxData
50
+
51
+
52
+ class FlashAttentionForwardSm90(FlashAttentionForwardBase):
53
+ def __init__(
54
+ self,
55
+ *args,
56
+ intra_wg_overlap: bool = True,
57
+ mma_pv_is_rs: bool = True,
58
+ paged_kv_non_tma: bool = False,
59
+ **kwargs,
60
+ ):
61
+ super().__init__(*args, **kwargs)
62
+ self.intra_wg_overlap = intra_wg_overlap
63
+ self.mma_pv_is_rs = mma_pv_is_rs
64
+ self.buffer_align_bytes = 1024
65
+ self.use_tma_KV = not paged_kv_non_tma
66
+ assert self.use_tma_KV or not (self.check_hdim_oob or self.check_hdim_v_oob), (
67
+ "Paged KV does not support irregular head dim"
68
+ )
69
+ self.cluster_shape_mn = (1, 1)
70
+ assert self.arch.is_family_of(Arch.sm_90a), "Only SM 9.x is supported"
71
+
72
+ def _get_smem_layout_atom(self):
73
+ sQ_layout_atom = warpgroup.make_smem_layout_atom(
74
+ sm90_utils_basic.get_smem_layout_atom(LayoutEnum.ROW_MAJOR, self.dtype, self.tile_hdim),
75
+ self.dtype,
76
+ )
77
+ sK_layout_atom = sQ_layout_atom
78
+ sV_layout_atom = warpgroup.make_smem_layout_atom(
79
+ sm90_utils_basic.get_smem_layout_atom(
80
+ LayoutEnum.ROW_MAJOR, self.dtype, self.tile_hdimv
81
+ ),
82
+ self.dtype,
83
+ )
84
+ sO_layout_atom = sV_layout_atom
85
+ if not self.mma_pv_is_rs:
86
+ sP_layout_atom = warpgroup.make_smem_layout_atom(
87
+ sm90_utils_basic.get_smem_layout_atom(
88
+ LayoutEnum.ROW_MAJOR, self.dtype, self.tile_n
89
+ ),
90
+ self.dtype,
91
+ )
92
+ else:
93
+ sP_layout_atom = None
94
+ return sQ_layout_atom, sK_layout_atom, sV_layout_atom, sO_layout_atom, sP_layout_atom
95
+
96
+ def _get_tiled_mma(self):
97
+ tiled_mma_qk = sm90_utils_basic.make_trivial_tiled_mma(
98
+ self.dtype,
99
+ self.dtype,
100
+ warpgroup.OperandMajorMode.K,
101
+ warpgroup.OperandMajorMode.K,
102
+ Float32,
103
+ atom_layout_mnk=(self.tile_m // 64, 1, 1),
104
+ tiler_mn=(64, self.tile_n),
105
+ )
106
+ tiled_mma_pv = sm90_utils_basic.make_trivial_tiled_mma(
107
+ self.dtype,
108
+ self.dtype,
109
+ warpgroup.OperandMajorMode.K,
110
+ warpgroup.OperandMajorMode.MN,
111
+ Float32,
112
+ atom_layout_mnk=(self.tile_m // 64, 1, 1), # Might need (1, 2, 1) for hdim 512
113
+ tiler_mn=(64, self.tile_hdimv),
114
+ a_source=warpgroup.OperandSource.RMEM
115
+ if self.mma_pv_is_rs
116
+ else warpgroup.OperandSource.SMEM,
117
+ )
118
+ return tiled_mma_qk, tiled_mma_pv
119
+
120
+ def _get_shared_storage_cls(self):
121
+ sQ_struct, sK_struct, sV_struct = [
122
+ cute.struct.Align[
123
+ cute.struct.MemRange[self.dtype, cute.cosize(layout)], self.buffer_align_bytes
124
+ ]
125
+ for layout in (self.sQ_layout, self.sK_layout, self.sV_layout)
126
+ ]
127
+ cosize_sQV = max(cute.cosize(self.sQ_layout), cute.cosize(self.sV_layout))
128
+ sQV_struct = cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sQV], 1024]
129
+ cosize_sP = cute.cosize(self.sP_layout) if const_expr(self.sP_layout is not None) else 0
130
+ sP_struct = cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sP], 1024]
131
+ # 1 stage * 2 for Q pipeline (full + empty), self.num_stages*2 for K, self.num_stages*2 for V,
132
+ mbar_ptr_Q_struct = cute.struct.MemRange[cutlass.Int64, 1 * 2]
133
+ mbar_ptr_K_struct = cute.struct.MemRange[cutlass.Int64, self.num_stages * 2]
134
+ mbar_ptr_V_struct = cute.struct.MemRange[cutlass.Int64, self.num_stages * 2]
135
+
136
+ @cute.struct
137
+ class SharedStorageQKV:
138
+ mbar_ptr_Q: mbar_ptr_Q_struct
139
+ mbar_ptr_K: mbar_ptr_K_struct
140
+ mbar_ptr_V: mbar_ptr_V_struct
141
+ sV: sV_struct
142
+ sQ: sQ_struct
143
+ sK: sK_struct
144
+ sP: sP_struct
145
+
146
+ @cute.struct
147
+ class SharedStorageSharedQV:
148
+ mbar_ptr_Q: mbar_ptr_Q_struct
149
+ mbar_ptr_K: mbar_ptr_K_struct
150
+ mbar_ptr_V: mbar_ptr_V_struct
151
+ sQ: sQV_struct
152
+ sK: sK_struct
153
+ sP: sP_struct
154
+
155
+ return SharedStorageQKV if const_expr(not self.Q_in_regs) else SharedStorageSharedQV
156
+
157
+ @cute.jit
158
+ def __call__(
159
+ self,
160
+ mQ: cute.Tensor, # (b, s_q, h, d) or (total_q, h, d) if there is cu_seqlens_q
161
+ mK: cute.Tensor, # (b_k, s_k, h_k, d) or (total_k, h_k, d) if there is cu_seqlens_k or (num_pages, page_size, h_k, d) if there is page_table
162
+ mV: cute.Tensor, # (b_k, s_k, h_k, dv) or (total_k, h_k, dv) if there is cu_seqlens_k or (num_pages, page_size, h_k, dv) if there is page_table
163
+ mO: cute.Tensor, # (b, s_q, h, dv) or (total_q, h, dv) if there is cu_seqlens_q
164
+ mLSE: Optional[cute.Tensor],
165
+ softmax_scale: Float32,
166
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
167
+ mCuSeqlensK: Optional[cute.Tensor] = None,
168
+ mSeqUsedQ: Optional[cute.Tensor] = None,
169
+ mSeqUsedK: Optional[cute.Tensor] = None,
170
+ mPageTable: Optional[cute.Tensor] = None, # (b_k, max_num_pages_per_seq)
171
+ window_size_left: Int32 | int | None = None,
172
+ window_size_right: Int32 | int | None = None,
173
+ learnable_sink: Optional[cute.Tensor] = None,
174
+ blocksparse_tensors: Optional[BlockSparseTensors] = None,
175
+ aux_data: AuxData = AuxData(),
176
+ mCuTotalMBlocks: Optional[cute.Tensor] = None,
177
+ mCuTotalSplitsMBlocks: Optional[cute.Tensor] = None,
178
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
179
+ stream: cuda.CUstream = None,
180
+ ):
181
+ """Configures and launches the flash attention kernel.
182
+
183
+ mQ/mK/mV/mO has same data types(supports fp16 and bf16) and same layout:
184
+ (batch_size, seqlen_q, num_head, head_dim):(_, _, _, 1)
185
+ """
186
+
187
+ self._check_type(
188
+ *(
189
+ t.element_type if t is not None else None
190
+ for t in (mQ, mK, mV, mO, mLSE, mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK)
191
+ )
192
+ )
193
+
194
+ self.varlen_q = mCuSeqlensQ is not None or mSeqUsedQ is not None
195
+
196
+ mQ, mK, mV, mO = [assume_tensor_aligned(t) for t in (mQ, mK, mV, mO)]
197
+ QO_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 2, 1]
198
+ mQ, mO = [layout_utils.select(t, QO_layout_transpose) for t in (mQ, mO)]
199
+ KV_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensK is None) else [0, 2, 1]
200
+ mK, mV = [layout_utils.select(t, KV_layout_transpose) for t in (mK, mV)]
201
+ LSE_layout_transpose = [2, 1, 0] if const_expr(mCuSeqlensQ is None) else [1, 0]
202
+ mLSE = (
203
+ layout_utils.select(mLSE, LSE_layout_transpose)
204
+ if const_expr(mLSE is not None)
205
+ else None
206
+ )
207
+
208
+ tiled_mma_qk, tiled_mma_pv = self._get_tiled_mma()
209
+ self.num_mma_threads = tiled_mma_qk.size
210
+ self.num_threads_per_warp_group = 128
211
+ self.num_wg_mma = self.num_mma_threads // self.num_threads_per_warp_group
212
+ assert self.num_wg_mma in [1, 2, 3]
213
+ self.num_threads = self.num_threads_per_warp_group * (self.num_wg_mma + 1)
214
+ self.num_producer_threads = 32
215
+ self.num_Q_load_threads = self.num_threads_per_warp_group # If not TMA_Q
216
+ self.num_epilogue_threads = self.num_mma_threads
217
+ self.num_mma_regs, self.num_producer_regs = {1: (256, 56), 2: (240, 24), 3: (160, 32)}[
218
+ self.num_wg_mma
219
+ ]
220
+ self.use_block_sparsity = cutlass.const_expr(blocksparse_tensors is not None)
221
+
222
+ self.use_scheduler_barrier = (
223
+ (self.num_wg_mma >= 2 and self.tile_hdim <= 128)
224
+ if const_expr(self.intra_wg_overlap)
225
+ else (self.num_wg_mma == 2)
226
+ )
227
+ self.use_tma_Q = self.arch >= Arch.sm_90 and not (
228
+ self.pack_gqa and self.tile_m % self.qhead_per_kvhead != 0
229
+ )
230
+ self.use_tma_O = self.use_tma_Q
231
+ # Producer needs more registers when doing cp.async Q or KV loads
232
+ if const_expr(self.num_wg_mma == 2 and (not self.use_tma_Q or not self.use_tma_KV)):
233
+ self.num_mma_regs, self.num_producer_regs = 224, 40
234
+ self.rescale_O_before_gemm = self.tile_hdimv > 128 and self.intra_wg_overlap
235
+ self._setup_attributes()
236
+ # TODO: we prob don't need most of what's in _setup_attributes
237
+ self.sQ_layout, self.sK_layout, self.sV_layout, self.sO_layout = [
238
+ sm90_utils.make_smem_layout(mX.element_type, LayoutEnum.ROW_MAJOR, shape, stage)
239
+ for mX, shape, stage in [
240
+ (mQ, (self.tile_m, self.tile_hdim), None),
241
+ (mK, (self.tile_n, self.tile_hdim), self.num_stages),
242
+ (mV, (self.tile_n, self.tile_hdimv), self.num_stages),
243
+ (mO, (self.tile_m, self.tile_hdimv), None),
244
+ ]
245
+ ]
246
+ self.sP_layout = None
247
+ if const_expr(not self.mma_pv_is_rs):
248
+ self.sP_layout = sm90_utils.make_smem_layout(
249
+ mV.element_type, LayoutEnum.ROW_MAJOR, (self.tile_m, self.tile_n)
250
+ )
251
+
252
+ SharedStorage = self._get_shared_storage_cls()
253
+
254
+ mQ_og, mO_og = mQ, mO
255
+ if const_expr(self.pack_gqa):
256
+ nheads_kv = mK.shape[2]
257
+ mQ = pack_gqa_layout(mQ, self.qhead_per_kvhead, nheads_kv, head_idx=2)
258
+ mO = pack_gqa_layout(mO, self.qhead_per_kvhead, nheads_kv, head_idx=2)
259
+ if const_expr(mLSE is not None):
260
+ mLSE = pack_gqa_layout(mLSE, self.qhead_per_kvhead, nheads_kv, head_idx=1)
261
+
262
+ # TMA
263
+ gmem_tiled_copy_Q = cpasync.CopyBulkTensorTileG2SOp()
264
+ gmem_tiled_copy_KV = cpasync.CopyBulkTensorTileG2SOp() # Might multicast
265
+ gmem_tiled_copy_O = cpasync.CopyBulkTensorTileS2GOp()
266
+ self.tma_copy_bytes = {
267
+ name: cute.size_in_bytes(mX.element_type, cute.select(layout, mode=[0, 1]))
268
+ for name, mX, layout in [
269
+ ("Q", mQ, self.sQ_layout),
270
+ ("K", mK, self.sK_layout),
271
+ ("V", mV, self.sV_layout),
272
+ ]
273
+ }
274
+ make_tiled_tma_atom_fn = (
275
+ partial(make_packgqa_tiled_tma_atom, qhead_per_kvhead=self.qhead_per_kvhead, head_idx=2)
276
+ if const_expr(self.pack_gqa)
277
+ else cpasync.make_tiled_tma_atom
278
+ )
279
+ tma_atom_Q, tma_tensor_Q = None, None
280
+ if const_expr(self.use_tma_Q):
281
+ tma_atom_Q, tma_tensor_Q = make_tiled_tma_atom_fn(
282
+ gmem_tiled_copy_Q,
283
+ mQ_og if const_expr(self.pack_gqa) else mQ,
284
+ self.sQ_layout,
285
+ (self.tile_m, self.tile_hdim), # No mcast
286
+ )
287
+ tma_atom_K, tma_tensor_K = None, None
288
+ tma_atom_V, tma_tensor_V = None, None
289
+ if const_expr(self.use_tma_KV):
290
+ tma_atom_K, tma_tensor_K = cpasync.make_tiled_tma_atom(
291
+ gmem_tiled_copy_KV,
292
+ mK,
293
+ cute.select(self.sK_layout, mode=[0, 1]),
294
+ (self.tile_n, self.tile_hdim),
295
+ 1, # No mcast for now
296
+ )
297
+ tma_atom_V, tma_tensor_V = cpasync.make_tiled_tma_atom(
298
+ gmem_tiled_copy_KV,
299
+ mV,
300
+ cute.select(self.sV_layout, mode=[0, 1]),
301
+ (self.tile_n, self.tile_hdimv),
302
+ 1, # No mcast for now
303
+ )
304
+ tma_atom_O, tma_tensor_O = None, None
305
+ if const_expr(self.use_tma_O):
306
+ mO_tma = mO_og if const_expr(self.pack_gqa) else mO
307
+ if const_expr(self.varlen_q):
308
+ mO_tma = copy_utils.create_ragged_tensor_for_tma(
309
+ mO_tma, ragged_dim=0, ptr_shift=True
310
+ )
311
+ tma_atom_O, tma_tensor_O = make_tiled_tma_atom_fn(
312
+ gmem_tiled_copy_O,
313
+ mO_tma,
314
+ self.sO_layout,
315
+ (self.tile_m, self.tile_hdimv), # No mcast
316
+ )
317
+ if const_expr(mCuSeqlensQ is not None or mSeqUsedQ is not None):
318
+ # TODO: dispatch to DynamicPersistentVarlenScheduler when appropriate
319
+ TileScheduler = SingleTileVarlenScheduler
320
+ else:
321
+ TileScheduler = (
322
+ SingleTileScheduler
323
+ if const_expr(not self.is_causal or self.is_local)
324
+ else SingleTileLPTScheduler
325
+ )
326
+ tile_sched_args = TileSchedulerArguments(
327
+ cute.ceil_div(cute.size(mQ.shape[0]), self.tile_m),
328
+ cute.size(mQ.shape[2]),
329
+ cute.size(mQ.shape[3])
330
+ if const_expr(mCuSeqlensQ is None)
331
+ else cute.size(mCuSeqlensQ.shape[0] - 1),
332
+ 1, # num_splits
333
+ cute.size(mK.shape[0])
334
+ if const_expr(mPageTable is None)
335
+ else mK.shape[0] * mPageTable.shape[1],
336
+ mQ.shape[1],
337
+ mV.shape[1],
338
+ total_q=cute.size(mQ.shape[0])
339
+ if const_expr(mCuSeqlensQ is not None)
340
+ else cute.size(mQ.shape[0]) * cute.size(mQ.shape[3]),
341
+ tile_shape_mn=(self.tile_m, self.tile_n),
342
+ mCuSeqlensQ=mCuSeqlensQ,
343
+ mSeqUsedQ=mSeqUsedQ,
344
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
345
+ element_size=self.dtype.width // 8,
346
+ is_persistent=False,
347
+ lpt=self.is_causal or self.is_local,
348
+ cu_total_m_blocks_ptr=mCuTotalMBlocks,
349
+ cu_total_splits_m_blocks_ptr=mCuTotalSplitsMBlocks,
350
+ )
351
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
352
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
353
+ softmax_scale_log2, softmax_scale = utils.compute_softmax_scale_log2(
354
+ softmax_scale, self.score_mod
355
+ )
356
+ window_size_left = Int32(window_size_left) if window_size_left is not None else None
357
+ window_size_right = Int32(window_size_right) if window_size_right is not None else None
358
+ fastdiv_mods = utils.compute_fastdiv_mods(
359
+ mQ, mK, self.qhead_per_kvhead, self.pack_gqa, aux_data.tensors, mPageTable
360
+ )
361
+
362
+ self.kernel(
363
+ tma_tensor_Q if const_expr(self.use_tma_Q) else mQ,
364
+ tma_tensor_K if const_expr(self.use_tma_KV) else mK,
365
+ tma_tensor_V if const_expr(self.use_tma_KV) else mV,
366
+ tma_tensor_O if const_expr(self.use_tma_O) else mO,
367
+ mLSE,
368
+ mCuSeqlensQ,
369
+ mCuSeqlensK,
370
+ mSeqUsedQ,
371
+ mSeqUsedK,
372
+ mPageTable,
373
+ tma_atom_Q,
374
+ tma_atom_K,
375
+ tma_atom_V,
376
+ tma_atom_O,
377
+ softmax_scale_log2,
378
+ softmax_scale,
379
+ window_size_left,
380
+ window_size_right,
381
+ learnable_sink,
382
+ blocksparse_tensors,
383
+ self.sQ_layout,
384
+ self.sK_layout,
385
+ self.sV_layout,
386
+ self.sO_layout,
387
+ self.sP_layout,
388
+ self.gmem_tiled_copy_Q,
389
+ self.gmem_tiled_copy_K,
390
+ self.gmem_tiled_copy_V,
391
+ self.gmem_tiled_copy_O,
392
+ tiled_mma_qk,
393
+ tiled_mma_pv,
394
+ tile_sched_params,
395
+ TileScheduler,
396
+ SharedStorage,
397
+ aux_data,
398
+ fastdiv_mods,
399
+ ).launch(
400
+ grid=grid_dim,
401
+ block=[self.num_threads, 1, 1],
402
+ stream=stream,
403
+ min_blocks_per_mp=1,
404
+ )
405
+
406
+ @cute.kernel
407
+ def kernel(
408
+ self,
409
+ mQ: cute.Tensor,
410
+ mK: cute.Tensor,
411
+ mV: cute.Tensor,
412
+ mO: cute.Tensor,
413
+ mLSE: Optional[cute.Tensor],
414
+ mCuSeqlensQ: Optional[cute.Tensor],
415
+ mCuSeqlensK: Optional[cute.Tensor],
416
+ mSeqUsedQ: Optional[cute.Tensor],
417
+ mSeqUsedK: Optional[cute.Tensor],
418
+ mPageTable: Optional[cute.Tensor],
419
+ tma_atom_Q: Optional[cute.CopyAtom],
420
+ tma_atom_K: Optional[cute.CopyAtom],
421
+ tma_atom_V: Optional[cute.CopyAtom],
422
+ tma_atom_O: Optional[cute.CopyAtom],
423
+ softmax_scale_log2: Float32,
424
+ softmax_scale: Optional[Float32],
425
+ window_size_left: Optional[Int32],
426
+ window_size_right: Optional[Int32],
427
+ learnable_sink: Optional[cute.Tensor],
428
+ blocksparse_tensors: Optional[BlockSparseTensors],
429
+ sQ_layout: cute.ComposedLayout,
430
+ sK_layout: cute.ComposedLayout,
431
+ sV_layout: cute.ComposedLayout,
432
+ sO_layout: cute.ComposedLayout,
433
+ sP_layout: cute.ComposedLayout | None,
434
+ gmem_tiled_copy_Q: cute.TiledCopy,
435
+ gmem_tiled_copy_K: cute.TiledCopy,
436
+ gmem_tiled_copy_V: cute.TiledCopy,
437
+ gmem_tiled_copy_O: cute.TiledCopy,
438
+ tiled_mma_qk: cute.TiledMma,
439
+ tiled_mma_pv: cute.TiledMma,
440
+ tile_sched_params: ParamsBase,
441
+ TileScheduler: cutlass.Constexpr[Callable],
442
+ SharedStorage: cutlass.Constexpr[Callable],
443
+ aux_data: AuxData = AuxData(),
444
+ fastdiv_mods=None,
445
+ ):
446
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
447
+ # Prefetch tma descriptor
448
+ if warp_idx == 0:
449
+ for tma_atom in (tma_atom_Q, tma_atom_K, tma_atom_V, tma_atom_O):
450
+ if const_expr(tma_atom is not None):
451
+ cpasync.prefetch_descriptor(tma_atom)
452
+
453
+ smem = cutlass.utils.SmemAllocator()
454
+ storage = smem.allocate(SharedStorage)
455
+
456
+ # Mbarrier / pipeline init
457
+ mbar_ptr_Q = storage.mbar_ptr_Q.data_ptr()
458
+
459
+ ThreadCooperativeGroup = partial(pipeline.CooperativeGroup, pipeline.Agent.Thread)
460
+ tma_warp = ThreadCooperativeGroup(1)
461
+ load_threads = ThreadCooperativeGroup(self.num_threads_per_warp_group)
462
+ mma_warps = ThreadCooperativeGroup(self.num_mma_threads // cute.arch.WARP_SIZE)
463
+ if const_expr(self.use_tma_Q):
464
+ pipeline_q = pipeline_custom.PipelineTmaAsync.create(
465
+ barrier_storage=mbar_ptr_Q,
466
+ num_stages=1,
467
+ producer_group=tma_warp,
468
+ consumer_group=mma_warps,
469
+ tx_count=self.tma_copy_bytes["Q"],
470
+ defer_sync=True,
471
+ )
472
+ else:
473
+ pipeline_q = pipeline_custom.PipelineCpAsync.create(
474
+ barrier_storage=mbar_ptr_Q,
475
+ num_stages=1,
476
+ producer_group=load_threads,
477
+ consumer_group=mma_warps,
478
+ defer_sync=True,
479
+ elect_one_release=True,
480
+ syncwarp_before_release=False,
481
+ )
482
+
483
+ if const_expr(self.use_tma_KV):
484
+ pipeline_k = pipeline_custom.PipelineTmaAsync.create(
485
+ barrier_storage=storage.mbar_ptr_K.data_ptr(),
486
+ num_stages=self.num_stages,
487
+ producer_group=tma_warp,
488
+ consumer_group=mma_warps,
489
+ tx_count=self.tma_copy_bytes["K"],
490
+ defer_sync=True,
491
+ )
492
+ pipeline_v = pipeline_custom.PipelineTmaAsync.create(
493
+ barrier_storage=storage.mbar_ptr_V.data_ptr(),
494
+ num_stages=self.num_stages,
495
+ producer_group=tma_warp,
496
+ consumer_group=mma_warps,
497
+ tx_count=self.tma_copy_bytes["V"],
498
+ defer_sync=True,
499
+ )
500
+ else:
501
+ pipeline_k = pipeline_custom.PipelineCpAsync.create(
502
+ barrier_storage=storage.mbar_ptr_K.data_ptr(),
503
+ num_stages=self.num_stages,
504
+ producer_group=load_threads,
505
+ consumer_group=mma_warps,
506
+ defer_sync=True,
507
+ elect_one_release=True,
508
+ syncwarp_before_release=False,
509
+ )
510
+ pipeline_v = pipeline_custom.PipelineCpAsync.create(
511
+ barrier_storage=storage.mbar_ptr_V.data_ptr(),
512
+ num_stages=self.num_stages,
513
+ producer_group=load_threads,
514
+ consumer_group=mma_warps,
515
+ defer_sync=True,
516
+ elect_one_release=True,
517
+ syncwarp_before_release=False,
518
+ )
519
+
520
+ # Cluster arrive after barrier init
521
+ pipeline_init_arrive(cluster_shape_mn=self.cluster_shape_mn, is_relaxed=True)
522
+
523
+ # ///////////////////////////////////////////////////////////////////////////////
524
+ # Get shared memory buffer
525
+ # ///////////////////////////////////////////////////////////////////////////////
526
+ sQ = storage.sQ.get_tensor(sQ_layout.outer, swizzle=sQ_layout.inner)
527
+ sK = storage.sK.get_tensor(sK_layout.outer, swizzle=sK_layout.inner)
528
+ if const_expr(not self.Q_in_regs):
529
+ sV = storage.sV.get_tensor(sV_layout.outer, swizzle=sV_layout.inner)
530
+ else:
531
+ sV = storage.sQ.get_tensor(
532
+ sV_layout.outer, swizzle=sV_layout.inner, dtype=mV.element_type
533
+ )
534
+ # Transpose view of V to tensor with layout (head_dim_v, tile_n) for tiled mma
535
+ sVt = layout_utils.transpose_view(sV)
536
+ sP = None
537
+ if const_expr(sP_layout is not None):
538
+ sP = storage.sP.get_tensor(sP_layout.outer, swizzle=sP_layout.inner)
539
+ # reuse sQ's data iterator
540
+ sO = storage.sQ.get_tensor(sO_layout.outer, swizzle=sO_layout.inner, dtype=self.dtype)
541
+
542
+ block_info = BlockInfo(
543
+ self.tile_m,
544
+ self.tile_n,
545
+ self.is_causal,
546
+ self.is_local,
547
+ False, # is_split_kv
548
+ window_size_left,
549
+ window_size_right,
550
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
551
+ )
552
+ SeqlenInfoCls = partial(
553
+ SeqlenInfoQK.create,
554
+ seqlen_q_static=mQ.shape[0] if const_expr(not self.pack_gqa) else mQ.shape[0][1],
555
+ seqlen_k_static=mK.shape[0]
556
+ if const_expr(mPageTable is None)
557
+ else mK.shape[0] * mPageTable.shape[1],
558
+ mCuSeqlensQ=mCuSeqlensQ,
559
+ mCuSeqlensK=mCuSeqlensK,
560
+ mSeqUsedQ=mSeqUsedQ,
561
+ mSeqUsedK=mSeqUsedK,
562
+ mCuTotalMBlocks=(
563
+ blocksparse_tensors.cu_total_m_blocks if blocksparse_tensors is not None else None
564
+ ),
565
+ mCuBlockIdxOffsets=(
566
+ blocksparse_tensors.cu_block_idx_offsets
567
+ if blocksparse_tensors is not None
568
+ else None
569
+ ),
570
+ # Don't need to pass in tile_mn because we won't access offset_padded
571
+ )
572
+ AttentionMaskCls = partial(
573
+ AttentionMask,
574
+ self.tile_m,
575
+ self.tile_n,
576
+ window_size_left=window_size_left,
577
+ window_size_right=window_size_right,
578
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
579
+ )
580
+ TileSchedulerCls = partial(TileScheduler.create, tile_sched_params)
581
+
582
+ # Cluster wait before starting
583
+ pipeline_init_wait(cluster_shape_mn=self.cluster_shape_mn)
584
+
585
+ if warp_idx < 4: # Producer
586
+ cute.arch.setmaxregister_decrease(self.num_producer_regs)
587
+ self.load(
588
+ mQ,
589
+ mK,
590
+ mV,
591
+ sQ,
592
+ sK,
593
+ sV,
594
+ tma_atom_Q,
595
+ tma_atom_K,
596
+ tma_atom_V,
597
+ pipeline_k,
598
+ pipeline_v,
599
+ pipeline_q,
600
+ gmem_tiled_copy_Q,
601
+ mPageTable,
602
+ blocksparse_tensors,
603
+ block_info,
604
+ SeqlenInfoCls,
605
+ TileSchedulerCls,
606
+ )
607
+
608
+ else: # Consumer
609
+ cute.arch.setmaxregister_increase(self.num_mma_regs)
610
+ # ///////////////////////////////////////////////////////////////////////////////
611
+ # Tile MMA compute thread partitions and allocate accumulators
612
+ # ///////////////////////////////////////////////////////////////////////////////
613
+ tidx, _, _ = cute.arch.thread_idx()
614
+ tidx = tidx - 128
615
+ self.mma(
616
+ tiled_mma_qk,
617
+ tiled_mma_pv,
618
+ mO,
619
+ mLSE,
620
+ sQ,
621
+ sK,
622
+ sVt,
623
+ sP,
624
+ sO,
625
+ learnable_sink,
626
+ pipeline_k,
627
+ pipeline_v,
628
+ pipeline_q,
629
+ gmem_tiled_copy_O,
630
+ tma_atom_O,
631
+ tidx,
632
+ softmax_scale_log2,
633
+ softmax_scale,
634
+ block_info,
635
+ SeqlenInfoCls,
636
+ AttentionMaskCls,
637
+ TileSchedulerCls,
638
+ blocksparse_tensors,
639
+ aux_data,
640
+ fastdiv_mods,
641
+ )
642
+
643
+ @cute.jit
644
+ def load(
645
+ self,
646
+ mQ: cute.Tensor,
647
+ mK: cute.Tensor,
648
+ mV: cute.Tensor,
649
+ sQ: cute.Tensor,
650
+ sK: cute.Tensor,
651
+ sV: cute.Tensor,
652
+ tma_atom_Q: Optional[cute.CopyAtom],
653
+ tma_atom_K: Optional[cute.CopyAtom],
654
+ tma_atom_V: Optional[cute.CopyAtom],
655
+ pipeline_k: pipeline.PipelineAsync,
656
+ pipeline_v: pipeline.PipelineAsync,
657
+ pipeline_q: pipeline.PipelineAsync,
658
+ gmem_tiled_copy_Q: cute.TiledCopy,
659
+ mPageTable: Optional[cute.Tensor],
660
+ blocksparse_tensors: Optional[BlockSparseTensors],
661
+ block_info: BlockInfo,
662
+ SeqlenInfoCls: Callable,
663
+ TileSchedulerCls: Callable,
664
+ ):
665
+ warp_idx_in_wg = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % 4
666
+ tidx, _, _ = cute.arch.thread_idx()
667
+
668
+ # TMA: only warp 0 loads. cp_async: all warps load.
669
+ # When not use_tma_Q, all 128 producer threads participate in Q loading.
670
+ is_load_warp = warp_idx_in_wg == 0 or const_expr(not self.use_tma_KV or not self.use_tma_Q)
671
+ # KV loading restricted to warp 0 for TMA, all warps for non-TMA KV
672
+ is_kv_load_warp = warp_idx_in_wg == 0 or const_expr(not self.use_tma_KV)
673
+
674
+ if is_load_warp:
675
+ q_producer_phase = Int32(1)
676
+ kv_producer_state = pipeline.make_pipeline_state(
677
+ pipeline.PipelineUserType.Producer, self.num_stages
678
+ )
679
+ tile_scheduler = TileSchedulerCls()
680
+ work_tile = tile_scheduler.initial_work_tile_info()
681
+ while work_tile.is_valid_tile:
682
+ # if work_tile.is_valid_tile:
683
+ m_block, head_idx, batch_idx, _ = work_tile.tile_idx
684
+ seqlen = SeqlenInfoCls(batch_idx)
685
+ mQ_cur = seqlen.offset_batch_Q(mQ, batch_idx, dim=3)[None, None, head_idx]
686
+ head_idx_kv = (
687
+ head_idx // self.qhead_per_kvhead if const_expr(not self.pack_gqa) else head_idx
688
+ )
689
+
690
+ load_Q = None
691
+ if const_expr(self.use_tma_Q):
692
+ gQ = cute.local_tile(mQ_cur, (self.tile_m, self.tile_hdim), (m_block, 0))
693
+ load_Q, _, _ = copy_utils.tma_get_copy_fn(
694
+ tma_atom_Q, 0, cute.make_layout(1), gQ, sQ, single_stage=True
695
+ )
696
+
697
+ paged_kv_manager = None
698
+ tma_load_K_fn = None
699
+ tma_load_V_fn = None
700
+ if const_expr(self.use_tma_KV):
701
+ # === TMA path (non-paged and paged with page_size == n_block_size) ===
702
+ if const_expr(mPageTable is not None):
703
+ # Paged TMA: keep page dimension indexable
704
+ mK_cur = mK[None, None, head_idx_kv, None]
705
+ mV_cur = mV[None, None, head_idx_kv, None]
706
+ gK = cute.local_tile(mK_cur, (self.tile_n, self.tile_hdim), (0, 0, None))
707
+ gV = cute.local_tile(mV_cur, (self.tile_n, self.tile_hdimv), (0, 0, None))
708
+ else:
709
+ # Non-paged TMA
710
+ mK_cur = seqlen.offset_batch_K(mK, batch_idx, dim=3)[
711
+ None, None, head_idx_kv
712
+ ]
713
+ mV_cur = seqlen.offset_batch_K(mV, batch_idx, dim=3)[
714
+ None, None, head_idx_kv
715
+ ]
716
+ gK = cute.local_tile(mK_cur, (self.tile_n, self.tile_hdim), (None, 0))
717
+ gV = cute.local_tile(mV_cur, (self.tile_n, self.tile_hdimv), (None, 0))
718
+ # TODO: mcast
719
+ tma_load_K_fn, _, _ = copy_utils.tma_get_copy_fn(
720
+ tma_atom_K, 0, cute.make_layout(1), gK, sK
721
+ )
722
+ tma_load_K_fn = copy_utils.tma_producer_copy_fn(tma_load_K_fn, pipeline_k)
723
+ tma_load_V_fn, _, _ = copy_utils.tma_get_copy_fn(
724
+ tma_atom_V, 0, cute.make_layout(1), gV, sV
725
+ )
726
+ tma_load_V_fn = copy_utils.tma_producer_copy_fn(tma_load_V_fn, pipeline_v)
727
+ else:
728
+ # === cp_async path (paged KV with page_size != n_block_size) ===
729
+ paged_kv_manager = PagedKVManager.create(
730
+ mPageTable,
731
+ mK,
732
+ mV,
733
+ FastDivmodDivisor(mK.shape[0]),
734
+ batch_idx,
735
+ head_idx_kv,
736
+ tidx,
737
+ seqlen.seqlen_k,
738
+ 0, # leftpad_k
739
+ self.tile_n,
740
+ self.tile_hdim,
741
+ self.tile_hdimv,
742
+ self.num_threads_per_warp_group,
743
+ mK.element_type,
744
+ arch=self.arch.major * 10 + self.arch.minor,
745
+ )
746
+
747
+ load_K = partial(
748
+ self.load_KV,
749
+ tma_load_K_fn,
750
+ paged_kv_manager,
751
+ sK,
752
+ pipeline_kv=pipeline_k,
753
+ K_or_V="K",
754
+ )
755
+ load_V = partial(
756
+ self.load_KV,
757
+ tma_load_V_fn,
758
+ paged_kv_manager,
759
+ sV,
760
+ pipeline_kv=pipeline_v,
761
+ K_or_V="V",
762
+ )
763
+
764
+ pack_gqa = None
765
+ if const_expr(not self.use_tma_Q):
766
+ pack_gqa = PackGQA(
767
+ self.tile_m, self.tile_hdim, self.check_hdim_oob, self.qhead_per_kvhead
768
+ )
769
+
770
+ if const_expr(not self.use_block_sparsity):
771
+ n_block_min, n_block_max = block_info.get_n_block_min_max(seqlen, m_block)
772
+ # if cute.arch.thread_idx()[0] == 0:
773
+ # cute.printf("m_block = %d, n_block_min: %d, n_block_max: %d", m_block, n_block_min, n_block_max)
774
+ # Clamp n_block to 0 when n_block_max == 0 (can happen with causal
775
+ # + pack_gqa when seqlen_k < tile_n). TMA handles n_block=-1
776
+ # gracefully (fills zeros), but cp.async would crash on
777
+ # out-of-bounds page table access.
778
+ n_block = (
779
+ n_block_max - 1
780
+ if const_expr(self.use_tma_KV)
781
+ else cutlass.max(n_block_max - 1, 0)
782
+ )
783
+ page_idx = (
784
+ mPageTable[batch_idx, n_block]
785
+ if const_expr(mPageTable is not None and self.use_tma_KV)
786
+ else None
787
+ )
788
+
789
+ # First iteration: load K on pipeline_k, Q on pipeline_q
790
+ if is_kv_load_warp:
791
+ pipeline_k.producer_acquire(kv_producer_state)
792
+ if const_expr(not self.use_tma_KV):
793
+ paged_kv_manager.load_page_table(n_block)
794
+ load_K(block=n_block, producer_state=kv_producer_state, page_idx=page_idx)
795
+ if const_expr(self.use_tma_Q):
796
+ if warp_idx_in_wg == 0:
797
+ pipeline_q.producer_acquire_w_index_phase(0, q_producer_phase)
798
+ load_Q(tma_bar_ptr=pipeline_q.sync_object_full.get_barrier(0))
799
+ q_producer_phase ^= 1
800
+ else:
801
+ pipeline_q.producer_acquire_w_index_phase(0, q_producer_phase)
802
+ pack_gqa.load_Q(
803
+ mQ_cur, sQ, gmem_tiled_copy_Q, tidx, m_block, seqlen.seqlen_q
804
+ )
805
+ cute.arch.cp_async_commit_group()
806
+ pipeline_q.producer_commit_w_index(0)
807
+ q_producer_phase ^= 1
808
+
809
+ if is_kv_load_warp:
810
+ if const_expr(not self.intra_wg_overlap or not self.use_tma_KV):
811
+ pipeline_v.producer_acquire(kv_producer_state)
812
+ load_V(
813
+ block=n_block, producer_state=kv_producer_state, page_idx=page_idx
814
+ )
815
+ kv_producer_state.advance()
816
+ for i in cutlass.range(n_block_max - 1 - n_block_min, unroll=1):
817
+ n_block = n_block_max - 1 - i - 1
818
+ page_idx = (
819
+ mPageTable[batch_idx, n_block]
820
+ if const_expr(mPageTable is not None and self.use_tma_KV)
821
+ else None
822
+ )
823
+ if const_expr(not self.use_tma_KV):
824
+ paged_kv_manager.load_page_table(n_block)
825
+ pipeline_k.producer_acquire(kv_producer_state)
826
+ load_K(
827
+ block=n_block,
828
+ producer_state=kv_producer_state,
829
+ page_idx=page_idx,
830
+ )
831
+ pipeline_v.producer_acquire(kv_producer_state)
832
+ load_V(
833
+ block=n_block,
834
+ producer_state=kv_producer_state,
835
+ page_idx=page_idx,
836
+ )
837
+ kv_producer_state.advance()
838
+ else:
839
+ for i in cutlass.range(n_block_max - 1 - n_block_min, unroll=1):
840
+ n_block_prev = n_block_max - i - 1
841
+ n_block = n_block_prev - 1
842
+ page_idx = (
843
+ mPageTable[batch_idx, n_block]
844
+ if const_expr(mPageTable is not None)
845
+ else None
846
+ )
847
+ page_idx_prev = (
848
+ mPageTable[batch_idx, n_block_prev]
849
+ if const_expr(mPageTable is not None)
850
+ else None
851
+ )
852
+ kv_producer_state_prev = kv_producer_state.clone()
853
+ kv_producer_state.advance()
854
+ pipeline_k.producer_acquire(kv_producer_state)
855
+ load_K(
856
+ block=n_block,
857
+ producer_state=kv_producer_state,
858
+ page_idx=page_idx,
859
+ )
860
+ pipeline_v.producer_acquire(kv_producer_state_prev)
861
+ load_V(
862
+ block=n_block_prev,
863
+ producer_state=kv_producer_state_prev,
864
+ page_idx=page_idx_prev,
865
+ )
866
+ n_block = n_block_min
867
+ page_idx = (
868
+ mPageTable[batch_idx, n_block]
869
+ if const_expr(mPageTable is not None)
870
+ else None
871
+ )
872
+ pipeline_v.producer_acquire(kv_producer_state)
873
+ load_V(
874
+ block=n_block, producer_state=kv_producer_state, page_idx=page_idx
875
+ )
876
+ kv_producer_state.advance()
877
+ else:
878
+ # Block sparsity: use TMA closures directly (not paged)
879
+ # Load Q on pipeline_q, separate from K/V pipeline
880
+ if const_expr(self.use_tma_Q):
881
+ if warp_idx_in_wg == 0:
882
+ pipeline_q.producer_acquire_w_index_phase(0, q_producer_phase)
883
+ load_Q(tma_bar_ptr=pipeline_q.sync_object_full.get_barrier(0))
884
+ q_producer_phase ^= 1
885
+ else:
886
+ pipeline_q.producer_acquire_w_index_phase(0, q_producer_phase)
887
+ pack_gqa.load_Q(
888
+ mQ_cur, sQ, gmem_tiled_copy_Q, tidx, m_block, seqlen.seqlen_q
889
+ )
890
+ cute.arch.cp_async_commit_group()
891
+ pipeline_q.producer_commit_w_index(0)
892
+ q_producer_phase ^= 1
893
+ if is_kv_load_warp:
894
+ kv_producer_state = produce_block_sparse_loads(
895
+ blocksparse_tensors,
896
+ batch_idx,
897
+ head_idx,
898
+ m_block,
899
+ seqlen,
900
+ kv_producer_state,
901
+ tma_load_K_fn,
902
+ tma_load_V_fn,
903
+ pipeline_k,
904
+ pipeline_v,
905
+ self.intra_wg_overlap,
906
+ self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
907
+ self.q_subtile_factor,
908
+ )
909
+
910
+ tile_scheduler.prefetch_next_work()
911
+ tile_scheduler.advance_to_next_work()
912
+ work_tile = tile_scheduler.get_current_work()
913
+ # End of persistent scheduler loop
914
+
915
+ # Producer tail is only useful for cluster to avoid early exit of blocks.
916
+ # We only need producer_tail on V since that's the last that's loaded, we don't
917
+ # need it for Q (no cluster) and K.
918
+ if is_kv_load_warp:
919
+ pipeline_v.producer_tail(kv_producer_state)
920
+
921
+ @cute.jit
922
+ def load_KV(
923
+ self,
924
+ tma_load_fn: Optional[Callable],
925
+ paged_kv_manager: Optional[PagedKVManager],
926
+ sX: cute.Tensor,
927
+ block: Int32,
928
+ pipeline_kv: pipeline.PipelineAsync,
929
+ producer_state: pipeline.PipelineState,
930
+ K_or_V: Literal["K", "V"],
931
+ page_idx: Optional[Int32] = None,
932
+ ):
933
+ if const_expr(self.use_tma_KV):
934
+ src_idx = block if const_expr(page_idx is None) else page_idx
935
+ tma_load_fn(src_idx=src_idx, producer_state=producer_state)
936
+ else:
937
+ paged_kv_manager.load_KV(block, sX[None, None, producer_state.index], K_or_V)
938
+ cute.arch.cp_async_commit_group()
939
+ pipeline_kv.producer_commit(producer_state)
940
+
941
+ @cute.jit
942
+ def mma(
943
+ self,
944
+ tiled_mma_qk: cute.TiledMma,
945
+ tiled_mma_pv: cute.TiledMma,
946
+ mO: cute.Tensor,
947
+ mLSE: Optional[cute.Tensor],
948
+ sQ: cute.Tensor,
949
+ sK: cute.Tensor,
950
+ sVt: cute.Tensor,
951
+ sP: Optional[cute.Tensor],
952
+ sO: cute.Tensor,
953
+ learnable_sink: Optional[cute.Tensor],
954
+ pipeline_k: pipeline.PipelineAsync,
955
+ pipeline_v: pipeline.PipelineAsync,
956
+ pipeline_q: pipeline.PipelineAsync,
957
+ gmem_tiled_copy_O: cute.TiledCopy,
958
+ tma_atom_O: Optional[cute.CopyAtom],
959
+ tidx: Int32,
960
+ softmax_scale_log2: Float32,
961
+ softmax_scale: Optional[Float32],
962
+ block_info: BlockInfo,
963
+ SeqlenInfoCls: Callable,
964
+ AttentionMaskCls: Callable,
965
+ TileSchedulerCls: Callable,
966
+ blocksparse_tensors: Optional[BlockSparseTensors],
967
+ aux_data: AuxData = AuxData(),
968
+ fastdiv_mods=None,
969
+ ):
970
+ aux_tensors = aux_data.tensors
971
+ warp_group_idx = cute.arch.make_warp_uniform(tidx // self.num_threads_per_warp_group)
972
+ warp_group_thread_layout = cute.make_layout(
973
+ self.num_wg_mma, stride=self.num_threads_per_warp_group
974
+ )
975
+ thr_mma_qk = tiled_mma_qk.get_slice(tidx)
976
+ wg_mma_qk = tiled_mma_qk.get_slice(warp_group_thread_layout(warp_group_idx))
977
+ wg_mma_pv = tiled_mma_pv.get_slice(warp_group_thread_layout(warp_group_idx))
978
+ _, tSrQ, tSrK = sm90_utils.partition_fragment_ABC(
979
+ wg_mma_qk, (self.tile_m, self.tile_n, self.tile_hdim), sQ, sK
980
+ )
981
+ mma_qk_fn = partial(
982
+ sm90_utils.gemm_zero_init, tiled_mma_qk, (self.tile_m, self.tile_n), tSrQ, tSrK
983
+ )
984
+ acc_O, tOrP, tOrVt = sm90_utils.partition_fragment_ABC(
985
+ wg_mma_pv, (self.tile_m, self.tile_hdimv, self.tile_n), sP, sVt
986
+ )
987
+ mma_pv_fn = partial(sm90_utils.gemm_w_idx, tiled_mma_pv, acc_O, tOrP, tOrVt)
988
+
989
+ # ///////////////////////////////////////////////////////////////////////////////
990
+ # Smem copy atom tiling
991
+ # ///////////////////////////////////////////////////////////////////////////////
992
+ smem_copy_atom_P = utils.get_smem_store_atom(
993
+ self.arch.major * 10 + self.arch.minor, self.dtype
994
+ )
995
+ smem_thr_copy_P = cute.make_tiled_copy_C(smem_copy_atom_P, tiled_mma_qk).get_slice(tidx)
996
+ tPsP = smem_thr_copy_P.partition_D(sP) if const_expr(sP is not None) else None
997
+ smem_copy_params = SimpleNamespace(smem_thr_copy_P=smem_thr_copy_P, tPsP=tPsP)
998
+
999
+ self.mma_init()
1000
+
1001
+ q_consumer_phase = Int32(0)
1002
+ kv_consumer_state = pipeline.make_pipeline_state(
1003
+ pipeline.PipelineUserType.Consumer, self.num_stages
1004
+ )
1005
+
1006
+ tile_scheduler = TileSchedulerCls()
1007
+ work_tile = tile_scheduler.initial_work_tile_info()
1008
+ softmax = Softmax.create(
1009
+ softmax_scale_log2,
1010
+ num_rows=acc_O.shape[0][0] * acc_O.shape[1],
1011
+ softmax_scale=softmax_scale,
1012
+ )
1013
+
1014
+ # For RescaleOBeforeGemm: persistent scores_scale across iterations
1015
+ scores_scale = None
1016
+ if const_expr(self.rescale_O_before_gemm):
1017
+ scores_scale = cute.make_rmem_tensor_like(softmax.row_max, Float32)
1018
+
1019
+ mma_one_n_block_all = partial(
1020
+ self.mma_one_n_block_intrawg_overlap
1021
+ if const_expr(self.intra_wg_overlap)
1022
+ else self.mma_one_n_block,
1023
+ mma_qk_fn=mma_qk_fn,
1024
+ pipeline_k=pipeline_k,
1025
+ pipeline_v=pipeline_v,
1026
+ acc_O=acc_O,
1027
+ tOrP=tOrP,
1028
+ smem_copy_params=smem_copy_params,
1029
+ check_inf=True,
1030
+ scores_scale=scores_scale,
1031
+ )
1032
+
1033
+ process_first_half_block = partial(
1034
+ self.first_half_block_overlap,
1035
+ mma_qk_fn=mma_qk_fn,
1036
+ pipeline_k=pipeline_k,
1037
+ tOrP=tOrP,
1038
+ smem_copy_params=smem_copy_params,
1039
+ scores_scale=scores_scale,
1040
+ softmax=softmax,
1041
+ acc_O=acc_O,
1042
+ )
1043
+ process_last_half_block = partial(
1044
+ self.last_half_block_overlap,
1045
+ pipeline_v=pipeline_v,
1046
+ mma_pv_fn=mma_pv_fn,
1047
+ scores_scale=scores_scale,
1048
+ softmax=softmax,
1049
+ acc_O=acc_O,
1050
+ )
1051
+ while work_tile.is_valid_tile:
1052
+ # if work_tile.is_valid_tile:
1053
+
1054
+ # shape: (atom_v_m * rest_m)
1055
+ m_block, head_idx, batch_idx, _ = work_tile.tile_idx
1056
+ seqlen = SeqlenInfoCls(batch_idx)
1057
+
1058
+ # Recompute fastdiv_mods if necessary for varlen with aux_tensors
1059
+ recompute_fastdiv_mods_q = cutlass.const_expr(
1060
+ aux_tensors is not None and (seqlen.has_cu_seqlens_q or seqlen.has_seqused_q)
1061
+ )
1062
+ recompute_fastdiv_mods_k = cutlass.const_expr(
1063
+ aux_tensors is not None and (seqlen.has_cu_seqlens_k or seqlen.has_seqused_k)
1064
+ )
1065
+ if cutlass.const_expr(fastdiv_mods is not None):
1066
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
1067
+ fastdiv_mods = (
1068
+ seqlen_q_divmod
1069
+ if not recompute_fastdiv_mods_q
1070
+ else FastDivmodDivisor(seqlen.seqlen_q),
1071
+ seqlen_k_divmod
1072
+ if not recompute_fastdiv_mods_k
1073
+ else FastDivmodDivisor(seqlen.seqlen_k),
1074
+ )
1075
+
1076
+ mask = AttentionMaskCls(seqlen)
1077
+ mask_fn = partial(
1078
+ mask.apply_mask,
1079
+ batch_idx=batch_idx,
1080
+ head_idx=head_idx,
1081
+ m_block=m_block,
1082
+ thr_mma=thr_mma_qk,
1083
+ mask_causal=self.is_causal,
1084
+ mask_local=self.is_local,
1085
+ aux_data=aux_data,
1086
+ fastdiv_mods=fastdiv_mods,
1087
+ )
1088
+ score_mod_fn = None
1089
+ if const_expr(self.score_mod is not None):
1090
+ score_mod_fn = partial(
1091
+ self.apply_score_mod,
1092
+ thr_mma_qk,
1093
+ batch_idx,
1094
+ head_idx,
1095
+ m_block,
1096
+ softmax_scale=softmax_scale,
1097
+ aux_data=aux_data,
1098
+ fastdiv_mods=fastdiv_mods,
1099
+ )
1100
+ mma_one_n_block = partial(
1101
+ mma_one_n_block_all, seqlen=seqlen, softmax=softmax, score_mod_fn=score_mod_fn
1102
+ )
1103
+ n_block_min, n_block_max = block_info.get_n_block_min_max(seqlen, m_block)
1104
+ pipeline_q.consumer_wait_w_index_phase(0, q_consumer_phase)
1105
+ # For performance reason, we separate out two kinds of iterations:
1106
+ # those that need masking on S, and those that don't.
1107
+ # We need masking on S for the very last block when K and V has length not multiple of tile_n.
1108
+ # We also need masking on S if it's causal, for the last several blocks.
1109
+ # softmax.reset() # Don't need reset as we explicitly call softmax w is_first=True
1110
+ O_should_accumulate = False
1111
+
1112
+ # ==========================================
1113
+ # MAINLOOP
1114
+ # ==========================================
1115
+ if const_expr(not self.use_block_sparsity):
1116
+ # ==========================================
1117
+ # No block-sparsity (original path)
1118
+ # ==========================================
1119
+ # First iteration with seqlen masking
1120
+ if const_expr(self.intra_wg_overlap):
1121
+ kv_consumer_state = process_first_half_block(
1122
+ n_block=n_block_max - 1,
1123
+ seqlen=seqlen,
1124
+ kv_consumer_state=kv_consumer_state,
1125
+ mask_fn=partial(mask_fn, mask_mod=self.mask_mod),
1126
+ score_mod_fn=score_mod_fn,
1127
+ is_first_block=True,
1128
+ )
1129
+ else:
1130
+ self.warp_scheduler_barrier_sync()
1131
+ kv_consumer_state = mma_one_n_block(
1132
+ kv_consumer_state,
1133
+ n_block=n_block_max - 1,
1134
+ seqlen=seqlen,
1135
+ mma_pv_fn=partial(mma_pv_fn, zero_init=True),
1136
+ is_first_n_block=True,
1137
+ mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=True),
1138
+ )
1139
+ O_should_accumulate = True
1140
+ # if cute.arch.thread_idx()[0] == 128: cute.printf("m_block = {}, n_block_max = {}, n_block_min = {}", m_block, n_block_max, n_block_min)
1141
+ n_block_max -= 1
1142
+ # Next couple of iterations with causal masking
1143
+ if const_expr(self.is_causal or self.is_local):
1144
+ n_block_min_causal_local_mask = block_info.get_n_block_min_causal_local_mask(
1145
+ seqlen, m_block, n_block_min
1146
+ )
1147
+ # if cute.arch.thread_idx()[0] == 128: cute.printf("n_block_min_causal_local_mask = {}", n_block_min_causal_local_mask)
1148
+ for n_tile in cutlass.range(
1149
+ n_block_max - n_block_min_causal_local_mask, unroll=1
1150
+ ):
1151
+ kv_consumer_state = mma_one_n_block(
1152
+ kv_consumer_state,
1153
+ n_block=n_block_max - 1 - n_tile,
1154
+ seqlen=seqlen,
1155
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
1156
+ mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=False),
1157
+ )
1158
+ O_should_accumulate = True
1159
+ n_block_max = cutlass.min(n_block_max, n_block_min_causal_local_mask)
1160
+ # The remaining iterations have no masking
1161
+ n_block_min_before_local_mask = block_info.get_n_block_min_before_local_mask(
1162
+ seqlen, m_block, n_block_min
1163
+ )
1164
+ # if cute.arch.thread_idx()[0] == 128: cute.printf("n_block_min_before_local_mask = {}, n_block_min = {}", n_block_min_before_local_mask, n_block_min)
1165
+ for n_tile in cutlass.range(n_block_max - n_block_min_before_local_mask, unroll=1):
1166
+ kv_consumer_state = mma_one_n_block(
1167
+ kv_consumer_state,
1168
+ n_block=n_block_max - 1 - n_tile,
1169
+ seqlen=seqlen,
1170
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
1171
+ mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=False),
1172
+ )
1173
+ O_should_accumulate = True
1174
+ # Separate iterations with local masking on the left
1175
+ if const_expr(self.is_local and block_info.window_size_left is not None):
1176
+ n_block_max = cutlass.min(n_block_max, n_block_min_before_local_mask)
1177
+ for n_tile in cutlass.range(n_block_max - n_block_min, unroll=1):
1178
+ kv_consumer_state = mma_one_n_block(
1179
+ kv_consumer_state,
1180
+ n_block=n_block_max - 1 - n_tile,
1181
+ seqlen=seqlen,
1182
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
1183
+ mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=False),
1184
+ )
1185
+ O_should_accumulate = True
1186
+ # Release Q pipeline so the producer can load the next tile's Q
1187
+ pipeline_q.consumer_release_w_index(0)
1188
+ # Last "half" iteration
1189
+ if const_expr(self.intra_wg_overlap):
1190
+ kv_consumer_state = process_last_half_block(
1191
+ kv_consumer_state=kv_consumer_state,
1192
+ zero_init=not O_should_accumulate,
1193
+ )
1194
+ O_should_accumulate = True
1195
+ else:
1196
+ self.warp_scheduler_barrier_arrive()
1197
+
1198
+ else:
1199
+ # ==========================================
1200
+ # Block sparsity
1201
+ # ==========================================
1202
+ kv_consumer_state, O_should_accumulate, processed_any = consume_block_sparse_loads(
1203
+ blocksparse_tensors,
1204
+ batch_idx,
1205
+ head_idx,
1206
+ m_block,
1207
+ seqlen,
1208
+ kv_consumer_state,
1209
+ mma_pv_fn,
1210
+ mma_one_n_block,
1211
+ process_first_half_block,
1212
+ process_last_half_block,
1213
+ mask_fn,
1214
+ score_mod_fn,
1215
+ O_should_accumulate,
1216
+ self.mask_mod,
1217
+ fastdiv_mods,
1218
+ self.intra_wg_overlap,
1219
+ self.warp_scheduler_barrier_sync,
1220
+ self.warp_scheduler_barrier_arrive,
1221
+ self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
1222
+ self.q_subtile_factor,
1223
+ )
1224
+
1225
+ # Release Q pipeline so the producer can load the next tile's Q
1226
+ pipeline_q.consumer_release_w_index(0)
1227
+
1228
+ # Handle empty case (when no blocks to process)
1229
+ if not processed_any:
1230
+ softmax.reset()
1231
+ acc_O.fill(0.0)
1232
+
1233
+ q_consumer_phase ^= 1
1234
+
1235
+ sink_val = None
1236
+ if const_expr(learnable_sink is not None):
1237
+ if const_expr(not self.pack_gqa):
1238
+ sink_val = Float32(learnable_sink[head_idx])
1239
+ else: # Each thread might have a different sink value due to different q_head
1240
+ sink_val = cute.make_rmem_tensor_like(softmax.row_max, Float32)
1241
+ cS = cute.make_identity_tensor((self.tile_m, self.tile_n))
1242
+ tScS_mn = layout_utils.reshape_acc_to_mn(thr_mma_qk.partition_C(cS))
1243
+ for r in cutlass.range(cute.size(sink_val), unroll_full=True):
1244
+ row = m_block * self.tile_m + tScS_mn[r][0]
1245
+ q_head_idx = row % self.qhead_per_kvhead + head_idx * self.qhead_per_kvhead
1246
+ sink_val[r] = Float32(learnable_sink[q_head_idx])
1247
+
1248
+ # normalize acc_O by row_sum and calculate the lse
1249
+ row_scale = softmax.finalize(sink_val=sink_val)
1250
+ softmax.rescale_O(acc_O, row_scale)
1251
+
1252
+ # ///////////////////////////////////////////////////////////////////////////////
1253
+ # Epilogue
1254
+ # ///////////////////////////////////////////////////////////////////////////////
1255
+ self.epilogue(
1256
+ acc_O,
1257
+ softmax.row_sum,
1258
+ mO,
1259
+ mLSE,
1260
+ sO,
1261
+ seqlen,
1262
+ gmem_tiled_copy_O,
1263
+ tma_atom_O,
1264
+ tiled_mma_pv,
1265
+ tidx,
1266
+ m_block,
1267
+ head_idx,
1268
+ batch_idx,
1269
+ )
1270
+
1271
+ tile_scheduler.advance_to_next_work()
1272
+ work_tile = tile_scheduler.get_current_work()
1273
+
1274
+ @cute.jit
1275
+ def first_half_block_overlap(
1276
+ self,
1277
+ n_block: Int32,
1278
+ mma_qk_fn: Callable,
1279
+ kv_consumer_state,
1280
+ pipeline_k,
1281
+ tOrP: cute.Tensor,
1282
+ smem_copy_params: SimpleNamespace,
1283
+ softmax: Softmax,
1284
+ seqlen: SeqlenInfoQK,
1285
+ scores_scale: Optional[cute.Tensor] = None,
1286
+ acc_O: Optional[cute.Tensor] = None,
1287
+ mask_fn: Callable = None,
1288
+ score_mod_fn: Optional[Callable] = None,
1289
+ is_first_block: bool = False,
1290
+ ):
1291
+ """Processes the first half block when using intra-warpgroup-overlap"""
1292
+
1293
+ pipeline_k.consumer_wait(kv_consumer_state, pipeline_k.consumer_try_wait(kv_consumer_state))
1294
+ acc_S = mma_qk_fn(B_idx=kv_consumer_state.index, wg_wait=0)
1295
+ pipeline_k.consumer_release(kv_consumer_state)
1296
+
1297
+ # Apply score modification if present
1298
+ if const_expr(score_mod_fn is not None):
1299
+ score_mod_fn(acc_S, n_block=n_block, seqlen=seqlen)
1300
+
1301
+ # Apply mask; mask_seqlen always True for first block
1302
+ # Caveat: if full block further right than mask block, seqlen masking is redundant;
1303
+ # however, masking is being applied anyway, so essentially no perf hit
1304
+ mask_fn(acc_S, n_block=n_block, mask_seqlen=True)
1305
+
1306
+ row_scale = softmax.online_softmax(acc_S, is_first=is_first_block)
1307
+
1308
+ tOrP_acc = layout_utils.reshape_acc_to_frgA(acc_S)
1309
+ tOrP_cur = (
1310
+ tOrP
1311
+ if const_expr(self.mma_pv_is_rs)
1312
+ else cute.make_rmem_tensor_like(tOrP_acc, self.dtype)
1313
+ )
1314
+ tOrP_cur.store(tOrP_acc.load().to(self.dtype))
1315
+
1316
+ if const_expr(not self.mma_pv_is_rs):
1317
+ tPrP = smem_copy_params.smem_thr_copy_P.retile(tOrP_cur)
1318
+ cute.copy(smem_copy_params.smem_thr_copy_P, tPrP, smem_copy_params.tPsP)
1319
+ # Fence and barrier to make smem store visible to WGMMA
1320
+ cute.arch.fence_view_async_shared()
1321
+ cute.arch.sync_warp()
1322
+
1323
+ # For RescaleOBeforeGemm: initialize acc_O
1324
+ if const_expr(self.rescale_O_before_gemm):
1325
+ acc_O.fill(0.0)
1326
+ scores_scale.store(row_scale.load())
1327
+
1328
+ return kv_consumer_state
1329
+
1330
+ @cute.jit
1331
+ def last_half_block_overlap(
1332
+ self,
1333
+ kv_consumer_state,
1334
+ pipeline_v,
1335
+ mma_pv_fn: Callable,
1336
+ zero_init: bool,
1337
+ scores_scale: Optional[cute.Tensor] = None,
1338
+ softmax: Optional[Softmax] = None,
1339
+ acc_O: Optional[cute.Tensor] = None,
1340
+ ):
1341
+ """Processes the final PV GEMM when using intra-warpgroup-overlap"""
1342
+
1343
+ # For RescaleOBeforeGemm: rescale O before the final PV GEMM
1344
+ if const_expr(self.rescale_O_before_gemm):
1345
+ softmax.rescale_O(acc_O, scores_scale)
1346
+
1347
+ pipeline_v.consumer_wait(kv_consumer_state, pipeline_v.consumer_try_wait(kv_consumer_state))
1348
+ mma_pv_fn(B_idx=kv_consumer_state.index, zero_init=zero_init, wg_wait=0)
1349
+ pipeline_v.consumer_release(kv_consumer_state)
1350
+ kv_consumer_state.advance()
1351
+ return kv_consumer_state
1352
+
1353
+ @cute.jit
1354
+ def mma_one_n_block(
1355
+ self,
1356
+ smem_pipe_read: pipeline.PipelineState | pipeline_custom.PipelineStateSimple,
1357
+ n_block: Int32,
1358
+ mma_qk_fn: Callable,
1359
+ mma_pv_fn: Callable,
1360
+ pipeline_k: pipeline.PipelineAsync,
1361
+ pipeline_v: pipeline.PipelineAsync,
1362
+ acc_O: cute.Tensor,
1363
+ tOrP: cute.Tensor,
1364
+ smem_copy_params: SimpleNamespace,
1365
+ softmax: Softmax,
1366
+ seqlen: SeqlenInfoQK,
1367
+ scores_scale: Optional[cute.Tensor] = None, # not used
1368
+ score_mod_fn: Optional[Callable] = None,
1369
+ mask_fn: Optional[Callable] = None,
1370
+ is_first_n_block: cutlass.Constexpr = False,
1371
+ check_inf: cutlass.Constexpr = True,
1372
+ ):
1373
+ pipeline_k.consumer_wait(smem_pipe_read, pipeline_k.consumer_try_wait(smem_pipe_read))
1374
+ # S = Q @ K.T
1375
+ acc_S = mma_qk_fn(B_idx=smem_pipe_read.index, wg_wait=-1)
1376
+ self.warp_scheduler_barrier_arrive()
1377
+ warpgroup.wait_group(0)
1378
+ pipeline_k.consumer_release(smem_pipe_read)
1379
+
1380
+ # handle score mods and masking
1381
+ if const_expr(score_mod_fn is not None):
1382
+ score_mod_fn(acc_S, n_block=n_block, seqlen=seqlen)
1383
+ if const_expr(mask_fn is not None):
1384
+ mask_fn(acc_S=acc_S, n_block=n_block)
1385
+
1386
+ row_scale = softmax.online_softmax(acc_S, is_first=is_first_n_block, check_inf=check_inf)
1387
+ # if cute.arch.thread_idx()[0] == 0: cute.print_tensor(layout_utils.reshape_acc_to_mn(acc_S))
1388
+ tOrP_acc = layout_utils.reshape_acc_to_frgA(acc_S)
1389
+ tOrP_cur = (
1390
+ tOrP
1391
+ if const_expr(self.mma_pv_is_rs)
1392
+ else cute.make_rmem_tensor_like(tOrP_acc, self.dtype)
1393
+ )
1394
+ # tOrP.store(tOrP_acc.load().to(self.dtype))
1395
+ # the "to(self.dtype)" conversion fails to vectorize for block sizes other
1396
+ # than 128 x 128, i.e. it calls convert on 1 fp32 element at a time instead of
1397
+ # 2 elements. So we just call ptx directly.
1398
+ utils.cvt_f16(tOrP_acc, tOrP_cur)
1399
+ if const_expr(not self.mma_pv_is_rs):
1400
+ tPrP = smem_copy_params.smem_thr_copy_P.retile(tOrP_cur)
1401
+ cute.copy(smem_copy_params.smem_thr_copy_P, tPrP, smem_copy_params.tPsP)
1402
+ softmax.rescale_O(acc_O, row_scale)
1403
+ if const_expr(not self.mma_pv_is_rs):
1404
+ # Fence and barrier to make sure smem store is visible to WGMMA
1405
+ cute.arch.fence_view_async_shared()
1406
+ cute.arch.sync_warp() # Only need syncwarp since each warp is using its own P values for MmaPV
1407
+ pipeline_v.consumer_wait(smem_pipe_read, pipeline_v.consumer_try_wait(smem_pipe_read))
1408
+ self.warp_scheduler_barrier_sync()
1409
+ # O += P @ V
1410
+ mma_pv_fn(B_idx=smem_pipe_read.index, wg_wait=0)
1411
+ pipeline_v.consumer_release(smem_pipe_read)
1412
+ smem_pipe_read.advance()
1413
+ return smem_pipe_read
1414
+
1415
+ @cute.jit
1416
+ def mma_one_n_block_intrawg_overlap(
1417
+ self,
1418
+ smem_pipe_read: pipeline.PipelineState | pipeline_custom.PipelineStateSimple,
1419
+ n_block: Int32,
1420
+ mma_qk_fn: Callable,
1421
+ mma_pv_fn: Callable,
1422
+ pipeline_k: pipeline.PipelineAsync,
1423
+ pipeline_v: pipeline.PipelineAsync,
1424
+ acc_O: cute.Tensor,
1425
+ tOrP: cute.Tensor,
1426
+ smem_copy_params: SimpleNamespace,
1427
+ softmax: Softmax,
1428
+ seqlen: SeqlenInfoQK,
1429
+ scores_scale: Optional[cute.Tensor] = None,
1430
+ score_mod_fn: Optional[Callable] = None,
1431
+ mask_fn: Optional[Callable] = None,
1432
+ check_inf: cutlass.Constexpr = True,
1433
+ ):
1434
+ smem_pipe_read_v = smem_pipe_read.clone()
1435
+ smem_pipe_read.advance()
1436
+ pipeline_k.consumer_wait(smem_pipe_read, pipeline_k.consumer_try_wait(smem_pipe_read))
1437
+ self.warp_scheduler_barrier_sync()
1438
+ # S = Q @ K.T
1439
+ acc_S = mma_qk_fn(B_idx=smem_pipe_read.index, wg_wait=-1)
1440
+ # RescaleOBeforeGemm: rescale O while QK GEMM is in flight, before PV GEMM
1441
+ if const_expr(self.rescale_O_before_gemm):
1442
+ softmax.rescale_O(acc_O, scores_scale)
1443
+ pipeline_v.consumer_wait(smem_pipe_read_v, pipeline_v.consumer_try_wait(smem_pipe_read_v))
1444
+ # O += P @ V
1445
+ mma_pv_fn(B_idx=smem_pipe_read_v.index, wg_wait=-1)
1446
+ self.warp_scheduler_barrier_arrive()
1447
+ warpgroup.wait_group(1)
1448
+ pipeline_k.consumer_release(smem_pipe_read)
1449
+
1450
+ # handle score mods and masking
1451
+ if const_expr(score_mod_fn is not None):
1452
+ score_mod_fn(acc_S, n_block=n_block, seqlen=seqlen)
1453
+ if const_expr(mask_fn is not None):
1454
+ mask_fn(acc_S=acc_S, n_block=n_block)
1455
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(layout_utils.reshape_acc_to_mn(acc_S))
1456
+
1457
+ row_scale = softmax.online_softmax(acc_S, check_inf=check_inf)
1458
+ warpgroup.wait_group(0)
1459
+ pipeline_v.consumer_release(smem_pipe_read_v)
1460
+ tOrP_acc = layout_utils.reshape_acc_to_frgA(acc_S)
1461
+ tOrP_cur = (
1462
+ tOrP
1463
+ if const_expr(self.mma_pv_is_rs)
1464
+ else cute.make_rmem_tensor_like(tOrP_acc, self.dtype)
1465
+ )
1466
+ # tOrP_cur.store(tOrP_acc.load().to(self.dtype))
1467
+ # the "to(self.dtype)" conversion fails to vectorize for block sizes other
1468
+ # than 128 x 128, i.e. it calls convert on 1 fp32 element at a time instead of
1469
+ # 2 elements. So we just call ptx directly.
1470
+ utils.cvt_f16(tOrP_acc, tOrP_cur)
1471
+ if const_expr(not self.mma_pv_is_rs):
1472
+ tPrP = smem_copy_params.smem_thr_copy_P.retile(tOrP_cur)
1473
+ cute.copy(smem_copy_params.smem_thr_copy_P, tPrP, smem_copy_params.tPsP)
1474
+ if const_expr(not self.rescale_O_before_gemm):
1475
+ softmax.rescale_O(acc_O, row_scale)
1476
+ if const_expr(self.rescale_O_before_gemm):
1477
+ scores_scale.store(row_scale.load())
1478
+ if const_expr(not self.mma_pv_is_rs):
1479
+ # Fence and barrier to make sure smem store is visible to WGMMA
1480
+ cute.arch.fence_view_async_shared()
1481
+ cute.arch.sync_warp() # Only need syncwarp since each warp is using its own P values for MmaPV
1482
+ return smem_pipe_read
1483
+
1484
+ @cute.jit
1485
+ def mma_init(self):
1486
+ warp_group_idx = utils.canonical_warp_group_idx(sync=False)
1487
+ if const_expr(self.use_scheduler_barrier):
1488
+ if warp_group_idx == 1:
1489
+ cute.arch.barrier_arrive(
1490
+ barrier_id=int(NamedBarrierFwd.WarpSchedulerWG1),
1491
+ number_of_threads=2 * self.num_threads_per_warp_group,
1492
+ )
1493
+
1494
+ @cute.jit
1495
+ def apply_score_mod(
1496
+ self,
1497
+ thr_mma_qk,
1498
+ batch_idx,
1499
+ head_idx,
1500
+ m_block,
1501
+ acc_S,
1502
+ n_block,
1503
+ softmax_scale,
1504
+ seqlen,
1505
+ aux_data: AuxData = AuxData(),
1506
+ fastdiv_mods=None,
1507
+ ):
1508
+ # Prepare index tensor
1509
+ cS = cute.make_identity_tensor((self.tile_m, self.tile_n))
1510
+ cS = cute.domain_offset((m_block * self.tile_m, n_block * self.tile_n), cS)
1511
+ tScS = thr_mma_qk.partition_C(cS)
1512
+
1513
+ apply_score_mod_inner(
1514
+ acc_S,
1515
+ tScS,
1516
+ self.score_mod,
1517
+ batch_idx,
1518
+ head_idx,
1519
+ softmax_scale,
1520
+ self.score_vec_size,
1521
+ self.qk_acc_dtype,
1522
+ aux_data,
1523
+ fastdiv_mods,
1524
+ seqlen_info=seqlen,
1525
+ constant_q_idx=None,
1526
+ qhead_per_kvhead=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
1527
+ )
1528
+
1529
+ def warp_scheduler_barrier_sync(self):
1530
+ if const_expr(self.use_scheduler_barrier):
1531
+ cute.arch.barrier(
1532
+ barrier_id=int(NamedBarrierFwd.WarpSchedulerWG1)
1533
+ - 1
1534
+ + utils.canonical_warp_group_idx(sync=False),
1535
+ number_of_threads=2 * self.num_threads_per_warp_group,
1536
+ )
1537
+
1538
+ def warp_scheduler_barrier_arrive(self):
1539
+ if const_expr(self.use_scheduler_barrier):
1540
+ assert self.num_wg_mma in [2, 3]
1541
+ cur_wg = utils.canonical_warp_group_idx(sync=False) - 1
1542
+ if const_expr(self.num_wg_mma == 2):
1543
+ next_wg = 1 - cur_wg
1544
+ else:
1545
+ t = cur_wg + 1
1546
+ next_wg = t % self.num_wg_mma
1547
+ cute.arch.barrier_arrive(
1548
+ barrier_id=int(NamedBarrierFwd.WarpSchedulerWG1) + next_wg,
1549
+ number_of_threads=2 * self.num_threads_per_warp_group,
1550
+ )
build/torch-cuda/interface.py CHANGED
The diff for this file is too large to render. See raw diff
 
build/torch-cuda/mask.py CHANGED
@@ -1,108 +1,157 @@
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 .quack import layout_utils
11
- from . import utils
 
12
  from .seqlen_info import SeqlenInfoQK
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
 
15
  @cute.jit
16
- def mask_r2p(X: cute.Tensor, col_limit: Int32, arch: int = 90, rank1: bool = False) -> None:
17
- # Bit manipulation, compiles down to the R2P instruction
18
- # For sm100: we know that tScS_t2r[i][1] == i, for the particular tmem copy atom we're using.
19
- # For sm90: instead of comparing limit to 0, 1, 8, 9, 16, 17, ...,
20
- # we compare a transformed version of limit to 0, 1, 2, 3, 4, 5, ...
21
- if const_expr(arch == 90):
22
- col_limit_transformed = col_limit // 8 * 2 + min(col_limit % 8, 2)
23
- else:
24
- col_limit_transformed = col_limit
 
 
25
  ncol = const_expr(cute.size(X.shape[cute.rank(X) - 1]) if not rank1 else cute.size(X.shape))
26
- # Ideally we'd move by 32 instead of 24, but mask >> i isn't correct for i == 31
27
- for s in cutlass.range_constexpr(cute.ceil_div(ncol, 24)):
28
- # Don't need to clamp to 32 since the shr.u32 instruction does that already
29
- col_limit_right_s = max(col_limit_transformed - s * 24, 0)
30
- # 0 -> 0b00...00, 1 -> 0b00...01, ..., 31 -> 0b01...11, 32 -> 0b11...11
31
- mask = (1 << col_limit_right_s) - 1
32
  # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
33
- for i in cutlass.range_constexpr(min(24, ncol - s * 24)):
34
- in_bound = cutlass.Boolean(mask & (1 << i))
35
- c = s * 24 + i
36
  if const_expr(rank1):
37
  X[c] = X[c] if in_bound else -Float32.inf
38
- # This is the equivalent of:
39
- # X[s * 24 + i] = X[s * 24 + i] if col_limit_right_s <= i else -Float32.inf
40
  else:
41
  for r in cutlass.range_constexpr(cute.size(X.shape[0])):
42
  X[r, c] = X[r, c] if in_bound else -Float32.inf
43
 
44
 
45
  @cute.jit
46
- def mask_r2p_transposed(X: cute.Tensor, row_limit_top: Int32, num_rep: int) -> None:
47
- # Bit manipulation, compiles down to the R2P instruction
48
- # For sm100: we know that tScS_t2r[i][0] has the form 0, 1, ..., 31, 64, ..., 127
49
- # or 0, 1, ..., 15, 32, ..., 47, 64, ...
50
- # We compare a transformed version of limit to 0, 1, 2, 3, 4, 5, ...
51
- # Here we hardcode for the case of 2 warp groups.
52
- num_wg = 2
53
- row_limit_top_transformed = row_limit_top // (num_rep * num_wg) * num_rep + min(
54
- row_limit_top % (num_rep * num_wg), num_rep
55
- )
56
- ncol = cute.size(X.shape)
57
- # Ideally we'd move by 32 instead of 24, but mask >> i isn't correct for i == 31
58
- for s in cutlass.range_constexpr(cute.ceil_div(ncol, 24)):
59
- row_limit_top_s = max(row_limit_top_transformed - s * 24, 0)
60
- # 0 -> 0b00...00, 1 -> 0b00...01, ..., 31 -> 0b01...11, 32 -> 0b11...11
61
- mask = (1 << row_limit_top_s) - 1
62
- # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
63
- for i in cutlass.range_constexpr(min(24, ncol - s * 24)):
64
- out_bound = cutlass.Boolean(mask & (1 << i))
65
- c = s * 24 + i
66
- X[c] = -Float32.inf if out_bound else X[c]
67
- # tidx = cute.arch.thread_idx()[0] % 256
68
- # if tidx == 128:
69
- # 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)
70
 
71
 
72
  @cute.jit
73
- def mask_r2p_dual_bound(
74
- X: cute.Tensor,
75
- col_limit_left: Int32, # Inclusive lower bound
76
- col_limit_right: Int32, # Exclusive upper bound
77
- ) -> None:
78
- """
79
- Dual-bound masking using two bitmasks for SM100, following mask_r2p.
80
- Masks elements where: NOT (col_limit_left <= col < col_limit_right)
81
 
82
- Uses bit manipulation to create a range mask:
83
- mask_right = (1 << right) - 1 -> bits (right-1)..0 are 1
84
- mask_left = (1 << left) - 1 -> bits (left-1)..0 are 1
85
- mask_range = mask_range = mask_right & ~ mask_left -> bits (right-1)..left are 1
86
- """
87
- ncol = const_expr(cute.size(X.shape))
88
 
89
- for s in cutlass.range_constexpr(cute.ceil_div(ncol, 24)):
90
- right_s = max(col_limit_right - s * 24, 0)
91
- left_s = max(col_limit_left - s * 24, 0)
92
 
93
- # otherwise cute dsl complains about python int too large to convert into c long
94
- right_s = min(right_s, 24)
95
- left_s = min(left_s, 24)
 
 
 
96
 
97
- # bits (right-1)..left are 1
98
- mask_right = (1 << right_s) - 1
99
- mask_left = (1 << left_s) - 1
100
- mask_range = mask_right & ~mask_left
101
 
102
- # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
103
- for i in cutlass.range_constexpr(min(24, ncol - s * 24)):
104
- in_bound = cutlass.Boolean(mask_range & (1 << i))
105
- c = s * 24 + i
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  X[c] = X[c] if in_bound else -Float32.inf
107
 
108
 
@@ -137,7 +186,7 @@ class AttentionMask:
137
  mask_causal: cutlass.Constexpr[bool],
138
  mask_local: cutlass.Constexpr[bool] = False,
139
  mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
140
- aux_tensors: Optional[list] = None,
141
  fastdiv_mods=(None, None),
142
  ) -> None:
143
  assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
@@ -161,8 +210,7 @@ class AttentionMask:
161
  seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
162
  if const_expr(not mask_causal and not mask_local and mask_mod is None):
163
  if const_expr(mask_seqlen):
164
- # The compiler now choses not to use R2P
165
- r2p = const_expr(False and not self.swap_AB)
166
  if const_expr(not r2p):
167
  # traverse column index.
168
  for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
@@ -170,7 +218,8 @@ class AttentionMask:
170
  for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
171
  acc_S_mn[r, c] = -Float32.inf if oob else acc_S_mn[r, c]
172
  else:
173
- mask_r2p(acc_S_mn, seqlenk_col_limit, arch=90)
 
174
 
175
  elif const_expr(
176
  not mask_causal and not mask_local and mask_mod is not None
@@ -183,7 +232,7 @@ class AttentionMask:
183
  and fastdiv_mods[1] is not None
184
  )
185
  wrap_aux_indices = const_expr(
186
- has_fastdiv and mask_seqlen and const_expr(aux_tensors is not None)
187
  )
188
 
189
  for r in cutlass.range_constexpr(nrow):
@@ -212,13 +261,14 @@ class AttentionMask:
212
  head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
213
  q_idx_ssa = utils.scalar_to_ssa(row_for_mod, cutlass.Int32)
214
  kv_idx_ssa = utils.scalar_to_ssa(col_for_mod, cutlass.Int32)
215
- mask_value = mask_mod(
 
216
  batch_idx_ssa,
217
  head_idx_ssa,
218
  q_idx_ssa,
219
  kv_idx_ssa,
220
  self.seqlen_info,
221
- aux_tensors,
222
  )
223
  cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
224
  if const_expr(mask_seqlen):
@@ -272,7 +322,12 @@ class AttentionMask:
272
  else acc_S_mn[r, c]
273
  )
274
  else:
275
- mask_r2p(acc_S_mn[r, None], col_limit_right, arch=90, rank1=True)
 
 
 
 
 
276
  else: # Local
277
  local_row_offset_right = (
278
  causal_row_offset + self.window_size_right
@@ -284,6 +339,7 @@ class AttentionMask:
284
  if const_expr(self.window_size_left is not None)
285
  else None
286
  )
 
287
  for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
288
  if const_expr(self.qhead_per_kvhead_packgqa == 1):
289
  row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
@@ -302,13 +358,22 @@ class AttentionMask:
302
  if const_expr(self.window_size_left is not None)
303
  else 0
304
  )
305
- # 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)
306
- # traverse column index.
307
- for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
308
- col_idx = t0ScS_mn[0, c][1]
309
- # only consider the column index, so the row index sets to 0.
310
- if col_idx >= col_limit_right or col_idx < col_limit_left:
311
- acc_S_mn[r, c] = -Float32.inf
 
 
 
 
 
 
 
 
 
312
  else: # swap_AB
313
  assert self.qhead_per_kvhead_packgqa == 1
314
  thr_row_offset = tScS_mn[0][ROW]
@@ -338,11 +403,18 @@ class AttentionMask:
338
  # column, by setting row limit to be self.tile_m.
339
  row_limit_top = (
340
  self.tile_m
341
- if col0 >= seqlenk_col_limit
342
- else col0 - causal_row_offset - self.window_size_right
 
 
 
 
 
 
 
 
 
343
  )
344
- # TODO: do we need col_limit_sink?
345
- row_limit_bot = col0 - causal_row_offset + self.window_size_left
346
  for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
347
  row_idx = t0ScS_mn[r, 0][ROW]
348
  acc_S_mn[r, c] = (
@@ -351,6 +423,194 @@ class AttentionMask:
351
  else acc_S_mn[r, c]
352
  )
353
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  @cute.jit
355
  def apply_mask_sm100(
356
  self,
@@ -365,10 +625,13 @@ class AttentionMask:
365
  mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
366
  batch_idx: Int32 = None,
367
  head_idx: Int32 = None,
368
- aux_tensors: Optional[list] = None,
369
  fastdiv_mods=(None, None),
370
  head_divmod=None,
 
371
  check_q_boundary: bool = False,
 
 
372
  ) -> None:
373
  assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
374
  acc_shape = (self.tile_m, self.tile_n)
@@ -382,8 +645,18 @@ class AttentionMask:
382
  if n_block < 0:
383
  n_block = 0
384
  seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n
385
- r2p = True
386
- if const_expr(not mask_causal and not mask_local and mask_mod is None):
 
 
 
 
 
 
 
 
 
 
387
  if const_expr(mask_seqlen):
388
  if const_expr(not r2p):
389
  for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
@@ -392,65 +665,58 @@ class AttentionMask:
392
  # For some reason the 2 lines above generate really bad SASS
393
  acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= seqlenk_col_limit else acc_S[i]
394
  else:
395
- mask_r2p(acc_S, seqlenk_col_limit, arch=100, rank1=True)
 
 
 
 
396
 
397
  elif const_expr(not mask_causal and not mask_local and mask_mod is not None):
398
- # Block sparse case w/ mask_mod
399
- has_fastdiv = const_expr(
400
- fastdiv_mods is not None
401
- and fastdiv_mods[0] is not None
402
- and fastdiv_mods[1] is not None
403
  )
404
- batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
405
-
406
- ncol = const_expr(cute.size(tScS_t2r.shape))
407
- for i in cutlass.range_constexpr(ncol):
408
- row_coord = tScS_t2r[i][0] if not self.swap_AB else tScS_t2r[i][1]
409
- col_coord = tScS_t2r[i][1] if not self.swap_AB else tScS_t2r[i][0]
410
- global_row = row_coord + m_block * self.tile_m
411
- global_col = col_coord + n_block * self.tile_n
412
-
413
- if const_expr(self.qhead_per_kvhead_packgqa != 1):
414
- assert head_divmod is not None
415
- mask_row, head_offset = divmod(global_row, head_divmod)
416
- head_idx_for_mod = head_idx * self.qhead_per_kvhead_packgqa + head_offset
417
- else:
418
- head_idx_for_mod = head_idx
419
- mask_row = global_row
420
-
421
- mask_row_for_mod = mask_row
422
- if const_expr(has_fastdiv and aux_tensors is not None):
423
- if check_q_boundary:
424
- _, mask_row_for_mod = divmod(mask_row, fastdiv_mods[0])
425
- global_col_for_mod = global_col
426
- if const_expr(has_fastdiv and mask_seqlen and aux_tensors is not None):
427
- _, global_col_for_mod = divmod(global_col, fastdiv_mods[1])
428
-
429
- head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
430
- mask_row_ssa = utils.scalar_to_ssa(mask_row_for_mod, cutlass.Int32)
431
- kv_idx_ssa = utils.scalar_to_ssa(global_col_for_mod, cutlass.Int32)
432
- mask_value = mask_mod(
433
- batch_idx_ssa,
434
- head_idx_ssa,
435
- mask_row_ssa,
436
- kv_idx_ssa,
437
- self.seqlen_info,
438
- aux_tensors,
439
  )
440
- cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
441
- acc_S[i] = acc_S[i] if cond else -Float32.inf
442
- if const_expr(mask_seqlen):
443
- acc_S[i] = -Float32.inf if global_col >= self.seqlen_k else acc_S[i]
444
- if check_q_boundary:
445
- acc_S[i] = -Float32.inf if mask_row >= self.seqlen_q else acc_S[i]
446
 
447
  else: # Causal or local
448
- causal_row_offset = 1 + self.seqlen_k - n_block * self.tile_n - self.seqlen_q
449
  row_idx = tScS_t2r[0][0] + m_block * self.tile_m
450
  if const_expr(self.qhead_per_kvhead_packgqa != 1):
451
  row_idx = row_idx // self.qhead_per_kvhead_packgqa
452
  if const_expr(mask_causal):
453
- col_limit_right = row_idx + causal_row_offset
454
  if const_expr(mask_seqlen):
455
  col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
456
  # if cute.arch.thread_idx()[0] % 32 == 0:
@@ -460,15 +726,19 @@ class AttentionMask:
460
  for i in cutlass.range(ncol, unroll_full=True):
461
  acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= col_limit_right else acc_S[i]
462
  else:
463
- mask_r2p(acc_S, col_limit_right, arch=100, rank1=True)
 
 
 
 
464
  else:
465
  local_row_offset_right = (
466
- causal_row_offset + self.window_size_right
467
  if const_expr(self.window_size_right is not None)
468
  else None
469
  )
470
  local_row_offset_left = (
471
- causal_row_offset - 1 - self.window_size_left
472
  if const_expr(self.window_size_left is not None)
473
  else None
474
  )
@@ -493,8 +763,15 @@ class AttentionMask:
493
  else acc_S[i]
494
  )
495
  else:
496
- # XOR-based R2P dual bound masking
497
- mask_r2p_dual_bound(acc_S, col_limit_left, col_limit_right)
 
 
 
 
 
 
 
498
 
499
  @cute.jit
500
  def apply_mask_sm100_transposed(
@@ -510,7 +787,7 @@ class AttentionMask:
510
  mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
511
  batch_idx: Int32 = None,
512
  head_idx: Int32 = None,
513
- aux_tensors: Optional[list] = None,
514
  fastdiv_mods=(None, None),
515
  is_full_block: bool = False,
516
  check_m_boundary: bool = True,
@@ -518,7 +795,7 @@ class AttentionMask:
518
  """
519
  Backward pass: mask S = K @ Q.T where n_block tiles seqlen_k and m_block tiles seqlen_q.
520
 
521
- Coordinate conventio:
522
  - ROW corresponds to Q (m_block)
523
  - COL corresponds to KV (n_block)
524
 
@@ -568,7 +845,7 @@ class AttentionMask:
568
  and fastdiv_mods[1] is not None
569
  )
570
  wrap_aux_indices = const_expr(
571
- has_fastdiv and mask_seqlen and const_expr(aux_tensors is not None)
572
  )
573
  batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
574
  head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32)
@@ -589,13 +866,14 @@ class AttentionMask:
589
  q_idx_ssa = utils.scalar_to_ssa(q_idx_for_mod, cutlass.Int32)
590
  kv_idx_ssa = utils.scalar_to_ssa(kv_idx_for_mod, cutlass.Int32)
591
 
592
- mask_value = mask_mod(
 
593
  batch_idx_ssa,
594
  head_idx_ssa,
595
  q_idx_ssa,
596
  kv_idx_ssa,
597
  self.seqlen_info,
598
- aux_tensors,
599
  )
600
  cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
601
  acc_S[i] = acc_S[i] if cond else -cutlass.Float32.inf
@@ -634,7 +912,13 @@ class AttentionMask:
634
  )
635
  else:
636
  num_rep = cute.size(tScS_t2r, mode=[0]) # 16 or 32
637
- mask_r2p_transposed(acc_S, row_limit_top, num_rep)
 
 
 
 
 
 
638
  else:
639
  if const_expr(self.window_size_right is not None):
640
  row_limit_top = causal_offset - self.window_size_right
@@ -645,9 +929,783 @@ class AttentionMask:
645
  if const_expr(mask_seqlen):
646
  if seqlenk_col_limit <= 0:
647
  row_limit_top = self.tile_m
648
- for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
649
- row_idx = t0ScS_t2r[i][ROW]
650
- local_mask = row_idx < row_limit_top
651
- if const_expr(self.window_size_left is not None):
652
- local_mask |= row_idx > row_limit_bot
653
- acc_S[i] = -cutlass.Float32.inf if local_mask else acc_S[i]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Copyright (c) 2025, Tri Dao.
2
 
3
+ from typing import Optional, Callable, TypeAlias, Tuple
4
  from dataclasses import dataclass
5
+ import enum
6
 
7
  import cutlass
8
  import cutlass.cute as cute
9
+ from cutlass import Float32, Int32, Uint32, const_expr
10
+ from cutlass.cutlass_dsl import min as dsl_min
11
 
12
  from .quack import layout_utils
13
+ from . import utils as utils
14
+ from .block_info import BlockInfo
15
  from .seqlen_info import SeqlenInfoQK
16
+ from .utils import AuxData
17
+
18
+ MaskGenFn: TypeAlias = Callable[[int], Uint32]
19
+ MASK_R2P_CHUNK_SIZE: int = 32
20
+
21
+
22
+ @cute.jit
23
+ def call_mask_mod(
24
+ mask_mod: cutlass.Constexpr,
25
+ batch_idx,
26
+ head_idx,
27
+ q_idx,
28
+ kv_idx,
29
+ seqlen_info,
30
+ aux_data: AuxData,
31
+ ):
32
+ # Compatibility shim for pre-aux_scalars mask_mod callables.
33
+ if const_expr(aux_data.scalars is not None):
34
+ return mask_mod(
35
+ batch_idx,
36
+ head_idx,
37
+ q_idx,
38
+ kv_idx,
39
+ seqlen_info,
40
+ aux_data.tensors,
41
+ aux_data.scalars,
42
+ )
43
+ return mask_mod(
44
+ batch_idx,
45
+ head_idx,
46
+ q_idx,
47
+ kv_idx,
48
+ seqlen_info,
49
+ aux_data.tensors,
50
+ )
51
+
52
+
53
+ @cute.jit
54
+ def r2p_bitmask_below(limit: Int32, s: int) -> Uint32:
55
+ """32-bit R2P bitmask keeping positions < limit (exclusive upper bound).
56
+
57
+ Positions 0..limit-1 in chunk `s` get bit=1 (keep), the rest bit=0 (mask).
58
+ Uses inline PTX to avoid shift-by-type-width UB.
59
+ """
60
+ m = max((s + 1) * MASK_R2P_CHUNK_SIZE - limit, 0)
61
+ return utils.shr_u32(Uint32(0xFFFFFFFF), Uint32(m))
62
+
63
+
64
+ @cute.jit
65
+ def r2p_bitmask_above(limit: Int32, s: int) -> Uint32:
66
+ """32-bit R2P bitmask keeping positions >= limit (inclusive lower bound).
67
+
68
+ Positions limit..31 in chunk `s` get bit=1 (keep), the rest bit=0 (mask).
69
+ Uses inline PTX to avoid shift-by-type-width UB.
70
+ """
71
+ n = max(limit - s * MASK_R2P_CHUNK_SIZE, 0)
72
+ return utils.shl_u32(Uint32(0xFFFFFFFF), Uint32(n))
73
 
74
 
75
  @cute.jit
76
+ def mask_r2p_lambda(
77
+ X: cute.Tensor,
78
+ mask_gen_fn: cutlass.Constexpr[MaskGenFn],
79
+ rank1: bool = False,
80
+ ) -> None:
81
+ """Apply R2P masking with a custom bitmask generator.
82
+
83
+ mask_gen_fn(chunk_idx: constexpr int) -> Uint32:
84
+ Returns a 32-bit bitmask for the chunk. Bit i set means column
85
+ chunk_idx * chunk_size + i is KEPT; bit i clear means masked to -inf.
86
+ """
87
  ncol = const_expr(cute.size(X.shape[cute.rank(X) - 1]) if not rank1 else cute.size(X.shape))
88
+ # 32-column chunks. The mask_gen_fn returns a Uint32 bitmask (1=keep).
89
+ CHUNK_SIZE = MASK_R2P_CHUNK_SIZE
90
+ for s in cutlass.range_constexpr(cute.ceil_div(ncol, CHUNK_SIZE)):
91
+ mask = mask_gen_fn(s)
 
 
92
  # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
93
+ for i in cutlass.range_constexpr(min(CHUNK_SIZE, ncol - s * CHUNK_SIZE)):
94
+ in_bound = cutlass.Boolean(mask & (Uint32(1) << i))
95
+ c = s * CHUNK_SIZE + i
96
  if const_expr(rank1):
97
  X[c] = X[c] if in_bound else -Float32.inf
 
 
98
  else:
99
  for r in cutlass.range_constexpr(cute.size(X.shape[0])):
100
  X[r, c] = X[r, c] if in_bound else -Float32.inf
101
 
102
 
103
  @cute.jit
104
+ def sm90_col_to_r2p_idx(col_limit: Int32) -> Int32:
105
+ """Transform SM90 MMA column coordinate to R2P element index.
106
+
107
+ SM90 MMA accumulator column indices are non-contiguous: 0, 1, 8, 9, 16, 17, ...
108
+ Element indices are contiguous: 0, 1, 2, 3, 4, 5, ...
109
+ This converts a column-space threshold to element-space for r2p_bitmask_below/above.
110
+ """
111
+ return col_limit // 8 * 2 + min(col_limit % 8, 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
 
114
  @cute.jit
115
+ def row_to_r2p_idx(x: Int32, num_rep: int, num_wg: int) -> Int32:
116
+ """Convert a row coordinate to an R2P element index in the warp-group interleaved layout.
 
 
 
 
 
 
117
 
118
+ In the SM100 backward pass, 2 warp groups share TMEM. The TMEM load atom
119
+ distributes rows in an interleaved pattern: elements 0..num_rep-1 map to
120
+ rows 0..num_rep-1 (warp group 0), elements num_rep..2*num_rep-1 map to
121
+ rows num_rep*num_wg..num_rep*num_wg+num_rep-1 (warp group 1), and so on.
122
+ Row-coordinate thresholds (causal limits, window bounds, uih_len) must be
123
+ converted to element indices before use with r2p_bitmask_above/below.
124
 
125
+ Rows not owned by this thread (in the gap between warp groups) are clamped
126
+ to the boundary element index, which is safe because R2P thresholds are
127
+ monotonic.
128
 
129
+ Example with num_rep=16, num_wg=2:
130
+ row 0 -> elem 0, row 15 -> elem 15,
131
+ row 16 -> elem 16 (clamped), row 31 -> elem 16 (clamped),
132
+ row 32 -> elem 16, row 33 -> elem 17, row 47 -> elem 31.
133
+ """
134
+ return x // (num_rep * num_wg) * num_rep + min(x % (num_rep * num_wg), num_rep)
135
 
 
 
 
 
136
 
137
+ @cute.jit
138
+ def apply_packed_mask_chunk(
139
+ X: cute.Tensor,
140
+ chunk_idx: cutlass.Constexpr[int],
141
+ mask: Uint32,
142
+ ) -> None:
143
+ """Apply one 32-bit keep mask to one 32-column chunk.
144
+
145
+ The one-iteration chunk loop keeps the same lowering pattern as mask_r2p_lambda.
146
+ """
147
+ ncol = const_expr(cute.size(X.shape))
148
+ col_base = chunk_idx * MASK_R2P_CHUNK_SIZE
149
+ for s in cutlass.range_constexpr(1):
150
+ for i in cutlass.range_constexpr(
151
+ min(MASK_R2P_CHUNK_SIZE, ncol - col_base - s * MASK_R2P_CHUNK_SIZE)
152
+ ):
153
+ in_bound = cutlass.Boolean(mask & (Uint32(1) << i))
154
+ c = col_base + s * MASK_R2P_CHUNK_SIZE + i
155
  X[c] = X[c] if in_bound else -Float32.inf
156
 
157
 
 
186
  mask_causal: cutlass.Constexpr[bool],
187
  mask_local: cutlass.Constexpr[bool] = False,
188
  mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
189
+ aux_data: AuxData = AuxData(),
190
  fastdiv_mods=(None, None),
191
  ) -> None:
192
  assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
 
210
  seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
211
  if const_expr(not mask_causal and not mask_local and mask_mod is None):
212
  if const_expr(mask_seqlen):
213
+ r2p = const_expr(not self.swap_AB)
 
214
  if const_expr(not r2p):
215
  # traverse column index.
216
  for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
 
218
  for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
219
  acc_S_mn[r, c] = -Float32.inf if oob else acc_S_mn[r, c]
220
  else:
221
+ seqlenk_col_limit_r2p = sm90_col_to_r2p_idx(seqlenk_col_limit)
222
+ mask_r2p_lambda(acc_S_mn, lambda s: r2p_bitmask_below(seqlenk_col_limit_r2p, s))
223
 
224
  elif const_expr(
225
  not mask_causal and not mask_local and mask_mod is not None
 
232
  and fastdiv_mods[1] is not None
233
  )
234
  wrap_aux_indices = const_expr(
235
+ has_fastdiv and mask_seqlen and const_expr(aux_data.tensors is not None)
236
  )
237
 
238
  for r in cutlass.range_constexpr(nrow):
 
261
  head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
262
  q_idx_ssa = utils.scalar_to_ssa(row_for_mod, cutlass.Int32)
263
  kv_idx_ssa = utils.scalar_to_ssa(col_for_mod, cutlass.Int32)
264
+ mask_value = call_mask_mod(
265
+ mask_mod,
266
  batch_idx_ssa,
267
  head_idx_ssa,
268
  q_idx_ssa,
269
  kv_idx_ssa,
270
  self.seqlen_info,
271
+ aux_data,
272
  )
273
  cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
274
  if const_expr(mask_seqlen):
 
322
  else acc_S_mn[r, c]
323
  )
324
  else:
325
+ col_limit_r2p = sm90_col_to_r2p_idx(col_limit_right)
326
+ mask_r2p_lambda(
327
+ acc_S_mn[r, None],
328
+ lambda s: r2p_bitmask_below(col_limit_r2p, s),
329
+ rank1=True,
330
+ )
331
  else: # Local
332
  local_row_offset_right = (
333
  causal_row_offset + self.window_size_right
 
339
  if const_expr(self.window_size_left is not None)
340
  else None
341
  )
342
+ r2p_local = const_expr(not self.swap_AB)
343
  for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
344
  if const_expr(self.qhead_per_kvhead_packgqa == 1):
345
  row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
 
358
  if const_expr(self.window_size_left is not None)
359
  else 0
360
  )
361
+ if const_expr(not r2p_local):
362
+ # traverse column index.
363
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
364
+ col_idx = t0ScS_mn[0, c][1]
365
+ if col_idx >= col_limit_right or col_idx < col_limit_left:
366
+ acc_S_mn[r, c] = -Float32.inf
367
+ else:
368
+ col_limit_right_r2p = sm90_col_to_r2p_idx(col_limit_right)
369
+ col_limit_left_r2p = sm90_col_to_r2p_idx(col_limit_left)
370
+
371
+ def mask_gen_fn(s: int) -> Uint32:
372
+ return r2p_bitmask_below(
373
+ col_limit_right_r2p, s
374
+ ) & r2p_bitmask_above(col_limit_left_r2p, s)
375
+
376
+ mask_r2p_lambda(acc_S_mn[r, None], mask_gen_fn, rank1=True)
377
  else: # swap_AB
378
  assert self.qhead_per_kvhead_packgqa == 1
379
  thr_row_offset = tScS_mn[0][ROW]
 
403
  # column, by setting row limit to be self.tile_m.
404
  row_limit_top = (
405
  self.tile_m
406
+ if col0 >= seqlenk_col_limit and mask_seqlen
407
+ else (
408
+ col0 - causal_row_offset - self.window_size_right
409
+ if const_expr(self.window_size_right is not None)
410
+ else 0
411
+ )
412
+ )
413
+ row_limit_bot = (
414
+ col0 - causal_row_offset + self.window_size_left
415
+ if const_expr(self.window_size_left is not None)
416
+ else self.tile_m
417
  )
 
 
418
  for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
419
  row_idx = t0ScS_mn[r, 0][ROW]
420
  acc_S_mn[r, c] = (
 
423
  else acc_S_mn[r, c]
424
  )
425
 
426
+ @cute.jit
427
+ def apply_mask_mod_sm100_scalar(
428
+ self,
429
+ acc_S: cute.Tensor,
430
+ tScS_t2r: cute.Tensor,
431
+ m_block: Int32,
432
+ n_block: Int32,
433
+ mask_seqlen: cutlass.Constexpr[bool],
434
+ mask_mod: cutlass.Constexpr[Callable],
435
+ batch_idx: Int32,
436
+ head_idx: Int32,
437
+ aux_data: AuxData = AuxData(),
438
+ fastdiv_mods=(None, None),
439
+ head_divmod=None,
440
+ check_q_boundary: bool = False,
441
+ ) -> None:
442
+ """Apply a scalar FlexAttention mask_mod to an SM100 accumulator fragment.
443
+
444
+ Each accumulator lane calls mask_mod once with logical (batch, head, q, kv)
445
+ indices. Pack-GQA rows are converted back to logical q/head indices before
446
+ the call. When aux tensors are present, indices are wrapped with fastdiv so
447
+ mask_mod never reads outside the per-example auxiliary storage.
448
+ """
449
+ has_fastdiv = const_expr(
450
+ fastdiv_mods is not None and fastdiv_mods[0] is not None and fastdiv_mods[1] is not None
451
+ )
452
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
453
+ ncol = const_expr(cute.size(tScS_t2r.shape))
454
+
455
+ for i in cutlass.range(ncol, unroll_full=True):
456
+ row_coord = tScS_t2r[i][0] if not self.swap_AB else tScS_t2r[i][1]
457
+ col_coord = tScS_t2r[i][1] if not self.swap_AB else tScS_t2r[i][0]
458
+ global_row = row_coord + m_block * self.tile_m
459
+ global_col = col_coord + n_block * self.tile_n
460
+
461
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
462
+ assert head_divmod is not None
463
+ mask_row, head_offset = divmod(global_row, head_divmod)
464
+ head_idx_for_mod = head_idx * self.qhead_per_kvhead_packgqa + head_offset
465
+ else:
466
+ head_idx_for_mod = head_idx
467
+ mask_row = global_row
468
+
469
+ mask_row_for_mod = mask_row
470
+ if const_expr(has_fastdiv and aux_data.tensors is not None):
471
+ if check_q_boundary:
472
+ _, mask_row_for_mod = divmod(mask_row, fastdiv_mods[0])
473
+ global_col_for_mod = global_col
474
+ if const_expr(has_fastdiv and mask_seqlen and aux_data.tensors is not None):
475
+ _, global_col_for_mod = divmod(global_col, fastdiv_mods[1])
476
+
477
+ head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
478
+ mask_row_ssa = utils.scalar_to_ssa(mask_row_for_mod, cutlass.Int32)
479
+ kv_idx_ssa = utils.scalar_to_ssa(global_col_for_mod, cutlass.Int32)
480
+ mask_value = call_mask_mod(
481
+ mask_mod,
482
+ batch_idx_ssa,
483
+ head_idx_ssa,
484
+ mask_row_ssa,
485
+ kv_idx_ssa,
486
+ self.seqlen_info,
487
+ aux_data,
488
+ )
489
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
490
+ acc_S[i] = acc_S[i] if cond else -Float32.inf
491
+ if const_expr(mask_seqlen):
492
+ acc_S[i] = -Float32.inf if global_col >= self.seqlen_k else acc_S[i]
493
+ if check_q_boundary:
494
+ acc_S[i] = -Float32.inf if mask_row >= self.seqlen_q else acc_S[i]
495
+
496
+ @cute.jit
497
+ def apply_mask_mod_sm100_vector(
498
+ self,
499
+ acc_S: cute.Tensor,
500
+ tScS_t2r: cute.Tensor,
501
+ m_block: Int32,
502
+ n_block: Int32,
503
+ mask_seqlen: cutlass.Constexpr[bool],
504
+ mask_mod: cutlass.Constexpr[Callable],
505
+ batch_idx: Int32,
506
+ head_idx: Int32,
507
+ vec_size: cutlass.Constexpr[int],
508
+ aux_data: AuxData = AuxData(),
509
+ fastdiv_mods=(None, None),
510
+ head_divmod=None,
511
+ check_q_boundary: bool = False,
512
+ ) -> None:
513
+ """Apply a vectorized FlexAttention mask_mod to an SM100 fragment.
514
+
515
+ mask_mod receives vec_size adjacent KV indices for one logical q row and
516
+ returns bit-packed Uint32 keep masks. Low bits correspond to lower KV
517
+ indices. The packed masks are combined with sequence-boundary checks, then
518
+ applied in 32-column chunks so the final masking lowers to R2P.
519
+ """
520
+ has_fastdiv = const_expr(
521
+ fastdiv_mods is not None and fastdiv_mods[0] is not None and fastdiv_mods[1] is not None
522
+ )
523
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
524
+ ncol = const_expr(cute.size(tScS_t2r.shape))
525
+ mask_vals_per_apply = const_expr(max(1, vec_size // 32))
526
+ calls_per_apply = const_expr(max(1, 32 // vec_size))
527
+ n_calls = const_expr(cute.ceil_div(ncol, vec_size))
528
+ mask_vals = cute.make_rmem_tensor(mask_vals_per_apply, dtype=cutlass.Uint32)
529
+
530
+ # Accumulate enough vector mask_mod calls to produce 32-bit chunks that
531
+ # apply_packed_mask_chunk can lower to R2P.
532
+ for s in cutlass.range_constexpr(n_calls):
533
+ if const_expr(s % calls_per_apply == 0):
534
+ for c in cutlass.range_constexpr(mask_vals_per_apply):
535
+ mask_vals[c] = cutlass.Uint32(0)
536
+ i = s * vec_size
537
+ row_coord = tScS_t2r[i][0] if not self.swap_AB else tScS_t2r[i][1]
538
+ col_coord = tScS_t2r[i][1] if not self.swap_AB else tScS_t2r[i][0]
539
+ global_row = row_coord + m_block * self.tile_m
540
+ global_col = col_coord + n_block * self.tile_n
541
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
542
+ assert head_divmod is not None
543
+ mask_row, head_offset = divmod(global_row, head_divmod)
544
+ head_idx_for_mod = head_idx * self.qhead_per_kvhead_packgqa + head_offset
545
+ else:
546
+ head_idx_for_mod = head_idx
547
+ mask_row = global_row
548
+ mask_row_for_mod = mask_row
549
+ if const_expr(has_fastdiv and aux_data.tensors is not None):
550
+ if check_q_boundary:
551
+ _, mask_row_for_mod = divmod(mask_row, fastdiv_mods[0])
552
+
553
+ head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32).broadcast_to(
554
+ (vec_size,)
555
+ )
556
+ mask_row_ssa = utils.scalar_to_ssa(mask_row_for_mod, cutlass.Int32).broadcast_to(
557
+ (vec_size,)
558
+ )
559
+ batch_idx_ssa_call = batch_idx_ssa.broadcast_to((vec_size,))
560
+ kv_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
561
+
562
+ # Build the per-lane KV indices for this vectorized mask_mod call.
563
+ for j in cutlass.range_constexpr(min(vec_size, ncol - i)):
564
+ col_j_coord = tScS_t2r[i + j][1] if not self.swap_AB else tScS_t2r[i + j][0]
565
+ col_j_global = col_j_coord + n_block * self.tile_n
566
+ col_j_for_mod = col_j_global
567
+ if const_expr(has_fastdiv and mask_seqlen and aux_data.tensors is not None):
568
+ _, col_j_for_mod = divmod(col_j_global, fastdiv_mods[1])
569
+ kv_idx_vec[j] = col_j_for_mod
570
+ kv_idx_ssa = kv_idx_vec.load()
571
+
572
+ # mask_value is already bit-packed by the vectorized mask_mod.
573
+ mask_value = call_mask_mod(
574
+ mask_mod,
575
+ batch_idx_ssa_call,
576
+ head_idx_ssa,
577
+ mask_row_ssa,
578
+ kv_idx_ssa,
579
+ self.seqlen_info,
580
+ aux_data,
581
+ )
582
+
583
+ # For vec_size < 32, multiple mask_mod calls fill one R2P chunk.
584
+ bit_offset = const_expr((s % calls_per_apply) * vec_size)
585
+ seqlen_thresh_call = (
586
+ self.seqlen_k - global_col if const_expr(mask_seqlen) else cutlass.Int32(0)
587
+ )
588
+ q_in_bounds = mask_row < self.seqlen_q if check_q_boundary else cutlass.Boolean(True)
589
+ for c in cutlass.range_constexpr(mask_vals_per_apply):
590
+ mask_val = mask_value[c]
591
+ if const_expr(vec_size < 32):
592
+ lane_keep = utils.shr_u32(
593
+ cutlass.Uint32(0xFFFFFFFF),
594
+ cutlass.Uint32(32 - vec_size),
595
+ )
596
+ mask_val = mask_val & lane_keep
597
+ if const_expr(mask_seqlen):
598
+ mask_val = mask_val & r2p_bitmask_below(seqlen_thresh_call, c)
599
+ if check_q_boundary:
600
+ mask_val = mask_val if q_in_bounds else cutlass.Uint32(0)
601
+ mask_vals[c] = mask_vals[c] | (mask_val << bit_offset)
602
+
603
+ # Apply only when the 32-bit chunk is complete, or at the tile tail.
604
+ is_last_in_apply = const_expr(s % calls_per_apply == calls_per_apply - 1)
605
+ is_last_overall = const_expr(s == n_calls - 1)
606
+ if const_expr(is_last_in_apply or is_last_overall):
607
+ apply_idx = s // calls_per_apply
608
+ for c in cutlass.range_constexpr(mask_vals_per_apply):
609
+ chunk_idx = apply_idx * mask_vals_per_apply + c
610
+ # Skip packed chunks that start past the accumulator fragment.
611
+ if const_expr(chunk_idx * 32 < ncol):
612
+ apply_packed_mask_chunk(acc_S, chunk_idx, mask_vals[c])
613
+
614
  @cute.jit
615
  def apply_mask_sm100(
616
  self,
 
625
  mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
626
  batch_idx: Int32 = None,
627
  head_idx: Int32 = None,
628
+ aux_data: AuxData = AuxData(),
629
  fastdiv_mods=(None, None),
630
  head_divmod=None,
631
+ vec_size: cutlass.Constexpr[int] = 1,
632
  check_q_boundary: bool = False,
633
+ r2p: bool = True,
634
+ rBitmask: Optional[cute.Tensor] = None,
635
  ) -> None:
636
  assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
637
  acc_shape = (self.tile_m, self.tile_n)
 
645
  if n_block < 0:
646
  n_block = 0
647
  seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n
648
+
649
+ if const_expr(rBitmask is not None):
650
+ ncol_packed = const_expr(cute.size(rBitmask.shape[0]))
651
+ for i in cutlass.range_constexpr(ncol_packed):
652
+ col_start = 32 * i # mask is bit-packed into uint32
653
+ curr_mask_val = rBitmask[i]
654
+ for j in cutlass.range_constexpr(32):
655
+ curr_col = col_start + j
656
+ mask = (curr_mask_val >> j) & 1
657
+ acc_S[curr_col] = acc_S[curr_col] if cutlass.Boolean(mask) else -Float32.inf
658
+
659
+ elif const_expr(not mask_causal and not mask_local and mask_mod is None):
660
  if const_expr(mask_seqlen):
661
  if const_expr(not r2p):
662
  for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
 
665
  # For some reason the 2 lines above generate really bad SASS
666
  acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= seqlenk_col_limit else acc_S[i]
667
  else:
668
+ mask_r2p_lambda(
669
+ acc_S,
670
+ lambda s: r2p_bitmask_below(seqlenk_col_limit, s),
671
+ rank1=True,
672
+ )
673
 
674
  elif const_expr(not mask_causal and not mask_local and mask_mod is not None):
675
+ # FlexAttention mask_mod vectorization is gated on `mask_mod.__vec_size__`.
676
+ # vec_size == 1 returns a scalar Boolean. vec_size > 1 returns packed
677
+ # Uint32 mask fragments: one word per 32 evaluated columns.
678
+ assert vec_size % 32 == 0 or 32 % vec_size == 0, (
679
+ "vec_size must divide 32 or be a multiple of 32"
680
  )
681
+ if const_expr(vec_size == 1):
682
+ self.apply_mask_mod_sm100_scalar(
683
+ acc_S,
684
+ tScS_t2r,
685
+ m_block,
686
+ n_block,
687
+ mask_seqlen,
688
+ mask_mod,
689
+ batch_idx,
690
+ head_idx,
691
+ aux_data,
692
+ fastdiv_mods,
693
+ head_divmod,
694
+ check_q_boundary,
695
+ )
696
+ else:
697
+ self.apply_mask_mod_sm100_vector(
698
+ acc_S,
699
+ tScS_t2r,
700
+ m_block,
701
+ n_block,
702
+ mask_seqlen,
703
+ mask_mod,
704
+ batch_idx,
705
+ head_idx,
706
+ vec_size,
707
+ aux_data,
708
+ fastdiv_mods,
709
+ head_divmod,
710
+ check_q_boundary,
 
 
 
 
 
711
  )
 
 
 
 
 
 
712
 
713
  else: # Causal or local
714
+ causal_row_offset = self.seqlen_k - n_block * self.tile_n - self.seqlen_q
715
  row_idx = tScS_t2r[0][0] + m_block * self.tile_m
716
  if const_expr(self.qhead_per_kvhead_packgqa != 1):
717
  row_idx = row_idx // self.qhead_per_kvhead_packgqa
718
  if const_expr(mask_causal):
719
+ col_limit_right = row_idx + causal_row_offset + 1
720
  if const_expr(mask_seqlen):
721
  col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
722
  # if cute.arch.thread_idx()[0] % 32 == 0:
 
726
  for i in cutlass.range(ncol, unroll_full=True):
727
  acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= col_limit_right else acc_S[i]
728
  else:
729
+ mask_r2p_lambda(
730
+ acc_S,
731
+ lambda s: r2p_bitmask_below(col_limit_right, s),
732
+ rank1=True,
733
+ )
734
  else:
735
  local_row_offset_right = (
736
+ causal_row_offset + 1 + self.window_size_right
737
  if const_expr(self.window_size_right is not None)
738
  else None
739
  )
740
  local_row_offset_left = (
741
+ causal_row_offset - self.window_size_left
742
  if const_expr(self.window_size_left is not None)
743
  else None
744
  )
 
763
  else acc_S[i]
764
  )
765
  else:
766
+ # Dual-bound R2P masking for SM100.
767
+ # Masks elements where: NOT (col_limit_left <= col < col_limit_right)
768
+
769
+ def mask_gen_fn(s: int) -> Uint32:
770
+ return r2p_bitmask_below(col_limit_right, s) & r2p_bitmask_above(
771
+ col_limit_left, s
772
+ )
773
+
774
+ mask_r2p_lambda(acc_S, mask_gen_fn, rank1=True)
775
 
776
  @cute.jit
777
  def apply_mask_sm100_transposed(
 
787
  mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
788
  batch_idx: Int32 = None,
789
  head_idx: Int32 = None,
790
+ aux_data: AuxData = AuxData(),
791
  fastdiv_mods=(None, None),
792
  is_full_block: bool = False,
793
  check_m_boundary: bool = True,
 
795
  """
796
  Backward pass: mask S = K @ Q.T where n_block tiles seqlen_k and m_block tiles seqlen_q.
797
 
798
+ Coordinate convention:
799
  - ROW corresponds to Q (m_block)
800
  - COL corresponds to KV (n_block)
801
 
 
845
  and fastdiv_mods[1] is not None
846
  )
847
  wrap_aux_indices = const_expr(
848
+ has_fastdiv and mask_seqlen and const_expr(aux_data.tensors is not None)
849
  )
850
  batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
851
  head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32)
 
866
  q_idx_ssa = utils.scalar_to_ssa(q_idx_for_mod, cutlass.Int32)
867
  kv_idx_ssa = utils.scalar_to_ssa(kv_idx_for_mod, cutlass.Int32)
868
 
869
+ mask_value = call_mask_mod(
870
+ mask_mod,
871
  batch_idx_ssa,
872
  head_idx_ssa,
873
  q_idx_ssa,
874
  kv_idx_ssa,
875
  self.seqlen_info,
876
+ aux_data,
877
  )
878
  cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
879
  acc_S[i] = acc_S[i] if cond else -cutlass.Float32.inf
 
912
  )
913
  else:
914
  num_rep = cute.size(tScS_t2r, mode=[0]) # 16 or 32
915
+ num_wg = 2
916
+ row_limit = row_to_r2p_idx(row_limit_top, num_rep, num_wg)
917
+ mask_r2p_lambda(
918
+ acc_S,
919
+ lambda s: r2p_bitmask_above(row_limit, s),
920
+ rank1=True,
921
+ )
922
  else:
923
  if const_expr(self.window_size_right is not None):
924
  row_limit_top = causal_offset - self.window_size_right
 
929
  if const_expr(mask_seqlen):
930
  if seqlenk_col_limit <= 0:
931
  row_limit_top = self.tile_m
932
+ r2p = True
933
+ if const_expr(not r2p):
934
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
935
+ row_idx = t0ScS_t2r[i][ROW]
936
+ local_mask = row_idx < row_limit_top
937
+ if const_expr(self.window_size_left is not None):
938
+ local_mask |= row_idx > row_limit_bot
939
+ acc_S[i] = -cutlass.Float32.inf if local_mask else acc_S[i]
940
+ else:
941
+
942
+ def mask_gen_fn(s: int) -> Uint32:
943
+ num_rep = cute.size(tScS_t2r, mode=[0])
944
+ num_wg = 2
945
+
946
+ row_limit = row_to_r2p_idx(row_limit_top, num_rep, num_wg)
947
+ mask = r2p_bitmask_above(row_limit, s)
948
+
949
+ if const_expr(self.window_size_left is not None):
950
+ row_limit_bottom = row_to_r2p_idx(row_limit_bot + 1, num_rep, num_wg)
951
+ mask = mask & r2p_bitmask_below(row_limit_bottom, s)
952
+
953
+ return mask
954
+
955
+ mask_r2p_lambda(
956
+ acc_S,
957
+ mask_gen_fn,
958
+ rank1=True,
959
+ )
960
+
961
+
962
+ # -----------------------------------------------------------------------------
963
+ # SM100 FMHA fused-mask policy layer (separate from generic mask primitives).
964
+ # -----------------------------------------------------------------------------
965
+
966
+
967
+ class Sm100MaskEnum(enum.Enum):
968
+ """Enumeration of mask types for FMHA operations.
969
+
970
+ - RESIDUAL_MASK: Residual mask for handling variable sequence lengths
971
+ - WINDOW_MASK: Window mask for attention which also includes causal and no mask
972
+ - WINDOW_MASK_INFERENCE: Same as the window mask, but has the limitation that the end of q is aligned with the end of k
973
+ - WINDOW_MASK_BWD: Window mask for backward pass
974
+ - WINDOW_MASK_BWD_INFERENCE: Same as the window mask for backward pass, but has the limitation that the end of q is aligned with the end of k
975
+ """
976
+
977
+ NO_MASK = enum.auto()
978
+ RESIDUAL_MASK = enum.auto()
979
+ CAUSAL_MASK = enum.auto()
980
+ WINDOW_MASK = enum.auto()
981
+ WINDOW_MASK_INFERENCE = enum.auto()
982
+ # Deprecated the following types
983
+ WINDOW_MASK_BWD = enum.auto()
984
+ WINDOW_MASK_BWD_INFERENCE = enum.auto()
985
+ RESIDUAL_MASK_BWD = enum.auto()
986
+
987
+
988
+ class Sm100FusedMask:
989
+ """A fused mask implementation for FMHA operations.
990
+
991
+ This class handles different types of attention masks including no mask,
992
+ residual mask for variable sequence lengths, and causal mask for
993
+ autoregressive attention patterns.
994
+
995
+ The class provides methods to:
996
+ - Calculate trip counts for different mask types
997
+ - Apply masks to attention scores
998
+ - Handle masked and unmasked trip calculations
999
+ """
1000
+
1001
+ def get_trip_count(
1002
+ mask_type: Sm100MaskEnum,
1003
+ blk_coord: cute.Coord,
1004
+ tile_shape: cute.Shape,
1005
+ seqlen_q: Int32,
1006
+ seqlen_k: Int32,
1007
+ window_size_left: Optional[Int32] = None,
1008
+ window_size_right: Optional[Int32] = None,
1009
+ ) -> Int32:
1010
+ """
1011
+ Calculate the number of trips needed for the current block.
1012
+
1013
+ The trip count depends on the mask type and the block coordinates.
1014
+ For causal masks, it considers the autoregressive constraint.
1015
+
1016
+ :param mask_type: Type of mask to use
1017
+ :type mask_type: utils.Sm100MaskEnum
1018
+ :param blk_coord: Block coordinates.
1019
+ :type blk_coord: cute.Coord
1020
+ :param tile_shape: Shape of the tile.
1021
+ :type tile_shape: cute.Shape
1022
+ :param seqlen_q: Query sequence length for attention computation.
1023
+ :type seqlen_q: Int32
1024
+ :param seqlen_k: Key sequence length for attention computation.
1025
+ :type seqlen_k: Int32
1026
+ :param window_size_left: Left-side sliding window size for attention masking.
1027
+ :type window_size_left: Optional[Int32]
1028
+ :param window_size_right: Right-side sliding window size for attention masking.
1029
+ :type window_size_right: Optional[Int32]
1030
+
1031
+ :return: Number of trips needed.
1032
+ :rtype: Int32
1033
+ """
1034
+ result = 0
1035
+ offset = 0
1036
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE):
1037
+ offset = seqlen_k - seqlen_q
1038
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE):
1039
+ offset = seqlen_q - seqlen_k
1040
+ if cutlass.const_expr(mask_type == Sm100MaskEnum.RESIDUAL_MASK):
1041
+ result = cute.ceil_div(seqlen_k, tile_shape[1])
1042
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.RESIDUAL_MASK_BWD):
1043
+ result = cute.ceil_div(seqlen_q, tile_shape[0])
1044
+ if cutlass.const_expr(
1045
+ mask_type == Sm100MaskEnum.WINDOW_MASK
1046
+ or mask_type == Sm100MaskEnum.WINDOW_MASK_INFERENCE
1047
+ ):
1048
+ if cutlass.const_expr(window_size_right is None):
1049
+ result = cute.ceil_div(seqlen_k, tile_shape[1])
1050
+ else:
1051
+ max_idx_q = (blk_coord[0] + 1) * tile_shape[0]
1052
+ idx_k = max_idx_q + offset + window_size_right
1053
+ tmp_blocks_k = cute.ceil_div(idx_k, tile_shape[1])
1054
+ max_blocks_k = cute.ceil_div(seqlen_k, tile_shape[1])
1055
+ result = dsl_min(max_blocks_k, tmp_blocks_k)
1056
+ if cutlass.const_expr(
1057
+ mask_type == Sm100MaskEnum.WINDOW_MASK_BWD
1058
+ or mask_type == Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE
1059
+ ):
1060
+ if cutlass.const_expr(window_size_left is None):
1061
+ result = cute.ceil_div(seqlen_q, tile_shape[0])
1062
+ else:
1063
+ max_idx_k = (blk_coord[1] + 1) * tile_shape[1]
1064
+ idx_k = max_idx_k + offset + window_size_left
1065
+ tmp_blocks_q = cute.ceil_div(idx_k, tile_shape[0])
1066
+ max_blocks_q = cute.ceil_div(seqlen_q, tile_shape[0])
1067
+ result = dsl_min(max_blocks_q, tmp_blocks_q)
1068
+ start_block = Sm100FusedMask.get_trip_start(
1069
+ mask_type,
1070
+ blk_coord,
1071
+ tile_shape,
1072
+ seqlen_q,
1073
+ seqlen_k,
1074
+ window_size_left,
1075
+ window_size_right,
1076
+ )
1077
+ result = result - start_block
1078
+ return result
1079
+
1080
+ @cute.jit
1081
+ def get_trip_start_count_via_block_info(
1082
+ blk_coord: cute.Coord,
1083
+ tile_shape: cute.Shape,
1084
+ seqlen_q: Int32,
1085
+ seqlen_k: Int32,
1086
+ is_causal: cutlass.Constexpr[bool] = False,
1087
+ is_local: cutlass.Constexpr[bool] = False,
1088
+ window_size_left: Optional[Int32] = None,
1089
+ window_size_right: Optional[Int32] = None,
1090
+ ) -> Tuple[Int32, Int32]:
1091
+ block_info = BlockInfo(
1092
+ tile_m=tile_shape[0],
1093
+ tile_n=tile_shape[1],
1094
+ is_causal=is_causal,
1095
+ is_local=is_local and not is_causal,
1096
+ window_size_left=window_size_left,
1097
+ window_size_right=window_size_right,
1098
+ )
1099
+
1100
+ seqlen_info = SeqlenInfoQK(
1101
+ offset_q=Int32(0),
1102
+ offset_k=Int32(0),
1103
+ padded_offset_q=Int32(0),
1104
+ padded_offset_k=Int32(0),
1105
+ seqlen_q=seqlen_q,
1106
+ seqlen_k=seqlen_k,
1107
+ m_block_offset=Int32(0),
1108
+ block_idx_offset=Int32(0),
1109
+ num_n_blocks=cute.ceil_div(seqlen_k, tile_shape[1]),
1110
+ has_cu_seqlens_q=False,
1111
+ has_cu_seqlens_k=False,
1112
+ has_seqused_q=False,
1113
+ has_seqused_k=False,
1114
+ has_cu_block_idx_offsets=False,
1115
+ )
1116
+ n_block_min, n_block_max = block_info.get_n_block_min_max(seqlen_info, blk_coord[0])
1117
+ return n_block_min, n_block_max - n_block_min
1118
+
1119
+ @cute.jit
1120
+ def get_trip_mask_bounds_via_block_info(
1121
+ blk_coord: cute.Coord,
1122
+ tile_shape: cute.Shape,
1123
+ seqlen_q: Int32,
1124
+ seqlen_k: Int32,
1125
+ is_causal: cutlass.Constexpr[bool] = False,
1126
+ is_local: cutlass.Constexpr[bool] = False,
1127
+ window_size_left: Optional[Int32] = None,
1128
+ window_size_right: Optional[Int32] = None,
1129
+ ) -> Tuple[Int32, Int32]:
1130
+ """Return SM100-style mask boundaries for dense iteration.
1131
+
1132
+ Returns:
1133
+ - n_block_min_causal_local_mask: right-side masked region start
1134
+ - n_block_min_before_local_mask: start of fully unmasked middle region
1135
+ """
1136
+ block_info = BlockInfo(
1137
+ tile_m=tile_shape[0],
1138
+ tile_n=tile_shape[1],
1139
+ is_causal=is_causal,
1140
+ is_local=is_local and not is_causal,
1141
+ window_size_left=window_size_left,
1142
+ window_size_right=window_size_right,
1143
+ )
1144
+ seqlen_info = SeqlenInfoQK(
1145
+ offset_q=Int32(0),
1146
+ offset_k=Int32(0),
1147
+ padded_offset_q=Int32(0),
1148
+ padded_offset_k=Int32(0),
1149
+ seqlen_q=seqlen_q,
1150
+ seqlen_k=seqlen_k,
1151
+ m_block_offset=Int32(0),
1152
+ block_idx_offset=Int32(0),
1153
+ num_n_blocks=cute.ceil_div(seqlen_k, tile_shape[1]),
1154
+ has_cu_seqlens_q=False,
1155
+ has_cu_seqlens_k=False,
1156
+ has_seqused_q=False,
1157
+ has_seqused_k=False,
1158
+ has_cu_block_idx_offsets=False,
1159
+ )
1160
+ n_block_min, _ = block_info.get_n_block_min_max(seqlen_info, blk_coord[0])
1161
+ n_block_min_causal_local_mask = block_info.get_n_block_min_causal_local_mask(
1162
+ seqlen_info, blk_coord[0], n_block_min
1163
+ )
1164
+ n_block_min_before_local_mask = block_info.get_n_block_min_before_local_mask(
1165
+ seqlen_info, blk_coord[0], n_block_min
1166
+ )
1167
+ return n_block_min_causal_local_mask, n_block_min_before_local_mask
1168
+
1169
+ @cute.jit
1170
+ def get_trip_start(
1171
+ mask_type: Sm100MaskEnum,
1172
+ blk_coord: cute.Coord,
1173
+ tile_shape: cute.Shape,
1174
+ seqlen_q: Int32,
1175
+ seqlen_k: Int32,
1176
+ window_size_left: Optional[Int32] = None,
1177
+ window_size_right: Optional[Int32] = None,
1178
+ ) -> Int32:
1179
+ """
1180
+ Get the start of the trip for the current block.
1181
+
1182
+ :param mask_type: Type of mask to use
1183
+ :type mask_type: utils.Sm100MaskEnum
1184
+ :param blk_coord: Block coordinates.
1185
+ :type blk_coord: cute.Coord
1186
+ :param tile_shape: Shape of the tile.
1187
+ :type tile_shape: cute.Shape
1188
+ :param seqlen_q: Query sequence length for attention computation.
1189
+ :type seqlen_q: Int32
1190
+ :param seqlen_k: Key sequence length for attention computation.
1191
+ :type seqlen_k: Int32
1192
+ :param window_size_left: Left-side sliding window size for attention masking.
1193
+ :type window_size_left: Optional[Int32]
1194
+ :param window_size_right: Right-side sliding window size for attention masking.
1195
+ :type window_size_right: Optional[Int32]
1196
+ """
1197
+ result = 0
1198
+ offset = 0
1199
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE):
1200
+ offset = seqlen_k - seqlen_q
1201
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE):
1202
+ offset = seqlen_q - seqlen_k
1203
+ if cutlass.const_expr(
1204
+ mask_type is Sm100MaskEnum.WINDOW_MASK
1205
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE
1206
+ ):
1207
+ if cutlass.const_expr(window_size_left is not None):
1208
+ min_idx_q = blk_coord[0] * tile_shape[0]
1209
+ idx_k = min_idx_q + offset - window_size_left
1210
+ tmp_blocks_k = idx_k // tile_shape[1]
1211
+ result = max(tmp_blocks_k, result)
1212
+ if cutlass.const_expr(
1213
+ mask_type is Sm100MaskEnum.WINDOW_MASK_BWD
1214
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE
1215
+ ):
1216
+ if cutlass.const_expr(window_size_right is not None):
1217
+ min_idx_k = blk_coord[1] * tile_shape[1]
1218
+ idx_q = min_idx_k + offset - window_size_right
1219
+ tmp_blocks_q = idx_q // tile_shape[0]
1220
+ result = max(tmp_blocks_q, result)
1221
+ return result
1222
+
1223
+ @cute.jit
1224
+ def get_leading_mask_id(
1225
+ mask_type: Sm100MaskEnum,
1226
+ blk_coord: cute.Coord,
1227
+ tile_shape: cute.Shape,
1228
+ seqlen_q: Int32,
1229
+ seqlen_k: Int32,
1230
+ window_size_left: Optional[Int32] = None,
1231
+ window_size_right: Optional[Int32] = None,
1232
+ ) -> Tuple[Int32, Int32]:
1233
+ """
1234
+ Get the begin and end tile idx for the leading mask.
1235
+
1236
+ :param mask_type: Type of mask to use
1237
+ :type mask_type: utils.Sm100MaskEnum
1238
+ :param blk_coord: Block coordinates.
1239
+ :type blk_coord: cute.Coord
1240
+ :param tile_shape: Shape of the tile.
1241
+ :type tile_shape: cute.Shape
1242
+ :param seqlen_q: Query sequence length for attention computation.
1243
+ :type seqlen_q: Int32
1244
+ :param seqlen_k: Key sequence length for attention computation.
1245
+ :type seqlen_k: Int32
1246
+ :param window_size_left: Left-side sliding window size for attention masking.
1247
+ :type window_size_left: Optional[Int32]
1248
+ :param window_size_right: Right-side sliding window size for attention masking.
1249
+ :type window_size_right: Optional[Int32]
1250
+
1251
+ :return: Tuple of (begin, end) tile idx for the leading mask.
1252
+ :rtype: Tuple[Int32, Int32]
1253
+ """
1254
+ offset = 0
1255
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE):
1256
+ offset = seqlen_k - seqlen_q
1257
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE):
1258
+ offset = seqlen_q - seqlen_k
1259
+ leading_mask_begin = Sm100FusedMask.get_trip_start(
1260
+ mask_type,
1261
+ blk_coord,
1262
+ tile_shape,
1263
+ seqlen_q,
1264
+ seqlen_k,
1265
+ window_size_left,
1266
+ window_size_right,
1267
+ )
1268
+ trip_count = Sm100FusedMask.get_trip_count(
1269
+ mask_type,
1270
+ blk_coord,
1271
+ tile_shape,
1272
+ seqlen_q,
1273
+ seqlen_k,
1274
+ window_size_left,
1275
+ window_size_right,
1276
+ )
1277
+
1278
+ leading_mask_end = leading_mask_begin
1279
+ if cutlass.const_expr(
1280
+ mask_type is Sm100MaskEnum.WINDOW_MASK
1281
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE
1282
+ ):
1283
+ if cutlass.const_expr(window_size_left is not None):
1284
+ min_idx_q = (blk_coord[0] + 1) * tile_shape[0] + offset - window_size_left
1285
+ leading_mask_end = dsl_min(
1286
+ cute.ceil_div(min_idx_q, tile_shape[1]) - 1,
1287
+ trip_count + leading_mask_begin - 1,
1288
+ )
1289
+ else:
1290
+ leading_mask_end = leading_mask_begin - 1
1291
+ elif cutlass.const_expr(
1292
+ mask_type is Sm100MaskEnum.WINDOW_MASK_BWD
1293
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE
1294
+ ):
1295
+ if cutlass.const_expr(window_size_right is not None):
1296
+ min_idx_k = (blk_coord[1] + 1) * tile_shape[1] + offset - window_size_right
1297
+ leading_mask_end = cute.ceil_div(min_idx_k, tile_shape[0]) - 1
1298
+ else:
1299
+ leading_mask_end = leading_mask_begin - 1
1300
+ return leading_mask_begin, leading_mask_end
1301
+
1302
+ @cute.jit
1303
+ def get_trailing_mask_id(
1304
+ mask_type: Sm100MaskEnum,
1305
+ blk_coord: cute.Coord,
1306
+ tile_shape: cute.Shape,
1307
+ seqlen_q: Int32,
1308
+ seqlen_k: Int32,
1309
+ window_size_left: Optional[Int32] = None,
1310
+ window_size_right: Optional[Int32] = None,
1311
+ ) -> Tuple[Optional[Int32], Optional[Int32]]:
1312
+ """
1313
+ Get the begin and end tile idx for the trailing mask.
1314
+
1315
+ :param mask_type: Type of mask to use
1316
+ :type mask_type: utils.Sm100MaskEnum
1317
+ :param blk_coord: Block coordinates.
1318
+ :type blk_coord: cute.Coord
1319
+ :param tile_shape: Shape of the tile.
1320
+ :type tile_shape: cute.Shape
1321
+ :param seqlen_q: Query sequence length for attention computation.
1322
+ :type seqlen_q: Int32
1323
+ :param seqlen_k: Key sequence length for attention computation.
1324
+ :type seqlen_k: Int32
1325
+ :param window_size_left: Left-side sliding window size for attention masking.
1326
+ :type window_size_left: Optional[Int32]
1327
+ :param window_size_right: Right-side sliding window size for attention masking.
1328
+ :type window_size_right: Optional[Int32]
1329
+
1330
+ :return: Tuple of (begin, end) tile idx for the trailing mask.
1331
+ :rtype: Tuple[Int32, Int32]
1332
+ """
1333
+ offset = 0
1334
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE):
1335
+ offset = seqlen_k - seqlen_q
1336
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE):
1337
+ offset = seqlen_q - seqlen_k
1338
+ trip_start = Sm100FusedMask.get_trip_start(
1339
+ mask_type,
1340
+ blk_coord,
1341
+ tile_shape,
1342
+ seqlen_q,
1343
+ seqlen_k,
1344
+ window_size_left,
1345
+ window_size_right,
1346
+ )
1347
+ trip_count = Sm100FusedMask.get_trip_count(
1348
+ mask_type,
1349
+ blk_coord,
1350
+ tile_shape,
1351
+ seqlen_q,
1352
+ seqlen_k,
1353
+ window_size_left,
1354
+ window_size_right,
1355
+ )
1356
+
1357
+ trailing_mask_begin, trailing_mask_end = None, None
1358
+ if cutlass.const_expr(
1359
+ mask_type is Sm100MaskEnum.WINDOW_MASK
1360
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE
1361
+ ):
1362
+ if cutlass.const_expr(window_size_right is not None):
1363
+ min_idx_q = blk_coord[0] * tile_shape[0] + offset + window_size_right
1364
+ trailing_mask_begin = dsl_min(
1365
+ min_idx_q // tile_shape[1], trip_count + trip_start - 1
1366
+ )
1367
+ trailing_mask_end = trip_count + trip_start - 1
1368
+ else:
1369
+ # last tile, we always apply mask on it regardless whether it's a residual tile
1370
+ trailing_mask_begin = trip_count + trip_start - 1
1371
+ trailing_mask_end = trip_count + trip_start - 1
1372
+ else:
1373
+ if cutlass.const_expr(window_size_left is not None):
1374
+ min_idx_k = blk_coord[1] * tile_shape[1] + offset + window_size_left + 1
1375
+ max_idx_k = (blk_coord[1] + 1) * tile_shape[1] + offset + window_size_left
1376
+ trailing_mask_begin = dsl_min(
1377
+ cute.ceil_div(min_idx_k, tile_shape[0]) - 1,
1378
+ trip_count + trip_start - 1,
1379
+ )
1380
+ trailing_mask_end = dsl_min(
1381
+ cute.ceil_div(max_idx_k, tile_shape[0]) - 1,
1382
+ trip_count + trip_start - 1,
1383
+ )
1384
+ else:
1385
+ # last tile, we always apply mask on it regardless whether it's a residual tile
1386
+ trailing_mask_begin = trip_count + trip_start - 1
1387
+ trailing_mask_end = trip_count + trip_start - 1
1388
+
1389
+ return trailing_mask_begin, trailing_mask_end
1390
+
1391
+ @cute.jit
1392
+ def get_masked_leading_count(
1393
+ mask_type: Sm100MaskEnum,
1394
+ blk_coord: cute.Coord,
1395
+ tile_shape: cute.Shape,
1396
+ seqlen_q: Int32,
1397
+ seqlen_k: Int32,
1398
+ window_size_left: Optional[Int32] = None,
1399
+ window_size_right: Optional[Int32] = None,
1400
+ ) -> Int32:
1401
+ """
1402
+ Calculate the number of masked trips for the leading mask.
1403
+
1404
+ This is used for blocks that need special handling due to masking.
1405
+
1406
+ :param mask_type: Type of mask to use
1407
+ :type mask_type: utils.Sm100MaskEnum
1408
+ :param blk_coord: Block coordinates.
1409
+ :type blk_coord: cute.Coord
1410
+ :param tile_shape: Shape of the tile.
1411
+ :type tile_shape: cute.Shape
1412
+ :param seqlen_q: Query sequence length for attention computation.
1413
+ :type seqlen_q: Int32
1414
+ :param seqlen_k: Key sequence length for attention computation.
1415
+ :type seqlen_k: Int32
1416
+ :param window_size_left: Left-side sliding window size for attention masking.
1417
+ :type window_size_left: Optional[Int32]
1418
+ :param window_size_right: Right-side sliding window size for attention masking.
1419
+ :type window_size_right: Optional[Int32]
1420
+
1421
+ :return: Number of masked trips.
1422
+ :rtype: Int32
1423
+ """
1424
+ result = 0
1425
+ if cutlass.const_expr(
1426
+ mask_type is not Sm100MaskEnum.RESIDUAL_MASK
1427
+ and mask_type is not Sm100MaskEnum.RESIDUAL_MASK_BWD
1428
+ ):
1429
+ if cutlass.const_expr(window_size_left is not None or window_size_right is not None):
1430
+ leading_mask_begin, leading_mask_end = Sm100FusedMask.get_leading_mask_id(
1431
+ mask_type,
1432
+ blk_coord,
1433
+ tile_shape,
1434
+ seqlen_q,
1435
+ seqlen_k,
1436
+ window_size_left,
1437
+ window_size_right,
1438
+ )
1439
+ result = max(leading_mask_end - leading_mask_begin + 1, 0)
1440
+
1441
+ return result
1442
+
1443
+ @cute.jit
1444
+ def get_masked_trailing_count(
1445
+ mask_type: Sm100MaskEnum,
1446
+ blk_coord: cute.Coord,
1447
+ tile_shape: cute.Shape,
1448
+ seqlen_q: Int32,
1449
+ seqlen_k: Int32,
1450
+ window_size_left: Optional[Int32] = None,
1451
+ window_size_right: Optional[Int32] = None,
1452
+ rem_count: Optional[Int32] = 0,
1453
+ ) -> Int32:
1454
+ """
1455
+ Calculate the number of masked trips for the trailing mask.
1456
+
1457
+ This is used for blocks that need special handling due to masking.
1458
+
1459
+ :param mask_type: Type of mask to use
1460
+ :type mask_type: utils.Sm100MaskEnum
1461
+ :param blk_coord: Block coordinates.
1462
+ :type blk_coord: cute.Coord
1463
+ :param tile_shape: Shape of the tile.
1464
+ :type tile_shape: cute.Shape
1465
+ :param seqlen_q: Query sequence length for attention computation.
1466
+ :type seqlen_q: Int32
1467
+ :param seqlen_k: Key sequence length for attention computation.
1468
+ :type seqlen_k: Int32
1469
+ :param window_size_left: Left-side sliding window size for attention masking.
1470
+ :type window_size_left: Optional[Int32]
1471
+ :param window_size_right: Right-side sliding window size for attention masking.
1472
+ :type window_size_right: Optional[Int32]
1473
+ :param rem_count: Remaining count from previous calculations.
1474
+ :type rem_count: Int32
1475
+
1476
+ :return: Number of masked trips.
1477
+ :rtype: Int32
1478
+ """
1479
+ result = 0
1480
+
1481
+ if cutlass.const_expr(
1482
+ mask_type is not Sm100MaskEnum.RESIDUAL_MASK
1483
+ and mask_type is not Sm100MaskEnum.RESIDUAL_MASK_BWD
1484
+ ):
1485
+ if cutlass.const_expr(window_size_left is not None or window_size_right is not None):
1486
+ trailing_mask_begin, trailing_mask_end = Sm100FusedMask.get_trailing_mask_id(
1487
+ mask_type,
1488
+ blk_coord,
1489
+ tile_shape,
1490
+ seqlen_q,
1491
+ seqlen_k,
1492
+ window_size_left,
1493
+ window_size_right,
1494
+ )
1495
+ leading_mask_begin, leading_mask_end = Sm100FusedMask.get_leading_mask_id(
1496
+ mask_type,
1497
+ blk_coord,
1498
+ tile_shape,
1499
+ seqlen_q,
1500
+ seqlen_k,
1501
+ window_size_left,
1502
+ window_size_right,
1503
+ )
1504
+ if cutlass.const_expr(
1505
+ trailing_mask_begin is not None and trailing_mask_end is not None
1506
+ ):
1507
+ if trailing_mask_begin <= leading_mask_end:
1508
+ result = max(trailing_mask_end - leading_mask_end, 0)
1509
+ else:
1510
+ result = max(trailing_mask_end - trailing_mask_begin + 1, 0)
1511
+ else:
1512
+ if seqlen_k % tile_shape[1] != 0:
1513
+ result = 1
1514
+ else:
1515
+ result = 0
1516
+
1517
+ return result + rem_count
1518
+
1519
+ @cute.jit
1520
+ def get_unmasked_trip_count(
1521
+ mask_type: Sm100MaskEnum,
1522
+ blk_coord: cute.Coord,
1523
+ tile_shape: cute.Shape,
1524
+ seqlen_q: Int32,
1525
+ seqlen_k: Int32,
1526
+ window_size_left: Optional[Int32] = None,
1527
+ window_size_right: Optional[Int32] = None,
1528
+ ) -> Int32:
1529
+ """
1530
+ Calculate the number of unmasked trips for the current block.
1531
+
1532
+ This represents the number of trips that don't require special
1533
+ masking treatment.
1534
+
1535
+ :param mask_type: Type of mask to use
1536
+ :type mask_type: utils.Sm100MaskEnum
1537
+ :param blk_coord: Block coordinates.
1538
+ :type blk_coord: cute.Coord
1539
+ :param tile_shape: Shape of the tile.
1540
+ :type tile_shape: cute.Shape
1541
+ :param seqlen_q: Query sequence length for attention computation.
1542
+ :type seqlen_q: Int32
1543
+ :param seqlen_k: Key sequence length for attention computation.
1544
+ :type seqlen_k: Int32
1545
+ :param window_size_left: Left-side sliding window size for attention masking.
1546
+ :type window_size_left: Optional[Int32]
1547
+ :param window_size_right: Right-side sliding window size for attention masking.
1548
+ :type window_size_right: Optional[Int32]
1549
+
1550
+ :return: Number of unmasked trips.
1551
+ :rtype: Int32
1552
+ """
1553
+ result = (
1554
+ Sm100FusedMask.get_trip_count(
1555
+ mask_type,
1556
+ blk_coord,
1557
+ tile_shape,
1558
+ seqlen_q,
1559
+ seqlen_k,
1560
+ window_size_left,
1561
+ window_size_right,
1562
+ )
1563
+ - Sm100FusedMask.get_masked_leading_count(
1564
+ mask_type,
1565
+ blk_coord,
1566
+ tile_shape,
1567
+ seqlen_q,
1568
+ seqlen_k,
1569
+ window_size_left,
1570
+ window_size_right,
1571
+ )
1572
+ - Sm100FusedMask.get_masked_trailing_count(
1573
+ mask_type,
1574
+ blk_coord,
1575
+ tile_shape,
1576
+ seqlen_q,
1577
+ seqlen_k,
1578
+ window_size_left,
1579
+ window_size_right,
1580
+ 0,
1581
+ )
1582
+ )
1583
+ return result
1584
+
1585
+ @cute.jit
1586
+ def apply_mask(
1587
+ mask_type: Sm100MaskEnum,
1588
+ acc_qk: cute.Tensor,
1589
+ index_qk: cute.Tensor,
1590
+ seqlen_q: Int32,
1591
+ seqlen_k: Int32,
1592
+ window_size_left: Optional[int] = None,
1593
+ window_size_right: Optional[int] = None,
1594
+ index_transform: cutlass.Constexpr = lambda index_q, index_k: (
1595
+ index_q,
1596
+ index_k,
1597
+ ),
1598
+ ):
1599
+ """
1600
+ Apply the appropriate mask to the attention scores.
1601
+
1602
+ This method modifies the attention scores (acc_qk) based on the mask type
1603
+ and the positions in the index tensor.
1604
+
1605
+ :param mask_type: Type of mask to use
1606
+ :type mask_type: utils.Sm100MaskEnum
1607
+ :param acc_qk: Accumulated QK attention scores tensor.
1608
+ :type acc_qk: cute.Tensor
1609
+ :param index_qk: Index tensor containing position information.
1610
+ :type index_qk: cute.Tensor
1611
+ :param seqlen_k: Key sequence length for attention computation.
1612
+ :type seqlen_k: Int32
1613
+ :param seqlen_q: Query sequence length for attention computation.
1614
+ :type seqlen_q: Optional[int]
1615
+ :param window_size_left: Left-side sliding window size for attention masking.
1616
+ :type window_size_left: Optional[int]
1617
+ :param window_size_right: Right-side sliding window size for attention masking.
1618
+ :type window_size_right: Optional[int]
1619
+ """
1620
+ offset = 0
1621
+ # NOTE: causal masking in this repo aligns the *end* of Q with the *end* of K
1622
+ # when seqlen_k != seqlen_q (same as the test/reference implementation):
1623
+ # k_index <= q_index + (seqlen_k - seqlen_q) + window_right
1624
+ # In our kernels, causal is represented by (window_left is None, window_right is not None).
1625
+ if cutlass.const_expr(window_size_left is None and window_size_right is not None):
1626
+ offset = seqlen_k - seqlen_q
1627
+ elif cutlass.const_expr(
1628
+ mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE
1629
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE
1630
+ ):
1631
+ offset = seqlen_k - seqlen_q
1632
+ for i in cutlass.range_constexpr(cute.size(acc_qk), unroll_full=True):
1633
+ index_q, index_k = index_transform(*index_qk[i])
1634
+ if cutlass.const_expr(window_size_left is not None or window_size_right is not None):
1635
+ if cutlass.const_expr(window_size_left is None):
1636
+ if index_q + offset + window_size_right < index_k:
1637
+ acc_qk[i] = -Float32.inf
1638
+ if index_k >= seqlen_k or index_q >= seqlen_q: # residual mask
1639
+ acc_qk[i] = -Float32.inf
1640
+ elif cutlass.const_expr(window_size_right is None):
1641
+ if index_q + offset - window_size_left > index_k:
1642
+ acc_qk[i] = -Float32.inf
1643
+ if index_k >= seqlen_k or index_q >= seqlen_q: # residual mask
1644
+ acc_qk[i] = -Float32.inf
1645
+ else:
1646
+ max_K_index = dsl_min(index_q + offset + window_size_right, seqlen_k)
1647
+ min_K_index = max(0, index_q + offset - window_size_left)
1648
+ if index_k > max_K_index or index_k < min_K_index:
1649
+ acc_qk[i] = -Float32.inf
1650
+ if index_k >= seqlen_k or index_q >= seqlen_q: # residual mask
1651
+ acc_qk[i] = -Float32.inf
1652
+
1653
+ if cutlass.const_expr(
1654
+ mask_type == Sm100MaskEnum.RESIDUAL_MASK
1655
+ or mask_type == Sm100MaskEnum.RESIDUAL_MASK_BWD
1656
+ ):
1657
+ if index_k >= seqlen_k or index_q >= seqlen_q:
1658
+ acc_qk[i] = -Float32.inf
1659
+
1660
+ @cute.jit
1661
+ def apply_mask_via_causal_local(
1662
+ acc_qk: cute.Tensor,
1663
+ index_qk: cute.Tensor,
1664
+ seqlen_q: Int32,
1665
+ seqlen_k: Int32,
1666
+ apply_semantic_window: cutlass.Constexpr[bool] = True,
1667
+ is_causal: cutlass.Constexpr[bool] = False,
1668
+ is_local: cutlass.Constexpr[bool] = False,
1669
+ window_size_left: Optional[int] = None,
1670
+ window_size_right: Optional[int] = None,
1671
+ index_transform: cutlass.Constexpr = lambda index_q, index_k: (
1672
+ index_q,
1673
+ index_k,
1674
+ ),
1675
+ ):
1676
+ """Apply forward mask without mask_type.
1677
+
1678
+ - If apply_semantic_window=True, apply causal/local window constraints.
1679
+ - Always apply residual OOB masking (index_k>=seqlen_k or index_q>=seqlen_q).
1680
+ """
1681
+ offset = 0
1682
+ if cutlass.const_expr(apply_semantic_window):
1683
+ # Match WINDOW_MASK_INFERENCE semantics: end-align Q/K when lengths differ.
1684
+ offset = seqlen_k - seqlen_q
1685
+ for i in cutlass.range_constexpr(cute.size(acc_qk), unroll_full=True):
1686
+ index_q, index_k = index_transform(*index_qk[i])
1687
+ if cutlass.const_expr(apply_semantic_window):
1688
+ if cutlass.const_expr(is_causal and not is_local):
1689
+ # Pure causal; tolerate both external forms:
1690
+ # - (None, None) from interface
1691
+ # - (None, 0) from fused-mask-style callers
1692
+ right = 0 if const_expr(window_size_right is None) else window_size_right
1693
+ if index_q + offset + right < index_k:
1694
+ acc_qk[i] = -Float32.inf
1695
+ elif cutlass.const_expr(
1696
+ is_local or window_size_left is not None or window_size_right is not None
1697
+ ):
1698
+ if cutlass.const_expr(window_size_left is None):
1699
+ if index_q + offset + window_size_right < index_k:
1700
+ acc_qk[i] = -Float32.inf
1701
+ elif cutlass.const_expr(window_size_right is None):
1702
+ if index_q + offset - window_size_left > index_k:
1703
+ acc_qk[i] = -Float32.inf
1704
+ else:
1705
+ max_K_index = dsl_min(index_q + offset + window_size_right, seqlen_k)
1706
+ min_K_index = max(0, index_q + offset - window_size_left)
1707
+ if index_k > max_K_index or index_k < min_K_index:
1708
+ acc_qk[i] = -Float32.inf
1709
+ # Residual mask is always needed for boundary protection.
1710
+ if index_k >= seqlen_k or index_q >= seqlen_q:
1711
+ acc_qk[i] = -Float32.inf
build/torch-cuda/metadata.json CHANGED
@@ -1,9 +1,93 @@
1
  {
2
- "version": 0,
 
 
 
3
  "license": "BSD-3-Clause",
 
4
  "python-depends": [
5
  "einops",
6
  "tvm-ffi",
7
  "nvidia-cutlass-dsl"
8
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  }
 
1
  {
2
+ "name": "flash-attn4",
3
+ "id": "_flash_attn4_cuda_d8eaf3c",
4
+ "version": 1,
5
+ "kernels-minver": "0.14.0",
6
  "license": "BSD-3-Clause",
7
+ "upstream": "https://github.com/Dao-AILab/flash-attention.git",
8
  "python-depends": [
9
  "einops",
10
  "tvm-ffi",
11
  "nvidia-cutlass-dsl"
12
+ ],
13
+ "kernel-depends": [],
14
+ "backend": {
15
+ "type": "cuda"
16
+ },
17
+ "digest": {
18
+ "algorithm": "sha256",
19
+ "files": {
20
+ "__init__.py": "9nhmToMqhkzqb88qdOhfpDICvN3LHSZhEM8HXa7VlcU=",
21
+ "_ops.py": "PVG5218SwxBt8nM52gD8xBjus4sHeK7/H9pZkvPRxCc=",
22
+ "_private_for_testing.py": "QXVDjtjb5NEXpgkZ93UyWJgHbrhi757Jlc8lkkTKETw=",
23
+ "ampere_helpers.py": "ytrBcKfaN54nw20EQ2BIDXEM+ojQgtrSud7NWOSEcRE=",
24
+ "barrier.py": "dnr4B6p1IbjRXubJU09HRaetIiSw2iuBbMScy56mwRk=",
25
+ "blackwell_helpers.py": "1G7e3qNjDRq24hiED6qbAJaN+9Lt+VfKRdbBSQBlP5U=",
26
+ "block_info.py": "c0GOkiVrwE1jkpNJFT+RPO1xdRD5ZM6RImIsvyLWnLc=",
27
+ "block_sparse_utils.py": "Y4Puv8qFy3MmuN1wY7/SvPHiOkKdZC5ItN3JJhwGqvo=",
28
+ "block_sparsity.py": "0PG41ePOKP/KS1j6eIo9pkMp7a7vrdCAysgmU/Eul+I=",
29
+ "cache_utils.py": "LbHAKc04S2NK2yEvmR6aB8yZuCeT2MCOrVv5lvEzQhA=",
30
+ "compute_block_sparsity.py": "p2+8YK5TnSM+G7g02pkrQxllSjOg/Lb4hbgNsKsttrM=",
31
+ "copy_utils.py": "D1Y+7rZlRgLRVB9yAre4g4bE29vSStawglg01l5QjAk=",
32
+ "cu_blocks_kernel.py": "oZ/Lf41VMM5o6vzUuDvd/dwDyNm7hqFfHkeC/gnSKW8=",
33
+ "cute_dsl_ptxas.py": "IfBnTJ9amdfDOQkuSdWCLTh7CkZziIvs/xrAc8taxhk=",
34
+ "cute_dsl_utils.py": "6ko8k13DHTgblp8bhmMeqkTJ4izAlr0S0TDIGiofkrs=",
35
+ "fa_logging.py": "vHHt+ksct69kt43vCeeQ/0xv5kq0/Pjj2WmfROjTwKA=",
36
+ "fast_math.py": "fl+CK/TQujaWdVjzrpoUCLQQy3JsMSIs1hcDVp0b5zg=",
37
+ "flash_bwd.py": "TRhiH4s/hQpkpQAKPt8lRMpg/92QqYUIMCok60L9gUo=",
38
+ "flash_bwd_mla_dk_sm100.py": "nPKlnayjEyxu0T5JqrXjojn5wzBQmwW7O0RmekOu6Q8=",
39
+ "flash_bwd_mla_dq_dqv_sm100.py": "WM/DT6DLexdrSs4EWskTENaRwN9VdGbmbEOVqY1ITgU=",
40
+ "flash_bwd_mla_sm100.py": "k6mUzSMehuxfkOxnhOHQEh0VoWWu0Wqczqs0BoCPNyY=",
41
+ "flash_bwd_postprocess.py": "N3dE8x0gau28oJMZDaRnOtQ+Eu8dJdG32lo5c4RrDRw=",
42
+ "flash_bwd_preprocess.py": "0lZNAuKMo4hZb5EF1IjtoMvR5FSR0Iq1MTDKWOD25Gk=",
43
+ "flash_bwd_sm100.py": "32wNaSsj3PLh7FqTTgKan3Mwe9iXrKFZOXwjXFkiEFw=",
44
+ "flash_bwd_sm120.py": "pYu0Z1Yt1TGkLrGUDQbHj7e7plUvtCW9lE5kJhM+TeM=",
45
+ "flash_bwd_sm90.py": "DvV+aDvDjC50LnEXdXUizK6+maRzsjFmcGO+h8bLOGc=",
46
+ "flash_fwd.py": "CJCCeosdamts+KoMuX2wYzCTK8d9veU4W9xDd8Qrw3Q=",
47
+ "flash_fwd_combine.py": "BGXmjQjrNksA0+TpgF3Trh6KlmejTiSQD5TMbgTWujM=",
48
+ "flash_fwd_mla_sm100.py": "wsXfpdNvwO4bw4Ol38H5t9n57N+E5Ln8gpkNiazXA4E=",
49
+ "flash_fwd_sm100.py": "8kqCPNeGuI56BDiSIXdfFZalVLz8SWmGpkHlfvYwmXg=",
50
+ "flash_fwd_sm120.py": "RGmi1PkGYo48xqAWYimrIb7XrokTOKZYYQx5XkxDKco=",
51
+ "flash_fwd_sm90.py": "zmqsNatHicpxW+dSRnPn2gNMt+TaI8HX+VEOr2hlyns=",
52
+ "interface.py": "WSarPGybE0wvH3K6tYnGrldr/z2A7TQojBcd7Dz7iWc=",
53
+ "mask.py": "45XBbPI1GDqaoT0k3Nq83gKu61ZOGrK+R+mWzWXWw+k=",
54
+ "mma_sm100_desc.py": "rG7Ciy8aS3l8ORp4B7iPg7QwRS1BgmLD36ew81FuOfk=",
55
+ "named_barrier.py": "H4D5a2kKKIjuEwHTd2ZdtunddLQC+A6qhn049ylNgTs=",
56
+ "pack_gqa.py": "Oqx6zBC5MrUYnj5E/tTBMRuDOUV5kCu0slYHy6jDvDg=",
57
+ "paged_kv.py": "aCfJSKrg3NnhitFjG9FbShvP+PuAt+gQNz01AjgCtao=",
58
+ "pipeline.py": "hJvOkccr4KXHRYl54QyCkPReBSU6g2AOMPuVQMYe1Wo=",
59
+ "prepare_scheduler.py": "83KHAGQBk7uz7WdmCwV3/k7gEbhiUYEc3VVMkx+LwiU=",
60
+ "quack/__init__.py": "m24hqJucTK14gZS18kAZhF29vDzEWaplx6wp6XN4T70=",
61
+ "quack/compile_utils.py": "qJ3oTsDlbAiddrJHtEO7LPYVqn/s+neNfiw+/KvfXZU=",
62
+ "quack/copy_utils.py": "48qGZz3ql2hwxAH+rr8g7hJoiy2drx4YRnXjb3TcEJo=",
63
+ "quack/cute_dsl_utils.py": "4jBC37wrqgJU/2Y3HFVbi46rQBkade3+ymPcHIxAYQk=",
64
+ "quack/dsl/__init__.py": "7mF8m94hdEj3jrzX0sk0LBszrP7WDhdcUJd4lNDOpGs=",
65
+ "quack/dsl/cute_tensor_indexing.py": "1QnPrnekkECXkgHC5NWY6JxyDmTTf5kgClCIYwxMfG0=",
66
+ "quack/layout_utils.py": "MZncP1XzRhg+PShPbamPQ5Tq8U8ot2FtFH5uSeyJYZQ=",
67
+ "quack/rounding.py": "jI6fP/qCjSMh9s8PtjPDYfTPs200C3xx9zKll1hlodU=",
68
+ "quack/sm90_utils.py": "QaLZ3MLlp9dKOD+VwdhxmqEstPdq8CVGaNwffbwspNw=",
69
+ "quack/utils.py": "/FsIscjaxmOSj/g6frh4gLgB6iuIZ6e3EeGNunKWBuw=",
70
+ "seqlen_info.py": "ljfpUQcu9fILvJfyYYGkM85kiXjmNP1p748eivq0dwk=",
71
+ "sm100_hd256_2cta_fmha_backward.py": "BFD5O/rIJi5X//5ecE/h+zQHhb9HLI8TmpZHHhcVmns=",
72
+ "sm100_hd256_2cta_fmha_backward_dkdvkernel.py": "EJiDHIFGQ8A63CHSzFrDF5kigtWNZMqsvXk/28zVWfI=",
73
+ "sm100_hd256_2cta_fmha_backward_dqkernel.py": "SKQ8lUEl1P4ZcsHiMaGBqQJFL2o0oZTJhF8oOx6iXDs=",
74
+ "sm100_hd256_2cta_fmha_forward.py": "aJ0icWUbC3D2iLlj8SDXb+eaM0/U4mljFlIaH8iEZd4=",
75
+ "softmax.py": "eCMvLFCXjlrgm0s24D5rrdbFFRMyah1ESWjB3BEgLu4=",
76
+ "testing.py": "M0K0R1EmZm5b/wUDFs0qWGhS98gusj/SEXJHDMQKFf8=",
77
+ "tile_scheduler.py": "ELRFy6rI+J+8Z2fMDLxYrCiuUMSaBLa4h10dIe27bmA=",
78
+ "topk_gather_kv.py": "WchMfXog4eIIIOhTKu0vE5k3dfn41jVqrl9wtafJvRw=",
79
+ "utils.py": "j+SX9mJpWWA998oBdZgS28MoIV69XwTRN0mVA6Hcha8="
80
+ }
81
+ },
82
+ "provenance": {
83
+ "kernel-builder": {
84
+ "version": "0.17.0-dev0",
85
+ "commit": "a7f0afdb29a6a3372b1d47180cc0c182454c5e3b",
86
+ "dirty": false
87
+ },
88
+ "kernel": {
89
+ "commit": "d8eaf3cbe010f4795d0b0c4b0103862387ccdc97",
90
+ "dirty": false
91
+ }
92
+ }
93
  }
build/torch-cuda/metadata.json.sigstore ADDED
@@ -0,0 +1 @@
 
 
1
+ {"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json","verificationMaterial":{"certificate":{"rawBytes":"MIIHLTCCBrKgAwIBAgIUC8FD02MzDpPjLuXMiJ3QOc8FBi4wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYxMDA5MDU0NzQyWhcNMjYxMDA5MDU1NzQyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvU7iFTJAhfZ8mmvV3pYwcuyy5onUd8y8YkK3oDJrIYFKZZeP51FT0kntoP0n4AGSPRV4Ro9wrRR7jPlxS/0VyKOCBdEwggXNMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBTf0+nPViQRlvmo2OkoVaLGLhhkPzBrBgNVHREBAf8EYTBfhl1odHRwczovL2dpdGh1Yi5jb20vaHVnZ2luZ2ZhY2Uva2VybmVscy1jb21tdW5pdHkvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueWFtbEByZWZzL2hlYWRzL21haW4wOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAfBgorBgEEAYO/MAECBBF3b3JrZmxvd19kaXNwYXRjaDA2BgorBgEEAYO/MAEDBChkOGVhZjNjYmUwMTBmNDc5NWQwYjBjNGIwMTAzODYyMzg3Y2NkYzk3MBMGCisGAQQBg78wAQQEBUJ1aWxkMCsGCisGAQQBg78wAQUEHWh1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5MB0GCisGAQQBg78wAQYED3JlZnMvaGVhZHMvbWFpbjA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wbQYKKwYBBAGDvzABCQRfDF1odHRwczovL2dpdGh1Yi5jb20vaHVnZ2luZ2ZhY2Uva2VybmVscy1jb21tdW5pdHkvLmdpdGh1Yi93b3JrZmxvd3MvYnVpbGQueWFtbEByZWZzL2hlYWRzL21haW4wOAYKKwYBBAGDvzABCgQqDChkOGVhZjNjYmUwMTBmNDc5NWQwYjBjNGIwMTAzODYyMzg3Y2NkYzk3MBsGCisGAQQBg78wAQsEDQwLc2VsZi1ob3N0ZWQwQAYKKwYBBAGDvzABDAQyDDBodHRwczovL2dpdGh1Yi5jb20vaHVnZ2luZ2ZhY2Uva2VybmVscy1jb21tdW5pdHkwOAYKKwYBBAGDvzABDQQqDChkOGVhZjNjYmUwMTBmNDc5NWQwYjBjNGIwMTAzODYyMzg3Y2NkYzk3MB8GCisGAQQBg78wAQ4EEQwPcmVmcy9oZWFkcy9tYWluMBoGCisGAQQBg78wAQ8EDAwKMTA3MTQ3NTUyOTAuBgorBgEEAYO/MAEQBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZTAYBgorBgEEAYO/MAERBAoMCDI1NzIwNzQzMG0GCisGAQQBg78wARIEXwxdaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5Ly5naXRodWIvd29ya2Zsb3dzL2J1aWxkLnlhbWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoZDhlYWYzY2JlMDEwZjQ3OTVkMGIwYzRiMDEwMzg2MjM4N2NjZGM5NzAhBgorBgEEAYO/MAEUBBMMEXdvcmtmbG93X2Rpc3BhdGNoMGQGCisGAQQBg78wARUEVgxUaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5L2FjdGlvbnMvcnVucy8zNzg5MDA3MjU2Mi9hdHRlbXB0cy8xMBYGCisGAQQBg78wARYECAwGcHVibGljMEYGCisGAQQBg78wARgEOAw2cmVwbzpodWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eTpyZWY6cmVmcy9oZWFkcy9tYWluMIGKBgorBgEEAdZ5AgQCBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGhHzP5igAABAMARzBFAiEAq9xS/SQssYBHs+y3wB9piTHTBtP5LMlf5JId4nIfA3sCIH5m7oZGU8V7XUlOnv+dvMJ3KQo46r1zQBPQkI9jcu5gMAoGCCqGSM49BAMDA2kAMGYCMQCrjTEdjE3f7UqDCOh3sVTk2MU9H7IJ2OkZwrbUwupS/xvK0ni6p5gl2AdH0pLBfkkCMQDkblT245iYWckRzNjXOOIUi4lNnrNhu5fdRtd9Z58/K5sQp21Z61tVdGfkOPF8UF4="},"tlogEntries":[{"logIndex":"3158296362","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"hashedrekord","version":"0.0.1"},"integratedTime":"1791524864","inclusionPromise":{"signedEntryTimestamp":"MEUCIHdcmCseFb7Vr5yOOIYHwnu8qbD12aVWYFJAvrIFWQlQAiEAiI/g9IvQbhN7/BHfD7fKpMlkheGsalXj3sjCz0fV9Eg="},"inclusionProof":{"logIndex":"3036392100","rootHash":"teeCGBfEA5uGpfYT9SeWxOIWLaZFezcGvaKdKW6ZXHk=","treeSize":"3036392181","hashes":["KyrJTiRKZ75+pT8Sx+ulXcJDqdovWH9dj1tSGpuz7bI=","GHJ44yU03czeAoYe92xG9t5dcLSj8DC6P93R9ir+YGw=","+MpZb3IeTXUQ1VXFXZgfVFzCvOX0MDxGanUQMFngYWw=","R62P9HI0DerDEF/J2NPUG+pRooZZjr87tb/6a02VAZs=","witl6lVGRqWEukI7TPfEwEterD0x0U/N+vLFrEgM3d0=","//9Wrtt/km05V/Uz1gBzHmw1++3e5qzCpZycY0id0hM=","5qY+JootET5QYakLD1VSLaWLYsBV5CNsny3SMmLgDck=","gnsD5AdzyM8v81+WoK8S73uW+oNypt2c3v4gw07kL6c=","wDpbIRy5hWWa+M5aB2/ZontGTidhd0Opkg9pOLRfJLc=","TDNBgrWNhMz/ZLmNeiUQjXJE6NabHjQEsHCekqo5DS8=","78RhvRg8R4WwHxVvzWwFGNaJwKZGwJSN7GYO7kn5vP8=","F5tQx3q6DM2bQD3L/G2KJ9rAjZ0VMzlAmuw1Urrltpg=","KbyZDOTON24dtLPkz0tlGLB0OsswC4djFUNHxHjMipI=","VPmQZaiIPP+jGVmSWjYHBv/V9OCTZbwibPFCdg+AHeQ=","GSGvSu0XsdO6cBgqJwZWqKj8191ujOCFNrbeVOnHXfU=","bDbLklSZE3cNPdbgkYjsldgt1tp4oyGy0Ey74MEDzVs=","kbqJ2Mh+r6zBJA5FYtUICq/XJ22GIh0GC+s04H3SZOs=","QsIovxEueg218cKeugrO+eGZOkloq5eRks0NcVdhADs=","QJUdtMG5fqTebI7bWTn0N8FLr9gz+BNgQr/7OsNQa1w=","hafZfJdzUX/yyCr3Anv7Qq1DWVaWxotaclNRcl/O9SQ=","NlOclldmKPA1tMKIskqHAwuQDYHp31YwpK+OOMrMccc=","qxzHanAzz57SDdmJe0B7bJK72NTIBbwEMGBBKvDOROw=","xH/DCseLHr9eKoYT8qsORZK7zVdEGYWHuVtsVrD95wY="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n3036392181\nteeCGBfEA5uGpfYT9SeWxOIWLaZFezcGvaKdKW6ZXHk=\n\n— rekor.sigstore.dev wNI9ajBFAiEApHSDrn9feEHa8pVsxt21mKS1GsE0GwW/yWXtEDWTr8UCIAt1IfpaBYiybTSBozWZJk38aPVtTQZe2Uk7H1KNyATZ\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJjODljNjJjNDZlMmNlNmY2MzNkMjM3NWIwMGY5ZDU2ZmUxZDM3MTY5MGI0YjEyZDIxODQ3ODliODU4ZTZhYjk2In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUNVM1VGTmxzVkxDSVloNVFRanlicHhranp2YU1VM0J6MTk1T2hTZTk1ZTh3SWdVK0dVdDZGK1c4Wll4ZjBQUXFqU001bVhWeXlrSDQrZmZLZnVIS1F5cmRFPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVaE1WRU5EUW5KTFowRjNTVUpCWjBsVlF6aEdSREF5VFhwRWNGQnFUSFZZVFdsS00xRlBZemhHUW1rMGQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFplRTFFUVRWTlJGVXdUbnBSZVZkb1kwNU5hbGw0VFVSQk5VMUVWVEZPZWxGNVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVYyVlRkcFJsUktRV2htV2podGJYWldNM0JaZDJOMWVYazFiMjVWWkRoNU9GbHJTek1LYjBSS2NrbFpSa3RhV21WUU5URkdWREJyYm5SdlVEQnVORUZIVTFCU1ZqUlNiemwzY2xKU04ycFFiSGhUTHpCV2VVdFBRMEprUlhkbloxaE9UVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXWkNaMDVXU0ZOTlJVZEVRVmRuUWxSbUNqQXJibEJXYVZGU2JIWnRiekpQYTI5V1lVeEhUR2hvYTFCNlFuSkNaMDVXU0ZKRlFrRm1PRVZaVkVKbWFHd3hiMlJJVW5kamVtOTJUREprY0dSSGFERUtXV2sxYW1JeU1IWmhTRlp1V2pKc2RWb3lXbWhaTWxWMllUSldlV0p0Vm5OamVURnFZakl4ZEdSWE5YQmtTR3QyVEcxa2NHUkhhREZaYVRrellqTktjZ3BhYlhoMlpETk5kbGx1Vm5CaVIxRjFaVmRHZEdKRlFubGFWMXA2VERKb2JGbFhVbnBNTWpGb1lWYzBkMDlSV1V0TGQxbENRa0ZIUkhaNlFVSkJVVkZ5Q21GSVVqQmpTRTAyVEhrNU1HSXlkR3hpYVRWb1dUTlNjR0l5TlhwTWJXUndaRWRvTVZsdVZucGFXRXBxWWpJMU1GcFhOVEJNYlU1MllsUkJaa0puYjNJS1FtZEZSVUZaVHk5TlFVVkRRa0pHTTJJelNuSmFiWGgyWkRFNWEyRllUbmRaV0ZKcVlVUkJNa0puYjNKQ1owVkZRVmxQTDAxQlJVUkNRMmhyVDBkV2FBcGFhazVxV1cxVmQwMVVRbTFPUkdNMVRsZFJkMWxxUW1wT1IwbDNUVlJCZWs5RVdYbE5lbWN6V1RKT2ExbDZhek5OUWsxSFEybHpSMEZSVVVKbk56aDNDa0ZSVVVWQ1ZVb3hZVmQ0YTAxRGMwZERhWE5IUVZGUlFtYzNPSGRCVVZWRlNGZG9NVm95WkhCaWJXUnRXVmRPYkV3eWRHeGpiVFZzWWtoTmRGa3lPWFFLWWxoV2RXRllValZOUWpCSFEybHpSMEZSVVVKbk56aDNRVkZaUlVRelNteGFiazEyWVVkV2FGcElUWFppVjBad1ltcEJOMEpuYjNKQ1owVkZRVmxQTHdwTlFVVkpRa013VFVzeWFEQmtTRUo2VDJrNGRtUkhPWEphVnpSMVdWZE9NR0ZYT1hWamVUVnVZVmhTYjJSWFNqRmpNbFo1V1RJNWRXUkhWblZrUXpWcUNtSXlNSGRpVVZsTFMzZFpRa0pCUjBSMmVrRkNRMUZTWmtSR01XOWtTRkozWTNwdmRrd3laSEJrUjJneFdXazFhbUl5TUhaaFNGWnVXakpzZFZveVdtZ0tXVEpWZG1FeVZubGliVlp6WTNreGFtSXlNWFJrVnpWd1pFaHJka3h0WkhCa1IyZ3hXV2s1TTJJelNuSmFiWGgyWkROTmRsbHVWbkJpUjFGMVpWZEdkQXBpUlVKNVdsZGFla3d5YUd4WlYxSjZUREl4YUdGWE5IZFBRVmxMUzNkWlFrSkJSMFIyZWtGQ1EyZFJjVVJEYUd0UFIxWm9XbXBPYWxsdFZYZE5WRUp0Q2s1RVl6Vk9WMUYzV1dwQ2FrNUhTWGROVkVGNlQwUlplVTE2WnpOWk1rNXJXWHByTTAxQ2MwZERhWE5IUVZGUlFtYzNPSGRCVVhORlJGRjNUR015Vm5NS1dta3hiMkl6VGpCYVYxRjNVVUZaUzB0M1dVSkNRVWRFZG5wQlFrUkJVWGxFUkVKdlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWVVoV2JncGFNbXgxV2pKYWFGa3lWWFpoTWxaNVltMVdjMk41TVdwaU1qRjBaRmMxY0dSSWEzZFBRVmxMUzNkWlFrSkJSMFIyZWtGQ1JGRlJjVVJEYUd0UFIxWm9DbHBxVG1wWmJWVjNUVlJDYlU1RVl6Vk9WMUYzV1dwQ2FrNUhTWGROVkVGNlQwUlplVTE2WnpOWk1rNXJXWHByTTAxQ09FZERhWE5IUVZGUlFtYzNPSGNLUVZFMFJVVlJkMUJqYlZadFkzazViMXBYUm10amVUbDBXVmRzZFUxQ2IwZERhWE5IUVZGUlFtYzNPSGRCVVRoRlJFRjNTMDFVUVROTlZGRXpUbFJWZVFwUFZFRjFRbWR2Y2tKblJVVkJXVTh2VFVGRlVVSkRRVTFJYldnd1pFaENlazlwT0haYU1td3dZVWhXYVV4dFRuWmlVemx2WkZka2JtRlhOVzVhYlVacUNscFVRVmxDWjI5eVFtZEZSVUZaVHk5TlFVVlNRa0Z2VFVORVNURk9la2wzVG5wUmVrMUhNRWREYVhOSFFWRlJRbWMzT0hkQlVrbEZXSGQ0WkdGSVVqQUtZMGhOTmt4NU9XNWhXRkp2WkZkSmRWa3lPWFJNTW1neFdqSmtjR0p0WkcxWlYwNXNUREowYkdOdE5XeGlTRTEwV1RJNWRHSllWblZoV0ZJMVRIazFiZ3BoV0ZKdlpGZEpkbVF5T1hsaE1scHpZak5rZWt3eVNqRmhWM2hyVEc1c2FHSlhlRUZqYlZadFkzazViMXBYUm10amVUbDBXVmRzZFUxRVowZERhWE5IQ2tGUlVVSm5OemgzUVZKTlJVdG5kMjlhUkdoc1dWZFplbGt5U214TlJFVjNXbXBSTTA5VVZtdE5SMGwzV1hwU2FVMUVSWGROZW1jeVRXcE5ORTR5VG1vS1drZE5OVTU2UVdoQ1oyOXlRbWRGUlVGWlR5OU5RVVZWUWtKTlRVVllaSFpqYlhSdFlrYzVNMWd5VW5Cak0wSm9aRWRPYjAxSFVVZERhWE5IUVZGUlFncG5OemgzUVZKVlJWWm5lRlZoU0ZJd1kwaE5Oa3g1T1c1aFdGSnZaRmRKZFZreU9YUk1NbWd4V2pKa2NHSnRaRzFaVjA1c1RESjBiR050Tld4aVNFMTBDbGt5T1hSaVdGWjFZVmhTTlV3eVJtcGtSMngyWW01TmRtTnVWblZqZVRoNlRucG5OVTFFUVROTmFsVXlUV2s1YUdSSVVteGlXRUl3WTNrNGVFMUNXVWNLUTJselIwRlJVVUpuTnpoM1FWSlpSVU5CZDBkalNGWnBZa2RzYWsxRldVZERhWE5IUVZGUlFtYzNPSGRCVW1kRlQwRjNNbU50Vm5kaWVuQnZaRmRrYmdwaFZ6VnVXbTFHYWxwVE9YSmFXRXAxV2xkNGVreFhUblppVnpFeFltMXNNR1ZVY0hsYVYxazJZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOU1VkTENrSm5iM0pDWjBWRlFXUmFOVUZuVVVOQ1NIZEZaV2RDTkVGSVdVRXpWREIzWVhOaVNFVlVTbXBIVWpSamJWZGpNMEZ4U2t0WWNtcGxVRXN6TDJnMGNIa0taME00Y0Rkdk5FRkJRVWRvU0hwUU5XbG5RVUZDUVUxQlVucENSa0ZwUlVGeE9YaFRMMU5SYzNOWlFraHpLM2t6ZDBJNWNHbFVTRlJDZEZBMVRFMXNaZ28xU2tsa05HNUpaa0V6YzBOSlNEVnROMjlhUjFVNFZqZFlWV3hQYm5ZclpIWk5Tak5MVVc4ME5uSXhlbEZDVUZGclNUbHFZM1UxWjAxQmIwZERRM0ZIQ2xOTk5EbENRVTFFUVRKclFVMUhXVU5OVVVOeWFsUkZaR3BGTTJZM1ZYRkVRMDlvTTNOV1ZHc3lUVlU1U0RkSlNqSlBhMXAzY21KVmQzVndVeTk0ZGtzS01HNXBObkExWjJ3eVFXUklNSEJNUW1acmEwTk5VVVJyWW14VU1qUTFhVmxYWTJ0U2VrNXFXRTlQU1ZWcE5HeE9ibkpPYUhVMVptUlNkR1E1V2pVNEx3cExOWE5SY0RJeFdqWXhkRlprUjJaclQxQkdPRlZHTkQwS0xTMHRMUzFGVGtRZ1EwVlNWRWxHU1VOQlZFVXRMUzB0TFFvPSJ9fX19"}],"timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyTADAgEAMIICwAYJKoZIhvcNAQcCoIICsTCCAq0CAQMxDTALBglghkgBZQMEAgEwgbcGCyqGSIb3DQEJEAEEoIGnBIGkMIGhAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQg4k983K1coZlkayD036+cJpsVAAkEZEsAMpxiUQTrwEACFDkWZsJr2b7zVR04BWQQhoVadpRtGA8yMDI2MTAwOTA1NDc0MlowAwIBAaAypDAwLjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MRUwEwYDVQQDEwxzaWdzdG9yZS10c2GgADGCAdswggHXAgEBMFEwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZAIUOhNULwyQYe68wUMvy4qOiyojiwwwCwYJYIZIAWUDBAIBoIH8MBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjYxMDA5MDU0NzQyWjAvBgkqhkiG9w0BCQQxIgQgLObLq7NGK87U9ONi2A7Ln6dRup/atfMcOdqLVbkL/G4wgY4GCyqGSIb3DQEJEAIvMX8wfTB7MHkEIIX5J7wHq2LKw7RDVsEO/IGyxog/2nq55thw2dE6zQW3MFUwPaQ7MDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAoGCCqGSM49BAMCBGcwZQIxAIcZfruFVmPc3PiaAgWG2V/Lf7wzIFR8Zzf04nbAW4f9QWNBAschM6N1+f5egCBMbgIwO4DVCem+xvxWkNErxMg+VRttorrtEnVgXG9eqCuTc0xyNiLSbTkH2iLHCFVwFz7Y"}]}},"messageSignature":{"messageDigest":{"algorithm":"SHA2_256","digest":"yJxixG4s5vYz0jdbAPnVb+HTcWkLSxLSGEeJuFjmq5Y="},"signature":"MEUCIQCU3UFNlsVLCIYh5QQjybpxkjzvaMU3Bz195OhSe95e8wIgU+GUt6F+W8ZYxf0PQqjSM5mXVyykH4+ffKfuHKQyrdE="}}
build/torch-cuda/mma_sm100_desc.py CHANGED
@@ -83,9 +83,9 @@ def to_UMMA_format(cutlass_type) -> int:
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
 
 
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.Float8E4M3FN:
87
  return MXF8F6F4Format.E4M3
88
+ if cutlass_type is cutlass.Float8E5M2:
89
  return MXF8F6F4Format.E5M2
90
  raise TypeError(f"Unsupported CUTLASS scalar type for A/B: {cutlass_type!r}")
91
 
build/torch-cuda/named_barrier.py CHANGED
@@ -12,6 +12,19 @@ class NamedBarrierFwd(enum.IntEnum):
12
  PEmpty = enum.auto()
13
 
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  class NamedBarrierBwd(enum.IntEnum):
16
  Epilogue = enum.auto()
17
  WarpSchedulerWG1 = enum.auto()
@@ -20,8 +33,10 @@ class NamedBarrierBwd(enum.IntEnum):
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):
@@ -30,3 +45,19 @@ class NamedBarrierBwdSm100(enum.IntEnum):
30
  Compute = enum.auto()
31
  dQaccReduce = enum.auto()
32
  TmemPtr = enum.auto()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  PEmpty = enum.auto()
13
 
14
 
15
+ class NamedBarrierFwdSm100(enum.IntEnum):
16
+ Epilogue = enum.auto() # starts from 1 as barrier 0 is reserved for sync_threads()
17
+ TmemPtr = enum.auto()
18
+ SoftmaxStatsW0 = enum.auto()
19
+ SoftmaxStatsW1 = enum.auto()
20
+ SoftmaxStatsW2 = enum.auto()
21
+ SoftmaxStatsW3 = enum.auto()
22
+ SoftmaxStatsW4 = enum.auto()
23
+ SoftmaxStatsW5 = enum.auto()
24
+ SoftmaxStatsW6 = enum.auto()
25
+ SoftmaxStatsW7 = enum.auto()
26
+
27
+
28
  class NamedBarrierBwd(enum.IntEnum):
29
  Epilogue = enum.auto()
30
  WarpSchedulerWG1 = enum.auto()
 
33
  PdS = enum.auto()
34
  dQFullWG0 = enum.auto()
35
  dQFullWG1 = enum.auto()
36
+ dQFullWG2 = enum.auto()
37
  dQEmptyWG0 = enum.auto()
38
  dQEmptyWG1 = enum.auto()
39
+ dQEmptyWG2 = enum.auto()
40
 
41
 
42
  class NamedBarrierBwdSm100(enum.IntEnum):
 
45
  Compute = enum.auto()
46
  dQaccReduce = enum.auto()
47
  TmemPtr = enum.auto()
48
+
49
+
50
+ class NamedBarrierFwdSm100_MLA2CTA(enum.IntEnum):
51
+ Epilogue = enum.auto()
52
+ TmemPtr = enum.auto()
53
+ Cpasync = enum.auto()
54
+ Softmax = enum.auto()
55
+ SoftmaxStatsFull = enum.auto()
56
+ SoftmaxStatsEmpty = enum.auto()
57
+
58
+
59
+ class NamedBarrierBwdSm100_MLA2CTA(enum.IntEnum):
60
+ Epilogue = enum.auto()
61
+ TmemPtr = enum.auto()
62
+ Cpasync = enum.auto()
63
+ Softmax = enum.auto()
build/torch-cuda/pack_gqa.py CHANGED
@@ -1,25 +1,123 @@
1
  # Copyright (c) 2025, Tri Dao.
2
 
 
 
3
 
4
  import cutlass
5
  import cutlass.cute as cute
 
 
6
 
7
  from .quack import layout_utils
8
- from . import utils
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  class PackGQA:
12
- def __init__(
13
- self,
14
- m_block_size: cutlass.Constexpr[int],
15
- head_dim_padded: cutlass.Constexpr[int],
16
- check_hdim_oob: cutlass.Constexpr[bool],
17
- qhead_per_kvhead: cutlass.Constexpr[bool],
18
- ):
19
- self.m_block_size = m_block_size
20
- self.head_dim_padded = head_dim_padded
21
- self.check_hdim_oob = check_hdim_oob
22
- self.qhead_per_kvhead = qhead_per_kvhead
23
 
24
  @cute.jit
25
  def compute_ptr(
@@ -32,7 +130,7 @@ class PackGQA:
32
  num_threads: cutlass.Constexpr[int],
33
  ):
34
  num_ptr_per_thread = cute.ceil_div(cute.size(cRows), threads_per_row)
35
- tPrPtr = cute.make_fragment(num_ptr_per_thread, cutlass.Int64)
36
  for i in cutlass.range_constexpr(num_ptr_per_thread):
37
  row = i * num_threads + cRows[tidx % threads_per_row][0]
38
  idx = block * self.m_block_size + row
 
1
  # Copyright (c) 2025, Tri Dao.
2
 
3
+ from dataclasses import dataclass
4
+ from typing import Union, Tuple
5
 
6
  import cutlass
7
  import cutlass.cute as cute
8
+ from cutlass.cute.nvgpu import cpasync
9
+
10
 
11
  from .quack import layout_utils
12
+ from . import utils as utils
13
+
14
+
15
+ def pack_gqa_layout(T, qhead_per_kvhead, nheads_kv, head_idx):
16
+ """Reshape a tensor to fold qhead_per_kvhead into the seqlen dimension (mode 0).
17
+
18
+ The head dimension is at mode ``head_idx``. Modes before it (1..head_idx-1)
19
+ are kept as-is (e.g. headdim for Q/O tensors), and modes after it are kept
20
+ as-is (e.g. batch).
21
+
22
+ For Q/O tensors (head_idx=2):
23
+ (seqlen_q, headdim, nheads, batch, ...) -> ((qhead_per_kvhead, seqlen_q), headdim, nheads_kv, batch, ...)
24
+ For LSE tensors (head_idx=1):
25
+ (seqlen_q, nheads, batch, ...) -> ((qhead_per_kvhead, seqlen_q), nheads_kv, batch, ...)
26
+ """
27
+ head_stride = T.stride[head_idx]
28
+ shape_packed = (
29
+ (qhead_per_kvhead, T.shape[0]),
30
+ *[T.shape[i] for i in range(1, head_idx)],
31
+ nheads_kv,
32
+ *[T.shape[i] for i in range(head_idx + 1, len(T.shape))],
33
+ )
34
+ stride_packed = (
35
+ (head_stride, T.stride[0]),
36
+ *[T.stride[i] for i in range(1, head_idx)],
37
+ head_stride * qhead_per_kvhead,
38
+ *[T.stride[i] for i in range(head_idx + 1, len(T.shape))],
39
+ )
40
+ return cute.make_tensor(T.iterator, cute.make_layout(shape_packed, stride=stride_packed))
41
+
42
+
43
+ def make_packgqa_tiled_tma_atom(
44
+ op: cute.atom.CopyOp,
45
+ gmem_tensor: cute.Tensor,
46
+ smem_layout: Union[cute.Layout, cute.ComposedLayout],
47
+ cta_tiler: Tuple[int, int],
48
+ qhead_per_kvhead: int,
49
+ head_idx: int,
50
+ ):
51
+ # This packing and unpacking of the layout is so that we keep the same TMA dimension as usual.
52
+ # e.g. for (seqlen, d, nheads, b) layout, we still have 4D TMA after packing to
53
+ # ((nheads, seqlen), d, b).
54
+ # If we instead pack directly to ((qhead_per_kvhead, seqlen), d, nheads_kv, b) we'd have 5D TMA.
55
+ # Pack headdim and seqlen dim into 1: (seqlen, d, nheads, b) -> ((nheads, seqlen), d, b)
56
+ gmem_tensor = layout_utils.select(
57
+ gmem_tensor, [head_idx, *range(head_idx), *range(head_idx + 1, cute.rank(gmem_tensor))]
58
+ )
59
+ gmem_tensor = cute.group_modes(gmem_tensor, 0, 2)
60
+ assert cta_tiler[0] % qhead_per_kvhead == 0, (
61
+ "CTA tile size in the seqlen dimension must be divisible by qhead_per_kvhead"
62
+ )
63
+ tma_atom, tma_tensor = cpasync.make_tiled_tma_atom(
64
+ op,
65
+ gmem_tensor,
66
+ smem_layout,
67
+ ((qhead_per_kvhead, cta_tiler[0] // qhead_per_kvhead), cta_tiler[1]), # No mcast
68
+ )
69
+ # Unpack from ((nheads, seqlen), d, b) -> ((qhead_per_kvhead, seqlen), d, nheads_kv, b)
70
+ T = tma_tensor
71
+ shape_packed = (
72
+ (qhead_per_kvhead, T.shape[0][1]),
73
+ *[T.shape[i] for i in range(1, head_idx)],
74
+ T.shape[0][0] // qhead_per_kvhead,
75
+ *[T.shape[i] for i in range(head_idx, len(T.shape))],
76
+ )
77
+ stride_packed = (
78
+ *[T.stride[i] for i in range(head_idx)],
79
+ T.stride[0][0] * qhead_per_kvhead,
80
+ *[T.stride[i] for i in range(head_idx, len(T.shape))],
81
+ )
82
+ tma_tensor = cute.make_tensor(T.iterator, cute.make_layout(shape_packed, stride=stride_packed))
83
+ return tma_atom, tma_tensor
84
 
85
 
86
+ def unpack_gqa_layout(T, qhead_per_kvhead, head_idx):
87
+ """Reverse of pack_gqa_layout: unfold qhead_per_kvhead from the seqlen dimension (mode 0).
88
+
89
+ The head dimension is at mode ``head_idx``. Modes before it (1..head_idx-1)
90
+ are kept as-is (e.g. headdim for Q/O tensors), and modes after it are kept
91
+ as-is (e.g. batch).
92
+
93
+ For Q/O tensors (head_idx=2):
94
+ ((qhead_per_kvhead, seqlen_q), headdim, nheads_kv, batch, ...) -> (seqlen_q, headdim, nheads, batch, ...)
95
+ For LSE tensors (head_idx=1):
96
+ ((qhead_per_kvhead, seqlen_q), nheads_kv, batch, ...) -> (seqlen_q, nheads, batch, ...)
97
+ """
98
+ seqlen_stride = T.stride[0][1]
99
+ head_stride = T.stride[0][0]
100
+ shape_unpacked = (
101
+ T.shape[0][1],
102
+ *[T.shape[i] for i in range(1, head_idx)],
103
+ T.shape[head_idx] * qhead_per_kvhead,
104
+ *[T.shape[i] for i in range(head_idx + 1, len(T.shape))],
105
+ )
106
+ stride_unpacked = (
107
+ seqlen_stride,
108
+ *[T.stride[i] for i in range(1, head_idx)],
109
+ head_stride,
110
+ *[T.stride[i] for i in range(head_idx + 1, len(T.shape))],
111
+ )
112
+ return cute.make_tensor(T.iterator, cute.make_layout(shape_unpacked, stride=stride_unpacked))
113
+
114
+
115
+ @dataclass
116
  class PackGQA:
117
+ m_block_size: cutlass.Constexpr[int]
118
+ head_dim_padded: cutlass.Constexpr[int]
119
+ check_hdim_oob: cutlass.Constexpr[bool]
120
+ qhead_per_kvhead: cutlass.Constexpr[bool]
 
 
 
 
 
 
 
121
 
122
  @cute.jit
123
  def compute_ptr(
 
130
  num_threads: cutlass.Constexpr[int],
131
  ):
132
  num_ptr_per_thread = cute.ceil_div(cute.size(cRows), threads_per_row)
133
+ tPrPtr = cute.make_rmem_tensor(num_ptr_per_thread, cutlass.Int64)
134
  for i in cutlass.range_constexpr(num_ptr_per_thread):
135
  row = i * num_threads + cRows[tidx % threads_per_row][0]
136
  idx = block * self.m_block_size + row
build/torch-cuda/paged_kv.py CHANGED
@@ -28,6 +28,9 @@ class PagedKVManager(ParamsBase):
28
  head_dim_padded: cutlass.Constexpr[Int32]
29
  head_dim_v_padded: cutlass.Constexpr[Int32]
30
 
 
 
 
31
  gmem_threads_per_row: cutlass.Constexpr[Int32]
32
  page_entry_per_thread: Int32
33
  async_copy_elems: Int32
@@ -55,7 +58,11 @@ class PagedKVManager(ParamsBase):
55
  head_dim_v_padded: cutlass.Constexpr[Int32],
56
  num_threads: cutlass.Constexpr[Int32],
57
  dtype: Type[cutlass.Numeric],
 
58
  ):
 
 
 
59
  universal_copy_bits = 128
60
  async_copy_elems = universal_copy_bits // dtype.width
61
  dtype_bytes = dtype.width // 8
@@ -79,7 +86,8 @@ class PagedKVManager(ParamsBase):
79
  val_layout = cute.make_layout((1, async_copy_elems))
80
  gmem_tiled_copy_KV = cute.make_tiled_copy_tv(atom_async_copy, thr_layout, val_layout)
81
  gmem_thr_copy_KV = gmem_tiled_copy_KV.get_slice(thread_idx)
82
- page_entry_per_thread = n_block_size // num_threads
 
83
 
84
  tPrPage = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
85
  tPrPageOffset = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
@@ -97,7 +105,9 @@ class PagedKVManager(ParamsBase):
97
  else:
98
  cV = cute.make_identity_tensor((n_block_size, head_dim_v_padded))
99
  tVcV = gmem_thr_copy_KV.partition_S(cV)
100
- tVpV = utils.predicate_k(tVcV, limit=mV_paged.shape[0])
 
 
101
 
102
  return PagedKVManager(
103
  mPageTable,
@@ -111,6 +121,8 @@ class PagedKVManager(ParamsBase):
111
  num_threads,
112
  head_dim_padded,
113
  head_dim_v_padded,
 
 
114
  gmem_threads_per_row,
115
  page_entry_per_thread,
116
  async_copy_elems,
@@ -144,24 +156,24 @@ class PagedKVManager(ParamsBase):
144
  self.tPrPageOffset[i] = page_offset
145
 
146
  @cute.jit
147
- def compute_X_ptr(self, K_or_V: str):
148
  tPrXPtr = cute.make_rmem_tensor((self.page_entry_per_thread,), cutlass.Int64)
 
 
 
 
149
  for i in cutlass.range(self.page_entry_per_thread, unroll=1):
150
  page = self.tPrPage[i]
151
  page_offset = self.tPrPageOffset[i]
152
- if const_expr(K_or_V == "K"):
153
- tPrXPtr[i] = utils.elem_pointer(self.mK_paged, (page_offset, 0, page)).toint()
154
  else:
155
- tPrXPtr[i] = utils.elem_pointer(self.mV_paged, (0, page_offset, page)).toint()
156
  return tPrXPtr
157
 
158
  @cute.jit
159
- def load_KV(self, n_block: Int32, sX: cute.Tensor, K_or_V: str):
160
- assert K_or_V in ("K", "V")
161
-
162
- tPrXPtr = self.compute_X_ptr(K_or_V)
163
-
164
- # Finesse sX layout to be (M, N).
165
  sX_pi = cute.make_tensor(
166
  sX.iterator,
167
  cute.make_layout(
@@ -169,10 +181,45 @@ class PagedKVManager(ParamsBase):
169
  stride=(sX.stride[0][0], (sX.stride[0][1], sX.stride[2])),
170
  ),
171
  )
172
-
173
  if const_expr(K_or_V == "V"):
174
- # Need to transpose V
175
  sX_pi = cute.make_tensor(sX_pi.iterator, cute.select(sX_pi.layout, mode=[1, 0]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
  head_dim = self.head_dim_v_padded if const_expr(K_or_V == "V") else self.head_dim_padded
178
  cX = cute.make_identity_tensor((self.n_block_size, head_dim))
@@ -198,17 +245,4 @@ class PagedKVManager(ParamsBase):
198
  )
199
  mX_paged_cur = cute.make_tensor(x_gmem_ptr, cute.make_layout((head_dim,)))
200
  mX_paged_cur_copy = cute.tiled_divide(mX_paged_cur, (self.async_copy_elems,))
201
-
202
- for k in cutlass.range_constexpr(cute.size(tXsX, mode=[2])):
203
- ki = tXcX[0, 0, k][1] // self.async_copy_elems
204
- mX_paged_cur_copy_ki = mX_paged_cur_copy[None, ki]
205
- tXsX_k = tXsX[None, m, k]
206
- mX_paged_cur_copy_ki = cute.make_tensor(
207
- mX_paged_cur_copy_ki.iterator, tXsX_k.layout
208
- )
209
- cute.copy(
210
- self.gmem_tiled_copy_KV,
211
- mX_paged_cur_copy_ki,
212
- tXsX_k,
213
- pred=should_load,
214
- )
 
28
  head_dim_padded: cutlass.Constexpr[Int32]
29
  head_dim_v_padded: cutlass.Constexpr[Int32]
30
 
31
+ arch: cutlass.Constexpr[Int32]
32
+ v_gmem_transposed: cutlass.Constexpr[bool]
33
+
34
  gmem_threads_per_row: cutlass.Constexpr[Int32]
35
  page_entry_per_thread: Int32
36
  async_copy_elems: Int32
 
58
  head_dim_v_padded: cutlass.Constexpr[Int32],
59
  num_threads: cutlass.Constexpr[Int32],
60
  dtype: Type[cutlass.Numeric],
61
+ arch: cutlass.Constexpr[int] = 100,
62
  ):
63
+ # SM100 transposes V in gmem to (dv, page_size, num_pages);
64
+ # SM90 keeps V as (page_size, dv, num_pages), same layout as K.
65
+ v_gmem_transposed = arch != 90
66
  universal_copy_bits = 128
67
  async_copy_elems = universal_copy_bits // dtype.width
68
  dtype_bytes = dtype.width // 8
 
86
  val_layout = cute.make_layout((1, async_copy_elems))
87
  gmem_tiled_copy_KV = cute.make_tiled_copy_tv(atom_async_copy, thr_layout, val_layout)
88
  gmem_thr_copy_KV = gmem_tiled_copy_KV.get_slice(thread_idx)
89
+ # Include the final partially populated wave of rows.
90
+ page_entry_per_thread = (n_block_size + num_threads - 1) // num_threads
91
 
92
  tPrPage = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
93
  tPrPageOffset = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
 
105
  else:
106
  cV = cute.make_identity_tensor((n_block_size, head_dim_v_padded))
107
  tVcV = gmem_thr_copy_KV.partition_S(cV)
108
+ # When V is transposed in gmem, dv is shape[0]; otherwise dv is shape[1] (same as K)
109
+ V_limit = cute.size(mV_paged.shape[0 if v_gmem_transposed else 1])
110
+ tVpV = utils.predicate_k(tVcV, limit=V_limit)
111
 
112
  return PagedKVManager(
113
  mPageTable,
 
121
  num_threads,
122
  head_dim_padded,
123
  head_dim_v_padded,
124
+ arch,
125
+ v_gmem_transposed,
126
  gmem_threads_per_row,
127
  page_entry_per_thread,
128
  async_copy_elems,
 
156
  self.tPrPageOffset[i] = page_offset
157
 
158
  @cute.jit
159
+ def compute_X_ptr(self, K_or_V: str, d_offset: int = 0):
160
  tPrXPtr = cute.make_rmem_tensor((self.page_entry_per_thread,), cutlass.Int64)
161
+ mX = self.mK_paged if const_expr(K_or_V == "K") else self.mV_paged
162
+ # K is always (page_size, d, num_pages). V matches K when not transposed,
163
+ # but is (dv, page_size, num_pages) when transposed (SM100).
164
+ transposed = const_expr(K_or_V == "V" and self.v_gmem_transposed)
165
  for i in cutlass.range(self.page_entry_per_thread, unroll=1):
166
  page = self.tPrPage[i]
167
  page_offset = self.tPrPageOffset[i]
168
+ if const_expr(transposed):
169
+ tPrXPtr[i] = utils.elem_pointer(mX, (d_offset, page_offset, page)).toint()
170
  else:
171
+ tPrXPtr[i] = utils.elem_pointer(mX, (page_offset, d_offset, page)).toint()
172
  return tPrXPtr
173
 
174
  @cute.jit
175
+ def _flatten_smem_sm100(self, sX: cute.Tensor, K_or_V: str):
176
+ """Flatten SM100 smem ((a,b), cta_split, k) to (a,(b,k)); transpose V to (d,page_size)."""
 
 
 
 
177
  sX_pi = cute.make_tensor(
178
  sX.iterator,
179
  cute.make_layout(
 
181
  stride=(sX.stride[0][0], (sX.stride[0][1], sX.stride[2])),
182
  ),
183
  )
 
184
  if const_expr(K_or_V == "V"):
 
185
  sX_pi = cute.make_tensor(sX_pi.iterator, cute.select(sX_pi.layout, mode=[1, 0]))
186
+ return sX_pi
187
+
188
+ @cute.jit
189
+ def _copy_row_async(
190
+ self,
191
+ tXsX: cute.Tensor,
192
+ tXcX: cute.Tensor,
193
+ mX_paged_cur_copy: cute.Tensor,
194
+ m: Int32,
195
+ should_load: cute.Tensor,
196
+ ):
197
+ """Issue cp.async copies for one row across all k-tiles."""
198
+ for k in cutlass.range_constexpr(cute.size(tXsX, mode=[2])):
199
+ ki = tXcX[0, 0, k][1] // self.async_copy_elems
200
+ mX_paged_cur_copy_ki = mX_paged_cur_copy[None, ki]
201
+ tXsX_k = tXsX[None, m, k]
202
+ mX_paged_cur_copy_ki = cute.make_tensor(mX_paged_cur_copy_ki.iterator, tXsX_k.layout)
203
+ cute.copy(
204
+ self.gmem_tiled_copy_KV,
205
+ mX_paged_cur_copy_ki,
206
+ tXsX_k,
207
+ pred=should_load,
208
+ )
209
+
210
+ @cute.jit
211
+ def load_KV(self, n_block: Int32, sX: cute.Tensor, K_or_V: str):
212
+ assert K_or_V in ("K", "V")
213
+
214
+ tPrXPtr = self.compute_X_ptr(K_or_V)
215
+
216
+ if const_expr(self.arch == 90):
217
+ # SM90: sX is already stage-sliced by caller (sK[None, None, stage]).
218
+ # Flatten hierarchical modes to get (n_block_size, head_dim).
219
+ sX_pi = cute.group_modes(sX, 0, 1)
220
+ # SM90 does NOT transpose V here (it's transposed via utils.transpose_view before MMA)
221
+ else:
222
+ sX_pi = self._flatten_smem_sm100(sX, K_or_V)
223
 
224
  head_dim = self.head_dim_v_padded if const_expr(K_or_V == "V") else self.head_dim_padded
225
  cX = cute.make_identity_tensor((self.n_block_size, head_dim))
 
245
  )
246
  mX_paged_cur = cute.make_tensor(x_gmem_ptr, cute.make_layout((head_dim,)))
247
  mX_paged_cur_copy = cute.tiled_divide(mX_paged_cur, (self.async_copy_elems,))
248
+ self._copy_row_async(tXsX, tXcX, mX_paged_cur_copy, m, should_load)
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-cuda/pipeline.py CHANGED
@@ -1,6 +1,5 @@
1
  # Copyright (c) 2025, Tri Dao.
2
 
3
- # import math
4
  from typing import Optional
5
  from dataclasses import dataclass
6
 
@@ -11,12 +10,31 @@ from cutlass.pipeline import PipelineState
11
  from cutlass.pipeline import PipelineUserType
12
  from cutlass.pipeline import NamedBarrier as NamedBarrierOg
13
  from cutlass.pipeline import PipelineAsync as PipelineAsyncOg
 
14
  from cutlass.pipeline import PipelineTmaAsync as PipelineTmaAsyncOg
15
  from cutlass.pipeline import PipelineTmaUmma as PipelineTmaUmmaOg
16
  from cutlass.pipeline import PipelineUmmaAsync as PipelineUmmaAsyncOg
17
  from cutlass.pipeline import PipelineAsyncUmma as PipelineAsyncUmmaOg
18
 
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  class PipelineStateSimple:
21
  """
22
  Pipeline state contains an index and phase bit corresponding to the current position in the circular buffer.
@@ -25,9 +43,6 @@ class PipelineStateSimple:
25
  """
26
 
27
  def __init__(self, stages: int, phase_index: Int32):
28
- # assert stages < 2**16
29
- # self._log_stages = int(math.log2(stages))
30
- # assert 1 << self._log_stages == stages, "Number of stages must be a power of 2."
31
  self._stages = stages
32
  self._phase_index = phase_index
33
 
@@ -36,13 +51,10 @@ class PipelineStateSimple:
36
 
37
  @property
38
  def stages(self) -> int:
39
- # return 1 << self._log_stages
40
  return self._stages
41
 
42
  @property
43
  def index(self) -> Int32:
44
- # return self._phase_index & 0xFFFF
45
- # return self._phase_index & ((1 << self._log_stages) - 1)
46
  if const_expr(self._stages == 1):
47
  return Int32(0)
48
  else:
@@ -50,11 +62,8 @@ class PipelineStateSimple:
50
 
51
  @property
52
  def phase(self) -> Int32:
53
- # return self._phase_index >> 16
54
  # PTX docs say that the phase parity needs to be 0 or 1, so by right we need to
55
  # take modulo 2. But in practice just passing the phase in without modulo works fine.
56
- # return (self._phase_index >> self._log_stages) % 2
57
- # return self._phase_index >> self._log_stages
58
  if const_expr(self._stages == 1):
59
  return self._phase_index
60
  else:
@@ -66,21 +75,6 @@ class PipelineStateSimple:
66
  else:
67
  self._phase_index += 1
68
 
69
- # def then_body(phase_index):
70
- # # XOR the phase bit and set the index to 0
71
- # return (phase_index & 0xFFFF0000) ^ (1 << 16)
72
-
73
- # def else_body(phase_index):
74
- # return phase_index
75
-
76
- # self._phase_index = if_generate(
77
- # (self._phase_index & 0xFFFF) == self.stages,
78
- # then_body,
79
- # else_body,
80
- # [self._phase_index],
81
- # [Int32],
82
- # )
83
-
84
  def __extract_mlir_values__(self):
85
  phase_index = self._phase_index
86
  return [phase_index.ir_value()]
@@ -94,7 +88,6 @@ def make_pipeline_state(type: PipelineUserType, stages: int):
94
  Creates a pipeline state. Producers are assumed to start with an empty buffer and have a flipped phase bit of 1.
95
  """
96
  if type is PipelineUserType.Producer:
97
- # return PipelineStateSimple(stages, Int32(1 << 16))
98
  return PipelineStateSimple(stages, Int32(stages))
99
  elif type is PipelineUserType.Consumer:
100
  return PipelineStateSimple(stages, Int32(0))
@@ -102,14 +95,73 @@ def make_pipeline_state(type: PipelineUserType, stages: int):
102
  assert False, "Error: invalid PipelineUserType specified for make_pipeline_state."
103
 
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  @dataclass(frozen=True)
106
  class NamedBarrier(NamedBarrierOg):
107
- @staticmethod
108
- def create(*args, **kwargs):
109
- obj = NamedBarrierOg.create(*args, **kwargs)
110
- # Can't assign to __class__ directly since the dataclass is frozen
111
- object.__setattr__(obj, "__class__", NamedBarrier)
112
- return obj
113
 
114
  @dsl_user_op
115
  def arrive_w_index(self, index: Int32, *, loc=None, ip=None) -> None:
@@ -134,72 +186,121 @@ class NamedBarrier(NamedBarrierOg):
134
  )
135
 
136
 
 
 
 
 
 
 
137
  @dataclass(frozen=True)
138
- class PipelineAsync(PipelineAsyncOg):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  @staticmethod
140
- def create(*args, **kwargs):
 
 
 
 
 
 
 
141
  obj = PipelineAsyncOg.create(*args, **kwargs)
142
- # Can't assign to __class__ directly since the dataclass is frozen
143
- # obj.__class__ = PipelineAsync
144
  object.__setattr__(obj, "__class__", PipelineAsync)
 
 
 
 
145
  return obj
146
 
147
  @dsl_user_op
148
- def producer_acquire_w_index_phase(
149
- self,
150
- index: Int32,
151
- phase: Int32,
152
- try_acquire_token: Optional[Boolean] = None,
153
- *,
154
- loc=None,
155
- ip=None,
156
- ):
157
- if_generate(
158
- try_acquire_token is None or try_acquire_token == 0,
159
- lambda: self.sync_object_empty.wait(index, phase, loc=loc, ip=ip),
160
- loc=loc,
161
- ip=ip,
162
  )
163
 
164
  @dsl_user_op
165
- def producer_commit_w_index(self, index: Int32, *, loc=None, ip=None):
166
- self.sync_object_full.arrive(index, self.producer_mask, loc=loc, ip=ip)
167
-
168
- @dsl_user_op
169
- def consumer_wait_w_index_phase(
170
- self,
171
- index: Int32,
172
- phase: Int32,
173
- try_wait_token: Optional[Boolean] = None,
174
- *,
175
- loc=None,
176
- ip=None,
177
- ):
178
- if_generate(
179
- try_wait_token is None or try_wait_token == 0,
180
- lambda: self.sync_object_full.wait(index, phase, loc=loc, ip=ip),
181
- loc=loc,
182
- ip=ip,
183
  )
184
 
185
- @dsl_user_op
186
- def consumer_release_w_index(self, index: Int32, *, loc=None, ip=None):
187
- self.sync_object_empty.arrive(index, self.consumer_mask, loc=loc, ip=ip)
 
 
188
 
189
 
190
  @dataclass(frozen=True)
191
- class PipelineTmaAsync(PipelineTmaAsyncOg):
192
- """
193
- Override producer_acquire to take in extra_tx_count parameter.
194
- """
195
 
196
  @staticmethod
197
- def create(*args, **kwargs):
198
- obj = PipelineTmaAsyncOg.create(*args, **kwargs)
199
- # Can't assign to __class__ directly since the dataclass is frozen
200
- object.__setattr__(obj, "__class__", PipelineTmaAsync)
 
 
 
 
 
 
201
  return obj
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  @dsl_user_op
204
  def producer_acquire(
205
  self,
@@ -226,19 +327,15 @@ class PipelineTmaAsync(PipelineTmaAsyncOg):
226
  self.sync_object_full.arrive_and_expect_tx(state.index, tx_count, loc=loc, ip=ip)
227
 
228
 
229
- @dataclass(frozen=True)
230
- class PipelineTmaUmma(PipelineTmaUmmaOg):
231
- """
232
- Override producer_acquire to take in extra_tx_count parameter.
233
- """
234
 
235
- @staticmethod
236
- def create(*args, **kwargs):
237
- obj = PipelineTmaUmmaOg.create(*args, **kwargs)
238
- # Can't assign to __class__ directly since the dataclass is frozen
239
- # obj.__class__ = PipelineTmaUmma
240
- object.__setattr__(obj, "__class__", PipelineTmaUmma)
241
- return obj
242
 
243
  @dsl_user_op
244
  def producer_acquire(
@@ -279,162 +376,27 @@ class PipelineTmaUmma(PipelineTmaUmmaOg):
279
  ip=ip,
280
  )
281
 
282
- @dsl_user_op
283
- def producer_acquire_w_index_phase(
284
- self,
285
- index: Int32,
286
- phase: Int32,
287
- try_acquire_token: Optional[Boolean] = None,
288
- *,
289
- loc=None,
290
- ip=None,
291
- ):
292
- """
293
- TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
294
- """
295
- if_generate(
296
- try_acquire_token is None or try_acquire_token == 0,
297
- lambda: self.sync_object_empty.wait(index, phase, loc=loc, ip=ip),
298
- loc=loc,
299
- ip=ip,
300
- )
301
- if_generate(
302
- self.is_leader_cta,
303
- lambda: self.sync_object_full.arrive(index, self.producer_mask, loc=loc, ip=ip),
304
- loc=loc,
305
- ip=ip,
306
- )
307
 
308
- @dsl_user_op
309
- def consumer_wait_w_index_phase(
310
- self,
311
- index: Int32,
312
- phase: Int32,
313
- try_wait_token: Optional[Boolean] = None,
314
- *,
315
- loc=None,
316
- ip=None,
317
- ):
318
- if_generate(
319
- try_wait_token is None or try_wait_token == 0,
320
- lambda: self.sync_object_full.wait(index, phase, loc=loc, ip=ip),
321
- loc=loc,
322
- ip=ip,
323
- )
324
 
325
- @dsl_user_op
326
- def consumer_release_w_index(self, index: Int32, *, loc=None, ip=None):
327
- """
328
- UMMA consumer release buffer empty, cta_group needs to be provided.
329
- """
330
- self.sync_object_empty.arrive(index, self.consumer_mask, self.cta_group, loc=loc, ip=ip)
331
 
332
 
333
  @dataclass(frozen=True)
334
- class PipelineUmmaAsync(PipelineUmmaAsyncOg):
335
- @staticmethod
336
- def create(*args, **kwargs):
337
- obj = PipelineUmmaAsyncOg.create(*args, **kwargs)
338
- # Can't assign to __class__ directly since the dataclass is frozen
339
- object.__setattr__(obj, "__class__", PipelineUmmaAsync)
340
- return obj
341
 
342
- @dsl_user_op
343
- def producer_acquire_w_index_phase(
344
- self,
345
- index: Int32,
346
- phase: Int32,
347
- try_acquire_token: Optional[Boolean] = None,
348
- *,
349
- loc=None,
350
- ip=None,
351
- ):
352
- if_generate(
353
- try_acquire_token is None or try_acquire_token == 0,
354
- lambda: self.sync_object_empty.wait(index, phase, loc=loc, ip=ip),
355
- loc=loc,
356
- ip=ip,
357
- )
358
 
359
- @dsl_user_op
360
- def producer_commit_w_index(self, index: Int32, *, loc=None, ip=None):
361
- """
362
- UMMA producer commit buffer full, cta_group needs to be provided.
363
- """
364
- self.sync_object_full.arrive(index, self.producer_mask, self.cta_group, loc=loc, ip=ip)
365
 
366
- @dsl_user_op
367
- def consumer_wait_w_index_phase(
368
- self,
369
- index: Int32,
370
- phase: Int32,
371
- try_wait_token: Optional[Boolean] = None,
372
- *,
373
- loc=None,
374
- ip=None,
375
- ):
376
- if_generate(
377
- try_wait_token is None or try_wait_token == 0,
378
- lambda: self.sync_object_full.wait(index, phase, loc=loc, ip=ip),
379
- loc=loc,
380
- ip=ip,
381
- )
382
 
383
- @dsl_user_op
384
- def consumer_release_w_index(self, index: Int32, *, loc=None, ip=None):
385
- self.sync_object_empty.arrive(index, self.consumer_mask, loc=loc, ip=ip)
386
 
387
 
388
  @dataclass(frozen=True)
389
- class PipelineAsyncUmma(PipelineAsyncUmmaOg):
390
- @staticmethod
391
- def create(*args, **kwargs):
392
- obj = PipelineAsyncUmmaOg.create(*args, **kwargs)
393
- # Can't assign to __class__ directly since the dataclass is frozen
394
- object.__setattr__(obj, "__class__", PipelineAsyncUmma)
395
- return obj
396
 
397
- @dsl_user_op
398
- def producer_acquire_w_index_phase(
399
- self,
400
- index: Int32,
401
- phase: Int32,
402
- try_acquire_token: Optional[Boolean] = None,
403
- *,
404
- loc=None,
405
- ip=None,
406
- ):
407
- if_generate(
408
- try_acquire_token is None or try_acquire_token == 0,
409
- lambda: self.sync_object_empty.wait(index, phase, loc=loc, ip=ip),
410
- loc=loc,
411
- ip=ip,
412
- )
413
-
414
- @dsl_user_op
415
- def producer_commit_w_index(self, index: Int32, *, loc=None, ip=None):
416
- self.sync_object_full.arrive(index, self.producer_mask, loc=loc, ip=ip)
417
-
418
- @dsl_user_op
419
- def consumer_wait_w_index_phase(
420
- self,
421
- index: Int32,
422
- phase: Int32,
423
- try_wait_token: Optional[Boolean] = None,
424
- *,
425
- loc=None,
426
- ip=None,
427
- ):
428
- if_generate(
429
- try_wait_token is None or try_wait_token == 0,
430
- lambda: self.sync_object_full.wait(index, phase, loc=loc, ip=ip),
431
- loc=loc,
432
- ip=ip,
433
- )
434
 
435
- @dsl_user_op
436
- def consumer_release_w_index(self, index: Int32, *, loc=None, ip=None):
437
- """
438
- UMMA consumer release buffer empty, cta_group needs to be provided.
439
- """
440
- self.sync_object_empty.arrive(index, self.consumer_mask, self.cta_group, loc=loc, ip=ip)
 
1
  # Copyright (c) 2025, Tri Dao.
2
 
 
3
  from typing import Optional
4
  from dataclasses import dataclass
5
 
 
10
  from cutlass.pipeline import PipelineUserType
11
  from cutlass.pipeline import NamedBarrier as NamedBarrierOg
12
  from cutlass.pipeline import PipelineAsync as PipelineAsyncOg
13
+ from cutlass.pipeline import PipelineCpAsync as PipelineCpAsyncOg
14
  from cutlass.pipeline import PipelineTmaAsync as PipelineTmaAsyncOg
15
  from cutlass.pipeline import PipelineTmaUmma as PipelineTmaUmmaOg
16
  from cutlass.pipeline import PipelineUmmaAsync as PipelineUmmaAsyncOg
17
  from cutlass.pipeline import PipelineAsyncUmma as PipelineAsyncUmmaOg
18
 
19
 
20
+ def _override_create(parent_cls, child_cls):
21
+ """Create a static factory that constructs parent_cls then re-classes to child_cls."""
22
+
23
+ @staticmethod
24
+ def create(*args, **kwargs):
25
+ obj = parent_cls.create(*args, **kwargs)
26
+ # Can't assign to __class__ directly since the dataclass is frozen
27
+ object.__setattr__(obj, "__class__", child_cls)
28
+ return obj
29
+
30
+ return create
31
+
32
+
33
+ def _make_state(index: Int32, phase: Int32) -> PipelineState:
34
+ """Construct a PipelineState from index and phase (count/stages unused by callers)."""
35
+ return PipelineState(stages=0, count=Int32(0), index=index, phase=phase)
36
+
37
+
38
  class PipelineStateSimple:
39
  """
40
  Pipeline state contains an index and phase bit corresponding to the current position in the circular buffer.
 
43
  """
44
 
45
  def __init__(self, stages: int, phase_index: Int32):
 
 
 
46
  self._stages = stages
47
  self._phase_index = phase_index
48
 
 
51
 
52
  @property
53
  def stages(self) -> int:
 
54
  return self._stages
55
 
56
  @property
57
  def index(self) -> Int32:
 
 
58
  if const_expr(self._stages == 1):
59
  return Int32(0)
60
  else:
 
62
 
63
  @property
64
  def phase(self) -> Int32:
 
65
  # PTX docs say that the phase parity needs to be 0 or 1, so by right we need to
66
  # take modulo 2. But in practice just passing the phase in without modulo works fine.
 
 
67
  if const_expr(self._stages == 1):
68
  return self._phase_index
69
  else:
 
75
  else:
76
  self._phase_index += 1
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  def __extract_mlir_values__(self):
79
  phase_index = self._phase_index
80
  return [phase_index.ir_value()]
 
88
  Creates a pipeline state. Producers are assumed to start with an empty buffer and have a flipped phase bit of 1.
89
  """
90
  if type is PipelineUserType.Producer:
 
91
  return PipelineStateSimple(stages, Int32(stages))
92
  elif type is PipelineUserType.Consumer:
93
  return PipelineStateSimple(stages, Int32(0))
 
95
  assert False, "Error: invalid PipelineUserType specified for make_pipeline_state."
96
 
97
 
98
+ # ── Shared helpers ───────────────────────────────────────────────────────────
99
+
100
+
101
+ def _call_with_elect_one(parent_method, self, state, elect_one, syncwarp, loc, ip):
102
+ """Optionally wrap a parent pipeline method call in sync_warp + elect_one."""
103
+ if const_expr(elect_one):
104
+ if const_expr(syncwarp):
105
+ cute.arch.sync_warp()
106
+ with cute.arch.elect_one():
107
+ parent_method(self, state, loc=loc, ip=ip)
108
+ else:
109
+ parent_method(self, state, loc=loc, ip=ip)
110
+
111
+
112
+ # ── Mixin: _w_index / _w_index_phase variants that delegate to parent ───────
113
+ # Each parent class has PipelineState-based methods (producer_acquire, producer_commit,
114
+ # consumer_wait, consumer_release). The _w_index_phase variants just construct a
115
+ # PipelineState from (index, phase) and delegate.
116
+
117
+
118
+ class _PipelineIndexPhaseMixin:
119
+ """Mixin providing _w_index_phase / _w_index methods that delegate to PipelineState-based parents."""
120
+
121
+ @dsl_user_op
122
+ def producer_acquire_w_index_phase(
123
+ self,
124
+ index: Int32,
125
+ phase: Int32,
126
+ try_acquire_token: Optional[Boolean] = None,
127
+ *,
128
+ loc=None,
129
+ ip=None,
130
+ ):
131
+ state = _make_state(index, phase)
132
+ # Call the parent's producer_acquire (which takes PipelineState)
133
+ self.producer_acquire(state, try_acquire_token, loc=loc, ip=ip)
134
+
135
+ @dsl_user_op
136
+ def producer_commit_w_index(self, index: Int32, *, loc=None, ip=None):
137
+ state = _make_state(index, Int32(0))
138
+ self.producer_commit(state, loc=loc, ip=ip)
139
+
140
+ @dsl_user_op
141
+ def consumer_wait_w_index_phase(
142
+ self,
143
+ index: Int32,
144
+ phase: Int32,
145
+ try_wait_token: Optional[Boolean] = None,
146
+ *,
147
+ loc=None,
148
+ ip=None,
149
+ ):
150
+ state = _make_state(index, phase)
151
+ self.consumer_wait(state, try_wait_token, loc=loc, ip=ip)
152
+
153
+ @dsl_user_op
154
+ def consumer_release_w_index(self, index: Int32, *, loc=None, ip=None):
155
+ state = _make_state(index, Int32(0))
156
+ self.consumer_release(state, loc=loc, ip=ip)
157
+
158
+
159
+ # ── NamedBarrier ─────────────────────────────────────────────────────────────
160
+
161
+
162
  @dataclass(frozen=True)
163
  class NamedBarrier(NamedBarrierOg):
164
+ create = _override_create(NamedBarrierOg, None) # patched below
 
 
 
 
 
165
 
166
  @dsl_user_op
167
  def arrive_w_index(self, index: Int32, *, loc=None, ip=None) -> None:
 
186
  )
187
 
188
 
189
+ NamedBarrier.create = _override_create(NamedBarrierOg, NamedBarrier)
190
+
191
+
192
+ # ── PipelineAsync ────────────────────────────────────────────────────────────
193
+
194
+
195
  @dataclass(frozen=True)
196
+ class PipelineAsync(_PipelineIndexPhaseMixin, PipelineAsyncOg):
197
+ """
198
+ PipelineAsync with optional elect_one for producer_commit and consumer_release.
199
+
200
+ When elect_one_*=True (set at create time), only one elected thread per warp
201
+ signals the barrier arrive. This is useful when the mask count is set to 1 per warp.
202
+
203
+ Args (to create):
204
+ elect_one_commit: If True, only elected thread signals producer_commit.
205
+ syncwarp_before_commit: If True (default), issue syncwarp before elect_one.
206
+ elect_one_release: If True, only elected thread signals consumer_release.
207
+ syncwarp_before_release: If True (default), issue syncwarp before elect_one.
208
+ Set syncwarp to False when threads are already converged (e.g. after wgmma wait_group).
209
+ """
210
+
211
+ _elect_one_commit: bool = False
212
+ _syncwarp_before_commit: bool = True
213
+ _elect_one_release: bool = False
214
+ _syncwarp_before_release: bool = True
215
+
216
  @staticmethod
217
+ def create(
218
+ *args,
219
+ elect_one_commit: bool = False,
220
+ syncwarp_before_commit: bool = True,
221
+ elect_one_release: bool = False,
222
+ syncwarp_before_release: bool = True,
223
+ **kwargs,
224
+ ):
225
  obj = PipelineAsyncOg.create(*args, **kwargs)
 
 
226
  object.__setattr__(obj, "__class__", PipelineAsync)
227
+ object.__setattr__(obj, "_elect_one_commit", elect_one_commit)
228
+ object.__setattr__(obj, "_syncwarp_before_commit", syncwarp_before_commit)
229
+ object.__setattr__(obj, "_elect_one_release", elect_one_release)
230
+ object.__setattr__(obj, "_syncwarp_before_release", syncwarp_before_release)
231
  return obj
232
 
233
  @dsl_user_op
234
+ def producer_commit(self, state: PipelineState, *, loc=None, ip=None):
235
+ _call_with_elect_one(
236
+ PipelineAsyncOg.producer_commit,
237
+ self,
238
+ state,
239
+ self._elect_one_commit,
240
+ self._syncwarp_before_commit,
241
+ loc,
242
+ ip,
 
 
 
 
 
243
  )
244
 
245
  @dsl_user_op
246
+ def consumer_release(self, state: PipelineState, *, loc=None, ip=None):
247
+ _call_with_elect_one(
248
+ PipelineAsyncOg.consumer_release,
249
+ self,
250
+ state,
251
+ self._elect_one_release,
252
+ self._syncwarp_before_release,
253
+ loc,
254
+ ip,
 
 
 
 
 
 
 
 
 
255
  )
256
 
257
+ # _w_index variants inherited from _PipelineIndexPhaseMixin, which delegate
258
+ # to producer_commit / consumer_release above.
259
+
260
+
261
+ # ── PipelineCpAsync ──────────────────────────────────────────────────────────
262
 
263
 
264
  @dataclass(frozen=True)
265
+ class PipelineCpAsync(_PipelineIndexPhaseMixin, PipelineCpAsyncOg):
266
+ _elect_one_release: bool = False
267
+ _syncwarp_before_release: bool = True
 
268
 
269
  @staticmethod
270
+ def create(
271
+ *args,
272
+ elect_one_release: bool = False,
273
+ syncwarp_before_release: bool = True,
274
+ **kwargs,
275
+ ):
276
+ obj = PipelineCpAsyncOg.create(*args, **kwargs)
277
+ object.__setattr__(obj, "__class__", PipelineCpAsync)
278
+ object.__setattr__(obj, "_elect_one_release", elect_one_release)
279
+ object.__setattr__(obj, "_syncwarp_before_release", syncwarp_before_release)
280
  return obj
281
 
282
+ @dsl_user_op
283
+ def consumer_release(self, state: PipelineState, *, loc=None, ip=None):
284
+ _call_with_elect_one(
285
+ PipelineCpAsyncOg.consumer_release,
286
+ self,
287
+ state,
288
+ self._elect_one_release,
289
+ self._syncwarp_before_release,
290
+ loc,
291
+ ip,
292
+ )
293
+
294
+ # _w_index variants inherited from _PipelineIndexPhaseMixin.
295
+
296
+
297
+ # ── PipelineTmaAsync ────────────────────────────────────────────────────────
298
+
299
+
300
+ @dataclass(frozen=True)
301
+ class PipelineTmaAsync(_PipelineIndexPhaseMixin, PipelineTmaAsyncOg):
302
+ """Override producer_acquire to take in extra_tx_count parameter."""
303
+
304
  @dsl_user_op
305
  def producer_acquire(
306
  self,
 
327
  self.sync_object_full.arrive_and_expect_tx(state.index, tx_count, loc=loc, ip=ip)
328
 
329
 
330
+ PipelineTmaAsync.create = _override_create(PipelineTmaAsyncOg, PipelineTmaAsync)
331
+
332
+
333
+ # ── PipelineTmaUmma ─────────────────────────────────────────────────────────
 
334
 
335
+
336
+ @dataclass(frozen=True)
337
+ class PipelineTmaUmma(_PipelineIndexPhaseMixin, PipelineTmaUmmaOg):
338
+ """Override producer_acquire to take in extra_tx_count parameter."""
 
 
 
339
 
340
  @dsl_user_op
341
  def producer_acquire(
 
376
  ip=ip,
377
  )
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
+ PipelineTmaUmma.create = _override_create(PipelineTmaUmmaOg, PipelineTmaUmma)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
+
383
+ # ── PipelineUmmaAsync ───────────────────────────────────────────────────────
 
 
 
 
384
 
385
 
386
  @dataclass(frozen=True)
387
+ class PipelineUmmaAsync(_PipelineIndexPhaseMixin, PipelineUmmaAsyncOg):
388
+ pass
 
 
 
 
 
389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
+ PipelineUmmaAsync.create = _override_create(PipelineUmmaAsyncOg, PipelineUmmaAsync)
 
 
 
 
 
392
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
 
394
+ # ── PipelineAsyncUmma ───────────────────────────────────────────────────────
 
 
395
 
396
 
397
  @dataclass(frozen=True)
398
+ class PipelineAsyncUmma(_PipelineIndexPhaseMixin, PipelineAsyncUmmaOg):
399
+ pass
 
 
 
 
 
400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
+ PipelineAsyncUmma.create = _override_create(PipelineAsyncUmmaOg, PipelineAsyncUmma)
 
 
 
 
 
build/torch-cuda/prepare_scheduler.py ADDED
@@ -0,0 +1,387 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_prepare_scheduler.cu
2
+ # from CUTLASS C++ to Cute-DSL.
3
+
4
+ from typing import Tuple, Optional, NamedTuple
5
+ import operator
6
+ import torch
7
+ import cuda.bindings.driver as cuda
8
+ import cutlass
9
+ import cutlass.cute as cute
10
+ from cutlass import Int32, const_expr, Float32
11
+ from cutlass.cute import FastDivmodDivisor
12
+ from . import utils as utils
13
+
14
+
15
+ class SchedulerMetadataTensorsTorch(NamedTuple):
16
+ """Class to store scheduler metadata for varlen"""
17
+
18
+ # tensors of shape (batch)
19
+ num_m_blocks_ptr: Optional[torch.Tensor]
20
+ num_splits_dynamic_ptr: Optional[torch.Tensor]
21
+ virtual_batch_idx_ptr: Optional[torch.Tensor]
22
+ num_nheads_in_l2_ptr: Optional[torch.Tensor]
23
+ # tensor of shape (1)
24
+ tile_count_semaphore: Optional[torch.Tensor]
25
+ # tensors of shape (batch + 1)
26
+ # cu_total_m_blocks[b+1] = sum_{i<=b} num_m_blocks[i]
27
+ # cu_total_splits_m_blocks[b+1] = sum_{i<=b} num_m_blocks[i] * num_splits_dynamic[i]
28
+ cu_total_m_blocks: Optional[torch.Tensor] = None
29
+ cu_total_splits_m_blocks: Optional[torch.Tensor] = None
30
+ blocks_to_batch_idx: Optional[torch.Tensor] = None
31
+
32
+
33
+ class FlashPrepareScheduler:
34
+ def __init__(
35
+ self,
36
+ num_warps: int,
37
+ tile_m: int,
38
+ tile_n: int,
39
+ nheads: int,
40
+ nheads_kv: int,
41
+ headdim: int,
42
+ headdim_v: Optional[int] = None,
43
+ is_causal: bool = False,
44
+ packgqa: bool = False,
45
+ sort: bool = False,
46
+ zfill_padded_output: bool = False,
47
+ ):
48
+ self.num_warps = num_warps
49
+ self.is_causal = is_causal
50
+ self.packgqa = packgqa
51
+ assert not sort, "LPT batch sort not yet implemented"
52
+ self.sort = sort
53
+ self.num_threads_per_warp = 32
54
+ self.tile_m = tile_m
55
+ self.tile_n = tile_n
56
+ self.d = headdim
57
+ self.dv = headdim_v if headdim_v is not None else headdim
58
+ self.k_num_batch_per_warp = 31
59
+ self.k_smem_size = 1
60
+ self.zfill_padded_output = zfill_padded_output
61
+
62
+ # for pack gqa, query heads per kv head is combined with seqlen_q
63
+ self.nheads_computed = nheads if not self.packgqa else nheads_kv
64
+
65
+ # L2 cache calculations
66
+ self.qhead_per_khead = nheads // nheads_kv
67
+ self.size_l2_divisor = (
68
+ 1
69
+ if self.qhead_per_khead == 1
70
+ else (
71
+ 2
72
+ if self.qhead_per_khead <= 2
73
+ else (4 if self.qhead_per_khead <= 4 else (8 if self.qhead_per_khead <= 8 else 16))
74
+ )
75
+ )
76
+ self.size_l2 = (32 * 1024 * 1024) // self.size_l2_divisor
77
+ element_size = 2
78
+ self.size_one_kvblock = self.tile_n * (self.d + self.dv) * element_size
79
+ self.max_kvblocks_in_l2 = (
80
+ self.size_l2 + self.size_one_kvblock - 1
81
+ ) // self.size_one_kvblock
82
+
83
+ @staticmethod
84
+ def get_grid_shape(num_batch: int) -> Tuple[int, int, int]:
85
+ num_ctas = (num_batch + (31 * 32 - 1)) // (31 * 32)
86
+ return (num_ctas, 1, 1)
87
+
88
+ @cute.jit
89
+ def __call__(
90
+ self,
91
+ seqlen_q_static: int,
92
+ seqlen_k_static: int,
93
+ seqlen_k_new_static: int,
94
+ mCuSeqlensQ: Optional[cute.Tensor],
95
+ mCuSeqlensK: Optional[cute.Tensor],
96
+ mCuSeqlensKNew: Optional[cute.Tensor],
97
+ mSeqUsedQ: Optional[cute.Tensor],
98
+ mSeqUsedK: Optional[cute.Tensor],
99
+ mLeftPadK: Optional[cute.Tensor],
100
+ num_batch: int,
101
+ num_splits_static: int,
102
+ tile_count_semaphore: Optional[cute.Tensor],
103
+ num_m_blocks_ptr: Optional[cute.Tensor],
104
+ num_splits_dynamic_ptr: Optional[cute.Tensor],
105
+ virtual_batch_idx_ptr: Optional[cute.Tensor],
106
+ num_nheads_in_l2_ptr: Optional[cute.Tensor],
107
+ n_blocks_per_split: Optional[int], # overrides heuristic
108
+ stream: cuda.CUstream,
109
+ ):
110
+ tile_m_divmod = FastDivmodDivisor(self.tile_m)
111
+ tile_n_divmod = FastDivmodDivisor(self.tile_n)
112
+
113
+ @cute.struct
114
+ class SharedStorage:
115
+ total_blocks_smem: cute.struct.MemRange[Int32, self.k_smem_size]
116
+
117
+ self.shared_storage = SharedStorage
118
+
119
+ block = (32 * self.num_warps, 1, 1)
120
+ grid = self.get_grid_shape(num_batch)
121
+
122
+ hardware_info = cutlass.utils.HardwareInfo()
123
+ num_sm = hardware_info.get_device_multiprocessor_count()
124
+
125
+ self.kernel(
126
+ seqlen_q_static,
127
+ seqlen_k_static,
128
+ seqlen_k_new_static,
129
+ mCuSeqlensQ,
130
+ mCuSeqlensK,
131
+ mCuSeqlensKNew,
132
+ mSeqUsedQ,
133
+ mSeqUsedK,
134
+ mLeftPadK,
135
+ num_batch,
136
+ num_sm,
137
+ num_splits_static,
138
+ tile_m_divmod,
139
+ tile_n_divmod,
140
+ tile_count_semaphore,
141
+ num_m_blocks_ptr,
142
+ num_splits_dynamic_ptr,
143
+ virtual_batch_idx_ptr,
144
+ num_nheads_in_l2_ptr,
145
+ n_blocks_per_split,
146
+ ).launch(
147
+ grid=grid,
148
+ block=block,
149
+ stream=stream,
150
+ smem=self.shared_storage.size_in_bytes(),
151
+ )
152
+
153
+ @cute.kernel
154
+ def kernel(
155
+ self,
156
+ seqlen_q_static: Int32,
157
+ seqlen_k_static: Int32,
158
+ seqlen_k_new_static: Int32,
159
+ mCuSeqlensQ: Optional[cute.Tensor],
160
+ mCuSeqlensK: Optional[cute.Tensor],
161
+ mCuSeqlensKNew: Optional[cute.Tensor],
162
+ mSeqUsedQ: Optional[cute.Tensor],
163
+ mSeqUsedK: Optional[cute.Tensor],
164
+ mLeftPadK: Optional[cute.Tensor],
165
+ num_batch: Int32,
166
+ num_sm: Int32,
167
+ num_splits_static: Int32,
168
+ tile_m_divmod: FastDivmodDivisor,
169
+ tile_n_divmod: FastDivmodDivisor,
170
+ tile_count_semaphore: Optional[cute.Tensor],
171
+ num_m_blocks_ptr: Optional[cute.Tensor],
172
+ num_splits_dynamic_ptr: Optional[cute.Tensor],
173
+ virtual_batch_idx_ptr: Optional[cute.Tensor],
174
+ num_nheads_in_l2_ptr: Optional[cute.Tensor],
175
+ n_blocks_per_split: Optional[Int32],
176
+ ):
177
+ bidx, _, _ = cute.arch.block_idx()
178
+ tidx, _, _ = cute.arch.thread_idx()
179
+ grid_dimx, _, _ = cute.arch.grid_dim()
180
+ warp_idx = cute.arch.warp_idx()
181
+ lane_idx = cute.arch.lane_idx()
182
+
183
+ smem = cutlass.utils.SmemAllocator()
184
+ storage = smem.allocate(self.shared_storage)
185
+ total_blocks_smem = storage.total_blocks_smem.get_tensor((1,))
186
+
187
+ if tidx == 0:
188
+ total_blocks_smem[0] = Int32(0)
189
+ cute.arch.sync_threads()
190
+
191
+ if const_expr(tile_count_semaphore is not None):
192
+ if tidx == 0:
193
+ tile_count_semaphore[0] = Int32(0)
194
+
195
+ batch_cta_idx_offset = bidx * self.k_num_batch_per_warp * self.num_warps
196
+ bidb_start = batch_cta_idx_offset + self.k_num_batch_per_warp * warp_idx
197
+ batch_idx = lane_idx + bidb_start
198
+
199
+ num_m_blocks, _ = self.get_num_m_blocks_and_seqlen(
200
+ lane_idx,
201
+ batch_idx,
202
+ mSeqUsedQ,
203
+ mCuSeqlensQ,
204
+ seqlen_q_static,
205
+ tile_m_divmod,
206
+ num_batch,
207
+ )
208
+
209
+ num_n_blocks = self.get_num_n_blocks(
210
+ lane_idx,
211
+ batch_idx,
212
+ mSeqUsedK,
213
+ mCuSeqlensK,
214
+ mCuSeqlensKNew,
215
+ seqlen_k_static,
216
+ seqlen_k_new_static,
217
+ mLeftPadK,
218
+ tile_n_divmod,
219
+ num_batch,
220
+ )
221
+
222
+ num_splits_dynamic = Int32(1)
223
+ if const_expr(n_blocks_per_split is not None):
224
+ num_splits_dynamic = cutlass.min(
225
+ cute.ceil_div(num_n_blocks, n_blocks_per_split), num_splits_static
226
+ )
227
+ if const_expr(self.zfill_padded_output):
228
+ num_splits_dynamic = cutlass.max(num_splits_dynamic, Int32(1))
229
+ if num_splits_dynamic > 0:
230
+ num_n_blocks = cute.ceil_div(num_n_blocks, num_splits_dynamic)
231
+ else:
232
+ if grid_dimx > 1 or num_splits_static == 1:
233
+ num_splits_dynamic = Int32(1)
234
+ else:
235
+ total_blocks = num_m_blocks * num_n_blocks
236
+ total_blocks = utils.warp_reduce(total_blocks, operator.add)
237
+ if lane_idx == 0:
238
+ utils.atomic_add_i32(total_blocks, total_blocks_smem.iterator)
239
+
240
+ cute.arch.sync_threads()
241
+
242
+ total_blocks = total_blocks_smem[0]
243
+
244
+ sm_margin = max(Float32(num_sm) / 128 + 0.001, 1.1) # e.g. 148/128 = 1.15625
245
+ blocks_per_sm = cutlass.max(
246
+ Int32(
247
+ (
248
+ Float32(total_blocks)
249
+ * sm_margin
250
+ * Float32(self.nheads_computed)
251
+ / Float32(num_sm)
252
+ )
253
+ ),
254
+ Int32(1),
255
+ )
256
+ # blocks_per_sm = cute.ceil_div(total_blocks * self.nheads_computed, num_sm)
257
+ num_splits_dynamic = cutlass.min(
258
+ cute.ceil_div(num_n_blocks, blocks_per_sm), num_splits_static
259
+ )
260
+ if const_expr(self.zfill_padded_output):
261
+ num_splits_dynamic = cutlass.max(num_splits_dynamic, Int32(1))
262
+ if num_splits_dynamic > 0:
263
+ num_n_blocks = cute.ceil_div(num_n_blocks, num_splits_dynamic)
264
+
265
+ if const_expr(self.sort):
266
+ # TODO: Implement sort logic
267
+ pass
268
+
269
+ if batch_idx < num_batch and lane_idx < self.k_num_batch_per_warp:
270
+ if const_expr(num_m_blocks_ptr is not None):
271
+ num_m_blocks_ptr[batch_idx] = num_m_blocks
272
+ if const_expr(num_splits_dynamic_ptr is not None):
273
+ num_splits_dynamic_ptr[batch_idx] = num_splits_dynamic
274
+ if const_expr(num_nheads_in_l2_ptr is not None):
275
+ nheads_in_l2 = self.get_num_nheads_in_l2(num_n_blocks)
276
+ num_nheads_in_l2_ptr[batch_idx] = nheads_in_l2
277
+
278
+ @cute.jit
279
+ def get_num_m_blocks_and_seqlen(
280
+ self,
281
+ lane_idx: Int32,
282
+ batch_idx: Int32,
283
+ mSeqUsedQ: Optional[cute.Tensor],
284
+ mCuSeqlensQ: Optional[cute.Tensor],
285
+ seqlen_q_static: Int32,
286
+ tile_m_divmod: FastDivmodDivisor,
287
+ num_batch: Int32,
288
+ ):
289
+ seqlen = Int32(0)
290
+ if const_expr(mSeqUsedQ is not None):
291
+ seqlen = mSeqUsedQ[batch_idx] if batch_idx < num_batch else Int32(0)
292
+ elif const_expr(mCuSeqlensQ is not None):
293
+ # Since k_num_batch_per_warp = 31, lane 31 never processes batches
294
+ # So shuffle_down is safe: lane 30 gets lane 31's value (which is 0)
295
+ # Only access cu_seqlens if batch_idx is valid (0 to num_batch inclusive)
296
+ cur_cu_seqlen = Int32(0)
297
+ if batch_idx <= num_batch:
298
+ cur_cu_seqlen = mCuSeqlensQ[batch_idx]
299
+ next_cu_seqlen = cute.arch.shuffle_sync_down(cur_cu_seqlen, offset=1)
300
+ seqlen = next_cu_seqlen - cur_cu_seqlen
301
+ else:
302
+ seqlen = seqlen_q_static
303
+
304
+ seqlen_for_blocks = seqlen
305
+ if const_expr(self.packgqa):
306
+ seqlen_for_blocks = seqlen * self.qhead_per_khead
307
+ num_m_blocks = (
308
+ (seqlen_for_blocks + self.tile_m - 1) // tile_m_divmod
309
+ if batch_idx < num_batch and lane_idx < self.k_num_batch_per_warp
310
+ else Int32(0)
311
+ )
312
+ return (num_m_blocks, seqlen)
313
+
314
+ @cute.jit
315
+ def get_num_n_blocks(
316
+ self,
317
+ lane_idx: Int32,
318
+ batch_idx: Int32,
319
+ mSeqUsedK: Optional[cute.Tensor],
320
+ mCuSeqlensK: Optional[cute.Tensor],
321
+ mCuSeqlensKNew: Optional[cute.Tensor],
322
+ seqlen_k_static: Int32,
323
+ seqlen_k_new_static: Int32,
324
+ mLeftPadK: Optional[cute.Tensor],
325
+ tile_n_divmod: FastDivmodDivisor,
326
+ num_batch: Int32,
327
+ ):
328
+ leftpad_k = (
329
+ mLeftPadK[batch_idx]
330
+ if const_expr(mLeftPadK is not None) and batch_idx < num_batch
331
+ else Int32(0)
332
+ )
333
+ seqlen = Int32(0)
334
+ if const_expr(mSeqUsedK is not None):
335
+ seqlen = mSeqUsedK[batch_idx] if batch_idx < num_batch else Int32(0)
336
+ elif const_expr(mCuSeqlensK is not None):
337
+ # Since k_num_batch_per_warp = 31, lane 31 never processes batches
338
+ # So shuffle_down is safe: lane 30 gets lane 31's value (which is 0)
339
+ # Only access cu_seqlens if batch_idx is valid (0 to num_batch inclusive)
340
+ cur_cu_seqlen = Int32(0)
341
+ if batch_idx <= num_batch:
342
+ cur_cu_seqlen = mCuSeqlensK[batch_idx]
343
+ next_cu_seqlen = cute.arch.shuffle_sync_down(cur_cu_seqlen, offset=1)
344
+ seqlen = next_cu_seqlen - cur_cu_seqlen
345
+ else:
346
+ seqlen = seqlen_k_static
347
+
348
+ seqlen_new = Int32(0)
349
+ if const_expr(mCuSeqlensKNew is not None):
350
+ # Since k_num_batch_per_warp = 31, lane 31 never processes batches
351
+ # So shuffle_down is safe: lane 30 gets lane 31's value (which is 0)
352
+ # Only access cu_seqlens if batch_idx is valid (0 to num_batch inclusive)
353
+ cur_cu_seqlen_new = Int32(0)
354
+ if batch_idx <= num_batch:
355
+ cur_cu_seqlen_new = mCuSeqlensKNew[batch_idx]
356
+ next_cu_seqlen_new = cute.arch.shuffle_sync_down(cur_cu_seqlen_new, offset=1)
357
+ seqlen_new = next_cu_seqlen_new - cur_cu_seqlen_new
358
+ else:
359
+ seqlen_new = seqlen_k_new_static
360
+ seqlen = seqlen - leftpad_k + seqlen_new
361
+ return (
362
+ (seqlen + self.tile_n - 1) // tile_n_divmod
363
+ if batch_idx < num_batch and lane_idx < self.k_num_batch_per_warp
364
+ else Int32(0)
365
+ )
366
+
367
+ @cute.jit
368
+ def get_num_nheads_in_l2(
369
+ self,
370
+ num_n_blocks: Int32,
371
+ ):
372
+ max_kvblocks_in_l2 = self.max_kvblocks_in_l2
373
+ qhead_per_khead = self.qhead_per_khead
374
+ nheads_in_l2 = Int32(16)
375
+ if num_n_blocks * Int32(16) <= max_kvblocks_in_l2:
376
+ nheads_in_l2 = Int32(16)
377
+ elif num_n_blocks * Int32(8) <= max_kvblocks_in_l2:
378
+ nheads_in_l2 = Int32(8)
379
+ elif num_n_blocks * Int32(4) <= max_kvblocks_in_l2:
380
+ nheads_in_l2 = Int32(4)
381
+ elif num_n_blocks * Int32(2) <= max_kvblocks_in_l2:
382
+ nheads_in_l2 = Int32(2)
383
+ else:
384
+ nheads_in_l2 = Int32(1)
385
+ if const_expr(not self.packgqa):
386
+ nheads_in_l2 *= qhead_per_khead
387
+ return cutlass.min(nheads_in_l2, self.nheads_computed)
build/torch-cuda/quack/__init__.py CHANGED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """Vendored subset of quack (https://github.com/Dao-AILab/quack).
2
+
3
+ Importing `.dsl` installs quack's CuTe tensor indexing patch. It must happen
4
+ here, exactly as upstream's `quack/__init__.py` does it, because
5
+ `quack.copy_utils` indexes tensors with `...` and CuTe does not support that
6
+ without the patch.
7
+ """
8
+
9
+ from . import dsl # noqa: F401
build/torch-cuda/quack/activation.py DELETED
@@ -1,568 +0,0 @@
1
- # Copyright (c) 2025, Tri Dao.
2
-
3
- import math
4
- from typing import Tuple
5
- from functools import partial
6
-
7
- import cutlass.cute as cute
8
- from cutlass import Float32, Boolean, const_expr
9
- from cutlass.cutlass_dsl import T, dsl_user_op
10
- from cutlass._mlir.dialects import llvm, nvvm
11
-
12
-
13
- F32_or_F32x2 = Float32 | Tuple[Float32, Float32]
14
-
15
-
16
- sub_packed_f32x2 = partial(
17
- cute.arch.calc_packed_f32x2_op,
18
- src_c=None,
19
- calc_func=nvvm.sub_packed_f32x2,
20
- )
21
-
22
-
23
- @dsl_user_op
24
- def tanh(a: float | Float32, *, loc=None, ip=None) -> Float32:
25
- return Float32(
26
- llvm.inline_asm(
27
- T.f32(),
28
- [Float32(a).ir_value(loc=loc, ip=ip)],
29
- "tanh.approx.f32 $0, $1;",
30
- "=f,f",
31
- has_side_effects=False,
32
- is_align_stack=False,
33
- asm_dialect=llvm.AsmDialect.AD_ATT,
34
- )
35
- )
36
-
37
-
38
- @dsl_user_op
39
- def sigmoid(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
40
- if const_expr(not isinstance(x, tuple)):
41
- # return 0.5 + 0.5 * cute.math.tanh(0.5 * x, fastmath=True)
42
- return 0.5 + 0.5 * tanh(0.5 * x)
43
- else:
44
- x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x)
45
- tanh_x_half = (tanh(x_half[0]), tanh(x_half[1]))
46
- return cute.arch.fma_packed_f32x2(tanh_x_half, (0.5, 0.5), (0.5, 0.5))
47
-
48
-
49
- @dsl_user_op
50
- def dsigmoid_from_output(out: Float32, dout: Float32, *, loc=None, ip=None) -> Float32:
51
- # return dout * out * (1.0 - out)
52
- return dout * (out - out * out)
53
-
54
-
55
- @dsl_user_op
56
- def relu(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
57
- if const_expr(not isinstance(x, tuple)):
58
- return cute.arch.fmax(x, Float32(0.0))
59
- else:
60
- return cute.arch.fmax(x[0], Float32(0.0)), cute.arch.fmax(x[1], Float32(0.0))
61
-
62
-
63
- @dsl_user_op
64
- @cute.jit
65
- def drelu(
66
- x: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
67
- ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
68
- if const_expr(not isinstance(x, tuple)):
69
- x_pos = Boolean(x > 0)
70
- return dout if x_pos else Float32(0.0), cute.arch.fmax(x, Float32(0.0))
71
- else:
72
- x0_pos = Boolean(x[0] > 0)
73
- x1_pos = Boolean(x[1] > 0)
74
- dx = (dout[0] if x0_pos else Float32(0.0), dout[1] if x1_pos else Float32(0.0))
75
- return dx, relu(x)
76
-
77
-
78
- @dsl_user_op
79
- def relu_sq(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
80
- if const_expr(not isinstance(x, tuple)):
81
- return cute.arch.fmax(x, Float32(0.0)) * x
82
- else:
83
- relu_x = (cute.arch.fmax(x[0], Float32(0.0)), cute.arch.fmax(x[1], Float32(0.0)))
84
- return cute.arch.mul_packed_f32x2(relu_x, x)
85
-
86
-
87
- @dsl_user_op
88
- @cute.jit
89
- def drelu_sq(
90
- x: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
91
- ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
92
- """
93
- ReLU squared backward pass: computes gradient w.r.t. x and recomputes forward
94
- Given: relu_sq_out = max(x, 0) * x, and dout = grad w.r.t. relu_sq_out
95
- Returns: (dx, relu_sq_out) where:
96
- - dx = dout * 2 * x if x > 0, else 0
97
- - relu_sq_out = max(x, 0) * x
98
- """
99
- if const_expr(not isinstance(x, tuple)):
100
- relu_x = relu(x)
101
- relu_sq_out = relu_x * x
102
- # Derivative: d/dx[max(x,0) * x] = 2*x if x > 0, else 0
103
- dx = 2.0 * (dout * relu_x)
104
- return dx, relu_sq_out
105
- else:
106
- relu_x = relu(x)
107
- relu_sq_out = cute.arch.mul_packed_f32x2(relu_x, x)
108
- dx = cute.arch.mul_packed_f32x2((2.0, 2.0), cute.arch.mul_packed_f32x2(dout, relu_x))
109
- return dx, relu_sq_out
110
-
111
-
112
- @dsl_user_op
113
- def gelu_tanh_approx(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
114
- """
115
- gelu(x) = 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
116
- = 0.5 * x * (1 + tanh(x * (0.797885 + 0.0356774 * x * x)))
117
- """
118
- sqrt_2_over_pi = math.sqrt(2 / math.pi) # ~0.797885
119
- sqrt_2_over_pi_coeff = 0.044715 * sqrt_2_over_pi # ~0.0356774
120
- if const_expr(not isinstance(x, tuple)):
121
- return 0.5 * (
122
- x
123
- # Currently cute.math.tanh(x, fastmath=True) generates very slow code
124
- # * (1 + cute.math.tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * (x * x)), fastmath=True))
125
- * (1.0 + tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * (x * x))))
126
- )
127
- else:
128
- x_sq = cute.arch.mul_packed_f32x2(x, x)
129
- x_sq_scaled = cute.arch.fma_packed_f32x2(
130
- x_sq, (sqrt_2_over_pi_coeff, sqrt_2_over_pi_coeff), (sqrt_2_over_pi, sqrt_2_over_pi)
131
- )
132
- z = cute.arch.mul_packed_f32x2(x, x_sq_scaled)
133
- tanh_z = (tanh(z[0]), tanh(z[1]))
134
- x_tanh_z = cute.arch.fma_packed_f32x2(tanh_z, x, x)
135
- return cute.arch.mul_packed_f32x2((0.5, 0.5), x_tanh_z)
136
-
137
-
138
- @dsl_user_op
139
- def dgelu_tanh_approx(
140
- x: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
141
- ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
142
- """
143
- GELU tanh approximation backward pass: computes gradient w.r.t. x and recomputes forward
144
- Given: gelu_out = 0.5 * x * (1 + tanh(x * (c1 + c2 * x^2))), and dout = grad w.r.t. gelu_out
145
- Returns: (dx, gelu_out)
146
-
147
- Derivative uses the chain rule:
148
- d/dx[gelu(x)] = 0.5 * (1 + tanh(z)) + 0.5 * x * sech^2(z) * dz/dx
149
- where z = x * (c1 + c2 * x^2), dz/dx = c1 + 3 * c2 * x^2
150
- and sech^2(z) = 1 - tanh^2(z)
151
- """
152
- sqrt_2_over_pi = math.sqrt(2 / math.pi) # c1 ~0.797885
153
- sqrt_2_over_pi_coeff = 0.044715 * sqrt_2_over_pi # c2 ~0.0356774
154
- sqrt_2_over_pi_coeff_3 = 3.0 * sqrt_2_over_pi_coeff # c3 ~0.01070322
155
-
156
- if const_expr(not isinstance(x, tuple)):
157
- # Compute z = x * (c1 + c2 * x^2)
158
- x_sq = x * x
159
- # tanh_z = cute.math.tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * x_sq), fastmath=True)
160
- tanh_z = tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * x_sq))
161
- half_tanh_z_plus_one = 0.5 + 0.5 * tanh_z
162
- gelu_out = x * half_tanh_z_plus_one
163
-
164
- # Compute gradient
165
- # sech^2(z) = 1 - tanh^2(z)
166
- sech2_z = 1 - tanh_z * tanh_z
167
- # dz/dx = c1 + 3 * c2 * x^2
168
- dz_dx = sqrt_2_over_pi + sqrt_2_over_pi_coeff_3 * x_sq
169
- # d/dx[gelu(x)] = 0.5 * (1 + tanh(z)) + 0.5 * x * sech^2(z) * dz/dx
170
- dgelu = half_tanh_z_plus_one + x * (0.5 * (sech2_z * dz_dx))
171
-
172
- dx = dout * dgelu
173
- return dx, gelu_out
174
- else:
175
- # Compute z = x * (c1 + c2 * x^2)
176
- x_sq = cute.arch.mul_packed_f32x2(x, x)
177
- x_sq_scaled = cute.arch.fma_packed_f32x2(
178
- x_sq, (sqrt_2_over_pi_coeff, sqrt_2_over_pi_coeff), (sqrt_2_over_pi, sqrt_2_over_pi)
179
- )
180
- z = cute.arch.mul_packed_f32x2(x, x_sq_scaled)
181
- tanh_z = (tanh(z[0]), tanh(z[1]))
182
- half_tanh_z_plus_one = cute.arch.fma_packed_f32x2(tanh_z, (0.5, 0.5), (0.5, 0.5))
183
- gelu_out = cute.arch.mul_packed_f32x2(x, half_tanh_z_plus_one)
184
-
185
- # Compute gradient
186
- # sech^2(z) = 1 - tanh^2(z)
187
- sech2_z = cute.arch.fma_packed_f32x2(tanh_z, (-tanh_z[0], -tanh_z[1]), (1.0, 1.0))
188
- # dz/dx = c1 + 3 * c2 * x^2
189
- dz_dx = cute.arch.fma_packed_f32x2(
190
- x_sq, (sqrt_2_over_pi_coeff_3, sqrt_2_over_pi_coeff_3), (sqrt_2_over_pi, sqrt_2_over_pi)
191
- )
192
- # d/dx[gelu(x)] = 0.5 * (1 + tanh(z)) + 0.5 * x * sech^2(z) * dz/dx
193
- sech2_dz_dx = cute.arch.mul_packed_f32x2(sech2_z, dz_dx)
194
- x_sech2_dz_dx = cute.arch.mul_packed_f32x2(x, sech2_dz_dx)
195
- dgelu = cute.arch.fma_packed_f32x2(x_sech2_dz_dx, (0.5, 0.5), half_tanh_z_plus_one)
196
-
197
- dx = cute.arch.mul_packed_f32x2(dout, dgelu)
198
- return dx, gelu_out
199
-
200
-
201
- @dsl_user_op
202
- @cute.jit
203
- def softplus(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
204
- if const_expr(not isinstance(x, tuple)):
205
- use_linear = Boolean(x > 20.0)
206
- return (
207
- cute.math.log(Float32(cute.math.exp(x, fastmath=True)) + 1.0, fastmath=True)
208
- if not use_linear
209
- else x
210
- )
211
- else:
212
- log2_e = math.log2(math.e)
213
- x_log2e = cute.arch.mul_packed_f32x2(x, (log2_e, log2_e))
214
- x_exp = (cute.math.exp(x_log2e[0], fastmath=True), cute.math.exp(x_log2e[1], fastmath=True))
215
- x_exp_p1 = cute.arch.add_packed_f32x2(x_exp, (1.0, 1.0))
216
- log_x_exp_p1 = (
217
- cute.math.log2(x_exp_p1[0], fastmath=True),
218
- cute.math.log2(x_exp_p1[1], fastmath=True),
219
- )
220
- ln2 = math.log(2.0)
221
- softplus_x = cute.arch.mul_packed_f32x2(log_x_exp_p1, (ln2, ln2))
222
- use_linear_0 = Boolean(x[0] > 20.0)
223
- use_linear_1 = Boolean(x[1] > 20.0)
224
- return (
225
- softplus_x[0] if not use_linear_0 else x[0],
226
- softplus_x[1] if not use_linear_1 else x[1],
227
- )
228
-
229
-
230
- @dsl_user_op
231
- @cute.jit
232
- def dsoftplus_from_output(out: Float32, dout: Float32, *, loc=None, ip=None) -> Float32:
233
- use_linear = Boolean(out > 20.0)
234
- # dx = dout * (1.0 - cute.math.exp(-out, fastmath=True)) if not use_linear else dout
235
- dx = dout - dout * cute.math.exp(-out, fastmath=True)
236
- return dx if not use_linear else dout
237
-
238
-
239
- @dsl_user_op
240
- def silu(x: F32_or_F32x2, *, already_halved: bool = False, loc=None, ip=None) -> F32_or_F32x2:
241
- """
242
- silu(x) = x * sigmoid(x) = x * (1 + tanh(x / 2)) / 2 = (0.5 * x) * tanh(0.5 * x) + (0.5 * x)
243
- This compiles down to 3 SASS instructions: FMUL to get 0.5 * x, MUFU.TANH, and FFMA.
244
- """
245
- if const_expr(not isinstance(x, tuple)):
246
- x_half = 0.5 * x if const_expr(not already_halved) else x
247
- # return x_half * cute.math.tanh(x_half, fastmath=True) + x_half
248
- return x_half * tanh(x_half) + x_half
249
- else:
250
- x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x) if const_expr(not already_halved) else x
251
- tanh_x_half = (tanh(x_half[0]), tanh(x_half[1]))
252
- return cute.arch.fma_packed_f32x2(x_half, tanh_x_half, x_half)
253
-
254
-
255
- @dsl_user_op
256
- def swiglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
257
- if const_expr(not isinstance(x, tuple)):
258
- return silu(x) * y
259
- else:
260
- return cute.arch.mul_packed_f32x2(silu(x), y)
261
-
262
-
263
- @dsl_user_op
264
- def dswiglu(
265
- x: F32_or_F32x2,
266
- y: F32_or_F32x2,
267
- dout: F32_or_F32x2,
268
- *,
269
- already_halved: bool = False,
270
- loc=None,
271
- ip=None,
272
- ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
273
- """
274
- SwiGLU backward pass: computes gradients w.r.t. x (gate) and y (up projection)
275
- Given: swiglu_out = silu(x) * y, and dout = grad w.r.t. swiglu_out
276
- Returns: (dx, dy, swiglu_out) where dx = dout * y * d_silu(x), dy = dout * silu(x)
277
-
278
- d_silu(x) = sigmoid(x) * (1 + x * (1 - sigmoid(x)))
279
-
280
- This has been optimized to use fewer instructions (i.e. we expand things out
281
- to use FFMA instead of FADD and FMUL).
282
- """
283
- if const_expr(not isinstance(x, tuple)):
284
- # Compute sigmoid(x) using tanh: sigmoid(x) = 0.5 * (1 + tanh(0.5 * x))
285
- # FMUL, MUFU.TANH, then FFMA
286
- if const_expr(not already_halved):
287
- sigmoid_x = sigmoid(x)
288
- silu_x = x * sigmoid_x # FMUL
289
- else:
290
- tanh_x = tanh(x) # MUFU.TANH
291
- sigmoid_x = 0.5 * tanh_x + 0.5 # FFMA
292
- silu_x = x * tanh_x + x # FFMA
293
- silu_x_dout = silu_x * dout # FMUL
294
- # d_silu(x) * dout
295
- # = sigmoid_x * (1 + x * (1 - sigmoid_x)) * dout
296
- # = (sigmoid_x + sigmoid_x * x * (1 - sigmoid_x)) * dout
297
- # = (sigmoid_x + silu_x * (1 - sigmoid_x)) * dout
298
- # = (sigmoid_x + silu_x - silu_x * sigmoid_x) * dout
299
- # = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x * dout
300
- d_silu_x_dout = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x_dout # FFMA, FFMA
301
- dx = d_silu_x_dout * y # FMUL
302
- dy = silu_x_dout
303
- swiglu_out = silu_x * y # FMUL
304
- # Overall it's 1 MUFU.TANH, 5 FMUL, 3 FFMA
305
- return dx, dy, swiglu_out
306
- else:
307
- # Compute sigmoid(x) and silu(x)
308
- if const_expr(not already_halved):
309
- sigmoid_x = sigmoid(x)
310
- silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_x)
311
- else:
312
- tanh_x = (tanh(x[0]), tanh(x[1]))
313
- sigmoid_x = cute.arch.fma_packed_f32x2(tanh_x, (0.5, 0.5), (0.5, 0.5))
314
- silu_x = cute.arch.fma_packed_f32x2(x, tanh_x, x)
315
- silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
316
- # d_silu(x) * dout = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x * dout
317
- sigmoid_x_minus_silu_x_sigmoid_x = cute.arch.fma_packed_f32x2(
318
- sigmoid_x, (-silu_x[0], -silu_x[1]), sigmoid_x
319
- )
320
- d_silu_x_dout = cute.arch.fma_packed_f32x2(
321
- sigmoid_x_minus_silu_x_sigmoid_x, dout, silu_x_dout
322
- )
323
- dx = cute.arch.mul_packed_f32x2(d_silu_x_dout, y)
324
- dy = silu_x_dout
325
- swiglu_out = cute.arch.mul_packed_f32x2(silu_x, y)
326
- return dx, dy, swiglu_out
327
-
328
-
329
- @dsl_user_op
330
- def swiglu_oai(
331
- x: F32_or_F32x2, y: F32_or_F32x2, alpha: float = 1.702, *, loc=None, ip=None
332
- ) -> F32_or_F32x2:
333
- """The swiglu variant used in gpt-oss, which has a scaling factor on x and bias of 1 to y.
334
- https://github.com/openai/gpt-oss/blob/7be9334950053a888e24887a57dac797a17d6e00/gpt_oss/torch/model.py#L249
335
- x * sigmoid(alpha * x) * (y + 1)
336
- Compile down to FMUL, FMUL, TANH, FFMA, FFMA
337
- """
338
- # Compute sigmoid(alpha * x) using tanh: sigmoid(z) = 0.5 * (1 + tanh(z/2))
339
- if const_expr(not isinstance(x, tuple)):
340
- x_half = 0.5 * x
341
- # silu_x = x_half * cute.math.tanh(alpha * x_half, fastmath=True) + x_half
342
- silu_x = x_half * tanh(alpha * x_half) + x_half
343
- return silu_x * y + silu_x
344
- else:
345
- x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x)
346
- alpha_x_half = cute.arch.mul_packed_f32x2((alpha, alpha), x_half)
347
- tanh_alpha_x_half = (tanh(alpha_x_half[0]), tanh(alpha_x_half[1]))
348
- silu_x = cute.arch.fma_packed_f32x2(x_half, tanh_alpha_x_half, x_half)
349
- return cute.arch.fma_packed_f32x2(silu_x, y, silu_x)
350
-
351
-
352
- @dsl_user_op
353
- def dswiglu_oai(
354
- x: F32_or_F32x2, y: F32_or_F32x2, dout: F32_or_F32x2, alpha: float = 1.702, *, loc=None, ip=None
355
- ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
356
- """
357
- Swiglu OAI backward pass: computes gradients w.r.t. x and y
358
- Given: swiglu_oai_out = x * sigmoid(alpha * x) * (y + 1), and dout = grad w.r.t. swiglu_oai_out
359
- Returns: (dx, dy, swiglu_oai_out)
360
-
361
- Derivative of x * sigmoid(alpha * x) w.r.t. x:
362
- d/dx[x * sigmoid(alpha * x)] = sigmoid(alpha * x) + alpha * x * sigmoid(alpha * x) * (1 - sigmoid(alpha * x))
363
- """
364
- if const_expr(not isinstance(x, tuple)):
365
- # Compute sigmoid(alpha * x) using tanh: sigmoid(z) = 0.5 * (1 + tanh(z/2))
366
- alpha_x_half = (0.5 * alpha) * x # FMUL
367
- # MUFU.TANH, then FFMA
368
- # sigmoid_alpha_x = 0.5 + 0.5 * cute.math.tanh(alpha_x_half, fastmath=True)
369
- sigmoid_alpha_x = 0.5 + 0.5 * tanh(alpha_x_half)
370
- silu_x = x * sigmoid_alpha_x # FMUL
371
- silu_x_dout = silu_x * dout # FMUL
372
- # FFMA, FFMA, FMUL
373
- d_silu_x_dout = (sigmoid_alpha_x + alpha * (silu_x - silu_x * sigmoid_alpha_x)) * dout
374
- dx = d_silu_x_dout * y + d_silu_x_dout # FFMA, instead of multiply by y + 1
375
- dy = silu_x_dout
376
- swiglu_out = silu_x * y + silu_x # FFMA, instead of multiply by y + 1
377
- # Overall it's 1 MUFU.TANH, 4 FMUL, 5 FFMA
378
- return dx, dy, swiglu_out
379
- else:
380
- # Compute sigmoid(alpha * x)
381
- alpha_x_half = cute.arch.mul_packed_f32x2(((0.5 * alpha), (0.5 * alpha)), x)
382
- tanh_alpha_x_half = (tanh(alpha_x_half[0]), tanh(alpha_x_half[1]))
383
- sigmoid_alpha_x = cute.arch.fma_packed_f32x2(tanh_alpha_x_half, (0.5, 0.5), (0.5, 0.5))
384
- silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_alpha_x)
385
- silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
386
- # d_silu_x_dout = (sigmoid_alpha_x + alpha * (silu_x - silu_x * sigmoid_alpha_x)) * dout
387
- silu_x_minus_product = cute.arch.fma_packed_f32x2(
388
- silu_x, (-sigmoid_alpha_x[0], -sigmoid_alpha_x[1]), silu_x
389
- )
390
- sigmoid_plus_alpha_diff = cute.arch.fma_packed_f32x2(
391
- (alpha, alpha), silu_x_minus_product, sigmoid_alpha_x
392
- )
393
- d_silu_x_dout = cute.arch.mul_packed_f32x2(sigmoid_plus_alpha_diff, dout)
394
- dx = cute.arch.fma_packed_f32x2(d_silu_x_dout, y, d_silu_x_dout)
395
- dy = silu_x_dout
396
- swiglu_out = cute.arch.fma_packed_f32x2(silu_x, y, silu_x)
397
- return dx, dy, swiglu_out
398
-
399
-
400
- @dsl_user_op
401
- def glu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
402
- """GLU: Gated Linear Unit
403
- glu(x, y) = sigmoid(x) * y
404
- Using tanh to compute sigmoid: sigmoid(x) = 0.5 * (1 + tanh(x/2))
405
- """
406
- if const_expr(not isinstance(x, tuple)):
407
- sigmoid_x = sigmoid(x) # FMUL, MUFU.TANH, then FFMA
408
- return sigmoid_x * y # FMUL
409
- else:
410
- sigmoid_x = sigmoid(x)
411
- return cute.arch.mul_packed_f32x2(sigmoid_x, y)
412
-
413
-
414
- @dsl_user_op
415
- def dglu(
416
- x: F32_or_F32x2, y: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
417
- ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
418
- """
419
- GLU backward pass: computes gradients w.r.t. x (gate) and y (up projection)
420
- Given: glu_out = sigmoid(x) * y, and dout = grad w.r.t. glu_out
421
- Returns: (dx, dy, glu_out) where:
422
- - dx = dout * y * sigmoid(x) * (1 - sigmoid(x))
423
- - dy = dout * sigmoid(x)
424
- - glu_out = sigmoid(x) * y
425
- """
426
- if const_expr(not isinstance(x, tuple)):
427
- # Compute sigmoid(x) using tanh: sigmoid(x) = 0.5 * (1 + tanh(x/2))
428
- sigmoid_x = sigmoid(x) # FMUL, MUFU.TANH, then FFMA
429
- sigmoid_x_dout = sigmoid_x * dout # FMUL
430
- glu_out = sigmoid_x * y # FMUL
431
- # dx = y * sigmoid(x) * (1 - sigmoid(x)) * dout
432
- # = y * (1 - sigmoid(x)) * sigmoid_x_dout
433
- # = (y - y * sigmoid(x)) * sigmoid_x_dout
434
- # = (y - glu_out) * sigmoid_x_dout
435
- dx = (y - glu_out) * sigmoid_x_dout # FADD, FMUL
436
- dy = sigmoid_x_dout
437
- # Total: 1 MUFU.TANH, 4 FMUL, 1 FADD, 1 FFMA
438
- return dx, dy, glu_out
439
- else:
440
- sigmoid_x = sigmoid(x)
441
- sigmoid_x_dout = cute.arch.mul_packed_f32x2(sigmoid_x, dout)
442
- glu_out = cute.arch.mul_packed_f32x2(sigmoid_x, y)
443
- # dx = (y - glu_out) * sigmoid_x_dout
444
- y_minus_glu_out = sub_packed_f32x2(y, glu_out)
445
- dx = cute.arch.mul_packed_f32x2(y_minus_glu_out, sigmoid_x_dout)
446
- dy = sigmoid_x_dout
447
- return dx, dy, glu_out
448
-
449
-
450
- @dsl_user_op
451
- def reglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
452
- """ReGLU: ReLU Gated Linear Unit
453
- reglu(x, y) = relu(x) * y = max(x, 0) * y
454
- """
455
- if const_expr(not isinstance(x, tuple)):
456
- return cute.arch.fmax(x, Float32(0.0)) * y
457
- else:
458
- relu_x = relu(x)
459
- return cute.arch.mul_packed_f32x2(relu_x, y)
460
-
461
-
462
- @dsl_user_op
463
- @cute.jit
464
- def dreglu(
465
- x: F32_or_F32x2, y: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
466
- ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
467
- """
468
- ReGLU backward pass: computes gradients w.r.t. x (gate) and y (up projection)
469
- Given: reglu_out = relu(x) * y, and dout = grad w.r.t. reglu_out
470
- Returns: (dx, dy, reglu_out) where:
471
- - dx = dout * y if x > 0, else 0
472
- - dy = dout * relu(x)
473
- - reglu_out = relu(x) * y
474
- """
475
- if const_expr(not isinstance(x, tuple)):
476
- x_pos = Boolean(x > 0)
477
- relu_x = cute.arch.fmax(x, Float32(0.0))
478
- dx = (dout * y) if x_pos else Float32(0.0)
479
- dy = dout * relu_x
480
- reglu_out = relu_x * y
481
- return dx, dy, reglu_out
482
- else:
483
- x0_pos = Boolean(x[0] > 0)
484
- x1_pos = Boolean(x[1] > 0)
485
- relu_x = relu(x)
486
- dout_y = cute.arch.mul_packed_f32x2(dout, y)
487
- dx = ((dout_y[0] if x0_pos else Float32(0.0)), (dout_y[1] if x1_pos else Float32(0.0)))
488
- dy = cute.arch.mul_packed_f32x2(dout, relu_x)
489
- reglu_out = cute.arch.mul_packed_f32x2(relu_x, y)
490
- return dx, dy, reglu_out
491
-
492
-
493
- @dsl_user_op
494
- def geglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
495
- """GeGLU: GELU Gated Linear Unit
496
- geglu(x, y) = gelu(x) * y
497
- Uses the tanh approximation of GELU
498
- """
499
- if const_expr(not isinstance(x, tuple)):
500
- return gelu_tanh_approx(x) * y
501
- else:
502
- return cute.arch.mul_packed_f32x2(gelu_tanh_approx(x), y)
503
-
504
-
505
- @dsl_user_op
506
- def dgeglu(
507
- x: F32_or_F32x2, y: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
508
- ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
509
- """
510
- GeGLU backward pass: computes gradients w.r.t. x (gate) and y (up projection)
511
- Given: geglu_out = gelu(x) * y, and dout = grad w.r.t. geglu_out
512
- Returns: (dx, dy, geglu_out) where:
513
- - dx = dout * y * d_gelu(x)
514
- - dy = dout * gelu(x)
515
- - geglu_out = gelu(x) * y
516
- """
517
- if const_expr(not isinstance(x, tuple)):
518
- # Reuse dgelu_tanh_approx to compute d_gelu(x) * dout and gelu(x)
519
- dgelu_x_dout, gelu_x = dgelu_tanh_approx(x, dout)
520
- # Compute gradients for geglu
521
- dx = dgelu_x_dout * y
522
- dy = gelu_x * dout
523
- geglu_out = gelu_x * y
524
- return dx, dy, geglu_out
525
- else:
526
- # Reuse dgelu_tanh_approx to compute d_gelu(x) * dout and gelu(x)
527
- dgelu_x_dout, gelu_x = dgelu_tanh_approx(x, dout)
528
- # Compute gradients for geglu
529
- dx = cute.arch.mul_packed_f32x2(dgelu_x_dout, y)
530
- dy = cute.arch.mul_packed_f32x2(gelu_x, dout)
531
- geglu_out = cute.arch.mul_packed_f32x2(gelu_x, y)
532
- return dx, dy, geglu_out
533
-
534
-
535
- # ============================================================================
536
- # Activation name -> function maps
537
- # ============================================================================
538
-
539
- act_fn_map = {
540
- None: None,
541
- "silu": silu,
542
- "relu": relu,
543
- "relu_sq": relu_sq,
544
- "gelu_tanh_approx": gelu_tanh_approx,
545
- }
546
-
547
- dact_fn_map = {
548
- None: None,
549
- "relu": drelu,
550
- "relu_sq": drelu_sq,
551
- "gelu_tanh_approx": dgelu_tanh_approx,
552
- }
553
-
554
- gate_fn_map = {
555
- "swiglu": swiglu,
556
- "swiglu_oai": swiglu_oai,
557
- "reglu": reglu,
558
- "geglu": geglu,
559
- "glu": glu,
560
- }
561
-
562
- dgate_fn_map = {
563
- "swiglu": dswiglu,
564
- "swiglu_oai": dswiglu_oai,
565
- "reglu": dreglu,
566
- "geglu": dgeglu,
567
- "glu": dglu,
568
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-cuda/quack/copy_utils.py CHANGED
@@ -1,22 +1,118 @@
1
- # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
 
3
- from typing import Optional, Type, Tuple, Callable, Sequence
4
  from functools import partial
5
 
6
  import cutlass
7
  import cutlass.cute as cute
 
8
 
9
  from cutlass import Int32, Int16, Boolean, const_expr
10
- from cutlass.cute.nvgpu import cpasync, warp, warpgroup
 
11
  from cutlass.cute.nvgpu.tcgen05.mma import CtaGroup # noqa
12
  from cutlass.cutlass_dsl import dsl_user_op
 
13
  import cutlass.pipeline
14
- from cutlass._mlir.dialects import llvm
15
  from cutlass._mlir import ir
16
  from cutlass._mlir.dialects import cute_nvgpu as _cute_nvgpu_ir
17
 
 
 
 
18
 
19
  Sm100MmaPeerBitMask = 0xFEFFFFFF
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
 
22
  @dsl_user_op
@@ -41,6 +137,30 @@ def cvt_copy(
41
  cute.copy(tiled_copy, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
42
 
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  @dsl_user_op
45
  def load_s2r(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
46
  dst = cute.make_rmem_tensor_like(src, src.element_type, loc=loc, ip=ip)
@@ -48,6 +168,13 @@ def load_s2r(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
48
  return dst
49
 
50
 
 
 
 
 
 
 
 
51
  @dsl_user_op
52
  def load_s2r_retile(
53
  tiled_copy: cute.TiledCopy,
@@ -68,11 +195,19 @@ def load_s2r_retile(
68
 
69
  @dsl_user_op
70
  def load_t2r(
71
- thr_copy: cute.ThrCopy, shape: cute.Shape, src: cute.Tensor, *, loc=None, ip=None
 
 
 
 
72
  ) -> cute.Tensor:
73
- cDst = cute.make_identity_tensor(shape)
74
- dst = cute.make_rmem_tensor(thr_copy.partition_D(cDst).shape, src.element_type, loc=loc, ip=ip)
75
- cute.copy(thr_copy, src, dst, loc=loc, ip=ip)
 
 
 
 
76
  return dst
77
 
78
 
@@ -294,18 +429,14 @@ def as_position_independent_swizzle_tensor(tensor: cute.Tensor) -> cute.Tensor:
294
  return cute.make_tensor(cute.recast_ptr(tensor.iterator, dtype=tensor.element_type), new_layout)
295
 
296
 
297
- def partition_D_position_independent(
298
- thr_copy: cute.core.ThrCopy, tensor: cute.Tensor
299
- ) -> cute.Tensor:
300
  return cute.make_tensor(
301
  swizzle_ptr(thr_copy.partition_D(tensor).iterator),
302
  thr_copy.partition_D(as_position_independent_swizzle_tensor(tensor)).layout,
303
  )
304
 
305
 
306
- def partition_S_position_independent(
307
- thr_copy: cute.core.ThrCopy, tensor: cute.Tensor
308
- ) -> cute.Tensor:
309
  return cute.make_tensor(
310
  swizzle_ptr(thr_copy.partition_S(tensor).iterator),
311
  thr_copy.partition_S(as_position_independent_swizzle_tensor(tensor)).layout,
@@ -346,12 +477,12 @@ def sm90_get_smem_load_op(
346
 
347
 
348
  def get_smem_store_atom(
349
- arch: cutlass.Constexpr[int],
350
  element_type: Type[cute.Numeric],
351
  transpose: bool = False,
352
  major_mode_size: Optional[int] = None,
353
  ) -> cute.CopyAtom:
354
- if const_expr(arch < 90 or element_type.width != 16):
 
355
  return cute.make_copy_atom(
356
  cute.nvgpu.CopyUniversalOp(),
357
  element_type,
@@ -370,12 +501,12 @@ def get_smem_store_atom(
370
 
371
 
372
  def get_smem_load_atom(
373
- arch: cutlass.Constexpr[int],
374
  element_type: Type[cute.Numeric],
375
  transpose: bool = False,
376
  major_mode_size: Optional[int] = None,
377
  ) -> cute.CopyAtom:
378
- if const_expr(arch < 90 or element_type.width != 16):
 
379
  return cute.make_copy_atom(
380
  cute.nvgpu.CopyUniversalOp(),
381
  element_type,
@@ -394,26 +525,35 @@ def get_smem_load_atom(
394
 
395
 
396
  def get_smem_store_C(
397
- tiled_mma: cute.TiledMma,
398
  sC: cute.Tensor,
399
  tidx: Int32,
400
- arch: int,
401
  transpose: bool = False,
402
  position_independent=False,
403
  major_mode_size: Optional[int] = None,
404
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
405
  dtype = sC.element_type
406
- copy_atom = get_smem_store_atom(arch, dtype, transpose, major_mode_size=major_mode_size)
407
- tiled_copy = cute.make_tiled_copy_C(copy_atom, tiled_mma)
 
 
 
 
 
 
 
 
408
  thr_copy = tiled_copy.get_slice(tidx)
409
  if const_expr(not position_independent):
410
  tRS_sC = thr_copy.partition_D(sC)
411
  else:
412
  tRS_sC = partition_D_position_independent(thr_copy, sC)
413
 
414
- def copy_fn(src: cute.Tensor, dst_idx: Optional[Int32] = None, **new_kwargs):
415
- dst_tensor = tRS_sC if const_expr(dst_idx is None) else tRS_sC[None, None, None, dst_idx]
416
  cvt_copy(tiled_copy, src, dst_tensor, retile=True, **new_kwargs)
 
 
417
 
418
  return copy_fn, thr_copy, tRS_sC
419
 
@@ -422,19 +562,18 @@ def get_smem_load_C(
422
  tiled_mma: cute.TiledMma,
423
  sC: cute.Tensor,
424
  tidx: Int32,
425
- arch: int,
426
  transpose: bool = False,
427
  position_independent=False,
428
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
429
  dtype = sC.element_type
430
- copy_atom = get_smem_load_atom(arch, dtype, transpose)
431
  tiled_copy = cute.make_tiled_copy_C(copy_atom, tiled_mma)
432
  thr_copy = tiled_copy.get_slice(tidx)
433
  if const_expr(not position_independent):
434
  tSR_sC = thr_copy.partition_S(sC)
435
  else:
436
  tSR_sC = partition_S_position_independent(thr_copy, sC)
437
- copy_atom_RS = get_smem_store_atom(arch, dtype, transpose)
438
  thr_copy_RS = cute.make_tiled_copy_C(copy_atom_RS, tiled_mma).get_slice(tidx)
439
  tRS_shape = thr_copy_RS.partition_S(cute.make_identity_tensor(sC.shape[:2])).shape
440
 
@@ -448,10 +587,18 @@ def get_smem_load_C(
448
  def epilog_smem_copy_atom(
449
  tiled_mma: cute.TiledMma, epi_tile: cute.Shape, transpose: bool = False
450
  ) -> cute.TiledCopy:
451
- copy_atom_C = cute.make_copy_atom(
452
- warp.StMatrix8x8x16bOp(transpose, num_matrices=4 if epi_tile[1] % 16 == 0 else 2),
453
- cutlass.Float16, # this is just to get the right source layout
454
- )
 
 
 
 
 
 
 
 
455
  tiled_copy_C_atom = cute.make_tiled_copy_C_atom(copy_atom_C, tiled_mma)
456
  return tiled_copy_C_atom
457
 
@@ -461,13 +608,12 @@ def get_smem_store_epi(
461
  epi_tile: cute.Shape,
462
  sC: Optional[cute.Tensor],
463
  tidx: Int32,
464
- arch: int,
465
  transpose: bool = False,
466
  position_independent=False,
467
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor, cute.Tensor]:
468
  dtype = sC.element_type if const_expr(sC is not None) else cutlass.Float16
 
469
  tiled_copy_C_atom = epilog_smem_copy_atom(tiled_mma, epi_tile)
470
- copy_atom = get_smem_store_atom(arch, dtype, transpose)
471
  tiled_copy = cute.make_tiled_copy_S(copy_atom, tiled_copy_C_atom)
472
  thr_copy = tiled_copy.get_slice(tidx)
473
  tRS_sC = None
@@ -488,11 +634,11 @@ def get_smem_store_epi(
488
 
489
 
490
  def get_smem_store_A(
491
- tiled_mma: cute.TiledMma, sA: cute.Tensor, tidx: Int32, arch: int, position_independent=False
492
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
493
  dtype = sA.element_type
494
- transpose = tiled_mma.op.a_major_mode == warpgroup.OperandMajorMode.MN
495
- copy_atom = get_smem_store_atom(arch, dtype, transpose)
496
  tiled_copy = cute.make_tiled_copy_A(copy_atom, tiled_mma)
497
  thr_copy = tiled_copy.get_slice(tidx)
498
  if const_expr(not position_independent):
@@ -510,13 +656,12 @@ def get_smem_load_A(
510
  tiled_mma: cute.TiledMma,
511
  sA: cute.Tensor,
512
  tidx: Int32,
513
- arch: int,
514
  with_dst_tensor: bool = False,
515
  position_independent=False,
516
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
517
  dtype = sA.element_type
518
- transpose = tiled_mma.op.a_major_mode == warpgroup.OperandMajorMode.MN
519
- copy_atom = get_smem_load_atom(arch, dtype, transpose)
520
  tiled_copy = cute.make_tiled_copy_A(copy_atom, tiled_mma)
521
  thr_copy = tiled_copy.get_slice(tidx)
522
  if const_expr(not position_independent):
@@ -545,19 +690,16 @@ def cpasync_reduce_bulk_add_f32(
545
  loc=None,
546
  ip=None,
547
  ):
548
- smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
549
  # cache_hint = cutlass.Int64(0x14F0000000000000) # EVICT_LAST
550
- llvm.inline_asm(
551
- None,
552
- [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value()],
553
- "cp.reduce.async.bulk.global.shared::cta.bulk_group.add.f32 [$0], [$1], $2;",
554
- "l,r,r",
555
  # [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value(), cache_hint.ir_value()],
556
  # "cp.reduce.async.bulk.global.shared::cta.bulk_group.L2::cache_hint.add.f32 [$0], [$1], $2, $3;",
557
  # "l,r,r,l",
558
- has_side_effects=True,
559
- is_align_stack=False,
560
- asm_dialect=llvm.AsmDialect.AD_ATT,
561
  )
562
 
563
 
@@ -648,33 +790,34 @@ def tma_gather4_load(
648
  """
649
  if len(row_indices) != 4:
650
  raise ValueError(f"gather4 requires exactly 4 row indices, got {len(row_indices)}")
651
- col_val = Int32(col_idx).ir_value()
652
- row_vals = [Int32(row_idx).ir_value() for row_idx in row_indices]
653
  # Convert pointers to integer addresses
654
- desc_addr = tma_desc_ptr.toint(loc=loc, ip=ip).ir_value()
655
- dst_addr = dst_smem_ptr.toint(loc=loc, ip=ip).ir_value()
656
  mbar_addr = mbarrier_ptr.toint(loc=loc, ip=ip)
657
  if num_cta > 1:
658
  # Executed by both CTAs. Set peer bit to 0 so that the
659
  # transaction bytes will update CTA0's barrier.
660
  mbar_addr = mbar_addr & Sm100MmaPeerBitMask
661
- mbar_addr = mbar_addr.ir_value()
662
  # Handle multicast_mask - may already be ir.Value or Python int
663
  multicast_mask_val = None
664
  if multicast_mask is not None:
665
- multicast_mask_val = Int16(multicast_mask).ir_value()
666
  assert multicast_mask_val is None, "multicast is not supported yet"
667
  # Emit inline PTX for TMA gather4
668
  # PTX: cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes
669
  # [dstMem], [tensorMap, {col, row0, row1, row2, row3}], [smem_bar];
670
  ptx = (
671
- f"cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes.cta_group::{num_cta} "
672
- "[$0], [$1, {$2, $3, $4, $5, $6}], [$7];"
 
673
  )
674
 
675
- llvm.inline_asm(
676
- None,
677
- [
678
  dst_addr,
679
  desc_addr,
680
  col_val,
@@ -684,11 +827,6 @@ def tma_gather4_load(
684
  row_vals[3],
685
  mbar_addr,
686
  ],
687
- ptx,
688
- "r,l,r,r,r,r,r,r", # constraints: register, long, 6x register
689
- has_side_effects=True,
690
- is_align_stack=False,
691
- asm_dialect=llvm.AsmDialect.AD_ATT,
692
  loc=loc,
693
  ip=ip,
694
  )
@@ -775,6 +913,161 @@ def tma_get_copy_fn(
775
  return (copy_tma if const_expr(not single_stage) else copy_tma_single_stage), s, g
776
 
777
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
778
  def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsync):
779
  def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
780
  copy(
@@ -787,6 +1080,18 @@ def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsyn
787
  return copy_fn
788
 
789
 
 
 
 
 
 
 
 
 
 
 
 
 
790
  @cute.jit
791
  def gather_m_get_copy_fn(
792
  thr_copy_A: cute.ThrCopy,
@@ -796,17 +1101,17 @@ def gather_m_get_copy_fn(
796
  limit_m: Int32,
797
  limit_k: Int32,
798
  ) -> Callable:
799
- tile_shape_mk = (cute.size(sA, mode=[0]), cute.size(sA, mode=[1]))
800
- tAsA = thr_copy_A.partition_D(sA)
801
  # k-major
802
  assert tAsA.shape[2] == 1
803
  tAsA = cute.group_modes(cute.slice_(tAsA, (None, None, 0, None)), 0, 2)
804
 
805
- is_even_m_smem = tile_shape_mk[0] % thr_copy_A.tiler_mn[0].shape == 0
806
  if const_expr(not is_even_m_smem):
807
- limit_m = min(limit_m, tile_shape_mk[0])
808
  elems_per_load = cute.size(tAsA.shape[0][0])
809
- cA = cute.make_identity_tensor(tile_shape_mk)
810
  tAcA = thr_copy_A.partition_S(cA)
811
  t0AcA = thr_copy_A.get_slice(0).partition_S(cA)
812
  # Instead of comparing tAcA to limit_m, we instead compare t0AcA to limit_m - tAcA[0][0]
@@ -828,13 +1133,13 @@ def gather_m_get_copy_fn(
828
  else:
829
  m_idx[m] = 0 # It's ok to load row 0 in the case of OOB
830
 
831
- mA_k = cute.logical_divide(mA, (None, tile_shape_mk[1]))
832
 
833
- def copy_fn(src_idx, dst_idx, pred: bool = False):
834
  tApA_k = None
835
  if const_expr(pred):
836
  tApA_k = cute.make_rmem_tensor(cols_per_thread, Boolean)
837
- limit_k_cur = limit_k - src_idx * tile_shape_mk[1]
838
  for k in cutlass.range(cols_per_thread, unroll_full=True):
839
  tApA_k[k] = t0AcA[0, 0, k][1] < limit_k_cur
840
  mA_cur = mA_k[None, (None, src_idx)]
@@ -997,11 +1302,162 @@ def gather_m_get_tma_copy_fn(
997
  tma_gather4_load_fn = partial(tma_gather4_load, tma_desc_ptr, num_cta=cta_group)
998
 
999
  def copy_fn(src_idx, dst_idx, tma_bar_ptr: cute.Pointer):
 
1000
  col_idx = tile_K * src_idx
1001
  for m in cutlass.range(cute.size(tSR_rAIdx, mode=[1]), unroll_full=True):
1002
  row_indices = [tSR_rAIdx[v, m] for v in range(4)]
1003
- smem_ptr = tSR_sA[None, m, None, dst_idx].iterator
1004
  with cute.arch.elect_one():
1005
  tma_gather4_load_fn(smem_ptr, tma_bar_ptr, col_idx, row_indices)
1006
 
1007
  return copy_fn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025-2026, QuACK team.
2
 
3
+ from typing import Any, Optional, Type, Tuple, Callable, Sequence
4
  from functools import partial
5
 
6
  import cutlass
7
  import cutlass.cute as cute
8
+ import cutlass.utils.blackwell_helpers as sm100_utils
9
 
10
  from cutlass import Int32, Int16, Boolean, const_expr
11
+ from cutlass.base_dsl.arch import Arch
12
+ from cutlass.cute.nvgpu import cpasync, tcgen05, warp
13
  from cutlass.cute.nvgpu.tcgen05.mma import CtaGroup # noqa
14
  from cutlass.cutlass_dsl import dsl_user_op
15
+ from cutlass.utils import LayoutEnum
16
  import cutlass.pipeline
 
17
  from cutlass._mlir import ir
18
  from cutlass._mlir.dialects import cute_nvgpu as _cute_nvgpu_ir
19
 
20
+ from . import layout_utils
21
+ from .utils import make_vector
22
+
23
 
24
  Sm100MmaPeerBitMask = 0xFEFFFFFF
25
+ _TCGEN05_TMEM_OPS = (
26
+ tcgen05.Ld16x128bOp,
27
+ tcgen05.Ld16x256bOp,
28
+ tcgen05.Ld16x32bx2Op,
29
+ tcgen05.Ld16x64bOp,
30
+ tcgen05.Ld32x32bOp,
31
+ tcgen05.LdRed16x32bx2Op,
32
+ tcgen05.LdRed32x32bOp,
33
+ tcgen05.St16x128bOp,
34
+ tcgen05.St16x256bOp,
35
+ tcgen05.St16x32bx2Op,
36
+ tcgen05.St16x64bOp,
37
+ tcgen05.St32x32bOp,
38
+ )
39
+ _TCGEN05_TMEM_STORE_OPS = (
40
+ tcgen05.St16x128bOp,
41
+ tcgen05.St16x256bOp,
42
+ tcgen05.St16x32bx2Op,
43
+ tcgen05.St16x64bOp,
44
+ tcgen05.St32x32bOp,
45
+ )
46
+
47
+
48
+ def tmem_store_atom_from_load_atom(
49
+ copy_atom_t2r: Any,
50
+ src_dtype: Type[cutlass.Numeric],
51
+ dst_dtype: Type[cutlass.Numeric],
52
+ ) -> cute.CopyAtom:
53
+ """Return the matching tcgen05 R2T store atom for a selected T2R load atom.
54
+
55
+ `src_dtype` is the register fragment dtype loaded by T2R; `dst_dtype` is
56
+ the TMEM element dtype to store. Ratio 1 uses CUTLASS's operation-family
57
+ mapping directly. Ratio 2 is intentionally narrow: we allow the current
58
+ Ld32x32b path by halving repeat, and the widest 16dp path by halving the
59
+ vector width. Narrower 16dp cross-family mappings are not mirrored by
60
+ CUTLASS's same-family helper, so they assert until validated.
61
+
62
+ C++ CuTe's operation-family mapping is `cute::TMEM::tmem_load_to_store`:
63
+ https://github.com/NVIDIA/cutlass/blob/main/include/cute/atom/copy_traits_sm100.hpp#L3274
64
+ """
65
+ load_op = copy_atom_t2r.op if const_expr(hasattr(copy_atom_t2r, "op")) else copy_atom_t2r
66
+ if const_expr(hasattr(load_op, "op")):
67
+ load_op = load_op.op
68
+ assert src_dtype.width >= dst_dtype.width, "TMEM R2T helper only supports narrowing stores"
69
+ assert src_dtype.width % dst_dtype.width == 0, "TMEM source/destination widths must divide"
70
+ ratio = src_dtype.width // dst_dtype.width
71
+ assert ratio in (1, 2), "TMEM R2T helper only supports src/dst width ratio 1 or 2"
72
+ repeat = load_op.repeat
73
+ unpack = tcgen05.Unpack.NONE
74
+ if const_expr(getattr(load_op, "pack", None) == tcgen05.Pack.PACK_16b_IN_32b):
75
+ unpack = tcgen05.Unpack.UNPACK_32b_IN_16b
76
+ if const_expr(isinstance(load_op, tcgen05.Ld16x64bOp)):
77
+ assert ratio == 1, "No validated ratio-2 store mapping for Ld16x64bOp"
78
+ store_op = tcgen05.St16x64bOp(repeat, unpack)
79
+ elif const_expr(isinstance(load_op, tcgen05.Ld16x128bOp)):
80
+ assert ratio == 1, "No validated ratio-2 store mapping for Ld16x128bOp"
81
+ store_op = tcgen05.St16x128bOp(repeat, unpack)
82
+ elif const_expr(isinstance(load_op, tcgen05.Ld16x256bOp)):
83
+ store_op = (
84
+ tcgen05.St16x256bOp(repeat, unpack)
85
+ if const_expr(ratio == 1)
86
+ else tcgen05.St16x128bOp(repeat, unpack)
87
+ )
88
+ elif const_expr(isinstance(load_op, tcgen05.Ld16x32bx2Op)):
89
+ assert ratio == 1, "No validated ratio-2 store mapping for Ld16x32bx2Op"
90
+ store_op = tcgen05.St16x32bx2Op(repeat, unpack)
91
+ elif const_expr(isinstance(load_op, tcgen05.Ld32x32bOp)):
92
+ if const_expr(ratio == 2):
93
+ assert repeat.value % 2 == 0, "Ld32x32b ratio-2 store needs even repeat"
94
+ repeat = tcgen05.Repetition(repeat.value // 2)
95
+ store_op = tcgen05.St32x32bOp(repeat, unpack)
96
+ else:
97
+ raise TypeError(f"Unsupported TMEM load op for store conversion: {type(load_op)}")
98
+ return cute.make_copy_atom(store_op, dst_dtype)
99
+
100
+
101
+ def _tmem_copy_reg_tv_layout(tiled_copy: cute.TiledCopy):
102
+ """Return the register-side TV layout for a tcgen05 tmem copy."""
103
+ op = tiled_copy.op
104
+ if const_expr(hasattr(op, "op")):
105
+ op = op.op
106
+ if const_expr(isinstance(op, _TCGEN05_TMEM_OPS)):
107
+ # TMEM stores read from registers; all other TMEM copy ops here write
108
+ # registers, including LdRed* reductions that upstream is_tmem_load
109
+ # intentionally does not classify as plain loads.
110
+ return (
111
+ tiled_copy.layout_src_tv_tiled
112
+ if const_expr(isinstance(op, _TCGEN05_TMEM_STORE_OPS))
113
+ else tiled_copy.layout_dst_tv_tiled
114
+ )
115
+ raise TypeError(f"Cannot infer tmem copy direction from tiled_copy.op={op}")
116
 
117
 
118
  @dsl_user_op
 
137
  cute.copy(tiled_copy, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
138
 
139
 
140
+ @dsl_user_op
141
+ def sr_cvt_copy(
142
+ tiled_copy: cute.TiledCopy,
143
+ src: cute.Tensor,
144
+ dst: cute.Tensor,
145
+ seed: Int32,
146
+ tidx: Int32,
147
+ *,
148
+ loc=None,
149
+ ip=None,
150
+ ) -> None:
151
+ """Like cvt_copy but uses stochastic rounding for FP32 -> BF16 conversion."""
152
+ assert isinstance(src.iterator, cute.Pointer) and src.memspace == cute.AddressSpace.rmem
153
+ from .rounding import convert_f32_to_bf16_sr
154
+ from cutlass.cute.tensor import TensorSSA
155
+
156
+ src_cvt = cute.make_rmem_tensor_like(src, dst.element_type)
157
+ src_vec = src.load()
158
+ raw_vec = convert_f32_to_bf16_sr(src_vec, seed, tidx, loc=loc, ip=ip)
159
+ src_cvt.store(TensorSSA(raw_vec, src_vec.shape, dst.element_type))
160
+ src = src_cvt
161
+ cute.copy(tiled_copy, src, dst, loc=loc, ip=ip)
162
+
163
+
164
  @dsl_user_op
165
  def load_s2r(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
166
  dst = cute.make_rmem_tensor_like(src, src.element_type, loc=loc, ip=ip)
 
168
  return dst
169
 
170
 
171
+ @dsl_user_op
172
+ def contiguous(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
173
+ dst = cute.make_rmem_tensor(src.shape, src.element_type, loc=loc, ip=ip)
174
+ cute.autovec_copy(src, dst, loc=loc, ip=ip)
175
+ return dst
176
+
177
+
178
  @dsl_user_op
179
  def load_s2r_retile(
180
  tiled_copy: cute.TiledCopy,
 
195
 
196
  @dsl_user_op
197
  def load_t2r(
198
+ tiled_copy: cute.TiledCopy,
199
+ src: cute.Tensor,
200
+ *,
201
+ loc=None,
202
+ ip=None,
203
  ) -> cute.Tensor:
204
+ """Load one tmem tile partition into rmem, deriving the rmem shape from `src`.
205
+
206
+ `src` should already be indexed to the tile being copied, with any
207
+ stage/subtile modes removed.
208
+ """
209
+ dst = tmem_reg_frag(tiled_copy, src, loc=loc, ip=ip)
210
+ cute.copy(tiled_copy, src, dst, loc=loc, ip=ip)
211
  return dst
212
 
213
 
 
429
  return cute.make_tensor(cute.recast_ptr(tensor.iterator, dtype=tensor.element_type), new_layout)
430
 
431
 
432
+ def partition_D_position_independent(thr_copy: cute.ThrCopy, tensor: cute.Tensor) -> cute.Tensor:
 
 
433
  return cute.make_tensor(
434
  swizzle_ptr(thr_copy.partition_D(tensor).iterator),
435
  thr_copy.partition_D(as_position_independent_swizzle_tensor(tensor)).layout,
436
  )
437
 
438
 
439
+ def partition_S_position_independent(thr_copy: cute.ThrCopy, tensor: cute.Tensor) -> cute.Tensor:
 
 
440
  return cute.make_tensor(
441
  swizzle_ptr(thr_copy.partition_S(tensor).iterator),
442
  thr_copy.partition_S(as_position_independent_swizzle_tensor(tensor)).layout,
 
477
 
478
 
479
  def get_smem_store_atom(
 
480
  element_type: Type[cute.Numeric],
481
  transpose: bool = False,
482
  major_mode_size: Optional[int] = None,
483
  ) -> cute.CopyAtom:
484
+ arch = cutlass.base_dsl.BaseDSL._get_dsl().get_arch_enum()
485
+ if const_expr(arch < Arch.sm_90 or element_type.width != 16):
486
  return cute.make_copy_atom(
487
  cute.nvgpu.CopyUniversalOp(),
488
  element_type,
 
501
 
502
 
503
  def get_smem_load_atom(
 
504
  element_type: Type[cute.Numeric],
505
  transpose: bool = False,
506
  major_mode_size: Optional[int] = None,
507
  ) -> cute.CopyAtom:
508
+ arch = cutlass.base_dsl.BaseDSL._get_dsl().get_arch_enum()
509
+ if const_expr(arch < Arch.sm_90 or element_type.width != 16):
510
  return cute.make_copy_atom(
511
  cute.nvgpu.CopyUniversalOp(),
512
  element_type,
 
525
 
526
 
527
  def get_smem_store_C(
528
+ tiled_mma: cute.TiledMma | cute.TiledCopy,
529
  sC: cute.Tensor,
530
  tidx: Int32,
 
531
  transpose: bool = False,
532
  position_independent=False,
533
  major_mode_size: Optional[int] = None,
534
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
535
  dtype = sC.element_type
536
+ if const_expr(isinstance(tiled_mma, cute.TiledCopy)):
537
+ tiled_copy_t2r = tiled_mma
538
+ layout = LayoutEnum.COL_MAJOR if const_expr(transpose) else LayoutEnum.ROW_MAJOR
539
+ copy_atom = sm100_utils.get_smem_store_op(
540
+ layout, dtype, tiled_copy_t2r.value_type, tiled_copy_t2r
541
+ )
542
+ tiled_copy = cute.make_tiled_copy_D(copy_atom, tiled_copy_t2r)
543
+ else:
544
+ copy_atom = get_smem_store_atom(dtype, transpose, major_mode_size=major_mode_size)
545
+ tiled_copy = cute.make_tiled_copy_C(copy_atom, tiled_mma)
546
  thr_copy = tiled_copy.get_slice(tidx)
547
  if const_expr(not position_independent):
548
  tRS_sC = thr_copy.partition_D(sC)
549
  else:
550
  tRS_sC = partition_D_position_independent(thr_copy, sC)
551
 
552
+ def copy_fn(src: cute.Tensor, dst_idx: Optional[Int32] = None, fence=False, **new_kwargs):
553
+ dst_tensor = tRS_sC if const_expr(dst_idx is None) else tRS_sC[..., dst_idx]
554
  cvt_copy(tiled_copy, src, dst_tensor, retile=True, **new_kwargs)
555
+ if const_expr(fence):
556
+ cute.arch.fence_view_async_shared()
557
 
558
  return copy_fn, thr_copy, tRS_sC
559
 
 
562
  tiled_mma: cute.TiledMma,
563
  sC: cute.Tensor,
564
  tidx: Int32,
 
565
  transpose: bool = False,
566
  position_independent=False,
567
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
568
  dtype = sC.element_type
569
+ copy_atom = get_smem_load_atom(dtype, transpose)
570
  tiled_copy = cute.make_tiled_copy_C(copy_atom, tiled_mma)
571
  thr_copy = tiled_copy.get_slice(tidx)
572
  if const_expr(not position_independent):
573
  tSR_sC = thr_copy.partition_S(sC)
574
  else:
575
  tSR_sC = partition_S_position_independent(thr_copy, sC)
576
+ copy_atom_RS = get_smem_store_atom(dtype, transpose)
577
  thr_copy_RS = cute.make_tiled_copy_C(copy_atom_RS, tiled_mma).get_slice(tidx)
578
  tRS_shape = thr_copy_RS.partition_S(cute.make_identity_tensor(sC.shape[:2])).shape
579
 
 
587
  def epilog_smem_copy_atom(
588
  tiled_mma: cute.TiledMma, epi_tile: cute.Shape, transpose: bool = False
589
  ) -> cute.TiledCopy:
590
+ arch = cutlass.base_dsl.BaseDSL._get_dsl().get_arch_enum()
591
+ if const_expr(arch < Arch.sm_90):
592
+ copy_atom_C = cute.make_copy_atom(
593
+ cute.nvgpu.CopyUniversalOp(),
594
+ cutlass.Float16, # this is just to get the right source layout
595
+ num_bits_per_copy=(2 if not transpose else 1) * cutlass.Float16.width,
596
+ )
597
+ else:
598
+ copy_atom_C = cute.make_copy_atom(
599
+ warp.StMatrix8x8x16bOp(transpose, num_matrices=4 if epi_tile[1] % 16 == 0 else 2),
600
+ cutlass.Float16, # this is just to get the right source layout
601
+ )
602
  tiled_copy_C_atom = cute.make_tiled_copy_C_atom(copy_atom_C, tiled_mma)
603
  return tiled_copy_C_atom
604
 
 
608
  epi_tile: cute.Shape,
609
  sC: Optional[cute.Tensor],
610
  tidx: Int32,
 
611
  transpose: bool = False,
612
  position_independent=False,
613
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor, cute.Tensor]:
614
  dtype = sC.element_type if const_expr(sC is not None) else cutlass.Float16
615
+ copy_atom = get_smem_store_atom(dtype, transpose)
616
  tiled_copy_C_atom = epilog_smem_copy_atom(tiled_mma, epi_tile)
 
617
  tiled_copy = cute.make_tiled_copy_S(copy_atom, tiled_copy_C_atom)
618
  thr_copy = tiled_copy.get_slice(tidx)
619
  tRS_sC = None
 
634
 
635
 
636
  def get_smem_store_A(
637
+ tiled_mma: cute.TiledMma, sA: cute.Tensor, tidx: Int32, position_independent=False
638
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
639
  dtype = sA.element_type
640
+ transpose = tiled_mma.op.a_major_mode == cute.nvgpu.OperandMajorMode.MN
641
+ copy_atom = get_smem_store_atom(dtype, transpose)
642
  tiled_copy = cute.make_tiled_copy_A(copy_atom, tiled_mma)
643
  thr_copy = tiled_copy.get_slice(tidx)
644
  if const_expr(not position_independent):
 
656
  tiled_mma: cute.TiledMma,
657
  sA: cute.Tensor,
658
  tidx: Int32,
 
659
  with_dst_tensor: bool = False,
660
  position_independent=False,
661
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
662
  dtype = sA.element_type
663
+ transpose = tiled_mma.op.a_major_mode == cute.nvgpu.OperandMajorMode.MN
664
+ copy_atom = get_smem_load_atom(dtype, transpose)
665
  tiled_copy = cute.make_tiled_copy_A(copy_atom, tiled_mma)
666
  thr_copy = tiled_copy.get_slice(tidx)
667
  if const_expr(not position_independent):
 
690
  loc=None,
691
  ip=None,
692
  ):
693
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip)
694
  # cache_hint = cutlass.Int64(0x14F0000000000000) # EVICT_LAST
695
+ cute.arch.inline_ptx(
696
+ "cp.reduce.async.bulk.global.shared::cta.bulk_group.add.f32 [{$r0}], [{$r1}], {$r2};",
697
+ read_only_args=[gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes)],
 
 
698
  # [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value(), cache_hint.ir_value()],
699
  # "cp.reduce.async.bulk.global.shared::cta.bulk_group.L2::cache_hint.add.f32 [$0], [$1], $2, $3;",
700
  # "l,r,r,l",
701
+ loc=loc,
702
+ ip=ip,
 
703
  )
704
 
705
 
 
790
  """
791
  if len(row_indices) != 4:
792
  raise ValueError(f"gather4 requires exactly 4 row indices, got {len(row_indices)}")
793
+ col_val = Int32(col_idx)
794
+ row_vals = [Int32(row_idx) for row_idx in row_indices]
795
  # Convert pointers to integer addresses
796
+ desc_addr = tma_desc_ptr.toint(loc=loc, ip=ip)
797
+ dst_addr = dst_smem_ptr.toint(loc=loc, ip=ip)
798
  mbar_addr = mbarrier_ptr.toint(loc=loc, ip=ip)
799
  if num_cta > 1:
800
  # Executed by both CTAs. Set peer bit to 0 so that the
801
  # transaction bytes will update CTA0's barrier.
802
  mbar_addr = mbar_addr & Sm100MmaPeerBitMask
803
+ mbar_addr = Int32(mbar_addr)
804
  # Handle multicast_mask - may already be ir.Value or Python int
805
  multicast_mask_val = None
806
  if multicast_mask is not None:
807
+ multicast_mask_val = Int16(multicast_mask)
808
  assert multicast_mask_val is None, "multicast is not supported yet"
809
  # Emit inline PTX for TMA gather4
810
  # PTX: cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes
811
  # [dstMem], [tensorMap, {col, row0, row1, row2, row3}], [smem_bar];
812
  ptx = (
813
+ "cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes."
814
+ f"cta_group::{num_cta} "
815
+ "[{$r0}], [{$r1}, {{$r2}, {$r3}, {$r4}, {$r5}, {$r6}}], [{$r7}];"
816
  )
817
 
818
+ cute.arch.inline_ptx(
819
+ ptx,
820
+ read_only_args=[
821
  dst_addr,
822
  desc_addr,
823
  col_val,
 
827
  row_vals[3],
828
  mbar_addr,
829
  ],
 
 
 
 
 
830
  loc=loc,
831
  ip=ip,
832
  )
 
913
  return (copy_tma if const_expr(not single_stage) else copy_tma_single_stage), s, g
914
 
915
 
916
+ def s2t_get_copy_fn(
917
+ src_tensor: cute.Tensor,
918
+ dst_tensor: cute.Tensor,
919
+ cta_group: tcgen05.CtaGroup,
920
+ ) -> Callable:
921
+ """
922
+ Make tiledCopy for smem to tmem load, then return a copy function over stages.
923
+
924
+ :param src_tensor: The source tensor in smem
925
+ :param dst_tensor: The destination tensor in tmem
926
+ """
927
+ assert src_tensor.element_type == dst_tensor.element_type
928
+ # (MMA, MMA_MN, MMA_K, STAGE)
929
+ src_compact = cute.filter_zeros(src_tensor)
930
+ # (MMA, MMA_MN, MMA_K)
931
+ dst_compact = cute.filter_zeros(dst_tensor)
932
+ # Make S2T CopyAtom and tiledCopy.
933
+ copy_atom = cute.make_copy_atom(tcgen05.Cp4x32x128bOp(cta_group), dst_tensor.element_type)
934
+ tiled_copy = tcgen05.make_s2t_copy(copy_atom, dst_compact)
935
+ thr_copy = tiled_copy.get_slice(0)
936
+ # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
937
+ src_partition = tcgen05.get_s2t_smem_desc_tensor(tiled_copy, thr_copy.partition_S(src_compact))
938
+ # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K)
939
+ dst_partition = thr_copy.partition_D(dst_compact)
940
+
941
+ @dsl_user_op
942
+ def copy_s2t(stage_idx, *, loc=None, ip=None, **new_kwargs):
943
+ # Stage slice of partitioned source tensor: ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K)
944
+ stage_coord = (None, None, None, None, stage_idx)
945
+ cute.copy(
946
+ tiled_copy, src_partition[stage_coord], dst_partition, loc=loc, ip=ip, **new_kwargs
947
+ )
948
+
949
+ return copy_s2t
950
+
951
+
952
+ # tcgen05 TMEM <-> RMEM helpers (t2r loads / r2t stores).
953
+ #
954
+ # The register-side fragment of a tmem copy is derivable by layout algebra,
955
+ # with no reference to the original (pre-partition) tile tensor:
956
+ # - the per-thread register VALUE shape is mode 1 of the tiled copy's
957
+ # register-side TV layout (`layout_dst_tv_tiled` for loads,
958
+ # `layout_src_tv_tiled` for stores). The tmem-side partition can't supply
959
+ # it: tmem partitioning is warp-collective, so its value mode counts tmem
960
+ # cells across the whole warp, not per-thread register elements.
961
+ # - the tile-iteration modes are shared between partition_S and partition_D
962
+ # (same tiler over the same tile extent), so they can be read off whichever
963
+ # side was already partitioned.
964
+ # This kills the make-a-fake/identity-tensor-and-partition_D dance previously
965
+ # needed at every t2r site.
966
+
967
+
968
+ def tmem_reg_frag(
969
+ tiled_copy: cute.TiledCopy,
970
+ partitioned: cute.Tensor,
971
+ num_extra_modes: int = 0,
972
+ dtype: Optional[Type[cutlass.Numeric]] = None,
973
+ *,
974
+ loc=None,
975
+ ip=None,
976
+ ) -> cute.Tensor:
977
+ """Allocate the per-thread register fragment for ONE tile of a tcgen05
978
+ tmem copy, given any partitioned view of it (tmem or otherwise).
979
+
980
+ `partitioned` is (V, iter..., extra...) as produced by partition_S/_D;
981
+ the trailing `num_extra_modes` modes (stage, epi-subtile, ...) are
982
+ excluded from the fragment and indexed at copy time instead. `dtype`
983
+ defaults to `partitioned.element_type`.
984
+ The register side is inferred from the tcgen05 load/store op: destination
985
+ for t2r loads, source for r2t stores."""
986
+ tv = _tmem_copy_reg_tv_layout(tiled_copy)
987
+ val_shape = tv.shape[1]
988
+ rank = cute.rank(partitioned.shape)
989
+ iters = tuple(partitioned.shape[i] for i in range(1, rank - num_extra_modes))
990
+ frag_dtype = partitioned.element_type if const_expr(dtype is None) else dtype
991
+ return cute.make_rmem_tensor((val_shape, *iters), frag_dtype, loc=loc, ip=ip)
992
+
993
+
994
+ def r2s_partition_from_t2r(
995
+ tiled_copy_t2r: cute.TiledCopy,
996
+ s: cute.Tensor,
997
+ tidx: Int32,
998
+ transpose: bool = False,
999
+ position_independent=False,
1000
+ ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]:
1001
+ """SMEM-store (r2s) side chained off a tmem-load tiled copy: the r2s copy
1002
+ inherits the t2r copy's per-thread value ownership via make_tiled_copy_D,
1003
+ so the loaded fragment can be stored (post-conversion) without a shuffle.
1004
+ By default the store atom is selected like SM100 GEMM epilogues:
1005
+ `get_smem_store_op(layout, dst_dtype, tiled_copy_t2r.value_type, tiled_copy_t2r)`,
1006
+ so the stmatrix shape follows the tmem-load atom. `transpose=True` maps to
1007
+ COL_MAJOR, otherwise ROW_MAJOR.
1008
+
1009
+ `s` is the staged SMEM tile; its trailing stage mode is excluded from the
1010
+ register fragment. `position_independent=True` partitions through a
1011
+ position-independent swizzle view, matching `get_smem_store_C`.
1012
+ Returns `(tiled_copy, tRS_r, tRS_s)`; store via
1013
+ `cute.copy(tiled_copy, tRS_r, tRS_s[..., idx])`."""
1014
+ dtype = s.element_type
1015
+ layout = LayoutEnum.COL_MAJOR if const_expr(transpose) else LayoutEnum.ROW_MAJOR
1016
+ copy_atom = sm100_utils.get_smem_store_op(
1017
+ layout, dtype, tiled_copy_t2r.value_type, tiled_copy_t2r
1018
+ )
1019
+ tiled_copy = cute.make_tiled_copy_D(copy_atom, tiled_copy_t2r)
1020
+ thr_copy = tiled_copy.get_slice(tidx)
1021
+ if const_expr(not position_independent):
1022
+ tRS_s = thr_copy.partition_D(s)
1023
+ else:
1024
+ tRS_s = partition_D_position_independent(thr_copy, s)
1025
+ rank = cute.rank(tRS_s.shape)
1026
+ frag_shape = tuple(tRS_s.shape[i] for i in range(rank - 1))
1027
+ tRS_r = cute.make_rmem_tensor(frag_shape, dtype)
1028
+ return tiled_copy, tRS_r, tRS_s
1029
+
1030
+
1031
+ def s2r_partition_from_t2r(
1032
+ tiled_copy_t2r: cute.TiledCopy,
1033
+ s: cute.Tensor,
1034
+ tidx: Int32,
1035
+ r_layout: cute.Layout,
1036
+ copy_atom: Optional[cute.CopyAtom] = None,
1037
+ transpose: bool = False,
1038
+ position_independent=False,
1039
+ ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor, cute.Tensor]:
1040
+ """SMEM-load (s2r) counterpart of `r2s_partition_from_t2r` (ldmatrix vs
1041
+ stmatrix), for reading an epilogue input that was TMA-staged into an
1042
+ epi-tile SMEM buffer (e.g. C in gemm, z in ssd) into registers
1043
+ element-aligned with the t2r fragments. The register fragment is
1044
+ allocated with `r_layout` (pass the r2s fragment's layout) so its linear
1045
+ element order matches the t2r/r2s fragments; `tSR_r` is its retiled view
1046
+ for the collective copy. Per-warp SMEM footprints of this load and the
1047
+ chained r2s store coincide, so reusing one buffer for input then output
1048
+ is warp-local (no inter-warp hazard).
1049
+
1050
+ `position_independent=True` partitions through a position-independent
1051
+ swizzle view, matching `get_smem_load_C`.
1052
+
1053
+ Returns `(tiled_copy, tRS_r, tSR_r, tSR_s)`; load via
1054
+ `cute.copy(tiled_copy, tSR_s[..., idx], tSR_r)` then read `tRS_r`."""
1055
+ dtype = s.element_type
1056
+ if const_expr(copy_atom is None):
1057
+ copy_atom = cute.make_copy_atom(
1058
+ warp.LdMatrix8x8x16bOp(transpose=transpose, num_matrices=4), dtype
1059
+ )
1060
+ tiled_copy = cute.make_tiled_copy_D(copy_atom, tiled_copy_t2r)
1061
+ thr_copy = tiled_copy.get_slice(tidx)
1062
+ if const_expr(not position_independent):
1063
+ tSR_s = thr_copy.partition_S(s)
1064
+ else:
1065
+ tSR_s = partition_S_position_independent(thr_copy, s)
1066
+ tRS_r = cute.make_rmem_tensor(r_layout, dtype)
1067
+ tSR_r = tiled_copy.retile(tRS_r)
1068
+ return tiled_copy, tRS_r, tSR_r, tSR_s
1069
+
1070
+
1071
  def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsync):
1072
  def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
1073
  copy(
 
1080
  return copy_fn
1081
 
1082
 
1083
+ def chain_tma_producer_copy_fns(copy_fns: Sequence[Optional[Callable]]):
1084
+ if not any(fn is not None for fn in copy_fns):
1085
+ return None
1086
+
1087
+ def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
1088
+ for fn in copy_fns:
1089
+ if const_expr(fn is not None):
1090
+ fn(src_idx=src_idx, producer_state=producer_state, **new_kwargs)
1091
+
1092
+ return copy_fn
1093
+
1094
+
1095
  @cute.jit
1096
  def gather_m_get_copy_fn(
1097
  thr_copy_A: cute.ThrCopy,
 
1101
  limit_m: Int32,
1102
  limit_k: Int32,
1103
  ) -> Callable:
1104
+ tile_M, tile_K = cute.size(sA, mode=[0]), cute.size(sA, mode=[1])
1105
+ tAsA = partition_D_position_independent(thr_copy_A, sA)
1106
  # k-major
1107
  assert tAsA.shape[2] == 1
1108
  tAsA = cute.group_modes(cute.slice_(tAsA, (None, None, 0, None)), 0, 2)
1109
 
1110
+ is_even_m_smem = tile_M % thr_copy_A.tiler_mn[0].shape == 0
1111
  if const_expr(not is_even_m_smem):
1112
+ limit_m = min(limit_m, tile_M)
1113
  elems_per_load = cute.size(tAsA.shape[0][0])
1114
+ cA = cute.make_identity_tensor((tile_M, tile_K))
1115
  tAcA = thr_copy_A.partition_S(cA)
1116
  t0AcA = thr_copy_A.get_slice(0).partition_S(cA)
1117
  # Instead of comparing tAcA to limit_m, we instead compare t0AcA to limit_m - tAcA[0][0]
 
1133
  else:
1134
  m_idx[m] = 0 # It's ok to load row 0 in the case of OOB
1135
 
1136
+ mA_k = cute.logical_divide(mA, (None, tile_K))
1137
 
1138
+ def copy_fn(src_idx, dst_idx, pred: cutlass.Constexpr[bool] = False):
1139
  tApA_k = None
1140
  if const_expr(pred):
1141
  tApA_k = cute.make_rmem_tensor(cols_per_thread, Boolean)
1142
+ limit_k_cur = limit_k - src_idx * tile_K
1143
  for k in cutlass.range(cols_per_thread, unroll_full=True):
1144
  tApA_k[k] = t0AcA[0, 0, k][1] < limit_k_cur
1145
  mA_cur = mA_k[None, (None, src_idx)]
 
1302
  tma_gather4_load_fn = partial(tma_gather4_load, tma_desc_ptr, num_cta=cta_group)
1303
 
1304
  def copy_fn(src_idx, dst_idx, tma_bar_ptr: cute.Pointer):
1305
+ tSR_sA_cur = tSR_sA[None, None, None, dst_idx]
1306
  col_idx = tile_K * src_idx
1307
  for m in cutlass.range(cute.size(tSR_rAIdx, mode=[1]), unroll_full=True):
1308
  row_indices = [tSR_rAIdx[v, m] for v in range(4)]
1309
+ smem_ptr = tSR_sA_cur[None, m, None].iterator
1310
  with cute.arch.elect_one():
1311
  tma_gather4_load_fn(smem_ptr, tma_bar_ptr, col_idx, row_indices)
1312
 
1313
  return copy_fn
1314
+
1315
+
1316
+ @cute.jit
1317
+ def gather_k_get_tma_copy_fn(
1318
+ tma_atom: cute.CopyAtom,
1319
+ sA: cute.Tensor, # ((4, tile_K/4), (tile_M,), STAGE) — K-grouped load layout
1320
+ sAIdx: cute.Tensor, # (tile_K, a_prefetch_stage) — K indices in smem
1321
+ col_idx: Int32, # M offset in global tensor (contiguous dim for M-major)
1322
+ warp_idx: Int32,
1323
+ num_warps: int,
1324
+ num_cta: int = 1,
1325
+ ) -> Tuple[Callable, Callable]:
1326
+ """Build a copy function for TMA gather4 in K dimension (M-major A).
1327
+
1328
+ Each gather4 instruction loads 4 K-columns × tile_M contiguous M-elements.
1329
+ col_idx is the absolute M position in the global tensor.
1330
+ K indices come from sAIdx (prefetched to smem by the scheduler warp).
1331
+
1332
+ Returns copy_fn(src_idx, dst_idx, tma_bar_ptr) which:
1333
+ Issues gather4 calls with those K indices as row_indices
1334
+ """
1335
+ tile_K = cute.size(sAIdx, mode=[0])
1336
+ assert tile_K % 4 == 0
1337
+ cta_group = num_cta
1338
+
1339
+ # Tiled copy for loading K indices from smem to registers (4 per vector, across warps)
1340
+ copy_AIdx_s2r = cute.make_tiled_copy_tv(
1341
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Int32, num_bits_per_copy=128),
1342
+ cute.make_layout(num_warps), # thr_layout
1343
+ cute.make_layout(4), # val_layout — 4 K indices per gather4
1344
+ )
1345
+ warp_idx = cute.arch.make_warp_uniform(warp_idx)
1346
+ warp_copy_AIdx_s2r = copy_AIdx_s2r.get_slice(warp_idx)
1347
+ tSR_sAIdx = warp_copy_AIdx_s2r.partition_S(sAIdx) # (((4,1),4,4))
1348
+ # ((4,1),4,(64,2),(1,4)):((64,0),1024,(1,4096),(0,8192))
1349
+ tSR_sA = warp_copy_AIdx_s2r.partition_S(layout_utils.transpose_view(sA))
1350
+ tma_desc_ptr = get_tma_desc_addr(tma_atom)
1351
+ tma_gather4_load_fn = partial(tma_gather4_load, tma_desc_ptr, num_cta=cta_group)
1352
+
1353
+ def prefetch_from_smem_fn(
1354
+ a_prefetch_pipeline,
1355
+ src_idx,
1356
+ dst_idx,
1357
+ a_prefetch_consumer_state,
1358
+ ) -> cute.Tensor:
1359
+ a_prefetch_pipeline.consumer_wait(a_prefetch_consumer_state)
1360
+ tSR_rAIdx = load_s2r(tSR_sAIdx[None, None, dst_idx])
1361
+ cute.arch.sync_warp()
1362
+ with cute.arch.elect_one():
1363
+ a_prefetch_pipeline.consumer_release(a_prefetch_consumer_state)
1364
+ return tSR_rAIdx
1365
+
1366
+ def copy_fn(src_idx, dst_idx, tSR_rAIdx, tma_bar_ptr: cute.Pointer):
1367
+ # Issue gather4: col_idx = M position, row_indices = 4 K positions
1368
+ tSR_sA_cur = tSR_sA[None, None, None, dst_idx]
1369
+ gather_dim = cute.size(tSR_sA_cur, mode=[2, 0]) # Typically 64
1370
+ for k in cutlass.range(cute.size(tSR_rAIdx, mode=[1]), unroll_full=True):
1371
+ row_indices = [tSR_rAIdx[v, k] for v in range(4)]
1372
+ for m in cutlass.range(cute.size(tSR_sA_cur, mode=[2, 1]), unroll_full=True):
1373
+ smem_ptr = tSR_sA_cur[None, k, (None, m)].iterator
1374
+ with cute.arch.elect_one():
1375
+ tma_gather4_load_fn(
1376
+ smem_ptr, tma_bar_ptr, col_idx + m * gather_dim, row_indices
1377
+ )
1378
+
1379
+ return copy_fn, prefetch_from_smem_fn
1380
+
1381
+
1382
+ # ---------------------------------------------------------------------------
1383
+ # Store helpers
1384
+ # ---------------------------------------------------------------------------
1385
+
1386
+
1387
+ @dsl_user_op
1388
+ @cute.jit
1389
+ def store(
1390
+ ptr: cute.Pointer,
1391
+ val,
1392
+ pred: Optional[Boolean] = None,
1393
+ cop: cutlass.Constexpr = None,
1394
+ *,
1395
+ loc=None,
1396
+ ip=None,
1397
+ ):
1398
+ """Store a scalar value via cute.arch.store.
1399
+
1400
+ ptr: cute.Pointer (any address space).
1401
+ val: DSL Numeric value.
1402
+ pred: None → unconditional. DSL Boolean → skipped when pred == 0.
1403
+ cop: Cache operator — "wb" (default), "cg", "cs" (streaming), "wt".
1404
+ """
1405
+ if const_expr(pred is None):
1406
+ cute.arch.store(ptr.llvm_ptr, type(val)(val), cop=cop, loc=loc, ip=ip)
1407
+ else:
1408
+ if pred:
1409
+ cute.arch.store(ptr.llvm_ptr, type(val)(val), cop=cop, loc=loc, ip=ip)
1410
+
1411
+
1412
+ @dsl_user_op
1413
+ @cute.jit
1414
+ def store_v2(
1415
+ ptr: cute.Pointer,
1416
+ v0,
1417
+ v1,
1418
+ pred: Optional[Boolean] = None,
1419
+ cop: cutlass.Constexpr = None,
1420
+ *,
1421
+ loc=None,
1422
+ ip=None,
1423
+ ):
1424
+ """Vectorized store of 2 elements via cute.arch.store.
1425
+
1426
+ Packs v0, v1 into an MLIR <2 x T> vector.
1427
+ ptr: cute.Pointer (any address space, must be aligned for vector width).
1428
+ cop: Cache operator — "wb" (default), "cg", "cs" (streaming), "wt".
1429
+ """
1430
+ vec = make_vector(type(v0), v0, v1, loc=loc, ip=ip)
1431
+ if const_expr(pred is None):
1432
+ cute.arch.store(ptr.llvm_ptr, vec, cop=cop, loc=loc, ip=ip)
1433
+ else:
1434
+ if pred:
1435
+ cute.arch.store(ptr.llvm_ptr, vec, cop=cop, loc=loc, ip=ip)
1436
+
1437
+
1438
+ @dsl_user_op
1439
+ @cute.jit
1440
+ def store_v4(
1441
+ ptr: cute.Pointer,
1442
+ v0,
1443
+ v1,
1444
+ v2,
1445
+ v3,
1446
+ pred: Optional[Boolean] = None,
1447
+ cop: cutlass.Constexpr = None,
1448
+ *,
1449
+ loc=None,
1450
+ ip=None,
1451
+ ):
1452
+ """Vectorized store of 4 elements via cute.arch.store.
1453
+
1454
+ Packs v0–v3 into an MLIR <4 x T> vector.
1455
+ ptr: cute.Pointer (any address space, must be aligned for vector width).
1456
+ cop: Cache operator — "wb" (default), "cg", "cs" (streaming), "wt".
1457
+ """
1458
+ vec = make_vector(type(v0), v0, v1, v2, v3, loc=loc, ip=ip)
1459
+ if const_expr(pred is None):
1460
+ cute.arch.store(ptr.llvm_ptr, vec, cop=cop, loc=loc, ip=ip)
1461
+ else:
1462
+ if pred:
1463
+ cute.arch.store(ptr.llvm_ptr, vec, cop=cop, loc=loc, ip=ip)
build/torch-cuda/quack/cute_dsl_utils.py CHANGED
@@ -4,6 +4,9 @@ from typing import Tuple, get_origin
4
  from functools import lru_cache
5
  from dataclasses import dataclass, fields
6
 
 
 
 
7
  import torch
8
 
9
  try:
@@ -14,7 +17,6 @@ except ImportError:
14
  import cutlass
15
  import cutlass.cute as cute
16
  from cutlass import Int32, Int64, Float16, BFloat16, Float32
17
- from cutlass.base_dsl.typing import JitArgument
18
  from cutlass.base_dsl.tvm_ffi_builder import spec
19
  from cutlass.cutlass_dsl import NumericMeta
20
 
@@ -61,15 +63,64 @@ torch2cute_dtype_map = {
61
 
62
 
63
  @lru_cache
64
- def get_max_active_clusters(cluster_size):
65
- return cutlass.utils.HardwareInfo().get_max_active_clusters(cluster_size=cluster_size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
 
68
  @lru_cache
69
- def get_device_capacity(device: torch.device = None) -> Tuple[int, int]:
 
 
 
 
 
 
 
 
70
  return torch.cuda.get_device_capability(device)
71
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def _partition_fields(obj):
74
  """Split dataclass fields into (constexpr_dict, non_constexpr_dict) by type."""
75
  all_fields = {field.name: getattr(obj, field.name) for field in fields(obj)}
@@ -138,28 +189,3 @@ class ParamsBase:
138
  return values
139
 
140
  __new_from_mlir_values__ = _new_from_mlir_values
141
-
142
-
143
- @dataclass
144
- class ArgumentsBase(JitArgument):
145
- def __c_pointers__(self):
146
- _, non_constexpr_fields = _partition_fields(self)
147
- c_ptrs = []
148
- for obj in non_constexpr_fields.values():
149
- if hasattr(obj, "__c_pointers__"):
150
- c_ptrs.extend(obj.__c_pointers__())
151
- return c_ptrs
152
-
153
- def __get_mlir_types__(self):
154
- _, non_constexpr_fields = _partition_fields(self)
155
- types, self._values_pos = [], []
156
- for obj in non_constexpr_fields.values():
157
- if hasattr(obj, "__get_mlir_types__"):
158
- obj_types = obj.__get_mlir_types__()
159
- types.extend(obj_types)
160
- self._values_pos.append(len(obj_types))
161
- else:
162
- self._values_pos.append(0)
163
- return types
164
-
165
- __new_from_mlir_values__ = _new_from_mlir_values
 
4
  from functools import lru_cache
5
  from dataclasses import dataclass, fields
6
 
7
+ import os
8
+ import re
9
+
10
  import torch
11
 
12
  try:
 
17
  import cutlass
18
  import cutlass.cute as cute
19
  from cutlass import Int32, Int64, Float16, BFloat16, Float32
 
20
  from cutlass.base_dsl.tvm_ffi_builder import spec
21
  from cutlass.cutlass_dsl import NumericMeta
22
 
 
63
 
64
 
65
  @lru_cache
66
+ def get_device_multiprocessor_count(device_id: int = 0) -> int:
67
+ return cutlass.utils.HardwareInfo(device_id).get_device_multiprocessor_count()
68
+
69
+
70
+ @lru_cache
71
+ def get_max_active_clusters(
72
+ cluster_size: int,
73
+ device_capacity: Tuple[int, int] | None = None,
74
+ device_id: int = 0,
75
+ ) -> int:
76
+ if device_capacity is None:
77
+ device_capacity = get_device_capacity()
78
+ if device_capacity[0] < 9:
79
+ if cluster_size != 1:
80
+ raise ValueError("SM8x kernels do not support CTA clusters; cluster_size must be 1")
81
+ return get_device_multiprocessor_count(device_id)
82
+ return cutlass.utils.HardwareInfo(device_id).get_max_active_clusters(cluster_size=cluster_size)
83
+
84
+
85
+ def _parse_arch_str(arch_str: str) -> Tuple[int, int]:
86
+ """Parse arch string (e.g. 'sm_90', 'sm90', '90', 'sm_100a') to (major, minor) tuple."""
87
+ match = re.match(r"^(?:sm_?)?(\d+)(\d)([af]?)$", arch_str.strip(), re.IGNORECASE)
88
+ if not match:
89
+ raise ValueError(f"Invalid QUACK_ARCH format: {arch_str!r} (expected e.g. '90', 'sm_90')")
90
+ major, minor, _ = match.groups()
91
+ return int(major), int(minor)
92
 
93
 
94
  @lru_cache
95
+ def _get_device_capacity_cached(device: torch.device = None) -> Tuple[int, int]:
96
+ """Return (major, minor) device capability.
97
+
98
+ Override with QUACK_ARCH (e.g. 'sm_90' or '90') for CPU-only compilation
99
+ without a GPU present.
100
+ """
101
+ arch_override = os.environ.get("QUACK_ARCH")
102
+ if arch_override is not None:
103
+ return _parse_arch_str(arch_override)
104
  return torch.cuda.get_device_capability(device)
105
 
106
 
107
+ def get_device_capacity(
108
+ device: torch.device | torch.Tensor | None = None,
109
+ ) -> Tuple[int, int]:
110
+ """Return (major, minor) device capability.
111
+
112
+ Override with QUACK_ARCH (e.g. 'sm_90' or '90') for CPU-only compilation
113
+ without a GPU present.
114
+
115
+ Accepts either a ``torch.device`` or a tensor and canonicalizes to the
116
+ underlying device before consulting the cached helper. This avoids leaking
117
+ tensors through the LRU cache key.
118
+ """
119
+ if isinstance(device, torch.Tensor):
120
+ device = device.device
121
+ return _get_device_capacity_cached(device)
122
+
123
+
124
  def _partition_fields(obj):
125
  """Split dataclass fields into (constexpr_dict, non_constexpr_dict) by type."""
126
  all_fields = {field.name: getattr(obj, field.name) for field in fields(obj)}
 
189
  return values
190
 
191
  __new_from_mlir_values__ = _new_from_mlir_values
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-cuda/quack/dsl/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CuTe DSL helpers and integration hooks.
2
+
3
+ Importing this package installs quack's Pythonic CuTe tensor indexing, which
4
+ monkey-patches CuTe's tensor classes process-wide so that `...` and `:` work in
5
+ `__getitem__`/`__setitem__`. `quack.copy_utils` relies on that sugar (e.g.
6
+ `tRS_sC[..., dst_idx]`), and the sm90 backward kernel reaches it.
7
+
8
+ Upstream's `quack.dsl` also exports `cute_op` from `torch_library_op`; nothing
9
+ vendored here uses it, so it is not copied.
10
+ """
11
+
12
+ from . import cute_tensor_indexing # noqa: F401
build/torch-cuda/quack/dsl/cute_tensor_indexing.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026, Tri Dao.
2
+ """Small compatibility layer for more Pythonic CuTe tensor indexing.
3
+
4
+ CuTe uses ``None`` as its underscore/full-mode slice marker, e.g. ``A[i, None]``.
5
+ This module teaches CuTe tensors the equivalent Python spelling ``:`` and expands
6
+ ``...`` to the right number of full-mode slices.
7
+
8
+ Importing this module intentionally mutates CuTe's tensor classes process-wide.
9
+ The original methods are retained as ``_quack_original_getitem`` and
10
+ ``_quack_original_setitem`` for debugging or manual rollback.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+ from cutlass.cutlass_dsl import dsl_user_op
18
+ import cutlass.cute.tensor as _cute_tensor
19
+
20
+
21
+ _ORIGINAL_GETITEM_ATTR = "_quack_original_getitem"
22
+ _ORIGINAL_SETITEM_ATTR = "_quack_original_setitem"
23
+ _PATCHED_ATTR = "_quack_extended_indexing"
24
+ _PATCHED_SETITEM_ATTR = f"{_PATCHED_ATTR}_setitem"
25
+
26
+
27
+ def _is_full_slice(idx: Any) -> bool:
28
+ return isinstance(idx, slice) and idx.start is None and idx.stop is None and idx.step is None
29
+
30
+
31
+ def _index_uses_ellipsis(idx: Any) -> bool:
32
+ if idx is Ellipsis:
33
+ return True
34
+ if isinstance(idx, tuple):
35
+ return any(_index_uses_ellipsis(item) for item in idx)
36
+ return False
37
+
38
+
39
+ def _shape_rank(shape: Any, idx: Any = None) -> int:
40
+ if shape is None:
41
+ suffix = f" in {idx!r}" if idx is not None else ""
42
+ raise ValueError(f"tensor shape is required to expand ellipsis{suffix}")
43
+ return _cute_tensor.rank(shape)
44
+
45
+
46
+ def _shape_mode(shape: Any, mode: int) -> Any:
47
+ if isinstance(shape, tuple) and mode < len(shape):
48
+ return shape[mode]
49
+ return None
50
+
51
+
52
+ def _canonicalize_cute_tensor_index(idx: Any, tensor_shape: Any = None) -> Any:
53
+ """Convert Python indexing sugar to CuTe's coordinate convention.
54
+
55
+ ``:`` becomes ``None`` (CuTe's full-mode/underscore marker) and ``...`` expands
56
+ within the current hierarchy level using ``tensor_shape``. Other slices like
57
+ ``1:4`` are intentionally rejected because CuTe tensor slicing only supports
58
+ keeping an entire mode or selecting a single coordinate.
59
+ """
60
+ if idx is Ellipsis:
61
+ return (None,) * _shape_rank(tensor_shape, idx)
62
+ if _is_full_slice(idx):
63
+ return None
64
+ if isinstance(idx, slice):
65
+ raise ValueError(f"CuTe Tensor indexing only supports full slices ':', got {idx!r}")
66
+ if not isinstance(idx, tuple):
67
+ return idx
68
+
69
+ ellipsis_count = sum(item is Ellipsis for item in idx)
70
+ if ellipsis_count > 1:
71
+ raise ValueError("CuTe Tensor indexing supports at most one ellipsis per tuple level")
72
+
73
+ explicit_modes = len(idx) - ellipsis_count
74
+ fill_modes = 0
75
+ if ellipsis_count:
76
+ tensor_rank = _shape_rank(tensor_shape, idx)
77
+ fill_modes = tensor_rank - explicit_modes
78
+ if fill_modes < 0:
79
+ raise ValueError(
80
+ f"ellipsis cannot expand index {idx!r} for rank-{tensor_rank} CuTe Tensor mode"
81
+ )
82
+
83
+ result: list[Any] = []
84
+ mode = 0
85
+ for item in idx:
86
+ if item is Ellipsis:
87
+ result.extend([None] * fill_modes)
88
+ mode += fill_modes
89
+ else:
90
+ result.append(_canonicalize_cute_tensor_index(item, _shape_mode(tensor_shape, mode)))
91
+ mode += 1
92
+ return tuple(result)
93
+
94
+
95
+ def _make_getitem(original_getitem: Any) -> Any:
96
+ @dsl_user_op
97
+ def _getitem(self: Any, idx: Any, *, loc: Any = None, ip: Any = None) -> Any:
98
+ tensor_shape = self.shape if _index_uses_ellipsis(idx) else None
99
+ idx = _canonicalize_cute_tensor_index(idx, tensor_shape)
100
+ return original_getitem(self, idx, loc=loc, ip=ip)
101
+
102
+ return _getitem
103
+
104
+
105
+ def _make_setitem(original_setitem: Any) -> Any:
106
+ @dsl_user_op
107
+ def _setitem(self: Any, idx: Any, data: Any, *, loc: Any = None, ip: Any = None) -> Any:
108
+ tensor_shape = self.shape if _index_uses_ellipsis(idx) else None
109
+ idx = _canonicalize_cute_tensor_index(idx, tensor_shape)
110
+ return original_setitem(self, idx, data, loc=loc, ip=ip)
111
+
112
+ return _setitem
113
+
114
+
115
+ def patch_cute_tensor_indexing() -> None:
116
+ """Monkey patch CuTe Tensor indexing with ``:``, ``...`` sugar.
117
+
118
+ The patch is idempotent and keeps the original CuTe implementation for all
119
+ canonical coordinates, so existing ``A[i, j, None]`` code continues to behave
120
+ exactly as before. It is a process-wide mutation of CuTe's tensor classes.
121
+ """
122
+ for cls in (_cute_tensor._Tensor, _cute_tensor.TensorSSA):
123
+ if _PATCHED_ATTR not in cls.__dict__:
124
+ setattr(cls, _ORIGINAL_GETITEM_ATTR, cls.__getitem__)
125
+ cls.__getitem__ = _make_getitem(cls.__getitem__) # type: ignore[method-assign]
126
+ setattr(cls, _PATCHED_ATTR, True)
127
+
128
+ # TensorSSA has no upstream __setitem__, so only _Tensor needs the store path patched.
129
+ tensor_cls = _cute_tensor._Tensor
130
+ if _PATCHED_SETITEM_ATTR not in tensor_cls.__dict__:
131
+ setattr(tensor_cls, _ORIGINAL_SETITEM_ATTR, tensor_cls.__setitem__)
132
+ tensor_cls.__setitem__ = _make_setitem(tensor_cls.__setitem__) # type: ignore[method-assign]
133
+ setattr(tensor_cls, _PATCHED_SETITEM_ATTR, True)
134
+
135
+
136
+ patch_cute_tensor_indexing()
137
+
138
+
139
+ __all__ = ["patch_cute_tensor_indexing"]
build/torch-cuda/quack/layout_utils.py CHANGED
@@ -18,6 +18,19 @@ def select(a: cute.Tensor, mode: list[int]) -> cute.Tensor:
18
  return cute.make_tensor(a.iterator, cute.select(a.layout, mode))
19
 
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  def expand(a: cute.Tensor, dim: int, size: Int32 | int) -> cute.Tensor:
22
  shape = (*a.shape[:dim], size, *a.shape[dim:])
23
  stride = (*a.layout.stride[:dim], 0, *a.layout.stride[dim:])
@@ -217,15 +230,16 @@ def convert_layout_acc_frgA(acc_layout: cute.Layout) -> cute.Layout:
217
  )
218
  else: # Sm80
219
  # (4, MMA_M, MMA_N) -> (4, MMA_M, (2, MMA_N / 2))
 
220
  l = cute.logical_divide(acc_layout, (None, None, 2))
221
  rA_mma_view = cute.make_layout(
222
  (
223
- (l.shape[0], l.shape[2][0]),
224
  l.shape[1],
225
  l.shape[2][1],
226
  ),
227
  stride=(
228
- (l.stride[0], l.stride[2][0]),
229
  l.stride[1],
230
  l.stride[2][1],
231
  ),
@@ -264,7 +278,7 @@ def convert_layout_zero_stride(
264
 
265
 
266
  def mma_partition_C_vec(
267
- sVec: cute.Tensor, thr_mma: cute.core.ThrMma, expand_shape: int, is_colvec: bool
268
  ) -> cute.Tensor:
269
  assert cute.rank(sVec) == 2
270
  assert sVec.stride[0] == 1
@@ -281,7 +295,7 @@ def mma_partition_C_vec(
281
 
282
 
283
  def mma_partition_A_vec(
284
- sVec: cute.Tensor, thr_mma: cute.core.ThrMma, expand_shape: int, is_colvec: bool
285
  ) -> cute.Tensor:
286
  assert cute.rank(sVec) == 2
287
  assert sVec.stride[0] == 1
@@ -295,3 +309,77 @@ def mma_partition_A_vec(
295
  sVec_mma = cute.make_tensor(sVec.iterator, cute.make_layout(shape, stride=stride))
296
  tC_sVec = make_acc_tensor_mn_view(thr_mma.partition_A(sVec_mma))
297
  return tC_sVec[None, 0, None] if const_expr(is_colvec) else tC_sVec[0, None, None]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  return cute.make_tensor(a.iterator, cute.select(a.layout, mode))
19
 
20
 
21
+ def concat_to_interleave(a: cute.Tensor, dim: int) -> cute.Tensor:
22
+ """Reshape a concat [first_half; second_half] layout to interleaved along `dim`.
23
+
24
+ Splits dimension `dim` (size 2N) into hierarchical (2, N) so that elements
25
+ from the first half and second half alternate: [first_0, second_0, first_1, ...].
26
+ Used to convert gated MLP weight layout from concat [gate; up] to interleaved.
27
+ """
28
+ half = cute.size(a, mode=[dim]) // 2
29
+ shape = (*a.shape[:dim], (2, half), *a.shape[dim + 1 :])
30
+ stride = (*a.stride[:dim], (half * a.stride[dim], a.stride[dim]), *a.stride[dim + 1 :])
31
+ return cute.make_tensor(a.iterator, cute.make_layout(shape, stride=stride))
32
+
33
+
34
  def expand(a: cute.Tensor, dim: int, size: Int32 | int) -> cute.Tensor:
35
  shape = (*a.shape[:dim], size, *a.shape[dim:])
36
  stride = (*a.layout.stride[:dim], 0, *a.layout.stride[dim:])
 
230
  )
231
  else: # Sm80
232
  # (4, MMA_M, MMA_N) -> (4, MMA_M, (2, MMA_N / 2))
233
+ assert acc_layout.shape[2] % 2 == 0
234
  l = cute.logical_divide(acc_layout, (None, None, 2))
235
  rA_mma_view = cute.make_layout(
236
  (
237
+ (l.shape[0][0], l.shape[0][1], l.shape[2][0]),
238
  l.shape[1],
239
  l.shape[2][1],
240
  ),
241
  stride=(
242
+ (l.stride[0][0], l.stride[0][1], l.stride[2][0]),
243
  l.stride[1],
244
  l.stride[2][1],
245
  ),
 
278
 
279
 
280
  def mma_partition_C_vec(
281
+ sVec: cute.Tensor, thr_mma: cute.ThrMma, expand_shape: int, is_colvec: bool
282
  ) -> cute.Tensor:
283
  assert cute.rank(sVec) == 2
284
  assert sVec.stride[0] == 1
 
295
 
296
 
297
  def mma_partition_A_vec(
298
+ sVec: cute.Tensor, thr_mma: cute.ThrMma, expand_shape: int, is_colvec: bool
299
  ) -> cute.Tensor:
300
  assert cute.rank(sVec) == 2
301
  assert sVec.stride[0] == 1
 
309
  sVec_mma = cute.make_tensor(sVec.iterator, cute.make_layout(shape, stride=stride))
310
  tC_sVec = make_acc_tensor_mn_view(thr_mma.partition_A(sVec_mma))
311
  return tC_sVec[None, 0, None] if const_expr(is_colvec) else tC_sVec[0, None, None]
312
+
313
+
314
+ def copy_partition_S_vec(
315
+ sVec: cute.Tensor, thr_copy: cute.ThrCopy, expand_shape: int, is_colvec: bool
316
+ ) -> cute.Tensor:
317
+ assert cute.rank(sVec) == 2
318
+ assert sVec.stride[0] == 1
319
+ stage = sVec.shape[1]
320
+ shape = (
321
+ (sVec.shape[0], expand_shape, stage)
322
+ if const_expr(is_colvec)
323
+ else (expand_shape, sVec.shape[0], stage)
324
+ )
325
+ stride = (1, 0, sVec.stride[1]) if const_expr(is_colvec) else (0, 1, sVec.stride[1])
326
+ sVec_thr = cute.make_tensor(sVec.iterator, cute.make_layout(shape, stride=stride))
327
+ tC_sVec = reshape_acc_to_mn(thr_copy.partition_S(sVec_thr))
328
+ return tC_sVec[None, 0, None] if const_expr(is_colvec) else tC_sVec[0, None, None]
329
+
330
+
331
+ def copy_partition_D_vec(
332
+ sVec: cute.Tensor, thr_copy: cute.ThrCopy, expand_shape: int, is_colvec: bool
333
+ ) -> cute.Tensor:
334
+ assert cute.rank(sVec) == 2
335
+ assert sVec.stride[0] == 1
336
+ stage = sVec.shape[1]
337
+ shape = (
338
+ (sVec.shape[0], expand_shape, stage)
339
+ if const_expr(is_colvec)
340
+ else (expand_shape, sVec.shape[0], stage)
341
+ )
342
+ stride = (1, 0, sVec.stride[1]) if const_expr(is_colvec) else (0, 1, sVec.stride[1])
343
+ sVec_thr = cute.make_tensor(sVec.iterator, cute.make_layout(shape, stride=stride))
344
+ tC_sVec = reshape_acc_to_mn(thr_copy.partition_D(sVec_thr))
345
+ return tC_sVec[None, 0, None] if const_expr(is_colvec) else tC_sVec[0, None, None]
346
+
347
+
348
+ def tile_atom_to_shape_SF_strided(
349
+ shape: cute.Shape,
350
+ sf_vec_size: int,
351
+ sf_strides,
352
+ ) -> cute.Layout:
353
+ """Build an SFA/SFB layout matching `shape` (A or B operand shape) but
354
+ honoring the scale tensor's actual strides instead of hardcoded packed
355
+ ones.
356
+
357
+ Mirrors `cutlass.utils.blockscaled_layout.tile_atom_to_shape_SF(shape,
358
+ sf_vec_size)`, except outer-mode strides come from `sf_strides` (pass
359
+ `mSFA.stride` / `mSFB.stride` directly). The inner 512-B atom
360
+ `((32, 4), (sf_vec_size, 4)) : ((16, 4), (0, 1))` is hardware-fixed.
361
+
362
+ Implementation uses `cute.blocked_product(atom, outer)`; `blocked_product`
363
+ scales the outer layout's strides by `cosize(atom) == 512`, so we divide
364
+ the byte strides by 512 (one tile) before handing them in.
365
+
366
+ Args:
367
+ shape: A/B operand shape. Rank-3 `(m/n, k, l)` or rank-2
368
+ `(total_mn, k)` (varlen_m).
369
+ sf_vec_size: Scale factor vector size (16 or 32).
370
+ sf_strides: Strides of the scale tensor, which has logical shape
371
+ `(L, rmn, rk, 512)` (rank 4). Only `sf_strides[0..2]` are used:
372
+ `sf_strides[1]` as the rmn stride, `sf_strides[2]` as the rk
373
+ stride, and `sf_strides[0]` as the L stride (only for rank-3
374
+ `shape`).
375
+ """
376
+ from cutlass.utils.blockscaled_layout import BlockScaledBasicChunk
377
+
378
+ atom = BlockScaledBasicChunk(sf_vec_size).layout
379
+ rmn = cute.ceil_div(shape[0], 128)
380
+ rk = cute.ceil_div(shape[1], sf_vec_size * 4)
381
+ outer = cute.make_layout((rmn, rk), stride=(sf_strides[1] // 512, sf_strides[2] // 512))
382
+ sf_layout = cute.blocked_product(atom, outer)
383
+ if const_expr(len(shape) == 3):
384
+ sf_layout = cute.append(sf_layout, cute.make_layout(shape[2], stride=sf_strides[0]))
385
+ return sf_layout
build/torch-cuda/quack/rounding.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025-2026, Vijay Thakkar, Tri Dao.
2
+ """Rounding mode control and stochastic rounding primitives for GEMM epilogues.
3
+
4
+ Provides a RoundingMode enum for configuring how epilogues downconvert the
5
+ accumulator dtype (typically FP32) to the output dtype before storing to gmem.
6
+ Stochastic rounding (RS) uses the hardware cvt.rs.satfinite.bf16x2.f32 PTX
7
+ instruction and is only supported on Blackwell (SM100+) GPUs.
8
+ """
9
+
10
+ from enum import IntEnum
11
+
12
+ import cutlass
13
+ import cutlass.cute as cute
14
+ from cutlass import Float32, Uint32
15
+ from cutlass._mlir import ir
16
+ from cutlass._mlir.dialects import llvm, vector
17
+ from cutlass.cutlass_dsl import dsl_user_op, Int32, T
18
+
19
+
20
+ class RoundingMode(IntEnum):
21
+ """Rounding modes for epilogue dtype downconversion.
22
+
23
+ RN — Round to nearest even (default hardware behavior)
24
+ RS — Stochastic rounding (SM100+ only, BF16 output only)
25
+ """
26
+
27
+ RN = 0
28
+ RS = 1
29
+
30
+
31
+ # Odd strides used to derive distinct Philox counters across output tiles and subtiles.
32
+ EPILOGUE_SR_SEED_M_STRIDE = 65537
33
+ EPILOGUE_SR_SEED_N_STRIDE = 257
34
+ EPILOGUE_SR_SEED_BATCH_STRIDE = 17
35
+ EPILOGUE_SR_SEED_SUBTILE_STRIDE = 7
36
+ EPILOGUE_SR_SEED_AUX_OUT_SALT = 0x9E3779B1
37
+
38
+ PHILOX_N_ROUNDS_DEFAULT = 7
39
+
40
+ PHILOX_ROUND_A = 0xD2511F53
41
+ PHILOX_ROUND_B = 0xCD9E8D57
42
+ PHILOX_KEY_A = 0x9E3779B9
43
+ PHILOX_KEY_B = 0xBB67AE85
44
+
45
+
46
+ @dsl_user_op
47
+ def epilogue_sr_seed(
48
+ base_seed: Int32,
49
+ tile_coord_mnkl: cute.Coord,
50
+ subtile_idx,
51
+ *,
52
+ loc=None,
53
+ ip=None,
54
+ ) -> Int32:
55
+ return base_seed + (
56
+ tile_coord_mnkl[0] * EPILOGUE_SR_SEED_M_STRIDE
57
+ + tile_coord_mnkl[1] * EPILOGUE_SR_SEED_N_STRIDE
58
+ + tile_coord_mnkl[3] * EPILOGUE_SR_SEED_BATCH_STRIDE
59
+ + subtile_idx * EPILOGUE_SR_SEED_SUBTILE_STRIDE
60
+ )
61
+
62
+
63
+ @dsl_user_op
64
+ def epilogue_aux_out_sr_seed(
65
+ base_seed: Int32,
66
+ tile_coord_mnkl: cute.Coord,
67
+ subtile_idx,
68
+ *,
69
+ loc=None,
70
+ ip=None,
71
+ ) -> Int32:
72
+ return epilogue_sr_seed(
73
+ base_seed + EPILOGUE_SR_SEED_AUX_OUT_SALT,
74
+ tile_coord_mnkl,
75
+ subtile_idx,
76
+ loc=loc,
77
+ ip=ip,
78
+ )
79
+
80
+
81
+ @dsl_user_op
82
+ def mul_wide_u32(a: Uint32, b: Uint32, *, loc=None, ip=None) -> tuple:
83
+ """Unsigned 32b x 32b -> 64 wide multiply via PTX `mul.wide.u32`.
84
+
85
+ Returns (hi, lo) as a pair of Uint32 values.
86
+ """
87
+ prod = cute.arch.mul_wide(Uint32(a), Uint32(b), loc=loc, ip=ip)
88
+ # ptxas folds the shift and to(Uint32) into the same IMAD.WIDE.U32 register pair
89
+ # that the previous inline PTX mov.b64 split produced.
90
+ hi = (prod >> 32).to(Uint32, loc=loc, ip=ip)
91
+ lo = prod.to(Uint32, loc=loc, ip=ip)
92
+ return hi, lo
93
+
94
+
95
+ @dsl_user_op
96
+ def cvt_f32x2_bf16x2_rs(
97
+ a: Float32,
98
+ b: Float32,
99
+ rand_bits: Uint32,
100
+ *,
101
+ loc=None,
102
+ ip=None,
103
+ ) -> cutlass.Int32:
104
+ """Convert 2 FP32 values to packed BF16x2 using stochastic rounding.
105
+
106
+ Uses Blackwell PTX instruction: cvt.rs.satfinite.bf16x2.f32 dst, src_hi, src_lo, rand
107
+ """
108
+ return cutlass.Int32(
109
+ llvm.inline_asm(
110
+ T.i32(),
111
+ [
112
+ Float32(a).ir_value(loc=loc, ip=ip),
113
+ Float32(b).ir_value(loc=loc, ip=ip),
114
+ Uint32(rand_bits).ir_value(loc=loc, ip=ip),
115
+ ],
116
+ "cvt.rs.satfinite.bf16x2.f32 $0, $2, $1, $3;",
117
+ "=r,f,f,r",
118
+ has_side_effects=False,
119
+ is_align_stack=False,
120
+ )
121
+ )
122
+
123
+
124
+ @dsl_user_op
125
+ def philox(
126
+ counter: Uint32,
127
+ key: Uint32,
128
+ n_rounds: int = PHILOX_N_ROUNDS_DEFAULT,
129
+ *,
130
+ loc=None,
131
+ ip=None,
132
+ ) -> tuple:
133
+ """Philox 4x32b counter-based random number generator.
134
+
135
+ Given a 32b counter and a 32b key, returns four pseudo-random uint32 words
136
+ produced by running n_rounds of the Philox 4x32 bijection. Each round
137
+ performs two wide 32x32->64 multiplies with the Philox constants.
138
+ """
139
+ c0 = Uint32(counter)
140
+ c1 = Uint32(0)
141
+ c2 = Uint32(0)
142
+ c3 = Uint32(0)
143
+ k0 = Uint32(key)
144
+ k1 = Uint32(0)
145
+
146
+ round_a = Uint32(PHILOX_ROUND_A)
147
+ round_b = Uint32(PHILOX_ROUND_B)
148
+ key_a = Uint32(PHILOX_KEY_A)
149
+ key_b = Uint32(PHILOX_KEY_B)
150
+
151
+ for _ in range(n_rounds):
152
+ hi_b, lo_b = mul_wide_u32(c2, round_b, loc=loc, ip=ip)
153
+ hi_a, lo_a = mul_wide_u32(c0, round_a, loc=loc, ip=ip)
154
+ c0 = hi_b ^ c1 ^ k0
155
+ c2 = hi_a ^ c3 ^ k1
156
+ c1 = lo_b
157
+ c3 = lo_a
158
+ k0 = k0 + key_a
159
+ k1 = k1 + key_b
160
+
161
+ return c0, c1, c2, c3
162
+
163
+
164
+ @dsl_user_op
165
+ def convert_f32_to_bf16_sr(
166
+ src_vec,
167
+ seed: Int32,
168
+ tid: Int32,
169
+ *,
170
+ loc=None,
171
+ ip=None,
172
+ ):
173
+ """Convert an MLIR FP32 vector to BF16 with stochastic rounding.
174
+
175
+ Processes elements in pairs using Philox PRNG for entropy and the hardware
176
+ cvt.rs.satfinite.bf16x2.f32 instruction.
177
+ """
178
+ src_vec_type = ir.VectorType(src_vec.type)
179
+ num_elems = src_vec_type.shape[0]
180
+ assert num_elems % 2 == 0, f"requires even number of elements, got {num_elems}"
181
+ num_pairs = num_elems // 2
182
+ assert num_pairs % 4 == 0, (
183
+ f"num_pairs must be divisible by 4 for stochastic rounding, got {num_pairs}"
184
+ )
185
+
186
+ dst_mlir_type = cutlass.BFloat16.mlir_type
187
+ dst_vec_type = ir.VectorType.get([num_elems], dst_mlir_type, loc=loc)
188
+
189
+ i32_vec_type = ir.VectorType.get([num_pairs], Int32.mlir_type, loc=loc)
190
+ i32_vec = llvm.mlir_undef(i32_vec_type, loc=loc, ip=ip)
191
+
192
+ for pair_idx in range(num_pairs):
193
+ lo_idx = pair_idx * 2
194
+ hi_idx = pair_idx * 2 + 1
195
+
196
+ src_lo = vector.extract(
197
+ src_vec,
198
+ dynamic_position=[],
199
+ static_position=[lo_idx],
200
+ loc=loc,
201
+ ip=ip,
202
+ )
203
+ src_hi = vector.extract(
204
+ src_vec,
205
+ dynamic_position=[],
206
+ static_position=[hi_idx],
207
+ loc=loc,
208
+ ip=ip,
209
+ )
210
+
211
+ group_idx = pair_idx // 4
212
+ intra_idx = pair_idx % 4
213
+ if intra_idx == 0:
214
+ counter = cutlass.Uint32(group_idx << 16) | cutlass.Uint32(tid)
215
+ rand_batch = philox(counter, cutlass.Uint32(seed))
216
+
217
+ entropy = rand_batch[intra_idx]
218
+ packed_i32 = cvt_f32x2_bf16x2_rs(Float32(src_lo), Float32(src_hi), entropy, loc=loc, ip=ip)
219
+
220
+ packed_i32_val = cutlass.Int32(packed_i32).ir_value(loc=loc, ip=ip)
221
+ i32_vec = vector.insert(
222
+ packed_i32_val,
223
+ i32_vec,
224
+ dynamic_position=[],
225
+ static_position=[pair_idx],
226
+ loc=loc,
227
+ ip=ip,
228
+ )
229
+
230
+ dst_vec = llvm.bitcast(dst_vec_type, i32_vec, loc=loc, ip=ip)
231
+ return dst_vec
build/torch-cuda/quack/sm90_utils.py CHANGED
@@ -1,6 +1,6 @@
1
  # Copyright (c) 2025, Tri Dao.
2
 
3
- from typing import Type, Union, Optional
4
 
5
  import cutlass
6
  import cutlass.cute as cute
@@ -42,6 +42,38 @@ def make_smem_layout(
42
  make_smem_layout_epi = make_smem_layout
43
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  @dsl_user_op
46
  def partition_for_epilogue(
47
  cT: cute.Tensor,
 
1
  # Copyright (c) 2025, Tri Dao.
2
 
3
+ from typing import Literal, Type, Union, Optional
4
 
5
  import cutlass
6
  import cutlass.cute as cute
 
42
  make_smem_layout_epi = make_smem_layout
43
 
44
 
45
+ def make_tiled_mma(
46
+ a_dtype: Type[Numeric],
47
+ a_major: Literal["K", "MN"],
48
+ b_major: Literal["K", "MN"],
49
+ tiler_n: int,
50
+ source: Literal["SS", "RS"] = "SS",
51
+ atom_layout_mnk: tuple = (1, 1, 1),
52
+ swap_AB: bool = False,
53
+ b_dtype: Optional[Type[Numeric]] = None,
54
+ acc_dtype: Type[Numeric] = Float32,
55
+ ) -> cute.TiledMma:
56
+ """`b_dtype` defaults to `a_dtype`; pass it for mixed-precision MMAs (e.g. fp8).
57
+ `acc_dtype` defaults to Float32."""
58
+ if b_dtype is None:
59
+ b_dtype = a_dtype
60
+ mode = {"K": cute.nvgpu.OperandMajorMode.K, "MN": cute.nvgpu.OperandMajorMode.MN}
61
+ a_mode, b_mode = mode[a_major], mode[b_major]
62
+ if swap_AB:
63
+ a_mode, b_mode = b_mode, a_mode
64
+ a_source = warpgroup.OperandSource.RMEM if source == "RS" else warpgroup.OperandSource.SMEM
65
+ return sm90_utils_og.make_trivial_tiled_mma(
66
+ a_dtype,
67
+ b_dtype,
68
+ a_mode,
69
+ b_mode,
70
+ acc_dtype,
71
+ atom_layout_mnk=atom_layout_mnk,
72
+ tiler_mn=(64, tiler_n),
73
+ a_source=a_source,
74
+ )
75
+
76
+
77
  @dsl_user_op
78
  def partition_for_epilogue(
79
  cT: cute.Tensor,
build/torch-cuda/quack/utils.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+
3
+ import math
4
+ from typing import Optional, Tuple
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+
9
+ from cutlass import Float32, Int32, const_expr
10
+ from cutlass._mlir.dialects import llvm, vector
11
+ from cutlass.cutlass_dsl import T, dsl_user_op
12
+
13
+
14
+ @dsl_user_op
15
+ def elem_pointer(x: cute.Tensor, coord: cute.Coord, *, loc=None, ip=None) -> cute.Pointer:
16
+ return x.iterator + cute.crd2idx(coord, x.layout, loc=loc, ip=ip)
17
+
18
+
19
+ @cute.jit
20
+ def load_scalar_or_pointer(x, dtype=Float32):
21
+ if const_expr(isinstance(x, cute.Pointer)):
22
+ return dtype(cute.make_tensor(x, cute.make_layout(1))[0])
23
+ else:
24
+ return x
25
+
26
+
27
+ @dsl_user_op
28
+ def set_block_rank(
29
+ smem_ptr: cute.Pointer, peer_cta_rank_in_cluster: Int32, *, loc=None, ip=None
30
+ ) -> Int32:
31
+ """Map the given smem pointer to the address at another CTA rank in the cluster."""
32
+ dsmem_ptr = cute.arch.map_dsmem_ptr(smem_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip)
33
+ return Int32(dsmem_ptr.toint(loc=loc, ip=ip))
34
+
35
+
36
+ @dsl_user_op
37
+ def store_shared_remote(
38
+ val: float | Float32 | Int32 | cutlass.Int64,
39
+ smem_ptr: cute.Pointer,
40
+ mbar_ptr: cute.Pointer,
41
+ peer_cta_rank_in_cluster: cute.typing.Int,
42
+ *,
43
+ loc=None,
44
+ ip=None,
45
+ ) -> None:
46
+ remote_smem_ptr_i32 = set_block_rank(smem_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip)
47
+ remote_mbar_ptr_i32 = set_block_rank(mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip)
48
+ if const_expr(isinstance(val, float)):
49
+ val = Float32(val)
50
+ assert isinstance(val, (Float32, Int32, cutlass.Int64)), "val must be Float32, Int32, or Int64"
51
+ suffix = {Float32: "f32", Int32: "s32", cutlass.Int64: "s64"}[type(val)]
52
+ cute.arch.inline_ptx(
53
+ f"st.async.shared::cluster.mbarrier::complete_tx::bytes.{suffix} "
54
+ "[{$r0}], {$r1}, [{$r2}];",
55
+ read_only_args=[remote_smem_ptr_i32, val, remote_mbar_ptr_i32],
56
+ loc=loc,
57
+ ip=ip,
58
+ )
59
+
60
+
61
+ @dsl_user_op
62
+ def store_shared_remote_x4(
63
+ val0: Float32 | Int32,
64
+ val1: Float32 | Int32,
65
+ val2: Float32 | Int32,
66
+ val3: Float32 | Int32,
67
+ smem_ptr: cute.Pointer,
68
+ mbar_ptr: cute.Pointer,
69
+ peer_cta_rank_in_cluster: cute.typing.Int,
70
+ *,
71
+ loc=None,
72
+ ip=None,
73
+ ) -> None:
74
+ remote_smem_ptr_i32 = set_block_rank(smem_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip)
75
+ remote_mbar_ptr_i32 = set_block_rank(mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip)
76
+ assert isinstance(val0, (Float32, Int32)), "val must be Float32, or Int32"
77
+ dtype = Float32 if isinstance(val0, Float32) else Int32
78
+ suffix = {Float32: "f32", Int32: "s32"}[dtype]
79
+ cute.arch.inline_ptx(
80
+ "{\n\t"
81
+ f".reg .v4 .{suffix} abcd;\n\t"
82
+ f"mov.{suffix} abcd.x, {{$r2}};\n\t"
83
+ f"mov.{suffix} abcd.y, {{$r3}};\n\t"
84
+ f"mov.{suffix} abcd.z, {{$r4}};\n\t"
85
+ f"mov.{suffix} abcd.w, {{$r5}};\n\t"
86
+ f"st.async.shared::cluster.mbarrier::complete_tx::bytes.v4.{suffix} "
87
+ "[{$r0}], abcd, [{$r1}];\n\t"
88
+ "}\n",
89
+ read_only_args=[
90
+ remote_smem_ptr_i32,
91
+ remote_mbar_ptr_i32,
92
+ dtype(val0),
93
+ dtype(val1),
94
+ dtype(val2),
95
+ dtype(val3),
96
+ ],
97
+ loc=loc,
98
+ ip=ip,
99
+ )
100
+
101
+
102
+ @cute.jit
103
+ def fill_oob(tXsX: cute.Tensor, tXpX: Optional[cute.Tensor], fill_value: cute.Numeric) -> None:
104
+ """Fill out-of-bounds values in shared memory tensor.
105
+
106
+ Args:
107
+ tXsX: Shared memory tensor to fill
108
+ tXpX: Predicate tensor indicating valid elements
109
+ fill_value: Value to fill OOB locations with
110
+ """
111
+ tXrX_fill = cute.make_rmem_tensor_like(tXsX[(None, 0), None, 0])
112
+ tXrX_fill.fill(fill_value)
113
+ for rest_v in cutlass.range_constexpr(tXsX.shape[0][1]):
114
+ for rest_k in cutlass.range_constexpr(tXsX.shape[2]):
115
+ if const_expr(tXpX is not None):
116
+ if not tXpX[rest_v, 0, rest_k]:
117
+ cute.autovec_copy(tXrX_fill, tXsX[(None, rest_v), None, rest_k])
118
+ else:
119
+ cute.autovec_copy(tXrX_fill, tXsX[(None, rest_v), None, rest_k])
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # General-purpose DSL store / vector helpers
124
+ # ---------------------------------------------------------------------------
125
+
126
+
127
+ @dsl_user_op
128
+ def make_vector(elem_type, *values, loc=None, ip=None):
129
+ """Build an MLIR vector <N x elem_type> from N scalar DSL values.
130
+
131
+ Example: make_vector(cutlass.Uint32, v0, v1) -> <2 x i32> MLIR vector
132
+ """
133
+ from cutlass._mlir import ir
134
+
135
+ n = len(values)
136
+ mlir_ty = elem_type.mlir_type
137
+ vec_ty = ir.VectorType.get([n], mlir_ty)
138
+ vec = llvm.mlir_undef(vec_ty, loc=loc, ip=ip)
139
+ for i, v in enumerate(values):
140
+ vec = vector.insert(
141
+ elem_type(v).ir_value(loc=loc, ip=ip),
142
+ vec,
143
+ dynamic_position=[],
144
+ static_position=[i],
145
+ loc=loc,
146
+ ip=ip,
147
+ )
148
+ return vec
149
+
150
+
151
+ @dsl_user_op
152
+ def f32x2_to_i64(a: Float32, b: Float32, *, loc=None, ip=None) -> cutlass.Int64:
153
+ vec_f32x2 = vector.from_elements(
154
+ T.vector(2, T.f32()), (a.ir_value(), b.ir_value()), loc=loc, ip=ip
155
+ )
156
+ vec_i64x1 = vector.bitcast(T.vector(1, T.i64()), vec_f32x2)
157
+ res = cutlass.Int64(
158
+ vector.extract(vec_i64x1, dynamic_position=[], static_position=[0], loc=loc, ip=ip)
159
+ )
160
+ return res
161
+
162
+
163
+ @dsl_user_op
164
+ def i64_to_f32x2(c: cutlass.Int64, *, loc=None, ip=None) -> Tuple[Float32, Float32]:
165
+ vec_i64x1 = vector.from_elements(T.vector(1, T.i64()), (c.ir_value(),), loc=loc, ip=ip)
166
+ vec_f32x2 = vector.bitcast(T.vector(2, T.f32()), vec_i64x1)
167
+ res0 = Float32(
168
+ vector.extract(vec_f32x2, dynamic_position=[], static_position=[0], loc=loc, ip=ip)
169
+ )
170
+ res1 = Float32(
171
+ vector.extract(vec_f32x2, dynamic_position=[], static_position=[1], loc=loc, ip=ip)
172
+ )
173
+ return res0, res1
174
+
175
+
176
+ @cute.jit
177
+ def warp_prefix_sum(val: Int32, lane: Optional[Int32] = None) -> Int32:
178
+ if const_expr(lane is None):
179
+ lane = cute.arch.lane_idx()
180
+ for i in cutlass.range_constexpr(int(math.log2(cute.arch.WARP_SIZE))):
181
+ offset = 1 << i
182
+ # Very important that we set mask_and_clamp to 0
183
+ partial_sum = cute.arch.shuffle_sync_up(val, offset=offset, mask_and_clamp=0)
184
+ if lane >= offset:
185
+ val += partial_sum
186
+ return val
187
+
188
+
189
+ @dsl_user_op
190
+ def domain_offset_aligned(
191
+ coord: cute.Coord, tensor: cute.Tensor, *, loc=None, ip=None
192
+ ) -> cute.Tensor:
193
+ assert isinstance(tensor.iterator, cute.Pointer)
194
+ # We assume that applying the offset does not change the pointer alignment
195
+ new_ptr = cute.make_ptr(
196
+ tensor.element_type,
197
+ elem_pointer(tensor, coord).toint(),
198
+ tensor.memspace,
199
+ assumed_align=tensor.iterator.alignment,
200
+ )
201
+ return cute.make_tensor(new_ptr, tensor.layout)