kernels-bot commited on
Commit
7151f25
·
verified ·
1 Parent(s): e24502c

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. build/torch-cuda/_ops.py +8 -2
  2. build/torch-cuda/_ops_compat.py +0 -10
  3. build/torch-cuda/functional/__init__.py +36 -37
  4. build/torch-cuda/functional/backward.py +1 -54
  5. build/torch-cuda/functional/forward.py +1 -50
  6. build/torch-cuda/functional/reduction_over_k_gather.py +6 -6
  7. build/torch-cuda/functional/tile_scheduler.py +0 -91
  8. build/torch-cuda/functional/triton_kernels/__init__.py +4 -1
  9. build/torch-cuda/metadata.json +117 -2
  10. build/torch-cuda/metadata.json.sigstore +1 -0
  11. build/torch-cuda/quack/__init__.py +12 -3
  12. build/torch-cuda/quack/_compile_worker.py +0 -102
  13. build/torch-cuda/quack/_ops_compat.py +9 -3
  14. build/torch-cuda/quack/activation.py +301 -72
  15. build/torch-cuda/quack/autotuner.py +176 -170
  16. build/torch-cuda/quack/bench/__init__.py +0 -0
  17. build/torch-cuda/quack/bench/bench_utils.py +202 -0
  18. build/torch-cuda/quack/blockscaled/__init__.py +31 -0
  19. build/torch-cuda/quack/{mx_utils.py → blockscaled/quantize.py} +0 -0
  20. build/torch-cuda/quack/{blockscaled_gemm_utils.py → blockscaled/utils.py} +185 -79
  21. build/torch-cuda/quack/broadcast_utils.py +3 -4
  22. build/torch-cuda/quack/cache/__init__.py +74 -0
  23. build/torch-cuda/quack/cache/_pool_preload.py +50 -0
  24. build/torch-cuda/quack/cache/async_compile.py +413 -0
  25. build/torch-cuda/quack/{cache_utils.py → cache/jit.py} +194 -57
  26. build/torch-cuda/quack/compile_utils.py +17 -2
  27. build/torch-cuda/quack/complex.py +292 -0
  28. build/torch-cuda/quack/copy_utils.py +613 -87
  29. build/torch-cuda/quack/cross_entropy.py +191 -141
  30. build/torch-cuda/quack/cute_dsl_utils.py +63 -3
  31. build/torch-cuda/quack/dsl/__init__.py +17 -0
  32. build/torch-cuda/quack/{cute_dsl_ptxas.py → dsl/cute_dsl_ptxas.py} +58 -14
  33. build/torch-cuda/quack/dsl/cute_tensor.py +167 -0
  34. build/torch-cuda/quack/dsl/cute_tensor_indexing.py +139 -0
  35. build/torch-cuda/quack/dsl/smem_struct.py +99 -0
  36. build/torch-cuda/quack/dsl/torch_library_op.py +65 -0
  37. build/torch-cuda/quack/epi_composable.py +161 -54
  38. build/torch-cuda/quack/epi_ops.py +640 -187
  39. build/torch-cuda/quack/epi_utils.py +12 -7
  40. build/torch-cuda/quack/fast_math.py +67 -21
  41. build/torch-cuda/quack/gemm.py +72 -25
  42. build/torch-cuda/quack/gemm_act.py +223 -113
  43. build/torch-cuda/quack/gemm_base.py +731 -0
  44. build/torch-cuda/quack/gemm_blockscaled_interface.py +0 -326
  45. build/torch-cuda/quack/gemm_config.py +40 -3
  46. build/torch-cuda/quack/gemm_dact.py +71 -93
  47. build/torch-cuda/quack/gemm_default_epi.py +18 -26
  48. build/torch-cuda/quack/gemm_interface.py +595 -240
  49. build/torch-cuda/quack/gemm_norm_act.py +46 -40
  50. build/torch-cuda/quack/gemm_sm100.py +411 -359
build/torch-cuda/_ops.py CHANGED
@@ -4,7 +4,13 @@ def get_backend() -> str:
4
  """Detect the backend by inspecting torch."""
5
  import torch
6
 
7
- if hasattr(torch, "neuron"):
 
 
 
 
 
 
8
  # Needs to be sorted before specific Torch builds, since Neuron
9
  # extension can be loaded into e.g. CUDA Torch builds.
10
  return "neuron"
@@ -22,7 +28,7 @@ def get_backend() -> str:
22
 
23
  def _find_ops_name() -> str:
24
  kernel_name = "sonic_moe"
25
- unique_id = "86f75d9"
26
  backend = get_backend()
27
  return f"_{kernel_name}_{backend}_{unique_id}"
28
 
 
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"
 
28
 
29
  def _find_ops_name() -> str:
30
  kernel_name = "sonic_moe"
31
+ unique_id = "83d1d6e"
32
  backend = get_backend()
33
  return f"_{kernel_name}_{backend}_{unique_id}"
34
 
build/torch-cuda/_ops_compat.py DELETED
@@ -1,10 +0,0 @@
1
- """Compatibility helpers for op namespacing in source and built layouts."""
2
-
3
- try:
4
- from ._ops import add_op_namespace_prefix as _generated_add_op_namespace_prefix
5
- except ImportError:
6
- def _generated_add_op_namespace_prefix(name: str) -> str:
7
- return name if "::" in name else f"sonicmoe::{name}"
8
-
9
- def add_op_namespace_prefix(name: str) -> str:
10
- return _generated_add_op_namespace_prefix(name)
 
 
 
 
 
 
 
 
 
 
 
build/torch-cuda/functional/__init__.py CHANGED
@@ -11,13 +11,11 @@ from ..quack.gemm_interface import gemm, gemm_dgated, gemm_gated
11
  from ..enums import ActivationType, is_glu
12
  from .backward import (
13
  _down_projection_backward_act,
14
- _down_projection_backward_weight,
15
  _token_broadcast_backward,
16
  _topk_softmax_bwd,
17
  _up_projection_backward_act,
18
- _up_projection_backward_weight,
19
  )
20
- from .forward import _down_projection_forward, _router_forward, _topk_softmax_fwd, _up_projection_forward
21
  from .triton_kernels import TC_topk_router_metadata_triton, general_routing_router_metadata_triton
22
 
23
 
@@ -107,17 +105,21 @@ class _UpProjection(torch.autograd.Function):
107
  else None
108
  )
109
 
110
- _up_projection_forward(
111
- x=x,
112
- w1=w1,
113
- h=h,
114
- a=a,
115
- b1=b1,
116
- expert_frequency_offset=expert_frequency_offset,
117
- x_gather_idx=x_gather_idx,
118
- activation_type=activation_type.value,
119
- is_inference_mode_enabled=is_inference_mode_enabled,
120
- concat_layout=concat_layout,
 
 
 
 
121
  )
122
 
123
  ctx.T = T
@@ -182,14 +184,15 @@ class _UpProjection(torch.autograd.Function):
182
  concat_layout=concat_layout,
183
  )
184
 
185
- _up_projection_backward_weight(
186
- x=x,
187
- dw1=dw1,
188
- dh=dh,
189
- expert_frequency_offset=expert_frequency_offset,
190
- x_gather_idx=x_gather_idx,
191
- is_glu_activation=is_glu_activation,
192
- concat_layout=concat_layout,
 
193
  )
194
 
195
  dx_reduced = torch.empty(T, H, dtype=dh.dtype, device=dh.device)
@@ -231,13 +234,7 @@ class _DownProjection(torch.autograd.Function):
231
 
232
  y = torch.empty(TK, H, dtype=a.dtype, device=a.device)
233
 
234
- _down_projection_forward(
235
- w2=w2,
236
- a=a,
237
- y=y,
238
- b2=b2,
239
- expert_frequency_offset=expert_frequency_offset,
240
- )
241
 
242
  o = torch.empty(T, H, device=a.device, dtype=a.dtype)
243
  topk_scores = topk_scores.view(-1)
@@ -266,6 +263,7 @@ class _DownProjection(torch.autograd.Function):
266
  expert_frequency_offset,
267
  x_gather_idx,
268
  s_scatter_idx,
 
269
  )
270
 
271
  return o
@@ -285,6 +283,7 @@ class _DownProjection(torch.autograd.Function):
285
  expert_frequency_offset,
286
  x_gather_idx,
287
  s_scatter_idx,
 
288
  ) = ctx.saved_tensors
289
 
290
  dw2 = torch.empty_like(w2)
@@ -313,12 +312,14 @@ class _DownProjection(torch.autograd.Function):
313
  activation_type=activation_type.value,
314
  )
315
 
316
- _down_projection_backward_weight(
317
- dout=dout,
318
- a_prime=a_prime,
319
- dw2=dw2,
320
- expert_frequency_offset=expert_frequency_offset,
321
- x_gather_idx=x_gather_idx,
 
 
322
  )
323
 
324
  # TC top-K routing
@@ -369,7 +370,6 @@ def moe_TC_softmax_topk_layer(
369
  if type(activation_type) == str:
370
  activation_type = ActivationType(activation_type)
371
 
372
- assert not torch.compiler.is_compiling()
373
  assert is_glu(activation_type), "QuACK GEMM does not support non GLU activation yet"
374
 
375
  a, h = _UpProjection.apply(
@@ -467,7 +467,6 @@ def moe_general_routing_inputs(
467
  num_activated_expert_per_token_offset,
468
  )
469
 
470
- assert not torch.compiler.is_compiling()
471
  assert is_glu(activation_type), "QuACK GEMM does not support non GLU activation yet"
472
 
473
  a, h = _UpProjection.apply(
 
11
  from ..enums import ActivationType, is_glu
12
  from .backward import (
13
  _down_projection_backward_act,
 
14
  _token_broadcast_backward,
15
  _topk_softmax_bwd,
16
  _up_projection_backward_act,
 
17
  )
18
+ from .forward import _router_forward, _topk_softmax_fwd
19
  from .triton_kernels import TC_topk_router_metadata_triton, general_routing_router_metadata_triton
20
 
21
 
 
105
  else None
106
  )
107
 
108
+ assert activation_type.value in (
109
+ "swiglu",
110
+ "geglu",
111
+ ), f"QuACK gemm_gated only supports glu activations, got {activation_type.value}"
112
+ gemm_gated(
113
+ x,
114
+ w1.permute(2, 1, 0),
115
+ activation=activation_type.value,
116
+ cu_seqlens_m=expert_frequency_offset,
117
+ A_idx=x_gather_idx,
118
+ preact_out=h,
119
+ postact_out=a,
120
+ store_preact=(not is_inference_mode_enabled),
121
+ bias=b1,
122
+ concat_layout=(("B", "bias") if b1 is not None else ("B",)) if concat_layout else None,
123
  )
124
 
125
  ctx.T = T
 
184
  concat_layout=concat_layout,
185
  )
186
 
187
+ gemm(
188
+ x.T,
189
+ dh,
190
+ out=dw1.permute(2, 1, 0),
191
+ cu_seqlens_k=expert_frequency_offset,
192
+ A_idx=x_gather_idx,
193
+ batch_idx_permute=None,
194
+ dynamic_scheduler=False,
195
+ concat_layout=(("out",) if concat_layout else None),
196
  )
197
 
198
  dx_reduced = torch.empty(T, H, dtype=dh.dtype, device=dh.device)
 
234
 
235
  y = torch.empty(TK, H, dtype=a.dtype, device=a.device)
236
 
237
+ gemm(a, w2.permute(2, 1, 0), out=y, cu_seqlens_m=expert_frequency_offset, bias=b2)
 
 
 
 
 
 
238
 
239
  o = torch.empty(T, H, device=a.device, dtype=a.dtype)
240
  topk_scores = topk_scores.view(-1)
 
263
  expert_frequency_offset,
264
  x_gather_idx,
265
  s_scatter_idx,
266
+ s_reverse_scatter_idx,
267
  )
268
 
269
  return o
 
283
  expert_frequency_offset,
284
  x_gather_idx,
285
  s_scatter_idx,
286
+ s_reverse_scatter_idx,
287
  ) = ctx.saved_tensors
288
 
289
  dw2 = torch.empty_like(w2)
 
312
  activation_type=activation_type.value,
313
  )
314
 
315
+ gemm(
316
+ dout.T,
317
+ a_prime,
318
+ out=dw2.permute(2, 0, 1),
319
+ cu_seqlens_k=expert_frequency_offset,
320
+ A_idx=x_gather_idx,
321
+ batch_idx_permute=None,
322
+ dynamic_scheduler=False,
323
  )
324
 
325
  # TC top-K routing
 
370
  if type(activation_type) == str:
371
  activation_type = ActivationType(activation_type)
372
 
 
373
  assert is_glu(activation_type), "QuACK GEMM does not support non GLU activation yet"
374
 
375
  a, h = _UpProjection.apply(
 
467
  num_activated_expert_per_token_offset,
468
  )
469
 
 
470
  assert is_glu(activation_type), "QuACK GEMM does not support non GLU activation yet"
471
 
472
  a, h = _UpProjection.apply(
build/torch-cuda/functional/backward.py CHANGED
@@ -11,7 +11,7 @@ import triton
11
  import triton.language as tl
12
  from ..quack.gemm_interface import gemm, gemm_dgated
13
 
14
- from .._ops_compat import add_op_namespace_prefix
15
  from ..utils import get_powers_of_2
16
  from .reduction_over_k_gather import token_gather_and_sum_varlen_K_triton
17
 
@@ -208,35 +208,6 @@ def _up_projection_backward_act(
208
  _up_projection_backward_act.compile_cache = {}
209
 
210
 
211
- @torch.library.custom_op(add_op_namespace_prefix("_up_projection_backward_weight"), mutates_args={"dw1"})
212
- def _up_projection_backward_weight(
213
- x: torch.Tensor,
214
- dw1: torch.Tensor,
215
- dh: torch.Tensor,
216
- expert_frequency_offset: torch.Tensor,
217
- x_gather_idx: torch.Tensor,
218
- is_glu_activation: bool,
219
- concat_layout: bool = False,
220
- ) -> None:
221
- I, H, E = dw1.size()
222
- if is_glu_activation:
223
- I //= 2
224
-
225
- gemm(
226
- x.T,
227
- dh,
228
- out=dw1.permute(2, 1, 0),
229
- cu_seqlens_k=expert_frequency_offset,
230
- A_idx=x_gather_idx,
231
- batch_idx_permute=None,
232
- dynamic_scheduler=False,
233
- concat_layout=(("out",) if concat_layout else None),
234
- )
235
-
236
-
237
- _up_projection_backward_weight.compile_cache = {}
238
-
239
-
240
  @torch.library.custom_op(add_op_namespace_prefix("_down_projection_backward_act"), mutates_args={"dh", "ds", "db2", "a_prime"})
241
  def _down_projection_backward_act(
242
  dout: torch.Tensor,
@@ -272,8 +243,6 @@ def _down_projection_backward_act(
272
  A_idx=x_gather_idx,
273
  dynamic_scheduler=False,
274
  )
275
- ds[s_scatter_idx] = ds_scattered
276
-
277
  if db2 is None:
278
  ds[s_scatter_idx] = ds_scattered
279
  else:
@@ -314,28 +283,6 @@ def _down_projection_backward_act(
314
  _down_projection_backward_act.compile_cache = {}
315
 
316
 
317
- @torch.library.custom_op(add_op_namespace_prefix("_down_projection_backward_weight"), mutates_args={"dw2"})
318
- def _down_projection_backward_weight(
319
- dout: torch.Tensor,
320
- a_prime: torch.Tensor,
321
- dw2: torch.Tensor,
322
- expert_frequency_offset: torch.Tensor,
323
- x_gather_idx: torch.Tensor,
324
- ) -> None:
325
- gemm(
326
- dout.T,
327
- a_prime,
328
- out=dw2.permute(2, 0, 1),
329
- cu_seqlens_k=expert_frequency_offset,
330
- A_idx=x_gather_idx,
331
- batch_idx_permute=None,
332
- dynamic_scheduler=False,
333
- )
334
-
335
-
336
- _down_projection_backward_weight.compile_cache = {}
337
-
338
-
339
  @torch.library.custom_op(add_op_namespace_prefix("_token_broadcast_backward"), mutates_args={"dx_reduced"})
340
  def _token_broadcast_backward(
341
  dx_reduced: torch.Tensor,
 
11
  import triton.language as tl
12
  from ..quack.gemm_interface import gemm, gemm_dgated
13
 
14
+ from .._ops import add_op_namespace_prefix
15
  from ..utils import get_powers_of_2
16
  from .reduction_over_k_gather import token_gather_and_sum_varlen_K_triton
17
 
 
208
  _up_projection_backward_act.compile_cache = {}
209
 
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  @torch.library.custom_op(add_op_namespace_prefix("_down_projection_backward_act"), mutates_args={"dh", "ds", "db2", "a_prime"})
212
  def _down_projection_backward_act(
213
  dout: torch.Tensor,
 
243
  A_idx=x_gather_idx,
244
  dynamic_scheduler=False,
245
  )
 
 
246
  if db2 is None:
247
  ds[s_scatter_idx] = ds_scattered
248
  else:
 
283
  _down_projection_backward_act.compile_cache = {}
284
 
285
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  @torch.library.custom_op(add_op_namespace_prefix("_token_broadcast_backward"), mutates_args={"dx_reduced"})
287
  def _token_broadcast_backward(
288
  dx_reduced: torch.Tensor,
build/torch-cuda/functional/forward.py CHANGED
@@ -9,9 +9,8 @@ import triton
9
  import triton.language as tl
10
  from cutlass.cute.runtime import from_dlpack
11
  from ..quack.cute_dsl_utils import torch2cute_dtype_map
12
- from ..quack.gemm_interface import gemm, gemm_gated
13
 
14
- from .._ops_compat import add_op_namespace_prefix
15
  from .reduction_over_k_gather import token_gather_and_sum_varlen_K_triton
16
  from .topk import Softmax_Over_TopK, TopK_Over_Softmax
17
 
@@ -62,54 +61,6 @@ def _topk_fwd(
62
  _topk_fwd.compile_cache = {}
63
 
64
 
65
- @torch.library.custom_op(add_op_namespace_prefix("_up_projection_forward"), mutates_args={"h", "a"})
66
- def _up_projection_forward(
67
- x: torch.Tensor,
68
- w1: torch.Tensor,
69
- h: torch.Tensor,
70
- a: torch.Tensor,
71
- b1: torch.Tensor | None,
72
- expert_frequency_offset: torch.Tensor,
73
- x_gather_idx: torch.Tensor,
74
- activation_type: str,
75
- is_inference_mode_enabled: bool = False,
76
- concat_layout: bool = False,
77
- ) -> None:
78
- assert activation_type in (
79
- "swiglu",
80
- "geglu",
81
- ), f"QuACK gemm_gated only supports glu activations, got {activation_type}"
82
- gemm_gated(
83
- x,
84
- w1.permute(2, 1, 0),
85
- activation=activation_type,
86
- cu_seqlens_m=expert_frequency_offset,
87
- A_idx=x_gather_idx,
88
- preact_out=h,
89
- postact_out=a,
90
- store_preact=(not is_inference_mode_enabled),
91
- bias=b1,
92
- concat_layout=(("B", "bias") if b1 is not None else ("B",)) if concat_layout else None,
93
- )
94
-
95
-
96
- _up_projection_forward.compile_cache = {}
97
-
98
-
99
- @torch.library.custom_op(add_op_namespace_prefix("_down_projection_forward"), mutates_args={"y"})
100
- def _down_projection_forward(
101
- w2: torch.Tensor,
102
- a: torch.Tensor,
103
- y: torch.Tensor,
104
- b2: torch.Tensor | None,
105
- expert_frequency_offset: torch.Tensor,
106
- ) -> None:
107
- gemm(a, w2.permute(2, 1, 0), out=y, cu_seqlens_m=expert_frequency_offset, bias=b2)
108
-
109
-
110
- _down_projection_forward.compile_cache = {}
111
-
112
-
113
  @torch.library.custom_op(add_op_namespace_prefix("_router_forward"), mutates_args={"o"})
114
  def _router_forward(
115
  y: torch.Tensor,
 
9
  import triton.language as tl
10
  from cutlass.cute.runtime import from_dlpack
11
  from ..quack.cute_dsl_utils import torch2cute_dtype_map
 
12
 
13
+ from .._ops import add_op_namespace_prefix
14
  from .reduction_over_k_gather import token_gather_and_sum_varlen_K_triton
15
  from .topk import Softmax_Over_TopK, TopK_Over_Softmax
16
 
 
61
  _topk_fwd.compile_cache = {}
62
 
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  @torch.library.custom_op(add_op_namespace_prefix("_router_forward"), mutates_args={"o"})
65
  def _router_forward(
66
  y: torch.Tensor,
build/torch-cuda/functional/reduction_over_k_gather.py CHANGED
@@ -71,12 +71,12 @@ def token_gather_sum_kernel(
71
  ):
72
  # 1D tiling over T only
73
  pid_t = tl.program_id(axis=0)
74
- t_idx = pid_t.to(tl.uint32)
75
 
76
  # Load segment starts and ends for this token
77
  if is_varlen_K:
78
- Ms = tl.load(M_offset_ptr + t_idx).to(tl.uint32)
79
- Me = tl.load(M_offset_ptr + t_idx + 1).to(tl.uint32)
80
  K_this_token = Me - Ms # actual K for this token
81
  else:
82
  Ms = MAX_K * t_idx
@@ -84,7 +84,7 @@ def token_gather_sum_kernel(
84
 
85
  # Outer loop over H tiles
86
  for h_tile in tl.static_range(triton.cdiv(H, BLOCK_H)):
87
- h_idx = (h_tile * BLOCK_H + tl.arange(0, BLOCK_H)).to(tl.uint32) # [BLOCK_H]
88
  m_h = h_idx < H
89
 
90
  # Initialize accumulator for this H tile
@@ -94,7 +94,7 @@ def token_gather_sum_kernel(
94
  for k_tile in tl.range(tl.cdiv(K_this_token, BLOCK_K)):
95
  k_offset = k_tile * BLOCK_K
96
 
97
- k_idx = (k_offset + tl.arange(0, BLOCK_K)).to(tl.uint32) # [BLOCK_K]
98
 
99
  # Mask for valid K indices
100
  m_k = k_idx < K_this_token # [BLOCK_K]
@@ -103,7 +103,7 @@ def token_gather_sum_kernel(
103
  m_abs = Ms + k_idx # [BLOCK_K]
104
 
105
  # Gather permuted indices
106
- perm_idx = tl.load(M_perm_ptr + m_abs, mask=m_k, other=0).to(tl.uint32) # [BLOCK_K]
107
 
108
  # Load x values: [BLOCK_K, BLOCK_H]
109
  x_ptrs = x_ptr + perm_idx[:, None] * stride_xM + h_idx[None, :] * stride_xH
 
71
  ):
72
  # 1D tiling over T only
73
  pid_t = tl.program_id(axis=0)
74
+ t_idx = pid_t.to(tl.int64)
75
 
76
  # Load segment starts and ends for this token
77
  if is_varlen_K:
78
+ Ms = tl.load(M_offset_ptr + t_idx).to(tl.int64)
79
+ Me = tl.load(M_offset_ptr + t_idx + 1).to(tl.int64)
80
  K_this_token = Me - Ms # actual K for this token
81
  else:
82
  Ms = MAX_K * t_idx
 
84
 
85
  # Outer loop over H tiles
86
  for h_tile in tl.static_range(triton.cdiv(H, BLOCK_H)):
87
+ h_idx = (h_tile * BLOCK_H + tl.arange(0, BLOCK_H)).to(tl.int64) # [BLOCK_H]
88
  m_h = h_idx < H
89
 
90
  # Initialize accumulator for this H tile
 
94
  for k_tile in tl.range(tl.cdiv(K_this_token, BLOCK_K)):
95
  k_offset = k_tile * BLOCK_K
96
 
97
+ k_idx = (k_offset + tl.arange(0, BLOCK_K)).to(tl.int64) # [BLOCK_K]
98
 
99
  # Mask for valid K indices
100
  m_k = k_idx < K_this_token # [BLOCK_K]
 
103
  m_abs = Ms + k_idx # [BLOCK_K]
104
 
105
  # Gather permuted indices
106
+ perm_idx = tl.load(M_perm_ptr + m_abs, mask=m_k, other=0).to(tl.int64) # [BLOCK_K]
107
 
108
  # Load x values: [BLOCK_K, BLOCK_H]
109
  x_ptrs = x_ptr + perm_idx[:, None] * stride_xM + h_idx[None, :] * stride_xH
build/torch-cuda/functional/tile_scheduler.py DELETED
@@ -1,91 +0,0 @@
1
- # ********************************************************************************
2
- # Copyright (c) 2025, Wentao Guo, Mayank Mishra, Xinle Cheng, Ion Stoica, Tri Dao
3
- # ********************************************************************************
4
-
5
- from __future__ import annotations
6
-
7
- import cutlass
8
- import cutlass.cute as cute
9
- from cutlass import Boolean, Int32, const_expr
10
- from ..quack.pipeline import PipelineStateWAdvance
11
- from ..quack.tile_scheduler import TileScheduler, VarlenMTileScheduler
12
-
13
-
14
- class SonicMoETileScheduler(TileScheduler):
15
- @staticmethod
16
- @cute.jit
17
- def create(
18
- params: TileScheduler.Params,
19
- tile_count: cute.Tensor | None = None,
20
- scheduler_pipeline: cutlass.pipeline.PipelineAsync | None = None,
21
- is_scheduler_warp: bool | Boolean = False,
22
- *,
23
- loc=None,
24
- ip=None,
25
- ) -> SonicMoETileScheduler:
26
- """is_scheduler_warp should only be true for one warp in the whole cluster"""
27
- stages = 0
28
- if const_expr(not params.is_persistent):
29
- cidx, cidy, _ = cute.arch.cluster_idx()
30
- cdimx, _, _ = cute.arch.cluster_dim()
31
- cluster_id = cidx + cidy * cdimx
32
- current_work_linear_idx = Int32(cluster_id)
33
- else:
34
- _, _, bidz = cute.arch.block_idx()
35
- current_work_linear_idx = Int32(bidz)
36
- if const_expr(params.tile_count_semaphore is not None):
37
- assert tile_count is not None
38
- assert scheduler_pipeline is not None
39
- stages = const_expr(cute.size(tile_count))
40
- return SonicMoETileScheduler(
41
- current_work_linear_idx,
42
- Int32(0), # num_tiles_executed
43
- tile_count,
44
- scheduler_pipeline,
45
- PipelineStateWAdvance(stages, Int32(0), Int32(0), Int32(1 if is_scheduler_warp else 0)),
46
- params,
47
- loc=loc,
48
- ip=ip,
49
- )
50
-
51
- def prefetch_next_work(self, *, advance_count: int = 1, loc=None, ip=None):
52
- old_current_work_linear_idx = self._current_work_linear_idx
53
- if const_expr(self.params.is_persistent):
54
- num_persistent_clusters = cute.arch.grid_dim()[2]
55
- self._current_work_linear_idx += advance_count * Int32(num_persistent_clusters)
56
- future_tile_coord_mnkl = self.get_current_work()
57
- self._current_work_linear_idx = old_current_work_linear_idx
58
- return future_tile_coord_mnkl
59
-
60
-
61
- class SonicMoEVarlenMTileScheduler(VarlenMTileScheduler, SonicMoETileScheduler):
62
- @staticmethod
63
- @cute.jit
64
- def create(
65
- params: VarlenMTileScheduler.Params,
66
- tile_count: cute.Tensor | None = None,
67
- scheduler_pipeline: cutlass.pipeline.PipelineAsync | None = None,
68
- is_scheduler_warp: bool | Boolean = False,
69
- *,
70
- loc=None,
71
- ip=None,
72
- ) -> SonicMoEVarlenMTileScheduler:
73
- stages = 0
74
- _, _, bidz = cute.arch.block_idx()
75
- current_work_linear_idx = Int32(bidz)
76
- if const_expr(params.tile_count_semaphore is not None):
77
- assert tile_count is not None
78
- assert scheduler_pipeline is not None
79
- stages = const_expr(cute.size(tile_count))
80
- return SonicMoEVarlenMTileScheduler(
81
- current_work_linear_idx,
82
- Int32(0), # num_tiles_executed
83
- Int32(0), # current_batch_idx
84
- Int32(0), # num_work_idx_before_cur_batch
85
- tile_count,
86
- scheduler_pipeline,
87
- PipelineStateWAdvance(stages, Int32(0), Int32(0), Int32(1 if is_scheduler_warp else 0)),
88
- params,
89
- loc=loc,
90
- ip=ip,
91
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-cuda/functional/triton_kernels/__init__.py CHANGED
@@ -1,10 +1,13 @@
 
 
 
1
  import math
2
 
3
  import torch
 
4
  import triton
5
  import triton.language as tl
6
 
7
- from ..._ops_compat import add_op_namespace_prefix
8
  from .bitmatrix import _bitmatrix_metadata_compute_stage1, _bitmatrix_metadata_compute_stage2, _keyed_add
9
 
10
 
 
1
+ # ********************************************************************************
2
+ # Copyright (c) 2026, Wentao Guo, Mayank Mishra, Xinle Cheng, Ion Stoica, Tri Dao
3
+ # ********************************************************************************
4
  import math
5
 
6
  import torch
7
+ from ..._ops import add_op_namespace_prefix
8
  import triton
9
  import triton.language as tl
10
 
 
11
  from .bitmatrix import _bitmatrix_metadata_compute_stage1, _bitmatrix_metadata_compute_stage2, _keyed_add
12
 
13
 
build/torch-cuda/metadata.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "sonic-moe",
3
- "id": "_sonic_moe_cuda_86f75d9",
4
- "version": 1,
5
  "license": "Apache-2.0",
6
  "python-depends": [
7
  "tvm-ffi",
@@ -9,5 +9,120 @@
9
  ],
10
  "backend": {
11
  "type": "cuda"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
  }
 
1
  {
2
  "name": "sonic-moe",
3
+ "id": "_sonic_moe_cuda_83d1d6e",
4
+ "version": 2,
5
  "license": "Apache-2.0",
6
  "python-depends": [
7
  "tvm-ffi",
 
9
  ],
10
  "backend": {
11
  "type": "cuda"
12
+ },
13
+ "digest": {
14
+ "algorithm": "sha256",
15
+ "files": {
16
+ "__init__.py": "DDepLW9l03NRbdlAuR2+UkWgJuQN/8ymgKZTMmdV0Dc=",
17
+ "_ops.py": "9EVL/sWTDKjpk2E9q9TV3k0BIZR8kzK88mQRF7lwkm8=",
18
+ "enums.py": "eeBuPJFCiiGCsL7OmYWCMfdvj4iQE4irrXun8niAUgg=",
19
+ "functional/__init__.py": "FDQaNUpQGY+3YeKXK9PNmtwKjXtn16GxsGdyFiQul08=",
20
+ "functional/backward.py": "W10eywFlVh1mMO+9oktuSWK7HllWZxzaByzQfXt1jM4=",
21
+ "functional/forward.py": "Tq1hGu14TvCXMptVrIZJQLgnAp0wSVzATEv9T5nc35w=",
22
+ "functional/reduction_over_k_gather.py": "fBYTaHbHTXpl7i2dtJ1InNIh0PIpxfE+6ImvPoNcOaU=",
23
+ "functional/topk.py": "Gsq7DJpeAuL0co/Zm9tSpyP0t9tSEwxSlRyjrTyCVhg=",
24
+ "functional/triton_kernels/__init__.py": "19C1f3DyiRGJfW3TcfHZH9x4q2vhqzAj2YkK/if9TUU=",
25
+ "functional/triton_kernels/bitmatrix.py": "kJ+VIr07n0e48+XQHSgQbLnmcRgFZ91M3NIUxNN4hwI=",
26
+ "jit.py": "pko7Lkttkab+0YDUkXbAAN3I/yMvTnw7d2jJE6zml+g=",
27
+ "moe.py": "7UtJ7Oe+401gE+pn0axai3MWrVipfJl9pJc72mSwB/g=",
28
+ "quack/__init__.py": "BsCyHg2lDdBg8lhuijV2CjN2CFJnQQuovcg5v4gM+Xo=",
29
+ "quack/_ops_compat.py": "Df2expY3Aqaob3ZuykC0qsSWm4DuUUSPUvI9J9gY9Rg=",
30
+ "quack/activation.py": "YIsLHpQauAokG2r/V9ToZnFzkCDll3JKU4IhTCsK8Hw=",
31
+ "quack/autotuner.py": "nbdngdlWYF19B4R6z6N7fgnDoO40MFghpeHlcKe2Yv8=",
32
+ "quack/bench/__init__.py": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
33
+ "quack/bench/bench_utils.py": "OZfHYnYliR7+o1paEdakdQ8YS22ySnOwPjwxuwfXpxE=",
34
+ "quack/blockscaled/__init__.py": "8aZJFIdaeT2ND6x77yrR/kAFVlSAkjx/nEwgPXrSv5s=",
35
+ "quack/blockscaled/quantize.py": "EPiyWvbHg1PC0UI5IRX3CqHTNk6HnjzhkUY+vkwgNSU=",
36
+ "quack/blockscaled/utils.py": "IE/QeL4IurRCCLLF/m/PKrZjuJ/Tp1j5jcgZggxy2eM=",
37
+ "quack/broadcast_utils.py": "HVUVszpqpbhv3ZrfCMSQVtqmjfRJJljYH+3J0WuSaMs=",
38
+ "quack/cache/__init__.py": "9qjakMy4yFmpI7XdQz4NDUyqfCti6xi0tkZFPnp9KVU=",
39
+ "quack/cache/_pool_preload.py": "qGKHxfFndDBCYyHUjSRDC7A+conMOQTqsjPfX5OCZc0=",
40
+ "quack/cache/async_compile.py": "BzJjQ5YswF4YAreXFDvbI05pQHzdtE5zxSdN4M7pOp4=",
41
+ "quack/cache/jit.py": "vXgLNNeAMrEY8a1fpigiiCUWZGCTbrtxMYWFrHIffkE=",
42
+ "quack/compile_utils.py": "d8AzPa2ONwKF3DhsB1E5orFzrNmJJinrYLJ0kstFq6k=",
43
+ "quack/complex.py": "WmYNde9V3ZDLaOt7ttCnjH4Dxahan4+PvLfoHHHU+jo=",
44
+ "quack/copy_utils.py": "0e1AtA1GZjFdc9Ts0mNT+gCftK0PVbDDkzOkXiBgSHE=",
45
+ "quack/cross_entropy.py": "OL8SJ435IOxTxL7L/C6vl/+3+BQWbIgMUt86rM8hJ84=",
46
+ "quack/cute_dsl_utils.py": "9lUc4T4L+Nm05V8doN+iiUqBFa2yBUSjYaVUseXCfH4=",
47
+ "quack/dsl/__init__.py": "v0w1PKdAXyF8yOzc/OwvR5+4M5rcdni62pCtVWpj2bI=",
48
+ "quack/dsl/cute_dsl_ptxas.py": "jbam6fUwzm4X5CZc29LevlWXBAzbedWu3j6hK/2lEmA=",
49
+ "quack/dsl/cute_tensor.py": "l1Z8XNzBSQuHwRKR6gX5+Se1DR39NoVR/JEV2IJbveo=",
50
+ "quack/dsl/cute_tensor_indexing.py": "1QnPrnekkECXkgHC5NWY6JxyDmTTf5kgClCIYwxMfG0=",
51
+ "quack/dsl/smem_struct.py": "piMjPM+wy8w/Ig6IZI/AOoVpeKsKSEes7xPxY2eiWCI=",
52
+ "quack/dsl/torch_library_op.py": "QfwtFO4P/3FSivWuY1LH2dL2xiead+m2VpK3iBKjKp0=",
53
+ "quack/epi_composable.py": "iCA8mYu9CLXNVvWmdP7847Pk+51tUYv6U6lTb5ptgfQ=",
54
+ "quack/epi_ops.py": "5+MCkOUqeLxFrzevU5OH4KXgxJJTKC+10jUHkxiDplA=",
55
+ "quack/epi_utils.py": "pdXiffZgdAbSA2WM5jkOIWvrPTBGqsvIkKrIffiAXFM=",
56
+ "quack/fast_math.py": "Nvhr8+bgjPAS+e/XKzlxO0Xzqa3gi8vRxV3gl82ECJI=",
57
+ "quack/gemm.py": "mAVLwuD+tC7O+NGhLzGaYQhsoWm4lo8h7TWEsqyJosA=",
58
+ "quack/gemm_act.py": "OIIB130+VHkMvyfP/kXCXzeNr7BkK68pIhxY2YwTqXM=",
59
+ "quack/gemm_base.py": "9RUR26bnwPd5LkM0PDo5201x0QdxgLO6VRZNCxBuDR8=",
60
+ "quack/gemm_config.py": "NinZfsUbbcaFyMR+yzuBtSfF2Q6TJPd3AoxAr89I5AQ=",
61
+ "quack/gemm_dact.py": "HV3dHIx4Er+QogUnp/lvBbkOAsXm2+z2YRiuOHyO5gQ=",
62
+ "quack/gemm_default_epi.py": "YFh3eGsC5wPVHiw//IoNQKiDH0u1ioT/5QPwtasRats=",
63
+ "quack/gemm_interface.py": "XTqyTBBZxOFEnE9/DCJjAsUQ3BBICY9nFgOSGjc8OZ4=",
64
+ "quack/gemm_norm_act.py": "u5CVGXIN4g1Z6pPD4fHKQkjDl1V/hdH1S3eWBlq8Hf0=",
65
+ "quack/gemm_sm100.py": "6vehfja8xPV1A78HBC6kUjHfOkUGGVtTiTLW7YVlr48=",
66
+ "quack/gemm_sm120.py": "aCy6GOMLgyvuFq+L4mRPXMXuKVcobswO6FSwLWhfx+k=",
67
+ "quack/gemm_sm80.py": "mGyJSOf7HSTJbOQawSJiu53hfKNg7HvSPzTVDLSwB4c=",
68
+ "quack/gemm_sm90.py": "38ONe3cM8g8hAEx4JomzbbkJ5lTo92UpbLNoUMn9EQE=",
69
+ "quack/gemm_sq_reduce.py": "N6JefW8NwxiMjFs6JssVu97Ysn9n4nErQkW2J6VMZHI=",
70
+ "quack/gemm_symmetric.py": "PRVtXM+nCya7nyiDH5ozvndGnfLX5TkM9kXYnSuJ5sg=",
71
+ "quack/gemm_tvm_ffi_utils.py": "76C1usihWxUD0lRi82AUO8OVjWVW5ypxIRelaFSAzTo=",
72
+ "quack/jax_utils.py": "z9CKuM2HbYXkgib8+mVNEvI5WF2HxBl8cgTCmiosTus=",
73
+ "quack/layout_utils.py": "I6MaaKCC67T+Yh/d6QdU4xbiqfPQ0ZJRMJ7/eUGFlFg=",
74
+ "quack/linear.py": "DK6ZcxbVB5PNnvRhUilds7BbRRdqovwNXJTPUOfUo2A=",
75
+ "quack/linear_cross_entropy.py": "UmkMdBB+8Tp4C4ikwjM/AGG/Ql+lWoqgmbADEHZ4I6o=",
76
+ "quack/mlp.py": "LKrK0VX3MToBM98oELVUMQJ3Zmgt93QNyZ0u7xdaFNE=",
77
+ "quack/nvmmh_heuristic.py": "mxImbqtdhY4NxYS6kZRL41zgG8oKa94uo5yLUtbLs2E=",
78
+ "quack/pipeline.py": "veqXF6OrbggTrcjhw4E/vDc7ufja27tifRDSuok4XHM=",
79
+ "quack/reduce.py": "Ul6PlgNnYQvm4dq948stCPrBdKA0Ct+jjgSeZFbxdNk=",
80
+ "quack/reduction_base.py": "cJc9fa+DsOFigD7d7bgNWR03gXkyXNzoSXwjPJxKTl0=",
81
+ "quack/rms_final_reduce.py": "9X2rSFH25BjNdLFDaJKmZeq/i1/gnmYhoYtCyPmAhrQ=",
82
+ "quack/rmsnorm.py": "yFyoI/p3ZyZqvOaDgN/+a3PbK1kJZvnYxjgLxAorgN4=",
83
+ "quack/rmsnorm_config.py": "HDOdXv5Ej2egQVQDVk4k1ynXUE0D0HpPaN9O7Qd9EK8=",
84
+ "quack/rotary.py": "A0FaRr7LKdWw+kjeHEg79I4eYb9+MbGKSMPuawocGBw=",
85
+ "quack/rounding.py": "jI6fP/qCjSMh9s8PtjPDYfTPs200C3xx9zKll1hlodU=",
86
+ "quack/sm100_utils.py": "+AkAPWzg+2cXZMwTijIVTvWWwZmk5m3bAsOJQTffzDo=",
87
+ "quack/sm80_utils.py": "0yOgRslAIq8B7MS8tIkgZuilOMIkYnJDUl7c2gjdkzg=",
88
+ "quack/sm90_utils.py": "gX9BE07yDSBMnrFdIJY0EXvyQuutTiXlCT8j5mHDLh0=",
89
+ "quack/softmax.py": "YQ4R6wVI2J/+FsjuTVGQ2Jj4shl4KU/YrHgFZmQ99do=",
90
+ "quack/softmax_jax.py": "VZ1phS40aMQDyx4QgsrWPxbkbdLnv3jNEc/+j2VqNN0=",
91
+ "quack/sort/bitonic_sort.py": "XjGKnZ51NI2N2BoHBgxz2Ne2OvYfnuOTgiOSnyjN08Q=",
92
+ "quack/sort/generate_sorting_networks.py": "al5BepGSLhn/bBPg64p2KpW7itx9gaCIGxhpqIceKB8=",
93
+ "quack/sort/sorting_networks.py": "okUmplKNQMAu8/lHClRiQf31X3tIe7MR07KybQJfH8Y=",
94
+ "quack/sort/utils.py": "lifuGThMk4D1VxfAUYDxrrUzHwxLGgLdoinpE8UVN3g=",
95
+ "quack/spec/__init__.py": "gZsivx8yPk0Tk7GqzHpOtrjN2b3dSB0/MNKVt0/e8qs=",
96
+ "quack/spec/mma.py": "3SSQiMkkncb3vuaoc9Dkr687vIBxEf7icnsdWJv5q58=",
97
+ "quack/spec/smem.py": "McZhRq5NJlHEFye+TQZpvDvnarRgLFKh2Hk6JslyOUA=",
98
+ "quack/spec/tensor_spec.py": "2d30W/TI7kwvc7tTw6/q1JU35/k0tJGD81JAXeX/P2A=",
99
+ "quack/spec/tma.py": "sz94fm7k50nGQ2jr7ubCzeZBX/YWZRmIerwp8P4+EFs=",
100
+ "quack/spec/tmem.py": "7YVy9F+Xn3iYEx2aDKiVW1xSIO8GMw+nyrwIdFgnrxs=",
101
+ "quack/sync/__init__.py": "5dkW0RJn9GodDYJeXxyQJfNaMBkXczp5BjkXeQK4/yg=",
102
+ "quack/sync/barrier.py": "sTHk/GcIC+OcgF6SK+YxSo7R4mdCB1n/6i42fdx+iVg=",
103
+ "quack/tensormap_manager.py": "twLLdCS6s8+BCYIh4XYdPtE3XX4OAASHklnsGssznPg=",
104
+ "quack/testing/__init__.py": "F4fOLg+7e5MRZUU+2kKdpR+XykrUhy3hrkrXke139+0=",
105
+ "quack/testing/pytest_plugin.py": "drWmLh35ffAa9J2VjGOiyhfWYBOcyr54HRNb1UkGTHo=",
106
+ "quack/testing/trace.py": "/pVNHOyiv1mgnvGKWy7RmreheJzUZx229+fMJoQZJo0=",
107
+ "quack/tile_scheduler.py": "VftFntcYMwF3t6op2H4arOSjmcQzvSqBp5YXjDSS5a0=",
108
+ "quack/topk.py": "md6UD7+W1OlZ3K0UDwhfT5fIT3kBrL3ccPYDuEcIedM=",
109
+ "quack/trace.py": "nmHaefNLu7k9MjI+LPYmGe69WU1UumN6D7IDAtX9sms=",
110
+ "quack/transform/__init__.py": "JhfsXbnxVdojWTz6VbGHCTkCANeR1b9Js2wDyldnO+c=",
111
+ "quack/transform/hadamard.py": "fehxPxaWlhWr1LHgBJijuFHQ2Q9houh/ZLEeO1WOvtQ=",
112
+ "quack/utils.py": "/FsIscjaxmOSj/g6frh4gLgB6iuIZ6e3EeGNunKWBuw=",
113
+ "quack/varlen_utils.py": "UtdJmeCWbAMeJhTYNF78cjN9CH3fEpNQvicoF1YhGL8=",
114
+ "utils.py": "mggbSpSYFQIJTyYgccngut/SeK2AqJfSXL1/CMZ8VyM="
115
+ }
116
+ },
117
+ "provenance": {
118
+ "kernel-builder": {
119
+ "version": "0.17.0-dev0",
120
+ "sha": "a6564d1f481adcbd3273099c0f4432e7b833e846",
121
+ "dirty": false
122
+ },
123
+ "kernel": {
124
+ "sha": "83d1d6e670b94603c6bdda6f039416ce988d7683",
125
+ "dirty": false
126
+ }
127
  }
128
  }
build/torch-cuda/metadata.json.sigstore ADDED
@@ -0,0 +1 @@
 
 
1
+ {"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial":{"certificate":{"rawBytes":"MIIHSjCCBtCgAwIBAgIUFiUTErfHXgcT0avZDUrtqVpjPVMwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYwODA0MDgyNzA5WhcNMjYwODA0MDgzNzA5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8E1Q2yBXTAmi4adPxCkrbQcPf17/xBZ3tfE6qg9RHWEdmi7WM5D7S43lqmnObiB4CkefF+OIxcSlHV8UJ2opOKOCBe8wggXrMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU36UElV8bsJe0ibWvj7caucWF9xgwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wawYDVR0RAQH/BGEwX4ZdaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5Ly5naXRodWIvd29ya2Zsb3dzL2J1aWxkLnlhbWxAcmVmcy9oZWFkcy9tYWluMDkGCisGAQQBg78wAQEEK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wHwYKKwYBBAGDvzABAgQRd29ya2Zsb3dfZGlzcGF0Y2gwNgYKKwYBBAGDvzABAwQoODNkMWQ2ZTY3MGI5NDYwM2M2YmRkYTZmMDM5NDE2Y2U5ODhkNzY4MzATBgorBgEEAYO/MAEEBAVCdWlsZDArBgorBgEEAYO/MAEFBB1odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eTAdBgorBgEEAYO/MAEGBA9yZWZzL2hlYWRzL21haW4wOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMG0GCisGAQQBg78wAQkEXwxdaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5Ly5naXRodWIvd29ya2Zsb3dzL2J1aWxkLnlhbWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoODNkMWQ2ZTY3MGI5NDYwM2M2YmRkYTZmMDM5NDE2Y2U5ODhkNzY4MzAbBgorBgEEAYO/MAELBA0MC3NlbGYtaG9zdGVkMEAGCisGAQQBg78wAQwEMgwwaHR0cHM6Ly9naXRodWIuY29tL2h1Z2dpbmdmYWNlL2tlcm5lbHMtY29tbXVuaXR5MDgGCisGAQQBg78wAQ0EKgwoODNkMWQ2ZTY3MGI5NDYwM2M2YmRkYTZmMDM5NDE2Y2U5ODhkNzY4MzAfBgorBgEEAYO/MAEOBBEMD3JlZnMvaGVhZHMvbWFpbjAaBgorBgEEAYO/MAEPBAwMCjEwNzE0NzU1MjkwLgYKKwYBBAGDvzABEAQgDB5odHRwczovL2dpdGh1Yi5jb20vaHVnZ2luZ2ZhY2UwGAYKKwYBBAGDvzABEQQKDAgyNTcyMDc0MzBtBgorBgEEAYO/MAESBF8MXWh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eS8uZ2l0aHViL3dvcmtmbG93cy9idWlsZC55YW1sQHJlZnMvaGVhZHMvbWFpbjA4BgorBgEEAYO/MAETBCoMKDgzZDFkNmU2NzBiOTQ2MDNjNmJkZGE2ZjAzOTQxNmNlOTg4ZDc2ODMwIQYKKwYBBAGDvzABFAQTDBF3b3JrZmxvd19kaXNwYXRjaDBkBgorBgEEAYO/MAEVBFYMVGh0dHBzOi8vZ2l0aHViLmNvbS9odWdnaW5nZmFjZS9rZXJuZWxzLWNvbW11bml0eS9hY3Rpb25zL3J1bnMvMzA4OTE2ODY1NTMvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzBGBgorBgEEAYO/MAEYBDgMNnJlcG86aHVnZ2luZ2ZhY2Uva2VybmVscy1jb21tdW5pdHk6cmVmOnJlZnMvaGVhZHMvbWFpbjCBiQYKKwYBBAHWeQIEAgR7BHkAdwB1AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABn8viPTkAAAQDAEYwRAIgfLsNELCVlyuHUG5H2cJ2naaV5F0SYQZv0YdXyC9K6asCIGpaO2l3hSUXIiLRBq6dsIXZHZyZqZ2/6nNPdklmZMlzMAoGCCqGSM49BAMDA2gAMGUCMHeyUvclqJRFc6yuhmZtURySzkJ6RpnXasTQ5hXcx4H/3BVH1hHtFeXi4epa0GNcLgIxAPspbTGLLNMVY+7Rhv7SaSsX5AGzaWVABXmQPDy7S7T4IJArASfsqBm3NmO0SR0vtA=="}, "tlogEntries":[{"logIndex":"2339551903", "logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}, "kindVersion":{"kind":"hashedrekord", "version":"0.0.1"}, "integratedTime":"1785832029", "inclusionPromise":{"signedEntryTimestamp":"MEYCIQDitpUL1h/KusQ9OMEtNPCsHo084FfnQmZK0WwFpKnoZAIhAPTvy8x20w6lcWucTqcZVLYevLSY3i2S/rLsZ34hFk44"}, "inclusionProof":{"logIndex":"2217647641", "rootHash":"nQRMpxeZMxZKoBM0Aid5ajFNCBrokJCPxAZe9n5BiME=", "treeSize":"2217647645", "hashes":["48NZ8qo8UwRJEAZ3CLfS1KfxJZxkxn+iOuNafjEb1/M=", "u2Qapxkuy0/IKrnn3gJDePaVd9YyBiTAyigk5uwxM8Y=", "yzLVy2ZAAv+TndVc45A90uQ1k7RULtWa9nqp7rNIt9U=", "ZeI1SGr9GP/SskvVce8yHk+6DRrxLrWqJpQnRQRaKpc=", "nQWu7U6/pF5XpZlDhkQlNjCedXmCTB3GYqdStVFhcGo=", "WeMerEQh8gnrs+xRVnIEQXfykDf4KiXgRHX8oVHQYIs=", "2uIbGD5iEQJHWVjTX0311tWS7CFZVaKdy3hZGbyhI8s=", "DLtcvkp4dRTrpmSEDyxo9Y5x1mCGHjNNX5a+Pu2gajA=", "7VIK52c6IvZNnE+Au/wuGXo7lX61HQyDCibo+Dfcvs8=", "jDU3DoSktlu0G+qce/KFrCSxbMWDf2MyMt8j1NVTV/g=", "p0bIaoDFhBhr45Lurz3ap+CC2ZtzymaJJb6Ij0aF6Xg=", "7xWj0qvDr9hJ4vooBulXteOWWqidK4mqZKahpqVHaoE=", "r3u4sx0tkA1pkDYoU+5zLDv1az999M9yZP3h7rXTwYE=", "8cBkID7yRXftjQqclK9Jj0hUDPpp7HMqoKXxzdkwOzg=", "i5Zl8FZrDwxCDv2e2DNO2M8JvpR/c11ElvCZS53/teA=", "xH/DCseLHr9eKoYT8qsORZK7zVdEGYWHuVtsVrD95wY="], "checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n2217647645\nnQRMpxeZMxZKoBM0Aid5ajFNCBrokJCPxAZe9n5BiME=\n\n— rekor.sigstore.dev wNI9ajBFAiEAm8BuxbwwhrjVkIerX6mU3KfouLswOpqLlyxRei9yV58CIEW4vOQQYXUx761ALJD54e5Q5qfizDBuosCfSV/l8+Jc\n"}}, "canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI0ZWYzZTQwM2RjMGJlM2Q1N2YzNWYyYjNkNzIwNTMwNDZkZmZlYjczZWQ2YmE3ODE4NGMwYjA0Njc5MzllNzU2In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJSElhM3JzWDI4a1ZOKzFPVDQyUDhLNk82anY3Wk9iSW9neXg1M2t5a0xFWkFpRUF5K1QxY0VyTjJ3M2xhRWpwaXRZYi9JNi9KMEhlOFNiQVN6UjBVVFdEYk84PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVaFRha05EUW5SRFowRjNTVUpCWjBsVlJtbFZWRVZ5WmtoWVoyTlVNR0YyV2tSVmNuUnhWbkJxVUZaTmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcFpkMDlFUVRCTlJHZDVUbnBCTlZkb1kwNU5hbGwzVDBSQk1FMUVaM3BPZWtFMVYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVU0UlRGUk1ubENXRlJCYldrMFlXUlFlRU5yY21KUlkxQm1NVGN2ZUVKYU0zUm1SVFlLY1djNVVraFhSV1J0YVRkWFRUVkVOMU0wTTJ4eGJXNVBZbWxDTkVOclpXWkdLMDlKZUdOVGJFaFdPRlZLTW05d1QwdFBRMEpsT0hkbloxaHlUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlV6TmxWRkNteFdPR0p6U21Vd2FXSlhkbW8zWTJGMVkxZEdPWGhuZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDJGM1dVUldVakJTUVZGSUwwSkhSWGRZTkZwa1lVaFNNR05JVFRaTWVUbHVZVmhTYjJSWFNYVlpNamwwVERKb01Wb3laSEJpYldSdFdWZE9iQXBNTW5Sc1kyMDFiR0pJVFhSWk1qbDBZbGhXZFdGWVVqVk1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU1rb3hZVmQ0YTB4dWJHaGlWM2hCQ21OdFZtMWplVGx2V2xkR2EyTjVPWFJaVjJ4MVRVUnJSME5wYzBkQlVWRkNaemM0ZDBGUlJVVkxNbWd3WkVoQ2VrOXBPSFprUnpseVdsYzBkVmxYVGpBS1lWYzVkV041Tlc1aFdGSnZaRmRLTVdNeVZubFpNamwxWkVkV2RXUkROV3BpTWpCM1NIZFpTMHQzV1VKQ1FVZEVkbnBCUWtGblVWSmtNamw1WVRKYWN3cGlNMlJtV2tkc2VtTkhSakJaTW1kM1RtZFpTMHQzV1VKQ1FVZEVkbnBCUWtGM1VXOVBSRTVyVFZkUk1scFVXVE5OUjBrMVRrUlpkMDB5VFRKWmJWSnJDbGxVV20xTlJFMDFUa1JGTWxreVZUVlBSR2hyVG5wWk5FMTZRVlJDWjI5eVFtZEZSVUZaVHk5TlFVVkZRa0ZXUTJSWGJITmFSRUZ5UW1kdmNrSm5SVVVLUVZsUEwwMUJSVVpDUWpGdlpGZGtibUZYTlc1YWJVWnFXbE01Y2xwWVNuVmFWM2g2VEZkT2RtSlhNVEZpYld3d1pWUkJaRUpuYjNKQ1owVkZRVmxQTHdwTlFVVkhRa0U1ZVZwWFducE1NbWhzV1ZkU2Vrd3lNV2hoVnpSM1QzZFpTMHQzV1VKQ1FVZEVkbnBCUWtOQlVYUkVRM1J2WkVoU2QyTjZiM1pNTTFKMkNtRXlWblZNYlVacVpFZHNkbUp1VFhWYU1td3dZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVY3dSME5wYzBkQlVWRkNaemM0ZDBGUmEwVUtXSGQ0WkdGSVVqQmpTRTAyVEhrNWJtRllVbTlrVjBsMVdUSTVkRXd5YURGYU1tUndZbTFrYlZsWFRteE1NblJzWTIwMWJHSklUWFJaTWpsMFlsaFdkUXBoV0ZJMVRIazFibUZZVW05a1YwbDJaREk1ZVdFeVduTmlNMlI2VERKS01XRlhlR3RNYm14b1lsZDRRV050Vm0xamVUbHZXbGRHYTJONU9YUlpWMngxQ2sxRVowZERhWE5IUVZGUlFtYzNPSGRCVVc5RlMyZDNiMDlFVG10TlYxRXlXbFJaTTAxSFNUVk9SRmwzVFRKTk1sbHRVbXRaVkZwdFRVUk5OVTVFUlRJS1dUSlZOVTlFYUd0T2VsazBUWHBCWWtKbmIzSkNaMFZGUVZsUEwwMUJSVXhDUVRCTlF6Tk9iR0pIV1hSaFJ6bDZaRWRXYTAxRlFVZERhWE5IUVZGUlFncG5OemgzUVZGM1JVMW5kM2RoU0ZJd1kwaE5Oa3g1T1c1aFdGSnZaRmRKZFZreU9YUk1NbWd4V2pKa2NHSnRaRzFaVjA1c1RESjBiR050Tld4aVNFMTBDbGt5T1hSaVdGWjFZVmhTTlUxRVowZERhWE5IUVZGUlFtYzNPSGRCVVRCRlMyZDNiMDlFVG10TlYxRXlXbFJaTTAxSFNUVk9SRmwzVFRKTk1sbHRVbXNLV1ZSYWJVMUVUVFZPUkVVeVdUSlZOVTlFYUd0T2VsazBUWHBCWmtKbmIzSkNaMFZGUVZsUEwwMUJSVTlDUWtWTlJETktiRnB1VFhaaFIxWm9Xa2hOZGdwaVYwWndZbXBCWVVKbmIzSkNaMFZGUVZsUEwwMUJSVkJDUVhkTlEycEZkMDU2UlRCT2VsVXhUV3ByZDB4bldVdExkMWxDUWtGSFJIWjZRVUpGUVZGbkNrUkNOVzlrU0ZKM1kzcHZka3d5WkhCa1IyZ3hXV2sxYW1JeU1IWmhTRlp1V2pKc2RWb3lXbWhaTWxWM1IwRlpTMHQzV1VKQ1FVZEVkbnBCUWtWUlVVc0tSRUZuZVU1VVkzbE5SR013VFhwQ2RFSm5iM0pDWjBWRlFWbFBMMDFCUlZOQ1JqaE5XRmRvTUdSSVFucFBhVGgyV2pKc01HRklWbWxNYlU1MllsTTVid3BrVjJSdVlWYzFibHB0Um1wYVV6bHlXbGhLZFZwWGVIcE1WMDUyWWxjeE1XSnRiREJsVXpoMVdqSnNNR0ZJVm1sTU0yUjJZMjEwYldKSE9UTmplVGxwQ21SWGJITmFRelUxV1ZjeGMxRklTbXhhYmsxMllVZFdhRnBJVFhaaVYwWndZbXBCTkVKbmIzSkNaMFZGUVZsUEwwMUJSVlJDUTI5TlMwUm5lbHBFUm1zS1RtMVZNazU2UW1sUFZGRXlUVVJPYWs1dFNtdGFSMFV5V21wQmVrOVVVWGhPYlU1c1QxUm5ORnBFWXpKUFJFMTNTVkZaUzB0M1dVSkNRVWRFZG5wQlFncEdRVkZVUkVKR00ySXpTbkphYlhoMlpERTVhMkZZVG5kWldGSnFZVVJDYTBKbmIzSkNaMFZGUVZsUEwwMUJSVlpDUmxsTlZrZG9NR1JJUW5wUGFUaDJDbG95YkRCaFNGWnBURzFPZG1KVE9XOWtWMlJ1WVZjMWJscHRSbXBhVXpseVdsaEtkVnBYZUhwTVYwNTJZbGN4TVdKdGJEQmxVemxvV1ROU2NHSXlOWG9LVEROS01XSnVUWFpOZWtFMFQxUkZNazlFV1RGT1ZFMTJXVmhTTUZwWE1YZGtTRTEyVFZSQlYwSm5iM0pDWjBWRlFWbFBMMDFCUlZkQ1FXZE5RbTVDTVFwWmJYaHdXWHBDUjBKbmIzSkNaMFZGUVZsUEwwMUJSVmxDUkdkTlRtNUtiR05IT0RaaFNGWnVXakpzZFZveVdtaFpNbFYyWVRKV2VXSnRWbk5qZVRGcUNtSXlNWFJrVnpWd1pFaHJObU50Vm0xUGJrcHNXbTVOZG1GSFZtaGFTRTEyWWxkR2NHSnFRMEpwVVZsTFMzZFpRa0pCU0ZkbFVVbEZRV2RTTjBKSWEwRUtaSGRDTVVGT01EbE5SM0pIZUhoRmVWbDRhMlZJU214dVRuZExhVk5zTmpRemFubDBMelJsUzJOdlFYWkxaVFpQUVVGQlFtNDRkbWxRVkd0QlFVRlJSQXBCUlZsM1VrRkpaMlpNYzA1RlRFTldiSGwxU0ZWSE5VZ3lZMG95Ym1GaFZqVkdNRk5aVVZwMk1GbGtXSGxET1VzMllYTkRTVWR3WVU4eWJETm9VMVZZQ2tscFRGSkNjVFprYzBsWVdraGFlVnB4V2pJdk5tNU9VR1JyYkcxYVRXeDZUVUZ2UjBORGNVZFRUVFE1UWtGTlJFRXlaMEZOUjFWRFRVaGxlVlYyWTJ3S2NVcFNSbU0yZVhWb2JWcDBWVko1VTNwclNqWlNjRzVZWVhOVVVUVm9XR040TkVndk0wSldTREZvU0hSR1pWaHBOR1Z3WVRCSFRtTk1aMGw0UVZCemNBcGlWRWRNVEU1TlZsa3JOMUpvZGpkVFlWTnpXRFZCUjNwaFYxWkJRbGh0VVZCRWVUZFROMVEwU1VwQmNrRlRabk54UW0welRtMVBNRk5TTUhaMFFUMDlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsifX19fQ=="}], "timestampVerificationData":{"rfc3161Timestamps":[{"signedTimestamp":"MIICyjADAgEAMIICwQYJKoZIhvcNAQcCoIICsjCCAq4CAQMxDTALBglghkgBZQMEAgEwgbgGCyqGSIb3DQEJEAEEoIGoBIGlMIGiAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgx31mO/vQ8UTM6GrWl7ysWYCg9VM1q5BDPZqvFVfIaYMCFQD1qIDvOHE1Cgo864r6GhDv1PSMORgPMjAyNjA4MDQwODI3MDlaMAMCAQGgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoAAxggHbMIIB1wIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MDgwNDA4MjcwOVowLwYJKoZIhvcNAQkEMSIEIHmfCoWI9C2TufxbIehLRJjjeOkcBfMZEcgFW3CNowVHMIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCCF+Se8B6tiysO0Q1bBDvyBssaIP9p6uebYcNnROs0FtzBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQ6E1QvDJBh7rzBQy/Lio6LKiOLDDAKBggqhkjOPQQDAgRnMGUCMHTZ3i8JqZ77m5Vccy3iUVKabZVryj/JRhzGaPfzud9V06kWLkG/Y8pmaHEOZXR4tAIxAOK0xDAb+/Kj7Vn16OaMNEQGjedN9J3fDf2Nyf+/TvnAlwVVpbX++c717VMpDOG6pw=="}]}}, "messageSignature":{"messageDigest":{"algorithm":"SHA2_256", "digest":"TvPkA9wL49V/NfKz1yBTBG3/63Pta6eBhMCwRnk551Y="}, "signature":"MEUCIHIa3rsX28kVN+1OT42P8K6O6jv7ZObIogyx53kykLEZAiEAy+T1cErN2w3laEjpitYb/I6/J0He8SbASzR0UTWDbO8="}}
build/torch-cuda/quack/__init__.py CHANGED
@@ -1,8 +1,17 @@
1
- __version__ = "0.3.11"
2
 
3
  import os
4
 
 
 
5
  if os.environ.get("CUTE_DSL_PTXAS_PATH", None) is not None:
6
- from . import cute_dsl_ptxas # noqa: F401
 
 
 
 
 
7
 
8
- cute_dsl_ptxas.patch()
 
 
 
1
+ __version__ = "0.6.1"
2
 
3
  import os
4
 
5
+ from . import dsl as _quack_dsl # noqa: F401
6
+
7
  if os.environ.get("CUTE_DSL_PTXAS_PATH", None) is not None:
8
+ from .dsl import cute_dsl_ptxas as _cute_dsl_ptxas
9
+
10
+ # Patch before importing any modules that instantiate CuTeDSL. The patch
11
+ # forces PTX dumping so the CUDA library loader can replace CUTLASS DSL's
12
+ # embedded ptxas-library cubin with one assembled by system ptxas.
13
+ _cute_dsl_ptxas.patch()
14
 
15
+ # Pythonic CuTe tensor indexing (`:` / `...` sugar) is installed as a side effect
16
+ # of importing `quack.dsl`, which imports `quack.dsl.cute_tensor_indexing` and
17
+ # monkey-patches CuTe's tensor classes process-wide.
build/torch-cuda/quack/_compile_worker.py DELETED
@@ -1,102 +0,0 @@
1
- # Copyright (c) 2025, Tri Dao.
2
- # Persistent subprocess worker for parallel autotuning pre-compilation.
3
- # Receives length-prefixed pickled tasks on stdin, creates FakeTensors
4
- # matching the parent's tensor metadata, and compiles with COMPILE_ONLY=True.
5
- # Stays alive to process multiple configs (amortizes import overhead).
6
-
7
- import importlib
8
- import pickle
9
- import struct
10
- import sys
11
-
12
- import torch
13
- from torch._subclasses.fake_tensor import FakeTensorMode
14
-
15
- from . import cache_utils
16
-
17
- cache_utils.COMPILE_ONLY = True
18
-
19
- _dtype_map = {
20
- "torch.float16": torch.float16,
21
- "torch.bfloat16": torch.bfloat16,
22
- "torch.float32": torch.float32,
23
- "torch.float64": torch.float64,
24
- "torch.int32": torch.int32,
25
- "torch.int64": torch.int64,
26
- "torch.int8": torch.int8,
27
- "torch.uint8": torch.uint8,
28
- "torch.bool": torch.bool,
29
- }
30
-
31
-
32
- def _make_fake_tensor(meta):
33
- shape = meta["shape"]
34
- stride = meta["stride"]
35
- dtype = _dtype_map[meta["dtype"]]
36
- return torch.empty_strided(shape, stride, dtype=dtype, device="cuda")
37
-
38
-
39
- def _recv(stream):
40
- """Read a length-prefixed pickled message. Returns None on EOF."""
41
- header = stream.read(4)
42
- if len(header) < 4:
43
- return None
44
- length = struct.unpack("<I", header)[0]
45
- if length == 0:
46
- return None
47
- data = stream.read(length)
48
- return pickle.loads(data)
49
-
50
-
51
- def _send(stream, msg):
52
- """Write a length-prefixed pickled message."""
53
- data = pickle.dumps(msg)
54
- stream.write(struct.pack("<I", len(data)))
55
- stream.write(data)
56
- stream.flush()
57
-
58
-
59
- def main():
60
- stdin = sys.stdin.buffer
61
- stdout = sys.stdout.buffer
62
-
63
- # Signal ready
64
- _send(stdout, "READY")
65
-
66
- fn_cache = {}
67
- while True:
68
- payload = _recv(stdin)
69
- if payload is None:
70
- break
71
-
72
- fn_module = payload["fn_module"]
73
- fn_qualname = payload["fn_qualname"]
74
- fn_key = (fn_module, fn_qualname)
75
- if fn_key not in fn_cache:
76
- mod = importlib.import_module(fn_module)
77
- obj = mod
78
- for part in fn_qualname.split("."):
79
- obj = getattr(obj, part)
80
- fn_cache[fn_key] = getattr(obj, "fn", obj)
81
- fn = fn_cache[fn_key]
82
-
83
- tensor_meta = payload["tensor_meta"]
84
- kwargs = payload["kwargs"]
85
- config_kwargs = payload["config_kwargs"]
86
-
87
- with FakeTensorMode():
88
- fake_args = []
89
- for meta in tensor_meta:
90
- if isinstance(meta, dict) and "shape" in meta:
91
- fake_args.append(_make_fake_tensor(meta))
92
- else:
93
- fake_args.append(meta)
94
- try:
95
- fn(*fake_args, **kwargs, **config_kwargs)
96
- _send(stdout, "OK")
97
- except Exception as e:
98
- _send(stdout, f"ERR:{e}")
99
-
100
-
101
- if __name__ == "__main__":
102
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-cuda/quack/_ops_compat.py CHANGED
@@ -1,4 +1,10 @@
1
- from .._ops_compat import add_op_namespace_prefix
2
 
3
- def add_quack_op_namespace_prefix(name: str) -> str:
4
- return add_op_namespace_prefix(f"quack__{name}")
 
 
 
 
 
 
 
1
+ from .._ops import add_op_namespace_prefix as _add_op_namespace_prefix
2
 
3
+
4
+
5
+ # For quack we need to prefix the function name because some names
6
+ # overlap between quack and sonic-moe itself. Name the function the
7
+ # same as the function it is wrapping for the prefix check to be
8
+ # happy.
9
+ def add_op_namespace_prefix(name: str) -> str:
10
+ return _add_op_namespace_prefix(f"quack__{name}")
build/torch-cuda/quack/activation.py CHANGED
@@ -4,10 +4,12 @@ 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]
@@ -21,30 +23,61 @@ sub_packed_f32x2 = partial(
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
  )
34
- )
35
 
36
 
37
  @dsl_user_op
38
- def sigmoid(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  if const_expr(not isinstance(x, tuple)):
40
  # return 0.5 + 0.5 * cute.math.tanh(0.5 * x, fastmath=True)
41
  return 0.5 + 0.5 * tanh(0.5 * x)
42
  else:
43
  x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x)
44
- tanh_x_half = (tanh(x_half[0]), tanh(x_half[1]))
45
  return cute.arch.fma_packed_f32x2(tanh_x_half, (0.5, 0.5), (0.5, 0.5))
46
 
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  @dsl_user_op
49
  def dsigmoid_from_output(out: Float32, dout: Float32, *, loc=None, ip=None) -> Float32:
50
  # return dout * out * (1.0 - out)
@@ -54,9 +87,9 @@ def dsigmoid_from_output(out: Float32, dout: Float32, *, loc=None, ip=None) -> F
54
  @dsl_user_op
55
  def relu(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
56
  if const_expr(not isinstance(x, tuple)):
57
- return cute.arch.fmax(x, Float32(0.0))
58
  else:
59
- return cute.arch.fmax(x[0], Float32(0.0)), cute.arch.fmax(x[1], Float32(0.0))
60
 
61
 
62
  @dsl_user_op
@@ -66,7 +99,7 @@ def drelu(
66
  ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
67
  if const_expr(not isinstance(x, tuple)):
68
  x_pos = Boolean(x > 0)
69
- return dout if x_pos else Float32(0.0), cute.arch.fmax(x, Float32(0.0))
70
  else:
71
  x0_pos = Boolean(x[0] > 0)
72
  x1_pos = Boolean(x[1] > 0)
@@ -77,9 +110,9 @@ def drelu(
77
  @dsl_user_op
78
  def relu_sq(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
79
  if const_expr(not isinstance(x, tuple)):
80
- return cute.arch.fmax(x, Float32(0.0)) * x
81
  else:
82
- relu_x = (cute.arch.fmax(x[0], Float32(0.0)), cute.arch.fmax(x[1], Float32(0.0)))
83
  return cute.arch.mul_packed_f32x2(relu_x, x)
84
 
85
 
@@ -117,19 +150,17 @@ def gelu_tanh_approx(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
117
  sqrt_2_over_pi = math.sqrt(2 / math.pi) # ~0.797885
118
  sqrt_2_over_pi_coeff = 0.044715 * sqrt_2_over_pi # ~0.0356774
119
  if const_expr(not isinstance(x, tuple)):
120
- return 0.5 * (
121
- x
122
- # Currently cute.math.tanh(x, fastmath=True) generates very slow code
123
- # * (1 + cute.math.tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * (x * x)), fastmath=True))
124
- * (1.0 + tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * (x * x))))
125
- )
126
  else:
127
  x_sq = cute.arch.mul_packed_f32x2(x, x)
128
  x_sq_scaled = cute.arch.fma_packed_f32x2(
129
  x_sq, (sqrt_2_over_pi_coeff, sqrt_2_over_pi_coeff), (sqrt_2_over_pi, sqrt_2_over_pi)
130
  )
131
  z = cute.arch.mul_packed_f32x2(x, x_sq_scaled)
132
- tanh_z = (tanh(z[0]), tanh(z[1]))
133
  x_tanh_z = cute.arch.fma_packed_f32x2(tanh_z, x, x)
134
  return cute.arch.mul_packed_f32x2((0.5, 0.5), x_tanh_z)
135
 
@@ -162,11 +193,16 @@ def dgelu_tanh_approx(
162
 
163
  # Compute gradient
164
  # sech^2(z) = 1 - tanh^2(z)
165
- sech2_z = 1 - tanh_z * tanh_z
 
 
 
166
  # dz/dx = c1 + 3 * c2 * x^2
167
  dz_dx = sqrt_2_over_pi + sqrt_2_over_pi_coeff_3 * x_sq
168
  # d/dx[gelu(x)] = 0.5 * (1 + tanh(z)) + 0.5 * x * sech^2(z) * dz/dx
169
- dgelu = half_tanh_z_plus_one + x * (0.5 * (sech2_z * dz_dx))
 
 
170
 
171
  dx = dout * dgelu
172
  return dx, gelu_out
@@ -177,7 +213,7 @@ def dgelu_tanh_approx(
177
  x_sq, (sqrt_2_over_pi_coeff, sqrt_2_over_pi_coeff), (sqrt_2_over_pi, sqrt_2_over_pi)
178
  )
179
  z = cute.arch.mul_packed_f32x2(x, x_sq_scaled)
180
- tanh_z = (tanh(z[0]), tanh(z[1]))
181
  half_tanh_z_plus_one = cute.arch.fma_packed_f32x2(tanh_z, (0.5, 0.5), (0.5, 0.5))
182
  gelu_out = cute.arch.mul_packed_f32x2(x, half_tanh_z_plus_one)
183
 
@@ -236,7 +272,18 @@ def dsoftplus_from_output(out: Float32, dout: Float32, *, loc=None, ip=None) ->
236
 
237
 
238
  @dsl_user_op
239
- def silu(x: F32_or_F32x2, *, already_halved: bool = False, loc=None, ip=None) -> F32_or_F32x2:
 
 
 
 
 
 
 
 
 
 
 
240
  """
241
  silu(x) = x * sigmoid(x) = x * (1 + tanh(x / 2)) / 2 = (0.5 * x) * tanh(0.5 * x) + (0.5 * x)
242
  This compiles down to 3 SASS instructions: FMUL to get 0.5 * x, MUFU.TANH, and FFMA.
@@ -247,10 +294,91 @@ def silu(x: F32_or_F32x2, *, already_halved: bool = False, loc=None, ip=None) ->
247
  return x_half * tanh(x_half) + x_half
248
  else:
249
  x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x) if const_expr(not already_halved) else x
250
- tanh_x_half = (tanh(x_half[0]), tanh(x_half[1]))
251
  return cute.arch.fma_packed_f32x2(x_half, tanh_x_half, x_half)
252
 
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  @dsl_user_op
255
  def swiglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
256
  if const_expr(not isinstance(x, tuple)):
@@ -259,13 +387,20 @@ def swiglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32
259
  return cute.arch.mul_packed_f32x2(silu(x), y)
260
 
261
 
 
 
 
 
 
 
 
 
262
  @dsl_user_op
263
  def dswiglu(
264
  x: F32_or_F32x2,
265
  y: F32_or_F32x2,
266
  dout: F32_or_F32x2,
267
  *,
268
- already_halved: bool = False,
269
  loc=None,
270
  ip=None,
271
  ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
@@ -280,15 +415,8 @@ def dswiglu(
280
  to use FFMA instead of FADD and FMUL).
281
  """
282
  if const_expr(not isinstance(x, tuple)):
283
- # Compute sigmoid(x) using tanh: sigmoid(x) = 0.5 * (1 + tanh(0.5 * x))
284
- # FMUL, MUFU.TANH, then FFMA
285
- if const_expr(not already_halved):
286
- sigmoid_x = sigmoid(x)
287
- silu_x = x * sigmoid_x # FMUL
288
- else:
289
- tanh_x = tanh(x) # MUFU.TANH
290
- sigmoid_x = 0.5 * tanh_x + 0.5 # FFMA
291
- silu_x = x * tanh_x + x # FFMA
292
  silu_x_dout = silu_x * dout # FMUL
293
  # d_silu(x) * dout
294
  # = sigmoid_x * (1 + x * (1 - sigmoid_x)) * dout
@@ -296,19 +424,65 @@ def dswiglu(
296
  # = (sigmoid_x + silu_x * (1 - sigmoid_x)) * dout
297
  # = (sigmoid_x + silu_x - silu_x * sigmoid_x) * dout
298
  # = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x * dout
299
- d_silu_x_dout = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x_dout # FFMA, FFMA
 
 
300
  dx = d_silu_x_dout * y # FMUL
301
  dy = silu_x_dout
302
  swiglu_out = silu_x * y # FMUL
303
- # Overall it's 1 MUFU.TANH, 5 FMUL, 3 FFMA
304
  return dx, dy, swiglu_out
305
  else:
306
  # Compute sigmoid(x) and silu(x)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  if const_expr(not already_halved):
308
- sigmoid_x = sigmoid(x)
309
  silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_x)
310
  else:
311
- tanh_x = (tanh(x[0]), tanh(x[1]))
312
  sigmoid_x = cute.arch.fma_packed_f32x2(tanh_x, (0.5, 0.5), (0.5, 0.5))
313
  silu_x = cute.arch.fma_packed_f32x2(x, tanh_x, x)
314
  silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
@@ -332,18 +506,31 @@ def swiglu_oai(
332
  """The swiglu variant used in gpt-oss, which has a scaling factor on x and bias of 1 to y.
333
  https://github.com/openai/gpt-oss/blob/7be9334950053a888e24887a57dac797a17d6e00/gpt_oss/torch/model.py#L249
334
  x * sigmoid(alpha * x) * (y + 1)
335
- Compile down to FMUL, FMUL, TANH, FFMA, FFMA
336
  """
337
- # Compute sigmoid(alpha * x) using tanh: sigmoid(z) = 0.5 * (1 + tanh(z/2))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  if const_expr(not isinstance(x, tuple)):
339
  x_half = 0.5 * x
340
- # silu_x = x_half * cute.math.tanh(alpha * x_half, fastmath=True) + x_half
341
  silu_x = x_half * tanh(alpha * x_half) + x_half
342
  return silu_x * y + silu_x
343
  else:
344
  x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x)
345
  alpha_x_half = cute.arch.mul_packed_f32x2((alpha, alpha), x_half)
346
- tanh_alpha_x_half = (tanh(alpha_x_half[0]), tanh(alpha_x_half[1]))
347
  silu_x = cute.arch.fma_packed_f32x2(x_half, tanh_alpha_x_half, x_half)
348
  return cute.arch.fma_packed_f32x2(silu_x, y, silu_x)
349
 
@@ -361,28 +548,62 @@ def dswiglu_oai(
361
  d/dx[x * sigmoid(alpha * x)] = sigmoid(alpha * x) + alpha * x * sigmoid(alpha * x) * (1 - sigmoid(alpha * x))
362
  """
363
  if const_expr(not isinstance(x, tuple)):
364
- # Compute sigmoid(alpha * x) using tanh: sigmoid(z) = 0.5 * (1 + tanh(z/2))
365
- alpha_x_half = (0.5 * alpha) * x # FMUL
366
- # MUFU.TANH, then FFMA
367
- # sigmoid_alpha_x = 0.5 + 0.5 * cute.math.tanh(alpha_x_half, fastmath=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  sigmoid_alpha_x = 0.5 + 0.5 * tanh(alpha_x_half)
369
- silu_x = x * sigmoid_alpha_x # FMUL
370
- silu_x_dout = silu_x * dout # FMUL
371
- # FFMA, FFMA, FMUL
372
- d_silu_x_dout = (sigmoid_alpha_x + alpha * (silu_x - silu_x * sigmoid_alpha_x)) * dout
373
- dx = d_silu_x_dout * y + d_silu_x_dout # FFMA, instead of multiply by y + 1
 
 
 
374
  dy = silu_x_dout
375
- swiglu_out = silu_x * y + silu_x # FFMA, instead of multiply by y + 1
376
- # Overall it's 1 MUFU.TANH, 4 FMUL, 5 FFMA
377
  return dx, dy, swiglu_out
378
  else:
379
- # Compute sigmoid(alpha * x)
380
  alpha_x_half = cute.arch.mul_packed_f32x2(((0.5 * alpha), (0.5 * alpha)), x)
381
- tanh_alpha_x_half = (tanh(alpha_x_half[0]), tanh(alpha_x_half[1]))
382
  sigmoid_alpha_x = cute.arch.fma_packed_f32x2(tanh_alpha_x_half, (0.5, 0.5), (0.5, 0.5))
383
  silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_alpha_x)
384
  silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
385
- # d_silu_x_dout = (sigmoid_alpha_x + alpha * (silu_x - silu_x * sigmoid_alpha_x)) * dout
386
  silu_x_minus_product = cute.arch.fma_packed_f32x2(
387
  silu_x, (-sigmoid_alpha_x[0], -sigmoid_alpha_x[1]), silu_x
388
  )
@@ -400,10 +621,9 @@ def dswiglu_oai(
400
  def glu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
401
  """GLU: Gated Linear Unit
402
  glu(x, y) = sigmoid(x) * y
403
- Using tanh to compute sigmoid: sigmoid(x) = 0.5 * (1 + tanh(x/2))
404
  """
405
  if const_expr(not isinstance(x, tuple)):
406
- sigmoid_x = sigmoid(x) # FMUL, MUFU.TANH, then FFMA
407
  return sigmoid_x * y # FMUL
408
  else:
409
  sigmoid_x = sigmoid(x)
@@ -423,8 +643,7 @@ def dglu(
423
  - glu_out = sigmoid(x) * y
424
  """
425
  if const_expr(not isinstance(x, tuple)):
426
- # Compute sigmoid(x) using tanh: sigmoid(x) = 0.5 * (1 + tanh(x/2))
427
- sigmoid_x = sigmoid(x) # FMUL, MUFU.TANH, then FFMA
428
  sigmoid_x_dout = sigmoid_x * dout # FMUL
429
  glu_out = sigmoid_x * y # FMUL
430
  # dx = y * sigmoid(x) * (1 - sigmoid(x)) * dout
@@ -452,9 +671,9 @@ def reglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x
452
  reglu(x, y) = relu(x) * y = max(x, 0) * y
453
  """
454
  if const_expr(not isinstance(x, tuple)):
455
- return cute.arch.fmax(x, Float32(0.0)) * y
456
  else:
457
- relu_x = relu(x)
458
  return cute.arch.mul_packed_f32x2(relu_x, y)
459
 
460
 
@@ -473,15 +692,16 @@ def dreglu(
473
  """
474
  if const_expr(not isinstance(x, tuple)):
475
  x_pos = Boolean(x > 0)
476
- relu_x = cute.arch.fmax(x, Float32(0.0))
477
- dx = (dout * y) if x_pos else Float32(0.0)
 
478
  dy = dout * relu_x
479
  reglu_out = relu_x * y
480
  return dx, dy, reglu_out
481
  else:
482
  x0_pos = Boolean(x[0] > 0)
483
  x1_pos = Boolean(x[1] > 0)
484
- relu_x = relu(x)
485
  dout_y = cute.arch.mul_packed_f32x2(dout, y)
486
  dx = ((dout_y[0] if x0_pos else Float32(0.0)), (dout_y[1] if x1_pos else Float32(0.0)))
487
  dy = cute.arch.mul_packed_f32x2(dout, relu_x)
@@ -538,21 +758,28 @@ def dgeglu(
538
  act_fn_map = {
539
  None: None,
540
  "silu": silu,
 
541
  "relu": relu,
542
  "relu_sq": relu_sq,
543
  "gelu_tanh_approx": gelu_tanh_approx,
 
544
  }
545
 
546
  dact_fn_map = {
547
  None: None,
 
 
548
  "relu": drelu,
549
  "relu_sq": drelu_sq,
550
  "gelu_tanh_approx": dgelu_tanh_approx,
 
551
  }
552
 
553
  gate_fn_map = {
554
  "swiglu": swiglu,
 
555
  "swiglu_oai": swiglu_oai,
 
556
  "reglu": reglu,
557
  "geglu": geglu,
558
  "glu": glu,
@@ -560,7 +787,9 @@ gate_fn_map = {
560
 
561
  dgate_fn_map = {
562
  "swiglu": dswiglu,
 
563
  "swiglu_oai": dswiglu_oai,
 
564
  "reglu": dreglu,
565
  "geglu": dgeglu,
566
  "glu": dglu,
 
4
  from typing import Tuple
5
  from functools import partial
6
 
7
+ import cutlass
8
  import cutlass.cute as cute
9
  from cutlass import Float32, Boolean, const_expr
10
+ from cutlass.cutlass_dsl import dsl_user_op
11
+ from cutlass._mlir.dialects import nvvm
12
+ from cutlass._mlir_helpers import math as mlir_math
13
 
14
 
15
  F32_or_F32x2 = Float32 | Tuple[Float32, Float32]
 
23
 
24
 
25
  @dsl_user_op
26
+ def tanh(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
27
+ if const_expr(not isinstance(x, tuple)):
28
+ return cute.math.tanh(x, fastmath=True, loc=loc, ip=ip)
29
+ else:
30
+ return (
31
+ cute.math.tanh(x[0], fastmath=True, loc=loc, ip=ip),
32
+ cute.math.tanh(x[1], fastmath=True, loc=loc, ip=ip),
 
 
33
  )
 
34
 
35
 
36
  @dsl_user_op
37
+ def dtanh(
38
+ x: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
39
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
40
+ if const_expr(not isinstance(x, tuple)):
41
+ tanh_x = tanh(x, loc=loc, ip=ip)
42
+ dx = dout * (1.0 - tanh_x * tanh_x)
43
+ return dx, tanh_x
44
+ else:
45
+ tanh_x = tanh(x, loc=loc, ip=ip)
46
+ sech2_x = cute.arch.fma_packed_f32x2(tanh_x, (-tanh_x[0], -tanh_x[1]), (1.0, 1.0))
47
+ dx = cute.arch.mul_packed_f32x2(dout, sech2_x)
48
+ return dx, tanh_x
49
+
50
+
51
+ @dsl_user_op
52
+ def sigmoid_tanh(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
53
  if const_expr(not isinstance(x, tuple)):
54
  # return 0.5 + 0.5 * cute.math.tanh(0.5 * x, fastmath=True)
55
  return 0.5 + 0.5 * tanh(0.5 * x)
56
  else:
57
  x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x)
58
+ tanh_x_half = tanh(x_half)
59
  return cute.arch.fma_packed_f32x2(tanh_x_half, (0.5, 0.5), (0.5, 0.5))
60
 
61
 
62
+ @dsl_user_op
63
+ def sigmoid(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
64
+ log2_e = math.log2(math.e)
65
+ if const_expr(not isinstance(x, tuple)):
66
+ exp_neg_x = cute.math.exp2(x * (-log2_e), fastmath=True, loc=loc, ip=ip)
67
+ return mlir_math.rcp(exp_neg_x + 1.0, approx=True, ftz=True, loc=loc, ip=ip)
68
+ else:
69
+ neg_x = cute.arch.mul_packed_f32x2(x, (-log2_e, -log2_e))
70
+ exp_neg_x = (
71
+ cute.math.exp2(neg_x[0], fastmath=True, loc=loc, ip=ip),
72
+ cute.math.exp2(neg_x[1], fastmath=True, loc=loc, ip=ip),
73
+ )
74
+ denom = cute.arch.add_packed_f32x2(exp_neg_x, (1.0, 1.0))
75
+ return (
76
+ mlir_math.rcp(denom[0], approx=True, ftz=True, loc=loc, ip=ip),
77
+ mlir_math.rcp(denom[1], approx=True, ftz=True, loc=loc, ip=ip),
78
+ )
79
+
80
+
81
  @dsl_user_op
82
  def dsigmoid_from_output(out: Float32, dout: Float32, *, loc=None, ip=None) -> Float32:
83
  # return dout * out * (1.0 - out)
 
87
  @dsl_user_op
88
  def relu(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
89
  if const_expr(not isinstance(x, tuple)):
90
+ return cutlass.max(x, Float32(0.0), loc=loc, ip=ip)
91
  else:
92
+ return relu(x[0], loc=loc, ip=ip), relu(x[1], loc=loc, ip=ip)
93
 
94
 
95
  @dsl_user_op
 
99
  ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
100
  if const_expr(not isinstance(x, tuple)):
101
  x_pos = Boolean(x > 0)
102
+ return dout if x_pos else Float32(0.0), relu(x, loc=loc, ip=ip)
103
  else:
104
  x0_pos = Boolean(x[0] > 0)
105
  x1_pos = Boolean(x[1] > 0)
 
110
  @dsl_user_op
111
  def relu_sq(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
112
  if const_expr(not isinstance(x, tuple)):
113
+ return relu(x, loc=loc, ip=ip) * x
114
  else:
115
+ relu_x = relu(x, loc=loc, ip=ip)
116
  return cute.arch.mul_packed_f32x2(relu_x, x)
117
 
118
 
 
150
  sqrt_2_over_pi = math.sqrt(2 / math.pi) # ~0.797885
151
  sqrt_2_over_pi_coeff = 0.044715 * sqrt_2_over_pi # ~0.0356774
152
  if const_expr(not isinstance(x, tuple)):
153
+ x_sq = x * x
154
+ z = x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * x_sq)
155
+ tanh_z = tanh(z)
156
+ return 0.5 * (x * tanh_z + x)
 
 
157
  else:
158
  x_sq = cute.arch.mul_packed_f32x2(x, x)
159
  x_sq_scaled = cute.arch.fma_packed_f32x2(
160
  x_sq, (sqrt_2_over_pi_coeff, sqrt_2_over_pi_coeff), (sqrt_2_over_pi, sqrt_2_over_pi)
161
  )
162
  z = cute.arch.mul_packed_f32x2(x, x_sq_scaled)
163
+ tanh_z = tanh(z)
164
  x_tanh_z = cute.arch.fma_packed_f32x2(tanh_z, x, x)
165
  return cute.arch.mul_packed_f32x2((0.5, 0.5), x_tanh_z)
166
 
 
193
 
194
  # Compute gradient
195
  # sech^2(z) = 1 - tanh^2(z)
196
+ # Keep this as a multiply-add expression so vectorize=True lowers to
197
+ # FFMA2 like the explicit F32x2 path; `1.0 - tanh_z * tanh_z` costs
198
+ # an extra FADD2/FMUL2 pair per vector.
199
+ sech2_z = tanh_z * (-tanh_z) + 1.0
200
  # dz/dx = c1 + 3 * c2 * x^2
201
  dz_dx = sqrt_2_over_pi + sqrt_2_over_pi_coeff_3 * x_sq
202
  # d/dx[gelu(x)] = 0.5 * (1 + tanh(z)) + 0.5 * x * sech^2(z) * dz/dx
203
+ sech2_dz_dx = sech2_z * dz_dx
204
+ x_sech2_dz_dx = x * sech2_dz_dx
205
+ dgelu = x_sech2_dz_dx * 0.5 + half_tanh_z_plus_one
206
 
207
  dx = dout * dgelu
208
  return dx, gelu_out
 
213
  x_sq, (sqrt_2_over_pi_coeff, sqrt_2_over_pi_coeff), (sqrt_2_over_pi, sqrt_2_over_pi)
214
  )
215
  z = cute.arch.mul_packed_f32x2(x, x_sq_scaled)
216
+ tanh_z = tanh(z)
217
  half_tanh_z_plus_one = cute.arch.fma_packed_f32x2(tanh_z, (0.5, 0.5), (0.5, 0.5))
218
  gelu_out = cute.arch.mul_packed_f32x2(x, half_tanh_z_plus_one)
219
 
 
272
 
273
 
274
  @dsl_user_op
275
+ def silu(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
276
+ """
277
+ silu(x) = x * sigmoid(x) = x * rcp(1 + exp(-x)).
278
+ """
279
+ if const_expr(not isinstance(x, tuple)):
280
+ return x * sigmoid(x, loc=loc, ip=ip)
281
+ else:
282
+ return cute.arch.mul_packed_f32x2(x, sigmoid(x, loc=loc, ip=ip))
283
+
284
+
285
+ @dsl_user_op
286
+ def silu_tanh(x: F32_or_F32x2, *, already_halved: bool = False, loc=None, ip=None) -> F32_or_F32x2:
287
  """
288
  silu(x) = x * sigmoid(x) = x * (1 + tanh(x / 2)) / 2 = (0.5 * x) * tanh(0.5 * x) + (0.5 * x)
289
  This compiles down to 3 SASS instructions: FMUL to get 0.5 * x, MUFU.TANH, and FFMA.
 
294
  return x_half * tanh(x_half) + x_half
295
  else:
296
  x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x) if const_expr(not already_halved) else x
297
+ tanh_x_half = tanh(x_half)
298
  return cute.arch.fma_packed_f32x2(x_half, tanh_x_half, x_half)
299
 
300
 
301
+ @dsl_user_op
302
+ def dsilu(
303
+ x: F32_or_F32x2,
304
+ dout: F32_or_F32x2,
305
+ *,
306
+ loc=None,
307
+ ip=None,
308
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
309
+ """
310
+ SiLU backward pass: computes d_silu(x) * dout and recomputes silu(x).
311
+
312
+ d_silu(x) = sigmoid(x) * (1 + x * (1 - sigmoid(x))).
313
+ """
314
+ if const_expr(not isinstance(x, tuple)):
315
+ sigmoid_x = sigmoid(x, loc=loc, ip=ip)
316
+ silu_x = x * sigmoid_x
317
+ # This form vectorizes cleanly with cutlass.range(..., vectorize=True):
318
+ # FADD2 (1 - sigmoid_x), FFMA2 (silu_x * tmp + sigmoid_x), FMUL2 (* dout).
319
+ d_silu_x_dout = (sigmoid_x + silu_x * (1.0 - sigmoid_x)) * dout
320
+ return d_silu_x_dout, silu_x
321
+ else:
322
+ sigmoid_x = sigmoid(x)
323
+ silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_x)
324
+ sigmoid_x_minus_silu_x_sigmoid_x = cute.arch.fma_packed_f32x2(
325
+ sigmoid_x, (-silu_x[0], -silu_x[1]), sigmoid_x
326
+ )
327
+ sigmoid_x_minus_silu_x_sigmoid_x_plus_silu_x = cute.arch.add_packed_f32x2(
328
+ sigmoid_x_minus_silu_x_sigmoid_x, silu_x
329
+ )
330
+ d_silu_x_dout = cute.arch.mul_packed_f32x2(
331
+ sigmoid_x_minus_silu_x_sigmoid_x_plus_silu_x, dout
332
+ )
333
+ return d_silu_x_dout, silu_x
334
+
335
+
336
+ @dsl_user_op
337
+ def dsilu_tanh(
338
+ x: F32_or_F32x2,
339
+ dout: F32_or_F32x2,
340
+ *,
341
+ already_halved: bool = False,
342
+ loc=None,
343
+ ip=None,
344
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
345
+ """
346
+ SiLU backward using sigmoid(x) = 0.5 * (1 + tanh(0.5 * x)).
347
+ """
348
+ if const_expr(not isinstance(x, tuple)):
349
+ if const_expr(not already_halved):
350
+ x_half = 0.5 * x
351
+ tanh_x_half = tanh(x_half)
352
+ sigmoid_x = 0.5 * tanh_x_half + 0.5
353
+ silu_x = x_half * tanh_x_half + x_half
354
+ else:
355
+ tanh_x = tanh(x)
356
+ sigmoid_x = 0.5 * tanh_x + 0.5
357
+ silu_x = x * tanh_x + x
358
+ d_silu_x_dout = (sigmoid_x + silu_x * (1.0 - sigmoid_x)) * dout
359
+ return d_silu_x_dout, silu_x
360
+ else:
361
+ if const_expr(not already_halved):
362
+ x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x)
363
+ tanh_x_half = tanh(x_half)
364
+ sigmoid_x = cute.arch.fma_packed_f32x2(tanh_x_half, (0.5, 0.5), (0.5, 0.5))
365
+ silu_x = cute.arch.fma_packed_f32x2(x_half, tanh_x_half, x_half)
366
+ else:
367
+ tanh_x = tanh(x)
368
+ sigmoid_x = cute.arch.fma_packed_f32x2(tanh_x, (0.5, 0.5), (0.5, 0.5))
369
+ silu_x = cute.arch.fma_packed_f32x2(x, tanh_x, x)
370
+ sigmoid_x_minus_silu_x_sigmoid_x = cute.arch.fma_packed_f32x2(
371
+ sigmoid_x, (-silu_x[0], -silu_x[1]), sigmoid_x
372
+ )
373
+ sigmoid_x_minus_silu_x_sigmoid_x_plus_silu_x = cute.arch.add_packed_f32x2(
374
+ sigmoid_x_minus_silu_x_sigmoid_x, silu_x
375
+ )
376
+ d_silu_x_dout = cute.arch.mul_packed_f32x2(
377
+ sigmoid_x_minus_silu_x_sigmoid_x_plus_silu_x, dout
378
+ )
379
+ return d_silu_x_dout, silu_x
380
+
381
+
382
  @dsl_user_op
383
  def swiglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
384
  if const_expr(not isinstance(x, tuple)):
 
387
  return cute.arch.mul_packed_f32x2(silu(x), y)
388
 
389
 
390
+ @dsl_user_op
391
+ def swiglu_tanh(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
392
+ if const_expr(not isinstance(x, tuple)):
393
+ return silu_tanh(x) * y
394
+ else:
395
+ return cute.arch.mul_packed_f32x2(silu_tanh(x), y)
396
+
397
+
398
  @dsl_user_op
399
  def dswiglu(
400
  x: F32_or_F32x2,
401
  y: F32_or_F32x2,
402
  dout: F32_or_F32x2,
403
  *,
 
404
  loc=None,
405
  ip=None,
406
  ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
 
415
  to use FFMA instead of FADD and FMUL).
416
  """
417
  if const_expr(not isinstance(x, tuple)):
418
+ sigmoid_x = sigmoid(x)
419
+ silu_x = x * sigmoid_x # FMUL
 
 
 
 
 
 
 
420
  silu_x_dout = silu_x * dout # FMUL
421
  # d_silu(x) * dout
422
  # = sigmoid_x * (1 + x * (1 - sigmoid_x)) * dout
 
424
  # = (sigmoid_x + silu_x * (1 - sigmoid_x)) * dout
425
  # = (sigmoid_x + silu_x - silu_x * sigmoid_x) * dout
426
  # = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x * dout
427
+ # This form lets ptxas recover the same two packed FFMA instructions
428
+ # as the explicit F32x2 path while reusing silu_x_dout for dy.
429
+ d_silu_x_dout = (sigmoid_x + (-silu_x) * sigmoid_x) * dout + silu_x_dout
430
  dx = d_silu_x_dout * y # FMUL
431
  dy = silu_x_dout
432
  swiglu_out = silu_x * y # FMUL
 
433
  return dx, dy, swiglu_out
434
  else:
435
  # Compute sigmoid(x) and silu(x)
436
+ sigmoid_x = sigmoid(x)
437
+ silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_x)
438
+ silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
439
+ # d_silu(x) * dout = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x * dout
440
+ sigmoid_x_minus_silu_x_sigmoid_x = cute.arch.fma_packed_f32x2(
441
+ sigmoid_x, (-silu_x[0], -silu_x[1]), sigmoid_x
442
+ )
443
+ d_silu_x_dout = cute.arch.fma_packed_f32x2(
444
+ sigmoid_x_minus_silu_x_sigmoid_x, dout, silu_x_dout
445
+ )
446
+ dx = cute.arch.mul_packed_f32x2(d_silu_x_dout, y)
447
+ dy = silu_x_dout
448
+ swiglu_out = cute.arch.mul_packed_f32x2(silu_x, y)
449
+ return dx, dy, swiglu_out
450
+
451
+
452
+ @dsl_user_op
453
+ def dswiglu_tanh(
454
+ x: F32_or_F32x2,
455
+ y: F32_or_F32x2,
456
+ dout: F32_or_F32x2,
457
+ *,
458
+ already_halved: bool = False,
459
+ loc=None,
460
+ ip=None,
461
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
462
+ """
463
+ SwiGLU backward using sigmoid(x) = 0.5 * (1 + tanh(0.5 * x)).
464
+ """
465
+ if const_expr(not isinstance(x, tuple)):
466
+ if const_expr(not already_halved):
467
+ sigmoid_x = sigmoid_tanh(x)
468
+ silu_x = x * sigmoid_x # FMUL
469
+ else:
470
+ tanh_x = tanh(x)
471
+ sigmoid_x = 0.5 * tanh_x + 0.5
472
+ silu_x = x * tanh_x + x
473
+ silu_x_dout = silu_x * dout
474
+ d_silu_x_dout = (sigmoid_x + (-silu_x) * sigmoid_x) * dout + silu_x_dout
475
+ dx = d_silu_x_dout * y
476
+ dy = silu_x_dout
477
+ swiglu_out = silu_x * y
478
+ # Overall it's 1 MUFU.TANH, 5 FMUL, 3 FFMA
479
+ return dx, dy, swiglu_out
480
+ else:
481
  if const_expr(not already_halved):
482
+ sigmoid_x = sigmoid_tanh(x)
483
  silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_x)
484
  else:
485
+ tanh_x = tanh(x)
486
  sigmoid_x = cute.arch.fma_packed_f32x2(tanh_x, (0.5, 0.5), (0.5, 0.5))
487
  silu_x = cute.arch.fma_packed_f32x2(x, tanh_x, x)
488
  silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
 
506
  """The swiglu variant used in gpt-oss, which has a scaling factor on x and bias of 1 to y.
507
  https://github.com/openai/gpt-oss/blob/7be9334950053a888e24887a57dac797a17d6e00/gpt_oss/torch/model.py#L249
508
  x * sigmoid(alpha * x) * (y + 1)
 
509
  """
510
+ if const_expr(not isinstance(x, tuple)):
511
+ sigmoid_alpha_x = sigmoid(alpha * x)
512
+ silu_x = x * sigmoid_alpha_x
513
+ return silu_x * y + silu_x
514
+ else:
515
+ alpha_x = cute.arch.mul_packed_f32x2((alpha, alpha), x)
516
+ sigmoid_alpha_x = sigmoid(alpha_x)
517
+ silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_alpha_x)
518
+ return cute.arch.fma_packed_f32x2(silu_x, y, silu_x)
519
+
520
+
521
+ @dsl_user_op
522
+ def swiglu_oai_tanh(
523
+ x: F32_or_F32x2, y: F32_or_F32x2, alpha: float = 1.702, *, loc=None, ip=None
524
+ ) -> F32_or_F32x2:
525
+ """Tanh-based swiglu_oai kept for SASS/accuracy comparison."""
526
  if const_expr(not isinstance(x, tuple)):
527
  x_half = 0.5 * x
 
528
  silu_x = x_half * tanh(alpha * x_half) + x_half
529
  return silu_x * y + silu_x
530
  else:
531
  x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x)
532
  alpha_x_half = cute.arch.mul_packed_f32x2((alpha, alpha), x_half)
533
+ tanh_alpha_x_half = tanh(alpha_x_half)
534
  silu_x = cute.arch.fma_packed_f32x2(x_half, tanh_alpha_x_half, x_half)
535
  return cute.arch.fma_packed_f32x2(silu_x, y, silu_x)
536
 
 
548
  d/dx[x * sigmoid(alpha * x)] = sigmoid(alpha * x) + alpha * x * sigmoid(alpha * x) * (1 - sigmoid(alpha * x))
549
  """
550
  if const_expr(not isinstance(x, tuple)):
551
+ sigmoid_alpha_x = sigmoid(alpha * x)
552
+ silu_x = x * sigmoid_alpha_x
553
+ silu_x_dout = silu_x * dout
554
+ # Keep this as two multiply-add expressions. With vectorize=True this
555
+ # matches the explicit F32x2 path; spelling it as (1 - sigmoid) costs
556
+ # an extra FADD2/FMUL2 pair per vector.
557
+ silu_x_minus_product = silu_x * (-sigmoid_alpha_x) + silu_x
558
+ sigmoid_plus_alpha_diff = silu_x_minus_product * alpha + sigmoid_alpha_x
559
+ d_silu_x_dout = sigmoid_plus_alpha_diff * dout
560
+ dx = d_silu_x_dout * y + d_silu_x_dout
561
+ dy = silu_x_dout
562
+ swiglu_out = silu_x * y + silu_x
563
+ return dx, dy, swiglu_out
564
+ else:
565
+ alpha_x = cute.arch.mul_packed_f32x2((alpha, alpha), x)
566
+ sigmoid_alpha_x = sigmoid(alpha_x)
567
+ silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_alpha_x)
568
+ silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
569
+ silu_x_minus_product = cute.arch.fma_packed_f32x2(
570
+ silu_x, (-sigmoid_alpha_x[0], -sigmoid_alpha_x[1]), silu_x
571
+ )
572
+ sigmoid_plus_alpha_diff = cute.arch.fma_packed_f32x2(
573
+ (alpha, alpha), silu_x_minus_product, sigmoid_alpha_x
574
+ )
575
+ d_silu_x_dout = cute.arch.mul_packed_f32x2(sigmoid_plus_alpha_diff, dout)
576
+ dx = cute.arch.fma_packed_f32x2(d_silu_x_dout, y, d_silu_x_dout)
577
+ dy = silu_x_dout
578
+ swiglu_out = cute.arch.fma_packed_f32x2(silu_x, y, silu_x)
579
+ return dx, dy, swiglu_out
580
+
581
+
582
+ @dsl_user_op
583
+ def dswiglu_oai_tanh(
584
+ x: F32_or_F32x2, y: F32_or_F32x2, dout: F32_or_F32x2, alpha: float = 1.702, *, loc=None, ip=None
585
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
586
+ """Tanh-based dswiglu_oai kept for SASS/accuracy comparison."""
587
+ if const_expr(not isinstance(x, tuple)):
588
+ alpha_x_half = (0.5 * alpha) * x
589
  sigmoid_alpha_x = 0.5 + 0.5 * tanh(alpha_x_half)
590
+ silu_x = x * sigmoid_alpha_x
591
+ silu_x_dout = silu_x * dout
592
+ # Same spelling as dswiglu_oai: this preserves the packed FFMA2 chain
593
+ # under cutlass.range(..., vectorize=True).
594
+ silu_x_minus_product = silu_x * (-sigmoid_alpha_x) + silu_x
595
+ sigmoid_plus_alpha_diff = silu_x_minus_product * alpha + sigmoid_alpha_x
596
+ d_silu_x_dout = sigmoid_plus_alpha_diff * dout
597
+ dx = d_silu_x_dout * y + d_silu_x_dout
598
  dy = silu_x_dout
599
+ swiglu_out = silu_x * y + silu_x
 
600
  return dx, dy, swiglu_out
601
  else:
 
602
  alpha_x_half = cute.arch.mul_packed_f32x2(((0.5 * alpha), (0.5 * alpha)), x)
603
+ tanh_alpha_x_half = tanh(alpha_x_half)
604
  sigmoid_alpha_x = cute.arch.fma_packed_f32x2(tanh_alpha_x_half, (0.5, 0.5), (0.5, 0.5))
605
  silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_alpha_x)
606
  silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
 
607
  silu_x_minus_product = cute.arch.fma_packed_f32x2(
608
  silu_x, (-sigmoid_alpha_x[0], -sigmoid_alpha_x[1]), silu_x
609
  )
 
621
  def glu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
622
  """GLU: Gated Linear Unit
623
  glu(x, y) = sigmoid(x) * y
 
624
  """
625
  if const_expr(not isinstance(x, tuple)):
626
+ sigmoid_x = sigmoid(x)
627
  return sigmoid_x * y # FMUL
628
  else:
629
  sigmoid_x = sigmoid(x)
 
643
  - glu_out = sigmoid(x) * y
644
  """
645
  if const_expr(not isinstance(x, tuple)):
646
+ sigmoid_x = sigmoid(x)
 
647
  sigmoid_x_dout = sigmoid_x * dout # FMUL
648
  glu_out = sigmoid_x * y # FMUL
649
  # dx = y * sigmoid(x) * (1 - sigmoid(x)) * dout
 
671
  reglu(x, y) = relu(x) * y = max(x, 0) * y
672
  """
673
  if const_expr(not isinstance(x, tuple)):
674
+ return relu(x, loc=loc, ip=ip) * y
675
  else:
676
+ relu_x = relu(x, loc=loc, ip=ip)
677
  return cute.arch.mul_packed_f32x2(relu_x, y)
678
 
679
 
 
692
  """
693
  if const_expr(not isinstance(x, tuple)):
694
  x_pos = Boolean(x > 0)
695
+ relu_x = relu(x, loc=loc, ip=ip)
696
+ dout_y = dout * y
697
+ dx = dout_y if x_pos else Float32(0.0)
698
  dy = dout * relu_x
699
  reglu_out = relu_x * y
700
  return dx, dy, reglu_out
701
  else:
702
  x0_pos = Boolean(x[0] > 0)
703
  x1_pos = Boolean(x[1] > 0)
704
+ relu_x = relu(x, loc=loc, ip=ip)
705
  dout_y = cute.arch.mul_packed_f32x2(dout, y)
706
  dx = ((dout_y[0] if x0_pos else Float32(0.0)), (dout_y[1] if x1_pos else Float32(0.0)))
707
  dy = cute.arch.mul_packed_f32x2(dout, relu_x)
 
758
  act_fn_map = {
759
  None: None,
760
  "silu": silu,
761
+ "silu-tanh": silu_tanh,
762
  "relu": relu,
763
  "relu_sq": relu_sq,
764
  "gelu_tanh_approx": gelu_tanh_approx,
765
+ "tanh": tanh,
766
  }
767
 
768
  dact_fn_map = {
769
  None: None,
770
+ "silu": dsilu,
771
+ "silu-tanh": dsilu_tanh,
772
  "relu": drelu,
773
  "relu_sq": drelu_sq,
774
  "gelu_tanh_approx": dgelu_tanh_approx,
775
+ "tanh": dtanh,
776
  }
777
 
778
  gate_fn_map = {
779
  "swiglu": swiglu,
780
+ "swiglu-tanh": swiglu_tanh,
781
  "swiglu_oai": swiglu_oai,
782
+ "swiglu_oai-tanh": swiglu_oai_tanh,
783
  "reglu": reglu,
784
  "geglu": geglu,
785
  "glu": glu,
 
787
 
788
  dgate_fn_map = {
789
  "swiglu": dswiglu,
790
+ "swiglu-tanh": dswiglu_tanh,
791
  "swiglu_oai": dswiglu_oai,
792
+ "swiglu_oai-tanh": dswiglu_oai_tanh,
793
  "reglu": dreglu,
794
  "geglu": dgeglu,
795
  "glu": dglu,
build/torch-cuda/quack/autotuner.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
 
5
  import builtins
6
  import os
 
7
  import time
8
  import inspect
9
  import base64
@@ -12,6 +13,11 @@ import json
12
  from pathlib import Path
13
  from functools import cached_property, partial
14
  from typing import Dict, Tuple, List, Optional, Any
 
 
 
 
 
15
 
16
  import torch
17
  from torch import Tensor
@@ -25,29 +31,6 @@ PACKAGE_NAME = "quack"
25
  VERSION = __version__
26
 
27
 
28
- def _get_current_cuda_device() -> str | None:
29
- """Return the physical CUDA device identifier for the current process.
30
-
31
- Maps the logical ``torch.cuda.current_device()`` index through
32
- ``CUDA_VISIBLE_DEVICES`` (if set) so the result is valid as a
33
- standalone ``CUDA_VISIBLE_DEVICES`` value (handles integer IDs,
34
- GPU UUIDs, and MIG IDs).
35
-
36
- Returns ``None`` if CUDA is not initialized or the device cannot
37
- be determined.
38
- """
39
- if not (torch.cuda.is_available() and torch.cuda.is_initialized()):
40
- return None
41
- logical_device = torch.cuda.current_device()
42
- parent_visible = os.environ.get("CUDA_VISIBLE_DEVICES")
43
- if parent_visible is not None:
44
- visible_devices = [d.strip() for d in parent_visible.split(",")]
45
- if logical_device < len(visible_devices):
46
- return visible_devices[logical_device]
47
- return None
48
- return str(logical_device)
49
-
50
-
51
  def get_home_dir():
52
  return os.getenv(f"{PACKAGE_NAME.upper()}_HOME", Path.home())
53
 
@@ -75,6 +58,14 @@ def _base32(key):
75
  return base64.b32encode(bytes.fromhex(key)).decode("utf-8").rstrip("=")
76
 
77
 
 
 
 
 
 
 
 
 
78
  def _gpu_warmup(duration_ms=200):
79
  """Saturate the GPU to reach thermal steady-state before benchmarking.
80
 
@@ -91,6 +82,21 @@ def _gpu_warmup(duration_ms=200):
91
  torch.cuda.synchronize()
92
 
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  class Autotuner:
95
  def __init__(
96
  self,
@@ -163,146 +169,6 @@ class Autotuner:
163
  return partial(triton.testing.do_bench, warmup=5, rep=25)
164
  return self._do_bench
165
 
166
- def _precompile(self, *args, configs, **kwargs):
167
- """Pre-compile all configs in parallel subprocesses to populate .o cache.
168
-
169
- cute.compile() is not thread-safe (MLIR thread-local state) and fork after
170
- CUDA init causes segfaults. So we spawn persistent subprocess workers: each
171
- has its own CUDA context, creates FakeTensors matching the parent's tensor
172
- metadata, and compiles with COMPILE_ONLY=True. Workers stay alive to amortize
173
- import overhead across multiple configs. The parent then loads instantly from
174
- the .o cache during benchmarking.
175
- """
176
- from .cache_utils import CACHE_ENABLED
177
-
178
- if not CACHE_ENABLED:
179
- return
180
-
181
- max_workers = min(len(configs), int(os.getenv("QUACK_COMPILE_WORKERS", "8")))
182
- if max_workers <= 1:
183
- return
184
-
185
- # Quick check: compile first config in-process. If it loads from .o cache
186
- # (<0.5s), the rest are likely cached too — skip spawning workers.
187
- t_check = time.time()
188
- try:
189
- current = dict(kwargs, **configs[0].all_kwargs())
190
- self.fn(*args, **current)
191
- except Exception:
192
- pass
193
- if time.time() - t_check < 0.5:
194
- return
195
-
196
- verbose = os.getenv(f"{PACKAGE_NAME.upper()}_PRINT_AUTOTUNING", None) == "1"
197
- if verbose:
198
- print(f"Pre-compiling {len(configs)} configs with {max_workers} workers")
199
- t0 = time.time()
200
-
201
- import pickle
202
- import struct
203
- import subprocess
204
- import sys
205
-
206
- def _send(stream, msg):
207
- data = pickle.dumps(msg)
208
- stream.write(struct.pack("<I", len(data)))
209
- stream.write(data)
210
- stream.flush()
211
-
212
- def _recv(stream):
213
- header = stream.read(4)
214
- if len(header) < 4:
215
- return None
216
- length = struct.unpack("<I", header)[0]
217
- return pickle.loads(stream.read(length)) if length else None
218
-
219
- # Serialize tensor metadata
220
- tensor_meta = []
221
- for arg in args:
222
- if isinstance(arg, Tensor):
223
- tensor_meta.append(
224
- {
225
- "shape": list(arg.shape),
226
- "stride": list(arg.stride()),
227
- "dtype": str(arg.dtype),
228
- }
229
- )
230
- else:
231
- tensor_meta.append(arg)
232
-
233
- fn_module = self.fn.__module__
234
- fn_qualname = self.fn.__qualname__
235
-
236
- # Restrict worker subprocesses to the parent's current CUDA device.
237
- # Without this, all workers default to cuda:0 and their CUDA context
238
- # initialization can OOM when many ranks share a node.
239
- worker_env = os.environ.copy()
240
- current_device = _get_current_cuda_device()
241
- if current_device is not None:
242
- worker_env["CUDA_VISIBLE_DEVICES"] = current_device
243
-
244
- # Launch persistent worker pool. When vendored under sonic_moe (loaded
245
- # via kernels.get_kernel), the quack package isn't importable as a
246
- # top-level module, so invoke the worker via its fully-qualified dotted
247
- # path and inject PYTHONPATH so the subprocess can import it.
248
- worker_module = __package__ + "._compile_worker" if __package__ else "quack._compile_worker"
249
- if __package__:
250
- import importlib.util
251
- spec = importlib.util.find_spec(__package__.split(".")[0])
252
- if spec is not None and spec.submodule_search_locations:
253
- pkg_parent = os.path.dirname(list(spec.submodule_search_locations)[0])
254
- existing_pp = worker_env.get("PYTHONPATH", "")
255
- worker_env["PYTHONPATH"] = (
256
- f"{pkg_parent}{os.pathsep}{existing_pp}" if existing_pp else pkg_parent
257
- )
258
-
259
- workers = []
260
- for _ in range(max_workers):
261
- p = subprocess.Popen(
262
- [sys.executable, "-m", worker_module],
263
- stdin=subprocess.PIPE,
264
- stdout=subprocess.PIPE,
265
- stderr=subprocess.DEVNULL if not verbose else None,
266
- env=worker_env,
267
- )
268
- ready = _recv(p.stdout)
269
- if ready != "READY":
270
- p.kill()
271
- continue
272
- workers.append(p)
273
-
274
- if not workers:
275
- return
276
-
277
- # Round-robin dispatch configs to workers
278
- pending = [0] * len(workers)
279
- for i, config in enumerate(configs):
280
- w = workers[i % len(workers)]
281
- _send(
282
- w.stdin,
283
- {
284
- "fn_module": fn_module,
285
- "fn_qualname": fn_qualname,
286
- "tensor_meta": tensor_meta,
287
- "kwargs": kwargs,
288
- "config_kwargs": config.all_kwargs(),
289
- },
290
- )
291
- pending[i % len(workers)] += 1
292
-
293
- # Collect all results
294
- for wi, w in enumerate(workers):
295
- for _ in range(pending[wi]):
296
- _recv(w.stdout)
297
-
298
- # Shutdown workers (close stdin → worker exits)
299
- for w in workers:
300
- w.stdin.close()
301
- w.wait()
302
-
303
- if verbose:
304
- print(f"Pre-compilation done in {time.time() - t0:.1f}s")
305
-
306
  def _bench(self, *args, config, **meta):
307
  verbose = os.environ.get(f"{PACKAGE_NAME.upper()}_PRINT_AUTOTUNING", None) == "1"
308
  if verbose:
@@ -320,6 +186,45 @@ class Autotuner:
320
  current = dict(meta, **config.all_kwargs())
321
  full_nargs = {**self.nargs, **current}
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  def kernel_call():
324
  if self.pre_hook is not None:
325
  self.pre_hook(full_nargs)
@@ -406,17 +311,118 @@ class Autotuner:
406
 
407
  @torch.compiler.disable # Don't want any tracing here
408
  def benchmark():
409
- self._precompile(*args, configs=pruned_configs, **kwargs)
410
- _gpu_warmup()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  bench_start = time.time()
412
- timings = {
413
- config: self._bench(*args, config=config, **kwargs)
414
- for config in pruned_configs
415
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  bench_end = time.time()
417
- if os.getenv(f"{PACKAGE_NAME.upper()}_PRINT_AUTOTUNING", None) == "1":
418
  for config, time_ in timings.items():
419
  print(f"[{config}] -> {time_[0]:.3f}ms")
 
 
 
 
 
 
 
 
 
 
420
  self.bench_time = bench_end - bench_start
421
  self.cache[key] = builtins.min(timings, key=timings.get)
422
  self.configs_timings = timings
 
4
 
5
  import builtins
6
  import os
7
+ import sys
8
  import time
9
  import inspect
10
  import base64
 
13
  from pathlib import Path
14
  from functools import cached_property, partial
15
  from typing import Dict, Tuple, List, Optional, Any
16
+ from .bench.bench_utils import (
17
+ _bench_cuda_graph_l2_rotate,
18
+ _clone_l2_rotate_inputs,
19
+ _pick_l2_rotate_count,
20
+ )
21
 
22
  import torch
23
  from torch import Tensor
 
31
  VERSION = __version__
32
 
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  def get_home_dir():
35
  return os.getenv(f"{PACKAGE_NAME.upper()}_HOME", Path.home())
36
 
 
58
  return base64.b32encode(bytes.fromhex(key)).decode("utf-8").rstrip("=")
59
 
60
 
61
+ #: How long a deferred config may wait on its pool compile before the bench
62
+ #: loop stops trusting the pool and benches it with the pool suppressed
63
+ #: (in-process compile). Guards against a wedged worker / a foreign flock
64
+ #: holder that never produces the .o; without it a permanently-"pending"
65
+ #: sha would rotate forever. Tests override this.
66
+ _POOL_WEDGE_TIMEOUT_S = 300.0
67
+
68
+
69
  def _gpu_warmup(duration_ms=200):
70
  """Saturate the GPU to reach thermal steady-state before benchmarking.
71
 
 
82
  torch.cuda.synchronize()
83
 
84
 
85
+ # ---------------------------------------------------------------------------
86
+ # Candidate-config compilation
87
+ #
88
+ # There is no separate precompile phase: the bench loop in ``benchmark()``
89
+ # (inside ``Autotuner.__call__``) runs under ``pool_scope()`` from
90
+ # quack.cache.async_compile. A config whose kernel misses the .o cache
91
+ # raises ``CompilePending`` from jit_cache after shipping the pickled
92
+ # ``_compile_*`` key to a CPU worker; the loop rotates that config to the
93
+ # back and benches whichever config is ready. Total wall stays
94
+ # max(parallel_compile, serial_bench), key discovery uses the real tensors
95
+ # in-process, and workers never launch kernels (they call the tensor-free
96
+ # ``_compile_*`` functions directly).
97
+ # ---------------------------------------------------------------------------
98
+
99
+
100
  class Autotuner:
101
  def __init__(
102
  self,
 
169
  return partial(triton.testing.do_bench, warmup=5, rep=25)
170
  return self._do_bench
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  def _bench(self, *args, config, **meta):
173
  verbose = os.environ.get(f"{PACKAGE_NAME.upper()}_PRINT_AUTOTUNING", None) == "1"
174
  if verbose:
 
186
  current = dict(meta, **config.all_kwargs())
187
  full_nargs = {**self.nargs, **current}
188
 
189
+ # Default path: L2-cold CUDA-graph round-robin bench. ``__call__``
190
+ # sets ``self._l2_cold_arg_sets`` / ``self._l2_cold_kwarg_sets`` to
191
+ # pre-cloned (args, kwargs) sets once per shape (reused across all
192
+ # configs). Round-robin over fresh sets keeps the kernel measured
193
+ # under the cache-cold conditions that match production access
194
+ # patterns, so the autotuner picks configs that win at the same
195
+ # workload the user actually runs.
196
+ l2_cold_arg_sets = getattr(self, "_l2_cold_arg_sets", None)
197
+ l2_cold_kwarg_sets = getattr(self, "_l2_cold_kwarg_sets", None)
198
+ has_hooks = self.pre_hook is not None or self.post_hook is not None
199
+ use_l2_cold = (
200
+ self._do_bench is None
201
+ and l2_cold_arg_sets is not None
202
+ and l2_cold_kwarg_sets is not None
203
+ and not has_hooks
204
+ )
205
+
206
+ if use_l2_cold:
207
+ try:
208
+ return _bench_cuda_graph_l2_rotate(
209
+ self.fn,
210
+ l2_cold_arg_sets,
211
+ l2_cold_kwarg_sets,
212
+ extra_kwargs=config.all_kwargs(),
213
+ quantiles=(0.5, 0.2, 0.8),
214
+ )
215
+ except (RuntimeError, MemoryError) as e:
216
+ # Narrow catch: only swallow GPU-side failures (smem
217
+ # overflow, kernel launch errors, OOM). Programming errors
218
+ # (TypeError, AssertionError, ValueError from conflict check
219
+ # above) propagate so the user sees them.
220
+ if verbose:
221
+ print(f"Autotuning failed with {type(e).__name__}: {e}")
222
+ return [float("inf"), float("inf"), float("inf")]
223
+
224
+ # Legacy path: triton.testing.do_bench or user-supplied do_bench.
225
+ # Used when (a) a custom do_bench was passed via the decorator's
226
+ # ``do_bench=`` arg, or (b) pre/post hooks are configured (the
227
+ # clone/restore inside hooks doesn't work under CUDA graph capture).
228
  def kernel_call():
229
  if self.pre_hook is not None:
230
  self.pre_hook(full_nargs)
 
311
 
312
  @torch.compiler.disable # Don't want any tracing here
313
  def benchmark():
314
+ # Compile/bench overlap via the async compile pool
315
+ # (quack.cache.async_compile): the bench loop runs inside
316
+ # pool_scope(). A config whose kernel isn't compiled yet
317
+ # raises CompilePending from jit_cache (after shipping the
318
+ # key to a CPU worker); the loop rotates it to the back
319
+ # and benches whichever config is ready, retrying once its
320
+ # .o lands. Discovery happens in-process with the real
321
+ # tensors (no fake-tensor reconstruction), and the pool
322
+ # workers replay the pickled _compile_* key directly --
323
+ # which never launches, by construction.
324
+ #
325
+ # CompilePending can only fire OUTSIDE CUDA graph capture:
326
+ # the L2-cold bench does priming launches before capture,
327
+ # and the legacy do_bench path warms up first, so a cold
328
+ # key raises at the first plain launch.
329
+ from collections import deque
330
+
331
+ from .cache.async_compile import (
332
+ CompilePending,
333
+ pool_scope,
334
+ suppress_pool,
335
+ )
336
+
337
  bench_start = time.time()
338
+ verbose = os.getenv(f"{PACKAGE_NAME.upper()}_PRINT_AUTOTUNING", None) == "1"
339
+ has_hooks = self.pre_hook is not None or self.post_hook is not None
340
+ timings = {}
341
+ _MAX_ATTEMPTS = 20
342
+ try:
343
+ _gpu_warmup()
344
+ # Pre-allocate cloned (args, kwargs) sets once per
345
+ # shape; the same sets are reused across all configs
346
+ # to avoid ~400x re-cloning. Skipped when hooks are
347
+ # present or a custom do_bench was supplied (legacy
348
+ # fallback in _bench).
349
+ if self._do_bench is None and not has_hooks:
350
+ try:
351
+ n_buffers = _pick_l2_rotate_count(args, kwargs)
352
+ arg_sets, kwarg_sets = _clone_l2_rotate_inputs(
353
+ args, kwargs, n_buffers
354
+ )
355
+ self._l2_cold_arg_sets = arg_sets
356
+ self._l2_cold_kwarg_sets = kwarg_sets
357
+ except (RuntimeError, MemoryError):
358
+ # Cloning failed (likely OOM at extreme N);
359
+ # legacy do_bench path will be used by _bench.
360
+ self._l2_cold_arg_sets = None
361
+ self._l2_cold_kwarg_sets = None
362
+ else:
363
+ self._l2_cold_arg_sets = None
364
+ self._l2_cold_kwarg_sets = None
365
+
366
+ with pool_scope() as pool:
367
+ queue = deque(pruned_configs)
368
+ awaiting = {} # id(config) -> sha
369
+ attempts = {} # id(config) -> int
370
+ deadline = {} # id(config) -> wedge deadline
371
+ spins = 0
372
+ while queue:
373
+ config = queue.popleft()
374
+ sha = awaiting.get(id(config))
375
+ wedged = sha is not None and time.monotonic() > deadline[id(config)]
376
+ if sha is not None and not wedged:
377
+ state, _ = pool.poll(sha)
378
+ if state == "pending":
379
+ queue.append(config)
380
+ spins += 1
381
+ if spins >= len(queue):
382
+ time.sleep(0.05)
383
+ spins = 0
384
+ continue
385
+ spins = 0
386
+ n = attempts.get(id(config), 0) + 1
387
+ attempts[id(config)] = n
388
+ try:
389
+ if wedged or n > _MAX_ATTEMPTS:
390
+ # Wedged pool: compile in-process so
391
+ # the sweep always terminates.
392
+ with suppress_pool():
393
+ timings[config] = self._bench(
394
+ *args, config=config, **kwargs
395
+ )
396
+ else:
397
+ timings[config] = self._bench(
398
+ *args, config=config, **kwargs
399
+ )
400
+ except CompilePending as e:
401
+ awaiting[id(config)] = e.sha
402
+ deadline.setdefault(
403
+ id(config),
404
+ time.monotonic() + _POOL_WEDGE_TIMEOUT_S,
405
+ )
406
+ queue.append(config)
407
+ finally:
408
+ # Free L2-cold sets before persisting the cache so the
409
+ # user's subsequent .fn(...) call has full HBM.
410
+ self._l2_cold_arg_sets = None
411
+ self._l2_cold_kwarg_sets = None
412
  bench_end = time.time()
413
+ if verbose:
414
  for config, time_ in timings.items():
415
  print(f"[{config}] -> {time_[0]:.3f}ms")
416
+ # Surface bench failures (configs returning inf timings)
417
+ # so smem-overflow / launch errors aren't silently masked.
418
+ n_failed = sum(1 for t in timings.values() if t[0] == float("inf"))
419
+ if n_failed:
420
+ print(
421
+ f"quack autotune: {n_failed}/{len(timings)} configs "
422
+ f"failed for {self.fn.__name__}{key}; "
423
+ f"set {PACKAGE_NAME.upper()}_PRINT_AUTOTUNING=1 for details",
424
+ file=sys.stderr,
425
+ )
426
  self.bench_time = bench_end - bench_start
427
  self.cache[key] = builtins.min(timings, key=timings.get)
428
  self.configs_timings = timings
build/torch-cuda/quack/bench/__init__.py ADDED
File without changes
build/torch-cuda/quack/bench/bench_utils.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Shared helpers for triton perf_report-based benchmarks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TYPE_CHECKING
7
+
8
+ import torch
9
+ from torch import Tensor
10
+
11
+ if TYPE_CHECKING:
12
+ import pandas as pd
13
+ from triton.testing import Benchmark
14
+
15
+
16
+ def run_and_print(mark, save_path=None):
17
+ """Run a triton ``Mark`` (from ``perf_report``) and print/save results.
18
+
19
+ Each runner is expected to return a ``dict[str, Any]`` mapping stat name to
20
+ value, e.g. ``{"ms": 0.123, "GB/s": 1234}``. All providers in a benchmark
21
+ must return the same set of keys. Values are written through unchanged --
22
+ rounding/formatting is the caller's responsibility.
23
+
24
+ Output columns are ``x_names + [f"{line_name} ({stat})" for ...]``.
25
+ """
26
+ benchmarks = mark.benchmarks if isinstance(mark.benchmarks, list) else [mark.benchmarks]
27
+ for bench in benchmarks:
28
+ df = _run_one(mark.fn, bench)
29
+ print(bench.plot_name + ":")
30
+ print(df.to_string())
31
+ if save_path:
32
+ os.makedirs(save_path, exist_ok=True)
33
+ df.to_csv(os.path.join(save_path, f"{bench.plot_name}.csv"), index=False)
34
+
35
+
36
+ def _run_one(fn, bench: Benchmark) -> pd.DataFrame:
37
+ try:
38
+ import pandas as pd
39
+ except ImportError as e:
40
+ raise ImportError(
41
+ "pandas is required to format benchmark results. "
42
+ "Install it with `pip install pandas` or `pip install -e '.[bench]'`."
43
+ ) from e
44
+
45
+ x_names = list(bench.x_names)
46
+ rows = []
47
+ stat_keys = None # locked in from the first runner result
48
+ for x in bench.x_vals:
49
+ if not isinstance(x, (list, tuple)):
50
+ x = [x] * len(x_names)
51
+ x_args = dict(zip(x_names, x))
52
+ row = list(x)
53
+ for line_val in bench.line_vals:
54
+ stats = fn(**x_args, **{bench.line_arg: line_val}, **bench.args)
55
+ if not isinstance(stats, dict):
56
+ raise TypeError(f"runner must return dict[str, Any], got {type(stats).__name__}")
57
+ if stat_keys is None:
58
+ stat_keys = list(stats.keys())
59
+ elif list(stats.keys()) != stat_keys:
60
+ raise ValueError(f"runner returned keys {list(stats.keys())}, expected {stat_keys}")
61
+ row.extend(stats[k] for k in stat_keys)
62
+ rows.append(row)
63
+ cols = list(x_names) + [
64
+ f"{name} ({stat})" for name in bench.line_names for stat in (stat_keys or [])
65
+ ]
66
+ return pd.DataFrame(rows, columns=cols)
67
+
68
+
69
+ def _bench_cuda_graph_l2_rotate(
70
+ fn,
71
+ arg_sets,
72
+ kwarg_sets,
73
+ extra_kwargs,
74
+ warmup_target_ms: float = 200.0,
75
+ n_timed_calls: int = 200,
76
+ quantiles=None,
77
+ ):
78
+ """L2-cold single-replay CUDA-graph benchmark.
79
+
80
+ Warmup is time-based: probe a single kernel launch to estimate the
81
+ per-call cost, then iterate round-robin over the pre-cloned
82
+ ``(arg_sets[i], kwarg_sets[i])`` pairs as a plain Python loop for
83
+ ``warmup_target_ms`` of wall-clock GPU work. Heavy pipelined configs
84
+ (TMA + smem_stages=3) need enough warmup to drain the pipeline-fill
85
+ phase or the timed window catches them artificially fast - a fixed
86
+ count of warmup launches underwarms heavy configs while overpaying
87
+ for cheap ones.
88
+
89
+ The timed window is a single ``graph.replay()`` of a captured CUDA
90
+ graph whose body records ``n_timed_calls`` round-robin invocations -
91
+ no Python loop, no per-launch CPU overhead - so the measurement is
92
+ just GPU work / total recorded calls.
93
+
94
+ Round-robin over fresh tensor sets defeats the L2-resident caching
95
+ that inflates short-kernel timing under ``triton.testing.do_bench``
96
+ (which calls the kernel on the same single tensor each iteration).
97
+ For cache-cold production workloads the round-robin number predicts
98
+ real-world latency; the L2-hot number favours wider layouts / deeper
99
+ smem stages that don't actually win once data has to come from HBM.
100
+
101
+ ``fn`` is called as ``fn(*arg_sets[i], **kwarg_sets[i], **extra_kwargs)``
102
+ once per recorded launch. ``extra_kwargs`` holds the per-config kwargs
103
+ (e.g. ``{"config": <RmsNormBwdConfig>}``) which don't need cloning;
104
+ keys in ``extra_kwargs`` must not overlap with ``kwarg_sets[i]``.
105
+ Returns ms/call, or a ``len(quantiles)``-list replicating that value
106
+ when ``quantiles`` is provided (for API parity with
107
+ ``triton.testing.do_bench``).
108
+ """
109
+ n_sets = len(arg_sets)
110
+ # Round timed-call count to a multiple of n_sets for even L2 turnover.
111
+ rounds_timed = max(1, n_timed_calls // n_sets)
112
+ total_timed_calls = rounds_timed * n_sets
113
+
114
+ # A few priming launches so the probe doesn't catch first-launch
115
+ # driver / kernel-load overhead.
116
+ for _ in range(3):
117
+ fn(*arg_sets[0], **kwarg_sets[0], **extra_kwargs)
118
+ torch.cuda.synchronize()
119
+
120
+ # Probe a single launch to estimate per-call ms; size the warmup loop
121
+ # to hit ``warmup_target_ms`` of GPU work regardless of kernel cost.
122
+ probe_start = torch.cuda.Event(enable_timing=True)
123
+ probe_end = torch.cuda.Event(enable_timing=True)
124
+ probe_start.record()
125
+ fn(*arg_sets[0], **kwarg_sets[0], **extra_kwargs)
126
+ probe_end.record()
127
+ torch.cuda.synchronize()
128
+ est_ms = max(probe_start.elapsed_time(probe_end), 1e-3)
129
+ n_warmup_calls = max(50, int(warmup_target_ms / est_ms))
130
+
131
+ # Warmup: plain Python loop over rotating sets, no graph capture.
132
+ for i in range(n_warmup_calls):
133
+ idx = i % n_sets
134
+ fn(*arg_sets[idx], **kwarg_sets[idx], **extra_kwargs)
135
+ torch.cuda.synchronize()
136
+
137
+ # Capture timed graph: a single replay covers all timed kernel launches.
138
+ timed_graph = torch.cuda.CUDAGraph()
139
+ with torch.cuda.graph(timed_graph):
140
+ for _ in range(rounds_timed):
141
+ for i in range(n_sets):
142
+ fn(*arg_sets[i], **kwarg_sets[i], **extra_kwargs)
143
+ torch.cuda.synchronize()
144
+
145
+ start_evt = torch.cuda.Event(enable_timing=True)
146
+ end_evt = torch.cuda.Event(enable_timing=True)
147
+ start_evt.record()
148
+ timed_graph.replay()
149
+ end_evt.record()
150
+ torch.cuda.synchronize()
151
+ ms = start_evt.elapsed_time(end_evt) / total_timed_calls
152
+
153
+ if quantiles:
154
+ return [ms for _ in quantiles]
155
+ return ms
156
+
157
+
158
+ def _clone_l2_rotate_inputs(args, kwargs, n_buffers: int):
159
+ """Clone tensor args AND tensor kwargs ``n_buffers`` times.
160
+
161
+ Returns ``(arg_sets, kwarg_sets)``: ``arg_sets[i]`` is a tuple matching
162
+ ``args``' positional shape with tensors cloned to fresh memory;
163
+ ``kwarg_sets[i]`` is a dict matching ``kwargs``' keys with tensor values
164
+ cloned. Non-tensor values (ints, strings, None, dataclasses, etc.) are
165
+ shared across all sets (no clone).
166
+
167
+ Both args and kwargs are cloned so that every recorded launch in the
168
+ L2-cold round-robin CUDA graph touches distinct GMEM addresses,
169
+ including for write-target kwargs like ``dw_partial`` / ``dx``.
170
+ """
171
+ arg_sets = []
172
+ kwarg_sets = []
173
+ for _ in range(n_buffers):
174
+ arg_sets.append(tuple(a.clone() if isinstance(a, Tensor) else a for a in args))
175
+ kwarg_sets.append({k: v.clone() if isinstance(v, Tensor) else v for k, v in kwargs.items()})
176
+ return arg_sets, kwarg_sets
177
+
178
+
179
+ def _pick_l2_rotate_count(
180
+ args, kwargs, target_ratio: int = 3, min_buffers: int = 4, max_buffers: int = 16
181
+ ):
182
+ """Pick ``n_bufs`` so cloned input bytes per round exceed
183
+ ``target_ratio * L2_size`` (defeats L2 reuse), capped by HBM headroom and
184
+ [min_buffers, max_buffers]. Counts tensor bytes across both ``args`` and
185
+ ``kwargs`` so write-target kwargs (``dw_partial``, ``dx``, etc.) are
186
+ included in the L2-turnover calculation.
187
+ """
188
+ if not torch.cuda.is_available():
189
+ return min_buffers
190
+ tensor_bytes = sum(a.numel() * a.element_size() for a in args if isinstance(a, Tensor)) + sum(
191
+ v.numel() * v.element_size() for v in kwargs.values() if isinstance(v, Tensor)
192
+ )
193
+ if tensor_bytes == 0:
194
+ return min_buffers
195
+ props = torch.cuda.get_device_properties(torch.cuda.current_device())
196
+ l2_size = props.L2_cache_size
197
+ n_by_l2 = (target_ratio * l2_size + tensor_bytes - 1) // tensor_bytes
198
+ free_bytes, _ = torch.cuda.mem_get_info()
199
+ # Leave half of free memory headroom for the kernel's own scratch + the
200
+ # user's other allocations.
201
+ n_by_mem = max(1, int(free_bytes * 0.5) // tensor_bytes)
202
+ return max(min_buffers, min(max_buffers, min(n_by_l2, n_by_mem)))
build/torch-cuda/quack/blockscaled/__init__.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026, Tri Dao.
2
+ """Blockscaled (MXFP8 / MXFP4 / NVFP4) GEMM support.
3
+
4
+ - :mod:`quack.blockscaled.quantize` — pure-PyTorch quantizers (ported from
5
+ torchao) with torch.compile'd fast paths.
6
+ - :mod:`quack.blockscaled.utils` — scale-factor packing/unpacking, operand
7
+ builders for tests/benchmarks, and the kernel-level compile path.
8
+
9
+ The GEMM entry points live in :mod:`quack.gemm_interface` (pass ``(A, SFA)`` /
10
+ ``(B, SFB)`` tuples).
11
+ """
12
+
13
+ from .quantize import ( # noqa: F401
14
+ nvfp4_per_tensor_scale,
15
+ to_blocked,
16
+ to_mx,
17
+ to_mx_compiled,
18
+ to_mxfp4,
19
+ to_mxfp4_compiled,
20
+ to_nvfp4,
21
+ to_nvfp4_compiled,
22
+ )
23
+ from .utils import ( # noqa: F401
24
+ BLOCKSCALED_FORMATS,
25
+ blockscaled_gemm_reference,
26
+ blockscaled_quantize,
27
+ dequant_operand,
28
+ pack_scale_2d_to_blocked_contig,
29
+ scale_blocked_for_cublas,
30
+ unpack_scale_blocked_to_2d,
31
+ )
build/torch-cuda/quack/{mx_utils.py → blockscaled/quantize.py} RENAMED
File without changes
build/torch-cuda/quack/{blockscaled_gemm_utils.py → blockscaled/utils.py} RENAMED
@@ -9,16 +9,16 @@ import torch
9
  import cutlass
10
  import cutlass.cute as cute
11
 
12
- from .compile_utils import make_fake_tensor as fake_tensor
13
- from .cute_dsl_utils import get_device_capacity, get_max_active_clusters
14
- from .gemm_default_epi import GemmDefaultSm100
15
- from .gemm_tvm_ffi_utils import div_for_dtype, make_scheduler_args
16
- from .mx_utils import (
17
  to_mx_compiled,
18
  to_mxfp4_compiled,
19
  to_nvfp4_compiled,
20
  )
21
- from .varlen_utils import VarlenArguments
22
 
23
 
24
  TORCH_DTYPE_MAP = {
@@ -188,7 +188,7 @@ def create_blockscaled_operand_tensor(
188
 
189
 
190
  def _pack_blockscaled_scales(ref_blocks: torch.Tensor) -> torch.Tensor:
191
- """Rearrange (mn, sf_k, l) scales into the (l, rm, rk, 512) blocked layout."""
192
  mn, sf_k, l = ref_blocks.shape
193
  rm = ceil_div(mn, 128)
194
  rk = ceil_div(sf_k, 4)
@@ -205,7 +205,7 @@ def _pack_blockscaled_scales(ref_blocks: torch.Tensor) -> torch.Tensor:
205
  k_idx[None, :, None] // 4,
206
  l_idx[None, None, :],
207
  ] = ref_blocks
208
- return packed_6d.view(l, rm, rk, 512)
209
 
210
 
211
  def create_blockscaled_scale_tensor(
@@ -237,10 +237,10 @@ def create_blockscaled_scale_tensor(
237
 
238
  def pack_scale_2d_to_blocked_contig(scale_2d: torch.Tensor) -> torch.Tensor:
239
  """Rearrange a (l, mn, sf_k) or (mn, sf_k) e8m0 scale tensor into the
240
- contiguous (l, rm, rk, 512) blocked layout shared by the quack kernel and
241
- cuBLAS's block-scaling. Each 512 B inner block holds one 128 MN × 4 K
242
- swizzled tile. Pads `mn` to a multiple of 128 and `sf_k` to a multiple of
243
- 4 with zeros."""
244
  if scale_2d.dim() == 2:
245
  scale_2d = scale_2d.unsqueeze(0)
246
  assert scale_2d.dim() == 3, f"expected (l, mn, sf_k), got shape {tuple(scale_2d.shape)}"
@@ -260,22 +260,92 @@ def pack_scale_2d_to_blocked_contig(scale_2d: torch.Tensor) -> torch.Tensor:
260
  blocks = padded.view(l, rm, 128, rk, 4).permute(0, 1, 3, 2, 4)
261
  # split 128 into (4 outer, 32 inner), then swap to (32, 4)
262
  blocks = blocks.reshape(l, rm, rk, 4, 32, 4).transpose(3, 4).contiguous()
263
- return blocks.view(l, rm, rk, 512).view(orig_dtype)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
 
266
  def scale_view_for_kernel(scale_contig: torch.Tensor, mn: int, sf_k: int, l: int) -> torch.Tensor:
267
- """Validate a (l, rm, rk, 512) scale tensor and return it unchanged.
268
- Only the innermost 512-B tile must be contiguous (stride 1, size 512);
269
- outer (L, rm, rk) strides are free — the kernel reads them from the
270
- passed tensor. This lets callers pass a slice/view of a larger buffer
271
- with no extra copy. Works for both E8M0 (MX) and E4M3 (NVFP4)."""
 
272
  rm = ceil_div(mn, 128)
273
  rk = ceil_div(sf_k, 4)
274
- assert scale_contig.shape == (l, rm, rk, 512), (
275
- f"expected (l, rm, rk, 512) = ({l}, {rm}, {rk}, 512), got {tuple(scale_contig.shape)}"
 
276
  )
277
- assert scale_contig.stride(-1) == 1, (
278
- f"innermost 512-B dim must be unit-stride, got stride {scale_contig.stride(-1)}"
 
279
  )
280
  return scale_contig
281
 
@@ -283,9 +353,9 @@ def scale_view_for_kernel(scale_contig: torch.Tensor, mn: int, sf_k: int, l: int
283
  def scale_blocked_for_cublas(
284
  scale_contig: torch.Tensor, mn: int, sf_k: int, l_idx: int = 0
285
  ) -> torch.Tensor:
286
- """Flatten a (l, rm, rk, 512) scale tensor to the 1D swizzled layout
287
  torch._scaled_mm expects. Uses a single l slice."""
288
- assert scale_contig.is_contiguous() and scale_contig.dim() == 4
289
  return scale_contig[l_idx].reshape(-1)
290
 
291
 
@@ -328,8 +398,8 @@ def create_blockscaled_operand_quantized(
328
  ref: (mn, k, l) float32 dequantized reference
329
  q_mkl: (mn, k, l) operand tensor in the layout the quack kernel consumes
330
  (float8_e4m3fn for fp8 formats; int8 with packed nibbles for fp4)
331
- scale_contig: (l, rm, rk, 512) contiguous scale storage. Each 512 B
332
- inner block is one 128 MN × 4 K swizzled tile. Byte layout matches
333
  cuBLAS `to_blocked`. Pass directly to the quack kernel, or use
334
  `scale_blocked_for_cublas` for cuBLAS.
335
  """
@@ -403,7 +473,7 @@ def create_blockscaled_varlen_m_operands(
403
  """Generate bf16 randn + quantize for a varlen_m blockscaled GEMM.
404
 
405
  Per-expert seqlens may be arbitrary (not required to be multiples of 128).
406
- SF is stored in dQaccum-style padded format: each expert `i`'s scales
407
  occupy `ceildiv(m_i, 128) * 128` rows at offset
408
  `(cu_seqlens_m[i] + i * 128) // 128 * 128` in the padded scale buffer.
409
  The kernel decodes via `VarlenManager.offset_batch_SFA` which applies the
@@ -414,10 +484,14 @@ def create_blockscaled_varlen_m_operands(
414
  b_ref: (num_experts, n, k) fp32 dequantized
415
  qa: (total_m, k) 2D K-major quantized operand (fp8) or (total_m, k/2) (fp4)
416
  qb: (n, k, num_experts) 3D K-major quantized operand (fp8) or (n, k/2, num_experts) (fp4)
417
- a_sc_contig: (1, total_padded_rm, rk, 512) — dQaccum-padded SFA.
418
  total_padded_rm = ((total_m + num_experts * 128) // 128).
419
- b_sc_contig: (num_experts, rn, rk, 512) — regular per-expert SFB.
420
  cu_seqlens_m: (num_experts+1,) int32
 
 
 
 
421
  """
422
  assert k % sf_vec_size == 0
423
  if seqlens_m is None:
@@ -429,23 +503,31 @@ def create_blockscaled_varlen_m_operands(
429
  std = randn_std if randn_std is not None else k**-0.5
430
  sf_k = k // sf_vec_size
431
 
432
- if ab_dtype == cutlass.Float8E4M3FN and sf_dtype == cutlass.Float8E8M0FNU and sf_vec_size == 32:
433
- from .mx_utils import to_mx_compiled
434
-
435
- to_fn = to_mx_compiled
436
- else:
437
- raise NotImplementedError(
438
- f"varlen_m currently only supports MXFP8 (got ab={ab_dtype}, sf={sf_dtype}, vec={sf_vec_size}). "
439
- "FP4 support pending."
440
- )
441
-
442
- # Quantize A: (total_m, k) bf16 -> (total_m, k) fp8 K-major.
 
 
 
 
 
 
 
 
 
443
  # A data itself is stored packed (no per-expert padding); only SFA is padded.
444
  a_hp = (torch.randn(total_m, k, dtype=torch.bfloat16, device="cuda") * std).contiguous()
445
- qa, sa_2d = to_fn(a_hp, sf_vec_size) # (total_m, k), (total_m, sf_k)
446
- a_ref = qa.float() * sa_2d.float().repeat_interleave(sf_vec_size, dim=-1)
447
 
448
- # Build padded SFA storage (dQaccum format). Each expert's m_i rows of
449
  # scales are written at padded tile offset `cu_seqlens[i] // 128 + i`.
450
  # Allocation: `ceildiv(total_m, 128) + (L - 1)` tiles — proven sufficient
451
  # in AI/varlen_blockscaled_sf_layout.md (proof 2's "tighter alternative").
@@ -462,24 +544,22 @@ def create_blockscaled_varlen_m_operands(
462
  offset += m_i
463
  a_sc_contig = pack_scale_2d_to_blocked_contig(sa_2d_padded.view(1, total_padded_m, sf_k))
464
 
465
- # Quantize B: (num_experts, n, k) bf16 -> (n, k, num_experts). b_major selects
466
- # k-major (stride (k, 1, n*k)) or n-major (stride (1, n, n*k)).
467
  assert b_major in ("k", "n"), f"b_major must be 'k' or 'n', got {b_major!r}"
468
  b_hp = (torch.randn(num_experts, n, k, dtype=torch.bfloat16, device="cuda") * std).contiguous()
469
- qb_flat, sb_2d = to_fn(b_hp.view(num_experts * n, k), sf_vec_size)
 
470
  if b_major == "k":
471
  qb = (
472
- qb_flat.view(num_experts, n, k).contiguous().permute(1, 2, 0)
473
- ) # (n, k, l) stride (k, 1, n*k)
474
  else:
475
  qb = (
476
- qb_flat.view(num_experts, n, k).transpose(1, 2).contiguous().permute(2, 1, 0)
477
  ) # (n, k, l) stride (1, n, n*k)
478
- sb_2d = sb_2d.view(num_experts, n, sf_k)
479
- b_sc_contig = pack_scale_2d_to_blocked_contig(sb_2d)
480
- b_ref = qb_flat.float().view(num_experts, n, k) * sb_2d.float().repeat_interleave(
481
- sf_vec_size, dim=-1
482
- )
483
 
484
  cu_seqlens_m = torch.tensor(
485
  [0] + list(itertools.accumulate(seqlens_m)), dtype=torch.int32, device="cuda"
@@ -498,23 +578,34 @@ def create_blockscaled_varlen_k_operands(
498
  *,
499
  randn_std: Optional[float] = None,
500
  seqlens_k: Optional[list] = None,
 
501
  ):
502
  """Generate bf16 randn + quantize for a varlen_k blockscaled GEMM.
503
 
504
- Per-expert `k_i` must be a multiple of `sf_vec_size` (quantization chunk)
505
- but NOT necessarily a multiple of `sf_vec_size * 4` (= 128 for MXFP8).
506
- The SF buffer uses dQaccum-style K padding: each expert `i`'s scales occupy
 
 
 
507
  `ceildiv(k_i, 128) * 128` bytes worth of K at offset
508
  `(cu_seqlens_k[i] + i * 128) // 128 * 128` (in source-K units). A and B
509
  operand data stay packed and unpadded along K — only their SF buffers pad.
510
 
 
 
 
 
 
 
 
511
  Returns (a_ref_list, b_ref_list, qa, qb, a_sc_contig, b_sc_contig, cu_seqlens_k):
512
  a_ref_list: list of per-expert (m, k_i) fp32 dequantized A.
513
  b_ref_list: list of per-expert (n, k_i) fp32 dequantized B.
514
  qa: (m, total_k) K-major fp8 (stride (total_k, 1)).
515
  qb: (n, total_k) K-major fp8 (stride (total_k, 1)).
516
- a_sc_contig: (1, rm, total_padded_rk, 512) dQaccum-padded SFA.
517
- b_sc_contig: (1, rn, total_padded_rk, 512) dQaccum-padded SFB.
518
  cu_seqlens_k: (num_experts+1,) int32.
519
  """
520
  if not (
@@ -530,30 +621,37 @@ def create_blockscaled_varlen_k_operands(
530
  f"seqlens_k length {len(seqlens_k)} != num_experts {num_experts}"
531
  )
532
  for i, k_i in enumerate(seqlens_k):
533
- assert k_i % sf_vec_size == 0, (
534
- f"seqlens_k[{i}]={k_i} must be divisible by sf_vec_size={sf_vec_size}"
535
- )
536
  total_k = int(sum(seqlens_k))
537
  std = randn_std if randn_std is not None else (max(seqlens_k)) ** -0.5
538
- sf_k_total = total_k // sf_vec_size
539
 
540
- from .mx_utils import to_mx_compiled
 
 
 
 
 
 
 
 
 
 
 
 
 
541
 
542
  a_q_list, a_sc_list, a_ref_list = [], [], []
543
  b_q_list, b_sc_list, b_ref_list = [], [], []
544
  for k_i in seqlens_k:
545
- # A slice: (m, k_i) bf16 -> fp8, scales (m, k_i // sf_vec_size).
546
- a_hp = (torch.randn(m, k_i, dtype=torch.bfloat16, device="cuda") * std).contiguous()
547
- a_q, a_sc = to_mx_compiled(a_hp, sf_vec_size)
548
  a_q_list.append(a_q)
549
  a_sc_list.append(a_sc)
550
- a_ref_list.append(a_q.float() * a_sc.float().repeat_interleave(sf_vec_size, dim=-1))
551
 
552
- b_hp = (torch.randn(n, k_i, dtype=torch.bfloat16, device="cuda") * std).contiguous()
553
- b_q, b_sc = to_mx_compiled(b_hp, sf_vec_size)
554
  b_q_list.append(b_q)
555
  b_sc_list.append(b_sc)
556
- b_ref_list.append(b_q.float() * b_sc.float().repeat_interleave(sf_vec_size, dim=-1))
557
 
558
  # Pack operand data along K: (m, total_k), (n, total_k). varlen_k's
559
  # ragged TMA descriptors are built for MN-major operands (stride 1 on
@@ -572,11 +670,15 @@ def create_blockscaled_varlen_k_operands(
572
  total_padded_rk = (total_k + tile - 1) // tile + (num_experts - 1)
573
  total_padded_k = total_padded_rk * tile
574
  total_padded_sf_k = total_padded_k // sf_vec_size
575
- sa_2d_padded = torch.zeros(m, total_padded_sf_k, dtype=a_sc_list[0].dtype, device="cuda")
576
- sb_2d_padded = torch.zeros(n, total_padded_sf_k, dtype=b_sc_list[0].dtype, device="cuda")
 
 
 
 
577
  k_offset = 0
578
  for i, k_i in enumerate(seqlens_k):
579
- sf_k_i = k_i // sf_vec_size
580
  k_offset_padded = (k_offset // tile + i) * tile
581
  sf_k_offset_padded = k_offset_padded // sf_vec_size
582
  sa_2d_padded[:, sf_k_offset_padded : sf_k_offset_padded + sf_k_i] = a_sc_list[i]
@@ -635,16 +737,20 @@ def compile_blockscaled_gemm_tvm_ffi(
635
  )
636
  stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
637
 
638
- from .gemm_tvm_ffi_utils import make_fake_varlen_args
639
 
640
  varlen_args_fake = make_fake_varlen_args(varlen_m, varlen_k, False, None) or VarlenArguments()
641
 
642
  # Fake operand tensors with sym_ints (varlen-aware shapes).
643
  if varlen_m:
644
  total_m_sym = cute.sym_int()
645
- n_sym, k_sym, l_sym = cute.sym_int(), cute.sym_int(), cute.sym_int()
646
- # Detect each operand's leading (stride-1) dim so m-major A / n-major B
647
- # are accepted for varlen_m (MXFP8 only fp4 is rejected upstream).
 
 
 
 
648
  fake_mA = fake_tensor(
649
  ab_dtype,
650
  (total_m_sym, k_sym),
@@ -709,7 +815,7 @@ def compile_blockscaled_gemm_tvm_ffi(
709
  varlen_args,
710
  stream,
711
  ):
712
- gemm(a, b, d, None, compile_epi_args, scheduler_args, varlen_args, stream, sfa, sfb, None)
713
 
714
  compiled = cute.compile(
715
  runner,
 
9
  import cutlass
10
  import cutlass.cute as cute
11
 
12
+ from ..compile_utils import make_fake_tensor as fake_tensor
13
+ from ..cute_dsl_utils import get_device_capacity, get_max_active_clusters
14
+ from ..gemm_default_epi import GemmDefaultSm100
15
+ from ..gemm_tvm_ffi_utils import div_for_dtype, make_scheduler_args
16
+ from .quantize import (
17
  to_mx_compiled,
18
  to_mxfp4_compiled,
19
  to_nvfp4_compiled,
20
  )
21
+ from ..varlen_utils import VarlenArguments
22
 
23
 
24
  TORCH_DTYPE_MAP = {
 
188
 
189
 
190
  def _pack_blockscaled_scales(ref_blocks: torch.Tensor) -> torch.Tensor:
191
+ """Rearrange (mn, sf_k, l) scales into the (l, rm, rk, 32, 4, 4) blocked layout."""
192
  mn, sf_k, l = ref_blocks.shape
193
  rm = ceil_div(mn, 128)
194
  rk = ceil_div(sf_k, 4)
 
205
  k_idx[None, :, None] // 4,
206
  l_idx[None, None, :],
207
  ] = ref_blocks
208
+ return packed_6d
209
 
210
 
211
  def create_blockscaled_scale_tensor(
 
237
 
238
  def pack_scale_2d_to_blocked_contig(scale_2d: torch.Tensor) -> torch.Tensor:
239
  """Rearrange a (l, mn, sf_k) or (mn, sf_k) e8m0 scale tensor into the
240
+ contiguous (l, rm, rk, 32, 4, 4) blocked layout shared by the quack kernel
241
+ and cuBLAS's block-scaling. Each inner (32, 4, 4) atom (512 B) holds one
242
+ 128 MN × 4 K swizzled tile. Pads `mn` to a multiple of 128 and `sf_k` to a
243
+ multiple of 4 with zeros."""
244
  if scale_2d.dim() == 2:
245
  scale_2d = scale_2d.unsqueeze(0)
246
  assert scale_2d.dim() == 3, f"expected (l, mn, sf_k), got shape {tuple(scale_2d.shape)}"
 
260
  blocks = padded.view(l, rm, 128, rk, 4).permute(0, 1, 3, 2, 4)
261
  # split 128 into (4 outer, 32 inner), then swap to (32, 4)
262
  blocks = blocks.reshape(l, rm, rk, 4, 32, 4).transpose(3, 4).contiguous()
263
+ return blocks.view(orig_dtype)
264
+
265
+
266
+ def unpack_scale_blocked_to_2d(blocked: torch.Tensor, mn: int, sf_k: int) -> torch.Tensor:
267
+ """Unswizzle (l, rm, rk, 32, 4, 4) blocked scale factors to (l, mn, sf_k)."""
268
+ l, rm, rk = blocked.shape[:3]
269
+ assert tuple(blocked.shape[3:]) == (32, 4, 4)
270
+ orig_dtype = blocked.dtype
271
+ u8 = blocked.view(torch.uint8)
272
+ # (32=m%32, 4=m//32, 4=k%4) -> (4, 32, 4) -> (l, rm, rk, 128, 4) -> (l, mn_pad, sf_k_pad)
273
+ u8 = u8.transpose(3, 4).reshape(l, rm, rk, 128, 4)
274
+ u8 = u8.permute(0, 1, 3, 2, 4).reshape(l, rm * 128, rk * 4)
275
+ return u8[:, :mn, :sf_k].contiguous().view(orig_dtype)
276
+
277
+
278
+ def dequant_operand(x: torch.Tensor) -> torch.Tensor:
279
+ """Dequantize an operand tensor to float32 values (without scale factors).
280
+
281
+ fp8 tensors convert directly; ``float4_e2m1fn_x2`` tensors unpack two codes
282
+ per byte (low nibble = even K, high nibble = odd K), doubling the last dim.
283
+ """
284
+ if x.dtype == torch.float4_e2m1fn_x2:
285
+ u8 = x.view(torch.uint8)
286
+ lo = _fp4_unpacked_to_value(u8 & 0x0F)
287
+ hi = _fp4_unpacked_to_value((u8 >> 4) & 0x0F)
288
+ return torch.stack([lo, hi], dim=-1).reshape(*x.shape[:-1], x.shape[-1] * 2)
289
+ return x.float()
290
+
291
+
292
+ BLOCKSCALED_FORMATS = {
293
+ # format: (torch operand dtype, torch SF dtype, sf_vec_size)
294
+ "mxfp8": (torch.float8_e4m3fn, torch.float8_e8m0fnu, 32),
295
+ "mxfp4": (torch.float4_e2m1fn_x2, torch.float8_e8m0fnu, 32),
296
+ "nvfp4": (torch.float4_e2m1fn_x2, torch.float8_e4m3fn, 16),
297
+ }
298
+
299
+
300
+ def blockscaled_quantize(
301
+ x: torch.Tensor, format: str = "mxfp8", per_tensor_scale: Optional[torch.Tensor] = None
302
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
303
+ """Quantize a (M, K) or (L, M, K) bf16/fp32 tensor along K for blockscaled GEMM.
304
+
305
+ Returns ``(q, sf)`` ready to pass as an ``(A, SFA)`` / ``(B, SFB)`` tuple to
306
+ :func:`quack.gemm_interface.gemm`:
307
+ q: same leading shape as ``x``; fp8 for mxfp8 (M, K), packed fp4x2 for
308
+ mxfp4/nvfp4 (M, K/2), K-contiguous.
309
+ sf: blocked scale factors, (rm, rk, 32, 4, 4) or (L, rm, rk, 32, 4, 4).
310
+ For nvfp4, ``per_tensor_scale`` (scalar fp32) folds the global scale; pass the
311
+ product of A's and B's per-tensor scales as ``alpha`` to the GEMM.
312
+ """
313
+ from .quantize import to_mx_compiled, to_mxfp4_compiled, to_nvfp4_compiled
314
+
315
+ assert format in BLOCKSCALED_FORMATS, f"unknown blockscaled format: {format}"
316
+ q_dtype, sf_dtype, sf_vec = BLOCKSCALED_FORMATS[format]
317
+ assert x.shape[-1] % sf_vec == 0, f"K ({x.shape[-1]}) must be divisible by {sf_vec}"
318
+ batched = x.ndim == 3
319
+ l, mn, k = x.shape if batched else (1, *x.shape)
320
+ x_flat = x.reshape(l * mn, k)
321
+ if format == "mxfp8":
322
+ q, sc = to_mx_compiled(x_flat, sf_vec)
323
+ elif format == "mxfp4":
324
+ q, sc = to_mxfp4_compiled(x_flat, sf_vec)
325
+ else:
326
+ q, sc, _ = to_nvfp4_compiled(x_flat, sf_vec, per_tensor_scale)
327
+ q = q.view(torch.uint8).view(q_dtype) if q_dtype == torch.float4_e2m1fn_x2 else q
328
+ q = q.reshape(*x.shape[:-1], -1)
329
+ sf = pack_scale_2d_to_blocked_contig(sc.view(l, mn, k // sf_vec))
330
+ return q, sf if batched else sf.squeeze(0)
331
 
332
 
333
  def scale_view_for_kernel(scale_contig: torch.Tensor, mn: int, sf_k: int, l: int) -> torch.Tensor:
334
+ """Validate a (l, rm, rk, 32, 4, 4) scale tensor and return it unchanged.
335
+ Only the innermost (32, 4, 4) atom (one 512 B tile) must be contiguous
336
+ (strides (16, 4, 1)); outer (L, rm, rk) strides are free — the kernel
337
+ reads them from the passed tensor. This lets callers pass a slice/view of
338
+ a larger buffer with no extra copy. Works for both E8M0 (MX) and E4M3
339
+ (NVFP4)."""
340
  rm = ceil_div(mn, 128)
341
  rk = ceil_div(sf_k, 4)
342
+ assert scale_contig.shape == (l, rm, rk, 32, 4, 4), (
343
+ f"expected (l, rm, rk, 32, 4, 4) = ({l}, {rm}, {rk}, 32, 4, 4), "
344
+ f"got {tuple(scale_contig.shape)}"
345
  )
346
+ assert scale_contig.stride()[-3:] == (16, 4, 1), (
347
+ f"inner (32, 4, 4) atom must be contiguous with strides (16, 4, 1), "
348
+ f"got {scale_contig.stride()[-3:]}"
349
  )
350
  return scale_contig
351
 
 
353
  def scale_blocked_for_cublas(
354
  scale_contig: torch.Tensor, mn: int, sf_k: int, l_idx: int = 0
355
  ) -> torch.Tensor:
356
+ """Flatten a (l, rm, rk, 32, 4, 4) scale tensor to the 1D swizzled layout
357
  torch._scaled_mm expects. Uses a single l slice."""
358
+ assert scale_contig.is_contiguous() and scale_contig.dim() == 6
359
  return scale_contig[l_idx].reshape(-1)
360
 
361
 
 
398
  ref: (mn, k, l) float32 dequantized reference
399
  q_mkl: (mn, k, l) operand tensor in the layout the quack kernel consumes
400
  (float8_e4m3fn for fp8 formats; int8 with packed nibbles for fp4)
401
+ scale_contig: (l, rm, rk, 32, 4, 4) contiguous scale storage. Each inner
402
+ (32, 4, 4) atom (512 B) is one 128 MN × 4 K swizzled tile. Byte layout matches
403
  cuBLAS `to_blocked`. Pass directly to the quack kernel, or use
404
  `scale_blocked_for_cublas` for cuBLAS.
405
  """
 
473
  """Generate bf16 randn + quantize for a varlen_m blockscaled GEMM.
474
 
475
  Per-expert seqlens may be arbitrary (not required to be multiples of 128).
476
+ SF is stored with tile-aligned per-batch padding: each expert `i`'s scales
477
  occupy `ceildiv(m_i, 128) * 128` rows at offset
478
  `(cu_seqlens_m[i] + i * 128) // 128 * 128` in the padded scale buffer.
479
  The kernel decodes via `VarlenManager.offset_batch_SFA` which applies the
 
484
  b_ref: (num_experts, n, k) fp32 dequantized
485
  qa: (total_m, k) 2D K-major quantized operand (fp8) or (total_m, k/2) (fp4)
486
  qb: (n, k, num_experts) 3D K-major quantized operand (fp8) or (n, k/2, num_experts) (fp4)
487
+ a_sc_contig: (1, total_padded_rm, rk, 32, 4, 4) — M-padded SFA (tile-aligned per batch).
488
  total_padded_rm = ((total_m + num_experts * 128) // 128).
489
+ b_sc_contig: (num_experts, rn, rk, 32, 4, 4) — regular per-expert SFB.
490
  cu_seqlens_m: (num_experts+1,) int32
491
+
492
+ Supports MXFP8 / MXFP4 / NVFP4; fp4 formats require b_major="k" (tcgen05
493
+ MMA needs K-major fp4 operands). NVFP4 uses no per-tensor scale here (it
494
+ would just fold into alpha).
495
  """
496
  assert k % sf_vec_size == 0
497
  if seqlens_m is None:
 
503
  std = randn_std if randn_std is not None else k**-0.5
504
  sf_k = k // sf_vec_size
505
 
506
+ fmt = _blockscaled_format_of(ab_dtype, sf_dtype, sf_vec_size)
507
+ if fmt != "mxfp8":
508
+ assert b_major == "k", f"{fmt} requires K-major operands, got b_major={b_major!r}"
509
+
510
+ def quantize(x2d):
511
+ """(rows, k) bf16 -> (q, scale_2d, dequant_ref); q is fp8 (rows, k) or fp4x2 (rows, k/2)."""
512
+ if fmt == "mxfp8":
513
+ q, sc = to_mx_compiled(x2d, sf_vec_size)
514
+ vals = q.float()
515
+ else:
516
+ if fmt == "mxfp4":
517
+ q_packed, sc = to_mxfp4_compiled(x2d, sf_vec_size)
518
+ else: # nvfp4
519
+ q_packed, sc, _ = to_nvfp4_compiled(x2d, sf_vec_size, None)
520
+ q = q_packed.view(torch.uint8).view(torch.float4_e2m1fn_x2)
521
+ vals = dequant_operand(q)
522
+ ref = vals * sc.float().repeat_interleave(sf_vec_size, dim=-1)
523
+ return q, sc, ref
524
+
525
+ # Quantize A: (total_m, k) bf16 -> (total_m, k[/2]) K-major.
526
  # A data itself is stored packed (no per-expert padding); only SFA is padded.
527
  a_hp = (torch.randn(total_m, k, dtype=torch.bfloat16, device="cuda") * std).contiguous()
528
+ qa, sa_2d, a_ref = quantize(a_hp) # (total_m, k[/2]), (total_m, sf_k), (total_m, k)
 
529
 
530
+ # Build padded SFA storage (tile-aligned per-batch). Each expert's m_i rows of
531
  # scales are written at padded tile offset `cu_seqlens[i] // 128 + i`.
532
  # Allocation: `ceildiv(total_m, 128) + (L - 1)` tiles — proven sufficient
533
  # in AI/varlen_blockscaled_sf_layout.md (proof 2's "tighter alternative").
 
544
  offset += m_i
545
  a_sc_contig = pack_scale_2d_to_blocked_contig(sa_2d_padded.view(1, total_padded_m, sf_k))
546
 
547
+ # Quantize B: (num_experts, n, k) bf16 -> (n, k[/2], num_experts). b_major selects
548
+ # k-major (stride (kb, 1, n*kb)) or n-major (stride (1, n, n*k), mxfp8 only).
549
  assert b_major in ("k", "n"), f"b_major must be 'k' or 'n', got {b_major!r}"
550
  b_hp = (torch.randn(num_experts, n, k, dtype=torch.bfloat16, device="cuda") * std).contiguous()
551
+ qb_flat, sb_2d, b_ref_flat = quantize(b_hp.view(num_experts * n, k))
552
+ kb = qb_flat.shape[-1] # k for fp8, k/2 for packed fp4
553
  if b_major == "k":
554
  qb = (
555
+ qb_flat.view(num_experts, n, kb).contiguous().permute(1, 2, 0)
556
+ ) # (n, kb, l) stride (kb, 1, n*kb)
557
  else:
558
  qb = (
559
+ qb_flat.view(num_experts, n, kb).transpose(1, 2).contiguous().permute(2, 1, 0)
560
  ) # (n, k, l) stride (1, n, n*k)
561
+ b_sc_contig = pack_scale_2d_to_blocked_contig(sb_2d.view(num_experts, n, sf_k))
562
+ b_ref = b_ref_flat.view(num_experts, n, k)
 
 
 
563
 
564
  cu_seqlens_m = torch.tensor(
565
  [0] + list(itertools.accumulate(seqlens_m)), dtype=torch.int32, device="cuda"
 
578
  *,
579
  randn_std: Optional[float] = None,
580
  seqlens_k: Optional[list] = None,
581
+ sf_pad_byte: int = 0,
582
  ):
583
  """Generate bf16 randn + quantize for a varlen_k blockscaled GEMM.
584
 
585
+ Per-expert `k_i` is arbitrary (any positive int): neither `sf_vec_size` nor
586
+ `sf_vec_size * 4` (= 128 for MXFP8) alignment is required. A non-multiple-of-32
587
+ `k_i` just means the expert's last scale block covers a partial chunk; the
588
+ kernel's ragged value TMA zero-fills beyond `cu_seqlens_k[i+1]`, so the tail
589
+ contributes exactly 0.
590
+ The SF buffer uses tile-aligned per-batch K padding: each expert `i`'s scales occupy
591
  `ceildiv(k_i, 128) * 128` bytes worth of K at offset
592
  `(cu_seqlens_k[i] + i * 128) // 128 * 128` (in source-K units). A and B
593
  operand data stay packed and unpadded along K — only their SF buffers pad.
594
 
595
+ SF pad regions inside each expert's last 512 B atom are loaded by the
596
+ kernel (TMA loads whole atom columns) but never consumed: the mma loop
597
+ skips the MMA instructions for pad k-blocks (one instruction per SF block
598
+ for mxfp8; see `GemmSm100.mma`), so the pad may hold arbitrary bytes —
599
+ including 0xFF (e8m0 NaN). `sf_pad_byte` sets the pad fill so tests can
600
+ poison it deliberately.
601
+
602
  Returns (a_ref_list, b_ref_list, qa, qb, a_sc_contig, b_sc_contig, cu_seqlens_k):
603
  a_ref_list: list of per-expert (m, k_i) fp32 dequantized A.
604
  b_ref_list: list of per-expert (n, k_i) fp32 dequantized B.
605
  qa: (m, total_k) K-major fp8 (stride (total_k, 1)).
606
  qb: (n, total_k) K-major fp8 (stride (total_k, 1)).
607
+ a_sc_contig: (1, rm, total_padded_rk, 32, 4, 4) K-padded SFA (tile-aligned per batch).
608
+ b_sc_contig: (1, rn, total_padded_rk, 32, 4, 4) K-padded SFB (tile-aligned per batch).
609
  cu_seqlens_k: (num_experts+1,) int32.
610
  """
611
  if not (
 
621
  f"seqlens_k length {len(seqlens_k)} != num_experts {num_experts}"
622
  )
623
  for i, k_i in enumerate(seqlens_k):
624
+ assert k_i > 0, f"seqlens_k[{i}]={k_i} must be positive"
 
 
625
  total_k = int(sum(seqlens_k))
626
  std = randn_std if randn_std is not None else (max(seqlens_k)) ** -0.5
 
627
 
628
+ from .quantize import to_mx_compiled
629
+
630
+ def quantize(mn, k_i):
631
+ # The quantizer reshapes K into sf_vec_size chunks, so zero-pad k_i up to a
632
+ # multiple of it; zeros never raise a chunk amax, so the real elements
633
+ # quantize identically. Values are sliced back to k_i; scales keep the
634
+ # ceil(k_i / sf_vec_size) blocks (the last one covers a partial chunk).
635
+ k_q = (k_i + sf_vec_size - 1) // sf_vec_size * sf_vec_size
636
+ hp = torch.zeros(mn, k_q, dtype=torch.bfloat16, device="cuda")
637
+ hp[:, :k_i] = torch.randn(mn, k_i, dtype=torch.bfloat16, device="cuda") * std
638
+ q, sc = to_mx_compiled(hp, sf_vec_size)
639
+ q = q[:, :k_i]
640
+ ref = q.float() * sc.float().repeat_interleave(sf_vec_size, dim=-1)[:, :k_i]
641
+ return q, sc, ref
642
 
643
  a_q_list, a_sc_list, a_ref_list = [], [], []
644
  b_q_list, b_sc_list, b_ref_list = [], [], []
645
  for k_i in seqlens_k:
646
+ a_q, a_sc, a_ref = quantize(m, k_i)
 
 
647
  a_q_list.append(a_q)
648
  a_sc_list.append(a_sc)
649
+ a_ref_list.append(a_ref)
650
 
651
+ b_q, b_sc, b_ref = quantize(n, k_i)
 
652
  b_q_list.append(b_q)
653
  b_sc_list.append(b_sc)
654
+ b_ref_list.append(b_ref)
655
 
656
  # Pack operand data along K: (m, total_k), (n, total_k). varlen_k's
657
  # ragged TMA descriptors are built for MN-major operands (stride 1 on
 
670
  total_padded_rk = (total_k + tile - 1) // tile + (num_experts - 1)
671
  total_padded_k = total_padded_rk * tile
672
  total_padded_sf_k = total_padded_k // sf_vec_size
673
+ sa_2d_padded = torch.full(
674
+ (m, total_padded_sf_k), sf_pad_byte, dtype=torch.uint8, device="cuda"
675
+ ).view(a_sc_list[0].dtype)
676
+ sb_2d_padded = torch.full(
677
+ (n, total_padded_sf_k), sf_pad_byte, dtype=torch.uint8, device="cuda"
678
+ ).view(b_sc_list[0].dtype)
679
  k_offset = 0
680
  for i, k_i in enumerate(seqlens_k):
681
+ sf_k_i = (k_i + sf_vec_size - 1) // sf_vec_size
682
  k_offset_padded = (k_offset // tile + i) * tile
683
  sf_k_offset_padded = k_offset_padded // sf_vec_size
684
  sa_2d_padded[:, sf_k_offset_padded : sf_k_offset_padded + sf_k_i] = a_sc_list[i]
 
737
  )
738
  stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
739
 
740
+ from ..gemm_tvm_ffi_utils import make_fake_varlen_args
741
 
742
  varlen_args_fake = make_fake_varlen_args(varlen_m, varlen_k, False, None) or VarlenArguments()
743
 
744
  # Fake operand tensors with sym_ints (varlen-aware shapes).
745
  if varlen_m:
746
  total_m_sym = cute.sym_int()
747
+ n_sym, l_sym = cute.sym_int(), cute.sym_int()
748
+ # Sub-byte (fp4) operands need the contiguous K extent statically divisible
749
+ # by the packing factor; harmless for 8-bit dtypes.
750
+ k_sym = cute.sym_int(divisibility=div_for_dtype(ab_dtype) if ab_dtype.width < 8 else 1)
751
+ # Detect B's leading (stride-1) dim so n-major B is accepted for varlen_m
752
+ # (mxfp8 only; fp4 is always K-major). A must be K-major for varlen_m —
753
+ # the public API enforces this (see quack/gemm.py) for all dtypes.
754
  fake_mA = fake_tensor(
755
  ab_dtype,
756
  (total_m_sym, k_sym),
 
815
  varlen_args,
816
  stream,
817
  ):
818
+ gemm(a, b, d, None, compile_epi_args, scheduler_args, varlen_args, stream, sfa, sfb)
819
 
820
  compiled = cute.compile(
821
  runner,
build/torch-cuda/quack/broadcast_utils.py CHANGED
@@ -5,18 +5,17 @@ import cutlass
5
  import cutlass.cute as cute
6
  from cutlass import Float32, const_expr
7
 
8
- from .layout_utils import make_acc_tensor_mn_view
9
 
10
 
11
  @cute.jit
12
  def vec_op(tCrC: cute.Tensor, tCrVec: cute.Tensor, op: Callable, is_colvec: bool) -> None:
13
  if const_expr(tCrC.element_type != Float32): # Convert to f32
14
- tCrC_f32 = cute.make_rmem_tensor(tCrC.shape, Float32)
15
- tCrC_f32.store(tCrC.load().to(Float32))
16
  else:
17
  tCrC_f32 = tCrC
18
  # this happens to work for frgA layout too, not just acc layout
19
- tCrC_f32_mn = make_acc_tensor_mn_view(tCrC_f32)
20
  if const_expr(is_colvec):
21
  assert cute.size(tCrC_f32_mn, mode=[0]) == cute.size(tCrVec)
22
  for r in cutlass.range(cute.size(tCrC_f32_mn, mode=[0]), unroll_full=True):
 
5
  import cutlass.cute as cute
6
  from cutlass import Float32, const_expr
7
 
8
+ from . import layout_utils
9
 
10
 
11
  @cute.jit
12
  def vec_op(tCrC: cute.Tensor, tCrVec: cute.Tensor, op: Callable, is_colvec: bool) -> None:
13
  if const_expr(tCrC.element_type != Float32): # Convert to f32
14
+ tCrC_f32 = tCrC.to(Float32)
 
15
  else:
16
  tCrC_f32 = tCrC
17
  # this happens to work for frgA layout too, not just acc layout
18
+ tCrC_f32_mn = layout_utils.reshape_acc_to_mn(tCrC_f32)
19
  if const_expr(is_colvec):
20
  assert cute.size(tCrC_f32_mn, mode=[0]) == cute.size(tCrVec)
21
  for r in cutlass.range(cute.size(tCrC_f32_mn, mode=[0]), unroll_full=True):
build/torch-cuda/quack/cache/__init__.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025-2026, Tri Dao.
2
+ """Persistent kernel-cache utilities for QuACK.
3
+
4
+ Public API
5
+ ----------
6
+
7
+ Persistent ``.o`` cache:
8
+ * :func:`jit_cache` — decorator that wraps a kernel-compile function with
9
+ in-memory + persistent ``.o`` caching (see :mod:`quack.cache.jit`).
10
+ * :data:`CACHE_ENABLED`, :data:`CACHE_DIR`, :data:`EXTRA_SOURCE_DIRS` —
11
+ static-config flags.
12
+ * :class:`FileLock`, :func:`get_cache_path`, :class:`CacheInfo` —
13
+ supporting types.
14
+
15
+ Async compilation (see :mod:`quack.cache.async_compile`):
16
+ * :class:`CompilePending` — raised by ``jit_cache`` on a cold miss while a
17
+ compile pool is active; the caller defers and retries once the ``.o``
18
+ lands.
19
+ * :func:`pool_scope` — activate a compile pool for a scoped block (used by
20
+ the autotuner's bench loop).
21
+
22
+ CRITICAL ORDERING: the static-config flags below MUST be defined before the
23
+ ``from quack.cache.jit import ...`` block. ``quack/cache/jit.py`` does
24
+ ``import quack.cache as _state`` at its module top; Python returns the
25
+ partially-initialized package object, and lookups inside ``jit_cache``'s
26
+ wrapper rely on these names already existing at that checkpoint. Reordering
27
+ the imports here, even via an auto-formatter, will break the first kernel
28
+ compile with ``AttributeError``. The defensive unit tests in
29
+ ``tests/test_cache.py`` exercise this path end-to-end.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import os
35
+ from pathlib import Path
36
+ from typing import List, Optional
37
+
38
+ CACHE_ENABLED: bool = os.getenv("QUACK_CACHE_ENABLED", "1") == "1"
39
+ CACHE_DIR: Optional[str] = os.getenv("QUACK_CACHE_DIR", None)
40
+
41
+ #: Downstream projects can append directories here to include their sources
42
+ #: in the cache fingerprint. Must be set before the first jit_cache call.
43
+ EXTRA_SOURCE_DIRS: List[Path] = []
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Public API surface. Imported AFTER the flags are defined.
48
+ # ---------------------------------------------------------------------------
49
+
50
+ from .jit import ( # noqa: E402
51
+ EXPORT_FUNC_NAME,
52
+ LOCK_TIMEOUT,
53
+ CacheInfo,
54
+ FileLock,
55
+ get_cache_path,
56
+ jit_cache,
57
+ )
58
+ from .async_compile import ( # noqa: E402
59
+ CompilePending,
60
+ pool_scope,
61
+ )
62
+
63
+ __all__ = [
64
+ # Persistent .o cache.
65
+ "jit_cache",
66
+ "CacheInfo",
67
+ "EXPORT_FUNC_NAME",
68
+ "LOCK_TIMEOUT",
69
+ "FileLock",
70
+ "get_cache_path",
71
+ # Async compilation.
72
+ "CompilePending",
73
+ "pool_scope",
74
+ ]
build/torch-cuda/quack/cache/_pool_preload.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026, Tri Dao.
2
+ """Forkserver preload module for the async compile pool.
3
+
4
+ Imported once inside the multiprocessing *forkserver* process (see
5
+ ``multiprocessing.set_forkserver_preload``). Every pool worker is then
6
+ ``fork()``-ed from that warm process and inherits the imported interpreter
7
+ state via copy-on-write: worker startup drops from ~13 s (torch 4 s +
8
+ cutlass/cute/tvm_ffi 9 s per spawn) to ~0.1 s per fork.
9
+
10
+ This is the same architecture as PyTorch Inductor's compile-worker
11
+ ``SubprocPool``: one sidecar pays the import, workers fork from it.
12
+
13
+ Fork-safety: nothing here may initialize CUDA (a forked child of a
14
+ CUDA-initialized process is undefined behavior). Importing torch and
15
+ cutlass does not create a CUDA context; workers additionally run with
16
+ ``CUDA_VISIBLE_DEVICES=""`` + ``QUACK_ARCH``/``CUTE_DSL_ARCH`` overrides so
17
+ the compile path never touches the driver (the same mechanism the CPU-only
18
+ compile workflow uses).
19
+ """
20
+
21
+ import os
22
+ import subprocess
23
+
24
+ # Pin the target arch BEFORE importing quack: import-time code paths (e.g.
25
+ # rmsnorm_config._detect_arch_major) consult QUACK_ARCH via
26
+ # get_device_capacity and would otherwise initialize CUDA — which both makes
27
+ # the forkserver's context leak into children and trips torch's forked-child
28
+ # guard. nvidia-smi queries the capability without creating a CUDA context.
29
+ if "QUACK_ARCH" not in os.environ:
30
+ try:
31
+ out = subprocess.run(
32
+ ["nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader"],
33
+ capture_output=True,
34
+ text=True,
35
+ timeout=10,
36
+ )
37
+ cap = out.stdout.strip().splitlines()[0].strip() # e.g. "9.0"
38
+ major, minor = cap.split(".")
39
+ os.environ["QUACK_ARCH"] = f"{major}{minor}"
40
+ os.environ.setdefault(
41
+ "CUTE_DSL_ARCH", f"sm_{major}{minor}a" if int(major) >= 9 else f"sm_{major}{minor}"
42
+ )
43
+ except Exception:
44
+ pass # CPU-only box: rely on user-provided env, as before
45
+
46
+ # Belt and suspenders: even if some import still tries to touch CUDA, make
47
+ # it see no devices rather than creating a context in the forkserver.
48
+ os.environ["CUDA_VISIBLE_DEVICES"] = ""
49
+
50
+ from .. import cache # noqa: F401, E402 (pulls torch, cutlass.cute, tvm_ffi)
build/torch-cuda/quack/cache/async_compile.py ADDED
@@ -0,0 +1,413 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026, Tri Dao.
2
+ """Async kernel compilation: defer-and-retry via a pool of CPU subprocesses.
3
+
4
+ When a pool is active, ``jit_cache`` handles a ``.o``-cache miss by
5
+ submitting the pickled ``(module, qualname, args, kwargs)`` of the
6
+ ``_compile_*`` function to the pool and raising :class:`CompilePending`
7
+ instead of compiling in-process. The caller defers the work item, runs
8
+ something else, and retries once the ``.o`` lands (a ~1 ms load). Two
9
+ callers implement this loop:
10
+
11
+ * the pytest plugin's ``--async-compile`` defer loop (tests are the work
12
+ items; see :mod:`quack.testing.pytest_plugin`);
13
+ * the autotuner's bench loop under :func:`pool_scope` (candidate configs
14
+ are the work items; see :class:`quack.autotuner.Autotuner`).
15
+
16
+ Design notes:
17
+
18
+ * **The ``.o`` file is the only rendezvous** between workers and consumers —
19
+ compiled kernels aren't picklable, so the persistent cache doubles as the
20
+ IPC channel, and the per-key ``flock`` in ``jit_cache`` doubles as
21
+ cross-process dedupe (multiple pools / xdist workers coexist safely;
22
+ :func:`_flock_held_exclusively` lets a consumer defer on a key some other
23
+ process is already compiling).
24
+ * **Workers never launch kernels by construction**: they call the
25
+ tensor-free ``_compile_*`` functions directly, GPU-blind (arch pinned via
26
+ ``QUACK_ARCH``/``CUTE_DSL_ARCH``, ``CUDA_VISIBLE_DEVICES=""``).
27
+ * **Worker startup is an Inductor-style sidecar**: a ``forkserver`` preloads
28
+ torch/cutlass once (:mod:`quack.cache._pool_preload`, ~13 s) and workers
29
+ fork from it copy-on-write (~0.1 s each). :func:`_neutral_main` keeps
30
+ multiprocessing child prep from re-executing the user's script.
31
+ * **Failure semantics**: a failed pool compile is never trusted — the
32
+ consumer falls through to an in-process compile so the real exception
33
+ surfaces with a local traceback.
34
+
35
+ Env knobs: ``QUACK_ASYNC_COMPILE_START=spawn`` (disable the fork sidecar),
36
+ ``QUACK_COMPILE_WORKERS`` (shared-executor size, default 8).
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import base64
42
+ import contextlib
43
+ import fcntl
44
+ import importlib
45
+ import os
46
+ import pickle
47
+ from concurrent.futures import Future, ProcessPoolExecutor
48
+ from multiprocessing import get_context
49
+ from typing import Optional
50
+
51
+
52
+ def _flock_held_exclusively(lock_path: str) -> bool:
53
+ """True if some process currently holds the flock exclusively.
54
+
55
+ Used to detect "another process is compiling this key right now" so the
56
+ consumer defers instead of submitting a duplicate compile to its own
57
+ pool (a duplicate would occupy a pool slot blocked on the same flock).
58
+ """
59
+ try:
60
+ fd = os.open(lock_path, os.O_RDONLY | os.O_CREAT)
61
+ except OSError:
62
+ return False
63
+ try:
64
+ try:
65
+ fcntl.flock(fd, fcntl.LOCK_SH | fcntl.LOCK_NB)
66
+ fcntl.flock(fd, fcntl.LOCK_UN)
67
+ return False
68
+ except OSError:
69
+ return True
70
+ finally:
71
+ os.close(fd)
72
+
73
+
74
+ class CompilePending(BaseException):
75
+ """A jit_cache miss was submitted to the async compile pool.
76
+
77
+ The caller (test) cannot proceed until the ``.o`` exists; the test runner
78
+ should defer the test and retry it later. Carries the cache ``sha`` so
79
+ the runner can poll for completion without re-running the test.
80
+
81
+ Derives from :class:`BaseException` (like ``KeyboardInterrupt``) so that
82
+ test-body ``except Exception`` / ``pytest.raises(Exception)`` blocks
83
+ cannot swallow it and turn a not-yet-run test into a false pass. Only
84
+ the plugin's phase hooks are supposed to catch it.
85
+ """
86
+
87
+ def __init__(self, sha: str, qualname: str):
88
+ super().__init__(f"kernel compile pending in pool: {qualname} [{sha[:12]}]")
89
+ self.sha = sha
90
+ self.qualname = qualname
91
+
92
+
93
+ def _detect_arch_env() -> tuple[Optional[str], Optional[str]]:
94
+ """Return (QUACK_ARCH, CUTE_DSL_ARCH) for GPU-blind pool workers.
95
+
96
+ An explicit ``QUACK_ARCH`` env override wins — CI cross-compiles for a
97
+ different arch than the runner's GPU (e.g. ``QUACK_ARCH=120`` on an
98
+ H100), and workers must compile for the *target* arch, not the physical
99
+ one. Otherwise detect from the parent's GPU. Either way the workers
100
+ themselves never touch the CUDA driver (no context per worker,
101
+ fork-safe).
102
+ """
103
+ quack_arch = os.environ.get("QUACK_ARCH")
104
+ if quack_arch is not None:
105
+ cute_arch = os.environ.get("CUTE_DSL_ARCH")
106
+ if cute_arch is None:
107
+ from ..cute_dsl_utils import _parse_arch_str
108
+
109
+ major, minor = _parse_arch_str(quack_arch)
110
+ cc = f"{major}{minor}"
111
+ cute_arch = f"sm_{cc}a" if major >= 9 else f"sm_{cc}"
112
+ return quack_arch, cute_arch
113
+ try:
114
+ import torch
115
+
116
+ if torch.cuda.is_available():
117
+ major, minor = torch.cuda.get_device_capability()
118
+ cc = f"{major}{minor}"
119
+ return cc, f"sm_{cc}a" if major >= 9 else f"sm_{cc}"
120
+ except Exception:
121
+ pass
122
+ return None, os.environ.get("CUTE_DSL_ARCH")
123
+
124
+
125
+ def _pool_initializer(quack_arch: Optional[str], cute_dsl_arch: Optional[str]):
126
+ # GPU-blind compilation: hide devices and pin the target arch via the
127
+ # same overrides the CPU-only compile workflow uses. Forked workers must
128
+ # never initialize CUDA (fork-safety), and spawned workers save the
129
+ # ~1-2 s + ~300 MB of a per-worker CUDA context.
130
+ if quack_arch is not None:
131
+ os.environ["QUACK_ARCH"] = quack_arch
132
+ os.environ["CUDA_VISIBLE_DEVICES"] = ""
133
+ if cute_dsl_arch is not None:
134
+ os.environ["CUTE_DSL_ARCH"] = cute_dsl_arch
135
+ # Pay the heavy torch/cutlass import at worker start (no-op under
136
+ # forkserver: the preload already imported it before the fork).
137
+ from .. import cache # noqa: F401
138
+
139
+
140
+ def _pool_worker(mod_name: str, qualname: str, key_b64: str, o_path: str) -> Optional[str]:
141
+ """Compile one key. Returns None on success, error string on failure."""
142
+ try:
143
+ obj = importlib.import_module(mod_name)
144
+ for part in qualname.split("."):
145
+ obj = getattr(obj, part)
146
+ args, kwargs = pickle.loads(base64.b64decode(key_b64))
147
+ obj(*args, **kwargs) # jit_cache wrapper: compiles + exports .o
148
+ if not os.path.exists(o_path):
149
+ return "compile succeeded but .o was not exported"
150
+ return None
151
+ except Exception as e:
152
+ return f"{type(e).__name__}: {e}"
153
+
154
+
155
+ def _make_executor(jobs: int) -> ProcessPoolExecutor:
156
+ """Build a compile-worker executor (Inductor-style forkserver sidecar).
157
+
158
+ Forkserver + preload: one sidecar process pays the ~13 s torch/cutlass
159
+ import once, workers fork from it in ~0.1 s each (copy-on-write). The
160
+ forkserver singleton is shared per-process, so multiple executors (the
161
+ test pool, the autotuner's) fork from the same warm sidecar. Opt out
162
+ with QUACK_ASYNC_COMPILE_START=spawn.
163
+ """
164
+ start_method = os.environ.get("QUACK_ASYNC_COMPILE_START", "forkserver")
165
+ ctx = get_context(start_method)
166
+ if start_method == "forkserver":
167
+ # vendored under sonic_moe.quack: preload the sibling module by package
168
+ # (upstream hardcodes the top-level "quack.cache._pool_preload").
169
+ ctx.set_forkserver_preload([__package__ + "._pool_preload"])
170
+ return ProcessPoolExecutor(
171
+ max_workers=jobs,
172
+ mp_context=ctx,
173
+ initializer=_pool_initializer,
174
+ initargs=_detect_arch_env(),
175
+ )
176
+
177
+
178
+ _shared_executor: Optional[ProcessPoolExecutor] = None
179
+
180
+
181
+ def get_shared_executor() -> ProcessPoolExecutor:
182
+ """Executor for ad-hoc compile tasks (e.g. autotuner precompile sweeps).
183
+
184
+ Reuses the active :class:`CompilePool`'s executor when one exists (the
185
+ pytest ``--async-compile`` session pool); otherwise lazily creates a
186
+ process-wide executor sized by ``QUACK_COMPILE_WORKERS`` (default 8).
187
+ Deliberately ignores :class:`suppress_pool` — suppression turns off the
188
+ *defer-on-miss* behavior of jit_cache, not access to compile workers.
189
+ """
190
+ global _shared_executor
191
+ if _active_pool is not None:
192
+ return _active_pool._executor
193
+ if _shared_executor is None:
194
+ import atexit
195
+
196
+ _shared_executor = _make_executor(int(os.environ.get("QUACK_COMPILE_WORKERS", "8")))
197
+ # Explicit teardown: without this, the executor is GC'd during
198
+ # interpreter shutdown after its weakref machinery is already gone,
199
+ # printing a spurious "Exception ignored in weakref_cb".
200
+ atexit.register(_shared_executor.shutdown, wait=False, cancel_futures=True)
201
+ return _shared_executor
202
+
203
+
204
+ @contextlib.contextmanager
205
+ def _neutral_main():
206
+ """Stop multiprocessing child prep from re-executing the user's script.
207
+
208
+ ``Process.start()`` captures preparation data from ``sys.modules['__main__']``:
209
+ for a path-based script the *child* re-runs the whole file via
210
+ ``runpy.run_path`` (so pickles referencing ``__main__`` resolve). Our
211
+ tasks never reference ``__main__`` — they resolve everything by module
212
+ name — and a user script that, say, builds CUDA tensors at import time
213
+ would kill every worker at spawn with "Cannot re-initialize CUDA in
214
+ forked subprocess". Executor workers are spawned synchronously inside
215
+ ``executor.submit`` (``_adjust_process_count``), so masking ``__main__``
216
+ with an empty stub for the duration of the submit is sufficient and
217
+ scoped. Single-threaded callers only (pytest defer loop, autotune bench
218
+ loop).
219
+ """
220
+ import sys
221
+ import types
222
+
223
+ real_main = sys.modules.get("__main__")
224
+ sys.modules["__main__"] = types.ModuleType("__main__") # no __file__/__spec__
225
+ try:
226
+ yield
227
+ finally:
228
+ if real_main is not None:
229
+ sys.modules["__main__"] = real_main
230
+
231
+
232
+ class CompilePool:
233
+ """Process pool + in-flight bookkeeping, keyed by jit_cache sha.
234
+
235
+ Owns its executor by default; pass ``executor=`` to share one (e.g.
236
+ :func:`pool_scope` wraps the session-long shared executor so scoped
237
+ pools don't respawn workers per autotune sweep). A shared executor is
238
+ not shut down by :meth:`shutdown` — only this pool's futures are
239
+ cancelled.
240
+ """
241
+
242
+ def __init__(self, jobs: Optional[int] = None, executor: Optional[ProcessPoolExecutor] = None):
243
+ self._own_executor = executor is None
244
+ self._executor = executor if executor is not None else _make_executor(jobs)
245
+ self._futures: dict[str, Future] = {}
246
+ # Keys being compiled by *another process* (e.g. a different xdist
247
+ # worker's pool), detected via the per-key flock. We defer on them
248
+ # without spending one of our own pool slots on a duplicate compile.
249
+ # sha -> (o_path, lock_path)
250
+ self._external: dict[str, tuple[str, str]] = {}
251
+ self.n_submitted = 0
252
+
253
+ def mark_external(self, sha: str, o_path: str, lock_path: str) -> None:
254
+ """Record that some other process is compiling ``sha`` right now."""
255
+ if sha not in self._futures:
256
+ self._external[sha] = (str(o_path), str(lock_path))
257
+
258
+ def prewarm(self) -> None:
259
+ """Start the sidecar + first worker now, off the critical path.
260
+
261
+ Same idea as Inductor's ``warm_pool()``: the forkserver's ~13 s
262
+ torch/cutlass preload import starts at the first ``Process`` spawn,
263
+ which is lazy (first submit). Submitting a no-op at session setup
264
+ overlaps that import with pytest collection and the leading warm
265
+ tests instead of the first cold compile.
266
+ """
267
+ with _neutral_main():
268
+ self._executor.submit(os.getpid)
269
+
270
+ def submit_raw(self, sha: str, mod: str, qualname: str, key_b64: str, o_path: str) -> None:
271
+ if sha in self._futures:
272
+ return
273
+ with _neutral_main():
274
+ self._futures[sha] = self._executor.submit(_pool_worker, mod, qualname, key_b64, o_path)
275
+ self.n_submitted += 1
276
+
277
+ def submit(self, sha: str, fn, args: tuple, kwargs: dict, o_path) -> bool:
278
+ """Submit a live jit_cache miss. Returns False if the key can't be
279
+ shipped to a subprocess (unpicklable args, ``<locals>`` qualname,
280
+ fn defined in ``__main__``) — the caller should compile in-process
281
+ instead."""
282
+ if sha in self._futures:
283
+ return True
284
+ if "<locals>" in fn.__qualname__ or fn.__module__ == "__main__":
285
+ # Not resolvable by module+qualname in a worker; compile in-process.
286
+ return False
287
+ try:
288
+ key_b64 = base64.b64encode(pickle.dumps((args, kwargs))).decode("ascii")
289
+ except Exception:
290
+ return False
291
+ self.submit_raw(sha, fn.__module__, fn.__qualname__, key_b64, str(o_path))
292
+ return True
293
+
294
+ def poll(self, sha: str) -> tuple[str, Optional[str]]:
295
+ """Return (state, error): state in {"new", "pending", "done", "failed"}."""
296
+ fut = self._futures.get(sha)
297
+ if fut is None:
298
+ ext = self._external.get(sha)
299
+ if ext is not None:
300
+ o_path, lock_path = ext
301
+ if os.path.exists(o_path):
302
+ del self._external[sha]
303
+ return "done", None
304
+ if _flock_held_exclusively(lock_path):
305
+ return "pending", None
306
+ # External compiler released the lock without producing a .o
307
+ # (crashed / failed): forget it so the next attempt submits
308
+ # to our own pool.
309
+ del self._external[sha]
310
+ return "new", None
311
+ if not fut.done():
312
+ return "pending", None
313
+ try:
314
+ err = fut.result()
315
+ except Exception as e: # BrokenProcessPool etc.
316
+ err = f"pool worker died: {type(e).__name__}: {e}"
317
+ return ("done", None) if err is None else ("failed", err)
318
+
319
+ def stats(self) -> dict:
320
+ done = sum(1 for f in self._futures.values() if f.done())
321
+ errors = []
322
+ for sha, f in self._futures.items():
323
+ if not f.done() or f.cancelled():
324
+ continue
325
+ exc = f.exception()
326
+ err = f"{type(exc).__name__}: {exc}" if exc is not None else f.result()
327
+ if err:
328
+ errors.append((sha, err))
329
+ return {
330
+ "submitted": self.n_submitted,
331
+ "done": done,
332
+ "failed": len(errors),
333
+ "errors": errors,
334
+ }
335
+
336
+ def shutdown(self) -> None:
337
+ if self._own_executor:
338
+ self._executor.shutdown(wait=False, cancel_futures=True)
339
+ else:
340
+ for fut in self._futures.values():
341
+ fut.cancel()
342
+
343
+
344
+ # --- module-level active pool -----------------------------------------------
345
+
346
+ _active_pool: Optional[CompilePool] = None
347
+ _suppress_depth = 0
348
+
349
+
350
+ class suppress_pool:
351
+ """Context manager: make :func:`get_active_pool` return None inside.
352
+
353
+ Used by the test runner for a deferred test's final attempt: compile
354
+ in-process (blocking) so a key that never completes in the pool still
355
+ produces a real result or a real traceback instead of deferring forever.
356
+ """
357
+
358
+ def __enter__(self):
359
+ global _suppress_depth
360
+ _suppress_depth += 1
361
+ return self
362
+
363
+ def __exit__(self, *exc):
364
+ global _suppress_depth
365
+ _suppress_depth -= 1
366
+
367
+
368
+ def activate(jobs: int) -> CompilePool:
369
+ """Activate the session-wide pool (idempotent). Used by the pytest plugin;
370
+ scoped callers should prefer :func:`pool_scope`."""
371
+ global _active_pool
372
+ if _active_pool is None:
373
+ _active_pool = CompilePool(jobs)
374
+ return _active_pool
375
+
376
+
377
+ def deactivate() -> None:
378
+ global _active_pool
379
+ if _active_pool is not None:
380
+ _active_pool.shutdown()
381
+ _active_pool = None
382
+
383
+
384
+ def get_active_pool() -> Optional[CompilePool]:
385
+ return None if _suppress_depth > 0 else _active_pool
386
+
387
+
388
+ @contextlib.contextmanager
389
+ def pool_scope():
390
+ """Activate a compile pool for the duration of the block.
391
+
392
+ Reuses the globally active pool when one exists (e.g. the pytest
393
+ ``--async-compile`` session pool); otherwise activates a temporary pool
394
+ backed by the shared executor and deactivates it on exit — so
395
+ ``CompilePending`` can only escape into code inside the block, never
396
+ into unrelated user code paths.
397
+
398
+ This is how the autotuner overlaps candidate-config compilation with
399
+ benchmarking: the bench loop runs inside ``pool_scope()``, catches
400
+ ``CompilePending`` per config, and retries a config once its ``.o``
401
+ lands (see ``Autotuner.__call__``).
402
+ """
403
+ global _active_pool
404
+ if _active_pool is not None:
405
+ yield _active_pool
406
+ return
407
+ pool = CompilePool(executor=get_shared_executor())
408
+ _active_pool = pool
409
+ try:
410
+ yield pool
411
+ finally:
412
+ _active_pool = None
413
+ pool.shutdown()
build/torch-cuda/quack/{cache_utils.py → cache/jit.py} RENAMED
@@ -1,15 +1,21 @@
1
  # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
- """Persistent .o cache for CuTe DSL compiled kernels.
3
 
4
- Compiled kernels are exported as object files (.o) via export_to_c.
5
- On subsequent runs the .o is loaded via tvm_ffi (~1ms) instead of
6
- re-generating IR + re-JIT'ing (~100ms per kernel).
7
 
8
- Controls:
9
- QUACK_CACHE_ENABLED=0 — disable persistent .o cache (default: enabled)
10
- QUACK_CACHE_DIR=path — override default cache directory
 
 
 
 
11
  """
12
 
 
 
13
  import fcntl
14
  import functools
15
  import hashlib
@@ -26,26 +32,20 @@ import cutlass
26
  import cutlass.cute as cute
27
  import tvm_ffi
28
 
29
- CACHE_ENABLED: bool = os.getenv("QUACK_CACHE_ENABLED", "1") == "1"
30
- CACHE_DIR: str | None = os.getenv("QUACK_CACHE_DIR", None)
31
- COMPILE_ONLY: bool = False
 
32
 
33
- # Downstream projects can append directories here to include their sources
34
- # in the cache fingerprint. Must be set before the first jit_cache call.
35
- EXTRA_SOURCE_DIRS: list[Path] = []
36
 
37
  EXPORT_FUNC_NAME = "func"
38
  LOCK_TIMEOUT = 60
39
  CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
40
 
41
 
42
- def _noop_kernel(*args, **kwargs):
43
- pass
44
-
45
-
46
  def get_cache_path() -> Path:
47
- if CACHE_DIR is not None:
48
- cache_dir = Path(CACHE_DIR)
49
  else:
50
  cache_dir = Path(tempfile.gettempdir()) / getuser() / "quack_cache"
51
  cache_dir.mkdir(parents=True, exist_ok=True)
@@ -70,8 +70,13 @@ def _compute_source_fingerprint() -> str:
70
  h.update(f"py{sys.version_info.major}.{sys.version_info.minor}".encode())
71
  h.update(f"cutlass={cutlass.__version__}".encode())
72
  h.update(f"tvm_ffi={tvm_ffi.__version__}".encode())
73
- _hash_source_dir(h, Path(__file__).resolve().parent)
74
- for extra_dir in EXTRA_SOURCE_DIRS:
 
 
 
 
 
75
  _hash_source_dir(h, Path(extra_dir).resolve())
76
  return h.hexdigest()
77
 
@@ -126,6 +131,25 @@ def jit_cache(fn):
126
 
127
  The decorated function should return a compiled kernel (i.e. call cute.compile).
128
  The disk cache key is (fn.__qualname__, *args, **sorted_kwargs).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  """
130
  cache = {}
131
  hits = 0
@@ -136,49 +160,162 @@ def jit_cache(fn):
136
  nonlocal hits, misses
137
  cache_key = args + tuple(sorted(kwargs.items())) if kwargs else args
138
 
139
- # 1. In-memory hit
 
 
 
 
140
  if cache_key in cache:
141
  hits += 1
142
- return _noop_kernel if COMPILE_ONLY else cache[cache_key]
143
-
144
- # 2. Disk hit
145
- disk_key = (fn.__qualname__,) + cache_key
146
- if CACHE_ENABLED:
147
- sha = _key_to_hash(disk_key)
148
- cache_path = get_cache_path() / _compute_source_fingerprint()
149
- cache_path.mkdir(parents=True, exist_ok=True)
150
- o_path = cache_path / f"{sha}.o"
151
- lock_path = cache_path / f"{sha}.lock"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  try:
153
  with FileLock(lock_path, exclusive=False, timeout=LOCK_TIMEOUT):
154
  if o_path.exists():
155
- m = cute.runtime.load_module(str(o_path), enable_tvm_ffi=True)
156
- loaded = m[EXPORT_FUNC_NAME]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  cache[cache_key] = loaded
158
  hits += 1
159
- return _noop_kernel if COMPILE_ONLY else loaded
160
- except RuntimeError:
161
- pass
162
-
163
- # 3. Compile
164
- misses += 1
165
- compiled_fn = fn(*args, **kwargs)
166
-
167
- # 4. Store
168
- cache[cache_key] = compiled_fn
169
- if CACHE_ENABLED:
170
- try:
171
- with FileLock(lock_path, exclusive=True, timeout=LOCK_TIMEOUT):
172
- if not o_path.exists():
173
- o_path.parent.mkdir(parents=True, exist_ok=True)
174
- compiled_fn.export_to_c(
175
- object_file_path=str(o_path),
176
- function_name=EXPORT_FUNC_NAME,
177
- )
178
- except Exception as e:
179
- print(f"quack cache: export failed for key {sha}: {e}")
180
-
181
- return _noop_kernel if COMPILE_ONLY else compiled_fn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
  def cache_clear():
184
  nonlocal hits, misses
 
1
  # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+ """Persistent ``.o`` cache for CuTe DSL compiled kernels.
3
 
4
+ Compiled kernels are exported as object files (``.o``) via ``export_to_c``. On
5
+ subsequent runs the ``.o`` is loaded via tvm_ffi (~1 ms) instead of
6
+ re-generating IR + re-JIT'ing (~500 ms per kernel).
7
 
8
+ Runtime config (``CACHE_ENABLED``, ``CACHE_DIR``, ``EXTRA_SOURCE_DIRS``)
9
+ lives in :mod:`quack.cache` (the package init).
10
+
11
+ When an async compile pool is active (see :mod:`quack.cache.async_compile`),
12
+ a cold miss is shipped to a CPU worker and :class:`CompilePending` is raised
13
+ instead of compiling in-process; the caller (pytest defer loop, autotune
14
+ bench loop) retries once the ``.o`` lands.
15
  """
16
 
17
+ from __future__ import annotations
18
+
19
  import fcntl
20
  import functools
21
  import hashlib
 
32
  import cutlass.cute as cute
33
  import tvm_ffi
34
 
35
+ # `quack.cache` (the package itself) holds the mutable runtime flags as a
36
+ # single source of truth; reads happen via attribute access on `_state` so we
37
+ # always see the live value, not a snapshot taken at module import.
38
+ from .. import cache as _state # noqa: E402 (intentional partial-import; see __init__.py)
39
 
 
 
 
40
 
41
  EXPORT_FUNC_NAME = "func"
42
  LOCK_TIMEOUT = 60
43
  CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
44
 
45
 
 
 
 
 
46
  def get_cache_path() -> Path:
47
+ if _state.CACHE_DIR is not None:
48
+ cache_dir = Path(_state.CACHE_DIR)
49
  else:
50
  cache_dir = Path(tempfile.gettempdir()) / getuser() / "quack_cache"
51
  cache_dir.mkdir(parents=True, exist_ok=True)
 
70
  h.update(f"py{sys.version_info.major}.{sys.version_info.minor}".encode())
71
  h.update(f"cutlass={cutlass.__version__}".encode())
72
  h.update(f"tvm_ffi={tvm_ffi.__version__}".encode())
73
+ # Hash the entire `quack` package, not just `quack/cache/`. Resolving via
74
+ # the top-level package import keeps the fingerprint stable regardless of
75
+ # where inside the package this file lives.
76
+ import importlib as _importlib; _quack = _importlib.import_module(__package__.rsplit(".", 1)[0])
77
+
78
+ _hash_source_dir(h, Path(_quack.__file__).resolve().parent)
79
+ for extra_dir in _state.EXTRA_SOURCE_DIRS:
80
  _hash_source_dir(h, Path(extra_dir).resolve())
81
  return h.hexdigest()
82
 
 
131
 
132
  The decorated function should return a compiled kernel (i.e. call cute.compile).
133
  The disk cache key is (fn.__qualname__, *args, **sorted_kwargs).
134
+
135
+ Concurrency model
136
+ -----------------
137
+ The disk side uses a per-key ``{sha}.lock`` file (advisory ``flock``):
138
+
139
+ * **Fast path (warm cache).** If the ``.o`` file already exists, we take a
140
+ shared lock just long enough to ``load_module`` it. Many readers can
141
+ proceed concurrently.
142
+ * **Slow path (cold cache).** The actual ``fn(*args, **kwargs)`` compile
143
+ runs *under* the exclusive lock. This serializes redundant compilations
144
+ of the same key across xdist workers / processes: if N processes race
145
+ on a cold key, only one calls ``cute.compile``; the rest wait for the
146
+ lock, see the ``.o`` appear, and load it. (Previously the compile ran
147
+ *between* the shared-lock check and the exclusive-lock export, so all
148
+ N processes wasted CPU compiling the same key in parallel — wall time
149
+ was unchanged but compile-CPU pressure scaled with concurrency, which
150
+ starved other compiles when many keys were cold at once.)
151
+
152
+ The lock is per-key, so distinct keys never contend with each other.
153
  """
154
  cache = {}
155
  hits = 0
 
160
  nonlocal hits, misses
161
  cache_key = args + tuple(sorted(kwargs.items())) if kwargs else args
162
 
163
+ # Snapshot once per call so a concurrent flip of ``_state.CACHE_ENABLED``
164
+ # mid-call can't desync the disk-path branch.
165
+ enabled = _state.CACHE_ENABLED
166
+
167
+ # 1. In-memory hit. Same process already compiled or loaded this key.
168
  if cache_key in cache:
169
  hits += 1
170
+ return cache[cache_key]
171
+
172
+ # 2. Cache disabled: pure in-process compile, no disk side effects.
173
+ if not enabled:
174
+ misses += 1
175
+ compiled_fn = fn(*args, **kwargs)
176
+ cache[cache_key] = compiled_fn
177
+ return compiled_fn
178
+
179
+ sha = _key_to_hash((fn.__qualname__,) + cache_key)
180
+ cache_path = get_cache_path() / _compute_source_fingerprint()
181
+ cache_path.mkdir(parents=True, exist_ok=True)
182
+ o_path = cache_path / f"{sha}.o"
183
+ lock_path = cache_path / f"{sha}.lock"
184
+
185
+ def _load_cached() -> object:
186
+ """Load the .o into a callable; caller guarantees existence."""
187
+ m = cute.runtime.load_module(str(o_path), enable_tvm_ffi=True)
188
+ return m[EXPORT_FUNC_NAME]
189
+
190
+ def _quarantine_corrupt(exc: Exception) -> None:
191
+ """A cached .o that fails to load (truncated write from a killed
192
+ worker, missing __tvm_ffi_func, ...) is a cache miss, not an error:
193
+ delete it so this and future processes recompile instead of failing
194
+ forever (the CI cache persists across runs)."""
195
+ print(
196
+ f"quack cache: corrupt cached object for key {sha} "
197
+ f"({type(exc).__name__}: {exc}); deleting and recompiling"
198
+ )
199
+ try:
200
+ o_path.unlink()
201
+ except OSError:
202
+ pass
203
+
204
+ # 3. Fast path: optimistic existence check, then shared-lock load.
205
+ # The unlocked ``.exists()`` is a no-cost short-circuit for warm
206
+ # caches; the shared lock guards against reading a partial file
207
+ # while a concurrent writer holds the exclusive lock.
208
+ if o_path.exists():
209
  try:
210
  with FileLock(lock_path, exclusive=False, timeout=LOCK_TIMEOUT):
211
  if o_path.exists():
212
+ try:
213
+ loaded = _load_cached()
214
+ except Exception as e:
215
+ # Corrupt entry: recover under the exclusive lock in
216
+ # the slow path (shared lock can't safely delete).
217
+ _quarantine_corrupt(e)
218
+ else:
219
+ cache[cache_key] = loaded
220
+ hits += 1
221
+ return loaded
222
+ except RuntimeError:
223
+ pass # lock timeout; fall through to slow path
224
+
225
+ # 3b. Async-compile pool: on a cold miss with a pool
226
+ # active, ship the key to a CPU subprocess and raise
227
+ # CompilePending instead of compiling in-process. The test runner
228
+ # defers the test and retries once the worker has exported the
229
+ # .o. Pool failures fall through to the in-process compile below
230
+ # so the real exception surfaces with a local traceback.
231
+ from . import async_compile as _async
232
+
233
+ pool = _async.get_active_pool()
234
+ if pool is not None:
235
+ state, err = pool.poll(sha)
236
+ if state == "new":
237
+ # If another process (e.g. a different xdist worker's pool)
238
+ # holds the exclusive per-key flock, it is compiling this key
239
+ # right now: defer on it instead of submitting a duplicate.
240
+ if _async._flock_held_exclusively(str(lock_path)):
241
+ pool.mark_external(sha, str(o_path), str(lock_path))
242
+ raise _async.CompilePending(sha, fn.__qualname__)
243
+ if pool.submit(sha, fn, args, kwargs, o_path):
244
+ raise _async.CompilePending(sha, fn.__qualname__)
245
+ # unpicklable key / <locals> qualname: compile in-process
246
+ elif state == "pending":
247
+ raise _async.CompilePending(sha, fn.__qualname__)
248
+ elif state == "done":
249
+ try:
250
+ with FileLock(lock_path, exclusive=False, timeout=LOCK_TIMEOUT):
251
+ if o_path.exists():
252
+ try:
253
+ loaded = _load_cached()
254
+ except Exception as e:
255
+ _quarantine_corrupt(e)
256
+ else:
257
+ cache[cache_key] = loaded
258
+ hits += 1
259
+ return loaded
260
+ except RuntimeError:
261
+ pass # lock timeout; fall through to slow path
262
+ else: # "failed"
263
+ print(
264
+ f"quack cache: async compile failed for {fn.__qualname__} "
265
+ f"[{sha[:12]}]: {err}; recompiling in-process for a real traceback"
266
+ )
267
+
268
+ # 4. Slow path: take EXCLUSIVE lock and compile under it. The recheck
269
+ # inside the lock catches the race where another process compiled
270
+ # while we were waiting; in that case we just load and return
271
+ # without duplicating the compile.
272
+ try:
273
+ with FileLock(lock_path, exclusive=True, timeout=LOCK_TIMEOUT):
274
+ if o_path.exists():
275
+ try:
276
+ loaded = _load_cached()
277
+ except Exception as e:
278
+ _quarantine_corrupt(e) # holds the exclusive lock: safe
279
+ else:
280
  cache[cache_key] = loaded
281
  hits += 1
282
+ return loaded
283
+
284
+ misses += 1
285
+ compiled_fn = fn(*args, **kwargs)
286
+ # Export to a private temp file, then atomically rename into
287
+ # place: a process killed mid-export (xdist worker OOM-kill,
288
+ # timeout) must never leave a truncated .o at the final path —
289
+ # the advisory flock dies with the process, and a persistent
290
+ # cache (CI keeps one in $HOME) would then fail every future
291
+ # run on this key with "Symbols not found: __tvm_ffi_func".
292
+ tmp_path = o_path.with_suffix(f".o.tmp.{os.getpid()}")
293
+ try:
294
+ compiled_fn.export_to_c(
295
+ object_file_path=str(tmp_path),
296
+ function_name=EXPORT_FUNC_NAME,
297
+ )
298
+ os.replace(tmp_path, o_path)
299
+ except Exception as e:
300
+ print(f"quack cache: export failed for key {sha}: {e}")
301
+ try:
302
+ tmp_path.unlink()
303
+ except OSError:
304
+ pass
305
+ cache[cache_key] = compiled_fn
306
+ return compiled_fn
307
+ except RuntimeError as e:
308
+ # Lock acquisition timed out (heavy contention or stuck holder).
309
+ # Fall back to in-process compile, no disk write. Better to do
310
+ # the work twice than to fail the test.
311
+ print(
312
+ f"quack cache: lock timeout for key {sha}: {e}; "
313
+ f"falling back to in-process compile without disk cache"
314
+ )
315
+ misses += 1
316
+ compiled_fn = fn(*args, **kwargs)
317
+ cache[cache_key] = compiled_fn
318
+ return compiled_fn
319
 
320
  def cache_clear():
321
  nonlocal hits, misses
build/torch-cuda/quack/compile_utils.py CHANGED
@@ -6,10 +6,20 @@ import cutlass.cute as cute
6
 
7
 
8
  def make_fake_tensor(dtype, shape, divisibility=1, leading_dim=-1) -> Optional[cute.Tensor]:
9
- if leading_dim < 0:
10
- leading_dim = len(shape) + leading_dim
 
 
 
 
 
 
 
 
11
  if dtype is None:
12
  return None
 
 
13
  stride = tuple(
14
  cute.sym_int64(divisibility=divisibility) if i != leading_dim else 1
15
  for i in range(len(shape))
@@ -17,3 +27,8 @@ def make_fake_tensor(dtype, shape, divisibility=1, leading_dim=-1) -> Optional[c
17
  return cute.runtime.make_fake_tensor(
18
  dtype, shape, stride=stride, assumed_align=divisibility * dtype.width // 8
19
  )
 
 
 
 
 
 
6
 
7
 
8
  def make_fake_tensor(dtype, shape, divisibility=1, leading_dim=-1) -> Optional[cute.Tensor]:
9
+ """Build a fake CuTe tensor with dynamic (sym) strides for tensor-free compilation.
10
+
11
+ ``leading_dim`` selects the dim whose stride is statically 1 (matching
12
+ ``from_dlpack(...).mark_layout_dynamic(leading_dim=...)``). Pass
13
+ ``leading_dim=None`` for a fully-dynamic layout with no static stride-1 dim
14
+ (matching ``mark_layout_dynamic()`` on a tensor without a contiguous dim).
15
+
16
+ ``divisibility`` is in elements; ``assumed_align`` (bytes) is
17
+ ``divisibility * dtype.width // 8``.
18
+ """
19
  if dtype is None:
20
  return None
21
+ if leading_dim is not None and leading_dim < 0:
22
+ leading_dim = len(shape) + leading_dim
23
  stride = tuple(
24
  cute.sym_int64(divisibility=divisibility) if i != leading_dim else 1
25
  for i in range(len(shape))
 
27
  return cute.runtime.make_fake_tensor(
28
  dtype, shape, stride=stride, assumed_align=divisibility * dtype.width // 8
29
  )
30
+
31
+
32
+ def make_fake_stream():
33
+ """Fake CUDA stream for tensor-free compilation (real stream comes from the TVM FFI env)."""
34
+ return cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
build/torch-cuda/quack/complex.py ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Complex64 element type for CuTe-DSL kernels.
2
+
3
+ Single-precision complex (re + imj) carried as f64-packed bits (re in the low
4
+ 32 bits, im in the high 32 bits). f64 is on `cute.MemRefType`'s element-type
5
+ allowlist; the natural `complex<f32>` MLIR type is not. Arithmetic methods
6
+ unpack each f64 into two Float32 lanes, compute, and repack -- the bitcasts
7
+ are folded out by ptxas.
8
+
9
+ Inherits from `Float32` (with `width=64, mlir_type=T.f64` overrides) so that
10
+ Python's subclass-precedence rule routes `Float32 OP Complex64` to our
11
+ reflected `__r*__` operators before Numeric's promotion logic sees the
12
+ operands. Without this, Float32-on-the-LEFT would silently promote through
13
+ Float64 conversion and corrupt the packed bits.
14
+
15
+ Boundary convention (tvm-ffi): the compiled kernel's ABI sees f64 storage.
16
+ At the call site, pass `torch.complex64` tensors as `t.view(torch.float64)`
17
+ -- use `complex_storage(t)` for the conversion.
18
+
19
+ See `AI/complex64_design_notes.md` for the why and what's been validated.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import ctypes
25
+
26
+ import numpy as np
27
+ import torch
28
+
29
+ import cutlass.cute as cute
30
+ from cutlass import Float32, Numeric
31
+ from cutlass._mlir import ir
32
+ from cutlass._mlir.dialects import arith
33
+ from cutlass._mlir.extras import types as T
34
+ from cutlass._mlir_helpers.arith import bitcast as _bitcast
35
+ from cutlass.base_dsl.typing import FloatMeta
36
+
37
+
38
+ class Complex64(Float32, metaclass=FloatMeta, width=64, mlir_type=T.f64):
39
+ """Complex64 carried as f64-packed bits (re in low 32, im in high 32).
40
+
41
+ `tensor.element_type is Complex64` inside the kernel; indexing returns
42
+ Complex64 instances; `+`, `-`, `*`, `__neg__`, and `conj()` work natively.
43
+ """
44
+
45
+ def __init__(self, x, im=None, *, loc=None, ip=None):
46
+ # Two-arg lane form: Complex64(re, im) packs (re, im) into f64 bits.
47
+ # Coerce both through Float32 so int / float / Float32 / ir.Value(f32)
48
+ # all work as inputs.
49
+ if im is not None:
50
+ # Static fast path: both args are Python int/float -- no MLIR
51
+ # context needed (lets host-side code call Complex64(2.5, -1.5)).
52
+ if isinstance(x, (int, float)) and isinstance(im, (int, float)):
53
+ Complex64.__init__(self, complex(x, im), loc=loc, ip=ip)
54
+ return
55
+ re_ssa = Float32(x).ir_value()
56
+ im_ssa = Float32(im).ir_value()
57
+ Numeric.__init__(self, Complex64._pack_ssa(re_ssa, im_ssa))
58
+ return
59
+
60
+ # Same-type copy MUST be checked first. `_cvt_to_dest` (cute/tensor.py)
61
+ # calls `data.to(element_type)` on every tensor write, which becomes
62
+ # `Complex64(complex_instance)`; falling through to the generic-Numeric
63
+ # branch below would re-pack as (real_view_of_packed_bits, 0) and
64
+ # silently corrupt the data.
65
+ if isinstance(x, Complex64):
66
+ Numeric.__init__(self, x.value)
67
+ return
68
+
69
+ if isinstance(x, complex):
70
+ f64_val = _pack_python_complex(x)
71
+ Numeric.__init__(self, f64_val)
72
+ return
73
+
74
+ if isinstance(x, ir.Value):
75
+ if x.type == T.f64():
76
+ # Already in our storage form (loaded from a Complex64 tensor,
77
+ # or output of _pack_ssa).
78
+ Numeric.__init__(self, x)
79
+ return
80
+ if x.type == T.f32():
81
+ packed = Complex64._pack_ssa(x, arith.constant(T.f32(), 0.0))
82
+ Numeric.__init__(self, packed)
83
+ return
84
+ raise TypeError(f"Complex64: ir.Value of unsupported type {x.type}")
85
+
86
+ if isinstance(x, Numeric):
87
+ # Float32, Int32, Float64, etc. -> coerce real lane through Float32,
88
+ # imag lane = 0. Float32(Float32) is a no-op, so this also handles
89
+ # the Float32 case cleanly.
90
+ re_ssa = Float32(x).ir_value()
91
+ packed = Complex64._pack_ssa(re_ssa, arith.constant(T.f32(), 0.0))
92
+ Numeric.__init__(self, packed)
93
+ return
94
+
95
+ if isinstance(x, (int, float)):
96
+ Complex64.__init__(self, complex(x, 0.0), loc=loc, ip=ip)
97
+ return
98
+
99
+ raise TypeError(f"Complex64: unsupported source type {type(x)}")
100
+
101
+ # ---- packing / unpacking primitives --------------------------------
102
+
103
+ @staticmethod
104
+ def _pack_ssa(re_f32, im_f32):
105
+ """Pack two f32 SSA lanes into one f64 SSA value (re lo, im hi)."""
106
+ re_i32 = _bitcast(re_f32, T.i32())
107
+ im_i32 = _bitcast(im_f32, T.i32())
108
+ re_i64 = arith.extui(T.i64(), re_i32)
109
+ im_i64 = arith.extui(T.i64(), im_i32)
110
+ hi = arith.shli(im_i64, arith.constant(T.i64(), 32))
111
+ return _bitcast(arith.ori(re_i64, hi), T.f64())
112
+
113
+ def _unpack(self):
114
+ """Split self -> (re_f32, im_f32) as Float32 SSA values."""
115
+ i64_ssa = _bitcast(self.ir_value(), T.i64())
116
+ lo32 = arith.trunci(T.i32(), i64_ssa)
117
+ hi32 = arith.trunci(T.i32(), arith.shrui(i64_ssa, arith.constant(T.i64(), 32)))
118
+ return Float32(_bitcast(lo32, T.f32())), Float32(_bitcast(hi32, T.f32()))
119
+
120
+ @staticmethod
121
+ def from_re_im(re: Float32, im: Float32) -> "Complex64":
122
+ """Build a Complex64 from two Float32 SSA lanes.
123
+
124
+ Equivalent to `Complex64(re, im)`; kept as an explicit name for the
125
+ hot-path call sites that want to skip the Float32 coercion in __init__.
126
+ """
127
+ return Complex64(Complex64._pack_ssa(re.ir_value(), im.ir_value()))
128
+
129
+ # Internal alias used by arithmetic methods.
130
+ _from_re_im = from_re_im
131
+
132
+ # ---- accessors ------------------------------------------------------
133
+
134
+ def real(self) -> Float32:
135
+ re, _ = self._unpack()
136
+ return re
137
+
138
+ def imag(self) -> Float32:
139
+ _, im = self._unpack()
140
+ return im
141
+
142
+ def conj(self) -> "Complex64":
143
+ re, im = self._unpack()
144
+ return Complex64._from_re_im(re, -im)
145
+
146
+ # ---- arithmetic -----------------------------------------------------
147
+
148
+ def __add__(self, other, *, loc=None, ip=None):
149
+ a_re, a_im = self._unpack()
150
+ b_re, b_im = _other_lanes(other)
151
+ return Complex64._from_re_im(a_re + b_re, a_im + b_im)
152
+
153
+ def __radd__(self, other, *, loc=None, ip=None):
154
+ return self.__add__(other, loc=loc, ip=ip)
155
+
156
+ def __sub__(self, other, *, loc=None, ip=None):
157
+ a_re, a_im = self._unpack()
158
+ b_re, b_im = _other_lanes(other)
159
+ return Complex64._from_re_im(a_re - b_re, a_im - b_im)
160
+
161
+ def __rsub__(self, other, *, loc=None, ip=None):
162
+ a_re, a_im = self._unpack()
163
+ b_re, b_im = _other_lanes(other)
164
+ return Complex64._from_re_im(b_re - a_re, b_im - a_im)
165
+
166
+ def __mul__(self, other, *, loc=None, ip=None):
167
+ a_re, a_im = self._unpack()
168
+ if isinstance(other, Complex64):
169
+ b_re, b_im = other._unpack()
170
+ return Complex64._from_re_im(a_re * b_re - a_im * b_im, a_re * b_im + a_im * b_re)
171
+ # Real scalar: (re, im) * s = (re*s, im*s)
172
+ s = Float32(other)
173
+ return Complex64._from_re_im(a_re * s, a_im * s)
174
+
175
+ def __rmul__(self, other, *, loc=None, ip=None):
176
+ return self.__mul__(other, loc=loc, ip=ip)
177
+
178
+ def __neg__(self, *, loc=None, ip=None):
179
+ re, im = self._unpack()
180
+ return Complex64._from_re_im(-re, -im)
181
+
182
+ # ---- runtime arg passing -------------------------------------------
183
+
184
+ def __c_pointers__(self):
185
+ # Scalar Complex64 args travel as 8 bytes (the packed-as-f64 value).
186
+ if not isinstance(self.value, float):
187
+ raise ValueError(
188
+ "Complex64 with a dynamic SSA value cannot be passed as a "
189
+ "kernel argument; only static values are supported"
190
+ )
191
+ return [ctypes.cast(ctypes.pointer(ctypes.c_double(self.value)), ctypes.c_void_p)]
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Internal helpers
196
+ # ---------------------------------------------------------------------------
197
+
198
+
199
+ def _pack_python_complex(c: complex) -> float:
200
+ """Compute the f64 representation of a complex bit-packed (re, im)."""
201
+ re_b = int(np.float32(c.real).view(np.uint32))
202
+ im_b = int(np.float32(c.imag).view(np.uint32))
203
+ return float(np.uint64((im_b << 32) | re_b).view(np.float64))
204
+
205
+
206
+ def _other_lanes(other):
207
+ """Unpack the RHS of a binary op into (re_f32, im_f32) Float32 lanes."""
208
+ if isinstance(other, Complex64):
209
+ return other._unpack()
210
+ return Float32(other), Float32(0.0)
211
+
212
+
213
+ def _retag_as_complex64(t):
214
+ """Restore `t.element_type is Complex64` after a code path that derived it
215
+ from MLIR (where complex64 collapses to Float64 / Int64 because
216
+ `Numeric.from_mlir_type` is a many-to-one lookup)."""
217
+ t._dtype = Complex64
218
+ return t
219
+
220
+
221
+ # ---------------------------------------------------------------------------
222
+ # Public helpers
223
+ # ---------------------------------------------------------------------------
224
+
225
+
226
+ def allocate_smem_complex(
227
+ allocator,
228
+ layout_or_shape,
229
+ byte_alignment: int = 16,
230
+ swizzle=None,
231
+ ):
232
+ """Allocate a `Complex64` smem tensor.
233
+
234
+ Wraps `cutlass.utils.SmemAllocator.allocate_tensor(Complex64, ...)` and
235
+ re-tags the result so `tensor.element_type is Complex64`. Without the
236
+ re-tag, the JIT-side tensor's element_type is `Float64` (derived from the
237
+ f64 memref) and writes go through `Complex64.to(Float64)` and corrupt the
238
+ packed bits.
239
+ """
240
+ t = allocator.allocate_tensor(
241
+ Complex64, layout_or_shape, byte_alignment=byte_alignment, swizzle=swizzle
242
+ )
243
+ return _retag_as_complex64(t)
244
+
245
+
246
+ def recast_to_complex64(src: cute.Tensor) -> cute.Tensor:
247
+ """Recast any tensor (e.g. Float32, Int64) to a `Complex64` tensor.
248
+
249
+ Wraps `cute.recast_tensor(src, Complex64)` and re-tags the result. Same
250
+ dtype-loss bug as `allocate_smem_complex` -- the underlying recast goes
251
+ through `make_tensor`, which derives element_type from the MLIR memref
252
+ (here f64) and gets back Float64.
253
+ """
254
+ return _retag_as_complex64(cute.recast_tensor(src, Complex64))
255
+
256
+
257
+ def complex_storage(t: torch.Tensor) -> torch.Tensor:
258
+ """View a `torch.complex64` tensor as `torch.float64` with the same memory.
259
+
260
+ Compiled kernels declared with `Complex64` element type have an f64 ABI;
261
+ use this at the boundary to satisfy tvm-ffi's dtype check without copying.
262
+ """
263
+ if t.dtype == torch.float64:
264
+ return t
265
+ if t.dtype != torch.complex64:
266
+ raise TypeError(
267
+ f"complex_storage expects torch.complex64 (or torch.float64 for "
268
+ f"already-converted storage), got {t.dtype}"
269
+ )
270
+ return t.view(torch.float64)
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # tvm-ffi registration
275
+ # ---------------------------------------------------------------------------
276
+
277
+
278
+ def _register_with_tvm_ffi() -> None:
279
+ """Teach tvm-ffi that Complex64 has an f64 ABI.
280
+
281
+ Both `NumericToTVMFFIDtype` (the type->dtype-string lookup) and
282
+ `AcceptableNumericTypesForScalar` (the allowlist for scalar kernel args)
283
+ are plain Python collections, so we extend them at import time.
284
+ """
285
+ from cutlass.cute import _tvm_ffi_args_spec_converter as _cv
286
+
287
+ _cv.NumericToTVMFFIDtype.setdefault(Complex64, "float64")
288
+ if Complex64 not in _cv.AcceptableNumericTypesForScalar:
289
+ _cv.AcceptableNumericTypesForScalar.append(Complex64)
290
+
291
+
292
+ _register_with_tvm_ffi()
build/torch-cuda/quack/copy_utils.py CHANGED
@@ -1,17 +1,19 @@
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
 
@@ -20,6 +22,97 @@ from .utils import make_vector
20
 
21
 
22
  Sm100MmaPeerBitMask = 0xFEFFFFFF
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
 
25
  @dsl_user_op
@@ -36,9 +129,7 @@ def cvt_copy(
36
  ) -> None:
37
  assert isinstance(src.iterator, cute.Pointer) and src.memspace == cute.AddressSpace.rmem
38
  if const_expr(src.element_type != dst.element_type):
39
- src_cvt = cute.make_rmem_tensor_like(src, dst.element_type)
40
- src_cvt.store(src.load().to(dst.element_type))
41
- src = src_cvt
42
  if const_expr(retile):
43
  src = tiled_copy.retile(src)
44
  cute.copy(tiled_copy, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
@@ -75,6 +166,13 @@ def load_s2r(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
75
  return dst
76
 
77
 
 
 
 
 
 
 
 
78
  @dsl_user_op
79
  def load_s2r_retile(
80
  tiled_copy: cute.TiledCopy,
@@ -95,11 +193,22 @@ def load_s2r_retile(
95
 
96
  @dsl_user_op
97
  def load_t2r(
98
- thr_copy: cute.ThrCopy, shape: cute.Shape, src: cute.Tensor, *, loc=None, ip=None
 
 
 
 
 
99
  ) -> cute.Tensor:
100
- cDst = cute.make_identity_tensor(shape)
101
- dst = cute.make_rmem_tensor(thr_copy.partition_D(cDst).shape, src.element_type, loc=loc, ip=ip)
102
- cute.copy(thr_copy, src, dst, loc=loc, ip=ip)
 
 
 
 
 
 
103
  return dst
104
 
105
 
@@ -321,18 +430,14 @@ def as_position_independent_swizzle_tensor(tensor: cute.Tensor) -> cute.Tensor:
321
  return cute.make_tensor(cute.recast_ptr(tensor.iterator, dtype=tensor.element_type), new_layout)
322
 
323
 
324
- def partition_D_position_independent(
325
- thr_copy: cute.core.ThrCopy, tensor: cute.Tensor
326
- ) -> cute.Tensor:
327
  return cute.make_tensor(
328
  swizzle_ptr(thr_copy.partition_D(tensor).iterator),
329
  thr_copy.partition_D(as_position_independent_swizzle_tensor(tensor)).layout,
330
  )
331
 
332
 
333
- def partition_S_position_independent(
334
- thr_copy: cute.core.ThrCopy, tensor: cute.Tensor
335
- ) -> cute.Tensor:
336
  return cute.make_tensor(
337
  swizzle_ptr(thr_copy.partition_S(tensor).iterator),
338
  thr_copy.partition_S(as_position_independent_swizzle_tensor(tensor)).layout,
@@ -373,12 +478,12 @@ def sm90_get_smem_load_op(
373
 
374
 
375
  def get_smem_store_atom(
376
- arch: cutlass.Constexpr[int],
377
  element_type: Type[cute.Numeric],
378
  transpose: bool = False,
379
  major_mode_size: Optional[int] = None,
380
  ) -> cute.CopyAtom:
381
- if const_expr(arch < 90 or element_type.width != 16):
 
382
  return cute.make_copy_atom(
383
  cute.nvgpu.CopyUniversalOp(),
384
  element_type,
@@ -397,12 +502,12 @@ def get_smem_store_atom(
397
 
398
 
399
  def get_smem_load_atom(
400
- arch: cutlass.Constexpr[int],
401
  element_type: Type[cute.Numeric],
402
  transpose: bool = False,
403
  major_mode_size: Optional[int] = None,
404
  ) -> cute.CopyAtom:
405
- if const_expr(arch < 90 or element_type.width != 16):
 
406
  return cute.make_copy_atom(
407
  cute.nvgpu.CopyUniversalOp(),
408
  element_type,
@@ -421,26 +526,35 @@ def get_smem_load_atom(
421
 
422
 
423
  def get_smem_store_C(
424
- tiled_mma: cute.TiledMma,
425
  sC: cute.Tensor,
426
  tidx: Int32,
427
- arch: int,
428
  transpose: bool = False,
429
  position_independent=False,
430
  major_mode_size: Optional[int] = None,
431
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
432
  dtype = sC.element_type
433
- copy_atom = get_smem_store_atom(arch, dtype, transpose, major_mode_size=major_mode_size)
434
- tiled_copy = cute.make_tiled_copy_C(copy_atom, tiled_mma)
 
 
 
 
 
 
 
 
435
  thr_copy = tiled_copy.get_slice(tidx)
436
  if const_expr(not position_independent):
437
  tRS_sC = thr_copy.partition_D(sC)
438
  else:
439
  tRS_sC = partition_D_position_independent(thr_copy, sC)
440
 
441
- def copy_fn(src: cute.Tensor, dst_idx: Optional[Int32] = None, **new_kwargs):
442
- dst_tensor = tRS_sC if const_expr(dst_idx is None) else tRS_sC[None, None, None, dst_idx]
443
  cvt_copy(tiled_copy, src, dst_tensor, retile=True, **new_kwargs)
 
 
444
 
445
  return copy_fn, thr_copy, tRS_sC
446
 
@@ -449,19 +563,18 @@ def get_smem_load_C(
449
  tiled_mma: cute.TiledMma,
450
  sC: cute.Tensor,
451
  tidx: Int32,
452
- arch: int,
453
  transpose: bool = False,
454
  position_independent=False,
455
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
456
  dtype = sC.element_type
457
- copy_atom = get_smem_load_atom(arch, dtype, transpose)
458
  tiled_copy = cute.make_tiled_copy_C(copy_atom, tiled_mma)
459
  thr_copy = tiled_copy.get_slice(tidx)
460
  if const_expr(not position_independent):
461
  tSR_sC = thr_copy.partition_S(sC)
462
  else:
463
  tSR_sC = partition_S_position_independent(thr_copy, sC)
464
- copy_atom_RS = get_smem_store_atom(arch, dtype, transpose)
465
  thr_copy_RS = cute.make_tiled_copy_C(copy_atom_RS, tiled_mma).get_slice(tidx)
466
  tRS_shape = thr_copy_RS.partition_S(cute.make_identity_tensor(sC.shape[:2])).shape
467
 
@@ -475,10 +588,18 @@ def get_smem_load_C(
475
  def epilog_smem_copy_atom(
476
  tiled_mma: cute.TiledMma, epi_tile: cute.Shape, transpose: bool = False
477
  ) -> cute.TiledCopy:
478
- copy_atom_C = cute.make_copy_atom(
479
- warp.StMatrix8x8x16bOp(transpose, num_matrices=4 if epi_tile[1] % 16 == 0 else 2),
480
- cutlass.Float16, # this is just to get the right source layout
481
- )
 
 
 
 
 
 
 
 
482
  tiled_copy_C_atom = cute.make_tiled_copy_C_atom(copy_atom_C, tiled_mma)
483
  return tiled_copy_C_atom
484
 
@@ -488,13 +609,12 @@ def get_smem_store_epi(
488
  epi_tile: cute.Shape,
489
  sC: Optional[cute.Tensor],
490
  tidx: Int32,
491
- arch: int,
492
  transpose: bool = False,
493
  position_independent=False,
494
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor, cute.Tensor]:
495
  dtype = sC.element_type if const_expr(sC is not None) else cutlass.Float16
 
496
  tiled_copy_C_atom = epilog_smem_copy_atom(tiled_mma, epi_tile)
497
- copy_atom = get_smem_store_atom(arch, dtype, transpose)
498
  tiled_copy = cute.make_tiled_copy_S(copy_atom, tiled_copy_C_atom)
499
  thr_copy = tiled_copy.get_slice(tidx)
500
  tRS_sC = None
@@ -515,11 +635,11 @@ def get_smem_store_epi(
515
 
516
 
517
  def get_smem_store_A(
518
- tiled_mma: cute.TiledMma, sA: cute.Tensor, tidx: Int32, arch: int, position_independent=False
519
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
520
  dtype = sA.element_type
521
- transpose = tiled_mma.op.a_major_mode == warpgroup.OperandMajorMode.MN
522
- copy_atom = get_smem_store_atom(arch, dtype, transpose)
523
  tiled_copy = cute.make_tiled_copy_A(copy_atom, tiled_mma)
524
  thr_copy = tiled_copy.get_slice(tidx)
525
  if const_expr(not position_independent):
@@ -537,13 +657,12 @@ def get_smem_load_A(
537
  tiled_mma: cute.TiledMma,
538
  sA: cute.Tensor,
539
  tidx: Int32,
540
- arch: int,
541
  with_dst_tensor: bool = False,
542
  position_independent=False,
543
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
544
  dtype = sA.element_type
545
- transpose = tiled_mma.op.a_major_mode == warpgroup.OperandMajorMode.MN
546
- copy_atom = get_smem_load_atom(arch, dtype, transpose)
547
  tiled_copy = cute.make_tiled_copy_A(copy_atom, tiled_mma)
548
  thr_copy = tiled_copy.get_slice(tidx)
549
  if const_expr(not position_independent):
@@ -563,6 +682,79 @@ def get_smem_load_A(
563
  return copy_fn if not with_dst_tensor else copy_fn_w_dst_tensor, thr_copy, tSR_sA
564
 
565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  @dsl_user_op
567
  def cpasync_reduce_bulk_add_f32(
568
  smem_ptr: cute.Pointer,
@@ -572,18 +764,14 @@ def cpasync_reduce_bulk_add_f32(
572
  loc=None,
573
  ip=None,
574
  ):
575
- smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
576
- # cache_hint = cutlass.Int64(0x14F0000000000000) # EVICT_LAST
577
- llvm.inline_asm(
578
- None,
579
- [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value()],
580
- "cp.reduce.async.bulk.global.shared::cta.bulk_group.add.f32 [$0], [$1], $2;",
581
- "l,r,r",
582
- # [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value(), cache_hint.ir_value()],
583
- # "cp.reduce.async.bulk.global.shared::cta.bulk_group.L2::cache_hint.add.f32 [$0], [$1], $2, $3;",
584
- # "l,r,r,l",
585
- has_side_effects=True,
586
- is_align_stack=False,
587
  )
588
 
589
 
@@ -674,33 +862,34 @@ def tma_gather4_load(
674
  """
675
  if len(row_indices) != 4:
676
  raise ValueError(f"gather4 requires exactly 4 row indices, got {len(row_indices)}")
677
- col_val = Int32(col_idx).ir_value()
678
- row_vals = [Int32(row_idx).ir_value() for row_idx in row_indices]
679
  # Convert pointers to integer addresses
680
- desc_addr = tma_desc_ptr.toint(loc=loc, ip=ip).ir_value()
681
- dst_addr = dst_smem_ptr.toint(loc=loc, ip=ip).ir_value()
682
  mbar_addr = mbarrier_ptr.toint(loc=loc, ip=ip)
683
  if num_cta > 1:
684
  # Executed by both CTAs. Set peer bit to 0 so that the
685
  # transaction bytes will update CTA0's barrier.
686
  mbar_addr = mbar_addr & Sm100MmaPeerBitMask
687
- mbar_addr = mbar_addr.ir_value()
688
  # Handle multicast_mask - may already be ir.Value or Python int
689
  multicast_mask_val = None
690
  if multicast_mask is not None:
691
- multicast_mask_val = Int16(multicast_mask).ir_value()
692
  assert multicast_mask_val is None, "multicast is not supported yet"
693
  # Emit inline PTX for TMA gather4
694
  # PTX: cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes
695
  # [dstMem], [tensorMap, {col, row0, row1, row2, row3}], [smem_bar];
696
  ptx = (
697
- f"cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes.cta_group::{num_cta} "
698
- "[$0], [$1, {$2, $3, $4, $5, $6}], [$7];"
 
699
  )
700
 
701
- llvm.inline_asm(
702
- None,
703
- [
704
  dst_addr,
705
  desc_addr,
706
  col_val,
@@ -710,10 +899,6 @@ def tma_gather4_load(
710
  row_vals[3],
711
  mbar_addr,
712
  ],
713
- ptx,
714
- "r,l,r,r,r,r,r,r", # constraints: register, long, 6x register
715
- has_side_effects=True,
716
- is_align_stack=False,
717
  loc=loc,
718
  ip=ip,
719
  )
@@ -723,34 +908,137 @@ def cpasync_bulk_get_copy_fn(
723
  src_tensor: cute.Tensor,
724
  dst_tensor: cute.Tensor,
725
  single_stage: bool = False,
 
726
  **kwargs,
727
  ) -> Callable:
 
 
 
 
 
 
 
 
 
 
728
  group_rank_src = const_expr(cute.rank(src_tensor) - (1 if not single_stage else 0))
729
  group_rank_dst = const_expr(cute.rank(dst_tensor) - (1 if not single_stage else 0))
730
  # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
731
  src = cute.group_modes(src_tensor, 0, group_rank_src)
732
  dst = cute.group_modes(dst_tensor, 0, group_rank_dst)
733
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
734
  def copy_bulk(src_idx, dst_idx, tma_bar_ptr: cute.Pointer, **new_kwargs):
 
735
  atom = cute.make_copy_atom(cpasync.CopyBulkG2SOp(), src.element_type)
736
- with cute.arch.elect_one():
737
- cute.copy(
738
- atom,
739
- src[None, src_idx],
740
- dst[None, dst_idx],
741
- mbar_ptr=tma_bar_ptr,
742
- **new_kwargs,
743
- **kwargs,
744
- )
745
 
746
  def copy_bulk_single_stage(tma_bar_ptr: cute.Pointer, **new_kwargs):
 
747
  atom = cute.make_copy_atom(cpasync.CopyBulkG2SOp(), src.element_type)
748
- with cute.arch.elect_one():
749
- cute.copy(atom, src, dst, mbar_ptr=tma_bar_ptr, **new_kwargs, **kwargs)
750
 
751
  return copy_bulk if const_expr(not single_stage) else copy_bulk_single_stage
752
 
753
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
754
  @dsl_user_op
755
  def tma_get_copy_fn(
756
  atom: cute.CopyAtom,
@@ -800,6 +1088,236 @@ def tma_get_copy_fn(
800
  return (copy_tma if const_expr(not single_stage) else copy_tma_single_stage), s, g
801
 
802
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
  def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsync):
804
  def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
805
  copy(
@@ -812,6 +1330,18 @@ def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsyn
812
  return copy_fn
813
 
814
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  @cute.jit
816
  def gather_m_get_copy_fn(
817
  thr_copy_A: cute.ThrCopy,
@@ -855,7 +1385,7 @@ def gather_m_get_copy_fn(
855
 
856
  mA_k = cute.logical_divide(mA, (None, tile_K))
857
 
858
- def copy_fn(src_idx, dst_idx, pred: bool = False):
859
  tApA_k = None
860
  if const_expr(pred):
861
  tApA_k = cute.make_rmem_tensor(cols_per_thread, Boolean)
@@ -964,9 +1494,7 @@ def gather_k_get_copy_fn(
964
  for k in cutlass.range(cols_per_thread):
965
  col_idx = tAcA[0, 0, k][1]
966
  k_idx[k] = sAIdx_cur[col_idx]
967
- cute.arch.sync_warp()
968
- with cute.arch.elect_one():
969
- a_prefetch_pipeline.consumer_release(a_prefetch_consumer_state)
970
  return k_idx, tApA_k
971
 
972
  def copy_fn(
@@ -1078,9 +1606,7 @@ def gather_k_get_tma_copy_fn(
1078
  ) -> cute.Tensor:
1079
  a_prefetch_pipeline.consumer_wait(a_prefetch_consumer_state)
1080
  tSR_rAIdx = load_s2r(tSR_sAIdx[None, None, dst_idx])
1081
- cute.arch.sync_warp()
1082
- with cute.arch.elect_one():
1083
- a_prefetch_pipeline.consumer_release(a_prefetch_consumer_state)
1084
  return tSR_rAIdx
1085
 
1086
  def copy_fn(src_idx, dst_idx, tSR_rAIdx, tma_bar_ptr: cute.Pointer):
 
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, block_copy
16
  import cutlass.pipeline
 
17
  from cutlass._mlir import ir
18
  from cutlass._mlir.dialects import cute_nvgpu as _cute_nvgpu_ir
19
 
 
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
 
129
  ) -> None:
130
  assert isinstance(src.iterator, cute.Pointer) and src.memspace == cute.AddressSpace.rmem
131
  if const_expr(src.element_type != dst.element_type):
132
+ src = src.to(dst.element_type, loc=loc, ip=ip)
 
 
133
  if const_expr(retile):
134
  src = tiled_copy.retile(src)
135
  cute.copy(tiled_copy, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
 
166
  return dst
167
 
168
 
169
+ @dsl_user_op
170
+ def contiguous(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
171
+ dst = cute.make_rmem_tensor(src.shape, src.element_type, loc=loc, ip=ip)
172
+ cute.autovec_copy(src, dst, loc=loc, ip=ip)
173
+ return dst
174
+
175
+
176
  @dsl_user_op
177
  def load_s2r_retile(
178
  tiled_copy: cute.TiledCopy,
 
193
 
194
  @dsl_user_op
195
  def load_t2r(
196
+ tiled_copy: cute.TiledCopy,
197
+ src: cute.Tensor,
198
+ *,
199
+ fence: bool = False,
200
+ loc=None,
201
+ ip=None,
202
  ) -> cute.Tensor:
203
+ """Load one tmem tile partition into rmem, deriving the rmem shape from `src`.
204
+
205
+ `src` should already be indexed to the tile being copied, with any
206
+ stage/subtile modes removed.
207
+ """
208
+ dst = tmem_reg_frag(tiled_copy, src, loc=loc, ip=ip)
209
+ cute.copy(tiled_copy, src, dst, loc=loc, ip=ip)
210
+ if const_expr(fence):
211
+ cute.arch.fence_view_async_tmem_load()
212
  return dst
213
 
214
 
 
430
  return cute.make_tensor(cute.recast_ptr(tensor.iterator, dtype=tensor.element_type), new_layout)
431
 
432
 
433
+ def partition_D_position_independent(thr_copy: cute.ThrCopy, tensor: cute.Tensor) -> cute.Tensor:
 
 
434
  return cute.make_tensor(
435
  swizzle_ptr(thr_copy.partition_D(tensor).iterator),
436
  thr_copy.partition_D(as_position_independent_swizzle_tensor(tensor)).layout,
437
  )
438
 
439
 
440
+ def partition_S_position_independent(thr_copy: cute.ThrCopy, tensor: cute.Tensor) -> cute.Tensor:
 
 
441
  return cute.make_tensor(
442
  swizzle_ptr(thr_copy.partition_S(tensor).iterator),
443
  thr_copy.partition_S(as_position_independent_swizzle_tensor(tensor)).layout,
 
478
 
479
 
480
  def get_smem_store_atom(
 
481
  element_type: Type[cute.Numeric],
482
  transpose: bool = False,
483
  major_mode_size: Optional[int] = None,
484
  ) -> cute.CopyAtom:
485
+ arch = cutlass.base_dsl.BaseDSL._get_dsl().get_arch_enum()
486
+ if const_expr(arch < Arch.sm_90 or element_type.width != 16):
487
  return cute.make_copy_atom(
488
  cute.nvgpu.CopyUniversalOp(),
489
  element_type,
 
502
 
503
 
504
  def get_smem_load_atom(
 
505
  element_type: Type[cute.Numeric],
506
  transpose: bool = False,
507
  major_mode_size: Optional[int] = None,
508
  ) -> cute.CopyAtom:
509
+ arch = cutlass.base_dsl.BaseDSL._get_dsl().get_arch_enum()
510
+ if const_expr(arch < Arch.sm_90 or element_type.width != 16):
511
  return cute.make_copy_atom(
512
  cute.nvgpu.CopyUniversalOp(),
513
  element_type,
 
526
 
527
 
528
  def get_smem_store_C(
529
+ tiled_mma: cute.TiledMma | cute.TiledCopy,
530
  sC: cute.Tensor,
531
  tidx: Int32,
 
532
  transpose: bool = False,
533
  position_independent=False,
534
  major_mode_size: Optional[int] = None,
535
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
536
  dtype = sC.element_type
537
+ if const_expr(isinstance(tiled_mma, cute.TiledCopy)):
538
+ tiled_copy_t2r = tiled_mma
539
+ layout = LayoutEnum.COL_MAJOR if const_expr(transpose) else LayoutEnum.ROW_MAJOR
540
+ copy_atom = sm100_utils.get_smem_store_op(
541
+ layout, dtype, tiled_copy_t2r.value_type, tiled_copy_t2r
542
+ )
543
+ tiled_copy = cute.make_tiled_copy_D(copy_atom, tiled_copy_t2r)
544
+ else:
545
+ copy_atom = get_smem_store_atom(dtype, transpose, major_mode_size=major_mode_size)
546
+ tiled_copy = cute.make_tiled_copy_C(copy_atom, tiled_mma)
547
  thr_copy = tiled_copy.get_slice(tidx)
548
  if const_expr(not position_independent):
549
  tRS_sC = thr_copy.partition_D(sC)
550
  else:
551
  tRS_sC = partition_D_position_independent(thr_copy, sC)
552
 
553
+ def copy_fn(src: cute.Tensor, dst_idx: Optional[Int32] = None, fence=False, **new_kwargs):
554
+ dst_tensor = tRS_sC if const_expr(dst_idx is None) else tRS_sC[..., dst_idx]
555
  cvt_copy(tiled_copy, src, dst_tensor, retile=True, **new_kwargs)
556
+ if const_expr(fence):
557
+ cute.arch.fence_view_async_shared()
558
 
559
  return copy_fn, thr_copy, tRS_sC
560
 
 
563
  tiled_mma: cute.TiledMma,
564
  sC: cute.Tensor,
565
  tidx: Int32,
 
566
  transpose: bool = False,
567
  position_independent=False,
568
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
569
  dtype = sC.element_type
570
+ copy_atom = get_smem_load_atom(dtype, transpose)
571
  tiled_copy = cute.make_tiled_copy_C(copy_atom, tiled_mma)
572
  thr_copy = tiled_copy.get_slice(tidx)
573
  if const_expr(not position_independent):
574
  tSR_sC = thr_copy.partition_S(sC)
575
  else:
576
  tSR_sC = partition_S_position_independent(thr_copy, sC)
577
+ copy_atom_RS = get_smem_store_atom(dtype, transpose)
578
  thr_copy_RS = cute.make_tiled_copy_C(copy_atom_RS, tiled_mma).get_slice(tidx)
579
  tRS_shape = thr_copy_RS.partition_S(cute.make_identity_tensor(sC.shape[:2])).shape
580
 
 
588
  def epilog_smem_copy_atom(
589
  tiled_mma: cute.TiledMma, epi_tile: cute.Shape, transpose: bool = False
590
  ) -> cute.TiledCopy:
591
+ arch = cutlass.base_dsl.BaseDSL._get_dsl().get_arch_enum()
592
+ if const_expr(arch < Arch.sm_90):
593
+ copy_atom_C = cute.make_copy_atom(
594
+ cute.nvgpu.CopyUniversalOp(),
595
+ cutlass.Float16, # this is just to get the right source layout
596
+ num_bits_per_copy=(2 if not transpose else 1) * cutlass.Float16.width,
597
+ )
598
+ else:
599
+ copy_atom_C = cute.make_copy_atom(
600
+ warp.StMatrix8x8x16bOp(transpose, num_matrices=4 if epi_tile[1] % 16 == 0 else 2),
601
+ cutlass.Float16, # this is just to get the right source layout
602
+ )
603
  tiled_copy_C_atom = cute.make_tiled_copy_C_atom(copy_atom_C, tiled_mma)
604
  return tiled_copy_C_atom
605
 
 
609
  epi_tile: cute.Shape,
610
  sC: Optional[cute.Tensor],
611
  tidx: Int32,
 
612
  transpose: bool = False,
613
  position_independent=False,
614
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor, cute.Tensor]:
615
  dtype = sC.element_type if const_expr(sC is not None) else cutlass.Float16
616
+ copy_atom = get_smem_store_atom(dtype, transpose)
617
  tiled_copy_C_atom = epilog_smem_copy_atom(tiled_mma, epi_tile)
 
618
  tiled_copy = cute.make_tiled_copy_S(copy_atom, tiled_copy_C_atom)
619
  thr_copy = tiled_copy.get_slice(tidx)
620
  tRS_sC = None
 
635
 
636
 
637
  def get_smem_store_A(
638
+ tiled_mma: cute.TiledMma, sA: cute.Tensor, tidx: Int32, position_independent=False
639
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
640
  dtype = sA.element_type
641
+ transpose = tiled_mma.op.a_major_mode == cute.nvgpu.OperandMajorMode.MN
642
+ copy_atom = get_smem_store_atom(dtype, transpose)
643
  tiled_copy = cute.make_tiled_copy_A(copy_atom, tiled_mma)
644
  thr_copy = tiled_copy.get_slice(tidx)
645
  if const_expr(not position_independent):
 
657
  tiled_mma: cute.TiledMma,
658
  sA: cute.Tensor,
659
  tidx: Int32,
 
660
  with_dst_tensor: bool = False,
661
  position_independent=False,
662
  ) -> Tuple[Callable, cute.TiledCopy, cute.Tensor]:
663
  dtype = sA.element_type
664
+ transpose = tiled_mma.op.a_major_mode == cute.nvgpu.OperandMajorMode.MN
665
+ copy_atom = get_smem_load_atom(dtype, transpose)
666
  tiled_copy = cute.make_tiled_copy_A(copy_atom, tiled_mma)
667
  thr_copy = tiled_copy.get_slice(tidx)
668
  if const_expr(not position_independent):
 
682
  return copy_fn if not with_dst_tensor else copy_fn_w_dst_tensor, thr_copy, tSR_sA
683
 
684
 
685
+ def _cpasync_reduction_kind_name(reduction_kind: Any) -> str:
686
+ name = (
687
+ reduction_kind.lower() if isinstance(reduction_kind, str) else reduction_kind.name.lower()
688
+ )
689
+ assert name in {"add", "min", "max", "inc", "dec", "and", "or", "xor"}, (
690
+ f"Unsupported cp.reduce.async.bulk reduction kind: {reduction_kind}"
691
+ )
692
+ return name
693
+
694
+
695
+ def _cpasync_bulk_reduce_suffix(
696
+ reduction_kind: Any,
697
+ dtype: Type[cutlass.Numeric],
698
+ ) -> str:
699
+ op = _cpasync_reduction_kind_name(reduction_kind)
700
+ if dtype is cutlass.Float16:
701
+ assert op in {"add", "min", "max"}, f"{op} is not supported for f16 bulk reduce"
702
+ return f"{op}.noftz.f16" if op == "add" else f"{op}.f16"
703
+ if dtype is cutlass.BFloat16:
704
+ assert op in {"add", "min", "max"}, f"{op} is not supported for bf16 bulk reduce"
705
+ return f"{op}.noftz.bf16" if op == "add" else f"{op}.bf16"
706
+ if dtype is cutlass.Float32:
707
+ assert op == "add", f"{op} is not supported for f32 bulk reduce"
708
+ return "add.f32"
709
+ if dtype is cutlass.Float64:
710
+ assert op == "add", f"{op} is not supported for f64 bulk reduce"
711
+ return "add.f64"
712
+
713
+ signed = getattr(dtype, "signed", None)
714
+ width = getattr(dtype, "width", None)
715
+ if signed is not None:
716
+ assert width in (32, 64), f"Unsupported integer bulk-reduce width: {width}"
717
+ if op in {"and", "or", "xor"}:
718
+ return f"{op}.b{width}"
719
+ if op in {"min", "max", "add"}:
720
+ return f"{op}.{'s' if signed else 'u'}{width}"
721
+ assert op in {"inc", "dec"} and dtype is cutlass.Uint32, (
722
+ f"{op} bulk reduce is only supported for u32"
723
+ )
724
+ return f"{op}.u32"
725
+
726
+ raise TypeError(f"Unsupported cp.reduce.async.bulk dtype: {dtype}")
727
+
728
+
729
+ @dsl_user_op
730
+ def cpasync_bulk_s2g(
731
+ smem_ptr: cute.Pointer,
732
+ gmem_ptr: cute.Pointer,
733
+ store_bytes: int | Int32,
734
+ *,
735
+ reduction_kind: Optional[Any] = None,
736
+ dtype: Optional[Type[cutlass.Numeric]] = None,
737
+ loc=None,
738
+ ip=None,
739
+ ):
740
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip)
741
+ if reduction_kind is None:
742
+ ptx = "cp.async.bulk.global.shared::cta.bulk_group [{$r0}], [{$r1}], {$r2};"
743
+ else:
744
+ assert dtype is not None, "dtype is required for cp.reduce.async.bulk"
745
+ ptx = (
746
+ "cp.reduce.async.bulk.global.shared::cta.bulk_group."
747
+ f"{_cpasync_bulk_reduce_suffix(reduction_kind, dtype)} "
748
+ "[{$r0}], [{$r1}], {$r2};"
749
+ )
750
+ cute.arch.inline_ptx(
751
+ ptx,
752
+ read_only_args=[gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes)],
753
+ loc=loc,
754
+ ip=ip,
755
+ )
756
+
757
+
758
  @dsl_user_op
759
  def cpasync_reduce_bulk_add_f32(
760
  smem_ptr: cute.Pointer,
 
764
  loc=None,
765
  ip=None,
766
  ):
767
+ cpasync_bulk_s2g(
768
+ smem_ptr,
769
+ gmem_ptr,
770
+ store_bytes,
771
+ reduction_kind=cpasync.ReductionOp.ADD,
772
+ dtype=cutlass.Float32,
773
+ loc=loc,
774
+ ip=ip,
 
 
 
 
775
  )
776
 
777
 
 
862
  """
863
  if len(row_indices) != 4:
864
  raise ValueError(f"gather4 requires exactly 4 row indices, got {len(row_indices)}")
865
+ col_val = Int32(col_idx)
866
+ row_vals = [Int32(row_idx) for row_idx in row_indices]
867
  # Convert pointers to integer addresses
868
+ desc_addr = tma_desc_ptr.toint(loc=loc, ip=ip)
869
+ dst_addr = dst_smem_ptr.toint(loc=loc, ip=ip)
870
  mbar_addr = mbarrier_ptr.toint(loc=loc, ip=ip)
871
  if num_cta > 1:
872
  # Executed by both CTAs. Set peer bit to 0 so that the
873
  # transaction bytes will update CTA0's barrier.
874
  mbar_addr = mbar_addr & Sm100MmaPeerBitMask
875
+ mbar_addr = Int32(mbar_addr)
876
  # Handle multicast_mask - may already be ir.Value or Python int
877
  multicast_mask_val = None
878
  if multicast_mask is not None:
879
+ multicast_mask_val = Int16(multicast_mask)
880
  assert multicast_mask_val is None, "multicast is not supported yet"
881
  # Emit inline PTX for TMA gather4
882
  # PTX: cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes
883
  # [dstMem], [tensorMap, {col, row0, row1, row2, row3}], [smem_bar];
884
  ptx = (
885
+ "cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes."
886
+ f"cta_group::{num_cta} "
887
+ "[{$r0}], [{$r1}, {{$r2}, {$r3}, {$r4}, {$r5}, {$r6}}], [{$r7}];"
888
  )
889
 
890
+ cute.arch.inline_ptx(
891
+ ptx,
892
+ read_only_args=[
893
  dst_addr,
894
  desc_addr,
895
  col_val,
 
899
  row_vals[3],
900
  mbar_addr,
901
  ],
 
 
 
 
902
  loc=loc,
903
  ip=ip,
904
  )
 
908
  src_tensor: cute.Tensor,
909
  dst_tensor: cute.Tensor,
910
  single_stage: bool = False,
911
+ reduction_kind: Optional[cute.nvgpu.cpasync.ReductionKind] = None,
912
  **kwargs,
913
  ) -> Callable:
914
+ src_is_smem = const_expr(
915
+ isinstance(src_tensor.iterator, cute.Pointer)
916
+ and src_tensor.memspace == cute.AddressSpace.smem
917
+ )
918
+ dst_is_smem = const_expr(
919
+ isinstance(dst_tensor.iterator, cute.Pointer)
920
+ and dst_tensor.memspace == cute.AddressSpace.smem
921
+ )
922
+ if const_expr(reduction_kind is not None):
923
+ assert src_is_smem and not dst_is_smem, "cp.reduce.async.bulk only supports SMEM -> GMEM"
924
  group_rank_src = const_expr(cute.rank(src_tensor) - (1 if not single_stage else 0))
925
  group_rank_dst = const_expr(cute.rank(dst_tensor) - (1 if not single_stage else 0))
926
  # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
927
  src = cute.group_modes(src_tensor, 0, group_rank_src)
928
  dst = cute.group_modes(dst_tensor, 0, group_rank_dst)
929
 
930
+ if const_expr(src_is_smem and not dst_is_smem):
931
+
932
+ def copy_bulk_s2g(src_idx, dst_idx, **new_kwargs):
933
+ store_bytes = const_expr(cute.size(src.shape[:-1]) * src.element_type.width // 8)
934
+ with cute.arch.elect_one():
935
+ cpasync_bulk_s2g(
936
+ src[None, src_idx].iterator,
937
+ dst[None, dst_idx].iterator,
938
+ store_bytes,
939
+ reduction_kind=reduction_kind,
940
+ dtype=src.element_type,
941
+ **new_kwargs,
942
+ **kwargs,
943
+ )
944
+
945
+ def copy_bulk_s2g_single_stage(**new_kwargs):
946
+ store_bytes = const_expr(cute.size(src.shape) * src.element_type.width // 8)
947
+ with cute.arch.elect_one():
948
+ cpasync_bulk_s2g(
949
+ src.iterator,
950
+ dst.iterator,
951
+ store_bytes,
952
+ reduction_kind=reduction_kind,
953
+ dtype=src.element_type,
954
+ **new_kwargs,
955
+ **kwargs,
956
+ )
957
+
958
+ return copy_bulk_s2g if const_expr(not single_stage) else copy_bulk_s2g_single_stage
959
+
960
  def copy_bulk(src_idx, dst_idx, tma_bar_ptr: cute.Pointer, **new_kwargs):
961
+ assert dst_is_smem and not src_is_smem, "cp.async.bulk G2S expects GMEM -> SMEM"
962
  atom = cute.make_copy_atom(cpasync.CopyBulkG2SOp(), src.element_type)
963
+ cute.copy(
964
+ atom,
965
+ src[None, src_idx],
966
+ dst[None, dst_idx],
967
+ mbar_ptr=tma_bar_ptr,
968
+ **new_kwargs,
969
+ **kwargs,
970
+ )
 
971
 
972
  def copy_bulk_single_stage(tma_bar_ptr: cute.Pointer, **new_kwargs):
973
+ assert dst_is_smem and not src_is_smem, "cp.async.bulk G2S expects GMEM -> SMEM"
974
  atom = cute.make_copy_atom(cpasync.CopyBulkG2SOp(), src.element_type)
975
+ cute.copy(atom, src, dst, mbar_ptr=tma_bar_ptr, **new_kwargs, **kwargs)
 
976
 
977
  return copy_bulk if const_expr(not single_stage) else copy_bulk_single_stage
978
 
979
 
980
+ def cpasync_bulk_get_store_or_add_fn(
981
+ src_tensor: cute.Tensor,
982
+ dst_tensor: cute.Tensor,
983
+ store_first_contribution: bool,
984
+ single_stage: bool = False,
985
+ **kwargs,
986
+ ) -> Callable:
987
+ assert not single_stage, "store-or-add helper only supports staged SMEM -> GMEM tensors"
988
+ src_is_smem = const_expr(
989
+ isinstance(src_tensor.iterator, cute.Pointer)
990
+ and src_tensor.memspace == cute.AddressSpace.smem
991
+ )
992
+ dst_is_smem = const_expr(
993
+ isinstance(dst_tensor.iterator, cute.Pointer)
994
+ and dst_tensor.memspace == cute.AddressSpace.smem
995
+ )
996
+ assert src_is_smem and not dst_is_smem, "store-or-add helper only supports SMEM -> GMEM"
997
+ group_rank_src = const_expr(cute.rank(src_tensor))
998
+ group_rank_dst = const_expr(cute.rank(dst_tensor))
999
+ src = cute.group_modes(src_tensor, 0, group_rank_src - 1)
1000
+ dst = cute.group_modes(dst_tensor, 0, group_rank_dst - 1)
1001
+
1002
+ @cute.jit
1003
+ def copy_bulk_s2g_store_or_add(src_idx, dst_idx, idx, **new_kwargs):
1004
+ store_bytes = const_expr(cute.size(src.shape[:-1]) * src.element_type.width // 8)
1005
+ src_ptr = src[None, src_idx].iterator
1006
+ dst_ptr = dst[None, dst_idx].iterator
1007
+ with cute.arch.elect_one():
1008
+ if const_expr(store_first_contribution):
1009
+ if idx == 0:
1010
+ cpasync_bulk_s2g(
1011
+ src_ptr,
1012
+ dst_ptr,
1013
+ store_bytes,
1014
+ reduction_kind=None,
1015
+ **new_kwargs,
1016
+ **kwargs,
1017
+ )
1018
+ else:
1019
+ cpasync_bulk_s2g(
1020
+ src_ptr,
1021
+ dst_ptr,
1022
+ store_bytes,
1023
+ reduction_kind=cpasync.ReductionOp.ADD,
1024
+ dtype=src.element_type,
1025
+ **new_kwargs,
1026
+ **kwargs,
1027
+ )
1028
+ else:
1029
+ cpasync_bulk_s2g(
1030
+ src_ptr,
1031
+ dst_ptr,
1032
+ store_bytes,
1033
+ reduction_kind=cpasync.ReductionOp.ADD,
1034
+ dtype=src.element_type,
1035
+ **new_kwargs,
1036
+ **kwargs,
1037
+ )
1038
+
1039
+ return copy_bulk_s2g_store_or_add
1040
+
1041
+
1042
  @dsl_user_op
1043
  def tma_get_copy_fn(
1044
  atom: cute.CopyAtom,
 
1088
  return (copy_tma if const_expr(not single_stage) else copy_tma_single_stage), s, g
1089
 
1090
 
1091
+ @dsl_user_op
1092
+ def tma_get_block_copy_fn(
1093
+ atom: cute.CopyAtom,
1094
+ src_tensor: cute.Tensor,
1095
+ dst_tensor: cute.Tensor,
1096
+ tma_multicast: Optional[dict] = None,
1097
+ single_stage: bool = False,
1098
+ *,
1099
+ loc=None,
1100
+ ip=None,
1101
+ **kwargs,
1102
+ ) -> Callable:
1103
+ src_is_smem = const_expr(
1104
+ isinstance(src_tensor.iterator, cute.Pointer)
1105
+ and src_tensor.memspace == cute.AddressSpace.smem
1106
+ )
1107
+ if const_expr(tma_multicast is not None and "use_2cta_mma_inst" not in tma_multicast):
1108
+ op = atom.op if const_expr(hasattr(atom, "op")) else atom
1109
+ tma_multicast = {
1110
+ **tma_multicast,
1111
+ "use_2cta_mma_inst": getattr(op, "cta_group", None) == tcgen05.CtaGroup.TWO,
1112
+ }
1113
+ smem_tensor, gmem_tensor = (src_tensor, dst_tensor) if src_is_smem else (dst_tensor, src_tensor)
1114
+ group_rank_smem = const_expr(cute.rank(smem_tensor) - (1 if not single_stage else 0))
1115
+ group_rank_gmem = const_expr(cute.rank(gmem_tensor) - (1 if not single_stage else 0))
1116
+ s = cute.group_modes(smem_tensor, 0, group_rank_smem)
1117
+ g = cute.group_modes(gmem_tensor, 0, group_rank_gmem)
1118
+ src, dst = (s, g) if src_is_smem else (g, s)
1119
+
1120
+ @dsl_user_op
1121
+ def copy_tma(src_idx, dst_idx, *, loc=None, ip=None, **new_kwargs):
1122
+ src_cur = src[None, src_idx]
1123
+ dst_cur = dst[None, dst_idx]
1124
+ if const_expr(tma_multicast is None):
1125
+ block_copy(atom, src_cur, dst_cur, **new_kwargs, **kwargs, loc=loc, ip=ip)
1126
+ else:
1127
+ block_copy(
1128
+ atom,
1129
+ src_cur,
1130
+ dst_cur,
1131
+ tma_multicast=tma_multicast,
1132
+ **new_kwargs,
1133
+ **kwargs,
1134
+ loc=loc,
1135
+ ip=ip,
1136
+ )
1137
+
1138
+ @dsl_user_op
1139
+ def copy_tma_single_stage(*, loc=None, ip=None, **new_kwargs):
1140
+ if const_expr(tma_multicast is None):
1141
+ block_copy(atom, src, dst, **new_kwargs, **kwargs, loc=loc, ip=ip)
1142
+ else:
1143
+ block_copy(
1144
+ atom,
1145
+ src,
1146
+ dst,
1147
+ tma_multicast=tma_multicast,
1148
+ **new_kwargs,
1149
+ **kwargs,
1150
+ loc=loc,
1151
+ ip=ip,
1152
+ )
1153
+
1154
+ return copy_tma if const_expr(not single_stage) else copy_tma_single_stage
1155
+
1156
+
1157
+ def s2t_get_copy_fn(
1158
+ src_tensor: cute.Tensor,
1159
+ dst_tensor: cute.Tensor,
1160
+ cta_group: tcgen05.CtaGroup,
1161
+ ) -> Callable:
1162
+ """
1163
+ Make tiledCopy for smem to tmem load, then return a copy function over stages.
1164
+
1165
+ :param src_tensor: The source tensor in smem
1166
+ :param dst_tensor: The destination tensor in tmem
1167
+ """
1168
+ assert src_tensor.element_type == dst_tensor.element_type
1169
+ # (MMA, MMA_MN, MMA_K, STAGE)
1170
+ src_compact = cute.filter_zeros(src_tensor)
1171
+ # (MMA, MMA_MN, MMA_K)
1172
+ dst_compact = cute.filter_zeros(dst_tensor)
1173
+ # Make S2T CopyAtom and tiledCopy.
1174
+ copy_atom = cute.make_copy_atom(tcgen05.Cp4x32x128bOp(cta_group), dst_tensor.element_type)
1175
+ tiled_copy = tcgen05.make_s2t_copy(copy_atom, dst_compact)
1176
+ thr_copy = tiled_copy.get_slice(0)
1177
+ # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
1178
+ src_partition = tcgen05.get_s2t_smem_desc_tensor(tiled_copy, thr_copy.partition_S(src_compact))
1179
+ # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K)
1180
+ dst_partition = thr_copy.partition_D(dst_compact)
1181
+
1182
+ @dsl_user_op
1183
+ def copy_s2t(stage_idx, *, loc=None, ip=None, **new_kwargs):
1184
+ # Stage slice of partitioned source tensor: ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K)
1185
+ stage_coord = (None, None, None, None, stage_idx)
1186
+ cute.copy(
1187
+ tiled_copy, src_partition[stage_coord], dst_partition, loc=loc, ip=ip, **new_kwargs
1188
+ )
1189
+
1190
+ return copy_s2t
1191
+
1192
+
1193
+ # tcgen05 TMEM <-> RMEM helpers (t2r loads / r2t stores).
1194
+ #
1195
+ # The register-side fragment of a tmem copy is derivable by layout algebra,
1196
+ # with no reference to the original (pre-partition) tile tensor:
1197
+ # - the per-thread register VALUE shape is mode 1 of the tiled copy's
1198
+ # register-side TV layout (`layout_dst_tv_tiled` for loads,
1199
+ # `layout_src_tv_tiled` for stores). The tmem-side partition can't supply
1200
+ # it: tmem partitioning is warp-collective, so its value mode counts tmem
1201
+ # cells across the whole warp, not per-thread register elements.
1202
+ # - the tile-iteration modes are shared between partition_S and partition_D
1203
+ # (same tiler over the same tile extent), so they can be read off whichever
1204
+ # side was already partitioned.
1205
+ # This kills the make-a-fake/identity-tensor-and-partition_D dance previously
1206
+ # needed at every t2r site.
1207
+
1208
+
1209
+ def tmem_reg_frag(
1210
+ tiled_copy: cute.TiledCopy,
1211
+ partitioned: cute.Tensor,
1212
+ num_extra_modes: int = 0,
1213
+ dtype: Optional[Type[cutlass.Numeric]] = None,
1214
+ *,
1215
+ loc=None,
1216
+ ip=None,
1217
+ ) -> cute.Tensor:
1218
+ """Allocate the per-thread register fragment for ONE tile of a tcgen05
1219
+ tmem copy, given any partitioned view of it (tmem or otherwise).
1220
+
1221
+ `partitioned` is (V, iter..., extra...) as produced by partition_S/_D;
1222
+ the trailing `num_extra_modes` modes (stage, epi-subtile, ...) are
1223
+ excluded from the fragment and indexed at copy time instead. `dtype`
1224
+ defaults to `partitioned.element_type`.
1225
+ The register side is inferred from the tcgen05 load/store op: destination
1226
+ for t2r loads, source for r2t stores."""
1227
+ tv = _tmem_copy_reg_tv_layout(tiled_copy)
1228
+ val_shape = tv.shape[1]
1229
+ rank = cute.rank(partitioned.shape)
1230
+ iters = tuple(partitioned.shape[i] for i in range(1, rank - num_extra_modes))
1231
+ frag_dtype = partitioned.element_type if const_expr(dtype is None) else dtype
1232
+ return cute.make_rmem_tensor((val_shape, *iters), frag_dtype, loc=loc, ip=ip)
1233
+
1234
+
1235
+ def coord_frag(tiled_copy: cute.TiledCopy, tidx: Int32, shape) -> cute.Tensor:
1236
+ """Per-thread (row, col) coordinates aligned with a tiled copy's register
1237
+ fragments (`tmem_reg_frag` / `load_t2r`): the register-side partition of
1238
+ an identity tensor over `shape`. Deliberately partition_D — partition_S of
1239
+ a TMEM tiled copy keeps whole warp-addressed atom tiles instead of
1240
+ distributing elements over lanes."""
1241
+ return tiled_copy.get_slice(tidx).partition_D(cute.make_identity_tensor(shape))
1242
+
1243
+
1244
+ def r2s_partition_from_t2r(
1245
+ tiled_copy_t2r: cute.TiledCopy,
1246
+ s: cute.Tensor,
1247
+ tidx: Int32,
1248
+ transpose: bool = False,
1249
+ position_independent=False,
1250
+ ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]:
1251
+ """SMEM-store (r2s) side chained off a tmem-load tiled copy: the r2s copy
1252
+ inherits the t2r copy's per-thread value ownership via make_tiled_copy_D,
1253
+ so the loaded fragment can be stored (post-conversion) without a shuffle.
1254
+ By default the store atom is selected like SM100 GEMM epilogues:
1255
+ `get_smem_store_op(layout, dst_dtype, tiled_copy_t2r.value_type, tiled_copy_t2r)`,
1256
+ so the stmatrix shape follows the tmem-load atom. `transpose=True` maps to
1257
+ COL_MAJOR, otherwise ROW_MAJOR.
1258
+
1259
+ `s` is the staged SMEM tile; its trailing stage mode is excluded from the
1260
+ register fragment. `position_independent=True` partitions through a
1261
+ position-independent swizzle view, matching `get_smem_store_C`.
1262
+ Returns `(tiled_copy, tRS_r, tRS_s)`; store via
1263
+ `cute.copy(tiled_copy, tRS_r, tRS_s[..., idx])`."""
1264
+ dtype = s.element_type
1265
+ layout = LayoutEnum.COL_MAJOR if const_expr(transpose) else LayoutEnum.ROW_MAJOR
1266
+ copy_atom = sm100_utils.get_smem_store_op(
1267
+ layout, dtype, tiled_copy_t2r.value_type, tiled_copy_t2r
1268
+ )
1269
+ tiled_copy = cute.make_tiled_copy_D(copy_atom, tiled_copy_t2r)
1270
+ thr_copy = tiled_copy.get_slice(tidx)
1271
+ if const_expr(not position_independent):
1272
+ tRS_s = thr_copy.partition_D(s)
1273
+ else:
1274
+ tRS_s = partition_D_position_independent(thr_copy, s)
1275
+ rank = cute.rank(tRS_s.shape)
1276
+ frag_shape = tuple(tRS_s.shape[i] for i in range(rank - 1))
1277
+ tRS_r = cute.make_rmem_tensor(frag_shape, dtype)
1278
+ return tiled_copy, tRS_r, tRS_s
1279
+
1280
+
1281
+ def s2r_partition_from_t2r(
1282
+ tiled_copy_t2r: cute.TiledCopy,
1283
+ s: cute.Tensor,
1284
+ tidx: Int32,
1285
+ r_layout: cute.Layout,
1286
+ copy_atom: Optional[cute.CopyAtom] = None,
1287
+ transpose: bool = False,
1288
+ position_independent=False,
1289
+ ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor, cute.Tensor]:
1290
+ """SMEM-load (s2r) counterpart of `r2s_partition_from_t2r` (ldmatrix vs
1291
+ stmatrix), for reading an epilogue input that was TMA-staged into an
1292
+ epi-tile SMEM buffer (e.g. C in gemm, z in ssd) into registers
1293
+ element-aligned with the t2r fragments. The register fragment is
1294
+ allocated with `r_layout` (pass the r2s fragment's layout) so its linear
1295
+ element order matches the t2r/r2s fragments; `tSR_r` is its retiled view
1296
+ for the collective copy. Per-warp SMEM footprints of this load and the
1297
+ chained r2s store coincide, so reusing one buffer for input then output
1298
+ is warp-local (no inter-warp hazard).
1299
+
1300
+ `position_independent=True` partitions through a position-independent
1301
+ swizzle view, matching `get_smem_load_C`.
1302
+
1303
+ Returns `(tiled_copy, tRS_r, tSR_r, tSR_s)`; load via
1304
+ `cute.copy(tiled_copy, tSR_s[..., idx], tSR_r)` then read `tRS_r`."""
1305
+ dtype = s.element_type
1306
+ if const_expr(copy_atom is None):
1307
+ copy_atom = cute.make_copy_atom(
1308
+ warp.LdMatrix8x8x16bOp(transpose=transpose, num_matrices=4), dtype
1309
+ )
1310
+ tiled_copy = cute.make_tiled_copy_D(copy_atom, tiled_copy_t2r)
1311
+ thr_copy = tiled_copy.get_slice(tidx)
1312
+ if const_expr(not position_independent):
1313
+ tSR_s = thr_copy.partition_S(s)
1314
+ else:
1315
+ tSR_s = partition_S_position_independent(thr_copy, s)
1316
+ tRS_r = cute.make_rmem_tensor(r_layout, dtype)
1317
+ tSR_r = tiled_copy.retile(tRS_r)
1318
+ return tiled_copy, tRS_r, tSR_r, tSR_s
1319
+
1320
+
1321
  def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsync):
1322
  def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
1323
  copy(
 
1330
  return copy_fn
1331
 
1332
 
1333
+ def chain_tma_producer_copy_fns(copy_fns: Sequence[Optional[Callable]]):
1334
+ if not any(fn is not None for fn in copy_fns):
1335
+ return None
1336
+
1337
+ def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
1338
+ for fn in copy_fns:
1339
+ if const_expr(fn is not None):
1340
+ fn(src_idx=src_idx, producer_state=producer_state, **new_kwargs)
1341
+
1342
+ return copy_fn
1343
+
1344
+
1345
  @cute.jit
1346
  def gather_m_get_copy_fn(
1347
  thr_copy_A: cute.ThrCopy,
 
1385
 
1386
  mA_k = cute.logical_divide(mA, (None, tile_K))
1387
 
1388
+ def copy_fn(src_idx, dst_idx, pred: cutlass.Constexpr[bool] = False):
1389
  tApA_k = None
1390
  if const_expr(pred):
1391
  tApA_k = cute.make_rmem_tensor(cols_per_thread, Boolean)
 
1494
  for k in cutlass.range(cols_per_thread):
1495
  col_idx = tAcA[0, 0, k][1]
1496
  k_idx[k] = sAIdx_cur[col_idx]
1497
+ a_prefetch_pipeline.consumer_release(a_prefetch_consumer_state)
 
 
1498
  return k_idx, tApA_k
1499
 
1500
  def copy_fn(
 
1606
  ) -> cute.Tensor:
1607
  a_prefetch_pipeline.consumer_wait(a_prefetch_consumer_state)
1608
  tSR_rAIdx = load_s2r(tSR_sAIdx[None, None, dst_idx])
1609
+ a_prefetch_pipeline.consumer_release(a_prefetch_consumer_state)
 
 
1610
  return tSR_rAIdx
1611
 
1612
  def copy_fn(src_idx, dst_idx, tSR_rAIdx, tma_bar_ptr: cute.Pointer):
build/torch-cuda/quack/cross_entropy.py CHANGED
@@ -5,7 +5,7 @@ from functools import partial
5
  from typing import Optional, Type, Literal
6
 
7
  import torch
8
- from ._ops_compat import add_quack_op_namespace_prefix
9
  from torch import Tensor
10
 
11
  import cuda.bindings.driver as cuda
@@ -18,11 +18,12 @@ from . import utils as utils
18
  from . import copy_utils as copy_utils
19
  from . import layout_utils as layout_utils
20
  from .compile_utils import make_fake_tensor as fake_tensor
 
21
  from .reduce import row_reduce, online_softmax_reduce
22
  from .reduction_base import ReductionBase
23
- from .cache_utils import jit_cache
24
  from .cute_dsl_utils import torch2cute_dtype_map
25
- from cutlass.base_dsl import Arch
26
 
27
 
28
  class CrossEntropy(ReductionBase):
@@ -75,6 +76,7 @@ class CrossEntropy(ReductionBase):
75
  mLoss: cute.Tensor, # (M,)
76
  mLSE: Optional[cute.Tensor], # (M,)
77
  mdX: Optional[cute.Tensor], # (M, N) - if provided, compute gradient
 
78
  ignore_index: Int32, # Index to ignore in loss computation
79
  stream: cuda.CUstream,
80
  ):
@@ -97,6 +99,7 @@ class CrossEntropy(ReductionBase):
97
  mLoss,
98
  mLSE,
99
  mdX,
 
100
  ignore_index,
101
  tiler_mn,
102
  tiled_copy,
@@ -117,6 +120,7 @@ class CrossEntropy(ReductionBase):
117
  mLoss: cute.Tensor, # (M,)
118
  mLSE: Optional[cute.Tensor], # (M,)
119
  mdX: Optional[cute.Tensor], # (M, N) - if provided, compute gradient
 
120
  ignore_index: Int32, # Index to ignore in loss computation
121
  tiler_mn: cute.Shape,
122
  tiled_copy: cute.TiledCopy,
@@ -156,8 +160,16 @@ class CrossEntropy(ReductionBase):
156
 
157
  row = tXcX[0][0]
158
  target = Int32.zero
 
159
  if row < shape[0]:
160
  target = Int32(mTarget[row])
 
 
 
 
 
 
 
161
 
162
  if row < shape[0]:
163
  copy(tXgX, tXsX, is_async=True)
@@ -221,7 +233,7 @@ class CrossEntropy(ReductionBase):
221
  ):
222
  lse = max_x + cute.math.log(denom, fastmath=True)
223
  # Set loss to 0 if this index should be ignored, otherwise compute normally
224
- loss_val = (lse - target_logit) if not should_ignore else Float32.zero
225
  mLoss[row] = mLoss.element_type(loss_val)
226
  if const_expr(mLSE is not None):
227
  mLSE[row] = lse
@@ -239,7 +251,6 @@ class CrossEntropy(ReductionBase):
239
  probs = exp_x * denom_inv
240
  gdX = cute.local_tile(mdX, tiler_mn, (bidx, cluster_y))
241
  tXgdX = thr_copy.partition_D(gdX)
242
- tXrdX = cute.make_rmem_tensor_like(tXgdX)
243
  tXcFull = thr_copy.partition_S(cX)
244
  # Compute gradient: probs for all classes, (probs - 1) for target class
245
  # If ignored, gradient is already zero
@@ -248,46 +259,58 @@ class CrossEntropy(ReductionBase):
248
  if not should_ignore:
249
  for i in cutlass.range(cute.size(tXrX), unroll_full=True):
250
  tXrdX_f32[i] = tXrdX_f32[i] if tXcFull[i][1] != target else tXrdX_f32[i] - 1.0
251
- tXrdX.store(tXrdX_f32.load().to(tXrdX.element_type))
 
 
252
  if row < shape[0]:
253
  copy(tXrdX, tXgdX)
254
 
255
-
256
- @jit_cache
257
- def _compile_cross_entropy_fwd(
258
- dtype, target_dtype, target_logit_dtype, N, has_lse, has_dx, target_logit_ndim
259
- ):
260
- batch_sym = cute.sym_int()
261
- div = math.gcd(128 // dtype.width, N)
262
- x_cute = fake_tensor(dtype, (batch_sym, N), div)
263
- dx_cute = fake_tensor(dtype, (batch_sym, N), div) if has_dx else None
264
- target_cute = fake_tensor(target_dtype, (batch_sym,))
265
- if target_logit_dtype is not None:
266
- if target_logit_ndim == 2:
267
- target_logit_cute = fake_tensor(target_logit_dtype, (batch_sym, cute.sym_int()), div)
 
 
 
 
 
 
 
 
 
 
 
268
  else:
269
- target_logit_cute = fake_tensor(target_logit_dtype, (batch_sym,))
270
- else:
271
- target_logit_cute = None
272
- loss_cute = fake_tensor(Float32, (batch_sym,))
273
- lse_cute = fake_tensor(Float32, (batch_sym,)) if has_lse else None
274
- # If there's dx, it's faster to not use online softmax since we want the exp(x - max)
275
- cross_entropy_op = CrossEntropy(dtype, N, online_softmax=not has_dx)
276
- return cute.compile(
277
- cross_entropy_op,
278
- x_cute,
279
- target_cute,
280
- target_logit_cute,
281
- loss_cute,
282
- lse_cute,
283
- dx_cute,
284
- Int32(0), # ignore_index, just for compilation
285
- cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True),
286
- options="--enable-tvm-ffi",
287
- )
288
 
289
 
290
- @torch.library.custom_op(add_quack_op_namespace_prefix("cross_entropy_fwd_out"), mutates_args={"loss", "lse", "dx"})
291
  def cross_entropy_fwd_out(
292
  x: Tensor,
293
  target: Tensor,
@@ -295,6 +318,7 @@ def cross_entropy_fwd_out(
295
  loss: Tensor,
296
  lse: Optional[Tensor],
297
  dx: Optional[Tensor],
 
298
  ignore_index: int = -100,
299
  ) -> None:
300
  """Cross entropy forward pass.
@@ -307,6 +331,7 @@ def cross_entropy_fwd_out(
307
  loss: Output loss tensor of shape (M,)
308
  lse: Optional output log-sum-exp tensor of shape (M,)
309
  dx: Optional output gradient tensor of shape (M, N)
 
310
  ignore_index: Index to ignore in loss computation
311
 
312
  Returns:
@@ -314,14 +339,12 @@ def cross_entropy_fwd_out(
314
  """
315
  assert x.dim() == 2, "Input must be 2D"
316
  assert target.dim() == 1, "Target must be 1D"
317
- assert x.is_cuda and target.is_cuda, "Tensors must be on CUDA device"
318
  assert x.dtype in [torch.float16, torch.bfloat16, torch.float32], "Unsupported input dtype"
319
  assert target.dtype in [torch.int32, torch.int64], "Target must be int32 or int64"
320
  if target_logit is not None:
321
- assert target_logit.is_cuda, "Target logits must be on CUDA device"
322
  assert target_logit.dtype in [torch.float16, torch.bfloat16, torch.float32]
323
- if dx is not None:
324
- assert dx.is_cuda, "dx must be on CUDA device"
325
  N = x.size(1)
326
  dtype = torch2cute_dtype_map[x.dtype]
327
  target_dtype = torch2cute_dtype_map[target.dtype]
@@ -329,54 +352,24 @@ def cross_entropy_fwd_out(
329
  torch2cute_dtype_map[target_logit.dtype] if target_logit is not None else None
330
  )
331
  target_logit_ndim = target_logit.ndim if target_logit is not None else None
332
- _compile_cross_entropy_fwd(
 
333
  dtype,
334
  target_dtype,
335
  target_logit_dtype,
336
  N,
337
  lse is not None,
338
  dx is not None,
 
339
  target_logit_ndim,
340
- )(x, target, target_logit, loss, lse, dx, Int32(ignore_index))
341
-
342
-
343
- @cross_entropy_fwd_out.register_fake
344
- def _cross_entropy_fwd_out_fake(
345
- x: Tensor,
346
- target: Tensor,
347
- target_logit: Optional[Tensor],
348
- loss: Tensor,
349
- lse: Optional[Tensor],
350
- dx: Optional[Tensor],
351
- ignore_index: int = -100,
352
- ) -> None:
353
- # See softmax.py _softmax_fwd_fake for why register_fake is needed.
354
- from .cache_utils import COMPILE_ONLY
355
-
356
- if COMPILE_ONLY and not isinstance(x.size(1), torch.SymInt):
357
- N = x.size(1)
358
- dtype = torch2cute_dtype_map[x.dtype]
359
- target_dtype = torch2cute_dtype_map[target.dtype]
360
- target_logit_dtype = (
361
- torch2cute_dtype_map[target_logit.dtype] if target_logit is not None else None
362
- )
363
- target_logit_ndim = target_logit.ndim if target_logit is not None else None
364
- _compile_cross_entropy_fwd(
365
- dtype,
366
- target_dtype,
367
- target_logit_dtype,
368
- N,
369
- lse is not None,
370
- dx is not None,
371
- target_logit_ndim,
372
- )
373
- _compile_cross_entropy_backward(dtype, target_dtype, N)
374
 
375
 
376
  def cross_entropy_fwd(
377
  x: torch.Tensor,
378
  target: torch.Tensor,
379
  target_logit: Optional[torch.Tensor] = None,
 
380
  ignore_index: int = -100,
381
  return_lse: bool = False,
382
  return_dx: bool = False,
@@ -387,7 +380,7 @@ def cross_entropy_fwd(
387
  loss = torch.empty(M, device=device, dtype=torch.float32)
388
  lse = torch.empty(M, device=device, dtype=torch.float32) if return_lse else None
389
  dx = (torch.empty_like(x) if not inplace_backward else x) if return_dx else None
390
- cross_entropy_fwd_out(x, target, target_logit, loss, lse, dx, ignore_index)
391
  if return_lse and return_dx:
392
  return loss, lse, dx
393
  elif return_lse:
@@ -432,6 +425,7 @@ class CrossEntropyBackward:
432
  mDLoss: cute.Tensor,
433
  mdX: cute.Tensor,
434
  mLSE: cute.Tensor,
 
435
  ignore_index: Int32, # Index to ignore in gradient computation
436
  stream: cuda.CUstream,
437
  ):
@@ -451,6 +445,7 @@ class CrossEntropyBackward:
451
  mDLoss,
452
  mdX,
453
  mLSE,
 
454
  ignore_index,
455
  mX.shape,
456
  tiler_mn,
@@ -474,6 +469,7 @@ class CrossEntropyBackward:
474
  mDLoss: cute.Tensor, # (M,)
475
  mdX: cute.Tensor, # (M, N)
476
  mLSE: cute.Tensor, # (M,)
 
477
  ignore_index: Int32, # Index to ignore in gradient computation
478
  shape: cute.Shape,
479
  tiler_mn: cute.Shape,
@@ -507,6 +503,18 @@ class CrossEntropyBackward:
507
  copy = partial(copy_utils.copy, pred=tXpX)
508
 
509
  row = tXcX[0][0]
 
 
 
 
 
 
 
 
 
 
 
 
510
  if row < shape[0]:
511
  copy(tXgX, tXsX, is_async=True)
512
  cute.arch.cp_async_commit_group()
@@ -516,13 +524,11 @@ class CrossEntropyBackward:
516
  cute.autovec_copy(tXsX, tXrX)
517
  x = tXrX.load().to(Float32)
518
 
519
- target = Int32.zero
520
  dloss = Float32.zero
521
  lse = Float32.zero
522
  if row < shape[0]:
523
- target = Int32(mTarget[row])
524
  should_ignore = Boolean(target == ignore_index)
525
- # Set dloss to 0 if this index should be ignored
526
  if not should_ignore:
527
  dloss = Float32(mDLoss[row])
528
  lse = Float32(mLSE[row])
@@ -534,32 +540,36 @@ class CrossEntropyBackward:
534
  for i in cutlass.range(cute.size(tXcFull), unroll_full=True):
535
  mask[i] = tXcFull[i][1] == target
536
  grad = cute.where(mask.load(), prob_shifted, probs)
537
- grad = grad * dloss
538
 
539
  tXrdX.store(grad.to(tXrdX.element_type))
540
  if row < shape[0]:
541
  copy(tXrdX, tXgdX)
542
 
543
-
544
- @jit_cache
545
- def _compile_cross_entropy_backward(dtype, target_dtype, N):
546
- batch_sym = cute.sym_int()
547
- div = math.gcd(128 // dtype.width, N)
548
- x_cute, dx_cute = [fake_tensor(dtype, (batch_sym, N), div)] * 2
549
- target_cute = fake_tensor(target_dtype, (batch_sym,))
550
- dloss_cute, lse_cute = [fake_tensor(Float32, (batch_sym,))] * 2
551
- cross_entropy_backward_op = CrossEntropyBackward(dtype, N)
552
- return cute.compile(
553
- cross_entropy_backward_op,
554
- x_cute,
555
- target_cute,
556
- dloss_cute,
557
- dx_cute,
558
- lse_cute,
559
- Int32(0), # ignore_index, just for compilation
560
- cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True),
561
- options="--enable-tvm-ffi",
562
- )
 
 
 
 
563
 
564
 
565
  def _cross_entropy_backward(
@@ -568,6 +578,7 @@ def _cross_entropy_backward(
568
  dloss: torch.Tensor,
569
  lse: torch.Tensor,
570
  dx: torch.Tensor,
 
571
  ignore_index=-100,
572
  ) -> None:
573
  """Cross entropy backward pass.
@@ -576,8 +587,11 @@ def _cross_entropy_backward(
576
  target: Target class indices tensor of shape (M,)
577
  dloss: Upstream gradients tensor of shape (M,)
578
  lse: Log-sum-exp values tensor of shape (M,)
 
 
 
579
  Returns:
580
- Input gradients tensor of shape (M, N)
581
  """
582
  assert x.dim() == 2, "Input must be 2D"
583
  assert target.dim() == 1, "Target must be 1D"
@@ -586,48 +600,33 @@ def _cross_entropy_backward(
586
  assert x.shape[0] == target.shape[0], "Batch dimensions must match"
587
  assert x.shape[0] == dloss.shape[0], "Batch dimensions must match"
588
  assert x.shape[0] == lse.shape[0], "Batch dimensions must match"
589
- assert x.is_cuda and target.is_cuda and dloss.is_cuda and lse.is_cuda, (
590
- "Tensors must be on CUDA device"
591
- )
592
  assert x.dtype in [torch.float16, torch.bfloat16, torch.float32], "Unsupported input dtype"
593
  assert target.dtype in [torch.int32, torch.int64], "Target must be int32 or int64"
 
 
 
 
 
594
  N = x.size(1)
595
  dtype = torch2cute_dtype_map[x.dtype]
596
  target_dtype = torch2cute_dtype_map[target.dtype]
597
- _compile_cross_entropy_backward(dtype, target_dtype, N)(
598
- x, target, dloss, dx, lse, Int32(ignore_index)
 
599
  )
600
 
601
 
602
- @torch.library.custom_op(add_quack_op_namespace_prefix("cross_entropy_bwd_out"), mutates_args={"dx"})
603
  def cross_entropy_bwd_out(
604
  x: torch.Tensor,
605
  target: torch.Tensor,
606
  dloss: torch.Tensor,
607
  lse: torch.Tensor,
608
  dx: torch.Tensor,
 
609
  ignore_index: int = -100,
610
  ) -> None:
611
- _cross_entropy_backward(x, target, dloss, lse, dx, ignore_index)
612
-
613
-
614
- @cross_entropy_bwd_out.register_fake
615
- def _cross_entropy_bwd_out_fake(
616
- x: torch.Tensor,
617
- target: torch.Tensor,
618
- dloss: torch.Tensor,
619
- lse: torch.Tensor,
620
- dx: torch.Tensor,
621
- ignore_index: int = -100,
622
- ) -> None:
623
- # See softmax.py _softmax_fwd_fake for why register_fake is needed.
624
- from .cache_utils import COMPILE_ONLY
625
-
626
- if COMPILE_ONLY and not isinstance(x.size(1), torch.SymInt):
627
- N = x.size(1)
628
- dtype = torch2cute_dtype_map[x.dtype]
629
- target_dtype = torch2cute_dtype_map[target.dtype]
630
- _compile_cross_entropy_backward(dtype, target_dtype, N)
631
 
632
 
633
  def cross_entropy_bwd(
@@ -635,51 +634,90 @@ def cross_entropy_bwd(
635
  target: torch.Tensor,
636
  dloss: torch.Tensor,
637
  lse: torch.Tensor,
 
638
  ignore_index: int = -100,
639
  inplace_backward: bool = False,
640
  ) -> None:
641
  if inplace_backward and not torch.compiler.is_compiling():
642
  dx = x
643
  _cross_entropy_backward(
644
- x=x, target=target, dloss=dloss, lse=lse, dx=x, ignore_index=ignore_index
 
 
 
 
 
 
645
  )
646
  else:
647
  dx = torch.empty_like(x)
648
  cross_entropy_bwd_out(
649
- x=x, target=target, dloss=dloss, lse=lse, dx=dx, ignore_index=ignore_index
 
 
 
 
 
 
650
  )
651
  return dx
652
 
653
 
654
  class CrossEntropyFunction(torch.autograd.Function):
655
  @staticmethod
656
- def forward(ctx, x, target, lse_partial=None, ignore_index=-100, inplace_backward=False):
 
 
 
 
 
 
 
 
657
  if lse_partial is None:
658
- loss, lse = cross_entropy_fwd(x, target, ignore_index=ignore_index, return_lse=True)
 
 
 
 
 
 
659
  else:
660
  # if we already compute partial lse, then to compute the final lse we treat
661
  # @lse_partial as @x and @x as @target_logit
662
  loss, lse = cross_entropy_fwd(
663
- lse_partial, target, target_logit=x, ignore_index=ignore_index, return_lse=True
 
 
 
 
 
664
  )
665
- ctx.save_for_backward(x, target, lse)
666
  ctx.ignore_index = ignore_index
667
  ctx.inplace_backward = inplace_backward
668
  return loss
669
 
670
  @staticmethod
671
  def backward(ctx, dloss):
672
- x, target, lse = ctx.saved_tensors
673
  dx = cross_entropy_bwd(
674
- x, target, dloss, lse, ctx.ignore_index, inplace_backward=ctx.inplace_backward
 
 
 
 
 
 
675
  )
676
- return dx, None, None, None, None
677
 
678
 
679
  def cross_entropy(
680
  x: torch.Tensor,
681
  target: torch.Tensor,
682
  lse_partial: Optional[torch.Tensor] = None,
 
683
  ignore_index: int = -100,
684
  reduction: Literal["none", "mean", "sum"] = "mean",
685
  inplace_backward: bool = False,
@@ -690,12 +728,13 @@ def cross_entropy(
690
  x: Input logits tensor of shape (M, N)
691
  target: Target class indices tensor of shape (M,)
692
  lse_partial: Optional precomputed log-sum-exp partial results
 
 
693
  reduction: Specifies the reduction to apply to the output:
694
  'none': no reduction will be applied (default)
695
  'mean': the sum of the output will be divided by the number of elements
696
  'sum': the output will be summed
697
  inplace_backward: Whether to perform backward pass in-place
698
- ignore_index: Index to ignore in loss computation (loss will be 0 for these indices)
699
 
700
  Returns:
701
  Cross entropy loss tensor:
@@ -703,8 +742,19 @@ def cross_entropy(
703
  - If reduction='mean': scalar tensor with mean loss
704
  - If reduction='sum': scalar tensor with sum of losses
705
  """
706
- loss = CrossEntropyFunction.apply(x, target, lse_partial, ignore_index, inplace_backward)
 
 
 
 
 
 
 
707
  if reduction == "mean":
 
 
 
 
708
  return loss.sum() / (target != ignore_index).sum().float()
709
  elif reduction == "sum":
710
  return loss.sum()
 
5
  from typing import Optional, Type, Literal
6
 
7
  import torch
8
+ from ._ops_compat import add_op_namespace_prefix
9
  from torch import Tensor
10
 
11
  import cuda.bindings.driver as cuda
 
18
  from . import copy_utils as copy_utils
19
  from . import layout_utils as layout_utils
20
  from .compile_utils import make_fake_tensor as fake_tensor
21
+ from .dsl import cute_op
22
  from .reduce import row_reduce, online_softmax_reduce
23
  from .reduction_base import ReductionBase
24
+ from .cache import jit_cache
25
  from .cute_dsl_utils import torch2cute_dtype_map
26
+ from cutlass.base_dsl.arch import Arch
27
 
28
 
29
  class CrossEntropy(ReductionBase):
 
76
  mLoss: cute.Tensor, # (M,)
77
  mLSE: Optional[cute.Tensor], # (M,)
78
  mdX: Optional[cute.Tensor], # (M, N) - if provided, compute gradient
79
+ mWeight: Optional[cute.Tensor],
80
  ignore_index: Int32, # Index to ignore in loss computation
81
  stream: cuda.CUstream,
82
  ):
 
99
  mLoss,
100
  mLSE,
101
  mdX,
102
+ mWeight,
103
  ignore_index,
104
  tiler_mn,
105
  tiled_copy,
 
120
  mLoss: cute.Tensor, # (M,)
121
  mLSE: Optional[cute.Tensor], # (M,)
122
  mdX: Optional[cute.Tensor], # (M, N) - if provided, compute gradient
123
+ mWeight: Optional[cute.Tensor],
124
  ignore_index: Int32, # Index to ignore in loss computation
125
  tiler_mn: cute.Shape,
126
  tiled_copy: cute.TiledCopy,
 
160
 
161
  row = tXcX[0][0]
162
  target = Int32.zero
163
+ target_weight = Float32.zero
164
  if row < shape[0]:
165
  target = Int32(mTarget[row])
166
+ if const_expr(mWeight is not None):
167
+ # Gate on target != ignore_index: ignore_index may be negative
168
+ # (PyTorch default -100), and indexing mWeight at that offset is OOB.
169
+ if target != ignore_index:
170
+ target_weight = Float32(mWeight[target])
171
+ else:
172
+ target_weight = 1.0
173
 
174
  if row < shape[0]:
175
  copy(tXgX, tXsX, is_async=True)
 
233
  ):
234
  lse = max_x + cute.math.log(denom, fastmath=True)
235
  # Set loss to 0 if this index should be ignored, otherwise compute normally
236
+ loss_val = target_weight * (lse - target_logit) if not should_ignore else Float32.zero
237
  mLoss[row] = mLoss.element_type(loss_val)
238
  if const_expr(mLSE is not None):
239
  mLSE[row] = lse
 
251
  probs = exp_x * denom_inv
252
  gdX = cute.local_tile(mdX, tiler_mn, (bidx, cluster_y))
253
  tXgdX = thr_copy.partition_D(gdX)
 
254
  tXcFull = thr_copy.partition_S(cX)
255
  # Compute gradient: probs for all classes, (probs - 1) for target class
256
  # If ignored, gradient is already zero
 
259
  if not should_ignore:
260
  for i in cutlass.range(cute.size(tXrX), unroll_full=True):
261
  tXrdX_f32[i] = tXrdX_f32[i] if tXcFull[i][1] != target else tXrdX_f32[i] - 1.0
262
+ if const_expr(mWeight is not None):
263
+ tXrdX_f32.store(tXrdX_f32.load() * target_weight)
264
+ tXrdX = tXrdX_f32.to(tXgdX.element_type)
265
  if row < shape[0]:
266
  copy(tXrdX, tXgdX)
267
 
268
+ @staticmethod
269
+ @jit_cache
270
+ def compile(
271
+ dtype,
272
+ target_dtype,
273
+ target_logit_dtype,
274
+ N,
275
+ has_lse,
276
+ has_dx,
277
+ weight_dtype,
278
+ target_logit_ndim,
279
+ ):
280
+ batch_sym = cute.sym_int()
281
+ div = math.gcd(128 // dtype.width, N)
282
+ x_cute = fake_tensor(dtype, (batch_sym, N), div)
283
+ dx_cute = fake_tensor(dtype, (batch_sym, N), div) if has_dx else None
284
+ target_cute = fake_tensor(target_dtype, (batch_sym,))
285
+ if target_logit_dtype is not None:
286
+ if target_logit_ndim == 2:
287
+ target_logit_cute = fake_tensor(
288
+ target_logit_dtype, (batch_sym, cute.sym_int()), div
289
+ )
290
+ else:
291
+ target_logit_cute = fake_tensor(target_logit_dtype, (batch_sym,))
292
  else:
293
+ target_logit_cute = None
294
+ loss_cute = fake_tensor(Float32, (batch_sym,))
295
+ lse_cute = fake_tensor(Float32, (batch_sym,)) if has_lse else None
296
+ weight_cute = fake_tensor(weight_dtype, (N,)) if weight_dtype is not None else None
297
+ # If there's dx, it's faster to not use online softmax since we want the exp(x - max)
298
+ return cute.compile(
299
+ CrossEntropy(dtype, N, online_softmax=not has_dx),
300
+ x_cute,
301
+ target_cute,
302
+ target_logit_cute,
303
+ loss_cute,
304
+ lse_cute,
305
+ dx_cute,
306
+ weight_cute,
307
+ Int32(0), # ignore_index, just for compilation
308
+ cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True),
309
+ options="--enable-tvm-ffi",
310
+ )
 
311
 
312
 
313
+ @cute_op(add_op_namespace_prefix("cross_entropy_fwd_out"), mutates_args={"loss", "lse", "dx"})
314
  def cross_entropy_fwd_out(
315
  x: Tensor,
316
  target: Tensor,
 
318
  loss: Tensor,
319
  lse: Optional[Tensor],
320
  dx: Optional[Tensor],
321
+ weight: Optional[Tensor],
322
  ignore_index: int = -100,
323
  ) -> None:
324
  """Cross entropy forward pass.
 
331
  loss: Output loss tensor of shape (M,)
332
  lse: Optional output log-sum-exp tensor of shape (M,)
333
  dx: Optional output gradient tensor of shape (M, N)
334
+ weight: Optional weight vector of shape (N,)
335
  ignore_index: Index to ignore in loss computation
336
 
337
  Returns:
 
339
  """
340
  assert x.dim() == 2, "Input must be 2D"
341
  assert target.dim() == 1, "Target must be 1D"
 
342
  assert x.dtype in [torch.float16, torch.bfloat16, torch.float32], "Unsupported input dtype"
343
  assert target.dtype in [torch.int32, torch.int64], "Target must be int32 or int64"
344
  if target_logit is not None:
 
345
  assert target_logit.dtype in [torch.float16, torch.bfloat16, torch.float32]
346
+ if x.size(0) == 0:
347
+ return
348
  N = x.size(1)
349
  dtype = torch2cute_dtype_map[x.dtype]
350
  target_dtype = torch2cute_dtype_map[target.dtype]
 
352
  torch2cute_dtype_map[target_logit.dtype] if target_logit is not None else None
353
  )
354
  target_logit_ndim = target_logit.ndim if target_logit is not None else None
355
+ weight_dtype = torch2cute_dtype_map[weight.dtype] if weight is not None else None
356
+ CrossEntropy.compile(
357
  dtype,
358
  target_dtype,
359
  target_logit_dtype,
360
  N,
361
  lse is not None,
362
  dx is not None,
363
+ weight_dtype,
364
  target_logit_ndim,
365
+ )(x, target, target_logit, loss, lse, dx, weight, Int32(ignore_index))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
 
368
  def cross_entropy_fwd(
369
  x: torch.Tensor,
370
  target: torch.Tensor,
371
  target_logit: Optional[torch.Tensor] = None,
372
+ weight: Optional[torch.Tensor] = None,
373
  ignore_index: int = -100,
374
  return_lse: bool = False,
375
  return_dx: bool = False,
 
380
  loss = torch.empty(M, device=device, dtype=torch.float32)
381
  lse = torch.empty(M, device=device, dtype=torch.float32) if return_lse else None
382
  dx = (torch.empty_like(x) if not inplace_backward else x) if return_dx else None
383
+ cross_entropy_fwd_out(x, target, target_logit, loss, lse, dx, weight, ignore_index)
384
  if return_lse and return_dx:
385
  return loss, lse, dx
386
  elif return_lse:
 
425
  mDLoss: cute.Tensor,
426
  mdX: cute.Tensor,
427
  mLSE: cute.Tensor,
428
+ mWeight: Optional[cute.Tensor],
429
  ignore_index: Int32, # Index to ignore in gradient computation
430
  stream: cuda.CUstream,
431
  ):
 
445
  mDLoss,
446
  mdX,
447
  mLSE,
448
+ mWeight,
449
  ignore_index,
450
  mX.shape,
451
  tiler_mn,
 
469
  mDLoss: cute.Tensor, # (M,)
470
  mdX: cute.Tensor, # (M, N)
471
  mLSE: cute.Tensor, # (M,)
472
+ mWeight: Optional[cute.Tensor],
473
  ignore_index: Int32, # Index to ignore in gradient computation
474
  shape: cute.Shape,
475
  tiler_mn: cute.Shape,
 
503
  copy = partial(copy_utils.copy, pred=tXpX)
504
 
505
  row = tXcX[0][0]
506
+ target = Int32.zero
507
+ target_weight = Float32.zero
508
+ if row < shape[0]:
509
+ target = Int32(mTarget[row])
510
+ if const_expr(mWeight is not None):
511
+ # Gate on target != ignore_index: ignore_index may be negative
512
+ # (PyTorch default -100), and indexing mWeight at that offset is OOB.
513
+ if target != ignore_index:
514
+ target_weight = Float32(mWeight[target])
515
+ else:
516
+ target_weight = 1.0
517
+
518
  if row < shape[0]:
519
  copy(tXgX, tXsX, is_async=True)
520
  cute.arch.cp_async_commit_group()
 
524
  cute.autovec_copy(tXsX, tXrX)
525
  x = tXrX.load().to(Float32)
526
 
 
527
  dloss = Float32.zero
528
  lse = Float32.zero
529
  if row < shape[0]:
 
530
  should_ignore = Boolean(target == ignore_index)
531
+ # dloss is set to 0 if this index should be ignored
532
  if not should_ignore:
533
  dloss = Float32(mDLoss[row])
534
  lse = Float32(mLSE[row])
 
540
  for i in cutlass.range(cute.size(tXcFull), unroll_full=True):
541
  mask[i] = tXcFull[i][1] == target
542
  grad = cute.where(mask.load(), prob_shifted, probs)
543
+ grad = grad * dloss * target_weight
544
 
545
  tXrdX.store(grad.to(tXrdX.element_type))
546
  if row < shape[0]:
547
  copy(tXrdX, tXgdX)
548
 
549
+ @staticmethod
550
+ @jit_cache
551
+ def compile(dtype, target_dtype, N, weight_dtype):
552
+ batch_sym = cute.sym_int()
553
+ div = math.gcd(128 // dtype.width, N)
554
+ x_cute, dx_cute = [fake_tensor(dtype, (batch_sym, N), div)] * 2
555
+ target_cute = fake_tensor(target_dtype, (batch_sym,))
556
+ dloss_cute = cute.runtime.make_fake_tensor(
557
+ Float32, (batch_sym,), stride=(cute.sym_int64(),)
558
+ )
559
+ lse_cute = fake_tensor(Float32, (batch_sym,))
560
+ weight_cute = fake_tensor(weight_dtype, (N,)) if weight_dtype is not None else None
561
+ return cute.compile(
562
+ CrossEntropyBackward(dtype, N),
563
+ x_cute,
564
+ target_cute,
565
+ dloss_cute,
566
+ dx_cute,
567
+ lse_cute,
568
+ weight_cute,
569
+ Int32(0), # ignore_index, just for compilation
570
+ cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True),
571
+ options="--enable-tvm-ffi",
572
+ )
573
 
574
 
575
  def _cross_entropy_backward(
 
578
  dloss: torch.Tensor,
579
  lse: torch.Tensor,
580
  dx: torch.Tensor,
581
+ weight: Optional[torch.Tensor] = None,
582
  ignore_index=-100,
583
  ) -> None:
584
  """Cross entropy backward pass.
 
587
  target: Target class indices tensor of shape (M,)
588
  dloss: Upstream gradients tensor of shape (M,)
589
  lse: Log-sum-exp values tensor of shape (M,)
590
+ dx: Output gradient tensor of shape (M, N)
591
+ weight: Optional per-class weight tensor of shape (N,)
592
+ ignore_index: Index to ignore in gradient computation
593
  Returns:
594
+ None (mutates dx in-place)
595
  """
596
  assert x.dim() == 2, "Input must be 2D"
597
  assert target.dim() == 1, "Target must be 1D"
 
600
  assert x.shape[0] == target.shape[0], "Batch dimensions must match"
601
  assert x.shape[0] == dloss.shape[0], "Batch dimensions must match"
602
  assert x.shape[0] == lse.shape[0], "Batch dimensions must match"
 
 
 
603
  assert x.dtype in [torch.float16, torch.bfloat16, torch.float32], "Unsupported input dtype"
604
  assert target.dtype in [torch.int32, torch.int64], "Target must be int32 or int64"
605
+ if weight is not None:
606
+ assert weight.is_cuda, "weight must be on CUDA device"
607
+ assert weight.is_floating_point(), "weight must be a floating-point tensor"
608
+ if x.size(0) == 0:
609
+ return
610
  N = x.size(1)
611
  dtype = torch2cute_dtype_map[x.dtype]
612
  target_dtype = torch2cute_dtype_map[target.dtype]
613
+ weight_dtype = torch2cute_dtype_map[weight.dtype] if weight is not None else None
614
+ CrossEntropyBackward.compile(dtype, target_dtype, N, weight_dtype)(
615
+ x, target, dloss, dx, lse, weight, Int32(ignore_index)
616
  )
617
 
618
 
619
+ @cute_op(add_op_namespace_prefix("cross_entropy_bwd_out"), mutates_args={"dx"})
620
  def cross_entropy_bwd_out(
621
  x: torch.Tensor,
622
  target: torch.Tensor,
623
  dloss: torch.Tensor,
624
  lse: torch.Tensor,
625
  dx: torch.Tensor,
626
+ weight: Optional[torch.Tensor] = None,
627
  ignore_index: int = -100,
628
  ) -> None:
629
+ _cross_entropy_backward(x, target, dloss, lse, dx, weight, ignore_index)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
 
631
 
632
  def cross_entropy_bwd(
 
634
  target: torch.Tensor,
635
  dloss: torch.Tensor,
636
  lse: torch.Tensor,
637
+ weight: Optional[torch.Tensor] = None,
638
  ignore_index: int = -100,
639
  inplace_backward: bool = False,
640
  ) -> None:
641
  if inplace_backward and not torch.compiler.is_compiling():
642
  dx = x
643
  _cross_entropy_backward(
644
+ x=x,
645
+ target=target,
646
+ dloss=dloss,
647
+ lse=lse,
648
+ dx=x,
649
+ weight=weight,
650
+ ignore_index=ignore_index,
651
  )
652
  else:
653
  dx = torch.empty_like(x)
654
  cross_entropy_bwd_out(
655
+ x=x,
656
+ target=target,
657
+ dloss=dloss,
658
+ lse=lse,
659
+ dx=dx,
660
+ weight=weight,
661
+ ignore_index=ignore_index,
662
  )
663
  return dx
664
 
665
 
666
  class CrossEntropyFunction(torch.autograd.Function):
667
  @staticmethod
668
+ def forward(
669
+ ctx,
670
+ x,
671
+ target,
672
+ lse_partial=None,
673
+ weight=None,
674
+ ignore_index=-100,
675
+ inplace_backward=False,
676
+ ):
677
  if lse_partial is None:
678
+ loss, lse = cross_entropy_fwd(
679
+ x,
680
+ target,
681
+ weight=weight,
682
+ ignore_index=ignore_index,
683
+ return_lse=True,
684
+ )
685
  else:
686
  # if we already compute partial lse, then to compute the final lse we treat
687
  # @lse_partial as @x and @x as @target_logit
688
  loss, lse = cross_entropy_fwd(
689
+ lse_partial,
690
+ target,
691
+ target_logit=x,
692
+ weight=weight,
693
+ ignore_index=ignore_index,
694
+ return_lse=True,
695
  )
696
+ ctx.save_for_backward(x, target, lse, weight)
697
  ctx.ignore_index = ignore_index
698
  ctx.inplace_backward = inplace_backward
699
  return loss
700
 
701
  @staticmethod
702
  def backward(ctx, dloss):
703
+ x, target, lse, weight = ctx.saved_tensors
704
  dx = cross_entropy_bwd(
705
+ x,
706
+ target,
707
+ dloss,
708
+ lse,
709
+ weight=weight,
710
+ ignore_index=ctx.ignore_index,
711
+ inplace_backward=ctx.inplace_backward,
712
  )
713
+ return dx, None, None, None, None, None
714
 
715
 
716
  def cross_entropy(
717
  x: torch.Tensor,
718
  target: torch.Tensor,
719
  lse_partial: Optional[torch.Tensor] = None,
720
+ weight: Optional[torch.Tensor] = None,
721
  ignore_index: int = -100,
722
  reduction: Literal["none", "mean", "sum"] = "mean",
723
  inplace_backward: bool = False,
 
728
  x: Input logits tensor of shape (M, N)
729
  target: Target class indices tensor of shape (M,)
730
  lse_partial: Optional precomputed log-sum-exp partial results
731
+ weight: Optional per-class weight tensor of shape (N,)
732
+ ignore_index: Index to ignore in loss computation (loss will be 0 for these indices)
733
  reduction: Specifies the reduction to apply to the output:
734
  'none': no reduction will be applied (default)
735
  'mean': the sum of the output will be divided by the number of elements
736
  'sum': the output will be summed
737
  inplace_backward: Whether to perform backward pass in-place
 
738
 
739
  Returns:
740
  Cross entropy loss tensor:
 
742
  - If reduction='mean': scalar tensor with mean loss
743
  - If reduction='sum': scalar tensor with sum of losses
744
  """
745
+ loss = CrossEntropyFunction.apply(
746
+ x,
747
+ target,
748
+ lse_partial,
749
+ weight,
750
+ ignore_index,
751
+ inplace_backward,
752
+ )
753
  if reduction == "mean":
754
+ if weight is not None:
755
+ valid = target != ignore_index
756
+ denom = (weight[target.clamp(min=0)] * valid).sum()
757
+ return loss.sum() / denom
758
  return loss.sum() / (target != ignore_index).sum().float()
759
  elif reduction == "sum":
760
  return loss.sum()
build/torch-cuda/quack/cute_dsl_utils.py CHANGED
@@ -59,12 +59,32 @@ torch2cute_dtype_map = {
59
  torch.float32: Float32,
60
  torch.int32: Int32,
61
  torch.int64: Int64,
 
 
 
 
 
62
  }
63
 
64
 
65
  @lru_cache
66
- def get_max_active_clusters(cluster_size):
67
- return cutlass.utils.HardwareInfo().get_max_active_clusters(cluster_size=cluster_size)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
 
70
  def _parse_arch_str(arch_str: str) -> Tuple[int, int]:
@@ -148,8 +168,46 @@ def _namedtuple_new_from_mlir_values(self, values):
148
  return self.__class__(*new_fields)
149
 
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  def mlir_namedtuple(cls):
152
- """Decorator that adds MLIR value reconstruction to a NamedTuple class.
 
 
 
 
 
153
 
154
  Usage::
155
 
@@ -158,6 +216,8 @@ def mlir_namedtuple(cls):
158
  tensor_arg: cute.Tensor
159
  const_arg: cutlass.Constexpr[int] = 0
160
  """
 
 
161
  cls.__new_from_mlir_values__ = _namedtuple_new_from_mlir_values
162
  return cls
163
 
 
59
  torch.float32: Float32,
60
  torch.int32: Int32,
61
  torch.int64: Int64,
62
+ torch.float8_e4m3fn: cutlass.Float8E4M3FN,
63
+ torch.float8_e5m2: cutlass.Float8E5M2,
64
+ torch.float8_e8m0fnu: cutlass.Float8E8M0FNU,
65
+ # Packed fp4: dlpack presents the logical (doubled) K extent to the DSL.
66
+ torch.float4_e2m1fn_x2: cutlass.Float4E2M1FN,
67
  }
68
 
69
 
70
  @lru_cache
71
+ def get_device_multiprocessor_count(device_id: int = 0) -> int:
72
+ return cutlass.utils.HardwareInfo(device_id).get_device_multiprocessor_count()
73
+
74
+
75
+ @lru_cache
76
+ def get_max_active_clusters(
77
+ cluster_size: int,
78
+ device_capacity: Tuple[int, int] | None = None,
79
+ device_id: int = 0,
80
+ ) -> int:
81
+ if device_capacity is None:
82
+ device_capacity = get_device_capacity()
83
+ if device_capacity[0] < 9:
84
+ if cluster_size != 1:
85
+ raise ValueError("SM8x kernels do not support CTA clusters; cluster_size must be 1")
86
+ return get_device_multiprocessor_count(device_id)
87
+ return cutlass.utils.HardwareInfo(device_id).get_max_active_clusters(cluster_size=cluster_size)
88
 
89
 
90
  def _parse_arch_str(arch_str: str) -> Tuple[int, int]:
 
168
  return self.__class__(*new_fields)
169
 
170
 
171
+ def _namedtuple_dynamic_fields(self):
172
+ """Yield fields that are represented by MLIR/runtime values.
173
+
174
+ Keep this in sync with ``_namedtuple_new_from_mlir_values``: fields that
175
+ are ``None`` or plain Python compile-time constants are preserved from the
176
+ compile-time template and do not consume MLIR block arguments.
177
+ """
178
+ for field_val in self:
179
+ if field_val is None or isinstance(field_val, StaticTypes):
180
+ continue
181
+ yield field_val
182
+
183
+
184
+ def _namedtuple_c_pointers(self):
185
+ """Generic ``JitArgument.__c_pointers__`` for ``@mlir_namedtuple`` classes."""
186
+ from cutlass.base_dsl.typing import get_c_pointers
187
+
188
+ ptrs = []
189
+ for field_val in _namedtuple_dynamic_fields(self):
190
+ ptrs.extend(get_c_pointers(field_val))
191
+ return ptrs
192
+
193
+
194
+ def _namedtuple_get_mlir_types(self):
195
+ """Generic ``JitArgument.__get_mlir_types__`` for ``@mlir_namedtuple`` classes."""
196
+ from cutlass.base_dsl.typing import get_mlir_types
197
+
198
+ types = []
199
+ for field_val in _namedtuple_dynamic_fields(self):
200
+ types.extend(get_mlir_types(field_val))
201
+ return types
202
+
203
+
204
  def mlir_namedtuple(cls):
205
+ """Decorator that makes a NamedTuple usable as a CuTe JIT argument.
206
+
207
+ Adds the full ``JitArgument`` protocol. This matters even when a concrete
208
+ instance has no dynamic fields: newer CuTe DSL versions warn for non-
209
+ constexpr compile-only arguments that flatten to zero MLIR values unless
210
+ they explicitly implement the protocol.
211
 
212
  Usage::
213
 
 
216
  tensor_arg: cute.Tensor
217
  const_arg: cutlass.Constexpr[int] = 0
218
  """
219
+ cls.__c_pointers__ = _namedtuple_c_pointers
220
+ cls.__get_mlir_types__ = _namedtuple_get_mlir_types
221
  cls.__new_from_mlir_values__ = _namedtuple_new_from_mlir_values
222
  return cls
223
 
build/torch-cuda/quack/dsl/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+
3
+ """CuTe DSL helpers and integration hooks."""
4
+
5
+ from . import cute_tensor_indexing # noqa: F401
6
+ from . import cute_tensor # noqa: F401
7
+ from .torch_library_op import cute_op
8
+
9
+ __all__ = ["cute_op"]
10
+
11
+
12
+ def __getattr__(name: str):
13
+ if name == "cute_op":
14
+ from .torch_library_op import cute_op
15
+
16
+ return cute_op
17
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
build/torch-cuda/quack/{cute_dsl_ptxas.py → dsl/cute_dsl_ptxas.py} RENAMED
@@ -3,35 +3,61 @@ System ptxas replacement for CUTLASS DSL.
3
 
4
  Usage::
5
 
6
- CUTE_DSL_KEEP_PTX=1 CUTE_DSL_PTXAS_PATH=/usr/local/cuda/bin/ptxas pytest tests/
7
 
8
  Environment variables:
9
  CUTE_DSL_PTXAS_PATH - Path to ptxas (e.g., /usr/local/cuda/bin/ptxas)
10
- CUTE_DSL_KEEP_PTX - Must be set to 1 before cutlass is imported
11
  CUTE_DSL_PTXAS_VERBOSE - Set to 1 for verbose output
12
  CUTE_DSL_DUMP_DIR - Directory for dumped PTX files (default: cwd)
13
- CUTE_DSL_KEEP_CUBIN - Set to 1 to save compiled cubin files
14
  """
15
 
 
16
  import os
17
- import sys
18
  import re
19
- import ctypes
20
  import subprocess
 
21
  from pathlib import Path
22
 
23
- import cutlass
24
-
25
 
26
  CUTE_DSL_PTXAS_PATH = os.environ.get("CUTE_DSL_PTXAS_PATH", None)
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  if CUTE_DSL_PTXAS_PATH:
29
- os.environ["CUTE_DSL_KEEP_PTX"] = "1"
 
 
 
 
30
  VERBOSE = os.environ.get("CUTE_DSL_PTXAS_VERBOSE", "0") == "1"
31
 
32
  _original_load_cuda_library = None
33
  _original_create_tvm_ffi_function = None
34
- _user_wanted_ptx = False # True if user originally set CUTE_DSL_KEEP_PTX=1
35
 
36
 
37
  def _log(msg: str):
@@ -201,18 +227,36 @@ def _patched_create_tvm_ffi_function(self):
201
  return _original_create_tvm_ffi_function(self)
202
 
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  def patch():
205
- """Install system ptxas hook. Call before importing cutlass."""
206
  global _original_load_cuda_library, _original_create_tvm_ffi_function, _user_wanted_ptx
207
 
208
  assert CUTE_DSL_PTXAS_PATH is not None
209
  if not os.path.isfile(CUTE_DSL_PTXAS_PATH) or not os.access(CUTE_DSL_PTXAS_PATH, os.X_OK):
210
  raise RuntimeError(f"ptxas not found: {CUTE_DSL_PTXAS_PATH}")
211
 
212
- _user_wanted_ptx = os.environ.get("CUTE_DSL_KEEP_PTX", "0") == "1"
213
- assert os.environ.get("CUTE_DSL_KEEP_PTX", "0") == "1", (
214
- "Require CUTE_DSL_KEEP_PTX=1 to use system's ptxas"
215
- )
216
 
217
  patched = False
218
  cuda_jit_function_cls = cutlass.cutlass_dsl.cuda_jit_executor.CudaDialectJitCompiledFunction
 
3
 
4
  Usage::
5
 
6
+ CUTE_DSL_PTXAS_PATH=/usr/local/cuda/bin/ptxas pytest tests/
7
 
8
  Environment variables:
9
  CUTE_DSL_PTXAS_PATH - Path to ptxas (e.g., /usr/local/cuda/bin/ptxas)
10
+ CUTE_DSL_KEEP=ptx - Optional; keep PTX files instead of deleting them
11
  CUTE_DSL_PTXAS_VERBOSE - Set to 1 for verbose output
12
  CUTE_DSL_DUMP_DIR - Directory for dumped PTX files (default: cwd)
13
+ CUTE_DSL_KEEP_CUBIN - Set to 1 to save system-ptxas cubin files
14
  """
15
 
16
+ import ctypes
17
  import os
 
18
  import re
 
19
  import subprocess
20
+ import sys
21
  from pathlib import Path
22
 
 
 
23
 
24
  CUTE_DSL_PTXAS_PATH = os.environ.get("CUTE_DSL_PTXAS_PATH", None)
25
 
26
+
27
+ def _keep_tokens() -> set[str]:
28
+ keep = os.environ.get("CUTE_DSL_KEEP", "")
29
+ return {token.strip().lower() for token in keep.split(",") if token.strip()}
30
+
31
+
32
+ def _env_requests_ptx() -> bool:
33
+ tokens = _keep_tokens()
34
+ return "all" in tokens or "ptx" in tokens or os.environ.get("CUTE_DSL_KEEP_PTX") == "1"
35
+
36
+
37
+ _USER_WANTED_PTX = _env_requests_ptx()
38
+
39
+
40
+ def _force_keep_ptx_env() -> None:
41
+ tokens = _keep_tokens()
42
+ if "all" not in tokens and "ptx" not in tokens:
43
+ tokens.add("ptx")
44
+ os.environ["CUTE_DSL_KEEP"] = ",".join(sorted(tokens))
45
+ # Keep the deprecated switch too so older CUTLASS DSL builds that do not
46
+ # understand CUTE_DSL_KEEP still dump PTX.
47
+ os.environ.setdefault("CUTE_DSL_KEEP_PTX", "1")
48
+
49
+
50
  if CUTE_DSL_PTXAS_PATH:
51
+ # Must happen before CuTeDSL's EnvironmentVarManager is instantiated.
52
+ _force_keep_ptx_env()
53
+
54
+ import cutlass # noqa: E402
55
+
56
  VERBOSE = os.environ.get("CUTE_DSL_PTXAS_VERBOSE", "0") == "1"
57
 
58
  _original_load_cuda_library = None
59
  _original_create_tvm_ffi_function = None
60
+ _user_wanted_ptx = False # True if user originally requested KEEP=ptx / KEEP_PTX=1
61
 
62
 
63
  def _log(msg: str):
 
227
  return _original_create_tvm_ffi_function(self)
228
 
229
 
230
+ def _force_live_keep_ptx() -> None:
231
+ """Update already-created CuTe DSL environment managers, if any.
232
+
233
+ quack imports this module before importing its kernels, but keeping this
234
+ fallback makes direct/late calls to ``patch()`` less fragile.
235
+ """
236
+ for cls_name in ("CuTeDSL", "CuteExperimentalDSL"):
237
+ dsl_cls = getattr(cutlass.cutlass_dsl, cls_name, None)
238
+ if dsl_cls is None:
239
+ continue
240
+ try:
241
+ envar = dsl_cls._get_dsl().envar
242
+ except Exception:
243
+ continue
244
+ envar.keep_ptx = True
245
+ if hasattr(envar, "keep_tokens"):
246
+ envar.keep_tokens = frozenset(set(envar.keep_tokens) | {"ptx"})
247
+
248
+
249
  def patch():
250
+ """Install system ptxas hook."""
251
  global _original_load_cuda_library, _original_create_tvm_ffi_function, _user_wanted_ptx
252
 
253
  assert CUTE_DSL_PTXAS_PATH is not None
254
  if not os.path.isfile(CUTE_DSL_PTXAS_PATH) or not os.access(CUTE_DSL_PTXAS_PATH, os.X_OK):
255
  raise RuntimeError(f"ptxas not found: {CUTE_DSL_PTXAS_PATH}")
256
 
257
+ _user_wanted_ptx = _USER_WANTED_PTX
258
+ _force_keep_ptx_env()
259
+ _force_live_keep_ptx()
 
260
 
261
  patched = False
262
  cuda_jit_function_cls = cutlass.cutlass_dsl.cuda_jit_executor.CudaDialectJitCompiledFunction
build/torch-cuda/quack/dsl/cute_tensor.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026, Tri Dao.
2
+ """Small CuTe tensor convenience helpers.
3
+
4
+ Importing this module intentionally mutates CuTe's tensor class process-wide so
5
+ fragments can be written in a more PyTorch-like style::
6
+
7
+ rmem_f32 = rmem_f16.to(Float32)
8
+ rmem_copy = rmem_view.clone()
9
+ rmem_contig = rmem_view.contiguous()
10
+
11
+ ``tensor.to(dtype)`` is exactly the explicit CuTe sequence::
12
+
13
+ dst = cute.make_rmem_tensor_like(src, dtype)
14
+ dst.store(src.load().to(dtype))
15
+
16
+ ``tensor.to(dtype, force_materialize=True)`` uses the same value conversion, then inserts an
17
+ opaque no-op SSA boundary on packed f16/bf16 lanes. This is useful when downstream codegen
18
+ would otherwise rematerialize a vector truncation for multiple consumers.
19
+
20
+ ``tensor.clone()`` materializes into ``cute.make_rmem_tensor_like(src)``.
21
+ ``tensor.contiguous()`` mirrors ``quack.copy_utils.contiguous``.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Any
27
+
28
+ import cutlass
29
+ import cutlass.cute as cute
30
+ import cutlass.cute.tensor as _cute_tensor
31
+ from cutlass.cutlass_dsl import T, dsl_user_op
32
+ from cutlass._mlir.dialects import llvm
33
+
34
+
35
+ _ORIGINAL_TO_ATTR = "_quack_original_to"
36
+ _PATCHED_TO_ATTR = "_quack_rmem_tensor_to"
37
+ _ORIGINAL_CLONE_ATTR = "_quack_original_clone"
38
+ _PATCHED_CLONE_ATTR = "_quack_tensor_clone"
39
+ _ORIGINAL_CONTIGUOUS_ATTR = "_quack_original_contiguous"
40
+ _PATCHED_CONTIGUOUS_ATTR = "_quack_tensor_contiguous"
41
+
42
+
43
+ @dsl_user_op
44
+ def _black_box_b32(x: cutlass.Int32, *, loc: Any = None, ip: Any = None) -> cutlass.Int32:
45
+ """Opaque identity for packed registers.
46
+
47
+ The empty asm emits no PTX instruction. The tied input constraint (``0``) makes the
48
+ output use the same register class/value as the input, while still creating an SSA boundary.
49
+ """
50
+
51
+ return cutlass.Int32(
52
+ llvm.inline_asm(
53
+ T.i32(),
54
+ [cutlass.Int32(x).ir_value(loc=loc, ip=ip)],
55
+ "",
56
+ "=r,0",
57
+ has_side_effects=False,
58
+ is_align_stack=False,
59
+ asm_dialect=llvm.AsmDialect.AD_ATT,
60
+ )
61
+ )
62
+
63
+
64
+ @cute.jit
65
+ def _to_f16_materialized(src: Any, dtype: Any) -> Any:
66
+ assert src.element_type is cutlass.Float32, "src must be Float32"
67
+ assert dtype in (cutlass.BFloat16, cutlass.Float16), "dtype must be BFloat16 or Float16"
68
+ assert cute.size(src.shape) % 2 == 0, "src must have an even number of elements"
69
+
70
+ # Why this exists: plain Tensor.to lowers f32 -> f16/bf16 as a vector truncation.
71
+ # In some kernels the converted fragment has multiple consumers (e.g. STSM and RS WGMMA
72
+ # in SM90 FlashAttention backward). Leaving the truncation as a high-level vector value
73
+ # lets later codegen rematerialize it for each consumer, producing extra packed converts
74
+ # and a worse WGMMA schedule. Storing the .to result, viewing the f16/bf16 pairs as i32,
75
+ # then passing each packed lane through an empty tied-operand asm creates an opaque SSA
76
+ # boundary. The asm emits no PTX instruction, but it forces one materialized packed value
77
+ # that downstream users share.
78
+ tmp = cute.make_rmem_tensor_like(src, dtype)
79
+ tmp.store(src.load().to(dtype))
80
+ dst = cute.make_rmem_tensor_like(src, dtype)
81
+ tmp_i32 = cute.recast_tensor(tmp, cutlass.Int32)
82
+ dst_i32 = cute.recast_tensor(dst, cutlass.Int32)
83
+ assert cute.size(dst_i32.shape) * 2 == cute.size(src.shape)
84
+ for i in cutlass.range(cute.size(dst_i32), unroll_full=True):
85
+ dst_i32[i] = _black_box_b32(tmp_i32[i])
86
+ return dst
87
+
88
+
89
+ def _make_to() -> Any:
90
+ @dsl_user_op
91
+ def _to(
92
+ self: Any,
93
+ dtype: Any,
94
+ *,
95
+ force_materialize: bool = False,
96
+ loc: Any = None,
97
+ ip: Any = None,
98
+ ) -> Any:
99
+ if self.memspace != cute.AddressSpace.rmem:
100
+ raise ValueError("Tensor.to(dtype) is only supported for rmem tensors")
101
+
102
+ if force_materialize:
103
+ return _to_f16_materialized(self, dtype)
104
+
105
+ dst = cute.make_rmem_tensor_like(self, dtype, loc=loc, ip=ip)
106
+ dst.store(self.load(loc=loc, ip=ip).to(dtype, loc=loc, ip=ip), loc=loc, ip=ip)
107
+ return dst
108
+
109
+ return _to
110
+
111
+
112
+ def _make_clone() -> Any:
113
+ @dsl_user_op
114
+ def _clone(self: Any, *, loc: Any = None, ip: Any = None) -> Any:
115
+ dst = cute.make_rmem_tensor_like(self, loc=loc, ip=ip)
116
+ cute.autovec_copy(self, dst, loc=loc, ip=ip)
117
+ return dst
118
+
119
+ return _clone
120
+
121
+
122
+ def _make_contiguous() -> Any:
123
+ @dsl_user_op
124
+ def _contiguous(self: Any, *, loc: Any = None, ip: Any = None) -> Any:
125
+ dst = cute.make_rmem_tensor(self.shape, self.element_type, loc=loc, ip=ip)
126
+ cute.autovec_copy(self, dst, loc=loc, ip=ip)
127
+ return dst
128
+
129
+ return _contiguous
130
+
131
+
132
+ def patch_cute_tensor() -> None:
133
+ """Monkey patch CuTe tensors with QuACK convenience methods.
134
+
135
+ The patch is idempotent. CuTe's immutable ``TensorSSA.to`` already handles
136
+ value conversion; this installs the analogous materializing conversion on
137
+ mutable register-backed ``_Tensor`` fragments, plus a ``contiguous`` method
138
+ equivalent to :func:`quack.copy_utils.contiguous`, and a ``clone`` method
139
+ that copies into a matching compact rmem tensor.
140
+ """
141
+ tensor_cls = _cute_tensor._Tensor
142
+ if _PATCHED_TO_ATTR not in tensor_cls.__dict__:
143
+ original_to = getattr(tensor_cls, "to", None)
144
+ if original_to is not None:
145
+ setattr(tensor_cls, _ORIGINAL_TO_ATTR, original_to)
146
+ tensor_cls.to = _make_to() # type: ignore[method-assign]
147
+ setattr(tensor_cls, _PATCHED_TO_ATTR, True)
148
+
149
+ if _PATCHED_CLONE_ATTR not in tensor_cls.__dict__:
150
+ original_clone = getattr(tensor_cls, "clone", None)
151
+ if original_clone is not None:
152
+ setattr(tensor_cls, _ORIGINAL_CLONE_ATTR, original_clone)
153
+ tensor_cls.clone = _make_clone() # type: ignore[method-assign]
154
+ setattr(tensor_cls, _PATCHED_CLONE_ATTR, True)
155
+
156
+ if _PATCHED_CONTIGUOUS_ATTR not in tensor_cls.__dict__:
157
+ original_contiguous = getattr(tensor_cls, "contiguous", None)
158
+ if original_contiguous is not None:
159
+ setattr(tensor_cls, _ORIGINAL_CONTIGUOUS_ATTR, original_contiguous)
160
+ tensor_cls.contiguous = _make_contiguous() # type: ignore[method-assign]
161
+ setattr(tensor_cls, _PATCHED_CONTIGUOUS_ATTR, True)
162
+
163
+
164
+ patch_cute_tensor()
165
+
166
+
167
+ __all__ = ["patch_cute_tensor"]
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/dsl/smem_struct.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026, Tri Dao.
2
+ # SPDX-License-Identifier: BSD-3-Clause
3
+
4
+ """Per-field smem-partition annotations for SharedStorage declarations.
5
+
6
+ A `cute.struct` lowers to ONE smem_alloca op carrying ONE `smem.partition_id`
7
+ attribute, so a single struct cannot mix RESERVED and USER fields at the IR
8
+ level. `Reserved[...]` + `@partitioned_struct` provide the single-declaration
9
+ sugar instead: at trace time the decorator splits the annotated class into two
10
+ plain cute.structs — fields wrapped in `Reserved[...]` go to a struct
11
+ allocated with `partition=SmemPartition.RESERVED` (low addresses, packing with
12
+ the pipeline mbarriers and the TMEM holding buf under the 1KB that is
13
+ otherwise alignment pad ahead of the 1024-aligned USER buffers), everything
14
+ else stays USER — and `.allocate(smem)` returns one namespace exposing all
15
+ fields uniformly.
16
+
17
+ @partitioned_struct
18
+ class SharedStorage:
19
+ sdt: Reserved[spec.smem_struct(128)] # RESERVED partition
20
+ sX: X.smem_struct(1024) # USER partition
21
+ ...
22
+
23
+ storage = SharedStorage.allocate(smem)
24
+ storage.sdt, storage.sX # fields, regardless of partition
25
+
26
+ This is trace-time-only machinery (no monkey-patching of the DSL).
27
+ """
28
+
29
+ from types import SimpleNamespace
30
+
31
+ import cutlass.cute as cute
32
+ from cutlass.utils import SmemPartition
33
+
34
+
35
+ class Reserved:
36
+ """Annotation marker: allocate this field in the RESERVED smem partition."""
37
+
38
+ def __init__(self, inner):
39
+ self.inner = inner
40
+
41
+ def __class_getitem__(cls, inner):
42
+ return cls(inner)
43
+
44
+
45
+ class PartitionedStruct:
46
+ """A SharedStorage declaration split by partition. Not a cute.struct itself:
47
+ holds one cute.struct per partition and allocates/merges them."""
48
+
49
+ def __init__(self, cls):
50
+ annotations = dict(cls.__annotations__)
51
+ reserved_ann = {k: v.inner for k, v in annotations.items() if isinstance(v, Reserved)}
52
+ user_ann = {k: v for k, v in annotations.items() if not isinstance(v, Reserved)}
53
+ self._user_struct = (
54
+ cute.struct(type(cls.__name__, (), {"__annotations__": user_ann})) if user_ann else None
55
+ )
56
+ self._reserved_struct = (
57
+ cute.struct(type(cls.__name__ + "Reserved", (), {"__annotations__": reserved_ann}))
58
+ if reserved_ann
59
+ else None
60
+ )
61
+ self._user_fields = list(user_ann)
62
+ self._reserved_fields = list(reserved_ann)
63
+
64
+ def size_in_bytes(self) -> int:
65
+ """USER-partition footprint (what counts against smem_capacity - 1KB)."""
66
+ return self._user_struct.size_in_bytes() if self._user_struct is not None else 0
67
+
68
+ def reserved_size_in_bytes(self) -> int:
69
+ """RESERVED-partition footprint of the declared fields (the pipeline
70
+ mbarriers / TMEM holding buf allocate there separately)."""
71
+ return self._reserved_struct.size_in_bytes() if self._reserved_struct is not None else 0
72
+
73
+ def allocate(self, smem) -> SimpleNamespace:
74
+ """Allocate both partitions (RESERVED first, at the partition base) and
75
+ return a namespace exposing every declared field. A partition whose
76
+ struct is empty for this config (every field zero-sized) is skipped —
77
+ smem_alloca rejects 0-byte layouts — and its fields come back as None;
78
+ callers only touch such fields under the same has_* guards that made
79
+ them zero-sized."""
80
+ fields = {}
81
+ if self._reserved_struct is not None:
82
+ if self._reserved_struct.size_in_bytes() > 0:
83
+ inst = smem.allocate(self._reserved_struct, partition=SmemPartition.RESERVED)
84
+ for name in self._reserved_fields:
85
+ fields[name] = getattr(inst, name)
86
+ else:
87
+ fields.update(dict.fromkeys(self._reserved_fields))
88
+ if self._user_struct is not None:
89
+ if self._user_struct.size_in_bytes() > 0:
90
+ inst = smem.allocate(self._user_struct)
91
+ for name in self._user_fields:
92
+ fields[name] = getattr(inst, name)
93
+ else:
94
+ fields.update(dict.fromkeys(self._user_fields))
95
+ return SimpleNamespace(**fields)
96
+
97
+
98
+ def partitioned_struct(cls) -> PartitionedStruct:
99
+ return PartitionedStruct(cls)
build/torch-cuda/quack/dsl/torch_library_op.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+ """``cute_op``: ``torch.library.custom_op`` for CuTe DSL kernels.
3
+
4
+ Same trick as ``torch.library.triton_op`` (register the impl as the fake/meta
5
+ kernel too), specialized for our setup: the fake is a pure no-op. Our ops
6
+ only mutate their inputs, so Dynamo / AOT autograd need no shape effect from
7
+ the fake, and kernel compilation is owned entirely by ``jit_cache`` (plus
8
+ the async compile pool) at real execution time.
9
+
10
+ This removes the need for hand-written ``_*_fake`` twins on each op.
11
+
12
+ Note: we deliberately do NOT gate on ``torch.compiler.is_compiling()`` —
13
+ that flag's underlying ``_is_compiling_flag`` is only set during
14
+ ``torch.export``, never during ``torch.compile``. Dynamo's
15
+ ``_get_fake_value_impl`` would otherwise run the body and surface
16
+ any ``_compile_*`` ``ValueError`` as a ``TorchRuntimeError`` graph break.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any, Callable, Iterable, Optional, Union
22
+
23
+ import torch
24
+
25
+ __all__ = ["cute_op"]
26
+
27
+
28
+ def cute_op(
29
+ name: str,
30
+ *,
31
+ mutates_args: Union[str, Iterable[str]],
32
+ schema: Optional[str] = None,
33
+ device_types: Optional[Union[str, Iterable[str]]] = None,
34
+ ) -> Callable:
35
+ """Like ``torch.library.triton_op``, but for CuTe DSL kernels.
36
+
37
+ Args:
38
+ name: ``"namespace::op_name"``.
39
+ mutates_args: Names of mutated tensor args.
40
+ schema: Optional explicit schema. Required when mutating an
41
+ ``Optional[Tensor]`` arg (PyTorch can't infer those).
42
+ device_types: Optional device-type restriction.
43
+ """
44
+
45
+ def dec(fn: Callable) -> Any:
46
+ kwargs: dict[str, Any] = {"mutates_args": mutates_args}
47
+ if schema is not None:
48
+ kwargs["schema"] = schema
49
+ if device_types is not None:
50
+ kwargs["device_types"] = device_types
51
+ op = torch.library.custom_op(name, fn, **kwargs)
52
+
53
+ @op.register_fake
54
+ def _fake(*args, **kw):
55
+ # Pure no-op: our ops only mutate their input tensors, so under
56
+ # torch.compile / AOT autograd tracing there is no fake output to
57
+ # produce, and running the body would pay compile latency at
58
+ # dynamo trace time (or crash for shape/dtype combos the kernel
59
+ # intentionally rejects). Kernel compilation is handled by
60
+ # jit_cache + the async compile pool at real execution time.
61
+ return
62
+
63
+ return op
64
+
65
+ return dec
build/torch-cuda/quack/epi_composable.py CHANGED
@@ -1,35 +1,45 @@
1
  # Copyright (c) 2025, Tri Dao.
2
  """ComposableEpiMixin: composes EpiOps into epilogue hook methods.
3
 
4
- Subclasses declare _epi_ops as a tuple of EpiOp instances. The mixin auto-generates
5
- epi_smem_bytes_per_stage, epi_get_smem_struct, epi_get_smem_tensors, epi_begin,
6
- epi_begin_loop, epi_end, and EpilogueParams by querying each op.
 
7
 
8
- epi_begin and epi_begin_loop return dicts keyed by op name, so epi_visit_subtile
9
- can access values by name (e.g. epi_loop_tensors["alpha"]).
 
 
 
 
10
 
11
- EpilogueParams is auto-generated from _epi_ops (via param_fields()) plus any
12
- _extra_param_fields declared on the subclass. Subclasses still define
13
- EpilogueArguments and epi_to_underlying_arguments manually.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  """
15
 
16
  from dataclasses import make_dataclass, MISSING
17
 
18
  import cutlass.cute as cute
19
- from cutlass import const_expr
20
-
21
- from .epi_ops import EpiContext, Scalar
22
-
23
 
24
- def _compute_smem_map(ops):
25
- """Pre-compute name smem tensor index for each non-Scalar op."""
26
- smem_map = {}
27
- idx = 0
28
- for op in ops:
29
- if not isinstance(op, Scalar):
30
- smem_map[op.name] = idx
31
- idx += 1
32
- return smem_map
33
 
34
 
35
  def _make_epi_params(epi_ops, extra_fields, bases):
@@ -52,15 +62,11 @@ class ComposableEpiMixin:
52
 
53
  _epi_ops = ()
54
  _extra_param_fields = () # [(name, type, default), ...] for non-op params (e.g. act_fn)
55
- _epi_param_bases = () # Base classes for EpilogueParams (e.g. (ParamsBase,))
56
- _epi_smem_map = {}
57
- _epi_has_async_ops = False
58
 
59
  def __init_subclass__(cls, **kwargs):
60
  super().__init_subclass__(**kwargs)
61
  if cls._epi_ops:
62
- cls._epi_smem_map = _compute_smem_map(cls._epi_ops)
63
- cls._epi_has_async_ops = any(op.needs_async_fence() for op in cls._epi_ops)
64
  # Auto-generate EpilogueParams if not explicitly defined on this class
65
  if "EpilogueParams" not in cls.__dict__:
66
  cls.EpilogueParams = _make_epi_params(
@@ -69,39 +75,89 @@ class ComposableEpiMixin:
69
 
70
  # --- Host-side: args → params ---
71
 
 
 
 
 
 
 
 
 
 
 
72
  def _epi_ops_to_params_dict(self, args):
73
- """Merge each op's to_params into a single dict. Subclasses call this,
74
- add custom fields, then construct self.EpilogueParams(**d)."""
 
 
 
 
 
75
  d = {}
76
  for op in self._epi_ops:
77
  d.update(op.to_params(self, args))
78
  return d
79
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  # --- Host-side: smem allocation (queried from ops) ---
81
 
82
  @classmethod
83
- def epi_smem_bytes_per_stage(cls, args, cta_tile_shape_mnk, epi_tile):
84
- return sum(
85
- op.smem_bytes(getattr(args, op.name, None), cta_tile_shape_mnk, epi_tile)
86
- for op in cls._epi_ops
87
- )
 
 
 
 
 
 
 
 
88
 
89
  def epi_get_smem_struct(self, params):
90
- fields = {}
91
  for op in self._epi_ops:
92
  result = op.smem_struct_field(self, params)
93
  if result is not None:
94
- name, ftype = result
95
- fields[name] = ftype
96
- EpiSharedStorage = type("EpiSharedStorage", (), {"__annotations__": fields})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  return cute.struct(EpiSharedStorage)
98
 
99
  def epi_get_smem_tensors(self, params, storage):
100
- return tuple(
101
- op.get_smem_tensor(self, params, storage.epi)
102
  for op in self._epi_ops
103
  if not isinstance(op, Scalar)
104
- )
105
 
106
  def epi_get_tma_atoms(self, params, *, loc=None, ip=None):
107
  atoms = []
@@ -123,6 +179,7 @@ class ComposableEpiMixin:
123
  varlen_manager,
124
  epilogue_barrier,
125
  tidx,
 
126
  ):
127
  ctx = EpiContext(
128
  self,
@@ -133,27 +190,24 @@ class ComposableEpiMixin:
133
  varlen_manager,
134
  epilogue_barrier,
135
  tidx,
 
136
  )
137
- smem_map = self._epi_smem_map
138
  results = {
139
  op.name: op.begin(
140
  self,
141
- getattr(params, op.name, None),
142
- epi_smem_tensors[smem_map[op.name]] if op.name in smem_map else None,
143
  ctx,
144
  )
145
  for op in self._epi_ops
146
  }
147
- if const_expr(self._epi_has_async_ops):
148
- has_async_data = any(
149
- getattr(params, op.name, None) is not None
150
- for op in self._epi_ops
151
- if op.needs_async_fence()
152
- )
153
- if const_expr(has_async_data):
154
- cute.arch.cp_async_commit_group()
155
- cute.arch.cp_async_wait_group(0)
156
- epilogue_barrier.arrive_and_wait()
157
  return results
158
 
159
  def epi_begin_loop(self, params, epi_tensors, epi_coord):
@@ -161,6 +215,59 @@ class ComposableEpiMixin:
161
  op.name: op.begin_loop(self, epi_tensors[op.name], epi_coord) for op in self._epi_ops
162
  }
163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  @cute.jit
165
  def epi_end(
166
  self,
@@ -176,7 +283,7 @@ class ComposableEpiMixin:
176
  for op in self._epi_ops:
177
  op.end(
178
  self,
179
- getattr(params, op.name, None),
180
  epi_tensors[op.name],
181
  epi_tile,
182
  tiled_copy_t2r,
 
1
  # Copyright (c) 2025, Tri Dao.
2
  """ComposableEpiMixin: composes EpiOps into epilogue hook methods.
3
 
4
+ Subclasses declare _epi_ops as a class-level tuple of EpiOp instances the
5
+ static *schema* for the epilogue. The mixin auto-generates epi_smem_bytes,
6
+ epi_get_smem_struct, epi_get_smem_tensors, epi_begin, epi_begin_loop,
7
+ epi_end_loop, epi_end, and EpilogueParams by querying each op.
8
 
9
+ Host-side, `_epi_ops_to_params_dict` (called from each subclass's
10
+ `epi_to_underlying_arguments`) filters `_epi_ops` automatically: it shadows
11
+ the class-level tuple with an instance-level tuple containing only the ops
12
+ whose argument tensor is non-None. All later iteration (host- and
13
+ device-side) walks the filtered tuple, so each EpiOp's hook methods can
14
+ assume their `param`/`arg_tensor` is non-None.
15
 
16
+ The two host-side hooks that run *before* `epi_to_underlying_arguments`
17
+ (`resolve_epi_m_major` and the classmethod `epi_smem_bytes`) filter inline
18
+ from `args`, preserving the same non-None invariant for `op.epi_m_major_score`
19
+ and `op.smem_bytes`. They have to run first because `epi_to_underlying_arguments`
20
+ itself depends on static attributes set up before it (e.g. `gemm.epi_tile`,
21
+ `gemm.epi_c_stage`), and those attributes are themselves derived from
22
+ `epi_m_major` and the epi smem budget — a chicken-and-egg ordering we resolve by
23
+ letting these two hooks see the raw `args` and filter inline.
24
+
25
+ epi_get_smem_tensors, epi_begin, and epi_begin_loop all return dicts keyed by
26
+ op name, so consumers access values by name (e.g. epi_smem_tensors["mAuxOut"],
27
+ epi_loop_tensors["alpha"]). Because inactive ops are filtered out, consumers
28
+ must use `.get(name)` (returns None for inactive ops) rather than `[name]`.
29
+
30
+ EpilogueParams is auto-generated from the full class-level _epi_ops (via
31
+ param_fields()) plus any _extra_param_fields declared on the subclass.
32
+ Subclasses still define EpilogueArguments and epi_to_underlying_arguments
33
+ manually.
34
  """
35
 
36
  from dataclasses import make_dataclass, MISSING
37
 
38
  import cutlass.cute as cute
39
+ from cutlass import Int32, const_expr
 
 
 
40
 
41
+ from .cute_dsl_utils import ParamsBase
42
+ from .epi_ops import EpiContext, EpiSmemBytes, Scalar
 
 
 
 
 
 
 
43
 
44
 
45
  def _make_epi_params(epi_ops, extra_fields, bases):
 
62
 
63
  _epi_ops = ()
64
  _extra_param_fields = () # [(name, type, default), ...] for non-op params (e.g. act_fn)
65
+ _epi_param_bases = (ParamsBase,) # Base classes for the auto-generated EpilogueParams
 
 
66
 
67
  def __init_subclass__(cls, **kwargs):
68
  super().__init_subclass__(**kwargs)
69
  if cls._epi_ops:
 
 
70
  # Auto-generate EpilogueParams if not explicitly defined on this class
71
  if "EpilogueParams" not in cls.__dict__:
72
  cls.EpilogueParams = _make_epi_params(
 
75
 
76
  # --- Host-side: args → params ---
77
 
78
+ def _filter_epi_ops(self, args):
79
+ """Shadow `_epi_ops` with an instance-level tuple of only the ops whose
80
+ arg is non-None. Called automatically by `_epi_ops_to_params_dict`, so
81
+ subclass `epi_to_underlying_arguments` methods don't need to invoke it
82
+ directly. After this runs, op hook methods can assume their
83
+ `param`/`arg_tensor` is non-None."""
84
+ self._epi_ops = tuple(
85
+ op for op in type(self)._epi_ops if getattr(args, op.name, None) is not None
86
+ )
87
+
88
  def _epi_ops_to_params_dict(self, args):
89
+ """Filter `_epi_ops` to active ops, then merge each op's to_params into
90
+ a single dict. Subclasses call this from epi_to_underlying_arguments,
91
+ add custom fields, then construct self.EpilogueParams(**d). Filtering
92
+ here means every later iteration of self._epi_ops (host- and
93
+ device-side) walks only active ops, and each op hook can assume its
94
+ arg is non-None."""
95
+ self._filter_epi_ops(args)
96
  d = {}
97
  for op in self._epi_ops:
98
  d.update(op.to_params(self, args))
99
  return d
100
 
101
+ def resolve_epi_m_major(self, args):
102
+ # Runs inside _setup_attributes, before epi_to_underlying_arguments,
103
+ # because epi_m_major drives epi_tile / smem layout choices that
104
+ # epi_to_underlying_arguments later consumes. self._epi_ops is still
105
+ # the class-level schema at this point, so we filter inline from args
106
+ # to keep op.epi_m_major_score's non-None invariant.
107
+ score = 0
108
+ for op in type(self)._epi_ops:
109
+ arg = getattr(args, op.name, None)
110
+ if arg is not None:
111
+ score += op.epi_m_major_score(arg, self)
112
+ return score >= 0
113
+
114
  # --- Host-side: smem allocation (queried from ops) ---
115
 
116
  @classmethod
117
+ def epi_smem_bytes(cls, args, cta_tile_shape_mnk, epi_tile, warp_shape_mnk=None):
118
+ # Runs inside _compute_stages, before epi_to_underlying_arguments,
119
+ # because the AB/epi stage counts (and therefore epi_c_stage) depend
120
+ # on the epi smem budget that this returns; epi_to_underlying_arguments
121
+ # then consumes epi_c_stage to build TileLoad's staged smem layout.
122
+ # Stays a classmethod because _compute_stages is a classmethod and may
123
+ # be invoked without an instance, so we filter inline from args.
124
+ result = EpiSmemBytes()
125
+ for op in cls._epi_ops:
126
+ arg = getattr(args, op.name, None)
127
+ if arg is not None:
128
+ result += op.smem_bytes(arg, cta_tile_shape_mnk, epi_tile, warp_shape_mnk)
129
+ return result
130
 
131
  def epi_get_smem_struct(self, params):
132
+ fields = []
133
  for op in self._epi_ops:
134
  result = op.smem_struct_field(self, params)
135
  if result is not None:
136
+ fields.append(result)
137
+
138
+ # cute.struct rejects empty annotations. When every active op contributes
139
+ # no smem (e.g. only Scalar ops, or no active ops at all), return a
140
+ # zero-byte placeholder matching gemm_base's default epi struct.
141
+ if not fields:
142
+ return cute.struct.MemRange[Int32, 0]
143
+
144
+ # Sort smallest-to-largest so smaller fields pack ahead of larger
145
+ # higher-aligned fields, reducing smem wasted to alignment padding.
146
+ def _field_bytes(name_ftype):
147
+ wrapper = type("_F", (), {"__annotations__": {name_ftype[0]: name_ftype[1]}})
148
+ return cute.struct(wrapper).size_in_bytes()
149
+
150
+ fields.sort(key=_field_bytes)
151
+ annotations = {name: ftype for name, ftype in fields}
152
+ EpiSharedStorage = type("EpiSharedStorage", (), {"__annotations__": annotations})
153
  return cute.struct(EpiSharedStorage)
154
 
155
  def epi_get_smem_tensors(self, params, storage):
156
+ return {
157
+ op.name: op.get_smem_tensor(self, params, storage.epi)
158
  for op in self._epi_ops
159
  if not isinstance(op, Scalar)
160
+ }
161
 
162
  def epi_get_tma_atoms(self, params, *, loc=None, ip=None):
163
  atoms = []
 
179
  varlen_manager,
180
  epilogue_barrier,
181
  tidx,
182
+ tRS_rD_layout=None,
183
  ):
184
  ctx = EpiContext(
185
  self,
 
190
  varlen_manager,
191
  epilogue_barrier,
192
  tidx,
193
+ tRS_rD_layout,
194
  )
 
195
  results = {
196
  op.name: op.begin(
197
  self,
198
+ getattr(params, op.name),
199
+ epi_smem_tensors.get(op.name),
200
  ctx,
201
  )
202
  for op in self._epi_ops
203
  }
204
+ # self._epi_ops is filtered to active ops, so any op needing a fence
205
+ # has a non-None tensor; no inner None check required.
206
+ has_async_data = any(op.needs_async_fence() for op in self._epi_ops)
207
+ if const_expr(has_async_data):
208
+ cute.arch.cp_async_commit_group()
209
+ cute.arch.cp_async_wait_group(0)
210
+ epilogue_barrier.arrive_and_wait()
 
 
 
211
  return results
212
 
213
  def epi_begin_loop(self, params, epi_tensors, epi_coord):
 
215
  op.name: op.begin_loop(self, epi_tensors[op.name], epi_coord) for op in self._epi_ops
216
  }
217
 
218
+ def epi_tile_load_g2s_copy_fns(
219
+ self,
220
+ params,
221
+ epi_smem_tensors,
222
+ tile_coord_mnkl,
223
+ varlen_manager,
224
+ epi_pipeline,
225
+ ):
226
+ return tuple(
227
+ op.load_g2s_copy_fn(
228
+ self,
229
+ params,
230
+ epi_smem_tensors.get(op.name),
231
+ tile_coord_mnkl,
232
+ varlen_manager,
233
+ epi_pipeline,
234
+ )
235
+ for op in self._epi_ops
236
+ if op.is_tile_load()
237
+ )
238
+
239
+ @cute.jit
240
+ def epi_tile_load_s2r(self, params, epi_tensors, stage_idx):
241
+ for op in self._epi_ops:
242
+ op.load_s2r(self, getattr(params, op.name), epi_tensors[op.name], stage_idx)
243
+
244
+ @cute.jit
245
+ def epi_end_loop(
246
+ self,
247
+ params,
248
+ epi_tensors,
249
+ epi_coord,
250
+ epi_tile,
251
+ tiled_copy_t2r,
252
+ tiled_copy_r2s,
253
+ tile_coord_mnkl,
254
+ varlen_manager,
255
+ tidx,
256
+ ):
257
+ for op in self._epi_ops:
258
+ op.end_loop(
259
+ self,
260
+ getattr(params, op.name),
261
+ epi_tensors[op.name],
262
+ epi_coord,
263
+ epi_tile,
264
+ tiled_copy_t2r,
265
+ tiled_copy_r2s,
266
+ tile_coord_mnkl,
267
+ varlen_manager,
268
+ tidx,
269
+ )
270
+
271
  @cute.jit
272
  def epi_end(
273
  self,
 
283
  for op in self._epi_ops:
284
  op.end(
285
  self,
286
+ getattr(params, op.name),
287
  epi_tensors[op.name],
288
  epi_tile,
289
  tiled_copy_t2r,
build/torch-cuda/quack/epi_ops.py CHANGED
@@ -5,14 +5,18 @@ Each EpiOp encapsulates a single tensor kind's behavior across the epilogue life
5
  smem allocation, begin (one-time per-tile setup), begin_loop (per-subtile extraction),
6
  end (cleanup).
7
 
8
- The ops are composed via ComposableEpiMixin which iterates over a static _epi_ops tuple
9
- to generate epi_smem_bytes_per_stage, epi_get_smem_struct, epi_get_smem_tensors,
10
- epi_begin, and epi_begin_loop automatically.
 
 
 
11
  """
12
 
13
  import math
14
  import operator
15
  from functools import partial
 
16
 
17
  import cutlass
18
  import cutlass.cute as cute
@@ -26,7 +30,12 @@ from . import layout_utils as layout_utils
26
 
27
 
28
  class EpiContext:
29
- """Shared context passed to EpiOp.begin methods. Bundles common arguments."""
 
 
 
 
 
30
 
31
  __slots__ = (
32
  "epi_tile",
@@ -36,6 +45,7 @@ class EpiContext:
36
  "varlen_manager",
37
  "epilogue_barrier",
38
  "tidx",
 
39
  "partition_for_epilogue_fn",
40
  "num_epi_threads",
41
  "batch_idx",
@@ -53,6 +63,7 @@ class EpiContext:
53
  varlen_manager,
54
  epilogue_barrier,
55
  tidx,
 
56
  ):
57
  self.epi_tile = epi_tile
58
  self.tiled_copy_t2r = tiled_copy_t2r
@@ -61,6 +72,7 @@ class EpiContext:
61
  self.varlen_manager = varlen_manager
62
  self.epilogue_barrier = epilogue_barrier
63
  self.tidx = tidx
 
64
  self.tile_M = gemm.cta_tile_shape_mnk[0]
65
  self.tile_N = gemm.cta_tile_shape_mnk[1]
66
  self.batch_idx = tile_coord_mnkl[3]
@@ -120,6 +132,31 @@ def _get_lane_warp_layouts(tiled_copy, reference_src=True):
120
  return lane_layout_MN, warp_layout_MN
121
 
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  class EpiOp:
124
  """Base class for composable epilogue operations."""
125
 
@@ -137,11 +174,15 @@ class EpiOp:
137
  Returns dict of {param_name: value}. Like EVT's to_underlying_arguments."""
138
  return {}
139
 
140
- # --- Host-side: smem allocation ---
141
- def smem_bytes(self, arg_tensor, cta_tile_shape_mnk, epi_tile):
142
- """Bytes of smem needed per stage. arg_tensor is the EpilogueArguments field."""
143
  return 0
144
 
 
 
 
 
 
145
  def smem_struct_field(self, gemm, params):
146
  """Return (field_name, field_type) for @cute.struct, or None if no smem needed.
147
  params is the full EpilogueParams object."""
@@ -156,6 +197,22 @@ class EpiOp:
156
  """Return list of TMA atoms for this op."""
157
  return []
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  # --- Device-side: kernel execution ---
160
  @cute.jit
161
  def begin(self, gemm, param, smem_tensor, ctx):
@@ -166,6 +223,27 @@ class EpiOp:
166
  """Per-subtile extraction. Returns value for epi_visit_subtile."""
167
  return state
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  def needs_async_fence(self):
170
  """Whether this op issues async copies that need a fence."""
171
  return False
@@ -201,14 +279,9 @@ class Scalar(EpiOp):
201
 
202
  @cute.jit
203
  def begin(self, gemm, param, smem_tensor, ctx):
204
- result = None
205
- if const_expr(param is not None):
206
- result = (
207
- utils.load_scalar_or_pointer(param, dtype=self.dtype)
208
- if const_expr(self.dtype is not None)
209
- else utils.load_scalar_or_pointer(param)
210
- )
211
- return result
212
 
213
 
214
  class VecLoad(EpiOp):
@@ -239,23 +312,20 @@ class VecLoad(EpiOp):
239
  def _coord_idx(self):
240
  return 1 if self.dim == 1 else 0
241
 
242
- def smem_bytes(self, arg_tensor, cta_tile_shape_mnk, epi_tile):
243
- if arg_tensor is None:
244
- return 0
245
- return self._tile_size(cta_tile_shape_mnk) * (arg_tensor.element_type.width // 8)
246
 
247
  def smem_struct_field(self, gemm, params):
248
- tensor = getattr(params, self.name, None)
249
- if tensor is None:
250
- size, dtype = 0, Float32
251
- else:
252
- size = self._tile_size(gemm.cta_tile_shape_mnk)
253
- dtype = tensor.element_type
254
- return (f"s_{self.name}", cute.struct.Align[cute.struct.MemRange[dtype, size], 16])
255
 
256
  def get_smem_tensor(self, gemm, params, storage_epi):
257
- if getattr(params, self.name, None) is None:
258
- return None
259
  return getattr(storage_epi, f"s_{self.name}").get_tensor(
260
  cute.make_layout(self._tile_size(gemm.cta_tile_shape_mnk))
261
  )
@@ -263,49 +333,61 @@ class VecLoad(EpiOp):
263
  def needs_async_fence(self):
264
  return True
265
 
 
 
 
 
266
  def _get_gmem_vec(self, param, ctx):
267
  """Get the global memory vector for this tile. Override for varlen."""
268
  return param[ctx.batch_idx, None]
269
 
270
  @cute.jit
271
  def begin(self, gemm, param, smem_tensor, ctx):
272
- tDsV = None
273
- if const_expr(param is not None):
274
- dtype = param.element_type
275
- num_copy_elems = const_expr(max(32, dtype.width)) // dtype.width
276
- thr_copy = copy_utils.tiled_copy_1d(
277
- dtype, ctx.num_epi_threads, num_copy_elems, is_async=True
278
- ).get_slice(ctx.tidx)
279
- mVec = self._get_gmem_vec(param, ctx)
280
- tile_dim = self._tile_dim(ctx)
281
- coord_idx = ctx.tile_coord_mnkl[self._coord_idx()]
282
- gVec = cute.local_tile(mVec, (tile_dim,), (coord_idx,))
283
- tVgV = thr_copy.partition_S(gVec)
284
- tVsV = thr_copy.partition_D(smem_tensor)
285
- tVcV = thr_copy.partition_S(cute.make_identity_tensor(tile_dim))
286
- limit = min(cute.size(mVec, mode=[0]) - coord_idx * tile_dim, tile_dim)
287
- pred = cute.make_rmem_tensor((1, cute.size(tVsV.shape[1])), Boolean)
288
- for m in cutlass.range(cute.size(tVsV.shape[1]), unroll_full=True):
289
- pred[0, m] = tVcV[0, m] < limit
290
- cute.copy(thr_copy, tVgV, tVsV, pred=pred)
291
- tDsV = ctx.partition_for_epilogue_fn(
292
- cute.make_tensor(
293
- smem_tensor.iterator,
294
- cute.make_layout((ctx.tile_M, ctx.tile_N), stride=self._broadcast_stride()),
295
- )
296
  )
297
- if const_expr(ctx.tiled_copy_t2r is not None):
298
- tDsV = ctx.tiled_copy_r2s.retile(tDsV)
299
- return tDsV
 
 
 
 
300
 
301
  @cute.jit
302
  def begin_loop(self, gemm, state, epi_coord):
303
- tDrV_cvt = None
304
- if const_expr(state is not None):
305
- tDsV_cur = cute.group_modes(state, 3, cute.rank(state))[None, None, None, epi_coord]
 
 
 
 
 
 
 
306
  tDrV = cute.make_rmem_tensor(tDsV_cur.layout, tDsV_cur.element_type)
307
  cute.autovec_copy(cute.filter_zeros(tDsV_cur), cute.filter_zeros(tDrV))
308
- tDrV_cvt = cute.make_rmem_tensor_like(tDrV, gemm.acc_dtype)
309
  tDrV_cvt.store(tDrV.load().to(gemm.acc_dtype))
310
  return tDrV_cvt
311
 
@@ -338,63 +420,47 @@ class ColVecLoad(VecLoad):
338
 
339
  @cute.jit
340
  def begin(self, gemm, param, smem_tensor, ctx):
341
- tDsV = None
342
- tDrV_cvt = None
343
- if const_expr(param is not None):
344
- dtype = param.element_type
345
- num_copy_elems = const_expr(max(32, dtype.width)) // dtype.width
346
- thr_copy = copy_utils.tiled_copy_1d(
347
- dtype, ctx.num_epi_threads, num_copy_elems, is_async=True
348
- ).get_slice(ctx.tidx)
349
- mVec = self._get_gmem_vec(param, ctx)
350
- tile_dim = self._tile_dim(ctx)
351
- coord_idx = ctx.tile_coord_mnkl[self._coord_idx()]
352
- gVec = cute.local_tile(mVec, (tile_dim,), (coord_idx,))
353
- tVgV = thr_copy.partition_S(gVec)
354
- tVsV = thr_copy.partition_D(smem_tensor)
355
- tVcV = thr_copy.partition_S(cute.make_identity_tensor(tile_dim))
356
- # ColVec uses varlen-aware limit
357
- limit = min(
358
- ctx.varlen_manager.len_m(ctx.batch_idx) - coord_idx * tile_dim,
359
- tile_dim,
360
- )
361
- pred = cute.make_rmem_tensor((1, cute.size(tVsV.shape[1])), Boolean)
362
- for m in cutlass.range(cute.size(tVsV.shape[1]), unroll_full=True):
363
- pred[0, m] = tVcV[0, m] < limit
364
- cute.copy(thr_copy, tVgV, tVsV, pred=pred)
365
- tDsV = ctx.partition_for_epilogue_fn(
366
- cute.make_tensor(
367
- smem_tensor.iterator,
368
- cute.make_layout((ctx.tile_M, ctx.tile_N), stride=self._broadcast_stride()),
369
- )
370
  )
371
- if const_expr(ctx.tiled_copy_t2r is not None):
372
- tDsV = ctx.tiled_copy_r2s.retile(tDsV)
373
- # Pre-allocate register tensor reused across begin_loop calls
374
- tDsV_sub = cute.group_modes(tDsV, 3, cute.rank(tDsV))[None, None, None, 0]
375
- tDrV_cvt = cute.make_rmem_tensor(tDsV_sub.layout, gemm.acc_dtype)
 
376
  return [tDsV, tDrV_cvt]
377
 
378
- @cute.jit
379
- def begin_loop(self, gemm, state, epi_coord):
380
- tDsV, tDrV_cvt = state[0], state[1]
381
- if const_expr(tDsV is not None):
382
- # Col vector is constant across N subtiles — only copy on first N subtile.
383
- # Assumes N-major epi subtile order: epi_tile_layout = ordered_layout(..., order=(1,0))
384
- epi_n = epi_coord[1]
385
- if epi_n == 0:
386
- tDsV_cur = cute.group_modes(tDsV, 3, cute.rank(tDsV))[None, None, None, epi_coord]
387
- tDrV = cute.make_rmem_tensor(tDsV_cur.layout, tDsV_cur.element_type)
388
- cute.autovec_copy(cute.filter_zeros(tDsV_cur), cute.filter_zeros(tDrV))
389
- tDrV_cvt.store(tDrV.load().to(gemm.acc_dtype))
390
- return tDrV_cvt
391
-
392
 
393
  class TileStore(EpiOp):
394
  """Tile-sized output tensor stored via TMA (e.g. postact).
395
 
396
  Args:
397
- name: field name in EpilogueArguments/Params (e.g. "mPostAct")
398
  epi_tile_fn: optional (gemm, epi_tile) -> epi_tile for half-tile (GemmGated)
399
  """
400
 
@@ -412,13 +478,13 @@ class TileStore(EpiOp):
412
  return f"epi_tile_{self.name}"
413
 
414
  def param_fields(self):
415
- from dataclasses import MISSING
416
-
417
  return [
418
- (self._tma_atom_key(), object, MISSING),
419
- (self.name, object, MISSING),
420
- (self._smem_layout_key(), object, MISSING),
421
- (self._epi_tile_key(), object, MISSING),
422
  ]
423
 
424
  def to_params(self, gemm, args):
@@ -434,50 +500,207 @@ class TileStore(EpiOp):
434
  self._epi_tile_key(): epi_tile_out,
435
  }
436
 
437
- def smem_bytes(self, arg_tensor, cta_tile_shape_mnk, epi_tile):
438
- if arg_tensor is None:
439
- return 0
440
  if self.epi_tile_fn is not None:
441
  epi_tile = self.epi_tile_fn(None, epi_tile)
442
- return cute.size(cute.shape(epi_tile)) * (arg_tensor.element_type.width // 8)
 
 
 
 
443
 
444
  def smem_struct_field(self, gemm, params):
445
- smem_layout_key = self._smem_layout_key()
446
- if not hasattr(params, smem_layout_key):
447
- return (f"s_{self.name}", cute.struct.MemRange[Float32, 0])
448
  return (
449
  f"s_{self.name}",
450
  cute.struct.Align[
451
  cute.struct.MemRange[
452
- gemm.postact_dtype,
453
- cute.cosize(getattr(params, smem_layout_key)),
454
  ],
455
  gemm.buffer_align_bytes,
456
  ],
457
  )
458
 
459
  def get_smem_tensor(self, gemm, params, storage_epi):
460
- smem_layout_key = self._smem_layout_key()
461
- if not hasattr(params, smem_layout_key):
462
- return None
463
- smem_layout = getattr(params, smem_layout_key)
464
  return getattr(storage_epi, f"s_{self.name}").get_tensor(
465
  smem_layout.outer,
466
  swizzle=smem_layout.inner,
467
  )
468
 
469
  def tma_atoms(self, gemm, params):
470
- tma_key = self._tma_atom_key()
471
- if hasattr(params, tma_key):
472
- return [getattr(params, tma_key)]
473
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
 
475
 
476
  @cute.jit
477
  def vec_multiply(gemm, tRS_rD, tDrColVec, tDrRowVec):
478
- """Multiply tRS_rD by colvec and/or rowvec in-place. Uses packed f32x2 on SM100+."""
479
  if const_expr(tDrColVec is not None):
480
- if const_expr(gemm.arch < 100):
481
  for i in cutlass.range(cute.size(tDrColVec), unroll_full=True):
482
  tRS_rD[i] *= tDrColVec[i]
483
  else:
@@ -487,7 +710,7 @@ def vec_multiply(gemm, tRS_rD, tDrColVec, tDrRowVec):
487
  (tDrColVec[2 * i], tDrColVec[2 * i + 1]),
488
  )
489
  if const_expr(tDrRowVec is not None):
490
- if const_expr(gemm.arch < 100):
491
  for i in cutlass.range(cute.size(tDrRowVec), unroll_full=True):
492
  tRS_rD[i] *= tDrRowVec[i]
493
  else:
@@ -503,13 +726,13 @@ def colvec_reduce_accumulate(gemm, tDrReduce, tRS_rInput, transform_fn=None, rSc
503
  """Accumulate transform_fn(input) or input * rScale into a ColVecReduce buffer.
504
 
505
  If transform_fn is provided, accumulates transform_fn(input[i]).
506
- If rScale is provided, accumulates input[i] * rScale[i] (uses mul/fma for SM100).
507
  If neither, accumulates input directly (identity).
508
  """
509
  if const_expr(tDrReduce is not None):
510
  if const_expr(transform_fn is None):
511
  transform_fn = lambda x: x
512
- if const_expr(gemm.arch < 100):
513
  for i in cutlass.range(cute.size(tDrReduce), unroll_full=True):
514
  val = transform_fn(tRS_rInput[i])
515
  tDrReduce[i] += val * rScale[i] if const_expr(rScale is not None) else val
@@ -521,6 +744,7 @@ def colvec_reduce_accumulate(gemm, tDrReduce, tRS_rInput, transform_fn=None, rSc
521
  for m in cutlass.range(cute.size(tDrReduce_mn, mode=[0]), unroll_full=True):
522
  inp = lambda n: (tRS_rInput_mn[m, 2 * n], tRS_rInput_mn[m, 2 * n + 1])
523
  val0 = transform_fn(inp(0))
 
524
  if const_expr(rScale is not None):
525
  row_sum = cute.arch.mul_packed_f32x2(val0, (rScale_mn[m, 0], rScale_mn[m, 1]))
526
  else:
@@ -536,14 +760,48 @@ def colvec_reduce_accumulate(gemm, tDrReduce, tRS_rInput, transform_fn=None, rSc
536
  tDrReduce_mn[m, 0] += row_sum[0] + row_sum[1]
537
 
538
 
539
- class ColVecReduce(EpiOp):
540
- """Column vector reduction: accumulates across N subtiles in registers,
541
- then warp-reduces and writes to gmem in epi_end.
542
 
543
- No smem. The accumulation itself happens in epi_visit_subtile (user code).
544
- This op handles the register allocation (begin), per-subtile slicing (begin_loop),
545
- and final warp reduction + gmem write (end).
546
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
 
548
  def param_fields(self):
549
  return [(self.name, object, None)]
@@ -551,31 +809,83 @@ class ColVecReduce(EpiOp):
551
  def to_params(self, gemm, args):
552
  return {self.name: assume_stride_divisibility(getattr(args, self.name))}
553
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
  @cute.jit
555
  def begin(self, gemm, param, smem_tensor, ctx):
556
- tDrReduce = None
557
- if const_expr(param is not None):
558
- colvec_mma_layout = cute.make_layout((ctx.tile_M, ctx.tile_N), stride=(1, 0))
559
- tDrReduce_layout = ctx.partition_for_epilogue_fn(
560
- cute.make_rmem_tensor(colvec_mma_layout, Float32)
561
- ).layout
562
- tDrReduce = cute.make_rmem_tensor(tDrReduce_layout, Float32)
563
- cute.filter_zeros(tDrReduce).fill(0.0)
564
- return tDrReduce
565
 
566
  @cute.jit
567
  def begin_loop(self, gemm, state, epi_coord):
568
- result = None
569
- if const_expr(state is not None):
570
- result = cute.group_modes(state, 3, cute.rank(state))[None, None, None, epi_coord]
 
571
  return result
572
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  @cute.jit
574
- def end(
575
  self,
576
  gemm,
577
  param,
578
  state,
 
579
  epi_tile,
580
  tiled_copy_t2r,
581
  tiled_copy_r2s,
@@ -583,9 +893,13 @@ class ColVecReduce(EpiOp):
583
  varlen_manager,
584
  tidx,
585
  ):
586
- """Intra-warp shuffle reduction across N lanes, then direct gmem write."""
587
- if const_expr(param is not None):
588
- tDrReduce = state
 
 
 
 
589
  tiled_copy = tiled_copy_t2r if tiled_copy_t2r is not None else tiled_copy_r2s
590
  reference_src = tiled_copy_t2r is None
591
 
@@ -593,26 +907,147 @@ class ColVecReduce(EpiOp):
593
  lane_layout_MN, warp_layout_MN = _get_lane_warp_layouts(tiled_copy, reference_src)
594
  # For ColVecReduce: reduce across N lanes (lanes_in_N threads share same M row)
595
  lanes_in_N = cute.size(lane_layout_MN, mode=[1])
 
596
  # Typically lanes_in_N is 4 for Sm90
597
  assert lanes_in_N == 1 << int(math.log2(lanes_in_N)), (
598
  "lanes_in_N must be a power of 2 for butterfly reduction"
599
  )
600
 
601
- # ── Intra-warp shuffle reduction across N lanes ──
602
  if const_expr(lanes_in_N > 1):
 
 
603
  assert lane_layout_MN.stride[1] == 1
604
- tDrReduce_flt = cute.filter_zeros(tDrReduce)
605
  for i in cutlass.range(cute.size(tDrReduce_flt), unroll_full=True):
606
  tDrReduce_flt[i] = cute.arch.warp_reduction(
607
  tDrReduce_flt[i], operator.add, threads_in_group=lanes_in_N
608
  )
609
 
610
  warp_N = warp_layout_MN[1]
611
- assert cute.size(warp_N) == 1, (
612
- "ColVecReduce assumes all reduction cols are within the same warp"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
  )
 
 
 
 
 
 
614
 
615
- # ── Direct gmem write (no inter-warp reduction needed: warps_in_N == 1) ──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
616
  partition_for_epilogue_fn = partial(
617
  partition_for_epilogue,
618
  epi_tile=epi_tile,
@@ -621,28 +1056,46 @@ class ColVecReduce(EpiOp):
621
  reference_src=tiled_copy_t2r is None,
622
  )
623
  tile_M, tile_N = gemm.cta_tile_shape_mnk[:2]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  batch_idx = tile_coord_mnkl[3]
625
- limit_n = param.shape[2] if not varlen_manager.varlen_m else param.shape[1]
626
- if tile_coord_mnkl[1] < limit_n:
627
- if const_expr(not varlen_manager.varlen_m):
628
- mColVec = param[batch_idx, None, tile_coord_mnkl[1]]
629
- else:
630
- mColVec = cute.domain_offset(
631
- (varlen_manager.params.cu_seqlens_m[batch_idx],),
632
- param[None, tile_coord_mnkl[1]],
633
- )
634
- gColVec = cute.local_tile(mColVec, (tile_M,), (tile_coord_mnkl[0],))
635
- limit_m = min(
636
- varlen_manager.len_m(batch_idx) - tile_coord_mnkl[0] * tile_M,
637
- tile_M,
638
- )
639
- tDcD = partition_for_epilogue_fn(cute.make_identity_tensor((tile_M, tile_N)))
640
- tDrReduce_m = layout_utils.convert_layout_zero_stride(tDrReduce, tDrReduce.layout)[
641
- None, 0
642
- ]
643
- tDcD_m = layout_utils.convert_layout_zero_stride(tDcD, tDrReduce.layout)[None, 0]
644
- if tDcD_m[0][1] == 0:
645
- for m in cutlass.range(cute.size(tDcD_m, mode=[0])):
646
- row_idx = tDcD_m[m][0]
647
- if row_idx < limit_m:
648
- gColVec[row_idx] = tDrReduce_m[m]
 
5
  smem allocation, begin (one-time per-tile setup), begin_loop (per-subtile extraction),
6
  end (cleanup).
7
 
8
+ The ops are composed via ComposableEpiMixin. Class-level `_epi_ops` is the
9
+ static schema; `_epi_ops_to_params_dict` (called from each subclass's
10
+ `epi_to_underlying_arguments`) shadows it with an instance-level tuple of only
11
+ the active ops (those whose arg tensor is non-None). All EpiOp hook methods
12
+ below therefore assume their `param` / `arg_tensor` is non-None — the
13
+ framework guarantees inactive ops are never iterated.
14
  """
15
 
16
  import math
17
  import operator
18
  from functools import partial
19
+ from typing import NamedTuple
20
 
21
  import cutlass
22
  import cutlass.cute as cute
 
30
 
31
 
32
  class EpiContext:
33
+ """Shared context passed to EpiOp.begin methods. Bundles common arguments.
34
+
35
+ `tRS_rD_layout` is only populated by callers that need TileLoad — it's the
36
+ register layout of the matmul output tile, which TileLoad uses to shape its
37
+ own register tile so it lines up element-wise with tRS_rD in epi_visit_subtile.
38
+ """
39
 
40
  __slots__ = (
41
  "epi_tile",
 
45
  "varlen_manager",
46
  "epilogue_barrier",
47
  "tidx",
48
+ "tRS_rD_layout",
49
  "partition_for_epilogue_fn",
50
  "num_epi_threads",
51
  "batch_idx",
 
63
  varlen_manager,
64
  epilogue_barrier,
65
  tidx,
66
+ tRS_rD_layout=None,
67
  ):
68
  self.epi_tile = epi_tile
69
  self.tiled_copy_t2r = tiled_copy_t2r
 
72
  self.varlen_manager = varlen_manager
73
  self.epilogue_barrier = epilogue_barrier
74
  self.tidx = tidx
75
+ self.tRS_rD_layout = tRS_rD_layout
76
  self.tile_M = gemm.cta_tile_shape_mnk[0]
77
  self.tile_N = gemm.cta_tile_shape_mnk[1]
78
  self.batch_idx = tile_coord_mnkl[3]
 
132
  return lane_layout_MN, warp_layout_MN
133
 
134
 
135
+ class EpiSmemBytes(NamedTuple):
136
+ """Shared-memory accounting for one epilogue op.
137
+
138
+ unstaged: allocated once per CTA tile.
139
+ d_stage: allocated per D/store epilogue stage.
140
+ c_stage: allocated per C/load epilogue stage.
141
+ """
142
+
143
+ unstaged: int = 0
144
+ d_stage: int = 0
145
+ c_stage: int = 0
146
+
147
+ def __add__(self, other):
148
+ return EpiSmemBytes(
149
+ self.unstaged + other.unstaged,
150
+ self.d_stage + other.d_stage,
151
+ self.c_stage + other.c_stage,
152
+ )
153
+
154
+ def __radd__(self, other):
155
+ if other == 0:
156
+ return self
157
+ return self.__add__(other)
158
+
159
+
160
  class EpiOp:
161
  """Base class for composable epilogue operations."""
162
 
 
174
  Returns dict of {param_name: value}. Like EVT's to_underlying_arguments."""
175
  return {}
176
 
177
+ def epi_m_major_score(self, arg_tensor, gemm):
178
+ """Preference for epilogue subtile order. Positive prefers M-major, negative N-major."""
 
179
  return 0
180
 
181
+ # --- Host-side: smem allocation ---
182
+ def smem_bytes(self, arg_tensor, cta_tile_shape_mnk, epi_tile, warp_shape_mnk=None):
183
+ """Bytes of smem needed by unstaged / D-stage / C-stage storage."""
184
+ return EpiSmemBytes()
185
+
186
  def smem_struct_field(self, gemm, params):
187
  """Return (field_name, field_type) for @cute.struct, or None if no smem needed.
188
  params is the full EpilogueParams object."""
 
197
  """Return list of TMA atoms for this op."""
198
  return []
199
 
200
+ def is_tile_load(self):
201
+ """Whether this op is a tile-sized epilogue input loaded through the C pipeline."""
202
+ return False
203
+
204
+ def load_g2s_copy_fn(
205
+ self,
206
+ gemm,
207
+ params,
208
+ smem_tensor,
209
+ tile_coord_mnkl,
210
+ varlen_manager,
211
+ epi_pipeline,
212
+ ):
213
+ """Return a per-subtile gmem->smem copy function, or None."""
214
+ return None
215
+
216
  # --- Device-side: kernel execution ---
217
  @cute.jit
218
  def begin(self, gemm, param, smem_tensor, ctx):
 
223
  """Per-subtile extraction. Returns value for epi_visit_subtile."""
224
  return state
225
 
226
+ @cute.jit
227
+ def load_s2r(self, gemm, param, state, stage_idx):
228
+ """Issue this op's tile-load smem->register copy for one epilogue stage."""
229
+ pass
230
+
231
+ def end_loop(
232
+ self,
233
+ gemm,
234
+ param,
235
+ state,
236
+ epi_coord,
237
+ epi_tile,
238
+ tiled_copy_t2r,
239
+ tiled_copy_r2s,
240
+ tile_coord_mnkl,
241
+ varlen_manager,
242
+ tidx,
243
+ ):
244
+ """Per-subtile cleanup after epi_visit_subtile."""
245
+ pass
246
+
247
  def needs_async_fence(self):
248
  """Whether this op issues async copies that need a fence."""
249
  return False
 
279
 
280
  @cute.jit
281
  def begin(self, gemm, param, smem_tensor, ctx):
282
+ if const_expr(self.dtype is not None):
283
+ return utils.load_scalar_or_pointer(param, dtype=self.dtype)
284
+ return utils.load_scalar_or_pointer(param)
 
 
 
 
 
285
 
286
 
287
  class VecLoad(EpiOp):
 
312
  def _coord_idx(self):
313
  return 1 if self.dim == 1 else 0
314
 
315
+ def smem_bytes(self, arg_tensor, cta_tile_shape_mnk, epi_tile, warp_shape_mnk=None):
316
+ return EpiSmemBytes(
317
+ unstaged=self._tile_size(cta_tile_shape_mnk) * (arg_tensor.element_type.width // 8)
318
+ )
319
 
320
  def smem_struct_field(self, gemm, params):
321
+ tensor = getattr(params, self.name)
322
+ size = self._tile_size(gemm.cta_tile_shape_mnk)
323
+ return (
324
+ f"s_{self.name}",
325
+ cute.struct.Align[cute.struct.MemRange[tensor.element_type, size], 16],
326
+ )
 
327
 
328
  def get_smem_tensor(self, gemm, params, storage_epi):
 
 
329
  return getattr(storage_epi, f"s_{self.name}").get_tensor(
330
  cute.make_layout(self._tile_size(gemm.cta_tile_shape_mnk))
331
  )
 
333
  def needs_async_fence(self):
334
  return True
335
 
336
+ def epi_m_major_score(self, arg_tensor, gemm):
337
+ # It costs more registers (say 4x) to keep rowvec in register vs keeping colvec in register
338
+ return 4 if self.dim == 1 else -1
339
+
340
  def _get_gmem_vec(self, param, ctx):
341
  """Get the global memory vector for this tile. Override for varlen."""
342
  return param[ctx.batch_idx, None]
343
 
344
  @cute.jit
345
  def begin(self, gemm, param, smem_tensor, ctx):
346
+ dtype = param.element_type
347
+ num_copy_elems = const_expr(max(32, dtype.width)) // dtype.width
348
+ thr_copy = copy_utils.tiled_copy_1d(
349
+ dtype, ctx.num_epi_threads, num_copy_elems, is_async=True
350
+ ).get_slice(ctx.tidx)
351
+ mVec = self._get_gmem_vec(param, ctx)
352
+ tile_dim = self._tile_dim(ctx)
353
+ coord_idx = ctx.tile_coord_mnkl[self._coord_idx()]
354
+ gVec = cute.local_tile(mVec, (tile_dim,), (coord_idx,))
355
+ tVgV = thr_copy.partition_S(gVec)
356
+ tVsV = thr_copy.partition_D(smem_tensor)
357
+ tVcV = thr_copy.partition_S(cute.make_identity_tensor(tile_dim))
358
+ limit = min(cute.size(mVec, mode=[0]) - coord_idx * tile_dim, tile_dim)
359
+ for m in cutlass.range(cute.size(tVsV.shape[1]), unroll_full=True):
360
+ if tVcV[0, m] < tile_dim: # Guard to avoid writing beyond the smem we've allocated
361
+ pred = cute.make_rmem_tensor(1, Boolean)
362
+ pred[0] = tVcV[0, m] < limit
363
+ cute.copy(thr_copy, tVgV[None, m], tVsV[None, m], pred=pred)
364
+ tDsV = ctx.partition_for_epilogue_fn(
365
+ cute.make_tensor(
366
+ smem_tensor.iterator,
367
+ cute.make_layout((ctx.tile_M, ctx.tile_N), stride=self._broadcast_stride()),
 
 
368
  )
369
+ )
370
+ if const_expr(ctx.tiled_copy_t2r is not None):
371
+ tDsV = ctx.tiled_copy_r2s.retile(tDsV)
372
+ # Pre-allocate register tensor reused across begin_loop calls
373
+ tDsV_sub = cute.group_modes(tDsV, 3, cute.rank(tDsV))[None, None, None, 0]
374
+ tDrV_cvt = cute.make_rmem_tensor(tDsV_sub.layout, gemm.acc_dtype)
375
+ return [tDsV, tDrV_cvt]
376
 
377
  @cute.jit
378
  def begin_loop(self, gemm, state, epi_coord):
379
+ tDsV, tDrV_cvt = state[0], state[1]
380
+ should_load = Boolean(True)
381
+ if const_expr(self.dim == 1):
382
+ if const_expr(gemm.epi_m_major):
383
+ should_load = epi_coord[0] == 0
384
+ else:
385
+ if const_expr(not gemm.epi_m_major):
386
+ should_load = epi_coord[1] == 0
387
+ if should_load:
388
+ tDsV_cur = cute.group_modes(tDsV, 3, cute.rank(tDsV))[None, None, None, epi_coord]
389
  tDrV = cute.make_rmem_tensor(tDsV_cur.layout, tDsV_cur.element_type)
390
  cute.autovec_copy(cute.filter_zeros(tDsV_cur), cute.filter_zeros(tDrV))
 
391
  tDrV_cvt.store(tDrV.load().to(gemm.acc_dtype))
392
  return tDrV_cvt
393
 
 
420
 
421
  @cute.jit
422
  def begin(self, gemm, param, smem_tensor, ctx):
423
+ dtype = param.element_type
424
+ num_copy_elems = const_expr(max(32, dtype.width)) // dtype.width
425
+ thr_copy = copy_utils.tiled_copy_1d(
426
+ dtype, ctx.num_epi_threads, num_copy_elems, is_async=True
427
+ ).get_slice(ctx.tidx)
428
+ mVec = self._get_gmem_vec(param, ctx)
429
+ tile_dim = self._tile_dim(ctx)
430
+ coord_idx = ctx.tile_coord_mnkl[self._coord_idx()]
431
+ gVec = cute.local_tile(mVec, (tile_dim,), (coord_idx,))
432
+ tVgV = thr_copy.partition_S(gVec)
433
+ tVsV = thr_copy.partition_D(smem_tensor)
434
+ tVcV = thr_copy.partition_S(cute.make_identity_tensor(tile_dim))
435
+ # ColVec uses varlen-aware limit
436
+ limit = min(
437
+ ctx.varlen_manager.len_m(ctx.batch_idx) - coord_idx * tile_dim,
438
+ tile_dim,
439
+ )
440
+ for m in cutlass.range(cute.size(tVsV.shape[1]), unroll_full=True):
441
+ if tVcV[0, m] < tile_dim: # Guard to avoid writing beyond the smem we've allocated
442
+ pred = cute.make_rmem_tensor(1, Boolean)
443
+ pred[0] = tVcV[0, m] < limit
444
+ cute.copy(thr_copy, tVgV[None, m], tVsV[None, m], pred=pred)
445
+ tDsV = ctx.partition_for_epilogue_fn(
446
+ cute.make_tensor(
447
+ smem_tensor.iterator,
448
+ cute.make_layout((ctx.tile_M, ctx.tile_N), stride=self._broadcast_stride()),
 
 
 
449
  )
450
+ )
451
+ if const_expr(ctx.tiled_copy_t2r is not None):
452
+ tDsV = ctx.tiled_copy_r2s.retile(tDsV)
453
+ # Pre-allocate register tensor reused across begin_loop calls
454
+ tDsV_sub = cute.group_modes(tDsV, 3, cute.rank(tDsV))[None, None, None, 0]
455
+ tDrV_cvt = cute.make_rmem_tensor(tDsV_sub.layout, gemm.acc_dtype)
456
  return [tDsV, tDrV_cvt]
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
 
459
  class TileStore(EpiOp):
460
  """Tile-sized output tensor stored via TMA (e.g. postact).
461
 
462
  Args:
463
+ name: field name in EpilogueArguments/Params (e.g. "mAuxOut")
464
  epi_tile_fn: optional (gemm, epi_tile) -> epi_tile for half-tile (GemmGated)
465
  """
466
 
 
478
  return f"epi_tile_{self.name}"
479
 
480
  def param_fields(self):
481
+ # Defaults are None so EpilogueParams can be constructed when this op is
482
+ # filtered out (inactive). Active calls always set all four via to_params.
483
  return [
484
+ (self._tma_atom_key(), object, None),
485
+ (self.name, object, None),
486
+ (self._smem_layout_key(), object, None),
487
+ (self._epi_tile_key(), object, None),
488
  ]
489
 
490
  def to_params(self, gemm, args):
 
500
  self._epi_tile_key(): epi_tile_out,
501
  }
502
 
503
+ def smem_bytes(self, arg_tensor, cta_tile_shape_mnk, epi_tile, warp_shape_mnk=None):
 
 
504
  if self.epi_tile_fn is not None:
505
  epi_tile = self.epi_tile_fn(None, epi_tile)
506
+ # epi_tile may contain Layout entries (from SM100's compute_epilogue_tile_shape
507
+ # fixup path), so extract the int shape first.
508
+ return EpiSmemBytes(
509
+ d_stage=cute.size(cute.shape(epi_tile)) * (arg_tensor.element_type.width // 8)
510
+ )
511
 
512
  def smem_struct_field(self, gemm, params):
513
+ smem_layout = getattr(params, self._smem_layout_key())
 
 
514
  return (
515
  f"s_{self.name}",
516
  cute.struct.Align[
517
  cute.struct.MemRange[
518
+ gemm.aux_out_dtype,
519
+ cute.cosize(smem_layout),
520
  ],
521
  gemm.buffer_align_bytes,
522
  ],
523
  )
524
 
525
  def get_smem_tensor(self, gemm, params, storage_epi):
526
+ smem_layout = getattr(params, self._smem_layout_key())
 
 
 
527
  return getattr(storage_epi, f"s_{self.name}").get_tensor(
528
  smem_layout.outer,
529
  swizzle=smem_layout.inner,
530
  )
531
 
532
  def tma_atoms(self, gemm, params):
533
+ return [getattr(params, self._tma_atom_key())]
534
+
535
+
536
+ class _TileLoadState(NamedTuple):
537
+ """Per-tile register state produced by TileLoad.begin and consumed by load_s2r /
538
+ begin_loop. tRS_rTile is the register tile partitioned to match tRS_rD's layout;
539
+ tSR_sTile / tSR_rTile drive the per-stage smem→register copy."""
540
+
541
+ tiled_copy_s2r: object
542
+ tRS_rTile: object
543
+ tSR_rTile: object
544
+ tSR_sTile: object
545
+
546
+
547
+ class TileLoad(EpiOp):
548
+ """Tile-sized auxiliary input loaded through the epilogue load pipeline.
549
+
550
+ TileLoad uses the same staged gmem->smem->register pipeline as GEMM's C operand,
551
+ but it is exposed to the epilogue as ``epi_loop_tensors[name]`` instead of as
552
+ ``tRS_rC``. That lets custom epilogues consume extra MxN tensors without using
553
+ the GEMM C argument.
554
+
555
+ Its shared memory is accounted as ``EpiSmemBytes.c_stage``, so it is allocated
556
+ per epilogue load stage. Multiple TileLoads are supported: each has its own TMA
557
+ descriptor and smem buffer, and the pipeline transaction count includes C plus
558
+ all enabled TileLoad buffers. Supported on SM90, SM100, and SM120.
559
+ """
560
+
561
+ def __init__(self, name, epi_tile_fn=None):
562
+ super().__init__(name)
563
+ self.epi_tile_fn = epi_tile_fn
564
+
565
+ def _tma_atom_key(self):
566
+ return f"tma_atom_{self.name}"
567
+
568
+ def _smem_layout_key(self):
569
+ return f"epi_{self.name}_smem_layout_staged"
570
+
571
+ def _epi_tile_key(self):
572
+ return f"epi_tile_{self.name}"
573
+
574
+ # The original LayoutEnum and element_type can't be recovered from the
575
+ # TMA-prepared tensor that ends up in params (`from_tensor` returns a typing
576
+ # annotation post-TMA, not a Numeric class). We stash both on the gemm at
577
+ # to_params time and read them back in begin(). The dtype is also exposed on
578
+ # the params dataclass for smem_struct_field.
579
+ def _layout_gemm_attr(self):
580
+ return f"_tile_load_layout_{self.name}"
581
+
582
+ def _dtype_gemm_attr(self):
583
+ return f"_tile_load_dtype_{self.name}"
584
+
585
+ def _dtype_field(self):
586
+ return f"{self.name}_dtype"
587
+
588
+ def param_fields(self):
589
+ # Defaults are None so EpilogueParams can be constructed when this op is
590
+ # filtered out (inactive). Active calls always set all five via to_params.
591
+ return [
592
+ (self._tma_atom_key(), object, None),
593
+ (self.name, object, None),
594
+ (self._smem_layout_key(), object, None),
595
+ (self._epi_tile_key(), object, None),
596
+ (self._dtype_field(), object, None),
597
+ ]
598
+
599
+ def to_params(self, gemm, args):
600
+ tensor = getattr(args, self.name)
601
+ setattr(gemm, self._layout_gemm_attr(), cutlass.utils.LayoutEnum.from_tensor(tensor))
602
+ setattr(gemm, self._dtype_gemm_attr(), tensor.element_type)
603
+ epi_tile = self.epi_tile_fn(gemm, gemm.epi_tile) if self.epi_tile_fn else None
604
+ tma_atom, tma_tensor, smem_layout, epi_tile_out = setup_epi_tensor(
605
+ gemm, tensor, epi_tile=epi_tile, op_type="load", stage=gemm.epi_c_stage
606
+ )
607
+ return {
608
+ self._tma_atom_key(): tma_atom,
609
+ self.name: tma_tensor,
610
+ self._smem_layout_key(): smem_layout,
611
+ self._epi_tile_key(): epi_tile_out,
612
+ self._dtype_field(): tensor.element_type,
613
+ }
614
+
615
+ def is_tile_load(self):
616
+ return True
617
+
618
+ def smem_bytes(self, arg_tensor, cta_tile_shape_mnk, epi_tile, warp_shape_mnk=None):
619
+ if self.epi_tile_fn is not None:
620
+ epi_tile = self.epi_tile_fn(None, epi_tile)
621
+ # epi_tile may contain Layout entries from SM100's compute_epilogue_tile_shape
622
+ # fixup; extract the int shape first.
623
+ return EpiSmemBytes(
624
+ c_stage=cute.size(cute.shape(epi_tile)) * (arg_tensor.element_type.width // 8)
625
+ )
626
+
627
+ def smem_struct_field(self, gemm, params):
628
+ smem_layout = getattr(params, self._smem_layout_key())
629
+ dtype = getattr(params, self._dtype_field())
630
+ return (
631
+ f"s_{self.name}",
632
+ cute.struct.Align[
633
+ cute.struct.MemRange[dtype, cute.cosize(smem_layout)],
634
+ gemm.buffer_align_bytes,
635
+ ],
636
+ )
637
+
638
+ def get_smem_tensor(self, gemm, params, storage_epi):
639
+ smem_layout = getattr(params, self._smem_layout_key())
640
+ return getattr(storage_epi, f"s_{self.name}").get_tensor(
641
+ smem_layout.outer,
642
+ swizzle=smem_layout.inner,
643
+ )
644
+
645
+ def tma_atoms(self, gemm, params):
646
+ return [getattr(params, self._tma_atom_key())]
647
+
648
+ def load_g2s_copy_fn(
649
+ self,
650
+ gemm,
651
+ params,
652
+ smem_tensor,
653
+ tile_coord_mnkl,
654
+ varlen_manager,
655
+ epi_pipeline,
656
+ ):
657
+ tensor = getattr(params, self.name)
658
+ batch_idx = tile_coord_mnkl[3]
659
+ copy_tile_fn, _, _ = gemm.epilog_gmem_copy_and_partition(
660
+ getattr(params, self._tma_atom_key()),
661
+ varlen_manager.offset_batch_epi(tensor, batch_idx),
662
+ gemm.cta_tile_shape_mnk[:2],
663
+ getattr(params, self._epi_tile_key()),
664
+ smem_tensor,
665
+ tile_coord_mnkl,
666
+ )
667
+ return copy_utils.tma_producer_copy_fn(copy_tile_fn, epi_pipeline)
668
+
669
+ @cute.jit
670
+ def begin(self, gemm, param, smem_tensor, ctx):
671
+ assert gemm.arch in (90, 100, 120), "TileLoad requires the SM90/SM100/SM120 epilogue path"
672
+ assert ctx.tRS_rD_layout is not None
673
+ smem_load_ref = ctx.tiled_copy_t2r if const_expr(gemm.arch == 100) else gemm.tiled_mma
674
+ tiled_copy_s2r, tRS_rTile, tSR_rTile, tSR_sTile = gemm.epilog_smem_load_and_partition(
675
+ smem_load_ref,
676
+ getattr(gemm, self._layout_gemm_attr()),
677
+ getattr(gemm, self._dtype_gemm_attr()),
678
+ smem_tensor,
679
+ ctx.tRS_rD_layout,
680
+ ctx.tidx,
681
+ )
682
+ # Shape: (s2r-copy-handle, register-tile-as-rD-layout, smem→r retile target,
683
+ # smem→r staged source). begin_loop returns tRS_rTile; load_s2r uses the rest.
684
+ return _TileLoadState(tiled_copy_s2r, tRS_rTile, tSR_rTile, tSR_sTile)
685
+
686
+ @cute.jit
687
+ def load_s2r(self, gemm, param, state, stage_idx):
688
+ cute.copy(
689
+ state.tiled_copy_s2r,
690
+ state.tSR_sTile[None, None, None, stage_idx],
691
+ state.tSR_rTile,
692
+ )
693
+
694
+ @cute.jit
695
+ def begin_loop(self, gemm, state, epi_coord):
696
+ return state.tRS_rTile
697
 
698
 
699
  @cute.jit
700
  def vec_multiply(gemm, tRS_rD, tDrColVec, tDrRowVec):
701
+ """Multiply tRS_rD by colvec and/or rowvec in-place. Uses packed f32x2 on SM100."""
702
  if const_expr(tDrColVec is not None):
703
+ if const_expr(gemm.arch != 100):
704
  for i in cutlass.range(cute.size(tDrColVec), unroll_full=True):
705
  tRS_rD[i] *= tDrColVec[i]
706
  else:
 
710
  (tDrColVec[2 * i], tDrColVec[2 * i + 1]),
711
  )
712
  if const_expr(tDrRowVec is not None):
713
+ if const_expr(gemm.arch != 100):
714
  for i in cutlass.range(cute.size(tDrRowVec), unroll_full=True):
715
  tRS_rD[i] *= tDrRowVec[i]
716
  else:
 
726
  """Accumulate transform_fn(input) or input * rScale into a ColVecReduce buffer.
727
 
728
  If transform_fn is provided, accumulates transform_fn(input[i]).
729
+ If rScale is provided, accumulates input[i] * rScale[i] (uses packed mul/fma for SM100).
730
  If neither, accumulates input directly (identity).
731
  """
732
  if const_expr(tDrReduce is not None):
733
  if const_expr(transform_fn is None):
734
  transform_fn = lambda x: x
735
+ if const_expr(gemm.arch != 100):
736
  for i in cutlass.range(cute.size(tDrReduce), unroll_full=True):
737
  val = transform_fn(tRS_rInput[i])
738
  tDrReduce[i] += val * rScale[i] if const_expr(rScale is not None) else val
 
744
  for m in cutlass.range(cute.size(tDrReduce_mn, mode=[0]), unroll_full=True):
745
  inp = lambda n: (tRS_rInput_mn[m, 2 * n], tRS_rInput_mn[m, 2 * n + 1])
746
  val0 = transform_fn(inp(0))
747
+ assert cute.size(tDrReduce_mn, mode=[1]) % 2 == 0
748
  if const_expr(rScale is not None):
749
  row_sum = cute.arch.mul_packed_f32x2(val0, (rScale_mn[m, 0], rScale_mn[m, 1]))
750
  else:
 
760
  tDrReduce_mn[m, 0] += row_sum[0] + row_sum[1]
761
 
762
 
763
+ @cute.jit
764
+ def rowvec_reduce_accumulate(gemm, tDrReduce, tRS_rInput, transform_fn=None, rScale=None):
765
+ """Accumulate transform_fn(input) or input * rScale into a RowVecReduce buffer.
766
 
767
+ Reduces along M dimension, keeping N. The zero-stride layout on M ensures
768
+ elements at different M positions but same N column accumulate correctly.
 
769
  """
770
+ if const_expr(tDrReduce is not None):
771
+ if const_expr(transform_fn is None):
772
+ transform_fn = lambda x: x
773
+ if const_expr(gemm.arch != 100):
774
+ for i in cutlass.range(cute.size(tDrReduce), unroll_full=True):
775
+ val = transform_fn(tRS_rInput[i])
776
+ tDrReduce[i] += val * rScale[i] if const_expr(rScale is not None) else val
777
+ else:
778
+ # Keep CUTLASS's linear fragment indexing, but use packed f32x2 arithmetic
779
+ # for any transform that accepts and returns an f32x2 tuple.
780
+ # We have to be careful to avoid tDrReduce[2 * i] and tDrReduce[2 * i + 1] aliasing
781
+ # each other. For SM100, tDrReduce has layout ((32,1),1,1):((1,0),0,0) or
782
+ # (((2,2,4),1),2,1):(((1,0,8),0),0,0), so this works. But it's error-prone.
783
+ for i in cutlass.range(cute.size(tRS_rInput) // 2, unroll_full=True):
784
+ acc = (tDrReduce[2 * i], tDrReduce[2 * i + 1])
785
+ val = (tRS_rInput[2 * i], tRS_rInput[2 * i + 1])
786
+ val = transform_fn(val)
787
+ if const_expr(rScale is not None):
788
+ scale = (rScale[2 * i], rScale[2 * i + 1])
789
+ tDrReduce[2 * i], tDrReduce[2 * i + 1] = cute.arch.fma_packed_f32x2(
790
+ val, scale, acc
791
+ )
792
+ else:
793
+ tDrReduce[2 * i], tDrReduce[2 * i + 1] = cute.arch.add_packed_f32x2(val, acc)
794
+ if const_expr(cute.size(tRS_rInput) % 2 != 0):
795
+ i = cute.size(tRS_rInput) - 1
796
+ val = transform_fn(tRS_rInput[i])
797
+ tDrReduce[i] += val * rScale[i] if const_expr(rScale is not None) else val
798
+
799
+
800
+ class VecReduce(EpiOp):
801
+ """Base class for row/column vector reductions."""
802
+
803
+ dim = 0 # 0 for colvec output along M, 1 for rowvec output along N
804
+ epi_m_major_preference = 0
805
 
806
  def param_fields(self):
807
  return [(self.name, object, None)]
 
809
  def to_params(self, gemm, args):
810
  return {self.name: assume_stride_divisibility(getattr(args, self.name))}
811
 
812
+ def epi_m_major_score(self, arg_tensor, gemm):
813
+ return self.epi_m_major_preference
814
+
815
+ def _tile_size(self, cta_tile_shape_mnk):
816
+ return cta_tile_shape_mnk[self.dim]
817
+
818
+ def _broadcast_stride(self):
819
+ # Col: stride (1,0) broadcasts along N. Row: stride (0,1) broadcasts along M.
820
+ return (1, 0) if self.dim == 0 else (0, 1)
821
+
822
+ def _reduce_dim(self):
823
+ return 1 - self.dim
824
+
825
+ def _smem_warps(self, warp_shape_mnk):
826
+ warps = warp_shape_mnk[self._reduce_dim()] if warp_shape_mnk is not None else 1
827
+ return max(warps - 1, 0)
828
+
829
+ def smem_bytes(self, arg_tensor, cta_tile_shape_mnk, epi_tile, warp_shape_mnk=None):
830
+ smem_warps = self._smem_warps(warp_shape_mnk)
831
+ if smem_warps == 0:
832
+ return EpiSmemBytes()
833
+ return EpiSmemBytes(
834
+ unstaged=self._tile_size(cta_tile_shape_mnk) * smem_warps * (Float32.width // 8)
835
+ )
836
+
837
+ def smem_struct_field(self, gemm, params):
838
+ smem_warps = self._smem_warps(gemm.epi_smem_warp_shape_mnk())
839
+ if smem_warps == 0:
840
+ return None
841
+ size = self._tile_size(gemm.cta_tile_shape_mnk) * smem_warps
842
+ return (f"s_{self.name}", cute.struct.Align[cute.struct.MemRange[Float32, size], 16])
843
+
844
+ def get_smem_tensor(self, gemm, params, storage_epi):
845
+ smem_warps = self._smem_warps(gemm.epi_smem_warp_shape_mnk())
846
+ if smem_warps == 0:
847
+ return None
848
+ return getattr(storage_epi, f"s_{self.name}").get_tensor(
849
+ cute.make_layout((self._tile_size(gemm.cta_tile_shape_mnk), smem_warps))
850
+ )
851
+
852
  @cute.jit
853
  def begin(self, gemm, param, smem_tensor, ctx):
854
+ vec_mma_layout = cute.make_layout((ctx.tile_M, ctx.tile_N), stride=self._broadcast_stride())
855
+ tDrReduce_layout = ctx.partition_for_epilogue_fn(
856
+ cute.make_rmem_tensor(vec_mma_layout, Float32)
857
+ ).layout
858
+ tDrReduce = cute.make_rmem_tensor(tDrReduce_layout, Float32)
859
+ return (tDrReduce, smem_tensor)
 
 
 
860
 
861
  @cute.jit
862
  def begin_loop(self, gemm, state, epi_coord):
863
+ tDrReduce = state[0]
864
+ result = tDrReduce[None, None, None, epi_coord[0], epi_coord[1]]
865
+ if const_expr(epi_coord[self._reduce_dim()] == 0):
866
+ cute.filter_zeros(result).fill(0.0)
867
  return result
868
 
869
+
870
+ class ColVecReduce(VecReduce):
871
+ """Column vector reduction: accumulates across N subtiles in registers,
872
+ then reduces across N lanes/warps and writes to gmem per completed M stripe.
873
+
874
+ The accumulation itself happens in epi_visit_subtile (user code).
875
+ This op handles the register allocation (begin), per-subtile slicing (begin_loop),
876
+ and reduction + gmem write (end_loop).
877
+ """
878
+
879
+ dim = 0
880
+ epi_m_major_preference = -1
881
+
882
  @cute.jit
883
+ def end_loop(
884
  self,
885
  gemm,
886
  param,
887
  state,
888
+ epi_coord,
889
  epi_tile,
890
  tiled_copy_t2r,
891
  tiled_copy_r2s,
 
893
  varlen_manager,
894
  tidx,
895
  ):
896
+ """Flush the current M stripe when the last N subtile has accumulated."""
897
+ epi_tile_shape = cute.zipped_divide(
898
+ cute.make_layout(gemm.cta_tile_shape_mnk[:2]), epi_tile
899
+ ).shape[1]
900
+ if const_expr(epi_coord[1] == epi_tile_shape[1] - 1):
901
+ tDrReduce, sDrReduce = state[0], state[1]
902
+ tDrReduce_cur = tDrReduce[None, None, None, epi_coord[0], epi_coord[1]]
903
  tiled_copy = tiled_copy_t2r if tiled_copy_t2r is not None else tiled_copy_r2s
904
  reference_src = tiled_copy_t2r is None
905
 
 
907
  lane_layout_MN, warp_layout_MN = _get_lane_warp_layouts(tiled_copy, reference_src)
908
  # For ColVecReduce: reduce across N lanes (lanes_in_N threads share same M row)
909
  lanes_in_N = cute.size(lane_layout_MN, mode=[1])
910
+ is_lane_n_leader = cute.arch.lane_idx() % lanes_in_N == 0
911
  # Typically lanes_in_N is 4 for Sm90
912
  assert lanes_in_N == 1 << int(math.log2(lanes_in_N)), (
913
  "lanes_in_N must be a power of 2 for butterfly reduction"
914
  )
915
 
916
+ # Intra-warp shuffle reduction across N lanes
917
  if const_expr(lanes_in_N > 1):
918
+ # Assumes threads for each M row are contiguous along N, so
919
+ # warp_reduction over groups of lanes_in_N matches lane_layout_MN.
920
  assert lane_layout_MN.stride[1] == 1
921
+ tDrReduce_flt = cute.filter_zeros(tDrReduce_cur)
922
  for i in cutlass.range(cute.size(tDrReduce_flt), unroll_full=True):
923
  tDrReduce_flt[i] = cute.arch.warp_reduction(
924
  tDrReduce_flt[i], operator.add, threads_in_group=lanes_in_N
925
  )
926
 
927
  warp_N = warp_layout_MN[1]
928
+ warps_in_N = const_expr(cute.size(warp_N))
929
+ partition_for_epilogue_fn = partial(
930
+ partition_for_epilogue,
931
+ epi_tile=epi_tile,
932
+ tiled_copy=tiled_copy,
933
+ tidx=tidx,
934
+ reference_src=tiled_copy_t2r is None,
935
+ )
936
+ tile_M, tile_N = gemm.cta_tile_shape_mnk[:2]
937
+ tDcD = partition_for_epilogue_fn(cute.make_identity_tensor((tile_M, tile_N)))
938
+ tDcD_cur = tDcD[None, None, None, epi_coord[0], epi_coord[1]]
939
+ tDrReduce_m = layout_utils.convert_layout_zero_stride(
940
+ tDrReduce_cur, tDrReduce_cur.layout
941
+ )[None, 0]
942
+ tDcD_m = layout_utils.convert_layout_zero_stride(tDcD_cur, tDrReduce_cur.layout)[
943
+ None, 0
944
+ ]
945
+
946
+ # Inter-warp reduction through smem
947
+ warp_idx = cute.arch.make_warp_uniform(tidx // cute.arch.WARP_SIZE)
948
+ warp_n_idx = warp_layout_MN.get_hier_coord(warp_idx)[1]
949
+ if const_expr(warps_in_N > 1):
950
+ if warp_n_idx > 0 and is_lane_n_leader:
951
+ for m in cutlass.range(cute.size(tDcD_m, mode=[0])):
952
+ row_idx = tDcD_m[m][0]
953
+ sDrReduce[row_idx, warp_n_idx - 1] = tDrReduce_m[m]
954
+ gemm.epilogue_barrier.arrive_and_wait()
955
+ if warp_n_idx == 0 and is_lane_n_leader:
956
+ for m in cutlass.range(cute.size(tDcD_m, mode=[0])):
957
+ row_idx = tDcD_m[m][0]
958
+ for warp_n in cutlass.range_constexpr(1, warps_in_N):
959
+ tDrReduce_m[m] += sDrReduce[row_idx, warp_n - 1]
960
+
961
+ # Write to gmem
962
+ batch_idx = tile_coord_mnkl[3]
963
+ limit_m = min(varlen_manager.len_m(batch_idx) - tile_coord_mnkl[0] * tile_M, tile_M)
964
+ limit_n_tiles = param.shape[2] if not varlen_manager.varlen_m else param.shape[1]
965
+ if const_expr(not varlen_manager.varlen_m):
966
+ mColVec = param[batch_idx, None, tile_coord_mnkl[1]]
967
+ else:
968
+ mColVec = cute.domain_offset(
969
+ (varlen_manager.params.cu_seqlens_m[batch_idx],),
970
+ param[None, tile_coord_mnkl[1]],
971
+ )
972
+ gColVec = cute.local_tile(mColVec, (tile_M,), (tile_coord_mnkl[0],))
973
+ should_write_gmem = (
974
+ is_lane_n_leader
975
+ if const_expr(warps_in_N == 1)
976
+ else warp_n_idx == 0 and is_lane_n_leader
977
  )
978
+ if tile_coord_mnkl[1] < limit_n_tiles and should_write_gmem:
979
+ for m in cutlass.range(cute.size(tDcD_m, mode=[0])):
980
+ row_idx = tDcD_m[m][0]
981
+ if row_idx < limit_m:
982
+ gColVec[row_idx] = tDrReduce_m[m]
983
+
984
 
985
+ class RowVecReduce(VecReduce):
986
+ """Row vector reduction: accumulates across M subtiles in registers,
987
+ then reduces across M lanes/warps and writes to gmem per completed N stripe.
988
+
989
+ Output shape is (L, ceildiv(M, tile_M), N): one partial sum per CTA-M tile per
990
+ N column. This mirrors ColVecReduce with M/N swapped.
991
+ """
992
+
993
+ dim = 1
994
+ epi_m_major_preference = 4
995
+
996
+ @cute.jit
997
+ def end_loop(
998
+ self,
999
+ gemm,
1000
+ param,
1001
+ state,
1002
+ epi_coord,
1003
+ epi_tile,
1004
+ tiled_copy_t2r,
1005
+ tiled_copy_r2s,
1006
+ tile_coord_mnkl,
1007
+ varlen_manager,
1008
+ tidx,
1009
+ ):
1010
+ """Flush the current N stripe when the last M subtile has accumulated."""
1011
+ epi_tile_shape = cute.zipped_divide(
1012
+ cute.make_layout(gemm.cta_tile_shape_mnk[:2]), epi_tile
1013
+ ).shape[1]
1014
+ if const_expr(epi_coord[0] == epi_tile_shape[0] - 1):
1015
+ tDrReduce, sDrReduce = state[0], state[1]
1016
+ tDrReduce_cur = tDrReduce[None, None, None, epi_coord[0], epi_coord[1]]
1017
+ tiled_copy = tiled_copy_t2r if tiled_copy_t2r is not None else tiled_copy_r2s
1018
+ reference_src = tiled_copy_t2r is None
1019
+
1020
+ # ── Derive lane layout from tiled_copy ──
1021
+ lane_layout_MN, warp_layout_MN = _get_lane_warp_layouts(tiled_copy, reference_src)
1022
+ # For RowVecReduce: reduce across M lanes (lanes_in_M threads share same N col)
1023
+ lanes_in_M = cute.size(lane_layout_MN, mode=[0])
1024
+ lanes_in_N = cute.size(lane_layout_MN, mode=[1])
1025
+ is_lane_m_leader = cute.arch.lane_idx() < lanes_in_N
1026
+ assert lanes_in_M == 1 << int(math.log2(lanes_in_M)), (
1027
+ "lanes_in_M must be a power of 2 for butterfly reduction"
1028
+ )
1029
+ if const_expr(lanes_in_N > 1):
1030
+ assert lane_layout_MN.stride[1] == 1, (
1031
+ "RowVecReduce assumes contiguous N lanes when lanes_in_N > 1"
1032
+ )
1033
+
1034
+ # Intra-warp shuffle reduction across M lanes. M lanes may be either contiguous
1035
+ # (SM100 N-major output) or strided by N lanes (SM100 M-major output).
1036
+ tDrReduce_n = layout_utils.convert_layout_zero_stride(
1037
+ tDrReduce_cur, tDrReduce_cur.layout
1038
+ )[None, 0]
1039
+ if const_expr(lanes_in_M > 1):
1040
+ for n in cutlass.range(cute.size(tDrReduce_n), unroll_full=True):
1041
+ reduction_rows = lanes_in_M // 2
1042
+ while reduction_rows > 0:
1043
+ tDrReduce_n[n] += cute.arch.shuffle_sync_bfly(
1044
+ tDrReduce_n[n],
1045
+ offset=cute.crd2idx((reduction_rows, 0), lane_layout_MN),
1046
+ )
1047
+ reduction_rows = reduction_rows // 2
1048
+
1049
+ warp_M = warp_layout_MN[0]
1050
+ warps_in_M = const_expr(cute.size(warp_M))
1051
  partition_for_epilogue_fn = partial(
1052
  partition_for_epilogue,
1053
  epi_tile=epi_tile,
 
1056
  reference_src=tiled_copy_t2r is None,
1057
  )
1058
  tile_M, tile_N = gemm.cta_tile_shape_mnk[:2]
1059
+ tDcD = partition_for_epilogue_fn(cute.make_identity_tensor((tile_M, tile_N)))
1060
+ tDcD_cur = tDcD[None, None, None, epi_coord[0], epi_coord[1]]
1061
+ tDcD_n = layout_utils.convert_layout_zero_stride(tDcD_cur, tDrReduce_cur.layout)[
1062
+ None, 0
1063
+ ]
1064
+
1065
+ # Inter-warp reduction through smem
1066
+ warp_idx = cute.arch.make_warp_uniform(tidx // cute.arch.WARP_SIZE)
1067
+ warp_m_idx = warp_layout_MN.get_hier_coord(warp_idx)[0]
1068
+ if const_expr(warps_in_M > 1):
1069
+ if warp_m_idx > 0 and is_lane_m_leader:
1070
+ for n in cutlass.range(cute.size(tDcD_n, mode=[0])):
1071
+ col_idx = tDcD_n[n][1]
1072
+ sDrReduce[col_idx, warp_m_idx - 1] = tDrReduce_n[n]
1073
+ gemm.epilogue_barrier.arrive_and_wait()
1074
+ if warp_m_idx == 0 and is_lane_m_leader:
1075
+ for n in cutlass.range(cute.size(tDcD_n, mode=[0])):
1076
+ col_idx = tDcD_n[n][1]
1077
+ for warp_m in cutlass.range_constexpr(1, warps_in_M):
1078
+ tDrReduce_n[n] += sDrReduce[col_idx, warp_m - 1]
1079
+
1080
+ # Write to gmem
1081
  batch_idx = tile_coord_mnkl[3]
1082
+ limit_m_tiles = param.shape[1] if not varlen_manager.varlen_m else param.shape[0]
1083
+ if const_expr(not varlen_manager.varlen_m):
1084
+ mRowVec = param[batch_idx, tile_coord_mnkl[0], None]
1085
+ else:
1086
+ mRowVec = param[tile_coord_mnkl[0], None]
1087
+ gRowVec = cute.local_tile(mRowVec, (tile_N,), (tile_coord_mnkl[1],))
1088
+ limit_n = min(
1089
+ cute.size(mRowVec, mode=[0]) - tile_coord_mnkl[1] * tile_N,
1090
+ tile_N,
1091
+ )
1092
+ should_write_gmem = (
1093
+ is_lane_m_leader
1094
+ if const_expr(warps_in_M == 1)
1095
+ else warp_m_idx == 0 and is_lane_m_leader
1096
+ )
1097
+ if tile_coord_mnkl[0] < limit_m_tiles and should_write_gmem:
1098
+ for n in cutlass.range(cute.size(tDcD_n, mode=[0])):
1099
+ col_idx = tDcD_n[n][1]
1100
+ if col_idx < limit_n:
1101
+ gRowVec[col_idx] = tDrReduce_n[n]
 
 
 
 
build/torch-cuda/quack/epi_utils.py CHANGED
@@ -32,27 +32,32 @@ def assume_broadcast_strides(*tensors):
32
  return [assume_stride_divisibility(t) for t in tensors]
33
 
34
 
35
- def setup_epi_tensor(gemm, tensor, epi_tile=None, op_type="store"):
36
- """Create TMA atom + smem layout for a supplemental epilogue tensor.
37
 
38
  Args:
39
- gemm: The GEMM object (provides arch, epi_stage, _make_tma_epi_atoms_and_tensors).
40
- tensor: The global memory tensor to set up TMA for.
41
  epi_tile: Epilogue tile shape. Defaults to gemm.epi_tile.
42
  op_type: "store" or "load".
43
 
44
  Returns:
45
- (tma_atom, tma_tensor, smem_layout_staged, epi_tile)
46
  """
47
  if epi_tile is None:
48
  epi_tile = gemm.epi_tile
 
 
49
  dtype = tensor.element_type
50
  layout = cutlass.utils.LayoutEnum.from_tensor(tensor)
51
  utils_cls = sm100_utils if gemm.arch >= 100 else sm90_utils
52
- smem_layout_staged = utils_cls.make_smem_layout_epi(dtype, layout, epi_tile, gemm.epi_stage)
 
 
 
53
  tma_input = (
54
  copy_utils.create_ragged_tensor_for_tma(tensor, ragged_dim=0, ptr_shift=True)
55
- if cute.rank(tensor) == 2
56
  else tensor
57
  )
58
  tma_atom, tma_tensor = gemm._make_tma_epi_atoms_and_tensors(
 
32
  return [assume_stride_divisibility(t) for t in tensors]
33
 
34
 
35
+ def setup_epi_tensor(gemm, tensor, epi_tile=None, op_type="store", stage=None):
36
+ """Create copy metadata + smem layout for a supplemental epilogue tensor.
37
 
38
  Args:
39
+ gemm: The GEMM object (provides arch, epi_stage, and epilogue layout helpers).
40
+ tensor: The global memory tensor to set up for the epilogue.
41
  epi_tile: Epilogue tile shape. Defaults to gemm.epi_tile.
42
  op_type: "store" or "load".
43
 
44
  Returns:
45
+ (copy_atom, tensor, smem_layout_staged, epi_tile). copy_atom is None for pre-TMA archs.
46
  """
47
  if epi_tile is None:
48
  epi_tile = gemm.epi_tile
49
+ if stage is None:
50
+ stage = gemm.epi_stage
51
  dtype = tensor.element_type
52
  layout = cutlass.utils.LayoutEnum.from_tensor(tensor)
53
  utils_cls = sm100_utils if gemm.arch >= 100 else sm90_utils
54
+ smem_layout_staged = utils_cls.make_smem_layout_epi(dtype, layout, epi_tile, stage)
55
+ # Ragging-for-TMA is for varlen_m stores that need a per-batch row offset baked
56
+ # into the TMA descriptor. Loads don't currently support varlen_m, so skip the
57
+ # ragging conversion.
58
  tma_input = (
59
  copy_utils.create_ragged_tensor_for_tma(tensor, ragged_dim=0, ptr_shift=True)
60
+ if op_type != "load" and cute.rank(tensor) == 2
61
  else tensor
62
  )
63
  tma_atom, tma_tensor = gemm._make_tma_epi_atoms_and_tensors(
build/torch-cuda/quack/fast_math.py CHANGED
@@ -2,32 +2,78 @@
2
 
3
  import cutlass
4
  import cutlass.cute as cute
 
 
5
  from cutlass.base_dsl.typing import Integer
6
- from cutlass.cutlass_dsl import dsl_user_op
7
-
8
-
9
- class FastDivmod(cute.FastDivmodDivisor):
10
- """We store the divisor along with the FastDivmodDivisor."""
11
-
12
- @dsl_user_op
13
- def __init__(
14
- self,
15
- divisor: Integer,
16
- is_power_of_2: bool = None,
17
- *,
18
- loc=None,
19
- ip=None,
20
- ):
21
- super().__init__(divisor, is_power_of_2=is_power_of_2, loc=loc, ip=ip)
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  self.divisor = divisor
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  def __extract_mlir_values__(self):
25
- """Extract MLIR values for Host->Device transfer."""
26
- return [self._divisor] + cutlass.extract_mlir_values(self.divisor)
 
 
 
 
 
27
 
28
  def __new_from_mlir_values__(self, values):
29
- """Reconstruct FastDivmodDivisor from MLIR values."""
30
  new_obj = object.__new__(FastDivmod)
31
- new_obj._divisor = values[0]
32
- new_obj.divisor = cutlass.new_from_mlir_values(self.divisor, values[1:])
 
 
 
 
33
  return new_obj
 
2
 
3
  import cutlass
4
  import cutlass.cute as cute
5
+ from cutlass import Int32, Uint32, Uint64
6
+ from cutlass._mlir.dialects import llvm
7
  from cutlass.base_dsl.typing import Integer
8
+
9
+
10
+ def ceil_log2(x: Integer) -> Int32:
11
+ """ceil(log2(x)) for 1 <= x < 2^31, as 32 - ctlz(x - 1). The llvm.intr.ctlz
12
+ lowers to lzcnt on the host (launch-prep path) and clz on device;
13
+ is_zero_poison=False makes ctlz(0) == 32, so x == 1 correctly yields 0."""
14
+ xm1 = (Int32(x) - 1).ir_value()
15
+ return Int32(32) - Int32(llvm.intr_ctlz(xm1, False))
16
+
17
+
18
+ class FastDivmod:
19
+ """Magic-number unsigned divmod: q = umulhi(n, magic) >> shift, r = n - q * divisor.
20
+
21
+ The multiplier and shift are precomputed on the host (kernel params), so the
22
+ device-side divmod is 3 uniform-datapath ops (IMAD.WIDE.U32 + USHF + IMAD) plus
23
+ a select handling divisor == 1 (magic == 0 sentinel, like nvjet). This is the
24
+ lean form without the add-back correction the stock cute FastDivmodDivisor
25
+ emits: with shift = max(ceil_log2(d) - 1, 0) the multiplier ceil(2^(32+s)/d)
26
+ fits 32 bits (worst case d = 2^(c-1)+1 gives m <= 2^32 - 1) and the result is
27
+ exact for all dividends < 2^31, i.e. any non-negative Int32. Same contract and
28
+ algorithm as C++ cutlass::FastDivmod. Negative dividends are OUT of contract
29
+ (they reinterpret through Uint32 to values >= 2^31 and silently divide wrong);
30
+ use cute.FastDivmodDivisor if signed or full-u32 dividends are ever needed.
31
+ """
32
+
33
+ def __init__(self, divisor: Integer):
34
+ if isinstance(divisor, int):
35
+ assert 0 < divisor < 1 << 31
36
+ divisor = Int32(divisor) # constants fold through the arithmetic below
37
  self.divisor = divisor
38
+ # Runs on the CPU at launch prep (host-side jit); the udiv is once per launch.
39
+ s = cutlass.max(ceil_log2(divisor) - 1, Int32(0))
40
+ pow_s = Uint64(Uint32(1) << Uint32(s))
41
+ numer = Uint64(0x100000000) * pow_s
42
+ magic = (numer + Uint64(Uint32(divisor)) - 1) // Uint64(Uint32(divisor))
43
+ self.magic = Uint32(magic & 0xFFFFFFFF) # 0 when divisor == 1
44
+ self.shift = Uint32(s)
45
+
46
+ def __rdivmod__(self, dividend: Integer):
47
+ q = Uint32(cute.arch.mul_hi(Uint32(dividend), self.magic)) >> self.shift
48
+ # divisor == 1 sentinel: magic wrapped to 0, so q is 0; select the dividend
49
+ # instead. The remainder then self-corrects: r = n - n * 1 = 0.
50
+ q = Int32(cutlass.select_(self.magic == Uint32(0), Int32(dividend), Int32(q)))
51
+ r = Int32(dividend) - q * Int32(self.divisor)
52
+ return q, r
53
+
54
+ def __rfloordiv__(self, dividend: Integer) -> Int32:
55
+ q, _ = self.__rdivmod__(dividend)
56
+ return q
57
+
58
+ def __rmod__(self, dividend: Integer) -> Int32:
59
+ _, r = self.__rdivmod__(dividend)
60
+ return r
61
 
62
  def __extract_mlir_values__(self):
63
+ values = []
64
+ self._values_pos = []
65
+ for obj in [self.magic, self.shift, self.divisor]:
66
+ obj_values = cutlass.extract_mlir_values(obj)
67
+ values += obj_values
68
+ self._values_pos.append(len(obj_values))
69
+ return values
70
 
71
  def __new_from_mlir_values__(self, values):
 
72
  new_obj = object.__new__(FastDivmod)
73
+ for name, n_items in zip(["magic", "shift", "divisor"], self._values_pos):
74
+ setattr(
75
+ new_obj, name, cutlass.new_from_mlir_values(getattr(self, name), values[:n_items])
76
+ )
77
+ values = values[n_items:]
78
+ new_obj._values_pos = self._values_pos
79
  return new_obj
build/torch-cuda/quack/gemm.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2025-2026, Tri Dao.
2
  # GEMM compilation via TVM-FFI with fake tensors and NamedTuple args.
3
 
4
  from typing import Optional
@@ -9,11 +9,12 @@ import cutlass.cute as cute
9
  from cutlass import Int32, Float32
10
  from cutlass.cute.runtime import make_ptr
11
 
12
- from .cache_utils import jit_cache
13
  from .compile_utils import make_fake_tensor as fake_tensor
14
  from .cute_dsl_utils import get_device_capacity, get_max_active_clusters, torch2cute_dtype_map
15
  from .gemm_default_epi import (
16
  GemmDefaultEpiMixin,
 
17
  GemmDefaultSm90,
18
  GemmDefaultSm100,
19
  GemmDefaultSm120,
@@ -28,7 +29,9 @@ from .gemm_tvm_ffi_utils import (
28
  make_fake_scheduler_args,
29
  make_fake_varlen_args,
30
  make_fake_gemm_tensors,
 
31
  compile_gemm_kernel,
 
32
  )
33
 
34
 
@@ -62,9 +65,12 @@ def _compile_gemm(
62
  device_capacity,
63
  rounding_mode,
64
  sr_seed_mode,
65
- has_trace_ptr,
 
 
66
  ):
67
  sm_to_cls = {
 
68
  9: GemmDefaultSm90,
69
  10: GemmDefaultSm100,
70
  11: GemmDefaultSm100,
@@ -111,10 +117,18 @@ def _compile_gemm(
111
  sr_seed=fake_scalar(sr_seed_mode, dtype=Int32),
112
  )
113
  scheduler_args = make_fake_scheduler_args(
114
- (is_dynamic_persistent and device_capacity[0] == 9), has_batch_idx_permute, l
115
  )
116
  aidx_len = m if varlen_m else (k if varlen_k else None)
117
  varlen_args = make_fake_varlen_args(varlen_m, varlen_k, gather_A, aidx_len)
 
 
 
 
 
 
 
 
118
  return compile_gemm_kernel(
119
  GemmCls,
120
  a_dtype,
@@ -132,9 +146,12 @@ def _compile_gemm(
132
  epi_args,
133
  scheduler_args,
134
  varlen_args,
135
- has_trace_ptr=has_trace_ptr,
 
136
  use_tma_gather=use_tma_gather,
137
  concat_layout=concat_layout or None,
 
 
138
  )
139
 
140
 
@@ -149,6 +166,8 @@ def gemm(
149
  tile_N: int,
150
  cluster_M: int,
151
  cluster_N: int,
 
 
152
  pingpong: bool = False,
153
  persistent: bool = True,
154
  is_dynamic_persistent: bool = False,
@@ -166,18 +185,24 @@ def gemm(
166
  sr_seed: int | Tensor = 0,
167
  use_tma_gather: bool = False,
168
  concat_layout: dict | None = None,
169
- trace_ptr=None, # Optional Int64 from TraceSession.ptr
 
 
 
 
 
 
 
170
  ) -> None:
171
  varlen_m = cu_seqlens_m is not None
172
  varlen_k = cu_seqlens_k is not None
173
  varlen = varlen_m or varlen_k
174
  gather_A = A_idx is not None
 
175
  assert not (varlen_m and varlen_k), "Only one of cu_seqlens_m and cu_seqlens_k"
176
  if gather_A:
177
  assert varlen, "gather_A requires varlen"
178
  assert cluster_N == 1, "gather_A requires cluster_N=1"
179
- if varlen:
180
- assert persistent, "varlen requires persistent=True"
181
  if add_to_output:
182
  assert not varlen_m, "Add to output not supported with varlen_m"
183
  if varlen_m:
@@ -188,15 +213,35 @@ def gemm(
188
  assert B.stride(-2) == 1, "varlen_k requires B to be n-major"
189
 
190
  device_capacity = get_device_capacity(A.device)
191
- assert device_capacity[0] in [9, 10, 11, 12], "Only SM90, SM100, SM110, and SM120 are supported"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  if use_tma_gather:
193
  assert device_capacity[0] in [10, 11], "TMA gather currently requires SM100/SM110"
194
  if rounding_mode == RoundingMode.RS:
195
  assert device_capacity[0] == 10, "Stochastic rounding (RoundingMode.RS) requires SM100"
196
- if is_dynamic_persistent and device_capacity[0] == 9:
197
  assert tile_count_semaphore is not None, (
198
- "Dynamic persistent tile scheduler in SM90 requires a semaphore in GMEM"
199
  )
 
 
 
 
200
 
201
  A_p, B_p, D_p, C_p = perm3d(A, B, D, C, varlen_m=varlen_m, varlen_k=varlen_k)
202
  a_major, b_major, d_major, c_major = get_majors(A_p, B_p, D_p, C_p)
@@ -210,6 +255,7 @@ def gemm(
210
  sr_seed_mode = (
211
  2 if isinstance(sr_seed, Tensor) else (1 if rounding_mode == RoundingMode.RS else 0)
212
  )
 
213
  compiled_fn = _compile_gemm(
214
  a_dtype,
215
  b_dtype,
@@ -219,8 +265,8 @@ def gemm(
219
  b_major,
220
  d_major,
221
  c_major,
222
- (tile_M, tile_N),
223
- (cluster_M, cluster_N, 1),
224
  pingpong,
225
  persistent,
226
  is_dynamic_persistent,
@@ -239,14 +285,11 @@ def gemm(
239
  device_capacity,
240
  rounding_mode,
241
  sr_seed_mode,
242
- trace_ptr is not None,
 
 
243
  )
244
 
245
- from .cache_utils import COMPILE_ONLY
246
-
247
- if COMPILE_ONLY:
248
- return
249
-
250
  def scalar_arg(scalar, mode, dtype=Float32):
251
  if mode == 0:
252
  return None
@@ -255,7 +298,10 @@ def gemm(
255
  else:
256
  return scalar.data_ptr()
257
 
258
- max_active_clusters = get_max_active_clusters(cluster_M * cluster_N) if persistent else 0
 
 
 
259
 
260
  epi_args = GemmDefaultEpiMixin.EpilogueArguments(
261
  alpha=scalar_arg(alpha, alpha_mode),
@@ -269,14 +315,15 @@ def gemm(
269
  scheduler_args = make_scheduler_args(
270
  max_active_clusters,
271
  max_swizzle_size,
272
- tile_count_semaphore,
 
 
 
273
  batch_idx_permute,
274
  )
275
  varlen_args = make_varlen_args(cu_seqlens_m, cu_seqlens_k, A_idx)
276
 
277
  if device_capacity[0] in [10, 11]:
278
- compiled_fn(
279
- A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args, None, None, trace_ptr
280
- )
281
  else:
282
- compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args, trace_ptr)
 
1
+ # Copyright (c) 2025-2026, QuACK team.
2
  # GEMM compilation via TVM-FFI with fake tensors and NamedTuple args.
3
 
4
  from typing import Optional
 
9
  from cutlass import Int32, Float32
10
  from cutlass.cute.runtime import make_ptr
11
 
12
+ from .cache import jit_cache
13
  from .compile_utils import make_fake_tensor as fake_tensor
14
  from .cute_dsl_utils import get_device_capacity, get_max_active_clusters, torch2cute_dtype_map
15
  from .gemm_default_epi import (
16
  GemmDefaultEpiMixin,
17
+ GemmDefaultSm80,
18
  GemmDefaultSm90,
19
  GemmDefaultSm100,
20
  GemmDefaultSm120,
 
29
  make_fake_scheduler_args,
30
  make_fake_varlen_args,
31
  make_fake_gemm_tensors,
32
+ make_fake_sf_tensor,
33
  compile_gemm_kernel,
34
+ validate_blockscaled_sf,
35
  )
36
 
37
 
 
65
  device_capacity,
66
  rounding_mode,
67
  sr_seed_mode,
68
+ num_warps,
69
+ sf_dtype=None,
70
+ sf_vec_size=None,
71
  ):
72
  sm_to_cls = {
73
+ 8: GemmDefaultSm80,
74
  9: GemmDefaultSm90,
75
  10: GemmDefaultSm100,
76
  11: GemmDefaultSm100,
 
117
  sr_seed=fake_scalar(sr_seed_mode, dtype=Int32),
118
  )
119
  scheduler_args = make_fake_scheduler_args(
120
+ (is_dynamic_persistent and device_capacity[0] <= 9), has_batch_idx_permute, l
121
  )
122
  aidx_len = m if varlen_m else (k if varlen_k else None)
123
  varlen_args = make_fake_varlen_args(varlen_m, varlen_k, gather_A, aidx_len)
124
+ if sf_dtype is not None:
125
+ # Padded SF buffers have a static batch dim of exactly 1 (not l): SFA for
126
+ # varlen_m (M-padded) and varlen_k (K-padded); SFB is K-padded too for
127
+ # varlen_k but stays per-batch (l, rn, rk, ...) for varlen_m.
128
+ mSFA = make_fake_sf_tensor(sf_dtype, 1 if (varlen_m or varlen_k) else l)
129
+ mSFB = make_fake_sf_tensor(sf_dtype, 1 if varlen_k else l)
130
+ else:
131
+ mSFA, mSFB = None, None
132
  return compile_gemm_kernel(
133
  GemmCls,
134
  a_dtype,
 
146
  epi_args,
147
  scheduler_args,
148
  varlen_args,
149
+ mSFA=mSFA,
150
+ mSFB=mSFB,
151
  use_tma_gather=use_tma_gather,
152
  concat_layout=concat_layout or None,
153
+ num_warps=num_warps,
154
+ sf_vec_size=sf_vec_size,
155
  )
156
 
157
 
 
166
  tile_N: int,
167
  cluster_M: int,
168
  cluster_N: int,
169
+ cluster_K: int = 1,
170
+ tile_K: int | None = None,
171
  pingpong: bool = False,
172
  persistent: bool = True,
173
  is_dynamic_persistent: bool = False,
 
185
  sr_seed: int | Tensor = 0,
186
  use_tma_gather: bool = False,
187
  concat_layout: dict | None = None,
188
+ num_warps: Optional[int] = None,
189
+ # SFA/SFB: (l, rm/rn, rk, 32, 4, 4) blocked scale factors. For varlen_m, SFA is
190
+ # M-padded (1, total_padded_rm, rk, 32, 4, 4) while SFB stays per-batch. For
191
+ # varlen_k, BOTH are K-padded (1, rm/rn, total_padded_rk, 32, 4, 4); pad bytes
192
+ # may be arbitrary (the kernel skips the MMA instructions covering them).
193
+ # See AI/varlen_blockscaled_sf_layout.md.
194
+ SFA: Optional[Tensor] = None,
195
+ SFB: Optional[Tensor] = None,
196
  ) -> None:
197
  varlen_m = cu_seqlens_m is not None
198
  varlen_k = cu_seqlens_k is not None
199
  varlen = varlen_m or varlen_k
200
  gather_A = A_idx is not None
201
+ blockscaled = SFA is not None
202
  assert not (varlen_m and varlen_k), "Only one of cu_seqlens_m and cu_seqlens_k"
203
  if gather_A:
204
  assert varlen, "gather_A requires varlen"
205
  assert cluster_N == 1, "gather_A requires cluster_N=1"
 
 
206
  if add_to_output:
207
  assert not varlen_m, "Add to output not supported with varlen_m"
208
  if varlen_m:
 
213
  assert B.stride(-2) == 1, "varlen_k requires B to be n-major"
214
 
215
  device_capacity = get_device_capacity(A.device)
216
+ assert device_capacity[0] in [8, 9, 10, 11, 12], (
217
+ "Only SM8x, SM90, SM100, SM110, and SM120 are supported"
218
+ )
219
+ sf_dtype, sf_vec_size = None, None
220
+ if blockscaled:
221
+ assert not gather_A, "Blockscaled GEMM does not support gather_A yet"
222
+ assert not concat_layout, "Blockscaled GEMM does not support concat_layout"
223
+ assert tile_K is None, "Blockscaled GEMM derives tile_K from the MMA instruction"
224
+ if varlen_m:
225
+ num_batches = cu_seqlens_m.shape[0] - 1
226
+ elif varlen_k:
227
+ num_batches = cu_seqlens_k.shape[0] - 1
228
+ else:
229
+ num_batches = None
230
+ sf_dtype, sf_vec_size = validate_blockscaled_sf(
231
+ A, B, SFA, SFB, device_capacity, num_batches=num_batches, varlen_k=varlen_k
232
+ )
233
  if use_tma_gather:
234
  assert device_capacity[0] in [10, 11], "TMA gather currently requires SM100/SM110"
235
  if rounding_mode == RoundingMode.RS:
236
  assert device_capacity[0] == 10, "Stochastic rounding (RoundingMode.RS) requires SM100"
237
+ if is_dynamic_persistent and device_capacity[0] <= 9:
238
  assert tile_count_semaphore is not None, (
239
+ "Dynamic persistent tile scheduler for SM8x and SM90 requires a semaphore in GMEM"
240
  )
241
+ if device_capacity[0] == 8:
242
+ if add_to_output:
243
+ C = D
244
+ add_to_output = False
245
 
246
  A_p, B_p, D_p, C_p = perm3d(A, B, D, C, varlen_m=varlen_m, varlen_k=varlen_k)
247
  a_major, b_major, d_major, c_major = get_majors(A_p, B_p, D_p, C_p)
 
255
  sr_seed_mode = (
256
  2 if isinstance(sr_seed, Tensor) else (1 if rounding_mode == RoundingMode.RS else 0)
257
  )
258
+ tile_shape_mnk = (tile_M, tile_N) if tile_K is None else (tile_M, tile_N, tile_K)
259
  compiled_fn = _compile_gemm(
260
  a_dtype,
261
  b_dtype,
 
265
  b_major,
266
  d_major,
267
  c_major,
268
+ tile_shape_mnk,
269
+ (cluster_M, cluster_N, cluster_K),
270
  pingpong,
271
  persistent,
272
  is_dynamic_persistent,
 
285
  device_capacity,
286
  rounding_mode,
287
  sr_seed_mode,
288
+ num_warps,
289
+ sf_dtype,
290
+ sf_vec_size,
291
  )
292
 
 
 
 
 
 
293
  def scalar_arg(scalar, mode, dtype=Float32):
294
  if mode == 0:
295
  return None
 
298
  else:
299
  return scalar.data_ptr()
300
 
301
+ cluster_size = cluster_M * cluster_N * cluster_K
302
+ max_active_clusters = (
303
+ get_max_active_clusters(cluster_size, device_capacity=device_capacity) if persistent else 0
304
+ )
305
 
306
  epi_args = GemmDefaultEpiMixin.EpilogueArguments(
307
  alpha=scalar_arg(alpha, alpha_mode),
 
315
  scheduler_args = make_scheduler_args(
316
  max_active_clusters,
317
  max_swizzle_size,
318
+ # Must mirror make_fake_scheduler_args in _compile_gemm: only the SM8x/SM90
319
+ # dynamic scheduler consumes the semaphore; SM100 uses CLC instead, and the
320
+ # compiled signature has None there.
321
+ tile_count_semaphore if (is_dynamic_persistent and device_capacity[0] <= 9) else None,
322
  batch_idx_permute,
323
  )
324
  varlen_args = make_varlen_args(cu_seqlens_m, cu_seqlens_k, A_idx)
325
 
326
  if device_capacity[0] in [10, 11]:
327
+ compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args, SFA, SFB)
 
 
328
  else:
329
+ compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args)
build/torch-cuda/quack/gemm_act.py CHANGED
@@ -1,26 +1,27 @@
1
  # Copyright (c) 2025, Wentao Guo, Tri Dao.
2
  from __future__ import annotations
3
- from typing import NamedTuple, Tuple, Optional, Callable
4
- from functools import partial
5
 
6
  from torch import Tensor
7
 
8
  import cutlass
9
  import cutlass.cute as cute
10
- import cutlass.utils.hopper_helpers as sm90_utils_og
11
  import cutlass.utils.blackwell_helpers as sm100_utils
12
  from cutlass import Int32, Float32, const_expr
13
  from cutlass.cute.runtime import make_ptr
 
14
 
15
  from .compile_utils import make_fake_tensor as fake_tensor
16
  from .cute_dsl_utils import (
17
- ParamsBase,
18
  mlir_namedtuple,
19
  get_device_capacity,
20
  get_max_active_clusters,
21
  torch2cute_dtype_map,
22
  )
23
- from .epi_ops import TileStore
 
 
24
  from .gemm_sm90 import GemmSm90
25
  from .gemm_sm100 import GemmSm100
26
  from .gemm_sm120 import GemmSm120
@@ -34,23 +35,32 @@ from .gemm_tvm_ffi_utils import (
34
  make_fake_varlen_args,
35
  div_for_dtype,
36
  make_fake_gemm_tensors,
 
37
  compile_gemm_kernel,
 
38
  )
39
- from .cache_utils import jit_cache
40
  from . import layout_utils as layout_utils
 
41
  from .layout_utils import permute_gated_Cregs_b16
42
  from .activation import act_fn_map, gate_fn_map
43
- from .rounding import RoundingMode
44
 
45
 
46
- class GemmActMixin(GemmDefaultEpiMixin):
47
- _epi_ops = (*GemmDefaultEpiMixin._epi_ops, TileStore("mPostAct"))
 
 
 
 
 
 
 
48
  _extra_param_fields = (("act_fn", cutlass.Constexpr, None),)
49
- _epi_param_bases = (ParamsBase,)
50
 
51
  @mlir_namedtuple
52
  class EpilogueArguments(NamedTuple):
53
- mPostAct: cute.Tensor
54
  act_fn: cutlass.Constexpr[Optional[Callable]] = None
55
  alpha: Optional[Float32 | cute.Tensor] = None
56
  beta: Optional[Float32 | cute.Tensor] = None
@@ -63,20 +73,39 @@ class GemmActMixin(GemmDefaultEpiMixin):
63
 
64
  def epi_to_underlying_arguments(self, args: EpilogueArguments, *, loc=None, ip=None):
65
  self.rounding_mode = args.rounding_mode
66
- self.postact_dtype = args.mPostAct.element_type
67
- self.postact_layout = cutlass.utils.LayoutEnum.from_tensor(args.mPostAct)
68
- self.cta_tile_shape_postact_mn = self.cta_tile_shape_mnk[:2]
69
  d = self._epi_ops_to_params_dict(args)
70
  d["act_fn"] = args.act_fn
71
  for key in ("mRowVecBroadcast", "mColVecBroadcast"):
72
- if key in self.concat_layout and key in d and d[key] is not None:
73
  d[key] = layout_utils.concat_to_interleave(d[key], 1)
74
  return self.EpilogueParams(**d)
75
 
76
- # epi_get_tma_atoms, epi_smem_bytes_per_stage, epi_get_smem_struct,
77
  # epi_get_smem_tensors are all inherited from ComposableEpiMixin via _epi_ops.
78
 
79
- def epi_setup_postact(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  self,
81
  params,
82
  epi_smem_tensors,
@@ -86,61 +115,56 @@ class GemmActMixin(GemmDefaultEpiMixin):
86
  varlen_manager,
87
  tidx,
88
  ):
89
- """Setup postact TMA copies and partitions before the epilogue loop."""
90
- sPostAct = epi_smem_tensors[self._epi_smem_map["mPostAct"]]
91
- get_smem_store_op = (
92
- partial(sm100_utils.get_smem_store_op, tiled_tmem_load=tiled_copy_t2r)
93
- if self.arch == 100
94
- else sm90_utils_og.sm90_get_smem_store_op
95
- )
96
- copy_atom_postact_r2s = get_smem_store_op(
97
- self.postact_layout, self.postact_dtype, self.acc_dtype
 
98
  )
99
- tiled_copy_postact_r2s = cute.make_tiled_copy_S(copy_atom_postact_r2s, tiled_copy_r2s)
100
- tRS_sPostAct = tiled_copy_postact_r2s.get_slice(tidx).partition_D(sPostAct)
101
  batch_idx = tile_coord_mnkl[3]
102
- copy_postact, _, _ = self.epilog_gmem_copy_and_partition(
103
- params.tma_atom_mPostAct,
104
- varlen_manager.offset_batch_epi(params.mPostAct, batch_idx),
105
- self.cta_tile_shape_postact_mn,
106
- params.epi_tile_mPostAct,
107
- sPostAct,
108
  tile_coord_mnkl,
109
  )
110
- return tiled_copy_postact_r2s, tRS_sPostAct, copy_postact
111
 
112
  @cute.jit
113
- def epi_convert_postact(
114
- self, tRS_rPostAct, sr_seed, tidx, tile_coord_mnkl, num_prev_subtiles, epi_idx
 
 
 
 
 
 
 
115
  ):
116
- """Convert postact from acc_dtype to postact_dtype. Override for custom postprocessing."""
117
  if const_expr(
118
  self.rounding_mode == RoundingMode.RS
119
- and tRS_rPostAct.element_type == cutlass.Float32
120
- and self.postact_dtype == cutlass.BFloat16
121
  ):
122
- from .rounding import convert_f32_to_bf16_sr
123
  from cutlass.cute.tensor import TensorSSA
124
 
125
- # Salt with 0x9E3779B1 to avoid sharing entropy with the D output seed
126
- seed = (
127
- sr_seed
128
- + 0x9E3779B1
129
- + (
130
- tile_coord_mnkl[0] * 65537
131
- + tile_coord_mnkl[1] * 257
132
- + tile_coord_mnkl[3] * 17
133
- + (num_prev_subtiles + epi_idx) * 7
134
- )
135
- )
136
- tRS_rPostAct_out = cute.make_rmem_tensor_like(tRS_rPostAct, self.postact_dtype)
137
- src_vec = tRS_rPostAct.load()
138
  raw_vec = convert_f32_to_bf16_sr(src_vec, seed, tidx)
139
- tRS_rPostAct_out.store(TensorSSA(raw_vec, src_vec.shape, self.postact_dtype))
140
  else:
141
- tRS_rPostAct_out = cute.make_rmem_tensor_like(tRS_rPostAct, self.postact_dtype)
142
- tRS_rPostAct_out.store(tRS_rPostAct.load().to(self.postact_dtype))
143
- return tRS_rPostAct_out
144
 
145
  @cute.jit
146
  def epi_visit_subtile(
@@ -149,29 +173,28 @@ class GemmActMixin(GemmDefaultEpiMixin):
149
  epi_loop_tensors: Tuple[cute.Tensor, ...],
150
  tRS_rD: cute.Tensor,
151
  tRS_rC: Optional[cute.Tensor] = None,
152
- ) -> Optional[cute.Tensor]:
153
  GemmDefaultEpiMixin.epi_visit_subtile(self, params, epi_loop_tensors, tRS_rD, tRS_rC)
154
  # Apply activation function if provided
155
  # If we don't have .shape here, the compiler generates local stores and loads
156
  if const_expr(params.act_fn is not None):
157
- tRS_rPostAct = cute.make_rmem_tensor(tRS_rD.layout.shape, self.acc_dtype)
158
- if const_expr(self.arch < 100):
159
- for i in cutlass.range(cute.size(tRS_rPostAct), unroll_full=True):
160
- tRS_rPostAct[i] = params.act_fn(tRS_rD[i])
161
- else:
162
- for i in cutlass.range(cute.size(tRS_rPostAct) // 2, unroll_full=True):
163
- tRS_rPostAct[2 * i], tRS_rPostAct[2 * i + 1] = params.act_fn(
164
- (tRS_rD[2 * i], tRS_rD[2 * i + 1])
165
- )
166
  else:
167
- tRS_rPostAct = tRS_rD
168
- return tRS_rPostAct
169
 
170
 
171
  class GemmActSm90(GemmActMixin, GemmSm90):
172
  pass
173
 
174
 
 
 
 
 
175
  class GemmActSm100(GemmActMixin, GemmSm100):
176
  pass
177
 
@@ -189,33 +212,37 @@ def _gated_epi_tile_fn(gemm, epi_tile):
189
 
190
  class GemmGatedMixin(GemmActMixin):
191
  _epi_ops = (
192
- *GemmDefaultEpiMixin._epi_ops,
193
- TileStore("mPostAct", epi_tile_fn=_gated_epi_tile_fn),
 
 
 
 
194
  )
195
 
196
  def epi_to_underlying_arguments(
197
  self, args: GemmActMixin.EpilogueArguments, *, loc=None, ip=None
198
  ) -> GemmActMixin.EpilogueParams:
199
- assert args.mPostAct.element_type.width == 16, (
200
  "GemmGated only supports 16bit postact for now"
201
  )
202
  assert self.d_layout is None or self.d_layout.is_n_major_c()
203
- assert cutlass.utils.LayoutEnum.from_tensor(args.mPostAct).is_n_major_c()
204
  if self.arch == 90:
205
  assert self.cta_tile_shape_mnk[1] % 32 == 0, (
206
  "GemmGatedSm90 requires tileN to be divisible by 32"
207
  )
208
  self.rounding_mode = args.rounding_mode
209
- self.postact_dtype = args.mPostAct.element_type
210
- self.postact_layout = cutlass.utils.LayoutEnum.from_tensor(args.mPostAct)
211
- self.cta_tile_shape_postact_mn = (
212
  self.cta_tile_shape_mnk[0],
213
  self.cta_tile_shape_mnk[1] // 2,
214
  )
215
  d = self._epi_ops_to_params_dict(args)
216
  d["act_fn"] = args.act_fn
217
  for key in ("mRowVecBroadcast", "mColVecBroadcast"):
218
- if key in self.concat_layout and key in d and d[key] is not None:
219
  d[key] = layout_utils.concat_to_interleave(d[key], 1)
220
  return self.EpilogueParams(**d)
221
 
@@ -226,43 +253,96 @@ class GemmGatedMixin(GemmActMixin):
226
  epi_loop_tensors: Tuple[cute.Tensor, ...],
227
  tRS_rD: cute.Tensor,
228
  tRS_rC: Optional[cute.Tensor] = None,
229
- ) -> Optional[cute.Tensor]:
230
  GemmDefaultEpiMixin.epi_visit_subtile(self, params, epi_loop_tensors, tRS_rD, tRS_rC)
231
- tRS_rPostAct_layout = cute.recast_layout(2, 1, tRS_rD.layout)
232
  # If we don't have .shape here, the compiler generates local stores and loads
233
- tRS_rPostAct = cute.make_rmem_tensor(tRS_rPostAct_layout.shape, self.acc_dtype)
234
- if const_expr(self.arch < 100):
235
- for i in cutlass.range(cute.size(tRS_rPostAct), unroll_full=True):
236
- tRS_rPostAct[i] = params.act_fn(tRS_rD[2 * i], tRS_rD[2 * i + 1])
237
- else:
238
- for i in cutlass.range(cute.size(tRS_rPostAct) // 2, unroll_full=True):
239
- tRS_rPostAct[2 * i], tRS_rPostAct[2 * i + 1] = params.act_fn(
240
- (tRS_rD[4 * i], tRS_rD[4 * i + 2]), (tRS_rD[4 * i + 1], tRS_rD[4 * i + 3])
241
- )
242
- return tRS_rPostAct
243
 
244
  @cute.jit
245
- def epi_convert_postact(
246
- self, tRS_rPostAct, sr_seed, tidx, tile_coord_mnkl, num_prev_subtiles, epi_idx
 
 
 
 
 
 
 
247
  ):
248
- tRS_rPostAct_out = GemmActMixin.epi_convert_postact(
249
- self, tRS_rPostAct, sr_seed, tidx, tile_coord_mnkl, num_prev_subtiles, epi_idx
 
 
 
 
 
 
 
250
  )
251
- if const_expr(self.arch == 90):
252
  # Only need this if we're using STSM
253
- permute_gated_Cregs_b16(tRS_rPostAct_out)
254
- return tRS_rPostAct_out
255
 
256
 
257
  class GemmGatedSm90(GemmGatedMixin, GemmSm90):
258
  pass
259
 
260
 
 
 
 
 
261
  class GemmGatedSm100(GemmGatedMixin, GemmSm100):
262
  pass
263
 
264
 
265
- class GemmGatedSm120(GemmGatedMixin, GemmSm120):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  pass
267
 
268
 
@@ -295,13 +375,25 @@ def _compile_gemm_act(
295
  rounding_mode=RoundingMode.RN,
296
  sr_seed_mode=0,
297
  use_tma_gather=False,
 
 
298
  ):
299
  sm_to_cls = {
300
- "act": {9: GemmActSm90, 10: GemmActSm100, 11: GemmActSm100, 12: GemmActSm120},
301
- "gated": {9: GemmGatedSm90, 10: GemmGatedSm100, 11: GemmGatedSm100, 12: GemmGatedSm120},
 
 
 
 
 
 
 
 
 
 
 
 
302
  }
303
- if device_capacity[0] == 12 and gemm_cls_name == "act":
304
- raise NotImplementedError("SM120 non-gated activation GEMM epilogue is not yet supported")
305
  GemmCls = sm_to_cls[gemm_cls_name][device_capacity[0]]
306
  pa_leading = 1 if postact_major == "n" else 0
307
  mA, mB, mD, mC, m, n, k, l = make_fake_gemm_tensors(
@@ -320,7 +412,7 @@ def _compile_gemm_act(
320
  div_pa = div_for_dtype(postact_dtype)
321
  pa_leading_dim = 1 if gemm_cls_name == "gated" else pa_leading
322
  pa_shape = (m, pa_n) if varlen_m else (m, pa_n, l)
323
- mPostAct = fake_tensor(postact_dtype, pa_shape, leading_dim=pa_leading_dim, divisibility=div_pa)
324
 
325
  mRowVec = fake_tensor(rowvec_dtype, (l, n), leading_dim=1, divisibility=4)
326
  if colvec_ndim == 2:
@@ -341,7 +433,7 @@ def _compile_gemm_act(
341
  return make_ptr(dtype, 0, cute.AddressSpace.gmem, assumed_align=4)
342
 
343
  epi_args = GemmCls.EpilogueArguments(
344
- mPostAct,
345
  act_fn,
346
  mRowVecBroadcast=mRowVec,
347
  mColVecBroadcast=mColVec,
@@ -352,6 +444,11 @@ def _compile_gemm_act(
352
  (is_dynamic_persistent and device_capacity[0] == 9), False, l
353
  )
354
  varlen_args = make_fake_varlen_args(varlen_m, False, gather_A, m if varlen_m else None)
 
 
 
 
 
355
  return compile_gemm_kernel(
356
  GemmCls,
357
  a_dtype,
@@ -369,8 +466,11 @@ def _compile_gemm_act(
369
  epi_args,
370
  scheduler_args,
371
  varlen_args,
 
 
372
  use_tma_gather=use_tma_gather,
373
  concat_layout=concat_layout or None,
 
374
  )
375
 
376
 
@@ -386,6 +486,7 @@ def gemm_act(
386
  tile_N: int,
387
  cluster_M: int,
388
  cluster_N: int,
 
389
  pingpong: bool = False,
390
  persistent: bool = True,
391
  is_dynamic_persistent: bool = False,
@@ -398,6 +499,8 @@ def gemm_act(
398
  sr_seed: int | Tensor = 0,
399
  use_tma_gather: bool = False,
400
  concat_layout: tuple | None = None,
 
 
401
  ) -> None:
402
  if activation in gate_fn_map:
403
  gemm_cls_name = "gated"
@@ -407,6 +510,7 @@ def gemm_act(
407
 
408
  varlen_m = cu_seqlens_m is not None
409
  gather_A = A_idx is not None
 
410
  if varlen_m:
411
  assert persistent, "varlen_m requires persistent=True"
412
  assert A.stride(-1) == 1, "varlen_m requires A to be k-major"
@@ -437,7 +541,16 @@ def gemm_act(
437
  colvec_ndim = colvec_bias.ndim if colvec_bias is not None else 0
438
 
439
  device_capacity = get_device_capacity(A.device)
440
- assert device_capacity[0] in [9, 10, 11, 12], "Only SM90, SM100, SM110, and SM120 are supported"
 
 
 
 
 
 
 
 
 
441
  if rounding_mode == RoundingMode.RS:
442
  assert device_capacity[0] == 10, "Stochastic rounding (RoundingMode.RS) requires SM100"
443
 
@@ -461,7 +574,7 @@ def gemm_act(
461
  d_major,
462
  c_major,
463
  postact_major,
464
- (tile_M, tile_N),
465
  (cluster_M, cluster_N, 1),
466
  pingpong,
467
  persistent,
@@ -478,13 +591,10 @@ def gemm_act(
478
  rounding_mode=rounding_mode,
479
  sr_seed_mode=sr_seed_mode,
480
  use_tma_gather=use_tma_gather,
 
 
481
  )
482
 
483
- from .cache_utils import COMPILE_ONLY
484
-
485
- if COMPILE_ONLY:
486
- return
487
-
488
  max_active_clusters = get_max_active_clusters(cluster_M * cluster_N) if persistent else 0
489
 
490
  def scalar_arg(scalar, mode, dtype=Int32):
@@ -511,9 +621,9 @@ def gemm_act(
511
  varlen_args = make_varlen_args(cu_seqlens_m, None, A_idx)
512
 
513
  if device_capacity[0] in [10, 11]:
514
- compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args, None, None, None)
515
  else:
516
- compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args, None)
517
 
518
 
519
  gemm_gated = gemm_act
 
1
  # Copyright (c) 2025, Wentao Guo, Tri Dao.
2
  from __future__ import annotations
3
+ import math
4
+ from typing import NamedTuple, Tuple, Optional, Callable, Type
5
 
6
  from torch import Tensor
7
 
8
  import cutlass
9
  import cutlass.cute as cute
 
10
  import cutlass.utils.blackwell_helpers as sm100_utils
11
  from cutlass import Int32, Float32, const_expr
12
  from cutlass.cute.runtime import make_ptr
13
+ from cutlass.cute.nvgpu import warp
14
 
15
  from .compile_utils import make_fake_tensor as fake_tensor
16
  from .cute_dsl_utils import (
 
17
  mlir_namedtuple,
18
  get_device_capacity,
19
  get_max_active_clusters,
20
  torch2cute_dtype_map,
21
  )
22
+ from .epi_composable import ComposableEpiMixin
23
+ from .epi_ops import ColVecLoad, RowVecLoad, Scalar, TileStore
24
+ from .gemm_sm80 import GemmSm80
25
  from .gemm_sm90 import GemmSm90
26
  from .gemm_sm100 import GemmSm100
27
  from .gemm_sm120 import GemmSm120
 
35
  make_fake_varlen_args,
36
  div_for_dtype,
37
  make_fake_gemm_tensors,
38
+ make_fake_sf_tensor,
39
  compile_gemm_kernel,
40
+ validate_blockscaled_sf,
41
  )
42
+ from .cache import jit_cache
43
  from . import layout_utils as layout_utils
44
+ from . import copy_utils as copy_utils
45
  from .layout_utils import permute_gated_Cregs_b16
46
  from .activation import act_fn_map, gate_fn_map
47
+ from .rounding import RoundingMode, convert_f32_to_bf16_sr, epilogue_aux_out_sr_seed
48
 
49
 
50
+ class GemmActMixin(ComposableEpiMixin):
51
+ _epi_ops = (
52
+ Scalar("alpha"),
53
+ Scalar("beta"),
54
+ Scalar("sr_seed", dtype=Int32),
55
+ RowVecLoad("mRowVecBroadcast"),
56
+ ColVecLoad("mColVecBroadcast"),
57
+ TileStore("mAuxOut"),
58
+ )
59
  _extra_param_fields = (("act_fn", cutlass.Constexpr, None),)
 
60
 
61
  @mlir_namedtuple
62
  class EpilogueArguments(NamedTuple):
63
+ mAuxOut: cute.Tensor
64
  act_fn: cutlass.Constexpr[Optional[Callable]] = None
65
  alpha: Optional[Float32 | cute.Tensor] = None
66
  beta: Optional[Float32 | cute.Tensor] = None
 
73
 
74
  def epi_to_underlying_arguments(self, args: EpilogueArguments, *, loc=None, ip=None):
75
  self.rounding_mode = args.rounding_mode
76
+ self.aux_out_dtype = args.mAuxOut.element_type
77
+ self.aux_out_layout = cutlass.utils.LayoutEnum.from_tensor(args.mAuxOut)
78
+ self.cta_tile_shape_aux_out_mn = self.cta_tile_shape_mnk[:2]
79
  d = self._epi_ops_to_params_dict(args)
80
  d["act_fn"] = args.act_fn
81
  for key in ("mRowVecBroadcast", "mColVecBroadcast"):
82
+ if key in self.concat_layout and key in d:
83
  d[key] = layout_utils.concat_to_interleave(d[key], 1)
84
  return self.EpilogueParams(**d)
85
 
86
+ # epi_get_tma_atoms, epi_smem_bytes, epi_get_smem_struct,
87
  # epi_get_smem_tensors are all inherited from ComposableEpiMixin via _epi_ops.
88
 
89
+ def epi_make_aux_out_copy_atom_r2s(self, params, tiled_copy_t2r):
90
+ """Build the register-to-shared copy atom used by aux outputs."""
91
+ if self.arch == 100:
92
+ return sm100_utils.get_smem_store_op(
93
+ self.aux_out_layout, self.aux_out_dtype, self.acc_dtype, tiled_copy_t2r
94
+ )
95
+ else:
96
+ return copy_utils.get_smem_store_atom(
97
+ self.aux_out_dtype,
98
+ transpose=self.aux_out_layout != cutlass.utils.LayoutEnum.ROW_MAJOR,
99
+ major_mode_size=cute.size(params.epi_tile_mAuxOut, mode=[1])
100
+ // self.atom_layout_mnk[1],
101
+ )
102
+
103
+ def epi_make_aux_out_tiled_copy_r2s(self, params, tiled_copy_r2s, tiled_copy_t2r):
104
+ """Build the register-to-shared tiled copy used by aux outputs."""
105
+ copy_atom_aux_out_r2s = self.epi_make_aux_out_copy_atom_r2s(params, tiled_copy_t2r)
106
+ return cute.make_tiled_copy_S(copy_atom_aux_out_r2s, tiled_copy_r2s)
107
+
108
+ def epi_setup_aux_out(
109
  self,
110
  params,
111
  epi_smem_tensors,
 
115
  varlen_manager,
116
  tidx,
117
  ):
118
+ """Setup aux output TMA copies and partitions before the epilogue loop.
119
+
120
+ Returns an empty tuple when mAuxOut wasn't supplied so the framework
121
+ skips the aux-out path.
122
+ """
123
+ if getattr(params, "mAuxOut", None) is None:
124
+ return ()
125
+ sAuxOut = epi_smem_tensors["mAuxOut"]
126
+ tiled_copy_aux_out_r2s = self.epi_make_aux_out_tiled_copy_r2s(
127
+ params, tiled_copy_r2s, tiled_copy_t2r
128
  )
129
+ tRS_sAuxOut = tiled_copy_aux_out_r2s.get_slice(tidx).partition_D(sAuxOut)
 
130
  batch_idx = tile_coord_mnkl[3]
131
+ copy_aux_out, _, _ = self.epilog_gmem_copy_and_partition(
132
+ params.tma_atom_mAuxOut,
133
+ varlen_manager.offset_batch_epi(params.mAuxOut, batch_idx),
134
+ self.cta_tile_shape_aux_out_mn,
135
+ params.epi_tile_mAuxOut,
136
+ sAuxOut,
137
  tile_coord_mnkl,
138
  )
139
+ return ((tiled_copy_aux_out_r2s, tRS_sAuxOut, copy_aux_out),)
140
 
141
  @cute.jit
142
+ def epi_convert_aux_out(
143
+ self,
144
+ output_idx: cutlass.Constexpr[int],
145
+ tRS_rAuxOut,
146
+ sr_seed,
147
+ tidx,
148
+ tile_coord_mnkl,
149
+ num_prev_subtiles,
150
+ epi_idx,
151
  ):
152
+ """Convert aux output from acc_dtype to aux_out_dtype. Override for custom postprocessing."""
153
  if const_expr(
154
  self.rounding_mode == RoundingMode.RS
155
+ and tRS_rAuxOut.element_type == cutlass.Float32
156
+ and self.aux_out_dtype == cutlass.BFloat16
157
  ):
 
158
  from cutlass.cute.tensor import TensorSSA
159
 
160
+ seed = epilogue_aux_out_sr_seed(sr_seed, tile_coord_mnkl, num_prev_subtiles + epi_idx)
161
+ tRS_rAuxOut_out = cute.make_rmem_tensor_like(tRS_rAuxOut, self.aux_out_dtype)
162
+ src_vec = tRS_rAuxOut.load()
 
 
 
 
 
 
 
 
 
 
163
  raw_vec = convert_f32_to_bf16_sr(src_vec, seed, tidx)
164
+ tRS_rAuxOut_out.store(TensorSSA(raw_vec, src_vec.shape, self.aux_out_dtype))
165
  else:
166
+ tRS_rAuxOut_out = tRS_rAuxOut.to(self.aux_out_dtype)
167
+ return tRS_rAuxOut_out
 
168
 
169
  @cute.jit
170
  def epi_visit_subtile(
 
173
  epi_loop_tensors: Tuple[cute.Tensor, ...],
174
  tRS_rD: cute.Tensor,
175
  tRS_rC: Optional[cute.Tensor] = None,
176
+ ) -> Tuple[cute.Tensor, ...]:
177
  GemmDefaultEpiMixin.epi_visit_subtile(self, params, epi_loop_tensors, tRS_rD, tRS_rC)
178
  # Apply activation function if provided
179
  # If we don't have .shape here, the compiler generates local stores and loads
180
  if const_expr(params.act_fn is not None):
181
+ tRS_rAuxOut = cute.make_rmem_tensor(tRS_rD.layout.shape, self.acc_dtype)
182
+ vectorize = const_expr(self.arch == 100)
183
+ for i in cutlass.range(cute.size(tRS_rAuxOut), unroll_full=True, vectorize=vectorize):
184
+ tRS_rAuxOut[i] = params.act_fn(tRS_rD[i])
 
 
 
 
 
185
  else:
186
+ tRS_rAuxOut = tRS_rD
187
+ return (tRS_rAuxOut,)
188
 
189
 
190
  class GemmActSm90(GemmActMixin, GemmSm90):
191
  pass
192
 
193
 
194
+ class GemmActSm80(GemmActMixin, GemmSm80):
195
+ pass
196
+
197
+
198
  class GemmActSm100(GemmActMixin, GemmSm100):
199
  pass
200
 
 
212
 
213
  class GemmGatedMixin(GemmActMixin):
214
  _epi_ops = (
215
+ Scalar("alpha"),
216
+ Scalar("beta"),
217
+ Scalar("sr_seed", dtype=Int32),
218
+ RowVecLoad("mRowVecBroadcast"),
219
+ ColVecLoad("mColVecBroadcast"),
220
+ TileStore("mAuxOut", epi_tile_fn=_gated_epi_tile_fn),
221
  )
222
 
223
  def epi_to_underlying_arguments(
224
  self, args: GemmActMixin.EpilogueArguments, *, loc=None, ip=None
225
  ) -> GemmActMixin.EpilogueParams:
226
+ assert args.mAuxOut.element_type.width == 16, (
227
  "GemmGated only supports 16bit postact for now"
228
  )
229
  assert self.d_layout is None or self.d_layout.is_n_major_c()
230
+ assert cutlass.utils.LayoutEnum.from_tensor(args.mAuxOut).is_n_major_c()
231
  if self.arch == 90:
232
  assert self.cta_tile_shape_mnk[1] % 32 == 0, (
233
  "GemmGatedSm90 requires tileN to be divisible by 32"
234
  )
235
  self.rounding_mode = args.rounding_mode
236
+ self.aux_out_dtype = args.mAuxOut.element_type
237
+ self.aux_out_layout = cutlass.utils.LayoutEnum.from_tensor(args.mAuxOut)
238
+ self.cta_tile_shape_aux_out_mn = (
239
  self.cta_tile_shape_mnk[0],
240
  self.cta_tile_shape_mnk[1] // 2,
241
  )
242
  d = self._epi_ops_to_params_dict(args)
243
  d["act_fn"] = args.act_fn
244
  for key in ("mRowVecBroadcast", "mColVecBroadcast"):
245
+ if key in self.concat_layout and key in d:
246
  d[key] = layout_utils.concat_to_interleave(d[key], 1)
247
  return self.EpilogueParams(**d)
248
 
 
253
  epi_loop_tensors: Tuple[cute.Tensor, ...],
254
  tRS_rD: cute.Tensor,
255
  tRS_rC: Optional[cute.Tensor] = None,
256
+ ) -> Tuple[cute.Tensor, ...]:
257
  GemmDefaultEpiMixin.epi_visit_subtile(self, params, epi_loop_tensors, tRS_rD, tRS_rC)
258
+ tRS_rAuxOut_layout = cute.recast_layout(2, 1, tRS_rD.layout)
259
  # If we don't have .shape here, the compiler generates local stores and loads
260
+ tRS_rAuxOut = cute.make_rmem_tensor(tRS_rAuxOut_layout.shape, self.acc_dtype)
261
+ tRS_rD_pair = cute.flat_divide(tRS_rD, cute.make_layout(2))
262
+ tRS_rGate = tRS_rD_pair[0, ...]
263
+ tRS_rUp = tRS_rD_pair[1, ...]
264
+ vectorize = const_expr(self.arch == 100)
265
+ for i in cutlass.range(cute.size(tRS_rAuxOut), unroll_full=True, vectorize=vectorize):
266
+ tRS_rAuxOut[i] = params.act_fn(tRS_rGate[i], tRS_rUp[i])
267
+ return (tRS_rAuxOut,)
 
 
268
 
269
  @cute.jit
270
+ def epi_convert_aux_out(
271
+ self,
272
+ output_idx: cutlass.Constexpr[int],
273
+ tRS_rAuxOut,
274
+ sr_seed,
275
+ tidx,
276
+ tile_coord_mnkl,
277
+ num_prev_subtiles,
278
+ epi_idx,
279
  ):
280
+ tRS_rAuxOut_out = GemmActMixin.epi_convert_aux_out(
281
+ self,
282
+ output_idx,
283
+ tRS_rAuxOut,
284
+ sr_seed,
285
+ tidx,
286
+ tile_coord_mnkl,
287
+ num_prev_subtiles,
288
+ epi_idx,
289
  )
290
+ if const_expr(self.arch in (90, 120)):
291
  # Only need this if we're using STSM
292
+ permute_gated_Cregs_b16(tRS_rAuxOut_out)
293
+ return tRS_rAuxOut_out
294
 
295
 
296
  class GemmGatedSm90(GemmGatedMixin, GemmSm90):
297
  pass
298
 
299
 
300
+ class GemmGatedSm80(GemmGatedMixin, GemmSm80):
301
+ pass
302
+
303
+
304
  class GemmGatedSm100(GemmGatedMixin, GemmSm100):
305
  pass
306
 
307
 
308
+ class GemmGatedSm120Mixin:
309
+ @staticmethod
310
+ def _compute_tile_shape_or_override(
311
+ cta_tile_shape_mnk: Tuple[int, int, int],
312
+ atom_layout_mnk: Tuple[int, int, int],
313
+ element_type: Optional[Type[cutlass.Numeric]] = None,
314
+ epi_tile_override: Tuple[int, int] | None = None,
315
+ ) -> Tuple[int, int]:
316
+ if epi_tile_override is not None:
317
+ return epi_tile_override
318
+ # Typically epi_tile is (64, 32) but since we want tile_n = 64 (see below), we might set
319
+ # tile_m = 32 if there's only 2 warps along the M direction.
320
+ tile_m = math.gcd(atom_layout_mnk[0] * 16, cute.size(cta_tile_shape_mnk, mode=[0]))
321
+ atom_n = atom_layout_mnk[1]
322
+ # E.g. if we have 2 warps along N direction, we want each warp to have 32 elems so that
323
+ # postact has 16 elements, which means tile_n should be 64.
324
+ tile_n = math.gcd(atom_n * 8 * 4, cute.size(cta_tile_shape_mnk, mode=[1]))
325
+ return (tile_m, tile_n)
326
+
327
+ def epi_make_aux_out_tiled_copy_r2s(self, params, tiled_copy_r2s, tiled_copy_t2r):
328
+ copy_atom_aux_out_r2s = self.epi_make_aux_out_copy_atom_r2s(params, tiled_copy_t2r)
329
+ copy_atom_postact_c = self.epi_make_aux_out_copy_atom_r2s(params, cutlass.Float16)
330
+ op = warp.MmaF16BF16Op(self.a_dtype, self.acc_dtype, self.mma_inst_mnk)
331
+ tC = cute.make_layout(self.atom_layout_mnk)
332
+ atom_m, atom_n, atom_k = self.atom_layout_mnk
333
+ permutation_mnk = (
334
+ self.mma_inst_mnk[0] * atom_m,
335
+ self.mma_inst_mnk[1] * atom_n * 2,
336
+ self.mma_inst_mnk[2] * atom_k,
337
+ )
338
+ tiled_mma_gated_postact = cute.make_tiled_mma(op, tC, permutation_mnk=permutation_mnk)
339
+ tiled_copy_aux_out_c_atom = cute.make_tiled_copy_C_atom(
340
+ copy_atom_postact_c, tiled_mma_gated_postact
341
+ )
342
+ return cute.make_tiled_copy_S(copy_atom_aux_out_r2s, tiled_copy_aux_out_c_atom)
343
+
344
+
345
+ class GemmGatedSm120(GemmGatedSm120Mixin, GemmGatedMixin, GemmSm120):
346
  pass
347
 
348
 
 
375
  rounding_mode=RoundingMode.RN,
376
  sr_seed_mode=0,
377
  use_tma_gather=False,
378
+ sf_dtype=None,
379
+ sf_vec_size=None,
380
  ):
381
  sm_to_cls = {
382
+ "act": {
383
+ 8: GemmActSm80,
384
+ 9: GemmActSm90,
385
+ 10: GemmActSm100,
386
+ 11: GemmActSm100,
387
+ 12: GemmActSm120,
388
+ },
389
+ "gated": {
390
+ 8: GemmGatedSm80,
391
+ 9: GemmGatedSm90,
392
+ 10: GemmGatedSm100,
393
+ 11: GemmGatedSm100,
394
+ 12: GemmGatedSm120,
395
+ },
396
  }
 
 
397
  GemmCls = sm_to_cls[gemm_cls_name][device_capacity[0]]
398
  pa_leading = 1 if postact_major == "n" else 0
399
  mA, mB, mD, mC, m, n, k, l = make_fake_gemm_tensors(
 
412
  div_pa = div_for_dtype(postact_dtype)
413
  pa_leading_dim = 1 if gemm_cls_name == "gated" else pa_leading
414
  pa_shape = (m, pa_n) if varlen_m else (m, pa_n, l)
415
+ mAuxOut = fake_tensor(postact_dtype, pa_shape, leading_dim=pa_leading_dim, divisibility=div_pa)
416
 
417
  mRowVec = fake_tensor(rowvec_dtype, (l, n), leading_dim=1, divisibility=4)
418
  if colvec_ndim == 2:
 
433
  return make_ptr(dtype, 0, cute.AddressSpace.gmem, assumed_align=4)
434
 
435
  epi_args = GemmCls.EpilogueArguments(
436
+ mAuxOut,
437
  act_fn,
438
  mRowVecBroadcast=mRowVec,
439
  mColVecBroadcast=mColVec,
 
444
  (is_dynamic_persistent and device_capacity[0] == 9), False, l
445
  )
446
  varlen_args = make_fake_varlen_args(varlen_m, False, gather_A, m if varlen_m else None)
447
+ if sf_dtype is not None:
448
+ mSFA = make_fake_sf_tensor(sf_dtype, l)
449
+ mSFB = make_fake_sf_tensor(sf_dtype, l)
450
+ else:
451
+ mSFA, mSFB = None, None
452
  return compile_gemm_kernel(
453
  GemmCls,
454
  a_dtype,
 
466
  epi_args,
467
  scheduler_args,
468
  varlen_args,
469
+ mSFA=mSFA,
470
+ mSFB=mSFB,
471
  use_tma_gather=use_tma_gather,
472
  concat_layout=concat_layout or None,
473
+ sf_vec_size=sf_vec_size,
474
  )
475
 
476
 
 
486
  tile_N: int,
487
  cluster_M: int,
488
  cluster_N: int,
489
+ tile_K: int | None = None,
490
  pingpong: bool = False,
491
  persistent: bool = True,
492
  is_dynamic_persistent: bool = False,
 
499
  sr_seed: int | Tensor = 0,
500
  use_tma_gather: bool = False,
501
  concat_layout: tuple | None = None,
502
+ SFA: Optional[Tensor] = None, # (l, rm, rk, 32, 4, 4) blocked scale factors
503
+ SFB: Optional[Tensor] = None, # (l, rn, rk, 32, 4, 4)
504
  ) -> None:
505
  if activation in gate_fn_map:
506
  gemm_cls_name = "gated"
 
510
 
511
  varlen_m = cu_seqlens_m is not None
512
  gather_A = A_idx is not None
513
+ blockscaled = SFA is not None
514
  if varlen_m:
515
  assert persistent, "varlen_m requires persistent=True"
516
  assert A.stride(-1) == 1, "varlen_m requires A to be k-major"
 
541
  colvec_ndim = colvec_bias.ndim if colvec_bias is not None else 0
542
 
543
  device_capacity = get_device_capacity(A.device)
544
+ assert device_capacity[0] in [8, 9, 10, 11, 12], (
545
+ "Only SM8x, SM90, SM100, SM110, and SM120 are supported"
546
+ )
547
+ sf_dtype, sf_vec_size = None, None
548
+ if blockscaled:
549
+ assert not varlen_m and not gather_A, "Blockscaled GEMM does not support varlen/gather yet"
550
+ assert not concat_layout, "Blockscaled GEMM does not support concat_layout"
551
+ assert tile_K is None, "Blockscaled GEMM derives tile_K from the MMA instruction"
552
+ # A / B are still (l, m, k) / (l, n, k) here (perm3d only made views).
553
+ sf_dtype, sf_vec_size = validate_blockscaled_sf(A, B, SFA, SFB, device_capacity)
554
  if rounding_mode == RoundingMode.RS:
555
  assert device_capacity[0] == 10, "Stochastic rounding (RoundingMode.RS) requires SM100"
556
 
 
574
  d_major,
575
  c_major,
576
  postact_major,
577
+ (tile_M, tile_N, tile_K) if tile_K is not None else (tile_M, tile_N),
578
  (cluster_M, cluster_N, 1),
579
  pingpong,
580
  persistent,
 
591
  rounding_mode=rounding_mode,
592
  sr_seed_mode=sr_seed_mode,
593
  use_tma_gather=use_tma_gather,
594
+ sf_dtype=sf_dtype,
595
+ sf_vec_size=sf_vec_size,
596
  )
597
 
 
 
 
 
 
598
  max_active_clusters = get_max_active_clusters(cluster_M * cluster_N) if persistent else 0
599
 
600
  def scalar_arg(scalar, mode, dtype=Int32):
 
621
  varlen_args = make_varlen_args(cu_seqlens_m, None, A_idx)
622
 
623
  if device_capacity[0] in [10, 11]:
624
+ compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args, SFA, SFB)
625
  else:
626
+ compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args)
627
 
628
 
629
  gemm_gated = gemm_act
build/torch-cuda/quack/gemm_base.py ADDED
@@ -0,0 +1,731 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2026, Tri Dao.
2
+
3
+ import enum
4
+ import math
5
+ from dataclasses import dataclass
6
+ from typing import Callable, Dict, Literal, Optional, Sequence, Tuple
7
+
8
+ import cutlass
9
+ import cutlass.cute as cute
10
+ import cutlass.pipeline as pipeline
11
+ from cutlass import Boolean, Int32, const_expr
12
+ from cutlass.cute.nvgpu import cpasync
13
+ from cutlass.utils import LayoutEnum
14
+
15
+ from . import copy_utils as copy_utils
16
+ from .cute_dsl_utils import ParamsBase
17
+ from .epi_ops import EpiSmemBytes
18
+ from .pipeline import PipelineTmaAsync, PipelineTmaCpAsync
19
+ from .rounding import RoundingMode, epilogue_sr_seed
20
+ from .tile_scheduler import (
21
+ PersistenceMode,
22
+ TileScheduler,
23
+ TileSchedulerArguments,
24
+ VarlenMTileScheduler,
25
+ VarlenMTileSchedulerArguments,
26
+ )
27
+ from .varlen_utils import VarlenManager
28
+
29
+
30
+ class NamedBarrierGemm(enum.IntEnum):
31
+ Epilogue = enum.auto() # starts from 1 as barrier 0 is reserved for sync_threads()
32
+ # For mainloop load warps to signal that the epilogue load warp can start.
33
+ # This is to avoid loading C too early, interfering with loading A and B.
34
+ EpilogueLoad = enum.auto()
35
+ MmaWG0 = enum.auto()
36
+ MmaWG1 = enum.auto()
37
+ EpiWG0 = enum.auto()
38
+ EpiWG1 = enum.auto()
39
+ TmemPtr = enum.auto()
40
+ # CLC-multicast throttle: CTA0 load warp arrives once per tile started,
41
+ # CTA0 scheduler warp syncs once per CLC query (2 warps, 64 threads).
42
+ ClcThrottle = enum.auto()
43
+
44
+
45
+ class GemmBase:
46
+ """Common non-mainloop pieces shared by GEMM architectures."""
47
+
48
+ arch = 0
49
+
50
+ @dataclass
51
+ class EpilogueArguments:
52
+ pass
53
+
54
+ EpilogueParams = ParamsBase
55
+
56
+ def epi_smem_warp_shape_mnk(self):
57
+ return (self.num_epi_warps, 1, 1)
58
+
59
+ @cute.jit
60
+ def epilogue(
61
+ self,
62
+ params: EpilogueParams,
63
+ epi_smem_tensors: Dict[str, cute.Tensor],
64
+ epi_pipeline: Optional[cutlass.pipeline.PipelineAsync],
65
+ epi_store_pipeline: Optional[cutlass.pipeline.PipelineAsync],
66
+ epi_read_state: Optional[cutlass.pipeline.PipelineState],
67
+ epi_producer_state: Optional[cutlass.pipeline.PipelineState],
68
+ epi_tile: cute.Tile,
69
+ load_acc_subtile: Callable,
70
+ tRS_rD: cute.Tensor,
71
+ tRS_rC: Optional[cute.Tensor],
72
+ tiled_copy_t2r: Optional[cute.TiledCopy], # Only for Sm100
73
+ tiled_copy_r2s: cute.TiledCopy,
74
+ tRS_sD: cute.Tensor,
75
+ tiled_copy_s2r: Optional[cute.ThrCopy],
76
+ tSR_rC: Optional[cute.Tensor],
77
+ tSR_sC: Optional[cute.Tensor],
78
+ copy_D: Optional[Callable],
79
+ copy_C: Optional[Callable],
80
+ tile_coord_mnkl: cute.Coord,
81
+ varlen_manager: VarlenManager,
82
+ epilogue_barrier: cutlass.pipeline.NamedBarrier,
83
+ tile_scheduler,
84
+ tidx: Int32,
85
+ is_tma_warp: cutlass.Boolean,
86
+ ) -> Tuple[cutlass.pipeline.PipelineState, cutlass.pipeline.PipelineState]:
87
+ has_C = const_expr(tRS_rC is not None)
88
+ has_epi_load = const_expr(self.epi_c_stage > 0)
89
+ has_D = const_expr(copy_D is not None)
90
+ use_tma_epi = const_expr(epi_store_pipeline is not None)
91
+ use_tma_c = const_expr(epi_pipeline is not None)
92
+ inline_epi_load = const_expr(copy_C is not None)
93
+ use_stochastic_rounding = const_expr(
94
+ self.rounding_mode == RoundingMode.RS
95
+ and self.acc_dtype == cutlass.Float32
96
+ and self.d_dtype == cutlass.BFloat16
97
+ )
98
+
99
+ # Setup aux outputs. Returns a tuple of ``(tiled_copy_r2s,
100
+ # tRS_sAuxOut, copy_aux_out)`` triples — empty for the default
101
+ # epilogue, one entry for the standard ``GemmAct``/``GemmGated``
102
+ # single-output mixins, multiple entries for multi-output mixins
103
+ # (e.g. ``T*tanh`` + ``1-tanh^2`` from one GEMM).
104
+ aux_out_ctxs = self.epi_setup_aux_out(
105
+ params,
106
+ epi_smem_tensors,
107
+ tiled_copy_r2s,
108
+ tiled_copy_t2r,
109
+ tile_coord_mnkl,
110
+ varlen_manager,
111
+ tidx,
112
+ )
113
+
114
+ epi_tile_shape = cute.zipped_divide(
115
+ cute.make_layout(self.cta_tile_shape_mnk[:2]), epi_tile
116
+ ).shape[1]
117
+ epi_tile_layout = cute.make_ordered_layout(
118
+ epi_tile_shape, order=(0, 1) if const_expr(self.epi_m_major) else (1, 0)
119
+ )
120
+ epi_tile_num = cute.size(epi_tile_shape)
121
+ num_prev_subtiles = tile_scheduler.num_tiles_executed * epi_tile_num
122
+
123
+ epi_tensors = self.epi_begin(
124
+ params,
125
+ epi_smem_tensors,
126
+ epi_tile,
127
+ tiled_copy_t2r,
128
+ tiled_copy_r2s,
129
+ tile_coord_mnkl,
130
+ varlen_manager,
131
+ epilogue_barrier,
132
+ tidx,
133
+ tRS_rD.layout,
134
+ )
135
+
136
+ if const_expr(inline_epi_load):
137
+ for epi_idx in cutlass.range(min(epi_tile_num, self.epi_c_stage), unroll=1):
138
+ epi_coord_C = epi_tile_layout.get_hier_coord(epi_idx)
139
+ if const_expr(use_tma_c):
140
+ if is_tma_warp:
141
+ epi_pipeline.producer_acquire(epi_producer_state)
142
+ copy_C(src_idx=epi_coord_C, producer_state=epi_producer_state)
143
+ epi_pipeline.producer_commit(epi_producer_state)
144
+ epi_producer_state.advance()
145
+ else:
146
+ # TODO: turn this to cp.async instead of direct G2R copy
147
+ copy_C(src_idx=epi_coord_C, dst_idx=epi_idx % self.epi_c_stage)
148
+ if const_expr(use_tma_c):
149
+ epilogue_barrier.arrive_and_wait()
150
+
151
+ for epi_idx in cutlass.range_constexpr(epi_tile_num):
152
+ epi_coord = epi_tile_layout.get_hier_coord(epi_idx) # (epi_m, epi_n)
153
+ # Copy from acc to D registers
154
+ load_acc_subtile(tRS_rD, epi_coord)
155
+ if const_expr(has_epi_load):
156
+ if const_expr(use_tma_c):
157
+ epi_pipeline.consumer_wait(epi_read_state)
158
+ if const_expr(has_C):
159
+ cute.copy(
160
+ tiled_copy_s2r, tSR_sC[None, None, None, epi_read_state.index], tSR_rC
161
+ )
162
+ self.epi_tile_load_s2r(params, epi_tensors, epi_read_state.index)
163
+ cute.arch.fence_view_async_shared()
164
+ epi_pipeline.consumer_release(epi_read_state)
165
+ epi_read_state.advance()
166
+ else:
167
+ c_buffer = epi_idx % self.epi_c_stage
168
+ cute.copy(tiled_copy_s2r, tSR_sC[None, None, None, c_buffer], tSR_rC)
169
+ # TODO: cp.async wait once we switch to cp.async
170
+ epilogue_barrier.arrive_and_wait()
171
+ epi_loop_tensors = self.epi_begin_loop(params, epi_tensors, epi_coord)
172
+ if const_expr(inline_epi_load and epi_idx + self.epi_c_stage < epi_tile_num):
173
+ epi_coord_C = epi_tile_layout.get_hier_coord(epi_idx + self.epi_c_stage)
174
+ if const_expr(use_tma_c):
175
+ if is_tma_warp:
176
+ epi_pipeline.producer_acquire(epi_producer_state)
177
+ copy_C(src_idx=epi_coord_C, producer_state=epi_producer_state)
178
+ epi_pipeline.producer_commit(epi_producer_state)
179
+ epi_producer_state.advance()
180
+ else:
181
+ epilogue_barrier.arrive_and_wait()
182
+ copy_C(
183
+ src_idx=epi_coord_C,
184
+ dst_idx=(epi_idx + self.epi_c_stage) % self.epi_c_stage,
185
+ )
186
+ # Returns a tuple of register tensors — one per aux output.
187
+ # Length matches ``aux_out_ctxs``. ``()`` for the default
188
+ # epilogue (no aux output).
189
+ tRS_rAuxOuts = self.epi_visit_subtile(params, epi_loop_tensors, tRS_rD, tRS_rC)
190
+ self.epi_end_loop(
191
+ params,
192
+ epi_tensors,
193
+ epi_coord,
194
+ epi_tile,
195
+ tiled_copy_t2r,
196
+ tiled_copy_r2s,
197
+ tile_coord_mnkl,
198
+ varlen_manager,
199
+ tidx,
200
+ )
201
+ # Convert each output to its storage dtype.
202
+ tRS_rAuxOuts_out = tuple(
203
+ self.epi_convert_aux_out(
204
+ i,
205
+ tRS_rAuxOuts[i],
206
+ epi_loop_tensors.get("sr_seed"),
207
+ tidx,
208
+ tile_coord_mnkl,
209
+ num_prev_subtiles,
210
+ epi_idx,
211
+ )
212
+ for i in range(len(aux_out_ctxs))
213
+ )
214
+ if const_expr(use_tma_epi):
215
+ if is_tma_warp:
216
+ epi_store_pipeline.producer_acquire()
217
+ else:
218
+ epilogue_barrier.arrive_and_wait()
219
+ if const_expr(use_tma_epi):
220
+ epilogue_barrier.arrive_and_wait()
221
+ epi_buffer = (num_prev_subtiles + epi_idx) % self.epi_stage
222
+ if const_expr(has_D):
223
+ tRS_sD_cur = tRS_sD[None, None, None, epi_buffer]
224
+ if const_expr(use_stochastic_rounding):
225
+ seed = epilogue_sr_seed(
226
+ epi_loop_tensors.get("sr_seed"),
227
+ tile_coord_mnkl,
228
+ num_prev_subtiles + epi_idx,
229
+ )
230
+ copy_utils.sr_cvt_copy(tiled_copy_r2s, tRS_rD, tRS_sD_cur, seed, tidx)
231
+ else:
232
+ copy_utils.cvt_copy(tiled_copy_r2s, tRS_rD, tRS_sD_cur)
233
+ # Copy each aux output from registers to shared memory. All share
234
+ # the same ``epi_buffer`` index so the s2g TMA stores below happen
235
+ # in lockstep after the fence.
236
+ for i in cutlass.range_constexpr(len(aux_out_ctxs)):
237
+ tiled_copy_aux_out_r2s, tRS_sAuxOut, _ = aux_out_ctxs[i]
238
+ cute.copy(
239
+ tiled_copy_aux_out_r2s,
240
+ # Need contiguous for Sm80 and Sm120 where acc layout is ((2, 2), MMA_M, MMA_N)
241
+ tiled_copy_aux_out_r2s.retile(tRS_rAuxOuts_out[i]).contiguous(),
242
+ tRS_sAuxOut[None, None, None, epi_buffer],
243
+ )
244
+ if const_expr(use_tma_epi):
245
+ cute.arch.fence_view_async_shared()
246
+ epilogue_barrier.arrive_and_wait()
247
+ if is_tma_warp:
248
+ if const_expr(has_D):
249
+ copy_D(src_idx=epi_buffer, dst_idx=epi_coord)
250
+ for i in cutlass.range_constexpr(len(aux_out_ctxs)):
251
+ _, _, copy_aux_out = aux_out_ctxs[i]
252
+ copy_aux_out(src_idx=epi_buffer, dst_idx=epi_coord)
253
+ epi_store_pipeline.producer_commit()
254
+ else:
255
+ epilogue_barrier.arrive_and_wait()
256
+ if const_expr(has_D):
257
+ copy_D(src_idx=epi_buffer, dst_idx=epi_coord)
258
+ for i in cutlass.range_constexpr(len(aux_out_ctxs)):
259
+ _, _, copy_aux_out = aux_out_ctxs[i]
260
+ copy_aux_out(src_idx=epi_buffer, dst_idx=epi_coord)
261
+ epilogue_barrier.arrive_and_wait()
262
+
263
+ self.epi_end(
264
+ params,
265
+ epi_tensors,
266
+ epi_tile,
267
+ tiled_copy_t2r,
268
+ tiled_copy_r2s,
269
+ tile_coord_mnkl,
270
+ varlen_manager,
271
+ tidx,
272
+ )
273
+
274
+ return epi_read_state, epi_producer_state
275
+
276
+ def get_scheduler_class(self, varlen_m: bool = False):
277
+ """Return the scheduler class to use. Override in subclasses for custom schedulers."""
278
+ return TileScheduler if not varlen_m else VarlenMTileScheduler
279
+
280
+ def resolve_epi_m_major(self, epilogue_args: EpilogueArguments):
281
+ return True
282
+
283
+ def get_scheduler_arguments(
284
+ self,
285
+ mA: cute.Tensor,
286
+ mB: cute.Tensor,
287
+ mD: Optional[cute.Tensor],
288
+ scheduler_args,
289
+ varlen_args,
290
+ epilogue_args,
291
+ ):
292
+ """Create scheduler arguments. Override in subclasses for custom schedulers."""
293
+ if const_expr(not self.is_persistent):
294
+ persistence_mode = PersistenceMode.NONE
295
+ else:
296
+ if const_expr(self.arch >= 100 and self.use_clc_persistence):
297
+ persistence_mode = PersistenceMode.CLC
298
+ elif const_expr(scheduler_args.tile_count_semaphore is not None):
299
+ persistence_mode = PersistenceMode.DYNAMIC
300
+ else:
301
+ persistence_mode = PersistenceMode.STATIC
302
+ if const_expr(varlen_args.mCuSeqlensM is None):
303
+ num_problems = (
304
+ mD.shape[2]
305
+ if mD is not None
306
+ else (
307
+ mB.shape[2]
308
+ if varlen_args.mCuSeqlensK is None
309
+ else varlen_args.mCuSeqlensK.shape[0] - 1
310
+ )
311
+ )
312
+ problem_shape_ntile_mnl = (
313
+ cute.ceil_div(cute.size(mA, mode=[0]), self.cta_tile_shape_mnk[0]),
314
+ cute.ceil_div(cute.size(mB, mode=[0]), self.cta_tile_shape_mnk[1]),
315
+ num_problems,
316
+ )
317
+ tile_sched_args = TileSchedulerArguments(
318
+ problem_shape_ntile_mnl=problem_shape_ntile_mnl,
319
+ raster_order=scheduler_args.raster_order,
320
+ group_size=scheduler_args.max_swizzle_size,
321
+ cluster_shape_mnk=self.cluster_shape_mnk,
322
+ tile_count_semaphore=scheduler_args.tile_count_semaphore,
323
+ batch_idx_permute=scheduler_args.batch_idx_permute,
324
+ persistence_mode=persistence_mode,
325
+ )
326
+ else:
327
+ assert (mD is not None) or (epilogue_args.mAuxOut is not None) or (not self.gather_A)
328
+ problem_shape_ntile_mnl = (
329
+ None,
330
+ cute.ceil_div(cute.size(mB, mode=[0]), self.cta_tile_shape_mnk[1]),
331
+ varlen_args.mCuSeqlensM.shape[0] - 1,
332
+ )
333
+ tile_sched_args = VarlenMTileSchedulerArguments(
334
+ problem_shape_ntile_mnl=problem_shape_ntile_mnl,
335
+ total_m=(
336
+ mD.shape[0]
337
+ if mD is not None
338
+ else (
339
+ varlen_args.mAIdx.shape[0]
340
+ if varlen_args.mAIdx is not None
341
+ else cute.size(mA, mode=[0])
342
+ )
343
+ ),
344
+ cu_seqlens_m=varlen_args.mCuSeqlensM,
345
+ max_active_clusters=scheduler_args.max_active_clusters,
346
+ raster_order=scheduler_args.raster_order,
347
+ group_size=scheduler_args.max_swizzle_size,
348
+ tile_shape_mn=self.cta_tile_shape_mnk[:2],
349
+ cluster_shape_mnk=self.cluster_shape_mnk,
350
+ tile_count_semaphore=scheduler_args.tile_count_semaphore,
351
+ persistence_mode=persistence_mode,
352
+ )
353
+ return tile_sched_args
354
+
355
+ @cute.jit
356
+ def epi_load_acc_subtile(
357
+ self,
358
+ tRS_rAcc: cute.Tensor,
359
+ tRS_rD: cute.Tensor,
360
+ epi_coord, # (int, int)
361
+ ):
362
+ cute.autovec_copy(tRS_rAcc[None, None, None, epi_coord], tRS_rD)
363
+
364
+ @cute.jit
365
+ def epi_begin(
366
+ self,
367
+ params: EpilogueParams,
368
+ epi_smem_tensors: Dict[str, cute.Tensor],
369
+ epi_tile: cute.Tile,
370
+ tiled_copy_t2r: Optional[cute.TiledCopy],
371
+ tiled_copy_r2s: cute.TiledCopy,
372
+ tile_coord_mnkl: cute.Coord,
373
+ varlen_manager: VarlenManager,
374
+ epilogue_barrier: cutlass.pipeline.NamedBarrier,
375
+ tidx: Int32,
376
+ tRS_rD_layout=None,
377
+ ) -> Tuple[cute.Tensor, ...]:
378
+ return ()
379
+
380
+ def epi_begin_loop(
381
+ self, params: EpilogueParams, epi_tensors: Tuple[cute.Tensor, ...], epi_coord: cute.Coord
382
+ ) -> Tuple[cute.Tensor, ...]:
383
+ return ()
384
+
385
+ def epi_visit_subtile(
386
+ self,
387
+ params: EpilogueParams,
388
+ epi_loop_tensors: Tuple[cute.Tensor, ...],
389
+ tRS_rD: cute.Tensor,
390
+ tRS_rC: Optional[cute.Tensor] = None,
391
+ ) -> Tuple[cute.Tensor, ...]:
392
+ return ()
393
+
394
+ def epi_visit_acc(
395
+ self,
396
+ params: EpilogueParams,
397
+ acc: cute.Tensor,
398
+ tiled_mma: cute.TiledMma,
399
+ tile_coord_mnkl: cute.Coord,
400
+ tidx: Int32,
401
+ ) -> None:
402
+ pass
403
+
404
+ @cute.jit
405
+ def epi_end_loop(
406
+ self,
407
+ params: EpilogueParams,
408
+ epi_tensors: Tuple[cute.Tensor, ...],
409
+ epi_coord: cute.Coord,
410
+ epi_tile: cute.Tile,
411
+ tiled_copy_t2r: Optional[cute.TiledCopy],
412
+ tiled_copy_r2s: cute.TiledCopy,
413
+ tile_coord_mnkl: cute.Coord,
414
+ varlen_manager,
415
+ tidx,
416
+ ) -> None:
417
+ pass
418
+
419
+ @cute.jit
420
+ def epi_end(
421
+ self,
422
+ params: EpilogueParams,
423
+ epi_tensors: Tuple[cute.Tensor, ...],
424
+ epi_tile: cute.Tile,
425
+ tiled_copy_t2r: Optional[cute.TiledCopy],
426
+ tiled_copy_r2s: cute.TiledCopy,
427
+ tile_coord_mnkl: cute.Coord,
428
+ varlen_manager,
429
+ tidx,
430
+ ) -> None:
431
+ pass
432
+
433
+ def epi_to_underlying_arguments(
434
+ self, args: EpilogueArguments, *, loc=None, ip=None
435
+ ) -> EpilogueParams:
436
+ return self.EpilogueParams()
437
+
438
+ def epi_get_tma_atoms(
439
+ self, params: EpilogueParams, *, loc=None, ip=None
440
+ ) -> list[cute.CopyAtom]:
441
+ """Subclasses can override this."""
442
+ return []
443
+
444
+ def epi_tile_load_g2s_copy_fns(
445
+ self,
446
+ params,
447
+ epi_smem_tensors,
448
+ tile_coord_mnkl,
449
+ varlen_manager,
450
+ epi_pipeline,
451
+ ):
452
+ return ()
453
+
454
+ @cute.jit
455
+ def epi_tile_load_s2r(self, params, epi_tensors, stage_idx):
456
+ pass
457
+
458
+ @staticmethod
459
+ def epi_smem_bytes(
460
+ args: Optional[EpilogueArguments],
461
+ cta_tile_shape_mnk: Tuple[int, int, int],
462
+ epi_tile: cute.Tile,
463
+ warp_shape_mnk: Tuple[int, int, int] | None = None,
464
+ ) -> EpiSmemBytes:
465
+ return EpiSmemBytes()
466
+
467
+ def epi_get_smem_struct(self, params: EpilogueParams):
468
+ return cute.struct.MemRange[Int32, 0] # Dummy struct
469
+
470
+ def epi_get_smem_tensors(self, params: EpilogueParams, storage) -> Dict[str, cute.Tensor]:
471
+ return {}
472
+
473
+ def epi_setup_aux_out(
474
+ self,
475
+ params,
476
+ epi_smem_tensors,
477
+ tiled_copy_r2s,
478
+ tiled_copy_t2r,
479
+ tile_coord_mnkl,
480
+ varlen_manager,
481
+ tidx,
482
+ ):
483
+ """Return a tuple of ``(tiled_copy_r2s, tRS_sAuxOut, copy_aux_out)``
484
+ triples — one per aux output. The default epilogue has no aux output,
485
+ so the tuple is empty.
486
+ """
487
+ return ()
488
+
489
+ @cute.jit
490
+ def epi_convert_aux_out(
491
+ self,
492
+ output_idx: cutlass.Constexpr[int],
493
+ tRS_rAuxOut,
494
+ sr_seed,
495
+ tidx,
496
+ tile_coord_mnkl,
497
+ num_prev_subtiles,
498
+ epi_idx,
499
+ ):
500
+ """Convert one aux output register tensor from acc_dtype to its storage
501
+ dtype. ``output_idx`` selects which aux output this call is for
502
+ (single-output mixins can ignore it).
503
+ """
504
+ return tRS_rAuxOut
505
+
506
+
507
+ class GemmTmaBase(GemmBase):
508
+ """Common TMA descriptor and pipeline helpers for SM90+ GEMM paths."""
509
+
510
+ @cute.jit
511
+ def load_tma(
512
+ self,
513
+ pipeline: cutlass.pipeline.PipelineAsync,
514
+ producer_state: cutlass.pipeline.PipelineState,
515
+ copy_fns: Sequence[Optional[Callable]],
516
+ k_tile_cnt: Int32,
517
+ ) -> cutlass.pipeline.PipelineState:
518
+ # Peek (try_wait) AB buffer empty for k_block = prefetch_k_tile_cnt.
519
+ peek_empty_status = Boolean(True)
520
+ if 0 < k_tile_cnt:
521
+ peek_empty_status = pipeline.producer_try_acquire(producer_state)
522
+ # TMA load
523
+ for k_tile in cutlass.range(k_tile_cnt, unroll=1):
524
+ # Wait for A/B buffers to be empty before loading into them.
525
+ # Also sets the transaction barrier for the A/B buffers.
526
+ pipeline.producer_acquire(producer_state, peek_empty_status)
527
+ tma_bar_ptr = pipeline.producer_get_barrier(producer_state)
528
+ smem_idx = producer_state.index
529
+ for copy_fn in copy_fns:
530
+ if const_expr(copy_fn is not None):
531
+ copy_fn(k_tile, smem_idx, tma_bar_ptr=tma_bar_ptr)
532
+ # Mainloop pipeline's producer commit is a NOP for TMA pipelines.
533
+ pipeline.producer_commit(producer_state)
534
+ producer_state.advance()
535
+ peek_empty_status = Boolean(True)
536
+ if k_tile + 1 < k_tile_cnt:
537
+ peek_empty_status = pipeline.producer_try_acquire(producer_state)
538
+ return producer_state
539
+
540
+ def _make_gmem_tiled_copy_A(self, dtype, major_mode, num_threads, copy_bits=128):
541
+ atom_async_copy = cute.make_copy_atom(
542
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
543
+ dtype,
544
+ num_bits_per_copy=copy_bits,
545
+ )
546
+ copy_elems = copy_bits // dtype.width
547
+ loads_per_cache_line = 128 * 8 // copy_bits # 128 bytes per cache line
548
+ shape_dim_1 = cute.size(self.cta_tile_shape_mnk[2]) // copy_elems
549
+ if shape_dim_1 > loads_per_cache_line:
550
+ shape_dim_1 = math.gcd(shape_dim_1, loads_per_cache_line)
551
+ # thread layout for copy
552
+ thread_layout = cute.make_layout(
553
+ (num_threads // shape_dim_1, shape_dim_1), stride=(shape_dim_1, 1)
554
+ )
555
+ if major_mode != LayoutEnum.ROW_MAJOR:
556
+ shape_dim_0 = cute.size(self.cta_tile_shape_mnk[0]) // copy_elems
557
+ if shape_dim_0 > loads_per_cache_line:
558
+ shape_dim_0 = math.gcd(shape_dim_0, loads_per_cache_line)
559
+ thread_layout = cute.make_layout(
560
+ (shape_dim_0, num_threads // shape_dim_0), stride=(1, shape_dim_0)
561
+ )
562
+ # Value layout for copy
563
+ value_layout = (
564
+ cute.make_layout((1, copy_elems))
565
+ if major_mode == LayoutEnum.ROW_MAJOR
566
+ else cute.make_layout((copy_elems, 1))
567
+ )
568
+ return cute.make_tiled_copy_tv(atom_async_copy, thread_layout, value_layout)
569
+
570
+ def make_tma_load_atoms_and_tensors(
571
+ self,
572
+ mA: cute.Tensor,
573
+ mB: cute.Tensor,
574
+ a_smem_layout: cute.ComposedLayout,
575
+ b_smem_layout: cute.ComposedLayout,
576
+ varlen_k: bool,
577
+ ):
578
+ tma_atom_a, tma_tensor_a = None, None
579
+ if const_expr(not self.gather_A):
580
+ tma_atom_a, tma_tensor_a = self._make_tma_atoms_and_tensors(
581
+ copy_utils.create_ragged_tensor_for_tma(mA, ragged_dim=1)
582
+ if varlen_k and not self.gather_A
583
+ else mA,
584
+ a_smem_layout,
585
+ (self.cta_tile_shape_mnk[0], self.cta_tile_shape_mnk[2]),
586
+ self.cluster_shape_mnk[1],
587
+ )
588
+ tma_atom_b, tma_tensor_b = self._make_tma_atoms_and_tensors(
589
+ copy_utils.create_ragged_tensor_for_tma(mB, ragged_dim=1) if varlen_k else mB,
590
+ b_smem_layout,
591
+ (self.cta_tile_shape_mnk[1], self.cta_tile_shape_mnk[2]),
592
+ self.cluster_shape_mnk[0],
593
+ )
594
+ return tma_atom_a, tma_tensor_a, tma_atom_b, tma_tensor_b
595
+
596
+ def make_tma_epilogue_atoms_and_tensors(
597
+ self,
598
+ mD: Optional[cute.Tensor],
599
+ mC: Optional[cute.Tensor],
600
+ epilogue_args,
601
+ varlen_m: bool,
602
+ ):
603
+ tma_atom_d, tma_tensor_d = None, None
604
+ if const_expr(mD is not None):
605
+ tma_atom_d, tma_tensor_d = self._make_tma_epi_atoms_and_tensors(
606
+ copy_utils.create_ragged_tensor_for_tma(mD, ragged_dim=0, ptr_shift=True)
607
+ if varlen_m
608
+ else mD,
609
+ self.epi_smem_layout_staged,
610
+ self.epi_tile,
611
+ op_type="store"
612
+ if not (hasattr(epilogue_args, "add_to_output") and epilogue_args.add_to_output)
613
+ else "add",
614
+ )
615
+ tma_atom_c, tma_tensor_c = None, None
616
+ if const_expr(mC is not None):
617
+ tma_atom_c, tma_tensor_c = self._make_tma_epi_atoms_and_tensors(
618
+ mC, self.epi_c_smem_layout_staged, self.epi_tile, op_type="load"
619
+ )
620
+ return tma_atom_d, tma_tensor_d, tma_atom_c, tma_tensor_c
621
+
622
+ def epilog_gmem_copy_and_partition(
623
+ self,
624
+ atom: cute.CopyAtom | cute.TiledCopy,
625
+ mD_mn: cute.Tensor,
626
+ tile_shape_mn: cute.Tile,
627
+ epi_tile: cute.Tile,
628
+ sD: cute.Tensor,
629
+ tile_coord_mnkl: cute.Coord,
630
+ ) -> Tuple[cute.Tensor, cute.Tensor]:
631
+ gD = cute.local_tile(mD_mn, tile_shape_mn, tile_coord_mnkl[:2]) # (bM, bN)
632
+ tDgD_for_tma_partition = cute.zipped_divide(gD, epi_tile)
633
+ is_s2g = isinstance(
634
+ atom.op, (cpasync.CopyBulkTensorTileS2GOp, cpasync.CopyReduceBulkTensorTileS2GOp)
635
+ )
636
+ src_tensor, dst_tensor = (
637
+ (sD, tDgD_for_tma_partition) if is_s2g else (tDgD_for_tma_partition, sD)
638
+ )
639
+ return copy_utils.tma_get_copy_fn(
640
+ atom,
641
+ cta_coord=0,
642
+ cta_layout=cute.make_layout(1),
643
+ src_tensor=src_tensor,
644
+ dst_tensor=dst_tensor,
645
+ )
646
+
647
+ def make_ab_pipeline(
648
+ self,
649
+ tiled_mma: cute.TiledMma,
650
+ cluster_layout_vmnk: cute.Layout,
651
+ ):
652
+ # Threads/warps participating in this pipeline
653
+ producer_cnt = 1 if const_expr(not self.gather_A) else 1 + self.num_ab_load_warps * 32
654
+ ab_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread, producer_cnt)
655
+ # Each warp will contribute to the arrive count with the number of mcast size
656
+ mcast_size = self.num_mcast_ctas_a + self.num_mcast_ctas_b - 1
657
+ consumer_arrive_cnt = mcast_size * tiled_mma.size // cute.arch.WARP_SIZE
658
+ ab_pipeline_consumer_group = pipeline.CooperativeGroup(
659
+ pipeline.Agent.Thread, consumer_arrive_cnt
660
+ )
661
+ pipeline_cls = pipeline.PipelineTmaAsync if not self.gather_A else PipelineTmaCpAsync
662
+ return pipeline_cls.create(
663
+ num_stages=self.ab_stage,
664
+ producer_group=ab_pipeline_producer_group,
665
+ consumer_group=ab_pipeline_consumer_group,
666
+ tx_count=self.num_tma_load_bytes,
667
+ cta_layout_vmnk=cluster_layout_vmnk,
668
+ defer_sync=True,
669
+ )
670
+
671
+ def make_epi_pipeline(
672
+ self,
673
+ tx_count: int,
674
+ ):
675
+ epi_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
676
+ # Each warp will contribute 1 to the arrive count
677
+ consumer_arrive_cnt = self.num_epi_warps
678
+ epi_pipeline_consumer_group = pipeline.CooperativeGroup(
679
+ pipeline.Agent.Thread, consumer_arrive_cnt
680
+ )
681
+ return PipelineTmaAsync.create(
682
+ num_stages=self.epi_c_stage,
683
+ producer_group=epi_pipeline_producer_group,
684
+ consumer_group=epi_pipeline_consumer_group,
685
+ tx_count=tx_count,
686
+ defer_sync=True,
687
+ elect_one_release=True,
688
+ syncwarp_before_release=True,
689
+ )
690
+
691
+ def make_epi_store_pipeline(self):
692
+ num_epi_threads = self.num_epi_warps * cute.arch.WARP_SIZE
693
+ epi_store_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread, num_epi_threads)
694
+ return pipeline.PipelineTmaStore.create(
695
+ num_stages=self.epi_stage, producer_group=epi_store_producer_group
696
+ )
697
+
698
+ @staticmethod
699
+ def _make_tma_epi_atoms_and_tensors(
700
+ tensor_d: cute.Tensor,
701
+ epi_smem_layout_staged: cute.ComposedLayout,
702
+ epi_tile: Tuple[int, int],
703
+ op_type: Literal["store", "load", "add"],
704
+ ) -> Tuple[cute.CopyAtom, cute.Tensor]:
705
+ """Create TMA atoms and tensors for storing D or loading C."""
706
+ assert op_type in ["load", "store", "add"]
707
+ epi_smem_layout = cute.slice_(epi_smem_layout_staged, (None, None, 0))
708
+ d_cta_v_layout = cute.composition(cute.make_identity_layout(tensor_d.shape), epi_tile)
709
+ op = {
710
+ "load": cpasync.CopyBulkTensorTileG2SOp(),
711
+ "store": cpasync.CopyBulkTensorTileS2GOp(),
712
+ "add": cpasync.CopyReduceBulkTensorTileS2GOp(cpasync.ReductionOp.ADD),
713
+ }[op_type]
714
+ tma_atom_d, tma_tensor_d = cpasync.make_tiled_tma_atom(
715
+ op, tensor_d, epi_smem_layout, d_cta_v_layout
716
+ )
717
+ return tma_atom_d, tma_tensor_d
718
+
719
+ @staticmethod
720
+ def _make_tma_atoms_and_tensors(
721
+ tensor: cute.Tensor,
722
+ smem_layout: cute.ComposedLayout,
723
+ smem_tile: Tuple[int, int],
724
+ mcast_dim: int,
725
+ ) -> Tuple[cute.CopyAtom, cute.Tensor]:
726
+ """Create TMA atoms and tensors for input tensors."""
727
+ # block_copy takes compiler-driven multicast metadata at the copy site,
728
+ # so the TMA atom itself must stay the non-multicast variant here.
729
+ op = cpasync.CopyBulkTensorTileG2SOp()
730
+ tma_atom, tma_tensor = cpasync.make_tiled_tma_atom(op, tensor, smem_layout, smem_tile)
731
+ return tma_atom, tma_tensor
build/torch-cuda/quack/gemm_blockscaled_interface.py DELETED
@@ -1,326 +0,0 @@
1
- # Copyright (c) 2026, Tri Dao.
2
- """PyTorch-friendly interface for the SM100 MXFP8 blockscaled GEMM.
3
-
4
- Shape / layout conventions (matches torch.matmul, torch._scaled_mm, cuBLAS):
5
- A: (M, K) or (L, M, K) dtype float8_e4m3fn, K-contiguous (row-major)
6
- B: (K, N) or (L, K, N) dtype float8_e4m3fn, K-contiguous (col-major)
7
- A_scale: (M, K/32) or (L, M, K/32) dtype float8_e8m0fnu, K-contiguous
8
- B_scale: (K/32, N) or (L, K/32, N) dtype float8_e8m0fnu, K-contiguous
9
- out: (M, N) or (L, M, N) dtype bfloat16/float16, contiguous
10
-
11
- "K-contiguous" means stride 1 on the K axis. This matches how torchao/cuBLAS
12
- use `torch._scaled_mm(a, b.t(), ...)`:
13
- - you store a weight as nn.Linear-style `W` of shape `(N, K)` row-major
14
- - you pass `W.mT` (a zero-copy view of shape (K, N) with K-contig) as B
15
- The interface applies `.mT` internally to reach the `(N, K) K-major` layout
16
- the quack kernel consumes. No data is copied.
17
- """
18
-
19
- from functools import lru_cache
20
- from typing import Optional, Tuple
21
-
22
- import torch
23
- from torch import Tensor
24
-
25
- import cutlass
26
-
27
- from .blockscaled_gemm_utils import (
28
- ceil_div,
29
- compile_blockscaled_gemm_tvm_ffi,
30
- pack_scale_2d_to_blocked_contig,
31
- scale_blocked_for_cublas,
32
- scale_view_for_kernel,
33
- )
34
- from .gemm_default_epi import GemmDefaultSm100
35
- from .mx_utils import to_mx
36
-
37
- _SF_VEC_SIZE = 32
38
- _TORCH_TO_CUTLASS_D = {
39
- torch.bfloat16: cutlass.BFloat16,
40
- torch.float16: cutlass.Float16,
41
- torch.float32: cutlass.Float32,
42
- }
43
-
44
-
45
- def _default_tiler_cluster(m: int, n: int) -> Tuple[Tuple[int, int], Tuple[int, int]]:
46
- """Pick a reasonable default (mma_tiler_mn, cluster_shape_mn)."""
47
- if m >= 512 and n >= 128:
48
- return (256, 128), (2, 1)
49
- return (128, 128), (1, 1)
50
-
51
-
52
- @lru_cache(maxsize=64)
53
- def _compile_cached(
54
- m: int,
55
- n: int,
56
- k: int,
57
- l: int,
58
- mma_tiler_mn: Tuple[int, int],
59
- cluster_shape_mn: Tuple[int, int],
60
- out_torch_dtype,
61
- ab_dtype_cutlass,
62
- sf_dtype_cutlass,
63
- ):
64
- """Compile kernel for a given (shape, dtype, tiler, cluster) and cache it."""
65
- dev = torch.device("cuda")
66
- rm = ceil_div(m, 128)
67
- rn = ceil_div(n, 128)
68
- rk = ceil_div(k // _SF_VEC_SIZE, 4)
69
- # K-major: (l, m, k) contiguous, viewed as (m, k, l) strides (k, 1, m*k)
70
- fake_mA = torch.empty(l, m, k, dtype=torch.float8_e4m3fn, device=dev).permute(1, 2, 0)
71
- fake_mB = torch.empty(l, n, k, dtype=torch.float8_e4m3fn, device=dev).permute(1, 2, 0)
72
- # N-major: (l, m, n) contiguous, viewed as (m, n, l) strides (n, 1, m*n)
73
- fake_mD = torch.empty(l, m, n, dtype=out_torch_dtype, device=dev).permute(1, 2, 0)
74
- fake_sc_A = torch.empty(l, rm, rk, 512, dtype=torch.float8_e8m0fnu, device=dev)
75
- fake_sc_B = torch.empty(l, rn, rk, 512, dtype=torch.float8_e8m0fnu, device=dev)
76
- fake_mSFA = scale_view_for_kernel(fake_sc_A, m, k // _SF_VEC_SIZE, l)
77
- fake_mSFB = scale_view_for_kernel(fake_sc_B, n, k // _SF_VEC_SIZE, l)
78
- return compile_blockscaled_gemm_tvm_ffi(
79
- ab_dtype_cutlass,
80
- sf_dtype_cutlass,
81
- _SF_VEC_SIZE,
82
- _TORCH_TO_CUTLASS_D[out_torch_dtype],
83
- mma_tiler_mn,
84
- cluster_shape_mn,
85
- fake_mA,
86
- fake_mB,
87
- fake_mD,
88
- fake_mSFA,
89
- fake_mSFB,
90
- )
91
-
92
-
93
- def _as_3d(x: Tensor, ndim_in: int) -> Tensor:
94
- """Add a leading batch dim if input is 2D. Returns a view."""
95
- if ndim_in == 2:
96
- return x.unsqueeze(0)
97
- return x
98
-
99
-
100
- def _to_kernel_layout(
101
- A: Tensor,
102
- B: Tensor,
103
- A_scale: Tensor,
104
- B_scale: Tensor,
105
- ) -> Tuple[int, int, int, int, Tensor, Tensor, Tensor, Tensor, Tensor, Tensor, bool]:
106
- """Normalize shapes/strides, validate, and repack scales. Returns
107
- (m, n, k, l, mA_mkl, mB_nkl, sc_contig_A, sc_contig_B, sfa_view, sfb_view, was_2d).
108
-
109
- A: (M,K) or (L,M,K) K-contig. B: (K,N) or (L,K,N) K-contig.
110
- A_scale: (M,K/32) or (L,M,K/32) K-contig. B_scale: (K/32,N) or (L,K/32,N) K-contig.
111
- """
112
- assert A.dtype == torch.float8_e4m3fn, f"A dtype must be float8_e4m3fn, got {A.dtype}"
113
- assert B.dtype == torch.float8_e4m3fn, f"B dtype must be float8_e4m3fn, got {B.dtype}"
114
- assert A_scale.dtype == torch.float8_e8m0fnu
115
- assert B_scale.dtype == torch.float8_e8m0fnu
116
- was_2d = A.dim() == 2
117
- # Flip B from (K,N) to (N,K) via .mT (zero-copy). User's B K-contig → .mT K-contig.
118
- A3 = _as_3d(A, A.dim()) # (l, m, k) K-contig row-major expected
119
- B3 = _as_3d(B, B.dim()).mT # (l, n, k) K-contig (view) from (l, k, n)
120
- l, m, k = A3.shape
121
- l2, n, k2 = B3.shape
122
- assert l == l2, f"batch mismatch: A={l}, B={l2}"
123
- assert k == k2, f"K mismatch: A K={k}, B K={k2}"
124
- assert k % _SF_VEC_SIZE == 0, f"K ({k}) must be divisible by {_SF_VEC_SIZE}"
125
- assert A3.stride(-1) == 1, "A must be K-contiguous (stride 1 on K)"
126
- assert B3.stride(-1) == 1, (
127
- "B must be K-contiguous on its K axis (pass .mT of an (N,K) row-major tensor)"
128
- )
129
- sf_k = k // _SF_VEC_SIZE
130
- as3 = _as_3d(A_scale, A_scale.dim()) # expected (l, m, sf_k) K-contig row-major
131
- bs3 = _as_3d(B_scale, B_scale.dim()).mT # (l, n, sf_k) K-contig (view) from (l, sf_k, n)
132
- assert as3.stride(-1) == 1, "A_scale must be K-contiguous"
133
- assert bs3.stride(-1) == 1, (
134
- "B_scale must be K-contiguous on its K axis (pass .mT of an (N, K/32) row-major tensor)"
135
- )
136
- assert as3.shape == (l, m, sf_k), (
137
- f"A_scale shape: expected (l={l},m={m},sf_k={sf_k}) K-contig, got {tuple(as3.shape)}"
138
- )
139
- assert bs3.shape == (l, n, sf_k), (
140
- f"B_scale shape: expected .mT of (l={l},sf_k={sf_k},n={n}) -> ({l},{n},{sf_k}), got {tuple(bs3.shape)}"
141
- )
142
- # Force row-major contiguous for packer/kernel consumption.
143
- # A3 / B3 are views — .contiguous() materializes (l,m,k) / (l,n,k) row-major.
144
- A3_c = A3.contiguous()
145
- B3_c = B3.contiguous()
146
- # (l, m, k) -> (m, k, l) K-major view (no copy; strides (k, 1, m*k))
147
- mA_mkl = A3_c.permute(1, 2, 0)
148
- mB_nkl = B3_c.permute(1, 2, 0)
149
- sc_contig_A = pack_scale_2d_to_blocked_contig(as3.contiguous())
150
- sc_contig_B = pack_scale_2d_to_blocked_contig(bs3.contiguous())
151
- sfa_view = scale_view_for_kernel(sc_contig_A, m, sf_k, l)
152
- sfb_view = scale_view_for_kernel(sc_contig_B, n, sf_k, l)
153
- return m, n, k, l, mA_mkl, mB_nkl, sc_contig_A, sc_contig_B, sfa_view, sfb_view, was_2d
154
-
155
-
156
- def mxfp8_gemm_out(
157
- A: Tensor,
158
- B: Tensor,
159
- A_scale: Tensor,
160
- B_scale: Tensor,
161
- out: Tensor,
162
- *,
163
- mma_tiler_mn: Optional[Tuple[int, int]] = None,
164
- cluster_shape_mn: Optional[Tuple[int, int]] = None,
165
- ) -> None:
166
- """MXFP8 blockscaled GEMM with pre-allocated output. See module doc for shape conventions."""
167
- m, n, k, l, mA, mB, _scA, _scB, sfa, sfb, was_2d = _to_kernel_layout(A, B, A_scale, B_scale)
168
- out_dtype = out.dtype
169
- assert out_dtype in _TORCH_TO_CUTLASS_D, f"unsupported out dtype: {out_dtype}"
170
- expected_out_shape = (m, n) if was_2d else (l, m, n)
171
- assert tuple(out.shape) == expected_out_shape, (
172
- f"out shape {tuple(out.shape)} != expected {expected_out_shape}"
173
- )
174
- assert out.is_contiguous(), "out must be contiguous"
175
- # View caller's contiguous (M,N) or (L,M,N) as (M,N,L) N-major strided view, no copy.
176
- out_3d = out.unsqueeze(0) if was_2d else out # (l, m, n)
177
- mD = out_3d.permute(1, 2, 0) # (m, n, l), strides (n, 1, m*n)
178
- if mma_tiler_mn is None or cluster_shape_mn is None:
179
- tlr, clu = _default_tiler_cluster(m, n)
180
- mma_tiler_mn = mma_tiler_mn or tlr
181
- cluster_shape_mn = cluster_shape_mn or clu
182
- if not GemmDefaultSm100.can_implement_blockscaled(
183
- cutlass.Float8E4M3FN,
184
- cutlass.Float8E8M0FNU,
185
- _SF_VEC_SIZE,
186
- _TORCH_TO_CUTLASS_D[out_dtype],
187
- mma_tiler_mn,
188
- cluster_shape_mn,
189
- m,
190
- n,
191
- k,
192
- l,
193
- "k",
194
- "k",
195
- "n",
196
- ):
197
- raise ValueError(
198
- f"unsupported config: m={m}, n={n}, k={k}, l={l}, "
199
- f"tiler={mma_tiler_mn}, cluster={cluster_shape_mn}"
200
- )
201
- runner = _compile_cached(
202
- m,
203
- n,
204
- k,
205
- l,
206
- mma_tiler_mn,
207
- cluster_shape_mn,
208
- out_dtype,
209
- cutlass.Float8E4M3FN,
210
- cutlass.Float8E8M0FNU,
211
- )
212
- runner(mA, mB, mD, sfa, sfb)
213
-
214
-
215
- def mxfp8_gemm(
216
- A: Tensor,
217
- B: Tensor,
218
- A_scale: Tensor,
219
- B_scale: Tensor,
220
- out: Optional[Tensor] = None,
221
- out_dtype: torch.dtype = torch.bfloat16,
222
- *,
223
- mma_tiler_mn: Optional[Tuple[int, int]] = None,
224
- cluster_shape_mn: Optional[Tuple[int, int]] = None,
225
- ) -> Tensor:
226
- """MXFP8 blockscaled GEMM. Allocates output if not provided."""
227
- if out is None:
228
- # A: (M,K) or (L,M,K); B: (K,N) or (L,K,N); out: (M,N) or (L,M,N)
229
- if A.dim() == 2:
230
- out_shape = (A.shape[0], B.shape[1])
231
- else:
232
- out_shape = (A.shape[0], A.shape[1], B.shape[2])
233
- out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
234
- mxfp8_gemm_out(
235
- A,
236
- B,
237
- A_scale,
238
- B_scale,
239
- out,
240
- mma_tiler_mn=mma_tiler_mn,
241
- cluster_shape_mn=cluster_shape_mn,
242
- )
243
- return out
244
-
245
-
246
- def mxfp8_quantize(x: Tensor) -> Tuple[Tensor, Tensor]:
247
- """Quantize a (..., K) bf16/fp32 tensor to MXFP8. Returns (qdata, scale_2d)
248
- in torchao-convention layout. Last dim (K) must be divisible by 32."""
249
- assert x.shape[-1] % _SF_VEC_SIZE == 0, (
250
- f"last dim ({x.shape[-1]}) must be divisible by {_SF_VEC_SIZE}"
251
- )
252
- return to_mx(x.contiguous(), _SF_VEC_SIZE)
253
-
254
-
255
- def mxfp8_gemm_quantize(
256
- A: Tensor,
257
- B: Tensor,
258
- out: Optional[Tensor] = None,
259
- out_dtype: torch.dtype = torch.bfloat16,
260
- *,
261
- mma_tiler_mn: Optional[Tuple[int, int]] = None,
262
- cluster_shape_mn: Optional[Tuple[int, int]] = None,
263
- ) -> Tensor:
264
- """High-level: quantize bf16 A, B_as_NK to MXFP8, then run C = A @ B_as_NK.mT.
265
- Inputs: A=(M,K)/(L,M,K), B_as_NK=(N,K)/(L,N,K) bf16/fp32. Quantization
266
- scales along the last (K) dim. Returned output has shape (M,N)/(L,M,N)."""
267
- A_q, A_sc = mxfp8_quantize(A)
268
- B_q, B_sc = mxfp8_quantize(B)
269
- # B_q, B_sc are (..., N, K) / (..., N, K/32). Flip to (..., K, N) / (..., K/32, N)
270
- # K-contig zero-copy views to match the interface convention.
271
- return mxfp8_gemm(
272
- A_q,
273
- B_q.mT,
274
- A_sc,
275
- B_sc.mT,
276
- out=out,
277
- out_dtype=out_dtype,
278
- mma_tiler_mn=mma_tiler_mn,
279
- cluster_shape_mn=cluster_shape_mn,
280
- )
281
-
282
-
283
- def mxfp8_gemm_cublas(
284
- A: Tensor,
285
- B: Tensor,
286
- A_scale: Tensor,
287
- B_scale: Tensor,
288
- out_dtype: torch.dtype = torch.bfloat16,
289
- ) -> Tensor:
290
- """Reference path via torch._scaled_mm. Requires l=1 (or 2D inputs)."""
291
- m, n, k, l, _mA, _mB, sc_A, sc_B, _sfa, _sfb, was_2d = _to_kernel_layout(A, B, A_scale, B_scale)
292
- assert l == 1, "torch._scaled_mm MXFP8 path is 2D only; pass 2D inputs or l=1"
293
- # torch._scaled_mm: A=(M,K) row-major, B=(K,N) col-major (both K-contig) -- same layout user gave us.
294
- a2d = A if A.dim() == 2 else A.squeeze(0)
295
- b2d = B if B.dim() == 2 else B.squeeze(0)
296
- sca = scale_blocked_for_cublas(sc_A, m, k // _SF_VEC_SIZE, 0)
297
- scb = scale_blocked_for_cublas(sc_B, n, k // _SF_VEC_SIZE, 0)
298
- out = torch._scaled_mm(
299
- a2d,
300
- b2d,
301
- scale_a=sca,
302
- scale_b=scb,
303
- out_dtype=out_dtype,
304
- )
305
- return out if was_2d else out.unsqueeze(0)
306
-
307
-
308
- def mxfp8_gemm_ref(
309
- A: Tensor,
310
- B: Tensor,
311
- A_scale: Tensor,
312
- B_scale: Tensor,
313
- out_dtype: torch.dtype = torch.bfloat16,
314
- ) -> Tensor:
315
- """Dequantize + plain matmul reference. A=(M,K), B=(K,N)."""
316
- was_2d = A.dim() == 2
317
- # (l, m, k)
318
- A3 = _as_3d(A, A.dim()).float()
319
- # B is (K, N)/(L, K, N); flip to (l, n, k) for dequant by last-dim
320
- B3 = _as_3d(B, B.dim()).mT.contiguous().float()
321
- as3 = _as_3d(A_scale, A_scale.dim()).float()
322
- bs3 = _as_3d(B_scale, B_scale.dim()).mT.contiguous().float()
323
- a_dq = A3 * as3.repeat_interleave(_SF_VEC_SIZE, dim=-1)
324
- b_dq = B3 * bs3.repeat_interleave(_SF_VEC_SIZE, dim=-1)
325
- out3 = torch.einsum("lmk,lnk->lmn", a_dq, b_dq).to(out_dtype)
326
- return out3.squeeze(0) if was_2d else out3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
build/torch-cuda/quack/gemm_config.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2025, Fri Dao.
2
  import itertools
3
  from typing import Optional, List
4
  from functools import partial
@@ -9,11 +9,14 @@ from dataclasses import dataclass
9
  class GemmConfig:
10
  tile_m: int = 128
11
  tile_n: int = 192
 
 
12
  pingpong: bool = True
13
  # by default, we use dynamic persistent tile scheduler on SM100 but not on SM90
14
  is_dynamic_persistent: bool = True
15
  cluster_m: int = 2
16
  cluster_n: int = 1
 
17
  swap_ab: bool = False
18
  # raster_order: int = 1
19
  max_swizzle_size: int = 8
@@ -70,6 +73,39 @@ def _get_sm90_configs(
70
  ]
71
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  def _get_sm100_configs(
74
  epilogue: Optional[str] = None,
75
  ) -> List[GemmConfig]:
@@ -141,14 +177,15 @@ def get_all_configs(
141
  epilogue: Optional[str] = None,
142
  tune_coop: bool = True,
143
  ) -> List[GemmConfig]:
144
- """Return autotuning configs for all supported device capabilities (sm90 + sm100 + sm120).
145
 
146
  Each GemmConfig is tagged with its target device_capacity, so the caller can
147
  filter at runtime based on the actual device. This avoids querying the device
148
  (and initializing a CUDA context) at import time.
149
  """
150
  return (
151
- _get_sm90_configs(epilogue, tune_coop)
 
152
  + _get_sm100_configs(epilogue)
153
  + _get_sm120_configs(epilogue, tune_coop)
154
  )
 
1
+ # Copyright (C) 2025, Tri Dao.
2
  import itertools
3
  from typing import Optional, List
4
  from functools import partial
 
9
  class GemmConfig:
10
  tile_m: int = 128
11
  tile_n: int = 192
12
+ tile_k: int | None = None
13
+ num_warps: int | None = None
14
  pingpong: bool = True
15
  # by default, we use dynamic persistent tile scheduler on SM100 but not on SM90
16
  is_dynamic_persistent: bool = True
17
  cluster_m: int = 2
18
  cluster_n: int = 1
19
+ cluster_k: int = 1
20
  swap_ab: bool = False
21
  # raster_order: int = 1
22
  max_swizzle_size: int = 8
 
73
  ]
74
 
75
 
76
+ def _get_sm80_configs() -> List[GemmConfig]:
77
+ tile_mn_warps_vals = [
78
+ (128, 128, 4),
79
+ (128, 128, 8),
80
+ (128, 160, 4),
81
+ # TODO: Make 128x160 work with 8 warps. It currently makes the accumulator
82
+ # N layout odd and fails epilogue retile.
83
+ (128, 192, 4),
84
+ (128, 192, 8),
85
+ (128, 256, 8),
86
+ (128, 64, 4),
87
+ (64, 128, 4),
88
+ ]
89
+ return [
90
+ GemmConfig(
91
+ tile_m=tile_m,
92
+ tile_n=tile_n,
93
+ tile_k=tile_k,
94
+ num_warps=num_warps,
95
+ pingpong=False,
96
+ cluster_m=1,
97
+ cluster_n=1,
98
+ swap_ab=swap_ab,
99
+ device_capacity=8,
100
+ is_dynamic_persistent=False,
101
+ use_tma_gather=False,
102
+ )
103
+ for (tile_m, tile_n, num_warps), tile_k, swap_ab in itertools.product(
104
+ tile_mn_warps_vals, [32, 64], [False, True]
105
+ )
106
+ ]
107
+
108
+
109
  def _get_sm100_configs(
110
  epilogue: Optional[str] = None,
111
  ) -> List[GemmConfig]:
 
177
  epilogue: Optional[str] = None,
178
  tune_coop: bool = True,
179
  ) -> List[GemmConfig]:
180
+ """Return autotuning configs for all supported device capabilities.
181
 
182
  Each GemmConfig is tagged with its target device_capacity, so the caller can
183
  filter at runtime based on the actual device. This avoids querying the device
184
  (and initializing a CUDA context) at import time.
185
  """
186
  return (
187
+ _get_sm80_configs()
188
+ + _get_sm90_configs(epilogue, tune_coop)
189
  + _get_sm100_configs(epilogue)
190
  + _get_sm120_configs(epilogue, tune_coop)
191
  )
build/torch-cuda/quack/gemm_dact.py CHANGED
@@ -8,15 +8,15 @@ from torch import Tensor
8
  import cutlass
9
  import cutlass.cute as cute
10
  from cutlass import Int32, Float32, const_expr
 
11
  from .gemm_sm90 import GemmSm90
12
  from .gemm_sm100 import GemmSm100
13
  from .gemm_sm120 import GemmSm120
14
  from .gemm_default_epi import GemmDefaultEpiMixin
15
  from .gemm_act import GemmActMixin
16
- from .epi_ops import ColVecReduce, colvec_reduce_accumulate
17
  from .compile_utils import make_fake_tensor as fake_tensor
18
  from .cute_dsl_utils import (
19
- ParamsBase,
20
  mlir_namedtuple,
21
  torch2cute_dtype_map,
22
  get_device_capacity,
@@ -33,10 +33,10 @@ from .gemm_tvm_ffi_utils import (
33
  make_fake_gemm_tensors,
34
  compile_gemm_kernel,
35
  )
36
- from .cache_utils import jit_cache
37
  from .rounding import RoundingMode
38
- from . import layout_utils as layout_utils
39
  from .activation import dact_fn_map, dgate_fn_map
 
40
 
41
 
42
  class GemmDActMixin(GemmActMixin):
@@ -51,36 +51,30 @@ class GemmDActMixin(GemmActMixin):
51
  epi_loop_tensors: Tuple[cute.Tensor, ...],
52
  tRS_rD: cute.Tensor,
53
  tRS_rC: Optional[cute.Tensor] = None,
54
- ) -> Optional[cute.Tensor]:
55
  assert tRS_rC is not None
56
  # We don't add C to the accumulator
57
  GemmDefaultEpiMixin.epi_visit_subtile(self, params, epi_loop_tensors, tRS_rD, tRS_rC=None)
58
- tRS_rC_acc = cute.make_rmem_tensor_like(tRS_rC, self.acc_dtype)
59
- tRS_rC_acc.store(tRS_rC.load().to(self.acc_dtype))
60
  # If we don't have .shape here, the compiler generates local stores and loads
61
  if const_expr(params.act_fn is not None):
62
- tRS_rPostAct = cute.make_rmem_tensor(tRS_rD.layout.shape, self.acc_dtype)
63
- if const_expr(self.arch < 100):
64
- for i in cutlass.range(cute.size(tRS_rPostAct), unroll_full=True):
65
- tRS_rD[i], tRS_rPostAct[i] = params.act_fn(tRS_rC_acc[i], tRS_rD[i])
66
- else:
67
- for i in cutlass.range(cute.size(tRS_rPostAct) // 2, unroll_full=True):
68
- (
69
- (tRS_rD[2 * i], tRS_rD[2 * i + 1]),
70
- (tRS_rPostAct[2 * i], tRS_rPostAct[2 * i + 1]),
71
- ) = params.act_fn(
72
- (tRS_rC_acc[2 * i], tRS_rC_acc[2 * i + 1]),
73
- (tRS_rD[2 * i], tRS_rD[2 * i + 1]),
74
- )
75
  else:
76
- tRS_rPostAct = tRS_rC_acc
77
- return tRS_rPostAct
78
 
79
 
80
  class GemmDActSm90(GemmDActMixin, GemmSm90):
81
  pass
82
 
83
 
 
 
 
 
84
  class GemmDActSm100(GemmDActMixin, GemmSm100):
85
  pass
86
 
@@ -92,17 +86,18 @@ class GemmDActSm120(GemmDActMixin, GemmSm120):
92
  class GemmDGatedMixin(GemmActMixin):
93
  # Different from GemmActMixin, here act_bwd_fn must take in 3 arguments (x, y, dout)
94
  # and return 3 arguments (dx, dy, out)
95
- _epi_ops = (*GemmActMixin._epi_ops, ColVecReduce("mColVecReduce"))
 
 
 
 
 
96
  _extra_param_fields = (("act_bwd_fn", cutlass.Constexpr, None),)
97
- _epi_param_bases = (ParamsBase,)
98
 
99
  @mlir_namedtuple
100
  class EpilogueArguments(NamedTuple):
101
- mPostAct: cute.Tensor
102
  act_bwd_fn: cutlass.Constexpr[Callable] = None
103
- alpha: Optional[Float32 | cute.Tensor] = None
104
- beta: Optional[Float32 | cute.Tensor] = None
105
- mRowVecBroadcast: Optional[cute.Tensor] = None
106
  mColVecBroadcast: Optional[cute.Tensor] = None
107
  mColVecReduce: Optional[cute.Tensor] = None
108
  rounding_mode: cutlass.Constexpr[int] = RoundingMode.RN
@@ -117,9 +112,9 @@ class GemmDGatedMixin(GemmActMixin):
117
  assert self.d_dtype.width == 32, "D storage type must be 32 bit"
118
  assert self.c_dtype.width == 32, "C storage type must be 32 bit"
119
  self.rounding_mode = args.rounding_mode
120
- self.postact_dtype = args.mPostAct.element_type
121
- self.postact_layout = cutlass.utils.LayoutEnum.from_tensor(args.mPostAct)
122
- self.cta_tile_shape_postact_mn = self.cta_tile_shape_mnk[:2]
123
  d = self._epi_ops_to_params_dict(args)
124
  d["act_bwd_fn"] = args.act_bwd_fn
125
  return self.EpilogueParams(**d)
@@ -133,13 +128,9 @@ class GemmDGatedMixin(GemmActMixin):
133
  epi_loop_tensors: Tuple[cute.Tensor, ...],
134
  tRS_rD: cute.Tensor,
135
  tRS_rC: Optional[cute.Tensor] = None,
136
- ) -> Optional[cute.Tensor]:
137
- alpha = epi_loop_tensors["alpha"]
138
- beta = epi_loop_tensors["beta"]
139
- tDrRowVec = epi_loop_tensors["mRowVecBroadcast"]
140
- tDrColVec = epi_loop_tensors["mColVecBroadcast"]
141
- tDrColVecReduce = epi_loop_tensors["mColVecReduce"]
142
- assert alpha is None and beta is None and tDrRowVec is None # We don't use these for now
143
  assert tRS_rC is not None
144
  implicit_dtype = self.implicit_dtype
145
  assert implicit_dtype.width == 16, "GemmDGatedMixin only supports 16bit for now"
@@ -150,7 +141,7 @@ class GemmDGatedMixin(GemmActMixin):
150
  tRS_rOut = cute.make_rmem_tensor_like(tRS_rD, Float32)
151
  tRS_rD_scaled = cute.make_rmem_tensor_like(tRS_rD)
152
  if const_expr(tDrColVec is not None): # Scale D by colvec
153
- if const_expr(self.arch < 100):
154
  tRS_rD_scaled.store(tRS_rD.load() * tDrColVec.load().to(tRS_rD.element_type))
155
  else:
156
  tDrColVec_mn = layout_utils.convert_layout_zero_stride(tDrColVec, tDrColVec.layout)
@@ -159,63 +150,45 @@ class GemmDGatedMixin(GemmActMixin):
159
  tRS_rD_scaled, tDrColVec.layout
160
  )
161
  for m in cutlass.range(cute.size(tDrColVec_mn, mode=[0]), unroll_full=True):
 
162
  for n in cutlass.range(
163
- cute.size(tDrColVec_mn, mode=[1]) // 2, unroll_full=True
164
  ):
165
- (
166
- tRS_rD_scaled_mn[m, 2 * n],
167
- tRS_rD_scaled_mn[m, 2 * n + 1],
168
- ) = cute.arch.mul_packed_f32x2(
169
- (tRS_rD_mn[m, 2 * n], tRS_rD_mn[m, 2 * n + 1]),
170
- (tDrColVec_mn[m, 0], tDrColVec_mn[m, 0]),
171
- )
172
  else:
173
  tRS_rD_scaled.store(tRS_rD.load())
174
- if const_expr(self.arch < 100):
175
- for i in cutlass.range(cute.size(tRS_rD)):
176
- (
177
- tRS_rdXY_f32x2[2 * i],
178
- tRS_rdXY_f32x2[2 * i + 1],
179
- tRS_rOut[i],
180
- ) = params.act_bwd_fn(
181
- tRS_rXY_f32x2[2 * i], tRS_rXY_f32x2[2 * i + 1], tRS_rD_scaled[i]
182
- )
183
- else:
184
- for i in cutlass.range(cute.size(tRS_rD) // 2):
185
- (
186
- (tRS_rdXY_f32x2[4 * i], tRS_rdXY_f32x2[4 * i + 2]),
187
- (tRS_rdXY_f32x2[4 * i + 1], tRS_rdXY_f32x2[4 * i + 3]),
188
- (tRS_rOut[2 * i], tRS_rOut[2 * i + 1]),
189
- ) = params.act_bwd_fn(
190
- (tRS_rXY_f32x2[4 * i], tRS_rXY_f32x2[4 * i + 2]),
191
- (tRS_rXY_f32x2[4 * i + 1], tRS_rXY_f32x2[4 * i + 3]),
192
- (tRS_rD_scaled[2 * i], tRS_rD_scaled[2 * i + 1]),
193
- )
194
  if const_expr(tDrColVecReduce is not None):
195
  # Accumulate postact * dout before D is scaled by colvec_scale
196
  colvec_reduce_accumulate(self, tDrColVecReduce, tRS_rOut, rScale=tRS_rD)
197
 
198
  if const_expr(tDrColVec is not None): # Scale Out by colvec
199
- if const_expr(self.arch < 100):
200
  tRS_rOut.store(tRS_rOut.load() * tDrColVec.load().to(tRS_rD.element_type))
201
  else:
202
  tDrColVec_mn = layout_utils.convert_layout_zero_stride(tDrColVec, tDrColVec.layout)
203
  tRS_rOut_mn = layout_utils.convert_layout_zero_stride(tRS_rOut, tDrColVec.layout)
204
  for m in cutlass.range(cute.size(tDrColVec_mn, mode=[0]), unroll_full=True):
 
205
  for n in cutlass.range(
206
- cute.size(tDrColVec_mn, mode=[1]) // 2, unroll_full=True
207
  ):
208
- tRS_rOut_mn[m, 2 * n], tRS_rOut_mn[m, 2 * n + 1] = (
209
- cute.arch.mul_packed_f32x2(
210
- (tRS_rOut_mn[m, 2 * n], tRS_rOut_mn[m, 2 * n + 1]),
211
- (tDrColVec_mn[m, 0], tDrColVec_mn[m, 0]),
212
- )
213
- )
214
  # Type conversion
215
  tRS_rdXY_f16x2 = cute.make_rmem_tensor(tRS_rdXY_f32x2.layout, implicit_dtype)
216
  tRS_rdXY_f16x2.store(tRS_rdXY_f32x2.load().to(implicit_dtype))
217
  tRS_rD.store(cute.recast_tensor(tRS_rdXY_f16x2, Float32).load())
218
- return tRS_rOut
219
 
220
  # epi_end is inherited from ComposableEpiMixin → delegates to ColVecReduce.end()
221
 
@@ -224,6 +197,10 @@ class GemmDGatedSm90(GemmDGatedMixin, GemmSm90):
224
  pass
225
 
226
 
 
 
 
 
227
  class GemmDGatedSm100(GemmDGatedMixin, GemmSm100):
228
  pass
229
 
@@ -263,16 +240,21 @@ def _compile_gemm_dact(
263
  ):
264
  is_dgated = gemm_cls_name == "dgated"
265
  sm_to_cls = {
266
- "dact": {9: GemmDActSm90, 10: GemmDActSm100, 11: GemmDActSm100, 12: GemmDActSm120},
 
 
 
 
 
 
267
  "dgated": {
 
268
  9: GemmDGatedSm90,
269
  10: GemmDGatedSm100,
270
  11: GemmDGatedSm100,
271
  12: GemmDGatedSm120,
272
  },
273
  }
274
- if device_capacity[0] == 12 and gemm_cls_name == "dact":
275
- raise NotImplementedError("SM120 non-gated dactivation GEMM epilogue is not yet supported")
276
  GemmCls = sm_to_cls[gemm_cls_name][device_capacity[0]]
277
  mA, mB, mD, mC, m, n, k, l = make_fake_gemm_tensors(
278
  a_dtype,
@@ -289,7 +271,7 @@ def _compile_gemm_dact(
289
  div_pa = div_for_dtype(postact_dtype)
290
  pa_leading = 1 if postact_major == "n" else 0
291
  pa_shape = (m, n) if varlen_m else (m, n, l)
292
- mPostAct = fake_tensor(postact_dtype, pa_shape, leading_dim=pa_leading, divisibility=div_pa)
293
 
294
  if is_dgated:
295
  act_fn = dgate_fn_map[activation]
@@ -316,7 +298,7 @@ def _compile_gemm_dact(
316
  divisibility=1,
317
  )
318
  epi_args = GemmCls.EpilogueArguments(
319
- mPostAct,
320
  act_fn,
321
  mColVecBroadcast=mColVec,
322
  mColVecReduce=mColVecReduce,
@@ -328,7 +310,7 @@ def _compile_gemm_dact(
328
  post_init = _set_implicit_dtype
329
  else:
330
  act_fn = dact_fn_map[activation]
331
- epi_args = GemmCls.EpilogueArguments(mPostAct, act_fn)
332
  post_init = None
333
 
334
  scheduler_args = make_fake_scheduler_args(
@@ -369,6 +351,7 @@ def gemm_dact(
369
  tile_N: int,
370
  cluster_M: int,
371
  cluster_N: int,
 
372
  pingpong: bool = True,
373
  persistent: bool = True,
374
  is_dynamic_persistent: bool = False,
@@ -432,7 +415,9 @@ def gemm_dact(
432
  postact_dtype = torch2cute_dtype_map[PostAct.dtype]
433
 
434
  device_capacity = get_device_capacity(A.device)
435
- assert device_capacity[0] in [9, 10, 11, 12], "Only SM90, SM100, SM110, and SM120 are supported"
 
 
436
 
437
  if is_dynamic_persistent and device_capacity[0] == 9:
438
  assert tile_count_semaphore is not None, (
@@ -451,7 +436,7 @@ def gemm_dact(
451
  d_major,
452
  c_major,
453
  postact_major,
454
- (tile_M, tile_N),
455
  (cluster_M, cluster_N, 1),
456
  pingpong,
457
  persistent,
@@ -468,11 +453,6 @@ def gemm_dact(
468
  use_tma_gather=use_tma_gather,
469
  )
470
 
471
- from .cache_utils import COMPILE_ONLY
472
-
473
- if COMPILE_ONLY:
474
- return
475
-
476
  max_active_clusters = get_max_active_clusters(cluster_M * cluster_N) if persistent else 0
477
  if is_dgated:
478
  epi_args = GemmDGatedMixin.EpilogueArguments(
@@ -498,11 +478,9 @@ def gemm_dact(
498
  varlen_args = make_varlen_args(cu_seqlens_m, None, A_idx)
499
 
500
  if device_capacity[0] in [10, 11]:
501
- compiled_fn(
502
- A_p, B_p, Out_p, PreAct_p, epi_args, scheduler_args, varlen_args, None, None, None
503
- )
504
  else:
505
- compiled_fn(A_p, B_p, Out_p, PreAct_p, epi_args, scheduler_args, varlen_args, None)
506
 
507
 
508
  gemm_dgated = gemm_dact
 
8
  import cutlass
9
  import cutlass.cute as cute
10
  from cutlass import Int32, Float32, const_expr
11
+ from .gemm_sm80 import GemmSm80
12
  from .gemm_sm90 import GemmSm90
13
  from .gemm_sm100 import GemmSm100
14
  from .gemm_sm120 import GemmSm120
15
  from .gemm_default_epi import GemmDefaultEpiMixin
16
  from .gemm_act import GemmActMixin
17
+ from .epi_ops import ColVecLoad, ColVecReduce, Scalar, TileStore, colvec_reduce_accumulate
18
  from .compile_utils import make_fake_tensor as fake_tensor
19
  from .cute_dsl_utils import (
 
20
  mlir_namedtuple,
21
  torch2cute_dtype_map,
22
  get_device_capacity,
 
33
  make_fake_gemm_tensors,
34
  compile_gemm_kernel,
35
  )
36
+ from .cache import jit_cache
37
  from .rounding import RoundingMode
 
38
  from .activation import dact_fn_map, dgate_fn_map
39
+ from . import layout_utils
40
 
41
 
42
  class GemmDActMixin(GemmActMixin):
 
51
  epi_loop_tensors: Tuple[cute.Tensor, ...],
52
  tRS_rD: cute.Tensor,
53
  tRS_rC: Optional[cute.Tensor] = None,
54
+ ) -> Tuple[cute.Tensor, ...]:
55
  assert tRS_rC is not None
56
  # We don't add C to the accumulator
57
  GemmDefaultEpiMixin.epi_visit_subtile(self, params, epi_loop_tensors, tRS_rD, tRS_rC=None)
58
+ tRS_rC_acc = tRS_rC.to(self.acc_dtype)
 
59
  # If we don't have .shape here, the compiler generates local stores and loads
60
  if const_expr(params.act_fn is not None):
61
+ tRS_rAuxOut = cute.make_rmem_tensor(tRS_rD.layout.shape, self.acc_dtype)
62
+ vectorize = const_expr(self.arch == 100)
63
+ for i in cutlass.range(cute.size(tRS_rAuxOut), unroll_full=True, vectorize=vectorize):
64
+ tRS_rD[i], tRS_rAuxOut[i] = params.act_fn(tRS_rC_acc[i], tRS_rD[i])
 
 
 
 
 
 
 
 
 
65
  else:
66
+ tRS_rAuxOut = tRS_rC_acc
67
+ return (tRS_rAuxOut,)
68
 
69
 
70
  class GemmDActSm90(GemmDActMixin, GemmSm90):
71
  pass
72
 
73
 
74
+ class GemmDActSm80(GemmDActMixin, GemmSm80):
75
+ pass
76
+
77
+
78
  class GemmDActSm100(GemmDActMixin, GemmSm100):
79
  pass
80
 
 
86
  class GemmDGatedMixin(GemmActMixin):
87
  # Different from GemmActMixin, here act_bwd_fn must take in 3 arguments (x, y, dout)
88
  # and return 3 arguments (dx, dy, out)
89
+ _epi_ops = (
90
+ ColVecLoad("mColVecBroadcast"),
91
+ Scalar("sr_seed", dtype=Int32),
92
+ TileStore("mAuxOut"),
93
+ ColVecReduce("mColVecReduce"),
94
+ )
95
  _extra_param_fields = (("act_bwd_fn", cutlass.Constexpr, None),)
 
96
 
97
  @mlir_namedtuple
98
  class EpilogueArguments(NamedTuple):
99
+ mAuxOut: cute.Tensor
100
  act_bwd_fn: cutlass.Constexpr[Callable] = None
 
 
 
101
  mColVecBroadcast: Optional[cute.Tensor] = None
102
  mColVecReduce: Optional[cute.Tensor] = None
103
  rounding_mode: cutlass.Constexpr[int] = RoundingMode.RN
 
112
  assert self.d_dtype.width == 32, "D storage type must be 32 bit"
113
  assert self.c_dtype.width == 32, "C storage type must be 32 bit"
114
  self.rounding_mode = args.rounding_mode
115
+ self.aux_out_dtype = args.mAuxOut.element_type
116
+ self.aux_out_layout = cutlass.utils.LayoutEnum.from_tensor(args.mAuxOut)
117
+ self.cta_tile_shape_aux_out_mn = self.cta_tile_shape_mnk[:2]
118
  d = self._epi_ops_to_params_dict(args)
119
  d["act_bwd_fn"] = args.act_bwd_fn
120
  return self.EpilogueParams(**d)
 
128
  epi_loop_tensors: Tuple[cute.Tensor, ...],
129
  tRS_rD: cute.Tensor,
130
  tRS_rC: Optional[cute.Tensor] = None,
131
+ ) -> Tuple[cute.Tensor, ...]:
132
+ tDrColVec = epi_loop_tensors.get("mColVecBroadcast")
133
+ tDrColVecReduce = epi_loop_tensors.get("mColVecReduce")
 
 
 
 
134
  assert tRS_rC is not None
135
  implicit_dtype = self.implicit_dtype
136
  assert implicit_dtype.width == 16, "GemmDGatedMixin only supports 16bit for now"
 
141
  tRS_rOut = cute.make_rmem_tensor_like(tRS_rD, Float32)
142
  tRS_rD_scaled = cute.make_rmem_tensor_like(tRS_rD)
143
  if const_expr(tDrColVec is not None): # Scale D by colvec
144
+ if const_expr(self.arch != 100):
145
  tRS_rD_scaled.store(tRS_rD.load() * tDrColVec.load().to(tRS_rD.element_type))
146
  else:
147
  tDrColVec_mn = layout_utils.convert_layout_zero_stride(tDrColVec, tDrColVec.layout)
 
150
  tRS_rD_scaled, tDrColVec.layout
151
  )
152
  for m in cutlass.range(cute.size(tDrColVec_mn, mode=[0]), unroll_full=True):
153
+ scale = tDrColVec_mn[m, 0]
154
  for n in cutlass.range(
155
+ cute.size(tDrColVec_mn, mode=[1]), unroll_full=True, vectorize=True
156
  ):
157
+ tRS_rD_scaled_mn[m, n] = tRS_rD_mn[m, n] * scale
 
 
 
 
 
 
158
  else:
159
  tRS_rD_scaled.store(tRS_rD.load())
160
+ tRS_rXY_pair = cute.flat_divide(tRS_rXY_f32x2, cute.make_layout(2))
161
+ tRS_rX = tRS_rXY_pair[0, ...]
162
+ tRS_rY = tRS_rXY_pair[1, ...]
163
+ tRS_rdXY_pair = cute.flat_divide(tRS_rdXY_f32x2, cute.make_layout(2))
164
+ tRS_rdX = tRS_rdXY_pair[0, ...]
165
+ tRS_rdY = tRS_rdXY_pair[1, ...]
166
+ vectorize = const_expr(self.arch == 100)
167
+ for i in cutlass.range(cute.size(tRS_rD), vectorize=vectorize):
168
+ tRS_rdX[i], tRS_rdY[i], tRS_rOut[i] = params.act_bwd_fn(
169
+ tRS_rX[i], tRS_rY[i], tRS_rD_scaled[i]
170
+ )
 
 
 
 
 
 
 
 
 
171
  if const_expr(tDrColVecReduce is not None):
172
  # Accumulate postact * dout before D is scaled by colvec_scale
173
  colvec_reduce_accumulate(self, tDrColVecReduce, tRS_rOut, rScale=tRS_rD)
174
 
175
  if const_expr(tDrColVec is not None): # Scale Out by colvec
176
+ if const_expr(self.arch != 100):
177
  tRS_rOut.store(tRS_rOut.load() * tDrColVec.load().to(tRS_rD.element_type))
178
  else:
179
  tDrColVec_mn = layout_utils.convert_layout_zero_stride(tDrColVec, tDrColVec.layout)
180
  tRS_rOut_mn = layout_utils.convert_layout_zero_stride(tRS_rOut, tDrColVec.layout)
181
  for m in cutlass.range(cute.size(tDrColVec_mn, mode=[0]), unroll_full=True):
182
+ scale = tDrColVec_mn[m, 0]
183
  for n in cutlass.range(
184
+ cute.size(tDrColVec_mn, mode=[1]), unroll_full=True, vectorize=True
185
  ):
186
+ tRS_rOut_mn[m, n] = tRS_rOut_mn[m, n] * scale
 
 
 
 
 
187
  # Type conversion
188
  tRS_rdXY_f16x2 = cute.make_rmem_tensor(tRS_rdXY_f32x2.layout, implicit_dtype)
189
  tRS_rdXY_f16x2.store(tRS_rdXY_f32x2.load().to(implicit_dtype))
190
  tRS_rD.store(cute.recast_tensor(tRS_rdXY_f16x2, Float32).load())
191
+ return (tRS_rOut,)
192
 
193
  # epi_end is inherited from ComposableEpiMixin → delegates to ColVecReduce.end()
194
 
 
197
  pass
198
 
199
 
200
+ class GemmDGatedSm80(GemmDGatedMixin, GemmSm80):
201
+ pass
202
+
203
+
204
  class GemmDGatedSm100(GemmDGatedMixin, GemmSm100):
205
  pass
206
 
 
240
  ):
241
  is_dgated = gemm_cls_name == "dgated"
242
  sm_to_cls = {
243
+ "dact": {
244
+ 8: GemmDActSm80,
245
+ 9: GemmDActSm90,
246
+ 10: GemmDActSm100,
247
+ 11: GemmDActSm100,
248
+ 12: GemmDActSm120,
249
+ },
250
  "dgated": {
251
+ 8: GemmDGatedSm80,
252
  9: GemmDGatedSm90,
253
  10: GemmDGatedSm100,
254
  11: GemmDGatedSm100,
255
  12: GemmDGatedSm120,
256
  },
257
  }
 
 
258
  GemmCls = sm_to_cls[gemm_cls_name][device_capacity[0]]
259
  mA, mB, mD, mC, m, n, k, l = make_fake_gemm_tensors(
260
  a_dtype,
 
271
  div_pa = div_for_dtype(postact_dtype)
272
  pa_leading = 1 if postact_major == "n" else 0
273
  pa_shape = (m, n) if varlen_m else (m, n, l)
274
+ mAuxOut = fake_tensor(postact_dtype, pa_shape, leading_dim=pa_leading, divisibility=div_pa)
275
 
276
  if is_dgated:
277
  act_fn = dgate_fn_map[activation]
 
298
  divisibility=1,
299
  )
300
  epi_args = GemmCls.EpilogueArguments(
301
+ mAuxOut,
302
  act_fn,
303
  mColVecBroadcast=mColVec,
304
  mColVecReduce=mColVecReduce,
 
310
  post_init = _set_implicit_dtype
311
  else:
312
  act_fn = dact_fn_map[activation]
313
+ epi_args = GemmCls.EpilogueArguments(mAuxOut, act_fn)
314
  post_init = None
315
 
316
  scheduler_args = make_fake_scheduler_args(
 
351
  tile_N: int,
352
  cluster_M: int,
353
  cluster_N: int,
354
+ tile_K: int | None = None,
355
  pingpong: bool = True,
356
  persistent: bool = True,
357
  is_dynamic_persistent: bool = False,
 
415
  postact_dtype = torch2cute_dtype_map[PostAct.dtype]
416
 
417
  device_capacity = get_device_capacity(A.device)
418
+ assert device_capacity[0] in [8, 9, 10, 11, 12], (
419
+ "Only SM8x, SM90, SM100, SM110, and SM120 are supported"
420
+ )
421
 
422
  if is_dynamic_persistent and device_capacity[0] == 9:
423
  assert tile_count_semaphore is not None, (
 
436
  d_major,
437
  c_major,
438
  postact_major,
439
+ (tile_M, tile_N, tile_K) if tile_K is not None else (tile_M, tile_N),
440
  (cluster_M, cluster_N, 1),
441
  pingpong,
442
  persistent,
 
453
  use_tma_gather=use_tma_gather,
454
  )
455
 
 
 
 
 
 
456
  max_active_clusters = get_max_active_clusters(cluster_M * cluster_N) if persistent else 0
457
  if is_dgated:
458
  epi_args = GemmDGatedMixin.EpilogueArguments(
 
478
  varlen_args = make_varlen_args(cu_seqlens_m, None, A_idx)
479
 
480
  if device_capacity[0] in [10, 11]:
481
+ compiled_fn(A_p, B_p, Out_p, PreAct_p, epi_args, scheduler_args, varlen_args, None, None)
 
 
482
  else:
483
+ compiled_fn(A_p, B_p, Out_p, PreAct_p, epi_args, scheduler_args, varlen_args)
484
 
485
 
486
  gemm_dgated = gemm_dact
build/torch-cuda/quack/gemm_default_epi.py CHANGED
@@ -1,5 +1,5 @@
1
  # Copyright (c) 2025, Wentao Guo, Tri Dao.
2
- from typing import NamedTuple, Optional
3
 
4
  import cutlass
5
  import cutlass.cute as cute
@@ -8,6 +8,7 @@ from cutlass import Int32, Float32, const_expr
8
  from .cute_dsl_utils import mlir_namedtuple
9
  from .epi_composable import ComposableEpiMixin
10
  from .epi_ops import Scalar, RowVecLoad, ColVecLoad
 
11
  from .gemm_sm90 import GemmSm90
12
  from .gemm_sm100 import GemmSm100
13
  from .gemm_sm120 import GemmSm120
@@ -41,7 +42,7 @@ class GemmDefaultEpiMixin(ComposableEpiMixin):
41
  self.rounding_mode = args.rounding_mode
42
  d = self._epi_ops_to_params_dict(args)
43
  for key in ("mRowVecBroadcast", "mColVecBroadcast"):
44
- if key in self.concat_layout and key in d and d[key] is not None:
45
  d[key] = layout_utils.concat_to_interleave(d[key], 1)
46
  return self.EpilogueParams(**d)
47
 
@@ -52,11 +53,18 @@ class GemmDefaultEpiMixin(ComposableEpiMixin):
52
  epi_loop_tensors,
53
  tRS_rD: cute.Tensor,
54
  tRS_rC: Optional[cute.Tensor] = None,
55
- ) -> Optional[cute.Tensor]:
56
- alpha = epi_loop_tensors["alpha"]
57
- beta = epi_loop_tensors["beta"]
58
- tDrRowVec = epi_loop_tensors["mRowVecBroadcast"]
59
- tDrColVec = epi_loop_tensors["mColVecBroadcast"]
 
 
 
 
 
 
 
60
  rD = tRS_rD.load()
61
  # Apply alpha scaling to accumulator if alpha is provided (not None)
62
  if const_expr(hasattr(params, "alpha") and params.alpha is not None):
@@ -77,27 +85,11 @@ class GemmDefaultEpiMixin(ComposableEpiMixin):
77
  if const_expr(tDrColVec is not None):
78
  for i in cutlass.range(cute.size(tDrColVec), unroll_full=True):
79
  tRS_rD[i] += tDrColVec[i]
80
- return None
81
 
82
- def epi_setup_postact(
83
- self,
84
- params,
85
- epi_smem_tensors,
86
- tiled_copy_r2s,
87
- tiled_copy_t2r,
88
- tile_coord_mnkl,
89
- varlen_manager,
90
- tidx,
91
- ):
92
- """Returns None — default epilogue has no postact output."""
93
- return None
94
 
95
- @cute.jit
96
- def epi_convert_postact(
97
- self, tRS_rPostAct, sr_seed, tidx, tile_coord_mnkl, num_prev_subtiles, epi_idx
98
- ):
99
- """Convert postact from acc_dtype to output dtype. Override for custom postprocessing."""
100
- return tRS_rPostAct
101
 
102
 
103
  class GemmDefaultSm90(GemmDefaultEpiMixin, GemmSm90):
 
1
  # Copyright (c) 2025, Wentao Guo, Tri Dao.
2
+ from typing import NamedTuple, Optional, Tuple
3
 
4
  import cutlass
5
  import cutlass.cute as cute
 
8
  from .cute_dsl_utils import mlir_namedtuple
9
  from .epi_composable import ComposableEpiMixin
10
  from .epi_ops import Scalar, RowVecLoad, ColVecLoad
11
+ from .gemm_sm80 import GemmSm80
12
  from .gemm_sm90 import GemmSm90
13
  from .gemm_sm100 import GemmSm100
14
  from .gemm_sm120 import GemmSm120
 
42
  self.rounding_mode = args.rounding_mode
43
  d = self._epi_ops_to_params_dict(args)
44
  for key in ("mRowVecBroadcast", "mColVecBroadcast"):
45
+ if key in self.concat_layout and key in d:
46
  d[key] = layout_utils.concat_to_interleave(d[key], 1)
47
  return self.EpilogueParams(**d)
48
 
 
53
  epi_loop_tensors,
54
  tRS_rD: cute.Tensor,
55
  tRS_rC: Optional[cute.Tensor] = None,
56
+ ) -> Tuple[cute.Tensor, ...]:
57
+ """Return a tuple of register tensors (one per aux output).
58
+
59
+ The returned tuple must be the same length as the tuple returned
60
+ from :meth:`epi_setup_aux_out`. The default impl returns ``()`` —
61
+ no aux outputs.
62
+ """
63
+ # Use .get(): inactive ops are filtered out of epi_loop_tensors.
64
+ alpha = epi_loop_tensors.get("alpha")
65
+ beta = epi_loop_tensors.get("beta")
66
+ tDrRowVec = epi_loop_tensors.get("mRowVecBroadcast")
67
+ tDrColVec = epi_loop_tensors.get("mColVecBroadcast")
68
  rD = tRS_rD.load()
69
  # Apply alpha scaling to accumulator if alpha is provided (not None)
70
  if const_expr(hasattr(params, "alpha") and params.alpha is not None):
 
85
  if const_expr(tDrColVec is not None):
86
  for i in cutlass.range(cute.size(tDrColVec), unroll_full=True):
87
  tRS_rD[i] += tDrColVec[i]
88
+ return ()
89
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ class GemmDefaultSm80(GemmDefaultEpiMixin, GemmSm80):
92
+ pass
 
 
 
 
93
 
94
 
95
  class GemmDefaultSm90(GemmDefaultEpiMixin, GemmSm90):
build/torch-cuda/quack/gemm_interface.py CHANGED
@@ -3,7 +3,7 @@ from typing import Optional, Tuple, Literal
3
  from functools import partial
4
 
5
  import torch
6
- from ._ops_compat import add_quack_op_namespace_prefix
7
  import torch.nn.functional as F
8
  from torch import Tensor
9
 
@@ -21,12 +21,49 @@ from .rms_final_reduce import rms_final_reduce
21
  from .rounding import RoundingMode
22
 
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  # Dictionary mapping activation names to PyTorch functions
25
  act_to_pytorch_fn_map = {
26
  None: lambda x: x,
 
 
27
  "relu": F.relu,
28
  "relu_sq": lambda x: F.relu(x).square(),
29
  "gelu_tanh_approx": partial(F.gelu, approximate="tanh"),
 
30
  }
31
 
32
 
@@ -34,22 +71,37 @@ act_to_pytorch_fn_map = {
34
  # Each function takes (gate, up) and returns postact
35
  gated_to_pytorch_fn_map = {
36
  "swiglu": lambda gate, up: F.silu(gate) * up,
 
37
  "swiglu_oai": lambda gate, up: gate * torch.sigmoid(1.702 * gate) * (up + 1),
 
38
  "reglu": lambda gate, up: F.relu(gate) * up,
39
  "geglu": lambda gate, up: F.gelu(gate, approximate="tanh") * up,
40
  "glu": lambda gate, up: torch.sigmoid(gate) * up,
41
  }
42
 
43
 
44
- ActActivation = Literal[None, "relu", "relu_sq", "gelu_tanh_approx"]
45
- GatedActivation = Literal["swiglu", "swiglu_oai", "reglu", "geglu", "glu"]
 
 
 
 
 
 
 
 
46
  Activation = Literal[
47
  None,
 
 
48
  "relu",
49
  "relu_sq",
50
  "gelu_tanh_approx",
 
51
  "swiglu",
 
52
  "swiglu_oai",
 
53
  "reglu",
54
  "geglu",
55
  "glu",
@@ -68,9 +120,88 @@ def _concat_interleave_bias(t):
68
  return t.unflatten(-1, (2, half)).transpose(-2, -1).flatten(-2, -1)
69
 
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  def default_config(device):
72
  cap = get_device_capacity(device)[0]
73
- if cap in [10, 11]:
 
 
 
 
 
 
 
 
 
 
 
 
74
  return GemmConfig(
75
  tile_m=256,
76
  tile_n=256,
@@ -101,6 +232,31 @@ def default_config(device):
101
  )
102
 
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  def nvmmh_config(A, B, device_capacity):
105
  """Use nvMatmulHeuristics to pick a config for pure GEMM (no varlen/gather/epilogue).
106
 
@@ -130,6 +286,21 @@ def prune_invalid_gemm_configs(configs, named_args: dict, **kwargs):
130
  # use_tma_gather only valid when gather_A is active on SM100/SM110
131
  if not gather_A or device_capacity not in [10, 11]:
132
  configs = [conf for conf in configs if not conf.kwargs["config"].use_tma_gather]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  return configs
134
 
135
 
@@ -157,26 +328,39 @@ def gemm_tuned(
157
  rounding_mode: int = RoundingMode.RN,
158
  sr_seed: int | Tensor = 0,
159
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
 
 
160
  ) -> None:
 
 
 
161
  if config is None:
162
- # Use nvMMH heuristic for pure GEMM (no varlen, no gather, no epilogue)
163
- is_pure_gemm = (
164
- cu_seqlens_m is None
165
- and cu_seqlens_k is None
166
- and A_idx is None
167
- and C is None
168
- and bias is None
169
- and not add_to_output
170
- )
171
- if is_pure_gemm:
172
- device_capacity = get_device_capacity(A.device)[0]
173
- config = nvmmh_config(A, B, device_capacity)
174
- if config is None:
175
- config = default_config(A.device)
 
 
 
 
176
  varlen_m = cu_seqlens_m is not None
177
  varlen_k = cu_seqlens_k is not None
178
  varlen = varlen_m or varlen_k
179
  gather_A = A_idx is not None
 
 
 
 
180
  if gather_A:
181
  assert varlen, "gather_A requires either varlen_m or varlen_k"
182
  assert config.cluster_n == 1, "gather_A requires cluster_n=1"
@@ -235,7 +419,8 @@ def gemm_tuned(
235
  config.tile_n,
236
  config.cluster_m,
237
  config.cluster_n,
238
- config.pingpong,
 
239
  persistent=True,
240
  is_dynamic_persistent=dynamic_scheduler,
241
  max_swizzle_size=config.max_swizzle_size,
@@ -252,6 +437,10 @@ def gemm_tuned(
252
  sr_seed=sr_seed,
253
  use_tma_gather=config.use_tma_gather,
254
  concat_layout=swapped_concat,
 
 
 
 
255
  )
256
 
257
 
@@ -274,12 +463,23 @@ def gemm_act_tuned(
274
  A_idx: Optional[Tensor] = None, # (total_M,) if gather_A with varlen_m
275
  dynamic_scheduler: bool = False,
276
  config: Optional[GemmConfig] = None,
 
 
277
  ) -> None:
 
 
 
278
  if config is None:
279
- config = default_config(A.device)
 
 
 
280
  varlen_m = cu_seqlens_m is not None
281
  if varlen_m:
282
  assert not config.swap_ab, "Variable-length sequences not supported with swap_ab"
 
 
 
283
  if A.ndim == 2 and not varlen_m:
284
  A = A.unsqueeze(0) # (1, M, K)
285
  B = B.mT # (N, K) or (L, N, K)
@@ -315,7 +515,8 @@ def gemm_act_tuned(
315
  config.tile_n,
316
  config.cluster_m,
317
  config.cluster_n,
318
- config.pingpong,
 
319
  persistent=True,
320
  is_dynamic_persistent=dynamic_scheduler,
321
  max_swizzle_size=config.max_swizzle_size,
@@ -324,6 +525,8 @@ def gemm_act_tuned(
324
  cu_seqlens_m=cu_seqlens_m,
325
  A_idx=A_idx,
326
  use_tma_gather=config.use_tma_gather,
 
 
327
  )
328
 
329
 
@@ -383,7 +586,8 @@ def gemm_dact_tuned(
383
  config.tile_n,
384
  config.cluster_m,
385
  config.cluster_n,
386
- config.pingpong,
 
387
  persistent=True,
388
  is_dynamic_persistent=dynamic_scheduler,
389
  max_swizzle_size=config.max_swizzle_size,
@@ -395,8 +599,10 @@ def gemm_dact_tuned(
395
 
396
  def gemm(
397
  # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (M, total_K) if varlen_k or (whatever, K) if gather_A with varlen_m or (M, whatever) if gather_A with varlen_k
398
- A: Tensor,
399
- B: Tensor, # (K, N) or (L, K, N) or (total_K, N) if varlen_k
 
 
400
  out: Optional[Tensor] = None, # (M, N) or (L, M, N) or (total_M, N) if varlen_m
401
  bias: Optional[Tensor] = None, # (N,) or (L, N)
402
  alpha: float | Tensor = 1.0,
@@ -412,8 +618,16 @@ def gemm(
412
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
413
  ) -> Tensor:
414
  """GEMM with optional output tensor and tuning control."""
 
 
 
 
 
 
415
  if out is None:
416
- out_dtype = A.dtype if out_dtype is None else out_dtype
 
 
417
  varlen_m = cu_seqlens_m is not None
418
  varlen_k = cu_seqlens_k is not None
419
  if varlen_m:
@@ -428,6 +642,15 @@ def gemm(
428
  (A.shape[0], B.shape[-1]) if A.ndim == 2 else (A.shape[0], A.shape[-2], B.shape[-1])
429
  )
430
  out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
 
 
 
 
 
 
 
 
 
431
  alpha_tensor = alpha if not isinstance(alpha, float) else None
432
  alpha = alpha if isinstance(alpha, float) else 1.0
433
  sr_seed_tensor = sr_seed if isinstance(sr_seed, Tensor) else None
@@ -450,12 +673,14 @@ def gemm(
450
  sr_seed=sr_seed_int,
451
  sr_seed_tensor=sr_seed_tensor,
452
  concat_layout=concat_str,
 
 
453
  )
454
  return out
455
 
456
 
457
  @torch.library.custom_op(
458
- add_quack_op_namespace_prefix("gemm_out"),
459
  mutates_args=("out",),
460
  device_types="cuda",
461
  # We have to split out alpha and alpha_tensor since torch.library requires
@@ -480,11 +705,18 @@ def gemm_out(
480
  sr_seed: int = 0,
481
  sr_seed_tensor: Optional[Tensor] = None,
482
  concat_layout: Optional[str] = None,
 
 
 
 
 
483
  ) -> None:
484
  """GEMM with pre-allocated output tensor."""
485
  fn = gemm_tuned if tuned else partial(gemm_tuned.fn, config=None)
486
- alpha = alpha_tensor if alpha_tensor is not None else alpha
487
- sr_seed_arg = sr_seed_tensor if sr_seed_tensor is not None else sr_seed
 
 
488
  fn(
489
  A,
490
  B,
@@ -499,7 +731,9 @@ def gemm_out(
499
  dynamic_scheduler=dynamic_scheduler,
500
  rounding_mode=rounding_mode,
501
  sr_seed=sr_seed_arg,
502
- concat_layout=tuple(concat_layout.split(",")) if concat_layout else None,
 
 
503
  )
504
 
505
 
@@ -572,10 +806,43 @@ def gemm_ref(
572
  return out
573
 
574
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
575
  def gemm_add(
576
  # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (M, total_K) if varlen_k or (whatever, K) if gather_A with varlen_m or (M, whatever) if gather_A with varlen_k
577
- A: Tensor,
578
- B: Tensor, # (K, N) or (L, K, N) or (total_K, N) if varlen_k
 
579
  C: Tensor, # (M, N) or (L, M, N) or (total_M, N) if varlen_m or (L, M, N) if varlen_k
580
  out: Optional[Tensor] = None, # (M, N) or (L, M, N) or (total_M, N) if varlen_m
581
  alpha: float | Tensor = 1.0,
@@ -590,8 +857,15 @@ def gemm_add(
590
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
591
  ) -> Tensor:
592
  """GEMM with addition and optional output tensor."""
 
 
 
 
 
 
593
  if out is None:
594
- out_dtype = A.dtype if out_dtype is None else out_dtype
 
595
  varlen_m = cu_seqlens_m is not None
596
  varlen_k = cu_seqlens_k is not None
597
  if varlen_m:
@@ -608,17 +882,26 @@ def gemm_add(
608
  )
609
  out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
610
  add_to_output = C is out and isinstance(beta, float) and beta == 1.0 and cu_seqlens_m is None
 
 
 
 
 
 
 
 
 
611
  alpha_tensor = alpha if not isinstance(alpha, float) else None
612
  alpha = alpha if isinstance(alpha, float) else 1.0
613
  beta_tensor = beta if not isinstance(beta, float) else None
614
  beta = beta if isinstance(beta, float) else 1.0
615
- alpha_arg = alpha_tensor if alpha_tensor is not None else alpha
616
- beta_arg = beta_tensor if beta_tensor is not None else beta
617
  concat_str = ",".join(concat_layout) if concat_layout else None
618
  if add_to_output:
619
  gemm_add_inplace(
620
- A,
621
- B,
622
  out,
623
  alpha=alpha_arg,
624
  beta=beta_arg,
@@ -648,12 +931,14 @@ def gemm_add(
648
  dynamic_scheduler=dynamic_scheduler,
649
  tuned=tuned,
650
  concat_layout=concat_str,
 
 
651
  )
652
  return out
653
 
654
 
655
  @torch.library.custom_op(
656
- add_quack_op_namespace_prefix("gemm_add_out"),
657
  mutates_args=("out",),
658
  device_types="cuda",
659
  # We have to split out alpha and alpha_tensor since torch.library requires
@@ -678,11 +963,13 @@ def gemm_add_out(
678
  dynamic_scheduler: bool = False,
679
  tuned: bool = True,
680
  concat_layout: Optional[str] = None,
 
 
681
  ) -> None:
682
  """GEMM with addition and pre-allocated output tensor."""
683
  fn = gemm_tuned if tuned else partial(gemm_tuned.fn, config=None)
684
- alpha = alpha_tensor if alpha_tensor is not None else alpha
685
- beta = beta_tensor if beta_tensor is not None else beta
686
  fn(
687
  A,
688
  B,
@@ -690,13 +977,15 @@ def gemm_add_out(
690
  C,
691
  alpha=alpha,
692
  beta=beta,
 
 
693
  cu_seqlens_m=cu_seqlens_m,
694
  cu_seqlens_k=cu_seqlens_k,
695
  A_idx=A_idx,
696
  batch_idx_permute=batch_idx_permute,
697
  add_to_output=add_to_output,
698
  dynamic_scheduler=dynamic_scheduler,
699
- concat_layout=tuple(concat_layout.split(",")) if concat_layout else None,
700
  )
701
 
702
 
@@ -783,8 +1072,9 @@ def gemm_add_ref(
783
 
784
  def gemm_add_inplace(
785
  # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (M, total_K) if varlen_k or (whatever, K) if gather_A with varlen_m or (M, whatever) if gather_A with varlen_k
786
- A: Tensor,
787
- B: Tensor, # (K, N) or (L, K, N) or (total_K, N) if varlen_k
 
788
  out: Tensor, # (M, N) or (L, M, N) or (total_M, N) if varlen_m or (L, M, N) if varlen_k
789
  alpha: float | Tensor = 1.0,
790
  beta: float | Tensor = 1.0,
@@ -808,10 +1098,24 @@ def gemm_add_inplace(
808
  dynamic_scheduler: Whether to use dynamic scheduler
809
  tuned: Whether to use autotuned configuration
810
  """
 
 
 
 
 
 
811
  alpha_tensor = alpha if not isinstance(alpha, float) else None
812
  alpha = alpha if isinstance(alpha, float) else 1.0
813
  beta_tensor = beta if not isinstance(beta, float) else None
814
  beta = beta if isinstance(beta, float) else 1.0
 
 
 
 
 
 
 
 
815
  gemm_add_inplace_op(
816
  A,
817
  B,
@@ -829,11 +1133,13 @@ def gemm_add_inplace(
829
  concat_layout=",".join(concat_layout)
830
  if isinstance(concat_layout, tuple)
831
  else concat_layout,
 
 
832
  )
833
 
834
 
835
  @torch.library.custom_op(
836
- add_quack_op_namespace_prefix("gemm_add_inplace"),
837
  mutates_args=("out",),
838
  device_types="cuda",
839
  # We have to split out alpha and alpha_tensor since torch.library requires
@@ -856,10 +1162,12 @@ def gemm_add_inplace_op(
856
  dynamic_scheduler: bool = False,
857
  tuned: bool = True,
858
  concat_layout: Optional[str] = None,
 
 
859
  ) -> None:
860
  fn = gemm_tuned if tuned else partial(gemm_tuned.fn, config=None)
861
- alpha = alpha_tensor if alpha_tensor is not None else alpha
862
- beta = beta_tensor if beta_tensor is not None else beta
863
  add_to_output = isinstance(beta, float) and beta == 1.0 and cu_seqlens_m is None
864
  # Use out as both input bias and output
865
  fn(
@@ -875,13 +1183,19 @@ def gemm_add_inplace_op(
875
  batch_idx_permute=batch_idx_permute,
876
  add_to_output=add_to_output,
877
  dynamic_scheduler=dynamic_scheduler,
878
- concat_layout=tuple(concat_layout.split(",")) if concat_layout else None,
 
 
879
  )
880
 
881
 
882
  def gemm_act(
883
- A: Tensor, # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (whatever, K) if gather_A with varlen_m
884
- B: Tensor, # (K, N) or (L, K, N)
 
 
 
 
885
  C: Optional[Tensor] = None, # (M, N) or (L, M, N) or (total_M, N) if varlen_m
886
  bias: Optional[Tensor] = None, # (N,) or (L, N)
887
  activation: Activation = None,
@@ -897,9 +1211,16 @@ def gemm_act(
897
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
898
  ) -> Tuple[Optional[Tensor], Tensor]:
899
  """GEMM with activation (or gated activation) and optional output tensors."""
 
 
 
 
 
 
900
  is_gated = activation in gated_to_pytorch_fn_map
901
- out_dtype = A.dtype if out_dtype is None else out_dtype
902
- postact_dtype = A.dtype if postact_dtype is None else postact_dtype
 
903
  varlen_m = cu_seqlens_m is not None
904
  # Determine output shape based on gather_A
905
  if varlen_m:
@@ -914,6 +1235,14 @@ def gemm_act(
914
  preact_out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
915
  if postact_out is None:
916
  postact_out = torch.empty(postact_shape, dtype=postact_dtype, device=A.device)
 
 
 
 
 
 
 
 
917
  concat_str = ",".join(concat_layout) if concat_layout else None
918
  if is_gated:
919
  gemm_gated_out(
@@ -929,6 +1258,8 @@ def gemm_act(
929
  dynamic_scheduler,
930
  tuned,
931
  concat_layout=concat_str,
 
 
932
  )
933
  else:
934
  gemm_act_out(
@@ -943,6 +1274,8 @@ def gemm_act(
943
  A_idx,
944
  dynamic_scheduler,
945
  tuned,
 
 
946
  )
947
  return preact_out, postact_out
948
 
@@ -951,10 +1284,10 @@ gemm_gated = gemm_act
951
 
952
 
953
  @torch.library.custom_op(
954
- add_quack_op_namespace_prefix("gemm_act_out"),
955
  mutates_args=("preact_out", "postact_out"),
956
  device_types="cuda",
957
- schema="(Tensor A, Tensor B, Tensor(a2!)? preact_out, Tensor(a3!) postact_out, Tensor? C=None, Tensor? bias=None, str? activation=None, Tensor? cu_seqlens_m=None, Tensor? A_idx=None, bool dynamic_scheduler=False, bool tuned=True) -> ()",
958
  )
959
  def gemm_act_out(
960
  A: Tensor, # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (whatever, K) if gather_A with varlen_m
@@ -968,10 +1301,25 @@ def gemm_act_out(
968
  A_idx: Optional[Tensor] = None, # (total_M,) if gather_A with varlen_m
969
  dynamic_scheduler: bool = False,
970
  tuned: bool = True,
 
 
971
  ) -> None:
972
  """GEMM with activation and pre-allocated output tensors."""
973
  fn = gemm_act_tuned if tuned else partial(gemm_act_tuned.fn, config=None)
974
- fn(A, B, preact_out, postact_out, C, bias, activation, cu_seqlens_m, A_idx, dynamic_scheduler)
 
 
 
 
 
 
 
 
 
 
 
 
 
975
 
976
 
977
  def gemm_act_ref(
@@ -1048,6 +1396,16 @@ def gemm_dact(
1048
  dx_out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
1049
  if postact_out is None:
1050
  postact_out = torch.empty(postact_shape, dtype=postact_dtype, device=A.device)
 
 
 
 
 
 
 
 
 
 
1051
  if is_dgated:
1052
  colvec_reduce_final = gemm_dgated_out(
1053
  A,
@@ -1063,10 +1421,10 @@ def gemm_dact(
1063
  dynamic_scheduler,
1064
  tuned,
1065
  )
1066
- if not colvec_reduce:
1067
- return dx_out, postact_out
1068
- else:
1069
- return dx_out, postact_out, colvec_reduce_final
1070
  else:
1071
  gemm_dact_out(
1072
  A,
@@ -1080,14 +1438,15 @@ def gemm_dact(
1080
  dynamic_scheduler,
1081
  tuned,
1082
  )
1083
- return dx_out, postact_out
 
1084
 
1085
 
1086
  gemm_dgated = gemm_dact
1087
 
1088
 
1089
  @torch.library.custom_op(
1090
- add_quack_op_namespace_prefix("gemm_dact_out"),
1091
  mutates_args=("dx_out", "postact_out"),
1092
  device_types="cuda",
1093
  schema="(Tensor A, Tensor B, Tensor PreAct, Tensor(a3!) dx_out, Tensor(a4!) postact_out, str? activation=None, Tensor? cu_seqlens_m=None, Tensor? A_idx=None, bool dynamic_scheduler=True, bool tuned=True) -> ()",
@@ -1152,11 +1511,27 @@ def gemm_dact_ref(
1152
  gemm_dgated_ref = gemm_dact_ref
1153
 
1154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1155
  @torch.library.custom_op(
1156
- add_quack_op_namespace_prefix("gemm_symmetric_out"),
1157
  mutates_args=("out",),
1158
  device_types="cuda",
1159
- schema="(Tensor A, Tensor B, Tensor(a2!) out, Tensor? C=None, bool dynamic_scheduler=False, float alpha=1.0, float beta=1.0) -> ()",
 
1160
  )
1161
  def gemm_symmetric_out(
1162
  A: Tensor, # (M, K) or (L, M, K)
@@ -1166,8 +1541,12 @@ def gemm_symmetric_out(
1166
  dynamic_scheduler: bool = False,
1167
  alpha: float = 1.0,
1168
  beta: float = 1.0,
 
 
1169
  ) -> None:
1170
  """GEMM with guaranteed symmetric output."""
 
 
1171
  if A.ndim == 2:
1172
  A = A.unsqueeze(0) # (1, M, K)
1173
  B = B.mT # (M, K) or (L, M, K)
@@ -1184,12 +1563,7 @@ def gemm_symmetric_out(
1184
  )
1185
  sm = get_device_capacity(A.device)[0]
1186
  # We want square tile per cluster
1187
- tile_m, tile_n, cluster_m, pingpong = {
1188
- 9: (128, 256, 2, False),
1189
- 10: (256, 256, 2, False),
1190
- 11: (256, 256, 2, False),
1191
- 12: (128, 128, 1, True),
1192
- }[sm]
1193
  gemm_symmetric_dispatch(
1194
  A,
1195
  B,
@@ -1229,11 +1603,29 @@ def gemm_symmetric(
1229
  if out is None:
1230
  out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
1231
 
 
1232
  alpha_val = alpha if isinstance(alpha, float) else 1.0
 
1233
  beta_val = beta if isinstance(beta, float) else 1.0
1234
 
 
 
 
 
 
 
 
 
1235
  gemm_symmetric_out(
1236
- A, B, out, C, dynamic_scheduler=dynamic_scheduler, alpha=alpha_val, beta=beta_val
 
 
 
 
 
 
 
 
1237
  )
1238
  return out
1239
 
@@ -1258,12 +1650,24 @@ def gemm_gated_tuned(
1258
  dynamic_scheduler: bool = False,
1259
  config: Optional[GemmConfig] = None,
1260
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
 
 
1261
  ) -> None:
 
 
 
1262
  if config is None:
1263
- config = default_config(A.device)
 
 
 
1264
  varlen_m = cu_seqlens_m is not None
1265
  if varlen_m:
1266
  assert not config.swap_ab, "Variable-length sequences not supported with swap_ab"
 
 
 
 
1267
  if A.ndim == 2 and not varlen_m:
1268
  A = A.unsqueeze(0) # (1, M, K)
1269
  B = B.mT # (N, K) or (L, N, K)
@@ -1307,7 +1711,8 @@ def gemm_gated_tuned(
1307
  config.tile_n,
1308
  config.cluster_m,
1309
  config.cluster_n,
1310
- config.pingpong,
 
1311
  persistent=True,
1312
  is_dynamic_persistent=dynamic_scheduler,
1313
  max_swizzle_size=config.max_swizzle_size,
@@ -1317,6 +1722,8 @@ def gemm_gated_tuned(
1317
  A_idx=A_idx,
1318
  use_tma_gather=config.use_tma_gather,
1319
  concat_layout=concat_layout,
 
 
1320
  )
1321
 
1322
 
@@ -1403,7 +1810,8 @@ def gemm_dgated_tuned(
1403
  config.tile_n,
1404
  config.cluster_m,
1405
  config.cluster_n,
1406
- config.pingpong,
 
1407
  persistent=True,
1408
  is_dynamic_persistent=dynamic_scheduler,
1409
  max_swizzle_size=config.max_swizzle_size,
@@ -1423,10 +1831,10 @@ def gemm_dgated_tuned(
1423
 
1424
 
1425
  @torch.library.custom_op(
1426
- add_quack_op_namespace_prefix("gemm_gated_out"),
1427
  mutates_args=("preact_out", "postact_out"),
1428
  device_types="cuda",
1429
- schema="(Tensor A, Tensor B, Tensor(a2!)? preact_out, Tensor(a3!) postact_out, Tensor? C=None, Tensor? bias=None, str activation='swiglu', Tensor? cu_seqlens_m=None, Tensor? A_idx=None, bool dynamic_scheduler=False, bool tuned=True, str? concat_layout=None) -> ()",
1430
  )
1431
  def gemm_gated_out(
1432
  A: Tensor, # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (whatever, K) if gather_A with varlen_m
@@ -1441,6 +1849,8 @@ def gemm_gated_out(
1441
  dynamic_scheduler: bool = False,
1442
  tuned: bool = True,
1443
  concat_layout: Optional[str] = None,
 
 
1444
  ) -> None:
1445
  """GEMM with gated activation and pre-allocated output tensors."""
1446
  fn = gemm_gated_tuned if tuned else partial(gemm_gated_tuned.fn, config=None)
@@ -1455,12 +1865,14 @@ def gemm_gated_out(
1455
  cu_seqlens_m,
1456
  A_idx,
1457
  dynamic_scheduler,
1458
- concat_layout=tuple(concat_layout.split(",")) if concat_layout else None,
 
 
1459
  )
1460
 
1461
 
1462
  @torch.library.custom_op(
1463
- add_quack_op_namespace_prefix("gemm_dgated_out"),
1464
  mutates_args=("dx_out", "postact_out"),
1465
  device_types="cuda",
1466
  schema="(Tensor A, Tensor B, Tensor PreAct, Tensor(a!) dx_out, Tensor(b!) postact_out, Tensor? colvec_scale=None, str activation='swiglu', bool colvec_reduce=False, Tensor? cu_seqlens_m=None, Tensor? A_idx=None, bool dynamic_scheduler=True, bool tuned=True) -> Tensor",
@@ -1499,7 +1911,7 @@ def gemm_dgated_out(
1499
  return result
1500
 
1501
 
1502
- @torch.library.register_fake(add_quack_op_namespace_prefix("gemm_dgated_out"))
1503
  def gemm_dgated_out_fake(
1504
  A: Tensor,
1505
  B: Tensor,
@@ -1514,20 +1926,6 @@ def gemm_dgated_out_fake(
1514
  dynamic_scheduler: bool = True,
1515
  tuned: bool = True,
1516
  ) -> Tensor:
1517
- _precompile_default_config(
1518
- gemm_dgated_tuned,
1519
- A,
1520
- B,
1521
- PreAct,
1522
- dx_out,
1523
- postact_out,
1524
- colvec_scale=colvec_scale,
1525
- activation=activation,
1526
- colvec_reduce=colvec_reduce,
1527
- cu_seqlens_m=cu_seqlens_m,
1528
- A_idx=A_idx,
1529
- dynamic_scheduler=dynamic_scheduler,
1530
- )
1531
  if not colvec_reduce:
1532
  return torch.empty(0, dtype=torch.float32, device=A.device)
1533
  else:
@@ -1541,25 +1939,6 @@ def gemm_dgated_out_fake(
1541
  return torch.empty(out_shape, dtype=torch.float32, device=A.device)
1542
 
1543
 
1544
- def _precompile_default_config(autotuned_fn, *args, **kwargs):
1545
- """Compile the default config in COMPILE_ONLY mode.
1546
-
1547
- Checks COMPILE_ONLY flag and SymInt guard, then calls the unwrapped function with
1548
- config=None (which selects the default config), triggering compilation (exports .o)
1549
- without benchmarking or kernel launch.
1550
- Tests use tuned=False which also selects the default config, so this is sufficient.
1551
- """
1552
- from .cache_utils import COMPILE_ONLY
1553
-
1554
- A = args[0] if args else kwargs.get("A")
1555
- if not COMPILE_ONLY or A is None or isinstance(A.shape[0], torch.SymInt):
1556
- return
1557
- try:
1558
- autotuned_fn.fn(*args, config=None, **kwargs)
1559
- except Exception:
1560
- pass
1561
-
1562
-
1563
  @gemm_add_inplace_op.register_fake
1564
  def gemm_add_inplace_fake(
1565
  A: Tensor,
@@ -1575,73 +1954,71 @@ def gemm_add_inplace_fake(
1575
  batch_idx_permute: Optional[Tensor] = None,
1576
  dynamic_scheduler: bool = False,
1577
  tuned: bool = True,
 
 
 
1578
  ) -> None:
1579
- alpha_val = alpha_tensor if alpha_tensor is not None else alpha
1580
- beta_val = beta_tensor if beta_tensor is not None else beta
1581
- add_to_output = isinstance(beta_val, float) and beta_val == 1.0 and cu_seqlens_m is None
1582
- _precompile_default_config(
1583
- gemm_tuned,
1584
- A,
1585
- B,
1586
- out,
1587
- out if not add_to_output else None,
1588
- alpha=alpha_val,
1589
- beta=beta_val,
1590
- cu_seqlens_m=cu_seqlens_m,
1591
- cu_seqlens_k=cu_seqlens_k,
1592
- A_idx=A_idx,
1593
- batch_idx_permute=batch_idx_permute,
1594
- add_to_output=add_to_output,
1595
- dynamic_scheduler=dynamic_scheduler,
1596
- )
 
 
 
 
 
 
 
 
1597
 
1598
 
1599
- def _register_precompile_fake(custom_op, autotuned_fn, rewrite=None):
1600
- """Register a fake that precompiles the default config in COMPILE_ONLY mode.
 
1601
 
1602
- For custom_ops that forward args to their autotuned fn. Binds all args by name,
1603
- strips 'tuned', applies optional rewrite(kw), then calls _precompile_default_config.
1604
- PyTorch normalizes all custom_op args to positional, so we use inspect.signature
1605
- to recover keyword names.
 
1606
  """
1607
- import inspect
 
 
1608
 
1609
- sig = inspect.signature(custom_op._init_fn)
1610
 
1611
- @custom_op.register_fake
1612
- def _fake(*args, **kwargs):
1613
- bound = sig.bind(*args, **kwargs)
1614
- bound.apply_defaults()
1615
- kw = dict(bound.arguments)
1616
- kw.pop("tuned", None)
1617
- if rewrite is not None:
1618
- rewrite(kw)
1619
- _precompile_default_config(autotuned_fn, **kw)
1620
-
1621
-
1622
- def _rewrite_merge_alpha(kwargs):
1623
- """Merge alpha_tensor into alpha for gemm_tuned; add C=None."""
1624
- at = kwargs.pop("alpha_tensor", None)
1625
- if at is not None:
1626
- kwargs["alpha"] = at
1627
- kwargs.setdefault("C", None)
1628
 
 
 
 
 
1629
 
1630
- def _rewrite_merge_alpha_beta(kwargs):
1631
- """Merge alpha_tensor/beta_tensor into alpha/beta for gemm_tuned."""
1632
- at = kwargs.pop("alpha_tensor", None)
1633
- if at is not None:
1634
- kwargs["alpha"] = at
1635
- bt = kwargs.pop("beta_tensor", None)
1636
- if bt is not None:
1637
- kwargs["beta"] = bt
1638
 
1639
 
1640
- _register_precompile_fake(gemm_out, gemm_tuned, rewrite=_rewrite_merge_alpha)
1641
- _register_precompile_fake(gemm_add_out, gemm_tuned, rewrite=_rewrite_merge_alpha_beta)
1642
- _register_precompile_fake(gemm_act_out, gemm_act_tuned)
1643
- _register_precompile_fake(gemm_dact_out, gemm_dact_tuned)
1644
- _register_precompile_fake(gemm_gated_out, gemm_gated_tuned)
1645
 
1646
 
1647
  @gemm_symmetric_out.register_fake
@@ -1653,35 +2030,12 @@ def gemm_symmetric_out_fake(
1653
  dynamic_scheduler: bool = False,
1654
  alpha: float = 1.0,
1655
  beta: float = 1.0,
 
 
1656
  ) -> None:
1657
- from .cache_utils import COMPILE_ONLY
1658
-
1659
- if not COMPILE_ONLY or isinstance(A.shape[0], torch.SymInt):
1660
- return
1661
- # gemm_symmetric is not autotuned, compile the single fixed config directly
1662
- sm = get_device_capacity(A.device)[0]
1663
- tile_m = 256 if sm == 10 else 128
1664
- tile_n = 128 if sm == 12 else 256
1665
- cluster_m = 1 if sm == 12 else 2
1666
- try:
1667
- gemm_symmetric_dispatch(
1668
- A.unsqueeze(0) if A.ndim == 2 else A,
1669
- (B.mT.unsqueeze(0) if B.ndim == 2 else B.mT),
1670
- out.unsqueeze(0) if out.ndim == 2 else out,
1671
- (C.unsqueeze(0) if C.ndim == 2 else C) if C is not None else None,
1672
- torch.zeros(1, dtype=torch.int32, device=A.device) if dynamic_scheduler else None,
1673
- tile_M=tile_m,
1674
- tile_N=tile_n,
1675
- cluster_M=cluster_m,
1676
- cluster_N=1,
1677
- pingpong=False,
1678
- persistent=True,
1679
- max_swizzle_size=8,
1680
- alpha=alpha,
1681
- beta=beta,
1682
- )
1683
- except Exception:
1684
- pass
1685
 
1686
 
1687
  ## ── gemm_rms ────────────────────────────────────────────────────────────────
@@ -1704,6 +2058,7 @@ def _gemm_rms_tuned(
1704
  out: Tensor, # (M, N) or (L, M, N)
1705
  C: Optional[Tensor] = None, # (M, N) or (L, M, N)
1706
  norm_weight: Optional[Tensor] = None, # (N,) or (L, N)
 
1707
  eps: float = 1e-6,
1708
  dynamic_scheduler: bool = False,
1709
  config: Optional[GemmConfig] = None,
@@ -1723,6 +2078,8 @@ def _gemm_rms_tuned(
1723
  C = C.unsqueeze(0)
1724
  if norm_weight is not None and norm_weight.ndim == 1:
1725
  norm_weight = norm_weight.unsqueeze(0) # (L, N)
 
 
1726
  # Allocate partial reduction buffer
1727
  tile_n = config.tile_n
1728
  n_tiles = (N + tile_n - 1) // tile_n
@@ -1746,11 +2103,13 @@ def _gemm_rms_tuned(
1746
  config.tile_n,
1747
  config.cluster_m,
1748
  config.cluster_n,
1749
- config.pingpong,
 
1750
  persistent=True,
1751
  is_dynamic_persistent=dynamic_scheduler,
1752
  max_swizzle_size=config.max_swizzle_size,
1753
  rowvec=norm_weight,
 
1754
  )
1755
  # Final reduction: rstd = rsqrt(sum(partials) / N + eps)
1756
  scale = 1.0 / N
@@ -1763,10 +2122,10 @@ def _gemm_rms_tuned(
1763
 
1764
 
1765
  @torch.library.custom_op(
1766
- add_quack_op_namespace_prefix("gemm_rms_out"),
1767
- mutates_args=("out",),
1768
  device_types="cuda",
1769
- schema="(Tensor A, Tensor B, Tensor(a!) out, Tensor? C=None, Tensor? norm_weight=None, float eps=1e-6, bool dynamic_scheduler=False, bool tuned=True) -> Tensor",
1770
  )
1771
  def _gemm_rms_out(
1772
  A: Tensor,
@@ -1774,6 +2133,7 @@ def _gemm_rms_out(
1774
  out: Tensor,
1775
  C: Optional[Tensor] = None,
1776
  norm_weight: Optional[Tensor] = None,
 
1777
  eps: float = 1e-6,
1778
  dynamic_scheduler: bool = False,
1779
  tuned: bool = True,
@@ -1781,6 +2141,7 @@ def _gemm_rms_out(
1781
  """GEMM + RMS + optional rowvec scaling.
1782
 
1783
  D_raw = A @ B (+ C), rstd = rsqrt(mean(D_raw^2) + eps), D_out = D_raw * norm_weight.
 
1784
  """
1785
  fn = _gemm_rms_tuned if tuned else partial(_gemm_rms_tuned.fn, config=None)
1786
  return fn(
@@ -1789,32 +2150,24 @@ def _gemm_rms_out(
1789
  out,
1790
  C=C,
1791
  norm_weight=norm_weight,
 
1792
  eps=eps,
1793
  dynamic_scheduler=dynamic_scheduler,
1794
  )
1795
 
1796
 
1797
- @torch.library.register_fake(add_quack_op_namespace_prefix("gemm_rms_out"))
1798
  def _gemm_rms_out_fake(
1799
  A: Tensor,
1800
  B: Tensor,
1801
  out: Tensor,
1802
  C: Optional[Tensor] = None,
1803
  norm_weight: Optional[Tensor] = None,
 
1804
  eps: float = 1e-6,
1805
  dynamic_scheduler: bool = False,
1806
  tuned: bool = True,
1807
  ) -> Tensor:
1808
- _precompile_default_config(
1809
- _gemm_rms_tuned,
1810
- A,
1811
- B,
1812
- out,
1813
- C=C,
1814
- norm_weight=norm_weight,
1815
- eps=eps,
1816
- dynamic_scheduler=dynamic_scheduler,
1817
- )
1818
  rstd_shape = A.shape[:-1]
1819
  return torch.empty(rstd_shape, dtype=torch.float32, device=A.device)
1820
 
@@ -1844,6 +2197,7 @@ def gemm_rms(
1844
  norm_weight: Optional[Tensor] = None, # (N,) or (L, N)
1845
  out: Optional[Tensor] = None, # (M, N) or (L, M, N)
1846
  out_dtype: Optional[torch.dtype] = None,
 
1847
  eps: float = 1e-6,
1848
  dynamic_scheduler: bool = False,
1849
  tuned: bool = True,
@@ -1851,6 +2205,7 @@ def gemm_rms(
1851
  """GEMM + RMS statistics + optional rowvec scaling.
1852
 
1853
  D_raw = A @ B (+ C), rstd = rsqrt(mean(D_raw^2) + eps), D_out = D_raw * norm_weight.
 
1854
  Returns (D_out, rstd).
1855
  """
1856
  out_dtype = A.dtype if out_dtype is None else out_dtype
@@ -1858,12 +2213,28 @@ def gemm_rms(
1858
  if out is None:
1859
  out_shape = (*A.shape[:-1], N)
1860
  out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1861
  rstd = _gemm_rms_out(
1862
  A,
1863
  B,
1864
  out,
1865
  C=C,
1866
  norm_weight=norm_weight,
 
1867
  eps=eps,
1868
  dynamic_scheduler=dynamic_scheduler,
1869
  tuned=tuned,
@@ -1927,7 +2298,8 @@ def gemm_norm_act_tuned(
1927
  config.tile_n,
1928
  config.cluster_m,
1929
  config.cluster_n,
1930
- config.pingpong,
 
1931
  persistent=True,
1932
  is_dynamic_persistent=dynamic_scheduler,
1933
  max_swizzle_size=config.max_swizzle_size,
@@ -1989,7 +2361,8 @@ def gemm_norm_gated_tuned(
1989
  config.tile_n,
1990
  config.cluster_m,
1991
  config.cluster_n,
1992
- config.pingpong,
 
1993
  persistent=True,
1994
  is_dynamic_persistent=dynamic_scheduler,
1995
  max_swizzle_size=config.max_swizzle_size,
@@ -1999,7 +2372,7 @@ def gemm_norm_gated_tuned(
1999
 
2000
 
2001
  @torch.library.custom_op(
2002
- add_quack_op_namespace_prefix("gemm_norm_act_out"),
2003
  mutates_args=("preact_out", "postact_out"),
2004
  device_types="cuda",
2005
  schema="(Tensor A, Tensor B, Tensor(a2!)? preact_out, Tensor(a3!) postact_out, Tensor? C=None, Tensor? rstd=None, str? activation=None, bool dynamic_scheduler=False, bool tuned=True) -> ()",
@@ -2019,23 +2392,11 @@ def gemm_norm_act_out(
2019
  fn(A, B, preact_out, postact_out, C, rstd, activation, dynamic_scheduler)
2020
 
2021
 
2022
- @torch.library.register_fake(add_quack_op_namespace_prefix("gemm_norm_act_out"))
2023
- def _gemm_norm_act_out_fake(
2024
- A,
2025
- B,
2026
- preact_out,
2027
- postact_out,
2028
- C=None,
2029
- rstd=None,
2030
- activation=None,
2031
- dynamic_scheduler=False,
2032
- tuned=True,
2033
- ) -> None:
2034
- pass
2035
 
2036
 
2037
  @torch.library.custom_op(
2038
- add_quack_op_namespace_prefix("gemm_norm_gated_out"),
2039
  mutates_args=("preact_out", "postact_out"),
2040
  device_types="cuda",
2041
  schema="(Tensor A, Tensor B, Tensor(a2!)? preact_out, Tensor(a3!) postact_out, Tensor? C=None, Tensor? rstd=None, str activation='swiglu', bool dynamic_scheduler=False, bool tuned=True) -> ()",
@@ -2055,19 +2416,7 @@ def gemm_norm_gated_out(
2055
  fn(A, B, preact_out, postact_out, C, rstd, activation, dynamic_scheduler)
2056
 
2057
 
2058
- @torch.library.register_fake(add_quack_op_namespace_prefix("gemm_norm_gated_out"))
2059
- def _gemm_norm_gated_out_fake(
2060
- A,
2061
- B,
2062
- preact_out,
2063
- postact_out,
2064
- C=None,
2065
- rstd=None,
2066
- activation="swiglu",
2067
- dynamic_scheduler=False,
2068
- tuned=True,
2069
- ) -> None:
2070
- pass
2071
 
2072
 
2073
  def gemm_norm_act(
@@ -2101,6 +2450,13 @@ def gemm_norm_act(
2101
  preact_out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
2102
  if postact_out is None:
2103
  postact_out = torch.empty(postact_shape, dtype=postact_dtype, device=A.device)
 
 
 
 
 
 
 
2104
  if is_gated:
2105
  gemm_norm_gated_out(
2106
  A,
@@ -2152,13 +2508,12 @@ def gemm_norm_act_ref(
2152
  if rstd is not None:
2153
  D = D * rstd.unsqueeze(-1)
2154
  preact = D.to(out_dtype) if store_preact else None
2155
- _act_map = {**act_to_pytorch_fn_map, "silu": F.silu}
2156
  if is_gated:
2157
  gate = D[..., ::2]
2158
  up = D[..., 1::2]
2159
  postact = gated_to_pytorch_fn_map[activation](gate, up).to(postact_dtype)
2160
  else:
2161
- postact = _act_map[activation](D).to(postact_dtype)
2162
  return preact, postact
2163
 
2164
 
 
3
  from functools import partial
4
 
5
  import torch
6
+ from ._ops_compat import add_op_namespace_prefix
7
  import torch.nn.functional as F
8
  from torch import Tensor
9
 
 
21
  from .rounding import RoundingMode
22
 
23
 
24
+ def _empty_k_matmul_into(
25
+ out: Tensor,
26
+ *,
27
+ bias: Optional[Tensor] = None,
28
+ C: Optional[Tensor] = None,
29
+ beta: float | Tensor = 1.0,
30
+ ) -> None:
31
+ """K=0 fast path: write `beta * C + bias` (or zero if neither) into `out`.
32
+
33
+ Used by every gemm-flavored wrapper to skip a kernel launch when the
34
+ contraction dim is empty. The matmul A @ B contributes zero, so the only
35
+ remaining terms are the C term and the (broadcast) bias.
36
+ """
37
+ if C is not None:
38
+ if isinstance(beta, float) and beta == 1.0:
39
+ out.copy_(C)
40
+ else:
41
+ torch.mul(C, beta, out=out)
42
+ else:
43
+ out.zero_()
44
+ if bias is not None:
45
+ out += bias
46
+
47
+
48
+ def _silu_tanh(x: Tensor) -> Tensor:
49
+ x_half = 0.5 * x
50
+ return x_half * torch.tanh(x_half) + x_half
51
+
52
+
53
+ def _swiglu_oai_tanh(gate: Tensor, up: Tensor, alpha: float = 1.702) -> Tensor:
54
+ gate_half = 0.5 * gate
55
+ return (gate_half * torch.tanh(alpha * gate_half) + gate_half) * (up + 1)
56
+
57
+
58
  # Dictionary mapping activation names to PyTorch functions
59
  act_to_pytorch_fn_map = {
60
  None: lambda x: x,
61
+ "silu": F.silu,
62
+ "silu-tanh": _silu_tanh,
63
  "relu": F.relu,
64
  "relu_sq": lambda x: F.relu(x).square(),
65
  "gelu_tanh_approx": partial(F.gelu, approximate="tanh"),
66
+ "tanh": torch.tanh,
67
  }
68
 
69
 
 
71
  # Each function takes (gate, up) and returns postact
72
  gated_to_pytorch_fn_map = {
73
  "swiglu": lambda gate, up: F.silu(gate) * up,
74
+ "swiglu-tanh": lambda gate, up: _silu_tanh(gate) * up,
75
  "swiglu_oai": lambda gate, up: gate * torch.sigmoid(1.702 * gate) * (up + 1),
76
+ "swiglu_oai-tanh": _swiglu_oai_tanh,
77
  "reglu": lambda gate, up: F.relu(gate) * up,
78
  "geglu": lambda gate, up: F.gelu(gate, approximate="tanh") * up,
79
  "glu": lambda gate, up: torch.sigmoid(gate) * up,
80
  }
81
 
82
 
83
+ ActActivation = Literal[None, "silu", "silu-tanh", "relu", "relu_sq", "gelu_tanh_approx", "tanh"]
84
+ GatedActivation = Literal[
85
+ "swiglu",
86
+ "swiglu-tanh",
87
+ "swiglu_oai",
88
+ "swiglu_oai-tanh",
89
+ "reglu",
90
+ "geglu",
91
+ "glu",
92
+ ]
93
  Activation = Literal[
94
  None,
95
+ "silu",
96
+ "silu-tanh",
97
  "relu",
98
  "relu_sq",
99
  "gelu_tanh_approx",
100
+ "tanh",
101
  "swiglu",
102
+ "swiglu-tanh",
103
  "swiglu_oai",
104
+ "swiglu_oai-tanh",
105
  "reglu",
106
  "geglu",
107
  "glu",
 
120
  return t.unflatten(-1, (2, half)).transpose(-2, -1).flatten(-2, -1)
121
 
122
 
123
+ # ── Blockscaled (MXFP8 / MXFP4 / NVFP4) helpers ─────────────────────────────
124
+ #
125
+ # A and B may be passed as ``(data, scale_factor)`` tuples. Scale factors use
126
+ # the canonical cuBLAS/CUTLASS 128x4 blocked layout: shape
127
+ # ``(M // 128, K // VEC // 4, 32, 4, 4)`` (optionally with a leading batch L),
128
+ # where the ``(32, 4, 4)`` inner block is ``(m % 32, (m // 32) % 4, k_block % 4)``
129
+ # with strides ``(16, 4, 1)`` — one contiguous 512-byte atom per 128 rows x 4
130
+ # K-blocks, matching torchao's ``to_blocked`` and ``torch._scaled_mm``.
131
+ # VEC (the quantization block along K) is implied by the SF dtype:
132
+ # float8_e8m0fnu -> 32 (MX formats), float8_e4m3fn -> 16 (NVFP4).
133
+ # fp4 operands use ``torch.float4_e2m1fn_x2`` storage: shapes carry packed K
134
+ # (two elements per byte); K here always refers to logical K.
135
+
136
+
137
+ def _unpack_operand(X) -> Tuple[Tensor, Optional[Tensor]]:
138
+ """Split an ``A`` / ``B`` argument into (data, scale_factor or None)."""
139
+ if isinstance(X, (tuple, list)):
140
+ data, sf = X
141
+ return data, sf
142
+ return X, None
143
+
144
+
145
+ def _sf_normalize(SF: Tensor, name: str) -> Tensor:
146
+ """Validate a user scale-factor tensor and add the batch dim if missing.
147
+
148
+ Requires ``(rm, rk, 32, 4, 4)`` or ``(L, rm, rk, 32, 4, 4)`` with the inner
149
+ ``(32, 4, 4)`` block contiguous (strides ``(16, 4, 1)`` — one 512 B atom);
150
+ outer strides are free, so slices of a larger scale buffer are accepted.
151
+ Returns a zero-copy ``(L, rm, rk, 32, 4, 4)`` view, which is what the
152
+ compiled kernel consumes directly (the kernel reads only the base pointer
153
+ and the outer strides; the inner atom layout is hardware-fixed).
154
+ """
155
+ assert SF.ndim in (5, 6) and tuple(SF.shape[-3:]) == (32, 4, 4), (
156
+ f"{name}: expected (rm, rk, 32, 4, 4) or (L, rm, rk, 32, 4, 4) blocked scale factors, "
157
+ f"got shape {tuple(SF.shape)}"
158
+ )
159
+ assert SF.stride()[-3:] == (16, 4, 1), (
160
+ f"{name}: inner (32, 4, 4) block must be contiguous with strides (16, 4, 1), "
161
+ f"got {SF.stride()[-3:]}"
162
+ )
163
+ return SF.unsqueeze(0) if SF.ndim == 5 else SF
164
+
165
+
166
+ def _logical_k(X: Tensor) -> int:
167
+ """Logical contraction extent of the last dim (fp4 packs two elements per byte)."""
168
+ return X.shape[-1] * (2 if X.dtype == torch.float4_e2m1fn_x2 else 1)
169
+
170
+
171
+ def _sf_encode(SF: Tensor) -> Tensor:
172
+ """View e8m0 scale factors as uint8 for the custom-op boundary.
173
+
174
+ Upstream PyTorch bug: a ``float8_e8m0fnu`` input to any mutable custom op
175
+ makes Inductor's ``decompose_auto_functionalized`` pass fail with
176
+ "auto_functionalized_v2 was not removed" (e4m3 is unaffected), breaking
177
+ torch.compile. The uint8 view is zero-copy and unambiguous (uint8 == e8m0,
178
+ e4m3 stays itself); :func:`_sf_decode` restores the dtype inside the op.
179
+ """
180
+ return SF.view(torch.uint8) if SF.dtype == torch.float8_e8m0fnu else SF
181
+
182
+
183
+ def _sf_decode(SF: Optional[Tensor]) -> Optional[Tensor]:
184
+ """Inverse of :func:`_sf_encode` (uint8 -> e8m0), applied inside op bodies."""
185
+ if SF is not None and SF.dtype == torch.uint8:
186
+ SF = SF.view(torch.float8_e8m0fnu)
187
+ return SF
188
+
189
+
190
  def default_config(device):
191
  cap = get_device_capacity(device)[0]
192
+ if cap == 8:
193
+ return GemmConfig(
194
+ tile_m=128,
195
+ tile_n=128,
196
+ tile_k=32,
197
+ num_warps=4,
198
+ cluster_m=1,
199
+ cluster_n=1,
200
+ pingpong=False,
201
+ is_dynamic_persistent=False,
202
+ device_capacity=8,
203
+ )
204
+ elif cap in [10, 11]:
205
  return GemmConfig(
206
  tile_m=256,
207
  tile_n=256,
 
232
  )
233
 
234
 
235
+ def blockscaled_default_config(m: int, n: int) -> GemmConfig:
236
+ """Default SM100 config for blockscaled GEMM.
237
+
238
+ Large shapes use a (256, 256) tile: it makes num_acc_stage == 1, which turns
239
+ on ``overlap_accum_sf`` (a second TMEM accumulator stage) so the per-tile
240
+ scale-apply + TMEM drain overlaps the next tile's MMA instead of
241
+ serializing after it.
242
+ """
243
+ if m >= 512 and n >= 256:
244
+ tile_m, tile_n, cluster = 256, 256, (2, 1)
245
+ elif m >= 512 and n >= 128:
246
+ tile_m, tile_n, cluster = 256, 128, (2, 1)
247
+ else:
248
+ tile_m, tile_n, cluster = 128, 128, (1, 1)
249
+ return GemmConfig(
250
+ tile_m=tile_m,
251
+ tile_n=tile_n,
252
+ cluster_m=cluster[0],
253
+ cluster_n=cluster[1],
254
+ pingpong=False,
255
+ is_dynamic_persistent=True,
256
+ device_capacity=10,
257
+ )
258
+
259
+
260
  def nvmmh_config(A, B, device_capacity):
261
  """Use nvMatmulHeuristics to pick a config for pure GEMM (no varlen/gather/epilogue).
262
 
 
286
  # use_tma_gather only valid when gather_A is active on SM100/SM110
287
  if not gather_A or device_capacity not in [10, 11]:
288
  configs = [conf for conf in configs if not conf.kwargs["config"].use_tma_gather]
289
+ if kwargs.get("SFA", None) is not None: # blockscaled (SM100 tcgen05 MMA constraints)
290
+
291
+ def _blockscaled_ok(c: GemmConfig) -> bool:
292
+ return (
293
+ c.device_capacity in (10, 11)
294
+ and not c.swap_ab # untested with blockscaled; SFA/SFB would swap too
295
+ and c.tile_k is None # tile_k is derived from the MMA instruction
296
+ and c.tile_m in (128, 256)
297
+ and c.tile_n in (64, 128, 192, 256)
298
+ # SF multicast is limited to 4 CTAs per cluster dim
299
+ and c.cluster_m <= 4
300
+ and c.cluster_n <= 4
301
+ )
302
+
303
+ configs = [conf for conf in configs if _blockscaled_ok(conf.kwargs["config"])]
304
  return configs
305
 
306
 
 
328
  rounding_mode: int = RoundingMode.RN,
329
  sr_seed: int | Tensor = 0,
330
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
331
+ SFA: Optional[Tensor] = None, # (L, rm, rk, 32, 4, 4) blocked scale factors
332
+ SFB: Optional[Tensor] = None, # (L, rn, rk, 32, 4, 4)
333
  ) -> None:
334
+ blockscaled = SFA is not None
335
+ if blockscaled:
336
+ SFA, SFB = _sf_decode(SFA), _sf_decode(SFB)
337
  if config is None:
338
+ if blockscaled:
339
+ m = A.shape[-2]
340
+ config = blockscaled_default_config(m, B.shape[-1])
341
+ else:
342
+ # Use nvMMH heuristic for pure GEMM (no varlen, no gather, no epilogue)
343
+ is_pure_gemm = (
344
+ cu_seqlens_m is None
345
+ and cu_seqlens_k is None
346
+ and A_idx is None
347
+ and C is None
348
+ and bias is None
349
+ and not add_to_output
350
+ )
351
+ if is_pure_gemm:
352
+ device_capacity = get_device_capacity(A.device)[0]
353
+ config = nvmmh_config(A, B, device_capacity)
354
+ if config is None:
355
+ config = default_config(A.device)
356
  varlen_m = cu_seqlens_m is not None
357
  varlen_k = cu_seqlens_k is not None
358
  varlen = varlen_m or varlen_k
359
  gather_A = A_idx is not None
360
+ if blockscaled:
361
+ assert not gather_A, "Blockscaled GEMM does not support gather_A yet"
362
+ assert not concat_layout, "Blockscaled GEMM does not support concat_layout"
363
+ assert not config.swap_ab, "Blockscaled GEMM does not support swap_ab yet"
364
  if gather_A:
365
  assert varlen, "gather_A requires either varlen_m or varlen_k"
366
  assert config.cluster_n == 1, "gather_A requires cluster_n=1"
 
419
  config.tile_n,
420
  config.cluster_m,
421
  config.cluster_n,
422
+ config.cluster_k,
423
+ pingpong=config.pingpong,
424
  persistent=True,
425
  is_dynamic_persistent=dynamic_scheduler,
426
  max_swizzle_size=config.max_swizzle_size,
 
437
  sr_seed=sr_seed,
438
  use_tma_gather=config.use_tma_gather,
439
  concat_layout=swapped_concat,
440
+ num_warps=config.num_warps,
441
+ tile_K=config.tile_k,
442
+ SFA=SFA,
443
+ SFB=SFB,
444
  )
445
 
446
 
 
463
  A_idx: Optional[Tensor] = None, # (total_M,) if gather_A with varlen_m
464
  dynamic_scheduler: bool = False,
465
  config: Optional[GemmConfig] = None,
466
+ SFA: Optional[Tensor] = None, # (L, rm, rk, 32, 4, 4) blocked scale factors
467
+ SFB: Optional[Tensor] = None, # (L, rn, rk, 32, 4, 4)
468
  ) -> None:
469
+ blockscaled = SFA is not None
470
+ if blockscaled:
471
+ SFA, SFB = _sf_decode(SFA), _sf_decode(SFB)
472
  if config is None:
473
+ if blockscaled:
474
+ config = blockscaled_default_config(A.shape[-2], B.shape[-1])
475
+ else:
476
+ config = default_config(A.device)
477
  varlen_m = cu_seqlens_m is not None
478
  if varlen_m:
479
  assert not config.swap_ab, "Variable-length sequences not supported with swap_ab"
480
+ if blockscaled:
481
+ assert not varlen_m and A_idx is None, "Blockscaled GEMM does not support varlen/gather yet"
482
+ assert not config.swap_ab, "Blockscaled GEMM does not support swap_ab yet"
483
  if A.ndim == 2 and not varlen_m:
484
  A = A.unsqueeze(0) # (1, M, K)
485
  B = B.mT # (N, K) or (L, N, K)
 
515
  config.tile_n,
516
  config.cluster_m,
517
  config.cluster_n,
518
+ tile_K=config.tile_k,
519
+ pingpong=config.pingpong,
520
  persistent=True,
521
  is_dynamic_persistent=dynamic_scheduler,
522
  max_swizzle_size=config.max_swizzle_size,
 
525
  cu_seqlens_m=cu_seqlens_m,
526
  A_idx=A_idx,
527
  use_tma_gather=config.use_tma_gather,
528
+ SFA=SFA,
529
+ SFB=SFB,
530
  )
531
 
532
 
 
586
  config.tile_n,
587
  config.cluster_m,
588
  config.cluster_n,
589
+ tile_K=config.tile_k,
590
+ pingpong=config.pingpong,
591
  persistent=True,
592
  is_dynamic_persistent=dynamic_scheduler,
593
  max_swizzle_size=config.max_swizzle_size,
 
599
 
600
  def gemm(
601
  # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (M, total_K) if varlen_k or (whatever, K) if gather_A with varlen_m or (M, whatever) if gather_A with varlen_k
602
+ # For blockscaled (MXFP8/MXFP4/NVFP4): a tuple (A, SFA) with A fp8/fp4 and SFA the
603
+ # blocked scale factors (rm, rk, 32, 4, 4) or (L, rm, rk, 32, 4, 4) see helpers above.
604
+ A: Tensor | Tuple[Tensor, Tensor],
605
+ B: Tensor | Tuple[Tensor, Tensor], # (K, N) or (L, K, N) or (total_K, N) if varlen_k
606
  out: Optional[Tensor] = None, # (M, N) or (L, M, N) or (total_M, N) if varlen_m
607
  bias: Optional[Tensor] = None, # (N,) or (L, N)
608
  alpha: float | Tensor = 1.0,
 
618
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
619
  ) -> Tensor:
620
  """GEMM with optional output tensor and tuning control."""
621
+ A, SFA = _unpack_operand(A)
622
+ B, SFB = _unpack_operand(B)
623
+ assert (SFA is None) == (SFB is None), "A and B must both (or neither) carry scale factors"
624
+ if SFA is not None:
625
+ SFA = _sf_encode(_sf_normalize(SFA, "SFA"))
626
+ SFB = _sf_encode(_sf_normalize(SFB, "SFB"))
627
  if out is None:
628
+ if out_dtype is None:
629
+ # Blockscaled inputs are fp8/fp4; default to bf16 output.
630
+ out_dtype = torch.bfloat16 if SFA is not None else A.dtype
631
  varlen_m = cu_seqlens_m is not None
632
  varlen_k = cu_seqlens_k is not None
633
  if varlen_m:
 
642
  (A.shape[0], B.shape[-1]) if A.ndim == 2 else (A.shape[0], A.shape[-2], B.shape[-1])
643
  )
644
  out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
645
+ # Empty-input fast path: skip kernel launch.
646
+ # M=0 / N=0 — the tile scheduler's ceil_div over a zero dim divides by zero.
647
+ # K=0 — the kernel rejects stride-0 inputs (stride must be divisible by 8);
648
+ # semantically the empty contraction yields a zero matrix.
649
+ if out.numel() == 0:
650
+ return out
651
+ if A.numel() == 0:
652
+ _empty_k_matmul_into(out, bias=bias)
653
+ return out
654
  alpha_tensor = alpha if not isinstance(alpha, float) else None
655
  alpha = alpha if isinstance(alpha, float) else 1.0
656
  sr_seed_tensor = sr_seed if isinstance(sr_seed, Tensor) else None
 
673
  sr_seed=sr_seed_int,
674
  sr_seed_tensor=sr_seed_tensor,
675
  concat_layout=concat_str,
676
+ SFA=SFA,
677
+ SFB=SFB,
678
  )
679
  return out
680
 
681
 
682
  @torch.library.custom_op(
683
+ add_op_namespace_prefix("gemm_out"),
684
  mutates_args=("out",),
685
  device_types="cuda",
686
  # We have to split out alpha and alpha_tensor since torch.library requires
 
705
  sr_seed: int = 0,
706
  sr_seed_tensor: Optional[Tensor] = None,
707
  concat_layout: Optional[str] = None,
708
+ # Blockscaled scale factors, (L, rm/rn, rk, 32, 4, 4); tuples are unpacked
709
+ # to these flat args before the custom-op boundary since torch.library
710
+ # schemas have no (Tensor, Tensor) argument type.
711
+ SFA: Optional[Tensor] = None,
712
+ SFB: Optional[Tensor] = None,
713
  ) -> None:
714
  """GEMM with pre-allocated output tensor."""
715
  fn = gemm_tuned if tuned else partial(gemm_tuned.fn, config=None)
716
+ # Shared helpers: drift between this eager body and the register_fake side
717
+ # is structurally impossible because both call the same functions.
718
+ alpha = _merge_tensor(alpha, alpha_tensor)
719
+ sr_seed_arg = _merge_tensor(sr_seed, sr_seed_tensor)
720
  fn(
721
  A,
722
  B,
 
731
  dynamic_scheduler=dynamic_scheduler,
732
  rounding_mode=rounding_mode,
733
  sr_seed=sr_seed_arg,
734
+ concat_layout=_parse_concat_layout(concat_layout),
735
+ SFA=SFA,
736
+ SFB=SFB,
737
  )
738
 
739
 
 
806
  return out
807
 
808
 
809
+ def gemm_blockscaled_ref(
810
+ A: Tuple[Tensor, Tensor], # ((M, K) or (L, M, K) fp8/fp4x2, blocked SFA)
811
+ B: Tuple[Tensor, Tensor], # ((K, N) or (L, K, N) fp8/fp4x2 K-contig, blocked SFB)
812
+ alpha: float | Tensor = 1.0,
813
+ out_dtype: torch.dtype = torch.bfloat16,
814
+ ) -> Tensor:
815
+ """Dequantize-and-matmul reference for blockscaled GEMM."""
816
+ from .blockscaled.utils import dequant_operand, unpack_scale_blocked_to_2d
817
+
818
+ A, SFA = _unpack_operand(A)
819
+ B, SFB = _unpack_operand(B)
820
+ SFA = _sf_normalize(SFA, "SFA")
821
+ SFB = _sf_normalize(SFB, "SFB")
822
+ sf_vec = 32 if SFA.dtype == torch.float8_e8m0fnu else 16
823
+ batched = A.ndim == 3
824
+ a3 = A if batched else A.unsqueeze(0) # (l, m, k_packed)
825
+ b3 = (B if batched else B.unsqueeze(0)).mT # (l, n, k_packed)
826
+ a_val = dequant_operand(a3) # (l, m, k) fp32
827
+ b_val = dequant_operand(b3)
828
+ l, m, k = a_val.shape
829
+ n = b_val.shape[1]
830
+ sfa = unpack_scale_blocked_to_2d(SFA, m, k // sf_vec).float()
831
+ sfb = unpack_scale_blocked_to_2d(SFB, n, k // sf_vec).float()
832
+ a_dq = a_val * sfa.repeat_interleave(sf_vec, dim=-1)
833
+ b_dq = b_val * sfb.repeat_interleave(sf_vec, dim=-1)
834
+ out = torch.einsum("lmk,lnk->lmn", a_dq, b_dq)
835
+ if not (isinstance(alpha, float) and alpha == 1.0):
836
+ out = out * alpha
837
+ out = out.to(out_dtype)
838
+ return out if batched else out.squeeze(0)
839
+
840
+
841
  def gemm_add(
842
  # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (M, total_K) if varlen_k or (whatever, K) if gather_A with varlen_m or (M, whatever) if gather_A with varlen_k
843
+ # For blockscaled: a tuple (A, SFA) — see gemm().
844
+ A: Tensor | Tuple[Tensor, Tensor],
845
+ B: Tensor | Tuple[Tensor, Tensor], # (K, N) or (L, K, N) or (total_K, N) if varlen_k
846
  C: Tensor, # (M, N) or (L, M, N) or (total_M, N) if varlen_m or (L, M, N) if varlen_k
847
  out: Optional[Tensor] = None, # (M, N) or (L, M, N) or (total_M, N) if varlen_m
848
  alpha: float | Tensor = 1.0,
 
857
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
858
  ) -> Tensor:
859
  """GEMM with addition and optional output tensor."""
860
+ A, SFA = _unpack_operand(A)
861
+ B, SFB = _unpack_operand(B)
862
+ assert (SFA is None) == (SFB is None), "A and B must both (or neither) carry scale factors"
863
+ if SFA is not None:
864
+ SFA = _sf_encode(_sf_normalize(SFA, "SFA"))
865
+ SFB = _sf_encode(_sf_normalize(SFB, "SFB"))
866
  if out is None:
867
+ if out_dtype is None:
868
+ out_dtype = torch.bfloat16 if SFA is not None else A.dtype
869
  varlen_m = cu_seqlens_m is not None
870
  varlen_k = cu_seqlens_k is not None
871
  if varlen_m:
 
882
  )
883
  out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
884
  add_to_output = C is out and isinstance(beta, float) and beta == 1.0 and cu_seqlens_m is None
885
+ # Empty-input fast path: skip kernel launch (see gemm() for rationale).
886
+ # K=0 reduces D = alpha*A@B + beta*C to D = beta*C.
887
+ if out.numel() == 0:
888
+ return out
889
+ if A.numel() == 0:
890
+ if add_to_output:
891
+ return out # out IS C, and out += alpha * 0 is a no-op
892
+ _empty_k_matmul_into(out, C=C, beta=beta)
893
+ return out
894
  alpha_tensor = alpha if not isinstance(alpha, float) else None
895
  alpha = alpha if isinstance(alpha, float) else 1.0
896
  beta_tensor = beta if not isinstance(beta, float) else None
897
  beta = beta if isinstance(beta, float) else 1.0
898
+ alpha_arg = _merge_tensor(alpha, alpha_tensor)
899
+ beta_arg = _merge_tensor(beta, beta_tensor)
900
  concat_str = ",".join(concat_layout) if concat_layout else None
901
  if add_to_output:
902
  gemm_add_inplace(
903
+ A if SFA is None else (A, SFA),
904
+ B if SFB is None else (B, SFB),
905
  out,
906
  alpha=alpha_arg,
907
  beta=beta_arg,
 
931
  dynamic_scheduler=dynamic_scheduler,
932
  tuned=tuned,
933
  concat_layout=concat_str,
934
+ SFA=SFA,
935
+ SFB=SFB,
936
  )
937
  return out
938
 
939
 
940
  @torch.library.custom_op(
941
+ add_op_namespace_prefix("gemm_add_out"),
942
  mutates_args=("out",),
943
  device_types="cuda",
944
  # We have to split out alpha and alpha_tensor since torch.library requires
 
963
  dynamic_scheduler: bool = False,
964
  tuned: bool = True,
965
  concat_layout: Optional[str] = None,
966
+ SFA: Optional[Tensor] = None, # blocked scale factors, (L, rm, rk, 32, 4, 4) (see gemm_out)
967
+ SFB: Optional[Tensor] = None,
968
  ) -> None:
969
  """GEMM with addition and pre-allocated output tensor."""
970
  fn = gemm_tuned if tuned else partial(gemm_tuned.fn, config=None)
971
+ alpha = _merge_tensor(alpha, alpha_tensor)
972
+ beta = _merge_tensor(beta, beta_tensor)
973
  fn(
974
  A,
975
  B,
 
977
  C,
978
  alpha=alpha,
979
  beta=beta,
980
+ SFA=SFA,
981
+ SFB=SFB,
982
  cu_seqlens_m=cu_seqlens_m,
983
  cu_seqlens_k=cu_seqlens_k,
984
  A_idx=A_idx,
985
  batch_idx_permute=batch_idx_permute,
986
  add_to_output=add_to_output,
987
  dynamic_scheduler=dynamic_scheduler,
988
+ concat_layout=_parse_concat_layout(concat_layout),
989
  )
990
 
991
 
 
1072
 
1073
  def gemm_add_inplace(
1074
  # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (M, total_K) if varlen_k or (whatever, K) if gather_A with varlen_m or (M, whatever) if gather_A with varlen_k
1075
+ # For blockscaled: a tuple (A, SFA) — see gemm().
1076
+ A: Tensor | Tuple[Tensor, Tensor],
1077
+ B: Tensor | Tuple[Tensor, Tensor], # (K, N) or (L, K, N) or (total_K, N) if varlen_k
1078
  out: Tensor, # (M, N) or (L, M, N) or (total_M, N) if varlen_m or (L, M, N) if varlen_k
1079
  alpha: float | Tensor = 1.0,
1080
  beta: float | Tensor = 1.0,
 
1098
  dynamic_scheduler: Whether to use dynamic scheduler
1099
  tuned: Whether to use autotuned configuration
1100
  """
1101
+ A, SFA = _unpack_operand(A)
1102
+ B, SFB = _unpack_operand(B)
1103
+ assert (SFA is None) == (SFB is None), "A and B must both (or neither) carry scale factors"
1104
+ if SFA is not None:
1105
+ SFA = _sf_encode(_sf_normalize(SFA, "SFA"))
1106
+ SFB = _sf_encode(_sf_normalize(SFB, "SFB"))
1107
  alpha_tensor = alpha if not isinstance(alpha, float) else None
1108
  alpha = alpha if isinstance(alpha, float) else 1.0
1109
  beta_tensor = beta if not isinstance(beta, float) else None
1110
  beta = beta if isinstance(beta, float) else 1.0
1111
+ # Empty-input fast path: out += alpha * A@B with K=0 reduces to out *= beta.
1112
+ # The matmul contributes zero, so use the helper with C=out.
1113
+ if out.numel() == 0:
1114
+ return
1115
+ if A.numel() == 0:
1116
+ if beta != 1.0 or beta_tensor is not None:
1117
+ out.mul_(_merge_tensor(beta, beta_tensor))
1118
+ return
1119
  gemm_add_inplace_op(
1120
  A,
1121
  B,
 
1133
  concat_layout=",".join(concat_layout)
1134
  if isinstance(concat_layout, tuple)
1135
  else concat_layout,
1136
+ SFA=SFA,
1137
+ SFB=SFB,
1138
  )
1139
 
1140
 
1141
  @torch.library.custom_op(
1142
+ add_op_namespace_prefix("gemm_add_inplace"),
1143
  mutates_args=("out",),
1144
  device_types="cuda",
1145
  # We have to split out alpha and alpha_tensor since torch.library requires
 
1162
  dynamic_scheduler: bool = False,
1163
  tuned: bool = True,
1164
  concat_layout: Optional[str] = None,
1165
+ SFA: Optional[Tensor] = None, # blocked scale factors, (L, rm, rk, 32, 4, 4) (see gemm_out)
1166
+ SFB: Optional[Tensor] = None,
1167
  ) -> None:
1168
  fn = gemm_tuned if tuned else partial(gemm_tuned.fn, config=None)
1169
+ alpha = _merge_tensor(alpha, alpha_tensor)
1170
+ beta = _merge_tensor(beta, beta_tensor)
1171
  add_to_output = isinstance(beta, float) and beta == 1.0 and cu_seqlens_m is None
1172
  # Use out as both input bias and output
1173
  fn(
 
1183
  batch_idx_permute=batch_idx_permute,
1184
  add_to_output=add_to_output,
1185
  dynamic_scheduler=dynamic_scheduler,
1186
+ concat_layout=_parse_concat_layout(concat_layout),
1187
+ SFA=SFA,
1188
+ SFB=SFB,
1189
  )
1190
 
1191
 
1192
  def gemm_act(
1193
+ # For blockscaled: a tuple (A, SFA) see gemm().
1194
+ A: Tensor
1195
+ | Tuple[
1196
+ Tensor, Tensor
1197
+ ], # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (whatever, K) if gather_A with varlen_m
1198
+ B: Tensor | Tuple[Tensor, Tensor], # (K, N) or (L, K, N)
1199
  C: Optional[Tensor] = None, # (M, N) or (L, M, N) or (total_M, N) if varlen_m
1200
  bias: Optional[Tensor] = None, # (N,) or (L, N)
1201
  activation: Activation = None,
 
1211
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
1212
  ) -> Tuple[Optional[Tensor], Tensor]:
1213
  """GEMM with activation (or gated activation) and optional output tensors."""
1214
+ A, SFA = _unpack_operand(A)
1215
+ B, SFB = _unpack_operand(B)
1216
+ assert (SFA is None) == (SFB is None), "A and B must both (or neither) carry scale factors"
1217
+ if SFA is not None:
1218
+ SFA = _sf_encode(_sf_normalize(SFA, "SFA"))
1219
+ SFB = _sf_encode(_sf_normalize(SFB, "SFB"))
1220
  is_gated = activation in gated_to_pytorch_fn_map
1221
+ default_dtype = torch.bfloat16 if SFA is not None else A.dtype
1222
+ out_dtype = default_dtype if out_dtype is None else out_dtype
1223
+ postact_dtype = default_dtype if postact_dtype is None else postact_dtype
1224
  varlen_m = cu_seqlens_m is not None
1225
  # Determine output shape based on gather_A
1226
  if varlen_m:
 
1235
  preact_out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
1236
  if postact_out is None:
1237
  postact_out = torch.empty(postact_shape, dtype=postact_dtype, device=A.device)
1238
+ # Empty-input fast path. For M=0 or N=0 the outputs are empty; for K=0
1239
+ # (A@B == 0) the no-bias / no-C surface yields preact=0 and act(0)=0 for
1240
+ # every supported activation, so both outputs are zero.
1241
+ if postact_out.numel() == 0 or A.numel() == 0:
1242
+ if preact_out is not None:
1243
+ _empty_k_matmul_into(preact_out)
1244
+ _empty_k_matmul_into(postact_out)
1245
+ return preact_out, postact_out
1246
  concat_str = ",".join(concat_layout) if concat_layout else None
1247
  if is_gated:
1248
  gemm_gated_out(
 
1258
  dynamic_scheduler,
1259
  tuned,
1260
  concat_layout=concat_str,
1261
+ SFA=SFA,
1262
+ SFB=SFB,
1263
  )
1264
  else:
1265
  gemm_act_out(
 
1274
  A_idx,
1275
  dynamic_scheduler,
1276
  tuned,
1277
+ SFA=SFA,
1278
+ SFB=SFB,
1279
  )
1280
  return preact_out, postact_out
1281
 
 
1284
 
1285
 
1286
  @torch.library.custom_op(
1287
+ add_op_namespace_prefix("gemm_act_out"),
1288
  mutates_args=("preact_out", "postact_out"),
1289
  device_types="cuda",
1290
+ schema="(Tensor A, Tensor B, Tensor(a2!)? preact_out, Tensor(a3!) postact_out, Tensor? C=None, Tensor? bias=None, str? activation=None, Tensor? cu_seqlens_m=None, Tensor? A_idx=None, bool dynamic_scheduler=False, bool tuned=True, Tensor? SFA=None, Tensor? SFB=None) -> ()",
1291
  )
1292
  def gemm_act_out(
1293
  A: Tensor, # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (whatever, K) if gather_A with varlen_m
 
1301
  A_idx: Optional[Tensor] = None, # (total_M,) if gather_A with varlen_m
1302
  dynamic_scheduler: bool = False,
1303
  tuned: bool = True,
1304
+ SFA: Optional[Tensor] = None, # blocked scale factors, (L, rm, rk, 32, 4, 4) (see gemm_out)
1305
+ SFB: Optional[Tensor] = None,
1306
  ) -> None:
1307
  """GEMM with activation and pre-allocated output tensors."""
1308
  fn = gemm_act_tuned if tuned else partial(gemm_act_tuned.fn, config=None)
1309
+ fn(
1310
+ A,
1311
+ B,
1312
+ preact_out,
1313
+ postact_out,
1314
+ C,
1315
+ bias,
1316
+ activation,
1317
+ cu_seqlens_m,
1318
+ A_idx,
1319
+ dynamic_scheduler,
1320
+ SFA=SFA,
1321
+ SFB=SFB,
1322
+ )
1323
 
1324
 
1325
  def gemm_act_ref(
 
1396
  dx_out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
1397
  if postact_out is None:
1398
  postact_out = torch.empty(postact_shape, dtype=postact_dtype, device=A.device)
1399
+ # Empty-input fast path: M=0 / N=0 → outputs are empty; K=0 (A.numel()==0)
1400
+ # makes the upstream GEMM gradient zero, so dx is zero regardless of activation.
1401
+ if dx_out.numel() == 0 or A.numel() == 0:
1402
+ _empty_k_matmul_into(dx_out)
1403
+ _empty_k_matmul_into(postact_out)
1404
+ results = [dx_out, postact_out]
1405
+ if colvec_reduce:
1406
+ colvec_shape = (*out_shape[:-1],)
1407
+ results.append(torch.zeros(colvec_shape, dtype=torch.float32, device=A.device))
1408
+ return tuple(results)
1409
  if is_dgated:
1410
  colvec_reduce_final = gemm_dgated_out(
1411
  A,
 
1421
  dynamic_scheduler,
1422
  tuned,
1423
  )
1424
+ results = [dx_out, postact_out]
1425
+ if colvec_reduce:
1426
+ results.append(colvec_reduce_final)
1427
+ return tuple(results)
1428
  else:
1429
  gemm_dact_out(
1430
  A,
 
1438
  dynamic_scheduler,
1439
  tuned,
1440
  )
1441
+ results = [dx_out, postact_out]
1442
+ return tuple(results)
1443
 
1444
 
1445
  gemm_dgated = gemm_dact
1446
 
1447
 
1448
  @torch.library.custom_op(
1449
+ add_op_namespace_prefix("gemm_dact_out"),
1450
  mutates_args=("dx_out", "postact_out"),
1451
  device_types="cuda",
1452
  schema="(Tensor A, Tensor B, Tensor PreAct, Tensor(a3!) dx_out, Tensor(a4!) postact_out, str? activation=None, Tensor? cu_seqlens_m=None, Tensor? A_idx=None, bool dynamic_scheduler=True, bool tuned=True) -> ()",
 
1511
  gemm_dgated_ref = gemm_dact_ref
1512
 
1513
 
1514
+ def _symmetric_gemm_config(sm: int) -> tuple[int, int, int, bool]:
1515
+ configs = {
1516
+ 8: (128, 128, 1, False),
1517
+ 9: (128, 256, 2, False),
1518
+ 10: (256, 256, 2, False),
1519
+ 11: (256, 256, 2, False),
1520
+ 12: (128, 128, 1, True),
1521
+ }
1522
+ if sm not in configs:
1523
+ raise NotImplementedError(
1524
+ "gemm_symmetric is only supported on SM8x, SM90, SM100, SM110, and SM120"
1525
+ )
1526
+ return configs[sm]
1527
+
1528
+
1529
  @torch.library.custom_op(
1530
+ add_op_namespace_prefix("gemm_symmetric_out"),
1531
  mutates_args=("out",),
1532
  device_types="cuda",
1533
+ # alpha/beta split into float + Tensor pair because torch.library requires
1534
+ # each schema arg to have a fixed type. See gemm_add_out for the pattern.
1535
  )
1536
  def gemm_symmetric_out(
1537
  A: Tensor, # (M, K) or (L, M, K)
 
1541
  dynamic_scheduler: bool = False,
1542
  alpha: float = 1.0,
1543
  beta: float = 1.0,
1544
+ alpha_tensor: Optional[Tensor] = None,
1545
+ beta_tensor: Optional[Tensor] = None,
1546
  ) -> None:
1547
  """GEMM with guaranteed symmetric output."""
1548
+ alpha = _merge_tensor(alpha, alpha_tensor)
1549
+ beta = _merge_tensor(beta, beta_tensor)
1550
  if A.ndim == 2:
1551
  A = A.unsqueeze(0) # (1, M, K)
1552
  B = B.mT # (M, K) or (L, M, K)
 
1563
  )
1564
  sm = get_device_capacity(A.device)[0]
1565
  # We want square tile per cluster
1566
+ tile_m, tile_n, cluster_m, pingpong = _symmetric_gemm_config(sm)
 
 
 
 
 
1567
  gemm_symmetric_dispatch(
1568
  A,
1569
  B,
 
1603
  if out is None:
1604
  out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
1605
 
1606
+ alpha_tensor = alpha if not isinstance(alpha, float) else None
1607
  alpha_val = alpha if isinstance(alpha, float) else 1.0
1608
+ beta_tensor = beta if not isinstance(beta, float) else None
1609
  beta_val = beta if isinstance(beta, float) else 1.0
1610
 
1611
+ # Empty-input fast path: out = alpha * A@A.T + beta * C reduces to beta * C
1612
+ # when K=0 (or just zeros / empty for M=0).
1613
+ if out.numel() == 0:
1614
+ return out
1615
+ if A.numel() == 0:
1616
+ _empty_k_matmul_into(out, C=C, beta=beta)
1617
+ return out
1618
+
1619
  gemm_symmetric_out(
1620
+ A,
1621
+ B,
1622
+ out,
1623
+ C,
1624
+ dynamic_scheduler=dynamic_scheduler,
1625
+ alpha=alpha_val,
1626
+ beta=beta_val,
1627
+ alpha_tensor=alpha_tensor,
1628
+ beta_tensor=beta_tensor,
1629
  )
1630
  return out
1631
 
 
1650
  dynamic_scheduler: bool = False,
1651
  config: Optional[GemmConfig] = None,
1652
  concat_layout: tuple | None = None, # tensors whose non-contiguous dim is concat [gate; up]
1653
+ SFA: Optional[Tensor] = None, # (L, rm, rk, 32, 4, 4) blocked scale factors
1654
+ SFB: Optional[Tensor] = None, # (L, rn, rk, 32, 4, 4)
1655
  ) -> None:
1656
+ blockscaled = SFA is not None
1657
+ if blockscaled:
1658
+ SFA, SFB = _sf_decode(SFA), _sf_decode(SFB)
1659
  if config is None:
1660
+ if blockscaled:
1661
+ config = blockscaled_default_config(A.shape[-2], B.shape[-1])
1662
+ else:
1663
+ config = default_config(A.device)
1664
  varlen_m = cu_seqlens_m is not None
1665
  if varlen_m:
1666
  assert not config.swap_ab, "Variable-length sequences not supported with swap_ab"
1667
+ if blockscaled:
1668
+ assert not varlen_m and A_idx is None, "Blockscaled GEMM does not support varlen/gather yet"
1669
+ assert not concat_layout, "Blockscaled GEMM does not support concat_layout"
1670
+ assert not config.swap_ab, "Blockscaled GEMM does not support swap_ab yet"
1671
  if A.ndim == 2 and not varlen_m:
1672
  A = A.unsqueeze(0) # (1, M, K)
1673
  B = B.mT # (N, K) or (L, N, K)
 
1711
  config.tile_n,
1712
  config.cluster_m,
1713
  config.cluster_n,
1714
+ tile_K=config.tile_k,
1715
+ pingpong=config.pingpong,
1716
  persistent=True,
1717
  is_dynamic_persistent=dynamic_scheduler,
1718
  max_swizzle_size=config.max_swizzle_size,
 
1722
  A_idx=A_idx,
1723
  use_tma_gather=config.use_tma_gather,
1724
  concat_layout=concat_layout,
1725
+ SFA=SFA,
1726
+ SFB=SFB,
1727
  )
1728
 
1729
 
 
1810
  config.tile_n,
1811
  config.cluster_m,
1812
  config.cluster_n,
1813
+ tile_K=config.tile_k,
1814
+ pingpong=config.pingpong,
1815
  persistent=True,
1816
  is_dynamic_persistent=dynamic_scheduler,
1817
  max_swizzle_size=config.max_swizzle_size,
 
1831
 
1832
 
1833
  @torch.library.custom_op(
1834
+ add_op_namespace_prefix("gemm_gated_out"),
1835
  mutates_args=("preact_out", "postact_out"),
1836
  device_types="cuda",
1837
+ schema="(Tensor A, Tensor B, Tensor(a2!)? preact_out, Tensor(a3!) postact_out, Tensor? C=None, Tensor? bias=None, str activation='swiglu', Tensor? cu_seqlens_m=None, Tensor? A_idx=None, bool dynamic_scheduler=False, bool tuned=True, str? concat_layout=None, Tensor? SFA=None, Tensor? SFB=None) -> ()",
1838
  )
1839
  def gemm_gated_out(
1840
  A: Tensor, # (M, K) or (L, M, K) or (total_M, K) if varlen_m or (whatever, K) if gather_A with varlen_m
 
1849
  dynamic_scheduler: bool = False,
1850
  tuned: bool = True,
1851
  concat_layout: Optional[str] = None,
1852
+ SFA: Optional[Tensor] = None, # blocked scale factors, (L, rm, rk, 32, 4, 4) (see gemm_out)
1853
+ SFB: Optional[Tensor] = None,
1854
  ) -> None:
1855
  """GEMM with gated activation and pre-allocated output tensors."""
1856
  fn = gemm_gated_tuned if tuned else partial(gemm_gated_tuned.fn, config=None)
 
1865
  cu_seqlens_m,
1866
  A_idx,
1867
  dynamic_scheduler,
1868
+ concat_layout=_parse_concat_layout(concat_layout),
1869
+ SFA=SFA,
1870
+ SFB=SFB,
1871
  )
1872
 
1873
 
1874
  @torch.library.custom_op(
1875
+ add_op_namespace_prefix("gemm_dgated_out"),
1876
  mutates_args=("dx_out", "postact_out"),
1877
  device_types="cuda",
1878
  schema="(Tensor A, Tensor B, Tensor PreAct, Tensor(a!) dx_out, Tensor(b!) postact_out, Tensor? colvec_scale=None, str activation='swiglu', bool colvec_reduce=False, Tensor? cu_seqlens_m=None, Tensor? A_idx=None, bool dynamic_scheduler=True, bool tuned=True) -> Tensor",
 
1911
  return result
1912
 
1913
 
1914
+ @torch.library.register_fake(add_op_namespace_prefix("gemm_dgated_out"))
1915
  def gemm_dgated_out_fake(
1916
  A: Tensor,
1917
  B: Tensor,
 
1926
  dynamic_scheduler: bool = True,
1927
  tuned: bool = True,
1928
  ) -> Tensor:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1929
  if not colvec_reduce:
1930
  return torch.empty(0, dtype=torch.float32, device=A.device)
1931
  else:
 
1939
  return torch.empty(out_shape, dtype=torch.float32, device=A.device)
1940
 
1941
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1942
  @gemm_add_inplace_op.register_fake
1943
  def gemm_add_inplace_fake(
1944
  A: Tensor,
 
1954
  batch_idx_permute: Optional[Tensor] = None,
1955
  dynamic_scheduler: bool = False,
1956
  tuned: bool = True,
1957
+ concat_layout: Optional[str] = None,
1958
+ SFA: Optional[Tensor] = None,
1959
+ SFB: Optional[Tensor] = None,
1960
  ) -> None:
1961
+ # Pure no-op: the op only mutates ``out``; kernel compilation is owned
1962
+ # by jit_cache + the async compile pool at real execution time.
1963
+ return
1964
+
1965
+
1966
+ # ---------------------------------------------------------------------------
1967
+ # Shared schema-split helpers.
1968
+ #
1969
+ # torch.library.custom_op requires a concrete type per arg, so union-typed
1970
+ # autotuned args (e.g. ``alpha: Union[float, Tensor]``, ``sr_seed: Union[int,
1971
+ # Tensor]``) are split into two fixed-typed schema kwargs at the custom_op
1972
+ # boundary (``alpha: float`` + ``alpha_tensor: Optional[Tensor]``). The eager
1973
+ # bodies merge them back into the unified form via :func:`_merge_tensor`
1974
+ # before calling the autotuned fn.
1975
+ # ---------------------------------------------------------------------------
1976
+
1977
+
1978
+ def _merge_tensor(value, tensor_value):
1979
+ """Return ``tensor_value`` if non-None, else ``value``.
1980
+
1981
+ Single source of truth for the ``Union[scalar, Tensor]`` schema-split
1982
+ merge. Used both inside eager bodies (where ``value = alpha,
1983
+ tensor_value = alpha_tensor``) and inside the fake path (which derives
1984
+ the split pairs from the custom_op signature).
1985
+ """
1986
+ return tensor_value if tensor_value is not None else value
1987
 
1988
 
1989
+ def _parse_concat_layout(value):
1990
+ """Coerce ``concat_layout`` from schema form (``Optional[str]``) to
1991
+ autotuned form (``Optional[tuple[str, ...]]``).
1992
 
1993
+ custom_op schemas can't express ``tuple[str, ...]``, so callers pass a
1994
+ comma-separated string. The autotuned fn keys on a tuple (via
1995
+ ``tuple(sorted(concat_layout))``); a stray string would be iterated
1996
+ char-by-char and silently produce a wrong, never-used compile signature.
1997
+ Single source of truth used by both eager bodies and the fake path.
1998
  """
1999
+ if value is None or isinstance(value, tuple):
2000
+ return value
2001
+ return tuple(value.split(",")) if value else None
2002
 
 
2003
 
2004
+ def _register_noop_fake(custom_op):
2005
+ """Register a pure no-op fake for a mutating custom op.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2006
 
2007
+ These ops only mutate their ``out`` argument, so Dynamo / AOT autograd
2008
+ need no shape effect from the fake; kernel compilation is owned by
2009
+ jit_cache + the async compile pool at real execution time.
2010
+ """
2011
 
2012
+ @custom_op.register_fake
2013
+ def _fake(*args, **kwargs):
2014
+ return
 
 
 
 
 
2015
 
2016
 
2017
+ _register_noop_fake(gemm_out)
2018
+ _register_noop_fake(gemm_add_out)
2019
+ _register_noop_fake(gemm_act_out)
2020
+ _register_noop_fake(gemm_dact_out)
2021
+ _register_noop_fake(gemm_gated_out)
2022
 
2023
 
2024
  @gemm_symmetric_out.register_fake
 
2030
  dynamic_scheduler: bool = False,
2031
  alpha: float = 1.0,
2032
  beta: float = 1.0,
2033
+ alpha_tensor: Optional[Tensor] = None,
2034
+ beta_tensor: Optional[Tensor] = None,
2035
  ) -> None:
2036
+ # Pure no-op: the op only mutates ``out``; kernel compilation is owned
2037
+ # by jit_cache + the async compile pool at real execution time.
2038
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2039
 
2040
 
2041
  ## ── gemm_rms ────────────────────────────────────────────────────────────────
 
2058
  out: Tensor, # (M, N) or (L, M, N)
2059
  C: Optional[Tensor] = None, # (M, N) or (L, M, N)
2060
  norm_weight: Optional[Tensor] = None, # (N,) or (L, N)
2061
+ premult_out: Optional[Tensor] = None, # (M, N) or (L, M, N) — pre-norm_weight snapshot
2062
  eps: float = 1e-6,
2063
  dynamic_scheduler: bool = False,
2064
  config: Optional[GemmConfig] = None,
 
2078
  C = C.unsqueeze(0)
2079
  if norm_weight is not None and norm_weight.ndim == 1:
2080
  norm_weight = norm_weight.unsqueeze(0) # (L, N)
2081
+ if premult_out is not None and premult_out.ndim == 2:
2082
+ premult_out = premult_out.unsqueeze(0)
2083
  # Allocate partial reduction buffer
2084
  tile_n = config.tile_n
2085
  n_tiles = (N + tile_n - 1) // tile_n
 
2103
  config.tile_n,
2104
  config.cluster_m,
2105
  config.cluster_n,
2106
+ tile_K=config.tile_k,
2107
+ pingpong=config.pingpong,
2108
  persistent=True,
2109
  is_dynamic_persistent=dynamic_scheduler,
2110
  max_swizzle_size=config.max_swizzle_size,
2111
  rowvec=norm_weight,
2112
+ aux_out=premult_out,
2113
  )
2114
  # Final reduction: rstd = rsqrt(sum(partials) / N + eps)
2115
  scale = 1.0 / N
 
2122
 
2123
 
2124
  @torch.library.custom_op(
2125
+ add_op_namespace_prefix("gemm_rms_out"),
2126
+ mutates_args=("out", "premult_out"),
2127
  device_types="cuda",
2128
+ schema="(Tensor A, Tensor B, Tensor(a!) out, Tensor? C=None, Tensor? norm_weight=None, Tensor(a2!)? premult_out=None, float eps=1e-6, bool dynamic_scheduler=False, bool tuned=True) -> Tensor",
2129
  )
2130
  def _gemm_rms_out(
2131
  A: Tensor,
 
2133
  out: Tensor,
2134
  C: Optional[Tensor] = None,
2135
  norm_weight: Optional[Tensor] = None,
2136
+ premult_out: Optional[Tensor] = None,
2137
  eps: float = 1e-6,
2138
  dynamic_scheduler: bool = False,
2139
  tuned: bool = True,
 
2141
  """GEMM + RMS + optional rowvec scaling.
2142
 
2143
  D_raw = A @ B (+ C), rstd = rsqrt(mean(D_raw^2) + eps), D_out = D_raw * norm_weight.
2144
+ If premult_out is provided, D_raw (the pre-norm_weight value) is also written to it.
2145
  """
2146
  fn = _gemm_rms_tuned if tuned else partial(_gemm_rms_tuned.fn, config=None)
2147
  return fn(
 
2150
  out,
2151
  C=C,
2152
  norm_weight=norm_weight,
2153
+ premult_out=premult_out,
2154
  eps=eps,
2155
  dynamic_scheduler=dynamic_scheduler,
2156
  )
2157
 
2158
 
2159
+ @torch.library.register_fake(add_op_namespace_prefix("gemm_rms_out"))
2160
  def _gemm_rms_out_fake(
2161
  A: Tensor,
2162
  B: Tensor,
2163
  out: Tensor,
2164
  C: Optional[Tensor] = None,
2165
  norm_weight: Optional[Tensor] = None,
2166
+ premult_out: Optional[Tensor] = None,
2167
  eps: float = 1e-6,
2168
  dynamic_scheduler: bool = False,
2169
  tuned: bool = True,
2170
  ) -> Tensor:
 
 
 
 
 
 
 
 
 
 
2171
  rstd_shape = A.shape[:-1]
2172
  return torch.empty(rstd_shape, dtype=torch.float32, device=A.device)
2173
 
 
2197
  norm_weight: Optional[Tensor] = None, # (N,) or (L, N)
2198
  out: Optional[Tensor] = None, # (M, N) or (L, M, N)
2199
  out_dtype: Optional[torch.dtype] = None,
2200
+ premult_out: Optional[Tensor] = None, # (M, N) or (L, M, N) — pre-norm_weight snapshot
2201
  eps: float = 1e-6,
2202
  dynamic_scheduler: bool = False,
2203
  tuned: bool = True,
 
2205
  """GEMM + RMS statistics + optional rowvec scaling.
2206
 
2207
  D_raw = A @ B (+ C), rstd = rsqrt(mean(D_raw^2) + eps), D_out = D_raw * norm_weight.
2208
+ If premult_out is provided, D_raw (the pre-norm_weight value) is also written to it.
2209
  Returns (D_out, rstd).
2210
  """
2211
  out_dtype = A.dtype if out_dtype is None else out_dtype
 
2213
  if out is None:
2214
  out_shape = (*A.shape[:-1], N)
2215
  out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
2216
+ # Empty-input fast path. Skipping the kernel also avoids a torch.library
2217
+ # adinplaceorview_impl IndexError that fires on empty inputs because
2218
+ # premult_out's positional slot isn't materialized in the boxed args tuple.
2219
+ # K=0 with no C reduces the matmul to zero, so D = 0 and rstd = rsqrt(eps).
2220
+ if out.numel() == 0 or A.numel() == 0:
2221
+ _empty_k_matmul_into(out)
2222
+ if premult_out is not None:
2223
+ _empty_k_matmul_into(premult_out)
2224
+ rstd_shape = A.shape[:-1]
2225
+ if A.numel() == 0 and out.numel() > 0:
2226
+ # K=0: rstd = rsqrt(0 + eps) for every row.
2227
+ rstd = torch.full(rstd_shape, eps**-0.5, dtype=torch.float32, device=A.device)
2228
+ else:
2229
+ rstd = torch.empty(rstd_shape, dtype=torch.float32, device=A.device)
2230
+ return out, rstd
2231
  rstd = _gemm_rms_out(
2232
  A,
2233
  B,
2234
  out,
2235
  C=C,
2236
  norm_weight=norm_weight,
2237
+ premult_out=premult_out,
2238
  eps=eps,
2239
  dynamic_scheduler=dynamic_scheduler,
2240
  tuned=tuned,
 
2298
  config.tile_n,
2299
  config.cluster_m,
2300
  config.cluster_n,
2301
+ tile_K=config.tile_k,
2302
+ pingpong=config.pingpong,
2303
  persistent=True,
2304
  is_dynamic_persistent=dynamic_scheduler,
2305
  max_swizzle_size=config.max_swizzle_size,
 
2361
  config.tile_n,
2362
  config.cluster_m,
2363
  config.cluster_n,
2364
+ tile_K=config.tile_k,
2365
+ pingpong=config.pingpong,
2366
  persistent=True,
2367
  is_dynamic_persistent=dynamic_scheduler,
2368
  max_swizzle_size=config.max_swizzle_size,
 
2372
 
2373
 
2374
  @torch.library.custom_op(
2375
+ add_op_namespace_prefix("gemm_norm_act_out"),
2376
  mutates_args=("preact_out", "postact_out"),
2377
  device_types="cuda",
2378
  schema="(Tensor A, Tensor B, Tensor(a2!)? preact_out, Tensor(a3!) postact_out, Tensor? C=None, Tensor? rstd=None, str? activation=None, bool dynamic_scheduler=False, bool tuned=True) -> ()",
 
2392
  fn(A, B, preact_out, postact_out, C, rstd, activation, dynamic_scheduler)
2393
 
2394
 
2395
+ _register_noop_fake(gemm_norm_act_out)
 
 
 
 
 
 
 
 
 
 
 
 
2396
 
2397
 
2398
  @torch.library.custom_op(
2399
+ add_op_namespace_prefix("gemm_norm_gated_out"),
2400
  mutates_args=("preact_out", "postact_out"),
2401
  device_types="cuda",
2402
  schema="(Tensor A, Tensor B, Tensor(a2!)? preact_out, Tensor(a3!) postact_out, Tensor? C=None, Tensor? rstd=None, str activation='swiglu', bool dynamic_scheduler=False, bool tuned=True) -> ()",
 
2416
  fn(A, B, preact_out, postact_out, C, rstd, activation, dynamic_scheduler)
2417
 
2418
 
2419
+ _register_noop_fake(gemm_norm_gated_out)
 
 
 
 
 
 
 
 
 
 
 
 
2420
 
2421
 
2422
  def gemm_norm_act(
 
2450
  preact_out = torch.empty(out_shape, dtype=out_dtype, device=A.device)
2451
  if postact_out is None:
2452
  postact_out = torch.empty(postact_shape, dtype=postact_dtype, device=A.device)
2453
+ # Empty-input fast path: skip kernel; zero both outputs (act(0)=0 for all
2454
+ # supported activations under the no-bias/no-C path of this test surface).
2455
+ if postact_out.numel() == 0 or A.numel() == 0:
2456
+ if preact_out is not None:
2457
+ _empty_k_matmul_into(preact_out)
2458
+ _empty_k_matmul_into(postact_out)
2459
+ return preact_out, postact_out
2460
  if is_gated:
2461
  gemm_norm_gated_out(
2462
  A,
 
2508
  if rstd is not None:
2509
  D = D * rstd.unsqueeze(-1)
2510
  preact = D.to(out_dtype) if store_preact else None
 
2511
  if is_gated:
2512
  gate = D[..., ::2]
2513
  up = D[..., 1::2]
2514
  postact = gated_to_pytorch_fn_map[activation](gate, up).to(postact_dtype)
2515
  else:
2516
+ postact = act_to_pytorch_fn_map[activation](D).to(postact_dtype)
2517
  return preact, postact
2518
 
2519
 
build/torch-cuda/quack/gemm_norm_act.py CHANGED
@@ -18,13 +18,14 @@ from .cute_dsl_utils import (
18
  get_device_capacity,
19
  get_max_active_clusters,
20
  )
 
21
  from .gemm_sm90 import GemmSm90
22
  from .gemm_sm100 import GemmSm100
23
  from .gemm_sm120 import GemmSm120
24
- from .gemm_act import GemmActMixin, GemmGatedMixin
25
  from .epi_ops import vec_multiply
26
  from .activation import act_fn_map, gate_fn_map
27
- from .cache_utils import jit_cache
28
  from .rounding import RoundingMode
29
  from .gemm_tvm_ffi_utils import (
30
  get_major,
@@ -54,9 +55,9 @@ class GemmNormActMixin(GemmActMixin):
54
  epi_loop_tensors: Tuple[cute.Tensor, ...],
55
  tRS_rD: cute.Tensor,
56
  tRS_rC: Optional[cute.Tensor] = None,
57
- ) -> Optional[cute.Tensor]:
58
- tDrRowVec = epi_loop_tensors["mRowVecBroadcast"]
59
- tDrColVec = epi_loop_tensors["mColVecBroadcast"]
60
  # Load accumulator and apply alpha/beta/C
61
  rD = tRS_rD.load()
62
  if const_expr(hasattr(params, "alpha") and params.alpha is not None):
@@ -73,24 +74,28 @@ class GemmNormActMixin(GemmActMixin):
73
  vec_multiply(self, tRS_rD, tDrColVec, tDrRowVec)
74
  # Apply activation
75
  if const_expr(params.act_fn is not None):
76
- tRS_rPostAct = cute.make_rmem_tensor(tRS_rD.layout.shape, self.acc_dtype)
77
- if const_expr(self.arch < 100):
78
- for i in cutlass.range(cute.size(tRS_rPostAct), unroll_full=True):
79
- tRS_rPostAct[i] = params.act_fn(tRS_rD[i])
80
  else:
81
- for i in cutlass.range(cute.size(tRS_rPostAct) // 2, unroll_full=True):
82
- tRS_rPostAct[2 * i], tRS_rPostAct[2 * i + 1] = params.act_fn(
83
  (tRS_rD[2 * i], tRS_rD[2 * i + 1])
84
  )
85
  else:
86
- tRS_rPostAct = tRS_rD
87
- return tRS_rPostAct
88
 
89
 
90
  class GemmNormActSm90(GemmNormActMixin, GemmSm90):
91
  pass
92
 
93
 
 
 
 
 
94
  class GemmNormActSm100(GemmNormActMixin, GemmSm100):
95
  pass
96
 
@@ -109,9 +114,9 @@ class GemmNormGatedMixin(GemmGatedMixin):
109
  epi_loop_tensors: Tuple[cute.Tensor, ...],
110
  tRS_rD: cute.Tensor,
111
  tRS_rC: Optional[cute.Tensor] = None,
112
- ) -> Optional[cute.Tensor]:
113
- tDrRowVec = epi_loop_tensors["mRowVecBroadcast"]
114
- tDrColVec = epi_loop_tensors["mColVecBroadcast"]
115
  # Load accumulator and apply alpha/beta/C
116
  rD = tRS_rD.load()
117
  if const_expr(hasattr(params, "alpha") and params.alpha is not None):
@@ -127,29 +132,30 @@ class GemmNormGatedMixin(GemmGatedMixin):
127
  # Multiply by colvec (rstd) and rowvec (norm_weight)
128
  vec_multiply(self, tRS_rD, tDrColVec, tDrRowVec)
129
  # Gated activation on normalized D
130
- tRS_rPostAct_layout = cute.recast_layout(2, 1, tRS_rD.layout)
131
- tRS_rPostAct = cute.make_rmem_tensor(tRS_rPostAct_layout.shape, self.acc_dtype)
132
- if const_expr(self.arch < 100):
133
- for i in cutlass.range(cute.size(tRS_rPostAct), unroll_full=True):
134
- tRS_rPostAct[i] = params.act_fn(tRS_rD[2 * i], tRS_rD[2 * i + 1])
135
- else:
136
- for i in cutlass.range(cute.size(tRS_rPostAct) // 2, unroll_full=True):
137
- tRS_rPostAct[2 * i], tRS_rPostAct[2 * i + 1] = params.act_fn(
138
- (tRS_rD[4 * i], tRS_rD[4 * i + 2]),
139
- (tRS_rD[4 * i + 1], tRS_rD[4 * i + 3]),
140
- )
141
- return tRS_rPostAct
142
 
143
 
144
  class GemmNormGatedSm90(GemmNormGatedMixin, GemmSm90):
145
  pass
146
 
147
 
 
 
 
 
148
  class GemmNormGatedSm100(GemmNormGatedMixin, GemmSm100):
149
  pass
150
 
151
 
152
- class GemmNormGatedSm120(GemmNormGatedMixin, GemmSm120):
153
  pass
154
 
155
 
@@ -183,12 +189,14 @@ def _compile_gemm_norm_act(
183
  ):
184
  sm_to_cls = {
185
  "norm_act": {
 
186
  9: GemmNormActSm90,
187
  10: GemmNormActSm100,
188
  11: GemmNormActSm100,
189
  12: GemmNormActSm120,
190
  },
191
  "norm_gated": {
 
192
  9: GemmNormGatedSm90,
193
  10: GemmNormGatedSm100,
194
  11: GemmNormGatedSm100,
@@ -213,7 +221,7 @@ def _compile_gemm_norm_act(
213
  pa_n = cute.sym_int() if gemm_cls_name == "norm_gated" else n
214
  pa_leading_dim = 1 if gemm_cls_name == "norm_gated" else pa_leading
215
  pa_shape = (m, pa_n) if varlen_m else (m, pa_n, l)
216
- mPostAct = fake_tensor(postact_dtype, pa_shape, leading_dim=pa_leading_dim, divisibility=div_pa)
217
 
218
  mRowVec = fake_tensor(rowvec_dtype, (l, n), leading_dim=1, divisibility=4)
219
  if colvec_ndim == 2:
@@ -234,7 +242,7 @@ def _compile_gemm_norm_act(
234
  return make_ptr(dtype, 0, cute.AddressSpace.gmem, assumed_align=4)
235
 
236
  epi_args = GemmCls.EpilogueArguments(
237
- mPostAct,
238
  act_fn,
239
  mRowVecBroadcast=mRowVec,
240
  mColVecBroadcast=mColVec,
@@ -277,6 +285,7 @@ def gemm_norm_act_fn(
277
  tile_N: int,
278
  cluster_M: int,
279
  cluster_N: int,
 
280
  pingpong: bool = False,
281
  persistent: bool = True,
282
  is_dynamic_persistent: bool = False,
@@ -326,7 +335,9 @@ def gemm_norm_act_fn(
326
  colvec_ndim = colvec.ndim if colvec is not None else 0
327
 
328
  device_capacity = get_device_capacity(A.device)
329
- assert device_capacity[0] in [9, 10, 11, 12], "Only SM90, SM100, SM110, and SM120 are supported"
 
 
330
  if rounding_mode == RoundingMode.RS:
331
  assert device_capacity[0] == 10, "Stochastic rounding requires SM100"
332
 
@@ -349,7 +360,7 @@ def gemm_norm_act_fn(
349
  d_major,
350
  c_major,
351
  postact_major,
352
- (tile_M, tile_N),
353
  (cluster_M, cluster_N, 1),
354
  pingpong,
355
  persistent,
@@ -366,11 +377,6 @@ def gemm_norm_act_fn(
366
  sr_seed_mode=sr_seed_mode,
367
  )
368
 
369
- from .cache_utils import COMPILE_ONLY
370
-
371
- if COMPILE_ONLY:
372
- return
373
-
374
  max_active_clusters = get_max_active_clusters(cluster_M * cluster_N) if persistent else 0
375
 
376
  def scalar_arg(scalar, mode, dtype=Int32):
@@ -395,6 +401,6 @@ def gemm_norm_act_fn(
395
  varlen_args = make_varlen_args(cu_seqlens_m, None, A_idx)
396
 
397
  if device_capacity[0] in [10, 11]:
398
- compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args, None, None, None)
399
  else:
400
- compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args, None)
 
18
  get_device_capacity,
19
  get_max_active_clusters,
20
  )
21
+ from .gemm_sm80 import GemmSm80
22
  from .gemm_sm90 import GemmSm90
23
  from .gemm_sm100 import GemmSm100
24
  from .gemm_sm120 import GemmSm120
25
+ from .gemm_act import GemmActMixin, GemmGatedMixin, GemmGatedSm120Mixin
26
  from .epi_ops import vec_multiply
27
  from .activation import act_fn_map, gate_fn_map
28
+ from .cache import jit_cache
29
  from .rounding import RoundingMode
30
  from .gemm_tvm_ffi_utils import (
31
  get_major,
 
55
  epi_loop_tensors: Tuple[cute.Tensor, ...],
56
  tRS_rD: cute.Tensor,
57
  tRS_rC: Optional[cute.Tensor] = None,
58
+ ) -> Tuple[cute.Tensor, ...]:
59
+ tDrRowVec = epi_loop_tensors.get("mRowVecBroadcast")
60
+ tDrColVec = epi_loop_tensors.get("mColVecBroadcast")
61
  # Load accumulator and apply alpha/beta/C
62
  rD = tRS_rD.load()
63
  if const_expr(hasattr(params, "alpha") and params.alpha is not None):
 
74
  vec_multiply(self, tRS_rD, tDrColVec, tDrRowVec)
75
  # Apply activation
76
  if const_expr(params.act_fn is not None):
77
+ tRS_rAuxOut = cute.make_rmem_tensor(tRS_rD.layout.shape, self.acc_dtype)
78
+ if const_expr(self.arch != 100):
79
+ for i in cutlass.range(cute.size(tRS_rAuxOut), unroll_full=True):
80
+ tRS_rAuxOut[i] = params.act_fn(tRS_rD[i])
81
  else:
82
+ for i in cutlass.range(cute.size(tRS_rAuxOut) // 2, unroll_full=True):
83
+ tRS_rAuxOut[2 * i], tRS_rAuxOut[2 * i + 1] = params.act_fn(
84
  (tRS_rD[2 * i], tRS_rD[2 * i + 1])
85
  )
86
  else:
87
+ tRS_rAuxOut = tRS_rD
88
+ return (tRS_rAuxOut,)
89
 
90
 
91
  class GemmNormActSm90(GemmNormActMixin, GemmSm90):
92
  pass
93
 
94
 
95
+ class GemmNormActSm80(GemmNormActMixin, GemmSm80):
96
+ pass
97
+
98
+
99
  class GemmNormActSm100(GemmNormActMixin, GemmSm100):
100
  pass
101
 
 
114
  epi_loop_tensors: Tuple[cute.Tensor, ...],
115
  tRS_rD: cute.Tensor,
116
  tRS_rC: Optional[cute.Tensor] = None,
117
+ ) -> Tuple[cute.Tensor, ...]:
118
+ tDrRowVec = epi_loop_tensors.get("mRowVecBroadcast")
119
+ tDrColVec = epi_loop_tensors.get("mColVecBroadcast")
120
  # Load accumulator and apply alpha/beta/C
121
  rD = tRS_rD.load()
122
  if const_expr(hasattr(params, "alpha") and params.alpha is not None):
 
132
  # Multiply by colvec (rstd) and rowvec (norm_weight)
133
  vec_multiply(self, tRS_rD, tDrColVec, tDrRowVec)
134
  # Gated activation on normalized D
135
+ tRS_rAuxOut_layout = cute.recast_layout(2, 1, tRS_rD.layout)
136
+ tRS_rAuxOut = cute.make_rmem_tensor(tRS_rAuxOut_layout.shape, self.acc_dtype)
137
+ tRS_rD_pair = cute.flat_divide(tRS_rD, cute.make_layout(2))
138
+ tRS_rGate = tRS_rD_pair[0, ...]
139
+ tRS_rUp = tRS_rD_pair[1, ...]
140
+ vectorize = const_expr(self.arch == 100)
141
+ for i in cutlass.range(cute.size(tRS_rAuxOut), unroll_full=True, vectorize=vectorize):
142
+ tRS_rAuxOut[i] = params.act_fn(tRS_rGate[i], tRS_rUp[i])
143
+ return (tRS_rAuxOut,)
 
 
 
144
 
145
 
146
  class GemmNormGatedSm90(GemmNormGatedMixin, GemmSm90):
147
  pass
148
 
149
 
150
+ class GemmNormGatedSm80(GemmNormGatedMixin, GemmSm80):
151
+ pass
152
+
153
+
154
  class GemmNormGatedSm100(GemmNormGatedMixin, GemmSm100):
155
  pass
156
 
157
 
158
+ class GemmNormGatedSm120(GemmGatedSm120Mixin, GemmNormGatedMixin, GemmSm120):
159
  pass
160
 
161
 
 
189
  ):
190
  sm_to_cls = {
191
  "norm_act": {
192
+ 8: GemmNormActSm80,
193
  9: GemmNormActSm90,
194
  10: GemmNormActSm100,
195
  11: GemmNormActSm100,
196
  12: GemmNormActSm120,
197
  },
198
  "norm_gated": {
199
+ 8: GemmNormGatedSm80,
200
  9: GemmNormGatedSm90,
201
  10: GemmNormGatedSm100,
202
  11: GemmNormGatedSm100,
 
221
  pa_n = cute.sym_int() if gemm_cls_name == "norm_gated" else n
222
  pa_leading_dim = 1 if gemm_cls_name == "norm_gated" else pa_leading
223
  pa_shape = (m, pa_n) if varlen_m else (m, pa_n, l)
224
+ mAuxOut = fake_tensor(postact_dtype, pa_shape, leading_dim=pa_leading_dim, divisibility=div_pa)
225
 
226
  mRowVec = fake_tensor(rowvec_dtype, (l, n), leading_dim=1, divisibility=4)
227
  if colvec_ndim == 2:
 
242
  return make_ptr(dtype, 0, cute.AddressSpace.gmem, assumed_align=4)
243
 
244
  epi_args = GemmCls.EpilogueArguments(
245
+ mAuxOut,
246
  act_fn,
247
  mRowVecBroadcast=mRowVec,
248
  mColVecBroadcast=mColVec,
 
285
  tile_N: int,
286
  cluster_M: int,
287
  cluster_N: int,
288
+ tile_K: int | None = None,
289
  pingpong: bool = False,
290
  persistent: bool = True,
291
  is_dynamic_persistent: bool = False,
 
335
  colvec_ndim = colvec.ndim if colvec is not None else 0
336
 
337
  device_capacity = get_device_capacity(A.device)
338
+ assert device_capacity[0] in [8, 9, 10, 11, 12], (
339
+ "Only SM8x, SM90, SM100, SM110, and SM120 are supported"
340
+ )
341
  if rounding_mode == RoundingMode.RS:
342
  assert device_capacity[0] == 10, "Stochastic rounding requires SM100"
343
 
 
360
  d_major,
361
  c_major,
362
  postact_major,
363
+ (tile_M, tile_N, tile_K) if tile_K is not None else (tile_M, tile_N),
364
  (cluster_M, cluster_N, 1),
365
  pingpong,
366
  persistent,
 
377
  sr_seed_mode=sr_seed_mode,
378
  )
379
 
 
 
 
 
 
380
  max_active_clusters = get_max_active_clusters(cluster_M * cluster_N) if persistent else 0
381
 
382
  def scalar_arg(scalar, mode, dtype=Int32):
 
401
  varlen_args = make_varlen_args(cu_seqlens_m, None, A_idx)
402
 
403
  if device_capacity[0] in [10, 11]:
404
+ compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args, None, None)
405
  else:
406
+ compiled_fn(A_p, B_p, D_p, C_p, epi_args, scheduler_args, varlen_args)
build/torch-cuda/quack/gemm_sm100.py CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright (c) 2025-2026, Tri Dao.
2
  # Based on the cute-dsl example:
3
  # https://github.com/NVIDIA/cutlass/blob/main/examples/python/CuTeDSL/blackwell/dense_gemm_persistent.py
4
 
@@ -23,11 +23,22 @@ from cutlass.cute.nvgpu.warp import (
23
  )
24
  from cutlass import Int32, Float32, Boolean, const_expr
25
  from cutlass.utils import LayoutEnum
 
26
 
27
- from .pipeline import PipelineTmaUmma, PipelineTmaCpAsyncUmma
 
 
 
 
 
 
 
 
 
 
28
  from .tile_scheduler import TileSchedulerOptions
29
  from .varlen_utils import VarlenArguments, VarlenManager
30
- from .gemm_sm90 import GemmSm90, NamedBarrierGemm
31
  from . import layout_utils
32
  from . import copy_utils as copy_utils
33
  from . import sm100_utils as quack_sm100_utils
@@ -64,29 +75,7 @@ SM100 tcgen05.mma instructions operate as follows:
64
  - Write accumulator to TMEM
65
  The accumulator in TMEM must then be loaded to registers before writing back to GMEM.
66
 
67
- Input arguments to this example is same as dense_gemm.py.
68
-
69
- .. code-block:: bash
70
-
71
- python examples/blackwell/dense_gemm_persistent.py \
72
- --ab_dtype Float16 --d_dtype Float16 --acc_dtype Float32 \
73
- --mma_tiler_mn 256,128 --cluster_shape_mn 2,1 \
74
- --mnkl 8192,8192,8192,1 \
75
- --use_2cta_instrs
76
-
77
- To collect performance with NCU profiler:
78
-
79
- .. code-block:: bash
80
-
81
- ncu python examples/blackwell/dense_gemm_persistent.py \
82
- --ab_dtype Float16 --d_dtype Float16 --acc_dtype Float32 \
83
- --mma_tiler_mn 256,128 --cluster_shape_mn 2,1 \
84
- --mnkl 8192,8192,8192,1 \
85
- --use_2cta_instrs \
86
- --warmup_iterations 1 --iterations 10 --skip_ref_check
87
-
88
-
89
- Constraints are same as dense_gemm.py:
90
  * Supported input data types: fp16, bf16, tf32, int8, uint8, fp8 (e4m3fn, e5m2),
91
  see detailed valid dtype combinations in below GemmSm100 class documentation
92
  * A/B tensor must have the same data type
@@ -99,15 +88,15 @@ Constraints are same as dense_gemm.py:
99
  """
100
 
101
 
102
- class GemmSm100(GemmSm90):
103
  """This class implements batched matrix multiplication (C = A x B) with support for various data types
104
  and architectural features specific to Blackwell GPUs with persistent tile scheduling and warp specialization.
105
 
106
  :param acc_dtype: Data type for accumulation during computation
107
  :type acc_dtype: type[cutlass.Numeric]
108
- :param mma_tiler_mn: Shape of the MMA tile. Pass (M, N) to default K to
109
  4 MMA instructions, or (M, N, K) to set the K tile size explicitly.
110
- :type mma_tiler_mn: Union[Tuple[int, int], Tuple[int, int, int]]
111
  :param cluster_shape_mn: Cluster dimensions (M,N) for parallel processing
112
  :type cluster_shape_mn: Tuple[int, int]
113
 
@@ -141,7 +130,7 @@ class GemmSm100(GemmSm90):
141
  Example:
142
  >>> gemm = GemmSm100(
143
  ... acc_dtype=Float32,
144
- ... mma_tiler_mn=(128, 128),
145
  ... cluster_shape_mn=(2, 2)
146
  ... )
147
  >>> gemm(mA, mB, mD, max_active_clusters, stream)
@@ -149,14 +138,14 @@ class GemmSm100(GemmSm90):
149
 
150
  arch = 100
151
 
152
- EpilogueArguments = GemmSm90.EpilogueArguments
153
- EpilogueParams = GemmSm90.EpilogueParams
154
 
155
  def __init__(
156
  self,
157
  acc_dtype: Type[cutlass.Numeric],
158
  a_dtype: Type[cutlass.Numeric], # ignored for now
159
- mma_tiler_mn: Union[Tuple[int, int], Tuple[int, int, int]],
160
  cluster_shape_mnk: Tuple[int, int, int],
161
  sf_vec_size: Optional[int] = None,
162
  gather_A: bool = False,
@@ -171,7 +160,7 @@ class GemmSm100(GemmSm90):
171
 
172
  1. MMA Instruction Settings (tcgen05):
173
  - acc_dtype: Data types for MMA accumulator.
174
- - mma_tiler_mn: The (M, N) shape of the MMA instruction tiler.
175
  - use_2cta_instrs: Boolean indicating if the tcgen05 MMA variant
176
  with cta_group=2 should be used.
177
 
@@ -180,27 +169,29 @@ class GemmSm100(GemmSm90):
180
 
181
  :param acc_dtype: Data type of the accumulator.
182
  :type acc_dtype: type[cutlass.Numeric]
183
- :param mma_tiler_mn: (M, N) or (M, N, K) shape of the MMA tile.
184
  If only (M, N) is given, K defaults to 4 * instruction K.
185
- :type mma_tiler_mn: Union[Tuple[int, int], Tuple[int, int, int]]
186
  :param cluster_shape_mnk: Tuple (ClusterM, ClusterN) shape of the cluster.
187
  :type cluster_shape_mnk: Tuple[int, int]
188
  """
189
 
190
  self.acc_dtype: Type[cutlass.Numeric] = acc_dtype
191
- self.use_2cta_instrs = mma_tiler_mn[0] in (256,)
 
 
 
 
192
  self.cluster_shape_mnk = cluster_shape_mnk
193
  assert cluster_shape_mnk[2] == 1, "Cluster shape K must be 1"
194
  # K dimension: if user provides 3 values, use their K; otherwise default in _setup_attributes
195
- if len(mma_tiler_mn) == 3:
196
- self.mma_tiler = tuple(mma_tiler_mn)
197
  else:
198
- self.mma_tiler = (*mma_tiler_mn, 0)
199
- self.sf_vec_size = sf_vec_size
200
- self.blockscaled = sf_vec_size is not None
201
  self.is_persistent = True
202
- self.pingpong = False # for compatibility with GemmSm90
203
  self.use_clc_persistence = use_clc_persistence
 
204
  self.gather_A = gather_A
205
  self.concat_layout = concat_layout or ()
206
  self.use_tma_gather = use_tma_gather
@@ -229,6 +220,23 @@ class GemmSm100(GemmSm90):
229
  barrier_id=int(NamedBarrierGemm.Epilogue),
230
  num_threads=self.num_epi_warps * cute.arch.WARP_SIZE,
231
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  # Register reallocation for gather_A (3 warp groups, 504 regs total, 168 per WG default).
233
  # Heavy epilogues (e.g. colvec_reduce in DGated) override these to avoid register spilling.
234
  # Without gather_A there are only 2 WGs (512 total, 256 per WG = max), no reallocation needed.
@@ -250,6 +258,15 @@ class GemmSm100(GemmSm90):
250
  # Multiple of 4 warps to increase/decrease number of registers
251
  assert self.threads_per_cta % 128 == 0
252
 
 
 
 
 
 
 
 
 
 
253
  def _setup_attributes(self, epilogue_args: EpilogueArguments, varlen_args: VarlenArguments):
254
  """Set up configurations that are dependent on GEMM inputs
255
 
@@ -264,6 +281,8 @@ class GemmSm100(GemmSm90):
264
  - Computing A/B/C shared memory layout
265
  - Computing tensor memory allocation columns
266
  """
 
 
267
  # Compute mma instruction shapes
268
  mma_inst_bits_k = 256
269
  # (MMA_Tile_Shape_M, MMA_Tile_Shape_N, MMA_Inst_Shape_K)
@@ -284,6 +303,7 @@ class GemmSm100(GemmSm90):
284
  if const_expr(not self.blockscaled):
285
  self.tiled_mma = sm100_utils.make_trivial_tiled_mma(
286
  self.a_dtype,
 
287
  self.a_major_mode,
288
  self.b_major_mode,
289
  self.acc_dtype,
@@ -294,6 +314,7 @@ class GemmSm100(GemmSm90):
294
  else:
295
  self.tiled_mma = sm100_utils.make_blockscaled_trivial_tiled_mma(
296
  self.a_dtype,
 
297
  self.a_major_mode,
298
  self.b_major_mode,
299
  self.sf_dtype,
@@ -303,6 +324,7 @@ class GemmSm100(GemmSm90):
303
  )
304
  self.tiled_mma_sfb = sm100_utils.make_blockscaled_trivial_tiled_mma(
305
  self.a_dtype,
 
306
  self.a_major_mode,
307
  self.b_major_mode,
308
  self.sf_dtype,
@@ -313,6 +335,10 @@ class GemmSm100(GemmSm90):
313
 
314
  # Compute mma/cluster/tile shapes
315
  if self.mma_tiler[2] > 0:
 
 
 
 
316
  mma_inst_tile_k = self.mma_tiler[2] // self.mma_inst_shape_mnk[2]
317
  else:
318
  mma_inst_tile_k = 4
@@ -340,6 +366,25 @@ class GemmSm100(GemmSm90):
340
  self.mma_tiler_sfb[1],
341
  self.mma_tiler_sfb[2],
342
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  else:
344
  self.cta_tile_shape_mnk_sfb = None
345
 
@@ -368,13 +413,25 @@ class GemmSm100(GemmSm90):
368
  self.is_sfb_mcast = self.num_mcast_ctas_sfb > 1
369
 
370
  # Compute epilogue subtile
 
 
 
 
 
 
 
 
 
 
 
 
371
  self.epi_tile = sm100_utils.compute_epilogue_tile_shape(
372
  self.cta_tile_shape_mnk,
373
  self.use_2cta_instrs,
374
  self.d_layout if self.d_layout is not None else LayoutEnum.ROW_MAJOR,
375
  self.d_dtype if self.d_dtype is not None else cutlass.BFloat16,
376
- layout_c=self.c_layout,
377
- elem_ty_c=self.c_dtype,
378
  )
379
  # TMA store tile starts must stay aligned when advancing across CTA-N tiles.
380
  # There's a bug w compute_epilogue_tile_shape (as of cutlass-dsl 4.4.2) where if
@@ -415,8 +472,16 @@ class GemmSm100(GemmSm90):
415
  prefetch_A_idx,
416
  cutlass.utils.get_smem_capacity_in_bytes(f"sm_{self.arch}"), # smem_capacity
417
  self.occupancy,
 
418
  )
419
- self.sched_stage = 1
 
 
 
 
 
 
 
420
  self.a_prefetch_stage = (
421
  0
422
  if not self.gather_A
@@ -512,7 +577,6 @@ class GemmSm100(GemmSm90):
512
  stream: cuda.CUstream,
513
  mSFA: Optional[cute.Tensor] = None,
514
  mSFB: Optional[cute.Tensor] = None,
515
- trace_ptr: Optional[cutlass.Int64] = None,
516
  ):
517
  """Execute the GEMM operation in steps:
518
  - Setup static attributes before smem/grid/tma computation
@@ -576,7 +640,7 @@ class GemmSm100(GemmSm90):
576
  # so non-packed buffers work (e.g. a slice of a larger scale tensor).
577
  # Only the innermost 512-B tile must be contiguous.
578
  # For varlen_m, mSFA is sized for per-expert 128-row-padded storage
579
- # (dQaccum format), so use its own M dim (= total_padded_rm * 128)
580
  # instead of mA.shape[0] (= total_m, unpadded).
581
  if const_expr(cute.rank(mA) == 3):
582
  sfa_shape = mA.shape
@@ -599,8 +663,12 @@ class GemmSm100(GemmSm90):
599
  a_smem_layout = cute.slice_(self.a_smem_layout_staged, (None, None, None, 0))
600
  b_smem_layout = cute.slice_(self.b_smem_layout_staged, (None, None, None, 0))
601
  tma_atom_a, tma_tensor_a = None, None
602
- a_op = sm100_utils.cluster_shape_to_tma_atom_A(
603
- self.cluster_shape_mnk, self.tiled_mma.thr_id
 
 
 
 
604
  )
605
  if const_expr(not self.gather_A):
606
  tma_atom_a, tma_tensor_a = cute.nvgpu.make_tiled_tma_atom_A(
@@ -628,9 +696,9 @@ class GemmSm100(GemmSm90):
628
  tma_smem_layout.shape,
629
  internal_type=(cutlass.TFloat32 if mA.element_type is Float32 else None),
630
  )
631
- b_op = sm100_utils.cluster_shape_to_tma_atom_B(
632
- self.cluster_shape_mnk, self.tiled_mma.thr_id
633
- )
634
  tma_atom_b, tma_tensor_b = cute.nvgpu.make_tiled_tma_atom_B(
635
  b_op,
636
  copy_utils.create_ragged_tensor_for_tma(mB, ragged_dim=1) if varlen_k else mB,
@@ -645,9 +713,7 @@ class GemmSm100(GemmSm90):
645
  tma_atom_sfb, tma_tensor_sfb = None, None
646
  if const_expr(self.blockscaled):
647
  # Setup TMA load for SFA
648
- sfa_op = sm100_utils.cluster_shape_to_tma_atom_A(
649
- self.cluster_shape_mnk, self.tiled_mma.thr_id
650
- )
651
  sfa_smem_layout = cute.slice_(self.sfa_smem_layout_staged, (None, None, None, 0))
652
  tma_atom_sfa, tma_tensor_sfa = cute.nvgpu.make_tiled_tma_atom_A(
653
  sfa_op,
@@ -659,9 +725,7 @@ class GemmSm100(GemmSm90):
659
  internal_type=cutlass.Int16,
660
  )
661
  # Setup TMA load for SFB
662
- sfb_op = sm100_utils.cluster_shape_to_tma_atom_SFB(
663
- self.cluster_shape_mnk, self.tiled_mma.thr_id
664
- )
665
  sfb_smem_layout = cute.slice_(self.sfb_smem_layout_staged, (None, None, None, 0))
666
  tma_atom_sfb, tma_tensor_sfb = cute.nvgpu.make_tiled_tma_atom_B(
667
  sfb_op,
@@ -672,9 +736,15 @@ class GemmSm100(GemmSm90):
672
  self.cluster_layout_sfb_vmnk.shape,
673
  internal_type=cutlass.Int16,
674
  )
675
- if const_expr(
676
- self.cta_tile_shape_mnk[1] == 192 and self.sf_dtype is cutlass.Float8E8M0FNU
677
- ):
 
 
 
 
 
 
678
  x = tma_tensor_sfb.stride[0][1]
679
  y = cute.ceil_div(tma_tensor_sfb.shape[0][1], 4)
680
  tma_tensor_sfb = cute.make_tensor(
@@ -702,28 +772,24 @@ class GemmSm100(GemmSm90):
702
  self.num_tma_load_bytes += sfa_copy_size + sfb_copy_size
703
  self.num_tma_load_bytes *= atom_thr_size
704
 
705
- # Setup TMA store for D
706
- tma_atom_d, tma_tensor_d = None, None
707
- if const_expr(mD is not None):
708
- tma_atom_d, tma_tensor_d = self._make_tma_epi_atoms_and_tensors(
709
- copy_utils.create_ragged_tensor_for_tma(mD, ragged_dim=0, ptr_shift=True)
710
- if varlen_m
711
- else mD,
712
- self.epi_smem_layout_staged,
713
- self.epi_tile,
714
- op_type="store"
715
- if not (hasattr(epilogue_args, "add_to_output") and epilogue_args.add_to_output)
716
- else "add",
717
- )
718
- tma_atom_c, tma_tensor_c = None, None
719
- if const_expr(mC is not None):
720
- tma_atom_c, tma_tensor_c = self._make_tma_epi_atoms_and_tensors(
721
- mC, self.epi_c_smem_layout_staged, self.epi_tile, op_type="load"
722
- )
723
 
724
  epilogue_params = self.epi_to_underlying_arguments(epilogue_args)
725
  varlen_params = VarlenManager.to_underlying_arguments(varlen_args)
726
 
 
 
 
 
 
 
 
 
 
 
727
  TileSchedulerCls = self.get_scheduler_class(varlen_m=varlen_m)
728
  tile_sched_args = self.get_scheduler_arguments(
729
  mA, mB, mD, scheduler_args, varlen_args, epilogue_args
@@ -750,19 +816,21 @@ class GemmSm100(GemmSm90):
750
  self.cta_tile_shape_mnk[0] if varlen_m else self.cta_tile_shape_mnk[2]
751
  )
752
 
753
- # Define shared storage for kernel
754
- @cute.struct
 
 
 
 
 
 
 
 
 
755
  class SharedStorage:
756
- ab_pipeline_array_ptr: cute.struct.MemRange[cutlass.Int64, self.ab_stage * 2]
757
- epi_pipeline_array_ptr: cute.struct.MemRange[cutlass.Int64, self.epi_c_stage * 2]
758
- acc_pipeline_array_ptr: cute.struct.MemRange[cutlass.Int64, self.num_acc_stage * 2]
759
- sched_pipeline_array_ptr: cute.struct.MemRange[cutlass.Int64, self.sched_stage * 2]
760
- a_prefetch_pipeline_array_ptr: cute.struct.MemRange[
761
- cutlass.Int64, self.a_prefetch_stage * 2
762
  ]
763
- sched_data: cute.struct.MemRange[Int32, self.sched_stage * 12]
764
- tmem_dealloc_mbar_ptr: cutlass.Int64
765
- tmem_holding_buf: Int32
766
  sAIdx: cute.struct.Align[cute.struct.MemRange[Int32, a_idx_smem_size], 16]
767
  # (EPI_TILE_M, EPI_TILE_N, STAGE)
768
  sD: cute.struct.Align[
@@ -831,7 +899,6 @@ class GemmSm100(GemmSm90):
831
  self.epi_tile,
832
  tile_sched_params,
833
  TileSchedulerCls,
834
- trace_ptr,
835
  ).launch(
836
  grid=grid,
837
  block=[self.threads_per_cta, 1, 1],
@@ -874,16 +941,11 @@ class GemmSm100(GemmSm90):
874
  epi_tile: cute.Tile,
875
  tile_sched_params,
876
  TileSchedulerCls: cutlass.Constexpr[Callable],
877
- trace_ptr: Optional[cutlass.Int64] = None,
878
  ):
879
  """
880
  GPU device kernel performing the Persistent batched GEMM computation.
881
  """
882
 
883
- from .trace import TraceContext
884
-
885
- tctx = TraceContext.create(trace_ptr)
886
-
887
  varlen_m = const_expr(varlen_params.cu_seqlens_m is not None)
888
  varlen_k = const_expr(varlen_params.cu_seqlens_k is not None)
889
  assert not (varlen_m and varlen_k)
@@ -891,6 +953,7 @@ class GemmSm100(GemmSm90):
891
  assert varlen_m or varlen_k
892
  has_D = const_expr(mD_mnl is not None)
893
  has_C = const_expr(mC_mnl is not None)
 
894
 
895
  warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
896
 
@@ -920,39 +983,26 @@ class GemmSm100(GemmSm90):
920
 
921
  # Alloc and init: a+b full/empty, accumulator full/empty, tensor memory dealloc barrier
922
  smem = cutlass.utils.SmemAllocator()
923
- storage = smem.allocate(self.shared_storage)
924
 
925
  # Initialize pipelines and states
926
  ab_pipeline = self.make_ab_pipeline(
927
  tiled_mma=tiled_mma,
928
  cluster_layout_vmnk=cluster_layout_vmnk,
929
- ab_pipeline_mbar_ptr=storage.ab_pipeline_array_ptr.data_ptr(),
930
  is_leader_cta=is_leader_cta,
931
  )
932
  epi_pipeline = None
933
- if const_expr(has_C):
934
- epi_pipeline = self.make_epi_pipeline(
935
- c_smem_layout=cute.slice_(epi_c_smem_layout, (None, None, 0)),
936
- epi_pipeline_mbar_ptr=storage.epi_pipeline_array_ptr.data_ptr(),
937
- )
938
- acc_pipeline = self.make_acc_pipeline(
939
- cluster_layout_vmnk=cluster_layout_vmnk,
940
- acc_pipeline_mbar_ptr=storage.acc_pipeline_array_ptr.data_ptr(),
941
- )
942
  sched_pipeline = None
943
  sched_data = None
944
  if const_expr(self.is_persistent):
945
- sched_pipeline = self.make_sched_pipeline(
946
- self.cluster_shape_mnk,
947
- sched_pipeline_mbar_ptr=storage.sched_pipeline_array_ptr.data_ptr(),
948
- has_C=has_C,
949
- )
950
- sched_data = storage.sched_data.get_tensor((12, self.sched_stage))
951
  a_prefetch_pipeline = None
952
  if const_expr(self.gather_A):
953
- a_prefetch_pipeline = self.make_a_prefetch_pipeline(
954
- storage.a_prefetch_pipeline_array_ptr.data_ptr(),
955
- )
956
 
957
  tmem_alloc_barrier = pipeline.NamedBarrier(
958
  barrier_id=int(NamedBarrierGemm.TmemPtr),
@@ -960,11 +1010,9 @@ class GemmSm100(GemmSm90):
960
  )
961
  # Tensor memory dealloc barrier init
962
  tmem = cutlass.utils.TmemAllocator(
963
- storage.tmem_holding_buf,
964
  barrier_for_retrieve=tmem_alloc_barrier,
965
  allocator_warp_id=self.epilog_warp_id[0],
966
  is_two_cta=use_2cta_instrs,
967
- two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar_ptr,
968
  )
969
 
970
  # Cluster arrive after barrier init
@@ -1020,13 +1068,18 @@ class GemmSm100(GemmSm90):
1020
  )
1021
 
1022
  TileSchedulerCls = partial(
1023
- TileSchedulerCls.create, tile_sched_params, sched_data, sched_pipeline
 
 
 
 
1024
  )
1025
 
1026
  epi_load_barrier = None
1027
- if const_expr(has_C):
1028
  epi_load_barrier = pipeline.NamedBarrier(
1029
- barrier_id=int(NamedBarrierGemm.EpilogueLoad), num_threads=2 * cute.arch.WARP_SIZE
 
1030
  )
1031
 
1032
  # Cluster wait before tensor memory alloc
@@ -1042,30 +1095,6 @@ class GemmSm100(GemmSm90):
1042
  cute.arch.griddepcontrol_wait()
1043
  if const_expr(self.gather_A):
1044
  cute.arch.setmaxregister_decrease(self.num_regs_other)
1045
- # Compute multicast mask for A/B buffer full
1046
- block_in_cluster_coord_vmnk = cluster_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
1047
- block_in_cluster_coord_sfb_vmnk = None
1048
- if const_expr(self.blockscaled):
1049
- block_in_cluster_coord_sfb_vmnk = cluster_layout_sfb_vmnk.get_flat_coord(
1050
- cta_rank_in_cluster
1051
- )
1052
- a_mcast_mask, b_mcast_mask = None, None
1053
- sfa_mcast_mask, sfb_mcast_mask = None, None
1054
- if const_expr(self.is_a_mcast or self.is_b_mcast or use_2cta_instrs):
1055
- a_mcast_mask = cpasync.create_tma_multicast_mask(
1056
- cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=2
1057
- )
1058
- b_mcast_mask = cpasync.create_tma_multicast_mask(
1059
- cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=1
1060
- )
1061
- if const_expr(self.blockscaled):
1062
- sfa_mcast_mask = cpasync.create_tma_multicast_mask(
1063
- cluster_layout_vmnk, block_in_cluster_coord_vmnk, mcast_mode=2
1064
- )
1065
- sfb_mcast_mask = cpasync.create_tma_multicast_mask(
1066
- cluster_layout_sfb_vmnk, block_in_cluster_coord_sfb_vmnk, mcast_mode=1
1067
- )
1068
-
1069
  # Persistent tile scheduling loop
1070
  tile_scheduler = TileSchedulerCls()
1071
  work_tile = tile_scheduler.initial_work_tile_info()
@@ -1076,7 +1105,16 @@ class GemmSm100(GemmSm90):
1076
  pipeline.PipelineUserType.Consumer, self.a_prefetch_stage
1077
  )
1078
  do_epi_load_barrier_arrive = Boolean(True)
 
 
 
 
 
 
 
 
1079
  while work_tile.is_valid_tile:
 
1080
  tile_coord_mnkl = work_tile.tile_idx
1081
  batch_idx = tile_coord_mnkl[3]
1082
  # Local_tile partition global tensors
@@ -1102,7 +1140,7 @@ class GemmSm100(GemmSm90):
1102
  )
1103
  if const_expr(self.blockscaled):
1104
  # (bM, bK)
1105
- # SFA uses padded per-expert offset (dQaccum format), not
1106
  # the A-data offset — allows varlen_m seqlens that aren't
1107
  # multiples of 128.
1108
  gSFA_mkl = cute.local_tile(
@@ -1111,38 +1149,36 @@ class GemmSm100(GemmSm90):
1111
  (mma_tile_coord_mnl[0], None),
1112
  )
1113
  # (bN, bK)
1114
- # SFB uses padded per-expert K offset in varlen_k (dQaccum format).
 
 
1115
  gSFB_nkl = cute.local_tile(
1116
  varlen_manager.offset_batch_SFB(mSFB_nkl, batch_idx),
1117
  cute.select(self.mma_tiler_sfb, [1, 2]),
1118
- (
1119
- (
1120
- mma_tile_coord_mnl[1] // 2
1121
- if self.cta_tile_shape_mnk[1] == 64
1122
- else mma_tile_coord_mnl[1]
1123
- ),
1124
- None,
1125
- ),
1126
  )
1127
 
1128
  # Partition global tensor for TiledMMA_A/B/D
1129
  # Then partition global/shared tensor for TMA load A/B
1130
  len_k = varlen_manager.len_k(batch_idx)
1131
- # TMA load A partition_S/D
1132
- a_cta_layout = cute.make_layout(
1133
- cute.slice_(cluster_layout_vmnk, (0, 0, None, 0)).shape
1134
- )
 
 
 
 
 
 
 
 
1135
  copy_A, prefetch_A = None, None
1136
  if const_expr(not self.gather_A):
1137
  # (MMA, MMA_M, MMA_K, RestK)
1138
  tCgA = thr_mma.partition_A(gA_mk)
1139
- copy_A, _, _ = copy_utils.tma_get_copy_fn(
1140
- tma_atom_a,
1141
- cta_coord=block_in_cluster_coord_vmnk[2],
1142
- cta_layout=a_cta_layout,
1143
- src_tensor=tCgA,
1144
- dst_tensor=sA,
1145
- mcast_mask=a_mcast_mask,
1146
  )
1147
  else:
1148
  # For varlen_m paths (TMA or cp.async): consume indices from
@@ -1162,9 +1198,7 @@ class GemmSm100(GemmSm90):
1162
  warp_idx,
1163
  )
1164
  if const_expr(varlen_m):
1165
- cute.arch.sync_warp()
1166
- with cute.arch.elect_one():
1167
- a_prefetch_pipeline.consumer_release(a_prefetch_consumer_state)
1168
  a_prefetch_consumer_state.advance()
1169
  if const_expr(prefetch_A is not None):
1170
  prefetch_A = partial(prefetch_A, a_prefetch_pipeline)
@@ -1176,52 +1210,33 @@ class GemmSm100(GemmSm90):
1176
  # (MMA, MMA_N, MMA_K)
1177
  tCgSFB = thr_mma_sfb.partition_B(gSFB_nkl)
1178
  # TMA load B partition_S/D
1179
- copy_B, _, _ = copy_utils.tma_get_copy_fn(
1180
- tma_atom_b,
1181
- cta_coord=block_in_cluster_coord_vmnk[1],
1182
- cta_layout=cute.make_layout(
1183
- cute.slice_(cluster_layout_vmnk, (0, None, 0, 0)).shape
1184
- ),
1185
- src_tensor=tCgB,
1186
- dst_tensor=sB,
1187
- mcast_mask=b_mcast_mask,
1188
  )
1189
  copy_SFA, copy_SFB = None, None
1190
  if const_expr(self.blockscaled):
1191
  # TMA load SFA partition_S/D
1192
- copy_SFA, _, _ = copy_utils.tma_get_copy_fn(
1193
  tma_atom_sfa,
1194
- cta_coord=block_in_cluster_coord_vmnk[2],
1195
- cta_layout=a_cta_layout,
1196
  src_tensor=tCgSFA,
1197
  dst_tensor=sSFA,
1198
- filter_zeros=True,
1199
- mcast_mask=sfa_mcast_mask,
1200
  )
1201
  # TMA load SFB partition_S/D
1202
- sfb_cta_layout = cute.make_layout(
1203
- cute.slice_(cluster_layout_sfb_vmnk, (0, None, 0, 0)).shape
1204
- )
1205
- copy_SFB, _, _ = copy_utils.tma_get_copy_fn(
1206
  tma_atom_sfb,
1207
- cta_coord=block_in_cluster_coord_sfb_vmnk[1],
1208
- cta_layout=sfb_cta_layout,
1209
  src_tensor=tCgSFB,
1210
  dst_tensor=sSFB,
1211
- filter_zeros=True,
1212
- mcast_mask=sfb_mcast_mask,
1213
  )
1214
  k_tile_cnt = cute.ceil_div(len_k, self.cta_tile_shape_mnk[2])
1215
- tctx.b("tma_load")
1216
  if const_expr(not self.gather_A):
1217
- ab_producer_state = self.load_AB(
1218
  ab_pipeline,
1219
  ab_producer_state,
1220
- copy_A,
1221
- copy_B,
1222
  k_tile_cnt,
1223
- copy_SFA,
1224
- copy_SFB,
1225
  )
1226
  elif const_expr(self.use_tma_gather):
1227
  ab_producer_state, a_prefetch_consumer_state = self.load_AB_tma_gather(
@@ -1243,7 +1258,7 @@ class GemmSm100(GemmSm90):
1243
  copy_B,
1244
  k_tile_cnt,
1245
  )
1246
- tctx.e("tma_load")
1247
  if const_expr(epi_load_barrier is not None):
1248
  # In the first work tile, the epi load warp will wait for the signal
1249
  # from the mainloop load warp to start loading C, to avoid interfering
@@ -1252,8 +1267,10 @@ class GemmSm100(GemmSm90):
1252
  epi_load_barrier.arrive()
1253
  do_epi_load_barrier_arrive = Boolean(False)
1254
  # Advance to next tile
 
1255
  tile_scheduler.advance_to_next_work()
1256
  work_tile = tile_scheduler.get_current_work()
 
1257
  # Wait A/B buffer empty
1258
  if warp_idx == self.ab_load_warp_id:
1259
  ab_pipeline.producer_tail(ab_producer_state)
@@ -1274,11 +1291,19 @@ class GemmSm100(GemmSm90):
1274
  work_tile = tile_scheduler.initial_work_tile_info()
1275
  while work_tile.is_valid_tile:
1276
  # Advance to next tile
 
1277
  tile_scheduler.advance_to_next_work(is_scheduler_warp=is_scheduler_warp)
 
 
1278
  work_tile = tile_scheduler.get_current_work()
 
1279
  # End of persistent scheduler loop
1280
  if is_scheduler_warp:
1281
  tile_scheduler.producer_tail()
 
 
 
 
1282
 
1283
  # Specialized A-index prefetch warp (gather_A only)
1284
  if const_expr(self.gather_A):
@@ -1359,9 +1384,9 @@ class GemmSm100(GemmSm90):
1359
  if const_expr(self.gather_A):
1360
  cute.arch.setmaxregister_decrease(self.num_regs_other)
1361
  # PDL: wait for prior kernel before any C TMA loads (matches cutlass C++ epi_load)
1362
- if const_expr(self.use_pdl and mC_mnl is not None):
1363
  cute.arch.griddepcontrol_wait()
1364
- if const_expr(mC_mnl is not None):
1365
  epi_producer_state = pipeline.make_pipeline_state(
1366
  pipeline.PipelineUserType.Producer, self.epi_c_stage
1367
  )
@@ -1373,22 +1398,41 @@ class GemmSm100(GemmSm90):
1373
  # Get tile coord from tile scheduler
1374
  tile_coord_mnkl = work_tile.tile_idx
1375
  batch_idx = tile_coord_mnkl[3]
1376
- copy_C_fn, _, bGS_gC = self.epilog_gmem_copy_and_partition(
1377
- tma_atom_c,
1378
- varlen_manager.offset_batch_epi(mC_mnl, batch_idx),
1379
- self.cta_tile_shape_mnk[:2],
1380
- epi_tile,
1381
- sC,
 
 
 
 
 
 
 
 
1382
  tile_coord_mnkl,
 
 
 
 
 
1383
  )
1384
- copy_C = copy_utils.tma_producer_copy_fn(copy_C_fn, epi_pipeline)
1385
  if do_epi_load_barrier_wait:
1386
  epi_load_barrier.arrive_and_wait()
1387
  do_epi_load_barrier_wait = Boolean(False)
1388
- epi_tile_num = const_expr(cute.size(bGS_gC, mode=[1]))
 
 
 
 
 
 
 
1389
  for epi_idx in cutlass.range(epi_tile_num, unroll=1):
1390
  epi_pipeline.producer_acquire(epi_producer_state)
1391
- copy_C(src_idx=epi_idx, producer_state=epi_producer_state)
1392
  # Epi pipeline's producer commit is a NOP
1393
  epi_pipeline.producer_commit(epi_producer_state)
1394
  epi_producer_state.advance()
@@ -1446,21 +1490,8 @@ class GemmSm100(GemmSm90):
1446
  cute.slice_(sfb_smem_layout, (None, None, None, 0)),
1447
  )
1448
  tCtSFB = cute.make_tensor(sfb_tmem_ptr, tCtSFB_layout)
1449
- # Partition for S2T copy of SFA/SFB
1450
- (
1451
- tiled_copy_s2t_sfa,
1452
- tCsSFA_compact_s2t,
1453
- tCtSFA_compact_s2t,
1454
- ) = self.mainloop_s2t_copy_and_partition(sSFA, tCtSFA)
1455
- (
1456
- tiled_copy_s2t_sfb,
1457
- tCsSFB_compact_s2t,
1458
- tCtSFB_compact_s2t,
1459
- ) = self.mainloop_s2t_copy_and_partition(sSFB, tCtSFB)
1460
  else:
1461
  tCtSFA, tCtSFB = None, None
1462
- tiled_copy_s2t_sfa, tCsSFA_compact_s2t, tCtSFA_compact_s2t = None, None, None
1463
- tiled_copy_s2t_sfb, tCsSFB_compact_s2t, tCtSFB_compact_s2t = None, None, None
1464
 
1465
  # Persistent tile scheduling loop
1466
  tile_scheduler = TileSchedulerCls()
@@ -1486,7 +1517,10 @@ class GemmSm100(GemmSm90):
1486
  )
1487
  tCtAcc = tCtAcc_base[None, None, None, acc_stage_idx]
1488
  tCtSFB_mma = tCtSFB
1489
- if const_expr(self.blockscaled and self.mma_inst_shape_mnk[1] in (64, 192)):
 
 
 
1490
  tCtSFB_mma = cute.make_tensor(
1491
  cute.recast_ptr(
1492
  sfb_tmem_base_ptr + Int32((tile_coord_mnkl[1] % 2) * 2),
@@ -1494,7 +1528,25 @@ class GemmSm100(GemmSm90):
1494
  ),
1495
  tCtSFB.layout,
1496
  )
1497
- tctx.b("mma")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1498
  ab_consumer_state, acc_producer_state, tiled_mma = self.mma(
1499
  ab_pipeline,
1500
  acc_pipeline,
@@ -1509,17 +1561,14 @@ class GemmSm100(GemmSm90):
1509
  cta_rank_in_cluster,
1510
  tCtSFA,
1511
  tCtSFB_mma,
1512
- tiled_copy_s2t_sfa,
1513
- tiled_copy_s2t_sfb,
1514
- tCsSFA_compact_s2t,
1515
- tCsSFB_compact_s2t,
1516
- tCtSFA_compact_s2t,
1517
- tCtSFB_compact_s2t,
1518
  )
1519
  if const_expr(self.overlap_accum_sf):
1520
  # After iter 0, 2, ..., shift tmem ptr by -256.
1521
  # After iter 1, 3, ..., shift tmem ptr by 256.
1522
- tCtSFA, tCtSFB, tCtSFA_compact_s2t, tCtSFB_compact_s2t = [
1523
  cute.make_tensor(
1524
  cute.recast_ptr(
1525
  # Doing tmem ptr arithmetic requires 32-bit type, wrong otherwise
@@ -1532,9 +1581,9 @@ class GemmSm100(GemmSm90):
1532
  ),
1533
  mT.layout,
1534
  )
1535
- for mT in [tCtSFA, tCtSFB, tCtSFA_compact_s2t, tCtSFB_compact_s2t]
1536
  ]
1537
- tctx.e("mma")
1538
  # Advance to next tile
1539
  tile_scheduler.advance_to_next_work()
1540
  work_tile = tile_scheduler.get_current_work()
@@ -1592,6 +1641,13 @@ class GemmSm100(GemmSm90):
1592
  pipeline.PipelineUserType.Consumer, self.epi_c_stage
1593
  )
1594
  while work_tile.is_valid_tile:
 
 
 
 
 
 
 
1595
  # Get tile coord from tile scheduler
1596
  tile_coord_mnkl = work_tile.tile_idx
1597
  batch_idx = tile_coord_mnkl[3]
@@ -1635,10 +1691,10 @@ class GemmSm100(GemmSm90):
1635
  acc_release_idx=self.iter_acc_early_release
1636
  if const_expr(self.overlap_accum_sf)
1637
  else epi_tile_num - 1,
1638
- clear_acc=varlen_k and k_len == 0,
1639
  )
1640
 
1641
- tctx.b("epilogue")
1642
  epi_read_state, _ = self.epilogue(
1643
  epilogue_params,
1644
  epi_smem_tensors,
@@ -1667,11 +1723,11 @@ class GemmSm100(GemmSm90):
1667
  )
1668
  # acc_pipeline.consumer_release was already called in self.epi_load_acc_subtile
1669
  acc_consumer_state.advance()
1670
- tctx.e("epilogue")
1671
 
1672
  # Advance to next tile
1673
  tile_scheduler.advance_to_next_work()
1674
- work_tile = tile_scheduler.get_current_work()
1675
 
1676
  # Wait for D store complete
1677
  if is_tma_warp:
@@ -1682,8 +1738,6 @@ class GemmSm100(GemmSm90):
1682
  tmem_alloc_barrier.arrive_and_wait()
1683
  tmem.free(acc_tmem_ptr)
1684
 
1685
- tctx.flush()
1686
-
1687
  @cute.jit
1688
  def _make_gather_A_copy(
1689
  self,
@@ -1876,19 +1930,15 @@ class GemmSm100(GemmSm90):
1876
  cta_rank_in_cluster: Int32,
1877
  tCtSFA: Optional[cute.Tensor] = None,
1878
  tCtSFB: Optional[cute.Tensor] = None,
1879
- tiled_copy_s2t_sfa: Optional[cute.TiledCopy] = None,
1880
- tiled_copy_s2t_sfb: Optional[cute.TiledCopy] = None,
1881
- tCsSFA_compact_s2t: Optional[cute.Tensor] = None,
1882
- tCsSFB_compact_s2t: Optional[cute.Tensor] = None,
1883
- tCtSFA_compact_s2t: Optional[cute.Tensor] = None,
1884
- tCtSFB_compact_s2t: Optional[cute.Tensor] = None,
1885
  ) -> Tuple[cutlass.pipeline.PipelineState, cutlass.pipeline.PipelineState, cute.TiledMma]:
1886
- blockscaled = const_expr(tiled_copy_s2t_sfa is not None)
1887
  if const_expr(blockscaled):
1888
  assert all(x is not None for x in (tCtSFA, tCtSFB))
1889
- assert all(x is not None for x in (tiled_copy_s2t_sfa, tiled_copy_s2t_sfb))
1890
- assert all(x is not None for x in (tCsSFA_compact_s2t, tCsSFB_compact_s2t))
1891
- assert all(x is not None for x in (tCtSFA_compact_s2t, tCtSFB_compact_s2t))
1892
  # If gather_A and use_2cta_instrs, the cp.async for the non-leader CTA will
1893
  # arrive at an mbarrier on the non-leader CTA side, then the mma warp of the non-leader
1894
  # CTA will wait for that then arrive at the mbarrier on the leader CTA.
@@ -1911,29 +1961,67 @@ class GemmSm100(GemmSm90):
1911
  if not is_leader_cta:
1912
  ab_pipeline.consumer_wait(ab_consumer_state, peek_ab_full_status)
1913
  with cute.arch.elect_one():
1914
- # The odd CTA signals the even CTA
1915
- ab_pipeline.sync_object_full.arrive_mbarrier(
1916
- ab_consumer_state.index, dst_rank=cta_rank_in_cluster & 0xFE
 
 
 
 
 
1917
  )
1918
  if is_leader_cta:
1919
  # Conditionally wait for AB buffer full
1920
  ab_pipeline.consumer_wait(ab_consumer_state, peek_ab_full_status)
 
 
 
 
 
 
 
 
1921
  # Copy SFA/SFB from smem to tmem
1922
  if const_expr(blockscaled):
1923
- s2t_stage_coord = (None, None, None, None, ab_consumer_state.index)
1924
- tCsSFA_compact_s2t_staged = tCsSFA_compact_s2t[s2t_stage_coord]
1925
- tCsSFB_compact_s2t_staged = tCsSFB_compact_s2t[s2t_stage_coord]
1926
- cute.copy(tiled_copy_s2t_sfa, tCsSFA_compact_s2t_staged, tCtSFA_compact_s2t)
1927
- cute.copy(tiled_copy_s2t_sfb, tCsSFB_compact_s2t_staged, tCtSFB_compact_s2t)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1928
  for k_blk_idx in cutlass.range(num_k_blocks, unroll_full=True):
1929
  k_blk_coord = (None, None, k_blk_idx, ab_consumer_state.index)
1930
  if const_expr(blockscaled):
1931
  # Set SFA/SFB tensor to tiled_mma
1932
  sf_kblock_coord = (None, None, k_blk_idx)
1933
- tiled_mma.set(tcgen05.Field.SFA, tCtSFA[sf_kblock_coord].iterator)
1934
- tiled_mma.set(tcgen05.Field.SFB, tCtSFB[sf_kblock_coord].iterator)
1935
- cute.gemm(tiled_mma, acc, tCrA[k_blk_coord], tCrB[k_blk_coord], acc)
1936
- tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
 
 
 
 
 
 
 
 
 
 
1937
  # Async arrive AB buffer empty
1938
  ab_pipeline.consumer_release(ab_consumer_state)
1939
  ab_consumer_state.advance()
@@ -1957,7 +2045,7 @@ class GemmSm100(GemmSm90):
1957
  tTR_tAcc: cute.Tensor,
1958
  tTR_rAcc: cute.Tensor,
1959
  tRS_rD: cute.Tensor,
1960
- epi_idx: int,
1961
  acc_pipeline: pipeline.PipelineAsync,
1962
  acc_consumer_state: pipeline.PipelineState,
1963
  acc_release_idx: int,
@@ -1965,50 +2053,15 @@ class GemmSm100(GemmSm90):
1965
  ):
1966
  if not clear_acc:
1967
  # Load accumulator from tensor memory buffer to register
1968
- cute.copy(tiled_copy_t2r, tTR_tAcc[None, None, None, epi_idx], tTR_rAcc)
1969
  tRS_rAcc = tiled_copy_r2s.retile(tTR_rAcc)
1970
  tRS_rD.store(tRS_rAcc.load())
1971
  else:
1972
  tRS_rD.fill(0.0)
1973
- if epi_idx == acc_release_idx:
 
1974
  cute.arch.fence_view_async_tmem_load()
1975
- with cute.arch.elect_one():
1976
- acc_pipeline.consumer_release(acc_consumer_state)
1977
-
1978
- def mainloop_s2t_copy_and_partition(
1979
- self,
1980
- sSF: cute.Tensor,
1981
- tSF: cute.Tensor,
1982
- ) -> Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]:
1983
- """
1984
- Make tiledCopy for smem to tmem load for scale factor tensor, then use it to partition smem memory (source) and tensor memory (destination).
1985
-
1986
- :param sSF: The scale factor tensor in smem
1987
- :type sSF: cute.Tensor
1988
- :param tSF: The scale factor tensor in tmem
1989
- :type tSF: cute.Tensor
1990
-
1991
- :return: A tuple containing (tiled_copy_s2t, tCsSF_compact_s2t, tCtSF_compact_s2t) where:
1992
- - tiled_copy_s2t: The tiled copy operation for smem to tmem load for scale factor tensor(s2t)
1993
- - tCsSF_compact_s2t: The partitioned scale factor tensor in smem
1994
- - tSF_compact_s2t: The partitioned scale factor tensor in tmem
1995
- :rtype: Tuple[cute.TiledCopy, cute.Tensor, cute.Tensor]
1996
- """
1997
- # (MMA, MMA_MN, MMA_K, STAGE)
1998
- tCsSF_compact = cute.filter_zeros(sSF)
1999
- # (MMA, MMA_MN, MMA_K)
2000
- tCtSF_compact = cute.filter_zeros(tSF)
2001
- # Make S2T CopyAtom and tiledCopy
2002
- copy_atom_s2t = cute.make_copy_atom(tcgen05.Cp4x32x128bOp(self.cta_group), self.sf_dtype)
2003
- tiled_copy_s2t = tcgen05.make_s2t_copy(copy_atom_s2t, tCtSF_compact)
2004
- thr_copy_s2t = tiled_copy_s2t.get_slice(0)
2005
- # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
2006
- tCsSF_compact_s2t_ = thr_copy_s2t.partition_S(tCsSF_compact)
2007
- # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K, STAGE)
2008
- tCsSF_compact_s2t = tcgen05.get_s2t_smem_desc_tensor(tiled_copy_s2t, tCsSF_compact_s2t_)
2009
- # ((ATOM_V, REST_V), Rest_Tiler, MMA_MN, MMA_K)
2010
- tCtSF_compact_s2t = thr_copy_s2t.partition_D(tCtSF_compact)
2011
- return tiled_copy_s2t, tCsSF_compact_s2t, tCtSF_compact_s2t
2012
 
2013
  def epilog_tmem_copy_and_partition(
2014
  self,
@@ -2142,7 +2195,6 @@ class GemmSm100(GemmSm90):
2142
  self,
2143
  tiled_mma: cute.TiledMma,
2144
  cluster_layout_vmnk: cute.Layout,
2145
- ab_pipeline_mbar_ptr: cute.Pointer,
2146
  is_leader_cta: Boolean,
2147
  ) -> pipeline.PipelineAsync:
2148
  # If gather_A and use_2cta_instrs, the cp.async for the non-leader CTA will
@@ -2155,9 +2207,11 @@ class GemmSm100(GemmSm90):
2155
  if const_expr(not self.gather_A or self.use_tma_gather):
2156
  producer_cnt = 1
2157
  else:
2158
- producer_cnt = self.num_ab_load_warps * 32 + (
2159
- 1 if const_expr(not self.use_2cta_instrs) else 2
2160
- )
 
 
2161
  ab_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread, producer_cnt)
2162
  # Each warp will contribute to the arrive count with the number of mcast size
2163
  mcast_size = self.num_mcast_ctas_a + self.num_mcast_ctas_b - 1
@@ -2165,19 +2219,8 @@ class GemmSm100(GemmSm90):
2165
  ab_pipeline_consumer_group = pipeline.CooperativeGroup(
2166
  pipeline.Agent.Thread, consumer_arrive_cnt
2167
  )
2168
- if const_expr(not self.gather_A):
2169
- pipeline_ab = pipeline.PipelineTmaUmma.create(
2170
- barrier_storage=ab_pipeline_mbar_ptr,
2171
- num_stages=self.ab_stage,
2172
- producer_group=ab_pipeline_producer_group,
2173
- consumer_group=ab_pipeline_consumer_group,
2174
- tx_count=self.num_tma_load_bytes,
2175
- cta_layout_vmnk=cluster_layout_vmnk,
2176
- defer_sync=True,
2177
- )
2178
- elif const_expr(self.use_tma_gather):
2179
  pipeline_ab = PipelineTmaUmma.create(
2180
- barrier_storage=ab_pipeline_mbar_ptr,
2181
  num_stages=self.ab_stage,
2182
  producer_group=ab_pipeline_producer_group,
2183
  consumer_group=ab_pipeline_consumer_group,
@@ -2187,40 +2230,35 @@ class GemmSm100(GemmSm90):
2187
  )
2188
  else:
2189
  pipeline_ab = PipelineTmaCpAsyncUmma.create(
2190
- barrier_storage=ab_pipeline_mbar_ptr,
2191
  num_stages=self.ab_stage,
2192
  producer_group=ab_pipeline_producer_group,
2193
  consumer_group=ab_pipeline_consumer_group,
2194
  tx_count=self.num_tma_load_bytes,
2195
  cta_layout_vmnk=cluster_layout_vmnk,
2196
- producer_drop_count=None
2197
- if not self.use_2cta_instrs
2198
- else (2 if not is_leader_cta else 0),
2199
  defer_sync=True,
2200
  )
2201
  return pipeline_ab
2202
 
2203
- def make_acc_pipeline(
2204
- self, cluster_layout_vmnk: cute.Layout, acc_pipeline_mbar_ptr: cute.Pointer
2205
- ) -> pipeline.PipelineAsync:
2206
  acc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
2207
  num_acc_consumer_threads = self.num_epi_warps * (2 if self.use_2cta_instrs else 1)
2208
  acc_pipeline_consumer_group = pipeline.CooperativeGroup(
2209
  pipeline.Agent.Thread, num_acc_consumer_threads
2210
  )
2211
- return pipeline.PipelineUmmaAsync.create(
2212
- barrier_storage=acc_pipeline_mbar_ptr,
2213
  num_stages=self.num_acc_stage,
2214
  producer_group=acc_pipeline_producer_group,
2215
  consumer_group=acc_pipeline_consumer_group,
2216
  cta_layout_vmnk=cluster_layout_vmnk,
2217
  defer_sync=True,
 
 
 
2218
  )
2219
 
2220
  def make_sched_pipeline(
2221
  self,
2222
  cluster_layout_mnk: cute.Layout,
2223
- sched_pipeline_mbar_ptr: cute.Pointer,
2224
  has_C: bool = False,
2225
  ) -> pipeline.PipelineAsync:
2226
  # Threads/warps participating in this pipeline
@@ -2237,32 +2275,40 @@ class GemmSm100(GemmSm90):
2237
  sched_pipeline_consumer_group = pipeline.CooperativeGroup(
2238
  pipeline.Agent.Thread, consumer_arrive_cnt
2239
  )
2240
- return pipeline.PipelineAsync.create(
2241
- barrier_storage=sched_pipeline_mbar_ptr,
 
 
 
 
 
 
2242
  num_stages=self.sched_stage,
2243
  producer_group=sched_pipeline_producer_group,
2244
  consumer_group=sched_pipeline_consumer_group,
2245
  # If there's cluster, the consumers must arrive at the mbar of CTA 0 in the cluster.
2246
  consumer_mask=None if const_expr(cluster_size == 1) else 0,
2247
  defer_sync=True,
 
 
 
2248
  )
2249
 
2250
  @cute.jit
2251
- def make_a_prefetch_pipeline(
2252
- self, a_prefetch_pipeline_mbar_ptr: cute.Pointer
2253
- ) -> pipeline.PipelineAsync:
2254
  producer_cnt = 32
2255
  a_prefetch_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread, producer_cnt)
2256
  consumer_arrive_cnt = self.num_ab_load_warps
2257
  a_prefetch_consumer_group = pipeline.CooperativeGroup(
2258
  pipeline.Agent.Thread, consumer_arrive_cnt
2259
  )
2260
- return pipeline.PipelineCpAsync.create(
2261
- barrier_storage=a_prefetch_pipeline_mbar_ptr,
2262
  num_stages=self.a_prefetch_stage,
2263
  producer_group=a_prefetch_producer_group,
2264
  consumer_group=a_prefetch_consumer_group,
2265
  defer_sync=True,
 
 
2266
  )
2267
 
2268
  @classmethod
@@ -2284,6 +2330,7 @@ class GemmSm100(GemmSm90):
2284
  prefetch_A_idx: Literal[None, "varlen_m", "varlen_k"],
2285
  smem_capacity: int,
2286
  occupancy: int,
 
2287
  ) -> Tuple[int, int, int]:
2288
  """Computes the number of stages for A/B/C operands based on heuristics.
2289
 
@@ -2319,7 +2366,15 @@ class GemmSm100(GemmSm90):
2319
 
2320
  # Default D stages
2321
  epi_stage = 4 if cute.size(epi_tile[1]) <= 16 else 2
2322
- epi_c_stage = 0 if c_dtype is None else (4 if cute.size(epi_tile[1]) <= 16 else 2)
 
 
 
 
 
 
 
 
2323
 
2324
  # Calculate smem layout and size for one stage of A, B, and C
2325
  a_smem_layout_staged_one = sm100_utils.make_smem_layout_a(
@@ -2373,13 +2428,13 @@ class GemmSm100(GemmSm90):
2373
  d_bytes_per_stage = (
2374
  cute.size_in_bytes(d_dtype, d_smem_layout_staged_one) if d_dtype is not None else 0
2375
  )
2376
- epi_bytes_per_stage = d_bytes_per_stage + cls.epi_smem_bytes_per_stage(
2377
- epilogue_args, cta_tile_shape_mnk, epi_tile
2378
- )
2379
- epi_bytes = epi_bytes_per_stage * epi_stage
2380
  if const_expr(c_dtype is not None):
2381
  c_bytes_per_stage = cute.size_in_bytes(c_dtype, c_smem_layout_staged_one)
2382
  epi_bytes += c_bytes_per_stage * epi_c_stage
 
 
2383
 
2384
  # Calculate A/B/SFA/SFB stages:
2385
  # Start with total smem per CTA (capacity / occupancy)
@@ -2391,7 +2446,8 @@ class GemmSm100(GemmSm90):
2391
  # Refine epilogue stages:
2392
  # Calculate remaining smem after allocating for A/B stages and reserved bytes
2393
  # Add remaining unused smem to epilogue
2394
- epi_stage += (remaining_bytes - ab_bytes_per_stage * ab_stage) // (epi_bytes_per_stage)
 
2395
  return num_acc_stage, ab_stage, epi_stage, epi_c_stage
2396
 
2397
  @staticmethod
@@ -2553,15 +2609,15 @@ class GemmSm100(GemmSm90):
2553
 
2554
  @staticmethod
2555
  def is_valid_mma_tiler_and_cluster_shape(
2556
- mma_tiler_mn: Union[Tuple[int, int], Tuple[int, int, int]],
2557
  cluster_shape_mn: Tuple[int, int],
2558
  blockscaled: bool,
2559
  ) -> bool:
2560
  """
2561
  Check if the mma tiler and cluster shape are valid
2562
 
2563
- :param mma_tiler_mn: The (M, N) shape of the MMA instruction tiler
2564
- :type mma_tiler_mn: Tuple[int, int]
2565
  :param cluster_shape_mn: The (ClusterM, ClusterN) shape of the CTA cluster
2566
  :type cluster_shape_mn: Tuple[int, int]
2567
 
@@ -2571,20 +2627,20 @@ class GemmSm100(GemmSm90):
2571
  is_valid = True
2572
  # Skip invalid mma tile shape
2573
  if not blockscaled:
2574
- if mma_tiler_mn[0] not in [64, 128, 256]:
2575
  is_valid = False
2576
  else:
2577
- if mma_tiler_mn[0] not in [128, 256]:
2578
  is_valid = False
2579
- mma_inst_n = mma_tiler_mn[1] if mma_tiler_mn[1] <= 256 else mma_tiler_mn[1] // 2
2580
  if not blockscaled:
2581
  if mma_inst_n not in range(32, 257, 32):
2582
  is_valid = False
2583
  else:
2584
  # Blockscaled currently supports tile_n in {64, 128, 192, 256}.
2585
- if mma_tiler_mn[1] not in [64, 128, 192, 256]:
2586
  is_valid = False
2587
- if cluster_shape_mn[0] % (2 if mma_tiler_mn[0] == 256 else 1) != 0:
2588
  is_valid = False
2589
  # Skip invalid cluster shape
2590
  is_power_of_2 = lambda x: x > 0 and (x & (x - 1)) == 0
@@ -2662,7 +2718,7 @@ class GemmSm100(GemmSm90):
2662
  sf_dtype: Type[cutlass.Numeric],
2663
  sf_vec_size: int,
2664
  d_dtype: Type[cutlass.Numeric],
2665
- mma_tiler_mn: Union[Tuple[int, int], Tuple[int, int, int]],
2666
  cluster_shape_mn: Tuple[int, int],
2667
  m: int,
2668
  n: int,
@@ -2680,13 +2736,9 @@ class GemmSm100(GemmSm90):
2680
  if ab_dtype is cutlass.Float4E2M1FN and not (a_major == "k" and b_major == "k"):
2681
  can_implement = False
2682
  if not GemmSm100.is_valid_mma_tiler_and_cluster_shape(
2683
- mma_tiler_mn, cluster_shape_mn, blockscaled=True
2684
  ):
2685
  can_implement = False
2686
- # Multi-tile N iteration with an asymmetric SFB atom size needs the same
2687
- # kind of special-case layout rewriting as tile_n==192.
2688
- if mma_tiler_mn[1] == 224 and n > 224:
2689
- can_implement = False
2690
  if not GemmSm100.is_valid_tensor_alignment(
2691
  m, n, k, l, ab_dtype, d_dtype, a_major, b_major, d_major
2692
  ):
@@ -2698,7 +2750,7 @@ class GemmSm100(GemmSm90):
2698
  ab_dtype: Type[cutlass.Numeric],
2699
  acc_dtype: Type[cutlass.Numeric],
2700
  d_dtype: Type[cutlass.Numeric],
2701
- mma_tiler_mn: Union[Tuple[int, int], Tuple[int, int, int]],
2702
  cluster_shape_mn: Tuple[int, int],
2703
  m: int,
2704
  n: int,
@@ -2717,8 +2769,8 @@ class GemmSm100(GemmSm90):
2717
  :type acc_dtype: Type[cutlass.Numeric]
2718
  :param d_dtype: The data type of the output tensor
2719
  :type d_dtype: Type[cutlass.Numeric]
2720
- :param mma_tiler_mn: The (M, N) shape of the MMA instruction tiler
2721
- :type mma_tiler_mn: Tuple[int, int]
2722
  :param cluster_shape_mn: The (ClusterM, ClusterN) shape of the CTA cluster
2723
  :type cluster_shape_mn: Tuple[int, int]
2724
  :param m: The number of rows in the A tensor
@@ -2745,7 +2797,7 @@ class GemmSm100(GemmSm90):
2745
  can_implement = False
2746
  # Skip invalid mma tile shape and cluster shape
2747
  if not GemmSm100.is_valid_mma_tiler_and_cluster_shape(
2748
- mma_tiler_mn, cluster_shape_mn, blockscaled=False
2749
  ):
2750
  can_implement = False
2751
  # Skip illegal problem shape for load/store alignment
 
1
+ # Copyright (c) 2025-2026, QuACK team.
2
  # Based on the cute-dsl example:
3
  # https://github.com/NVIDIA/cutlass/blob/main/examples/python/CuTeDSL/blackwell/dense_gemm_persistent.py
4
 
 
23
  )
24
  from cutlass import Int32, Float32, Boolean, const_expr
25
  from cutlass.utils import LayoutEnum
26
+ from cutlass.cute.experimental import iket
27
 
28
+
29
+ from .pipeline import (
30
+ PipelineAsync as QuackPipelineAsync,
31
+ PipelineCpAsync,
32
+ PipelineTmaUmma,
33
+ PipelineTmaCpAsyncUmma,
34
+ PipelineUmmaAsync,
35
+ mbarrier_arrive_release_cluster,
36
+ mbarrier_acquire_cluster,
37
+ )
38
+ from .dsl.smem_struct import Reserved, partitioned_struct
39
  from .tile_scheduler import TileSchedulerOptions
40
  from .varlen_utils import VarlenArguments, VarlenManager
41
+ from .gemm_base import GemmTmaBase, NamedBarrierGemm
42
  from . import layout_utils
43
  from . import copy_utils as copy_utils
44
  from . import sm100_utils as quack_sm100_utils
 
75
  - Write accumulator to TMEM
76
  The accumulator in TMEM must then be loaded to registers before writing back to GMEM.
77
 
78
+ Constraints:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  * Supported input data types: fp16, bf16, tf32, int8, uint8, fp8 (e4m3fn, e5m2),
80
  see detailed valid dtype combinations in below GemmSm100 class documentation
81
  * A/B tensor must have the same data type
 
88
  """
89
 
90
 
91
+ class GemmSm100(GemmTmaBase):
92
  """This class implements batched matrix multiplication (C = A x B) with support for various data types
93
  and architectural features specific to Blackwell GPUs with persistent tile scheduling and warp specialization.
94
 
95
  :param acc_dtype: Data type for accumulation during computation
96
  :type acc_dtype: type[cutlass.Numeric]
97
+ :param mma_tiler_mnk: Shape of the MMA tile. Pass (M, N) to default K to
98
  4 MMA instructions, or (M, N, K) to set the K tile size explicitly.
99
+ :type mma_tiler_mnk: Union[Tuple[int, int], Tuple[int, int, int]]
100
  :param cluster_shape_mn: Cluster dimensions (M,N) for parallel processing
101
  :type cluster_shape_mn: Tuple[int, int]
102
 
 
130
  Example:
131
  >>> gemm = GemmSm100(
132
  ... acc_dtype=Float32,
133
+ ... mma_tiler_mnk=(128, 128),
134
  ... cluster_shape_mn=(2, 2)
135
  ... )
136
  >>> gemm(mA, mB, mD, max_active_clusters, stream)
 
138
 
139
  arch = 100
140
 
141
+ EpilogueArguments = GemmTmaBase.EpilogueArguments
142
+ EpilogueParams = GemmTmaBase.EpilogueParams
143
 
144
  def __init__(
145
  self,
146
  acc_dtype: Type[cutlass.Numeric],
147
  a_dtype: Type[cutlass.Numeric], # ignored for now
148
+ mma_tiler_mnk: Union[Tuple[int, int], Tuple[int, int, int]],
149
  cluster_shape_mnk: Tuple[int, int, int],
150
  sf_vec_size: Optional[int] = None,
151
  gather_A: bool = False,
 
160
 
161
  1. MMA Instruction Settings (tcgen05):
162
  - acc_dtype: Data types for MMA accumulator.
163
+ - mma_tiler_mnk: The (M, N) or (M, N, K) shape of the MMA instruction tiler.
164
  - use_2cta_instrs: Boolean indicating if the tcgen05 MMA variant
165
  with cta_group=2 should be used.
166
 
 
169
 
170
  :param acc_dtype: Data type of the accumulator.
171
  :type acc_dtype: type[cutlass.Numeric]
172
+ :param mma_tiler_mnk: (M, N) or (M, N, K) shape of the MMA tile.
173
  If only (M, N) is given, K defaults to 4 * instruction K.
174
+ :type mma_tiler_mnk: Union[Tuple[int, int], Tuple[int, int, int]]
175
  :param cluster_shape_mnk: Tuple (ClusterM, ClusterN) shape of the cluster.
176
  :type cluster_shape_mnk: Tuple[int, int]
177
  """
178
 
179
  self.acc_dtype: Type[cutlass.Numeric] = acc_dtype
180
+ self.sf_vec_size = sf_vec_size
181
+ self.blockscaled = sf_vec_size is not None
182
+ assert len(mma_tiler_mnk) in [2, 3], "MMA tiler must be (M, N) or (M, N, K)"
183
+ valid_2cta_m = (128, 256) if not self.blockscaled else (256,)
184
+ self.use_2cta_instrs = cluster_shape_mnk[0] % 2 == 0 and mma_tiler_mnk[0] in valid_2cta_m
185
  self.cluster_shape_mnk = cluster_shape_mnk
186
  assert cluster_shape_mnk[2] == 1, "Cluster shape K must be 1"
187
  # K dimension: if user provides 3 values, use their K; otherwise default in _setup_attributes
188
+ if len(mma_tiler_mnk) == 3:
189
+ self.mma_tiler = tuple(mma_tiler_mnk)
190
  else:
191
+ self.mma_tiler = (*mma_tiler_mnk, 0)
 
 
192
  self.is_persistent = True
 
193
  self.use_clc_persistence = use_clc_persistence
194
+ self.epi_m_major = True
195
  self.gather_A = gather_A
196
  self.concat_layout = concat_layout or ()
197
  self.use_tma_gather = use_tma_gather
 
220
  barrier_id=int(NamedBarrierGemm.Epilogue),
221
  num_threads=self.num_epi_warps * cute.arch.WARP_SIZE,
222
  )
223
+ # CLC throttle: paces query issue to tile consumption so the multi-stage
224
+ # lookahead can't over-cancel the pending pool. Producer = CTA0 load warp
225
+ # (arrive per tile started), consumer = CTA0 scheduler warp (sync per
226
+ # query); 2 warps => 64 threads. Lives here (not the scheduler) because a
227
+ # NamedBarrier id is a whole-CTA resource coordinated by NamedBarrierGemm,
228
+ # and the participating-thread count is arch-specific (warp layout). A
229
+ # single barrier suffices: the dependency chain commit(k+1) <- fetch(k+1)
230
+ # <- query(k+1) <- sync(k) forces strict producer/consumer alternation, so
231
+ # <= 1 credit is ever outstanding; bar.sync also gives a hardware wakeup vs
232
+ # an mbarrier pipeline's PHASECHK + NANOSLEEP polling.
233
+ self.clc_throttle_barrier = (
234
+ pipeline.NamedBarrier(
235
+ barrier_id=int(NamedBarrierGemm.ClcThrottle), num_threads=2 * cute.arch.WARP_SIZE
236
+ )
237
+ if self.use_clc_persistence
238
+ else None
239
+ )
240
  # Register reallocation for gather_A (3 warp groups, 504 regs total, 168 per WG default).
241
  # Heavy epilogues (e.g. colvec_reduce in DGated) override these to avoid register spilling.
242
  # Without gather_A there are only 2 WGs (512 total, 256 per WG = max), no reallocation needed.
 
258
  # Multiple of 4 warps to increase/decrease number of registers
259
  assert self.threads_per_cta % 128 == 0
260
 
261
+ def epi_smem_warp_shape_mnk(self):
262
+ # Mirrors cutlass.utils.blackwell_helpers.compute_epilogue_tile_shape:
263
+ # the epilogue tmem layout uses two M warps and two N warps when the
264
+ # per-CTA M tile is 64 and the kernel uses 2-CTA instructions.
265
+ warp_m, warp_n = (
266
+ (2, 2) if self.cta_tile_shape_mnk[0] == 64 and self.use_2cta_instrs else (4, 1)
267
+ )
268
+ return (warp_m, warp_n, 1)
269
+
270
  def _setup_attributes(self, epilogue_args: EpilogueArguments, varlen_args: VarlenArguments):
271
  """Set up configurations that are dependent on GEMM inputs
272
 
 
281
  - Computing A/B/C shared memory layout
282
  - Computing tensor memory allocation columns
283
  """
284
+ self.epi_m_major = self.resolve_epi_m_major(epilogue_args)
285
+
286
  # Compute mma instruction shapes
287
  mma_inst_bits_k = 256
288
  # (MMA_Tile_Shape_M, MMA_Tile_Shape_N, MMA_Inst_Shape_K)
 
303
  if const_expr(not self.blockscaled):
304
  self.tiled_mma = sm100_utils.make_trivial_tiled_mma(
305
  self.a_dtype,
306
+ self.b_dtype,
307
  self.a_major_mode,
308
  self.b_major_mode,
309
  self.acc_dtype,
 
314
  else:
315
  self.tiled_mma = sm100_utils.make_blockscaled_trivial_tiled_mma(
316
  self.a_dtype,
317
+ self.b_dtype,
318
  self.a_major_mode,
319
  self.b_major_mode,
320
  self.sf_dtype,
 
324
  )
325
  self.tiled_mma_sfb = sm100_utils.make_blockscaled_trivial_tiled_mma(
326
  self.a_dtype,
327
+ self.b_dtype,
328
  self.a_major_mode,
329
  self.b_major_mode,
330
  self.sf_dtype,
 
335
 
336
  # Compute mma/cluster/tile shapes
337
  if self.mma_tiler[2] > 0:
338
+ assert self.mma_tiler[2] % self.mma_inst_shape_mnk[2] == 0, (
339
+ f"MMA tiler K ({self.mma_tiler[2]}) must be divisible by "
340
+ f"MMA instruction K ({self.mma_inst_shape_mnk[2]})"
341
+ )
342
  mma_inst_tile_k = self.mma_tiler[2] // self.mma_inst_shape_mnk[2]
343
  else:
344
  mma_inst_tile_k = 4
 
366
  self.mma_tiler_sfb[1],
367
  self.mma_tiler_sfb[2],
368
  )
369
+ # The SF atom fixed by the tcgen05 MMA (BlockScaledBasicChunk) is 128 wide
370
+ # in N, but cta_tile_n need not be a multiple of 128. Two derived
371
+ # quantities localize all the resulting special handling:
372
+ # - sfb_tiles_per_atom: adjacent N-tiles that share one 128-wide atom
373
+ # (tile_n=64) load the same SFB atom; gmem N-tile coords are divided
374
+ # by this.
375
+ # - sfb_n_atom_misaligned: tile_n an odd multiple of 64 (64, 192) puts
376
+ # odd N-tiles 64 into an atom; the MMA's SFB tmem base shifts by 2
377
+ # columns for odd N-tile coords, and tile_n=192 additionally needs
378
+ # the overlapped-window TMA remap at the SFB TMA setup in __call__.
379
+ # tile_n=224 is rejected: its tiles start at 32-column offsets within
380
+ # the atom ((224*j) % 128 cycles 0/96/64/32), which neither mechanism
381
+ # covers.
382
+ assert self.cta_tile_shape_mnk[1] in (64, 128, 192, 256), (
383
+ f"blockscaled tile_n must be in (64, 128, 192, 256), "
384
+ f"got {self.cta_tile_shape_mnk[1]}"
385
+ )
386
+ self.sfb_tiles_per_atom = max(128 // self.cta_tile_shape_mnk[1], 1)
387
+ self.sfb_n_atom_misaligned = (self.cta_tile_shape_mnk[1] // 64) % 2 == 1
388
  else:
389
  self.cta_tile_shape_mnk_sfb = None
390
 
 
413
  self.is_sfb_mcast = self.num_mcast_ctas_sfb > 1
414
 
415
  # Compute epilogue subtile
416
+ tile_load_layout = None
417
+ tile_load_dtype = None
418
+ # If TileLoad exists without C, use the first non-None tile-load tensor as
419
+ # the C-like input for SM100's epilogue tile shape. Multiple TileLoads
420
+ # share the same epi_tile shape.
421
+ for op in getattr(self, "_epi_ops", ()):
422
+ if op.is_tile_load():
423
+ tile_load_tensor = getattr(epilogue_args, op.name, None)
424
+ if tile_load_tensor is not None:
425
+ tile_load_layout = LayoutEnum.from_tensor(tile_load_tensor)
426
+ tile_load_dtype = tile_load_tensor.element_type
427
+ break
428
  self.epi_tile = sm100_utils.compute_epilogue_tile_shape(
429
  self.cta_tile_shape_mnk,
430
  self.use_2cta_instrs,
431
  self.d_layout if self.d_layout is not None else LayoutEnum.ROW_MAJOR,
432
  self.d_dtype if self.d_dtype is not None else cutlass.BFloat16,
433
+ layout_c=self.c_layout if self.c_layout is not None else tile_load_layout,
434
+ elem_ty_c=self.c_dtype if self.c_dtype is not None else tile_load_dtype,
435
  )
436
  # TMA store tile starts must stay aligned when advancing across CTA-N tiles.
437
  # There's a bug w compute_epilogue_tile_shape (as of cutlass-dsl 4.4.2) where if
 
472
  prefetch_A_idx,
473
  cutlass.utils.get_smem_capacity_in_bytes(f"sm_{self.arch}"), # smem_capacity
474
  self.occupancy,
475
+ self.epi_smem_warp_shape_mnk(),
476
  )
477
+ # With CLC the try_cancel response lands directly in the consumer slot, so
478
+ # the next query can only be issued once all consumers (cluster-wide)
479
+ # release that slot. >=2 stages keep a query in flight while the previous
480
+ # tile's info is still being read (cutlass's SchedulerPipelineStageCount
481
+ # >= 2); the 3rd stage buys response slack for epilogue-bound tiles (e.g.
482
+ # symmetric's double store, ~3% at M=8192 K=512) and costs only 12 smem
483
+ # ints + one mbarrier pair.
484
+ self.sched_stage = 3 if self.use_clc_persistence else 1
485
  self.a_prefetch_stage = (
486
  0
487
  if not self.gather_A
 
577
  stream: cuda.CUstream,
578
  mSFA: Optional[cute.Tensor] = None,
579
  mSFB: Optional[cute.Tensor] = None,
 
580
  ):
581
  """Execute the GEMM operation in steps:
582
  - Setup static attributes before smem/grid/tma computation
 
640
  # so non-packed buffers work (e.g. a slice of a larger scale tensor).
641
  # Only the innermost 512-B tile must be contiguous.
642
  # For varlen_m, mSFA is sized for per-expert 128-row-padded storage
643
+ # (tile-aligned per-batch padding), so use its own M dim (= total_padded_rm * 128)
644
  # instead of mA.shape[0] (= total_m, unpadded).
645
  if const_expr(cute.rank(mA) == 3):
646
  sfa_shape = mA.shape
 
663
  a_smem_layout = cute.slice_(self.a_smem_layout_staged, (None, None, None, 0))
664
  b_smem_layout = cute.slice_(self.b_smem_layout_staged, (None, None, None, 0))
665
  tma_atom_a, tma_tensor_a = None, None
666
+ a_op = (
667
+ cpasync.CopyBulkTensorTileG2SOp(self.cta_group)
668
+ if const_expr(not self.gather_A)
669
+ else sm100_utils.cluster_shape_to_tma_atom_A(
670
+ self.cluster_shape_mnk, self.tiled_mma.thr_id
671
+ )
672
  )
673
  if const_expr(not self.gather_A):
674
  tma_atom_a, tma_tensor_a = cute.nvgpu.make_tiled_tma_atom_A(
 
696
  tma_smem_layout.shape,
697
  internal_type=(cutlass.TFloat32 if mA.element_type is Float32 else None),
698
  )
699
+ # block_copy takes compiler-driven multicast metadata at the copy site,
700
+ # so the TMA atom itself must stay the non-multicast variant here.
701
+ b_op = cpasync.CopyBulkTensorTileG2SOp(self.cta_group)
702
  tma_atom_b, tma_tensor_b = cute.nvgpu.make_tiled_tma_atom_B(
703
  b_op,
704
  copy_utils.create_ragged_tensor_for_tma(mB, ragged_dim=1) if varlen_k else mB,
 
713
  tma_atom_sfb, tma_tensor_sfb = None, None
714
  if const_expr(self.blockscaled):
715
  # Setup TMA load for SFA
716
+ sfa_op = cpasync.CopyBulkTensorTileG2SOp(self.cta_group)
 
 
717
  sfa_smem_layout = cute.slice_(self.sfa_smem_layout_staged, (None, None, None, 0))
718
  tma_atom_sfa, tma_tensor_sfa = cute.nvgpu.make_tiled_tma_atom_A(
719
  sfa_op,
 
725
  internal_type=cutlass.Int16,
726
  )
727
  # Setup TMA load for SFB
728
+ sfb_op = cpasync.CopyBulkTensorTileG2SOp(self.cta_group)
 
 
729
  sfb_smem_layout = cute.slice_(self.sfb_smem_layout_staged, (None, None, None, 0))
730
  tma_atom_sfb, tma_tensor_sfb = cute.nvgpu.make_tiled_tma_atom_B(
731
  sfb_op,
 
736
  self.cluster_layout_sfb_vmnk.shape,
737
  internal_type=cutlass.Int16,
738
  )
739
+ # tile_n=192 spans 1.5 SF atoms, so consecutive N-tiles straddle atom
740
+ # boundaries and a TMA box can't be placed at a half-atom offset.
741
+ # Instead each tile loads a 2-atom (256-wide) window: remap the gmem
742
+ # atom sequence to [a0 a1 | a1 a2 | a3 a4 | a4 a5 | ...] (groups of 4
743
+ # presented atoms at offsets (0, x, x, 2x), advancing by 3 atoms) so
744
+ # that tile j's window lands on atoms (3j//2, 3j//2 + 1). Odd tiles
745
+ # start 64 into their first atom; the mma warp corrects for that via
746
+ # the sfb_n_atom_misaligned tmem offset.
747
+ if const_expr(self.cta_tile_shape_mnk[1] == 192):
748
  x = tma_tensor_sfb.stride[0][1]
749
  y = cute.ceil_div(tma_tensor_sfb.shape[0][1], 4)
750
  tma_tensor_sfb = cute.make_tensor(
 
772
  self.num_tma_load_bytes += sfa_copy_size + sfb_copy_size
773
  self.num_tma_load_bytes *= atom_thr_size
774
 
775
+ # Setup TMA store for D and TMA load for C.
776
+ tma_atom_d, tma_tensor_d, tma_atom_c, tma_tensor_c = (
777
+ self.make_tma_epilogue_atoms_and_tensors(mD, mC, epilogue_args, varlen_m)
778
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
779
 
780
  epilogue_params = self.epi_to_underlying_arguments(epilogue_args)
781
  varlen_params = VarlenManager.to_underlying_arguments(varlen_args)
782
 
783
+ self.epi_load_bytes_per_stage = self.epi_smem_bytes(
784
+ epilogue_args,
785
+ self.cta_tile_shape_mnk,
786
+ self.epi_tile,
787
+ self.epi_smem_warp_shape_mnk(),
788
+ ).c_stage
789
+ if const_expr(mC is not None):
790
+ c_smem_layout = cute.slice_(self.epi_c_smem_layout_staged, (None, None, 0))
791
+ self.epi_load_bytes_per_stage += cute.size_in_bytes(self.c_dtype, c_smem_layout)
792
+
793
  TileSchedulerCls = self.get_scheduler_class(varlen_m=varlen_m)
794
  tile_sched_args = self.get_scheduler_arguments(
795
  mA, mB, mD, scheduler_args, varlen_args, epilogue_args
 
816
  self.cta_tile_shape_mnk[0] if varlen_m else self.cta_tile_shape_mnk[2]
817
  )
818
 
819
+ # Define shared storage for kernel. sched_data lives in the RESERVED
820
+ # smem partition (with the pipeline mbarriers / TMEM holding buf): a
821
+ # small buffer before the 1024-byte aligned epilogue tensors would add
822
+ # a 1 KiB pad; CLC responses use i128 copies, so it stays 16-byte
823
+ # aligned.
824
+ # 4 Int32 per stage, shared by the two (mode-exclusive) users: STATIC/DYNAMIC
825
+ # store the STAS-broadcast (pid_m, pid_n, batch_idx, is_valid); CLC stores the
826
+ # 16-byte try_cancel response (16B-aligned since each stage slot is 16 bytes).
827
+ sched_smem_size = 4 * self.sched_stage if self.is_persistent else 0
828
+
829
+ @partitioned_struct
830
  class SharedStorage:
831
+ sched_data: Reserved[
832
+ cute.struct.Align[cute.struct.MemRange[Int32, sched_smem_size], 16]
 
 
 
 
833
  ]
 
 
 
834
  sAIdx: cute.struct.Align[cute.struct.MemRange[Int32, a_idx_smem_size], 16]
835
  # (EPI_TILE_M, EPI_TILE_N, STAGE)
836
  sD: cute.struct.Align[
 
899
  self.epi_tile,
900
  tile_sched_params,
901
  TileSchedulerCls,
 
902
  ).launch(
903
  grid=grid,
904
  block=[self.threads_per_cta, 1, 1],
 
941
  epi_tile: cute.Tile,
942
  tile_sched_params,
943
  TileSchedulerCls: cutlass.Constexpr[Callable],
 
944
  ):
945
  """
946
  GPU device kernel performing the Persistent batched GEMM computation.
947
  """
948
 
 
 
 
 
949
  varlen_m = const_expr(varlen_params.cu_seqlens_m is not None)
950
  varlen_k = const_expr(varlen_params.cu_seqlens_k is not None)
951
  assert not (varlen_m and varlen_k)
 
953
  assert varlen_m or varlen_k
954
  has_D = const_expr(mD_mnl is not None)
955
  has_C = const_expr(mC_mnl is not None)
956
+ has_epi_load = const_expr(self.epi_c_stage > 0)
957
 
958
  warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
959
 
 
983
 
984
  # Alloc and init: a+b full/empty, accumulator full/empty, tensor memory dealloc barrier
985
  smem = cutlass.utils.SmemAllocator()
986
+ storage = self.shared_storage.allocate(smem)
987
 
988
  # Initialize pipelines and states
989
  ab_pipeline = self.make_ab_pipeline(
990
  tiled_mma=tiled_mma,
991
  cluster_layout_vmnk=cluster_layout_vmnk,
 
992
  is_leader_cta=is_leader_cta,
993
  )
994
  epi_pipeline = None
995
+ if const_expr(has_epi_load):
996
+ epi_pipeline = self.make_epi_pipeline(tx_count=self.epi_load_bytes_per_stage)
997
+ acc_pipeline = self.make_acc_pipeline(cluster_layout_vmnk=cluster_layout_vmnk)
 
 
 
 
 
 
998
  sched_pipeline = None
999
  sched_data = None
1000
  if const_expr(self.is_persistent):
1001
+ sched_pipeline = self.make_sched_pipeline(self.cluster_shape_mnk, has_C=has_epi_load)
1002
+ sched_data = storage.sched_data.get_tensor(cute.make_layout((4, self.sched_stage)))
 
 
 
 
1003
  a_prefetch_pipeline = None
1004
  if const_expr(self.gather_A):
1005
+ a_prefetch_pipeline = self.make_a_prefetch_pipeline()
 
 
1006
 
1007
  tmem_alloc_barrier = pipeline.NamedBarrier(
1008
  barrier_id=int(NamedBarrierGemm.TmemPtr),
 
1010
  )
1011
  # Tensor memory dealloc barrier init
1012
  tmem = cutlass.utils.TmemAllocator(
 
1013
  barrier_for_retrieve=tmem_alloc_barrier,
1014
  allocator_warp_id=self.epilog_warp_id[0],
1015
  is_two_cta=use_2cta_instrs,
 
1016
  )
1017
 
1018
  # Cluster arrive after barrier init
 
1068
  )
1069
 
1070
  TileSchedulerCls = partial(
1071
+ TileSchedulerCls.create,
1072
+ tile_sched_params,
1073
+ sched_data,
1074
+ sched_pipeline,
1075
+ throttle_barrier=self.clc_throttle_barrier,
1076
  )
1077
 
1078
  epi_load_barrier = None
1079
+ if const_expr(has_epi_load):
1080
  epi_load_barrier = pipeline.NamedBarrier(
1081
+ barrier_id=int(NamedBarrierGemm.EpilogueLoad),
1082
+ num_threads=(self.num_ab_load_warps + 1) * cute.arch.WARP_SIZE,
1083
  )
1084
 
1085
  # Cluster wait before tensor memory alloc
 
1095
  cute.arch.griddepcontrol_wait()
1096
  if const_expr(self.gather_A):
1097
  cute.arch.setmaxregister_decrease(self.num_regs_other)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1098
  # Persistent tile scheduling loop
1099
  tile_scheduler = TileSchedulerCls()
1100
  work_tile = tile_scheduler.initial_work_tile_info()
 
1105
  pipeline.PipelineUserType.Consumer, self.a_prefetch_stage
1106
  )
1107
  do_epi_load_barrier_arrive = Boolean(True)
1108
+ # CLC throttle producer: only the first load warp of CTA 0 in the
1109
+ # cluster signals; commit once per work tile started, or the scheduler
1110
+ # warp starves of credits.
1111
+ is_throttle_producer = Boolean(warp_idx == self.ab_load_warp_id)
1112
+ if const_expr(cute.size(cluster_layout_vmnk) > 1):
1113
+ is_throttle_producer = is_throttle_producer & Boolean(
1114
+ cute.arch.block_idx_in_cluster() == 0
1115
+ )
1116
  while work_tile.is_valid_tile:
1117
+ tile_scheduler.throttle_producer_commit(is_throttle_producer)
1118
  tile_coord_mnkl = work_tile.tile_idx
1119
  batch_idx = tile_coord_mnkl[3]
1120
  # Local_tile partition global tensors
 
1140
  )
1141
  if const_expr(self.blockscaled):
1142
  # (bM, bK)
1143
+ # SFA uses the tile-aligned per-batch offset (padded SF layout), not
1144
  # the A-data offset — allows varlen_m seqlens that aren't
1145
  # multiples of 128.
1146
  gSFA_mkl = cute.local_tile(
 
1149
  (mma_tile_coord_mnl[0], None),
1150
  )
1151
  # (bN, bK)
1152
+ # SFB uses the tile-aligned per-batch K offset in varlen_k (padded SF layout).
1153
+ # N-tiles sharing one 128-wide SF atom (tile_n=64) load the same
1154
+ # atom, so the gmem N-tile coord is divided by sfb_tiles_per_atom.
1155
  gSFB_nkl = cute.local_tile(
1156
  varlen_manager.offset_batch_SFB(mSFB_nkl, batch_idx),
1157
  cute.select(self.mma_tiler_sfb, [1, 2]),
1158
+ (mma_tile_coord_mnl[1] // self.sfb_tiles_per_atom, None),
 
 
 
 
 
 
 
1159
  )
1160
 
1161
  # Partition global tensor for TiledMMA_A/B/D
1162
  # Then partition global/shared tensor for TMA load A/B
1163
  len_k = varlen_manager.len_k(batch_idx)
1164
+ # block_copy's lowering wants the coordinate held fixed by the
1165
+ # multicast mask: A/SFA are same-M across N peers, while B/SFB
1166
+ # are same-N across M peers. Degenerate cluster dimensions are
1167
+ # left for the compiler lowering to simplify.
1168
+ a_tma_multicast = {
1169
+ "cluster_shape": self.cluster_shape_mnk[:2],
1170
+ "multicast_dim": "M",
1171
+ }
1172
+ b_tma_multicast = {
1173
+ "cluster_shape": self.cluster_shape_mnk[:2],
1174
+ "multicast_dim": "N",
1175
+ }
1176
  copy_A, prefetch_A = None, None
1177
  if const_expr(not self.gather_A):
1178
  # (MMA, MMA_M, MMA_K, RestK)
1179
  tCgA = thr_mma.partition_A(gA_mk)
1180
+ copy_A = copy_utils.tma_get_block_copy_fn(
1181
+ tma_atom_a, src_tensor=tCgA, dst_tensor=sA, tma_multicast=a_tma_multicast
 
 
 
 
 
1182
  )
1183
  else:
1184
  # For varlen_m paths (TMA or cp.async): consume indices from
 
1198
  warp_idx,
1199
  )
1200
  if const_expr(varlen_m):
1201
+ a_prefetch_pipeline.consumer_release(a_prefetch_consumer_state)
 
 
1202
  a_prefetch_consumer_state.advance()
1203
  if const_expr(prefetch_A is not None):
1204
  prefetch_A = partial(prefetch_A, a_prefetch_pipeline)
 
1210
  # (MMA, MMA_N, MMA_K)
1211
  tCgSFB = thr_mma_sfb.partition_B(gSFB_nkl)
1212
  # TMA load B partition_S/D
1213
+ copy_B = copy_utils.tma_get_block_copy_fn(
1214
+ tma_atom_b, src_tensor=tCgB, dst_tensor=sB, tma_multicast=b_tma_multicast
 
 
 
 
 
 
 
1215
  )
1216
  copy_SFA, copy_SFB = None, None
1217
  if const_expr(self.blockscaled):
1218
  # TMA load SFA partition_S/D
1219
+ copy_SFA = copy_utils.tma_get_block_copy_fn(
1220
  tma_atom_sfa,
 
 
1221
  src_tensor=tCgSFA,
1222
  dst_tensor=sSFA,
1223
+ tma_multicast=a_tma_multicast,
 
1224
  )
1225
  # TMA load SFB partition_S/D
1226
+ copy_SFB = copy_utils.tma_get_block_copy_fn(
 
 
 
1227
  tma_atom_sfb,
 
 
1228
  src_tensor=tCgSFB,
1229
  dst_tensor=sSFB,
1230
+ tma_multicast=b_tma_multicast,
 
1231
  )
1232
  k_tile_cnt = cute.ceil_div(len_k, self.cta_tile_shape_mnk[2])
1233
+ iket.range_push("tma_load")
1234
  if const_expr(not self.gather_A):
1235
+ ab_producer_state = self.load_tma(
1236
  ab_pipeline,
1237
  ab_producer_state,
1238
+ [copy_A, copy_B, copy_SFA, copy_SFB],
 
1239
  k_tile_cnt,
 
 
1240
  )
1241
  elif const_expr(self.use_tma_gather):
1242
  ab_producer_state, a_prefetch_consumer_state = self.load_AB_tma_gather(
 
1258
  copy_B,
1259
  k_tile_cnt,
1260
  )
1261
+ iket.range_pop()
1262
  if const_expr(epi_load_barrier is not None):
1263
  # In the first work tile, the epi load warp will wait for the signal
1264
  # from the mainloop load warp to start loading C, to avoid interfering
 
1267
  epi_load_barrier.arrive()
1268
  do_epi_load_barrier_arrive = Boolean(False)
1269
  # Advance to next tile
1270
+ iket.range_push("sched_fetch")
1271
  tile_scheduler.advance_to_next_work()
1272
  work_tile = tile_scheduler.get_current_work()
1273
+ iket.range_pop()
1274
  # Wait A/B buffer empty
1275
  if warp_idx == self.ab_load_warp_id:
1276
  ab_pipeline.producer_tail(ab_producer_state)
 
1291
  work_tile = tile_scheduler.initial_work_tile_info()
1292
  while work_tile.is_valid_tile:
1293
  # Advance to next tile
1294
+ iket.range_push("clc_produce")
1295
  tile_scheduler.advance_to_next_work(is_scheduler_warp=is_scheduler_warp)
1296
+ iket.range_pop()
1297
+ iket.range_push("clc_consume")
1298
  work_tile = tile_scheduler.get_current_work()
1299
+ iket.range_pop()
1300
  # End of persistent scheduler loop
1301
  if is_scheduler_warp:
1302
  tile_scheduler.producer_tail()
1303
+ # Drain the pending-cluster tail (varlen padding) with unobserved
1304
+ # cancels so it never launches; see cancel_pending_tail for the
1305
+ # grant-monotonicity assumption this relies on.
1306
+ tile_scheduler.cancel_pending_tail()
1307
 
1308
  # Specialized A-index prefetch warp (gather_A only)
1309
  if const_expr(self.gather_A):
 
1384
  if const_expr(self.gather_A):
1385
  cute.arch.setmaxregister_decrease(self.num_regs_other)
1386
  # PDL: wait for prior kernel before any C TMA loads (matches cutlass C++ epi_load)
1387
+ if const_expr(self.use_pdl and has_epi_load):
1388
  cute.arch.griddepcontrol_wait()
1389
+ if const_expr(has_epi_load):
1390
  epi_producer_state = pipeline.make_pipeline_state(
1391
  pipeline.PipelineUserType.Producer, self.epi_c_stage
1392
  )
 
1398
  # Get tile coord from tile scheduler
1399
  tile_coord_mnkl = work_tile.tile_idx
1400
  batch_idx = tile_coord_mnkl[3]
1401
+ copy_C = None
1402
+ if const_expr(has_C):
1403
+ copy_C_fn, _, _ = self.epilog_gmem_copy_and_partition(
1404
+ tma_atom_c,
1405
+ varlen_manager.offset_batch_epi(mC_mnl, batch_idx),
1406
+ self.cta_tile_shape_mnk[:2],
1407
+ epi_tile,
1408
+ sC,
1409
+ tile_coord_mnkl,
1410
+ )
1411
+ copy_C = copy_utils.tma_producer_copy_fn(copy_C_fn, epi_pipeline)
1412
+ tile_load_copy_fns = self.epi_tile_load_g2s_copy_fns(
1413
+ epilogue_params,
1414
+ epi_smem_tensors,
1415
  tile_coord_mnkl,
1416
+ varlen_manager,
1417
+ epi_pipeline,
1418
+ )
1419
+ copy_epi_load = copy_utils.chain_tma_producer_copy_fns(
1420
+ (copy_C, *tile_load_copy_fns)
1421
  )
 
1422
  if do_epi_load_barrier_wait:
1423
  epi_load_barrier.arrive_and_wait()
1424
  do_epi_load_barrier_wait = Boolean(False)
1425
+ epi_tile_num = const_expr(
1426
+ cute.size(
1427
+ cute.zipped_divide(
1428
+ cute.make_layout(self.cta_tile_shape_mnk[:2]), epi_tile
1429
+ ),
1430
+ mode=[1],
1431
+ )
1432
+ )
1433
  for epi_idx in cutlass.range(epi_tile_num, unroll=1):
1434
  epi_pipeline.producer_acquire(epi_producer_state)
1435
+ copy_epi_load(src_idx=epi_idx, producer_state=epi_producer_state)
1436
  # Epi pipeline's producer commit is a NOP
1437
  epi_pipeline.producer_commit(epi_producer_state)
1438
  epi_producer_state.advance()
 
1490
  cute.slice_(sfb_smem_layout, (None, None, None, 0)),
1491
  )
1492
  tCtSFB = cute.make_tensor(sfb_tmem_ptr, tCtSFB_layout)
 
 
 
 
 
 
 
 
 
 
 
1493
  else:
1494
  tCtSFA, tCtSFB = None, None
 
 
1495
 
1496
  # Persistent tile scheduling loop
1497
  tile_scheduler = TileSchedulerCls()
 
1517
  )
1518
  tCtAcc = tCtAcc_base[None, None, None, acc_stage_idx]
1519
  tCtSFB_mma = tCtSFB
1520
+ if const_expr(self.blockscaled and self.sfb_n_atom_misaligned):
1521
+ # Odd N-tiles start 64 into a 128-wide SF atom: shift the SFB
1522
+ # tmem base by 2 columns (in the atom layout (32,4):(16,4),
1523
+ # N+64 is element offset 8 = 2 tmem columns).
1524
  tCtSFB_mma = cute.make_tensor(
1525
  cute.recast_ptr(
1526
  sfb_tmem_base_ptr + Int32((tile_coord_mnkl[1] % 2) * 2),
 
1528
  ),
1529
  tCtSFB.layout,
1530
  )
1531
+ copy_s2t_sfa, copy_s2t_sfb = None, None
1532
+ sf_valid_insts = None
1533
+ if const_expr(self.blockscaled):
1534
+ copy_s2t_sfa = copy_utils.s2t_get_copy_fn(sSFA, tCtSFA, self.cta_group)
1535
+ copy_s2t_sfb = copy_utils.s2t_get_copy_fn(sSFB, tCtSFB, self.cta_group)
1536
+ # Exploits the fact that for mxfp8 the MMA instruction K size
1537
+ # equals the SF vec size (== 32), so one instruction consumes
1538
+ # exactly one SF block and the mma loop can skip the
1539
+ # instructions for SF pad blocks on a ragged-K last tile (see
1540
+ # the comment in self.mma). fp4 has inst_k 64 spanning
1541
+ # multiple SF blocks, but we don't do varlen_k for
1542
+ # mxfp4/nvfp4. Valid instructions in that tile; % maps
1543
+ # "aligned or full tile" to 0 = nothing to skip.
1544
+ if const_expr(self.mma_inst_shape_mnk[2] == self.sf_vec_size):
1545
+ num_insts = self.mma_tiler[2] // self.mma_inst_shape_mnk[2]
1546
+ sf_valid_insts = (
1547
+ cute.ceil_div(k_len % self.mma_tiler[2], self.sf_vec_size) % num_insts
1548
+ )
1549
+ iket.range_push("mma")
1550
  ab_consumer_state, acc_producer_state, tiled_mma = self.mma(
1551
  ab_pipeline,
1552
  acc_pipeline,
 
1561
  cta_rank_in_cluster,
1562
  tCtSFA,
1563
  tCtSFB_mma,
1564
+ copy_s2t_sfa,
1565
+ copy_s2t_sfb,
1566
+ sf_valid_insts,
 
 
 
1567
  )
1568
  if const_expr(self.overlap_accum_sf):
1569
  # After iter 0, 2, ..., shift tmem ptr by -256.
1570
  # After iter 1, 3, ..., shift tmem ptr by 256.
1571
+ tCtSFA, tCtSFB = [
1572
  cute.make_tensor(
1573
  cute.recast_ptr(
1574
  # Doing tmem ptr arithmetic requires 32-bit type, wrong otherwise
 
1581
  ),
1582
  mT.layout,
1583
  )
1584
+ for mT in [tCtSFA, tCtSFB]
1585
  ]
1586
+ iket.range_pop()
1587
  # Advance to next tile
1588
  tile_scheduler.advance_to_next_work()
1589
  work_tile = tile_scheduler.get_current_work()
 
1641
  pipeline.PipelineUserType.Consumer, self.epi_c_stage
1642
  )
1643
  while work_tile.is_valid_tile:
1644
+ # Prefetch the next work tile before the epilogue: the response is
1645
+ # already in smem (3-stage sched pipeline), and consuming it here
1646
+ # hides the ~300ns decode (swizzle + async fence) behind this tile's
1647
+ # epilogue — the pacing chain for small-K / double-store epilogues.
1648
+ # advance_to_next_work stays after the body: num_tiles_executed must
1649
+ # count completed tiles during the body (sD stage cycling).
1650
+ next_work_tile = tile_scheduler.get_current_work()
1651
  # Get tile coord from tile scheduler
1652
  tile_coord_mnkl = work_tile.tile_idx
1653
  batch_idx = tile_coord_mnkl[3]
 
1691
  acc_release_idx=self.iter_acc_early_release
1692
  if const_expr(self.overlap_accum_sf)
1693
  else epi_tile_num - 1,
1694
+ clear_acc=(varlen_k and k_len == 0),
1695
  )
1696
 
1697
+ iket.range_push("epilogue")
1698
  epi_read_state, _ = self.epilogue(
1699
  epilogue_params,
1700
  epi_smem_tensors,
 
1723
  )
1724
  # acc_pipeline.consumer_release was already called in self.epi_load_acc_subtile
1725
  acc_consumer_state.advance()
1726
+ iket.range_pop()
1727
 
1728
  # Advance to next tile
1729
  tile_scheduler.advance_to_next_work()
1730
+ work_tile = next_work_tile
1731
 
1732
  # Wait for D store complete
1733
  if is_tma_warp:
 
1738
  tmem_alloc_barrier.arrive_and_wait()
1739
  tmem.free(acc_tmem_ptr)
1740
 
 
 
1741
  @cute.jit
1742
  def _make_gather_A_copy(
1743
  self,
 
1930
  cta_rank_in_cluster: Int32,
1931
  tCtSFA: Optional[cute.Tensor] = None,
1932
  tCtSFB: Optional[cute.Tensor] = None,
1933
+ copy_s2t_sfa: Optional[Callable] = None,
1934
+ copy_s2t_sfb: Optional[Callable] = None,
1935
+ sf_valid_insts_last_tile: Optional[Int32] = None,
 
 
 
1936
  ) -> Tuple[cutlass.pipeline.PipelineState, cutlass.pipeline.PipelineState, cute.TiledMma]:
1937
+ blockscaled = const_expr(copy_s2t_sfa is not None)
1938
  if const_expr(blockscaled):
1939
  assert all(x is not None for x in (tCtSFA, tCtSFB))
1940
+ assert copy_s2t_sfb is not None
1941
+ skip_sf_pad_insts = const_expr(sf_valid_insts_last_tile is not None)
 
1942
  # If gather_A and use_2cta_instrs, the cp.async for the non-leader CTA will
1943
  # arrive at an mbarrier on the non-leader CTA side, then the mma warp of the non-leader
1944
  # CTA will wait for that then arrive at the mbarrier on the leader CTA.
 
1961
  if not is_leader_cta:
1962
  ab_pipeline.consumer_wait(ab_consumer_state, peek_ab_full_status)
1963
  with cute.arch.elect_one():
1964
+ # The odd CTA signals the even CTA. The arrive must release this
1965
+ # CTA's cp.async smem writes at cluster scope so that the leader's
1966
+ # 2-CTA MMA, which reads our smem over DSMEM, is guaranteed to
1967
+ # observe them; a plain mbarrier.arrive is only release.cta
1968
+ # (https://github.com/Dao-AILab/quack/issues/63).
1969
+ mbarrier_arrive_release_cluster(
1970
+ ab_pipeline.sync_object_full.get_barrier(ab_consumer_state.index),
1971
+ cta_rank_in_cluster & 0xFE,
1972
  )
1973
  if is_leader_cta:
1974
  # Conditionally wait for AB buffer full
1975
  ab_pipeline.consumer_wait(ab_consumer_state, peek_ab_full_status)
1976
+ if const_expr(need_nonleader_cta):
1977
+ # consumer_wait acquires at cta scope only; pair the non-leader's
1978
+ # cluster-scope release with a cluster-scope acquire of the (already
1979
+ # completed) phase before the MMA reads the peer CTA's smem.
1980
+ mbarrier_acquire_cluster(
1981
+ ab_pipeline.sync_object_full.get_barrier(ab_consumer_state.index),
1982
+ ab_consumer_state.phase,
1983
+ )
1984
  # Copy SFA/SFB from smem to tmem
1985
  if const_expr(blockscaled):
1986
+ copy_s2t_sfa(ab_consumer_state.index)
1987
+ copy_s2t_sfb(ab_consumer_state.index)
1988
+ # Ragged K: the last k-tile's SF atom holds pad bytes beyond the
1989
+ # valid scale blocks. We exploit the fact that for mxfp8 the MMA
1990
+ # instruction K size equals the SF vec size (both 32), i.e. each
1991
+ # instruction consumes exactly one SF block: skipping the
1992
+ # instructions for pad blocks — whose A/B values are
1993
+ # TMA-zero-filled and contribute nothing — means the pad scales
1994
+ # are never consumed and the gmem pad may be arbitrary (e8m0 0xFF
1995
+ # = NaN would otherwise poison the accumulator via 0-value x
1996
+ # NaN-scale products). Instruction issue is a leader-only
1997
+ # decision, so this covers 2-CTA MMA too. fp4 has inst_k 64 (2
1998
+ # SF blocks for mxfp4, 4 for nvfp4), but we don't do varlen_k
1999
+ # for those formats.
2000
+ # (The set/gemm sequence is duplicated below because the DSL
2001
+ # rejects closures capturing staged values inside a dynamic if.)
2002
+ if const_expr(skip_sf_pad_insts):
2003
+ num_mma_insts = Int32(num_k_blocks)
2004
+ if sf_valid_insts_last_tile > 0 and k_tile == k_tile_cnt - 1:
2005
+ num_mma_insts = sf_valid_insts_last_tile
2006
  for k_blk_idx in cutlass.range(num_k_blocks, unroll_full=True):
2007
  k_blk_coord = (None, None, k_blk_idx, ab_consumer_state.index)
2008
  if const_expr(blockscaled):
2009
  # Set SFA/SFB tensor to tiled_mma
2010
  sf_kblock_coord = (None, None, k_blk_idx)
2011
+ if const_expr(skip_sf_pad_insts):
2012
+ if k_blk_idx < num_mma_insts:
2013
+ tiled_mma.set(tcgen05.Field.SFA, tCtSFA[sf_kblock_coord].iterator)
2014
+ tiled_mma.set(tcgen05.Field.SFB, tCtSFB[sf_kblock_coord].iterator)
2015
+ cute.gemm(tiled_mma, acc, tCrA[k_blk_coord], tCrB[k_blk_coord], acc)
2016
+ tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
2017
+ else:
2018
+ tiled_mma.set(tcgen05.Field.SFA, tCtSFA[sf_kblock_coord].iterator)
2019
+ tiled_mma.set(tcgen05.Field.SFB, tCtSFB[sf_kblock_coord].iterator)
2020
+ cute.gemm(tiled_mma, acc, tCrA[k_blk_coord], tCrB[k_blk_coord], acc)
2021
+ tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
2022
+ else:
2023
+ cute.gemm(tiled_mma, acc, tCrA[k_blk_coord], tCrB[k_blk_coord], acc)
2024
+ tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
2025
  # Async arrive AB buffer empty
2026
  ab_pipeline.consumer_release(ab_consumer_state)
2027
  ab_consumer_state.advance()
 
2045
  tTR_tAcc: cute.Tensor,
2046
  tTR_rAcc: cute.Tensor,
2047
  tRS_rD: cute.Tensor,
2048
+ epi_coord: [int, int],
2049
  acc_pipeline: pipeline.PipelineAsync,
2050
  acc_consumer_state: pipeline.PipelineState,
2051
  acc_release_idx: int,
 
2053
  ):
2054
  if not clear_acc:
2055
  # Load accumulator from tensor memory buffer to register
2056
+ cute.copy(tiled_copy_t2r, tTR_tAcc[None, None, None, epi_coord], tTR_rAcc)
2057
  tRS_rAcc = tiled_copy_r2s.retile(tTR_rAcc)
2058
  tRS_rD.store(tRS_rAcc.load())
2059
  else:
2060
  tRS_rD.fill(0.0)
2061
+ assert epi_coord[0] == 0 # For Sm100, we assume epi_M = 1
2062
+ if epi_coord[1] == acc_release_idx:
2063
  cute.arch.fence_view_async_tmem_load()
2064
+ acc_pipeline.consumer_release(acc_consumer_state)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2065
 
2066
  def epilog_tmem_copy_and_partition(
2067
  self,
 
2195
  self,
2196
  tiled_mma: cute.TiledMma,
2197
  cluster_layout_vmnk: cute.Layout,
 
2198
  is_leader_cta: Boolean,
2199
  ) -> pipeline.PipelineAsync:
2200
  # If gather_A and use_2cta_instrs, the cp.async for the non-leader CTA will
 
2207
  if const_expr(not self.gather_A or self.use_tma_gather):
2208
  producer_cnt = 1
2209
  else:
2210
+ producer_cnt = self.num_ab_load_warps * cute.arch.WARP_SIZE
2211
+ if const_expr(not self.use_2cta_instrs):
2212
+ producer_cnt += 1
2213
+ else:
2214
+ producer_cnt += Int32(2) if is_leader_cta else Int32(0)
2215
  ab_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread, producer_cnt)
2216
  # Each warp will contribute to the arrive count with the number of mcast size
2217
  mcast_size = self.num_mcast_ctas_a + self.num_mcast_ctas_b - 1
 
2219
  ab_pipeline_consumer_group = pipeline.CooperativeGroup(
2220
  pipeline.Agent.Thread, consumer_arrive_cnt
2221
  )
2222
+ if const_expr(not self.gather_A or self.use_tma_gather):
 
 
 
 
 
 
 
 
 
 
2223
  pipeline_ab = PipelineTmaUmma.create(
 
2224
  num_stages=self.ab_stage,
2225
  producer_group=ab_pipeline_producer_group,
2226
  consumer_group=ab_pipeline_consumer_group,
 
2230
  )
2231
  else:
2232
  pipeline_ab = PipelineTmaCpAsyncUmma.create(
 
2233
  num_stages=self.ab_stage,
2234
  producer_group=ab_pipeline_producer_group,
2235
  consumer_group=ab_pipeline_consumer_group,
2236
  tx_count=self.num_tma_load_bytes,
2237
  cta_layout_vmnk=cluster_layout_vmnk,
 
 
 
2238
  defer_sync=True,
2239
  )
2240
  return pipeline_ab
2241
 
2242
+ def make_acc_pipeline(self, cluster_layout_vmnk: cute.Layout) -> pipeline.PipelineAsync:
 
 
2243
  acc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
2244
  num_acc_consumer_threads = self.num_epi_warps * (2 if self.use_2cta_instrs else 1)
2245
  acc_pipeline_consumer_group = pipeline.CooperativeGroup(
2246
  pipeline.Agent.Thread, num_acc_consumer_threads
2247
  )
2248
+ return PipelineUmmaAsync.create(
 
2249
  num_stages=self.num_acc_stage,
2250
  producer_group=acc_pipeline_producer_group,
2251
  consumer_group=acc_pipeline_consumer_group,
2252
  cta_layout_vmnk=cluster_layout_vmnk,
2253
  defer_sync=True,
2254
+ elect_one_release=True,
2255
+ # TMEM load consumers are already ordered by fence_view_async_tmem_load()
2256
+ syncwarp_before_release=False,
2257
  )
2258
 
2259
  def make_sched_pipeline(
2260
  self,
2261
  cluster_layout_mnk: cute.Layout,
 
2262
  has_C: bool = False,
2263
  ) -> pipeline.PipelineAsync:
2264
  # Threads/warps participating in this pipeline
 
2275
  sched_pipeline_consumer_group = pipeline.CooperativeGroup(
2276
  pipeline.Agent.Thread, consumer_arrive_cnt
2277
  )
2278
+ # Plain PipelineAsync on purpose (vs the DSL example's PipelineClcFetchAsync):
2279
+ # expect_tx is per-phase mbarrier state, so each mode's producer arms the full
2280
+ # barrier as a transaction barrier itself — CLC's multicast try_cancel or
2281
+ # STATIC/DYNAMIC's STAS st.async, both arrive_and_expect_tx(16) per CTA — and
2282
+ # only the consumer protocol (wait full, elect-one arrive at CTA 0's empty
2283
+ # barrier) is shared across modes. A CLC-specific pipeline would hardwire the
2284
+ # producer and still need this one for STATIC/DYNAMIC.
2285
+ return QuackPipelineAsync.create(
2286
  num_stages=self.sched_stage,
2287
  producer_group=sched_pipeline_producer_group,
2288
  consumer_group=sched_pipeline_consumer_group,
2289
  # If there's cluster, the consumers must arrive at the mbar of CTA 0 in the cluster.
2290
  consumer_mask=None if const_expr(cluster_size == 1) else 0,
2291
  defer_sync=True,
2292
+ # One arrive per consumer warp (consumer_arrive_cnt counts warps): syncwarp
2293
+ # so every lane's slot read is complete, then one elected lane signals.
2294
+ elect_one_release=True,
2295
  )
2296
 
2297
  @cute.jit
2298
+ def make_a_prefetch_pipeline(self) -> pipeline.PipelineAsync:
 
 
2299
  producer_cnt = 32
2300
  a_prefetch_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread, producer_cnt)
2301
  consumer_arrive_cnt = self.num_ab_load_warps
2302
  a_prefetch_consumer_group = pipeline.CooperativeGroup(
2303
  pipeline.Agent.Thread, consumer_arrive_cnt
2304
  )
2305
+ return PipelineCpAsync.create(
 
2306
  num_stages=self.a_prefetch_stage,
2307
  producer_group=a_prefetch_producer_group,
2308
  consumer_group=a_prefetch_consumer_group,
2309
  defer_sync=True,
2310
+ elect_one_release=True,
2311
+ syncwarp_before_release=True,
2312
  )
2313
 
2314
  @classmethod
 
2330
  prefetch_A_idx: Literal[None, "varlen_m", "varlen_k"],
2331
  smem_capacity: int,
2332
  occupancy: int,
2333
+ warp_shape_mnk: Tuple[int, int, int] | None = None,
2334
  ) -> Tuple[int, int, int]:
2335
  """Computes the number of stages for A/B/C operands based on heuristics.
2336
 
 
2366
 
2367
  # Default D stages
2368
  epi_stage = 4 if cute.size(epi_tile[1]) <= 16 else 2
2369
+ epi_smem_bytes = cls.epi_smem_bytes(
2370
+ epilogue_args, cta_tile_shape_mnk, epi_tile, warp_shape_mnk
2371
+ )
2372
+ has_tile_load = epi_smem_bytes.c_stage > 0
2373
+ epi_c_stage = (
2374
+ 0
2375
+ if c_dtype is None and not has_tile_load
2376
+ else (4 if cute.size(epi_tile[1]) <= 16 else 2)
2377
+ )
2378
 
2379
  # Calculate smem layout and size for one stage of A, B, and C
2380
  a_smem_layout_staged_one = sm100_utils.make_smem_layout_a(
 
2428
  d_bytes_per_stage = (
2429
  cute.size_in_bytes(d_dtype, d_smem_layout_staged_one) if d_dtype is not None else 0
2430
  )
2431
+ epi_bytes_per_stage = d_bytes_per_stage + epi_smem_bytes.d_stage
2432
+ epi_bytes = epi_smem_bytes.unstaged + epi_bytes_per_stage * epi_stage
 
 
2433
  if const_expr(c_dtype is not None):
2434
  c_bytes_per_stage = cute.size_in_bytes(c_dtype, c_smem_layout_staged_one)
2435
  epi_bytes += c_bytes_per_stage * epi_c_stage
2436
+ if const_expr(has_tile_load):
2437
+ epi_bytes += epi_smem_bytes.c_stage * epi_c_stage
2438
 
2439
  # Calculate A/B/SFA/SFB stages:
2440
  # Start with total smem per CTA (capacity / occupancy)
 
2446
  # Refine epilogue stages:
2447
  # Calculate remaining smem after allocating for A/B stages and reserved bytes
2448
  # Add remaining unused smem to epilogue
2449
+ if epi_bytes_per_stage > 0:
2450
+ epi_stage += (remaining_bytes - ab_bytes_per_stage * ab_stage) // epi_bytes_per_stage
2451
  return num_acc_stage, ab_stage, epi_stage, epi_c_stage
2452
 
2453
  @staticmethod
 
2609
 
2610
  @staticmethod
2611
  def is_valid_mma_tiler_and_cluster_shape(
2612
+ mma_tiler_mnk: Union[Tuple[int, int], Tuple[int, int, int]],
2613
  cluster_shape_mn: Tuple[int, int],
2614
  blockscaled: bool,
2615
  ) -> bool:
2616
  """
2617
  Check if the mma tiler and cluster shape are valid
2618
 
2619
+ :param mma_tiler_mnk: The (M, N) or (M, N, K) shape of the MMA instruction tiler
2620
+ :type mma_tiler_mnk: Union[Tuple[int, int], Tuple[int, int, int]]
2621
  :param cluster_shape_mn: The (ClusterM, ClusterN) shape of the CTA cluster
2622
  :type cluster_shape_mn: Tuple[int, int]
2623
 
 
2627
  is_valid = True
2628
  # Skip invalid mma tile shape
2629
  if not blockscaled:
2630
+ if mma_tiler_mnk[0] not in [64, 128, 256]:
2631
  is_valid = False
2632
  else:
2633
+ if mma_tiler_mnk[0] not in [128, 256]:
2634
  is_valid = False
2635
+ mma_inst_n = mma_tiler_mnk[1] if mma_tiler_mnk[1] <= 256 else mma_tiler_mnk[1] // 2
2636
  if not blockscaled:
2637
  if mma_inst_n not in range(32, 257, 32):
2638
  is_valid = False
2639
  else:
2640
  # Blockscaled currently supports tile_n in {64, 128, 192, 256}.
2641
+ if mma_tiler_mnk[1] not in [64, 128, 192, 256]:
2642
  is_valid = False
2643
+ if cluster_shape_mn[0] % (2 if mma_tiler_mnk[0] == 256 else 1) != 0:
2644
  is_valid = False
2645
  # Skip invalid cluster shape
2646
  is_power_of_2 = lambda x: x > 0 and (x & (x - 1)) == 0
 
2718
  sf_dtype: Type[cutlass.Numeric],
2719
  sf_vec_size: int,
2720
  d_dtype: Type[cutlass.Numeric],
2721
+ mma_tiler_mnk: Union[Tuple[int, int], Tuple[int, int, int]],
2722
  cluster_shape_mn: Tuple[int, int],
2723
  m: int,
2724
  n: int,
 
2736
  if ab_dtype is cutlass.Float4E2M1FN and not (a_major == "k" and b_major == "k"):
2737
  can_implement = False
2738
  if not GemmSm100.is_valid_mma_tiler_and_cluster_shape(
2739
+ mma_tiler_mnk, cluster_shape_mn, blockscaled=True
2740
  ):
2741
  can_implement = False
 
 
 
 
2742
  if not GemmSm100.is_valid_tensor_alignment(
2743
  m, n, k, l, ab_dtype, d_dtype, a_major, b_major, d_major
2744
  ):
 
2750
  ab_dtype: Type[cutlass.Numeric],
2751
  acc_dtype: Type[cutlass.Numeric],
2752
  d_dtype: Type[cutlass.Numeric],
2753
+ mma_tiler_mnk: Union[Tuple[int, int], Tuple[int, int, int]],
2754
  cluster_shape_mn: Tuple[int, int],
2755
  m: int,
2756
  n: int,
 
2769
  :type acc_dtype: Type[cutlass.Numeric]
2770
  :param d_dtype: The data type of the output tensor
2771
  :type d_dtype: Type[cutlass.Numeric]
2772
+ :param mma_tiler_mnk: The (M, N) or (M, N, K) shape of the MMA instruction tiler
2773
+ :type mma_tiler_mnk: Union[Tuple[int, int], Tuple[int, int, int]]
2774
  :param cluster_shape_mn: The (ClusterM, ClusterN) shape of the CTA cluster
2775
  :type cluster_shape_mn: Tuple[int, int]
2776
  :param m: The number of rows in the A tensor
 
2797
  can_implement = False
2798
  # Skip invalid mma tile shape and cluster shape
2799
  if not GemmSm100.is_valid_mma_tiler_and_cluster_shape(
2800
+ mma_tiler_mnk, cluster_shape_mn, blockscaled=False
2801
  ):
2802
  can_implement = False
2803
  # Skip illegal problem shape for load/store alignment