liangsu9988 commited on
Commit
8c8128e
·
verified ·
1 Parent(s): 9f76311

Promote latest kernel artifacts to main

Browse files
Files changed (47) hide show
  1. .gitattributes +3 -35
  2. CARD.md +64 -0
  3. README.md +16 -0
  4. VALIDATION.md +29 -0
  5. build/torch-cuda/__init__.py +77 -0
  6. build/torch-cuda/_ops.py +7 -0
  7. build/torch-cuda/flashrt_fa4/__init__.py +2 -0
  8. build/torch-cuda/flashrt_fa4/cute/.flake8 +4 -0
  9. build/torch-cuda/flashrt_fa4/cute/AUTHORS +8 -0
  10. build/torch-cuda/flashrt_fa4/cute/LICENSE +29 -0
  11. build/torch-cuda/flashrt_fa4/cute/__init__.py +25 -0
  12. build/torch-cuda/flashrt_fa4/cute/ampere_helpers.py +103 -0
  13. build/torch-cuda/flashrt_fa4/cute/barrier.py +71 -0
  14. build/torch-cuda/flashrt_fa4/cute/blackwell_helpers.py +1113 -0
  15. build/torch-cuda/flashrt_fa4/cute/block_info.py +156 -0
  16. build/torch-cuda/flashrt_fa4/cute/block_sparse_utils.py +1437 -0
  17. build/torch-cuda/flashrt_fa4/cute/block_sparsity.py +638 -0
  18. build/torch-cuda/flashrt_fa4/cute/cache_utils.py +281 -0
  19. build/torch-cuda/flashrt_fa4/cute/copy_utils.py +372 -0
  20. build/torch-cuda/flashrt_fa4/cute/cute_dsl_ptxas.py +151 -0
  21. build/torch-cuda/flashrt_fa4/cute/cute_dsl_utils.py +156 -0
  22. build/torch-cuda/flashrt_fa4/cute/fa_logging.py +97 -0
  23. build/torch-cuda/flashrt_fa4/cute/fast_math.py +21 -0
  24. build/torch-cuda/flashrt_fa4/cute/flash_fwd_combine.py +698 -0
  25. build/torch-cuda/flashrt_fa4/cute/flash_fwd_sm100.py +0 -0
  26. build/torch-cuda/flashrt_fa4/cute/interface_fwd_sm100.py +1595 -0
  27. build/torch-cuda/flashrt_fa4/cute/mask.py +1470 -0
  28. build/torch-cuda/flashrt_fa4/cute/mma_sm100_desc.py +296 -0
  29. build/torch-cuda/flashrt_fa4/cute/named_barrier.py +56 -0
  30. build/torch-cuda/flashrt_fa4/cute/pack_gqa.py +263 -0
  31. build/torch-cuda/flashrt_fa4/cute/paged_kv.py +246 -0
  32. build/torch-cuda/flashrt_fa4/cute/pipeline.py +402 -0
  33. build/torch-cuda/flashrt_fa4/cute/seqlen_info.py +287 -0
  34. build/torch-cuda/flashrt_fa4/cute/sm100_hd256_2cta_fmha_forward.py +1882 -0
  35. build/torch-cuda/flashrt_fa4/cute/softmax.py +628 -0
  36. build/torch-cuda/flashrt_fa4/cute/testing.py +476 -0
  37. build/torch-cuda/flashrt_fa4/cute/tile_scheduler.py +1636 -0
  38. build/torch-cuda/flashrt_fa4/cute/utils.py +948 -0
  39. build/torch-cuda/metadata.json +72 -0
  40. build/torch-cuda/quack/__init__.py +0 -0
  41. build/torch-cuda/quack/activation.py +568 -0
  42. build/torch-cuda/quack/compile_utils.py +19 -0
  43. build/torch-cuda/quack/copy_utils.py +1185 -0
  44. build/torch-cuda/quack/cute_dsl_utils.py +159 -0
  45. build/torch-cuda/quack/layout_utils.py +331 -0
  46. build/torch-cuda/quack/sm90_utils.py +161 -0
  47. build/torch-cuda/quack/utils.py +324 -0
.gitattributes CHANGED
@@ -1,35 +1,3 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.so filter=lfs diff=lfs merge=lfs -text
2
+ *.pyd filter=lfs diff=lfs merge=lfs -text
3
+ *.dylib filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
CARD.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: kernels
3
+ license: bsd-3-clause
4
+ tags: [cuda, blackwell, flash-attention, cute-dsl, inference]
5
+ ---
6
+
7
+ # flashrt/fa4-cute-runtime
8
+
9
+ Forward-only FlashAttention-4 CuTe DSL runtime used by FlashRT's GROOT N1.7
10
+ and PI0.5 Thor pipelines. The source is vendored under the private `flashrt_fa4`
11
+ namespace and does not shadow an installed `flash_attn` package.
12
+
13
+ This package adds the dedicated Blackwell D256 2CTA forward path required by
14
+ PI0.5's 8-Q/1-KV-head PaliGemma encoder. The community FlashAttention-4 package
15
+ already covers the D48/D72/D128 family; it does not currently expose D256 on
16
+ SM100/SM110.
17
+
18
+ ## Functions
19
+
20
+ - `flash_attn_func`
21
+ - `flash_attn_varlen_func`
22
+ - `forward_static`
23
+
24
+ ```python
25
+ from kernels import get_kernel
26
+
27
+ fa4 = get_kernel("flashrt/fa4-cute-runtime", version=1)
28
+ out = fa4.flash_attn_func(q, k, v, causal=False)
29
+ ```
30
+
31
+ The vendored forward wrapper returns `(out, lse)`; use `result[0]` when only
32
+ the attention output is needed.
33
+
34
+ For a CUDA Graph hot path, preallocate the output and use the allocation-free
35
+ entry point:
36
+
37
+ ```python
38
+ out = torch.empty_like(q)
39
+ fa4.forward_static(q, k, v, out, causal=False)
40
+ ```
41
+
42
+ For a padded fixed-shape graph, pass the valid K/V length as a CUDA int32
43
+ tensor. PI0.5 uses this form for its encoder cache:
44
+
45
+ ```python
46
+ seqused_k = torch.tensor([valid_k], device="cuda", dtype=torch.int32)
47
+ fa4.forward_static(
48
+ q, k_padded, v_padded, out,
49
+ causal=False,
50
+ pack_gqa=True,
51
+ seqused_k=seqused_k,
52
+ )
53
+ ```
54
+
55
+ Inputs follow FlashAttention's `(batch, sequence, heads, head_dim)` contract.
56
+ Qualified model profiles include D72 MHA and D256 GQA (8 Q heads / 1 KV head),
57
+ with both dense and `seqused_k` execution. This package targets SM100-family
58
+ Blackwell forward inference and requires
59
+ CUDA 13 plus `nvidia-cutlass-dsl` 4.4.x, 4.5.x, or 4.6.x. The wrapper selects Thor's
60
+ accepted architecture alias according to the installed DSL version.
61
+
62
+ This is an execution backend rather than a universal SDPA replacement. Select
63
+ it with model-shape profiling; the GROOT causal GQA profile benefits while some
64
+ short vision profiles remain faster on PyTorch SDPA.
README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # fa4-cute-runtime
2
+
3
+ Forward-only, SM100-family FlashAttention-4 CuTe DSL package used by FlashRT's
4
+ production GROOT N1.7 and PI0.5 Thor runtimes. In addition to the standard
5
+ D48/D72/D128 path, it includes the dedicated D256 2CTA forward kernel needed
6
+ by PI0.5's PaliGemma encoder.
7
+
8
+ The package exposes `flash_attn_func`, `flash_attn_varlen_func`, and the
9
+ preallocated `forward_static` CUDA Graph entry, including `seqused_k` for
10
+ fixed-shape padded K/V. It keeps the vendored
11
+ implementation in the private `flashrt_fa4` namespace and vendors the required
12
+ quack helper subset, so it cannot replace or mutate upstream `flash_attn`
13
+ imports.
14
+
15
+ See [CARD.md](CARD.md) for usage and [VALIDATION.md](VALIDATION.md) for the
16
+ release gate.
VALIDATION.md ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Validation
2
+
3
+ Required on NVIDIA Thor with CUDA 13 and CUTLASS DSL 4.4.x, 4.5.x, or 4.6.x:
4
+
5
+ ```bash
6
+ python tests/test_fa4_cute_runtime.py --backend source --mode full
7
+ python tests/test_fa4_cute_runtime.py --backend installed --mode full \
8
+ --artifact build/torch-cuda
9
+ ```
10
+
11
+ The gate covers non-causal and causal GQA, head dimensions 48/72/128, and the
12
+ PI0.5 D256 encoder family. D256 cases include dense lengths 320/968 and padded
13
+ `seqused_k` lengths 456/712 in a 968-token buffer. Every case reports
14
+ max/p99/mean/cosine metrics and requires bitwise CUDA Graph replay after JIT
15
+ warmup.
16
+
17
+ On 2026-08-06, the current source snapshot passed `8/8` rows on NVIDIA Thor
18
+ (SM110), PyTorch `2.13.0+cu130`, CUDA 13.0, and CUTLASS DSL 4.4.2. D48/D72/D128
19
+ and all four D256 rows passed; worst p99 absolute error was `0.000244` and
20
+ worst cosine was `0.99999982`. CUDA Graph replay was bitwise deterministic.
21
+ The torch-noarch final-layout import probe also passed from the isolated Thor
22
+ environment.
23
+
24
+ The same `8/8` source matrix passed on the same Thor with CUTLASS DSL 4.6.1.
25
+ The 4.6 compatibility aliases cover public API relocations only; D48/D72/D128
26
+ and D256 dense/seqused results matched the 4.4.2 gate.
27
+
28
+ Installed-artifact execution and performance must still be rerun after every
29
+ Hub rebuild. Source qualification alone is not a release artifact claim.
build/torch-cuda/__init__.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Forward-only FlashAttention-4 CuTe runtime for SM100-family GPUs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+
8
+ import cutlass
9
+ from cutlass import cute
10
+
11
+
12
+ def _version_tuple(value: str) -> tuple[int, int]:
13
+ fields = value.split(".")
14
+ return tuple(int(field) for field in fields[:2]) # type: ignore[return-value]
15
+
16
+
17
+ _DSL_VERSION = _version_tuple(str(getattr(cutlass, "__version__", "0.0")))
18
+ if not ((4, 4) <= _DSL_VERSION < (4, 7)):
19
+ raise RuntimeError(
20
+ "fa4-cute-runtime requires nvidia-cutlass-dsl 4.4.x, 4.5.x, or 4.6.x; "
21
+ f"found {getattr(cutlass, '__version__', 'unknown')}"
22
+ )
23
+
24
+ os.environ.setdefault(
25
+ "CUTE_DSL_ARCH", "sm_101a" if _DSL_VERSION >= (4, 5) else "sm_110a"
26
+ )
27
+ os.environ.setdefault("FLASH_ATTENTION_ARCH", "sm_100a")
28
+
29
+ # CUTLASS DSL 4.6 promoted these public types out of ``cute.core``. The
30
+ # vendored FA4 sources use the 4.4/4.5 annotation paths; restoring the aliases
31
+ # keeps those annotations importable without changing generated kernels.
32
+ if _DSL_VERSION >= (4, 6):
33
+ if not hasattr(cute.core, "ThrMma"):
34
+ cute.core.ThrMma = cute.ThrMma
35
+ if not hasattr(cute.core, "ThrCopy"):
36
+ cute.core.ThrCopy = cute.ThrCopy
37
+ if not hasattr(cute, "make_fragment"):
38
+ cute.make_fragment = cute.make_rmem_tensor
39
+
40
+ # Kernel Hub imports a noarch variant under a content-derived module name after
41
+ # flattening this package into the variant root. Preserve the public package
42
+ # alias used by the vendored FA4 absolute imports.
43
+ sys.modules.setdefault("fa4_cute_runtime", sys.modules[__name__])
44
+
45
+ # kernel-builder copies only the directory matching ``general.name`` for a
46
+ # torch-noarch package. The private vendor and quack subset therefore live
47
+ # inside this module and use package-qualified imports.
48
+ from .flashrt_fa4.cute import flash_attn_func, flash_attn_varlen_func # noqa: E402
49
+ from .flashrt_fa4.cute.interface_fwd_sm100 import _flash_attn_fwd # noqa: E402
50
+
51
+
52
+ def forward_static(
53
+ q,
54
+ k,
55
+ v,
56
+ out,
57
+ *,
58
+ softmax_scale=None,
59
+ causal=False,
60
+ pack_gqa=None,
61
+ seqused_k=None,
62
+ ):
63
+ """Run forward attention into a caller-owned output tensor."""
64
+ result, _ = _flash_attn_fwd(
65
+ q,
66
+ k,
67
+ v,
68
+ softmax_scale=softmax_scale,
69
+ causal=causal,
70
+ pack_gqa=pack_gqa,
71
+ seqused_k=seqused_k,
72
+ out=out,
73
+ )
74
+ return result
75
+
76
+
77
+ __all__ = ["flash_attn_func", "flash_attn_varlen_func", "forward_static"]
build/torch-cuda/_ops.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ _OPS_NAME = '_fa4_cute_runtime_cuda_8aeea60'
4
+ ops = getattr(torch.ops, _OPS_NAME)
5
+
6
+ def add_op_namespace_prefix(op_name: str) -> str:
7
+ return f'{_OPS_NAME}::{op_name}'
build/torch-cuda/flashrt_fa4/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from pkgutil import extend_path
2
+ __path__ = extend_path(__path__, __name__)
build/torch-cuda/flashrt_fa4/cute/.flake8 ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ [flake8]
2
+ max-line-length = 100
3
+ # W503: line break before binary operator
4
+ ignore = E731, E741, F841, W503
build/torch-cuda/flashrt_fa4/cute/AUTHORS ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Tri Dao
2
+ Jay Shah
3
+ Ted Zadouri
4
+ Markus Hoehnerbach
5
+ Vijay Thakkar
6
+ Timmy Liu
7
+ Driss Guessous
8
+ Reuben Stern
build/torch-cuda/flashrt_fa4/cute/LICENSE ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2022, the respective contributors, as shown by the AUTHORS file.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
build/torch-cuda/flashrt_fa4/cute/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flash Attention CUTE (CUDA Template Engine) implementation.
2
+
3
+ FlashRT vendors a forward / SM100-only subset of FlashAttention-4 for Thor
4
+ (sm_110). The public entry point lives in ``interface_fwd_sm100`` instead of
5
+ the upstream ``interface`` module so that importing this package does NOT pull
6
+ in backward, SM80/SM90/SM120, or MLA kernels. The HD256 2CTA forward kernel is
7
+ kept for Pi0.5 encoder attention; see ``VENDOR.md``.
8
+ """
9
+
10
+ from importlib.metadata import PackageNotFoundError, version
11
+
12
+ try:
13
+ __version__ = version("fa4")
14
+ except PackageNotFoundError:
15
+ __version__ = "0.0.0"
16
+
17
+ from .interface_fwd_sm100 import (
18
+ flash_attn_func,
19
+ flash_attn_varlen_func,
20
+ )
21
+
22
+ __all__ = [
23
+ "flash_attn_func",
24
+ "flash_attn_varlen_func",
25
+ ]
build/torch-cuda/flashrt_fa4/cute/ampere_helpers.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ from typing import Type, Callable, Optional
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+
7
+
8
+ def get_smem_layout_atom(dtype: Type[cutlass.Numeric], k_dim: int) -> cute.ComposedLayout:
9
+ dtype_byte = cutlass.const_expr(dtype.width // 8)
10
+ bytes_per_row = cutlass.const_expr(k_dim * dtype_byte)
11
+ smem_k_block_size = (
12
+ cutlass.const_expr(
13
+ 128
14
+ if bytes_per_row % 128 == 0
15
+ else (64 if bytes_per_row % 64 == 0 else (32 if bytes_per_row % 32 == 0 else 16))
16
+ )
17
+ // dtype_byte
18
+ )
19
+ swizzle_bits = (
20
+ 4
21
+ if smem_k_block_size == 128
22
+ else (3 if smem_k_block_size == 64 else (2 if smem_k_block_size == 32 else 1))
23
+ )
24
+ swizzle_base = 2 if dtype_byte == 4 else (3 if dtype_byte == 2 else 4)
25
+ return cute.make_composed_layout(
26
+ cute.make_swizzle(swizzle_bits, swizzle_base, swizzle_base),
27
+ 0,
28
+ cute.make_ordered_layout(
29
+ (8 if cutlass.const_expr(k_dim % 32 == 0) else 16, smem_k_block_size), order=(1, 0)
30
+ ),
31
+ )
32
+
33
+
34
+ @cute.jit
35
+ def gemm(
36
+ tiled_mma: cute.TiledMma,
37
+ acc: cute.Tensor,
38
+ tCrA: cute.Tensor,
39
+ tCrB: cute.Tensor,
40
+ tCsA: cute.Tensor,
41
+ tCsB: cute.Tensor,
42
+ smem_thr_copy_A: cute.TiledCopy,
43
+ smem_thr_copy_B: cute.TiledCopy,
44
+ hook_fn: Optional[Callable] = None,
45
+ A_in_regs: cutlass.Constexpr[bool] = False,
46
+ B_in_regs: cutlass.Constexpr[bool] = False,
47
+ swap_AB: cutlass.Constexpr[bool] = False,
48
+ ) -> None:
49
+ if cutlass.const_expr(swap_AB):
50
+ gemm(
51
+ tiled_mma,
52
+ acc,
53
+ tCrB,
54
+ tCrA,
55
+ tCsB,
56
+ tCsA,
57
+ smem_thr_copy_B,
58
+ smem_thr_copy_A,
59
+ hook_fn,
60
+ A_in_regs=B_in_regs,
61
+ B_in_regs=A_in_regs,
62
+ swap_AB=False,
63
+ )
64
+ else:
65
+ tCrA_copy_view = smem_thr_copy_A.retile(tCrA)
66
+ tCrB_copy_view = smem_thr_copy_B.retile(tCrB)
67
+ if cutlass.const_expr(not A_in_regs):
68
+ cute.copy(smem_thr_copy_A, tCsA[None, None, 0], tCrA_copy_view[None, None, 0])
69
+ if cutlass.const_expr(not B_in_regs):
70
+ cute.copy(smem_thr_copy_B, tCsB[None, None, 0], tCrB_copy_view[None, None, 0])
71
+ for k in cutlass.range_constexpr(cute.size(tCsA.shape[2])):
72
+ if k < cute.size(tCsA.shape[2]) - 1:
73
+ if cutlass.const_expr(not A_in_regs):
74
+ cute.copy(
75
+ smem_thr_copy_A, tCsA[None, None, k + 1], tCrA_copy_view[None, None, k + 1]
76
+ )
77
+ if cutlass.const_expr(not B_in_regs):
78
+ cute.copy(
79
+ smem_thr_copy_B, tCsB[None, None, k + 1], tCrB_copy_view[None, None, k + 1]
80
+ )
81
+ cute.gemm(tiled_mma, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
82
+ if cutlass.const_expr(k == 0 and hook_fn is not None):
83
+ hook_fn()
84
+
85
+
86
+ @cute.jit
87
+ def gemm_rs(
88
+ tiled_mma: cute.TiledMma,
89
+ acc: cute.Tensor,
90
+ tCrA: cute.Tensor,
91
+ tCrB: cute.Tensor,
92
+ tCsB: cute.Tensor,
93
+ smem_thr_copy_B: cute.TiledCopy,
94
+ hook_fn: Optional[Callable] = None,
95
+ ) -> None:
96
+ tCrB_copy_view = smem_thr_copy_B.retile(tCrB)
97
+ cute.copy(smem_thr_copy_B, tCsB[None, None, 0], tCrB_copy_view[None, None, 0])
98
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
99
+ if cutlass.const_expr(k < cute.size(tCrA.shape[2]) - 1):
100
+ cute.copy(smem_thr_copy_B, tCsB[None, None, k + 1], tCrB_copy_view[None, None, k + 1])
101
+ cute.gemm(tiled_mma, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
102
+ if cutlass.const_expr(k == 0 and hook_fn is not None):
103
+ hook_fn()
build/torch-cuda/flashrt_fa4/cute/barrier.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cutlass
2
+ import cutlass.cute as cute
3
+ from cutlass import Int32
4
+ from cutlass.cutlass_dsl import T, dsl_user_op
5
+ from cutlass._mlir.dialects import llvm
6
+
7
+
8
+ @dsl_user_op
9
+ def ld_acquire(lock_ptr: cute.Pointer, *, loc=None, ip=None) -> cutlass.Int32:
10
+ lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
11
+ state = llvm.inline_asm(
12
+ T.i32(),
13
+ [lock_ptr_i64],
14
+ "ld.global.acquire.gpu.b32 $0, [$1];",
15
+ "=r,l",
16
+ has_side_effects=True,
17
+ is_align_stack=False,
18
+ asm_dialect=llvm.AsmDialect.AD_ATT,
19
+ )
20
+ return cutlass.Int32(state)
21
+
22
+
23
+ @dsl_user_op
24
+ def red_relaxed(
25
+ lock_ptr: cute.Pointer, val: cutlass.Constexpr[Int32], *, loc=None, ip=None
26
+ ) -> None:
27
+ lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
28
+ llvm.inline_asm(
29
+ None,
30
+ [lock_ptr_i64, Int32(val).ir_value(loc=loc, ip=ip)],
31
+ "red.relaxed.gpu.global.add.s32 [$0], $1;",
32
+ "l,r",
33
+ has_side_effects=True,
34
+ is_align_stack=False,
35
+ asm_dialect=llvm.AsmDialect.AD_ATT,
36
+ )
37
+
38
+
39
+ @dsl_user_op
40
+ def red_release(
41
+ lock_ptr: cute.Pointer, val: cutlass.Constexpr[Int32], *, loc=None, ip=None
42
+ ) -> None:
43
+ lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
44
+ llvm.inline_asm(
45
+ None,
46
+ [lock_ptr_i64, Int32(val).ir_value(loc=loc, ip=ip)],
47
+ "red.release.gpu.global.add.s32 [$0], $1;",
48
+ "l,r",
49
+ has_side_effects=True,
50
+ is_align_stack=False,
51
+ asm_dialect=llvm.AsmDialect.AD_ATT,
52
+ )
53
+
54
+
55
+ @cute.jit
56
+ def wait_eq(lock_ptr: cute.Pointer, thread_idx: int | Int32, flag_offset: int, val: Int32) -> None:
57
+ flag_ptr = lock_ptr + flag_offset
58
+ if thread_idx == 0:
59
+ read_val = Int32(0)
60
+ while read_val != val:
61
+ read_val = ld_acquire(flag_ptr)
62
+
63
+
64
+ @cute.jit
65
+ def arrive_inc(
66
+ lock_ptr: cute.Pointer, thread_idx: int | Int32, flag_offset: int, val: cutlass.Constexpr[Int32]
67
+ ) -> None:
68
+ flag_ptr = lock_ptr + flag_offset
69
+ if thread_idx == 0:
70
+ red_release(flag_ptr, val)
71
+ # red_relaxed(flag_ptr, val)
build/torch-cuda/flashrt_fa4/cute/blackwell_helpers.py ADDED
@@ -0,0 +1,1113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ from typing import Optional, Tuple
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+ from cutlass import Int32, Boolean, const_expr
7
+ from cutlass.cute.nvgpu import tcgen05
8
+ from cutlass._mlir.dialects import llvm
9
+
10
+ import fa4_cute_runtime.flashrt_fa4.cute.mma_sm100_desc as sm100_desc
11
+
12
+
13
+ def _tcgen05_mma_kind(op: cute.nvgpu.tcgen05.mma.MmaOp) -> str:
14
+ if isinstance(op, tcgen05.mma.MmaF16BF16Op):
15
+ return "f16"
16
+ if isinstance(op, tcgen05.mma.MmaTF32Op):
17
+ return "tf32"
18
+ if isinstance(op, tcgen05.mma.MmaI8Op):
19
+ return "i8"
20
+ if isinstance(op, tcgen05.mma.MmaFP8Op):
21
+ return "f8f6f4"
22
+ if isinstance(op, tcgen05.mma.MmaMXF8Op):
23
+ return "mxf8f6f4"
24
+ if isinstance(op, tcgen05.mma.MmaMXF4Op):
25
+ return "mxf4"
26
+ if isinstance(op, tcgen05.mma.MmaMXF4NVF4Op):
27
+ return "mxf4nvf4"
28
+ raise TypeError(f"Unsupported tcgen05 MMA op kind: {type(op).__name__}")
29
+
30
+
31
+ @cute.jit
32
+ def gemm_w_idx(
33
+ tiled_mma: cute.TiledMma,
34
+ acc: cute.Tensor,
35
+ tCrA: cute.Tensor,
36
+ tCrB: cute.Tensor,
37
+ A_idx: Optional[Int32] = None,
38
+ B_idx: Optional[Int32] = None,
39
+ zero_init: bool | Boolean = False,
40
+ swap_AB: bool = False,
41
+ num_unroll_groups: int = 1,
42
+ ) -> None:
43
+ if const_expr(swap_AB):
44
+ return gemm_w_idx(
45
+ tiled_mma, acc, tCrB, tCrA, B_idx, A_idx, zero_init=zero_init, swap_AB=False
46
+ )
47
+ else:
48
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
49
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
50
+
51
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
52
+ for k in cutlass.range(
53
+ cute.size(tCrA.shape[2]), unroll=cute.size(tCrA.shape[2]) // num_unroll_groups
54
+ ):
55
+ mma_atom.set(tcgen05.Field.ACCUMULATE, not zero_init or k != 0)
56
+ cute.gemm(mma_atom, acc, rA[None, None, k], rB[None, None, k], acc)
57
+
58
+
59
+ @cute.jit
60
+ def gemm_ptx_w_idx(
61
+ tiled_mma: cute.TiledMma,
62
+ acc: cute.Tensor,
63
+ tCrA: cute.Tensor,
64
+ tCrB: cute.Tensor,
65
+ sA: Optional[cute.Tensor],
66
+ sB: cute.Tensor,
67
+ A_idx: Optional[Int32] = None,
68
+ B_idx: Optional[Int32] = None,
69
+ zero_init: bool | Boolean = False,
70
+ cta_group: int = 1,
71
+ **kwargs,
72
+ ) -> None:
73
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
74
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
75
+ sA_cur = None
76
+ if const_expr(sA is not None):
77
+ sA_cur = sA if const_expr(A_idx is None) else sA[None, None, None, A_idx]
78
+ sB_cur = sB if const_expr(B_idx is None) else sB[None, None, None, B_idx]
79
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
80
+ acc_tmem_addr = acc.iterator.toint()
81
+ gemm_ptx_partial(
82
+ mma_atom.op,
83
+ acc_tmem_addr,
84
+ rA,
85
+ rB,
86
+ sA_cur,
87
+ sB_cur,
88
+ zero_init=zero_init,
89
+ cta_group=cta_group,
90
+ **kwargs,
91
+ )
92
+
93
+
94
+ @cute.jit
95
+ def gemm(
96
+ tiled_mma: cute.TiledMma,
97
+ acc: cute.Tensor,
98
+ tCrA: cute.Tensor,
99
+ tCrB: cute.Tensor,
100
+ zero_init: bool | Boolean = False,
101
+ ) -> None:
102
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
103
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
104
+ mma_atom.set(tcgen05.Field.ACCUMULATE, not zero_init or k != 0)
105
+ cute.gemm(mma_atom, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
106
+
107
+
108
+ def i64_to_i32x2(i: int) -> Tuple[int, int]:
109
+ """Convert a 64-bit integer to a tuple of two 32-bit integers."""
110
+ return i & 0xFFFF_FFFF, (i >> 32) & 0xFFFF_FFFF
111
+
112
+
113
+ @cute.jit
114
+ def gemm_ptx(
115
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
116
+ acc: cute.Tensor,
117
+ tCrA: cute.Tensor,
118
+ tCrB: cute.Tensor,
119
+ sA: Optional[cute.Tensor],
120
+ sB: cute.Tensor,
121
+ zero_init: bool | Boolean = False,
122
+ ) -> None:
123
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
124
+ if const_expr(not is_ts):
125
+ assert sA is not None, "sA must be provided when a_src is not TMEM"
126
+ sA_layout = sA.layout if sA is not None else None
127
+ sB_layout = sB.layout
128
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
129
+ kind = _tcgen05_mma_kind(op)
130
+ if const_expr(not is_ts):
131
+ sA_swizzle = sA.iterator.type.swizzle_type
132
+ smem_desc_base_a: int = const_expr(
133
+ sm100_desc.make_smem_desc_base(
134
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
135
+ sA_swizzle,
136
+ sm100_desc.Major.K
137
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
138
+ else sm100_desc.Major.MN,
139
+ )
140
+ )
141
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
142
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
143
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
144
+ else:
145
+ smem_desc_base_a = None
146
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
147
+ sB_swizzle = sB.iterator.type.swizzle_type
148
+ smem_desc_base_b: int = const_expr(
149
+ sm100_desc.make_smem_desc_base(
150
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
151
+ sB_swizzle,
152
+ sm100_desc.Major.K
153
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
154
+ else sm100_desc.Major.MN,
155
+ )
156
+ )
157
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
158
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
159
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
160
+
161
+ if const_expr(not is_ts):
162
+ smem_desc_start_a_lo = Int32(smem_desc_base_a_lo) | sm100_desc.make_smem_desc_start_addr(
163
+ sA[None, None, 0].iterator
164
+ )
165
+ else:
166
+ smem_desc_start_a_lo = None
167
+ smem_desc_start_b_lo = Int32(smem_desc_base_b_lo) | sm100_desc.make_smem_desc_start_addr(
168
+ sB[None, None, 0].iterator
169
+ )
170
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
171
+ if const_expr(not is_ts):
172
+ smem_desc_a_lo = smem_desc_start_a_lo + (
173
+ (cute.crd2idx((0, 0, k), sA_layout) * sA.element_type.width // 8) >> 4
174
+ )
175
+ smem_desc_b_lo = smem_desc_start_b_lo + (
176
+ (cute.crd2idx((0, 0, k), sB_layout) * sB.element_type.width // 8) >> 4
177
+ )
178
+ # with cute.arch.elect_one():
179
+ # cute.printf("smem_desc_a_lo = {}, smem_desc_b_lo = {}", smem_desc_a_lo, smem_desc_b_lo)
180
+ # cute.printf("smem_desc_a_lo_correct = {}, smem_desc_b_lo_correct = {}", smem_desc_a_lo_correct, smem_desc_b_lo_correct)
181
+ with cute.arch.elect_one():
182
+ if const_expr(not is_ts):
183
+ llvm.inline_asm(
184
+ None,
185
+ [
186
+ acc.iterator.toint().ir_value(),
187
+ smem_desc_a_lo.ir_value(),
188
+ smem_desc_b_lo.ir_value(),
189
+ Int32(not zero_init or k != 0).ir_value(),
190
+ ],
191
+ "{\n\t"
192
+ ".reg .pred p;\n\t"
193
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
194
+ ".reg .b32 idesc;\n\t"
195
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
196
+ f"mov.b64 smem_desc_a, {{$1, {hex(smem_desc_a_hi)}}};\n\t"
197
+ f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
198
+ "setp.ne.b32 p, $3, 0;\n\t"
199
+ f"tcgen05.mma.cta_group::1.kind::{kind} [$0], smem_desc_a, smem_desc_b, idesc, p;\n\t"
200
+ "}\n",
201
+ "r,r,r,r",
202
+ has_side_effects=True,
203
+ is_align_stack=False,
204
+ asm_dialect=llvm.AsmDialect.AD_ATT,
205
+ )
206
+ else:
207
+ llvm.inline_asm(
208
+ None,
209
+ [
210
+ acc.iterator.toint().ir_value(),
211
+ tCrA[None, None, k].iterator.toint().ir_value(),
212
+ smem_desc_b_lo.ir_value(),
213
+ Int32(not zero_init or k != 0).ir_value(),
214
+ ],
215
+ "{\n\t"
216
+ ".reg .pred p;\n\t"
217
+ ".reg .b64 smem_desc_b;\n\t"
218
+ f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
219
+ "setp.ne.b32 p, $3, 0;\n\t"
220
+ f"tcgen05.mma.cta_group::1.kind::{kind} [$0], [$1], smem_desc_b, {hex(idesc)}, p;\n\t"
221
+ "}\n",
222
+ "r,r,r,r",
223
+ has_side_effects=True,
224
+ is_align_stack=False,
225
+ asm_dialect=llvm.AsmDialect.AD_ATT,
226
+ )
227
+
228
+
229
+ @cute.jit
230
+ def gemm_ptx_loop(
231
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
232
+ acc: cute.Tensor,
233
+ tCrA: cute.Tensor,
234
+ tCrB: cute.Tensor,
235
+ sA: Optional[cute.Tensor],
236
+ sB: cute.Tensor,
237
+ zero_init: bool | Boolean = False,
238
+ ) -> None:
239
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
240
+ if const_expr(not is_ts):
241
+ assert sA is not None, "sA must be provided when a_src is not TMEM"
242
+ sA_layout = sA.layout if sA is not None else tCrA.layout
243
+ sB_layout = sB.layout
244
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
245
+ kind = _tcgen05_mma_kind(op)
246
+ if const_expr(not is_ts):
247
+ sA_swizzle = sA.iterator.type.swizzle_type
248
+ smem_desc_base_a: int = const_expr(
249
+ sm100_desc.make_smem_desc_base(
250
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
251
+ sA_swizzle,
252
+ sm100_desc.Major.K
253
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
254
+ else sm100_desc.Major.MN,
255
+ )
256
+ )
257
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
258
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
259
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
260
+ else:
261
+ smem_desc_base_a = None
262
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
263
+ sB_swizzle = sB.iterator.type.swizzle_type
264
+ smem_desc_base_b: int = const_expr(
265
+ sm100_desc.make_smem_desc_base(
266
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
267
+ sB_swizzle,
268
+ sm100_desc.Major.K
269
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
270
+ else sm100_desc.Major.MN,
271
+ )
272
+ )
273
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
274
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
275
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
276
+
277
+ if const_expr(not is_ts):
278
+ offset_a = [
279
+ (cute.crd2idx((0, 0, k), sA_layout) * sA.element_type.width // 8) >> 4
280
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2]))
281
+ ]
282
+ else:
283
+ offset_a = [
284
+ cute.crd2idx((0, 0, k), sA_layout) * op.a_dtype.width // 32
285
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2]))
286
+ ]
287
+ offset_a_diff = [
288
+ offset_a[k] - offset_a[k - 1] for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
289
+ ]
290
+ offset_b = [
291
+ (cute.crd2idx((0, 0, k), sB_layout) * sB.element_type.width // 8) >> 4
292
+ for k in cutlass.range_constexpr(cute.size(tCrB.shape[2]))
293
+ ]
294
+ offset_b_diff = [
295
+ offset_b[k] - offset_b[k - 1] for k in cutlass.range_constexpr(1, cute.size(tCrB.shape[2]))
296
+ ]
297
+
298
+ if const_expr(not is_ts):
299
+ smem_desc_start_a_lo = Int32(
300
+ smem_desc_base_a_lo | sm100_desc.make_smem_desc_start_addr(sA[None, None, 0].iterator)
301
+ )
302
+ else:
303
+ smem_desc_start_a_lo = None
304
+ smem_desc_start_b_lo = Int32(
305
+ smem_desc_base_b_lo | sm100_desc.make_smem_desc_start_addr(sB[None, None, 0].iterator)
306
+ )
307
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
308
+ if const_expr(not is_ts):
309
+ llvm.inline_asm(
310
+ None,
311
+ [
312
+ acc.iterator.toint().ir_value(),
313
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
314
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
315
+ Int32(not zero_init).ir_value(),
316
+ ],
317
+ "{\n\t"
318
+ ".reg .pred leader_thread;\n\t"
319
+ ".reg .pred p;\n\t"
320
+ ".reg .b32 idesc;\n\t"
321
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
322
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
323
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
324
+ "elect.sync _|leader_thread, -1;\n\t"
325
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
326
+ "mov.b32 smem_desc_a_lo, $1;\n\t"
327
+ "mov.b32 smem_desc_b_lo, $2;\n\t"
328
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
329
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
330
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
331
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
332
+ "setp.ne.b32 p, $3, 0;\n\t"
333
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
334
+ + "".join(
335
+ (
336
+ f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
337
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
338
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
339
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
340
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
341
+ )
342
+ for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
343
+ )
344
+ + "}\n",
345
+ "r,r,r,r",
346
+ has_side_effects=True,
347
+ is_align_stack=False,
348
+ asm_dialect=llvm.AsmDialect.AD_ATT,
349
+ )
350
+ else:
351
+ llvm.inline_asm(
352
+ None,
353
+ [
354
+ acc.iterator.toint().ir_value(),
355
+ Int32(tCrA[None, None, 0].iterator.toint()).ir_value(),
356
+ Int32(smem_desc_start_b_lo).ir_value(),
357
+ Int32(not zero_init).ir_value(),
358
+ ],
359
+ "{\n\t"
360
+ ".reg .pred leader_thread;\n\t"
361
+ ".reg .pred p;\n\t"
362
+ ".reg .b32 idesc;\n\t"
363
+ ".reg .b32 tmem_a;\n\t"
364
+ ".reg .b32 smem_desc_b_lo;\n\t"
365
+ ".reg .b32 smem_desc_b_hi;\n\t"
366
+ ".reg .b64 smem_desc_b;\n\t"
367
+ "elect.sync _|leader_thread, -1;\n\t"
368
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
369
+ "mov.b32 tmem_a, $1;\n\t"
370
+ "mov.b32 smem_desc_b_lo, $2;\n\t"
371
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
372
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
373
+ "setp.ne.b32 p, $3, 0;\n\t"
374
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
375
+ + "".join(
376
+ (
377
+ # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
378
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
379
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
380
+ # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, 1;\n\t"
381
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
382
+ )
383
+ for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
384
+ )
385
+ + "}\n",
386
+ "r,r,r,r",
387
+ has_side_effects=True,
388
+ is_align_stack=False,
389
+ asm_dialect=llvm.AsmDialect.AD_ATT,
390
+ )
391
+
392
+
393
+ @cute.jit
394
+ def gemm_ptx_partial(
395
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
396
+ acc_tmem_addr: Int32,
397
+ tCrA: cute.Tensor,
398
+ tCrB: cute.Tensor,
399
+ sA: Optional[cute.Tensor],
400
+ sB: cute.Tensor,
401
+ mbar_ptr: Optional[cutlass.Pointer] = None,
402
+ mbar_phase: Optional[Int32] = None,
403
+ split_arrive: Optional[int] = None,
404
+ zero_init: bool | Boolean = False,
405
+ # sA_offset: Int32 = 0,
406
+ # acc_offset: Int32 = 0,
407
+ tA_addr: Optional[Int32] = None,
408
+ cta_group: int = 1,
409
+ ) -> None:
410
+ # acc_tmem_addr += acc_offset
411
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
412
+ if const_expr(not is_ts):
413
+ assert sA is not None, "sA must be provided when a_src is not TMEM"
414
+ sA_layout = sA.layout if sA is not None else tCrA.layout
415
+ sB_layout = sB.layout
416
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
417
+ kind = _tcgen05_mma_kind(op)
418
+ if const_expr(not is_ts):
419
+ sA_swizzle = sA.iterator.type.swizzle_type
420
+ smem_desc_base_a: int = const_expr(
421
+ sm100_desc.make_smem_desc_base(
422
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
423
+ sA_swizzle,
424
+ sm100_desc.Major.K
425
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
426
+ else sm100_desc.Major.MN,
427
+ )
428
+ )
429
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
430
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
431
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
432
+ else:
433
+ smem_desc_base_a = None
434
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
435
+ sB_swizzle = sB.iterator.type.swizzle_type
436
+ smem_desc_base_b: int = const_expr(
437
+ sm100_desc.make_smem_desc_base(
438
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
439
+ sB_swizzle,
440
+ sm100_desc.Major.K
441
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
442
+ else sm100_desc.Major.MN,
443
+ )
444
+ )
445
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
446
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
447
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
448
+
449
+ tCrA_layout = (
450
+ tCrA.layout
451
+ if const_expr(not is_ts)
452
+ else cute.recast_layout(32, tCrA.element_type.width, tCrA.layout)
453
+ )
454
+ offset_a = [cute.crd2idx((0, 0, k), tCrA_layout) for k in range(cute.size(tCrA.shape[2]))]
455
+ offset_a_diff = [offset_a[k] - offset_a[k - 1] for k in range(1, cute.size(tCrA.shape[2]))]
456
+ offset_b = [cute.crd2idx((0, 0, k), tCrB.layout) for k in range(cute.size(tCrB.shape[2]))]
457
+ offset_b_diff = [offset_b[k] - offset_b[k - 1] for k in range(1, cute.size(tCrB.shape[2]))]
458
+
459
+ if const_expr(not is_ts):
460
+ smem_desc_start_a_lo = Int32(
461
+ smem_desc_base_a_lo | sm100_desc.make_smem_desc_start_addr(sA[None, None, 0].iterator)
462
+ )
463
+ # ) + sA_offset
464
+ else:
465
+ smem_desc_start_a_lo = None
466
+ smem_desc_start_b_lo = Int32(
467
+ smem_desc_base_b_lo | sm100_desc.make_smem_desc_start_addr(sB[None, None, 0].iterator)
468
+ )
469
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
470
+ if const_expr(not is_ts):
471
+ assert mbar_ptr is None, "mbar_ptr must be None when a_src is not TMEM"
472
+ llvm.inline_asm(
473
+ None,
474
+ [
475
+ # acc.iterator.toint().ir_value(),
476
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
477
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
478
+ Int32(not zero_init).ir_value(),
479
+ Int32(cute.arch.make_warp_uniform(acc_tmem_addr)).ir_value(),
480
+ ],
481
+ "{\n\t"
482
+ ".reg .pred leader_thread;\n\t"
483
+ ".reg .pred p;\n\t"
484
+ ".reg .b32 idesc;\n\t"
485
+ ".reg .b32 tmem_acc;\n\t"
486
+ ".reg .b32 smem_desc_a_lo_start, smem_desc_b_lo_start;\n\t"
487
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
488
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
489
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
490
+ "elect.sync _|leader_thread, -1;\n\t"
491
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
492
+ # f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
493
+ f"mov.b32 tmem_acc, $3;\n\t"
494
+ "mov.b32 smem_desc_a_lo_start, $0;\n\t"
495
+ "mov.b32 smem_desc_b_lo_start, $1;\n\t"
496
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
497
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
498
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo_start, smem_desc_a_hi}};\n\t"
499
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
500
+ "setp.ne.b32 p, $2, 0;\n\t"
501
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
502
+ + "".join(
503
+ (
504
+ # f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
505
+ # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
506
+ f"add.u32 smem_desc_a_lo, smem_desc_a_lo_start, {hex(offset_a[k])};\n\t"
507
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
508
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
509
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
510
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
511
+ )
512
+ for k in range(1, cute.size(tCrA.shape[2]))
513
+ )
514
+ + "}\n",
515
+ # "r,r,r",
516
+ "r,r,r,r",
517
+ has_side_effects=True,
518
+ is_align_stack=False,
519
+ asm_dialect=llvm.AsmDialect.AD_ATT,
520
+ )
521
+ else:
522
+ # For TS gemm, somehow tCrA.iterator.toint() returns 0 no matter what, so we need to
523
+ # explicitly pass in the tA_addr for correctness.
524
+ tA_addr = tCrA[None, None, 0].iterator.toint() if tA_addr is None else tA_addr
525
+ input_args = [
526
+ # Int32(cute.arch.make_warp_uniform(tCrA[None, None, 0].iterator.toint())).ir_value(),
527
+ Int32(cute.arch.make_warp_uniform(tA_addr)).ir_value(),
528
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
529
+ Int32(not zero_init).ir_value(),
530
+ Int32(cute.arch.make_warp_uniform(acc_tmem_addr)).ir_value(),
531
+ ]
532
+ if const_expr(mbar_ptr is not None):
533
+ assert mbar_phase is not None, "mbar_phase must be provided when mbar_ptr is not None"
534
+ assert split_arrive is not None, (
535
+ "split_arrive must be provided when mbar_ptr is not None"
536
+ )
537
+ split_arrive_idx = split_arrive // op.shape_mnk[2]
538
+ input_args.append(mbar_ptr.toint().ir_value())
539
+ input_args.append(Int32(mbar_phase).ir_value())
540
+ mbar_wait_str = (
541
+ ".reg .pred P1; \n\t"
542
+ "LAB_WAIT: \n\t"
543
+ "mbarrier.try_wait.parity.shared::cta.b64 P1, [$4], $5, 10000000; \n\t"
544
+ "@P1 bra DONE; \n\t"
545
+ "bra LAB_WAIT; \n\t"
546
+ "DONE: \n\t"
547
+ )
548
+ else:
549
+ mbar_wait_str = ""
550
+ llvm.inline_asm(
551
+ None,
552
+ # [
553
+ # # acc.iterator.toint().ir_value(),
554
+ # Int32(tCrA[None, None, 0].iterator.toint()).ir_value(),
555
+ # Int32(smem_desc_start_b_lo).ir_value(),
556
+ # Int32(not zero_init).ir_value(),
557
+ # ],
558
+ input_args,
559
+ "{\n\t"
560
+ ".reg .pred leader_thread;\n\t"
561
+ ".reg .pred p;\n\t"
562
+ ".reg .b32 idesc;\n\t"
563
+ ".reg .b32 tmem_acc;\n\t"
564
+ ".reg .b32 tmem_a;\n\t"
565
+ ".reg .b32 smem_desc_b_lo_start;\n\t"
566
+ ".reg .b32 smem_desc_b_lo;\n\t"
567
+ ".reg .b32 smem_desc_b_hi;\n\t"
568
+ ".reg .b64 smem_desc_b;\n\t"
569
+ "elect.sync _|leader_thread, -1;\n\t"
570
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
571
+ # f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
572
+ f"mov.b32 tmem_acc, $3;\n\t"
573
+ f"mov.b32 tmem_a, $0;\n\t"
574
+ f"mov.b32 smem_desc_b_lo_start, $1;\n\t"
575
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
576
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
577
+ "setp.ne.b32 p, $2, 0;\n\t"
578
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
579
+ + "".join(
580
+ (
581
+ # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
582
+ # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
583
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
584
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
585
+ # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, 1;\n\t"
586
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
587
+ )
588
+ for k in range(
589
+ 1,
590
+ cute.size(tCrA.shape[2]) if const_expr(mbar_ptr is None) else split_arrive_idx,
591
+ )
592
+ )
593
+ + mbar_wait_str
594
+ + (
595
+ "".join(
596
+ (
597
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
598
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
599
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
600
+ )
601
+ for k in range(split_arrive_idx, cute.size(tCrA.shape[2]))
602
+ )
603
+ if const_expr(mbar_ptr is not None)
604
+ else ""
605
+ )
606
+ + "}\n",
607
+ "r,r,r,r" if const_expr(mbar_ptr is None) else "r,r,r,r,r,r",
608
+ has_side_effects=True,
609
+ is_align_stack=False,
610
+ asm_dialect=llvm.AsmDialect.AD_ATT,
611
+ )
612
+
613
+
614
+ @cute.jit
615
+ def gemm_ptx_partial1(
616
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
617
+ acc_tmem_addr: cutlass.Constexpr[int],
618
+ tCrA: cute.Tensor,
619
+ tCrB: cute.Tensor,
620
+ sA_base_addr_for_desc: Int32,
621
+ sA_addr_offset_for_desc: cutlass.Constexpr[int],
622
+ sA_stage: Int32,
623
+ sB_base_addr_for_desc: Int32,
624
+ sB_addr_offset_for_desc: cutlass.Constexpr[int],
625
+ sB_stage: Int32,
626
+ sA_layout: Optional[cute.Layout],
627
+ sB_layout: Optional[cute.Layout],
628
+ sA_swizzle: Optional[cute.Swizzle],
629
+ sB_swizzle: cute.Swizzle,
630
+ zero_init: bool | Boolean = False,
631
+ ) -> None:
632
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
633
+ if const_expr(not is_ts):
634
+ assert sA_layout is not None, "sA_layout must be provided when a_src is not TMEM"
635
+ assert sA_swizzle is not None, "sA_swizzle must be provided when a_src is not TMEM"
636
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
637
+ kind = _tcgen05_mma_kind(op)
638
+ if const_expr(not is_ts):
639
+ smem_desc_base_a: int = const_expr(
640
+ sm100_desc.make_smem_desc_base(
641
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
642
+ sA_swizzle,
643
+ sm100_desc.Major.K
644
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
645
+ else sm100_desc.Major.MN,
646
+ )
647
+ )
648
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
649
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
650
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
651
+ else:
652
+ smem_desc_base_a = None
653
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
654
+ smem_desc_base_b: int = const_expr(
655
+ sm100_desc.make_smem_desc_base(
656
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
657
+ sB_swizzle,
658
+ sm100_desc.Major.K
659
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
660
+ else sm100_desc.Major.MN,
661
+ )
662
+ )
663
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
664
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
665
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
666
+ mask = [Int32(0)] * 4
667
+
668
+ if const_expr(not is_ts):
669
+ offset_a = [
670
+ (cute.crd2idx((0, 0, k), sA_layout) * op.a_dtype.width // 8) >> 4
671
+ for k in range(cute.size(tCrA.shape[2]))
672
+ ]
673
+ else:
674
+ offset_a = [
675
+ cute.crd2idx((0, 0, k), sA_layout) * op.a_dtype.width // 32
676
+ for k in range(cute.size(tCrA.shape[2]))
677
+ ]
678
+ offset_a_diff = [offset_a[k] - offset_a[k - 1] for k in range(1, cute.size(tCrA.shape[2]))]
679
+ offset_b = [
680
+ (cute.crd2idx((0, 0, k), sB_layout) * op.b_dtype.width // 8) >> 4
681
+ for k in range(cute.size(tCrB.shape[2]))
682
+ ]
683
+ offset_b_diff = [offset_b[k] - offset_b[k - 1] for k in range(1, cute.size(tCrB.shape[2]))]
684
+
685
+ if const_expr(not is_ts):
686
+ # smem_desc_start_a_lo = Int32(smem_desc_base_a_lo | sm100_desc.make_smem_desc_start_addr(sA[None, None, 0].iterator))
687
+ smem_desc_start_a_lo = const_expr(smem_desc_base_a_lo)
688
+ else:
689
+ smem_desc_start_a_lo = None
690
+ # smem_desc_start_b_lo = Int32(smem_desc_base_b_lo | sm100_desc.make_smem_desc_start_addr(sB[None, None, 0].iterator))
691
+ smem_desc_start_b_lo = const_expr(smem_desc_base_b_lo)
692
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
693
+ if const_expr(not is_ts):
694
+ llvm.inline_asm(
695
+ None,
696
+ [
697
+ # acc.iterator.toint().ir_value(),
698
+ # Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
699
+ Int32(sA_base_addr_for_desc).ir_value(),
700
+ Int32(sA_stage).ir_value(),
701
+ # Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
702
+ Int32(sB_base_addr_for_desc).ir_value(),
703
+ Int32(sB_stage).ir_value(),
704
+ Int32(not zero_init).ir_value(),
705
+ mask[0].ir_value(),
706
+ mask[1].ir_value(),
707
+ mask[2].ir_value(),
708
+ mask[3].ir_value(),
709
+ ],
710
+ "{\n\t"
711
+ ".reg .pred leader_thread;\n\t"
712
+ ".reg .pred p;\n\t"
713
+ ".reg .b32 idesc;\n\t"
714
+ ".reg .b32 tmem_acc;\n\t"
715
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
716
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
717
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
718
+ "elect.sync _|leader_thread, -1;\n\t"
719
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
720
+ f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
721
+ # "mov.b32 smem_desc_a_lo, $0;\n\t"
722
+ # f"add.u32 smem_desc_a_lo, $0, {hex(smem_desc_start_a_lo)};\n\t"
723
+ f"mad.lo.u32 smem_desc_a_lo, $1, {hex(sA_addr_offset_for_desc)}, $0;\n\t"
724
+ # "mov.b32 smem_desc_b_lo, $2;\n\t"
725
+ f"mad.lo.u32 smem_desc_b_lo, $3, {hex(sB_addr_offset_for_desc)}, $2;\n\t"
726
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
727
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
728
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
729
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
730
+ "setp.ne.b32 p, $4, 0;\n\t"
731
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, {{$5, $6, $7, $8}}, {pred_str};\n\t"
732
+ + "".join(
733
+ (
734
+ f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
735
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
736
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
737
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
738
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, {{$5, $6, $7, $8}}, 1;\n\t"
739
+ )
740
+ for k in range(1, cute.size(tCrA.shape[2]))
741
+ )
742
+ + "}\n",
743
+ "r,r,r,r,r,r,r,r,r",
744
+ has_side_effects=True,
745
+ is_align_stack=False,
746
+ asm_dialect=llvm.AsmDialect.AD_ATT,
747
+ )
748
+ else:
749
+ llvm.inline_asm(
750
+ None,
751
+ [
752
+ # acc.iterator.toint().ir_value(),
753
+ Int32(tCrA[None, None, 0].iterator.toint()).ir_value(),
754
+ Int32(smem_desc_start_b_lo).ir_value(),
755
+ Int32(not zero_init).ir_value(),
756
+ mask[0].ir_value(),
757
+ mask[1].ir_value(),
758
+ mask[2].ir_value(),
759
+ mask[3].ir_value(),
760
+ ],
761
+ "{\n\t"
762
+ ".reg .pred leader_thread;\n\t"
763
+ ".reg .pred p;\n\t"
764
+ ".reg .b32 idesc;\n\t"
765
+ ".reg .b32 tmem_a;\n\t"
766
+ ".reg .b32 smem_desc_b_lo;\n\t"
767
+ ".reg .b32 smem_desc_b_hi;\n\t"
768
+ ".reg .b64 smem_desc_b;\n\t"
769
+ "elect.sync _|leader_thread, -1;\n\t"
770
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
771
+ f"mov.b32 tmem_a, $1;\n\t"
772
+ f"mov.b32 smem_desc_b_lo, $2;\n\t"
773
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
774
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
775
+ "setp.ne.b32 p, $3, 0;\n\t"
776
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], [tmem_a], smem_desc_b, idesc, {{$4, $5, $6, $7}}, {pred_str};\n\t"
777
+ + "".join(
778
+ (
779
+ f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
780
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
781
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
782
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::{kind} [$0], [tmem_a], smem_desc_b, idesc, {{$4, $5, $6, $7}}, 1;\n\t"
783
+ )
784
+ for k in range(1, cute.size(tCrA.shape[2]))
785
+ )
786
+ + "}\n",
787
+ "r,r,r,r,r,r,r,r",
788
+ has_side_effects=True,
789
+ is_align_stack=False,
790
+ asm_dialect=llvm.AsmDialect.AD_ATT,
791
+ )
792
+
793
+
794
+ @cute.jit
795
+ def gemm_ptx_precomputed(
796
+ acc_tmem_addr: Int32,
797
+ smem_desc_start_a: Int32, # If TS, then this is the tmem start address for A
798
+ smem_desc_start_b: Int32,
799
+ idesc: int,
800
+ smem_desc_base_a: Optional[int],
801
+ smem_desc_base_b: int,
802
+ tCrA_layout: cute.Layout,
803
+ tCrB_layout: cute.Layout,
804
+ mbar_ptr: Optional[cutlass.Pointer] = None,
805
+ mbar_phase: Optional[Int32] = None,
806
+ zero_init: bool | Boolean = False,
807
+ cta_group: int = 1,
808
+ kind: str = "f16",
809
+ ) -> None:
810
+ # acc_tmem_addr += acc_offset
811
+ is_ts = const_expr(smem_desc_base_a is None)
812
+ num_k_tile = cute.size(tCrA_layout.shape[2])
813
+ if const_expr(not is_ts):
814
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
815
+ else:
816
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
817
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
818
+
819
+ tCrA_layout = (
820
+ tCrA_layout
821
+ if const_expr(not is_ts)
822
+ # else cute.recast_layout(32, tCrA.element_type.width, tCrA_layout)
823
+ # currently hard-coding the width to 16
824
+ else cute.recast_layout(32, 16, tCrA_layout)
825
+ )
826
+ offset_a = [cute.crd2idx((0, 0, k), tCrA_layout) for k in range(num_k_tile)]
827
+ offset_a_diff = [offset_a[k] - offset_a[k - 1] for k in range(1, num_k_tile)]
828
+ offset_b = [cute.crd2idx((0, 0, k), tCrB_layout) for k in range(num_k_tile)]
829
+ offset_b_diff = [offset_b[k] - offset_b[k - 1] for k in range(1, num_k_tile)]
830
+
831
+ smem_desc_start_a_lo = None
832
+ if const_expr(not is_ts):
833
+ smem_desc_start_a_lo = Int32(smem_desc_base_a_lo | smem_desc_start_a)
834
+ # smem_desc_start_a_lo = smem_desc_start_a
835
+ smem_desc_start_b_lo = Int32(smem_desc_base_b_lo | smem_desc_start_b)
836
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
837
+ if const_expr(not is_ts):
838
+ assert mbar_ptr is None, "mbar_ptr must be None when a_src is not TMEM"
839
+ llvm.inline_asm(
840
+ None,
841
+ [
842
+ # acc.iterator.toint().ir_value(),
843
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
844
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
845
+ Int32(not zero_init).ir_value(),
846
+ Int32(cute.arch.make_warp_uniform(acc_tmem_addr)).ir_value(),
847
+ ],
848
+ "{\n\t"
849
+ ".reg .pred leader_thread;\n\t"
850
+ ".reg .pred p;\n\t"
851
+ ".reg .b32 idesc;\n\t"
852
+ ".reg .b32 tmem_acc;\n\t"
853
+ ".reg .b32 smem_desc_a_lo_start, smem_desc_b_lo_start;\n\t"
854
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
855
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
856
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
857
+ "elect.sync _|leader_thread, -1;\n\t"
858
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
859
+ # f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
860
+ f"mov.b32 tmem_acc, $3;\n\t"
861
+ "mov.b32 smem_desc_a_lo_start, $0;\n\t"
862
+ "mov.b32 smem_desc_b_lo_start, $1;\n\t"
863
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
864
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
865
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo_start, smem_desc_a_hi}};\n\t"
866
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
867
+ "setp.ne.b32 p, $2, 0;\n\t"
868
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
869
+ + "".join(
870
+ (
871
+ # f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
872
+ # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
873
+ f"add.s32 smem_desc_a_lo, smem_desc_a_lo_start, {hex(offset_a[k])};\n\t"
874
+ f"add.s32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
875
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
876
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
877
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
878
+ )
879
+ for k in range(1, num_k_tile)
880
+ )
881
+ + "}\n",
882
+ # "r,r,r",
883
+ "r,r,r,r",
884
+ has_side_effects=True,
885
+ is_align_stack=False,
886
+ asm_dialect=llvm.AsmDialect.AD_ATT,
887
+ )
888
+ else:
889
+ input_args = [
890
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_a)).ir_value(),
891
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
892
+ Int32(not zero_init).ir_value(),
893
+ Int32(cute.arch.make_warp_uniform(acc_tmem_addr)).ir_value(),
894
+ ]
895
+ if const_expr(mbar_ptr is not None):
896
+ assert mbar_phase is not None, "mbar_phase must be provided when mbar_ptr is not None"
897
+ input_args.append(mbar_ptr.toint().ir_value())
898
+ input_args.append(Int32(mbar_phase).ir_value())
899
+ mbar_wait_str = (
900
+ ".reg .pred P1; \n\t"
901
+ "LAB_WAIT: \n\t"
902
+ "mbarrier.try_wait.parity.shared::cta.b64 P1, [$4], $5, 10000000; \n\t"
903
+ "@P1 bra DONE; \n\t"
904
+ "bra LAB_WAIT; \n\t"
905
+ "DONE: \n\t"
906
+ )
907
+ else:
908
+ mbar_wait_str = ""
909
+ llvm.inline_asm(
910
+ None,
911
+ # [
912
+ # # acc.iterator.toint().ir_value(),
913
+ # Int32(tCrA_layout[None, None, 0].iterator.toint()).ir_value(),
914
+ # Int32(smem_desc_start_b_lo).ir_value(),
915
+ # Int32(not zero_init).ir_value(),
916
+ # ],
917
+ input_args,
918
+ "{\n\t"
919
+ ".reg .pred leader_thread;\n\t"
920
+ ".reg .pred p;\n\t"
921
+ ".reg .b32 idesc;\n\t"
922
+ ".reg .b32 tmem_acc;\n\t"
923
+ ".reg .b32 tmem_a;\n\t"
924
+ ".reg .b32 smem_desc_b_lo_start;\n\t"
925
+ ".reg .b32 smem_desc_b_lo;\n\t"
926
+ ".reg .b32 smem_desc_b_hi;\n\t"
927
+ ".reg .b64 smem_desc_b;\n\t"
928
+ "elect.sync _|leader_thread, -1;\n\t"
929
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
930
+ # f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
931
+ f"mov.b32 tmem_acc, $3;\n\t"
932
+ f"mov.b32 tmem_a, $0;\n\t"
933
+ f"mov.b32 smem_desc_b_lo_start, $1;\n\t"
934
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
935
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
936
+ "setp.ne.b32 p, $2, 0;\n\t"
937
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
938
+ + "".join(
939
+ (
940
+ # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
941
+ # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
942
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
943
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
944
+ # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, 1;\n\t"
945
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
946
+ )
947
+ for k in range(
948
+ 1,
949
+ num_k_tile if const_expr(mbar_ptr is None) else num_k_tile // 4 * 3,
950
+ )
951
+ )
952
+ + mbar_wait_str
953
+ + (
954
+ "".join(
955
+ (
956
+ # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
957
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
958
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
959
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
960
+ )
961
+ for k in range(num_k_tile // 4 * 3, num_k_tile)
962
+ )
963
+ if const_expr(mbar_ptr is not None)
964
+ else ""
965
+ )
966
+ + "}\n",
967
+ "r,r,r,r" if const_expr(mbar_ptr is None) else "r,r,r,r,r,r",
968
+ has_side_effects=True,
969
+ is_align_stack=False,
970
+ asm_dialect=llvm.AsmDialect.AD_ATT,
971
+ )
972
+
973
+
974
+ @cute.jit
975
+ def declare_ptx_smem_desc(
976
+ smem_desc_start_a: Int32, # If TS, then this is the tmem start address for A
977
+ smem_desc_base_a: Optional[int],
978
+ tCrA_layout: cute.Layout,
979
+ var_name_prefix: str = "smem_desc",
980
+ ) -> None:
981
+ is_ts = const_expr(smem_desc_base_a is None)
982
+ num_k_tile = cute.size(tCrA_layout.shape[2])
983
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
984
+ if const_expr(not is_ts):
985
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
986
+ tCrA_layout = (
987
+ tCrA_layout
988
+ if const_expr(not is_ts)
989
+ # else cute.recast_layout(32, tCrA.element_type.width, tCrA_layout)
990
+ # currently hard-coding the width to 16
991
+ else cute.recast_layout(32, 16, tCrA_layout)
992
+ )
993
+ offset_a = [cute.crd2idx((0, 0, k), tCrA_layout) for k in range(num_k_tile)]
994
+ smem_desc_start_a_lo = None
995
+ if const_expr(not is_ts):
996
+ smem_desc_start_a_lo = Int32(smem_desc_base_a_lo | smem_desc_start_a)
997
+ if const_expr(not is_ts):
998
+ llvm.inline_asm(
999
+ None,
1000
+ [Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value()],
1001
+ f".reg .b32 {var_name_prefix}_lo;\n\t"
1002
+ f".reg .b64 {var_name_prefix}_<{num_k_tile}>;\n\t"
1003
+ f"mov.b64 {var_name_prefix}_0, {{$0, {hex(smem_desc_a_hi)}}};\n\t"
1004
+ + "".join(
1005
+ (
1006
+ f"add.s32 {var_name_prefix}_lo, $0, {hex(offset_a[k])};\n\t"
1007
+ f"mov.b64 {var_name_prefix}_{k}, {{{var_name_prefix}_lo, {hex(smem_desc_a_hi)}}};\n\t"
1008
+ )
1009
+ for k in range(1, num_k_tile)
1010
+ ),
1011
+ "r",
1012
+ has_side_effects=True,
1013
+ is_align_stack=False,
1014
+ asm_dialect=llvm.AsmDialect.AD_ATT,
1015
+ )
1016
+
1017
+
1018
+ @cute.jit
1019
+ def declare_ptx_idesc(op: cute.nvgpu.tcgen05.mma.MmaOp, var_name: str = "idesc") -> None:
1020
+ idesc = const_expr(sm100_desc.mma_op_to_idesc(op))
1021
+ llvm.inline_asm(
1022
+ None,
1023
+ [],
1024
+ f".reg .b32 {var_name};\n\t" # noqa
1025
+ f"mov.b32 {var_name}, {hex(idesc)};\n\t",
1026
+ constraints="",
1027
+ has_side_effects=True,
1028
+ is_align_stack=False,
1029
+ asm_dialect=llvm.AsmDialect.AD_ATT,
1030
+ )
1031
+
1032
+
1033
+ @cute.jit
1034
+ def gemm_ptx_precomputed_varname(
1035
+ acc_tmem_addr: Int32,
1036
+ smem_desc_start_b: Int32,
1037
+ # idesc: int,
1038
+ smem_desc_base_b: int,
1039
+ tCrB_layout: cute.Layout,
1040
+ smem_var_name_prefix: str,
1041
+ idesc_var_name: str,
1042
+ smem_offset: int,
1043
+ zero_init: bool | Boolean = False,
1044
+ cta_group: int = 1,
1045
+ kind: str = "f16",
1046
+ ) -> None:
1047
+ is_ts = False
1048
+ num_k_tile = cute.size(tCrB_layout.shape[2])
1049
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
1050
+ offset_b = [cute.crd2idx((0, 0, k), tCrB_layout) for k in range(num_k_tile)]
1051
+
1052
+ smem_desc_start_b_lo = Int32(smem_desc_base_b_lo | smem_desc_start_b)
1053
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
1054
+ if const_expr(not is_ts):
1055
+ llvm.inline_asm(
1056
+ None,
1057
+ [
1058
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
1059
+ Int32(not zero_init).ir_value(),
1060
+ Int32(cute.arch.make_warp_uniform(acc_tmem_addr)).ir_value(),
1061
+ ],
1062
+ "{\n\t"
1063
+ ".reg .pred leader_thread;\n\t"
1064
+ ".reg .pred p;\n\t"
1065
+ # ".reg .b32 idesc;\n\t"
1066
+ ".reg .b32 tmem_acc;\n\t"
1067
+ ".reg .b32 smem_desc_b_lo_start;\n\t"
1068
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
1069
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
1070
+ # ".reg .b64 smem_desc_b;\n\t"
1071
+ f".reg .b64 smem_desc_b_<{num_k_tile}>;\n\t"
1072
+ "elect.sync _|leader_thread, -1;\n\t"
1073
+ # f"mov.b32 idesc, {hex(idesc)};\n\t"
1074
+ # f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
1075
+ f"mov.b32 tmem_acc, $2;\n\t"
1076
+ "mov.b32 smem_desc_b_lo_start, $0;\n\t"
1077
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
1078
+ f"mov.b64 {{smem_desc_a_lo, smem_desc_a_hi}}, {smem_var_name_prefix}_0;\n\t"
1079
+ f"add.s32 smem_desc_a_lo, smem_desc_a_lo, {smem_offset};\n\t"
1080
+ f"mov.b64 {smem_var_name_prefix}_0, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
1081
+ f"mov.b64 smem_desc_b_0, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
1082
+ + "".join(
1083
+ (
1084
+ f"mov.b64 {{smem_desc_a_lo, smem_desc_a_hi}}, {smem_var_name_prefix}_{k};\n\t"
1085
+ f"add.s32 smem_desc_a_lo, smem_desc_a_lo, {smem_offset};\n\t"
1086
+ f"add.s32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
1087
+ f"mov.b64 {smem_var_name_prefix}_{k}, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
1088
+ f"mov.b64 smem_desc_b_{k}, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
1089
+ )
1090
+ for k in range(1, num_k_tile)
1091
+ )
1092
+ + "setp.ne.b32 p, $1, 0;\n\t"
1093
+ # f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_0, smem_desc_b, idesc, {pred_str};\n\t"
1094
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], {smem_var_name_prefix}_0, smem_desc_b_0, {idesc_var_name}, {pred_str};\n\t"
1095
+ + "".join(
1096
+ (
1097
+ # f"mov.b64 {{smem_desc_a_lo, smem_desc_a_hi}}, {smem_var_name_prefix}_{k};\n\t"
1098
+ # f"add.s32 smem_desc_a_lo, smem_desc_a_lo, {smem_offset};\n\t"
1099
+ # f"add.s32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
1100
+ # f"mov.b64 {smem_var_name_prefix}_{k}, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
1101
+ # f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
1102
+ # f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_{k}, smem_desc_b, idesc, 1;\n\t"
1103
+ # f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::f16 [tmem_acc], {smem_var_name_prefix}_{k}, smem_desc_b, {idesc_var_name}, 1;\n\t"
1104
+ f"@leader_thread tcgen05.mma.cta_group::{cta_group}.kind::{kind} [tmem_acc], {smem_var_name_prefix}_{k}, smem_desc_b_{k}, {idesc_var_name}, 1;\n\t"
1105
+ )
1106
+ for k in range(1, num_k_tile)
1107
+ )
1108
+ + "}\n",
1109
+ "r,r,r",
1110
+ has_side_effects=True,
1111
+ is_align_stack=False,
1112
+ asm_dialect=llvm.AsmDialect.AD_ATT,
1113
+ )
build/torch-cuda/flashrt_fa4/cute/block_info.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ from typing import Tuple, Optional
3
+ from dataclasses import dataclass
4
+
5
+ import cutlass
6
+ import cutlass.cute as cute
7
+ from cutlass import Int32, const_expr
8
+
9
+ from fa4_cute_runtime.flashrt_fa4.cute.seqlen_info import SeqlenInfoQK, SeqlenInfoQKNewK
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class BlockInfo:
14
+ tile_m: cutlass.Constexpr[int]
15
+ tile_n: cutlass.Constexpr[int]
16
+ is_causal: cutlass.Constexpr[bool]
17
+ is_local: cutlass.Constexpr[bool] = False
18
+ is_split_kv: cutlass.Constexpr[bool] = False
19
+ window_size_left: Optional[Int32] = None
20
+ window_size_right: Optional[Int32] = None
21
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
22
+
23
+ @cute.jit
24
+ def get_n_block_min_max(
25
+ self,
26
+ seqlen_info: SeqlenInfoQK,
27
+ m_block: Int32,
28
+ split_idx: Int32 = 0,
29
+ num_splits: Int32 = 1,
30
+ ) -> Tuple[Int32, Int32]:
31
+ n_block_max = cute.ceil_div(seqlen_info.seqlen_k, self.tile_n)
32
+ if const_expr(self.is_causal or (self.is_local and self.window_size_right is not None)):
33
+ m_idx_max = (m_block + 1) * self.tile_m
34
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
35
+ m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
36
+ n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
37
+ n_idx_right = n_idx if const_expr(self.is_causal) else n_idx + self.window_size_right
38
+ n_block_max = min(n_block_max, cute.ceil_div(n_idx_right, self.tile_n))
39
+ n_block_min = 0
40
+ if const_expr(self.is_local and self.window_size_left is not None):
41
+ m_idx_min = m_block * self.tile_m
42
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
43
+ m_idx_min = m_idx_min // self.qhead_per_kvhead_packgqa
44
+ n_idx = m_idx_min + seqlen_info.seqlen_k - seqlen_info.seqlen_q
45
+ n_idx_left = n_idx - self.window_size_left
46
+ n_block_min = cutlass.max(n_idx_left // self.tile_n, 0)
47
+ if cutlass.const_expr(self.is_split_kv):
48
+ num_n_blocks_per_split = (
49
+ Int32(0)
50
+ if n_block_max <= n_block_min
51
+ else (n_block_max - n_block_min + num_splits - 1) // num_splits
52
+ )
53
+ n_block_min = n_block_min + split_idx * num_n_blocks_per_split
54
+ n_block_max = cutlass.min(n_block_min + num_n_blocks_per_split, n_block_max)
55
+ return n_block_min, n_block_max
56
+
57
+ @cute.jit
58
+ def get_m_block_min_max(self, seqlen_info: SeqlenInfoQK, n_block: Int32) -> Tuple[Int32, Int32]:
59
+ m_block_max = cute.ceil_div(seqlen_info.seqlen_q, self.tile_m)
60
+ m_block_min = 0
61
+ if const_expr(self.is_causal or (self.is_local and self.window_size_right is not None)):
62
+ n_idx_min = n_block * self.tile_n
63
+ m_idx = n_idx_min + seqlen_info.seqlen_q - seqlen_info.seqlen_k
64
+ m_idx_right = m_idx if const_expr(self.is_causal) else m_idx - self.window_size_right
65
+ m_block_min = max(m_block_min, m_idx_right // self.tile_m)
66
+ if const_expr(self.is_local and self.window_size_left is not None):
67
+ n_idx_max = (n_block + 1) * self.tile_n
68
+ m_idx = n_idx_max + seqlen_info.seqlen_q - seqlen_info.seqlen_k
69
+ m_idx_left = m_idx + self.window_size_left
70
+ m_block_max = min(m_block_max, cute.ceil_div(m_idx_left, self.tile_m))
71
+ return m_block_min, m_block_max
72
+
73
+ @cute.jit
74
+ def get_n_block_k_new_min_max(
75
+ self,
76
+ seqlen_info: SeqlenInfoQKNewK,
77
+ m_block: Int32,
78
+ split_idx: Int32 = 0,
79
+ num_splits: Int32 = 1,
80
+ ) -> Tuple[Int32, Int32]:
81
+ """Get the block range for new K tokens (append KV).
82
+
83
+ First computes the full n_block range via get_n_block_min_max, then maps
84
+ those blocks into the new-K index space by subtracting seqlen_k_og.
85
+ """
86
+ n_block_min, n_block_max = self.get_n_block_min_max(
87
+ seqlen_info,
88
+ m_block,
89
+ split_idx,
90
+ num_splits,
91
+ )
92
+ idx_k_new_min = cutlass.max(n_block_min * self.tile_n - seqlen_info.seqlen_k_og, 0)
93
+ idx_k_new_max = cutlass.min(
94
+ n_block_max * self.tile_n - seqlen_info.seqlen_k_og, seqlen_info.seqlen_k_new
95
+ )
96
+ n_block_new_min = idx_k_new_min // self.tile_n
97
+ n_block_new_max = (
98
+ cute.ceil_div(idx_k_new_max, self.tile_n)
99
+ if idx_k_new_max > idx_k_new_min
100
+ else n_block_new_min
101
+ )
102
+ return n_block_new_min, n_block_new_max
103
+
104
+ @cute.jit
105
+ def get_n_block_min_causal_local_mask(
106
+ self,
107
+ seqlen_info: SeqlenInfoQK,
108
+ m_block: Int32,
109
+ n_block_min: Int32,
110
+ ) -> Int32:
111
+ """If we have separate iterations with causal or local masking at the start, where do we stop"""
112
+ m_idx_min = m_block * self.tile_m
113
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
114
+ m_idx_min = m_idx_min // self.qhead_per_kvhead_packgqa
115
+ n_idx = m_idx_min + seqlen_info.seqlen_k - seqlen_info.seqlen_q
116
+ n_idx_right = (
117
+ n_idx
118
+ if const_expr(not self.is_local or self.window_size_right is None)
119
+ else n_idx + self.window_size_right
120
+ )
121
+ return cutlass.max(n_block_min, n_idx_right // self.tile_n)
122
+
123
+ @cute.jit
124
+ def get_n_block_min_before_local_mask(
125
+ self,
126
+ seqlen_info: SeqlenInfoQK,
127
+ m_block: Int32,
128
+ n_block_min: Int32,
129
+ ) -> Int32:
130
+ """If we have separate iterations with local masking at the end, where do we stop the non-masked iterations"""
131
+ if const_expr(not self.is_local or self.window_size_left is None):
132
+ return n_block_min
133
+ else:
134
+ m_idx_max = (m_block + 1) * self.tile_m
135
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
136
+ m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
137
+ n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
138
+ n_idx_left = n_idx - self.window_size_left
139
+ return cutlass.max(n_block_min, cute.ceil_div(n_idx_left, self.tile_n))
140
+
141
+ @cute.jit
142
+ def get_n_block_max_for_m_block(
143
+ self,
144
+ seqlen_info: SeqlenInfoQK,
145
+ m_block: Int32,
146
+ n_block_global_max: Int32,
147
+ ) -> Int32:
148
+ if const_expr(self.is_causal or self.window_size_right is not None):
149
+ m_idx_max = (m_block + 1) * self.tile_m
150
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
151
+ m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
152
+ n_idx_right = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
153
+ if const_expr(self.window_size_right is not None):
154
+ n_idx_right += self.window_size_right
155
+ return min(n_block_global_max, cute.ceil_div(n_idx_right, self.tile_n))
156
+ return n_block_global_max
build/torch-cuda/flashrt_fa4/cute/block_sparse_utils.py ADDED
@@ -0,0 +1,1437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Block-sparse runtime utilities for CUTE DSL kernels.
3
+
4
+ This module contains runtime execution functions for block-sparse attention kernels.
5
+ These utilities are used by CUTE DSL kernels to produce and consume block-sparse loads.
6
+ """
7
+
8
+ from typing import Callable, Optional
9
+ from functools import partial
10
+ import math
11
+ import cutlass
12
+ import cutlass.cute as cute
13
+ from cutlass import Float32, Int32, const_expr
14
+
15
+ from fa4_cute_runtime.quack import copy_utils
16
+
17
+ # Import data structures from block_sparsity
18
+ from fa4_cute_runtime.flashrt_fa4.cute.block_sparsity import BlockSparseTensors
19
+ from fa4_cute_runtime.flashrt_fa4.cute.named_barrier import NamedBarrierBwd
20
+
21
+
22
+ # NOTE [SM100 block-sparse empty tiles: mbarrier contract]
23
+ #
24
+ # For block-sparse SM100 forward, a given (m_block, stage) Q tile can have zero active
25
+ # KV blocks (total_block_cnt == 0). In that case there is no seqlen_kv iteration, so
26
+ # the softmax warp-group has no row stats to publish.
27
+ #
28
+ # The correction warp-group seeds fully-masked-row stats and runs the usual correction
29
+ # epilogue so output/LSE have well-defined values. Both warp-groups must still perform
30
+ # the softmax<->correction mbarrier handshake so phases advance correctly across
31
+ # empty->empty and empty->non-empty tile sequences.
32
+ #
33
+ # In the no-sink case, this corresponds to the usual fully-masked-row convention:
34
+ # output is zero and LSE is -inf.
35
+ #
36
+ # Barrier contract (each is `mbar_ptr + <offset> + stage`):
37
+ #
38
+ # Producer/consumer pairs:
39
+ # - `mbar_softmax_corr_full` : softmax arrive -> correction wait
40
+ # - `mbar_softmax_corr_empty` : correction arrive -> softmax wait
41
+ # - `mbar_P_full_O_rescaled` : softmax arrive (+ correction arrive) -> MMA wait
42
+ # - `mbar_P_full_2` : softmax arrive -> MMA wait
43
+ # - `mbar_corr_epi_full_/empty` : correction <-> epilogue (only when epilogue is separate)
44
+ #
45
+ # Empty tile (`total_block_cnt == 0`):
46
+ # - Softmax: skips the seqlen_kv softmax path entirely (no P stores, no `mbar_P_full_*`).
47
+ # It only arrives `mbar_softmax_corr_full` once per stage as a synthetic "no work" signal.
48
+ # At the `softmax_loop` level, softmax unconditionally waits `mbar_softmax_corr_empty`
49
+ # before each tile (when block-sparse) to drain a prior correction arrival and keep
50
+ # phases aligned across non-empty -> empty transitions.
51
+ # - Correction: waits `mbar_softmax_corr_full`, seeds stats + runs `correction_epilogue(scale=0)`,
52
+ # and arrives `mbar_softmax_corr_empty` (and `mbar_corr_epi_full_/empty` when applicable).
53
+ # - No `mbar_P_full_*` barriers are arrived (no P, no MMA O); only the softmax<->correction
54
+ # (and correction<->epilogue) handshakes advance phases.
55
+ #
56
+ # Non-empty tile:
57
+ # - Softmax: runs `softmax_step` (produces P) and uses `mbar_softmax_corr_full/empty` to
58
+ # publish row_max (during seqlen_kv) and final row stats (once per tile), and to advance phases;
59
+ # arrives `mbar_P_full_*` when P is stored.
60
+ # - Correction: waits `mbar_softmax_corr_full`, may rescale/release O, arrives `mbar_softmax_corr_empty`
61
+ # to ack/advance, and arrives `mbar_P_full_O_rescaled` when MMA can proceed.
62
+ #
63
+ # Backward (SM100):
64
+ # - Empty KV tile: for a given `n_block`, `total_m_block_cnt == 0` means no Q tiles contribute.
65
+ # - Both the load and compute loops guard all pipeline work on `process_tile`, so empty tiles
66
+ # skip producer/consumer operations entirely (no per-tile mbarrier phase handshake like forward).
67
+ # - In the `not dKV_postprocess` path, dK/dV for empty KV tiles are explicitly written as zeros
68
+ # even when `process_tile == False` (see `flash_bwd_sm100.py` `should_zero_dKV`).
69
+
70
+
71
+ @cute.jit
72
+ def load_block_list(
73
+ block_indices: cute.Tensor,
74
+ block_count,
75
+ first_block_preloaded: cutlass.Constexpr,
76
+ kv_producer_state,
77
+ load_K,
78
+ load_V,
79
+ pipeline_k,
80
+ pipeline_v,
81
+ intra_wg_overlap: cutlass.Constexpr,
82
+ ):
83
+ """Iterate over the sparse blocks and load K, V into the pipeline.
84
+ For the intra_wg_overlap case, we overlap the loads of K and V. And this
85
+ means we need to pipeline the last V load from the partial block case,
86
+ with the loads for the full blocks. Set first_block_preloaded when the
87
+ caller has already issued the first K load for the list.
88
+
89
+ Q is loaded separately on its own mbarrier before this function is called.
90
+
91
+ Note:
92
+ we iterate along the block_n indices in reverse.
93
+
94
+ Returns:
95
+ Updated kv_producer_state after processing the block list.
96
+
97
+ """
98
+ if block_count > 0:
99
+ if const_expr(not intra_wg_overlap):
100
+ for offset in cutlass.range(block_count):
101
+ n_block = block_indices[block_count - 1 - offset]
102
+ pipeline_k.producer_acquire(kv_producer_state)
103
+ load_K(src_idx=n_block, producer_state=kv_producer_state)
104
+ pipeline_v.producer_acquire(kv_producer_state)
105
+ load_V(src_idx=n_block, producer_state=kv_producer_state)
106
+ kv_producer_state.advance()
107
+ else:
108
+ n_block_first = block_indices[block_count - 1]
109
+ if const_expr(not first_block_preloaded):
110
+ pipeline_k.producer_acquire(kv_producer_state)
111
+ load_K(src_idx=n_block_first, producer_state=kv_producer_state)
112
+
113
+ for idx in cutlass.range(block_count - 1, unroll=1):
114
+ n_block_prev = block_indices[block_count - 1 - idx]
115
+ n_block = block_indices[block_count - 2 - idx]
116
+ kv_producer_state_prev = kv_producer_state.clone()
117
+ kv_producer_state.advance()
118
+ pipeline_k.producer_acquire(kv_producer_state)
119
+ load_K(src_idx=n_block, producer_state=kv_producer_state)
120
+ pipeline_v.producer_acquire(kv_producer_state_prev)
121
+ load_V(src_idx=n_block_prev, producer_state=kv_producer_state_prev)
122
+
123
+ return kv_producer_state
124
+
125
+
126
+ @cute.jit
127
+ def finish_overlap_v_load(
128
+ block_indices: cute.Tensor,
129
+ block_count,
130
+ load_V,
131
+ pipeline_v,
132
+ kv_producer_state,
133
+ ):
134
+ """Load the final V block after overlapped K/V loads."""
135
+ if block_count > 0:
136
+ n_block_last = block_indices[0]
137
+ pipeline_v.producer_acquire(kv_producer_state)
138
+ load_V(src_idx=n_block_last, producer_state=kv_producer_state)
139
+ kv_producer_state.advance()
140
+
141
+ return kv_producer_state
142
+
143
+
144
+ @cute.jit
145
+ def sparse_tensor_m_block(
146
+ m_block,
147
+ qhead_per_kvhead: cutlass.Constexpr[int],
148
+ q_subtile_factor: cutlass.Constexpr[int],
149
+ ):
150
+ """Map packed m_block indices to block-sparse tensor indices."""
151
+ block = m_block
152
+ if const_expr(qhead_per_kvhead != 1):
153
+ block = block // qhead_per_kvhead
154
+ if const_expr(q_subtile_factor != 1):
155
+ block = block // q_subtile_factor
156
+ return block
157
+
158
+
159
+ @cute.jit
160
+ def produce_block_sparse_loads(
161
+ blocksparse_tensors: BlockSparseTensors,
162
+ batch_idx,
163
+ head_idx,
164
+ m_block,
165
+ kv_producer_state,
166
+ load_K,
167
+ load_V,
168
+ pipeline_k,
169
+ pipeline_v,
170
+ intra_wg_overlap: cutlass.Constexpr,
171
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
172
+ q_subtile_factor: cutlass.Constexpr[int] = 1,
173
+ ):
174
+ """Iterate over the mask and full block lists for a single tile.
175
+
176
+ Q is loaded separately on its own mbarrier before this function is called.
177
+
178
+ The masked (partial) list may leave the last V load pending when intra-warp-group
179
+ overlap is enabled. The first full block must consume that pending V while
180
+ issuing its own K load on the next pipeline stage.
181
+
182
+ In the intra-wg-overlap path, the last masked block leaves its V copy in flight
183
+ while we advance the producer state to start the next full K. Either the full list
184
+ overlaps that pending V load, or, if no full blocks exist, we explicitly drain it.
185
+
186
+ Args:
187
+ qhead_per_kvhead: Pack-GQA factor. When > 1, m_block is in packed space and
188
+ must be converted to unpacked for sparse tensor indexing.
189
+ """
190
+
191
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx, *_ = blocksparse_tensors
192
+
193
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
194
+
195
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
196
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
197
+
198
+ if const_expr(full_block_cnt is not None):
199
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
200
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
201
+ else:
202
+ curr_full_block_cnt = Int32(0)
203
+ curr_full_block_idx = None
204
+
205
+ mask_empty = curr_mask_block_cnt == 0
206
+ full_empty = curr_full_block_cnt == 0
207
+
208
+ if mask_empty:
209
+ # No masked blocks: the full list owns the initial K load.
210
+ kv_producer_state = load_block_list(
211
+ curr_full_block_idx,
212
+ curr_full_block_cnt,
213
+ first_block_preloaded=False,
214
+ kv_producer_state=kv_producer_state,
215
+ load_K=load_K,
216
+ load_V=load_V,
217
+ pipeline_k=pipeline_k,
218
+ pipeline_v=pipeline_v,
219
+ intra_wg_overlap=intra_wg_overlap,
220
+ )
221
+
222
+ if const_expr(intra_wg_overlap) and curr_full_block_cnt > 0:
223
+ kv_producer_state = finish_overlap_v_load(
224
+ curr_full_block_idx,
225
+ curr_full_block_cnt,
226
+ load_V,
227
+ pipeline_v,
228
+ kv_producer_state,
229
+ )
230
+ else:
231
+ # Masked blocks present. When overlap is disabled this fully drains the list.
232
+ kv_producer_state = load_block_list(
233
+ curr_mask_block_idx,
234
+ curr_mask_block_cnt,
235
+ first_block_preloaded=False,
236
+ kv_producer_state=kv_producer_state,
237
+ load_K=load_K,
238
+ load_V=load_V,
239
+ pipeline_k=pipeline_k,
240
+ pipeline_v=pipeline_v,
241
+ intra_wg_overlap=intra_wg_overlap,
242
+ )
243
+
244
+ if full_empty:
245
+ if const_expr(intra_wg_overlap):
246
+ kv_producer_state = finish_overlap_v_load(
247
+ curr_mask_block_idx,
248
+ curr_mask_block_cnt,
249
+ load_V,
250
+ pipeline_v,
251
+ kv_producer_state,
252
+ )
253
+ else:
254
+ if const_expr(intra_wg_overlap):
255
+ # Bridge the masked list to the full list by overlapping the pending masked V
256
+ # with the first full K load.
257
+ n_block_mask_last = curr_mask_block_idx[0]
258
+ n_block_full_first = curr_full_block_idx[curr_full_block_cnt - 1]
259
+ kv_producer_state_prev = kv_producer_state.clone()
260
+ kv_producer_state.advance()
261
+ pipeline_k.producer_acquire(kv_producer_state)
262
+ load_K(src_idx=n_block_full_first, producer_state=kv_producer_state)
263
+ pipeline_v.producer_acquire(kv_producer_state_prev)
264
+ load_V(src_idx=n_block_mask_last, producer_state=kv_producer_state_prev)
265
+
266
+ kv_producer_state = load_block_list(
267
+ curr_full_block_idx,
268
+ curr_full_block_cnt,
269
+ first_block_preloaded=True,
270
+ kv_producer_state=kv_producer_state,
271
+ load_K=load_K,
272
+ load_V=load_V,
273
+ pipeline_k=pipeline_k,
274
+ pipeline_v=pipeline_v,
275
+ intra_wg_overlap=intra_wg_overlap,
276
+ )
277
+
278
+ kv_producer_state = finish_overlap_v_load(
279
+ curr_full_block_idx,
280
+ curr_full_block_cnt,
281
+ load_V,
282
+ pipeline_v,
283
+ kv_producer_state,
284
+ )
285
+ else:
286
+ # Non-overlap path with both lists: run the full list normally.
287
+ kv_producer_state = load_block_list(
288
+ curr_full_block_idx,
289
+ curr_full_block_cnt,
290
+ first_block_preloaded=False,
291
+ kv_producer_state=kv_producer_state,
292
+ load_K=load_K,
293
+ load_V=load_V,
294
+ pipeline_k=pipeline_k,
295
+ pipeline_v=pipeline_v,
296
+ intra_wg_overlap=intra_wg_overlap,
297
+ )
298
+
299
+ return kv_producer_state
300
+
301
+
302
+ @cute.jit
303
+ def consume_block_sparse_loads(
304
+ blocksparse_tensors: BlockSparseTensors,
305
+ batch_idx,
306
+ head_idx,
307
+ m_block,
308
+ seqlen,
309
+ kv_consumer_state,
310
+ mma_pv_fn,
311
+ mma_one_n_block,
312
+ process_first_half_block,
313
+ process_last_half_block,
314
+ mask_fn,
315
+ score_mod_fn,
316
+ O_should_accumulate,
317
+ mask_mod,
318
+ fastdiv_mods,
319
+ intra_wg_overlap: cutlass.Constexpr,
320
+ warp_scheduler_barrier_sync: Callable,
321
+ warp_scheduler_barrier_arrive: Callable,
322
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
323
+ q_subtile_factor: cutlass.Constexpr[int] = 1,
324
+ ):
325
+ """Consume the mask and full block lists for a single tile on the consumer side.
326
+
327
+ Mirrors `produce_block_sparse_loads` so that the consumer pipeline uses
328
+ the same sparse tensor indexing.
329
+
330
+ Args:
331
+ qhead_per_kvhead: Pack-GQA factor. When > 1, m_block is in packed space and
332
+ must be converted to unpacked for sparse tensor indexing.
333
+ """
334
+
335
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx, *_ = blocksparse_tensors
336
+
337
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
338
+
339
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
340
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
341
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
342
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
343
+
344
+ processed_any = curr_mask_block_cnt + curr_full_block_cnt > 0
345
+
346
+ if const_expr(not intra_wg_overlap):
347
+ if curr_mask_block_cnt > 0:
348
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
349
+ warp_scheduler_barrier_sync()
350
+ kv_consumer_state = mma_one_n_block(
351
+ kv_consumer_state,
352
+ n_block=mask_n_block,
353
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
354
+ mask_fn=partial(
355
+ mask_fn,
356
+ mask_mod=mask_mod,
357
+ mask_seqlen=True,
358
+ fastdiv_mods=fastdiv_mods if cutlass.const_expr(mask_mod is not None) else None,
359
+ ),
360
+ is_first_n_block=True,
361
+ )
362
+ O_should_accumulate = True
363
+ for i in cutlass.range(1, curr_mask_block_cnt):
364
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1 - i]
365
+ kv_consumer_state = mma_one_n_block(
366
+ kv_consumer_state,
367
+ n_block=mask_n_block,
368
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
369
+ mask_fn=partial(mask_fn, mask_mod=mask_mod, mask_seqlen=False),
370
+ is_first_n_block=False,
371
+ )
372
+ O_should_accumulate = True
373
+ if curr_full_block_cnt == 0:
374
+ warp_scheduler_barrier_arrive()
375
+
376
+ if curr_full_block_cnt > 0:
377
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1]
378
+ if curr_mask_block_cnt == 0:
379
+ warp_scheduler_barrier_sync()
380
+ kv_consumer_state = mma_one_n_block(
381
+ kv_consumer_state,
382
+ n_block=full_n_block,
383
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
384
+ mask_fn=partial(mask_fn, mask_seqlen=True),
385
+ is_first_n_block=True,
386
+ )
387
+ O_should_accumulate = True
388
+ for i in cutlass.range(1, curr_full_block_cnt):
389
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
390
+ kv_consumer_state = mma_one_n_block(
391
+ kv_consumer_state,
392
+ n_block=full_n_block,
393
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
394
+ mask_fn=partial(mask_fn, mask_seqlen=False),
395
+ is_first_n_block=False,
396
+ )
397
+ O_should_accumulate = True
398
+ else:
399
+ kv_consumer_state = mma_one_n_block(
400
+ kv_consumer_state,
401
+ n_block=full_n_block,
402
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
403
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
404
+ is_first_n_block=False,
405
+ )
406
+ O_should_accumulate = True
407
+ for i in cutlass.range(1, curr_full_block_cnt):
408
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
409
+ kv_consumer_state = mma_one_n_block(
410
+ kv_consumer_state,
411
+ n_block=full_n_block,
412
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
413
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=False),
414
+ is_first_n_block=False,
415
+ )
416
+ O_should_accumulate = True
417
+ warp_scheduler_barrier_arrive()
418
+ else:
419
+ if curr_mask_block_cnt > 0:
420
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
421
+ kv_consumer_state = process_first_half_block(
422
+ n_block=mask_n_block,
423
+ seqlen=seqlen,
424
+ kv_consumer_state=kv_consumer_state,
425
+ mask_fn=partial(
426
+ mask_fn,
427
+ mask_mod=mask_mod,
428
+ mask_seqlen=True,
429
+ fastdiv_mods=fastdiv_mods if cutlass.const_expr(mask_mod is not None) else None,
430
+ ),
431
+ score_mod_fn=score_mod_fn,
432
+ is_first_block=True,
433
+ )
434
+ for i in cutlass.range(1, curr_mask_block_cnt):
435
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1 - i]
436
+ kv_consumer_state = mma_one_n_block(
437
+ kv_consumer_state,
438
+ n_block=mask_n_block,
439
+ seqlen=seqlen,
440
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
441
+ mask_fn=partial(mask_fn, mask_mod=mask_mod, mask_seqlen=False),
442
+ )
443
+ O_should_accumulate = True
444
+
445
+ if curr_full_block_cnt > 0:
446
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1]
447
+ if curr_mask_block_cnt == 0:
448
+ kv_consumer_state = process_first_half_block(
449
+ n_block=full_n_block,
450
+ seqlen=seqlen,
451
+ kv_consumer_state=kv_consumer_state,
452
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
453
+ score_mod_fn=score_mod_fn,
454
+ is_first_block=True,
455
+ )
456
+ else:
457
+ kv_consumer_state = mma_one_n_block(
458
+ kv_consumer_state,
459
+ n_block=full_n_block,
460
+ seqlen=seqlen,
461
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
462
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
463
+ )
464
+ O_should_accumulate = True
465
+ for i in cutlass.range(1, curr_full_block_cnt):
466
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
467
+ kv_consumer_state = mma_one_n_block(
468
+ kv_consumer_state,
469
+ n_block=full_n_block,
470
+ seqlen=seqlen,
471
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
472
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=False),
473
+ )
474
+ O_should_accumulate = True
475
+
476
+ if curr_mask_block_cnt + curr_full_block_cnt > 0:
477
+ kv_consumer_state = process_last_half_block(
478
+ kv_consumer_state=kv_consumer_state,
479
+ zero_init=not O_should_accumulate,
480
+ )
481
+ O_should_accumulate = True
482
+
483
+ return kv_consumer_state, O_should_accumulate, processed_any
484
+
485
+
486
+ @cute.jit
487
+ def load_block_list_sm100(
488
+ block_indices: cute.Tensor,
489
+ block_count,
490
+ load_q_with_first: cutlass.Constexpr,
491
+ q_stage: cutlass.Constexpr,
492
+ kv_producer_state,
493
+ load_Q,
494
+ load_K,
495
+ load_V,
496
+ pipeline_kv,
497
+ ):
498
+ """SM100 version of load_block_list (no intra_wg_overlap, no extra_tx_count)."""
499
+ if block_count > 0:
500
+ # First iteration: load Q alongside K if requested
501
+ n_block_first = block_indices[block_count - 1]
502
+
503
+ if const_expr(load_q_with_first):
504
+ # SM100 loads Q0 and optionally Q1
505
+ load_Q(block=0, stage=0)
506
+ if const_expr(q_stage == 2):
507
+ load_Q(block=1, stage=1)
508
+
509
+ # SM100 doesn't use producer_acquire for pipeline_kv in load path
510
+ # The pipeline barriers are handled inside load_KV
511
+ load_K(block=n_block_first, producer_state=kv_producer_state, page_idx=None)
512
+ kv_producer_state.advance()
513
+ load_V(block=n_block_first, producer_state=kv_producer_state, page_idx=None)
514
+ kv_producer_state.advance()
515
+
516
+ # Remaining blocks
517
+ for offset in cutlass.range(1, block_count):
518
+ n_block = block_indices[block_count - 1 - offset]
519
+ load_K(block=n_block, producer_state=kv_producer_state, page_idx=None)
520
+ kv_producer_state.advance()
521
+ load_V(block=n_block, producer_state=kv_producer_state, page_idx=None)
522
+ kv_producer_state.advance()
523
+
524
+ return kv_producer_state
525
+
526
+
527
+ # SM100-specific tile processor using SM100 helpers
528
+ @cute.jit
529
+ def produce_block_sparse_loads_sm100(
530
+ blocksparse_tensors: BlockSparseTensors,
531
+ batch_idx,
532
+ head_idx,
533
+ m_block,
534
+ kv_producer_state,
535
+ load_Q,
536
+ load_K,
537
+ load_V,
538
+ pipeline_kv,
539
+ q_stage: cutlass.Constexpr,
540
+ q_producer_phase: Int32,
541
+ qhead_per_kvhead: cutlass.Constexpr,
542
+ q_subtile_factor: cutlass.Constexpr,
543
+ ):
544
+ """SM100 entry point for sparse block iteration.
545
+
546
+ SM100 uses PipelineTmaUmma which doesn't support extra_tx_count, so we use
547
+ simplified block processing that just calls producer_acquire without extras.
548
+
549
+ Args:
550
+ m_block: which tile of m we are processing
551
+ qhead_per_kvhead: Constexpr pack factor
552
+ """
553
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
554
+
555
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx, *_ = blocksparse_tensors
556
+
557
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
558
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
559
+
560
+ if const_expr(full_block_cnt is not None):
561
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
562
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
563
+ else:
564
+ curr_full_block_cnt = Int32(0)
565
+ curr_full_block_idx = None
566
+
567
+ mask_empty = curr_mask_block_cnt == 0
568
+ full_empty = curr_full_block_cnt == 0
569
+
570
+ q_phase_flipped = False
571
+
572
+ if mask_empty:
573
+ # No masked blocks: process full list with Q loading
574
+ kv_producer_state = load_block_list_sm100(
575
+ curr_full_block_idx,
576
+ curr_full_block_cnt,
577
+ load_q_with_first=True,
578
+ q_stage=q_stage,
579
+ kv_producer_state=kv_producer_state,
580
+ load_Q=load_Q,
581
+ load_K=load_K,
582
+ load_V=load_V,
583
+ pipeline_kv=pipeline_kv,
584
+ )
585
+ q_phase_flipped = not full_empty
586
+ else:
587
+ # Process masked blocks with Q loading
588
+ kv_producer_state = load_block_list_sm100(
589
+ curr_mask_block_idx,
590
+ curr_mask_block_cnt,
591
+ load_q_with_first=True,
592
+ q_stage=q_stage,
593
+ kv_producer_state=kv_producer_state,
594
+ load_Q=load_Q,
595
+ load_K=load_K,
596
+ load_V=load_V,
597
+ pipeline_kv=pipeline_kv,
598
+ )
599
+ q_phase_flipped = True
600
+
601
+ if not full_empty:
602
+ # Process full blocks without Q loading
603
+ kv_producer_state = load_block_list_sm100(
604
+ curr_full_block_idx,
605
+ curr_full_block_cnt,
606
+ load_q_with_first=False,
607
+ q_stage=q_stage,
608
+ kv_producer_state=kv_producer_state,
609
+ load_Q=load_Q,
610
+ load_K=load_K,
611
+ load_V=load_V,
612
+ pipeline_kv=pipeline_kv,
613
+ )
614
+
615
+ if q_phase_flipped:
616
+ q_producer_phase ^= 1
617
+
618
+ return kv_producer_state, q_producer_phase
619
+
620
+
621
+ @cute.jit
622
+ def get_total_block_count(
623
+ blocksparse_tensors: BlockSparseTensors,
624
+ batch_idx,
625
+ head_idx,
626
+ m_block,
627
+ qhead_per_kvhead: cutlass.Constexpr,
628
+ q_subtile_factor: cutlass.Constexpr,
629
+ ):
630
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
631
+
632
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx, *_ = blocksparse_tensors
633
+ if const_expr(full_block_cnt is not None):
634
+ return (
635
+ mask_block_cnt[batch_idx, head_idx, m_block_sparse]
636
+ + full_block_cnt[batch_idx, head_idx, m_block_sparse]
637
+ )
638
+ else:
639
+ return mask_block_cnt[batch_idx, head_idx, m_block_sparse]
640
+
641
+
642
+ @cute.jit
643
+ def handle_block_sparse_empty_tile_correction_sm100(
644
+ tidx: Int32,
645
+ q_stage: cutlass.Constexpr,
646
+ m_block_size: cutlass.Constexpr,
647
+ qhead_per_kvhead,
648
+ pack_gqa: cutlass.Constexpr,
649
+ is_split_kv: cutlass.Constexpr,
650
+ learnable_sink,
651
+ mLSE,
652
+ seqlen,
653
+ m_block: Int32,
654
+ head_idx: Int32,
655
+ batch_idx: Int32,
656
+ split_idx: Int32,
657
+ sScale: cute.Tensor,
658
+ stats: list,
659
+ correction_epilogue: Callable,
660
+ thr_mma_pv: cute.core.ThrMma,
661
+ tOtO: cute.Tensor,
662
+ sO: cute.Tensor,
663
+ pipeline_sm_stats: cutlass.pipeline.PipelineAsync,
664
+ sm_stats_barrier: cutlass.pipeline.NamedBarrier,
665
+ pipeline_o_epi: cutlass.pipeline.PipelineAsync,
666
+ sm_stats_consumer_phase: Int32,
667
+ o_corr_consumer_phase: Int32,
668
+ corr_epi_producer_phase: Int32,
669
+ softmax_scale_log2: Float32,
670
+ max_offset: Float32,
671
+ max_offset_scale: Float32,
672
+ mO_cur: Optional[cute.Tensor] = None,
673
+ gO: Optional[cute.Tensor] = None,
674
+ gmem_tiled_copy_O: Optional[cute.TiledCopy] = None,
675
+ ):
676
+ """Handle SM100 forward block-sparse tiles with no active KV blocks.
677
+
678
+ This path is taken when `total_block_cnt == 0`. The softmax warp-group still
679
+ arrives `mbar_softmax_corr_full` (synthetic "no work") so the correction
680
+ warp-group can:
681
+
682
+ - seed fully-masked-row stats (row_sum=1; row_max=-inf when tracked) for LSE
683
+ - run `correction_epilogue` with `scale=0` so the output tile is written as zeros
684
+ (independent of any prior tmem contents)
685
+ - wait on `mbar_softmax_corr_full` and arrive `mbar_softmax_corr_empty`
686
+ (and `mbar_corr_epi_*` when applicable) so phases stay aligned across tiles
687
+
688
+ This helper intentionally does not touch `mbar_P_full_*` since no P is produced.
689
+ See NOTE [SM100 block-sparse empty tiles: mbarrier contract].
690
+ """
691
+ LOG2_E = Float32(math.log2(math.e))
692
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % 4
693
+
694
+ for stage in cutlass.range_constexpr(q_stage):
695
+ row_sum_value = Float32(1.0)
696
+ row_max_value = (
697
+ -Float32.inf if const_expr(mLSE is not None or learnable_sink is not None) else None
698
+ )
699
+ if const_expr(learnable_sink is not None):
700
+ sink_val = -Float32.inf
701
+ if const_expr(not pack_gqa):
702
+ sink_val = Float32(learnable_sink[head_idx])
703
+ elif tidx < m_block_size:
704
+ q_head_idx = (
705
+ (q_stage * m_block + stage) * m_block_size + tidx
706
+ ) % qhead_per_kvhead + head_idx * qhead_per_kvhead
707
+ sink_val = Float32(learnable_sink[q_head_idx])
708
+ if sink_val != -Float32.inf and (const_expr(not is_split_kv) or split_idx == 0):
709
+ if row_max_value == -Float32.inf:
710
+ row_max_value = sink_val * (LOG2_E / softmax_scale_log2)
711
+ row_sum_value = max_offset_scale
712
+ else:
713
+ row_sum_value = row_sum_value + cute.math.exp2(
714
+ sink_val * LOG2_E - row_max_value * softmax_scale_log2 + max_offset,
715
+ fastmath=True,
716
+ )
717
+ if tidx < m_block_size:
718
+ scale_row_idx = tidx + stage * m_block_size
719
+ sScale[scale_row_idx] = row_sum_value
720
+ if const_expr(mLSE is not None or learnable_sink is not None):
721
+ sScale[scale_row_idx + q_stage * m_block_size] = row_max_value
722
+ acc_flag = row_sum_value == Float32(0.0) or row_sum_value != row_sum_value
723
+ stats[stage] = (row_sum_value, row_max_value, acc_flag)
724
+
725
+ # See NOTE [SM100 block-sparse empty tiles: mbarrier contract].
726
+ # pipeline_sm_stats.consumer_wait_w_index_phase(stage, sm_stats_consumer_phase)
727
+ sm_stats_barrier.arrive_and_wait_w_index(index=stage * 4 + warp_idx)
728
+ pipeline_sm_stats.consumer_release_w_index(stage)
729
+
730
+ if const_expr(gmem_tiled_copy_O is None):
731
+ pipeline_o_epi.producer_acquire_w_index_phase(stage, corr_epi_producer_phase)
732
+
733
+ gO_stage = gO[None, None, stage] if const_expr(gO is not None) else None
734
+ correction_epilogue(
735
+ thr_mma_pv,
736
+ tOtO[None, None, None, stage],
737
+ tidx,
738
+ stage,
739
+ m_block,
740
+ seqlen.seqlen_q,
741
+ Float32(0.0), # zero scale ensures empty tile writes zeros into staged outputs
742
+ sO[None, None, stage],
743
+ mO_cur,
744
+ gO_stage,
745
+ gmem_tiled_copy_O,
746
+ )
747
+ if const_expr(gmem_tiled_copy_O is None):
748
+ pipeline_o_epi.producer_commit_w_index(stage)
749
+
750
+ sm_stats_consumer_phase ^= 1
751
+ corr_epi_producer_phase ^= 1
752
+
753
+ return (
754
+ sm_stats_consumer_phase,
755
+ o_corr_consumer_phase,
756
+ corr_epi_producer_phase,
757
+ )
758
+
759
+
760
+ @cute.jit
761
+ def softmax_block_sparse_sm100(
762
+ blocksparse_tensors: BlockSparseTensors,
763
+ batch_idx,
764
+ head_idx,
765
+ m_block,
766
+ softmax_step: Callable,
767
+ mask_fn: Callable,
768
+ mask_fn_none: Callable,
769
+ mma_si_consumer_phase: Int32,
770
+ si_corr_producer_phase: Int32,
771
+ s0_s1_sequence_phase: Int32,
772
+ pipeline_sm_stats: cutlass.pipeline.PipelineAsync,
773
+ sm_stats_barrier: cutlass.pipeline.NamedBarrier,
774
+ q_stage: cutlass.Constexpr,
775
+ stage_idx: Int32,
776
+ check_m_boundary: bool,
777
+ qhead_per_kvhead: cutlass.Constexpr,
778
+ q_subtile_factor: cutlass.Constexpr[int] = 1,
779
+ ):
780
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % 4
781
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
782
+
783
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx, *_ = blocksparse_tensors
784
+
785
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
786
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
787
+
788
+ if const_expr(full_block_cnt is not None):
789
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
790
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
791
+ else:
792
+ curr_full_block_cnt = Int32(0)
793
+ curr_full_block_idx = None
794
+
795
+ total_block_cnt = curr_mask_block_cnt + curr_full_block_cnt
796
+
797
+ if total_block_cnt == 0:
798
+ sm_stats_barrier.arrive_w_index(index=stage_idx * 4 + warp_idx)
799
+ else:
800
+ if curr_mask_block_cnt > 0:
801
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
802
+ (
803
+ mma_si_consumer_phase,
804
+ si_corr_producer_phase,
805
+ s0_s1_sequence_phase,
806
+ ) = softmax_step(
807
+ mma_si_consumer_phase,
808
+ si_corr_producer_phase,
809
+ s0_s1_sequence_phase,
810
+ mask_n_block,
811
+ is_first=True,
812
+ mask_fn=partial(mask_fn, mask_seqlen=True, check_q_boundary=check_m_boundary),
813
+ )
814
+ for i in cutlass.range(1, curr_mask_block_cnt):
815
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1 - i]
816
+ (
817
+ mma_si_consumer_phase,
818
+ si_corr_producer_phase,
819
+ s0_s1_sequence_phase,
820
+ ) = softmax_step(
821
+ mma_si_consumer_phase,
822
+ si_corr_producer_phase,
823
+ s0_s1_sequence_phase,
824
+ mask_n_block,
825
+ mask_fn=partial(mask_fn, mask_seqlen=False, check_q_boundary=check_m_boundary),
826
+ )
827
+
828
+ if curr_full_block_cnt > 0:
829
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1]
830
+ if curr_mask_block_cnt == 0:
831
+ (
832
+ mma_si_consumer_phase,
833
+ si_corr_producer_phase,
834
+ s0_s1_sequence_phase,
835
+ ) = softmax_step(
836
+ mma_si_consumer_phase,
837
+ si_corr_producer_phase,
838
+ s0_s1_sequence_phase,
839
+ full_n_block,
840
+ is_first=True,
841
+ mask_fn=partial(
842
+ mask_fn_none, mask_seqlen=True, check_q_boundary=check_m_boundary
843
+ ),
844
+ )
845
+ else:
846
+ (
847
+ mma_si_consumer_phase,
848
+ si_corr_producer_phase,
849
+ s0_s1_sequence_phase,
850
+ ) = softmax_step(
851
+ mma_si_consumer_phase,
852
+ si_corr_producer_phase,
853
+ s0_s1_sequence_phase,
854
+ full_n_block,
855
+ is_first=False,
856
+ mask_fn=partial(
857
+ mask_fn_none, mask_seqlen=False, check_q_boundary=check_m_boundary
858
+ ),
859
+ )
860
+ for i in cutlass.range(1, curr_full_block_cnt):
861
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
862
+ (
863
+ mma_si_consumer_phase,
864
+ si_corr_producer_phase,
865
+ s0_s1_sequence_phase,
866
+ ) = softmax_step(
867
+ mma_si_consumer_phase,
868
+ si_corr_producer_phase,
869
+ s0_s1_sequence_phase,
870
+ full_n_block,
871
+ mask_fn=partial(
872
+ mask_fn_none, mask_seqlen=False, check_q_boundary=check_m_boundary
873
+ ),
874
+ )
875
+
876
+ return (
877
+ mma_si_consumer_phase,
878
+ si_corr_producer_phase,
879
+ s0_s1_sequence_phase,
880
+ total_block_cnt == 0,
881
+ )
882
+
883
+
884
+ # =============================================================================
885
+ # Backward-specific block-sparse helpers (SM100)
886
+ # =============================================================================
887
+ #
888
+ # In backward, iteration is transposed compared to forward:
889
+ # - Forward: outer loop over m_blocks (Q tiles), inner loop over n_blocks (KV tiles)
890
+ # - Backward: outer loop over n_blocks (KV tiles), inner loop over m_blocks (Q tiles)
891
+ #
892
+ # The backward block-sparse tensors use "Q direction" indexing:
893
+ # - q_block_cnt[batch, head, n_block] → count of m_blocks to process for this KV tile
894
+ # - q_block_idx[batch, head, n_block, :] → indices of m_blocks to process
895
+ #
896
+
897
+
898
+ @cute.jit
899
+ def get_total_q_block_count_bwd(
900
+ blocksparse_tensors: BlockSparseTensors,
901
+ batch_idx,
902
+ head_idx,
903
+ n_block,
904
+ subtile_factor: cutlass.Constexpr = 1,
905
+ m_block_max: int = 0,
906
+ ):
907
+ """Count total tile iterations for given n_block (KV tile) in backward."""
908
+ q_block_cnt, _, full_block_cnt, _, *_ = blocksparse_tensors
909
+ total = q_block_cnt[batch_idx, head_idx, n_block]
910
+ if const_expr(full_block_cnt is not None):
911
+ total = total + full_block_cnt[batch_idx, head_idx, n_block]
912
+ return total * subtile_factor
913
+
914
+
915
+ @cute.jit
916
+ def produce_block_sparse_q_loads_bwd_sm100(
917
+ blocksparse_tensors: BlockSparseTensors,
918
+ batch_idx,
919
+ head_idx,
920
+ n_block,
921
+ # Pipeline states (will be returned after advancing)
922
+ producer_state_Q_LSE,
923
+ producer_state_dO_dPsum,
924
+ # Pipelines
925
+ pipeline_Q,
926
+ pipeline_LSE,
927
+ pipeline_dO,
928
+ pipeline_dPsum,
929
+ # Load functions
930
+ load_K,
931
+ load_V,
932
+ load_Q,
933
+ load_dO,
934
+ copy_stats,
935
+ # Global tensors for LSE/dPsum
936
+ gLSE,
937
+ sLSE,
938
+ gdPsum,
939
+ sdPsum,
940
+ # TMA copy bytes for extra_tx_count
941
+ tma_copy_bytes_K,
942
+ tma_copy_bytes_V,
943
+ # Flags for which loads to perform
944
+ should_load_Q: cutlass.Constexpr,
945
+ should_load_dO: cutlass.Constexpr,
946
+ # Subtiling factor and bounds
947
+ subtile_factor: cutlass.Constexpr = 1,
948
+ m_block_max: int = 0,
949
+ ):
950
+ """SM100 backward block sparse loading with subtiling.
951
+
952
+ Returns updated (producer_state_Q_LSE, producer_state_dO_dPsum).
953
+ First iteration loads K/V alongside Q/dO; subsequent iterations load only Q/dO.
954
+ """
955
+ (
956
+ curr_q_cnt,
957
+ curr_q_idx,
958
+ curr_full_cnt,
959
+ curr_full_idx,
960
+ loop_count,
961
+ ) = get_block_sparse_iteration_info_bwd(
962
+ blocksparse_tensors, batch_idx, head_idx, n_block, subtile_factor, m_block_max
963
+ )
964
+
965
+ for iter_idx in cutlass.range(loop_count, unroll=1):
966
+ m_block, _ = get_m_block_from_iter_bwd(
967
+ iter_idx,
968
+ curr_q_cnt,
969
+ curr_q_idx,
970
+ curr_full_cnt,
971
+ curr_full_idx,
972
+ subtile_factor,
973
+ m_block_max,
974
+ )
975
+ m_block_safe = m_block
976
+ if m_block_max > 0:
977
+ m_block_safe = cutlass.min(m_block, m_block_max - 1)
978
+
979
+ if iter_idx == 0:
980
+ # First block: load K/V alongside Q/dO
981
+ if const_expr(should_load_Q):
982
+ pipeline_Q.producer_acquire(producer_state_Q_LSE, extra_tx_count=tma_copy_bytes_K)
983
+ load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q_LSE))
984
+ load_Q(m_block_safe, producer_state=producer_state_Q_LSE)
985
+ pipeline_Q.producer_commit(producer_state_Q_LSE)
986
+ pipeline_LSE.producer_acquire(producer_state_Q_LSE)
987
+ with cute.arch.elect_one():
988
+ copy_stats(
989
+ gLSE[None, m_block_safe],
990
+ sLSE[None, producer_state_Q_LSE.index],
991
+ mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_Q_LSE),
992
+ )
993
+ producer_state_Q_LSE.advance()
994
+ if const_expr(should_load_dO):
995
+ pipeline_dO.producer_acquire(
996
+ producer_state_dO_dPsum, extra_tx_count=tma_copy_bytes_V
997
+ )
998
+ load_V(tma_bar_ptr=pipeline_dO.producer_get_barrier(producer_state_dO_dPsum))
999
+ load_dO(m_block_safe, producer_state=producer_state_dO_dPsum)
1000
+ pipeline_dO.producer_commit(producer_state_dO_dPsum)
1001
+ pipeline_dPsum.producer_acquire(producer_state_dO_dPsum)
1002
+ with cute.arch.elect_one():
1003
+ copy_stats(
1004
+ gdPsum[None, m_block_safe],
1005
+ sdPsum[None, producer_state_dO_dPsum.index],
1006
+ mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dO_dPsum),
1007
+ )
1008
+ producer_state_dO_dPsum.advance()
1009
+ else:
1010
+ # Subsequent blocks: just load Q/dO (K/V already loaded)
1011
+ if const_expr(should_load_Q):
1012
+ pipeline_Q.producer_acquire(producer_state_Q_LSE)
1013
+ load_Q(m_block_safe, producer_state=producer_state_Q_LSE)
1014
+ pipeline_Q.producer_commit(producer_state_Q_LSE)
1015
+ pipeline_LSE.producer_acquire(producer_state_Q_LSE)
1016
+ with cute.arch.elect_one():
1017
+ copy_stats(
1018
+ gLSE[None, m_block_safe],
1019
+ sLSE[None, producer_state_Q_LSE.index],
1020
+ mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_Q_LSE),
1021
+ )
1022
+ producer_state_Q_LSE.advance()
1023
+ if const_expr(should_load_dO):
1024
+ pipeline_dO.producer_acquire(producer_state_dO_dPsum)
1025
+ load_dO(m_block_safe, producer_state=producer_state_dO_dPsum)
1026
+ pipeline_dO.producer_commit(producer_state_dO_dPsum)
1027
+ pipeline_dPsum.producer_acquire(producer_state_dO_dPsum)
1028
+ with cute.arch.elect_one():
1029
+ copy_stats(
1030
+ gdPsum[None, m_block_safe],
1031
+ sdPsum[None, producer_state_dO_dPsum.index],
1032
+ mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dO_dPsum),
1033
+ )
1034
+ producer_state_dO_dPsum.advance()
1035
+
1036
+ return producer_state_Q_LSE, producer_state_dO_dPsum
1037
+
1038
+
1039
+ @cute.jit
1040
+ def get_block_sparse_iteration_info_bwd(
1041
+ blocksparse_tensors: BlockSparseTensors,
1042
+ batch_idx,
1043
+ head_idx,
1044
+ n_block,
1045
+ subtile_factor: cutlass.Constexpr = 1,
1046
+ m_block_max: int = 0,
1047
+ ):
1048
+ """Extract block-sparse iteration info for backward pass.
1049
+
1050
+ Returns (curr_q_cnt, curr_q_idx, curr_full_cnt, curr_full_idx, total_count).
1051
+ """
1052
+ q_cnt, q_idx, full_cnt, full_idx, *_ = blocksparse_tensors
1053
+ curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1054
+ curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1055
+
1056
+ if const_expr(full_cnt is not None):
1057
+ curr_full_cnt = full_cnt[batch_idx, head_idx, n_block]
1058
+ curr_full_idx = full_idx[batch_idx, head_idx, n_block, None]
1059
+ else:
1060
+ curr_full_cnt = Int32(0)
1061
+ curr_full_idx = None
1062
+
1063
+ sparse_block_count = curr_q_cnt
1064
+ if const_expr(full_cnt is not None):
1065
+ sparse_block_count = sparse_block_count + curr_full_cnt
1066
+ total_count = sparse_block_count * subtile_factor
1067
+
1068
+ return curr_q_cnt, curr_q_idx, curr_full_cnt, curr_full_idx, total_count
1069
+
1070
+
1071
+ @cute.jit
1072
+ def get_m_block_from_iter_bwd(
1073
+ iter_idx,
1074
+ curr_q_cnt,
1075
+ curr_q_idx: cute.Tensor,
1076
+ curr_full_cnt,
1077
+ curr_full_idx: Optional[cute.Tensor],
1078
+ subtile_factor: cutlass.Constexpr = 1,
1079
+ m_block_max: int = 0,
1080
+ ):
1081
+ """Derive m_block index and is_full_block flag from iteration index.
1082
+
1083
+ Returns (m_block, is_full_block):
1084
+ - m_block: The actual Q-tile block index
1085
+ - is_full_block: True if this is a full block (no mask_mod needed)
1086
+ """
1087
+ sparse_iter_idx = iter_idx // subtile_factor
1088
+ subtile_offset = iter_idx % subtile_factor
1089
+
1090
+ sparse_m_block = Int32(0)
1091
+ is_full_block = False
1092
+ if const_expr(curr_full_idx is not None):
1093
+ if sparse_iter_idx < curr_q_cnt:
1094
+ sparse_m_block = curr_q_idx[sparse_iter_idx]
1095
+ else:
1096
+ sparse_m_block = curr_full_idx[sparse_iter_idx - curr_q_cnt]
1097
+ is_full_block = True
1098
+ else:
1099
+ sparse_m_block = curr_q_idx[sparse_iter_idx]
1100
+
1101
+ return sparse_m_block * subtile_factor + subtile_offset, is_full_block
1102
+
1103
+
1104
+ @cute.jit
1105
+ def _load_q_do_block_sm90(
1106
+ m_block,
1107
+ producer_state_Q,
1108
+ producer_state_dO,
1109
+ pipeline_Q,
1110
+ pipeline_dO,
1111
+ load_K,
1112
+ load_V,
1113
+ load_Q,
1114
+ load_dO,
1115
+ load_LSE,
1116
+ load_dPsum,
1117
+ tma_copy_bytes_K,
1118
+ tma_copy_bytes_V,
1119
+ Q_stage_eq_dO_stage: cutlass.Constexpr,
1120
+ load_kv: bool,
1121
+ ):
1122
+ """Load one Q/dO block, optionally loading K/V on first iteration."""
1123
+ if load_kv:
1124
+ pipeline_Q.producer_acquire(producer_state_Q, extra_tx_count=tma_copy_bytes_K)
1125
+ load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q))
1126
+ else:
1127
+ pipeline_Q.producer_acquire(producer_state_Q)
1128
+ load_Q(m_block, producer_state=producer_state_Q)
1129
+ load_LSE(m_block, producer_state=producer_state_Q)
1130
+
1131
+ producer_state_dO_cur = (
1132
+ producer_state_dO if const_expr(not Q_stage_eq_dO_stage) else producer_state_Q
1133
+ )
1134
+ if load_kv:
1135
+ pipeline_dO.producer_acquire(producer_state_dO_cur, extra_tx_count=tma_copy_bytes_V)
1136
+ load_V(tma_bar_ptr=pipeline_dO.producer_get_barrier(producer_state_dO_cur))
1137
+ else:
1138
+ pipeline_dO.producer_acquire(producer_state_dO_cur)
1139
+ load_dO(m_block, producer_state=producer_state_dO_cur)
1140
+ load_dPsum(m_block, producer_state=producer_state_dO_cur)
1141
+
1142
+ producer_state_Q.advance()
1143
+ producer_state_dO.advance()
1144
+ return producer_state_Q, producer_state_dO
1145
+
1146
+
1147
+ @cute.jit
1148
+ def produce_block_sparse_q_loads_bwd_sm90(
1149
+ blocksparse_tensors: BlockSparseTensors,
1150
+ batch_idx,
1151
+ head_idx,
1152
+ n_block,
1153
+ producer_state_Q,
1154
+ producer_state_dO,
1155
+ pipeline_Q,
1156
+ pipeline_dO,
1157
+ load_K,
1158
+ load_V,
1159
+ load_Q,
1160
+ load_dO,
1161
+ load_LSE,
1162
+ load_dPsum,
1163
+ tma_copy_bytes_K,
1164
+ tma_copy_bytes_V,
1165
+ Q_stage_eq_dO_stage: cutlass.Constexpr,
1166
+ subtile_factor: cutlass.Constexpr,
1167
+ m_block_max: int,
1168
+ ):
1169
+ """SM90 backward block sparse loading with separate partial/full loops.
1170
+
1171
+ K/V are loaded with the first valid block. Iterates partial blocks first,
1172
+ then full blocks, matching consumer order.
1173
+
1174
+ Returns updated (producer_state_Q, producer_state_dO).
1175
+ """
1176
+ q_cnt, q_idx, full_cnt, full_idx, *_ = blocksparse_tensors
1177
+ curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1178
+ curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1179
+
1180
+ if const_expr(full_cnt is not None):
1181
+ curr_full_cnt = full_cnt[batch_idx, head_idx, n_block]
1182
+ curr_full_idx = full_idx[batch_idx, head_idx, n_block, None]
1183
+ else:
1184
+ curr_full_cnt = Int32(0)
1185
+ curr_full_idx = None
1186
+
1187
+ kv_loaded = False
1188
+
1189
+ for iter_idx in cutlass.range(curr_q_cnt * subtile_factor, unroll=1):
1190
+ sparse_idx = iter_idx // subtile_factor
1191
+ subtile_offset = iter_idx % subtile_factor
1192
+ m_block = curr_q_idx[sparse_idx] * subtile_factor + subtile_offset
1193
+
1194
+ if m_block < m_block_max:
1195
+ producer_state_Q, producer_state_dO = _load_q_do_block_sm90(
1196
+ m_block,
1197
+ producer_state_Q,
1198
+ producer_state_dO,
1199
+ pipeline_Q,
1200
+ pipeline_dO,
1201
+ load_K,
1202
+ load_V,
1203
+ load_Q,
1204
+ load_dO,
1205
+ load_LSE,
1206
+ load_dPsum,
1207
+ tma_copy_bytes_K,
1208
+ tma_copy_bytes_V,
1209
+ Q_stage_eq_dO_stage,
1210
+ load_kv=not kv_loaded,
1211
+ )
1212
+ kv_loaded = True
1213
+
1214
+ if const_expr(full_cnt is not None):
1215
+ for iter_idx in cutlass.range(curr_full_cnt * subtile_factor, unroll=1):
1216
+ sparse_idx = iter_idx // subtile_factor
1217
+ subtile_offset = iter_idx % subtile_factor
1218
+ m_block = curr_full_idx[sparse_idx] * subtile_factor + subtile_offset
1219
+
1220
+ if m_block < m_block_max:
1221
+ producer_state_Q, producer_state_dO = _load_q_do_block_sm90(
1222
+ m_block,
1223
+ producer_state_Q,
1224
+ producer_state_dO,
1225
+ pipeline_Q,
1226
+ pipeline_dO,
1227
+ load_K,
1228
+ load_V,
1229
+ load_Q,
1230
+ load_dO,
1231
+ load_LSE,
1232
+ load_dPsum,
1233
+ tma_copy_bytes_K,
1234
+ tma_copy_bytes_V,
1235
+ Q_stage_eq_dO_stage,
1236
+ load_kv=not kv_loaded,
1237
+ )
1238
+ kv_loaded = True
1239
+
1240
+ return producer_state_Q, producer_state_dO
1241
+
1242
+
1243
+ @cute.jit
1244
+ def consume_block_sparse_mma_bwd_sm90(
1245
+ blocksparse_tensors: BlockSparseTensors,
1246
+ batch_idx,
1247
+ head_idx,
1248
+ n_block,
1249
+ consumer_state_Q,
1250
+ consumer_state_dO,
1251
+ mma_one_m_block_fn,
1252
+ mask,
1253
+ mask_mod,
1254
+ is_causal: cutlass.Constexpr,
1255
+ is_local: cutlass.Constexpr,
1256
+ thr_mma_SdP,
1257
+ score_mod_fn=None,
1258
+ score_mod_bwd_fn=None,
1259
+ subtile_factor: cutlass.Constexpr = 1,
1260
+ m_block_max: int = 0,
1261
+ aux_tensors=None,
1262
+ fastdiv_mods=(None, None),
1263
+ ):
1264
+ """SM90 backward block sparse MMA consumption with separate partial/full loops.
1265
+
1266
+ Partial blocks are processed first (with mask_mod applied), then full blocks
1267
+ (without mask_mod). This ensures mask_mod is only applied where needed.
1268
+
1269
+ Returns updated (consumer_state_Q, consumer_state_dO).
1270
+ """
1271
+ q_cnt, q_idx, full_cnt, full_idx, *_ = blocksparse_tensors
1272
+ curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1273
+ curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1274
+
1275
+ if const_expr(full_cnt is not None):
1276
+ curr_full_cnt = full_cnt[batch_idx, head_idx, n_block]
1277
+ curr_full_idx = full_idx[batch_idx, head_idx, n_block, None]
1278
+ else:
1279
+ curr_full_cnt = Int32(0)
1280
+ curr_full_idx = None
1281
+
1282
+ dKV_accumulate = False
1283
+
1284
+ mask_fn_partial = partial(
1285
+ mask.apply_mask,
1286
+ batch_idx=batch_idx,
1287
+ head_idx=head_idx,
1288
+ n_block=n_block,
1289
+ thr_mma=thr_mma_SdP,
1290
+ mask_seqlen=True,
1291
+ mask_causal=is_causal,
1292
+ mask_local=is_local,
1293
+ mask_mod=mask_mod,
1294
+ aux_tensors=aux_tensors,
1295
+ fastdiv_mods=fastdiv_mods,
1296
+ )
1297
+
1298
+ mask_fn_full = partial(
1299
+ mask.apply_mask,
1300
+ batch_idx=batch_idx,
1301
+ head_idx=head_idx,
1302
+ n_block=n_block,
1303
+ thr_mma=thr_mma_SdP,
1304
+ mask_seqlen=True,
1305
+ mask_causal=is_causal,
1306
+ mask_local=is_local,
1307
+ aux_tensors=aux_tensors,
1308
+ fastdiv_mods=fastdiv_mods,
1309
+ )
1310
+
1311
+ for iter_idx in cutlass.range(curr_q_cnt * subtile_factor, unroll=1):
1312
+ sparse_idx = iter_idx // subtile_factor
1313
+ subtile_offset = iter_idx % subtile_factor
1314
+ m_block = curr_q_idx[sparse_idx] * subtile_factor + subtile_offset
1315
+
1316
+ if m_block < m_block_max:
1317
+ consumer_state_Q, consumer_state_dO = mma_one_m_block_fn(
1318
+ m_block,
1319
+ consumer_state_Q,
1320
+ consumer_state_dO,
1321
+ mask_fn=mask_fn_partial,
1322
+ score_mod_fn=score_mod_fn,
1323
+ score_mod_bwd_fn=score_mod_bwd_fn,
1324
+ dKV_accumulate=dKV_accumulate,
1325
+ )
1326
+ dKV_accumulate = True
1327
+
1328
+ if const_expr(full_cnt is not None):
1329
+ for iter_idx in cutlass.range(curr_full_cnt * subtile_factor, unroll=1):
1330
+ sparse_idx = iter_idx // subtile_factor
1331
+ subtile_offset = iter_idx % subtile_factor
1332
+ m_block = curr_full_idx[sparse_idx] * subtile_factor + subtile_offset
1333
+
1334
+ if m_block < m_block_max:
1335
+ consumer_state_Q, consumer_state_dO = mma_one_m_block_fn(
1336
+ m_block,
1337
+ consumer_state_Q,
1338
+ consumer_state_dO,
1339
+ mask_fn=mask_fn_full,
1340
+ score_mod_fn=score_mod_fn,
1341
+ score_mod_bwd_fn=score_mod_bwd_fn,
1342
+ dKV_accumulate=dKV_accumulate,
1343
+ )
1344
+ dKV_accumulate = True
1345
+
1346
+ return consumer_state_Q, consumer_state_dO
1347
+
1348
+
1349
+ @cute.jit
1350
+ def _store_one_dQaccum_sm90(
1351
+ m_block,
1352
+ sdQaccum: cute.Tensor,
1353
+ gdQaccum: cute.Tensor,
1354
+ num_dQ_warp_groups: cutlass.Constexpr,
1355
+ num_threads_per_warp_group: cutlass.Constexpr,
1356
+ tma_copy_bytes_dQ,
1357
+ ):
1358
+ """Store dQaccum for a single m_block."""
1359
+ for warp_group_idx in cutlass.range_constexpr(num_dQ_warp_groups):
1360
+ cute.arch.cp_async_bulk_wait_group(num_dQ_warp_groups - 1 - warp_group_idx, read=True)
1361
+ cute.arch.barrier_arrive(
1362
+ barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1363
+ number_of_threads=num_threads_per_warp_group + cute.arch.WARP_SIZE,
1364
+ )
1365
+ for warp_group_idx in cutlass.range_constexpr(num_dQ_warp_groups):
1366
+ cute.arch.barrier(
1367
+ barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1368
+ number_of_threads=num_threads_per_warp_group + cute.arch.WARP_SIZE,
1369
+ )
1370
+ with cute.arch.elect_one():
1371
+ copy_utils.cpasync_reduce_bulk_add_f32(
1372
+ sdQaccum[None, warp_group_idx].iterator,
1373
+ gdQaccum[(None, warp_group_idx), m_block].iterator,
1374
+ tma_copy_bytes_dQ,
1375
+ )
1376
+ cute.arch.cp_async_bulk_commit_group()
1377
+
1378
+
1379
+ @cute.jit
1380
+ def dQaccum_store_block_sparse_bwd_sm90(
1381
+ blocksparse_tensors: BlockSparseTensors,
1382
+ batch_idx,
1383
+ head_idx,
1384
+ n_block,
1385
+ sdQaccum: cute.Tensor,
1386
+ gdQaccum: cute.Tensor,
1387
+ subtile_factor: cutlass.Constexpr,
1388
+ m_block_max: int,
1389
+ num_dQ_warp_groups: cutlass.Constexpr,
1390
+ num_threads_per_warp_group: cutlass.Constexpr,
1391
+ tma_copy_bytes_dQ,
1392
+ ):
1393
+ """SM90 backward block sparse dQaccum store with separate partial/full loops.
1394
+
1395
+ Iterates partial blocks first, then full blocks, matching producer/consumer order.
1396
+ """
1397
+ q_cnt, q_idx, full_cnt, full_idx, *_ = blocksparse_tensors
1398
+ curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1399
+ curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1400
+
1401
+ if const_expr(full_cnt is not None):
1402
+ curr_full_cnt = full_cnt[batch_idx, head_idx, n_block]
1403
+ curr_full_idx = full_idx[batch_idx, head_idx, n_block, None]
1404
+ else:
1405
+ curr_full_cnt = Int32(0)
1406
+ curr_full_idx = None
1407
+
1408
+ for iter_idx in cutlass.range(curr_q_cnt * subtile_factor, unroll=1):
1409
+ sparse_idx = iter_idx // subtile_factor
1410
+ subtile_offset = iter_idx % subtile_factor
1411
+ m_block = curr_q_idx[sparse_idx] * subtile_factor + subtile_offset
1412
+
1413
+ if m_block < m_block_max:
1414
+ _store_one_dQaccum_sm90(
1415
+ m_block,
1416
+ sdQaccum,
1417
+ gdQaccum,
1418
+ num_dQ_warp_groups,
1419
+ num_threads_per_warp_group,
1420
+ tma_copy_bytes_dQ,
1421
+ )
1422
+
1423
+ if const_expr(full_cnt is not None):
1424
+ for iter_idx in cutlass.range(curr_full_cnt * subtile_factor, unroll=1):
1425
+ sparse_idx = iter_idx // subtile_factor
1426
+ subtile_offset = iter_idx % subtile_factor
1427
+ m_block = curr_full_idx[sparse_idx] * subtile_factor + subtile_offset
1428
+
1429
+ if m_block < m_block_max:
1430
+ _store_one_dQaccum_sm90(
1431
+ m_block,
1432
+ sdQaccum,
1433
+ gdQaccum,
1434
+ num_dQ_warp_groups,
1435
+ num_threads_per_warp_group,
1436
+ tma_copy_bytes_dQ,
1437
+ )
build/torch-cuda/flashrt_fa4/cute/block_sparsity.py ADDED
@@ -0,0 +1,638 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Block-sparsity utilities for FlexAttention
3
+ """
4
+
5
+ from typing import Callable, NamedTuple, Tuple
6
+
7
+ import cutlass.cute as cute
8
+ import torch
9
+
10
+ from fa4_cute_runtime.flashrt_fa4.cute.cute_dsl_utils import get_broadcast_dims, to_cute_tensor
11
+
12
+
13
+ def ceildiv(a: int, b: int) -> int:
14
+ return (a + b - 1) // b
15
+
16
+
17
+ class BlockSparseTensors(NamedTuple):
18
+ mask_block_cnt: cute.Tensor
19
+ mask_block_idx: cute.Tensor
20
+ full_block_cnt: cute.Tensor | None
21
+ full_block_idx: cute.Tensor | None
22
+ dq_write_order: cute.Tensor | None = None
23
+ dq_write_order_full: cute.Tensor | None = None
24
+
25
+ def __new_from_mlir_values__(self, values):
26
+ if len(values) == 2:
27
+ values = (*values, None, None, None, None)
28
+ elif len(values) == 4:
29
+ values = (*values, None, None)
30
+ return BlockSparseTensors(*values)
31
+
32
+
33
+ class BlockSparseTensorsTorch(NamedTuple):
34
+ mask_block_cnt: torch.Tensor
35
+ mask_block_idx: torch.Tensor
36
+ full_block_cnt: torch.Tensor | None = None
37
+ full_block_idx: torch.Tensor | None = None
38
+ block_size: tuple[int, int] | None = None
39
+ dq_write_order: torch.Tensor | None = None
40
+ dq_write_order_full: torch.Tensor | None = None
41
+ spt: bool | None = None
42
+
43
+
44
+ def _ordered_to_dense_simple(
45
+ num_blocks: torch.Tensor,
46
+ indices: torch.Tensor,
47
+ num_cols: int,
48
+ ) -> torch.Tensor:
49
+ """Convert ordered sparse representation to dense binary matrix.
50
+
51
+ Args:
52
+ num_blocks: [B, H, num_rows] count of valid entries per row
53
+ indices: [B, H, num_rows, max_entries] column indices (valid entries packed left)
54
+ num_cols: total number of columns
55
+
56
+ Returns:
57
+ dense: [B, H, num_rows, num_cols] binary int32 matrix
58
+ """
59
+ B, H, num_rows, max_entries = indices.shape
60
+ device = indices.device
61
+ dense = torch.zeros(B, H, num_rows, num_cols + 1, dtype=torch.int32, device=device)
62
+ col_range = torch.arange(max_entries, device=device)
63
+ valid = col_range[None, None, None, :] < num_blocks[:, :, :, None]
64
+ safe_indices = torch.where(valid, indices.long(), num_cols)
65
+ row_idx = torch.arange(num_rows, device=device)[None, None, :, None].expand_as(indices)
66
+ b_idx = torch.arange(B, device=device)[:, None, None, None].expand_as(indices)
67
+ h_idx = torch.arange(H, device=device)[None, :, None, None].expand_as(indices)
68
+ dense[b_idx, h_idx, row_idx, safe_indices] = 1
69
+ return dense[:, :, :, :num_cols]
70
+
71
+
72
+ def compute_dq_write_order(
73
+ fwd_mask_cnt: torch.Tensor,
74
+ fwd_mask_idx: torch.Tensor,
75
+ fwd_full_cnt: torch.Tensor | None,
76
+ fwd_full_idx: torch.Tensor | None,
77
+ bwd_mask_cnt: torch.Tensor,
78
+ bwd_mask_idx: torch.Tensor,
79
+ bwd_full_cnt: torch.Tensor | None,
80
+ bwd_full_idx: torch.Tensor | None,
81
+ spt: bool = False,
82
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
83
+ """Compute dQ write-order metadata for deterministic block-sparse backward.
84
+
85
+ For each (n_block, i) in the backward iteration, computes the semaphore
86
+ lock value: the rank of n_block in the combined (partial + full) sorted
87
+ contributor list for the target m_block.
88
+
89
+ Lock values are assigned in ascending n_block order (or descending if spt=True)
90
+ to guarantee deadlock-freedom with the CTA scheduling order.
91
+
92
+ Args:
93
+ fwd_mask_cnt: [B, H, num_m_blocks] partial contributor counts per m_block
94
+ fwd_mask_idx: [B, H, num_m_blocks, max_kv] partial contributor n_block indices (ascending)
95
+ fwd_full_cnt: [B, H, num_m_blocks] full contributor counts per m_block (optional)
96
+ fwd_full_idx: [B, H, num_m_blocks, max_kv] full contributor n_block indices (optional)
97
+ bwd_mask_cnt: [B, H, num_n_blocks] partial iteration counts per n_block
98
+ bwd_mask_idx: [B, H, num_n_blocks, max_q] partial iteration m_block indices
99
+ bwd_full_cnt: [B, H, num_n_blocks] full iteration counts per n_block (optional)
100
+ bwd_full_idx: [B, H, num_n_blocks, max_q] full iteration m_block indices (optional)
101
+ spt: if True, reverse ordering (highest n_block gets lock_value=0)
102
+
103
+ Returns:
104
+ (dq_write_order, dq_write_order_full): tensors parallel to bwd_mask_idx
105
+ and bwd_full_idx respectively, containing lock values.
106
+ """
107
+ device = fwd_mask_idx.device
108
+ B, H, num_m, max_kv_partial = fwd_mask_idx.shape
109
+ _, _, num_n, max_q_partial = bwd_mask_idx.shape
110
+
111
+ has_full = fwd_full_cnt is not None and fwd_full_idx is not None
112
+
113
+ dense_partial = _ordered_to_dense_simple(fwd_mask_cnt, fwd_mask_idx, num_n)
114
+ if has_full:
115
+ dense_full = _ordered_to_dense_simple(fwd_full_cnt, fwd_full_idx, num_n)
116
+ dense = (dense_partial + dense_full).clamp(max=1)
117
+ else:
118
+ dense = dense_partial
119
+
120
+ cumsum = dense.cumsum(dim=-1)
121
+ rank_table = (cumsum - dense).to(torch.int32)
122
+
123
+ if spt:
124
+ total_per_m = cumsum[:, :, :, -1:]
125
+ rank_table = (total_per_m - 1 - rank_table).to(torch.int32)
126
+
127
+ def _gather_write_order(bwd_idx, bwd_cnt):
128
+ b_i = torch.arange(B, device=device)[:, None, None, None].expand_as(bwd_idx)
129
+ h_i = torch.arange(H, device=device)[None, :, None, None].expand_as(bwd_idx)
130
+ n_i = torch.arange(bwd_idx.shape[2], device=device)[None, None, :, None].expand_as(bwd_idx)
131
+ m_vals = bwd_idx.long().clamp(0, num_m - 1)
132
+ return rank_table[b_i, h_i, m_vals, n_i].to(torch.int32)
133
+
134
+ dq_write_order = _gather_write_order(bwd_mask_idx, bwd_mask_cnt)
135
+
136
+ dq_write_order_full = None
137
+ if has_full and bwd_full_cnt is not None and bwd_full_idx is not None:
138
+ dq_write_order_full = _gather_write_order(bwd_full_idx, bwd_full_cnt)
139
+
140
+ return dq_write_order, dq_write_order_full
141
+
142
+
143
+ def compute_dq_write_order_from_block_mask(
144
+ block_mask,
145
+ spt: bool = False,
146
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
147
+ (
148
+ _seq_q,
149
+ _seq_k,
150
+ kv_mask_cnt,
151
+ kv_mask_idx,
152
+ full_kv_cnt,
153
+ full_kv_idx,
154
+ q_mask_cnt,
155
+ q_mask_idx,
156
+ full_q_cnt,
157
+ full_q_idx,
158
+ *_,
159
+ ) = block_mask.as_tuple()
160
+ return compute_dq_write_order(
161
+ kv_mask_cnt,
162
+ kv_mask_idx,
163
+ full_kv_cnt,
164
+ full_kv_idx,
165
+ q_mask_cnt,
166
+ q_mask_idx,
167
+ full_q_cnt,
168
+ full_q_idx,
169
+ spt=spt,
170
+ )
171
+
172
+
173
+ def get_sparse_q_block_size(
174
+ tensors: BlockSparseTensorsTorch | None,
175
+ seqlen_q: int,
176
+ ) -> int | None:
177
+ """Return the Q sparse block size, or None when sparsity is unset or ambiguous."""
178
+ if tensors is None:
179
+ return None
180
+ if tensors.block_size is not None:
181
+ return tensors.block_size[0]
182
+ num_m_blocks = tensors.mask_block_idx.shape[2]
183
+ min_block_size = ceildiv(seqlen_q, num_m_blocks)
184
+ max_block_size = seqlen_q if num_m_blocks == 1 else (seqlen_q - 1) // (num_m_blocks - 1)
185
+ if min_block_size != max_block_size:
186
+ return None
187
+ return min_block_size
188
+
189
+
190
+ def _expand_sparsity_tensor(
191
+ tensor: torch.Tensor,
192
+ expected_shape: Tuple[int, ...],
193
+ tensor_name: str,
194
+ context: str | None,
195
+ hint: str | Callable[[], str] | None,
196
+ ) -> torch.Tensor:
197
+ """Check if we need to expand the tensor to expected shape, and do so if possible."""
198
+ needs_expand = tensor.shape != expected_shape
199
+ if not needs_expand:
200
+ return tensor
201
+ can_expand = all(map(lambda cur, tgt: cur == tgt or cur == 1, tensor.shape, expected_shape))
202
+ if not can_expand:
203
+ context_clause = f" ({context})" if context else ""
204
+ resolved_hint = hint() if callable(hint) else hint
205
+ hint_clause = f" Hint: {resolved_hint}" if resolved_hint else ""
206
+ raise ValueError(
207
+ f"{tensor_name}{context_clause} with shape {tensor.shape} cannot be expanded to expected shape {expected_shape}."
208
+ f"{hint_clause}"
209
+ )
210
+ return tensor.expand(*expected_shape)
211
+
212
+
213
+ def _check_and_expand_block(
214
+ name: str,
215
+ cnt: torch.Tensor | None,
216
+ idx: torch.Tensor | None,
217
+ expected_count_shape: Tuple[int, int, int],
218
+ expected_index_shape: Tuple[int, int, int, int],
219
+ context: str | None,
220
+ hint: str | Callable[[], str] | None,
221
+ ) -> Tuple[torch.Tensor | None, torch.Tensor | None]:
222
+ if (cnt is None) != (idx is None):
223
+ raise ValueError(
224
+ f"{name}_block_cnt and {name}_block_idx must both be provided or both be None"
225
+ )
226
+ if cnt is None or idx is None:
227
+ return None, None
228
+ if cnt.dtype != torch.int32 or idx.dtype != torch.int32:
229
+ raise ValueError(f"{name}_block tensors must have dtype torch.int32")
230
+ if cnt.device != idx.device:
231
+ raise ValueError(f"{name}_block_cnt and {name}_block_idx must be on the same device")
232
+ if not cnt.is_cuda or not idx.is_cuda:
233
+ raise ValueError(f"{name}_block tensors must live on CUDA")
234
+ expanded_cnt = _expand_sparsity_tensor(
235
+ cnt, expected_count_shape, f"{name}_block_cnt", context, hint
236
+ )
237
+ # [Note] Allow Compact block sparse indices
238
+ # Allow the last dimension (n_blocks) of idx to be <= expected, since
239
+ # FA4 only accesses indices 0..cnt-1 per query tile. This enables compact
240
+ # index tensors that avoid O(N^2) memory at long sequence lengths.
241
+ if idx.ndim == 4 and idx.shape[3] <= expected_index_shape[3]:
242
+ expected_index_shape = (*expected_index_shape[:3], idx.shape[3])
243
+ expanded_idx = _expand_sparsity_tensor(
244
+ idx, expected_index_shape, f"{name}_block_idx", context, hint
245
+ )
246
+ return expanded_cnt, expanded_idx
247
+
248
+
249
+ def _check_and_expand_metadata_tensor(
250
+ name: str,
251
+ tensor: torch.Tensor | None,
252
+ expected_shape: Tuple[int, ...],
253
+ context: str | None,
254
+ hint: str | Callable[[], str] | None,
255
+ device: torch.device,
256
+ ) -> torch.Tensor | None:
257
+ if tensor is None:
258
+ return None
259
+ if tensor.dtype != torch.int32:
260
+ raise ValueError(f"{name} must have dtype torch.int32")
261
+ if tensor.device != device:
262
+ raise ValueError(f"{name} must be on the same device as block sparse tensors")
263
+ if not tensor.is_cuda:
264
+ raise ValueError(f"{name} must live on CUDA")
265
+ return _expand_sparsity_tensor(tensor, expected_shape, name, context, hint)
266
+
267
+
268
+ def get_block_sparse_expected_shapes(
269
+ batch_size: int,
270
+ num_head: int,
271
+ seqlen_q: int,
272
+ seqlen_k: int,
273
+ m_block_size: int,
274
+ n_block_size: int,
275
+ q_stage: int,
276
+ ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
277
+ """Return (expected_count_shape, expected_index_shape) for block sparse normalization."""
278
+ m_block_size_effective = q_stage * m_block_size
279
+ expected_m_blocks = ceildiv(seqlen_q, m_block_size_effective)
280
+ expected_n_blocks = ceildiv(seqlen_k, n_block_size)
281
+ expected_count_shape = (batch_size, num_head, expected_m_blocks)
282
+ expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
283
+ return expected_count_shape, expected_index_shape
284
+
285
+
286
+ def infer_block_sparse_expected_shapes(
287
+ tensors: BlockSparseTensorsTorch,
288
+ *,
289
+ batch_size: int,
290
+ num_head: int,
291
+ seqlen_q: int,
292
+ seqlen_k: int,
293
+ m_block_size: int,
294
+ n_block_size: int,
295
+ q_stage: int,
296
+ context: str,
297
+ sparse_block_size_q: int | None = None,
298
+ sparse_block_size_kv: int | None = None,
299
+ ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int], int]:
300
+ """Infer shapes and scaling for block-sparse tensors.
301
+
302
+ Expectations:
303
+ - mask_block_cnt is (B, H, M) and mask_block_idx is (B, H, M, N).
304
+ - Batch/head dims may be 1 for broadcast, or match the requested sizes.
305
+ - sparse_block_size_kv must match tile_n.
306
+ - sparse_block_size_q must be a multiple of q_stage * tile_m.
307
+ - If sparse_block_size_q is omitted and seqlen_q/num_m_blocks is ambiguous,
308
+ the caller must provide block_size to disambiguate. TODO will make this required in a future PR.
309
+ """
310
+ base_m_block = q_stage * m_block_size
311
+ base_n_block = n_block_size
312
+ if sparse_block_size_kv is None:
313
+ sparse_block_size_kv = base_n_block
314
+ if sparse_block_size_kv != base_n_block:
315
+ raise ValueError(f"Block sparse tensors{context} require BLOCK_SIZE_KV={base_n_block}.")
316
+ if tensors.mask_block_idx is None:
317
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
318
+ num_m_blocks = tensors.mask_block_idx.shape[2]
319
+
320
+ if sparse_block_size_q is None:
321
+ sparse_block_size_q = get_sparse_q_block_size(tensors, seqlen_q)
322
+ if sparse_block_size_q is None and base_m_block != 1:
323
+ raise ValueError(
324
+ f"Block sparse tensors{context} require explicit sparse_block_size[0] "
325
+ f"to disambiguate block size for seqlen_q={seqlen_q} and num_m_blocks={num_m_blocks}."
326
+ )
327
+ if sparse_block_size_q is None:
328
+ sparse_block_size_q = ceildiv(seqlen_q, num_m_blocks)
329
+
330
+ if sparse_block_size_q % base_m_block != 0:
331
+ raise ValueError(
332
+ f"Block sparse tensors{context} have block size {sparse_block_size_q}, "
333
+ f"which must be a multiple of {base_m_block}."
334
+ )
335
+
336
+ expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
337
+ expected_n_blocks = ceildiv(seqlen_k, sparse_block_size_kv)
338
+ q_subtile_factor = sparse_block_size_q // base_m_block
339
+ expected_count_shape = (batch_size, num_head, expected_m_blocks)
340
+ expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
341
+
342
+ mask_block_cnt = tensors.mask_block_cnt
343
+ mask_block_idx = tensors.mask_block_idx
344
+ if mask_block_cnt is None or mask_block_idx is None:
345
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
346
+ if mask_block_cnt.ndim != 3 or mask_block_idx.ndim != 4:
347
+ raise ValueError(
348
+ f"Block sparse tensors{context} must have shapes (B, H, M) and (B, H, M, N)."
349
+ )
350
+ for dim_name, cur, tgt in (
351
+ ("batch", mask_block_cnt.shape[0], expected_count_shape[0]),
352
+ ("head", mask_block_cnt.shape[1], expected_count_shape[1]),
353
+ ):
354
+ if cur != tgt and cur != 1:
355
+ raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
356
+ for dim_name, cur, tgt in (
357
+ ("batch", mask_block_idx.shape[0], expected_index_shape[0]),
358
+ ("head", mask_block_idx.shape[1], expected_index_shape[1]),
359
+ ):
360
+ if cur != tgt and cur != 1:
361
+ raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
362
+ if mask_block_cnt.shape[2] != mask_block_idx.shape[2]:
363
+ raise ValueError(f"Block sparse tensors{context} must share the same m-block dimension.")
364
+ # [Note] Allow Compact block sparse indices: FA4 only accesses indices 0..cnt-1
365
+ # per query tile, so idx.shape[3] can be <= expected_n_blocks.
366
+ if mask_block_idx.shape[3] > expected_n_blocks:
367
+ raise ValueError(
368
+ f"Block sparse tensors{context} n-block dimension must be <= {expected_n_blocks}."
369
+ )
370
+ if expected_m_blocks != num_m_blocks:
371
+ raise ValueError(
372
+ f"Block sparse tensors{context} m-block dimension {num_m_blocks} does not match "
373
+ f"sparse_block_size_q={sparse_block_size_q}. "
374
+ f"Set BlockSparseTensorsTorch.block_size to match the BlockMask BLOCK_SIZE."
375
+ )
376
+ return expected_count_shape, expected_index_shape, q_subtile_factor
377
+
378
+
379
+ def get_block_sparse_expected_shapes_bwd(
380
+ batch_size: int,
381
+ num_head: int,
382
+ seqlen_q: int,
383
+ seqlen_k: int,
384
+ m_block_size: int,
385
+ n_block_size: int,
386
+ subtile_factor: int,
387
+ ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
388
+ """Return (expected_count_shape, expected_index_shape) for backward block sparse normalization.
389
+
390
+ Backward uses Q-direction indexing (transposed from forward), where shapes are
391
+ indexed by N-blocks first, then M-blocks. The sparse_block_size_q is determined
392
+ by subtile_factor * m_block_size.
393
+ """
394
+ sparse_block_size_q = subtile_factor * m_block_size
395
+ expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
396
+ expected_n_blocks = ceildiv(seqlen_k, n_block_size)
397
+ expected_count_shape = (batch_size, num_head, expected_n_blocks)
398
+ expected_index_shape = (batch_size, num_head, expected_n_blocks, expected_m_blocks)
399
+ return expected_count_shape, expected_index_shape
400
+
401
+
402
+ def normalize_block_sparse_tensors(
403
+ tensors: BlockSparseTensorsTorch,
404
+ *,
405
+ expected_count_shape: Tuple[int, int, int],
406
+ expected_index_shape: Tuple[int, int, int, int],
407
+ context: str | None = None,
408
+ hint: str | Callable[[], str] | None = None,
409
+ ) -> BlockSparseTensorsTorch:
410
+ if tensors.mask_block_cnt is None or tensors.mask_block_idx is None:
411
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
412
+
413
+ mask_cnt, mask_idx = _check_and_expand_block(
414
+ "mask",
415
+ tensors.mask_block_cnt,
416
+ tensors.mask_block_idx,
417
+ expected_count_shape,
418
+ expected_index_shape,
419
+ context,
420
+ hint,
421
+ )
422
+ if mask_cnt is None or mask_idx is None:
423
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
424
+
425
+ full_cnt, full_idx = _check_and_expand_block(
426
+ "full",
427
+ tensors.full_block_cnt,
428
+ tensors.full_block_idx,
429
+ expected_count_shape,
430
+ expected_index_shape,
431
+ context,
432
+ hint,
433
+ )
434
+ if full_cnt is not None and mask_cnt.device != full_cnt.device:
435
+ raise ValueError("All block sparse tensors must be on the same device")
436
+
437
+ dq_write_order = _check_and_expand_metadata_tensor(
438
+ "dq_write_order",
439
+ tensors.dq_write_order,
440
+ tuple(mask_idx.shape),
441
+ context,
442
+ hint,
443
+ mask_cnt.device,
444
+ )
445
+ dq_write_order_full = _check_and_expand_metadata_tensor(
446
+ "dq_write_order_full",
447
+ tensors.dq_write_order_full,
448
+ tuple(full_idx.shape) if full_idx is not None else expected_index_shape,
449
+ context,
450
+ hint,
451
+ mask_cnt.device,
452
+ )
453
+ spt = tensors.spt
454
+ if spt is not None and not isinstance(spt, bool):
455
+ raise ValueError("spt must be a bool when provided")
456
+ if spt is not None and dq_write_order is None:
457
+ raise ValueError("spt requires dq_write_order to be provided")
458
+
459
+ return BlockSparseTensorsTorch(
460
+ mask_block_cnt=mask_cnt,
461
+ mask_block_idx=mask_idx,
462
+ full_block_cnt=full_cnt,
463
+ full_block_idx=full_idx,
464
+ block_size=tensors.block_size,
465
+ dq_write_order=dq_write_order,
466
+ dq_write_order_full=dq_write_order_full,
467
+ spt=spt,
468
+ )
469
+
470
+
471
+ def is_block_sparsity_enabled(tensors: BlockSparseTensorsTorch) -> bool:
472
+ return any(t is not None for t in (tensors.full_block_cnt, tensors.mask_block_cnt))
473
+
474
+
475
+ def get_block_sparse_broadcast_pattern(
476
+ tensors: BlockSparseTensorsTorch,
477
+ ) -> Tuple[Tuple[bool, ...], ...] | None:
478
+ """Return broadcast pattern for block sparse tensors by checking actual strides.
479
+
480
+ Returns a tuple of broadcast patterns (one per tensor) where each pattern
481
+ is a tuple of bools indicating which dims have stride=0.
482
+ This is used in compile keys to ensure kernels are recompiled when
483
+ broadcast patterns change, since CuTe's mark_layout_dynamic() keeps
484
+ stride=0 as static.
485
+
486
+ The tensors should already be expanded/normalized before calling this function.
487
+
488
+ Returns None if block sparsity is not enabled.
489
+ """
490
+ if not is_block_sparsity_enabled(tensors):
491
+ return None
492
+
493
+ patterns = []
494
+ for tensor in (
495
+ tensors.mask_block_cnt,
496
+ tensors.mask_block_idx,
497
+ tensors.full_block_cnt,
498
+ tensors.full_block_idx,
499
+ tensors.dq_write_order,
500
+ tensors.dq_write_order_full,
501
+ ):
502
+ if tensor is not None:
503
+ patterns.append(get_broadcast_dims(tensor))
504
+ else:
505
+ patterns.append(None)
506
+ return tuple(patterns)
507
+
508
+
509
+ def normalize_block_sparse_config(
510
+ tensors: BlockSparseTensorsTorch,
511
+ *,
512
+ batch_size: int,
513
+ num_head: int,
514
+ seqlen_q: int,
515
+ seqlen_k: int,
516
+ block_size: tuple[int, int],
517
+ q_stage: int,
518
+ ) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None, int]:
519
+ m_block_size, n_block_size = block_size
520
+ if tensors.block_size is None:
521
+ sparse_block_size_q, sparse_block_size_kv = None, n_block_size
522
+ else:
523
+ sparse_block_size_q, sparse_block_size_kv = tensors.block_size
524
+ if sparse_block_size_kv != n_block_size:
525
+ raise ValueError(
526
+ f"Block sparsity requires sparse_block_size[1]={n_block_size} to match tile_n."
527
+ )
528
+ expected_count_shape, expected_index_shape, q_subtile_factor = (
529
+ infer_block_sparse_expected_shapes(
530
+ tensors,
531
+ batch_size=batch_size,
532
+ num_head=num_head,
533
+ seqlen_q=seqlen_q,
534
+ seqlen_k=seqlen_k,
535
+ m_block_size=m_block_size,
536
+ n_block_size=n_block_size,
537
+ q_stage=q_stage,
538
+ context="forward",
539
+ sparse_block_size_q=sparse_block_size_q,
540
+ sparse_block_size_kv=sparse_block_size_kv,
541
+ )
542
+ )
543
+ normalized_tensors = normalize_block_sparse_tensors(
544
+ tensors,
545
+ expected_count_shape=expected_count_shape,
546
+ expected_index_shape=expected_index_shape,
547
+ )
548
+ return (
549
+ normalized_tensors,
550
+ get_block_sparse_broadcast_pattern(normalized_tensors),
551
+ q_subtile_factor,
552
+ )
553
+
554
+
555
+ def normalize_block_sparse_config_bwd(
556
+ tensors: BlockSparseTensorsTorch,
557
+ *,
558
+ batch_size: int,
559
+ num_head: int,
560
+ seqlen_q: int,
561
+ seqlen_k: int,
562
+ block_size: tuple[int, int],
563
+ subtile_factor: int,
564
+ ) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None]:
565
+ m_block_size, n_block_size = block_size
566
+ if tensors.block_size is None:
567
+ sparse_block_size_q, sparse_block_size_kv = subtile_factor * m_block_size, n_block_size
568
+ else:
569
+ sparse_block_size_q, sparse_block_size_kv = tensors.block_size
570
+ if sparse_block_size_q != subtile_factor * m_block_size:
571
+ raise ValueError(
572
+ f"Block sparsity expects sparse_block_size_q={subtile_factor * m_block_size} "
573
+ f"for subtile_factor={subtile_factor}."
574
+ )
575
+ if sparse_block_size_kv != n_block_size:
576
+ raise ValueError(
577
+ f"Block sparsity expects sparse_block_size[1]={n_block_size} to match tile_n."
578
+ )
579
+ expected_count_shape, expected_index_shape = get_block_sparse_expected_shapes_bwd(
580
+ batch_size,
581
+ num_head,
582
+ seqlen_q,
583
+ seqlen_k,
584
+ m_block_size,
585
+ n_block_size,
586
+ subtile_factor,
587
+ )
588
+ normalized_tensors = normalize_block_sparse_tensors(
589
+ tensors,
590
+ expected_count_shape=expected_count_shape,
591
+ expected_index_shape=expected_index_shape,
592
+ context="_flash_attn_bwd",
593
+ hint=lambda: (
594
+ f"Backward expects Q-direction block-sparse tensors (q_mask_cnt/q_mask_idx, "
595
+ f"and optionally full_q_cnt/full_q_idx). Regenerate the backward BlockMask with "
596
+ f"BLOCK_SIZE=({subtile_factor * m_block_size}, {n_block_size})."
597
+ ),
598
+ )
599
+ return normalized_tensors, get_block_sparse_broadcast_pattern(normalized_tensors)
600
+
601
+
602
+ def to_cute_block_sparse_tensors(
603
+ tensors: BlockSparseTensorsTorch, enable_tvm_ffi: bool = True
604
+ ) -> BlockSparseTensors | None:
605
+ """Convert torch block sparsity tensors to CuTe tensors, optionally for tvm ffi"""
606
+ if not is_block_sparsity_enabled(tensors):
607
+ return None
608
+ mask_block_cnt_tensor, mask_block_idx_tensor = [
609
+ to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
610
+ for t in (tensors.mask_block_cnt, tensors.mask_block_idx)
611
+ ]
612
+ full_block_cnt_tensor, full_block_idx_tensor = [
613
+ to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
614
+ if t is not None
615
+ else None
616
+ for t in (tensors.full_block_cnt, tensors.full_block_idx)
617
+ ]
618
+ dq_write_order_tensor, dq_write_order_full_tensor = [
619
+ to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
620
+ if t is not None
621
+ else None
622
+ for t in (tensors.dq_write_order, tensors.dq_write_order_full)
623
+ ]
624
+
625
+ return BlockSparseTensors(
626
+ mask_block_cnt_tensor,
627
+ mask_block_idx_tensor,
628
+ full_block_cnt_tensor,
629
+ full_block_idx_tensor,
630
+ dq_write_order_tensor,
631
+ dq_write_order_full_tensor,
632
+ )
633
+
634
+
635
+ def fast_sampling(mask_mod):
636
+ """Convenience decorator to mark mask_mod as safe for 5-point fast sampling"""
637
+ mask_mod.use_fast_sampling = True
638
+ return mask_mod
build/torch-cuda/flashrt_fa4/cute/cache_utils.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Manage Ahead-of-Time (AOT) compiled kernels
2
+ import fcntl
3
+ import hashlib
4
+ import os
5
+ import pickle
6
+ import sys
7
+ import tempfile
8
+ import time
9
+ from functools import lru_cache
10
+ from getpass import getuser
11
+ from pathlib import Path
12
+ from typing import Hashable, TypeAlias
13
+
14
+ import ctypes
15
+
16
+ import cutlass
17
+ import cutlass.cute as cute
18
+ import tvm_ffi
19
+ from cutlass.cutlass_dsl import JitCompiledFunction
20
+ from fa4_cute_runtime.flashrt_fa4.cute.fa_logging import fa_log
21
+
22
+ # Pre-load cute DSL runtime libraries with RTLD_GLOBAL so that their symbols
23
+ # (e.g. _cudaLibraryLoadData) are visible to .so modules loaded later via dlopen.
24
+ # Upstream cute.runtime.load_module loads these without RTLD_GLOBAL, which causes
25
+ # "undefined symbol" errors when loading cached kernels from disk.
26
+ for _lib_path in cute.runtime.find_runtime_libraries(enable_tvm_ffi=False):
27
+ if Path(_lib_path).exists():
28
+ ctypes.CDLL(_lib_path, mode=ctypes.RTLD_GLOBAL)
29
+
30
+ CompileKeyType: TypeAlias = tuple[Hashable, ...]
31
+ CallableFunction: TypeAlias = JitCompiledFunction | tvm_ffi.Function
32
+
33
+ # Enable cache via `FLASH_ATTENTION_CUTE_DSL_CACHE_ENABLED=1`
34
+ CUTE_DSL_CACHE_ENABLED: bool = os.getenv("FLASH_ATTENTION_CUTE_DSL_CACHE_ENABLED", "0") == "1"
35
+
36
+
37
+ # Customize cache dir via `FLASH_ATTENTION_CUTE_DSL_CACHE_DIR`, default is
38
+ # `/tmp/${USER}/flash_attention_cute_dsl_cache``
39
+ CUTE_DSL_CACHE_DIR: str | None = os.getenv("FLASH_ATTENTION_CUTE_DSL_CACHE_DIR", None)
40
+
41
+
42
+ def get_cache_path() -> Path:
43
+ if CUTE_DSL_CACHE_DIR is not None:
44
+ cache_dir = Path(CUTE_DSL_CACHE_DIR)
45
+ else:
46
+ cache_dir = Path(tempfile.gettempdir()) / getuser() / "flash_attention_cute_dsl_cache"
47
+ cache_dir.mkdir(parents=True, exist_ok=True)
48
+ return cache_dir
49
+
50
+
51
+ @lru_cache(maxsize=1)
52
+ def _compute_source_fingerprint() -> str:
53
+ """
54
+ Hash all CuTe Python sources plus runtime ABI stamps into a short fingerprint.
55
+
56
+ The fingerprint changes whenever:
57
+ - Any .py file under flash_attn/cute is added, removed, renamed, or modified.
58
+ - The Python minor version changes (e.g. 3.13 -> 3.14).
59
+ - The cutlass or tvm_ffi package version changes.
60
+
61
+ Computed once per process and cached.
62
+ """
63
+ cute_root = Path(__file__).resolve().parent
64
+ h = hashlib.sha256()
65
+
66
+ h.update(f"py{sys.version_info.major}.{sys.version_info.minor}".encode())
67
+ h.update(f"cutlass={cutlass.__version__}".encode())
68
+ h.update(f"tvm_ffi={tvm_ffi.__version__}".encode())
69
+
70
+ for src in sorted(cute_root.rglob("*.py")):
71
+ if not src.is_file():
72
+ continue
73
+ h.update(src.relative_to(cute_root).as_posix().encode())
74
+ content = src.read_bytes()
75
+ h.update(len(content).to_bytes(8, "little"))
76
+ h.update(content)
77
+
78
+ return h.hexdigest()
79
+
80
+
81
+ class FileLock:
82
+ """Context manager for advisory file locks using fcntl.flock.
83
+
84
+ Supports exclusive (write) and shared (read) locks.
85
+ Always blocks with polling until the lock is acquired or timeout is reached.
86
+
87
+ Usage:
88
+ with FileLock(lock_path, exclusive=True, timeout=15, label="abc"):
89
+ # do work under lock
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ lock_path: Path,
95
+ exclusive: bool,
96
+ timeout: float = 15,
97
+ label: str = "",
98
+ ):
99
+ """
100
+ Args:
101
+ lock_path: Path to the lock file on disk.
102
+ exclusive: True for exclusive (write) lock, False for shared (read) lock.
103
+ timeout: Max seconds to wait for lock acquisition before raising RuntimeError.
104
+ label: Optional human-readable label for error messages.
105
+ """
106
+ self.lock_path: Path = lock_path
107
+ self.exclusive: bool = exclusive
108
+ self.timeout: float = timeout
109
+ self.label: str = label
110
+ self._fd: int = -1
111
+
112
+ @property
113
+ def _lock_label(self) -> str:
114
+ kind = "exclusive" if self.exclusive else "shared"
115
+ return f"{kind} {self.label}" if self.label else kind
116
+
117
+ def __enter__(self) -> "FileLock":
118
+ open_flags = os.O_WRONLY | os.O_CREAT if self.exclusive else os.O_RDONLY | os.O_CREAT
119
+ lock_type = fcntl.LOCK_EX if self.exclusive else fcntl.LOCK_SH
120
+
121
+ self._fd = os.open(str(self.lock_path), open_flags)
122
+
123
+ deadline = time.monotonic() + self.timeout
124
+ acquired = False
125
+ while time.monotonic() < deadline:
126
+ try:
127
+ fcntl.flock(self._fd, lock_type | fcntl.LOCK_NB)
128
+ acquired = True
129
+ break
130
+ except OSError:
131
+ time.sleep(0.1)
132
+ if not acquired:
133
+ os.close(self._fd)
134
+ self._fd = None
135
+ raise RuntimeError(
136
+ f"Timed out after {self.timeout}s waiting for "
137
+ f"{self._lock_label} lock: {self.lock_path}"
138
+ )
139
+
140
+ return self
141
+
142
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
143
+ if self._fd is not None:
144
+ fcntl.flock(self._fd, fcntl.LOCK_UN)
145
+ os.close(self._fd)
146
+ self._fd = None
147
+
148
+
149
+ class JITCache:
150
+ """
151
+ In-memory cache for compiled functions.
152
+ """
153
+
154
+ def __init__(self):
155
+ self.cache: dict[CompileKeyType, CallableFunction] = {}
156
+
157
+ def __setitem__(self, key: CompileKeyType, fn: JitCompiledFunction) -> None:
158
+ self.cache[key] = fn
159
+
160
+ def __getitem__(self, key: CompileKeyType) -> CallableFunction:
161
+ return self.cache[key]
162
+
163
+ def __contains__(self, key: CompileKeyType) -> bool:
164
+ return key in self.cache
165
+
166
+ def clear(self) -> None:
167
+ """
168
+ Clear in-memory cache of compiled functions
169
+ """
170
+ self.cache.clear()
171
+
172
+
173
+ class JITPersistentCache(JITCache):
174
+ """
175
+ In-memory cache for compiled functions, which is also backed by persistent storage.
176
+ Use cutedsl ahead-of-time (AOT) compilation, only supporting enable_tvm_ffi=True
177
+ """
178
+
179
+ EXPORT_FUNCTION_PREFIX = "func"
180
+ LOCK_TIMEOUT_SECONDS = 15
181
+
182
+ def __init__(self, cache_path: Path):
183
+ super().__init__()
184
+ cache_path.mkdir(parents=True, exist_ok=True)
185
+ self.cache_path: Path = cache_path
186
+
187
+ def __setitem__(self, key: CompileKeyType, fn: JitCompiledFunction) -> None:
188
+ JITCache.__setitem__(self, key, fn)
189
+ self._try_export_to_storage(key, fn)
190
+
191
+ def __getitem__(self, key: CompileKeyType) -> CallableFunction:
192
+ # Use __contains__ to try populating in-memory cache with persistent storage
193
+ self.__contains__(key)
194
+ return JITCache.__getitem__(self, key)
195
+
196
+ def __contains__(self, key: CompileKeyType) -> bool:
197
+ # Checks in-memory cache first, then tries loading from storage.
198
+ # When returning True, guarantees the in-memory cache is populated.
199
+ if JITCache.__contains__(self, key):
200
+ return True
201
+ return self._try_load_from_storage(key)
202
+
203
+ def _try_load_from_storage(self, key: CompileKeyType) -> bool:
204
+ """
205
+ Try to load a function from persistent storage into in-memory cache.
206
+ Returns True if loaded successfully, False if not found on disk.
207
+ Holds a shared lock during loading to prevent concurrent writes.
208
+ """
209
+ sha256_hex = self._key_to_hash(key)
210
+ obj_path = self.cache_path / f"{sha256_hex}.o"
211
+ with FileLock(
212
+ self._lock_path(sha256_hex),
213
+ exclusive=False,
214
+ timeout=self.LOCK_TIMEOUT_SECONDS,
215
+ label=sha256_hex,
216
+ ):
217
+ if obj_path.exists():
218
+ fa_log(1, f"Loading compiled function from disk: {obj_path}")
219
+ m = cute.runtime.load_module(str(obj_path), enable_tvm_ffi=True)
220
+ fn = getattr(m, self.EXPORT_FUNCTION_PREFIX)
221
+ JITCache.__setitem__(self, key, fn)
222
+ return True
223
+ else:
224
+ fa_log(1, f"Cache miss on disk for key hash {sha256_hex}")
225
+ return False
226
+
227
+ def _try_export_to_storage(self, key: CompileKeyType, fn: JitCompiledFunction) -> None:
228
+ """Export a compiled function to persistent storage under exclusive lock."""
229
+ sha256_hex = self._key_to_hash(key)
230
+ with FileLock(
231
+ self._lock_path(sha256_hex),
232
+ exclusive=True,
233
+ timeout=self.LOCK_TIMEOUT_SECONDS,
234
+ label=sha256_hex,
235
+ ):
236
+ obj_path = self.cache_path / f"{sha256_hex}.o"
237
+ if obj_path.exists():
238
+ # Another process already exported.
239
+ fa_log(1, f"Skipping export, already on disk: {obj_path}")
240
+ return
241
+ fa_log(1, f"Exporting compiled function to disk: {obj_path}")
242
+ fn.export_to_c(
243
+ object_file_path=str(obj_path),
244
+ function_name=self.EXPORT_FUNCTION_PREFIX,
245
+ )
246
+ fa_log(1, f"Successfully exported compiled function to disk: {obj_path}")
247
+
248
+ def _key_to_hash(self, key: CompileKeyType) -> str:
249
+ return hashlib.sha256(pickle.dumps(key)).hexdigest()
250
+
251
+ def _lock_path(self, sha256_hex: str) -> Path:
252
+ return self.cache_path / f"{sha256_hex}.lock"
253
+
254
+ def clear(self) -> None:
255
+ """
256
+ Not only clear the in-memory cache. Also purge persistent compilation cache.
257
+ """
258
+ fa_log(1, f"Clearing persistent cache at {self.cache_path}")
259
+ super().clear()
260
+ for child in self.cache_path.iterdir():
261
+ child.unlink()
262
+
263
+
264
+ def get_jit_cache(name: str | None = None) -> JITCache:
265
+ """
266
+ JIT cache factory.
267
+ `name` is an optional identifier to create subdirectories to manage cache.
268
+
269
+ When persistent caching is enabled, artifacts are namespaced under a
270
+ source fingerprint directory so that code or dependency changes
271
+ automatically invalidate stale entries.
272
+ """
273
+ if CUTE_DSL_CACHE_ENABLED:
274
+ path = get_cache_path() / _compute_source_fingerprint()
275
+ if name:
276
+ path = path / name
277
+ fa_log(1, f"Creating persistent JIT cache at {path}")
278
+ return JITPersistentCache(path)
279
+ else:
280
+ fa_log(1, "Persistent cache disabled, using in-memory JIT cache")
281
+ return JITCache()
build/torch-cuda/flashrt_fa4/cute/copy_utils.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+
3
+ import math
4
+ from typing import Optional, Type, Callable
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+ from cutlass import Float32, Int32, const_expr
9
+ from cutlass.cute.nvgpu import cpasync
10
+ import cutlass.utils.blackwell_helpers as sm100_utils
11
+ from cutlass.cutlass_dsl import T, dsl_user_op
12
+ from cutlass._mlir.dialects import llvm
13
+ import cutlass.pipeline
14
+
15
+
16
+ @dsl_user_op
17
+ def cvt_copy(
18
+ atom: cute.CopyAtom,
19
+ src: cute.Tensor,
20
+ dst: cute.Tensor,
21
+ *,
22
+ pred: Optional[cute.Tensor] = None,
23
+ loc=None,
24
+ ip=None,
25
+ **kwargs,
26
+ ) -> None:
27
+ assert isinstance(src.iterator, cute.Pointer) and src.memspace == cute.AddressSpace.rmem
28
+ if const_expr(src.element_type != dst.element_type):
29
+ src_cvt = cute.make_fragment_like(src, dst.element_type, loc=loc, ip=ip)
30
+ src_cvt.store(src.load().to(dst.element_type))
31
+ src = src_cvt
32
+ cute.copy(atom, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
33
+
34
+
35
+ @dsl_user_op
36
+ def load_s2r(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
37
+ dst = cute.make_fragment_like(src, src.element_type, loc=loc, ip=ip)
38
+ cute.autovec_copy(src, dst, loc=loc, ip=ip)
39
+ return dst
40
+
41
+
42
+ @dsl_user_op
43
+ def get_copy_atom(
44
+ dtype: Type[cutlass.Numeric], num_copy_elems: int, is_async: bool = False, *, loc=None, ip=None
45
+ ) -> cute.CopyAtom:
46
+ num_copy_bits = const_expr(min(128, num_copy_elems * dtype.width))
47
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
48
+ return cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
49
+
50
+
51
+ @dsl_user_op
52
+ def make_tmem_copy(
53
+ tmem_copy_atom: cute.CopyAtom, num_wg: int = 1, *, loc=None, ip=None
54
+ ) -> cute.CopyAtom:
55
+ num_dp, num_bits, num_rep, _ = sm100_utils.get_tmem_copy_properties(tmem_copy_atom)
56
+ assert num_dp == 32
57
+ assert num_bits == 32
58
+ tiler_mn = (cute.make_layout((128 * num_rep * num_wg // 32, 32), stride=(32, 1)),)
59
+ layout_tv = cute.make_layout(
60
+ ((32, 4, num_wg), (num_rep, 32)), stride=((0, 1, 4 * num_rep), (4, 4 * num_rep * num_wg))
61
+ )
62
+ return cute.make_tiled_copy(tmem_copy_atom, layout_tv, tiler_mn)
63
+
64
+
65
+ @dsl_user_op
66
+ def copy(
67
+ src: cute.Tensor,
68
+ dst: cute.Tensor,
69
+ *,
70
+ pred: Optional[cute.Tensor] = None,
71
+ num_copy_elems: int = 1,
72
+ is_async: bool = False,
73
+ loc=None,
74
+ ip=None,
75
+ **kwargs,
76
+ ) -> None:
77
+ copy_atom = get_copy_atom(src.element_type, num_copy_elems, is_async)
78
+ cute.copy(copy_atom, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
79
+
80
+
81
+ def tiled_copy_1d(
82
+ dtype: Type[cutlass.Numeric], num_threads: int, num_copy_elems: int = 1, is_async: bool = False
83
+ ) -> cute.TiledCopy:
84
+ num_copy_bits = num_copy_elems * dtype.width
85
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
86
+ copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
87
+ thr_layout = cute.make_layout(num_threads)
88
+ val_layout = cute.make_layout(num_copy_elems)
89
+ return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
90
+
91
+
92
+ def tiled_copy_2d(
93
+ dtype: Type[cutlass.Numeric], major_mode_size: int, num_threads: int, is_async: bool = False
94
+ ) -> cute.TiledCopy:
95
+ num_copy_bits = math.gcd(major_mode_size, 128 // dtype.width) * dtype.width
96
+ copy_elems = num_copy_bits // dtype.width
97
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
98
+ copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
99
+ gmem_threads_per_row = major_mode_size // copy_elems
100
+ assert num_threads % gmem_threads_per_row == 0
101
+ thr_layout = cute.make_ordered_layout(
102
+ (num_threads // gmem_threads_per_row, gmem_threads_per_row),
103
+ order=(1, 0),
104
+ )
105
+ val_layout = cute.make_layout((1, copy_elems))
106
+ return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
107
+
108
+
109
+ @dsl_user_op
110
+ def atomic_add_fp32x4(
111
+ a: Float32, b: Float32, c: Float32, d: Float32, gmem_ptr: cute.Pointer, *, loc=None, ip=None
112
+ ) -> None:
113
+ gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
114
+ # cache_hint = cutlass.Int64(0x12F0000000000000)
115
+ llvm.inline_asm(
116
+ None,
117
+ [
118
+ gmem_ptr_i64,
119
+ Float32(a).ir_value(loc=loc, ip=ip),
120
+ Float32(b).ir_value(loc=loc, ip=ip),
121
+ Float32(c).ir_value(loc=loc, ip=ip),
122
+ Float32(d).ir_value(loc=loc, ip=ip),
123
+ ],
124
+ # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip), cache_hint.ir_value()],
125
+ "{\n\t"
126
+ # ".reg .b128 abcd;\n\t"
127
+ # "mov.b128 abcd, {$1, $2, $3, $4};\n\t"
128
+ ".reg .v4 .f32 abcd;\n\t"
129
+ # "mov.b128 abcd, {$1, $2, $3, $4};\n\t"
130
+ "mov.f32 abcd.x, $1;\n\t"
131
+ "mov.f32 abcd.y, $2;\n\t"
132
+ "mov.f32 abcd.z, $3;\n\t"
133
+ "mov.f32 abcd.w, $4;\n\t"
134
+ "red.global.add.v4.f32 [$0], abcd;\n\t"
135
+ # "red.global.add.L2::cache_hint.v4.f32 [$0], abcd, 0x14F0000000000000;\n\t"
136
+ "}\n",
137
+ # "red.global.add.L2::cache_hint.f32 [$0], $1, 0x12F0000000000000;",
138
+ # "red.global.add.L2::cache_hint.f32 [$0], $1, $2;",
139
+ "l,f,f,f,f",
140
+ # "l,f,l",
141
+ has_side_effects=True,
142
+ is_align_stack=False,
143
+ asm_dialect=llvm.AsmDialect.AD_ATT,
144
+ )
145
+
146
+
147
+ @dsl_user_op
148
+ def set_block_rank(
149
+ smem_ptr: cute.Pointer, peer_cta_rank_in_cluster: Int32, *, loc=None, ip=None
150
+ ) -> Int32:
151
+ """Map the given smem pointer to the address at another CTA rank in the cluster."""
152
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
153
+ return Int32(
154
+ llvm.inline_asm(
155
+ T.i32(),
156
+ [smem_ptr_i32, peer_cta_rank_in_cluster.ir_value()],
157
+ "mapa.shared::cluster.u32 $0, $1, $2;",
158
+ "=r,r,r",
159
+ has_side_effects=False,
160
+ is_align_stack=False,
161
+ asm_dialect=llvm.AsmDialect.AD_ATT,
162
+ )
163
+ )
164
+
165
+
166
+ @dsl_user_op
167
+ def store_shared_remote_fp32x4(
168
+ a: Float32,
169
+ b: Float32,
170
+ c: Float32,
171
+ d: Float32,
172
+ smem_ptr: cute.Pointer,
173
+ mbar_ptr: cute.Pointer,
174
+ peer_cta_rank_in_cluster: Int32,
175
+ *,
176
+ loc=None,
177
+ ip=None,
178
+ ) -> None:
179
+ remote_smem_ptr_i32 = set_block_rank(
180
+ smem_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
181
+ ).ir_value()
182
+ remote_mbar_ptr_i32 = set_block_rank(
183
+ mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
184
+ ).ir_value()
185
+ llvm.inline_asm(
186
+ None,
187
+ [
188
+ remote_smem_ptr_i32,
189
+ remote_mbar_ptr_i32,
190
+ Float32(a).ir_value(loc=loc, ip=ip),
191
+ Float32(b).ir_value(loc=loc, ip=ip),
192
+ Float32(c).ir_value(loc=loc, ip=ip),
193
+ Float32(d).ir_value(loc=loc, ip=ip),
194
+ ],
195
+ "{\n\t"
196
+ ".reg .v4 .f32 abcd;\n\t"
197
+ "mov.f32 abcd.x, $2;\n\t"
198
+ "mov.f32 abcd.y, $3;\n\t"
199
+ "mov.f32 abcd.z, $4;\n\t"
200
+ "mov.f32 abcd.w, $5;\n\t"
201
+ "st.async.shared::cluster.mbarrier::complete_tx::bytes.v4.f32 [$0], abcd, [$1];\n\t"
202
+ "}\n",
203
+ "r,r,f,f,f,f",
204
+ has_side_effects=True,
205
+ is_align_stack=False,
206
+ asm_dialect=llvm.AsmDialect.AD_ATT,
207
+ )
208
+
209
+
210
+ @dsl_user_op
211
+ def cpasync_bulk_s2cluster(
212
+ smem_src_ptr: cute.Pointer,
213
+ smem_dst_ptr: cute.Pointer,
214
+ mbar_ptr: cute.Pointer,
215
+ size: int | Int32,
216
+ peer_cta_rank_in_cluster: Int32,
217
+ *,
218
+ loc=None,
219
+ ip=None,
220
+ ):
221
+ smem_src_ptr_i32 = smem_src_ptr.toint(loc=loc, ip=ip).ir_value()
222
+ smem_dst_ptr_i32 = set_block_rank(
223
+ smem_dst_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
224
+ ).ir_value()
225
+ mbar_ptr_i32 = set_block_rank(mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip).ir_value()
226
+ llvm.inline_asm(
227
+ None,
228
+ [
229
+ smem_dst_ptr_i32,
230
+ smem_src_ptr_i32,
231
+ mbar_ptr_i32,
232
+ Int32(size).ir_value(loc=loc, ip=ip),
233
+ ],
234
+ "cp.async.bulk.shared::cluster.shared::cta.mbarrier::complete_tx::bytes [$0], [$1], $3, [$2];",
235
+ "r,r,r,r",
236
+ has_side_effects=True,
237
+ is_align_stack=False,
238
+ asm_dialect=llvm.AsmDialect.AD_ATT,
239
+ )
240
+
241
+
242
+ @dsl_user_op
243
+ def cpasync_bulk_g2s(
244
+ gmem_ptr: cute.Pointer,
245
+ smem_ptr: cute.Pointer,
246
+ tma_bar_ptr: cute.Pointer,
247
+ size: int | Int32,
248
+ *,
249
+ loc=None,
250
+ ip=None,
251
+ ):
252
+ gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
253
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
254
+ mbar_ptr_i32 = tma_bar_ptr.toint(loc=loc, ip=ip).ir_value()
255
+ llvm.inline_asm(
256
+ None,
257
+ [gmem_ptr_i64, smem_ptr_i32, mbar_ptr_i32, Int32(size).ir_value()],
258
+ "cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes [$1], [$0], $3, [$2];",
259
+ "l,r,r,r",
260
+ has_side_effects=True,
261
+ is_align_stack=False,
262
+ asm_dialect=llvm.AsmDialect.AD_ATT,
263
+ )
264
+
265
+
266
+ @dsl_user_op
267
+ def cpasync_reduce_bulk_add_f32(
268
+ smem_ptr: cute.Pointer,
269
+ gmem_ptr: cute.Pointer,
270
+ store_bytes: int | Int32,
271
+ *,
272
+ loc=None,
273
+ ip=None,
274
+ ):
275
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
276
+ # cache_hint = cutlass.Int64(0x14F0000000000000) # EVICT_LAST
277
+ llvm.inline_asm(
278
+ None,
279
+ [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value()],
280
+ "cp.reduce.async.bulk.global.shared::cta.bulk_group.add.f32 [$0], [$1], $2;",
281
+ "l,r,r",
282
+ # [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value(), cache_hint.ir_value()],
283
+ # "cp.reduce.async.bulk.global.shared::cta.bulk_group.L2::cache_hint.add.f32 [$0], [$1], $2, $3;",
284
+ # "l,r,r,l",
285
+ has_side_effects=True,
286
+ is_align_stack=False,
287
+ asm_dialect=llvm.AsmDialect.AD_ATT,
288
+ )
289
+
290
+
291
+ def cpasync_bulk_get_copy_fn(
292
+ src_tensor: cute.Tensor,
293
+ dst_tensor: cute.Tensor,
294
+ single_stage: bool = False,
295
+ **kwargs,
296
+ ) -> Callable:
297
+ # src_is_smem = const_expr(
298
+ # isinstance(src_tensor.iterator, cute.Pointer)
299
+ # and src_tensor.memspace == cute.AddressSpace.smem
300
+ # )
301
+ group_rank_src = const_expr(cute.rank(src_tensor) - (1 if not single_stage else 0))
302
+ group_rank_dst = const_expr(cute.rank(dst_tensor) - (1 if not single_stage else 0))
303
+ # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
304
+ src = cute.group_modes(src_tensor, 0, group_rank_src)
305
+ dst = cute.group_modes(dst_tensor, 0, group_rank_dst)
306
+
307
+ def copy_bulk(src_idx, dst_idx, **new_kwargs):
308
+ size = const_expr(cute.size(src.shape[:-1]) * src.element_type.width // 8)
309
+ cpasync_bulk_g2s(
310
+ src[None, src_idx].iterator,
311
+ dst[None, dst_idx].iterator,
312
+ size=size,
313
+ **new_kwargs,
314
+ **kwargs,
315
+ )
316
+
317
+ def copy_bulk_single_stage(**new_kwargs):
318
+ size = const_expr(cute.size(src.shape) * src.element_type.width // 8)
319
+ cpasync_bulk_g2s(src.iterator, dst.iterator, size=size, **new_kwargs, **kwargs)
320
+
321
+ return copy_bulk if const_expr(not single_stage) else copy_bulk_single_stage
322
+
323
+
324
+ def tma_get_copy_fn(
325
+ atom: cute.CopyAtom,
326
+ cta_coord: cute.Coord,
327
+ cta_layout: cute.Layout,
328
+ src_tensor: cute.Tensor,
329
+ dst_tensor: cute.Tensor,
330
+ filter_zeros: bool = False,
331
+ single_stage: bool = False,
332
+ **kwargs,
333
+ ) -> Callable:
334
+ src_is_smem = const_expr(
335
+ isinstance(src_tensor.iterator, cute.Pointer)
336
+ and src_tensor.memspace == cute.AddressSpace.smem
337
+ )
338
+ smem_tensor, gmem_tensor = (src_tensor, dst_tensor) if src_is_smem else (dst_tensor, src_tensor)
339
+ group_rank_smem = const_expr(cute.rank(smem_tensor) - (1 if not single_stage else 0))
340
+ group_rank_gmem = const_expr(cute.rank(gmem_tensor) - (1 if not single_stage else 0))
341
+ # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
342
+ s, g = cpasync.tma_partition(
343
+ atom,
344
+ cta_coord,
345
+ cta_layout,
346
+ cute.group_modes(smem_tensor, 0, group_rank_smem),
347
+ cute.group_modes(gmem_tensor, 0, group_rank_gmem),
348
+ )
349
+ if const_expr(filter_zeros):
350
+ s = cute.filter_zeros(s)
351
+ g = cute.filter_zeros(g)
352
+ src, dst = (s, g) if src_is_smem else (g, s)
353
+
354
+ def copy_tma(src_idx, dst_idx, **new_kwargs):
355
+ cute.copy(atom, src[None, src_idx], dst[None, dst_idx], **new_kwargs, **kwargs)
356
+
357
+ def copy_tma_single_stage(**new_kwargs):
358
+ cute.copy(atom, src, dst, **new_kwargs, **kwargs)
359
+
360
+ return (copy_tma if const_expr(not single_stage) else copy_tma_single_stage), s, g
361
+
362
+
363
+ def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsync):
364
+ def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
365
+ copy(
366
+ src_idx=src_idx,
367
+ dst_idx=producer_state.index,
368
+ tma_bar_ptr=pipeline.producer_get_barrier(producer_state),
369
+ **new_kwargs,
370
+ )
371
+
372
+ return copy_fn
build/torch-cuda/flashrt_fa4/cute/cute_dsl_ptxas.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ System ptxas replacement for CUTLASS DSL.
3
+ Environment variables:
4
+ CUTE_DSL_PTXAS_PATH - Path to ptxas (e.g., /usr/local/cuda/bin/ptxas)
5
+ CUTE_DSL_PTXAS_VERBOSE - Set to 1 for verbose output
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import re
11
+ import ctypes
12
+ import subprocess
13
+ from pathlib import Path
14
+
15
+ import cutlass
16
+
17
+
18
+ CUTE_DSL_PTXAS_PATH = os.environ.get("CUTE_DSL_PTXAS_PATH", None)
19
+ VERBOSE = os.environ.get("CUTE_DSL_PTXAS_VERBOSE", "0") == "1"
20
+
21
+ _original_load_cuda_library = None
22
+ _user_wanted_ptx = False # True if user originally set CUTE_DSL_KEEP_PTX=1
23
+
24
+
25
+ def _log(msg):
26
+ if VERBOSE:
27
+ print(f"[ptxas] {msg}", file=sys.stderr)
28
+
29
+
30
+ def _get_ptx(compiled_func) -> tuple[str, Path] | None:
31
+ """Find and read PTX file, stripping null bytes."""
32
+ func_name = getattr(compiled_func, "function_name", None)
33
+ if not func_name:
34
+ return None
35
+
36
+ dump_dir = os.environ.get("CUTE_DSL_DUMP_DIR", Path.cwd())
37
+ for ptx_path in Path(dump_dir).glob(f"*{func_name}*.ptx"):
38
+ content = ptx_path.read_text().rstrip("\x00")
39
+ if ".entry " in content and content.rstrip().endswith("}"):
40
+ _log(f"Found PTX: {ptx_path}")
41
+ return content, ptx_path
42
+ return None
43
+
44
+
45
+ def _compile_ptx(ptx_path: Path, ptx_content: str) -> bytes:
46
+ """Compile PTX to cubin using system ptxas."""
47
+ # Extract arch from PTX
48
+ match = re.search(r"\.target\s+(sm_\d+[a-z]?)", ptx_content)
49
+ arch = match.group(1) if match else "sm_90a"
50
+
51
+ # Write stripped content back if needed
52
+ if ptx_path.read_text() != ptx_content:
53
+ ptx_path.write_text(ptx_content)
54
+
55
+ # Compile
56
+ cubin_tmp = ptx_path.with_suffix(".cubin.tmp")
57
+ try:
58
+ assert CUTE_DSL_PTXAS_PATH is not None
59
+ result = subprocess.run(
60
+ [CUTE_DSL_PTXAS_PATH, f"-arch={arch}", "-O3", "-o", str(cubin_tmp), str(ptx_path)],
61
+ capture_output=True,
62
+ text=True,
63
+ )
64
+ if result.returncode != 0:
65
+ raise RuntimeError(f"ptxas failed: {result.stderr}")
66
+
67
+ cubin_data = cubin_tmp.read_bytes()
68
+ _log(f"Compiled {ptx_path.name} -> {len(cubin_data)} bytes ({arch})")
69
+
70
+ # Save cubin if CUTE_DSL_KEEP_CUBIN is set
71
+ if os.environ.get("CUTE_DSL_KEEP_CUBIN", "0") == "1":
72
+ cubin_out = ptx_path.with_suffix(".cubin")
73
+ cubin_out.write_bytes(cubin_data)
74
+ _log(f"Saved: {cubin_out}")
75
+
76
+ return cubin_data
77
+ finally:
78
+ cubin_tmp.unlink(missing_ok=True)
79
+
80
+
81
+ def _patched_load_cuda_library(self):
82
+ """Replacement for _load_cuda_library that uses system ptxas."""
83
+
84
+ result = _get_ptx(self)
85
+ if not result:
86
+ _log("PTX not found, falling back to embedded ptxas")
87
+ return _original_load_cuda_library(self)
88
+
89
+ ptx_content, ptx_path = result
90
+
91
+ try:
92
+ cubin = _compile_ptx(ptx_path, ptx_content)
93
+ except Exception as e:
94
+ _log(f"Compilation failed ({e}), falling back to embedded ptxas")
95
+ return _original_load_cuda_library(self)
96
+
97
+ # Load cubin
98
+ import cuda.bindings.runtime as cuda_runtime
99
+
100
+ err, library = cuda_runtime.cudaLibraryLoadData(cubin, None, None, 0, None, None, 0)
101
+ if err != cuda_runtime.cudaError_t.cudaSuccess:
102
+ _log(f"cudaLibraryLoadData failed ({err}), falling back to embedded ptxas")
103
+ return _original_load_cuda_library(self)
104
+
105
+ # Register kernels on all devices
106
+ _, cuda_load_to_device = self._get_cuda_init_and_load()
107
+ lib_ptr = ctypes.c_void_p(int(library))
108
+ dev_id = ctypes.c_int32(0)
109
+ err_val = ctypes.c_int32(0)
110
+ args = (ctypes.c_void_p * 3)(
111
+ ctypes.cast(ctypes.pointer(lib_ptr), ctypes.c_void_p),
112
+ ctypes.cast(ctypes.pointer(dev_id), ctypes.c_void_p),
113
+ ctypes.cast(ctypes.pointer(err_val), ctypes.c_void_p),
114
+ )
115
+
116
+ for dev in range(self.num_devices):
117
+ dev_id.value = dev
118
+ cuda_load_to_device(args)
119
+ if err_val.value != 0:
120
+ _log("cuda_load_to_device failed, falling back to embedded ptxas")
121
+ return _original_load_cuda_library(self)
122
+
123
+ _log(f"Loaded kernel from {ptx_path.name}")
124
+
125
+ # Delete PTX if user didn't originally want it kept
126
+ if not _user_wanted_ptx:
127
+ ptx_path.unlink(missing_ok=True)
128
+
129
+ return [cuda_runtime.cudaLibrary_t(lib_ptr.value)]
130
+
131
+
132
+ def patch():
133
+ """Install system ptxas hook. Call before importing cutlass."""
134
+ global _original_load_cuda_library, _user_wanted_ptx
135
+
136
+ assert CUTE_DSL_PTXAS_PATH is not None
137
+ if not os.path.isfile(CUTE_DSL_PTXAS_PATH) or not os.access(CUTE_DSL_PTXAS_PATH, os.X_OK):
138
+ raise RuntimeError(f"ptxas not found: {CUTE_DSL_PTXAS_PATH}")
139
+
140
+ # Track if user originally wanted PTX kept
141
+ _user_wanted_ptx = os.environ.get("CUTE_DSL_KEEP_PTX", "0") == "1"
142
+ # os.environ['CUTE_DSL_KEEP_PTX'] = '1'
143
+ assert os.environ.get("CUTE_DSL_KEEP_PTX", "0") == "1", (
144
+ "Require CUTE_DSL_KEEP_PTX=1 to use system's ptxas"
145
+ )
146
+
147
+ cls = cutlass.cutlass_dsl.cuda_jit_executor.CudaDialectJitCompiledFunction
148
+ _original_load_cuda_library = cls._load_cuda_library
149
+ cls._load_cuda_library = _patched_load_cuda_library
150
+ _log("Patch applied")
151
+ return
build/torch-cuda/flashrt_fa4/cute/cute_dsl_utils.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Tuple
4
+ from functools import lru_cache
5
+
6
+ import torch
7
+
8
+ try:
9
+ from triton.tools.disasm import extract
10
+ except ImportError:
11
+ extract = None
12
+
13
+ import cutlass
14
+ import cutlass.cute as cute
15
+ from cutlass.cutlass_dsl import NumericMeta
16
+ from cutlass.cute.runtime import from_dlpack
17
+
18
+ StaticTypes = (cutlass.Constexpr, NumericMeta, int, bool, str, float, type(None))
19
+
20
+
21
+ load_cubin_module_data_og = cutlass.base_dsl.runtime.cuda.load_cubin_module_data
22
+ cute_compile_og = cute.compile
23
+
24
+
25
+ torch2cute_dtype_map = {
26
+ torch.float16: cutlass.Float16,
27
+ torch.bfloat16: cutlass.BFloat16,
28
+ torch.float32: cutlass.Float32,
29
+ torch.float8_e4m3fn: cutlass.Float8E4M3FN,
30
+ torch.float8_e5m2: cutlass.Float8E5M2,
31
+ }
32
+
33
+
34
+ @lru_cache
35
+ def get_max_active_clusters(cluster_size):
36
+ return cutlass.utils.HardwareInfo().get_max_active_clusters(cluster_size=cluster_size)
37
+
38
+
39
+ @lru_cache
40
+ def get_device_capacity(device: torch.device = None) -> Tuple[int, int]:
41
+ return torch.cuda.get_device_capability(device)
42
+
43
+
44
+ def assume_strides_aligned(t):
45
+ """Assume all strides except the last are divisible by 128 bits.
46
+
47
+ Python int strides (e.g., stride=0 from GQA expand) are kept as-is
48
+ since they're static and don't need alignment assumptions.
49
+ """
50
+ divby = 128 // t.element_type.width
51
+ strides = tuple(s if isinstance(s, int) else cute.assume(s, divby=divby) for s in t.stride[:-1])
52
+ return (*strides, t.stride[-1])
53
+
54
+
55
+ def assume_tensor_aligned(t):
56
+ """Rebuild a tensor with 128-bit aligned stride assumptions. Passes through None."""
57
+ if t is None:
58
+ return None
59
+ return cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=assume_strides_aligned(t)))
60
+
61
+
62
+ def to_cute_tensor(t, assumed_align=16, leading_dim=-1, fully_dynamic=False, enable_tvm_ffi=True):
63
+ """Convert torch tensor to cute tensor for TVM FFI. leading_dim=-1 defaults to t.ndim-1."""
64
+ # NOTE: torch 2.9.1 doesn't support fp8 via DLPack but 2.11.0 nightly does
65
+ # currently export raw bytes as uint8 and tell cutlass correct type
66
+ # can directly export as fp8 when torch supports it
67
+ if t.dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
68
+ tensor = from_dlpack(
69
+ t.view(torch.uint8).detach(),
70
+ assumed_align=assumed_align,
71
+ enable_tvm_ffi=enable_tvm_ffi,
72
+ )
73
+ tensor.element_type = (
74
+ cutlass.Float8E4M3FN if t.dtype == torch.float8_e4m3fn else cutlass.Float8E5M2
75
+ )
76
+ else:
77
+ tensor = from_dlpack(t.detach(), assumed_align=assumed_align, enable_tvm_ffi=enable_tvm_ffi)
78
+ if fully_dynamic:
79
+ return tensor.mark_layout_dynamic()
80
+ if leading_dim == -1:
81
+ leading_dim = t.ndim - 1
82
+ return tensor.mark_layout_dynamic(leading_dim=leading_dim)
83
+
84
+
85
+ def to_cute_aux_tensor(t, enable_tvm_ffi=True):
86
+ """Convert torch tensor to cute tensor for TVM FFI, tailored to FlexAttention aux tensors.
87
+ This allows the user to specify alignment and leading dimension for aux tensors used in
88
+ custom score_mod callables.
89
+ """
90
+ assumed_align: int = getattr(t, "__assumed_align__", None)
91
+ leading_dim: int = getattr(t, "__leading_dim__", None)
92
+ fully_dynamic: bool = leading_dim is None
93
+
94
+ return to_cute_tensor(
95
+ t,
96
+ assumed_align=assumed_align,
97
+ leading_dim=leading_dim,
98
+ fully_dynamic=fully_dynamic,
99
+ enable_tvm_ffi=enable_tvm_ffi,
100
+ )
101
+
102
+
103
+ def get_aux_tensor_metadata(aux_tensors):
104
+ return tuple(
105
+ (
106
+ getattr(t, "__assumed_align__", 0),
107
+ getattr(t, "__leading_dim__", -1),
108
+ hasattr(t, "__leading_dim__"),
109
+ )
110
+ for t in aux_tensors
111
+ )
112
+
113
+
114
+ def get_broadcast_dims(tensor: torch.Tensor) -> Tuple[bool, ...]:
115
+ """Return tuple of bools indicating which dims have stride=0 (broadcast).
116
+
117
+ This is useful for compile keys since CuTe's mark_layout_dynamic() keeps
118
+ stride=0 as static, meaning kernels compiled with different broadcast
119
+ patterns are not interchangeable.
120
+ """
121
+ return tuple(s == 0 for s in tensor.stride())
122
+
123
+
124
+ # credit: monellz (https://github.com/NVIDIA/cutlass/issues/2658#issuecomment-3630564264)
125
+ def dump_kernel_attributes(compiled_kernel):
126
+ from cuda.bindings import driver
127
+ from cutlass.utils import HardwareInfo
128
+ import torch
129
+
130
+ device_id = torch.cuda.current_device()
131
+ hardware_info = HardwareInfo(device_id=device_id)
132
+ cubin_data = compiled_kernel.artifacts.CUBIN
133
+ assert cubin_data is not None, "cubin_data is None, need '--keep-cubin' option when compiling"
134
+ cuda_library = hardware_info._checkCudaErrors(
135
+ driver.cuLibraryLoadData(cubin_data, None, None, 0, None, None, 0)
136
+ )
137
+ kernels = hardware_info._checkCudaErrors(driver.cuLibraryEnumerateKernels(1, cuda_library))
138
+ kernel = hardware_info._checkCudaErrors(driver.cuKernelGetFunction(kernels[0]))
139
+ # more metrics: https://docs.nvidia.com/cuda/cuda-driver-api/group__CUDA__EXEC.html#group__CUDA__EXEC_1g5e92a1b0d8d1b82cb00dcfb2de15961b
140
+ local_size_bytes = hardware_info._checkCudaErrors(
141
+ driver.cuFuncGetAttribute(
142
+ driver.CUfunction_attribute.CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES,
143
+ kernel,
144
+ )
145
+ )
146
+ num_regs = hardware_info._checkCudaErrors(
147
+ driver.cuFuncGetAttribute(
148
+ driver.CUfunction_attribute.CU_FUNC_ATTRIBUTE_NUM_REGS,
149
+ kernel,
150
+ )
151
+ )
152
+
153
+ print("--- Kernel Info ---")
154
+ print(f"local_size_bytes: {local_size_bytes}")
155
+ print(f"num_regs: {num_regs}")
156
+ print("--- End Kernel Info ---")
build/torch-cuda/flashrt_fa4/cute/fa_logging.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ """Unified FlashAttention logging controlled by a single ``FA_LOG_LEVEL`` env var.
4
+
5
+ Host-side messages go through Python ``logging`` (logger name ``flash_attn``).
6
+ A default ``StreamHandler`` is attached automatically when ``FA_LOG_LEVEL >= 1``
7
+ so that standalone scripts get output without extra setup; applications that
8
+ configure their own logging can remove or replace it via the standard API.
9
+
10
+ FA_LOG_LEVEL mapping::
11
+
12
+ 0 off nothing logged
13
+ 1 host host-side summaries only (no kernel printf)
14
+ 2 kernel host + curated kernel traces
15
+ 3 max host + all kernel traces (noisy, perf hit)
16
+
17
+ Set via environment variable::
18
+
19
+ FA_LOG_LEVEL=1 python train.py
20
+
21
+ Device-side ``cute.printf`` calls are compile-time eliminated via
22
+ ``cutlass.const_expr`` when the log level is below the callsite threshold,
23
+ so there is zero performance cost when device logging is off.
24
+ Changing the log level after kernel compilation requires a recompile
25
+ (the level participates in the forward compile key).
26
+ """
27
+
28
+ import logging
29
+ import os
30
+ import sys
31
+
32
+ import cutlass.cute as cute
33
+ from cutlass import const_expr
34
+
35
+ _LOG_LEVEL_NAMES = {"off": 0, "host": 1, "kernel": 2, "max": 3}
36
+
37
+
38
+ def _parse_log_level(raw: str) -> int:
39
+ if raw in _LOG_LEVEL_NAMES:
40
+ return _LOG_LEVEL_NAMES[raw]
41
+ try:
42
+ level = int(raw)
43
+ except ValueError:
44
+ return 0
45
+ return max(0, min(level, 3))
46
+
47
+
48
+ _fa_log_level: int = _parse_log_level(os.environ.get("FA_LOG_LEVEL", "0"))
49
+
50
+ _logger = logging.getLogger("flashrt_fa4")
51
+ _logger.addHandler(logging.NullHandler())
52
+ _default_handler: logging.Handler | None = None
53
+
54
+
55
+ def _configure_default_handler() -> None:
56
+ global _default_handler
57
+ if _fa_log_level >= 1:
58
+ if _default_handler is None:
59
+ _default_handler = logging.StreamHandler(sys.stdout)
60
+ _default_handler.setFormatter(logging.Formatter("[FA] %(message)s"))
61
+ _logger.addHandler(_default_handler)
62
+ _logger.setLevel(logging.DEBUG)
63
+ else:
64
+ if _default_handler is not None:
65
+ _logger.removeHandler(_default_handler)
66
+ _default_handler = None
67
+ _logger.setLevel(logging.WARNING)
68
+
69
+
70
+ _configure_default_handler()
71
+
72
+
73
+ def get_fa_log_level() -> int:
74
+ return _fa_log_level
75
+
76
+
77
+ def set_fa_log_level(level: int | str) -> None:
78
+ """Set the FA log level programmatically.
79
+
80
+ Host logging takes effect immediately. Device logging changes only
81
+ affect kernels compiled after this call (new compile-key selection).
82
+ """
83
+ global _fa_log_level
84
+ if isinstance(level, str):
85
+ level = _parse_log_level(level)
86
+ _fa_log_level = max(0, min(int(level), 3))
87
+ _configure_default_handler()
88
+
89
+
90
+ def fa_log(level: int, msg: str):
91
+ if _fa_log_level >= level:
92
+ _logger.info(msg)
93
+
94
+
95
+ def fa_printf(level: int, fmt, *args):
96
+ if const_expr(_fa_log_level >= level):
97
+ cute.printf(fmt, *args)
build/torch-cuda/flashrt_fa4/cute/fast_math.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import cutlass
4
+ import cutlass.cute as cute
5
+ from cutlass import Int32
6
+
7
+
8
+ @cute.jit
9
+ def clz(x: Int32) -> Int32:
10
+ # for i in cutlass.range_constexpr(32):
11
+ # if (1 << (31 - i)) & x:
12
+ # return Int32(i)
13
+ # return Int32(32)
14
+ # Early exit is not supported yet
15
+ res = Int32(32)
16
+ done = False
17
+ for i in cutlass.range(32):
18
+ if ((1 << (31 - i)) & x) and not done:
19
+ res = Int32(i)
20
+ done = True
21
+ return res
build/torch-cuda/flashrt_fa4/cute/flash_fwd_combine.py ADDED
@@ -0,0 +1,698 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_combine_kernel.h
3
+ # from Cutlass C++ to Cute-DSL.
4
+ import math
5
+ from typing import Type, Optional
6
+ from functools import partial
7
+
8
+ import cuda.bindings.driver as cuda
9
+
10
+ import cutlass
11
+ import cutlass.cute as cute
12
+ from cutlass.cute.nvgpu import cpasync
13
+ from cutlass import Float32, Int32, Boolean, const_expr
14
+
15
+ from fa4_cute_runtime.flashrt_fa4.cute import utils
16
+ from fa4_cute_runtime.flashrt_fa4.cute.cute_dsl_utils import assume_tensor_aligned
17
+ from fa4_cute_runtime.flashrt_fa4.cute.seqlen_info import SeqlenInfo
18
+ from cutlass.cute import FastDivmodDivisor
19
+
20
+
21
+ class FlashAttentionForwardCombine:
22
+ def __init__(
23
+ self,
24
+ dtype: Type[cutlass.Numeric],
25
+ dtype_partial: Type[cutlass.Numeric],
26
+ head_dim: int,
27
+ tile_m: int = 8,
28
+ k_block_size: int = 64,
29
+ log_max_splits: int = 4,
30
+ num_threads: int = 256,
31
+ stages: int = 4,
32
+ ):
33
+ """
34
+ Forward combine kernel for split attention computation.
35
+
36
+ :param dtype: output data type
37
+ :param dtype_partial: partial accumulation data type
38
+ :param head_dim: head dimension
39
+ :param tile_m: m block size
40
+ :param k_block_size: k block size
41
+ :param log_max_splits: log2 of maximum splits
42
+ :param num_threads: number of threads
43
+ :param varlen: whether using variable length sequences
44
+ :param stages: number of pipeline stages
45
+ """
46
+ self.dtype = dtype
47
+ self.dtype_partial = dtype_partial
48
+ self.head_dim = head_dim
49
+ self.tile_m = tile_m
50
+ self.k_block_size = k_block_size
51
+ self.max_splits = 1 << log_max_splits
52
+ self.num_threads = num_threads
53
+ self.is_even_k = head_dim % k_block_size == 0
54
+ self.stages = stages
55
+
56
+ @staticmethod
57
+ def can_implement(
58
+ dtype,
59
+ dtype_partial,
60
+ head_dim,
61
+ tile_m,
62
+ k_block_size,
63
+ log_max_splits,
64
+ num_threads,
65
+ ) -> bool:
66
+ """Check if the kernel can be implemented with the given parameters."""
67
+ if dtype not in [cutlass.Float16, cutlass.BFloat16, cutlass.Float32]:
68
+ return False
69
+ if dtype_partial not in [cutlass.Float16, cutlass.BFloat16, Float32]:
70
+ return False
71
+ if head_dim % 8 != 0:
72
+ return False
73
+ if num_threads % 32 != 0:
74
+ return False
75
+ if tile_m % 8 != 0:
76
+ return False
77
+ max_splits = 1 << log_max_splits
78
+ if max_splits > 256:
79
+ return False
80
+ if (tile_m * max_splits) % num_threads != 0:
81
+ return False
82
+ return True
83
+
84
+ def _setup_attributes(self):
85
+ # GMEM copy setup for O partial
86
+ universal_copy_bits = 128
87
+ async_copy_elems = universal_copy_bits // self.dtype_partial.width
88
+ assert self.k_block_size % async_copy_elems == 0
89
+
90
+ k_block_gmem = (
91
+ 128 if self.k_block_size % 128 == 0 else (64 if self.k_block_size % 64 == 0 else 32)
92
+ )
93
+ gmem_threads_per_row = k_block_gmem // async_copy_elems
94
+ assert self.num_threads % gmem_threads_per_row == 0
95
+
96
+ # Async copy atom for O partial load
97
+ atom_async_copy_partial = cute.make_copy_atom(
98
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
99
+ self.dtype_partial,
100
+ num_bits_per_copy=universal_copy_bits,
101
+ )
102
+ tOpartial_layout = cute.make_ordered_layout(
103
+ (self.num_threads // gmem_threads_per_row, gmem_threads_per_row),
104
+ order=(1, 0),
105
+ )
106
+ vOpartial_layout = cute.make_layout((1, async_copy_elems)) # 4 vals per load
107
+ self.gmem_tiled_copy_O_partial = cute.make_tiled_copy_tv(
108
+ atom_async_copy_partial, tOpartial_layout, vOpartial_layout
109
+ )
110
+
111
+ # GMEM copy setup for final O (use universal copy for store)
112
+ atom_universal_copy = cute.make_copy_atom(
113
+ cute.nvgpu.CopyUniversalOp(),
114
+ self.dtype,
115
+ num_bits_per_copy=async_copy_elems * self.dtype.width,
116
+ )
117
+ self.gmem_tiled_copy_O = cute.make_tiled_copy_tv(
118
+ atom_universal_copy,
119
+ tOpartial_layout,
120
+ vOpartial_layout, # 4 vals per store
121
+ )
122
+
123
+ # LSE copy setup with async copy (alignment = 1)
124
+ lse_copy_bits = Float32.width # 1 element per copy, width is in bits
125
+ m_block_smem = (
126
+ 128
127
+ if self.tile_m % 128 == 0
128
+ else (
129
+ 64
130
+ if self.tile_m % 64 == 0
131
+ else (32 if self.tile_m % 32 == 0 else (16 if self.tile_m % 16 == 0 else 8))
132
+ )
133
+ )
134
+ gmem_threads_per_row_lse = m_block_smem
135
+ assert self.num_threads % gmem_threads_per_row_lse == 0
136
+
137
+ # Async copy atom for LSE load
138
+ atom_async_copy_lse = cute.make_copy_atom(
139
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.ALWAYS),
140
+ Float32,
141
+ num_bits_per_copy=lse_copy_bits,
142
+ )
143
+ tLSE_layout = cute.make_ordered_layout(
144
+ (self.num_threads // gmem_threads_per_row_lse, gmem_threads_per_row_lse),
145
+ order=(1, 0),
146
+ )
147
+ vLSE_layout = cute.make_layout(1)
148
+ self.gmem_tiled_copy_LSE = cute.make_tiled_copy_tv(
149
+ atom_async_copy_lse, tLSE_layout, vLSE_layout
150
+ )
151
+
152
+ # ///////////////////////////////////////////////////////////////////////////////
153
+ # Shared memory
154
+ # ///////////////////////////////////////////////////////////////////////////////
155
+
156
+ # Shared memory to register copy for LSE
157
+ self.smem_threads_per_col_lse = self.num_threads // m_block_smem
158
+ assert 32 % self.smem_threads_per_col_lse == 0 # Must divide warp size
159
+
160
+ s2r_layout_atom_lse = cute.make_ordered_layout(
161
+ (self.smem_threads_per_col_lse, self.num_threads // self.smem_threads_per_col_lse),
162
+ order=(0, 1),
163
+ )
164
+ self.s2r_tiled_copy_LSE = cute.make_tiled_copy_tv(
165
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32),
166
+ s2r_layout_atom_lse,
167
+ cute.make_layout(1),
168
+ )
169
+
170
+ # LSE shared memory layout with swizzling to avoid bank conflicts
171
+ # This works for kBlockMSmem = 8, 16, 32, 64, 128, no bank conflicts
172
+ if const_expr(m_block_smem == 8):
173
+ smem_lse_swizzle = cute.make_swizzle(5, 0, 5)
174
+ elif const_expr(m_block_smem == 16):
175
+ smem_lse_swizzle = cute.make_swizzle(4, 0, 4)
176
+ else:
177
+ smem_lse_swizzle = cute.make_swizzle(3, 2, 3)
178
+ smem_layout_atom_lse = cute.make_composed_layout(
179
+ smem_lse_swizzle, 0, cute.make_ordered_layout((8, m_block_smem), order=(1, 0))
180
+ )
181
+ self.smem_layout_lse = cute.tile_to_shape(
182
+ smem_layout_atom_lse, (self.max_splits, self.tile_m), (0, 1)
183
+ )
184
+
185
+ # O partial shared memory layout (simple layout for pipeline stages)
186
+ self.smem_layout_o = cute.make_ordered_layout(
187
+ (self.tile_m, self.k_block_size, self.stages), order=(1, 0, 2)
188
+ )
189
+
190
+ @cute.jit
191
+ def __call__(
192
+ self,
193
+ mO_partial: cute.Tensor,
194
+ mLSE_partial: cute.Tensor,
195
+ mO: cute.Tensor,
196
+ mLSE: Optional[cute.Tensor] = None,
197
+ cu_seqlens: Optional[cute.Tensor] = None,
198
+ seqused: Optional[cute.Tensor] = None,
199
+ num_splits_dynamic_ptr: Optional[cute.Tensor] = None,
200
+ varlen_batch_idx: Optional[cute.Tensor] = None,
201
+ semaphore_to_reset: Optional[cute.Tensor] = None,
202
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
203
+ stream: cuda.CUstream = None,
204
+ ):
205
+ # Type checking
206
+ if const_expr(not (mO_partial.element_type == self.dtype_partial)):
207
+ raise TypeError("O partial tensor must match dtype_partial")
208
+ if const_expr(not (mO.element_type == self.dtype)):
209
+ raise TypeError("O tensor must match dtype")
210
+ if const_expr(mLSE_partial.element_type not in [Float32]):
211
+ raise TypeError("LSE partial tensor must be Float32")
212
+ if const_expr(mLSE is not None and mLSE.element_type not in [Float32]):
213
+ raise TypeError("LSE tensor must be Float32")
214
+
215
+ # Shape validation - input tensors are in user format, need to be converted to kernel format
216
+ if const_expr(len(mO_partial.shape) not in [4, 5]):
217
+ raise ValueError(
218
+ "O partial tensor must have 4 or 5 dimensions: (num_splits, batch, seqlen, nheads, headdim) or (num_splits, total_q, nheads, headdim)"
219
+ )
220
+ if const_expr(len(mLSE_partial.shape) not in [3, 4]):
221
+ raise ValueError(
222
+ "LSE partial tensor must have 3 or 4 dimensions: (num_splits, batch, seqlen, nheads) or (num_splits, total_q, nheads)"
223
+ )
224
+ if const_expr(len(mO.shape) not in [3, 4]):
225
+ raise ValueError(
226
+ "O tensor must have 3 or 4 dimensions: (batch, seqlen, nheads, headdim) or (total_q, nheads, headdim)"
227
+ )
228
+ if const_expr(mLSE is not None and len(mLSE.shape) not in [2, 3]):
229
+ raise ValueError(
230
+ "LSE tensor must have 2 or 3 dimensions: (batch, seqlen, nheads) or (total_q, nheads)"
231
+ )
232
+
233
+ mO_partial, mO = [assume_tensor_aligned(t) for t in (mO_partial, mO)]
234
+ # (num_splits, b, seqlen, h, d) -> (seqlen, d, num_splits, h, b)
235
+ # or (num_splits, total_q, h, d) -> (total_q, d, num_splits, h)
236
+ O_partial_layout_transpose = (
237
+ [2, 4, 0, 3, 1] if const_expr(cu_seqlens is None) else [1, 3, 0, 2]
238
+ )
239
+ # (b, seqlen, h, d) -> (seqlen, d, h, b) or (total_q, h, d) -> (total_q, d, h)
240
+ mO_partial = cute.make_tensor(
241
+ mO_partial.iterator, cute.select(mO_partial.layout, mode=O_partial_layout_transpose)
242
+ )
243
+ O_layout_transpose = [1, 3, 2, 0] if const_expr(cu_seqlens is None) else [0, 2, 1]
244
+ mO = cute.make_tensor(mO.iterator, cute.select(mO.layout, mode=O_layout_transpose))
245
+ # (num_splits, b, seqlen, h) -> (seqlen, num_splits, h, b)
246
+ # or (num_splits, total_q, h) -> (total_q, num_splits, h)
247
+ LSE_partial_layout_transpose = [2, 0, 3, 1] if const_expr(cu_seqlens is None) else [1, 0, 2]
248
+ mLSE_partial = cute.make_tensor(
249
+ mLSE_partial.iterator,
250
+ cute.select(mLSE_partial.layout, mode=LSE_partial_layout_transpose),
251
+ )
252
+ # (b, seqlen, h) -> (seqlen, h, b) or (total_q, h) -> (total_q, h)
253
+ LSE_layout_transpose = [1, 2, 0] if const_expr(cu_seqlens is None) else [0, 1]
254
+ mLSE = (
255
+ cute.make_tensor(mLSE.iterator, cute.select(mLSE.layout, mode=LSE_layout_transpose))
256
+ if mLSE is not None
257
+ else None
258
+ )
259
+
260
+ # Determine if we have variable length sequences
261
+ varlen = const_expr(cu_seqlens is not None or seqused is not None)
262
+
263
+ self._setup_attributes()
264
+
265
+ @cute.struct
266
+ class SharedStorage:
267
+ sLSE: cute.struct.Align[
268
+ cute.struct.MemRange[Float32, cute.cosize(self.smem_layout_lse)], 128
269
+ ]
270
+ sMaxValidSplit: cute.struct.Align[cute.struct.MemRange[Int32, self.tile_m], 128]
271
+ sO: cute.struct.Align[
272
+ cute.struct.MemRange[self.dtype_partial, cute.cosize(self.smem_layout_o)], 128
273
+ ]
274
+
275
+ smem_size = SharedStorage.size_in_bytes()
276
+
277
+ # Grid dimensions: (ceil_div(seqlen, m_block), ceil_div(head_dim, k_block), num_head * batch)
278
+ seqlen = mO_partial.shape[0]
279
+ num_head = mO_partial.shape[3]
280
+ batch_size = (
281
+ mO_partial.shape[4]
282
+ if const_expr(cu_seqlens is None)
283
+ else Int32(cu_seqlens.shape[0] - 1)
284
+ )
285
+
286
+ # Create FastDivmodDivisor objects for efficient division
287
+ seqlen_divmod = FastDivmodDivisor(seqlen)
288
+ head_divmod = FastDivmodDivisor(num_head)
289
+
290
+ grid_dim = (
291
+ cute.ceil_div(seqlen * num_head, self.tile_m),
292
+ cute.ceil_div(self.head_dim, self.k_block_size),
293
+ batch_size,
294
+ )
295
+
296
+ self.kernel(
297
+ mO_partial,
298
+ mLSE_partial,
299
+ mO,
300
+ mLSE,
301
+ cu_seqlens,
302
+ seqused,
303
+ num_splits_dynamic_ptr,
304
+ varlen_batch_idx,
305
+ semaphore_to_reset,
306
+ SharedStorage,
307
+ self.smem_layout_lse,
308
+ self.smem_layout_o,
309
+ self.gmem_tiled_copy_O_partial,
310
+ self.gmem_tiled_copy_O,
311
+ self.gmem_tiled_copy_LSE,
312
+ self.s2r_tiled_copy_LSE,
313
+ seqlen_divmod,
314
+ head_divmod,
315
+ varlen,
316
+ ).launch(
317
+ grid=grid_dim,
318
+ block=[self.num_threads, 1, 1],
319
+ smem=smem_size,
320
+ stream=stream,
321
+ )
322
+
323
+ @cute.kernel
324
+ def kernel(
325
+ self,
326
+ mO_partial: cute.Tensor,
327
+ mLSE_partial: cute.Tensor,
328
+ mO: cute.Tensor,
329
+ mLSE: Optional[cute.Tensor],
330
+ cu_seqlens: Optional[cute.Tensor],
331
+ seqused: Optional[cute.Tensor],
332
+ num_splits_dynamic_ptr: Optional[cute.Tensor],
333
+ varlen_batch_idx: Optional[cute.Tensor],
334
+ semaphore_to_reset: Optional[cute.Tensor],
335
+ SharedStorage: cutlass.Constexpr,
336
+ smem_layout_lse: cute.Layout | cute.ComposedLayout,
337
+ smem_layout_o: cute.Layout,
338
+ gmem_tiled_copy_O_partial: cute.TiledCopy,
339
+ gmem_tiled_copy_O: cute.TiledCopy,
340
+ gmem_tiled_copy_LSE: cute.TiledCopy,
341
+ s2r_tiled_copy_LSE: cute.TiledCopy,
342
+ seqlen_divmod: FastDivmodDivisor,
343
+ head_divmod: FastDivmodDivisor,
344
+ varlen: cutlass.Constexpr[bool],
345
+ ):
346
+ # Thread and block indices
347
+ tidx, _, _ = cute.arch.thread_idx()
348
+ m_block, k_block, maybe_virtual_batch = cute.arch.block_idx()
349
+
350
+ # Map virtual batch index to real batch index (for persistent tile schedulers)
351
+ batch_idx = (
352
+ varlen_batch_idx[maybe_virtual_batch]
353
+ if const_expr(varlen_batch_idx is not None)
354
+ else maybe_virtual_batch
355
+ )
356
+
357
+ # ///////////////////////////////////////////////////////////////////////////////
358
+ # Get shared memory buffer
359
+ # ///////////////////////////////////////////////////////////////////////////////
360
+ smem = cutlass.utils.SmemAllocator()
361
+ storage = smem.allocate(SharedStorage)
362
+ sLSE = storage.sLSE.get_tensor(smem_layout_lse)
363
+ sMaxValidSplit = storage.sMaxValidSplit.get_tensor((self.tile_m,))
364
+ sO = storage.sO.get_tensor(smem_layout_o)
365
+
366
+ # Handle semaphore reset — wait for dependent grids first
367
+ if const_expr(semaphore_to_reset is not None):
368
+ if (
369
+ tidx == 0
370
+ and m_block == cute.arch.grid_dim()[0] - 1
371
+ and k_block == cute.arch.grid_dim()[1] - 1
372
+ and maybe_virtual_batch == cute.arch.grid_dim()[2] - 1
373
+ ):
374
+ cute.arch.griddepcontrol_wait()
375
+ semaphore_to_reset[0] = 0
376
+
377
+ # Get number of splits (use maybe_virtual_batch for per-batch-slot splits)
378
+ num_splits = (
379
+ num_splits_dynamic_ptr[maybe_virtual_batch]
380
+ if const_expr(num_splits_dynamic_ptr is not None)
381
+ else mLSE_partial.shape[1]
382
+ )
383
+ # Handle variable length sequences using SeqlenInfo
384
+ seqlen_info = SeqlenInfo.create(
385
+ batch_idx=batch_idx,
386
+ seqlen_static=mO_partial.shape[0],
387
+ cu_seqlens=cu_seqlens,
388
+ seqused=seqused,
389
+ # Don't need to pass in tile size since we won't use offset_padded
390
+ )
391
+ seqlen, offset = seqlen_info.seqlen, seqlen_info.offset
392
+
393
+ # Extract number of heads (head index will be determined dynamically)
394
+ num_head = mO_partial.shape[3]
395
+ max_idx = seqlen * num_head
396
+
397
+ # Early exit for single split if dynamic
398
+ if (const_expr(num_splits_dynamic_ptr is None) or num_splits > 1) and (
399
+ const_expr(not varlen) or m_block * self.tile_m < max_idx
400
+ ):
401
+ # Wait for dependent grids (e.g., the main attention kernel that produces O_partial/LSE_partial)
402
+ cute.arch.griddepcontrol_wait()
403
+
404
+ # ===============================
405
+ # Step 1: Load LSE_partial from gmem to shared memory
406
+ # ===============================
407
+
408
+ mLSE_partial_cur = seqlen_info.offset_batch(mLSE_partial, batch_idx, dim=3)
409
+ mLSE_partial_copy = cute.tiled_divide(mLSE_partial_cur, (1,))
410
+ gmem_thr_copy_LSE = gmem_tiled_copy_LSE.get_slice(tidx)
411
+ tLSEsLSE = gmem_thr_copy_LSE.partition_D(sLSE)
412
+ # Create identity tensor for coordinate tracking
413
+ cLSE = cute.make_identity_tensor((self.max_splits, self.tile_m))
414
+ tLSEcLSE = gmem_thr_copy_LSE.partition_S(cLSE)
415
+
416
+ # Load LSE partial values
417
+ for m in cutlass.range(cute.size(tLSEcLSE, mode=[2]), unroll_full=True):
418
+ mi = tLSEcLSE[0, 0, m][1] # Get m coordinate
419
+ idx = m_block * self.tile_m + mi
420
+ if idx < max_idx:
421
+ # Calculate actual sequence position and head using FastDivmodDivisor
422
+ if const_expr(not varlen):
423
+ head_idx, m_idx = divmod(idx, seqlen_divmod)
424
+ else:
425
+ head_idx = idx // seqlen
426
+ m_idx = idx - head_idx * seqlen
427
+ mLSE_partial_cur_copy = mLSE_partial_copy[None, m_idx, None, head_idx]
428
+ for s in cutlass.range(cute.size(tLSEcLSE, mode=[1]), unroll_full=True):
429
+ si = tLSEcLSE[0, s, 0][0] # Get split coordinate
430
+ if si < num_splits:
431
+ cute.copy(
432
+ gmem_thr_copy_LSE,
433
+ mLSE_partial_cur_copy[None, si],
434
+ tLSEsLSE[None, s, m],
435
+ )
436
+ else:
437
+ tLSEsLSE[None, s, m].fill(-Float32.inf)
438
+ # Don't need to zero out the rest of the LSEs, as we will not write the output to gmem
439
+ cute.arch.cp_async_commit_group()
440
+
441
+ # ===============================
442
+ # Step 2: Load O_partial for pipeline stages
443
+ # ===============================
444
+
445
+ gmem_thr_copy_O_partial = gmem_tiled_copy_O_partial.get_slice(tidx)
446
+ cO = cute.make_identity_tensor((self.tile_m, self.k_block_size))
447
+ tOcO = gmem_thr_copy_O_partial.partition_D(cO)
448
+ tOsO_partial = gmem_thr_copy_O_partial.partition_D(sO)
449
+ mO_partial_cur = seqlen_info.offset_batch(mO_partial, batch_idx, dim=4)
450
+
451
+ # Precompute these values to avoid recomputing them in the loop
452
+ num_rows = const_expr(cute.size(tOcO, mode=[1]))
453
+ tOmidx = cute.make_rmem_tensor(num_rows, cutlass.Int32)
454
+ tOhidx = cute.make_rmem_tensor(num_rows, cutlass.Int32)
455
+ tOrOptr = cute.make_rmem_tensor(num_rows, cutlass.Int64)
456
+ for m in cutlass.range(num_rows, unroll_full=True):
457
+ mi = tOcO[0, m, 0][0] # m coordinate
458
+ idx = m_block * self.tile_m + mi
459
+ if const_expr(not varlen):
460
+ tOhidx[m], tOmidx[m] = divmod(idx, seqlen_divmod)
461
+ else:
462
+ tOhidx[m] = idx // seqlen
463
+ tOmidx[m] = idx - tOhidx[m] * seqlen
464
+ tOrOptr[m] = utils.elem_pointer(
465
+ mO_partial_cur, (tOmidx[m], k_block * self.k_block_size, 0, tOhidx[m])
466
+ ).toint()
467
+ if idx >= max_idx:
468
+ tOhidx[m] = -1
469
+
470
+ tOpO = None
471
+ if const_expr(not self.is_even_k):
472
+ tOpO = cute.make_rmem_tensor(cute.size(tOcO, mode=[2]), Boolean)
473
+ for k in cutlass.range(cute.size(tOpO), unroll_full=True):
474
+ tOpO[k] = tOcO[0, 0, k][1] < mO_partial.shape[1] - k_block * self.k_block_size
475
+ # if cute.arch.thread_idx()[0] == 0 and k_block == 1: cute.print_tensor(tOpO)
476
+
477
+ load_O_partial = partial(
478
+ self.load_O_partial,
479
+ gmem_tiled_copy_O_partial,
480
+ tOrOptr,
481
+ tOsO_partial,
482
+ tOhidx,
483
+ tOpO,
484
+ tOcO,
485
+ mO_partial_cur.layout,
486
+ )
487
+
488
+ # Load first few stages of O_partial
489
+ for stage in cutlass.range(self.stages - 1, unroll_full=True):
490
+ if stage < num_splits:
491
+ load_O_partial(stage, stage)
492
+ cute.arch.cp_async_commit_group()
493
+
494
+ # ===============================
495
+ # Step 3: Load and transpose LSE from smem to registers
496
+ # ===============================
497
+
498
+ # Wait for LSE and initial O partial stages to complete
499
+ cute.arch.cp_async_wait_group(self.stages - 1)
500
+ cute.arch.sync_threads()
501
+ # if cute.arch.thread_idx()[0] == 0:
502
+ # # cute.print_tensor(sLSE)
503
+ # for i in range(64):
504
+ # cute.printf("sLSE[%d, 0] = %f", i, sLSE[i, 0])
505
+ # cute.arch.sync_threads()
506
+
507
+ s2r_thr_copy_LSE = s2r_tiled_copy_LSE.get_slice(tidx)
508
+ ts2rsLSE = s2r_thr_copy_LSE.partition_S(sLSE)
509
+ ts2rrLSE = cute.make_rmem_tensor_like(ts2rsLSE)
510
+ cute.copy(s2r_tiled_copy_LSE, ts2rsLSE, ts2rrLSE)
511
+
512
+ # ===============================
513
+ # Step 4: Compute final LSE along split dimension
514
+ # ===============================
515
+
516
+ lse_sum = cute.make_rmem_tensor(cute.size(ts2rrLSE, mode=[2]), Float32)
517
+ ts2rcLSE = s2r_thr_copy_LSE.partition_D(cLSE)
518
+ # We compute the max valid split for each row to short-circuit the computation later
519
+ max_valid_split = cute.make_rmem_tensor(cute.size(ts2rrLSE, mode=[2]), Int32)
520
+ assert cute.size(ts2rrLSE, mode=[0]) == 1
521
+ # Compute max, scales, and final LSE for each row
522
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
523
+ # Find max LSE value across splits
524
+ threads_per_col = const_expr(self.smem_threads_per_col_lse)
525
+ lse_max = cute.arch.warp_reduction_max(
526
+ ts2rrLSE[None, None, m]
527
+ .load()
528
+ .reduce(cute.ReductionOp.MAX, init_val=-Float32.inf, reduction_profile=0),
529
+ threads_in_group=threads_per_col,
530
+ )
531
+ # if cute.arch.thread_idx()[0] == 0: cute.printf(lse_max)
532
+ # Find max valid split index
533
+ max_valid_idx = -1
534
+ for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
535
+ if ts2rrLSE[0, s, m] != -Float32.inf:
536
+ max_valid_idx = ts2rcLSE[0, s, 0][0] # Get split coordinate
537
+ # if cute.arch.thread_idx()[0] < 32: cute.printf(max_valid_idx)
538
+ max_valid_split[m] = cute.arch.warp_reduction_max(
539
+ max_valid_idx, threads_in_group=threads_per_col
540
+ )
541
+ # Compute exp scales and sum
542
+ lse_max_cur = (
543
+ 0.0 if lse_max == -Float32.inf else lse_max
544
+ ) # In case all local LSEs are -inf
545
+ LOG2_E = math.log2(math.e)
546
+ lse_sum_cur = 0.0
547
+ for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
548
+ scale = cute.math.exp2(
549
+ ts2rrLSE[0, s, m] * LOG2_E - (lse_max_cur * LOG2_E), fastmath=True
550
+ )
551
+ lse_sum_cur += scale
552
+ ts2rrLSE[0, s, m] = scale # Store scale for later use
553
+ lse_sum_cur = cute.arch.warp_reduction_sum(
554
+ lse_sum_cur, threads_in_group=threads_per_col
555
+ )
556
+ lse_sum[m] = cute.math.log(lse_sum_cur, fastmath=True) + lse_max
557
+ # Normalize scales
558
+ inv_sum = (
559
+ 0.0 if (lse_sum_cur == 0.0 or lse_sum_cur != lse_sum_cur) else 1.0 / lse_sum_cur
560
+ )
561
+ ts2rrLSE[None, None, m].store(ts2rrLSE[None, None, m].load() * inv_sum)
562
+ # Store the scales exp(lse - lse_logsum) back to smem
563
+ cute.copy(s2r_tiled_copy_LSE, ts2rrLSE, ts2rsLSE)
564
+
565
+ # Store max valid split to smem
566
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
567
+ if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
568
+ mi = ts2rcLSE[0, 0, m][1]
569
+ if mi < self.tile_m:
570
+ sMaxValidSplit[mi] = max_valid_split[m]
571
+
572
+ # ===============================
573
+ # Step 5: Store final LSE to gmem
574
+ # ===============================
575
+
576
+ if const_expr(mLSE is not None):
577
+ if const_expr(cu_seqlens is None):
578
+ mLSE_cur = mLSE[None, None, batch_idx]
579
+ else:
580
+ mLSE_cur = cute.domain_offset((offset, 0), mLSE)
581
+ if k_block == 0: # Only first k_block writes LSE when mLSE is provided
582
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
583
+ if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
584
+ mi = ts2rcLSE[0, 0, m][1]
585
+ idx = m_block * self.tile_m + mi
586
+ if idx < max_idx:
587
+ if const_expr(not varlen):
588
+ head_idx, m_idx = divmod(idx, seqlen_divmod)
589
+ else:
590
+ head_idx = idx // seqlen
591
+ m_idx = idx - head_idx * seqlen
592
+ mLSE_cur[m_idx, head_idx] = lse_sum[m]
593
+
594
+ # ===============================
595
+ # Step 6: Read O_partial and accumulate final O
596
+ # ===============================
597
+
598
+ cute.arch.sync_threads()
599
+
600
+ # Get max valid split for this thread
601
+ thr_max_valid_split = sMaxValidSplit[tOcO[0, 0, 0][0]]
602
+ for m in cutlass.range(1, cute.size(tOcO, mode=[1]), unroll_full=True):
603
+ thr_max_valid_split = max(thr_max_valid_split, sMaxValidSplit[tOcO[0, m, 0][0]])
604
+
605
+ tOrO_partial = cute.make_rmem_tensor_like(tOsO_partial[None, None, None, 0])
606
+ tOrO = cute.make_rmem_tensor_like(tOrO_partial, Float32)
607
+ tOrO.fill(0.0)
608
+
609
+ stage_load = self.stages - 1
610
+ stage_compute = 0
611
+
612
+ # Main accumulation loop
613
+ for s in cutlass.range(thr_max_valid_split + 1, unroll=4):
614
+ # Get scales for this split
615
+ scale = cute.make_rmem_tensor(num_rows, Float32)
616
+ for m in cutlass.range(num_rows, unroll_full=True):
617
+ scale[m] = sLSE[s, tOcO[0, m, 0][0]] # Get scale from smem
618
+
619
+ # Load next stage if needed
620
+ split_to_load = s + self.stages - 1
621
+ if split_to_load <= thr_max_valid_split:
622
+ load_O_partial(split_to_load, stage_load)
623
+ cute.arch.cp_async_commit_group()
624
+ stage_load = 0 if stage_load == self.stages - 1 else stage_load + 1
625
+
626
+ # Wait for the current stage to be ready
627
+ cute.arch.cp_async_wait_group(self.stages - 1)
628
+ # We don't need __syncthreads() because each thread is just reading its own data from smem
629
+ # Copy from smem to registers
630
+ cute.autovec_copy(tOsO_partial[None, None, None, stage_compute], tOrO_partial)
631
+ stage_compute = 0 if stage_compute == self.stages - 1 else stage_compute + 1
632
+
633
+ # Accumulate scaled partial results
634
+ for m in cutlass.range(num_rows, unroll_full=True):
635
+ if tOhidx[m] >= 0 and scale[m] > 0.0:
636
+ tOrO[None, m, None].store(
637
+ tOrO[None, m, None].load()
638
+ + scale[m] * tOrO_partial[None, m, None].load().to(Float32)
639
+ )
640
+
641
+ # ===============================
642
+ # Step 7: Write final O to gmem
643
+ # ===============================
644
+
645
+ rO = cute.make_rmem_tensor_like(tOrO, self.dtype)
646
+ rO.store(tOrO.load().to(self.dtype))
647
+ mO_cur = seqlen_info.offset_batch(mO, batch_idx, dim=3)
648
+ if const_expr(cu_seqlens is None):
649
+ mO_cur = mO[None, None, None, batch_idx]
650
+ else:
651
+ mO_cur = cute.domain_offset((offset, 0, 0), mO)
652
+ mO_cur = utils.domain_offset_aligned((0, k_block * self.k_block_size, 0), mO_cur)
653
+ elems_per_store = const_expr(cute.size(gmem_tiled_copy_O.layout_tv_tiled[1]))
654
+ # mO_cur_copy = cute.tiled_divide(mO_cur, (1, elems_per_store,))
655
+ gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
656
+ # Write final results
657
+ for m in cutlass.range(num_rows, unroll_full=True):
658
+ if tOhidx[m] >= 0:
659
+ mO_cur_copy = cute.tiled_divide(
660
+ mO_cur[tOmidx[m], None, tOhidx[m]], (elems_per_store,)
661
+ )
662
+ for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
663
+ k_idx = tOcO[0, 0, k][1] // elems_per_store
664
+ if const_expr(self.is_even_k) or tOpO[k]:
665
+ cute.copy(gmem_thr_copy_O, rO[None, m, k], mO_cur_copy[None, k_idx])
666
+
667
+ @cute.jit
668
+ def load_O_partial(
669
+ self,
670
+ gmem_tiled_copy_O_partial: cute.TiledCopy,
671
+ tOrOptr: cute.Tensor,
672
+ tOsO_partial: cute.Tensor,
673
+ tOhidx: cute.Tensor,
674
+ tOpO: Optional[cute.Tensor],
675
+ tOcO: cute.Tensor,
676
+ mO_cur_partial_layout: cute.Layout,
677
+ split: Int32,
678
+ stage: Int32,
679
+ ) -> None:
680
+ elems_per_load = const_expr(cute.size(gmem_tiled_copy_O_partial.layout_tv_tiled[1]))
681
+ tOsO_partial_cur = tOsO_partial[None, None, None, stage]
682
+ for m in cutlass.range(cute.size(tOcO, [1]), unroll_full=True):
683
+ if tOhidx[m] >= 0:
684
+ o_gmem_ptr = cute.make_ptr(
685
+ tOsO_partial.element_type, tOrOptr[m], cute.AddressSpace.gmem, assumed_align=16
686
+ )
687
+ mO_partial_cur = cute.make_tensor(
688
+ o_gmem_ptr, cute.slice_(mO_cur_partial_layout, (0, None, None, 0))
689
+ )
690
+ mO_partial_cur_copy = cute.tiled_divide(mO_partial_cur, (elems_per_load,))
691
+ for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
692
+ k_idx = tOcO[0, 0, k][1] // elems_per_load
693
+ if const_expr(tOpO is None) or tOpO[k]:
694
+ cute.copy(
695
+ gmem_tiled_copy_O_partial,
696
+ mO_partial_cur_copy[None, k_idx, split],
697
+ tOsO_partial_cur[None, m, k],
698
+ )
build/torch-cuda/flashrt_fa4/cute/flash_fwd_sm100.py ADDED
The diff for this file is too large to render. See raw diff
 
build/torch-cuda/flashrt_fa4/cute/interface_fwd_sm100.py ADDED
@@ -0,0 +1,1595 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # [2025-07-04] Version in Cute-DSL, for Hopper and Blackwell. You'll need install nvidia-cutlass-dsl==4.2.0.
3
+
4
+ import os
5
+ import math
6
+ from dataclasses import dataclass
7
+ from functools import lru_cache
8
+ from typing import Optional, Tuple, Callable
9
+
10
+ import torch
11
+
12
+
13
+ import cuda.bindings.driver as cuda
14
+
15
+ import cutlass
16
+ import cutlass.cute as cute
17
+ from cutlass import Int32, Float32
18
+ from fa4_cute_runtime.quack.compile_utils import make_fake_tensor as fake_tensor
19
+ from fa4_cute_runtime.flashrt_fa4.cute.cache_utils import get_jit_cache
20
+ from fa4_cute_runtime.flashrt_fa4.cute.testing import is_fake_mode
21
+
22
+
23
+ if os.environ.get("CUTE_DSL_PTXAS_PATH", None) is not None:
24
+ from fa4_cute_runtime.flashrt_fa4.cute import cute_dsl_ptxas # noqa: F401
25
+
26
+ # Patch to dump ptx and then use system ptxas to compile to cubin
27
+ cute_dsl_ptxas.patch()
28
+
29
+
30
+ from fa4_cute_runtime.flashrt_fa4.cute import utils
31
+ from fa4_cute_runtime.flashrt_fa4.cute import fa_logging
32
+ from fa4_cute_runtime.flashrt_fa4.cute.cute_dsl_utils import (
33
+ to_cute_tensor, to_cute_aux_tensor, get_aux_tensor_metadata, get_broadcast_dims,
34
+ )
35
+ from fa4_cute_runtime.flashrt_fa4.cute.flash_fwd_sm100 import FlashAttentionForwardSm100, DescaleTensors
36
+ from fa4_cute_runtime.flashrt_fa4.cute.flash_fwd_combine import FlashAttentionForwardCombine
37
+
38
+ # --- FlashRT Thor vendor: forward / SM100-only trim -------------------------
39
+ # This vendored copy keeps ONLY the SM100 (Blackwell, including sm_110/Thor)
40
+ # forward kernel. The SM80/SM90/SM120 forward kernels, every backward kernel,
41
+ # the MLA forward kernel, and the head_dim=256 2CTA kernels were removed (see
42
+ # VENDOR.md). The names below are still referenced by dead arch-dispatch
43
+ # branches inside `_flash_attn_fwd`; on Blackwell those branches never run. If
44
+ # one is reached on unsupported hardware it raises a clear error instead of an
45
+ # obscure NameError. Backward is not provided (inference / forward only).
46
+ def _fa4_trimmed(_name):
47
+ def _raise(*_a, **_k):
48
+ raise RuntimeError(
49
+ f"{_name} was removed from FlashRT's forward/SM100-only FA4 vendor. "
50
+ "This build supports Blackwell SM100/SM110 forward attention only; "
51
+ "use upstream flash-attn for other architectures or training."
52
+ )
53
+ return _raise
54
+
55
+ FlashAttentionForwardSm80 = _fa4_trimmed("FlashAttentionForwardSm80")
56
+ FlashAttentionForwardSm90 = _fa4_trimmed("FlashAttentionForwardSm90")
57
+ FlashAttentionForwardSm120 = _fa4_trimmed("FlashAttentionForwardSm120")
58
+ FlashAttentionMLAForwardSm100 = _fa4_trimmed("FlashAttentionMLAForwardSm100")
59
+ from fa4_cute_runtime.flashrt_fa4.cute.sm100_hd256_2cta_fmha_forward import (
60
+ BlackwellFusedMultiHeadAttentionForward,
61
+ )
62
+
63
+ from fa4_cute_runtime.flashrt_fa4.cute.block_sparsity import (
64
+ BlockSparseTensorsTorch,
65
+ get_sparse_q_block_size,
66
+ to_cute_block_sparse_tensors,
67
+ normalize_block_sparse_config,
68
+ normalize_block_sparse_config_bwd,
69
+ )
70
+
71
+ def _parse_arch_str(arch_str):
72
+ """Parse arch string (e.g. 'sm_80', 'sm_90a', '80', '100') to int (e.g. 80, 90, 100)."""
73
+ import re
74
+ match = re.match(r"^(?:sm_?|SM_?)?(\d+)(\d)([af]?)$", arch_str)
75
+ if not match:
76
+ raise ValueError(f"Invalid arch format: {arch_str}")
77
+ major, minor, _ = match.groups()
78
+ return int(major) * 10 + int(minor)
79
+
80
+
81
+ @lru_cache(maxsize=None)
82
+ def _get_device_arch():
83
+ """Cached device arch check.
84
+
85
+ Override with FLASH_ATTENTION_ARCH (e.g. 'sm_80' or '80') to select which
86
+ kernel path to use (SM80/SM90/SM100/SM120) independently of the compilation
87
+ target (CUTE_DSL_ARCH).
88
+
89
+ For CPU-only compilation (no GPU), set both:
90
+ FLASH_ATTENTION_ARCH=sm_80 (kernel selection)
91
+ CUTE_DSL_ARCH=sm_80 (compilation target)
92
+ """
93
+ arch_override = os.environ.get("FLASH_ATTENTION_ARCH", None)
94
+ if arch_override is not None:
95
+ return _parse_arch_str(arch_override)
96
+ major, minor = torch.cuda.get_device_capability()
97
+ return major * 10 + int(minor)
98
+
99
+
100
+ # Resolve the dispatch architecture once when the module is imported. The Hub
101
+ # wrapper sets FLASH_ATTENTION_ARCH before importing this module, so tracing a
102
+ # model never executes device discovery or parses environment strings.
103
+ _IMPORT_DEVICE_ARCH = _get_device_arch()
104
+
105
+
106
+ def _validate_head_dims(head_dim: int, head_dim_v: int, compute_capability: int, alignment: int) -> None:
107
+ """Validate head dimension constraints based on compute capability."""
108
+ is_deepseek_shape = head_dim == 192 and head_dim_v == 128
109
+ is_deepseek_mla_absorbed_shape = head_dim == 64 and head_dim_v == 512
110
+ is_dedicate_kernel_shape = head_dim == 256 and head_dim_v == 256
111
+ is_standard_range = 8 <= head_dim <= 128 and 8 <= head_dim_v <= 128
112
+
113
+ is_sm90_range = 8 <= head_dim <= 256 and 8 <= head_dim_v <= 256
114
+ if compute_capability == 9:
115
+ assert is_sm90_range and head_dim % alignment == 0 and head_dim_v % alignment == 0, (
116
+ f"(head_dim, head_dim_v)=({head_dim}, {head_dim_v}) is not supported on SM90. "
117
+ f"head_dim and head_dim_v must be between 8 and 256 and divisible by {alignment}."
118
+ )
119
+ elif compute_capability in [10, 11]:
120
+ assert (is_standard_range or is_deepseek_shape or is_deepseek_mla_absorbed_shape or is_dedicate_kernel_shape) and head_dim % alignment == 0 and head_dim_v % alignment == 0, (
121
+ f"(head_dim, head_dim_v)=({head_dim}, {head_dim_v}) is not supported on SM100/SM110. "
122
+ f"head_dim and head_dim_v must be between 8 and 128 and divisible by {alignment}, or (192, 128) for DeepSeek, or (256, 256) for hd256."
123
+ )
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class FwdConfig:
128
+ m_block_size: int
129
+ n_block_size: int
130
+ mma_pv_is_rs: bool
131
+ intra_wg_overlap: bool
132
+
133
+
134
+ def _tile_size_fwd_sm90(head_dim, head_dim_v, is_causal, is_local, sparse_block_size_q=None):
135
+ """Return FwdConfig for SM90 forward.
136
+
137
+ Tile sizes and flags based on tile_size_fwd_sm90 in hopper/tile_size.h, adjusted
138
+ for the Python kernel's different register/smem tradeoffs (benchmarked on H100 SXM).
139
+
140
+ When sparse_block_size_q is set, tile_m must divide it. For head_dim <= 96 the
141
+ optimal tile_m=192 is used when compatible, otherwise we fall back to 128.
142
+ """
143
+ if head_dim <= 64:
144
+ # C++: 192×192 non-causal, 192×128 causal/local.
145
+ # Python: 192×128 RS+OL is consistently best across seqlens.
146
+ if sparse_block_size_q is not None and sparse_block_size_q % 192 != 0:
147
+ return FwdConfig(128, 128, True, True)
148
+ return FwdConfig(192, 128, True, True)
149
+ elif head_dim <= 96:
150
+ # C++: 192×144 noRS+OL for all cases.
151
+ # Python: RS is catastrophic with 192× tiles (~300 vs ~600 TFLOPS).
152
+ # noRS+OL is always required. Causal: 192×128 slightly better short seqlen.
153
+ if sparse_block_size_q is not None and sparse_block_size_q % 192 != 0:
154
+ return FwdConfig(128, 128, False, True)
155
+ if is_causal or is_local:
156
+ return FwdConfig(192, 128, False, True)
157
+ else:
158
+ return FwdConfig(192, 144, False, True)
159
+ elif head_dim <= 128:
160
+ return FwdConfig(128, 128, True, True)
161
+ elif head_dim <= 192:
162
+ tile_n = 96 if is_local else (128 if head_dim_v <= 128 else 112)
163
+ return FwdConfig(128, tile_n, True, True)
164
+ else: # hdim 256
165
+ tile_n = 64 if is_local else 80
166
+ return FwdConfig(128, tile_n, True, True)
167
+
168
+ @dataclass(frozen=True)
169
+ class BwdConfig:
170
+ m_block_size: int
171
+ n_block_size: int
172
+ num_stages_Q: int
173
+ num_stages_dO: int
174
+ num_stages_PdS: int
175
+ SdP_swapAB: bool
176
+ dKV_swapAB: bool
177
+ dQ_swapAB: bool
178
+ AtomLayoutMSdP: int
179
+ AtomLayoutNdKV: int
180
+ AtomLayoutMdQ: int
181
+ num_wg: int = 2 # MMA warp groups (total threads = (num_wg + 1) * 128)
182
+ dQ_single_wg: bool = False
183
+
184
+
185
+ def _tile_size_bwd_sm90(head_dim, head_dim_v, causal, local, sparse_block_size_q=None):
186
+ """Return BwdConfig for SM90.
187
+
188
+ Configs based on C++ FA3 hopper/flash_bwd_launch_template.h,
189
+ benchmarked on H100 SXM.
190
+ """
191
+ if head_dim <= 64:
192
+ # C++ FA3: 128, 128, 64, ..., 2, 2, true, false, false, 2, 1, 2, 2
193
+ return BwdConfig(
194
+ m_block_size=128, n_block_size=128,
195
+ num_stages_Q=2, num_stages_dO=2, num_stages_PdS=2,
196
+ SdP_swapAB=True, dKV_swapAB=False, dQ_swapAB=False,
197
+ AtomLayoutMSdP=1, AtomLayoutNdKV=2, AtomLayoutMdQ=2,
198
+ )
199
+ elif head_dim <= 96:
200
+ # C++ FA3: 64, 128, 96, dQ_swapAB=False
201
+ return BwdConfig(
202
+ m_block_size=64, n_block_size=128,
203
+ num_stages_Q=2, num_stages_dO=2, num_stages_PdS=2,
204
+ SdP_swapAB=True, dKV_swapAB=False, dQ_swapAB=False,
205
+ AtomLayoutMSdP=1, AtomLayoutNdKV=2, AtomLayoutMdQ=1,
206
+ dQ_single_wg=True,
207
+ )
208
+ elif head_dim <= 128:
209
+ # C++ FA3: causal/local: 64, 128; non-causal: 80, 128 with dQ_swapAB
210
+ is_causal_or_local = causal or local
211
+ m_block_size = 64 if is_causal_or_local else 80
212
+ if sparse_block_size_q is not None and sparse_block_size_q % m_block_size != 0:
213
+ m_block_size = 64
214
+ return BwdConfig(
215
+ m_block_size=m_block_size,
216
+ n_block_size=128,
217
+ num_stages_Q=2, num_stages_dO=2, num_stages_PdS=2,
218
+ SdP_swapAB=True, dKV_swapAB=False,
219
+ dQ_swapAB=m_block_size % 64 != 0,
220
+ AtomLayoutMSdP=1, AtomLayoutNdKV=2, AtomLayoutMdQ=1,
221
+ )
222
+ elif head_dim <= 192:
223
+ hdimv128 = head_dim_v <= 128
224
+ if hdimv128:
225
+ return BwdConfig(
226
+ m_block_size=64, n_block_size=96,
227
+ num_stages_Q=2, num_stages_dO=2, num_stages_PdS=1,
228
+ SdP_swapAB=False, dKV_swapAB=True, dQ_swapAB=False,
229
+ AtomLayoutMSdP=1, AtomLayoutNdKV=2, AtomLayoutMdQ=1,
230
+ num_wg=2,
231
+ )
232
+ else:
233
+ return BwdConfig(
234
+ m_block_size=64, n_block_size=96,
235
+ num_stages_Q=2, num_stages_dO=1, num_stages_PdS=1,
236
+ SdP_swapAB=False, dKV_swapAB=True, dQ_swapAB=False,
237
+ AtomLayoutMSdP=1, AtomLayoutNdKV=2, AtomLayoutMdQ=1,
238
+ num_wg=2,
239
+ )
240
+ else:
241
+ # hdim 256
242
+ return BwdConfig(
243
+ m_block_size=64, n_block_size=64,
244
+ num_stages_Q=1, num_stages_dO=1, num_stages_PdS=1,
245
+ SdP_swapAB=False, dKV_swapAB=False, dQ_swapAB=False,
246
+ AtomLayoutMSdP=1, AtomLayoutNdKV=1, AtomLayoutMdQ=1,
247
+ )
248
+
249
+
250
+
251
+ def maybe_contiguous(x):
252
+ return x.contiguous() if x is not None and x.stride(-1) != 1 else x
253
+
254
+
255
+ def _validate_tensor(t, name, expected_shape, expected_dtype, expected_device):
256
+ assert t.shape == expected_shape, f"{name} shape {t.shape} != expected {expected_shape}"
257
+ assert t.dtype == expected_dtype, f"{name} dtype {t.dtype} != expected {expected_dtype}"
258
+ assert t.device == expected_device, f"{name} device {t.device} != expected {expected_device}"
259
+ if not is_fake_mode():
260
+ assert t.is_cuda, f"{name} must be on CUDA"
261
+
262
+ torch2cute_dtype_map = {
263
+ torch.float16: cutlass.Float16,
264
+ torch.bfloat16: cutlass.BFloat16,
265
+ torch.float32: cutlass.Float32,
266
+ torch.float8_e4m3fn: cutlass.Float8E4M3FN,
267
+ torch.float8_e5m2: cutlass.Float8E5M2,
268
+ }
269
+
270
+
271
+ def num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, max_splits):
272
+ # If num_n_blocks is too small, use 1 split. For example, we never split for hdim = 128 and seqlen_k = 512.
273
+ if num_n_blocks <= 4:
274
+ return 1
275
+
276
+ # NOTE: We should revisit this heuristic after persistence is supported for split KV.
277
+ # Sometimes, it's ideal to over-schedule splits for better efficiency.
278
+ return min(num_SMs // total_mblocks, max_splits, num_n_blocks)
279
+
280
+
281
+ def _resolve_causal_local_window(causal, window_size_left, window_size_right, mask_mod=None):
282
+ """Resolve causal/local/window settings into canonical form.
283
+
284
+ Returns (causal, local, window_size_left, window_size_right).
285
+ """
286
+ if mask_mod is not None:
287
+ return False, False, window_size_left, window_size_right
288
+ if causal:
289
+ window_size_right = 0
290
+ if window_size_left is not None and window_size_right is not None and window_size_left + window_size_right < 0:
291
+ window_size_left = None
292
+ window_size_right = None
293
+ if window_size_left is not None or window_size_right is not None:
294
+ if window_size_left is None and window_size_right == 0:
295
+ causal, local = True, False
296
+ window_size_right = None
297
+ else:
298
+ causal, local = False, True
299
+ else:
300
+ local = False
301
+ return causal, local, window_size_left, window_size_right
302
+
303
+ def _flash_attn_fwd(
304
+ q: torch.Tensor,
305
+ k: torch.Tensor,
306
+ v: torch.Tensor,
307
+ qv: Optional[torch.Tensor] = None,
308
+ cu_seqlens_q: Optional[torch.Tensor] = None,
309
+ cu_seqlens_k: Optional[torch.Tensor] = None,
310
+ seqused_q: Optional[torch.Tensor] = None,
311
+ seqused_k: Optional[torch.Tensor] = None,
312
+ max_seqlen_q: Optional[int] = None,
313
+ max_seqlen_k: Optional[int] = None,
314
+ min_seqlen_k: Optional[int] = None,
315
+ page_table: Optional[torch.Tensor] = None,
316
+ softmax_scale: Optional[float] = None,
317
+ causal: bool = False,
318
+ softcap: Optional[float] = None,
319
+ window_size_left: Optional[int] = None,
320
+ window_size_right: Optional[int] = None,
321
+ learnable_sink: Optional[torch.Tensor] = None,
322
+ tile_mn: Optional[Tuple[int, int]] = None,
323
+ mma_pv_is_rs: Optional[bool] = None,
324
+ intra_wg_overlap: Optional[bool] = None,
325
+ num_threads: int = 384,
326
+ num_splits: int = 1,
327
+ pack_gqa: Optional[bool] = None,
328
+ _arch: Optional[int] = None,
329
+ score_mod: Optional[Callable] = None,
330
+ mask_mod: Optional[Callable] = None,
331
+ block_sparse_tensors: Optional[BlockSparseTensorsTorch] = None,
332
+ return_lse: bool = False,
333
+ out: Optional[torch.Tensor] = None,
334
+ lse: Optional[torch.Tensor] = None,
335
+ aux_tensors: Optional[list[torch.Tensor]] = None,
336
+ q_descale: Optional[torch.Tensor] = None,
337
+ k_descale: Optional[torch.Tensor] = None,
338
+ v_descale: Optional[torch.Tensor] = None,
339
+ gather_kv_indices: Optional[torch.Tensor] = None,
340
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
341
+ """Forward pass for FlashAttention.
342
+
343
+ Args:
344
+ ...
345
+ score_mod: A callable that takes the attention scores and applies a modification.
346
+ mask_mod: A callable that takes token position information and selectively masks
347
+ block_sparse_tensors: A tuple of tensors used for block sparsity.
348
+ return_lse: Whether to return the log softmax of the attention scores. If set to True will always calculate
349
+ The returned LSE supports taking gradient.
350
+ out: Optional pre-allocated output tensor. If None, will be allocated internally.
351
+ lse: Optional pre-allocated log-sum-exp tensor. If None, will be allocated when needed.
352
+ aux_tensors: Some score_mods will want to read from global aux_tensors. This is how we thread them through to the inner kernel.
353
+ """
354
+ q, k, v = [maybe_contiguous(t) for t in (q, k, v)]
355
+ q_descale, k_descale, v_descale = [maybe_contiguous(t) for t in (q_descale, k_descale, v_descale)]
356
+ num_head, head_dim = q.shape[-2:]
357
+ if cu_seqlens_q is None:
358
+ batch_size, seqlen_q = q.shape[:2]
359
+ total_q = batch_size * seqlen_q
360
+ else:
361
+ batch_size = cu_seqlens_q.shape[0] - 1
362
+ seqlen_q = None
363
+ total_q = q.shape[0]
364
+ if page_table is not None:
365
+ assert cu_seqlens_k is None, "page_table is not supported with cu_seqlens_k"
366
+ assert page_table.dtype == torch.int32, "page_table must be int32"
367
+ assert page_table.stride(-1) == 1, "page_table must be contiguous in the last dimension"
368
+ max_num_pages_per_seq = page_table.shape[1]
369
+ assert page_table.shape == (batch_size, max_num_pages_per_seq)
370
+ num_pages, page_size = k.shape[:2]
371
+ seqlen_k = num_pages * page_size
372
+ else:
373
+ num_pages, page_size = None, None
374
+ seqlen_k = k.shape[-3]
375
+ num_head_kv = k.shape[-2]
376
+ head_dim_v = v.shape[-1]
377
+ if cu_seqlens_k is None:
378
+ if page_table is None:
379
+ assert k.shape == (batch_size, seqlen_k, num_head_kv, head_dim)
380
+ assert v.shape == (batch_size, seqlen_k, num_head_kv, head_dim_v)
381
+ else:
382
+ assert k.shape == (num_pages, page_size, num_head_kv, head_dim)
383
+ assert v.shape == (num_pages, page_size, num_head_kv, head_dim_v)
384
+ else:
385
+ assert k.shape == (seqlen_k, num_head_kv, head_dim)
386
+ assert v.shape == (seqlen_k, num_head_kv, head_dim_v)
387
+ assert cu_seqlens_k.shape == (batch_size + 1,), (
388
+ "cu_seqlens_k must have shape (batch_size + 1,)"
389
+ )
390
+
391
+ if cu_seqlens_q is not None:
392
+ assert cu_seqlens_q.shape == (batch_size + 1,), (
393
+ "cu_seqlens_q must have shape (batch_size + 1,)"
394
+ )
395
+ assert seqused_q is None or seqused_q.shape == (batch_size,), (
396
+ "seqused_q must have shape (batch_size,)"
397
+ )
398
+ assert seqused_k is None or seqused_k.shape == (batch_size,), (
399
+ "seqused_k must have shape (batch_size,)"
400
+ )
401
+ assert q.dtype in [torch.float16, torch.bfloat16, torch.float8_e4m3fn, torch.float8_e5m2], (
402
+ "inputs must be float16, bfloat16, fp8 e4m3fn, or fp8 e5m2"
403
+ )
404
+ assert q.dtype == k.dtype == v.dtype, "inputs must have the same dtype"
405
+ for t in [cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k]:
406
+ if t is not None:
407
+ assert t.dtype == torch.int32, (
408
+ "cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k must be int32"
409
+ )
410
+ assert t.stride(0) == 1, (
411
+ "cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k must be contiguous"
412
+ )
413
+ if learnable_sink is not None:
414
+ assert learnable_sink.shape == (num_head,)
415
+ assert learnable_sink.dtype == torch.bfloat16, "learnable_sink must be bfloat16"
416
+
417
+ if not is_fake_mode():
418
+ assert all(
419
+ t is None or t.is_cuda
420
+ for t in (
421
+ q,
422
+ k,
423
+ v,
424
+ q_descale,
425
+ k_descale,
426
+ v_descale,
427
+ cu_seqlens_q,
428
+ cu_seqlens_k,
429
+ seqused_q,
430
+ seqused_k,
431
+ page_table,
432
+ learnable_sink,
433
+ )
434
+ ), "inputs must be on CUDA device"
435
+ arch = _IMPORT_DEVICE_ARCH if _arch is None else _arch
436
+ assert arch // 10 in [8, 9, 10, 11, 12], "Unsupported compute capability. Supported: 8.x, 9.x, 10.x, 11.x, 12.x"
437
+ assert num_head % num_head_kv == 0, "num_head must be divisible by num_head_kv"
438
+ alignment = 16 // q.element_size()
439
+ if arch // 10 not in [8, 12]:
440
+ _validate_head_dims(head_dim, head_dim_v, arch // 10, alignment)
441
+ if softmax_scale is None:
442
+ softmax_scale = 1.0 / math.sqrt(head_dim) if qv is None else 1.0 / math.sqrt(head_dim + head_dim_v)
443
+ if softcap == 0.0:
444
+ softcap = None
445
+ qhead_per_kvhead = num_head // num_head_kv
446
+ if pack_gqa is None:
447
+ pack_gqa = qhead_per_kvhead > 1
448
+
449
+ is_fp8 = q.dtype in (torch.float8_e4m3fn, torch.float8_e5m2)
450
+ if is_fp8 and (q.requires_grad or k.requires_grad or v.requires_grad):
451
+ raise NotImplementedError("FA4 CuTe FP8 backward is not supported yet (forward-only).")
452
+ out_torch_dtype = torch.bfloat16 if is_fp8 else q.dtype
453
+ device = q.device
454
+ q_batch_seqlen_shape = (batch_size, seqlen_q) if cu_seqlens_q is None else (total_q,)
455
+ lse_shape = (batch_size, num_head, seqlen_q) if cu_seqlens_q is None else (num_head, total_q)
456
+ requires_grad = q.requires_grad or k.requires_grad or v.requires_grad
457
+
458
+ if out is None:
459
+ out = torch.empty(
460
+ *q_batch_seqlen_shape, num_head, head_dim_v, dtype=out_torch_dtype, device=device
461
+ )
462
+ else:
463
+ _validate_tensor(out, "out", (*q_batch_seqlen_shape, num_head, head_dim_v), out_torch_dtype, device)
464
+
465
+ if lse is None:
466
+ lse = (
467
+ torch.empty(lse_shape, dtype=torch.float32, device=device)
468
+ if requires_grad or return_lse
469
+ else None
470
+ )
471
+ elif lse is not None:
472
+ _validate_tensor(lse, "lse", lse_shape, torch.float32, device)
473
+
474
+ if seqlen_k == 0:
475
+ out.zero_()
476
+ if lse is not None:
477
+ lse.fill_(float("-inf"))
478
+ return out, lse
479
+
480
+ if is_fp8:
481
+ for t, name in ((q_descale, "q_descale"), (k_descale, "k_descale"), (v_descale, "v_descale")):
482
+ if t is not None:
483
+ _validate_tensor(t, name, (batch_size, num_head_kv), torch.float32, device)
484
+ else:
485
+ assert q_descale is None and k_descale is None and v_descale is None, (
486
+ "q_descale/k_descale/v_descale are only supported for FP8 inputs"
487
+ )
488
+
489
+ dtype = torch2cute_dtype_map[q.dtype]
490
+ if is_fp8:
491
+ assert arch // 10 == 10, "FP8 is only supported on SM100 (compute capability 10.x) for FA4 CuTe."
492
+ use_block_sparsity = block_sparse_tensors is not None
493
+
494
+ causal, local, window_size_left, window_size_right = _resolve_causal_local_window(
495
+ causal, window_size_left, window_size_right, mask_mod
496
+ )
497
+
498
+ requested_use_clc_scheduler = utils._get_use_clc_scheduler_default()
499
+ requested_disable_2cta = utils._get_disable_2cta_default()
500
+
501
+ current_stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
502
+
503
+ # SM80/SM120: uses SM80 MMA, 128 threads (4 warps)
504
+ if arch // 10 in [8, 12]:
505
+ num_threads = 128
506
+
507
+ fwd_cfg = FwdConfig(128, 128, True, True) # default
508
+ if tile_mn is None:
509
+ if arch // 10 == 12:
510
+ # SM120 tile sizes tuned for 99 KB SMEM capacity:
511
+ # D<=64: 128x128 → 48 KB (good occupancy)
512
+ # D>64: 128x64 → 64 KB (128x128 would use 96 KB, hurting occupancy)
513
+ if head_dim <= 64:
514
+ fwd_cfg = FwdConfig(128, 128, True, True)
515
+ else:
516
+ fwd_cfg = FwdConfig(128, 64, True, True)
517
+ elif arch // 10 == 8:
518
+ fwd_cfg = FwdConfig(128, 64, True, True) # SM80, should tune
519
+ elif arch // 10 == 9:
520
+ sparse_q = get_sparse_q_block_size(block_sparse_tensors, seqlen_q)
521
+ fwd_cfg = _tile_size_fwd_sm90(head_dim, head_dim_v, causal, local, sparse_block_size_q=sparse_q)
522
+ else:
523
+ fwd_cfg = FwdConfig(tile_mn[0], tile_mn[1], fwd_cfg.mma_pv_is_rs, fwd_cfg.intra_wg_overlap)
524
+ tile_m, tile_n = fwd_cfg.m_block_size, fwd_cfg.n_block_size
525
+ if mma_pv_is_rs is None:
526
+ mma_pv_is_rs = fwd_cfg.mma_pv_is_rs
527
+ if intra_wg_overlap is None:
528
+ intra_wg_overlap = fwd_cfg.intra_wg_overlap
529
+
530
+ # TODO: fix GQA + SplitKV + non-varlen
531
+ if pack_gqa and num_splits != 1 and cu_seqlens_q is None:
532
+ pack_gqa = False
533
+
534
+ if pack_gqa and qv is not None and 128 % qhead_per_kvhead != 0:
535
+ pack_gqa = False
536
+
537
+ if max_seqlen_q is None:
538
+ max_seqlen_q = seqlen_q if cu_seqlens_q is None else total_q
539
+ if max_seqlen_k is None:
540
+ max_seqlen_k = seqlen_k
541
+ if cu_seqlens_k is None and seqused_k is None:
542
+ min_seqlen_k = seqlen_k
543
+ seqlen_q_packgqa = max_seqlen_q * qhead_per_kvhead
544
+ if arch // 10 == 10:
545
+ q_stage = 2 if seqlen_q_packgqa > tile_m else 1
546
+ else:
547
+ q_stage = 1
548
+
549
+ m_block_size_effective = q_stage * tile_m
550
+ seqlen_k_loaded = max_seqlen_k if not local else max(0, min(max_seqlen_k, (window_size_right or max_seqlen_k) + (window_size_left or max_seqlen_k) + 1 + tile_m))
551
+ num_m_blocks = (seqlen_q_packgqa + m_block_size_effective - 1) // m_block_size_effective
552
+ total_mblocks = batch_size * num_head_kv * num_m_blocks
553
+ num_n_blocks = (seqlen_k_loaded + tile_n - 1) // tile_n
554
+ num_SMs = 132 if is_fake_mode() else torch.cuda.get_device_properties(device).multi_processor_count
555
+ if num_splits < 1:
556
+ num_splits = num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, 128)
557
+
558
+ # SplitKV uses float32 partial output, which doubles the O buffer size
559
+ # in shared memory, causing OOM for diff-headdim (192, 128)
560
+ if arch // 10 in [10, 11] and head_dim != head_dim_v and num_splits > 1:
561
+ if num_n_blocks >= 64 and head_dim_v != 512:
562
+ tile_n = 64
563
+ num_n_blocks = (seqlen_k_loaded + tile_n - 1) // tile_n
564
+ num_splits = num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, 128)
565
+ else:
566
+ num_splits = 1
567
+
568
+ is_split_kv = num_splits > 1
569
+ if is_split_kv:
570
+ out_partial = torch.empty(num_splits, *q_batch_seqlen_shape, num_head, head_dim_v, dtype=torch.float32, device=device)
571
+ lse_partial = torch.empty(num_splits, *lse_shape, dtype=torch.float32, device=device)
572
+
573
+ use_2cta_instrs = (
574
+ arch // 10 in [10, 11]
575
+ and not requested_disable_2cta
576
+ and not causal
577
+ and not local
578
+ and not is_split_kv
579
+ and cu_seqlens_q is None
580
+ and seqused_q is None
581
+ and not use_block_sparsity
582
+ and page_size in [None, 128]
583
+ and int(math.ceil(head_dim / 16) * 16) in [128, 192]
584
+ and int(math.ceil(head_dim_v / 16) * 16) == 128
585
+ and seqlen_q_packgqa > 2 * tile_m
586
+ and (tile_m % qhead_per_kvhead == 0 or not pack_gqa)
587
+ )
588
+
589
+ # hd=256 2CTA forward uses the dedicated kernel on both SM100 and SM110.
590
+ use_dedicated_hd256_kernel = (
591
+ arch // 10 in [10, 11]
592
+ and head_dim == 256
593
+ and head_dim_v == 256
594
+ )
595
+ use_2cta_instrs = use_2cta_instrs or use_dedicated_hd256_kernel
596
+
597
+ if softcap is not None:
598
+ assert score_mod is None, "softcap and score_mod cannot be used together"
599
+ score_mod = utils.create_softcap_scoremod(softcap)
600
+ elif score_mod is not None:
601
+ if arch // 10 == 8:
602
+ raise NotImplementedError("Custom user-provided score_mod is not supported on SM8x architectures.")
603
+
604
+ # hash score and mask mods for compile cache
605
+ score_mod_hash = utils.hash_callable(score_mod) if score_mod is not None else False
606
+ mask_mod_hash = utils.hash_callable(mask_mod) if mask_mod is not None else False
607
+
608
+ is_varlen = (
609
+ cu_seqlens_q is not None
610
+ or cu_seqlens_k is not None
611
+ or seqused_q is not None
612
+ or seqused_k is not None
613
+ )
614
+
615
+ # CLC regressed for varlen MHA and dense noncausal. Imbalanced varlen shapes
616
+ # keep more K/V blocks in flight and hurt L2; dense noncausal mostly just
617
+ # pays work-stealing overhead.
618
+ is_varlen_mha = is_varlen and qhead_per_kvhead == 1
619
+ is_dense_noncausal = not is_varlen and not causal and not local
620
+ use_clc_scheduler = requested_use_clc_scheduler and not is_varlen_mha and not is_dense_noncausal
621
+
622
+ if mask_mod is not None:
623
+ if is_varlen:
624
+ raise NotImplementedError(
625
+ "mask_mod with aux_tensors is not yet supported for varlen sequences. This will be fixed in a future PR."
626
+ )
627
+
628
+ if use_block_sparsity:
629
+ if is_varlen:
630
+ raise NotImplementedError(
631
+ "Block sparsity is not yet supported for varlen sequences. This will be fixed in a future PR."
632
+ )
633
+ # NB: pack_gqa requires block sparse head dim == 1 (broadcasted)
634
+ if pack_gqa and block_sparse_tensors.mask_block_cnt.shape[1] != 1:
635
+ pack_gqa = False
636
+ if is_split_kv:
637
+ raise NotImplementedError(
638
+ "Block sparsity is not yet supported with SplitKV. TODO: partition sparse block lists per split."
639
+ )
640
+
641
+ # See get_broadcast_dims for why this is needed in compile key
642
+ block_sparse_broadcast_pattern = None
643
+ normalized_block_sparse_tensors = None
644
+ q_subtile_factor = None
645
+ if block_sparse_tensors is not None:
646
+ if seqlen_q is None:
647
+ raise ValueError("Block sparsity requires fixed-length sequences (seqlen_q must be known).")
648
+ (
649
+ normalized_block_sparse_tensors,
650
+ block_sparse_broadcast_pattern,
651
+ q_subtile_factor,
652
+ ) = normalize_block_sparse_config(
653
+ block_sparse_tensors,
654
+ batch_size=batch_size,
655
+ num_head=num_head,
656
+ seqlen_q=seqlen_q,
657
+ seqlen_k=seqlen_k,
658
+ block_size=(tile_m, tile_n),
659
+ q_stage=q_stage,
660
+ )
661
+ if aux_tensors is not None:
662
+ aux_tensor_metadata = get_aux_tensor_metadata(aux_tensors)
663
+ else:
664
+ aux_tensor_metadata = None
665
+
666
+ if qv is not None:
667
+ assert arch // 10 in [10, 11], "only support Blackwell arch with qv"
668
+ assert qv.shape[:-1] == q.shape[:-1]
669
+ assert qv.shape[-1] == head_dim_v
670
+ assert head_dim == 64 and head_dim_v == 512, "only support MLA weight absorbed shape with qv"
671
+ assert not local, "local not yet supported with qv"
672
+ assert page_table is None, "page table not yet supported with qv"
673
+ assert q_descale is None and k_descale is None and v_descale is None, (
674
+ "q_descale/k_descale/v_descale are not yet supported with qv"
675
+ )
676
+
677
+ assert not is_split_kv, "split kv not supported with qv"
678
+ assert learnable_sink is None
679
+ assert softcap is None
680
+ assert score_mod is None
681
+ assert mask_mod is None
682
+
683
+ qv = maybe_contiguous(qv)
684
+
685
+ gather_kv_length = 2048
686
+ sparse_kv = gather_kv_indices is not None
687
+ disable_sparse_kv_bitmask = False
688
+ if sparse_kv:
689
+ assert gather_kv_indices.shape[:-1] == q.shape[:-2]
690
+ gather_kv_length = gather_kv_indices.shape[-1]
691
+ assert gather_kv_length % 256 == 0
692
+ if min_seqlen_k is None or causal:
693
+ disable_sparse_kv_bitmask = False
694
+ else:
695
+ # seqlen_k_boundary = min_seqlen_k - max_seqlen_q + 1 if causal else min_seqlen_k
696
+ seqlen_k_boundary = min_seqlen_k
697
+ disable_sparse_kv_bitmask = seqlen_k_boundary >= gather_kv_length
698
+ else:
699
+ assert gather_kv_indices is None, "gather_kv_indices is only supported with qv"
700
+ gather_kv_length = None
701
+ sparse_kv = None
702
+ disable_sparse_kv_bitmask = None
703
+
704
+ compile_key = (
705
+ dtype,
706
+ head_dim,
707
+ head_dim_v,
708
+ qhead_per_kvhead,
709
+ causal,
710
+ score_mod_hash,
711
+ mask_mod_hash,
712
+ use_block_sparsity,
713
+ block_sparse_broadcast_pattern,
714
+ aux_tensor_metadata,
715
+ lse is None,
716
+ cu_seqlens_q is None,
717
+ cu_seqlens_k is None,
718
+ seqused_q is None,
719
+ seqused_k is None,
720
+ page_table is not None,
721
+ window_size_left is not None,
722
+ window_size_right is not None,
723
+ learnable_sink is not None,
724
+ q_descale is not None,
725
+ k_descale is not None,
726
+ v_descale is not None,
727
+ tile_m,
728
+ tile_n,
729
+ q_stage,
730
+ num_threads,
731
+ is_split_kv,
732
+ pack_gqa,
733
+ arch,
734
+ page_size not in [None, tile_n], # paged KV non-TMA
735
+ use_2cta_instrs,
736
+ q_subtile_factor,
737
+ mma_pv_is_rs,
738
+ intra_wg_overlap,
739
+ use_clc_scheduler,
740
+ qv is not None,
741
+ gather_kv_length,
742
+ sparse_kv,
743
+ disable_sparse_kv_bitmask,
744
+ fa_logging.get_fa_log_level(),
745
+ )
746
+
747
+ if compile_key not in _flash_attn_fwd.compile_cache:
748
+ (
749
+ cu_seqlens_q_tensor,
750
+ cu_seqlens_k_tensor,
751
+ seqused_q_tensor,
752
+ seqused_k_tensor,
753
+ learnable_sink_tensor,
754
+ ) = [
755
+ to_cute_tensor(t, assumed_align=4, leading_dim=0)
756
+ if t is not None
757
+ else None
758
+ for t in (cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k, learnable_sink)
759
+ ]
760
+ page_table_tensor = (
761
+ to_cute_tensor(page_table, assumed_align=4, leading_dim=1)
762
+ if page_table is not None
763
+ else None
764
+ )
765
+ q_tensor, k_tensor, v_tensor, o_tensor = [
766
+ to_cute_tensor(t) for t in (q, k, v, out if not is_split_kv else out_partial)
767
+ ]
768
+ if is_split_kv:
769
+ lse_tensor = to_cute_tensor(lse_partial, assumed_align=4)
770
+ elif lse is not None:
771
+ lse_tensor = to_cute_tensor(lse, assumed_align=4)
772
+ else:
773
+ lse_tensor = None
774
+
775
+ q_descale_tensor = (
776
+ to_cute_tensor(q_descale, assumed_align=4, leading_dim=1)
777
+ if q_descale is not None
778
+ else None
779
+ )
780
+ k_descale_tensor = (
781
+ to_cute_tensor(k_descale, assumed_align=4, leading_dim=1)
782
+ if k_descale is not None
783
+ else None
784
+ )
785
+ v_descale_tensor = (
786
+ to_cute_tensor(v_descale, assumed_align=4, leading_dim=1)
787
+ if v_descale is not None
788
+ else None
789
+ )
790
+ descale_tensors_tensor = (
791
+ DescaleTensors(
792
+ q_descale=q_descale_tensor,
793
+ k_descale=k_descale_tensor,
794
+ v_descale=v_descale_tensor,
795
+ )
796
+ if q_descale_tensor is not None
797
+ or k_descale_tensor is not None
798
+ or v_descale_tensor is not None
799
+ else None
800
+ )
801
+
802
+ sparse_tensors = None
803
+ if normalized_block_sparse_tensors is not None:
804
+ sparse_tensors = to_cute_block_sparse_tensors(normalized_block_sparse_tensors)
805
+
806
+ cute_aux_tensors = None
807
+ aux_tensor_metadata = None
808
+ if aux_tensors is not None:
809
+ cute_aux_tensors = [to_cute_aux_tensor(buf) for buf in aux_tensors]
810
+
811
+ qv_tensor = to_cute_tensor(qv) if qv is not None else None
812
+ gather_kv_indices_tensor = to_cute_tensor(gather_kv_indices) if gather_kv_indices is not None else None
813
+
814
+ if arch // 10 == 8:
815
+ assert page_table is None, "paged KV not supported on SM 8.0"
816
+ assert not is_split_kv, "SplitKV not supported on SM 8.0"
817
+ fa_fwd = FlashAttentionForwardSm80(
818
+ dtype,
819
+ head_dim,
820
+ head_dim_v,
821
+ qhead_per_kvhead,
822
+ is_causal=causal,
823
+ is_local=local,
824
+ pack_gqa=pack_gqa,
825
+ tile_m=tile_m,
826
+ tile_n=tile_n,
827
+ num_stages=1,
828
+ num_threads=num_threads,
829
+ Q_in_regs=False,
830
+ score_mod=score_mod,
831
+ mask_mod=mask_mod,
832
+ has_aux_tensors=aux_tensors is not None,
833
+ )
834
+ elif arch // 10 == 9:
835
+ assert not is_split_kv, "SplitKV not supported on SM 9.0"
836
+ fa_fwd = FlashAttentionForwardSm90(
837
+ dtype,
838
+ head_dim,
839
+ head_dim_v,
840
+ qhead_per_kvhead,
841
+ is_causal=causal,
842
+ is_local=local,
843
+ pack_gqa=pack_gqa,
844
+ tile_m=tile_m,
845
+ tile_n=tile_n,
846
+ # num_stages=1,
847
+ num_stages=2,
848
+ num_threads=num_threads,
849
+ Q_in_regs=False,
850
+ intra_wg_overlap=intra_wg_overlap,
851
+ mma_pv_is_rs=mma_pv_is_rs,
852
+ mask_mod=mask_mod,
853
+ score_mod=score_mod,
854
+ has_aux_tensors=aux_tensors is not None,
855
+ q_subtile_factor=q_subtile_factor,
856
+ paged_kv_non_tma=page_size not in [None, tile_n],
857
+ )
858
+ elif arch // 10 in [10, 11]:
859
+ if qv is not None:
860
+ fa_fwd = FlashAttentionMLAForwardSm100(
861
+ is_causal=causal,
862
+ use_cpasync_load_KV=sparse_kv,
863
+ topk_length=gather_kv_length,
864
+ is_topk_gather=sparse_kv,
865
+ pack_gqa=pack_gqa,
866
+ qhead_per_kvhead=qhead_per_kvhead,
867
+ nheads_kv=num_head_kv,
868
+ is_varlen_q=cu_seqlens_q is not None or seqused_q is not None,
869
+ disable_bitmask=disable_sparse_kv_bitmask,
870
+ )
871
+ else:
872
+ if use_dedicated_hd256_kernel:
873
+ # hd=256 2CTA forward: check for currently unsupported features
874
+ assert softcap is None, "SM100 forward with head_dim=256 does not support softcap"
875
+ assert not use_block_sparsity, \
876
+ "SM100 forward with head_dim=256 does not support block sparsity"
877
+ assert learnable_sink is None, \
878
+ "SM100 forward with head_dim=256 does not support learnable_sink"
879
+ assert seqused_q is None, \
880
+ "SM100 forward with head_dim=256 does not support seqused_q"
881
+ if page_table is not None:
882
+ assert max_seqlen_k % page_size == 0, (
883
+ f"SM100 hd256 2CTA paged KV requires max_seqlen_k divisible by "
884
+ f"page_size ({page_size}), got max_seqlen_k={max_seqlen_k}"
885
+ )
886
+ assert page_table.shape[1] == max_seqlen_k // page_size, (
887
+ f"SM100 hd256 2CTA paged KV requires page_table.shape[1] == "
888
+ f"max_seqlen_k // page_size ({max_seqlen_k} // {page_size} = "
889
+ f"{max_seqlen_k // page_size}), got {page_table.shape[1]}; "
890
+ f"pass page_table[:, :{max_seqlen_k // page_size}] to slice to "
891
+ f"the actual sequence length"
892
+ )
893
+ assert page_table.stride(0) == page_table.shape[1], (
894
+ f"SM100 hd256 2CTA paged KV requires a fully contiguous page_table "
895
+ f"(stride(0)={page_table.stride(0)} must equal "
896
+ f"shape[1]={page_table.shape[1]})"
897
+ )
898
+ # pack_gqa is an auto-selected optimization; disable it for hd256 kernel
899
+ pack_gqa = False
900
+
901
+ flash_fwd_obj_cls = (
902
+ BlackwellFusedMultiHeadAttentionForward
903
+ if use_dedicated_hd256_kernel
904
+ else FlashAttentionForwardSm100
905
+ )
906
+
907
+ fa_fwd = flash_fwd_obj_cls(
908
+ head_dim,
909
+ head_dim_v,
910
+ qhead_per_kvhead=qhead_per_kvhead,
911
+ is_causal=causal,
912
+ is_local=local,
913
+ is_split_kv=is_split_kv,
914
+ pack_gqa=pack_gqa,
915
+ m_block_size=tile_m,
916
+ n_block_size=tile_n,
917
+ q_stage=q_stage,
918
+ is_persistent=not causal
919
+ and not local
920
+ and cu_seqlens_q is None
921
+ and seqused_q is None
922
+ and not is_split_kv,
923
+ score_mod=score_mod,
924
+ mask_mod=mask_mod,
925
+ has_aux_tensors=aux_tensors is not None,
926
+ paged_kv_non_tma=page_size not in [None, tile_n],
927
+ is_varlen_q=cu_seqlens_q is not None or seqused_q is not None,
928
+ q_subtile_factor=q_subtile_factor,
929
+ use_2cta_instrs=use_2cta_instrs,
930
+ use_clc_scheduler=use_clc_scheduler,
931
+ )
932
+ elif arch // 10 == 12:
933
+ # SM120 (Blackwell GeForce / DGX Spark): uses SM80 MMA with SM120 SMEM capacity
934
+ assert not use_block_sparsity, "Block sparsity not supported on SM 12.0"
935
+ assert page_table is None, "Paged KV not supported on SM 12.0 in this PR"
936
+ assert not is_split_kv, "SplitKV not supported on SM 12.0 in this PR"
937
+ fa_fwd = FlashAttentionForwardSm120(
938
+ dtype,
939
+ head_dim,
940
+ head_dim_v,
941
+ qhead_per_kvhead,
942
+ is_causal=causal,
943
+ is_local=local,
944
+ pack_gqa=pack_gqa,
945
+ tile_m=tile_m,
946
+ tile_n=tile_n,
947
+ num_stages=1,
948
+ num_threads=num_threads,
949
+ Q_in_regs=False,
950
+ score_mod=score_mod,
951
+ mask_mod=mask_mod,
952
+ has_aux_tensors=aux_tensors is not None,
953
+ )
954
+ else:
955
+ raise ValueError(
956
+ f"Unsupported compute capability: {arch}. Supported: 8.x, 9.x, 10.x, 11.x, 12.x"
957
+ )
958
+ # TODO: check @can_implement
959
+ if qv is not None:
960
+ _flash_attn_fwd.compile_cache[compile_key] = cute.compile(
961
+ fa_fwd,
962
+ q_tensor,
963
+ qv_tensor,
964
+ k_tensor,
965
+ v_tensor,
966
+ o_tensor,
967
+ lse_tensor,
968
+ softmax_scale,
969
+ cu_seqlens_q_tensor,
970
+ cu_seqlens_k_tensor,
971
+ seqused_q_tensor,
972
+ seqused_k_tensor,
973
+ gather_kv_indices_tensor,
974
+ page_table_tensor,
975
+ window_size_left,
976
+ window_size_right,
977
+ current_stream,
978
+ options="--enable-tvm-ffi",
979
+ )
980
+ else:
981
+ compile_args = [
982
+ fa_fwd,
983
+ q_tensor,
984
+ k_tensor,
985
+ v_tensor,
986
+ o_tensor,
987
+ lse_tensor,
988
+ softmax_scale,
989
+ cu_seqlens_q_tensor,
990
+ cu_seqlens_k_tensor,
991
+ seqused_q_tensor,
992
+ seqused_k_tensor,
993
+ page_table_tensor,
994
+ window_size_left,
995
+ window_size_right,
996
+ learnable_sink_tensor,
997
+ sparse_tensors,
998
+ cute_aux_tensors,
999
+ current_stream,
1000
+ ]
1001
+ if arch // 10 in [10, 11]:
1002
+ compile_args.insert(-3, descale_tensors_tensor)
1003
+ _flash_attn_fwd.compile_cache[compile_key] = cute.compile(*compile_args, options="--enable-tvm-ffi")
1004
+
1005
+ if not is_fake_mode():
1006
+ q_call, k_call, v_call = q.detach(), k.detach(), v.detach()
1007
+ qv_call = qv.detach() if qv is not None else None
1008
+ if is_fp8:
1009
+ # need uint8 workaround until we pin torch >= 2.11.0 where fp8 export is supported
1010
+ q_call = q_call.view(torch.uint8)
1011
+ k_call = k_call.view(torch.uint8)
1012
+ v_call = v_call.view(torch.uint8)
1013
+ if qv_call is not None:
1014
+ qv_call = qv_call.view(torch.uint8)
1015
+ descale_tensors = (
1016
+ DescaleTensors(q_descale=q_descale, k_descale=k_descale, v_descale=v_descale)
1017
+ if q_descale is not None or k_descale is not None or v_descale is not None
1018
+ else None
1019
+ )
1020
+ if qv is not None:
1021
+ _flash_attn_fwd.compile_cache[compile_key](
1022
+ q_call,
1023
+ qv_call,
1024
+ k_call,
1025
+ v_call,
1026
+ out.detach(),
1027
+ lse,
1028
+ softmax_scale,
1029
+ cu_seqlens_q,
1030
+ cu_seqlens_k,
1031
+ seqused_q,
1032
+ seqused_k,
1033
+ gather_kv_indices,
1034
+ page_table,
1035
+ window_size_left,
1036
+ window_size_right,
1037
+ )
1038
+ else:
1039
+ call_args = [
1040
+ q_call,
1041
+ k_call,
1042
+ v_call,
1043
+ out.detach() if not is_split_kv else out_partial,
1044
+ lse_partial if is_split_kv else lse,
1045
+ softmax_scale,
1046
+ cu_seqlens_q,
1047
+ cu_seqlens_k,
1048
+ seqused_q,
1049
+ seqused_k,
1050
+ page_table,
1051
+ window_size_left,
1052
+ window_size_right,
1053
+ learnable_sink,
1054
+ ]
1055
+ if arch // 10 in [10, 11]:
1056
+ call_args.append(descale_tensors)
1057
+ call_args.extend([
1058
+ (
1059
+ normalized_block_sparse_tensors.mask_block_cnt,
1060
+ normalized_block_sparse_tensors.mask_block_idx,
1061
+ normalized_block_sparse_tensors.full_block_cnt,
1062
+ normalized_block_sparse_tensors.full_block_idx,
1063
+ normalized_block_sparse_tensors.dq_write_order,
1064
+ normalized_block_sparse_tensors.dq_write_order_full,
1065
+ )
1066
+ if normalized_block_sparse_tensors is not None
1067
+ else None,
1068
+ aux_tensors,
1069
+ ])
1070
+ _flash_attn_fwd.compile_cache[compile_key](*call_args)
1071
+ if is_split_kv:
1072
+ _flash_attn_fwd_combine(
1073
+ out_partial,
1074
+ lse_partial.transpose(-1, -2),
1075
+ out,
1076
+ lse.transpose(-1, -2) if lse is not None else None,
1077
+ cu_seqlens_q,
1078
+ seqused_q,
1079
+ )
1080
+ return out, lse
1081
+
1082
+
1083
+ _flash_attn_fwd.compile_cache = get_jit_cache("fwd")
1084
+
1085
+
1086
+ class FlashAttnFunc(torch.autograd.Function):
1087
+ @staticmethod
1088
+ def forward(
1089
+ ctx,
1090
+ q: torch.Tensor,
1091
+ k: torch.Tensor,
1092
+ v: torch.Tensor,
1093
+ qv: Optional[torch.Tensor] = None,
1094
+ gather_kv_indices: Optional[torch.Tensor] = None,
1095
+ softmax_scale: Optional[float] = None,
1096
+ causal: bool = False,
1097
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1098
+ learnable_sink: Optional[torch.Tensor] = None,
1099
+ softcap: float = 0.0,
1100
+ num_splits: int = 1,
1101
+ pack_gqa: Optional[bool] = None,
1102
+ deterministic: bool = False,
1103
+ score_mod: Optional[Callable] = None,
1104
+ score_mod_bwd: Optional[Callable] = None,
1105
+ mask_mod: Optional[Callable] = None,
1106
+ aux_tensors: Optional[list] = None,
1107
+ block_sparse_tensors: Optional[BlockSparseTensorsTorch] = None,
1108
+ block_sparse_tensors_bwd: Optional[BlockSparseTensorsTorch] = None,
1109
+ return_lse: bool = False,
1110
+ ):
1111
+ out, lse = _flash_attn_fwd(
1112
+ q,
1113
+ k,
1114
+ v,
1115
+ qv=qv,
1116
+ softmax_scale=softmax_scale,
1117
+ causal=causal,
1118
+ window_size_left=window_size[0],
1119
+ window_size_right=window_size[1],
1120
+ learnable_sink=learnable_sink,
1121
+ softcap=softcap,
1122
+ num_splits=num_splits,
1123
+ pack_gqa=pack_gqa,
1124
+ score_mod=score_mod,
1125
+ mask_mod=mask_mod,
1126
+ aux_tensors=aux_tensors,
1127
+ block_sparse_tensors=block_sparse_tensors,
1128
+ return_lse=return_lse,
1129
+ gather_kv_indices=gather_kv_indices,
1130
+ )
1131
+ ctx.save_for_backward(q, k, v, out, lse, *(aux_tensors or ()))
1132
+ ctx.softmax_scale = softmax_scale
1133
+ ctx.causal = causal
1134
+ ctx.window_size = window_size
1135
+ ctx.softcap = softcap
1136
+ ctx.deterministic = deterministic
1137
+ ctx.return_lse = return_lse
1138
+ ctx.score_mod = score_mod
1139
+ ctx.score_mod_bwd = score_mod_bwd
1140
+ ctx.mask_mod = mask_mod
1141
+ ctx.block_sparse_tensors_bwd = block_sparse_tensors_bwd
1142
+ ctx.set_materialize_grads(False)
1143
+ return out, lse
1144
+
1145
+ @staticmethod
1146
+ def backward(ctx, dout, dlse):
1147
+ raise NotImplementedError(
1148
+ "FlashRT's FA4 vendor is forward / inference-only; the backward "
1149
+ "kernels were removed (see VENDOR.md)."
1150
+ )
1151
+
1152
+
1153
+ class FlashAttnVarlenFunc(torch.autograd.Function):
1154
+ @staticmethod
1155
+ def forward(
1156
+ ctx,
1157
+ q: torch.Tensor,
1158
+ k: torch.Tensor,
1159
+ v: torch.Tensor,
1160
+ qv: Optional[torch.Tensor] = None,
1161
+ cu_seqlens_q: Optional[torch.Tensor] = None,
1162
+ cu_seqlens_k: Optional[torch.Tensor] = None,
1163
+ seqused_q: Optional[torch.Tensor] = None,
1164
+ seqused_k: Optional[torch.Tensor] = None,
1165
+ max_seqlen_q: Optional[int] = None,
1166
+ max_seqlen_k: Optional[int] = None,
1167
+ min_seqlen_k: Optional[int] = None,
1168
+ gather_kv_indices: Optional[torch.Tensor] = None,
1169
+ page_table: Optional[torch.Tensor] = None,
1170
+ softmax_scale: Optional[float] = None,
1171
+ causal: bool = False,
1172
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1173
+ learnable_sink: Optional[torch.Tensor] = None,
1174
+ softcap: float = 0.0,
1175
+ num_splits: int = 1,
1176
+ pack_gqa: Optional[bool] = None,
1177
+ deterministic: bool = False,
1178
+ score_mod: Optional[Callable] = None,
1179
+ score_mod_bwd: Optional[Callable] = None,
1180
+ aux_tensors: Optional[list] = None,
1181
+ return_lse: bool = False,
1182
+ ):
1183
+ out, lse = _flash_attn_fwd(
1184
+ q,
1185
+ k,
1186
+ v,
1187
+ qv=qv,
1188
+ cu_seqlens_q=cu_seqlens_q,
1189
+ cu_seqlens_k=cu_seqlens_k,
1190
+ seqused_q=seqused_q,
1191
+ seqused_k=seqused_k,
1192
+ max_seqlen_q=max_seqlen_q,
1193
+ max_seqlen_k=max_seqlen_k,
1194
+ min_seqlen_k=min_seqlen_k,
1195
+ page_table=page_table,
1196
+ softmax_scale=softmax_scale,
1197
+ causal=causal,
1198
+ window_size_left=window_size[0],
1199
+ window_size_right=window_size[1],
1200
+ learnable_sink=learnable_sink,
1201
+ softcap=softcap,
1202
+ num_splits=num_splits,
1203
+ pack_gqa=pack_gqa,
1204
+ score_mod=score_mod,
1205
+ aux_tensors=aux_tensors,
1206
+ return_lse=return_lse,
1207
+ gather_kv_indices=gather_kv_indices,
1208
+ )
1209
+ ctx.save_for_backward(
1210
+ q,
1211
+ k,
1212
+ v,
1213
+ out,
1214
+ lse,
1215
+ cu_seqlens_q,
1216
+ cu_seqlens_k,
1217
+ seqused_q,
1218
+ seqused_k,
1219
+ *(aux_tensors or ()),
1220
+ )
1221
+ ctx.softmax_scale = softmax_scale
1222
+ ctx.causal = causal
1223
+ ctx.window_size = window_size
1224
+ ctx.softcap = softcap
1225
+ ctx.deterministic = deterministic
1226
+ ctx.max_seqlen_q = max_seqlen_q
1227
+ ctx.max_seqlen_k = max_seqlen_k
1228
+ ctx.return_lse = return_lse
1229
+ ctx.score_mod = score_mod
1230
+ ctx.score_mod_bwd = score_mod_bwd
1231
+ ctx.set_materialize_grads(False)
1232
+ return out, lse
1233
+
1234
+ @staticmethod
1235
+ def backward(ctx, dout, dlse):
1236
+ raise NotImplementedError(
1237
+ "FlashRT's FA4 vendor is forward / inference-only; the backward "
1238
+ "kernels were removed (see VENDOR.md)."
1239
+ )
1240
+
1241
+
1242
+ def flash_attn_func(
1243
+ q: torch.Tensor,
1244
+ k: torch.Tensor,
1245
+ v: torch.Tensor,
1246
+ qv: Optional[torch.Tensor] = None,
1247
+ gather_kv_indices: Optional[torch.Tensor] = None,
1248
+ softmax_scale: Optional[float] = None,
1249
+ causal: bool = False,
1250
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1251
+ learnable_sink: Optional[torch.Tensor] = None,
1252
+ softcap: float = 0.0,
1253
+ num_splits: int = 1,
1254
+ pack_gqa: Optional[bool] = None,
1255
+ deterministic: bool = False,
1256
+ score_mod: Optional[Callable] = None,
1257
+ score_mod_bwd: Optional[Callable] = None,
1258
+ mask_mod: Optional[Callable] = None,
1259
+ aux_tensors: Optional[list] = None,
1260
+ block_sparse_tensors: Optional[BlockSparseTensorsTorch] = None,
1261
+ block_sparse_tensors_bwd: Optional[BlockSparseTensorsTorch] = None,
1262
+ return_lse: bool = False,
1263
+ ):
1264
+ return FlashAttnFunc.apply(
1265
+ q,
1266
+ k,
1267
+ v,
1268
+ qv,
1269
+ gather_kv_indices,
1270
+ softmax_scale,
1271
+ causal,
1272
+ window_size,
1273
+ learnable_sink,
1274
+ softcap,
1275
+ num_splits,
1276
+ pack_gqa,
1277
+ deterministic,
1278
+ score_mod,
1279
+ score_mod_bwd,
1280
+ mask_mod,
1281
+ aux_tensors,
1282
+ block_sparse_tensors,
1283
+ block_sparse_tensors_bwd,
1284
+ return_lse,
1285
+ )
1286
+
1287
+
1288
+ def flash_attn_varlen_func(
1289
+ q: torch.Tensor,
1290
+ k: torch.Tensor,
1291
+ v: torch.Tensor,
1292
+ qv: Optional[torch.Tensor] = None,
1293
+ cu_seqlens_q: Optional[torch.Tensor] = None,
1294
+ cu_seqlens_k: Optional[torch.Tensor] = None,
1295
+ max_seqlen_q: Optional[int] = None,
1296
+ max_seqlen_k: Optional[int] = None,
1297
+ min_seqlen_k: Optional[int] = None,
1298
+ seqused_q: Optional[torch.Tensor] = None,
1299
+ seqused_k: Optional[torch.Tensor] = None,
1300
+ gather_kv_indices: Optional[torch.Tensor] = None,
1301
+ page_table: Optional[torch.Tensor] = None,
1302
+ softmax_scale: Optional[float] = None,
1303
+ causal: bool = False,
1304
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1305
+ learnable_sink: Optional[torch.Tensor] = None,
1306
+ softcap: float = 0.0,
1307
+ num_splits: int = 1,
1308
+ pack_gqa: Optional[bool] = None,
1309
+ deterministic: bool = False,
1310
+ score_mod: Optional[Callable] = None,
1311
+ score_mod_bwd: Optional[Callable] = None,
1312
+ aux_tensors: Optional[list] = None,
1313
+ return_lse: bool = False,
1314
+ ):
1315
+ """
1316
+ Explanation of some optional arguments:
1317
+
1318
+ qv: we write the MLA weight absorbed formula as
1319
+ O = softmax(scale * (Q @ K.T + Qv @ V.T)) @ V
1320
+ where Q = q_pe, Qv = q_nope, K = pe_cache, V = kv_cache.
1321
+
1322
+ gather_kv_indices: a tensor of shape (batch, seqlen_q, gather_kv_length) or
1323
+ (total_q, gather_kv_length) if there is cu_seqlens_q.
1324
+ Currently, only used for topk sparsity with MLA absorption kernel.
1325
+
1326
+ min_seqlen_k: for varlen, specifies the minimum kv sequence length for any batch.
1327
+ Used with gather_kv_indices to determine if we need oob masking.
1328
+ """
1329
+ return FlashAttnVarlenFunc.apply(
1330
+ q,
1331
+ k,
1332
+ v,
1333
+ qv,
1334
+ cu_seqlens_q,
1335
+ cu_seqlens_k,
1336
+ seqused_q,
1337
+ seqused_k,
1338
+ max_seqlen_q,
1339
+ max_seqlen_k,
1340
+ min_seqlen_k,
1341
+ gather_kv_indices,
1342
+ page_table,
1343
+ softmax_scale,
1344
+ causal,
1345
+ window_size,
1346
+ learnable_sink,
1347
+ softcap,
1348
+ num_splits,
1349
+ pack_gqa,
1350
+ deterministic,
1351
+ score_mod,
1352
+ score_mod_bwd,
1353
+ aux_tensors,
1354
+ return_lse,
1355
+ )
1356
+
1357
+
1358
+ def _compile_fwd_combine(
1359
+ dtype, dtype_partial, head_dim, tile_m, k_block_size, log_max_splits,
1360
+ has_cu_seqlens, has_seqused, has_lse, has_varlen_batch_idx,
1361
+ ):
1362
+ """Compile fwd combine kernel using cute fake tensors (no real GPU tensors needed)."""
1363
+ sym = cute.sym_int
1364
+ div = 128 // dtype_partial.width # 16-byte alignment in elements
1365
+
1366
+ fa_combine = FlashAttentionForwardCombine(
1367
+ dtype=dtype,
1368
+ dtype_partial=dtype_partial,
1369
+ head_dim=head_dim,
1370
+ tile_m=tile_m,
1371
+ k_block_size=k_block_size,
1372
+ log_max_splits=log_max_splits,
1373
+ )
1374
+ if not fa_combine.can_implement(
1375
+ dtype, dtype_partial, head_dim, tile_m, k_block_size, log_max_splits,
1376
+ num_threads=256,
1377
+ ):
1378
+ raise RuntimeError(
1379
+ "FlashAttention combine kernel cannot be implemented with given parameters"
1380
+ )
1381
+
1382
+ if has_cu_seqlens:
1383
+ # Varlen: (num_splits, total_q, nheads, headdim)
1384
+ num_splits, total_q, nheads = sym(), sym(), sym()
1385
+ mO_partial = fake_tensor(dtype_partial, (num_splits, total_q, nheads, head_dim), divisibility=div)
1386
+ mLSE_partial = fake_tensor(Float32, (num_splits, total_q, nheads), divisibility=1, leading_dim=1)
1387
+ mO = fake_tensor(dtype, (total_q, nheads, head_dim), divisibility=div)
1388
+ mLSE = fake_tensor(Float32, (total_q, nheads), divisibility=1, leading_dim=0) if has_lse else None
1389
+ else:
1390
+ # Batched: (num_splits, batch, seqlen, nheads, headdim)
1391
+ num_splits, batch, seqlen, nheads = sym(), sym(), sym(), sym()
1392
+ mO_partial = fake_tensor(dtype_partial, (num_splits, batch, seqlen, nheads, head_dim), divisibility=div)
1393
+ mLSE_partial = fake_tensor(Float32, (num_splits, batch, seqlen, nheads), divisibility=1, leading_dim=2)
1394
+ mO = fake_tensor(dtype, (batch, seqlen, nheads, head_dim), divisibility=div)
1395
+ mLSE = fake_tensor(Float32, (batch, seqlen, nheads), divisibility=1, leading_dim=1) if has_lse else None
1396
+ batch = mO_partial.shape[1]
1397
+
1398
+ batch_for_1d = batch if not has_cu_seqlens else sym()
1399
+ batchp1 = sym()
1400
+ mCuSeqlens = fake_tensor(Int32, (batchp1,), divisibility=1) if has_cu_seqlens else None
1401
+ mSeqused = fake_tensor(Int32, (batch_for_1d,), divisibility=1) if has_seqused else None
1402
+ mNumSplitsDynamic = None # Not parametrized in compile_key
1403
+ mVarlenBatchIdx = fake_tensor(Int32, (batch_for_1d,), divisibility=1) if has_varlen_batch_idx else None
1404
+ mSemaphore = None # Not parametrized in compile_key
1405
+
1406
+ return cute.compile(
1407
+ fa_combine,
1408
+ mO_partial, mLSE_partial, mO, mLSE,
1409
+ mCuSeqlens, mSeqused, mNumSplitsDynamic, mVarlenBatchIdx, mSemaphore,
1410
+ cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True),
1411
+ options="--enable-tvm-ffi",
1412
+ )
1413
+
1414
+
1415
+ def _flash_attn_fwd_combine(
1416
+ out_partial: torch.Tensor,
1417
+ lse_partial: torch.Tensor,
1418
+ out: torch.Tensor,
1419
+ lse: Optional[torch.Tensor] = None,
1420
+ cu_seqlens: Optional[torch.Tensor] = None,
1421
+ seqused: Optional[torch.Tensor] = None,
1422
+ num_splits_dynamic_ptr: Optional[torch.Tensor] = None,
1423
+ varlen_batch_idx: Optional[torch.Tensor] = None,
1424
+ semaphore_to_reset: Optional[torch.Tensor] = None,
1425
+ ) -> None:
1426
+ """Forward combine kernel for split attention computation.
1427
+
1428
+ Combines partial outputs and log-sum-exp values from multiple splits
1429
+ of attention computation into final outputs.
1430
+
1431
+ Args:
1432
+ out_partial: Partial outputs tensor (num_splits, batch, seqlen, nheads, headdim) or
1433
+ (num_splits, total_q, nheads, headdim) if there's cu_seqlens
1434
+ lse_partial: Partial LSE tensor (num_splits, batch, seqlen, nheads) or
1435
+ (num_splits, total_q, nheads) if there's cu_seqlens
1436
+ out: Output tensor (batch, seqlen, nheads, headdim) or (total_q, nheads, headdim) if there's cu_seqlens
1437
+ lse: Output LSE tensor (batch, seqlen, nheads) or (total_q, nheads) if there's cu_seqlens.
1438
+ cu_seqlens: Cumulative sequence lengths for variable length sequences
1439
+ seqused: Used sequence lengths for each batch
1440
+ num_splits_dynamic_ptr: Dynamic number of splits per batch
1441
+ semaphore_to_reset: Semaphore for synchronization
1442
+ k_block_size: Block size for head dimension
1443
+
1444
+ Returns:
1445
+ None
1446
+ """
1447
+ assert out_partial.dtype in [torch.float16, torch.bfloat16, torch.float32], (
1448
+ "out_partial must be fp16, bf16, or fp32"
1449
+ )
1450
+ if not is_fake_mode():
1451
+ assert out_partial.is_cuda and lse_partial.is_cuda, "tensors must be on CUDA device"
1452
+ # Determine if this is variable length based on dimensions
1453
+ is_varlen = out_partial.dim() == 4
1454
+ # Validate optional tensors
1455
+ for t, name in [
1456
+ (cu_seqlens, "cu_seqlens"),
1457
+ (seqused, "seqused"),
1458
+ (num_splits_dynamic_ptr, "num_splits_dynamic_ptr"),
1459
+ ]:
1460
+ if t is not None:
1461
+ if not is_fake_mode():
1462
+ assert t.is_cuda, f"{name} must be on CUDA device"
1463
+ assert t.is_contiguous(), f"{name} must be contiguous"
1464
+ head_dim = out_partial.shape[-1]
1465
+ num_splits = out_partial.shape[0]
1466
+ assert num_splits <= 256
1467
+ # If hdim is 96 or 192, it's faster to round them to 128 or 256 respectively
1468
+ # so that kBlockM is smaller and we have more parallelism.
1469
+ k_block_size = 64 if head_dim <= 64 else 128
1470
+ # We want kBlockM to be as small as possible to maximize parallelism.
1471
+ # E.g., if hdim is 64, we want kBlockM to be 16 so that we can use 256 threads, each reading 4 elements (floats).
1472
+ tile_m = 8 if k_block_size % 128 == 0 else (16 if k_block_size % 64 == 0 else 32)
1473
+ log_max_splits = max(math.ceil(math.log2(num_splits)), 4)
1474
+ if tile_m == 8:
1475
+ # If kBlockM == 8 then the minimum number of splits is 32.
1476
+ # TODO: we can deal w this by using 128 threads instead
1477
+ log_max_splits = max(log_max_splits, 5)
1478
+
1479
+ # Create combine kernel configuration
1480
+ dtype = torch2cute_dtype_map[out.dtype]
1481
+ dtype_partial = torch2cute_dtype_map[out_partial.dtype]
1482
+ compile_key = (
1483
+ dtype,
1484
+ dtype_partial,
1485
+ head_dim,
1486
+ tile_m,
1487
+ k_block_size,
1488
+ log_max_splits,
1489
+ cu_seqlens is not None,
1490
+ seqused is not None,
1491
+ lse is not None,
1492
+ varlen_batch_idx is not None,
1493
+ )
1494
+ if compile_key not in _flash_attn_fwd_combine.compile_cache:
1495
+ _flash_attn_fwd_combine.compile_cache[compile_key] = _compile_fwd_combine(
1496
+ *compile_key
1497
+ )
1498
+ if not is_fake_mode():
1499
+ _flash_attn_fwd_combine.compile_cache[compile_key](
1500
+ out_partial, lse_partial, out, lse,
1501
+ cu_seqlens, seqused, num_splits_dynamic_ptr, varlen_batch_idx,
1502
+ semaphore_to_reset,
1503
+ )
1504
+
1505
+
1506
+ _flash_attn_fwd_combine.compile_cache = get_jit_cache("fwd_combine")
1507
+
1508
+
1509
+ def flash_attn_combine(
1510
+ out_partial: torch.Tensor,
1511
+ lse_partial: torch.Tensor,
1512
+ out: Optional[torch.Tensor] = None,
1513
+ out_dtype: Optional[torch.dtype] = None,
1514
+ cu_seqlens: Optional[torch.Tensor] = None,
1515
+ seqused: Optional[torch.Tensor] = None,
1516
+ varlen_batch_idx: Optional[torch.Tensor] = None,
1517
+ return_lse: bool = True,
1518
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
1519
+ """Flash Attention combine function for split attention computation.
1520
+
1521
+ Combines partial outputs and log-sum-exp values from multiple splits
1522
+ of attention computation into final outputs. This is the main user-facing
1523
+ interface for the combine kernel.
1524
+
1525
+ Args:
1526
+ out_partial: Partial outputs tensor with shape:
1527
+ - (num_splits, batch_size, seqlen, num_heads, head_size) for regular batched input
1528
+ - (num_splits, total_q, num_heads, head_size) for variable length input
1529
+ lse_partial: Partial LSE tensor with shape:
1530
+ - (num_splits, batch_size, seqlen, num_heads) for regular batched input
1531
+ - (num_splits, total_q, num_heads) for variable length input
1532
+ out: Optional output tensor. If None, will be created automatically.
1533
+ out_dtype: Optional output dtype. If None, will use fp16/bf16 based on input.
1534
+ cu_seqlens: Cumulative sequence lengths for variable length sequences
1535
+ seqused: Used sequence lengths for each batch
1536
+ varlen_batch_idx: Optional mapping from virtual batch index to real batch index
1537
+ (int32 tensor of shape (batch_size,)). Used by persistent tile schedulers
1538
+ that reorder batch processing for load balancing.
1539
+ return_lse: Whether to return the combined LSE tensor. Default is True.
1540
+
1541
+ Returns:
1542
+ Tuple of (out, lse) where:
1543
+ - out: Combined output tensor with shape (batch_size, seqlen, num_heads, head_size)
1544
+ or (total_q, num_heads, head_size) for varlen
1545
+ - lse: Combined log-sum-exp tensor with shape (batch_size, seqlen, num_heads)
1546
+ or (total_q, num_heads) for varlen. None if return_lse=False
1547
+
1548
+ Note:
1549
+ This function expects the input tensors to be in the format produced by
1550
+ split attention computation, where the first dimension is num_splits.
1551
+ The permuting from user format to kernel format is now done inside the kernel.
1552
+ """
1553
+ # Input validation
1554
+ assert out_partial.dim() in [4, 5], "out_partial must have 4 or 5 dimensions"
1555
+ # Determine if this is variable length based on dimensions
1556
+ is_varlen = out_partial.dim() == 4
1557
+ if is_varlen:
1558
+ # Variable length: (num_splits, total_q, num_heads, head_size)
1559
+ num_splits, total_q, num_heads, head_size = out_partial.shape
1560
+ batch_size = 1 # Treat as single batch for varlen
1561
+ seqlen = total_q
1562
+ else:
1563
+ # Regular batched: (num_splits, batch_size, seqlen, num_heads, head_size)
1564
+ num_splits, batch_size, seqlen, num_heads, head_size = out_partial.shape
1565
+ # Determine output dtype
1566
+ if out_dtype is None:
1567
+ out_dtype = out_partial.dtype
1568
+ # Create output if not provided
1569
+ device = out_partial.device
1570
+ if out is None:
1571
+ if is_varlen:
1572
+ out = torch.empty(total_q, num_heads, head_size, dtype=out_dtype, device=device)
1573
+ else:
1574
+ out = torch.empty(
1575
+ batch_size, seqlen, num_heads, head_size, dtype=out_dtype, device=device
1576
+ )
1577
+ # Create lse output only if requested
1578
+ if return_lse:
1579
+ if is_varlen:
1580
+ lse = torch.empty(num_heads, total_q, dtype=torch.float32, device=device)
1581
+ else:
1582
+ lse = torch.empty(batch_size, num_heads, seqlen, dtype=torch.float32, device=device)
1583
+ lse = lse.transpose(-1, -2)
1584
+ else:
1585
+ lse = None
1586
+ _flash_attn_fwd_combine(
1587
+ out_partial,
1588
+ lse_partial,
1589
+ out,
1590
+ lse,
1591
+ cu_seqlens,
1592
+ seqused,
1593
+ varlen_batch_idx=varlen_batch_idx,
1594
+ )
1595
+ return out, lse
build/torch-cuda/flashrt_fa4/cute/mask.py ADDED
@@ -0,0 +1,1470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Optional, Callable, TypeAlias, Tuple
4
+ from dataclasses import dataclass
5
+ import enum
6
+
7
+ import cutlass
8
+ import cutlass.cute as cute
9
+ from cutlass import Float32, Int32, Uint32, const_expr
10
+ from cutlass.cutlass_dsl import min as dsl_min
11
+
12
+ from fa4_cute_runtime.quack import layout_utils
13
+ import fa4_cute_runtime.flashrt_fa4.cute.utils as utils
14
+ from fa4_cute_runtime.flashrt_fa4.cute.block_info import BlockInfo
15
+ from fa4_cute_runtime.flashrt_fa4.cute.seqlen_info import SeqlenInfoQK
16
+
17
+ MaskGenFn: TypeAlias = Callable[[int], Uint32]
18
+ MASK_R2P_CHUNK_SIZE: int = 32
19
+
20
+
21
+ @cute.jit
22
+ def r2p_bitmask_below(limit: Int32, s: int) -> Uint32:
23
+ """32-bit R2P bitmask keeping positions < limit (exclusive upper bound).
24
+
25
+ Positions 0..limit-1 in chunk `s` get bit=1 (keep), the rest bit=0 (mask).
26
+ Uses inline PTX to avoid shift-by-type-width UB.
27
+ """
28
+ m = max((s + 1) * MASK_R2P_CHUNK_SIZE - limit, 0)
29
+ return utils.shr_u32(Uint32(0xFFFFFFFF), Uint32(m))
30
+
31
+
32
+ @cute.jit
33
+ def r2p_bitmask_above(limit: Int32, s: int) -> Uint32:
34
+ """32-bit R2P bitmask keeping positions >= limit (inclusive lower bound).
35
+
36
+ Positions limit..31 in chunk `s` get bit=1 (keep), the rest bit=0 (mask).
37
+ Uses inline PTX to avoid shift-by-type-width UB.
38
+ """
39
+ n = max(limit - s * MASK_R2P_CHUNK_SIZE, 0)
40
+ return utils.shl_u32(Uint32(0xFFFFFFFF), Uint32(n))
41
+
42
+
43
+ @cute.jit
44
+ def mask_r2p_lambda(
45
+ X: cute.Tensor,
46
+ mask_gen_fn: cutlass.Constexpr[MaskGenFn],
47
+ rank1: bool = False,
48
+ ) -> None:
49
+ """Apply R2P masking with a custom bitmask generator.
50
+
51
+ mask_gen_fn(chunk_idx: constexpr int) -> Uint32:
52
+ Returns a 32-bit bitmask for the chunk. Bit i set means column
53
+ chunk_idx * chunk_size + i is KEPT; bit i clear means masked to -inf.
54
+ """
55
+ ncol = const_expr(cute.size(X.shape[cute.rank(X) - 1]) if not rank1 else cute.size(X.shape))
56
+ # 32-column chunks. The mask_gen_fn returns a Uint32 bitmask (1=keep).
57
+ CHUNK_SIZE = MASK_R2P_CHUNK_SIZE
58
+ for s in cutlass.range_constexpr(cute.ceil_div(ncol, CHUNK_SIZE)):
59
+ mask = mask_gen_fn(s)
60
+ # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
61
+ for i in cutlass.range_constexpr(min(CHUNK_SIZE, ncol - s * CHUNK_SIZE)):
62
+ in_bound = cutlass.Boolean(mask & (Uint32(1) << i))
63
+ c = s * CHUNK_SIZE + i
64
+ if const_expr(rank1):
65
+ X[c] = X[c] if in_bound else -Float32.inf
66
+ else:
67
+ for r in cutlass.range_constexpr(cute.size(X.shape[0])):
68
+ X[r, c] = X[r, c] if in_bound else -Float32.inf
69
+
70
+
71
+ @cute.jit
72
+ def sm90_col_to_r2p_idx(col_limit: Int32) -> Int32:
73
+ """Transform SM90 MMA column coordinate to R2P element index.
74
+
75
+ SM90 MMA accumulator column indices are non-contiguous: 0, 1, 8, 9, 16, 17, ...
76
+ Element indices are contiguous: 0, 1, 2, 3, 4, 5, ...
77
+ This converts a column-space threshold to element-space for r2p_bitmask_below/above.
78
+ """
79
+ return col_limit // 8 * 2 + min(col_limit % 8, 2)
80
+
81
+
82
+ @cute.jit
83
+ def row_to_r2p_idx(x: Int32, num_rep: int, num_wg: int) -> Int32:
84
+ """Convert a row coordinate to an R2P element index in the warp-group interleaved layout.
85
+
86
+ In the SM100 backward pass, 2 warp groups share TMEM. The TMEM load atom
87
+ distributes rows in an interleaved pattern: elements 0..num_rep-1 map to
88
+ rows 0..num_rep-1 (warp group 0), elements num_rep..2*num_rep-1 map to
89
+ rows num_rep*num_wg..num_rep*num_wg+num_rep-1 (warp group 1), and so on.
90
+ Row-coordinate thresholds (causal limits, window bounds, uih_len) must be
91
+ converted to element indices before use with r2p_bitmask_above/below.
92
+
93
+ Rows not owned by this thread (in the gap between warp groups) are clamped
94
+ to the boundary element index, which is safe because R2P thresholds are
95
+ monotonic.
96
+
97
+ Example with num_rep=16, num_wg=2:
98
+ row 0 -> elem 0, row 15 -> elem 15,
99
+ row 16 -> elem 16 (clamped), row 31 -> elem 16 (clamped),
100
+ row 32 -> elem 16, row 33 -> elem 17, row 47 -> elem 31.
101
+ """
102
+ return x // (num_rep * num_wg) * num_rep + min(x % (num_rep * num_wg), num_rep)
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class AttentionMask:
107
+ tile_m: cutlass.Constexpr[int]
108
+ tile_n: cutlass.Constexpr[int]
109
+ seqlen_info: SeqlenInfoQK
110
+ window_size_left: Optional[Int32] = None
111
+ window_size_right: Optional[Int32] = None
112
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1 # only pass in if we're doing PackGQA
113
+ swap_AB: cutlass.Constexpr[bool] = False
114
+
115
+ @property
116
+ def seqlen_q(self) -> Int32:
117
+ return self.seqlen_info.seqlen_q
118
+
119
+ @property
120
+ def seqlen_k(self) -> Int32:
121
+ return self.seqlen_info.seqlen_k
122
+
123
+ @cute.jit
124
+ def apply_mask(
125
+ self,
126
+ acc_S: cute.Tensor,
127
+ batch_idx: cutlass.Int32,
128
+ head_idx: cutlass.Int32,
129
+ m_block: cutlass.Int32,
130
+ n_block: cutlass.Int32,
131
+ thr_mma: cute.TiledMma,
132
+ mask_seqlen: cutlass.Constexpr[bool],
133
+ mask_causal: cutlass.Constexpr[bool],
134
+ mask_local: cutlass.Constexpr[bool] = False,
135
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
136
+ aux_tensors: Optional[list] = None,
137
+ fastdiv_mods=(None, None),
138
+ ) -> None:
139
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
140
+ acc_S_mn = layout_utils.reshape_acc_to_mn(acc_S, transpose=self.swap_AB)
141
+ acc_shape = (self.tile_m, self.tile_n)
142
+ cS = cute.make_identity_tensor(acc_shape if not self.swap_AB else acc_shape[::-1])
143
+ tScS_mn = layout_utils.reshape_acc_to_mn(thr_mma.partition_C(cS), transpose=self.swap_AB)
144
+ # We use t0ScS as these indices are known at compile time. We then must subtract the
145
+ # column limit by the thread column offset.
146
+ t0ScS_mn = layout_utils.reshape_acc_to_mn(
147
+ thr_mma.get_slice(0).partition_C(cS), transpose=self.swap_AB
148
+ )
149
+ ROW = 0 if const_expr(not self.swap_AB) else 1
150
+ COL = 1 if const_expr(not self.swap_AB) else 0
151
+ thr_col_offset = tScS_mn[0][COL]
152
+ # To handle edge cases of completely masked out rows where n_block_max = 0,
153
+ # we treat negative n_blocks as 0th n_block
154
+ # TODO: find more transparent solution
155
+ if n_block < 0:
156
+ n_block = 0
157
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
158
+ if const_expr(not mask_causal and not mask_local and mask_mod is None):
159
+ if const_expr(mask_seqlen):
160
+ r2p = const_expr(not self.swap_AB)
161
+ if const_expr(not r2p):
162
+ # traverse column index.
163
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
164
+ oob = t0ScS_mn[0, c][COL] >= seqlenk_col_limit
165
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
166
+ acc_S_mn[r, c] = -Float32.inf if oob else acc_S_mn[r, c]
167
+ else:
168
+ seqlenk_col_limit_r2p = sm90_col_to_r2p_idx(seqlenk_col_limit)
169
+ mask_r2p_lambda(acc_S_mn, lambda s: r2p_bitmask_below(seqlenk_col_limit_r2p, s))
170
+
171
+ elif const_expr(
172
+ not mask_causal and not mask_local and mask_mod is not None
173
+ ): # FlexAttention mask mod
174
+ nrow = const_expr(cute.size(tScS_mn.shape[0]))
175
+ ncol = const_expr(cute.size(tScS_mn.shape[1]))
176
+ has_fastdiv = const_expr(
177
+ fastdiv_mods is not None
178
+ and fastdiv_mods[0] is not None
179
+ and fastdiv_mods[1] is not None
180
+ )
181
+ wrap_aux_indices = const_expr(
182
+ has_fastdiv and mask_seqlen and const_expr(aux_tensors is not None)
183
+ )
184
+
185
+ for r in cutlass.range_constexpr(nrow):
186
+ # Respect swap_AB: ROW/COL determine which coordinate component corresponds to Q/KV.
187
+ local_row = tScS_mn[r, 0][ROW]
188
+ global_row_idx = local_row + m_block * self.tile_m
189
+ row_for_mod = global_row_idx
190
+ head_idx_for_mod = head_idx
191
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
192
+ head_offset = global_row_idx % self.qhead_per_kvhead_packgqa
193
+ head_idx_for_mod = head_idx * self.qhead_per_kvhead_packgqa + head_offset
194
+ row_for_mod = global_row_idx // self.qhead_per_kvhead_packgqa
195
+ row_for_seqlen = row_for_mod
196
+ if const_expr(wrap_aux_indices):
197
+ _, row_for_mod = divmod(row_for_mod, fastdiv_mods[0])
198
+
199
+ for col in cutlass.range_constexpr(ncol):
200
+ col_idx_local = t0ScS_mn[0, col][COL]
201
+ # Convert to absolute column index
202
+ global_col_idx = thr_col_offset + col_idx_local + n_block * self.tile_n
203
+ col_for_mod = global_col_idx
204
+ if const_expr(wrap_aux_indices):
205
+ _, col_for_mod = divmod(global_col_idx, fastdiv_mods[1])
206
+
207
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
208
+ head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
209
+ q_idx_ssa = utils.scalar_to_ssa(row_for_mod, cutlass.Int32)
210
+ kv_idx_ssa = utils.scalar_to_ssa(col_for_mod, cutlass.Int32)
211
+ mask_value = mask_mod(
212
+ batch_idx_ssa,
213
+ head_idx_ssa,
214
+ q_idx_ssa,
215
+ kv_idx_ssa,
216
+ self.seqlen_info,
217
+ aux_tensors,
218
+ )
219
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
220
+ if const_expr(mask_seqlen):
221
+ out_of_bounds = (row_for_seqlen >= self.seqlen_q) or (
222
+ global_col_idx >= self.seqlen_k
223
+ )
224
+ if out_of_bounds:
225
+ acc_S_mn[r, col] = -cutlass.Float32.inf
226
+ else:
227
+ acc_S_mn[r, col] = acc_S_mn[r, col] if cond else -cutlass.Float32.inf
228
+ else:
229
+ acc_S_mn[r, col] = acc_S_mn[r, col] if cond else -cutlass.Float32.inf
230
+
231
+ else: # Causal or local
232
+ if const_expr(not self.swap_AB):
233
+ # If PackGQA, we split the work of compute divmod among threads in the same row
234
+ threads_per_row = thr_mma.tv_layout_C.shape[0][0]
235
+ mma_m_idx = None
236
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
237
+ assert not self.swap_AB, "swap_AB with PackGQA not supported yet"
238
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, (
239
+ "threads_per_row must divide WARP_SIZE"
240
+ )
241
+ assert cute.size(acc_S_mn.shape[0]) <= threads_per_row
242
+ tidx = thr_mma.thr_idx
243
+ mma_m_idx = (
244
+ m_block * self.tile_m + tScS_mn[tidx % threads_per_row, 0][0]
245
+ ) // self.qhead_per_kvhead_packgqa
246
+ causal_row_offset = (
247
+ 1 + self.seqlen_k - n_block * self.tile_n - self.seqlen_q - thr_col_offset
248
+ )
249
+ if const_expr(mask_causal):
250
+ r2p = const_expr(not self.swap_AB) # R2P trick, see apply_mask_sm100
251
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
252
+ # get the column index limit based on current row. Only consider the row index, so the column index sets to 0.
253
+ if const_expr(self.qhead_per_kvhead_packgqa == 1):
254
+ row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
255
+ else:
256
+ row_idx = utils.shuffle_sync(
257
+ mma_m_idx, r % threads_per_row, width=threads_per_row
258
+ )
259
+ col_limit_right = row_idx + causal_row_offset
260
+ if const_expr(mask_seqlen):
261
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
262
+ if const_expr(not r2p):
263
+ # traverse column index.
264
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
265
+ acc_S_mn[r, c] = (
266
+ -Float32.inf
267
+ if t0ScS_mn[0, c][1] >= col_limit_right
268
+ else acc_S_mn[r, c]
269
+ )
270
+ else:
271
+ col_limit_r2p = sm90_col_to_r2p_idx(col_limit_right)
272
+ mask_r2p_lambda(
273
+ acc_S_mn[r, None],
274
+ lambda s: r2p_bitmask_below(col_limit_r2p, s),
275
+ rank1=True,
276
+ )
277
+ else: # Local
278
+ local_row_offset_right = (
279
+ causal_row_offset + self.window_size_right
280
+ if const_expr(self.window_size_right is not None)
281
+ else None
282
+ )
283
+ local_row_offset_left = (
284
+ causal_row_offset - 1 - self.window_size_left
285
+ if const_expr(self.window_size_left is not None)
286
+ else None
287
+ )
288
+ r2p_local = const_expr(not self.swap_AB)
289
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
290
+ if const_expr(self.qhead_per_kvhead_packgqa == 1):
291
+ row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
292
+ else:
293
+ row_idx = utils.shuffle_sync(
294
+ mma_m_idx, r % threads_per_row, width=threads_per_row
295
+ )
296
+ if const_expr(self.window_size_right is not None):
297
+ col_limit_right = row_idx + local_row_offset_right
298
+ else:
299
+ col_limit_right = self.tile_n
300
+ if const_expr(mask_seqlen):
301
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
302
+ col_limit_left = (
303
+ row_idx + local_row_offset_left
304
+ if const_expr(self.window_size_left is not None)
305
+ else 0
306
+ )
307
+ if const_expr(not r2p_local):
308
+ # traverse column index.
309
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
310
+ col_idx = t0ScS_mn[0, c][1]
311
+ if col_idx >= col_limit_right or col_idx < col_limit_left:
312
+ acc_S_mn[r, c] = -Float32.inf
313
+ else:
314
+ col_limit_right_r2p = sm90_col_to_r2p_idx(col_limit_right)
315
+ col_limit_left_r2p = sm90_col_to_r2p_idx(col_limit_left)
316
+
317
+ def mask_gen_fn(s: int) -> Uint32:
318
+ return r2p_bitmask_below(
319
+ col_limit_right_r2p, s
320
+ ) & r2p_bitmask_above(col_limit_left_r2p, s)
321
+
322
+ mask_r2p_lambda(acc_S_mn[r, None], mask_gen_fn, rank1=True)
323
+ else: # swap_AB
324
+ assert self.qhead_per_kvhead_packgqa == 1
325
+ thr_row_offset = tScS_mn[0][ROW]
326
+ causal_row_offset = (
327
+ seqlenk_col_limit - self.seqlen_q + m_block * self.tile_m + thr_row_offset
328
+ )
329
+ if const_expr(mask_causal):
330
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
331
+ col0 = t0ScS_mn[0, c][COL]
332
+ # If col0 is beyond the column limit, we want to mask out the entire
333
+ # column, by setting row limit to be self.tile_m.
334
+ row_limit_top = (
335
+ self.tile_m
336
+ if col0 >= seqlenk_col_limit and mask_seqlen
337
+ else col0 - causal_row_offset
338
+ )
339
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
340
+ acc_S_mn[r, c] = (
341
+ -Float32.inf
342
+ if t0ScS_mn[r, 0][ROW] < row_limit_top
343
+ else acc_S_mn[r, c]
344
+ )
345
+ else:
346
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
347
+ col0 = t0ScS_mn[0, c][COL]
348
+ # If col0 is beyond the column limit, we want to mask out the entire
349
+ # column, by setting row limit to be self.tile_m.
350
+ row_limit_top = (
351
+ self.tile_m
352
+ if col0 >= seqlenk_col_limit and mask_seqlen
353
+ else (
354
+ col0 - causal_row_offset - self.window_size_right
355
+ if const_expr(self.window_size_right is not None)
356
+ else 0
357
+ )
358
+ )
359
+ row_limit_bot = (
360
+ col0 - causal_row_offset + self.window_size_left
361
+ if const_expr(self.window_size_left is not None)
362
+ else self.tile_m
363
+ )
364
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
365
+ row_idx = t0ScS_mn[r, 0][ROW]
366
+ acc_S_mn[r, c] = (
367
+ -Float32.inf
368
+ if row_idx < row_limit_top or row_idx > row_limit_bot
369
+ else acc_S_mn[r, c]
370
+ )
371
+
372
+ @cute.jit
373
+ def apply_mask_sm100(
374
+ self,
375
+ acc_S: cute.Tensor,
376
+ m_block: Int32,
377
+ n_block: Int32,
378
+ thr_mma: cute.TiledMma,
379
+ thr_tmem_load: cute.TiledCopy,
380
+ mask_seqlen: cutlass.Constexpr[bool],
381
+ mask_causal: cutlass.Constexpr[bool],
382
+ mask_local: cutlass.Constexpr[bool] = False,
383
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
384
+ batch_idx: Int32 = None,
385
+ head_idx: Int32 = None,
386
+ aux_tensors: Optional[list] = None,
387
+ fastdiv_mods=(None, None),
388
+ head_divmod=None,
389
+ check_q_boundary: bool = False,
390
+ r2p: bool = True,
391
+ rBitmask: Optional[cute.Tensor] = None,
392
+ ) -> None:
393
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
394
+ acc_shape = (self.tile_m, self.tile_n)
395
+ cS = cute.make_identity_tensor(acc_shape if not self.swap_AB else acc_shape[::-1])
396
+ tScS = thr_mma.partition_C(cS)
397
+ tScS = tScS[(None, None), 0, 0]
398
+ tScS_t2r = thr_tmem_load.partition_D(tScS)
399
+ # To handle edge cases of completely masked out rows where n_block_max = 0,
400
+ # we treat negative n_blocks as 0th n_block
401
+ # TODO: find more transparent solution
402
+ if n_block < 0:
403
+ n_block = 0
404
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n
405
+
406
+ if const_expr(rBitmask is not None):
407
+ ncol_packed = const_expr(cute.size(rBitmask.shape[0]))
408
+ for i in cutlass.range_constexpr(ncol_packed):
409
+ col_start = 32 * i # mask is bit-packed into uint32
410
+ curr_mask_val = rBitmask[i]
411
+ for j in cutlass.range_constexpr(32):
412
+ curr_col = col_start + j
413
+ mask = (curr_mask_val >> j) & 1
414
+ acc_S[curr_col] = acc_S[curr_col] if cutlass.Boolean(mask) else -Float32.inf
415
+
416
+ elif const_expr(not mask_causal and not mask_local and mask_mod is None):
417
+ if const_expr(mask_seqlen):
418
+ if const_expr(not r2p):
419
+ for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
420
+ # if tScS_t2r[i][1] >= seqlenk_col_limit:
421
+ # acc_S[i] = -Float32.inf
422
+ # For some reason the 2 lines above generate really bad SASS
423
+ acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= seqlenk_col_limit else acc_S[i]
424
+ else:
425
+ mask_r2p_lambda(
426
+ acc_S,
427
+ lambda s: r2p_bitmask_below(seqlenk_col_limit, s),
428
+ rank1=True,
429
+ )
430
+
431
+ elif const_expr(not mask_causal and not mask_local and mask_mod is not None):
432
+ # Block sparse case w/ mask_mod
433
+ has_fastdiv = const_expr(
434
+ fastdiv_mods is not None
435
+ and fastdiv_mods[0] is not None
436
+ and fastdiv_mods[1] is not None
437
+ )
438
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
439
+
440
+ ncol = const_expr(cute.size(tScS_t2r.shape))
441
+ for i in cutlass.range_constexpr(ncol):
442
+ row_coord = tScS_t2r[i][0] if not self.swap_AB else tScS_t2r[i][1]
443
+ col_coord = tScS_t2r[i][1] if not self.swap_AB else tScS_t2r[i][0]
444
+ global_row = row_coord + m_block * self.tile_m
445
+ global_col = col_coord + n_block * self.tile_n
446
+
447
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
448
+ assert head_divmod is not None
449
+ mask_row, head_offset = divmod(global_row, head_divmod)
450
+ head_idx_for_mod = head_idx * self.qhead_per_kvhead_packgqa + head_offset
451
+ else:
452
+ head_idx_for_mod = head_idx
453
+ mask_row = global_row
454
+
455
+ mask_row_for_mod = mask_row
456
+ if const_expr(has_fastdiv and aux_tensors is not None):
457
+ if check_q_boundary:
458
+ _, mask_row_for_mod = divmod(mask_row, fastdiv_mods[0])
459
+ global_col_for_mod = global_col
460
+ if const_expr(has_fastdiv and mask_seqlen and aux_tensors is not None):
461
+ _, global_col_for_mod = divmod(global_col, fastdiv_mods[1])
462
+
463
+ head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
464
+ mask_row_ssa = utils.scalar_to_ssa(mask_row_for_mod, cutlass.Int32)
465
+ kv_idx_ssa = utils.scalar_to_ssa(global_col_for_mod, cutlass.Int32)
466
+ mask_value = mask_mod(
467
+ batch_idx_ssa,
468
+ head_idx_ssa,
469
+ mask_row_ssa,
470
+ kv_idx_ssa,
471
+ self.seqlen_info,
472
+ aux_tensors,
473
+ )
474
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
475
+ acc_S[i] = acc_S[i] if cond else -Float32.inf
476
+ if const_expr(mask_seqlen):
477
+ acc_S[i] = -Float32.inf if global_col >= self.seqlen_k else acc_S[i]
478
+ if check_q_boundary:
479
+ acc_S[i] = -Float32.inf if mask_row >= self.seqlen_q else acc_S[i]
480
+
481
+ else: # Causal or local
482
+ causal_row_offset = self.seqlen_k - n_block * self.tile_n - self.seqlen_q
483
+ row_idx = tScS_t2r[0][0] + m_block * self.tile_m
484
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
485
+ row_idx = row_idx // self.qhead_per_kvhead_packgqa
486
+ if const_expr(mask_causal):
487
+ col_limit_right = row_idx + causal_row_offset + 1
488
+ if const_expr(mask_seqlen):
489
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
490
+ # if cute.arch.thread_idx()[0] % 32 == 0:
491
+ # cute.printf("tidx = %d, tidx tmem = %d, row_idx = %d, col_limit_right = %d, causal_row_offset = %d\n", cute.arch.thread_idx()[0], thr_tmem_load.thr_idx, row_idx, col_limit_right, causal_row_offset)
492
+ ncol = const_expr(cute.size(tScS_t2r.shape))
493
+ if const_expr(not r2p):
494
+ for i in cutlass.range(ncol, unroll_full=True):
495
+ acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= col_limit_right else acc_S[i]
496
+ else:
497
+ mask_r2p_lambda(
498
+ acc_S,
499
+ lambda s: r2p_bitmask_below(col_limit_right, s),
500
+ rank1=True,
501
+ )
502
+ else:
503
+ local_row_offset_right = (
504
+ causal_row_offset + 1 + self.window_size_right
505
+ if const_expr(self.window_size_right is not None)
506
+ else None
507
+ )
508
+ local_row_offset_left = (
509
+ causal_row_offset - self.window_size_left
510
+ if const_expr(self.window_size_left is not None)
511
+ else None
512
+ )
513
+ if const_expr(self.window_size_right is not None):
514
+ col_limit_right = row_idx + local_row_offset_right
515
+ else:
516
+ col_limit_right = self.tile_n
517
+ if const_expr(mask_seqlen):
518
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
519
+ col_limit_left = (
520
+ row_idx + local_row_offset_left
521
+ if const_expr(self.window_size_left is not None)
522
+ else 0
523
+ )
524
+ if const_expr(not r2p):
525
+ # if cute.arch.thread_idx()[0] == 0 or cute.arch.thread_idx()[0] == 128: cute.printf("m_block = {}, n_block = {}, row_idx = {}, causal_row_offset = {}, col_limit_right = {}, col_limit_left = {}", m_block, n_block, row_idx, causal_row_offset, col_limit_right, col_limit_left)
526
+ for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
527
+ col_idx = tScS_t2r[i][1]
528
+ acc_S[i] = (
529
+ -Float32.inf
530
+ if col_idx >= col_limit_right or col_idx < col_limit_left
531
+ else acc_S[i]
532
+ )
533
+ else:
534
+ # Dual-bound R2P masking for SM100.
535
+ # Masks elements where: NOT (col_limit_left <= col < col_limit_right)
536
+
537
+ def mask_gen_fn(s: int) -> Uint32:
538
+ return r2p_bitmask_below(col_limit_right, s) & r2p_bitmask_above(
539
+ col_limit_left, s
540
+ )
541
+
542
+ mask_r2p_lambda(acc_S, mask_gen_fn, rank1=True)
543
+
544
+ @cute.jit
545
+ def apply_mask_sm100_transposed(
546
+ self,
547
+ acc_S: cute.Tensor,
548
+ tScS_t2r: cute.Tensor,
549
+ t0ScS_t2r: cute.Tensor,
550
+ m_block: cutlass.Int32,
551
+ n_block: cutlass.Int32,
552
+ mask_seqlen: cutlass.Constexpr,
553
+ mask_causal: cutlass.Constexpr,
554
+ mask_local: cutlass.Constexpr,
555
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
556
+ batch_idx: Int32 = None,
557
+ head_idx: Int32 = None,
558
+ aux_tensors: Optional[list] = None,
559
+ fastdiv_mods=(None, None),
560
+ is_full_block: bool = False,
561
+ check_m_boundary: bool = True,
562
+ ) -> None:
563
+ """
564
+ Backward pass: mask S = K @ Q.T where n_block tiles seqlen_k and m_block tiles seqlen_q.
565
+
566
+ Coordinate convention:
567
+ - ROW corresponds to Q (m_block)
568
+ - COL corresponds to KV (n_block)
569
+
570
+ is_full_block: If True, skip mask_mod (all elements valid). Only apply seqlen masking.
571
+ check_m_boundary: If False, skip seqlen_q boundary check (optimization for non-boundary m_blocks).
572
+ When iterating m_blocks in forward order, only the last m_block may be partial.
573
+ """
574
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
575
+ ROW = 0 if const_expr(not self.swap_AB) else 1
576
+ COL = 1 if const_expr(not self.swap_AB) else 0
577
+ # assert t0ScS_t2r[0][COL] == 0, "col0 == 0" # tmp comment for 2-cta bwd
578
+ thr_col_offset = tScS_t2r[0][COL]
579
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
580
+
581
+ if const_expr(not mask_causal and not mask_local and mask_mod is not None):
582
+ # Block sparse case with mask_mod (backward)
583
+ #
584
+ # Coordinate convention: ROW → Q (m_block), COL → KV (n_block).
585
+ # These already account for swap_AB.
586
+ #
587
+ # FULL blocks: mask_mod returns True for all elements, so skip it.
588
+ # Still need seqlen bounds check (elements may be OOB on last m_block).
589
+ # PARTIAL blocks: apply mask_mod element-wise, then seqlen bounds.
590
+ if is_full_block:
591
+ if const_expr(mask_seqlen):
592
+ if seqlenk_col_limit <= 0:
593
+ # Entire tile is OOB for K
594
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
595
+ acc_S[i] = -cutlass.Float32.inf
596
+ elif check_m_boundary:
597
+ # Last m_block: check Q and K boundaries
598
+ ncol = const_expr(cute.size(tScS_t2r.shape))
599
+ for i in cutlass.range_constexpr(ncol):
600
+ row_coord = tScS_t2r[i][ROW]
601
+ col_coord = tScS_t2r[i][COL]
602
+ global_q = row_coord + m_block * self.tile_m
603
+ global_kv = col_coord + n_block * self.tile_n
604
+ q_out_of_bounds = global_q >= self.seqlen_q
605
+ kv_out_of_bounds = global_kv >= self.seqlen_k
606
+ out_of_bounds = q_out_of_bounds or kv_out_of_bounds
607
+ acc_S[i] = -cutlass.Float32.inf if out_of_bounds else acc_S[i]
608
+ else:
609
+ # Partial block
610
+ has_fastdiv = const_expr(
611
+ fastdiv_mods is not None
612
+ and fastdiv_mods[0] is not None
613
+ and fastdiv_mods[1] is not None
614
+ )
615
+ wrap_aux_indices = const_expr(
616
+ has_fastdiv and mask_seqlen and const_expr(aux_tensors is not None)
617
+ )
618
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
619
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32)
620
+
621
+ ncol = const_expr(cute.size(tScS_t2r.shape))
622
+ for i in cutlass.range_constexpr(ncol):
623
+ row_coord = tScS_t2r[i][ROW]
624
+ col_coord = tScS_t2r[i][COL]
625
+ global_q = row_coord + m_block * self.tile_m
626
+ global_kv = col_coord + n_block * self.tile_n
627
+
628
+ q_idx_for_mod = global_q
629
+ kv_idx_for_mod = global_kv
630
+ if const_expr(wrap_aux_indices):
631
+ _, q_idx_for_mod = divmod(global_q, fastdiv_mods[0])
632
+ _, kv_idx_for_mod = divmod(global_kv, fastdiv_mods[1])
633
+
634
+ q_idx_ssa = utils.scalar_to_ssa(q_idx_for_mod, cutlass.Int32)
635
+ kv_idx_ssa = utils.scalar_to_ssa(kv_idx_for_mod, cutlass.Int32)
636
+
637
+ mask_value = mask_mod(
638
+ batch_idx_ssa,
639
+ head_idx_ssa,
640
+ q_idx_ssa,
641
+ kv_idx_ssa,
642
+ self.seqlen_info,
643
+ aux_tensors,
644
+ )
645
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
646
+ acc_S[i] = acc_S[i] if cond else -cutlass.Float32.inf
647
+
648
+ if const_expr(mask_seqlen):
649
+ # check_m_boundary=False skips q check for non-boundary m_blocks
650
+ q_out_of_bounds = check_m_boundary and (global_q >= self.seqlen_q)
651
+ kv_out_of_bounds = global_kv >= self.seqlen_k
652
+ out_of_bounds = q_out_of_bounds or kv_out_of_bounds
653
+ acc_S[i] = -cutlass.Float32.inf if out_of_bounds else acc_S[i]
654
+
655
+ elif const_expr(not mask_causal and not mask_local):
656
+ if const_expr(mask_seqlen):
657
+ if seqlenk_col_limit <= 0:
658
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
659
+ acc_S[i] = -cutlass.Float32.inf
660
+ else: # Causal or local
661
+ thr_row_offset = tScS_t2r[0][ROW]
662
+ seqlenq_row_limit = self.seqlen_q - m_block * self.tile_m - thr_row_offset
663
+ causal_offset = seqlenq_row_limit - seqlenk_col_limit
664
+ if const_expr(mask_causal):
665
+ # tidx = cute.arch.thread_idx()[0] % 256
666
+ # if tidx < 32:
667
+ # cute.printf("tidx = {}, {} {}, {} {}", tidx, tScS_t2r[0][0], tScS_t2r[0][1], tScS_t2r[1][0], tScS_t2r[1][1])
668
+ row_limit_top = causal_offset
669
+ if const_expr(mask_seqlen):
670
+ # If col is beyond the column limit, we want to mask out the entire
671
+ # column, by setting row limit to be self.tile_m.
672
+ if seqlenk_col_limit <= 0:
673
+ row_limit_top = self.tile_m
674
+ r2p = True
675
+ if const_expr(not r2p):
676
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
677
+ acc_S[i] = (
678
+ -cutlass.Float32.inf if t0ScS_t2r[i][ROW] < row_limit_top else acc_S[i]
679
+ )
680
+ else:
681
+ num_rep = cute.size(tScS_t2r, mode=[0]) # 16 or 32
682
+ num_wg = 2
683
+ row_limit = row_to_r2p_idx(row_limit_top, num_rep, num_wg)
684
+ mask_r2p_lambda(
685
+ acc_S,
686
+ lambda s: r2p_bitmask_above(row_limit, s),
687
+ rank1=True,
688
+ )
689
+ else:
690
+ if const_expr(self.window_size_right is not None):
691
+ row_limit_top = causal_offset - self.window_size_right
692
+ else:
693
+ row_limit_top = 0
694
+ if const_expr(self.window_size_left is not None):
695
+ row_limit_bot = causal_offset + self.window_size_left
696
+ if const_expr(mask_seqlen):
697
+ if seqlenk_col_limit <= 0:
698
+ row_limit_top = self.tile_m
699
+ r2p = True
700
+ if const_expr(not r2p):
701
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
702
+ row_idx = t0ScS_t2r[i][ROW]
703
+ local_mask = row_idx < row_limit_top
704
+ if const_expr(self.window_size_left is not None):
705
+ local_mask |= row_idx > row_limit_bot
706
+ acc_S[i] = -cutlass.Float32.inf if local_mask else acc_S[i]
707
+ else:
708
+
709
+ def mask_gen_fn(s: int) -> Uint32:
710
+ num_rep = cute.size(tScS_t2r, mode=[0])
711
+ num_wg = 2
712
+
713
+ row_limit = row_to_r2p_idx(row_limit_top, num_rep, num_wg)
714
+ mask = r2p_bitmask_above(row_limit, s)
715
+
716
+ if const_expr(self.window_size_left is not None):
717
+ row_limit_bottom = row_to_r2p_idx(row_limit_bot + 1, num_rep, num_wg)
718
+ mask = mask & r2p_bitmask_below(row_limit_bottom, s)
719
+
720
+ return mask
721
+
722
+ mask_r2p_lambda(
723
+ acc_S,
724
+ mask_gen_fn,
725
+ rank1=True,
726
+ )
727
+
728
+
729
+ # -----------------------------------------------------------------------------
730
+ # SM100 FMHA fused-mask policy layer (separate from generic mask primitives).
731
+ # -----------------------------------------------------------------------------
732
+
733
+
734
+ class Sm100MaskEnum(enum.Enum):
735
+ """Enumeration of mask types for FMHA operations.
736
+
737
+ - RESIDUAL_MASK: Residual mask for handling variable sequence lengths
738
+ - WINDOW_MASK: Window mask for attention which also includes causal and no mask
739
+ - WINDOW_MASK_INFERENCE: Same as the window mask, but has the limitation that the end of q is aligned with the end of k
740
+ - WINDOW_MASK_BWD: Window mask for backward pass
741
+ - WINDOW_MASK_BWD_INFERENCE: Same as the window mask for backward pass, but has the limitation that the end of q is aligned with the end of k
742
+ """
743
+
744
+ NO_MASK = enum.auto()
745
+ RESIDUAL_MASK = enum.auto()
746
+ CAUSAL_MASK = enum.auto()
747
+ WINDOW_MASK = enum.auto()
748
+ WINDOW_MASK_INFERENCE = enum.auto()
749
+ # Deprecated the following types
750
+ WINDOW_MASK_BWD = enum.auto()
751
+ WINDOW_MASK_BWD_INFERENCE = enum.auto()
752
+ RESIDUAL_MASK_BWD = enum.auto()
753
+
754
+
755
+ class Sm100FusedMask:
756
+ """A fused mask implementation for FMHA operations.
757
+
758
+ This class handles different types of attention masks including no mask,
759
+ residual mask for variable sequence lengths, and causal mask for
760
+ autoregressive attention patterns.
761
+
762
+ The class provides methods to:
763
+ - Calculate trip counts for different mask types
764
+ - Apply masks to attention scores
765
+ - Handle masked and unmasked trip calculations
766
+ """
767
+
768
+ def get_trip_count(
769
+ mask_type: Sm100MaskEnum,
770
+ blk_coord: cute.Coord,
771
+ tile_shape: cute.Shape,
772
+ seqlen_q: Int32,
773
+ seqlen_k: Int32,
774
+ window_size_left: Optional[Int32] = None,
775
+ window_size_right: Optional[Int32] = None,
776
+ ) -> Int32:
777
+ """
778
+ Calculate the number of trips needed for the current block.
779
+
780
+ The trip count depends on the mask type and the block coordinates.
781
+ For causal masks, it considers the autoregressive constraint.
782
+
783
+ :param mask_type: Type of mask to use
784
+ :type mask_type: utils.Sm100MaskEnum
785
+ :param blk_coord: Block coordinates.
786
+ :type blk_coord: cute.Coord
787
+ :param tile_shape: Shape of the tile.
788
+ :type tile_shape: cute.Shape
789
+ :param seqlen_q: Query sequence length for attention computation.
790
+ :type seqlen_q: Int32
791
+ :param seqlen_k: Key sequence length for attention computation.
792
+ :type seqlen_k: Int32
793
+ :param window_size_left: Left-side sliding window size for attention masking.
794
+ :type window_size_left: Optional[Int32]
795
+ :param window_size_right: Right-side sliding window size for attention masking.
796
+ :type window_size_right: Optional[Int32]
797
+
798
+ :return: Number of trips needed.
799
+ :rtype: Int32
800
+ """
801
+ result = 0
802
+ offset = 0
803
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE):
804
+ offset = seqlen_k - seqlen_q
805
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE):
806
+ offset = seqlen_q - seqlen_k
807
+ if cutlass.const_expr(mask_type == Sm100MaskEnum.RESIDUAL_MASK):
808
+ result = cute.ceil_div(seqlen_k, tile_shape[1])
809
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.RESIDUAL_MASK_BWD):
810
+ result = cute.ceil_div(seqlen_q, tile_shape[0])
811
+ if cutlass.const_expr(
812
+ mask_type == Sm100MaskEnum.WINDOW_MASK
813
+ or mask_type == Sm100MaskEnum.WINDOW_MASK_INFERENCE
814
+ ):
815
+ if cutlass.const_expr(window_size_right is None):
816
+ result = cute.ceil_div(seqlen_k, tile_shape[1])
817
+ else:
818
+ max_idx_q = (blk_coord[0] + 1) * tile_shape[0]
819
+ idx_k = max_idx_q + offset + window_size_right
820
+ tmp_blocks_k = cute.ceil_div(idx_k, tile_shape[1])
821
+ max_blocks_k = cute.ceil_div(seqlen_k, tile_shape[1])
822
+ result = dsl_min(max_blocks_k, tmp_blocks_k)
823
+ if cutlass.const_expr(
824
+ mask_type == Sm100MaskEnum.WINDOW_MASK_BWD
825
+ or mask_type == Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE
826
+ ):
827
+ if cutlass.const_expr(window_size_left is None):
828
+ result = cute.ceil_div(seqlen_q, tile_shape[0])
829
+ else:
830
+ max_idx_k = (blk_coord[1] + 1) * tile_shape[1]
831
+ idx_k = max_idx_k + offset + window_size_left
832
+ tmp_blocks_q = cute.ceil_div(idx_k, tile_shape[0])
833
+ max_blocks_q = cute.ceil_div(seqlen_q, tile_shape[0])
834
+ result = dsl_min(max_blocks_q, tmp_blocks_q)
835
+ start_block = Sm100FusedMask.get_trip_start(
836
+ mask_type,
837
+ blk_coord,
838
+ tile_shape,
839
+ seqlen_q,
840
+ seqlen_k,
841
+ window_size_left,
842
+ window_size_right,
843
+ )
844
+ result = result - start_block
845
+ return result
846
+
847
+ @cute.jit
848
+ def get_trip_start_count_via_block_info(
849
+ blk_coord: cute.Coord,
850
+ tile_shape: cute.Shape,
851
+ seqlen_q: Int32,
852
+ seqlen_k: Int32,
853
+ is_causal: cutlass.Constexpr[bool] = False,
854
+ is_local: cutlass.Constexpr[bool] = False,
855
+ window_size_left: Optional[Int32] = None,
856
+ window_size_right: Optional[Int32] = None,
857
+ ) -> Tuple[Int32, Int32]:
858
+ block_info = BlockInfo(
859
+ tile_m=tile_shape[0],
860
+ tile_n=tile_shape[1],
861
+ is_causal=is_causal,
862
+ is_local=is_local and not is_causal,
863
+ window_size_left=window_size_left,
864
+ window_size_right=window_size_right,
865
+ )
866
+
867
+ seqlen_info = SeqlenInfoQK(
868
+ offset_q=Int32(0),
869
+ offset_k=Int32(0),
870
+ padded_offset_q=Int32(0),
871
+ padded_offset_k=Int32(0),
872
+ seqlen_q=seqlen_q,
873
+ seqlen_k=seqlen_k,
874
+ has_cu_seqlens_q=False,
875
+ has_cu_seqlens_k=False,
876
+ has_seqused_q=False,
877
+ has_seqused_k=False,
878
+ )
879
+ n_block_min, n_block_max = block_info.get_n_block_min_max(seqlen_info, blk_coord[0])
880
+ return n_block_min, n_block_max - n_block_min
881
+
882
+ @cute.jit
883
+ def get_trip_mask_bounds_via_block_info(
884
+ blk_coord: cute.Coord,
885
+ tile_shape: cute.Shape,
886
+ seqlen_q: Int32,
887
+ seqlen_k: Int32,
888
+ is_causal: cutlass.Constexpr[bool] = False,
889
+ is_local: cutlass.Constexpr[bool] = False,
890
+ window_size_left: Optional[Int32] = None,
891
+ window_size_right: Optional[Int32] = None,
892
+ ) -> Tuple[Int32, Int32]:
893
+ """Return SM100-style mask boundaries for dense iteration.
894
+
895
+ Returns:
896
+ - n_block_min_causal_local_mask: right-side masked region start
897
+ - n_block_min_before_local_mask: start of fully unmasked middle region
898
+ """
899
+ block_info = BlockInfo(
900
+ tile_m=tile_shape[0],
901
+ tile_n=tile_shape[1],
902
+ is_causal=is_causal,
903
+ is_local=is_local and not is_causal,
904
+ window_size_left=window_size_left,
905
+ window_size_right=window_size_right,
906
+ )
907
+ seqlen_info = SeqlenInfoQK(
908
+ offset_q=Int32(0),
909
+ offset_k=Int32(0),
910
+ padded_offset_q=Int32(0),
911
+ padded_offset_k=Int32(0),
912
+ seqlen_q=seqlen_q,
913
+ seqlen_k=seqlen_k,
914
+ has_cu_seqlens_q=False,
915
+ has_cu_seqlens_k=False,
916
+ has_seqused_q=False,
917
+ has_seqused_k=False,
918
+ )
919
+ n_block_min, _ = block_info.get_n_block_min_max(seqlen_info, blk_coord[0])
920
+ n_block_min_causal_local_mask = block_info.get_n_block_min_causal_local_mask(
921
+ seqlen_info, blk_coord[0], n_block_min
922
+ )
923
+ n_block_min_before_local_mask = block_info.get_n_block_min_before_local_mask(
924
+ seqlen_info, blk_coord[0], n_block_min
925
+ )
926
+ return n_block_min_causal_local_mask, n_block_min_before_local_mask
927
+
928
+ @cute.jit
929
+ def get_trip_start(
930
+ mask_type: Sm100MaskEnum,
931
+ blk_coord: cute.Coord,
932
+ tile_shape: cute.Shape,
933
+ seqlen_q: Int32,
934
+ seqlen_k: Int32,
935
+ window_size_left: Optional[Int32] = None,
936
+ window_size_right: Optional[Int32] = None,
937
+ ) -> Int32:
938
+ """
939
+ Get the start of the trip for the current block.
940
+
941
+ :param mask_type: Type of mask to use
942
+ :type mask_type: utils.Sm100MaskEnum
943
+ :param blk_coord: Block coordinates.
944
+ :type blk_coord: cute.Coord
945
+ :param tile_shape: Shape of the tile.
946
+ :type tile_shape: cute.Shape
947
+ :param seqlen_q: Query sequence length for attention computation.
948
+ :type seqlen_q: Int32
949
+ :param seqlen_k: Key sequence length for attention computation.
950
+ :type seqlen_k: Int32
951
+ :param window_size_left: Left-side sliding window size for attention masking.
952
+ :type window_size_left: Optional[Int32]
953
+ :param window_size_right: Right-side sliding window size for attention masking.
954
+ :type window_size_right: Optional[Int32]
955
+ """
956
+ result = 0
957
+ offset = 0
958
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE):
959
+ offset = seqlen_k - seqlen_q
960
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE):
961
+ offset = seqlen_q - seqlen_k
962
+ if cutlass.const_expr(
963
+ mask_type is Sm100MaskEnum.WINDOW_MASK
964
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE
965
+ ):
966
+ if cutlass.const_expr(window_size_left is not None):
967
+ min_idx_q = blk_coord[0] * tile_shape[0]
968
+ idx_k = min_idx_q + offset - window_size_left
969
+ tmp_blocks_k = idx_k // tile_shape[1]
970
+ result = max(tmp_blocks_k, result)
971
+ if cutlass.const_expr(
972
+ mask_type is Sm100MaskEnum.WINDOW_MASK_BWD
973
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE
974
+ ):
975
+ if cutlass.const_expr(window_size_right is not None):
976
+ min_idx_k = blk_coord[1] * tile_shape[1]
977
+ idx_q = min_idx_k + offset - window_size_right
978
+ tmp_blocks_q = idx_q // tile_shape[0]
979
+ result = max(tmp_blocks_q, result)
980
+ return result
981
+
982
+ @cute.jit
983
+ def get_leading_mask_id(
984
+ mask_type: Sm100MaskEnum,
985
+ blk_coord: cute.Coord,
986
+ tile_shape: cute.Shape,
987
+ seqlen_q: Int32,
988
+ seqlen_k: Int32,
989
+ window_size_left: Optional[Int32] = None,
990
+ window_size_right: Optional[Int32] = None,
991
+ ) -> Tuple[Int32, Int32]:
992
+ """
993
+ Get the begin and end tile idx for the leading mask.
994
+
995
+ :param mask_type: Type of mask to use
996
+ :type mask_type: utils.Sm100MaskEnum
997
+ :param blk_coord: Block coordinates.
998
+ :type blk_coord: cute.Coord
999
+ :param tile_shape: Shape of the tile.
1000
+ :type tile_shape: cute.Shape
1001
+ :param seqlen_q: Query sequence length for attention computation.
1002
+ :type seqlen_q: Int32
1003
+ :param seqlen_k: Key sequence length for attention computation.
1004
+ :type seqlen_k: Int32
1005
+ :param window_size_left: Left-side sliding window size for attention masking.
1006
+ :type window_size_left: Optional[Int32]
1007
+ :param window_size_right: Right-side sliding window size for attention masking.
1008
+ :type window_size_right: Optional[Int32]
1009
+
1010
+ :return: Tuple of (begin, end) tile idx for the leading mask.
1011
+ :rtype: Tuple[Int32, Int32]
1012
+ """
1013
+ offset = 0
1014
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE):
1015
+ offset = seqlen_k - seqlen_q
1016
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE):
1017
+ offset = seqlen_q - seqlen_k
1018
+ leading_mask_begin = Sm100FusedMask.get_trip_start(
1019
+ mask_type,
1020
+ blk_coord,
1021
+ tile_shape,
1022
+ seqlen_q,
1023
+ seqlen_k,
1024
+ window_size_left,
1025
+ window_size_right,
1026
+ )
1027
+ trip_count = Sm100FusedMask.get_trip_count(
1028
+ mask_type,
1029
+ blk_coord,
1030
+ tile_shape,
1031
+ seqlen_q,
1032
+ seqlen_k,
1033
+ window_size_left,
1034
+ window_size_right,
1035
+ )
1036
+
1037
+ leading_mask_end = leading_mask_begin
1038
+ if cutlass.const_expr(
1039
+ mask_type is Sm100MaskEnum.WINDOW_MASK
1040
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE
1041
+ ):
1042
+ if cutlass.const_expr(window_size_left is not None):
1043
+ min_idx_q = (blk_coord[0] + 1) * tile_shape[0] + offset - window_size_left
1044
+ leading_mask_end = dsl_min(
1045
+ cute.ceil_div(min_idx_q, tile_shape[1]) - 1,
1046
+ trip_count + leading_mask_begin - 1,
1047
+ )
1048
+ else:
1049
+ leading_mask_end = leading_mask_begin - 1
1050
+ elif cutlass.const_expr(
1051
+ mask_type is Sm100MaskEnum.WINDOW_MASK_BWD
1052
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE
1053
+ ):
1054
+ if cutlass.const_expr(window_size_right is not None):
1055
+ min_idx_k = (blk_coord[1] + 1) * tile_shape[1] + offset - window_size_right
1056
+ leading_mask_end = cute.ceil_div(min_idx_k, tile_shape[0]) - 1
1057
+ else:
1058
+ leading_mask_end = leading_mask_begin - 1
1059
+ return leading_mask_begin, leading_mask_end
1060
+
1061
+ @cute.jit
1062
+ def get_trailing_mask_id(
1063
+ mask_type: Sm100MaskEnum,
1064
+ blk_coord: cute.Coord,
1065
+ tile_shape: cute.Shape,
1066
+ seqlen_q: Int32,
1067
+ seqlen_k: Int32,
1068
+ window_size_left: Optional[Int32] = None,
1069
+ window_size_right: Optional[Int32] = None,
1070
+ ) -> Tuple[Optional[Int32], Optional[Int32]]:
1071
+ """
1072
+ Get the begin and end tile idx for the trailing mask.
1073
+
1074
+ :param mask_type: Type of mask to use
1075
+ :type mask_type: utils.Sm100MaskEnum
1076
+ :param blk_coord: Block coordinates.
1077
+ :type blk_coord: cute.Coord
1078
+ :param tile_shape: Shape of the tile.
1079
+ :type tile_shape: cute.Shape
1080
+ :param seqlen_q: Query sequence length for attention computation.
1081
+ :type seqlen_q: Int32
1082
+ :param seqlen_k: Key sequence length for attention computation.
1083
+ :type seqlen_k: Int32
1084
+ :param window_size_left: Left-side sliding window size for attention masking.
1085
+ :type window_size_left: Optional[Int32]
1086
+ :param window_size_right: Right-side sliding window size for attention masking.
1087
+ :type window_size_right: Optional[Int32]
1088
+
1089
+ :return: Tuple of (begin, end) tile idx for the trailing mask.
1090
+ :rtype: Tuple[Int32, Int32]
1091
+ """
1092
+ offset = 0
1093
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE):
1094
+ offset = seqlen_k - seqlen_q
1095
+ if cutlass.const_expr(mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE):
1096
+ offset = seqlen_q - seqlen_k
1097
+ trip_start = Sm100FusedMask.get_trip_start(
1098
+ mask_type,
1099
+ blk_coord,
1100
+ tile_shape,
1101
+ seqlen_q,
1102
+ seqlen_k,
1103
+ window_size_left,
1104
+ window_size_right,
1105
+ )
1106
+ trip_count = Sm100FusedMask.get_trip_count(
1107
+ mask_type,
1108
+ blk_coord,
1109
+ tile_shape,
1110
+ seqlen_q,
1111
+ seqlen_k,
1112
+ window_size_left,
1113
+ window_size_right,
1114
+ )
1115
+
1116
+ trailing_mask_begin, trailing_mask_end = None, None
1117
+ if cutlass.const_expr(
1118
+ mask_type is Sm100MaskEnum.WINDOW_MASK
1119
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE
1120
+ ):
1121
+ if cutlass.const_expr(window_size_right is not None):
1122
+ min_idx_q = blk_coord[0] * tile_shape[0] + offset + window_size_right
1123
+ trailing_mask_begin = dsl_min(
1124
+ min_idx_q // tile_shape[1], trip_count + trip_start - 1
1125
+ )
1126
+ trailing_mask_end = trip_count + trip_start - 1
1127
+ else:
1128
+ # last tile, we always apply mask on it regardless whether it's a residual tile
1129
+ trailing_mask_begin = trip_count + trip_start - 1
1130
+ trailing_mask_end = trip_count + trip_start - 1
1131
+ else:
1132
+ if cutlass.const_expr(window_size_left is not None):
1133
+ min_idx_k = blk_coord[1] * tile_shape[1] + offset + window_size_left + 1
1134
+ max_idx_k = (blk_coord[1] + 1) * tile_shape[1] + offset + window_size_left
1135
+ trailing_mask_begin = dsl_min(
1136
+ cute.ceil_div(min_idx_k, tile_shape[0]) - 1,
1137
+ trip_count + trip_start - 1,
1138
+ )
1139
+ trailing_mask_end = dsl_min(
1140
+ cute.ceil_div(max_idx_k, tile_shape[0]) - 1,
1141
+ trip_count + trip_start - 1,
1142
+ )
1143
+ else:
1144
+ # last tile, we always apply mask on it regardless whether it's a residual tile
1145
+ trailing_mask_begin = trip_count + trip_start - 1
1146
+ trailing_mask_end = trip_count + trip_start - 1
1147
+
1148
+ return trailing_mask_begin, trailing_mask_end
1149
+
1150
+ @cute.jit
1151
+ def get_masked_leading_count(
1152
+ mask_type: Sm100MaskEnum,
1153
+ blk_coord: cute.Coord,
1154
+ tile_shape: cute.Shape,
1155
+ seqlen_q: Int32,
1156
+ seqlen_k: Int32,
1157
+ window_size_left: Optional[Int32] = None,
1158
+ window_size_right: Optional[Int32] = None,
1159
+ ) -> Int32:
1160
+ """
1161
+ Calculate the number of masked trips for the leading mask.
1162
+
1163
+ This is used for blocks that need special handling due to masking.
1164
+
1165
+ :param mask_type: Type of mask to use
1166
+ :type mask_type: utils.Sm100MaskEnum
1167
+ :param blk_coord: Block coordinates.
1168
+ :type blk_coord: cute.Coord
1169
+ :param tile_shape: Shape of the tile.
1170
+ :type tile_shape: cute.Shape
1171
+ :param seqlen_q: Query sequence length for attention computation.
1172
+ :type seqlen_q: Int32
1173
+ :param seqlen_k: Key sequence length for attention computation.
1174
+ :type seqlen_k: Int32
1175
+ :param window_size_left: Left-side sliding window size for attention masking.
1176
+ :type window_size_left: Optional[Int32]
1177
+ :param window_size_right: Right-side sliding window size for attention masking.
1178
+ :type window_size_right: Optional[Int32]
1179
+
1180
+ :return: Number of masked trips.
1181
+ :rtype: Int32
1182
+ """
1183
+ result = 0
1184
+ if cutlass.const_expr(
1185
+ mask_type is not Sm100MaskEnum.RESIDUAL_MASK
1186
+ and mask_type is not Sm100MaskEnum.RESIDUAL_MASK_BWD
1187
+ ):
1188
+ if cutlass.const_expr(window_size_left is not None or window_size_right is not None):
1189
+ leading_mask_begin, leading_mask_end = Sm100FusedMask.get_leading_mask_id(
1190
+ mask_type,
1191
+ blk_coord,
1192
+ tile_shape,
1193
+ seqlen_q,
1194
+ seqlen_k,
1195
+ window_size_left,
1196
+ window_size_right,
1197
+ )
1198
+ result = max(leading_mask_end - leading_mask_begin + 1, 0)
1199
+
1200
+ return result
1201
+
1202
+ @cute.jit
1203
+ def get_masked_trailing_count(
1204
+ mask_type: Sm100MaskEnum,
1205
+ blk_coord: cute.Coord,
1206
+ tile_shape: cute.Shape,
1207
+ seqlen_q: Int32,
1208
+ seqlen_k: Int32,
1209
+ window_size_left: Optional[Int32] = None,
1210
+ window_size_right: Optional[Int32] = None,
1211
+ rem_count: Optional[Int32] = 0,
1212
+ ) -> Int32:
1213
+ """
1214
+ Calculate the number of masked trips for the trailing mask.
1215
+
1216
+ This is used for blocks that need special handling due to masking.
1217
+
1218
+ :param mask_type: Type of mask to use
1219
+ :type mask_type: utils.Sm100MaskEnum
1220
+ :param blk_coord: Block coordinates.
1221
+ :type blk_coord: cute.Coord
1222
+ :param tile_shape: Shape of the tile.
1223
+ :type tile_shape: cute.Shape
1224
+ :param seqlen_q: Query sequence length for attention computation.
1225
+ :type seqlen_q: Int32
1226
+ :param seqlen_k: Key sequence length for attention computation.
1227
+ :type seqlen_k: Int32
1228
+ :param window_size_left: Left-side sliding window size for attention masking.
1229
+ :type window_size_left: Optional[Int32]
1230
+ :param window_size_right: Right-side sliding window size for attention masking.
1231
+ :type window_size_right: Optional[Int32]
1232
+ :param rem_count: Remaining count from previous calculations.
1233
+ :type rem_count: Int32
1234
+
1235
+ :return: Number of masked trips.
1236
+ :rtype: Int32
1237
+ """
1238
+ result = 0
1239
+
1240
+ if cutlass.const_expr(
1241
+ mask_type is not Sm100MaskEnum.RESIDUAL_MASK
1242
+ and mask_type is not Sm100MaskEnum.RESIDUAL_MASK_BWD
1243
+ ):
1244
+ if cutlass.const_expr(window_size_left is not None or window_size_right is not None):
1245
+ trailing_mask_begin, trailing_mask_end = Sm100FusedMask.get_trailing_mask_id(
1246
+ mask_type,
1247
+ blk_coord,
1248
+ tile_shape,
1249
+ seqlen_q,
1250
+ seqlen_k,
1251
+ window_size_left,
1252
+ window_size_right,
1253
+ )
1254
+ leading_mask_begin, leading_mask_end = Sm100FusedMask.get_leading_mask_id(
1255
+ mask_type,
1256
+ blk_coord,
1257
+ tile_shape,
1258
+ seqlen_q,
1259
+ seqlen_k,
1260
+ window_size_left,
1261
+ window_size_right,
1262
+ )
1263
+ if cutlass.const_expr(
1264
+ trailing_mask_begin is not None and trailing_mask_end is not None
1265
+ ):
1266
+ if trailing_mask_begin <= leading_mask_end:
1267
+ result = max(trailing_mask_end - leading_mask_end, 0)
1268
+ else:
1269
+ result = max(trailing_mask_end - trailing_mask_begin + 1, 0)
1270
+ else:
1271
+ if seqlen_k % tile_shape[1] != 0:
1272
+ result = 1
1273
+ else:
1274
+ result = 0
1275
+
1276
+ return result + rem_count
1277
+
1278
+ @cute.jit
1279
+ def get_unmasked_trip_count(
1280
+ mask_type: Sm100MaskEnum,
1281
+ blk_coord: cute.Coord,
1282
+ tile_shape: cute.Shape,
1283
+ seqlen_q: Int32,
1284
+ seqlen_k: Int32,
1285
+ window_size_left: Optional[Int32] = None,
1286
+ window_size_right: Optional[Int32] = None,
1287
+ ) -> Int32:
1288
+ """
1289
+ Calculate the number of unmasked trips for the current block.
1290
+
1291
+ This represents the number of trips that don't require special
1292
+ masking treatment.
1293
+
1294
+ :param mask_type: Type of mask to use
1295
+ :type mask_type: utils.Sm100MaskEnum
1296
+ :param blk_coord: Block coordinates.
1297
+ :type blk_coord: cute.Coord
1298
+ :param tile_shape: Shape of the tile.
1299
+ :type tile_shape: cute.Shape
1300
+ :param seqlen_q: Query sequence length for attention computation.
1301
+ :type seqlen_q: Int32
1302
+ :param seqlen_k: Key sequence length for attention computation.
1303
+ :type seqlen_k: Int32
1304
+ :param window_size_left: Left-side sliding window size for attention masking.
1305
+ :type window_size_left: Optional[Int32]
1306
+ :param window_size_right: Right-side sliding window size for attention masking.
1307
+ :type window_size_right: Optional[Int32]
1308
+
1309
+ :return: Number of unmasked trips.
1310
+ :rtype: Int32
1311
+ """
1312
+ result = (
1313
+ Sm100FusedMask.get_trip_count(
1314
+ mask_type,
1315
+ blk_coord,
1316
+ tile_shape,
1317
+ seqlen_q,
1318
+ seqlen_k,
1319
+ window_size_left,
1320
+ window_size_right,
1321
+ )
1322
+ - Sm100FusedMask.get_masked_leading_count(
1323
+ mask_type,
1324
+ blk_coord,
1325
+ tile_shape,
1326
+ seqlen_q,
1327
+ seqlen_k,
1328
+ window_size_left,
1329
+ window_size_right,
1330
+ )
1331
+ - Sm100FusedMask.get_masked_trailing_count(
1332
+ mask_type,
1333
+ blk_coord,
1334
+ tile_shape,
1335
+ seqlen_q,
1336
+ seqlen_k,
1337
+ window_size_left,
1338
+ window_size_right,
1339
+ 0,
1340
+ )
1341
+ )
1342
+ return result
1343
+
1344
+ @cute.jit
1345
+ def apply_mask(
1346
+ mask_type: Sm100MaskEnum,
1347
+ acc_qk: cute.Tensor,
1348
+ index_qk: cute.Tensor,
1349
+ seqlen_q: Int32,
1350
+ seqlen_k: Int32,
1351
+ window_size_left: Optional[int] = None,
1352
+ window_size_right: Optional[int] = None,
1353
+ index_transform: cutlass.Constexpr = lambda index_q, index_k: (
1354
+ index_q,
1355
+ index_k,
1356
+ ),
1357
+ ):
1358
+ """
1359
+ Apply the appropriate mask to the attention scores.
1360
+
1361
+ This method modifies the attention scores (acc_qk) based on the mask type
1362
+ and the positions in the index tensor.
1363
+
1364
+ :param mask_type: Type of mask to use
1365
+ :type mask_type: utils.Sm100MaskEnum
1366
+ :param acc_qk: Accumulated QK attention scores tensor.
1367
+ :type acc_qk: cute.Tensor
1368
+ :param index_qk: Index tensor containing position information.
1369
+ :type index_qk: cute.Tensor
1370
+ :param seqlen_k: Key sequence length for attention computation.
1371
+ :type seqlen_k: Int32
1372
+ :param seqlen_q: Query sequence length for attention computation.
1373
+ :type seqlen_q: Optional[int]
1374
+ :param window_size_left: Left-side sliding window size for attention masking.
1375
+ :type window_size_left: Optional[int]
1376
+ :param window_size_right: Right-side sliding window size for attention masking.
1377
+ :type window_size_right: Optional[int]
1378
+ """
1379
+ offset = 0
1380
+ # NOTE: causal masking in this repo aligns the *end* of Q with the *end* of K
1381
+ # when seqlen_k != seqlen_q (same as the test/reference implementation):
1382
+ # k_index <= q_index + (seqlen_k - seqlen_q) + window_right
1383
+ # In our kernels, causal is represented by (window_left is None, window_right is not None).
1384
+ if cutlass.const_expr(window_size_left is None and window_size_right is not None):
1385
+ offset = seqlen_k - seqlen_q
1386
+ elif cutlass.const_expr(
1387
+ mask_type is Sm100MaskEnum.WINDOW_MASK_INFERENCE
1388
+ or mask_type is Sm100MaskEnum.WINDOW_MASK_BWD_INFERENCE
1389
+ ):
1390
+ offset = seqlen_k - seqlen_q
1391
+ for i in cutlass.range_constexpr(cute.size(acc_qk), unroll_full=True):
1392
+ index_q, index_k = index_transform(*index_qk[i])
1393
+ if cutlass.const_expr(window_size_left is not None or window_size_right is not None):
1394
+ if cutlass.const_expr(window_size_left is None):
1395
+ if index_q + offset + window_size_right < index_k:
1396
+ acc_qk[i] = -Float32.inf
1397
+ if index_k >= seqlen_k or index_q >= seqlen_q: # residual mask
1398
+ acc_qk[i] = -Float32.inf
1399
+ elif cutlass.const_expr(window_size_right is None):
1400
+ if index_q + offset - window_size_left > index_k:
1401
+ acc_qk[i] = -Float32.inf
1402
+ if index_k >= seqlen_k or index_q >= seqlen_q: # residual mask
1403
+ acc_qk[i] = -Float32.inf
1404
+ else:
1405
+ max_K_index = dsl_min(index_q + offset + window_size_right, seqlen_k)
1406
+ min_K_index = max(0, index_q + offset - window_size_left)
1407
+ if index_k > max_K_index or index_k < min_K_index:
1408
+ acc_qk[i] = -Float32.inf
1409
+ if index_k >= seqlen_k or index_q >= seqlen_q: # residual mask
1410
+ acc_qk[i] = -Float32.inf
1411
+
1412
+ if cutlass.const_expr(
1413
+ mask_type == Sm100MaskEnum.RESIDUAL_MASK
1414
+ or mask_type == Sm100MaskEnum.RESIDUAL_MASK_BWD
1415
+ ):
1416
+ if index_k >= seqlen_k or index_q >= seqlen_q:
1417
+ acc_qk[i] = -Float32.inf
1418
+
1419
+ @cute.jit
1420
+ def apply_mask_via_causal_local(
1421
+ acc_qk: cute.Tensor,
1422
+ index_qk: cute.Tensor,
1423
+ seqlen_q: Int32,
1424
+ seqlen_k: Int32,
1425
+ apply_semantic_window: cutlass.Constexpr[bool] = True,
1426
+ is_causal: cutlass.Constexpr[bool] = False,
1427
+ is_local: cutlass.Constexpr[bool] = False,
1428
+ window_size_left: Optional[int] = None,
1429
+ window_size_right: Optional[int] = None,
1430
+ index_transform: cutlass.Constexpr = lambda index_q, index_k: (
1431
+ index_q,
1432
+ index_k,
1433
+ ),
1434
+ ):
1435
+ """Apply forward mask without mask_type.
1436
+
1437
+ - If apply_semantic_window=True, apply causal/local window constraints.
1438
+ - Always apply residual OOB masking (index_k>=seqlen_k or index_q>=seqlen_q).
1439
+ """
1440
+ offset = 0
1441
+ if cutlass.const_expr(apply_semantic_window):
1442
+ # Match WINDOW_MASK_INFERENCE semantics: end-align Q/K when lengths differ.
1443
+ offset = seqlen_k - seqlen_q
1444
+ for i in cutlass.range_constexpr(cute.size(acc_qk), unroll_full=True):
1445
+ index_q, index_k = index_transform(*index_qk[i])
1446
+ if cutlass.const_expr(apply_semantic_window):
1447
+ if cutlass.const_expr(is_causal and not is_local):
1448
+ # Pure causal; tolerate both external forms:
1449
+ # - (None, None) from interface
1450
+ # - (None, 0) from fused-mask-style callers
1451
+ right = 0 if const_expr(window_size_right is None) else window_size_right
1452
+ if index_q + offset + right < index_k:
1453
+ acc_qk[i] = -Float32.inf
1454
+ elif cutlass.const_expr(
1455
+ is_local or window_size_left is not None or window_size_right is not None
1456
+ ):
1457
+ if cutlass.const_expr(window_size_left is None):
1458
+ if index_q + offset + window_size_right < index_k:
1459
+ acc_qk[i] = -Float32.inf
1460
+ elif cutlass.const_expr(window_size_right is None):
1461
+ if index_q + offset - window_size_left > index_k:
1462
+ acc_qk[i] = -Float32.inf
1463
+ else:
1464
+ max_K_index = dsl_min(index_q + offset + window_size_right, seqlen_k)
1465
+ min_K_index = max(0, index_q + offset - window_size_left)
1466
+ if index_k > max_K_index or index_k < min_K_index:
1467
+ acc_qk[i] = -Float32.inf
1468
+ # Residual mask is always needed for boundary protection.
1469
+ if index_k >= seqlen_k or index_q >= seqlen_q:
1470
+ acc_qk[i] = -Float32.inf
build/torch-cuda/flashrt_fa4/cute/mma_sm100_desc.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ # Ported Cutlass code from C++ to Python:
3
+ # https://github.com/NVIDIA/cutlass/blob/main/include/cute/arch/mma_sm100_desc.hpp
4
+ # https://github.com/NVIDIA/cutlass/blob/main/include/cute/atom/mma_traits_sm100.hpp
5
+
6
+ from enum import IntEnum
7
+
8
+ import cutlass
9
+ import cutlass.cute as cute
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Enumerations that match the HW encodings (values MUST stay identical)
13
+ # ---------------------------------------------------------------------------
14
+
15
+
16
+ class Major(IntEnum): # matrix “layout” in the ISA docs
17
+ K = 0
18
+ MN = 1
19
+
20
+
21
+ class ScaleIn(IntEnum): # negate flags
22
+ One = 0
23
+ Neg = 1
24
+
25
+
26
+ class Saturate(IntEnum):
27
+ False_ = 0
28
+ True_ = 1
29
+
30
+
31
+ class CFormat(IntEnum): # 2-bit field (bits 4-5)
32
+ F16 = 0
33
+ F32 = 1
34
+ S32 = 2
35
+
36
+
37
+ class F16F32Format(IntEnum): # 3-bit field (A/B element type)
38
+ F16 = 0
39
+ BF16 = 1
40
+ TF32 = 2
41
+
42
+
43
+ class S8Format(IntEnum):
44
+ UINT8 = 0
45
+ INT8 = 1
46
+
47
+
48
+ class MXF8F6F4Format(IntEnum):
49
+ E4M3 = 0
50
+ E5M2 = 1
51
+ E2M3 = 3
52
+ E3M2 = 4
53
+ E2M1 = 5
54
+
55
+
56
+ class MaxShift(IntEnum):
57
+ NoShift = 0
58
+ MaxShift8 = 1
59
+ MaxShift16 = 2
60
+ MaxShift32 = 3
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # CUTLASS-type → encoding helpers
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ def to_UMMA_format(cutlass_type) -> int:
69
+ """
70
+ Map a CUTLASS scalar class to the 3-bit encoding for Matrix A/B.
71
+ """
72
+ if cutlass_type is cutlass.Int8:
73
+ return S8Format.INT8
74
+ # Unsigned 8-bit (if available in your CUTLASS build)
75
+ if cutlass_type is cutlass.Uint8:
76
+ return S8Format.UINT8
77
+ # FP-16 / BF-16
78
+ if cutlass_type is cutlass.Float16:
79
+ return F16F32Format.F16
80
+ if cutlass_type is cutlass.BFloat16:
81
+ return F16F32Format.BF16
82
+ # TensorFloat-32 (8-bit exponent, 10-bit mantissa packed in 19 bits)
83
+ if cutlass_type is cutlass.TFloat32:
84
+ return F16F32Format.TF32
85
+ # Float-8 / Float-6 / Float-4 – add whenever CUTLASS exposes them
86
+ if cutlass_type is cutlass.Float8E4M3FN:
87
+ return MXF8F6F4Format.E4M3
88
+ if cutlass_type is cutlass.Float8E5M2:
89
+ return MXF8F6F4Format.E5M2
90
+ raise TypeError(f"Unsupported CUTLASS scalar type for A/B: {cutlass_type!r}")
91
+
92
+
93
+ def to_C_format(cutlass_type) -> int:
94
+ """
95
+ Map a CUTLASS scalar class to the 2-bit accumulator encoding.
96
+ """
97
+ if cutlass_type is cutlass.Float16:
98
+ return CFormat.F16
99
+ if cutlass_type is cutlass.Float32:
100
+ return CFormat.F32
101
+ if cutlass_type is cutlass.Int32:
102
+ return CFormat.S32
103
+ raise TypeError(f"Unsupported CUTLASS scalar type for accumulator: {cutlass_type!r}")
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # The constructor – accepts only CUTLASS scalar classes
108
+ # ---------------------------------------------------------------------------
109
+
110
+
111
+ def make_instr_desc(
112
+ a_type, # CUTLASS scalar class, e.g. cutlass.Int8
113
+ b_type,
114
+ c_type,
115
+ M: int, # 64, 128 or 256
116
+ N: int, # 8 … 256 (multiple of 8)
117
+ a_major: Major,
118
+ b_major: Major,
119
+ a_neg: ScaleIn = ScaleIn.One,
120
+ b_neg: ScaleIn = ScaleIn.One,
121
+ c_sat: Saturate = Saturate.False_,
122
+ is_sparse: bool = False,
123
+ max_shift: MaxShift = MaxShift.NoShift,
124
+ ) -> int:
125
+ """
126
+ Build the 32-bit instruction descriptor for Blackwell MMA.
127
+ All matrix/accumulator **types must be CUTLASS scalar classes** –
128
+ passing integers is forbidden.
129
+ """
130
+ # --- encode element formats -------------------------------------------------
131
+ a_fmt = int(to_UMMA_format(a_type))
132
+ b_fmt = int(to_UMMA_format(b_type))
133
+ c_fmt = int(to_C_format(c_type))
134
+
135
+ # --- range checks on M/N -----------------------------------------------------
136
+ if M not in (64, 128, 256):
137
+ raise ValueError("M must be 64, 128 or 256")
138
+ if N < 8 or N > 256 or (N & 7):
139
+ raise ValueError("N must be a multiple of 8 in the range 8…256")
140
+
141
+ m_dim = M >> 4 # 5-bit field
142
+ n_dim = N >> 3 # 6-bit field
143
+
144
+ # fmt: off
145
+ # --- pack the bit-fields -----------------------------------------------------
146
+ desc = 0
147
+ desc |= (0 & 0x3) << 0 # sparse_id2 (always 0 here)
148
+ desc |= (int(is_sparse) & 0x1) << 2 # sparse_flag
149
+ desc |= (int(c_sat) & 0x1) << 3 # saturate
150
+ desc |= (c_fmt & 0x3) << 4 # c_format
151
+ desc |= (a_fmt & 0x7) << 7 # a_format
152
+ desc |= (b_fmt & 0x7) << 10 # b_format
153
+ desc |= (int(a_neg) & 0x1) << 13 # a_negate
154
+ desc |= (int(b_neg) & 0x1) << 14 # b_negate
155
+ desc |= (int(a_major) & 0x1) << 15 # a_major
156
+ desc |= (int(b_major) & 0x1) << 16 # b_major
157
+ desc |= (n_dim & 0x3F) << 17 # n_dim (6 bits)
158
+ desc |= (m_dim & 0x1F) << 24 # m_dim (5 bits)
159
+ desc |= (int(max_shift) & 0x3) << 30 # max_shift (2 bits)
160
+ # fmt: on
161
+
162
+ return desc & 0xFFFF_FFFF # ensure 32-bit result
163
+
164
+
165
+ def mma_op_to_idesc(op: cute.nvgpu.tcgen05.mma.MmaOp):
166
+ return make_instr_desc(
167
+ op.a_dtype,
168
+ op.b_dtype,
169
+ op.acc_dtype,
170
+ op.shape_mnk[0],
171
+ op.shape_mnk[1],
172
+ Major.K if op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K else Major.MN,
173
+ Major.K if op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K else Major.MN,
174
+ )
175
+
176
+
177
+ class LayoutType(IntEnum): # occupies the top-3 bits [61:64)
178
+ SWIZZLE_NONE = 0 # (a.k.a. “INTERLEAVE” in older docs)
179
+ SWIZZLE_128B_BASE32B = 1
180
+ SWIZZLE_128B = 2
181
+ SWIZZLE_64B = 4
182
+ SWIZZLE_32B = 6
183
+ # values 3,5,7 are reserved / illegal for UMMA
184
+
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Helpers – figure out the SWIZZLE_* family from the tensor layout
188
+ # ---------------------------------------------------------------------------
189
+
190
+
191
+ def _layout_type(swizzle: cute.Swizzle) -> LayoutType:
192
+ B, M, S = swizzle.num_bits, swizzle.num_base, swizzle.num_shift
193
+
194
+ if M == 4: # Swizzle<*,4,3>
195
+ if S != 3:
196
+ raise ValueError("Unexpected swizzle shift – want S==3 for M==4")
197
+ return {
198
+ 0: LayoutType.SWIZZLE_NONE,
199
+ 1: LayoutType.SWIZZLE_32B,
200
+ 2: LayoutType.SWIZZLE_64B,
201
+ 3: LayoutType.SWIZZLE_128B,
202
+ }[B] # KeyError ⇒ invalid B→ raise
203
+ if M == 5: # Swizzle<2,5,2> (the only legal triple for M==5)
204
+ if (B, S) != (2, 2):
205
+ raise ValueError("Only Swizzle<2,5,2> supported for 128B_BASE32B")
206
+ return LayoutType.SWIZZLE_128B_BASE32B
207
+
208
+ # Any other (M,B,S) triple is not a UMMA-legal shared-memory layout
209
+ raise ValueError("Unsupported swizzle triple for UMMA smem descriptor")
210
+
211
+
212
+ def make_smem_desc_base(layout: cute.Layout, swizzle: cute.Swizzle, major: Major) -> int:
213
+ """
214
+ Convert a 2-D *shared-memory* Cute layout into the Blackwell 64-bit
215
+ smem-descriptor, without the smem start address.
216
+ layout must correspond to layout of an uint128 tensor.
217
+ """
218
+ # ------------------------------------------------------------------ meta
219
+ layout_type = _layout_type(swizzle) # resolve SWIZZLE_* family
220
+
221
+ VERSION = 1 # bits 46–47
222
+ LBO_MODE = 0 # bit 52
223
+ BASE_OFFSET = 0 # bits 49–51 (CUTLASS always 0)
224
+
225
+ # ---------------------------------------------------------- strides (units: uint128_t = 16 B)
226
+ swizzle_atom_mn_size = {
227
+ LayoutType.SWIZZLE_NONE: 1,
228
+ LayoutType.SWIZZLE_32B: 2,
229
+ LayoutType.SWIZZLE_64B: 4,
230
+ LayoutType.SWIZZLE_128B: 8,
231
+ LayoutType.SWIZZLE_128B_BASE32B: 8,
232
+ }[layout_type]
233
+
234
+ if major is Major.MN:
235
+ swizzle_atom_k_size = 4 if layout_type is LayoutType.SWIZZLE_128B_BASE32B else 8
236
+ canonical_layout = cute.logical_divide(layout, (swizzle_atom_mn_size, swizzle_atom_k_size))
237
+ if not cute.is_congruent(canonical_layout, ((1, 1), (1, 1))):
238
+ raise ValueError("Not a canonical UMMA_MN Layout: Expected profile failure.")
239
+ stride_00 = canonical_layout.stride[0][0]
240
+ if layout_type is not LayoutType.SWIZZLE_NONE and stride_00 != 1:
241
+ raise ValueError("Not a canonical UMMA_MN Layout: Expected stride failure.")
242
+ stride_10 = canonical_layout.stride[1][0]
243
+ if stride_10 != swizzle_atom_mn_size:
244
+ raise ValueError("Not a canonical UMMA_MN Layout: Expected stride failure.")
245
+ stride_01, stride_11 = canonical_layout.stride[0][1], canonical_layout.stride[1][1]
246
+ if layout_type is LayoutType.SWIZZLE_NONE:
247
+ stride_byte_offset, leading_byte_offset = stride_01, stride_11
248
+ else:
249
+ stride_byte_offset, leading_byte_offset = stride_11, stride_01
250
+ else:
251
+ if layout_type == LayoutType.SWIZZLE_128B_BASE32B:
252
+ raise ValueError("SWIZZLE_128B_BASE32B is invalid for Major-K")
253
+ if not cute.size(layout.shape[0]) % 8 == 0:
254
+ raise ValueError("Not a canonical UMMA_K Layout: Expected MN-size multiple of 8.")
255
+ canonical_layout = cute.logical_divide(layout, (8, 2))
256
+ if not cute.is_congruent(canonical_layout, ((1, 1), (1, 1))):
257
+ raise ValueError("Not a canonical UMMA_K Layout: Expected profile failure.")
258
+ stride_00 = canonical_layout.stride[0][0]
259
+ if stride_00 != swizzle_atom_mn_size:
260
+ raise ValueError("Not a canonical UMMA_K Layout: Expected stride failure.")
261
+ stride_10 = canonical_layout.stride[1][0]
262
+ if layout_type is not LayoutType.SWIZZLE_NONE and stride_10 != 1:
263
+ raise ValueError("Not a canonical UMMA_K Layout: Expected stride failure.")
264
+ stride_01 = canonical_layout.stride[0][1]
265
+ stride_byte_offset, leading_byte_offset = stride_01, stride_10
266
+
267
+ # ------------------------------------------------------------------ pack
268
+ desc = 0
269
+ # leading_byte_offset_ [16:30)
270
+ desc |= (leading_byte_offset & 0x3FFF) << 16
271
+ # stride_byte_offset_ [32:46)
272
+ desc |= (stride_byte_offset & 0x3FFF) << 32
273
+ # version_ [46:48)
274
+ desc |= (VERSION & 0x3) << 46
275
+ # base_offset_ [49:52)
276
+ desc |= (BASE_OFFSET & 0x7) << 49
277
+ # lbo_mode_ [52:53)
278
+ desc |= (LBO_MODE & 0x1) << 52
279
+ # layout_type_ [61:64)
280
+ desc |= (int(layout_type) & 0x7) << 61
281
+
282
+ return desc & 0xFFFF_FFFF_FFFF_FFFF # force 64-bit width
283
+
284
+
285
+ def make_smem_desc_start_addr(start_addr: cute.Pointer) -> cutlass.Int32:
286
+ # 14 bits, remove 4 LSB (bits 0-13 in desc)
287
+ return (start_addr.toint() & 0x3FFFF) >> 4
288
+
289
+
290
+ def smem_desc_base_from_tensor(sA: cute.Tensor, major: Major) -> int:
291
+ sA_swizzle = sA.iterator.type.swizzle_type
292
+ return make_smem_desc_base(
293
+ cute.recast_layout(128, sA.element_type.width, sA.layout[0]),
294
+ sA_swizzle,
295
+ major,
296
+ )
build/torch-cuda/flashrt_fa4/cute/named_barrier.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+
3
+ import enum
4
+
5
+
6
+ class NamedBarrierFwd(enum.IntEnum):
7
+ Epilogue = enum.auto() # starts from 1 as barrier 0 is reserved for sync_threads()
8
+ WarpSchedulerWG1 = enum.auto()
9
+ WarpSchedulerWG2 = enum.auto()
10
+ WarpSchedulerWG3 = enum.auto()
11
+ PFull = enum.auto()
12
+ PEmpty = enum.auto()
13
+
14
+
15
+ class NamedBarrierFwdSm100(enum.IntEnum):
16
+ Epilogue = enum.auto() # starts from 1 as barrier 0 is reserved for sync_threads()
17
+ TmemPtr = enum.auto()
18
+ SoftmaxStatsW0 = enum.auto()
19
+ SoftmaxStatsW1 = enum.auto()
20
+ SoftmaxStatsW2 = enum.auto()
21
+ SoftmaxStatsW3 = enum.auto()
22
+ SoftmaxStatsW4 = enum.auto()
23
+ SoftmaxStatsW5 = enum.auto()
24
+ SoftmaxStatsW6 = enum.auto()
25
+ SoftmaxStatsW7 = enum.auto()
26
+
27
+
28
+ class NamedBarrierBwd(enum.IntEnum):
29
+ Epilogue = enum.auto()
30
+ WarpSchedulerWG1 = enum.auto()
31
+ WarpSchedulerWG2 = enum.auto()
32
+ WarpSchedulerWG3 = enum.auto()
33
+ PdS = enum.auto()
34
+ dQFullWG0 = enum.auto()
35
+ dQFullWG1 = enum.auto()
36
+ dQFullWG2 = enum.auto()
37
+ dQEmptyWG0 = enum.auto()
38
+ dQEmptyWG1 = enum.auto()
39
+ dQEmptyWG2 = enum.auto()
40
+
41
+
42
+ class NamedBarrierBwdSm100(enum.IntEnum):
43
+ EpilogueWG1 = enum.auto()
44
+ EpilogueWG2 = enum.auto()
45
+ Compute = enum.auto()
46
+ dQaccReduce = enum.auto()
47
+ TmemPtr = enum.auto()
48
+
49
+
50
+ class NamedBarrierFwdSm100_MLA2CTA(enum.IntEnum):
51
+ Epilogue = enum.auto()
52
+ TmemPtr = enum.auto()
53
+ Cpasync = enum.auto()
54
+ Softmax = enum.auto()
55
+ SoftmaxStatsFull = enum.auto()
56
+ SoftmaxStatsEmpty = enum.auto()
build/torch-cuda/flashrt_fa4/cute/pack_gqa.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Union, Tuple
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+ from cutlass.cute.nvgpu import cpasync
9
+
10
+
11
+ from fa4_cute_runtime.quack import layout_utils
12
+ import fa4_cute_runtime.flashrt_fa4.cute.utils as utils
13
+
14
+
15
+ def pack_gqa_layout(T, qhead_per_kvhead, nheads_kv, head_idx):
16
+ """Reshape a tensor to fold qhead_per_kvhead into the seqlen dimension (mode 0).
17
+
18
+ The head dimension is at mode ``head_idx``. Modes before it (1..head_idx-1)
19
+ are kept as-is (e.g. headdim for Q/O tensors), and modes after it are kept
20
+ as-is (e.g. batch).
21
+
22
+ For Q/O tensors (head_idx=2):
23
+ (seqlen_q, headdim, nheads, batch, ...) -> ((qhead_per_kvhead, seqlen_q), headdim, nheads_kv, batch, ...)
24
+ For LSE tensors (head_idx=1):
25
+ (seqlen_q, nheads, batch, ...) -> ((qhead_per_kvhead, seqlen_q), nheads_kv, batch, ...)
26
+ """
27
+ head_stride = T.stride[head_idx]
28
+ shape_packed = (
29
+ (qhead_per_kvhead, T.shape[0]),
30
+ *[T.shape[i] for i in range(1, head_idx)],
31
+ nheads_kv,
32
+ *[T.shape[i] for i in range(head_idx + 1, len(T.shape))],
33
+ )
34
+ stride_packed = (
35
+ (head_stride, T.stride[0]),
36
+ *[T.stride[i] for i in range(1, head_idx)],
37
+ head_stride * qhead_per_kvhead,
38
+ *[T.stride[i] for i in range(head_idx + 1, len(T.shape))],
39
+ )
40
+ return cute.make_tensor(T.iterator, cute.make_layout(shape_packed, stride=stride_packed))
41
+
42
+
43
+ def make_packgqa_tiled_tma_atom(
44
+ op: cute.atom.CopyOp,
45
+ gmem_tensor: cute.Tensor,
46
+ smem_layout: Union[cute.Layout, cute.ComposedLayout],
47
+ cta_tiler: Tuple[int, int],
48
+ qhead_per_kvhead: int,
49
+ head_idx: int,
50
+ ):
51
+ # This packing and unpacking of the layout is so that we keep the same TMA dimension as usual.
52
+ # e.g. for (seqlen, d, nheads, b) layout, we still have 4D TMA after packing to
53
+ # ((nheads, seqlen), d, b).
54
+ # If we instead pack directly to ((qhead_per_kvhead, seqlen), d, nheads_kv, b) we'd have 5D TMA.
55
+ # Pack headdim and seqlen dim into 1: (seqlen, d, nheads, b) -> ((nheads, seqlen), d, b)
56
+ gmem_tensor = layout_utils.select(
57
+ gmem_tensor, [head_idx, *range(head_idx), *range(head_idx + 1, cute.rank(gmem_tensor))]
58
+ )
59
+ gmem_tensor = cute.group_modes(gmem_tensor, 0, 2)
60
+ assert cta_tiler[0] % qhead_per_kvhead == 0, (
61
+ "CTA tile size in the seqlen dimension must be divisible by qhead_per_kvhead"
62
+ )
63
+ tma_atom, tma_tensor = cpasync.make_tiled_tma_atom(
64
+ op,
65
+ gmem_tensor,
66
+ smem_layout,
67
+ ((qhead_per_kvhead, cta_tiler[0] // qhead_per_kvhead), cta_tiler[1]), # No mcast
68
+ )
69
+ # Unpack from ((nheads, seqlen), d, b) -> ((qhead_per_kvhead, seqlen), d, nheads_kv, b)
70
+ T = tma_tensor
71
+ shape_packed = (
72
+ (qhead_per_kvhead, T.shape[0][1]),
73
+ *[T.shape[i] for i in range(1, head_idx)],
74
+ T.shape[0][0] // qhead_per_kvhead,
75
+ *[T.shape[i] for i in range(head_idx, len(T.shape))],
76
+ )
77
+ stride_packed = (
78
+ *[T.stride[i] for i in range(head_idx)],
79
+ T.stride[0][0] * qhead_per_kvhead,
80
+ *[T.stride[i] for i in range(head_idx, len(T.shape))],
81
+ )
82
+ tma_tensor = cute.make_tensor(T.iterator, cute.make_layout(shape_packed, stride=stride_packed))
83
+ return tma_atom, tma_tensor
84
+
85
+
86
+ def unpack_gqa_layout(T, qhead_per_kvhead, head_idx):
87
+ """Reverse of pack_gqa_layout: unfold qhead_per_kvhead from the seqlen dimension (mode 0).
88
+
89
+ The head dimension is at mode ``head_idx``. Modes before it (1..head_idx-1)
90
+ are kept as-is (e.g. headdim for Q/O tensors), and modes after it are kept
91
+ as-is (e.g. batch).
92
+
93
+ For Q/O tensors (head_idx=2):
94
+ ((qhead_per_kvhead, seqlen_q), headdim, nheads_kv, batch, ...) -> (seqlen_q, headdim, nheads, batch, ...)
95
+ For LSE tensors (head_idx=1):
96
+ ((qhead_per_kvhead, seqlen_q), nheads_kv, batch, ...) -> (seqlen_q, nheads, batch, ...)
97
+ """
98
+ seqlen_stride = T.stride[0][1]
99
+ head_stride = T.stride[0][0]
100
+ shape_unpacked = (
101
+ T.shape[0][1],
102
+ *[T.shape[i] for i in range(1, head_idx)],
103
+ T.shape[head_idx] * qhead_per_kvhead,
104
+ *[T.shape[i] for i in range(head_idx + 1, len(T.shape))],
105
+ )
106
+ stride_unpacked = (
107
+ seqlen_stride,
108
+ *[T.stride[i] for i in range(1, head_idx)],
109
+ head_stride,
110
+ *[T.stride[i] for i in range(head_idx + 1, len(T.shape))],
111
+ )
112
+ return cute.make_tensor(T.iterator, cute.make_layout(shape_unpacked, stride=stride_unpacked))
113
+
114
+
115
+ @dataclass
116
+ class PackGQA:
117
+ m_block_size: cutlass.Constexpr[int]
118
+ head_dim_padded: cutlass.Constexpr[int]
119
+ check_hdim_oob: cutlass.Constexpr[bool]
120
+ qhead_per_kvhead: cutlass.Constexpr[bool]
121
+
122
+ @cute.jit
123
+ def compute_ptr(
124
+ self,
125
+ tensor: cute.Tensor,
126
+ cRows: cute.Tensor,
127
+ tidx: cutlass.Int32,
128
+ block: cutlass.Int32,
129
+ threads_per_row: cutlass.Constexpr[int],
130
+ num_threads: cutlass.Constexpr[int],
131
+ ):
132
+ num_ptr_per_thread = cute.ceil_div(cute.size(cRows), threads_per_row)
133
+ tPrPtr = cute.make_fragment(num_ptr_per_thread, cutlass.Int64)
134
+ for i in cutlass.range_constexpr(num_ptr_per_thread):
135
+ row = i * num_threads + cRows[tidx % threads_per_row][0]
136
+ idx = block * self.m_block_size + row
137
+ m_idx = idx // self.qhead_per_kvhead
138
+ h_idx = idx - m_idx * self.qhead_per_kvhead
139
+ tPrPtr[i] = utils.elem_pointer(tensor, ((h_idx, m_idx),)).toint()
140
+ return tPrPtr
141
+
142
+ @cute.jit
143
+ def load_Q(
144
+ self,
145
+ mQ: cute.Tensor, # ((qhead_per_kvhead, seqlen_q), headdim)
146
+ sQ: cute.Tensor, # (m_block_size, head_dim_padded)
147
+ gmem_tiled_copy: cute.TiledCopy,
148
+ tidx: cutlass.Int32,
149
+ block: cutlass.Int32,
150
+ seqlen: cutlass.Int32,
151
+ ):
152
+ gmem_thr_copy = gmem_tiled_copy.get_slice(tidx)
153
+ cQ = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
154
+ tQsQ = gmem_thr_copy.partition_D(sQ)
155
+ tQcQ = gmem_thr_copy.partition_S(cQ)
156
+ t0QcQ = gmem_thr_copy.get_slice(0).partition_S(cQ)
157
+ tQpQ = utils.predicate_k(tQcQ, limit=mQ.shape[1])
158
+ tQcQ_row = tQcQ[0, None, 0]
159
+ threads_per_row = gmem_tiled_copy.layout_tv_tiled.shape[0][0]
160
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, "threads_per_row must divide WARP_SIZE"
161
+ num_threads = gmem_tiled_copy.size
162
+ tPrQPtr = self.compute_ptr(mQ[None, 0], tQcQ_row, tidx, block, threads_per_row, num_threads)
163
+ for m in cutlass.range_constexpr(cute.size(tQsQ.shape[1])):
164
+ q_ptr_i64 = utils.shuffle_sync(
165
+ tPrQPtr[m // threads_per_row], m % threads_per_row, width=threads_per_row
166
+ )
167
+ q_gmem_ptr = cute.make_ptr(
168
+ mQ.element_type, q_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
169
+ )
170
+ if (
171
+ t0QcQ[0, m, 0][0]
172
+ < seqlen * self.qhead_per_kvhead - block * self.m_block_size - tQcQ_row[0][0]
173
+ ):
174
+ mQ_cur = cute.make_tensor(q_gmem_ptr, (self.head_dim_padded,))
175
+ elems_per_load = cute.size(tQsQ.shape[0][0])
176
+ mQ_cur_copy = cute.tiled_divide(mQ_cur, (elems_per_load,))
177
+ for k in cutlass.range_constexpr(cute.size(tQsQ.shape[2])):
178
+ ki = tQcQ[0, 0, k][1] // elems_per_load
179
+ cute.copy(
180
+ gmem_thr_copy,
181
+ mQ_cur_copy[None, ki],
182
+ tQsQ[None, m, k],
183
+ pred=tQpQ[None, m, k] if cutlass.const_expr(self.check_hdim_oob) else None,
184
+ )
185
+ # We don't need to clear the sQ smem tiles since we'll only write out the valid outputs
186
+
187
+ @cute.jit
188
+ def store_LSE(
189
+ self,
190
+ mLSE: cute.Tensor, # (qhead_per_kvhead, seqlen_q)
191
+ tLSErLSE: cute.Tensor, # (m_block_size, head_dim_padded)
192
+ tiled_mma: cute.TiledMma,
193
+ tidx: cutlass.Int32,
194
+ block: cutlass.Int32,
195
+ seqlen: cutlass.Int32,
196
+ ):
197
+ thr_mma = tiled_mma.get_slice(tidx)
198
+ caccO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
199
+ taccOcO = thr_mma.partition_C(caccO)
200
+ taccOcO_row = layout_utils.reshape_acc_to_mn(taccOcO)[None, 0]
201
+ assert cute.size(tLSErLSE) == cute.size(taccOcO_row)
202
+ threads_per_row = tiled_mma.tv_layout_C.shape[0][0]
203
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, "threads_per_row must divide WARP_SIZE"
204
+ assert cute.size(tLSErLSE) <= threads_per_row
205
+ num_threads = tiled_mma.size
206
+ tPrLSEPtr = self.compute_ptr(mLSE, taccOcO_row, tidx, block, threads_per_row, num_threads)
207
+ for m in cutlass.range_constexpr(cute.size(tLSErLSE)):
208
+ lse_ptr_i64 = utils.shuffle_sync(
209
+ tPrLSEPtr[m // threads_per_row],
210
+ m % threads_per_row,
211
+ width=threads_per_row,
212
+ )
213
+ lse_gmem_ptr = cute.make_ptr(
214
+ mLSE.element_type, lse_ptr_i64, cute.AddressSpace.gmem, assumed_align=4
215
+ )
216
+ row = block * self.m_block_size + taccOcO_row[m][0]
217
+ # Only the thread corresponding to column 0 writes out the lse to gmem
218
+ if taccOcO[0][1] == 0 and row < seqlen * self.qhead_per_kvhead:
219
+ mLSE_copy = cute.make_tensor(lse_gmem_ptr, (1,))
220
+ mLSE_copy[0] = tLSErLSE[m]
221
+
222
+ @cute.jit
223
+ def store_O(
224
+ self,
225
+ mO: cute.Tensor, # ((qhead_per_kvhead, seqlen_q), headdim)
226
+ tOrO: cute.Tensor, # (m_block_size, head_dim_padded) split across threads according to gmem_tiled_copy
227
+ gmem_tiled_copy: cute.TiledCopy,
228
+ tidx: cutlass.Int32,
229
+ block: cutlass.Int32,
230
+ seqlen: cutlass.Int32,
231
+ ):
232
+ gmem_thr_copy = gmem_tiled_copy.get_slice(tidx)
233
+ cO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
234
+ tOcO = gmem_thr_copy.partition_S(cO)
235
+ t0OcO = gmem_thr_copy.get_slice(0).partition_S(cO)
236
+ tOpO = utils.predicate_k(tOcO, limit=mO.shape[1])
237
+ tOcO_row = tOcO[0, None, 0]
238
+ threads_per_row = gmem_tiled_copy.layout_tv_tiled.shape[0][0]
239
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, "threads_per_row must divide WARP_SIZE"
240
+ num_threads = gmem_tiled_copy.size
241
+ tPrOPtr = self.compute_ptr(mO[None, 0], tOcO_row, tidx, block, threads_per_row, num_threads)
242
+ for m in cutlass.range_constexpr(cute.size(tOrO.shape[1])):
243
+ o_ptr_i64 = utils.shuffle_sync(
244
+ tPrOPtr[m // threads_per_row], m % threads_per_row, width=threads_per_row
245
+ )
246
+ o_gmem_ptr = cute.make_ptr(
247
+ mO.element_type, o_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
248
+ )
249
+ if (
250
+ t0OcO[0, m, 0][0]
251
+ < seqlen * self.qhead_per_kvhead - block * self.m_block_size - tOcO_row[0][0]
252
+ ):
253
+ mO_cur = cute.make_tensor(o_gmem_ptr, (self.head_dim_padded,))
254
+ elems_per_load = cute.size(tOrO.shape[0][0])
255
+ mO_cur_copy = cute.tiled_divide(mO_cur, (elems_per_load,))
256
+ for k in cutlass.range_constexpr(cute.size(tOrO.shape[2])):
257
+ ki = tOcO[0, 0, k][1] // elems_per_load
258
+ cute.copy(
259
+ gmem_thr_copy,
260
+ tOrO[None, m, k],
261
+ mO_cur_copy[None, ki],
262
+ pred=tOpO[None, m, k] if cutlass.const_expr(self.check_hdim_oob) else None,
263
+ )
build/torch-cuda/flashrt_fa4/cute/paged_kv.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Type
2
+ from dataclasses import dataclass
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+ from cutlass.cute.nvgpu import cpasync
7
+ from cutlass import Int32, const_expr
8
+
9
+ from fa4_cute_runtime.flashrt_fa4.cute import utils
10
+ from fa4_cute_runtime.quack.cute_dsl_utils import ParamsBase
11
+ from cutlass.cute import FastDivmodDivisor
12
+
13
+ import math
14
+
15
+
16
+ @dataclass
17
+ class PagedKVManager(ParamsBase):
18
+ mPageTable: cute.Tensor
19
+ mK_paged: cute.Tensor
20
+ mV_paged: cute.Tensor
21
+ thread_idx: Int32
22
+
23
+ page_size_divmod: FastDivmodDivisor
24
+ seqlen_k: Int32
25
+ leftpad_k: Int32
26
+ n_block_size: Int32
27
+ num_threads: cutlass.Constexpr[Int32]
28
+ head_dim_padded: cutlass.Constexpr[Int32]
29
+ head_dim_v_padded: cutlass.Constexpr[Int32]
30
+
31
+ arch: cutlass.Constexpr[Int32]
32
+ v_gmem_transposed: cutlass.Constexpr[bool]
33
+
34
+ gmem_threads_per_row: cutlass.Constexpr[Int32]
35
+ page_entry_per_thread: Int32
36
+ async_copy_elems: Int32
37
+
38
+ gmem_tiled_copy_KV: cute.TiledCopy
39
+ gmem_thr_copy_KV: cute.TiledCopy
40
+ tPrPage: cute.Tensor
41
+ tPrPageOffset: cute.Tensor
42
+ tKpK: cute.Tensor
43
+ tVpV: cute.Tensor
44
+
45
+ @staticmethod
46
+ def create(
47
+ mPageTable: cute.Tensor,
48
+ mK_paged: cute.Tensor,
49
+ mV_paged: cute.Tensor,
50
+ page_size_divmod: FastDivmodDivisor,
51
+ bidb: Int32,
52
+ bidh: Int32,
53
+ thread_idx: Int32,
54
+ seqlen_k: Int32,
55
+ leftpad_k: Int32,
56
+ n_block_size: cutlass.Constexpr[Int32],
57
+ head_dim_padded: cutlass.Constexpr[Int32],
58
+ head_dim_v_padded: cutlass.Constexpr[Int32],
59
+ num_threads: cutlass.Constexpr[Int32],
60
+ dtype: Type[cutlass.Numeric],
61
+ arch: cutlass.Constexpr[int] = 100,
62
+ ):
63
+ # SM100 transposes V in gmem to (dv, page_size, num_pages);
64
+ # SM90 keeps V as (page_size, dv, num_pages), same layout as K.
65
+ v_gmem_transposed = arch != 90
66
+ universal_copy_bits = 128
67
+ async_copy_elems = universal_copy_bits // dtype.width
68
+ dtype_bytes = dtype.width // 8
69
+ gmem_k_block_size = math.gcd(
70
+ head_dim_padded,
71
+ head_dim_v_padded,
72
+ 128 // dtype_bytes,
73
+ )
74
+ assert gmem_k_block_size % async_copy_elems == 0
75
+ gmem_threads_per_row = gmem_k_block_size // async_copy_elems
76
+ assert cute.arch.WARP_SIZE % gmem_threads_per_row == 0
77
+ atom_async_copy = cute.make_copy_atom(
78
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
79
+ dtype,
80
+ num_bits_per_copy=universal_copy_bits,
81
+ )
82
+ thr_layout = cute.make_ordered_layout(
83
+ (num_threads // gmem_threads_per_row, gmem_threads_per_row),
84
+ order=(1, 0),
85
+ )
86
+ val_layout = cute.make_layout((1, async_copy_elems))
87
+ gmem_tiled_copy_KV = cute.make_tiled_copy_tv(atom_async_copy, thr_layout, val_layout)
88
+ gmem_thr_copy_KV = gmem_tiled_copy_KV.get_slice(thread_idx)
89
+ page_entry_per_thread = n_block_size // num_threads
90
+
91
+ tPrPage = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
92
+ tPrPageOffset = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
93
+
94
+ mPageTable = mPageTable[bidb, None]
95
+ mK_paged = mK_paged[None, None, bidh, None]
96
+ mV_paged = mV_paged[None, None, bidh, None]
97
+
98
+ cK = cute.make_identity_tensor((n_block_size, head_dim_padded))
99
+ tKcK = gmem_thr_copy_KV.partition_S(cK)
100
+ tKpK = utils.predicate_k(tKcK, limit=mK_paged.shape[1])
101
+
102
+ if const_expr(head_dim_padded == head_dim_v_padded):
103
+ tVpV = tKpK
104
+ else:
105
+ cV = cute.make_identity_tensor((n_block_size, head_dim_v_padded))
106
+ tVcV = gmem_thr_copy_KV.partition_S(cV)
107
+ # When V is transposed in gmem, dv is shape[0]; otherwise dv is shape[1] (same as K)
108
+ tVpV = utils.predicate_k(tVcV, limit=mV_paged.shape[0 if v_gmem_transposed else 1])
109
+
110
+ return PagedKVManager(
111
+ mPageTable,
112
+ mK_paged,
113
+ mV_paged,
114
+ thread_idx,
115
+ page_size_divmod,
116
+ seqlen_k,
117
+ leftpad_k,
118
+ n_block_size,
119
+ num_threads,
120
+ head_dim_padded,
121
+ head_dim_v_padded,
122
+ arch,
123
+ v_gmem_transposed,
124
+ gmem_threads_per_row,
125
+ page_entry_per_thread,
126
+ async_copy_elems,
127
+ gmem_tiled_copy_KV,
128
+ gmem_thr_copy_KV,
129
+ tPrPage,
130
+ tPrPageOffset,
131
+ tKpK,
132
+ tVpV,
133
+ )
134
+
135
+ @cute.jit
136
+ def load_page_table(self, n_block: Int32):
137
+ for i in cutlass.range(self.page_entry_per_thread, unroll=1):
138
+ row = (
139
+ i * self.num_threads
140
+ + (self.thread_idx % self.gmem_threads_per_row)
141
+ * (self.num_threads // self.gmem_threads_per_row)
142
+ + (self.thread_idx // self.gmem_threads_per_row)
143
+ )
144
+ row_idx = n_block * self.n_block_size + row
145
+
146
+ page_idx, page_offset = divmod(row_idx + self.leftpad_k, self.page_size_divmod)
147
+
148
+ is_valid = (
149
+ (i + 1) * self.num_threads <= self.n_block_size or row < self.n_block_size
150
+ ) and row_idx < self.seqlen_k
151
+ page = self.mPageTable[page_idx] if is_valid else 0
152
+
153
+ self.tPrPage[i] = page
154
+ self.tPrPageOffset[i] = page_offset
155
+
156
+ @cute.jit
157
+ def compute_X_ptr(self, K_or_V: str):
158
+ tPrXPtr = cute.make_rmem_tensor((self.page_entry_per_thread,), cutlass.Int64)
159
+ mX = self.mK_paged if const_expr(K_or_V == "K") else self.mV_paged
160
+ # K is always (page_size, d, num_pages). V matches K when not transposed,
161
+ # but is (dv, page_size, num_pages) when transposed (SM100).
162
+ transposed = const_expr(K_or_V == "V" and self.v_gmem_transposed)
163
+ for i in cutlass.range(self.page_entry_per_thread, unroll=1):
164
+ page = self.tPrPage[i]
165
+ page_offset = self.tPrPageOffset[i]
166
+ if const_expr(transposed):
167
+ tPrXPtr[i] = utils.elem_pointer(mX, (0, page_offset, page)).toint()
168
+ else:
169
+ tPrXPtr[i] = utils.elem_pointer(mX, (page_offset, 0, page)).toint()
170
+ return tPrXPtr
171
+
172
+ @cute.jit
173
+ def _flatten_smem_sm100(self, sX: cute.Tensor, K_or_V: str):
174
+ """Flatten SM100 smem ((a,b), cta_split, k) to (a,(b,k)); transpose V to (d,page_size)."""
175
+ sX_pi = cute.make_tensor(
176
+ sX.iterator,
177
+ cute.make_layout(
178
+ (sX.shape[0][0], (sX.shape[0][1], sX.shape[2])),
179
+ stride=(sX.stride[0][0], (sX.stride[0][1], sX.stride[2])),
180
+ ),
181
+ )
182
+ if const_expr(K_or_V == "V"):
183
+ sX_pi = cute.make_tensor(sX_pi.iterator, cute.select(sX_pi.layout, mode=[1, 0]))
184
+ return sX_pi
185
+
186
+ @cute.jit
187
+ def _copy_row_async(
188
+ self,
189
+ tXsX: cute.Tensor,
190
+ tXcX: cute.Tensor,
191
+ mX_paged_cur_copy: cute.Tensor,
192
+ m: Int32,
193
+ should_load: cute.Tensor,
194
+ ):
195
+ """Issue cp.async copies for one row across all k-tiles."""
196
+ for k in cutlass.range_constexpr(cute.size(tXsX, mode=[2])):
197
+ ki = tXcX[0, 0, k][1] // self.async_copy_elems
198
+ mX_paged_cur_copy_ki = mX_paged_cur_copy[None, ki]
199
+ tXsX_k = tXsX[None, m, k]
200
+ mX_paged_cur_copy_ki = cute.make_tensor(mX_paged_cur_copy_ki.iterator, tXsX_k.layout)
201
+ cute.copy(
202
+ self.gmem_tiled_copy_KV,
203
+ mX_paged_cur_copy_ki,
204
+ tXsX_k,
205
+ pred=should_load,
206
+ )
207
+
208
+ @cute.jit
209
+ def load_KV(self, n_block: Int32, sX: cute.Tensor, K_or_V: str):
210
+ assert K_or_V in ("K", "V")
211
+
212
+ tPrXPtr = self.compute_X_ptr(K_or_V)
213
+
214
+ if const_expr(self.arch == 90):
215
+ # SM90: sX is already stage-sliced by caller (sK[None, None, stage]).
216
+ # Flatten hierarchical modes to get (n_block_size, head_dim).
217
+ sX_pi = cute.group_modes(sX, 0, 1)
218
+ # SM90 does NOT transpose V here (it's transposed via utils.transpose_view before MMA)
219
+ else:
220
+ sX_pi = self._flatten_smem_sm100(sX, K_or_V)
221
+
222
+ head_dim = self.head_dim_v_padded if const_expr(K_or_V == "V") else self.head_dim_padded
223
+ cX = cute.make_identity_tensor((self.n_block_size, head_dim))
224
+ tXsX = self.gmem_thr_copy_KV.partition_D(sX_pi)
225
+ tXcX = self.gmem_thr_copy_KV.partition_S(cX)
226
+ tXc0X = self.gmem_thr_copy_KV.get_slice(0).partition_S(cX)
227
+
228
+ seqlenk_row_limit = (
229
+ self.seqlen_k - n_block * self.n_block_size - tXcX[0][0] if n_block >= 0 else 0
230
+ )
231
+ for m in cutlass.range_constexpr(cute.size(tXsX, mode=[1])):
232
+ row_valid = tXc0X[0, m, 0][0] < seqlenk_row_limit
233
+ should_load = cute.make_fragment_like(tXsX[(0, None), m, 0], cute.Boolean)
234
+ should_load.fill(row_valid)
235
+
236
+ x_ptr_i64 = utils.shuffle_sync(
237
+ tPrXPtr[m // self.gmem_threads_per_row],
238
+ m % self.gmem_threads_per_row,
239
+ width=self.gmem_threads_per_row,
240
+ )
241
+ x_gmem_ptr = cute.make_ptr(
242
+ self.mK_paged.element_type, x_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
243
+ )
244
+ mX_paged_cur = cute.make_tensor(x_gmem_ptr, cute.make_layout((head_dim,)))
245
+ mX_paged_cur_copy = cute.tiled_divide(mX_paged_cur, (self.async_copy_elems,))
246
+ self._copy_row_async(tXsX, tXcX, mX_paged_cur_copy, m, should_load)
build/torch-cuda/flashrt_fa4/cute/pipeline.py ADDED
@@ -0,0 +1,402 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Optional
4
+ from dataclasses import dataclass
5
+
6
+ import cutlass.cute as cute
7
+ from cutlass import Boolean, Int32, const_expr
8
+ from cutlass.cutlass_dsl import if_generate, dsl_user_op
9
+ from cutlass.pipeline import PipelineState
10
+ from cutlass.pipeline import PipelineUserType
11
+ from cutlass.pipeline import NamedBarrier as NamedBarrierOg
12
+ from cutlass.pipeline import PipelineAsync as PipelineAsyncOg
13
+ from cutlass.pipeline import PipelineCpAsync as PipelineCpAsyncOg
14
+ from cutlass.pipeline import PipelineTmaAsync as PipelineTmaAsyncOg
15
+ from cutlass.pipeline import PipelineTmaUmma as PipelineTmaUmmaOg
16
+ from cutlass.pipeline import PipelineUmmaAsync as PipelineUmmaAsyncOg
17
+ from cutlass.pipeline import PipelineAsyncUmma as PipelineAsyncUmmaOg
18
+
19
+
20
+ def _override_create(parent_cls, child_cls):
21
+ """Create a static factory that constructs parent_cls then re-classes to child_cls."""
22
+
23
+ @staticmethod
24
+ def create(*args, **kwargs):
25
+ obj = parent_cls.create(*args, **kwargs)
26
+ # Can't assign to __class__ directly since the dataclass is frozen
27
+ object.__setattr__(obj, "__class__", child_cls)
28
+ return obj
29
+
30
+ return create
31
+
32
+
33
+ def _make_state(index: Int32, phase: Int32) -> PipelineState:
34
+ """Construct a PipelineState from index and phase (count/stages unused by callers)."""
35
+ return PipelineState(stages=0, count=Int32(0), index=index, phase=phase)
36
+
37
+
38
+ class PipelineStateSimple:
39
+ """
40
+ Pipeline state contains an index and phase bit corresponding to the current position in the circular buffer.
41
+ Use a single Int32 to store both the index and phase bit, then we use divmod to get the
42
+ index and phase. If stages is a power of 2, divmod turns into bit twiddling.
43
+ """
44
+
45
+ def __init__(self, stages: int, phase_index: Int32):
46
+ self._stages = stages
47
+ self._phase_index = phase_index
48
+
49
+ def clone(self) -> "PipelineStateSimple":
50
+ return PipelineStateSimple(self.stages, self._phase_index)
51
+
52
+ @property
53
+ def stages(self) -> int:
54
+ return self._stages
55
+
56
+ @property
57
+ def index(self) -> Int32:
58
+ if const_expr(self._stages == 1):
59
+ return Int32(0)
60
+ else:
61
+ return self._phase_index % self._stages
62
+
63
+ @property
64
+ def phase(self) -> Int32:
65
+ # PTX docs say that the phase parity needs to be 0 or 1, so by right we need to
66
+ # take modulo 2. But in practice just passing the phase in without modulo works fine.
67
+ if const_expr(self._stages == 1):
68
+ return self._phase_index
69
+ else:
70
+ return self._phase_index // self._stages
71
+
72
+ def advance(self):
73
+ if const_expr(self._stages == 1):
74
+ self._phase_index ^= 1
75
+ else:
76
+ self._phase_index += 1
77
+
78
+ def __extract_mlir_values__(self):
79
+ phase_index = self._phase_index
80
+ return [phase_index.ir_value()]
81
+
82
+ def __new_from_mlir_values__(self, values):
83
+ return PipelineStateSimple(self.stages, Int32(values[0]))
84
+
85
+
86
+ def make_pipeline_state(type: PipelineUserType, stages: int):
87
+ """
88
+ Creates a pipeline state. Producers are assumed to start with an empty buffer and have a flipped phase bit of 1.
89
+ """
90
+ if type is PipelineUserType.Producer:
91
+ return PipelineStateSimple(stages, Int32(stages))
92
+ elif type is PipelineUserType.Consumer:
93
+ return PipelineStateSimple(stages, Int32(0))
94
+ else:
95
+ assert False, "Error: invalid PipelineUserType specified for make_pipeline_state."
96
+
97
+
98
+ # ── Shared helpers ───────────────────────────────────────────────────────────
99
+
100
+
101
+ def _call_with_elect_one(parent_method, self, state, elect_one, syncwarp, loc, ip):
102
+ """Optionally wrap a parent pipeline method call in sync_warp + elect_one."""
103
+ if const_expr(elect_one):
104
+ if const_expr(syncwarp):
105
+ cute.arch.sync_warp()
106
+ with cute.arch.elect_one():
107
+ parent_method(self, state, loc=loc, ip=ip)
108
+ else:
109
+ parent_method(self, state, loc=loc, ip=ip)
110
+
111
+
112
+ # ── Mixin: _w_index / _w_index_phase variants that delegate to parent ───────
113
+ # Each parent class has PipelineState-based methods (producer_acquire, producer_commit,
114
+ # consumer_wait, consumer_release). The _w_index_phase variants just construct a
115
+ # PipelineState from (index, phase) and delegate.
116
+
117
+
118
+ class _PipelineIndexPhaseMixin:
119
+ """Mixin providing _w_index_phase / _w_index methods that delegate to PipelineState-based parents."""
120
+
121
+ @dsl_user_op
122
+ def producer_acquire_w_index_phase(
123
+ self,
124
+ index: Int32,
125
+ phase: Int32,
126
+ try_acquire_token: Optional[Boolean] = None,
127
+ *,
128
+ loc=None,
129
+ ip=None,
130
+ ):
131
+ state = _make_state(index, phase)
132
+ # Call the parent's producer_acquire (which takes PipelineState)
133
+ self.producer_acquire(state, try_acquire_token, loc=loc, ip=ip)
134
+
135
+ @dsl_user_op
136
+ def producer_commit_w_index(self, index: Int32, *, loc=None, ip=None):
137
+ state = _make_state(index, Int32(0))
138
+ self.producer_commit(state, loc=loc, ip=ip)
139
+
140
+ @dsl_user_op
141
+ def consumer_wait_w_index_phase(
142
+ self,
143
+ index: Int32,
144
+ phase: Int32,
145
+ try_wait_token: Optional[Boolean] = None,
146
+ *,
147
+ loc=None,
148
+ ip=None,
149
+ ):
150
+ state = _make_state(index, phase)
151
+ self.consumer_wait(state, try_wait_token, loc=loc, ip=ip)
152
+
153
+ @dsl_user_op
154
+ def consumer_release_w_index(self, index: Int32, *, loc=None, ip=None):
155
+ state = _make_state(index, Int32(0))
156
+ self.consumer_release(state, loc=loc, ip=ip)
157
+
158
+
159
+ # ── NamedBarrier ─────────────────────────────────────────────────────────────
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class NamedBarrier(NamedBarrierOg):
164
+ create = _override_create(NamedBarrierOg, None) # patched below
165
+
166
+ @dsl_user_op
167
+ def arrive_w_index(self, index: Int32, *, loc=None, ip=None) -> None:
168
+ """
169
+ The aligned flavor of arrive is used when all threads in the CTA will execute the
170
+ same instruction. See PTX documentation.
171
+ """
172
+ cute.arch.barrier_arrive(
173
+ barrier_id=self.barrier_id + index,
174
+ number_of_threads=self.num_threads,
175
+ loc=loc,
176
+ ip=ip,
177
+ )
178
+
179
+ @dsl_user_op
180
+ def arrive_and_wait_w_index(self, index: Int32, *, loc=None, ip=None) -> None:
181
+ cute.arch.barrier(
182
+ barrier_id=self.barrier_id + index,
183
+ number_of_threads=self.num_threads,
184
+ loc=loc,
185
+ ip=ip,
186
+ )
187
+
188
+
189
+ NamedBarrier.create = _override_create(NamedBarrierOg, NamedBarrier)
190
+
191
+
192
+ # ── PipelineAsync ────────────────────────────────────────────────────────────
193
+
194
+
195
+ @dataclass(frozen=True)
196
+ class PipelineAsync(_PipelineIndexPhaseMixin, PipelineAsyncOg):
197
+ """
198
+ PipelineAsync with optional elect_one for producer_commit and consumer_release.
199
+
200
+ When elect_one_*=True (set at create time), only one elected thread per warp
201
+ signals the barrier arrive. This is useful when the mask count is set to 1 per warp.
202
+
203
+ Args (to create):
204
+ elect_one_commit: If True, only elected thread signals producer_commit.
205
+ syncwarp_before_commit: If True (default), issue syncwarp before elect_one.
206
+ elect_one_release: If True, only elected thread signals consumer_release.
207
+ syncwarp_before_release: If True (default), issue syncwarp before elect_one.
208
+ Set syncwarp to False when threads are already converged (e.g. after wgmma wait_group).
209
+ """
210
+
211
+ _elect_one_commit: bool = False
212
+ _syncwarp_before_commit: bool = True
213
+ _elect_one_release: bool = False
214
+ _syncwarp_before_release: bool = True
215
+
216
+ @staticmethod
217
+ def create(
218
+ *args,
219
+ elect_one_commit: bool = False,
220
+ syncwarp_before_commit: bool = True,
221
+ elect_one_release: bool = False,
222
+ syncwarp_before_release: bool = True,
223
+ **kwargs,
224
+ ):
225
+ obj = PipelineAsyncOg.create(*args, **kwargs)
226
+ object.__setattr__(obj, "__class__", PipelineAsync)
227
+ object.__setattr__(obj, "_elect_one_commit", elect_one_commit)
228
+ object.__setattr__(obj, "_syncwarp_before_commit", syncwarp_before_commit)
229
+ object.__setattr__(obj, "_elect_one_release", elect_one_release)
230
+ object.__setattr__(obj, "_syncwarp_before_release", syncwarp_before_release)
231
+ return obj
232
+
233
+ @dsl_user_op
234
+ def producer_commit(self, state: PipelineState, *, loc=None, ip=None):
235
+ _call_with_elect_one(
236
+ PipelineAsyncOg.producer_commit,
237
+ self,
238
+ state,
239
+ self._elect_one_commit,
240
+ self._syncwarp_before_commit,
241
+ loc,
242
+ ip,
243
+ )
244
+
245
+ @dsl_user_op
246
+ def consumer_release(self, state: PipelineState, *, loc=None, ip=None):
247
+ _call_with_elect_one(
248
+ PipelineAsyncOg.consumer_release,
249
+ self,
250
+ state,
251
+ self._elect_one_release,
252
+ self._syncwarp_before_release,
253
+ loc,
254
+ ip,
255
+ )
256
+
257
+ # _w_index variants inherited from _PipelineIndexPhaseMixin, which delegate
258
+ # to producer_commit / consumer_release above.
259
+
260
+
261
+ # ── PipelineCpAsync ──────────────────────────────────────────────────────────
262
+
263
+
264
+ @dataclass(frozen=True)
265
+ class PipelineCpAsync(_PipelineIndexPhaseMixin, PipelineCpAsyncOg):
266
+ _elect_one_release: bool = False
267
+ _syncwarp_before_release: bool = True
268
+
269
+ @staticmethod
270
+ def create(
271
+ *args,
272
+ elect_one_release: bool = False,
273
+ syncwarp_before_release: bool = True,
274
+ **kwargs,
275
+ ):
276
+ obj = PipelineCpAsyncOg.create(*args, **kwargs)
277
+ object.__setattr__(obj, "__class__", PipelineCpAsync)
278
+ object.__setattr__(obj, "_elect_one_release", elect_one_release)
279
+ object.__setattr__(obj, "_syncwarp_before_release", syncwarp_before_release)
280
+ return obj
281
+
282
+ @dsl_user_op
283
+ def consumer_release(self, state: PipelineState, *, loc=None, ip=None):
284
+ _call_with_elect_one(
285
+ PipelineCpAsyncOg.consumer_release,
286
+ self,
287
+ state,
288
+ self._elect_one_release,
289
+ self._syncwarp_before_release,
290
+ loc,
291
+ ip,
292
+ )
293
+
294
+ # _w_index variants inherited from _PipelineIndexPhaseMixin.
295
+
296
+
297
+ # ── PipelineTmaAsync ────────────────────────────────────────────────────────
298
+
299
+
300
+ @dataclass(frozen=True)
301
+ class PipelineTmaAsync(_PipelineIndexPhaseMixin, PipelineTmaAsyncOg):
302
+ """Override producer_acquire to take in extra_tx_count parameter."""
303
+
304
+ @dsl_user_op
305
+ def producer_acquire(
306
+ self,
307
+ state: PipelineState,
308
+ try_acquire_token: Optional[Boolean] = None,
309
+ extra_tx_count: int = 0,
310
+ *,
311
+ loc=None,
312
+ ip=None,
313
+ ):
314
+ """
315
+ TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
316
+ """
317
+ if_generate(
318
+ try_acquire_token is None or try_acquire_token == 0,
319
+ lambda: self.sync_object_empty.wait(state.index, state.phase, loc=loc, ip=ip),
320
+ loc=loc,
321
+ ip=ip,
322
+ )
323
+ if const_expr(extra_tx_count == 0):
324
+ self.sync_object_full.arrive(state.index, self.producer_mask, loc=loc, ip=ip)
325
+ else:
326
+ tx_count = self.sync_object_full.tx_count + extra_tx_count
327
+ self.sync_object_full.arrive_and_expect_tx(state.index, tx_count, loc=loc, ip=ip)
328
+
329
+
330
+ PipelineTmaAsync.create = _override_create(PipelineTmaAsyncOg, PipelineTmaAsync)
331
+
332
+
333
+ # ── PipelineTmaUmma ─────────────────────────────────────────────────────────
334
+
335
+
336
+ @dataclass(frozen=True)
337
+ class PipelineTmaUmma(_PipelineIndexPhaseMixin, PipelineTmaUmmaOg):
338
+ """Override producer_acquire to take in extra_tx_count parameter."""
339
+
340
+ @dsl_user_op
341
+ def producer_acquire(
342
+ self,
343
+ state: PipelineState,
344
+ try_acquire_token: Optional[Boolean] = None,
345
+ extra_tx_count: int = 0,
346
+ *,
347
+ loc=None,
348
+ ip=None,
349
+ ):
350
+ """
351
+ TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
352
+ """
353
+ if_generate(
354
+ try_acquire_token is None or try_acquire_token == 0,
355
+ lambda: self.sync_object_empty.wait(state.index, state.phase, loc=loc, ip=ip),
356
+ loc=loc,
357
+ ip=ip,
358
+ )
359
+ if const_expr(extra_tx_count == 0):
360
+ if_generate(
361
+ self.is_leader_cta,
362
+ lambda: self.sync_object_full.arrive(
363
+ state.index, self.producer_mask, loc=loc, ip=ip
364
+ ),
365
+ loc=loc,
366
+ ip=ip,
367
+ )
368
+ else:
369
+ tx_count = self.sync_object_full.tx_count + extra_tx_count
370
+ if_generate(
371
+ self.is_leader_cta,
372
+ lambda: self.sync_object_full.arrive_and_expect_tx(
373
+ state.index, tx_count, loc=loc, ip=ip
374
+ ),
375
+ loc=loc,
376
+ ip=ip,
377
+ )
378
+
379
+
380
+ PipelineTmaUmma.create = _override_create(PipelineTmaUmmaOg, PipelineTmaUmma)
381
+
382
+
383
+ # ── PipelineUmmaAsync ───────────────────────────────────────────────────────
384
+
385
+
386
+ @dataclass(frozen=True)
387
+ class PipelineUmmaAsync(_PipelineIndexPhaseMixin, PipelineUmmaAsyncOg):
388
+ pass
389
+
390
+
391
+ PipelineUmmaAsync.create = _override_create(PipelineUmmaAsyncOg, PipelineUmmaAsync)
392
+
393
+
394
+ # ── PipelineAsyncUmma ───────────────────────────────────────────────────────
395
+
396
+
397
+ @dataclass(frozen=True)
398
+ class PipelineAsyncUmma(_PipelineIndexPhaseMixin, PipelineAsyncUmmaOg):
399
+ pass
400
+
401
+
402
+ PipelineAsyncUmma.create = _override_create(PipelineAsyncUmmaOg, PipelineAsyncUmma)
build/torch-cuda/flashrt_fa4/cute/seqlen_info.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from dataclasses import dataclass
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+ from cutlass import Int32, const_expr
7
+
8
+ from fa4_cute_runtime.quack import copy_utils
9
+
10
+ """
11
+ This consolidates all the info related to sequence length. This is so that we can do all
12
+ the gmem reads once at the beginning of each tile, rather than having to repeat these reads
13
+ to compute various things like n_block_min, n_block_max, etc.
14
+ """
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class SeqlenInfo:
19
+ offset: Int32
20
+ offset_padded: Int32
21
+ seqlen: Int32
22
+ has_cu_seqlens: cutlass.Constexpr[bool] = False
23
+
24
+ @staticmethod
25
+ def create(
26
+ batch_idx: Int32,
27
+ seqlen_static: Int32,
28
+ cu_seqlens: Optional[cute.Tensor] = None,
29
+ seqused: Optional[cute.Tensor] = None,
30
+ tile: cutlass.Constexpr[int] = 128,
31
+ ):
32
+ offset = 0 if const_expr(cu_seqlens is None) else cu_seqlens[batch_idx]
33
+ offset_padded = (
34
+ 0
35
+ if const_expr(cu_seqlens is None)
36
+ # Add divby so that the compiler knows the alignment when moving by offset_padded
37
+ else cute.assume((offset + batch_idx * tile) // tile * tile, divby=tile)
38
+ )
39
+ if const_expr(seqused is not None):
40
+ seqlen = seqused[batch_idx]
41
+ elif const_expr(cu_seqlens is not None):
42
+ seqlen = cu_seqlens[batch_idx + 1] - cu_seqlens[batch_idx]
43
+ else:
44
+ seqlen = seqlen_static
45
+ return SeqlenInfo(offset, offset_padded, seqlen, has_cu_seqlens=cu_seqlens is not None)
46
+
47
+ def offset_batch(
48
+ self,
49
+ mT: cute.Tensor,
50
+ batch_idx: Int32,
51
+ dim: int,
52
+ padded: cutlass.Constexpr[bool] = False,
53
+ multiple: int = 1,
54
+ ) -> cute.Tensor:
55
+ """Offset a tensor by batch index. batch dim is at position `dim`, seqlen is at dim=0."""
56
+ if const_expr(not self.has_cu_seqlens):
57
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mT) - 1 - dim)
58
+ return mT[idx]
59
+ else:
60
+ off = multiple * (self.offset if const_expr(not padded) else self.offset_padded)
61
+ offset = off if const_expr(cute.rank(mT.shape[0]) == 1) else (0, off)
62
+ idx = (offset,) + (None,) * (cute.rank(mT) - 1)
63
+ return cute.domain_offset(idx, mT)
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class SeqlenInfoQK:
68
+ offset_q: Int32
69
+ offset_k: Int32
70
+ padded_offset_q: Int32
71
+ padded_offset_k: Int32
72
+ seqlen_q: Int32
73
+ seqlen_k: Int32
74
+ has_cu_seqlens_q: cutlass.Constexpr[bool]
75
+ has_cu_seqlens_k: cutlass.Constexpr[bool]
76
+ has_seqused_q: cutlass.Constexpr[bool]
77
+ has_seqused_k: cutlass.Constexpr[bool]
78
+
79
+ @staticmethod
80
+ def create(
81
+ batch_idx: Int32,
82
+ seqlen_q_static: Int32,
83
+ seqlen_k_static: Int32,
84
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
85
+ mCuSeqlensK: Optional[cute.Tensor] = None,
86
+ mSeqUsedQ: Optional[cute.Tensor] = None,
87
+ mSeqUsedK: Optional[cute.Tensor] = None,
88
+ tile_m: cutlass.Constexpr[Int32] = 128,
89
+ tile_n: cutlass.Constexpr[Int32] = 128,
90
+ ):
91
+ offset_q = 0 if const_expr(mCuSeqlensQ is None) else mCuSeqlensQ[batch_idx]
92
+ offset_k = 0 if const_expr(mCuSeqlensK is None) else mCuSeqlensK[batch_idx]
93
+ padded_offset_q = (
94
+ 0
95
+ if const_expr(mCuSeqlensQ is None)
96
+ else cute.assume((offset_q + batch_idx * tile_m) // tile_m * tile_m, divby=tile_m)
97
+ )
98
+ padded_offset_k = (
99
+ 0
100
+ if const_expr(mCuSeqlensK is None)
101
+ else cute.assume((offset_k + batch_idx * tile_n) // tile_n * tile_n, divby=tile_n)
102
+ )
103
+ if const_expr(mSeqUsedQ is not None):
104
+ seqlen_q = mSeqUsedQ[batch_idx]
105
+ else:
106
+ seqlen_q = (
107
+ seqlen_q_static
108
+ if const_expr(mCuSeqlensQ is None)
109
+ else mCuSeqlensQ[batch_idx + 1] - offset_q
110
+ )
111
+ if const_expr(mSeqUsedK is not None):
112
+ seqlen_k = mSeqUsedK[batch_idx]
113
+ else:
114
+ seqlen_k = (
115
+ seqlen_k_static
116
+ if const_expr(mCuSeqlensK is None)
117
+ else mCuSeqlensK[batch_idx + 1] - offset_k
118
+ )
119
+ return SeqlenInfoQK(
120
+ offset_q,
121
+ offset_k,
122
+ padded_offset_q,
123
+ padded_offset_k,
124
+ seqlen_q,
125
+ seqlen_k,
126
+ has_cu_seqlens_q=mCuSeqlensQ is not None,
127
+ has_cu_seqlens_k=mCuSeqlensK is not None,
128
+ has_seqused_q=mSeqUsedQ is not None,
129
+ has_seqused_k=mSeqUsedK is not None,
130
+ )
131
+
132
+ def offset_batch_Q(
133
+ self,
134
+ mQ: cute.Tensor,
135
+ batch_idx: Int32,
136
+ dim: int,
137
+ padded: cutlass.Constexpr[bool] = False,
138
+ ragged: cutlass.Constexpr[bool] = False,
139
+ ) -> cute.Tensor:
140
+ """Seqlen must be the first dimension of mQ"""
141
+ if const_expr(not ragged):
142
+ if const_expr(not self.has_cu_seqlens_q):
143
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mQ) - 1 - dim)
144
+ return mQ[idx]
145
+ else:
146
+ offset_q = self.offset_q if const_expr(not padded) else self.padded_offset_q
147
+ offset_q = offset_q if const_expr(cute.rank(mQ.shape[0]) == 1) else (None, offset_q)
148
+ idx = (offset_q,) + (None,) * (cute.rank(mQ) - 1)
149
+ return cute.domain_offset(idx, mQ)
150
+ else:
151
+ if const_expr(not self.has_cu_seqlens_q):
152
+ offset_q = 0
153
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mQ) - 1 - dim)
154
+ mQ = mQ[idx]
155
+ else:
156
+ offset_q = self.offset_q if const_expr(not padded) else self.padded_offset_q
157
+ if const_expr(cute.rank(mQ.shape[0]) == 1):
158
+ return copy_utils.offset_ragged_tensor(
159
+ mQ, offset_q, self.seqlen_q, ragged_dim=0, ptr_shift=True
160
+ )
161
+ else: # PackGQA
162
+ assert cute.rank(mQ.shape[0]) == 2
163
+ # Unpack before calling offset_ragged_tensor, then pack
164
+ idx = ((None, None),) + (None,) * (cute.rank(mQ) - 1)
165
+ mQ = mQ[idx]
166
+ mQ = copy_utils.offset_ragged_tensor(
167
+ mQ, offset_q, self.seqlen_q, ragged_dim=1, ptr_shift=True
168
+ )
169
+ return cute.group_modes(mQ, 0, 2)
170
+
171
+ def offset_batch_K(
172
+ self,
173
+ mK: cute.Tensor,
174
+ batch_idx: Int32,
175
+ dim: int,
176
+ padded: cutlass.Constexpr[bool] = False,
177
+ ragged: cutlass.Constexpr[bool] = False,
178
+ multiple: int = 1,
179
+ ) -> cute.Tensor:
180
+ """Seqlen must be the first dimension of mK"""
181
+ if const_expr(not ragged):
182
+ if const_expr(not self.has_cu_seqlens_k):
183
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mK) - 1 - dim)
184
+ return mK[idx]
185
+ else:
186
+ offset_k = self.offset_k if const_expr(not padded) else self.padded_offset_k
187
+ offset_k *= multiple
188
+ idx = (offset_k,) + (None,) * (cute.rank(mK) - 1)
189
+ return cute.domain_offset(idx, mK)
190
+ else:
191
+ if const_expr(not self.has_cu_seqlens_k):
192
+ offset_k = 0
193
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mK) - 1 - dim)
194
+ mK = mK[idx]
195
+ else:
196
+ offset_k = self.offset_k if const_expr(not padded) else self.padded_offset_k
197
+ offset_k *= multiple
198
+ return copy_utils.offset_ragged_tensor(
199
+ mK, offset_k, self.seqlen_k, ragged_dim=0, ptr_shift=True
200
+ )
201
+
202
+
203
+ @dataclass(frozen=True)
204
+ class SeqlenInfoQKNewK:
205
+ """Sequence length info for append-KV with left-padding and new K support.
206
+
207
+ Extends SeqlenInfoQK with:
208
+ - leftpad_k: left padding for K (tokens to skip at the start of the KV cache)
209
+ - offset_k_new: offset into the new K tensor
210
+ - seqlen_k_og: original K length (before appending new K), excluding leftpad
211
+ - seqlen_k_new: length of new K to append
212
+ - seqlen_k: total K length (seqlen_k_og + seqlen_k_new)
213
+ - seqlen_rotary: position for rotary embedding computation
214
+ """
215
+
216
+ leftpad_k: Int32
217
+ offset_q: Int32
218
+ offset_k: Int32
219
+ offset_k_new: Int32
220
+ seqlen_q: Int32
221
+ seqlen_k_og: Int32
222
+ seqlen_k_new: Int32
223
+ seqlen_k: Int32
224
+ seqlen_rotary: Int32
225
+
226
+ @staticmethod
227
+ def create(
228
+ batch_idx: Int32,
229
+ seqlen_q_static: Int32,
230
+ seqlen_k_static: Int32,
231
+ shape_K_new_0: Int32,
232
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
233
+ mCuSeqlensK: Optional[cute.Tensor] = None,
234
+ mCuSeqlensKNew: Optional[cute.Tensor] = None,
235
+ mSeqUsedQ: Optional[cute.Tensor] = None,
236
+ mSeqUsedK: Optional[cute.Tensor] = None,
237
+ mLeftpadK: Optional[cute.Tensor] = None,
238
+ mSeqlensRotary: Optional[cute.Tensor] = None,
239
+ ):
240
+ leftpad_k = 0 if const_expr(mLeftpadK is None) else mLeftpadK[batch_idx]
241
+ offset_q = 0 if const_expr(mCuSeqlensQ is None) else mCuSeqlensQ[batch_idx]
242
+ if const_expr(mCuSeqlensK is not None):
243
+ offset_k = mCuSeqlensK[batch_idx] + leftpad_k
244
+ else:
245
+ offset_k = leftpad_k if const_expr(mCuSeqlensQ is not None) else 0
246
+ offset_k_new = 0 if const_expr(mCuSeqlensKNew is None) else mCuSeqlensKNew[batch_idx]
247
+ # seqlen_q
248
+ if const_expr(mSeqUsedQ is not None):
249
+ seqlen_q = mSeqUsedQ[batch_idx]
250
+ elif const_expr(mCuSeqlensQ is not None):
251
+ seqlen_q = mCuSeqlensQ[batch_idx + 1] - mCuSeqlensQ[batch_idx]
252
+ else:
253
+ seqlen_q = seqlen_q_static
254
+ # seqlen_k_og: original K length (excluding leftpad)
255
+ if const_expr(mSeqUsedK is not None):
256
+ seqlen_k_og = mSeqUsedK[batch_idx] - leftpad_k
257
+ elif const_expr(mCuSeqlensK is not None):
258
+ seqlen_k_og = mCuSeqlensK[batch_idx + 1] - mCuSeqlensK[batch_idx] - leftpad_k
259
+ else:
260
+ seqlen_k_og = (
261
+ seqlen_k_static - leftpad_k
262
+ if const_expr(mCuSeqlensQ is not None)
263
+ else seqlen_k_static
264
+ )
265
+ # seqlen_k_new
266
+ if const_expr(mCuSeqlensKNew is None):
267
+ seqlen_k_new = 0 if const_expr(mCuSeqlensQ is None) else shape_K_new_0
268
+ else:
269
+ seqlen_k_new = mCuSeqlensKNew[batch_idx + 1] - mCuSeqlensKNew[batch_idx]
270
+ seqlen_k = seqlen_k_og if const_expr(mCuSeqlensQ is None) else seqlen_k_og + seqlen_k_new
271
+
272
+ # seqlen_rotary: defaults to seqlen_k_og + leftpad_k unless explicitly provided
273
+ if const_expr(mSeqlensRotary is not None):
274
+ seqlen_rotary = mSeqlensRotary[batch_idx]
275
+ else:
276
+ seqlen_rotary = seqlen_k_og + leftpad_k
277
+ return SeqlenInfoQKNewK(
278
+ leftpad_k,
279
+ offset_q,
280
+ offset_k,
281
+ offset_k_new,
282
+ seqlen_q,
283
+ seqlen_k_og,
284
+ seqlen_k_new,
285
+ seqlen_k,
286
+ seqlen_rotary,
287
+ )
build/torch-cuda/flashrt_fa4/cute/sm100_hd256_2cta_fmha_forward.py ADDED
@@ -0,0 +1,1882 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Siyu Wang, Shengbin Di, Yuxi Chi, Johnsonms, Linfeng Zheng, Haoyan Huang, Lanbo Li, Yun Zhong, Man Yuan, Minmin Sun, Yong Li, Wei Lin.
2
+
3
+ import math
4
+ from typing import Tuple, Optional
5
+
6
+ import cuda.bindings.driver as cuda
7
+
8
+ import cutlass
9
+ import cutlass.cute as cute
10
+ import cutlass.cute.nvgpu.tcgen05 as tcgen05
11
+ import cutlass.utils as utils
12
+ import cutlass.pipeline as pipeline
13
+ import cutlass.utils.blackwell_helpers as sm100_utils
14
+ from cutlass.cute.typing import Int32, Int64, Float32
15
+
16
+ from cutlass.utils import ClcDynamicPersistentTileScheduler
17
+ from fa4_cute_runtime.flashrt_fa4.cute.tile_scheduler import (
18
+ ClcState,
19
+ compute_sm100_fmha_grid as compute_grid,
20
+ compute_sm100_fmha_grid_clc as compute_grid_clc,
21
+ make_sm100_thread_cooperative_group as make_thread_cooperative_group,
22
+ Sm100FmhaStaticTileScheduler as FmhaStaticTileScheduler,
23
+ Sm100FmhaStaticTileSchedulerParams as FmhaStaticTileSchedulerParams,
24
+ Sm100FmhaClcDynamicTileScheduler as FmhaClcDynamicTileScheduler,
25
+ Sm100FmhaClcDynamicTileSchedulerParams as FmhaClcDynamicTileSchedulerParams,
26
+ )
27
+ from fa4_cute_runtime.flashrt_fa4.cute.mask import (
28
+ Sm100FusedMask as FusedMask,
29
+ )
30
+ from fa4_cute_runtime.flashrt_fa4.cute.tile_scheduler import SM100_TMEM_CAPACITY_COLUMNS
31
+ from fa4_cute_runtime.flashrt_fa4.cute.flash_fwd_sm100 import DescaleTensors, _TUNING_CONFIG
32
+ from fa4_cute_runtime.flashrt_fa4.cute.utils import ex2_emulation_2
33
+
34
+
35
+ class BlackwellFusedMultiHeadAttentionForward:
36
+ def __init__(
37
+ self,
38
+ head_dim: int,
39
+ head_dim_v: Optional[int] = None,
40
+ qhead_per_kvhead: int = 1,
41
+ is_causal: bool = False,
42
+ is_local: bool = False,
43
+ is_split_kv: bool = False,
44
+ pack_gqa: bool = False,
45
+ q_subtile_factor: int | None = None,
46
+ m_block_size: int = 128,
47
+ n_block_size: int = 128,
48
+ q_stage: int = 2,
49
+ is_persistent: bool = True,
50
+ score_mod=None,
51
+ mask_mod=None,
52
+ has_aux_tensors: bool = False,
53
+ paged_kv_non_tma: bool = False,
54
+ is_varlen_q: bool = False,
55
+ use_2cta_instrs: bool = False,
56
+ use_clc_scheduler: bool = False,
57
+ ):
58
+ head_dim_v = head_dim if head_dim_v is None else head_dim_v
59
+ assert head_dim == 256 and head_dim_v == 256, (
60
+ "SM100 dedicated kernel only supports (head_dim, head_dim_v) = (256, 256)"
61
+ )
62
+ assert score_mod is None, "SM100 forward with head_dim=256 does not support score_mod"
63
+ assert mask_mod is None, "SM100 forward with head_dim=256 does not support mask_mod"
64
+ assert not has_aux_tensors, "SM100 forward with head_dim=256 does not support aux tensors"
65
+ assert not paged_kv_non_tma, (
66
+ "SM100 hd256 2CTA supports TMA paged KV only (page_size must equal tile_n=128)"
67
+ )
68
+ assert not pack_gqa, "SM100 forward with head_dim=256 does not support pack_gqa"
69
+ assert not is_split_kv, "SM100 forward with head_dim=256 does not support SplitKV"
70
+ assert q_subtile_factor is None, (
71
+ "SM100 forward with head_dim=256 does not support q_subtile_factor"
72
+ )
73
+ assert m_block_size == 128 and n_block_size == 128, (
74
+ "SM100 dedicated kernel only supports tile_m=128 and tile_n=128"
75
+ )
76
+ # q_stage / persistence / scheduler knobs are accepted for interface parity,
77
+ # but this dedicated kernel uses fixed internal settings.
78
+
79
+ qk_acc_dtype = cutlass.Float32
80
+ pv_acc_dtype = cutlass.Float32
81
+ mma_tiler = (128, 128, head_dim)
82
+ self.qk_acc_dtype = qk_acc_dtype
83
+ self.pv_acc_dtype = pv_acc_dtype
84
+ self.qhead_per_kvhead = qhead_per_kvhead
85
+ self.mma_tiler = mma_tiler
86
+ assert mma_tiler[0] == 128 and mma_tiler[1] == 128, "Only 128x128 tile impl is supported"
87
+ assert mma_tiler[2] == 256, "Only 256 is supported for 128x128 tile impl"
88
+ self.cta_tiler = (
89
+ mma_tiler[0],
90
+ mma_tiler[1],
91
+ mma_tiler[2],
92
+ )
93
+ self.qk_mma_tiler = (
94
+ 2 * mma_tiler[0],
95
+ mma_tiler[1],
96
+ min(self.cta_tiler[2], 128),
97
+ )
98
+ self.pv_mma_tiler = self.qk_mma_tiler
99
+ self.pv_block_tiler = (
100
+ self.pv_mma_tiler[0] // 2,
101
+ self.pv_mma_tiler[1],
102
+ self.pv_mma_tiler[2],
103
+ )
104
+ self.iterations_qk = self.cta_tiler[2] // self.qk_mma_tiler[2]
105
+ self.iterations_pv = self.cta_tiler[2] // self.pv_mma_tiler[1]
106
+ self.cluster_shape_mn = (2, 1)
107
+ self.tmem_warp_shape_mn = (4, 1)
108
+ # Dedicated hd256 kernel uses fixed scheduling policy.
109
+ self.is_persistent = False
110
+ self.is_causal = is_causal
111
+ self.is_local = is_local
112
+ self.use_semantic_trip_range = is_causal or is_local
113
+ self.use_clc_scheduler = False
114
+
115
+ self.softmax_warp_ids = (0, 1, 2, 3)
116
+ self.correction_warp_ids = (4, 5, 6, 7)
117
+ self.mma_warp_id = 8
118
+ self.load_warp_id = 9
119
+ self.empty_warp_id = (10, 11)
120
+ self.sched_warp_id = self.empty_warp_id[0] if use_clc_scheduler else None
121
+ self.tmem_alloc_cols = SM100_TMEM_CAPACITY_COLUMNS
122
+
123
+ self.threads_per_warp = 32
124
+ self.threads_per_cta = self.threads_per_warp * len(
125
+ (
126
+ *self.softmax_warp_ids, # this is to get a round num threads
127
+ *self.correction_warp_ids,
128
+ self.mma_warp_id,
129
+ self.load_warp_id,
130
+ *self.empty_warp_id,
131
+ )
132
+ )
133
+
134
+ self.tmem_alloc_barrier = pipeline.NamedBarrier(
135
+ barrier_id=1,
136
+ num_threads=self.threads_per_cta,
137
+ )
138
+
139
+ self.tmem_s_offset = 0
140
+ self.tmem_o_offset = 256
141
+ self.tmem_p_offset = self.tmem_s_offset
142
+
143
+ _tune_key = (True, is_causal, 256, False) # hd256: always 2cta, no sm103 variant
144
+ _tune = _TUNING_CONFIG.get(_tune_key, {})
145
+ self.num_regs_softmax = _tune.get("num_regs_softmax", 256)
146
+ self.num_regs_correction = _tune.get("num_regs_correction", 160)
147
+ self.num_regs_other = 32 # fixed for hd256; not derived from 512 budget like other kernels
148
+ self.ex2_emu_freq = _tune.get("ex2_emu_freq", 4)
149
+ self.ex2_emu_res = _tune.get("ex2_emu_res", 3)
150
+ self.ex2_emu_start_frg = _tune.get("ex2_emu_start_frg", 0)
151
+
152
+ self.buffer_align_bytes = 1024
153
+
154
+ def _setup_attributes(self):
155
+ self.q_stage = self.iterations_qk
156
+ self.kv_stage = 4
157
+ self.qk_acc_stage = 2
158
+ self.mma_corr_stage = 1
159
+ if cutlass.const_expr(self.use_clc_scheduler):
160
+ self.num_clc_stage = 1
161
+ self.num_clc_response_bytes = 16
162
+
163
+ @cute.jit
164
+ def __call__(
165
+ self,
166
+ mQ: cute.Tensor,
167
+ mK: cute.Tensor,
168
+ mV: cute.Tensor,
169
+ mO: cute.Tensor,
170
+ mLSE: Optional[cute.Tensor],
171
+ softmax_scale: Float32,
172
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
173
+ mCuSeqlensK: Optional[cute.Tensor] = None,
174
+ mSeqUsedQ: Optional[cute.Tensor] = None,
175
+ mSeqUsedK: Optional[cute.Tensor] = None,
176
+ mPageTable: Optional[cute.Tensor] = None,
177
+ window_size_left: Int32 | int | None = None,
178
+ window_size_right: Int32 | int | None = None,
179
+ learnable_sink: Optional[cute.Tensor] = None,
180
+ descale_tensors: Optional[DescaleTensors] = None,
181
+ blocksparse_tensors: Optional[cute.Tensor] = None,
182
+ aux_tensors: Optional[list] = None,
183
+ stream: cuda.CUstream = None,
184
+ ):
185
+ # Keep parity with FlashAttentionForwardSm100.__call__ interface.
186
+ # (TODO@wangsiyu) Implement these features.
187
+ assert mSeqUsedQ is None, (
188
+ "SM100 forward with head_dim=256 does not support seqused_q"
189
+ )
190
+ assert learnable_sink is None, (
191
+ "SM100 forward with head_dim=256 does not support learnable_sink"
192
+ )
193
+ assert blocksparse_tensors is None, (
194
+ "SM100 forward with head_dim=256 does not support block sparsity"
195
+ )
196
+ assert aux_tensors is None, "SM100 forward with head_dim=256 does not support aux_tensors"
197
+ assert not self.is_local, (
198
+ "SM100 forward with head_dim=256 does not support local attention yet"
199
+ )
200
+ assert window_size_left is None and window_size_right is None, (
201
+ "SM100 forward with head_dim=256 does not support runtime window_size overrides"
202
+ )
203
+ assert descale_tensors is None, (
204
+ "SM100 forward with head_dim=256 does not support descale_tensors"
205
+ )
206
+
207
+ q_tensor, k_tensor, v_tensor, o_tensor = mQ, mK, mV, mO
208
+ lse_tensor = mLSE
209
+ cum_seqlen_q = mCuSeqlensQ
210
+ cum_seqlen_k = mCuSeqlensK
211
+
212
+ q_rank = len(mQ.shape)
213
+ k_rank = len(mK.shape)
214
+ if cutlass.const_expr(cum_seqlen_q is not None):
215
+ # Varlen path accepts either legacy 5D tensors or standard 3D tensors.
216
+ if cutlass.const_expr(q_rank == 5):
217
+ s_q = mQ.shape[1]
218
+ h_q = mQ.shape[2] * mQ.shape[3]
219
+ d = mQ.shape[4]
220
+ elif cutlass.const_expr(q_rank == 3):
221
+ s_q = mQ.shape[0]
222
+ h_q = mQ.shape[1]
223
+ d = mQ.shape[2]
224
+ else:
225
+ raise RuntimeError(f"hd256 forward varlen expects q rank 3 or 5, got rank {q_rank}")
226
+ else:
227
+ # Non-varlen path accepts either legacy 5D tensors or standard 4D tensors.
228
+ if cutlass.const_expr(q_rank == 5):
229
+ s_q = mQ.shape[1]
230
+ h_q = mQ.shape[2] * mQ.shape[3]
231
+ d = mQ.shape[4]
232
+ elif cutlass.const_expr(q_rank == 4):
233
+ s_q = mQ.shape[1]
234
+ h_q = mQ.shape[2]
235
+ d = mQ.shape[3]
236
+ else:
237
+ raise RuntimeError(
238
+ f"hd256 forward non-varlen expects q rank 4 or 5, got rank {q_rank}"
239
+ )
240
+
241
+ if cutlass.const_expr(cum_seqlen_k is not None):
242
+ if cutlass.const_expr(k_rank == 5):
243
+ s_k = mK.shape[1]
244
+ h_k = mK.shape[2]
245
+ elif cutlass.const_expr(k_rank == 3):
246
+ s_k = mK.shape[0]
247
+ h_k = mK.shape[1]
248
+ else:
249
+ raise RuntimeError(f"hd256 forward varlen expects k rank 3 or 5, got rank {k_rank}")
250
+ else:
251
+ if cutlass.const_expr(k_rank == 5):
252
+ s_k = mK.shape[1]
253
+ h_k = mK.shape[2]
254
+ elif cutlass.const_expr(k_rank == 4):
255
+ s_k = mK.shape[1]
256
+ h_k = mK.shape[2]
257
+ else:
258
+ raise RuntimeError(
259
+ f"hd256 forward non-varlen expects k rank 4 or 5, got rank {k_rank}"
260
+ )
261
+ if cutlass.const_expr(cum_seqlen_q is not None):
262
+ b = mCuSeqlensQ.shape[0] - 1
263
+ elif cutlass.const_expr(cum_seqlen_k is not None):
264
+ b = mCuSeqlensK.shape[0] - 1
265
+ else:
266
+ b = mQ.shape[0]
267
+
268
+ scale_softmax = softmax_scale
269
+ scale_softmax_log2 = softmax_scale * math.log2(math.exp(1.0))
270
+ scale_output = 1.0
271
+ s_lse = s_q
272
+ h_r = h_q // h_k
273
+ s_q64 = Int64(s_q)
274
+ s_k64 = Int64(s_k)
275
+ s_lse64 = Int64(s_lse)
276
+ d64 = cute.assume(Int64(d), divby=128)
277
+ h_r64 = Int64(h_r)
278
+ h_k64 = Int64(h_k)
279
+ b64 = Int64(b)
280
+ s_q_total = (
281
+ q_tensor.shape[1]
282
+ if cum_seqlen_q is not None and q_rank == 5
283
+ else (q_tensor.shape[0] if cum_seqlen_q is not None else s_q64)
284
+ )
285
+ s_k_total = (
286
+ k_tensor.shape[1]
287
+ if cum_seqlen_k is not None and k_rank == 5
288
+ else (k_tensor.shape[0] if cum_seqlen_k is not None else s_k64)
289
+ )
290
+ stride_b_qo = h_r64 * h_k64 * s_q64 * d64 if cum_seqlen_q is None else 0
291
+ stride_b_kv = h_k64 * s_k64 * d64 if cum_seqlen_k is None else 0
292
+ b_lse = b64 if cum_seqlen_q is None else 1
293
+ stride_b_lse = h_r64 * h_k64 * s_lse64 if cum_seqlen_q is None else 0
294
+
295
+ # (s, d, ((h_r, h_k), b))
296
+ q_layout = cute.make_layout(
297
+ (s_q_total, d, ((h_r, h_k), b)),
298
+ stride=(d64 * h_r64 * h_k64, 1, ((d64, d64 * h_r64), stride_b_qo)),
299
+ )
300
+ q = cute.make_tensor(q_tensor.iterator, q_layout)
301
+ if cutlass.const_expr(mPageTable is not None):
302
+ # Paged: K layout (num_pages, page_size, h_k, d); page_table maps kv_coord→physical page.
303
+ num_pages = k_tensor.shape[0]
304
+ page_size = k_tensor.shape[1]
305
+ page_size64 = Int64(page_size)
306
+ max_seqlen_k_paged = Int32(mPageTable.shape[1] * page_size)
307
+ k_paged_layout = cute.make_layout(
308
+ (page_size, d, h_k, num_pages),
309
+ stride=(d64 * h_k64, 1, d64, page_size64 * d64 * h_k64),
310
+ )
311
+ k = cute.make_tensor(k_tensor.iterator, k_paged_layout)
312
+ v_paged_layout = cute.make_layout(
313
+ (d, page_size, h_k, num_pages),
314
+ stride=(1, d64 * h_k64, d64, page_size64 * d64 * h_k64),
315
+ )
316
+ v = cute.make_tensor(v_tensor.iterator, v_paged_layout)
317
+ page_table_layout = cute.make_layout(
318
+ (b, mPageTable.shape[1]),
319
+ stride=(Int64(mPageTable.shape[1]), 1),
320
+ )
321
+ page_table = cute.make_tensor(mPageTable.iterator, page_table_layout)
322
+ else:
323
+ # (s, d, ((h_r, h_k), b)), 0-stride for h_r to broadcast
324
+ k_layout = cute.make_layout(
325
+ (s_k_total, d, ((h_r, h_k), b)),
326
+ stride=(d64 * h_k64, 1, ((0, d64), stride_b_kv)),
327
+ )
328
+ k = cute.make_tensor(k_tensor.iterator, k_layout)
329
+ # (d, s, ((h_r, h_k), b)), 0-stride for h_r to broadcast
330
+ v_layout = cute.make_layout(
331
+ (d, s_k_total, ((h_r, h_k), b)),
332
+ stride=(1, d64 * h_k64, ((0, d64), stride_b_kv)),
333
+ )
334
+ v = cute.make_tensor(v_tensor.iterator, v_layout)
335
+ page_table = None
336
+ max_seqlen_k_paged = None
337
+ # (s, d, ((h_r, h_k), b))
338
+ o_layout = cute.make_layout(
339
+ (s_q_total, d, ((h_r, h_k), b)),
340
+ stride=(d64 * h_r64 * h_k64, 1, ((d64, d64 * h_r64), stride_b_qo)),
341
+ )
342
+ o = cute.make_tensor(o_tensor.iterator, o_layout)
343
+ if cutlass.const_expr(lse_tensor is not None):
344
+ # (s, ((h_r, h_k), b))
345
+ lse_layout = cute.make_layout(
346
+ (s_lse64, ((h_r, h_k), b_lse)),
347
+ stride=(1, ((s_lse64, h_r64 * s_lse64), stride_b_lse)),
348
+ )
349
+ lse = cute.make_tensor(lse_tensor.iterator, lse_layout)
350
+ else:
351
+ lse = None
352
+
353
+ # setup static attributes before smem/grid/tma computation
354
+ self.q_dtype = q.element_type
355
+ self.k_dtype = k.element_type
356
+ self.v_dtype = v.element_type
357
+ self.o_dtype = o.element_type
358
+ self.tilePlikeFP32 = self.qk_mma_tiler[1] // Float32.width * self.q_dtype.width
359
+
360
+ if cutlass.const_expr(self.use_clc_scheduler):
361
+ self.tile_sched_params, grid = compute_grid_clc(
362
+ (s_q, o.shape[1], o.shape[2]) if cum_seqlen_q is not None else o.shape,
363
+ self.cta_tiler,
364
+ (*self.cluster_shape_mn, 1),
365
+ )
366
+ else:
367
+ self.tile_sched_params, grid = compute_grid(
368
+ (s_q, o.shape[1], o.shape[2]) if cum_seqlen_q is not None else o.shape,
369
+ self.cta_tiler,
370
+ self.is_persistent,
371
+ )
372
+
373
+ self.q_major_mode = utils.LayoutEnum.from_tensor(q).mma_major_mode()
374
+ self.k_major_mode = utils.LayoutEnum.from_tensor(k).mma_major_mode()
375
+ self.v_major_mode = utils.LayoutEnum.from_tensor(v).mma_major_mode()
376
+ self.o_layout = utils.LayoutEnum.from_tensor(o)
377
+
378
+ if cutlass.const_expr(self.q_major_mode != tcgen05.OperandMajorMode.K):
379
+ raise RuntimeError("The layout of q is not supported")
380
+ if cutlass.const_expr(self.k_major_mode != tcgen05.OperandMajorMode.K):
381
+ raise RuntimeError("The layout of k is not supported")
382
+ if cutlass.const_expr(self.v_major_mode != tcgen05.OperandMajorMode.MN):
383
+ raise RuntimeError("The layout of v is not supported")
384
+
385
+ # check type consistency
386
+ if cutlass.const_expr(self.q_dtype != self.k_dtype):
387
+ raise TypeError(f"Type mismatch: {self.q_dtype} != {self.k_dtype}")
388
+ if cutlass.const_expr(self.q_dtype != self.v_dtype):
389
+ raise TypeError(f"Type mismatch: {self.q_dtype} != {self.v_dtype}")
390
+ self._setup_attributes()
391
+
392
+ cta_group = tcgen05.CtaGroup.TWO
393
+ # the intermediate tensor p is from tmem & k-major
394
+ p_source = tcgen05.OperandSource.TMEM
395
+ p_major_mode = tcgen05.OperandMajorMode.K
396
+ qk_tiled_mma = sm100_utils.make_trivial_tiled_mma(
397
+ self.q_dtype,
398
+ self.q_major_mode,
399
+ self.k_major_mode,
400
+ self.qk_acc_dtype,
401
+ cta_group,
402
+ self.qk_mma_tiler[:2],
403
+ )
404
+ pv_tiled_mma = sm100_utils.make_trivial_tiled_mma(
405
+ self.v_dtype,
406
+ p_major_mode,
407
+ self.v_major_mode,
408
+ self.pv_acc_dtype,
409
+ cta_group,
410
+ self.pv_mma_tiler[:2],
411
+ p_source,
412
+ )
413
+
414
+ self.cluster_shape_mnk = (*self.cluster_shape_mn, 1)
415
+ self.cluster_layout_vmnk = cute.tiled_divide(
416
+ cute.make_layout(self.cluster_shape_mnk),
417
+ (qk_tiled_mma.thr_id.shape,),
418
+ )
419
+
420
+ self.epi_tile = self.pv_block_tiler[:2]
421
+
422
+ q_smem_layout_staged = sm100_utils.make_smem_layout_a(
423
+ qk_tiled_mma,
424
+ self.qk_mma_tiler,
425
+ self.q_dtype,
426
+ self.q_stage,
427
+ )
428
+ k_smem_layout_staged = sm100_utils.make_smem_layout_b(
429
+ qk_tiled_mma,
430
+ self.qk_mma_tiler,
431
+ self.k_dtype,
432
+ self.kv_stage,
433
+ )
434
+ p_tmem_layout_staged = sm100_utils.make_smem_layout_a(
435
+ pv_tiled_mma,
436
+ self.pv_mma_tiler,
437
+ self.q_dtype,
438
+ self.qk_acc_stage,
439
+ )
440
+ p_tmem_layout = cute.select(p_tmem_layout_staged, mode=[0, 1, 2])
441
+ v_smem_layout_staged = sm100_utils.make_smem_layout_b(
442
+ pv_tiled_mma,
443
+ self.pv_mma_tiler,
444
+ self.v_dtype,
445
+ self.kv_stage,
446
+ )
447
+ # TMA load for Q
448
+ tma_load_op = cute.nvgpu.cpasync.CopyBulkTensorTileG2SOp(cta_group)
449
+
450
+ q_smem_layout = cute.select(q_smem_layout_staged, mode=[0, 1, 2])
451
+ tma_atom_q, tma_tensor_q = cute.nvgpu.make_tiled_tma_atom_A(
452
+ tma_load_op,
453
+ q,
454
+ q_smem_layout,
455
+ self.qk_mma_tiler,
456
+ qk_tiled_mma,
457
+ self.cluster_layout_vmnk.shape,
458
+ )
459
+
460
+ # TMA load for K
461
+ k_smem_layout = cute.select(k_smem_layout_staged, mode=[0, 1, 2])
462
+ tma_atom_k, tma_tensor_k = cute.nvgpu.make_tiled_tma_atom_B(
463
+ tma_load_op,
464
+ k,
465
+ k_smem_layout,
466
+ self.qk_mma_tiler,
467
+ qk_tiled_mma,
468
+ self.cluster_layout_vmnk.shape,
469
+ )
470
+ # TMA load for V
471
+ v_smem_layout = cute.select(v_smem_layout_staged, mode=[0, 1, 2])
472
+ tma_atom_v, tma_tensor_v = cute.nvgpu.make_tiled_tma_atom_B(
473
+ tma_load_op,
474
+ v,
475
+ v_smem_layout,
476
+ self.pv_mma_tiler,
477
+ pv_tiled_mma,
478
+ self.cluster_layout_vmnk.shape,
479
+ )
480
+
481
+ q_copy_size = cute.size_in_bytes(self.q_dtype, q_smem_layout)
482
+ k_copy_size = cute.size_in_bytes(self.k_dtype, k_smem_layout)
483
+ self.tma_copy_q_bytes = q_copy_size * cute.size(qk_tiled_mma.thr_id.shape)
484
+ self.tma_copy_kv_bytes = k_copy_size * cute.size(qk_tiled_mma.thr_id.shape)
485
+
486
+ @cute.struct
487
+ class SharedStorage:
488
+ # TMA G2S load barriers: LOAD warp (producer) -> MMA warp (consumer)
489
+ load_q_mbar_ptr: cute.struct.MemRange[
490
+ Int64, self.q_stage * 2
491
+ ] # load_q_{producer,consumer}
492
+ load_kv_mbar_ptr: cute.struct.MemRange[
493
+ Int64, self.kv_stage * 2
494
+ ] # load_kv_{producer,consumer}
495
+ mma_s_mbar_ptr: cute.struct.MemRange[Int64, self.qk_acc_stage * 2]
496
+ p_mma_mbar_ptr: cute.struct.MemRange[Int64, self.qk_acc_stage * 2]
497
+ # Softmax -> Correction signaling barriers (row_max/row_sum vec ready)
498
+ s_corr_mbar_ptr: cute.struct.MemRange[
499
+ Int64, self.qk_acc_stage * 2
500
+ ] # s_corr_{producer,consumer}
501
+ sum_mbar_ptr: cute.struct.MemRange[Int64, 2]
502
+ # MMA -> Correction ownership barriers for O_partial tokens (online rescale/finalize)
503
+ mma_corr_mbar_ptr: cute.struct.MemRange[
504
+ Int64, self.mma_corr_stage * 2
505
+ ] # mma_corr_{producer,consumer}
506
+ # A CTA-wide "TMEM lifetime" barrier used to safely deallocate TMEM after all users finish.
507
+ tmem_dealloc_mbar_ptr: Int64
508
+ # Tmem holding buffer
509
+ tmem_holding_buf: Int32
510
+ # CLC pipeline barriers and response buffer
511
+ clc_mbar_ptr: cute.struct.MemRange[Int64, 2]
512
+ clc_response: cute.struct.MemRange[Int32, 4]
513
+
514
+ self.shared_storage = SharedStorage
515
+
516
+ grid = cute.round_up(grid, self.cluster_shape_mnk)
517
+ # Launch the kernel synchronously
518
+ self.kernel(
519
+ qk_tiled_mma,
520
+ pv_tiled_mma,
521
+ tma_atom_q,
522
+ tma_tensor_q,
523
+ tma_atom_k,
524
+ tma_tensor_k,
525
+ tma_atom_v,
526
+ tma_tensor_v,
527
+ o,
528
+ cum_seqlen_q,
529
+ cum_seqlen_k,
530
+ mSeqUsedK,
531
+ lse,
532
+ scale_softmax_log2,
533
+ scale_softmax,
534
+ scale_output,
535
+ page_table,
536
+ max_seqlen_k_paged,
537
+ window_size_left,
538
+ window_size_right,
539
+ self.cluster_layout_vmnk,
540
+ q_smem_layout_staged,
541
+ k_smem_layout_staged,
542
+ p_tmem_layout,
543
+ v_smem_layout_staged,
544
+ self.tile_sched_params,
545
+ ).launch(
546
+ grid=grid,
547
+ block=[self.threads_per_cta, 1, 1],
548
+ cluster=self.cluster_shape_mnk,
549
+ stream=stream,
550
+ min_blocks_per_mp=1,
551
+ )
552
+
553
+ # GPU device kernel
554
+ @cute.kernel
555
+ def kernel(
556
+ self,
557
+ qk_tiled_mma: cute.TiledMma,
558
+ pv_tiled_mma: cute.TiledMma,
559
+ tma_atom_q: cute.CopyAtom,
560
+ mQ_qdl: cute.Tensor,
561
+ tma_atom_k: cute.CopyAtom,
562
+ mK_kdl: cute.Tensor,
563
+ tma_atom_v: cute.CopyAtom,
564
+ mV_dkl: cute.Tensor,
565
+ mO_qdl: cute.Tensor,
566
+ cum_seqlen_q: Optional[cute.Tensor],
567
+ cum_seqlen_k: Optional[cute.Tensor],
568
+ mSeqUsedK: Optional[cute.Tensor],
569
+ mLSE: Optional[cute.Tensor],
570
+ scale_softmax_log2: Float32,
571
+ scale_softmax: Float32,
572
+ scale_output: Float32,
573
+ mPageTable: Optional[cute.Tensor],
574
+ max_seqlen_k: Optional[Int32],
575
+ window_size_left: Optional[Int32],
576
+ window_size_right: Optional[Int32],
577
+ cluster_layout_vmnk: cute.Layout,
578
+ q_smem_layout_staged: cute.ComposedLayout,
579
+ k_smem_layout_staged: cute.ComposedLayout,
580
+ p_tmem_layout_staged: cute.ComposedLayout,
581
+ v_smem_layout_staged: cute.ComposedLayout,
582
+ tile_sched_params: FmhaStaticTileSchedulerParams | FmhaClcDynamicTileSchedulerParams,
583
+ ):
584
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
585
+
586
+ #
587
+ # Prefetch tma desc
588
+ #
589
+ if warp_idx == self.load_warp_id:
590
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_q)
591
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_k)
592
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_v)
593
+
594
+ bidx, _, _ = cute.arch.block_idx()
595
+ mma_tile_coord_v = bidx % cute.size(qk_tiled_mma.thr_id.shape)
596
+ cta_rank_in_cluster = cute.arch.make_warp_uniform(cute.arch.block_idx_in_cluster())
597
+ block_in_cluster_coord_vmnk = cluster_layout_vmnk.get_flat_coord(cta_rank_in_cluster)
598
+
599
+ # Alloc
600
+ smem = utils.SmemAllocator()
601
+ storage = smem.allocate(self.shared_storage)
602
+
603
+ load_q_producer, load_q_consumer = pipeline.PipelineTmaUmma.create(
604
+ num_stages=self.q_stage,
605
+ producer_group=make_thread_cooperative_group(len([self.load_warp_id])),
606
+ consumer_group=make_thread_cooperative_group(len([self.mma_warp_id])),
607
+ tx_count=self.tma_copy_q_bytes,
608
+ barrier_storage=storage.load_q_mbar_ptr.data_ptr(),
609
+ cta_layout_vmnk=cluster_layout_vmnk,
610
+ defer_sync=True,
611
+ ).make_participants()
612
+ load_kv_producer, load_kv_consumer = pipeline.PipelineTmaUmma.create(
613
+ num_stages=self.kv_stage,
614
+ producer_group=make_thread_cooperative_group(len([self.load_warp_id])),
615
+ consumer_group=make_thread_cooperative_group(len([self.mma_warp_id])),
616
+ tx_count=self.tma_copy_kv_bytes,
617
+ barrier_storage=storage.load_kv_mbar_ptr.data_ptr(),
618
+ cta_layout_vmnk=cluster_layout_vmnk,
619
+ defer_sync=True,
620
+ ).make_participants()
621
+ mma_s_producer, mma_s_consumer = pipeline.PipelineUmmaAsync.create(
622
+ num_stages=self.qk_acc_stage,
623
+ producer_group=make_thread_cooperative_group(len([self.mma_warp_id])),
624
+ consumer_group=make_thread_cooperative_group(
625
+ len(self.softmax_warp_ids) * self.threads_per_warp * self.cluster_shape_mnk[0],
626
+ ),
627
+ barrier_storage=storage.mma_s_mbar_ptr.data_ptr(),
628
+ cta_layout_vmnk=cluster_layout_vmnk,
629
+ defer_sync=True,
630
+ ).make_participants()
631
+ p_mma_producer, p_mma_consumer = pipeline.PipelineAsyncUmma.create(
632
+ num_stages=self.qk_acc_stage,
633
+ producer_group=make_thread_cooperative_group(
634
+ len(self.softmax_warp_ids) * self.threads_per_warp * self.cluster_shape_mnk[0],
635
+ ),
636
+ consumer_group=make_thread_cooperative_group(len([self.mma_warp_id])),
637
+ barrier_storage=storage.p_mma_mbar_ptr.data_ptr(),
638
+ cta_layout_vmnk=cluster_layout_vmnk,
639
+ defer_sync=True,
640
+ ).make_participants()
641
+ s_corr_producer, s_corr_consumer = pipeline.PipelineAsync.create(
642
+ num_stages=self.qk_acc_stage,
643
+ producer_group=make_thread_cooperative_group(
644
+ self.threads_per_warp * len(self.softmax_warp_ids)
645
+ ),
646
+ consumer_group=make_thread_cooperative_group(
647
+ self.threads_per_warp * len(self.correction_warp_ids)
648
+ ),
649
+ barrier_storage=storage.s_corr_mbar_ptr.data_ptr(),
650
+ defer_sync=True,
651
+ ).make_participants()
652
+ sum_producer, sum_consumer = pipeline.PipelineAsync.create(
653
+ num_stages=1,
654
+ producer_group=make_thread_cooperative_group(
655
+ self.threads_per_warp * len(self.softmax_warp_ids)
656
+ ),
657
+ consumer_group=make_thread_cooperative_group(
658
+ self.threads_per_warp * len(self.correction_warp_ids)
659
+ ),
660
+ barrier_storage=storage.sum_mbar_ptr.data_ptr(),
661
+ defer_sync=True,
662
+ ).make_participants()
663
+ mma_corr_producer, mma_corr_consumer = pipeline.PipelineUmmaAsync.create(
664
+ num_stages=self.mma_corr_stage,
665
+ producer_group=make_thread_cooperative_group(len([self.mma_warp_id])),
666
+ consumer_group=make_thread_cooperative_group(
667
+ len(self.correction_warp_ids) * self.threads_per_warp * self.cluster_shape_mnk[0],
668
+ ),
669
+ barrier_storage=storage.mma_corr_mbar_ptr.data_ptr(),
670
+ cta_layout_vmnk=cluster_layout_vmnk,
671
+ defer_sync=True,
672
+ ).make_participants()
673
+ # Tensor memory dealloc barrier init
674
+ tmem = utils.TmemAllocator(
675
+ storage.tmem_holding_buf,
676
+ barrier_for_retrieve=self.tmem_alloc_barrier,
677
+ allocator_warp_id=self.correction_warp_ids[0],
678
+ is_two_cta=True,
679
+ two_cta_tmem_dealloc_mbar_ptr=storage.tmem_dealloc_mbar_ptr,
680
+ )
681
+ tmem.allocate(self.tmem_alloc_cols)
682
+ tmem.wait_for_alloc()
683
+ tmem_ptr = tmem.retrieve_ptr(self.qk_acc_dtype)
684
+ # Initialize CLC state if using dynamic scheduler
685
+ if cutlass.const_expr(self.use_clc_scheduler):
686
+ clc_pipeline_producer_group = pipeline.CooperativeGroup(pipeline.Agent.Thread)
687
+ cluster_size = cute.size(self.cluster_shape_mnk)
688
+ num_clc_consumer_threads = self.threads_per_warp * (
689
+ 1 # sched_warp (CTA 0 only)
690
+ + cluster_size
691
+ * (
692
+ len(self.softmax_warp_ids)
693
+ + len(self.correction_warp_ids)
694
+ + 1 # mma_warp
695
+ + 1 # load_warp
696
+ )
697
+ )
698
+ clc_pipeline_consumer_group = pipeline.CooperativeGroup(
699
+ pipeline.Agent.Thread, num_clc_consumer_threads
700
+ )
701
+ clc_response_ptr = storage.clc_response.data_ptr()
702
+ clc = ClcState.create(
703
+ hw_scheduler=ClcDynamicPersistentTileScheduler.create(
704
+ self.tile_sched_params.clc_hw_params(),
705
+ cute.arch.block_idx(),
706
+ cute.arch.grid_dim(),
707
+ clc_response_ptr,
708
+ ),
709
+ pipeline=pipeline.PipelineClcFetchAsync.create(
710
+ barrier_storage=storage.clc_mbar_ptr.data_ptr(),
711
+ num_stages=self.num_clc_stage,
712
+ producer_group=clc_pipeline_producer_group,
713
+ consumer_group=clc_pipeline_consumer_group,
714
+ tx_count=self.num_clc_response_bytes,
715
+ cta_layout_vmnk=cluster_layout_vmnk,
716
+ defer_sync=True,
717
+ ),
718
+ consumer_state=pipeline.make_pipeline_state(
719
+ pipeline.PipelineUserType.Consumer, self.num_clc_stage
720
+ ),
721
+ producer_state=pipeline.make_pipeline_state(
722
+ pipeline.PipelineUserType.Producer, self.num_clc_stage
723
+ ),
724
+ )
725
+ else:
726
+ clc = None
727
+ clc_response_ptr = None
728
+
729
+ # Cluster arrive after barrier init
730
+ pipeline.pipeline_init_arrive(cluster_shape_mn=cluster_layout_vmnk, is_relaxed=True)
731
+
732
+ sQ = smem.allocate_tensor(
733
+ element_type=self.q_dtype,
734
+ layout=q_smem_layout_staged.outer,
735
+ swizzle=q_smem_layout_staged.inner,
736
+ byte_alignment=128,
737
+ )
738
+ sK = smem.allocate_tensor(
739
+ element_type=self.k_dtype,
740
+ layout=k_smem_layout_staged.outer,
741
+ swizzle=k_smem_layout_staged.inner,
742
+ byte_alignment=128,
743
+ )
744
+ # K and V now use separate memory since we removed the transform stage
745
+ sV = smem.allocate_tensor(
746
+ element_type=self.v_dtype,
747
+ layout=v_smem_layout_staged.outer,
748
+ swizzle=v_smem_layout_staged.inner,
749
+ byte_alignment=128,
750
+ )
751
+
752
+ sSum = smem.allocate_tensor(
753
+ element_type=self.qk_acc_dtype,
754
+ layout=cute.make_layout(len(self.softmax_warp_ids) * self.threads_per_warp),
755
+ byte_alignment=128,
756
+ )
757
+ qk_thr_mma = qk_tiled_mma.get_slice(mma_tile_coord_v) # default 1sm
758
+ pv_thr_mma = pv_tiled_mma.get_slice(mma_tile_coord_v) # default 1sm
759
+ tSrQ = qk_thr_mma.make_fragment_A(sQ)
760
+ tSrK = qk_thr_mma.make_fragment_B(sK)
761
+ tOrV = pv_thr_mma.make_fragment_B(sV)
762
+ qk_acc_shape = qk_thr_mma.partition_shape_C((self.qk_mma_tiler[0], self.qk_mma_tiler[1]))
763
+ tStS = qk_thr_mma.make_fragment_C(cute.append(qk_acc_shape, self.qk_acc_stage))
764
+ pv_acc_shape = pv_thr_mma.partition_shape_C((self.pv_mma_tiler[0], self.pv_mma_tiler[1]))
765
+ tOtO = pv_thr_mma.make_fragment_C(pv_acc_shape)
766
+ tOtO_layout = cute.append(
767
+ tOtO.layout,
768
+ cute.make_layout(
769
+ self.iterations_pv,
770
+ stride=self.pv_mma_tiler[1] // self.tmem_warp_shape_mn[1],
771
+ ),
772
+ )
773
+ tStS = cute.make_tensor(tStS.iterator + self.tmem_s_offset, tStS.layout)
774
+ tOtO_staged = cute.make_tensor(tOtO.iterator + self.tmem_o_offset, tOtO_layout)
775
+
776
+ # ///////////////////////////////////////////////////////////////////////////////
777
+ # EMPTY
778
+ # ///////////////////////////////////////////////////////////////////////////////
779
+ for _i in cutlass.range_constexpr(len(self.empty_warp_id)):
780
+ if warp_idx == self.empty_warp_id[_i]:
781
+ cute.arch.warpgroup_reg_dealloc(self.num_regs_other)
782
+
783
+ if cutlass.const_expr(self.use_clc_scheduler):
784
+ tile_sched = FmhaClcDynamicTileScheduler.create(
785
+ tile_sched_params,
786
+ cute.arch.block_idx(),
787
+ cute.arch.grid_dim(),
788
+ clc_response_ptr,
789
+ clc,
790
+ )
791
+ else:
792
+ blk_idx = cute.arch.block_idx()
793
+ tile_sched = FmhaStaticTileScheduler(
794
+ tile_sched_params, blk_idx[0], blk_idx, cute.arch.grid_dim()
795
+ )
796
+ work_tile = tile_sched.initial_work_tile_info()
797
+
798
+ # Cluster wait
799
+ pipeline.pipeline_init_wait(cluster_shape_mn=cluster_layout_vmnk)
800
+
801
+ # ///////////////////////////////////////////////////////////////////////////////
802
+ # LOAD
803
+ # ///////////////////////////////////////////////////////////////////////////////
804
+ if warp_idx == self.load_warp_id:
805
+ cute.arch.warpgroup_reg_dealloc(self.num_regs_other)
806
+ while work_tile.is_valid_tile:
807
+ curr_block_coord = work_tile.tile_idx # (q_tile_idx, 0, (head_idx, batch_idx))
808
+ mma_block_coord = (
809
+ curr_block_coord[0] // cute.size(qk_tiled_mma.thr_id.shape),
810
+ curr_block_coord[1],
811
+ curr_block_coord[2],
812
+ )
813
+ continue_cond = False
814
+ batch_coord = curr_block_coord[2][1]
815
+ seqlen_q = mQ_qdl.shape[0]
816
+ seqlen_k = (
817
+ mK_kdl.shape[0] if cutlass.const_expr(mPageTable is None) else max_seqlen_k
818
+ )
819
+ cuseqlen_q = Int32(0)
820
+ cuseqlen_k = Int32(0)
821
+ block_offset = (
822
+ Int32(0),
823
+ Int32(0),
824
+ Int32(0),
825
+ ((Int32(0), Int32(0)), Int32(0)),
826
+ )
827
+ if cutlass.const_expr(cum_seqlen_q is not None):
828
+ cuseqlen_q = cum_seqlen_q[batch_coord]
829
+ seqlen_q = cum_seqlen_q[batch_coord + 1] - cuseqlen_q
830
+ if cutlass.const_expr(cum_seqlen_k is not None):
831
+ cuseqlen_k = cum_seqlen_k[batch_coord]
832
+ seqlen_k = cum_seqlen_k[batch_coord + 1] - cuseqlen_k
833
+ block_offset = (
834
+ cuseqlen_q,
835
+ cuseqlen_k,
836
+ Int32(0),
837
+ ((Int32(0), Int32(0)), Int32(0)),
838
+ )
839
+ continue_cond = not FmhaStaticTileScheduler.check_valid_work_for_seqlen_q(
840
+ self.qk_mma_tiler[0],
841
+ mma_block_coord[0],
842
+ seqlen_q,
843
+ )
844
+ if cutlass.const_expr(mSeqUsedK is not None):
845
+ seqlen_k = mSeqUsedK[batch_coord]
846
+ if not continue_cond:
847
+ mQ_qdl_ = cute.domain_offset(cute.select(block_offset, mode=[0, 2, 3]), mQ_qdl)
848
+ # Local tile partition global tensors
849
+ q_cta_layout = cute.make_layout(
850
+ cute.slice_(cluster_layout_vmnk, (0, 0, None, 0)).shape
851
+ )
852
+ # (bM, bK, loopM, loopK, loopL)
853
+ gQ_qdl = cute.flat_divide(mQ_qdl_, cute.select(self.qk_mma_tiler, mode=[0, 2]))
854
+ tSgQ_qdl = qk_thr_mma.partition_A(gQ_qdl)
855
+ tQsQ, tQgQ_qdl = cute.nvgpu.cpasync.tma_partition(
856
+ tma_atom_q,
857
+ block_in_cluster_coord_vmnk[2],
858
+ q_cta_layout,
859
+ cute.group_modes(sQ, 0, 3),
860
+ cute.group_modes(tSgQ_qdl, 0, 3),
861
+ )
862
+ kv_cta_layout = cute.make_layout(
863
+ cute.slice_(cluster_layout_vmnk, (0, None, 0, 0)).shape
864
+ )
865
+ if cutlass.const_expr(mPageTable is None):
866
+ # Dense path: domain_offset K/V by batch block, select batch via mma_block_coord[2].
867
+ mK_kdl_ = cute.domain_offset(
868
+ cute.select(block_offset, mode=[1, 2, 3]), mK_kdl
869
+ )
870
+ mV_dkl_ = cute.domain_offset(
871
+ cute.select(block_offset, mode=[2, 1, 3]), mV_dkl
872
+ )
873
+ gK_kdl = cute.flat_divide(
874
+ mK_kdl_, cute.select(self.qk_mma_tiler, mode=[1, 2])
875
+ )
876
+ tSgK_kdl = qk_thr_mma.partition_B(gK_kdl)
877
+ tKsK, tKgK_kdl = cute.nvgpu.cpasync.tma_partition(
878
+ tma_atom_k,
879
+ block_in_cluster_coord_vmnk[1],
880
+ kv_cta_layout,
881
+ cute.group_modes(sK, 0, 3),
882
+ cute.group_modes(tSgK_kdl, 0, 3),
883
+ )
884
+ gV_dkl = cute.flat_divide(
885
+ mV_dkl_, cute.select(self.pv_mma_tiler, mode=[1, 2])
886
+ )
887
+ tSgV_dkl = pv_thr_mma.partition_B(gV_dkl)
888
+ tVsV, tVgV_dkl = cute.nvgpu.cpasync.tma_partition(
889
+ tma_atom_v,
890
+ block_in_cluster_coord_vmnk[1],
891
+ kv_cta_layout,
892
+ cute.group_modes(sV, 0, 3),
893
+ cute.group_modes(tSgV_dkl, 0, 3),
894
+ )
895
+ # ((atom_v, rest_v), RestN, RestK)
896
+ tKgK = tKgK_kdl[None, None, None, mma_block_coord[2]]
897
+ tVgV = tVgV_dkl[None, None, None, mma_block_coord[2]]
898
+ else:
899
+ # Paged path: slice K/V by KV head, keep num_pages dim for page_idx-based TMA.
900
+ head_kv_coord = curr_block_coord[2][0] // self.qhead_per_kvhead
901
+ mK_kdl_ = mK_kdl[None, None, head_kv_coord, None]
902
+ mV_dkl_ = mV_dkl[None, None, head_kv_coord, None]
903
+ gK_kdl = cute.flat_divide(
904
+ mK_kdl_, cute.select(self.qk_mma_tiler, mode=[1, 2])
905
+ )
906
+ tSgK_kdl = qk_thr_mma.partition_B(gK_kdl)
907
+ tKsK, tKgK_kdl = cute.nvgpu.cpasync.tma_partition(
908
+ tma_atom_k,
909
+ block_in_cluster_coord_vmnk[1],
910
+ kv_cta_layout,
911
+ cute.group_modes(sK, 0, 3),
912
+ cute.group_modes(tSgK_kdl, 0, 3),
913
+ )
914
+ gV_dkl = cute.flat_divide(
915
+ mV_dkl_, cute.select(self.pv_mma_tiler, mode=[1, 2])
916
+ )
917
+ tSgV_dkl = pv_thr_mma.partition_B(gV_dkl)
918
+ tVsV, tVgV_dkl = cute.nvgpu.cpasync.tma_partition(
919
+ tma_atom_v,
920
+ block_in_cluster_coord_vmnk[1],
921
+ kv_cta_layout,
922
+ cute.group_modes(sV, 0, 3),
923
+ cute.group_modes(tSgV_dkl, 0, 3),
924
+ )
925
+ tKgK = tKgK_kdl
926
+ tVgV = tVgV_dkl
927
+ # ((atom_v, rest_v), RestK)
928
+ tQgQ = tQgQ_qdl[None, mma_block_coord[0], None, mma_block_coord[2]]
929
+
930
+ seqlen_kv_loop_start, seqlen_kv_loop_steps = (
931
+ FusedMask.get_trip_start_count_via_block_info(
932
+ mma_block_coord,
933
+ self.qk_mma_tiler,
934
+ seqlen_q,
935
+ seqlen_k,
936
+ self.is_causal,
937
+ self.is_local,
938
+ window_size_left,
939
+ window_size_right,
940
+ )
941
+ )
942
+ seqlen_kv_loop_end = seqlen_kv_loop_start + seqlen_kv_loop_steps
943
+ # Q
944
+ for iter in cutlass.range(self.iterations_qk, unroll=1):
945
+ q_handle = load_q_producer.acquire_and_advance()
946
+ cute.copy(
947
+ tma_atom_q,
948
+ tQgQ[None, iter],
949
+ tQsQ[None, q_handle.index],
950
+ tma_bar_ptr=q_handle.barrier,
951
+ )
952
+
953
+ # K0
954
+ kv_coord = seqlen_kv_loop_start
955
+ k_page_idx = (
956
+ mPageTable[batch_coord, kv_coord]
957
+ if cutlass.const_expr(mPageTable is not None)
958
+ else None
959
+ )
960
+ for iter in cutlass.range(self.iterations_qk, unroll=1):
961
+ k_handle = load_kv_producer.acquire_and_advance()
962
+ cute.copy(
963
+ tma_atom_k,
964
+ tKgK[None, kv_coord, iter]
965
+ if cutlass.const_expr(mPageTable is None)
966
+ else tKgK[None, 0, iter, k_page_idx],
967
+ tKsK[None, k_handle.index],
968
+ tma_bar_ptr=k_handle.barrier,
969
+ )
970
+ kv_coord += 1
971
+ # v_page_idx_prev carries K[i-1]'s page index for use as V[i-1]'s page
972
+ # (K and V for the same KV block share the same physical page).
973
+ # Also serves as the Vend page index when seqlen_kv_loop_steps == 1.
974
+ v_page_idx_prev = (
975
+ k_page_idx if cutlass.const_expr(mPageTable is not None) else None
976
+ )
977
+ # Prefetch K1 page after K0 TMA dispatch to hide L2 latency.
978
+ if cutlass.const_expr(mPageTable is not None):
979
+ if seqlen_kv_loop_steps > 1:
980
+ k_page_idx = mPageTable[batch_coord, kv_coord]
981
+
982
+ for i in cutlass.range(1, seqlen_kv_loop_steps, 1, unroll=1):
983
+ # Ki: k_page_idx was prefetched at end of previous iteration
984
+ # (or in the prologue for i==1); L2 latency already hidden.
985
+ for iter in cutlass.range(self.iterations_qk, unroll=1):
986
+ k_handle = load_kv_producer.acquire_and_advance()
987
+ cute.copy(
988
+ tma_atom_k,
989
+ tKgK[None, kv_coord, iter]
990
+ if cutlass.const_expr(mPageTable is None)
991
+ else tKgK[None, 0, iter, k_page_idx],
992
+ tKsK[None, k_handle.index],
993
+ tma_bar_ptr=k_handle.barrier,
994
+ )
995
+ # Vi-1: reuse v_page_idx_prev (= K[i-1]'s page), no extra GMEM read.
996
+ for iter in cutlass.range(self.iterations_pv, unroll=1):
997
+ v_handle = load_kv_producer.acquire_and_advance()
998
+ cute.copy(
999
+ tma_atom_v,
1000
+ tVgV[None, iter, kv_coord - 1]
1001
+ if cutlass.const_expr(mPageTable is None)
1002
+ else tVgV[None, iter, 0, v_page_idx_prev],
1003
+ tVsV[None, v_handle.index],
1004
+ tma_bar_ptr=v_handle.barrier,
1005
+ )
1006
+ v_page_idx_prev = (
1007
+ k_page_idx if cutlass.const_expr(mPageTable is not None) else None
1008
+ )
1009
+ kv_coord += 1
1010
+ # Prefetch next K page while V TMA is in flight.
1011
+ if cutlass.const_expr(mPageTable is not None):
1012
+ if kv_coord < seqlen_kv_loop_end:
1013
+ k_page_idx = mPageTable[batch_coord, kv_coord]
1014
+ # Vend: reuse v_page_idx_prev (= K[end-1]'s page), no extra GMEM read.
1015
+ for iter in cutlass.range(self.iterations_pv, unroll=1):
1016
+ v_handle = load_kv_producer.acquire_and_advance()
1017
+ cute.copy(
1018
+ tma_atom_v,
1019
+ tVgV[None, iter, seqlen_kv_loop_end - 1]
1020
+ if cutlass.const_expr(mPageTable is None)
1021
+ else tVgV[None, iter, 0, v_page_idx_prev],
1022
+ tVsV[None, v_handle.index],
1023
+ tma_bar_ptr=v_handle.barrier,
1024
+ )
1025
+
1026
+ work_tile = tile_sched.advance_to_next_work()
1027
+ # End of persistent scheduler loop
1028
+ load_kv_producer.tail()
1029
+ load_q_producer.tail()
1030
+
1031
+ # ///////////////////////////////////////////////////////////////////////////////
1032
+ # MMA
1033
+ # ///////////////////////////////////////////////////////////////////////////////
1034
+ if warp_idx == self.mma_warp_id:
1035
+ cute.arch.warpgroup_reg_dealloc(self.num_regs_other)
1036
+
1037
+ while work_tile.is_valid_tile:
1038
+ curr_block_coord = work_tile.tile_idx
1039
+ mma_block_coord = (
1040
+ curr_block_coord[0] // cute.size(qk_tiled_mma.thr_id.shape),
1041
+ curr_block_coord[1],
1042
+ curr_block_coord[2],
1043
+ )
1044
+ continue_cond = False
1045
+ seqlen_q = mQ_qdl.shape[0]
1046
+ seqlen_k = (
1047
+ mK_kdl.shape[0] if cutlass.const_expr(mPageTable is None) else max_seqlen_k
1048
+ )
1049
+ batch_coord = curr_block_coord[2][1]
1050
+ if cutlass.const_expr(cum_seqlen_q is not None):
1051
+ cuseqlen_q = cum_seqlen_q[batch_coord]
1052
+ seqlen_q = cum_seqlen_q[batch_coord + 1] - cuseqlen_q
1053
+ continue_cond = not FmhaStaticTileScheduler.check_valid_work_for_seqlen_q(
1054
+ self.qk_mma_tiler[0],
1055
+ mma_block_coord[0],
1056
+ seqlen_q,
1057
+ )
1058
+ if cutlass.const_expr(mSeqUsedK is not None):
1059
+ seqlen_k = mSeqUsedK[batch_coord]
1060
+
1061
+ if not continue_cond:
1062
+ if cutlass.const_expr(cum_seqlen_k is not None):
1063
+ cuseqlen_k = cum_seqlen_k[batch_coord]
1064
+ seqlen_k = cum_seqlen_k[batch_coord + 1] - cuseqlen_k
1065
+
1066
+ seqlen_kv_loop_start, seqlen_kv_loop_steps = (
1067
+ FusedMask.get_trip_start_count_via_block_info(
1068
+ mma_block_coord,
1069
+ self.qk_mma_tiler,
1070
+ seqlen_q,
1071
+ seqlen_k,
1072
+ self.is_causal,
1073
+ self.is_local,
1074
+ window_size_left,
1075
+ window_size_right,
1076
+ )
1077
+ )
1078
+ seqlen_kv_loop_end = seqlen_kv_loop_start + seqlen_kv_loop_steps
1079
+
1080
+ cta_rank_in_cluster = cute.arch.make_warp_uniform(
1081
+ cute.arch.block_idx_in_cluster()
1082
+ )
1083
+ is_leader_cta = cta_rank_in_cluster % 2 == 0
1084
+ load_q_releaser = load_q_consumer.clone()
1085
+ pv_tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
1086
+ if seqlen_kv_loop_steps > 1:
1087
+ # QK0
1088
+ if is_leader_cta:
1089
+ s_handle = mma_s_producer.acquire_and_advance()
1090
+ tStS_slice = tStS[None, None, None, s_handle.index]
1091
+ qk_tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
1092
+ for iter in cutlass.range(self.iterations_qk, unroll=1):
1093
+ load_q_consumer.wait_and_advance()
1094
+ tSrQ_slice = tSrQ[None, None, None, iter]
1095
+ k_handle = load_kv_consumer.wait_and_advance()
1096
+ tSrK_trans_slice = tSrK[None, None, None, k_handle.index]
1097
+ num_kphases = cute.size(tSrQ_slice, mode=[2])
1098
+ for kphase_idx in cutlass.range(num_kphases, unroll_full=True):
1099
+ kphase_coord = (None, None, kphase_idx)
1100
+ cute.gemm(
1101
+ qk_tiled_mma,
1102
+ tStS_slice,
1103
+ tSrQ_slice[kphase_coord],
1104
+ tSrK_trans_slice[kphase_coord],
1105
+ tStS_slice,
1106
+ )
1107
+ qk_tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
1108
+ k_handle.release()
1109
+ s_handle.commit()
1110
+ for i in cutlass.range(1, seqlen_kv_loop_steps - 1, 1, unroll=1):
1111
+ # QKi
1112
+ if is_leader_cta:
1113
+ s_handle = mma_s_producer.acquire_and_advance()
1114
+ tStS_slice = tStS[None, None, None, s_handle.index]
1115
+ qk_tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
1116
+ for iter in cutlass.range(self.iterations_qk, unroll=1):
1117
+ tSrQ_slice = tSrQ[None, None, None, iter]
1118
+ k_handle = load_kv_consumer.wait_and_advance()
1119
+ tSrK_trans_slice = tSrK[None, None, None, k_handle.index]
1120
+ num_kphases = cute.size(tSrQ_slice, mode=[2])
1121
+ for kphase_idx in cutlass.range(num_kphases, unroll_full=True):
1122
+ kphase_coord = (None, None, kphase_idx)
1123
+ cute.gemm(
1124
+ qk_tiled_mma,
1125
+ tStS_slice,
1126
+ tSrQ_slice[kphase_coord],
1127
+ tSrK_trans_slice[kphase_coord],
1128
+ tStS_slice,
1129
+ )
1130
+ qk_tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
1131
+ k_handle.release()
1132
+ s_handle.commit()
1133
+
1134
+ # PVi-1
1135
+ p_handle = p_mma_consumer.wait_and_advance()
1136
+ o_handle = mma_corr_producer.acquire_and_advance()
1137
+ pv_whether_acc = pv_tiled_mma.get(tcgen05.Field.ACCUMULATE)
1138
+ for iter in cutlass.range(self.iterations_pv, unroll=1):
1139
+ v_handle = load_kv_consumer.wait_and_advance()
1140
+ pv_tiled_mma.set(tcgen05.Field.ACCUMULATE, pv_whether_acc)
1141
+ tOtO_slice = tOtO_staged[None, None, None, iter]
1142
+ tStS_slice = tStS[None, None, None, p_handle.index]
1143
+ tP = cute.make_tensor(
1144
+ tStS_slice.iterator, p_tmem_layout_staged.outer
1145
+ )
1146
+ tOrP = pv_thr_mma.make_fragment_A(tP)
1147
+ tOrP_slice = cute.make_tensor(
1148
+ cute.recast_ptr(tStS_slice.iterator, dtype=self.q_dtype),
1149
+ tOrP.layout,
1150
+ )
1151
+ tOrV_slice = tOrV[None, None, None, v_handle.index]
1152
+ num_kphases = cute.size(tOrV_slice, mode=[2])
1153
+ for kphase_idx in cutlass.range(num_kphases, unroll_full=True):
1154
+ kphase_coord = (None, None, kphase_idx)
1155
+ cute.gemm(
1156
+ pv_tiled_mma,
1157
+ tOtO_slice,
1158
+ tOrP_slice[kphase_coord],
1159
+ tOrV_slice[kphase_coord],
1160
+ tOtO_slice,
1161
+ )
1162
+ pv_tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
1163
+ v_handle.release()
1164
+ o_handle.commit()
1165
+ p_handle.release()
1166
+ if is_leader_cta:
1167
+ # QKend
1168
+ s_handle = mma_s_producer.acquire_and_advance()
1169
+ tStS_slice = tStS[None, None, None, s_handle.index]
1170
+ qk_tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
1171
+ for iter in cutlass.range(self.iterations_qk, unroll=1):
1172
+ tSrQ_slice = tSrQ[None, None, None, iter]
1173
+ k_handle = load_kv_consumer.wait_and_advance()
1174
+ tSrK_trans_slice = tSrK[None, None, None, k_handle.index]
1175
+ num_kphases = cute.size(tSrQ_slice, mode=[2])
1176
+ for kphase_idx in cutlass.range(num_kphases, unroll_full=True):
1177
+ kphase_coord = (None, None, kphase_idx)
1178
+ cute.gemm(
1179
+ qk_tiled_mma,
1180
+ tStS_slice,
1181
+ tSrQ_slice[kphase_coord],
1182
+ tSrK_trans_slice[kphase_coord],
1183
+ tStS_slice,
1184
+ )
1185
+ qk_tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
1186
+ k_handle.release()
1187
+ load_q_releaser.release()
1188
+ load_q_releaser.advance()
1189
+ s_handle.commit()
1190
+
1191
+ # PVend-1
1192
+ p_handle = p_mma_consumer.wait_and_advance()
1193
+ o_handle = mma_corr_producer.acquire_and_advance()
1194
+ pv_whether_acc = pv_tiled_mma.get(tcgen05.Field.ACCUMULATE)
1195
+ for iter in cutlass.range(self.iterations_pv, unroll=1):
1196
+ v_handle = load_kv_consumer.wait_and_advance()
1197
+ pv_tiled_mma.set(tcgen05.Field.ACCUMULATE, pv_whether_acc)
1198
+ tOtO_slice = tOtO_staged[None, None, None, iter]
1199
+ tStS_slice = tStS[None, None, None, p_handle.index]
1200
+ tP = cute.make_tensor(
1201
+ tStS_slice.iterator, p_tmem_layout_staged.outer
1202
+ )
1203
+ tOrP = pv_thr_mma.make_fragment_A(tP)
1204
+ tOrP_slice = cute.make_tensor(
1205
+ cute.recast_ptr(tStS_slice.iterator, dtype=self.q_dtype),
1206
+ tOrP.layout,
1207
+ )
1208
+ tOrV_slice = tOrV[None, None, None, v_handle.index]
1209
+ num_kphases = cute.size(tOrV_slice, mode=[2])
1210
+ for kphase_idx in cutlass.range(num_kphases, unroll_full=True):
1211
+ kphase_coord = (None, None, kphase_idx)
1212
+ cute.gemm(
1213
+ pv_tiled_mma,
1214
+ tOtO_slice,
1215
+ tOrP_slice[kphase_coord],
1216
+ tOrV_slice[kphase_coord],
1217
+ tOtO_slice,
1218
+ )
1219
+ pv_tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
1220
+ v_handle.release()
1221
+ o_handle.commit()
1222
+ p_handle.release()
1223
+ else:
1224
+ if is_leader_cta:
1225
+ # QK0
1226
+ s_handle = mma_s_producer.acquire_and_advance()
1227
+ tStS_slice = tStS[None, None, None, s_handle.index]
1228
+ qk_tiled_mma.set(tcgen05.Field.ACCUMULATE, False)
1229
+ for iter in cutlass.range(self.iterations_qk, unroll=1):
1230
+ load_q_consumer.wait_and_advance()
1231
+ tSrQ_slice = tSrQ[None, None, None, iter]
1232
+ k_handle = load_kv_consumer.wait_and_advance()
1233
+ tSrK_trans_slice = tSrK[None, None, None, k_handle.index]
1234
+ num_kphases = cute.size(tSrQ_slice, mode=[2])
1235
+ for kphase_idx in cutlass.range(num_kphases, unroll_full=True):
1236
+ kphase_coord = (None, None, kphase_idx)
1237
+ cute.gemm(
1238
+ qk_tiled_mma,
1239
+ tStS_slice,
1240
+ tSrQ_slice[kphase_coord],
1241
+ tSrK_trans_slice[kphase_coord],
1242
+ tStS_slice,
1243
+ )
1244
+ qk_tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
1245
+ k_handle.release()
1246
+ load_q_releaser.release()
1247
+ load_q_releaser.advance()
1248
+ s_handle.commit()
1249
+
1250
+ if is_leader_cta:
1251
+ # PVend
1252
+ p_handle = p_mma_consumer.wait_and_advance()
1253
+ o_handle = mma_corr_producer.acquire_and_advance()
1254
+ pv_whether_acc = pv_tiled_mma.get(tcgen05.Field.ACCUMULATE)
1255
+ for iter in cutlass.range(self.iterations_pv, unroll=1):
1256
+ v_handle = load_kv_consumer.wait_and_advance()
1257
+ pv_tiled_mma.set(tcgen05.Field.ACCUMULATE, pv_whether_acc)
1258
+ tOtO_slice = tOtO_staged[None, None, None, iter]
1259
+ tStS_slice = tStS[None, None, None, p_handle.index]
1260
+ tP = cute.make_tensor(tStS_slice.iterator, p_tmem_layout_staged.outer)
1261
+ tOrP = pv_thr_mma.make_fragment_A(tP)
1262
+ tOrP_slice = cute.make_tensor(
1263
+ cute.recast_ptr(tStS_slice.iterator, dtype=self.q_dtype),
1264
+ tOrP.layout,
1265
+ )
1266
+ tOrV_slice = tOrV[None, None, None, v_handle.index]
1267
+ num_kphases = cute.size(tOrV_slice, mode=[2])
1268
+ for kphase_idx in cutlass.range(num_kphases, unroll_full=True):
1269
+ kphase_coord = (None, None, kphase_idx)
1270
+ cute.gemm(
1271
+ pv_tiled_mma,
1272
+ tOtO_slice,
1273
+ tOrP_slice[kphase_coord],
1274
+ tOrV_slice[kphase_coord],
1275
+ tOtO_slice,
1276
+ )
1277
+ pv_tiled_mma.set(tcgen05.Field.ACCUMULATE, True)
1278
+ v_handle.release()
1279
+ o_handle.commit()
1280
+ p_handle.release()
1281
+ work_tile = tile_sched.advance_to_next_work()
1282
+ # End of persistent scheduler loop
1283
+ mma_s_producer.tail()
1284
+ mma_corr_producer.tail()
1285
+
1286
+ if warp_idx < self.correction_warp_ids[0] and warp_idx >= self.softmax_warp_ids[0]:
1287
+ # increase register after decreasing
1288
+ cute.arch.warpgroup_reg_alloc(self.num_regs_softmax)
1289
+
1290
+ while work_tile.is_valid_tile:
1291
+ curr_block_coord = work_tile.tile_idx
1292
+ mma_block_coord = (
1293
+ curr_block_coord[0] // cute.size(qk_tiled_mma.thr_id.shape),
1294
+ curr_block_coord[1],
1295
+ curr_block_coord[2],
1296
+ )
1297
+ batch_coord = curr_block_coord[2][1]
1298
+ continue_cond = False
1299
+ seqlen_q = mQ_qdl.shape[0]
1300
+ seqlen_k = (
1301
+ mK_kdl.shape[0] if cutlass.const_expr(mPageTable is None) else max_seqlen_k
1302
+ )
1303
+ cuseqlen_q = Int32(0)
1304
+ if cutlass.const_expr(cum_seqlen_q is not None):
1305
+ cuseqlen_q = cum_seqlen_q[batch_coord]
1306
+ seqlen_q = cum_seqlen_q[batch_coord + 1] - cuseqlen_q
1307
+ continue_cond = not FmhaStaticTileScheduler.check_valid_work_for_seqlen_q(
1308
+ self.qk_mma_tiler[0],
1309
+ mma_block_coord[0],
1310
+ seqlen_q,
1311
+ )
1312
+ if cutlass.const_expr(mSeqUsedK is not None):
1313
+ seqlen_k = mSeqUsedK[batch_coord]
1314
+ if not continue_cond:
1315
+ if cutlass.const_expr(cum_seqlen_k is not None):
1316
+ cuseqlen_k = cum_seqlen_k[batch_coord]
1317
+ seqlen_k = cum_seqlen_k[batch_coord + 1] - cuseqlen_k
1318
+
1319
+ row_max = -Float32.inf
1320
+ row_max_prev = -Float32.inf
1321
+ row_sum = 0.0
1322
+
1323
+ start_count, trip_count = FusedMask.get_trip_start_count_via_block_info(
1324
+ mma_block_coord,
1325
+ self.qk_mma_tiler,
1326
+ seqlen_q,
1327
+ seqlen_k,
1328
+ self.is_causal,
1329
+ self.is_local,
1330
+ window_size_left,
1331
+ window_size_right,
1332
+ )
1333
+ end_count = start_count + trip_count
1334
+ if cutlass.const_expr(self.use_semantic_trip_range):
1335
+ n_block_min_causal_local_mask, n_block_min_before_local_mask = (
1336
+ FusedMask.get_trip_mask_bounds_via_block_info(
1337
+ mma_block_coord,
1338
+ self.qk_mma_tiler,
1339
+ seqlen_q,
1340
+ seqlen_k,
1341
+ self.is_causal,
1342
+ self.is_local,
1343
+ window_size_left,
1344
+ window_size_right,
1345
+ )
1346
+ )
1347
+ cS_base = cute.make_identity_tensor(
1348
+ (self.qk_mma_tiler[0], self.qk_mma_tiler[1])
1349
+ )
1350
+ cS = cute.domain_offset((mma_block_coord[0] * self.qk_mma_tiler[0], 0), cS_base)
1351
+ tScS = qk_thr_mma.partition_C(cS)
1352
+
1353
+ for step in cutlass.range(start_count, end_count, 1, unroll=1):
1354
+ cS_iter = cute.domain_offset((0, step * self.qk_mma_tiler[1]), cS)
1355
+ tScS_iter = qk_thr_mma.partition_C(cS_iter)
1356
+ if cutlass.const_expr(self.use_semantic_trip_range):
1357
+ need_apply_mask = (
1358
+ step >= n_block_min_causal_local_mask
1359
+ or step < n_block_min_before_local_mask
1360
+ )
1361
+ else:
1362
+ # Residual path only needs seqlen masking on the last K tile.
1363
+ need_apply_mask = step == end_count - 1
1364
+ # Si -> Pi
1365
+ (
1366
+ row_max,
1367
+ row_sum,
1368
+ mma_s_consumer,
1369
+ p_mma_producer,
1370
+ s_corr_producer,
1371
+ ) = self.softmax_step(
1372
+ (need_apply_mask, window_size_left, window_size_right),
1373
+ (
1374
+ row_max_prev,
1375
+ row_sum,
1376
+ seqlen_q,
1377
+ seqlen_k,
1378
+ scale_softmax_log2,
1379
+ ),
1380
+ (tStS, tScS_iter),
1381
+ (mma_s_consumer, p_mma_producer, s_corr_producer),
1382
+ )
1383
+ row_max_prev = row_max
1384
+ sum_producer = self.store_sum_max(
1385
+ row_max,
1386
+ mLSE,
1387
+ row_sum,
1388
+ sSum,
1389
+ sum_producer,
1390
+ curr_block_coord,
1391
+ seqlen_q,
1392
+ cum_seqlen_q,
1393
+ cuseqlen_q,
1394
+ scale_softmax,
1395
+ )
1396
+ work_tile = tile_sched.advance_to_next_work()
1397
+ p_mma_producer.tail()
1398
+ s_corr_producer.tail()
1399
+
1400
+ # ///////////////////////////////////////////////////////////////////////////////
1401
+ # Correction
1402
+ # ///////////////////////////////////////////////////////////////////////////////
1403
+ if warp_idx >= self.correction_warp_ids[0] and warp_idx < self.mma_warp_id:
1404
+ cute.arch.warpgroup_reg_dealloc(self.num_regs_correction)
1405
+
1406
+ while work_tile.is_valid_tile:
1407
+ curr_block_coord = work_tile.tile_idx
1408
+ mma_block_coord = (
1409
+ curr_block_coord[0] // cute.size(qk_tiled_mma.thr_id.shape),
1410
+ curr_block_coord[1],
1411
+ curr_block_coord[2],
1412
+ )
1413
+ batch_coord = curr_block_coord[2][1]
1414
+ seqlen_q = mQ_qdl.shape[0]
1415
+ seqlen_k = (
1416
+ mK_kdl.shape[0] if cutlass.const_expr(mPageTable is None) else max_seqlen_k
1417
+ )
1418
+ continue_cond = False
1419
+ cuseqlen_q = Int32(0)
1420
+ if cutlass.const_expr(cum_seqlen_q is not None):
1421
+ cuseqlen_q = cum_seqlen_q[batch_coord]
1422
+ seqlen_q = cum_seqlen_q[batch_coord + 1] - cuseqlen_q
1423
+ continue_cond = not FmhaStaticTileScheduler.check_valid_work_for_seqlen_q(
1424
+ self.qk_mma_tiler[0],
1425
+ mma_block_coord[0],
1426
+ seqlen_q,
1427
+ )
1428
+ if cutlass.const_expr(mSeqUsedK is not None):
1429
+ seqlen_k = mSeqUsedK[batch_coord]
1430
+
1431
+ if not continue_cond:
1432
+ if cutlass.const_expr(cum_seqlen_k is not None):
1433
+ cuseqlen_k = cum_seqlen_k[batch_coord]
1434
+ seqlen_k = cum_seqlen_k[batch_coord + 1] - cuseqlen_k
1435
+
1436
+ mO_qdl_eff = mO_qdl
1437
+ if cutlass.const_expr(cum_seqlen_q is not None):
1438
+ block_offset_o = (
1439
+ cuseqlen_q,
1440
+ Int32(0),
1441
+ Int32(0),
1442
+ ((Int32(0), Int32(0)), Int32(0)),
1443
+ )
1444
+ mO_qdl_eff = cute.domain_offset(
1445
+ cute.select(block_offset_o, mode=[0, 2, 3]), mO_qdl
1446
+ )
1447
+
1448
+ # (bM, bN, loopM, loopN, loopL)
1449
+ gO_qdl = cute.flat_divide(
1450
+ mO_qdl_eff, cute.select(self.pv_block_tiler, mode=[0, 1])
1451
+ )
1452
+ cO_qdl = cute.flat_divide(
1453
+ cute.make_identity_tensor(mO_qdl_eff.shape),
1454
+ cute.select(self.pv_block_tiler, mode=[0, 1]),
1455
+ )
1456
+
1457
+ _, seqlen_kv_loop_steps = FusedMask.get_trip_start_count_via_block_info(
1458
+ mma_block_coord,
1459
+ self.qk_mma_tiler,
1460
+ seqlen_q,
1461
+ seqlen_k,
1462
+ self.is_causal,
1463
+ self.is_local,
1464
+ window_size_left,
1465
+ window_size_right,
1466
+ )
1467
+ gO_staged = gO_qdl[None, None, curr_block_coord[0], None, curr_block_coord[2]]
1468
+ cO_staged = cO_qdl[None, None, curr_block_coord[0], None, curr_block_coord[2]]
1469
+ cS = cute.make_identity_tensor((self.qk_mma_tiler[0], self.qk_mma_tiler[1]))
1470
+ tScS = qk_thr_mma.partition_C(cS)
1471
+
1472
+ # Empty step as the first step is no need for correction
1473
+ stats_handle = s_corr_consumer.wait_and_advance()
1474
+ stats_handle.release()
1475
+ for step in cutlass.range(1, seqlen_kv_loop_steps, 1, unroll=1):
1476
+ # Oi-1 -> Oi
1477
+ mma_corr_consumer, s_corr_consumer = self.correction_rescale(
1478
+ scale_softmax_log2,
1479
+ (s_corr_consumer, tStS, tScS),
1480
+ (mma_corr_consumer, tOtO_staged, cO_staged),
1481
+ self.epi_tile,
1482
+ )
1483
+ # O_partial -> O_final
1484
+ mma_corr_consumer, sum_consumer = self.correction_epilog(
1485
+ (seqlen_q, scale_output),
1486
+ (sum_consumer, sSum),
1487
+ (mma_corr_consumer, gO_staged, cO_staged, tOtO_staged),
1488
+ self.epi_tile,
1489
+ )
1490
+ work_tile = tile_sched.advance_to_next_work()
1491
+ # NOTE: tmem.free() moved to kernel end to enable cluster-wide sync
1492
+
1493
+ # ///////////////////////////////////////////////////////////////////////////////
1494
+ # Scheduler Warp (only for CLC dynamic scheduler)
1495
+ # ///////////////////////////////////////////////////////////////////////////////
1496
+ if cutlass.const_expr(self.use_clc_scheduler):
1497
+ is_first_cta_in_cluster = cta_rank_in_cluster == 0
1498
+
1499
+ if warp_idx == self.sched_warp_id and is_first_cta_in_cluster:
1500
+ cute.arch.warpgroup_reg_dealloc(self.num_regs_other)
1501
+ while work_tile.is_valid_tile:
1502
+ tile_sched.prefetch_next_work()
1503
+ work_tile = tile_sched.advance_to_next_work()
1504
+ tile_sched.producer_tail()
1505
+
1506
+ # ///////////////////////////////////////////////////////////////////////////////
1507
+ # Empty warps reg dealloc
1508
+ # ///////////////////////////////////////////////////////////////////////////////
1509
+ if cutlass.const_expr(self.use_clc_scheduler):
1510
+ if warp_idx > self.load_warp_id:
1511
+ if not (warp_idx == self.sched_warp_id and is_first_cta_in_cluster):
1512
+ cute.arch.warpgroup_reg_dealloc(self.num_regs_other)
1513
+ else:
1514
+ if warp_idx > self.load_warp_id:
1515
+ cute.arch.warpgroup_reg_dealloc(self.num_regs_other)
1516
+
1517
+ # ///////////////////////////////////////////////////////////////////////////////
1518
+ # Cooperative TMEM Deallocation (2CTA)
1519
+ # ///////////////////////////////////////////////////////////////////////////////
1520
+ # All warps (including scheduler) have finished by this point.
1521
+ # Cluster-wide sync ensures both CTAs reach here before dealloc.
1522
+ cute.arch.cluster_arrive()
1523
+ cute.arch.cluster_wait()
1524
+ tmem.relinquish_alloc_permit()
1525
+ tmem.free(tmem_ptr)
1526
+
1527
+ return
1528
+
1529
+ @cute.jit
1530
+ def softmax_step(
1531
+ self,
1532
+ mask_args: Tuple,
1533
+ value_args: Tuple,
1534
+ tensor_args: Tuple,
1535
+ pipeline_args: Tuple,
1536
+ ) -> Tuple[Float32, Float32, pipeline.PipelineConsumer, pipeline.PipelineProducer]:
1537
+ need_apply_mask, window_size_left, window_size_right = mask_args
1538
+ row_max, row_sum, seqlen_q, seqlen_k, scale_softmax_log2 = value_args
1539
+ tStS, tScS = tensor_args
1540
+ mma_s_consumer, p_mma_producer, s_corr_producer = pipeline_args
1541
+ tidx, _, _ = cute.arch.thread_idx()
1542
+ thread_idx = tidx % (self.threads_per_warp * len(self.softmax_warp_ids))
1543
+ s_handle = mma_s_consumer.wait_and_advance()
1544
+ tStS_slice = tStS[(None, None), 0, 0, s_handle.index]
1545
+ tScS_slice = tScS[(None, None), 0, 0]
1546
+ tmem_load_atom = cute.make_copy_atom(
1547
+ tcgen05.Ld32x32bOp(tcgen05.Repetition(32)), self.qk_acc_dtype
1548
+ )
1549
+ tmem_tiled_load = tcgen05.make_tmem_copy(tmem_load_atom, tStS_slice)
1550
+ thr_load = tmem_tiled_load.get_slice(thread_idx)
1551
+ tTMEM_LOADtS = thr_load.partition_S(tStS_slice)
1552
+ tTMEM_LOADcS = thr_load.partition_D(tScS_slice)
1553
+ tTMEM_LOADrS = cute.make_rmem_tensor(tTMEM_LOADcS.shape, self.qk_acc_dtype)
1554
+ cute.copy(tmem_tiled_load, tTMEM_LOADtS, tTMEM_LOADrS)
1555
+
1556
+ cute.arch.fence_view_async_tmem_load()
1557
+ s_handle.release()
1558
+ if need_apply_mask:
1559
+ FusedMask.apply_mask_via_causal_local(
1560
+ tTMEM_LOADrS,
1561
+ tTMEM_LOADcS,
1562
+ seqlen_q,
1563
+ seqlen_k,
1564
+ self.use_semantic_trip_range,
1565
+ self.is_causal,
1566
+ self.is_local,
1567
+ window_size_left,
1568
+ window_size_right,
1569
+ )
1570
+ old_row_max = row_max
1571
+ row_max = tTMEM_LOADrS.load().reduce(cute.ReductionOp.MAX, row_max, 0)
1572
+ row_max_safe = row_max
1573
+ if row_max == -cutlass.Float32.inf:
1574
+ row_max_safe = 0.0
1575
+
1576
+ stats_handle = s_corr_producer.acquire_and_advance()
1577
+ stats_layout = cute.composition(
1578
+ tStS_slice.layout, cute.make_layout((tStS_slice.shape[0], 2))
1579
+ )
1580
+ stats_c_layout = cute.composition(
1581
+ tScS_slice.layout, cute.make_layout((tScS_slice.shape[0], 2))
1582
+ )
1583
+ tOtStats = cute.make_tensor(tStS_slice.iterator + self.tilePlikeFP32, stats_layout)
1584
+ tOcStats = cute.make_tensor(tScS_slice.iterator, stats_c_layout)
1585
+ tmem_store_stats_atom = cute.make_copy_atom(
1586
+ tcgen05.copy.St32x32bOp(tcgen05.copy.Repetition(2)),
1587
+ self.qk_acc_dtype,
1588
+ )
1589
+ tiled_tmem_store_stats = tcgen05.make_tmem_copy(tmem_store_stats_atom, tOtStats)
1590
+ thr_tmem_store_stats = tiled_tmem_store_stats.get_slice(thread_idx)
1591
+ tTMEM_STOREcStats = thr_tmem_store_stats.partition_S(tOcStats)
1592
+ tTMEM_STORErStats = cute.make_rmem_tensor(tTMEM_STOREcStats.shape, self.qk_acc_dtype)
1593
+ tTMEM_STORErStats[0] = old_row_max
1594
+ tTMEM_STORErStats[1] = row_max_safe
1595
+ tTMEM_STOREtStats = thr_tmem_store_stats.partition_D(tOtStats)
1596
+ cute.copy(tiled_tmem_store_stats, tTMEM_STORErStats, tTMEM_STOREtStats)
1597
+ cute.arch.fence_view_async_tmem_store()
1598
+ stats_handle.commit()
1599
+
1600
+ scale = scale_softmax_log2
1601
+ minus_row_max_scale = (0.0 - row_max_safe) * scale
1602
+ # Acquire P write slot early — overlaps any pipeline stall with exp2 compute
1603
+ p_handle = p_mma_producer.acquire_and_advance()
1604
+ # Fragment-based FMA + exp2 + bf16 conversion
1605
+ # Trades SFU for FMA via polynomial emulation on a fraction of elements
1606
+ ex2_frg_tile = 32
1607
+ ex2_frg_cnt = cute.size(tTMEM_LOADrS) // ex2_frg_tile
1608
+ tTMEM_LOADrS_ex2 = cute.logical_divide(tTMEM_LOADrS, cute.make_layout(ex2_frg_tile))
1609
+ tTMEM_STORErP = cute.make_rmem_tensor(tTMEM_LOADrS.shape, self.q_dtype)
1610
+ tTMEM_STORErP_ex2 = cute.logical_divide(tTMEM_STORErP, cute.make_layout(ex2_frg_tile))
1611
+ for j in cutlass.range_constexpr(ex2_frg_cnt):
1612
+ for k in cutlass.range_constexpr(0, ex2_frg_tile, 2):
1613
+ tTMEM_LOADrS_ex2[k, j], tTMEM_LOADrS_ex2[k + 1, j] = cute.arch.fma_packed_f32x2(
1614
+ (tTMEM_LOADrS_ex2[k, j], tTMEM_LOADrS_ex2[k + 1, j]),
1615
+ (scale, scale),
1616
+ (minus_row_max_scale, minus_row_max_scale),
1617
+ )
1618
+ if cutlass.const_expr(self.ex2_emu_freq == 0):
1619
+ tTMEM_LOADrS_ex2[k, j] = cute.math.exp2(tTMEM_LOADrS_ex2[k, j], fastmath=True)
1620
+ tTMEM_LOADrS_ex2[k + 1, j] = cute.math.exp2(
1621
+ tTMEM_LOADrS_ex2[k + 1, j], fastmath=True
1622
+ )
1623
+ else:
1624
+ if cutlass.const_expr(
1625
+ k % self.ex2_emu_freq < self.ex2_emu_freq - self.ex2_emu_res
1626
+ or j >= ex2_frg_cnt - 1
1627
+ or j < self.ex2_emu_start_frg
1628
+ ):
1629
+ tTMEM_LOADrS_ex2[k, j] = cute.math.exp2(
1630
+ tTMEM_LOADrS_ex2[k, j], fastmath=True
1631
+ )
1632
+ tTMEM_LOADrS_ex2[k + 1, j] = cute.math.exp2(
1633
+ tTMEM_LOADrS_ex2[k + 1, j], fastmath=True
1634
+ )
1635
+ else:
1636
+ tTMEM_LOADrS_ex2[k, j], tTMEM_LOADrS_ex2[k + 1, j] = ex2_emulation_2(
1637
+ tTMEM_LOADrS_ex2[k, j], tTMEM_LOADrS_ex2[k + 1, j]
1638
+ )
1639
+ tTMEM_STORErP_ex2[None, j].store(tTMEM_LOADrS_ex2[None, j].load().to(self.q_dtype))
1640
+ tmem_store_atom = cute.make_copy_atom(
1641
+ tcgen05.St32x32bOp(tcgen05.Repetition(32)), self.qk_acc_dtype
1642
+ )
1643
+ tilePlikeFP32 = tStS_slice.shape[1] // Float32.width * self.q_dtype.width
1644
+ tStS_P_layout = cute.composition(
1645
+ tStS_slice.layout, cute.make_layout((tStS_slice.shape[0], tilePlikeFP32))
1646
+ )
1647
+ tStS_P = cute.make_tensor(tStS_slice.iterator, tStS_P_layout)
1648
+ tScS_P_layout = cute.composition(
1649
+ tScS_slice.layout, cute.make_layout((tScS_slice.shape[0], tilePlikeFP32))
1650
+ )
1651
+ tScS_P = cute.make_tensor(tScS_slice.iterator, tScS_P_layout)
1652
+ tmem_tiled_store = tcgen05.make_tmem_copy(tmem_store_atom, tStS_P)
1653
+ thr_store = tmem_tiled_store.get_slice(thread_idx)
1654
+ tTMEM_STOREtP = thr_store.partition_D(tStS_P)
1655
+ tTMEM_STOREcS = thr_store.partition_S(tScS_P)
1656
+ tTMEM_STORErP_ = cute.make_tensor(
1657
+ cute.recast_ptr(tTMEM_STORErP.iterator, dtype=self.qk_acc_dtype),
1658
+ tTMEM_STOREcS.shape,
1659
+ )
1660
+ cute.copy(tmem_tiled_store, tTMEM_STORErP_, tTMEM_STOREtP)
1661
+ cute.arch.fence_view_async_tmem_store()
1662
+
1663
+ p_handle.commit()
1664
+ acc_scale_ = scale * (old_row_max - row_max_safe)
1665
+ acc_scale = cute.math.exp2(acc_scale_, fastmath=True) * 0.5
1666
+ # TODO: calc row sum with TensorSSA
1667
+ row_sum *= acc_scale
1668
+ local_row_sum_0 = (row_sum, row_sum)
1669
+ local_row_sum_1 = (0.0, 0.0)
1670
+ local_row_sum_2 = (0.0, 0.0)
1671
+ local_row_sum_3 = (0.0, 0.0)
1672
+ reduction_unroll = 4
1673
+ frg_tile = cute.size(tTMEM_LOADrS) // reduction_unroll
1674
+ tTMEM_LOADrS_frg = cute.logical_divide(tTMEM_LOADrS, cute.make_layout(frg_tile))
1675
+ for j in cutlass.range_constexpr(0, cute.size(tTMEM_LOADrS_frg, mode=[0]), 2):
1676
+ local_row_sum_0 = cute.arch.add_packed_f32x2(
1677
+ local_row_sum_0, (tTMEM_LOADrS_frg[j, 0], tTMEM_LOADrS_frg[j + 1, 0])
1678
+ )
1679
+ local_row_sum_1 = cute.arch.add_packed_f32x2(
1680
+ local_row_sum_1, (tTMEM_LOADrS_frg[j, 1], tTMEM_LOADrS_frg[j + 1, 1])
1681
+ )
1682
+ local_row_sum_2 = cute.arch.add_packed_f32x2(
1683
+ local_row_sum_2, (tTMEM_LOADrS_frg[j, 2], tTMEM_LOADrS_frg[j + 1, 2])
1684
+ )
1685
+ local_row_sum_3 = cute.arch.add_packed_f32x2(
1686
+ local_row_sum_3, (tTMEM_LOADrS_frg[j, 3], tTMEM_LOADrS_frg[j + 1, 3])
1687
+ )
1688
+ local_row_sum_0 = cute.arch.add_packed_f32x2(local_row_sum_0, local_row_sum_1)
1689
+ local_row_sum_2 = cute.arch.add_packed_f32x2(local_row_sum_2, local_row_sum_3)
1690
+ local_row_sum_0 = cute.arch.add_packed_f32x2(local_row_sum_0, local_row_sum_2)
1691
+ row_sum = local_row_sum_0[0] + local_row_sum_0[1]
1692
+ return row_max, row_sum, mma_s_consumer, p_mma_producer, s_corr_producer
1693
+
1694
+ @cute.jit
1695
+ def correction_rescale(
1696
+ self,
1697
+ scale_softmax_log2: Float32,
1698
+ stats_args: tuple,
1699
+ o_args: tuple,
1700
+ epi_tile: cute.Tile,
1701
+ ) -> pipeline.PipelineConsumer:
1702
+ (s_corr_consumer, tStS, tScS) = stats_args
1703
+ (mma_o_consumer, tOtO_staged, cO_staged) = o_args
1704
+ tidx, _, _ = cute.arch.thread_idx()
1705
+ thread_idx = tidx % (self.threads_per_warp * len(self.softmax_warp_ids))
1706
+
1707
+ stats_handle = s_corr_consumer.wait_and_advance()
1708
+ tStS_slice = tStS[(None, None), 0, 0, stats_handle.index]
1709
+ tScS_slice = tScS[(None, None), 0, 0]
1710
+ stats_layout = cute.composition(
1711
+ tStS_slice.layout, cute.make_layout((tStS_slice.shape[0], 2))
1712
+ )
1713
+ stats_c_layout = cute.composition(
1714
+ tScS_slice.layout, cute.make_layout((tScS_slice.shape[0], 2))
1715
+ )
1716
+ tOtStats = cute.make_tensor(tStS_slice.iterator + self.tilePlikeFP32, stats_layout)
1717
+ tOcStats = cute.make_tensor(tScS_slice.iterator, stats_c_layout)
1718
+ tmem_load_stats_atom = cute.make_copy_atom(
1719
+ tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(2)),
1720
+ self.qk_acc_dtype,
1721
+ )
1722
+ tiled_tmem_load_stats = tcgen05.make_tmem_copy(tmem_load_stats_atom, tOtStats)
1723
+ thr_tmem_load_stats = tiled_tmem_load_stats.get_slice(thread_idx)
1724
+ tTMEM_LOADtStats = thr_tmem_load_stats.partition_S(tOtStats)
1725
+ tTMEM_LOADcStats = thr_tmem_load_stats.partition_D(tOcStats)
1726
+ tTMEM_LOADrStats = cute.make_rmem_tensor(tTMEM_LOADcStats.shape, self.qk_acc_dtype)
1727
+ cute.copy(tiled_tmem_load_stats, tTMEM_LOADtStats, tTMEM_LOADrStats)
1728
+
1729
+ scale = scale_softmax_log2 * (tTMEM_LOADrStats[0] - tTMEM_LOADrStats[1])
1730
+ scale = cute.math.exp2(scale, fastmath=True)
1731
+ stats_handle.release()
1732
+ o_handle = mma_o_consumer.wait_and_advance()
1733
+ for iter in cutlass.range(self.iterations_pv, unroll_full=True):
1734
+ tOtO = tOtO_staged[(None, None), 0, 0, iter]
1735
+ cO = cO_staged[None, None, iter]
1736
+ tOtO_epi = cute.zipped_divide(tOtO, epi_tile)
1737
+ cO_epi = cute.zipped_divide(cO, epi_tile)
1738
+ tmem_load_atom = cute.make_copy_atom(
1739
+ tcgen05.Ld32x32bOp(tcgen05.Repetition(16)),
1740
+ self.pv_acc_dtype,
1741
+ )
1742
+ tmem_tiled_load = tcgen05.make_tmem_copy(tmem_load_atom, tOtO_epi)
1743
+ thr_load = tmem_tiled_load.get_slice(thread_idx)
1744
+ tmem_store_atom = cute.make_copy_atom(
1745
+ tcgen05.St32x32bOp(tcgen05.Repetition(16)),
1746
+ self.pv_acc_dtype,
1747
+ )
1748
+ tmem_store_atom = tcgen05.make_tmem_copy(tmem_store_atom, tOtO_epi)
1749
+ thr_store = tmem_store_atom.get_slice(thread_idx)
1750
+ tTMEM_LOADtO = thr_load.partition_S(tOtO_epi)
1751
+ tTMEM_LOADcO = thr_load.partition_D(cO_epi)
1752
+ tTMEM_STOREtO = thr_store.partition_D(tOtO_epi)
1753
+ tTMrO = cute.make_rmem_tensor_like(
1754
+ cute.append(
1755
+ cute.make_layout(tTMEM_LOADcO[None, 0, 0].shape),
1756
+ cute.make_layout(2, stride=cute.size(tTMEM_LOADcO[None, 0, 0].shape)),
1757
+ ),
1758
+ self.pv_acc_dtype,
1759
+ )
1760
+ tTMEM_LOADtO_0 = tTMEM_LOADtO[None, 0, 0]
1761
+ cute.copy(tmem_tiled_load, tTMEM_LOADtO_0, tTMrO[None, 0])
1762
+ iter_num = cute.size(tTMEM_LOADtO, mode=[1])
1763
+ for i in cutlass.range(1, iter_num, unroll_full=True):
1764
+ tTMEM_LOADtO_i = tTMEM_LOADtO[None, i, 0]
1765
+ cute.copy(tmem_tiled_load, tTMEM_LOADtO_i, tTMrO[None, i % 2])
1766
+ for j in cutlass.range(0, cute.size(tTMrO, mode=[0]), 2, unroll_full=True):
1767
+ tTMrO[j, (i - 1) % 2], tTMrO[j + 1, (i - 1) % 2] = cute.arch.mul_packed_f32x2(
1768
+ (tTMrO[j, (i - 1) % 2], tTMrO[j + 1, (i - 1) % 2]),
1769
+ (scale, scale),
1770
+ )
1771
+ tTMEM_STOREtO_prev_i = tTMEM_STOREtO[None, i - 1, 0]
1772
+ cute.copy(tmem_store_atom, tTMrO[None, (i - 1) % 2], tTMEM_STOREtO_prev_i)
1773
+
1774
+ for j in cutlass.range(0, cute.size(tTMrO, mode=[0]), 2, unroll_full=True):
1775
+ tTMrO[j, (iter_num - 1) % 2], tTMrO[j + 1, (iter_num - 1) % 2] = (
1776
+ cute.arch.mul_packed_f32x2(
1777
+ (
1778
+ tTMrO[j, (iter_num - 1) % 2],
1779
+ tTMrO[j + 1, (iter_num - 1) % 2],
1780
+ ),
1781
+ (scale, scale),
1782
+ )
1783
+ )
1784
+ cute.copy(
1785
+ tmem_store_atom,
1786
+ tTMrO[None, (iter_num - 1) % 2],
1787
+ tTMEM_STOREtO[None, iter_num - 1, 0],
1788
+ )
1789
+ cute.arch.fence_view_async_tmem_store()
1790
+ o_handle.release()
1791
+ return mma_o_consumer, s_corr_consumer
1792
+
1793
+ @cute.jit
1794
+ def correction_epilog(
1795
+ self,
1796
+ value_args: Tuple,
1797
+ sum_args: Tuple,
1798
+ o_args: Tuple,
1799
+ epi_tile: cute.Tile,
1800
+ ) -> Tuple[pipeline.PipelineConsumer, pipeline.PipelineProducer]:
1801
+ (seqlen_q, scale_output) = value_args
1802
+ (sum_consumer, sSum) = sum_args
1803
+ (mma_o_consumer, gO_staged, cO_staged, tOtO_staged) = o_args
1804
+ tidx, _, _ = cute.arch.thread_idx()
1805
+ thread_idx = tidx % (self.threads_per_warp * len(self.softmax_warp_ids))
1806
+ sum_handle = sum_consumer.wait_and_advance()
1807
+ row_sum = sSum[thread_idx]
1808
+ cute.arch.fence_view_async_shared()
1809
+ sum_handle.release()
1810
+ scale = scale_output / row_sum
1811
+ o_handle = mma_o_consumer.wait_and_advance()
1812
+ for iter in cutlass.range(self.iterations_pv):
1813
+ gO = gO_staged[None, None, iter]
1814
+ cO = cO_staged[None, None, iter]
1815
+ tOtO = tOtO_staged[(None, None), 0, 0, iter]
1816
+ tOtO_epi = cute.zipped_divide(tOtO, epi_tile)
1817
+ cO_epi = cute.zipped_divide(cO, epi_tile)
1818
+ gO_epi = cute.zipped_divide(gO, epi_tile)
1819
+ tidx, _, _ = cute.arch.thread_idx()
1820
+ thread_idx = tidx % (self.threads_per_warp * len(self.softmax_warp_ids))
1821
+ tmem_copy_atom = cute.make_copy_atom(
1822
+ tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(32)), self.pv_acc_dtype
1823
+ )
1824
+ tiled_tmem_load = tcgen05.make_tmem_copy(tmem_copy_atom, tOtO_epi)
1825
+ thr_tmem_load = tiled_tmem_load.get_slice(thread_idx)
1826
+ tTMEM_LOADtO = thr_tmem_load.partition_S(tOtO_epi)
1827
+ tTMEM_LOADgO = thr_tmem_load.partition_D(gO_epi)
1828
+ tTMEM_LOADcO = thr_tmem_load.partition_D(cO_epi)
1829
+ for i in cutlass.range(cute.size(tTMEM_LOADtO, mode=[1]), unroll_full=True):
1830
+ tTMEM_LOADtO_i = tTMEM_LOADtO[None, i, 0]
1831
+ tTMEM_LOADgO_i = tTMEM_LOADgO[None, i, 0]
1832
+ tTMEM_LOADcO_i = tTMEM_LOADcO[None, i, 0]
1833
+ tTMrO = cute.make_rmem_tensor(tTMEM_LOADcO[None, 0, i].shape, self.pv_acc_dtype)
1834
+ cute.copy(tiled_tmem_load, tTMEM_LOADtO_i, tTMrO)
1835
+ for j in cutlass.range(0, cute.size(tTMrO), 2, unroll_full=True):
1836
+ tTMrO[j], tTMrO[j + 1] = cute.arch.mul_packed_f32x2(
1837
+ (tTMrO[j], tTMrO[j + 1]),
1838
+ (scale, scale),
1839
+ )
1840
+ tSMrO = cute.make_rmem_tensor(tTMrO.shape, self.o_dtype)
1841
+ o_vec = tTMrO.load()
1842
+ tSMrO.store(o_vec.to(self.o_dtype))
1843
+ if cute.elem_less(tTMEM_LOADcO_i[0][0], seqlen_q):
1844
+ cute.autovec_copy(tSMrO, tTMEM_LOADgO_i)
1845
+ o_handle.release()
1846
+ return mma_o_consumer, sum_consumer
1847
+
1848
+ @cute.jit
1849
+ def store_sum_max(
1850
+ self,
1851
+ row_max,
1852
+ mLSE,
1853
+ row_sum,
1854
+ sSum,
1855
+ sum_producer,
1856
+ current_block_coord,
1857
+ seqlen_q,
1858
+ cum_seqlen_q,
1859
+ cuseqlen_q,
1860
+ scale_softmax,
1861
+ ):
1862
+ tidx, _, _ = cute.arch.thread_idx()
1863
+ thread_idx = tidx % (self.threads_per_warp * len(self.softmax_warp_ids))
1864
+ sum_handle = sum_producer.acquire_and_advance()
1865
+ sSum[thread_idx] = row_sum
1866
+ cute.arch.fence_view_async_shared()
1867
+ sum_handle.commit()
1868
+
1869
+ if cutlass.const_expr(mLSE is not None):
1870
+ q_idx = current_block_coord[0] * self.cta_tiler[0] + tidx
1871
+ hb_idx = (
1872
+ (current_block_coord[2][0], Int32(0))
1873
+ if cutlass.const_expr(cum_seqlen_q is not None)
1874
+ else current_block_coord[2]
1875
+ )
1876
+ lse_value = scale_softmax * row_max + cute.math.log(row_sum, fastmath=True)
1877
+ if cute.elem_less(q_idx, seqlen_q):
1878
+ global_q_idx = (
1879
+ q_idx + cuseqlen_q if cutlass.const_expr(cum_seqlen_q is not None) else q_idx
1880
+ )
1881
+ mLSE[global_q_idx, hb_idx] = lse_value
1882
+ return sum_producer
build/torch-cuda/flashrt_fa4/cute/softmax.py ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import math
4
+ import operator
5
+ from typing import Tuple
6
+ from dataclasses import dataclass
7
+
8
+ import cutlass
9
+ import cutlass.cute as cute
10
+ from cutlass import Float32, Boolean
11
+
12
+ from fa4_cute_runtime.quack import layout_utils
13
+ import fa4_cute_runtime.flashrt_fa4.cute.utils as utils
14
+ from fa4_cute_runtime.quack.cute_dsl_utils import ParamsBase
15
+ from fa4_cute_runtime.flashrt_fa4.cute.seqlen_info import SeqlenInfoQK
16
+
17
+
18
+ @dataclass
19
+ class Softmax(ParamsBase):
20
+ scale_log2: Float32
21
+ num_rows: cutlass.Constexpr[int]
22
+ row_max: cute.Tensor
23
+ row_sum: cute.Tensor
24
+ arch: cutlass.Constexpr[int] = 80
25
+ softmax_scale: Float32 | None = None
26
+
27
+ @staticmethod
28
+ def create(
29
+ scale_log2: Float32,
30
+ num_rows: cutlass.Constexpr[int],
31
+ arch: cutlass.Constexpr[int] = 80,
32
+ softmax_scale: Float32 | None = None,
33
+ ):
34
+ row_max = cute.make_rmem_tensor(num_rows, Float32)
35
+ row_sum = cute.make_rmem_tensor(num_rows, Float32)
36
+ return Softmax(scale_log2, num_rows, row_max, row_sum, arch, softmax_scale)
37
+
38
+ def reset(self) -> None:
39
+ self.row_max.fill(-Float32.inf)
40
+ self.row_sum.fill(0.0)
41
+
42
+ def _compute_row_max(
43
+ self, acc_S_row: cute.TensorSSA, init_val: float | Float32 | None = None
44
+ ) -> Float32:
45
+ return utils.fmax_reduce(acc_S_row, init_val, arch=self.arch)
46
+
47
+ def _compute_row_sum(
48
+ self, acc_S_row_exp: cute.TensorSSA, init_val: float | Float32 | None = None
49
+ ) -> Float32:
50
+ return utils.fadd_reduce(acc_S_row_exp, init_val, arch=self.arch)
51
+
52
+ @cute.jit
53
+ def online_softmax(
54
+ self,
55
+ acc_S: cute.Tensor,
56
+ is_first: cutlass.Constexpr[bool] = False,
57
+ check_inf: cutlass.Constexpr[bool] = True,
58
+ ) -> cute.Tensor:
59
+ """Apply online softmax and return the row_scale to rescale O.
60
+
61
+ :param acc_S: acc_S tensor
62
+ :type acc_S: cute.Tensor
63
+ :param is_first: is first n_block
64
+ :type is_first: cutlass.Constexpr
65
+ """
66
+ # Change acc_S to M,N layout view.
67
+ acc_S_mn = layout_utils.reshape_acc_to_mn(acc_S)
68
+ row_scale = cute.make_fragment_like(self.row_max, Float32)
69
+
70
+ row_max = self.row_max
71
+ row_sum = self.row_sum
72
+ scale_log2 = self.scale_log2
73
+ arch = self.arch
74
+
75
+ # Each iteration processes one row of acc_S
76
+ for r in cutlass.range(cute.size(row_max), unroll_full=True):
77
+ acc_S_row = acc_S_mn[r, None].load() # (n_block_size)
78
+
79
+ row_max_cur = utils.fmax_reduce(
80
+ acc_S_row,
81
+ init_val=row_max[r] if cutlass.const_expr(not is_first) else None,
82
+ arch=arch,
83
+ )
84
+
85
+ row_max_cur = cute.arch.warp_reduction_max(row_max_cur, threads_in_group=4)
86
+ # Update row_max before changing row_max_cur to safe value for -inf
87
+ row_max_prev = row_max[r]
88
+ row_max[r] = row_max_cur
89
+
90
+ if cutlass.const_expr(check_inf):
91
+ row_max_cur = 0.0 if row_max_cur == -Float32.inf else row_max_cur
92
+
93
+ if cutlass.const_expr(is_first):
94
+ row_max_cur_scaled = row_max_cur * scale_log2
95
+ acc_S_row_exp = cute.math.exp2(
96
+ acc_S_row * scale_log2 - row_max_cur_scaled, fastmath=True
97
+ )
98
+ acc_S_row_sum = utils.fadd_reduce(acc_S_row_exp, init_val=None, arch=arch)
99
+ row_scale[r] = 1.0
100
+ else:
101
+ row_max_cur_scaled = row_max_cur * scale_log2
102
+ acc_S_row_exp = cute.math.exp2(
103
+ acc_S_row * scale_log2 - row_max_cur_scaled, fastmath=True
104
+ )
105
+ # row_scale[r] = cute.math.exp2(row_max_prev * self.scale_log2 - row_max_cur_scaled)
106
+ row_scale[r] = cute.math.exp2(
107
+ (row_max_prev - row_max_cur) * scale_log2, fastmath=True
108
+ )
109
+ acc_S_row_sum = utils.fadd_reduce(
110
+ acc_S_row_exp, init_val=row_sum[r] * row_scale[r], arch=arch
111
+ )
112
+
113
+ row_sum[r] = acc_S_row_sum
114
+ acc_S_mn[r, None].store(acc_S_row_exp)
115
+
116
+ return row_scale
117
+
118
+ @cute.jit
119
+ def finalize(
120
+ self, final_scale: Float32 = 1.0, sink_val: Float32 | cute.Tensor | None = None
121
+ ) -> cute.Tensor:
122
+ """Finalize the online softmax by computing the scale and logsumexp."""
123
+ if cutlass.const_expr(sink_val is not None and isinstance(sink_val, cute.Tensor)):
124
+ assert cute.size(sink_val) == cute.size(self.row_sum)
125
+ row_sum = self.row_sum
126
+ row_max = self.row_max
127
+ scale_log2 = self.scale_log2
128
+
129
+ # quad reduction for row_sum as we didn't do it during each iteration of online softmax
130
+ row_sum.store(utils.warp_reduce(row_sum.load(), operator.add, width=4))
131
+ row_scale = cute.make_fragment_like(row_max, Float32)
132
+
133
+ for r in cutlass.range(cute.size(row_sum), unroll_full=True):
134
+ if cutlass.const_expr(sink_val is not None):
135
+ sink_val_cur = sink_val if not isinstance(sink_val, cute.Tensor) else sink_val[r]
136
+ LOG2_E = math.log2(math.e)
137
+ row_sum[r] += cute.math.exp2(
138
+ sink_val_cur * LOG2_E - row_max[r] * scale_log2, fastmath=True
139
+ )
140
+
141
+ # if row_sum is zero or nan, set acc_O_mn_row to 1.0
142
+ acc_O_mn_row_is_zero_or_nan = row_sum[r] == 0.0 or row_sum[r] != row_sum[r]
143
+ row_scale[r] = (
144
+ cute.arch.rcp_approx(row_sum[r] if not acc_O_mn_row_is_zero_or_nan else 1.0)
145
+ ) * final_scale
146
+ row_sum_cur = row_sum[r]
147
+ LN2 = math.log(2.0)
148
+ row_sum[r] = (
149
+ (row_max[r] * scale_log2 + cute.math.log2(row_sum_cur, fastmath=True)) * LN2
150
+ if not acc_O_mn_row_is_zero_or_nan
151
+ else -Float32.inf
152
+ )
153
+ return row_scale
154
+
155
+ @cute.jit
156
+ def rescale_O(self, acc_O: cute.Tensor, row_scale: cute.Tensor) -> None:
157
+ """Scale each row of acc_O by the given scale tensor.
158
+ :param acc_O: input tensor
159
+ :type acc_O: cute.Tensor
160
+ :param row_scale: row_scale tensor
161
+ :type row_scale: cute.Tensor
162
+ """
163
+ acc_O_mn = layout_utils.reshape_acc_to_mn(acc_O)
164
+ assert cute.size(row_scale) == cute.size(acc_O_mn, mode=[0])
165
+ for r in cutlass.range(cute.size(row_scale), unroll_full=True):
166
+ acc_O_mn[r, None].store(acc_O_mn[r, None].load() * row_scale[r])
167
+
168
+
169
+ @dataclass
170
+ class SoftmaxSm100(Softmax):
171
+ rescale_threshold: cutlass.Constexpr[float] = 0.0
172
+ max_offset: cutlass.Constexpr[int] = 0
173
+
174
+ @staticmethod
175
+ def create(
176
+ scale_log2: Float32,
177
+ rescale_threshold: cutlass.Constexpr[float] = 0.0,
178
+ softmax_scale: Float32 | None = None,
179
+ max_offset: cutlass.Constexpr[int] = 0,
180
+ ):
181
+ num_rows = 1
182
+ arch = 100
183
+ row_max = cute.make_rmem_tensor(num_rows, Float32)
184
+ row_sum = cute.make_rmem_tensor(num_rows, Float32)
185
+ return SoftmaxSm100(
186
+ scale_log2,
187
+ num_rows,
188
+ row_max,
189
+ row_sum,
190
+ arch,
191
+ softmax_scale,
192
+ rescale_threshold=rescale_threshold,
193
+ max_offset=max_offset,
194
+ )
195
+
196
+ @cute.jit
197
+ def compute_row_max_local(self, acc_S_row: cute.TensorSSA, is_first: Boolean) -> Float32:
198
+ if cutlass.const_expr(is_first):
199
+ row_max_new = self._compute_row_max(acc_S_row)
200
+ else:
201
+ row_max_old = self.row_max[0]
202
+ row_max_new = self._compute_row_max(acc_S_row, init_val=row_max_old)
203
+ return row_max_new
204
+
205
+ @cute.jit
206
+ def update_row_max_from_local(
207
+ self,
208
+ row_max_new: Float32,
209
+ is_first: Boolean,
210
+ ) -> Tuple[Float32, Float32]:
211
+ if cutlass.const_expr(is_first):
212
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
213
+ acc_scale = 0.0
214
+ else:
215
+ row_max_old = self.row_max[0]
216
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
217
+ acc_scale_ = (row_max_old - row_max_safe) * self.scale_log2
218
+ acc_scale = cute.math.exp2(acc_scale_)
219
+ if cutlass.const_expr(self.rescale_threshold > 0.0):
220
+ if acc_scale_ >= -self.rescale_threshold:
221
+ row_max_new = row_max_old
222
+ row_max_safe = row_max_old
223
+ acc_scale = 1.0
224
+ self.row_max[0] = row_max_new
225
+ return row_max_safe, acc_scale
226
+
227
+ @cute.jit
228
+ def update_row_max(self, acc_S_row: cute.TensorSSA, is_first: int) -> Tuple[Float32, Float32]:
229
+ if cutlass.const_expr(is_first):
230
+ row_max_new = self._compute_row_max(acc_S_row)
231
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
232
+ acc_scale = 0.0
233
+ else:
234
+ row_max_old = self.row_max[0]
235
+ row_max_new = self._compute_row_max(acc_S_row, init_val=row_max_old)
236
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
237
+ acc_scale_ = (row_max_old - row_max_safe) * self.scale_log2
238
+ acc_scale = cute.math.exp2(acc_scale_, fastmath=True)
239
+ if cutlass.const_expr(self.rescale_threshold > 0.0):
240
+ if acc_scale_ >= -self.rescale_threshold:
241
+ row_max_new = row_max_old
242
+ row_max_safe = row_max_old
243
+ acc_scale = 1.0
244
+ self.row_max[0] = row_max_new
245
+ return row_max_safe, acc_scale
246
+
247
+ def update_row_sum(
248
+ self, acc_S_row_exp: cute.TensorSSA, row_scale: Float32, is_first: int = False
249
+ ) -> None:
250
+ init_val = self.row_sum[0] * row_scale if cutlass.const_expr(not is_first) else None
251
+ # self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=self.row_sum[0] * row_scale)
252
+ self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=init_val)
253
+ # tmp = self._compute_row_sum(acc_S_row_exp)
254
+ # self.row_sum[0] = self.row_sum[0] * row_scale + tmp
255
+
256
+ @cute.jit
257
+ def scale_subtract_rowmax(
258
+ self,
259
+ acc_S_row: cute.Tensor,
260
+ row_max: Float32,
261
+ ):
262
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
263
+ row_max_scaled = row_max * self.scale_log2
264
+ max_offset = Float32(self.max_offset)
265
+ bias = max_offset - row_max_scaled
266
+ for i in cutlass.range(0, cute.size(acc_S_row.shape), 2, unroll_full=True):
267
+ acc_S_row[i], acc_S_row[i + 1] = cute.arch.fma_packed_f32x2(
268
+ (acc_S_row[i], acc_S_row[i + 1]),
269
+ (self.scale_log2, self.scale_log2),
270
+ (bias, bias),
271
+ )
272
+
273
+ @cute.jit
274
+ def apply_exp2_convert(
275
+ self,
276
+ acc_S_row: cute.Tensor,
277
+ acc_S_row_converted: cute.Tensor,
278
+ ex2_emu_freq: cutlass.Constexpr[int] = 0,
279
+ ex2_emu_res: cutlass.Constexpr[int] = 4,
280
+ ex2_emu_start_frg: cutlass.Constexpr[int] = 0,
281
+ ):
282
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
283
+ frg_tile = 32
284
+ assert frg_tile % 2 == 0
285
+ frg_cnt = cute.size(acc_S_row) // frg_tile
286
+ assert cute.size(acc_S_row) % frg_tile == 0
287
+ acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
288
+ acc_S_row_converted_frg = cute.logical_divide(
289
+ acc_S_row_converted, cute.make_layout(frg_tile)
290
+ )
291
+ for j in cutlass.range_constexpr(frg_cnt):
292
+ for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
293
+ # acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
294
+ # acc_S_row_frg[k + 1, j] = cute.math.exp2(acc_S_row_frg[k + 1, j], fastmath=True)
295
+ if cutlass.const_expr(ex2_emu_freq == 0):
296
+ acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
297
+ acc_S_row_frg[k + 1, j] = cute.math.exp2(acc_S_row_frg[k + 1, j], fastmath=True)
298
+ else:
299
+ if cutlass.const_expr(
300
+ k % ex2_emu_freq < ex2_emu_freq - ex2_emu_res
301
+ or j >= frg_cnt - 1
302
+ or j < ex2_emu_start_frg
303
+ ):
304
+ acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
305
+ acc_S_row_frg[k + 1, j] = cute.math.exp2(
306
+ acc_S_row_frg[k + 1, j], fastmath=True
307
+ )
308
+ else:
309
+ # acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = utils.e2e_asm2(acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j])
310
+ acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = utils.ex2_emulation_2(
311
+ acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]
312
+ )
313
+ acc_S_row_converted_frg[None, j].store(
314
+ acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
315
+ )
316
+
317
+ @cute.jit
318
+ def scale_apply_exp2_convert(
319
+ self,
320
+ acc_S_row: cute.Tensor,
321
+ row_max: Float32,
322
+ acc_S_row_converted: cute.Tensor,
323
+ ):
324
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
325
+ minus_row_max_scaled = -row_max * self.scale_log2
326
+ for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
327
+ acc_S_row[i], acc_S_row[i + 1] = cute.arch.fma_packed_f32x2(
328
+ (acc_S_row[i], acc_S_row[i + 1]),
329
+ (self.scale_log2, self.scale_log2),
330
+ (minus_row_max_scaled, minus_row_max_scaled),
331
+ )
332
+
333
+ # for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
334
+ # acc_S_row[i], acc_S_row[i + 1] = cute.arch.fma_packed_f32x2(
335
+ # (acc_S_row[i], acc_S_row[i + 1]),
336
+ # (self.scale_log2, self.scale_log2),
337
+ # (minus_row_max_scaled, minus_row_max_scaled),
338
+ # )
339
+ # acc_S_row[i] = cute.math.exp2(acc_S_row[i], fastmath=True)
340
+ # acc_S_row[i + 1] = cute.math.exp2(acc_S_row[i + 1], fastmath=True)
341
+
342
+ frg_tile = 32
343
+ assert frg_tile % 2 == 0
344
+ frg_cnt = cute.size(acc_S_row) // frg_tile
345
+ assert cute.size(acc_S_row) % frg_tile == 0
346
+ acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
347
+ acc_S_row_converted_frg = cute.logical_divide(
348
+ acc_S_row_converted, cute.make_layout(frg_tile)
349
+ )
350
+ for j in cutlass.range_constexpr(frg_cnt):
351
+ for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
352
+ # acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = (
353
+ # cute.arch.fma_packed_f32x2(
354
+ # (acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]),
355
+ # (self.scale_log2, self.scale_log2),
356
+ # (minus_row_max_scaled, minus_row_max_scaled),
357
+ # )
358
+ # )
359
+ # acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
360
+ # acc_S_row_frg[k + 1, j] = cute.math.exp2(acc_S_row_frg[k + 1, j], fastmath=True)
361
+ acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
362
+ acc_S_row_frg[k + 1, j] = cute.math.exp2(acc_S_row_frg[k + 1, j], fastmath=True)
363
+ acc_S_row_converted_frg[None, j].store(
364
+ acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
365
+ )
366
+
367
+
368
+ @cute.jit
369
+ def floor_if_packed(
370
+ q_idx,
371
+ qhead_per_kvhead: cutlass.Constexpr[int],
372
+ ) -> cute.Tensor:
373
+ """Convert q_idx to packed format for Pack-GQA."""
374
+ if cutlass.const_expr(qhead_per_kvhead == 1):
375
+ return q_idx
376
+ return q_idx // qhead_per_kvhead
377
+
378
+
379
+ @cute.jit
380
+ def apply_score_mod_inner(
381
+ score_tensor,
382
+ index_tensor,
383
+ score_mod: cutlass.Constexpr,
384
+ batch_idx,
385
+ head_idx,
386
+ softmax_scale,
387
+ vec_size: cutlass.Constexpr,
388
+ qk_acc_dtype: cutlass.Constexpr,
389
+ aux_tensors,
390
+ fastdiv_mods,
391
+ seqlen_info: SeqlenInfoQK,
392
+ constant_q_idx: cutlass.Constexpr,
393
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
394
+ transpose_indices: cutlass.Constexpr[bool] = False,
395
+ ):
396
+ """Shared implementation for applying score modification.
397
+
398
+ Args:
399
+ score_tensor: The scores to modify (acc_S for flash_fwd, tSrS_t2r for sm100)
400
+ index_tensor: Index positions (tScS for flash_fwd, tScS_t2r for sm100)
401
+ score_mod: The score modification function to apply
402
+ batch_idx: Batch index
403
+ head_idx: Head index
404
+ softmax_scale: Scale to apply
405
+ vec_size: Vector size for processing elements
406
+ qk_acc_dtype: Data type for accumulator
407
+ aux_tensors: Optional aux_tensors for FlexAttention
408
+ fastdiv_mods: Tuple of (seqlen_q_divmod, seqlen_k_divmod) for wrapping
409
+ seqlen_info: Sequence length info
410
+ constant_q_idx: If provided, use this constant for all q_idx values
411
+ If None, compute q_idx per-element
412
+ qhead_per_kvhead_packgqa: Pack-GQA replication factor. Divide q_idx by this
413
+ when greater than 1 so score mods see logical heads.
414
+ transpose_indices: If True, swap q_idx/kv_idx in index_tensor (for bwd kernel where S is transposed)
415
+ """
416
+ # Index positions in the index_tensor tuple
417
+ # Forward: index_tensor[...][0] = q_idx, index_tensor[...][1] = kv_idx
418
+ # Backward (transposed): index_tensor[...][0] = kv_idx, index_tensor[...][1] = q_idx
419
+ if cutlass.const_expr(transpose_indices):
420
+ q_idx_pos = cutlass.const_expr(1)
421
+ kv_idx_pos = cutlass.const_expr(0)
422
+ else:
423
+ q_idx_pos = cutlass.const_expr(0)
424
+ kv_idx_pos = cutlass.const_expr(1)
425
+
426
+ n_vals = cutlass.const_expr(cute.size(score_tensor.shape))
427
+ score_vec = cute.make_rmem_tensor(vec_size, qk_acc_dtype)
428
+ kv_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
429
+
430
+ # SSA values for batch (constant across all elements)
431
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32).broadcast_to((vec_size,))
432
+
433
+ # Handle q_idx based on whether it's constant
434
+ q_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
435
+
436
+ # For Pack-GQA with non-constant q_idx, we need per-element head indices
437
+ # since a thread may process multiple query head indices
438
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
439
+ head_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
440
+
441
+ for i in cutlass.range(0, n_vals, vec_size, unroll_full=True):
442
+ for j in cutlass.range(vec_size, unroll_full=True):
443
+ score_vec[j] = score_tensor[i + j] * softmax_scale
444
+
445
+ # Extract head offset from packed q_idx for Pack-GQA
446
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
447
+ q_idx_packed = index_tensor[i + j][q_idx_pos]
448
+ # Building up the logical q_head idx: final_q_head = kv_head * qhead_per_kvhead + (q_physical % qhead_per_kvhead)
449
+ q_idx_logical = q_idx_packed // qhead_per_kvhead
450
+ head_offset = q_idx_packed - q_idx_logical * qhead_per_kvhead
451
+ head_idx_vec[j] = head_idx * qhead_per_kvhead + head_offset
452
+
453
+ # If we will do loads we mod, in order to not read OOB
454
+ if cutlass.const_expr(aux_tensors is not None and fastdiv_mods is not None):
455
+ if cutlass.const_expr(constant_q_idx is None):
456
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
457
+ q_idx_floored = floor_if_packed(
458
+ index_tensor[i + j][q_idx_pos], qhead_per_kvhead
459
+ )
460
+ _, q_idx_wrapped = divmod(q_idx_floored, seqlen_q_divmod)
461
+ q_idx_vec[j] = q_idx_wrapped
462
+ else:
463
+ _, seqlen_k_divmod = fastdiv_mods
464
+
465
+ _, kv_idx_wrapped = divmod(index_tensor[i + j][kv_idx_pos], seqlen_k_divmod)
466
+ kv_idx_vec[j] = kv_idx_wrapped
467
+ else:
468
+ # No bounds checking - direct indexing
469
+ if constant_q_idx is None:
470
+ q_idx_vec[j] = floor_if_packed(index_tensor[i + j][q_idx_pos], qhead_per_kvhead)
471
+ kv_idx_vec[j] = index_tensor[i + j][kv_idx_pos]
472
+
473
+ # Convert to SSA for score_mod call
474
+ score_ssa = score_vec.load()
475
+ kv_idx_ssa = kv_idx_vec.load()
476
+ if cutlass.const_expr(constant_q_idx is None):
477
+ q_idx_ssa = q_idx_vec.load()
478
+ else:
479
+ # NB we do not apply Pack-GQA division here, as constant_q_idx is assumed to already be logical
480
+ q_idx_const = constant_q_idx
481
+ q_idx_ssa = utils.scalar_to_ssa(q_idx_const, cutlass.Int32).broadcast_to((vec_size,))
482
+
483
+ # Compute head_idx_ssa: per-element for Pack-GQA with non-constant q_idx, constant otherwise
484
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
485
+ head_idx_ssa = head_idx_vec.load()
486
+ else:
487
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32).broadcast_to((vec_size,))
488
+
489
+ aux_args = []
490
+ if cutlass.const_expr(aux_tensors is not None):
491
+ aux_args = aux_tensors
492
+
493
+ post_mod_scores = score_mod(
494
+ score_ssa,
495
+ batch_idx_ssa,
496
+ head_idx_ssa,
497
+ q_idx=q_idx_ssa,
498
+ kv_idx=kv_idx_ssa,
499
+ seqlen_info=seqlen_info,
500
+ aux_tensors=aux_args,
501
+ )
502
+
503
+ # Write back modified scores
504
+ score_vec.store(post_mod_scores)
505
+ for j in cutlass.range(vec_size, unroll_full=True):
506
+ score_tensor[i + j] = score_vec[j]
507
+
508
+
509
+ @cute.jit
510
+ def apply_score_mod_bwd_inner(
511
+ grad_tensor,
512
+ score_tensor,
513
+ index_tensor,
514
+ score_mod_bwd: cutlass.Constexpr,
515
+ batch_idx,
516
+ head_idx,
517
+ softmax_scale,
518
+ vec_size: cutlass.Constexpr,
519
+ qk_acc_dtype: cutlass.Constexpr,
520
+ aux_tensors,
521
+ fastdiv_mods,
522
+ seqlen_info,
523
+ constant_q_idx: cutlass.Constexpr,
524
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
525
+ transpose_indices: cutlass.Constexpr[bool] = False,
526
+ ):
527
+ """Apply backward score modification (joint graph).
528
+
529
+ Args:
530
+ grad_tensor: in/out: dlogits rewritten in-place with d(scaled_scores)
531
+ score_tensor: pre-mod scores (unscaled QK tile), scaled by softmax_scale internally
532
+ index_tensor: Index positions (same as forward)
533
+ score_mod_bwd: The backward score modification function (joint graph)
534
+ batch_idx: Batch index
535
+ head_idx: Head index
536
+ softmax_scale: Scale to apply to score_tensor
537
+ vec_size: Vector size for processing elements
538
+ qk_acc_dtype: Data type for accumulator
539
+ aux_tensors: Optional aux_tensors for FlexAttention
540
+ fastdiv_mods: Tuple of (seqlen_q_divmod, seqlen_k_divmod) for wrapping
541
+ seqlen_info: Sequence length info
542
+ constant_q_idx: If provided, use this constant for all q_idx values
543
+ qhead_per_kvhead: Pack-GQA replication factor
544
+ transpose_indices: If True, swap q_idx/kv_idx in index_tensor
545
+ """
546
+ # Index positions in the index_tensor tuple
547
+ # Forward: index_tensor[...][0] = q_idx, index_tensor[...][1] = kv_idx
548
+ # Backward (transposed): index_tensor[...][0] = kv_idx, index_tensor[...][1] = q_idx
549
+ if cutlass.const_expr(transpose_indices):
550
+ q_idx_pos = cutlass.const_expr(1)
551
+ kv_idx_pos = cutlass.const_expr(0)
552
+ else:
553
+ q_idx_pos = cutlass.const_expr(0)
554
+ kv_idx_pos = cutlass.const_expr(1)
555
+ n_vals = cutlass.const_expr(cute.size(grad_tensor.shape))
556
+ grad_vec = cute.make_fragment(vec_size, qk_acc_dtype)
557
+ score_vec = cute.make_fragment(vec_size, qk_acc_dtype)
558
+ kv_idx_vec = cute.make_fragment(vec_size, cutlass.Int32)
559
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32).broadcast_to((vec_size,))
560
+ q_idx_vec = cute.make_fragment(vec_size, cutlass.Int32)
561
+
562
+ # For Pack-GQA with non-constant q_idx, we need per-element head indices
563
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
564
+ head_idx_vec = cute.make_fragment(vec_size, cutlass.Int32)
565
+
566
+ for i in cutlass.range(0, n_vals, vec_size, unroll_full=True):
567
+ for j in cutlass.range(vec_size, unroll_full=True):
568
+ grad_vec[j] = grad_tensor[i + j]
569
+ # Scale score so joint graph sees same value as forward score_mod
570
+ score_vec[j] = score_tensor[i + j] * softmax_scale
571
+
572
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
573
+ q_idx_packed = index_tensor[i + j][q_idx_pos]
574
+ q_idx_logical = q_idx_packed // qhead_per_kvhead
575
+ head_offset = q_idx_packed - q_idx_logical * qhead_per_kvhead
576
+ head_idx_vec[j] = head_idx * qhead_per_kvhead + head_offset
577
+
578
+ if cutlass.const_expr(aux_tensors is not None and fastdiv_mods is not None):
579
+ if cutlass.const_expr(constant_q_idx is None):
580
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
581
+ q_idx_floored = floor_if_packed(
582
+ index_tensor[i + j][q_idx_pos], qhead_per_kvhead
583
+ )
584
+ _, q_idx_wrapped = divmod(q_idx_floored, seqlen_q_divmod)
585
+ q_idx_vec[j] = q_idx_wrapped
586
+ else:
587
+ _, seqlen_k_divmod = fastdiv_mods
588
+
589
+ _, kv_idx_wrapped = divmod(index_tensor[i + j][kv_idx_pos], seqlen_k_divmod)
590
+ kv_idx_vec[j] = kv_idx_wrapped
591
+ else:
592
+ # No bounds checking - direct indexing
593
+ if constant_q_idx is None:
594
+ q_idx_vec[j] = floor_if_packed(index_tensor[i + j][q_idx_pos], qhead_per_kvhead)
595
+ kv_idx_vec[j] = index_tensor[i + j][kv_idx_pos]
596
+
597
+ grad_ssa = grad_vec.load()
598
+ score_ssa = score_vec.load()
599
+ kv_idx_ssa = kv_idx_vec.load()
600
+
601
+ if cutlass.const_expr(constant_q_idx is None):
602
+ q_idx_ssa = q_idx_vec.load()
603
+ else:
604
+ q_idx_ssa = utils.scalar_to_ssa(constant_q_idx, cutlass.Int32).broadcast_to((vec_size,))
605
+
606
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
607
+ head_idx_ssa = head_idx_vec.load()
608
+ else:
609
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32).broadcast_to((vec_size,))
610
+
611
+ aux_args = []
612
+ if cutlass.const_expr(aux_tensors is not None):
613
+ aux_args = aux_tensors
614
+
615
+ grad_out_ssa = score_mod_bwd(
616
+ grad_ssa,
617
+ score_ssa,
618
+ batch_idx_ssa,
619
+ head_idx_ssa,
620
+ q_idx=q_idx_ssa,
621
+ kv_idx=kv_idx_ssa,
622
+ seqlen_info=seqlen_info,
623
+ aux_tensors=aux_args,
624
+ )
625
+
626
+ grad_vec.store(grad_out_ssa)
627
+ for j in cutlass.range(vec_size, unroll_full=True):
628
+ grad_tensor[i + j] = grad_vec[j]
build/torch-cuda/flashrt_fa4/cute/testing.py ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from contextlib import nullcontext
3
+ from functools import wraps
4
+ from typing import Optional
5
+
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from einops import rearrange, repeat
9
+ from torch._guards import active_fake_mode
10
+ from torch._subclasses.fake_tensor import FakeTensorMode
11
+
12
+
13
+ class IndexFirstAxis(torch.autograd.Function):
14
+ @staticmethod
15
+ def forward(ctx, input, indices):
16
+ ctx.save_for_backward(indices)
17
+ assert input.ndim >= 2
18
+ ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
19
+ second_dim = other_shape.numel()
20
+ return torch.gather(
21
+ rearrange(input, "b ... -> b (...)"),
22
+ 0,
23
+ repeat(indices, "z -> z d", d=second_dim),
24
+ ).reshape(-1, *other_shape)
25
+
26
+ @staticmethod
27
+ def backward(ctx, grad_output):
28
+ (indices,) = ctx.saved_tensors
29
+ assert grad_output.ndim >= 2
30
+ other_shape = grad_output.shape[1:]
31
+ grad_output = rearrange(grad_output, "b ... -> b (...)")
32
+ grad_input = torch.zeros(
33
+ [ctx.first_axis_dim, grad_output.shape[1]],
34
+ device=grad_output.device,
35
+ dtype=grad_output.dtype,
36
+ )
37
+ grad_input.scatter_(0, repeat(indices, "z -> z d", d=grad_output.shape[1]), grad_output)
38
+ return grad_input.reshape(ctx.first_axis_dim, *other_shape), None
39
+
40
+
41
+ index_first_axis = IndexFirstAxis.apply
42
+
43
+
44
+ class IndexPutFirstAxis(torch.autograd.Function):
45
+ @staticmethod
46
+ def forward(ctx, values, indices, first_axis_dim):
47
+ ctx.save_for_backward(indices)
48
+ assert indices.ndim == 1
49
+ assert values.ndim >= 2
50
+ output = torch.zeros(
51
+ first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
52
+ )
53
+ output[indices] = values
54
+ return output
55
+
56
+ @staticmethod
57
+ def backward(ctx, grad_output):
58
+ (indices,) = ctx.saved_tensors
59
+ grad_values = grad_output[indices]
60
+ return grad_values, None, None
61
+
62
+
63
+ index_put_first_axis = IndexPutFirstAxis.apply
64
+
65
+
66
+ def unpad_input(hidden_states, attention_mask, unused_mask=None):
67
+ all_masks = (attention_mask + unused_mask) if unused_mask is not None else attention_mask
68
+ seqlens_in_batch = all_masks.sum(dim=-1, dtype=torch.int32)
69
+ used_seqlens_in_batch = attention_mask.sum(dim=-1, dtype=torch.int32)
70
+ in_fake_mode = active_fake_mode() is not None
71
+ if not in_fake_mode:
72
+ indices = torch.nonzero(all_masks.flatten(), as_tuple=False).flatten()
73
+ max_seqlen_in_batch = seqlens_in_batch.max().item()
74
+ else:
75
+ # torch.nonzero and .item() are not supported in FakeTensorMode
76
+ batch_size, seqlen = attention_mask.shape
77
+ indices = torch.arange(batch_size * seqlen, device=hidden_states.device)
78
+ max_seqlen_in_batch = seqlen
79
+ cu_seqlens = F.pad(torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0))
80
+ return (
81
+ index_first_axis(rearrange(hidden_states, "b s ... -> (b s) ..."), indices),
82
+ indices,
83
+ cu_seqlens,
84
+ max_seqlen_in_batch,
85
+ used_seqlens_in_batch,
86
+ )
87
+
88
+
89
+ def pad_input(hidden_states, indices, batch, seqlen):
90
+ output = index_put_first_axis(hidden_states, indices, batch * seqlen)
91
+ return rearrange(output, "(b s) ... -> b s ...", b=batch)
92
+
93
+
94
+ def generate_random_padding_mask(
95
+ max_seqlen, batch_size, device, mode="random", zero_lengths=False, min_seqlen=None
96
+ ):
97
+ assert mode in ["full", "random", "third"]
98
+ min_seqlen = min_seqlen if min_seqlen is not None else 0 if zero_lengths else 1
99
+ if mode == "full":
100
+ lengths = torch.full((batch_size, 1), max_seqlen, device=device, dtype=torch.int32)
101
+ elif mode == "random":
102
+ lengths = torch.randint(
103
+ max(min_seqlen, max_seqlen - 20),
104
+ max_seqlen + 1,
105
+ (batch_size, 1),
106
+ device=device,
107
+ )
108
+ else:
109
+ lengths = torch.randint(
110
+ max(min_seqlen, max_seqlen // 3),
111
+ max_seqlen + 1,
112
+ (batch_size, 1),
113
+ device=device,
114
+ )
115
+
116
+ if zero_lengths:
117
+ for i in range(batch_size):
118
+ if i % 5 == 0:
119
+ lengths[i] = 0
120
+ lengths[-1] = 0
121
+ padding_mask = (
122
+ repeat(torch.arange(max_seqlen, device=device), "s -> b s", b=batch_size) < lengths
123
+ )
124
+ return padding_mask
125
+
126
+
127
+ def generate_qkv(
128
+ q,
129
+ k,
130
+ v,
131
+ query_padding_mask=None,
132
+ key_padding_mask=None,
133
+ qv=None,
134
+ kvpacked=False,
135
+ qkvpacked=False,
136
+ query_unused_mask=None,
137
+ key_unused_mask=None,
138
+ ):
139
+ assert not (kvpacked and qkvpacked)
140
+ batch_size, seqlen_q, nheads, d = q.shape
141
+ d_v = v.shape[-1]
142
+ _, seqlen_k, nheads_k, _ = k.shape
143
+ assert k.shape == (batch_size, seqlen_k, nheads_k, d)
144
+ assert v.shape == (batch_size, seqlen_k, nheads_k, d_v)
145
+ if query_unused_mask is not None or key_unused_mask is not None:
146
+ assert not kvpacked
147
+ assert not qkvpacked
148
+
149
+ if query_padding_mask is not None:
150
+ q_unpad, indices_q, cu_seqlens_q, max_seqlen_q, seqused_q = unpad_input(
151
+ q, query_padding_mask, query_unused_mask
152
+ )
153
+ output_pad_fn = lambda output_unpad: pad_input(
154
+ output_unpad, indices_q, batch_size, seqlen_q
155
+ )
156
+ qv_unpad = rearrange(qv, "b s ... -> (b s) ...")[indices_q] if qv is not None else None
157
+ else:
158
+ q_unpad = rearrange(q, "b s h d -> (b s) h d")
159
+ cu_seqlens_q = torch.arange(
160
+ 0, (batch_size + 1) * seqlen_q, step=seqlen_q, dtype=torch.int32, device=q_unpad.device
161
+ )
162
+ seqused_q = None
163
+ max_seqlen_q = seqlen_q
164
+ output_pad_fn = lambda output_unpad: rearrange(
165
+ output_unpad, "(b s) h d -> b s h d", b=batch_size
166
+ )
167
+ qv_unpad = rearrange(qv, "b s ... -> (b s) ...") if qv is not None else None
168
+
169
+ if key_padding_mask is not None:
170
+ k_unpad, indices_k, cu_seqlens_k, max_seqlen_k, seqused_k = unpad_input(
171
+ k, key_padding_mask, key_unused_mask
172
+ )
173
+ v_unpad, *_ = unpad_input(v, key_padding_mask, key_unused_mask)
174
+ else:
175
+ k_unpad = rearrange(k, "b s h d -> (b s) h d")
176
+ v_unpad = rearrange(v, "b s h d -> (b s) h d")
177
+ cu_seqlens_k = torch.arange(
178
+ 0, (batch_size + 1) * seqlen_k, step=seqlen_k, dtype=torch.int32, device=k_unpad.device
179
+ )
180
+ seqused_k = None
181
+ max_seqlen_k = seqlen_k
182
+
183
+ if qkvpacked:
184
+ assert (query_padding_mask == key_padding_mask).all()
185
+ assert nheads == nheads_k
186
+ qkv_unpad = torch.stack([q_unpad, k_unpad, v_unpad], dim=1)
187
+ qkv = torch.stack([q, k, v], dim=2)
188
+ if query_padding_mask is not None:
189
+ dqkv_pad_fn = lambda dqkv_unpad: pad_input(dqkv_unpad, indices_q, batch_size, seqlen_q)
190
+ else:
191
+ dqkv_pad_fn = lambda dqkv_unpad: rearrange(
192
+ dqkv_unpad, "(b s) t h d -> b s t h d", b=batch_size
193
+ )
194
+ return (
195
+ qkv_unpad.detach().requires_grad_(),
196
+ cu_seqlens_q,
197
+ max_seqlen_q,
198
+ qkv.detach().requires_grad_(),
199
+ output_pad_fn,
200
+ dqkv_pad_fn,
201
+ )
202
+ elif kvpacked:
203
+ kv_unpad = torch.stack([k_unpad, v_unpad], dim=1)
204
+ kv = torch.stack([k, v], dim=2)
205
+ dq_pad_fn = output_pad_fn
206
+ if key_padding_mask is not None:
207
+ dkv_pad_fn = lambda dkv_unpad: pad_input(dkv_unpad, indices_k, batch_size, seqlen_k)
208
+ else:
209
+ dkv_pad_fn = lambda dkv_unpad: rearrange(
210
+ dkv_unpad, "(b s) t h d -> b s t h d", b=batch_size
211
+ )
212
+ return (
213
+ q_unpad.detach().requires_grad_(),
214
+ kv_unpad.detach().requires_grad_(),
215
+ cu_seqlens_q,
216
+ cu_seqlens_k,
217
+ max_seqlen_q,
218
+ max_seqlen_k,
219
+ q.detach().requires_grad_(),
220
+ kv.detach().requires_grad_(),
221
+ output_pad_fn,
222
+ dq_pad_fn,
223
+ dkv_pad_fn,
224
+ )
225
+ else:
226
+ dq_pad_fn = output_pad_fn
227
+ if key_padding_mask is not None:
228
+ dk_pad_fn = lambda dk_unpad: pad_input(dk_unpad, indices_k, batch_size, seqlen_k)
229
+ else:
230
+ dk_pad_fn = lambda dk_unpad: rearrange(dk_unpad, "(b s) h d -> b s h d", b=batch_size)
231
+ return (
232
+ q_unpad.detach().requires_grad_(),
233
+ k_unpad.detach().requires_grad_(),
234
+ v_unpad.detach().requires_grad_(),
235
+ qv_unpad.detach() if qv is not None else None,
236
+ cu_seqlens_q,
237
+ cu_seqlens_k,
238
+ seqused_q,
239
+ seqused_k,
240
+ max_seqlen_q,
241
+ max_seqlen_k,
242
+ q.detach().requires_grad_(),
243
+ k.detach().requires_grad_(),
244
+ v.detach().requires_grad_(),
245
+ qv.detach() if qv is not None else None,
246
+ output_pad_fn,
247
+ dq_pad_fn,
248
+ dk_pad_fn,
249
+ )
250
+
251
+
252
+ def construct_local_mask(
253
+ seqlen_q,
254
+ seqlen_k,
255
+ window_size=(None, None),
256
+ sink_token_length=0,
257
+ query_padding_mask=None,
258
+ key_padding_mask=None,
259
+ key_leftpad=None,
260
+ device=None,
261
+ ):
262
+ row_idx = rearrange(torch.arange(seqlen_q, device=device, dtype=torch.long), "s -> s 1")
263
+ col_idx = torch.arange(seqlen_k, device=device, dtype=torch.long)
264
+ if key_leftpad is not None:
265
+ key_leftpad = rearrange(key_leftpad, "b -> b 1 1 1")
266
+ col_idx = repeat(col_idx, "s -> b 1 1 s", b=key_leftpad.shape[0])
267
+ col_idx = torch.where(col_idx >= key_leftpad, col_idx - key_leftpad, 2**32)
268
+ sk = (
269
+ seqlen_k
270
+ if key_padding_mask is None
271
+ else rearrange(key_padding_mask.sum(-1), "b -> b 1 1 1")
272
+ )
273
+ sq = (
274
+ seqlen_q
275
+ if query_padding_mask is None
276
+ else rearrange(query_padding_mask.sum(-1), "b -> b 1 1 1")
277
+ )
278
+ if window_size[0] is None:
279
+ return col_idx > row_idx + sk - sq + window_size[1]
280
+ else:
281
+ sk = torch.full_like(col_idx, seqlen_k) if key_padding_mask is None else sk
282
+ if window_size[1] is None:
283
+ local_mask_left = col_idx > sk
284
+ else:
285
+ local_mask_left = col_idx > torch.minimum(row_idx + sk - sq + window_size[1], sk)
286
+ return torch.logical_or(
287
+ local_mask_left,
288
+ torch.logical_and(
289
+ col_idx < row_idx + sk - sq - window_size[0], col_idx >= sink_token_length
290
+ ),
291
+ )
292
+
293
+
294
+ def construct_chunk_mask(
295
+ seqlen_q,
296
+ seqlen_k,
297
+ attention_chunk,
298
+ query_padding_mask=None,
299
+ key_padding_mask=None,
300
+ key_leftpad=None,
301
+ device=None,
302
+ ):
303
+ row_idx = rearrange(torch.arange(seqlen_q, device=device, dtype=torch.long), "s -> s 1")
304
+ col_idx = torch.arange(seqlen_k, device=device, dtype=torch.long)
305
+ if key_leftpad is not None:
306
+ key_leftpad = rearrange(key_leftpad, "b -> b 1 1 1")
307
+ col_idx = repeat(col_idx, "s -> b 1 1 s", b=key_leftpad.shape[0])
308
+ col_idx = torch.where(col_idx >= key_leftpad, col_idx - key_leftpad, 2**32)
309
+ sk = (
310
+ seqlen_k
311
+ if key_padding_mask is None
312
+ else rearrange(key_padding_mask.sum(-1), "b -> b 1 1 1")
313
+ )
314
+ sq = (
315
+ seqlen_q
316
+ if query_padding_mask is None
317
+ else rearrange(query_padding_mask.sum(-1), "b -> b 1 1 1")
318
+ )
319
+ sk = torch.full_like(col_idx, seqlen_k) if key_padding_mask is None else sk
320
+ col_limit_left_chunk = row_idx + sk - sq - (row_idx + sk - sq) % attention_chunk
321
+ return torch.logical_or(
322
+ col_idx < col_limit_left_chunk, col_idx >= col_limit_left_chunk + attention_chunk
323
+ )
324
+
325
+
326
+ def attention_ref(
327
+ q,
328
+ k,
329
+ v,
330
+ query_padding_mask=None,
331
+ key_padding_mask=None,
332
+ key_leftpad=None,
333
+ attn_bias=None,
334
+ dropout_p=0.0,
335
+ dropout_mask=None,
336
+ causal=False,
337
+ qv=None,
338
+ q_descale=None,
339
+ k_descale=None,
340
+ v_descale=None,
341
+ window_size=(None, None),
342
+ attention_chunk=0,
343
+ sink_token_length=0,
344
+ learnable_sink: Optional[torch.Tensor] = None,
345
+ softcap=0.0,
346
+ upcast=True,
347
+ reorder_ops=False,
348
+ intermediate_dtype=None,
349
+ return_lse=False,
350
+ gather_kv_indices=None,
351
+ ):
352
+ if causal:
353
+ window_size = (window_size[0], 0)
354
+ dtype_og = q.dtype
355
+ if upcast:
356
+ q, k, v = q.float(), k.float(), v.float()
357
+ qv = qv.float() if qv is not None else None
358
+ if q_descale is not None:
359
+ q_descale = repeat(q_descale, "b h -> b 1 (h g) 1", g=q.shape[2] // k.shape[2])
360
+ q = (q.float() * q_descale).to(q.dtype)
361
+ qv = (qv.float() * q_descale).to(qv.dtype) if qv is not None else None
362
+ if k_descale is not None:
363
+ k = (k.float() * rearrange(k_descale, "b h -> b 1 h 1")).to(dtype=k.dtype)
364
+ if v_descale is not None:
365
+ v = (v.float() * rearrange(v_descale, "b h -> b 1 h 1")).to(dtype=v.dtype)
366
+ seqlen_q, seqlen_k = q.shape[1], k.shape[1]
367
+ k = repeat(k, "b s h d -> b s (h g) d", g=q.shape[2] // k.shape[2])
368
+ v = repeat(v, "b s h d -> b s (h g) d", g=q.shape[2] // v.shape[2])
369
+ d = q.shape[-1]
370
+ dv = v.shape[-1]
371
+ softmax_scale = 1.0 / math.sqrt(d if qv is None else d + dv)
372
+ if not reorder_ops:
373
+ scores = torch.einsum("bthd,bshd->bhts", q * softmax_scale, k)
374
+ else:
375
+ scores = torch.einsum("bthd,bshd->bhts", q, k * softmax_scale)
376
+ if qv is not None:
377
+ scores = scores + torch.einsum("bthd,bshd->bhts", qv * softmax_scale, v)
378
+ if softcap > 0:
379
+ scores = torch.tanh(scores / softcap) * softcap
380
+ if key_padding_mask is not None:
381
+ scores.masked_fill_(rearrange(~key_padding_mask, "b s -> b 1 1 s"), float("-inf"))
382
+ local_mask = None
383
+ if window_size[0] is not None or window_size[1] is not None:
384
+ local_mask = construct_local_mask(
385
+ seqlen_q,
386
+ seqlen_k,
387
+ window_size,
388
+ sink_token_length,
389
+ query_padding_mask,
390
+ key_padding_mask,
391
+ key_leftpad=key_leftpad,
392
+ device=q.device,
393
+ )
394
+ if attention_chunk > 0:
395
+ chunk_mask = construct_chunk_mask(
396
+ seqlen_q,
397
+ seqlen_k,
398
+ attention_chunk,
399
+ query_padding_mask,
400
+ key_padding_mask,
401
+ key_leftpad=key_leftpad,
402
+ device=q.device,
403
+ )
404
+ local_mask = (
405
+ torch.logical_or(local_mask, chunk_mask) if local_mask is not None else chunk_mask
406
+ )
407
+ if gather_kv_indices is not None:
408
+ batch = q.shape[0]
409
+ topk_len = gather_kv_indices.shape[2]
410
+ if topk_len < seqlen_k:
411
+ topk_index_mask = torch.full(
412
+ (batch, seqlen_q, seqlen_k), False, device="cuda"
413
+ ).scatter_(-1, gather_kv_indices, True)
414
+ scores.masked_fill_(rearrange(~topk_index_mask, "b t s -> b 1 t s"), float("-inf"))
415
+ if local_mask is not None:
416
+ scores.masked_fill_(local_mask, float("-inf"))
417
+ if attn_bias is not None:
418
+ scores = scores + attn_bias
419
+ # After all masks are applied, before softmax:
420
+ # scores shape: [b, h, t, s]
421
+ lse = torch.logsumexp(scores, dim=-1) # [b, h, t]
422
+ if learnable_sink is None:
423
+ attention = torch.softmax(scores, dim=-1).to(v.dtype)
424
+ else:
425
+ scores_fp32 = scores.to(torch.float32)
426
+ logits_max = torch.amax(scores_fp32, dim=-1, keepdim=True)
427
+ learnable_sink = rearrange(learnable_sink, "h -> h 1 1")
428
+ logits_or_sinks_max = torch.maximum(learnable_sink, logits_max)
429
+ unnormalized_scores = torch.exp(scores_fp32 - logits_or_sinks_max)
430
+ normalizer = unnormalized_scores.sum(dim=-1, keepdim=True) + torch.exp(
431
+ learnable_sink - logits_or_sinks_max
432
+ )
433
+ # LSE with sink: log(Z) = log(normalizer) + max
434
+ lse = (torch.log(normalizer.squeeze(-1)) + logits_or_sinks_max.squeeze(-1)).to(dtype_og)
435
+ attention = (unnormalized_scores / normalizer).to(v.dtype)
436
+ if query_padding_mask is not None:
437
+ attention = attention.masked_fill(rearrange(~query_padding_mask, "b s -> b 1 s 1"), 0.0)
438
+ if key_padding_mask is not None:
439
+ attention = attention.masked_fill(rearrange(~key_padding_mask, "b s -> b 1 1 s"), 0.0)
440
+ if local_mask is not None:
441
+ attention = attention.masked_fill(torch.all(local_mask, dim=-1, keepdim=True), 0.0)
442
+ dropout_scaling = 1.0 / (1 - dropout_p)
443
+ if dropout_mask is not None:
444
+ attention_drop = attention.masked_fill(~dropout_mask, 0.0)
445
+ else:
446
+ attention_drop = attention
447
+ if intermediate_dtype is not None:
448
+ attention_drop = attention_drop.to(intermediate_dtype).to(attention_drop.dtype)
449
+ output = torch.einsum("bhts,bshd->bthd", attention_drop, v * dropout_scaling)
450
+ if query_padding_mask is not None:
451
+ output.masked_fill_(rearrange(~query_padding_mask, "b s -> b s 1 1"), 0.0)
452
+ if return_lse:
453
+ return output.to(dtype_og), attention.to(dtype_og), lse.to(dtype_og)
454
+ return output.to(dtype=dtype_og), attention.to(dtype=dtype_og)
455
+
456
+
457
+ def maybe_fake_tensor_mode(fake: bool = True):
458
+ """
459
+ One way to populate/pre-compile cache is to use torch fake tensor mode,
460
+ which does not allocate actual GPU tensors but retains tensor shape/dtype
461
+ metadata for cute.compile.
462
+ """
463
+
464
+ def decorator(fn):
465
+ @wraps(fn)
466
+ def wrapper(*args, **kwargs):
467
+ with FakeTensorMode() if fake else nullcontext():
468
+ return fn(*args, **kwargs)
469
+
470
+ return wrapper
471
+
472
+ return decorator
473
+
474
+
475
+ def is_fake_mode() -> bool:
476
+ return active_fake_mode() is not None
build/torch-cuda/flashrt_fa4/cute/tile_scheduler.py ADDED
@@ -0,0 +1,1636 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao, Siyu Wang, Shengbin Di, Yuxi Chi, Johnsonms, Linfeng Zheng, Haoyan Huang, Lanbo Li, Yun Zhong, Man Yuan, Minmin Sun, Yong Li, Wei Lin.
2
+
3
+ from enum import IntEnum, auto
4
+ from typing import Optional, Tuple, Protocol, runtime_checkable
5
+ from dataclasses import dataclass
6
+
7
+ try:
8
+ from typing import override
9
+ except ImportError: # Python < 3.12
10
+ from typing_extensions import override
11
+
12
+ import cutlass
13
+ from cutlass.pipeline import PipelineClcFetchAsync, PipelineState
14
+ from cutlass._mlir import ir
15
+ import cutlass.cute as cute
16
+ from cutlass import Int32, const_expr
17
+ from cutlass.cute import FastDivmodDivisor
18
+ from cutlass.utils import ClcDynamicPersistentTileScheduler, ClcDynamicPersistentTileSchedulerParams
19
+ from cutlass.cute.typing import Boolean
20
+ from cutlass.cutlass_dsl import (
21
+ min as dsl_min,
22
+ extract_mlir_values,
23
+ new_from_mlir_values,
24
+ )
25
+ from cutlass.utils.hardware_info import HardwareInfo
26
+
27
+ from fa4_cute_runtime.quack.cute_dsl_utils import ParamsBase
28
+
29
+ import fa4_cute_runtime.flashrt_fa4.cute.utils as utils
30
+ from fa4_cute_runtime.flashrt_fa4.cute.fast_math import clz
31
+
32
+
33
+ class SchedulingMode(IntEnum):
34
+ NONE = auto()
35
+ STATIC = auto()
36
+ DYNAMIC = auto()
37
+ CLC = auto()
38
+
39
+
40
+ @dataclass
41
+ class ClcState(ParamsBase):
42
+ """Owns the runtime state shared by CLC-capable tile schedulers.
43
+
44
+ `FlashAttentionForwardSm100` constructs this state because it owns the CLC
45
+ response buffer, mbarrier storage, and launch geometry needed to initialize
46
+ the hardware scheduler and async pipeline. Individual tile schedulers then
47
+ consume this state and map the returned hardware work tiles into their own
48
+ logical `WorkTileInfo` coordinates.
49
+
50
+ To add CLC support to a scheduler:
51
+ - implement `clc_problem_shape(params)` so the kernel can create the hardware scheduler
52
+ - accept `clc: ClcState | None` in `create(...)` / `__init__`
53
+ - map `clc.initial_work_tile_info()` and `clc.get_current_work()` into scheduler coordinates
54
+ """
55
+
56
+ _hw_scheduler: ClcDynamicPersistentTileScheduler
57
+ _pipeline: PipelineClcFetchAsync
58
+ _consumer_state: PipelineState
59
+ _producer_state: PipelineState
60
+
61
+ @staticmethod
62
+ def create(
63
+ *,
64
+ hw_scheduler: ClcDynamicPersistentTileScheduler,
65
+ pipeline: PipelineClcFetchAsync,
66
+ consumer_state: PipelineState,
67
+ producer_state: PipelineState,
68
+ ) -> "ClcState":
69
+ return ClcState(hw_scheduler, pipeline, consumer_state, producer_state)
70
+
71
+ def initial_work_tile_info(self):
72
+ return self._hw_scheduler.initial_work_tile_info()
73
+
74
+ def get_current_work(self):
75
+ return self._hw_scheduler.get_current_work()
76
+
77
+ def prefetch_next_work(self, *, loc=None, ip=None):
78
+ self._pipeline.producer_acquire(self._producer_state, loc=loc, ip=ip)
79
+ mbarrier_addr = self._pipeline.producer_get_barrier(self._producer_state, loc=loc, ip=ip)
80
+ self._hw_scheduler.advance_to_next_work(mbarrier_addr, loc=loc, ip=ip)
81
+ self._producer_state.advance(loc=loc, ip=ip)
82
+
83
+ def consumer_wait(self, *, loc=None, ip=None):
84
+ self._pipeline.consumer_wait(self._consumer_state, loc=loc, ip=ip)
85
+
86
+ def consumer_release(self, *, loc=None, ip=None):
87
+ self._pipeline.consumer_release(self._consumer_state, loc=loc, ip=ip)
88
+ self._consumer_state.advance(loc=loc, ip=ip)
89
+
90
+ def producer_tail(self, *, loc=None, ip=None):
91
+ self._pipeline.producer_tail(self._producer_state, loc=loc, ip=ip)
92
+
93
+
94
+ class WorkTileInfo(cutlass.utils.WorkTileInfo):
95
+ """Altered WorkTileInfo which includes four axes: (block, head, batch, split)"""
96
+
97
+ @override
98
+ def __new_from_mlir_values__(self, values: list[ir.Value]) -> "WorkTileInfo":
99
+ assert len(values) == 5
100
+ new_tile_idx = cutlass.new_from_mlir_values(self._tile_idx, values[:-1])
101
+ new_is_valid_tile = cutlass.new_from_mlir_values(self._is_valid_tile, [values[-1]])
102
+ return WorkTileInfo(new_tile_idx, new_is_valid_tile)
103
+
104
+
105
+ @runtime_checkable
106
+ class TileSchedulerProtocol(Protocol):
107
+ """Protocol defining the interface all tile schedulers must implement.
108
+
109
+ Schedulers are responsible for:
110
+ 1. Coordinate mapping: linear tile index -> (m_block, head, batch, split)
111
+ 2. Work distribution: how to get the next tile (static grid-stride vs CLC dynamic)
112
+ """
113
+
114
+ def get_current_work(self) -> WorkTileInfo:
115
+ """Get the current work tile coordinates."""
116
+ ...
117
+
118
+ def initial_work_tile_info(self) -> WorkTileInfo:
119
+ """Get the initial work tile for this CTA."""
120
+ ...
121
+
122
+ def advance_to_next_work(self, *, loc=None, ip=None):
123
+ """Consumer-side advance: move to next tile and return it.
124
+
125
+ For static schedulers: grid-stride increment + get_current_work.
126
+ For CLC schedulers: consumer wait + get_current_work + consumer release + state advance.
127
+ """
128
+ ...
129
+
130
+ def prefetch_next_work(self, *, loc=None, ip=None) -> None:
131
+ """Producer-side prefetch of next work tile (no-op for static schedulers).
132
+
133
+ For CLC schedulers: producer acquire + issue CLC query + producer state advance.
134
+ Only called by the scheduler warp.
135
+ """
136
+ ...
137
+
138
+ def producer_tail(self, *, loc=None, ip=None) -> None:
139
+ """Producer-side cleanup after the last tile.
140
+
141
+ No-op for static schedulers. For CLC schedulers: pipeline producer_tail.
142
+ """
143
+ ...
144
+
145
+
146
+ @dataclass
147
+ class TileSchedulerArguments(ParamsBase):
148
+ num_block: Int32
149
+ num_head: Int32
150
+ num_batch: Int32
151
+ num_splits: Int32
152
+ seqlen_k: Int32
153
+ headdim: Int32
154
+ headdim_v: Int32
155
+ total_q: Int32
156
+ tile_shape_mn: cutlass.Constexpr[Tuple[int, int]]
157
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
158
+ mCuSeqlensQ: Optional[cute.Tensor] = None
159
+ mSeqUsedQ: Optional[cute.Tensor] = None
160
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
161
+ element_size: cutlass.Constexpr[int] = 2
162
+ is_persistent: cutlass.Constexpr[bool] = False
163
+ lpt: cutlass.Constexpr[bool] = False
164
+ is_split_kv: cutlass.Constexpr[bool] = False
165
+ head_swizzle: cutlass.Constexpr[bool] = False
166
+ use_cluster_idx: cutlass.Constexpr[bool] = False
167
+
168
+
169
+ class SingleTileScheduler:
170
+ @dataclass
171
+ class Params(ParamsBase):
172
+ num_block: Int32
173
+ num_head: Int32
174
+ num_batch: Int32
175
+ num_splits: Int32
176
+ num_splits_divmod: FastDivmodDivisor
177
+ is_split_kv: cutlass.Constexpr[bool] = False
178
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
179
+ use_cluster_idx: cutlass.Constexpr[bool] = False
180
+
181
+ @staticmethod
182
+ def create(
183
+ args: TileSchedulerArguments, *, loc=None, ip=None
184
+ ) -> "SingleTileScheduler.Params":
185
+ return SingleTileScheduler.Params(
186
+ args.num_block,
187
+ args.num_head,
188
+ args.num_batch,
189
+ args.num_splits,
190
+ FastDivmodDivisor(args.num_splits),
191
+ args.is_split_kv,
192
+ args.cluster_shape_mn,
193
+ args.use_cluster_idx,
194
+ )
195
+
196
+ def __init__(self, params: Params, blk_coord: cute.Coord, *, loc=None, ip=None):
197
+ self.params = params
198
+ self._blk_coord = blk_coord
199
+ self._is_first_block = True
200
+ self._loc = loc
201
+ self._ip = ip
202
+
203
+ @staticmethod
204
+ def to_underlying_arguments(
205
+ args: TileSchedulerArguments,
206
+ *,
207
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
208
+ loc=None,
209
+ ip=None,
210
+ ) -> Params:
211
+ assert scheduling_mode == SchedulingMode.STATIC, (
212
+ f"SingleTileScheduler only supports STATIC, got {scheduling_mode!r}"
213
+ )
214
+ return SingleTileScheduler.Params.create(args, loc=loc, ip=ip)
215
+
216
+ @staticmethod
217
+ def create(
218
+ params: Params, clc: ClcState | None = None, *, loc=None, ip=None
219
+ ) -> "SingleTileScheduler":
220
+ if const_expr(cute.size(params.cluster_shape_mn) == 1 or not params.use_cluster_idx):
221
+ blk_coord = cute.arch.block_idx()
222
+ else:
223
+ blk_coord = cute.arch.cluster_idx()
224
+ return SingleTileScheduler(params, blk_coord, loc=loc, ip=ip)
225
+
226
+ # called by host
227
+ @staticmethod
228
+ def get_grid_shape(
229
+ params: Params,
230
+ *,
231
+ loc=None,
232
+ ip=None,
233
+ ) -> Tuple[Int32, Int32, Int32]:
234
+ # TODO: this hard-codes the fact that we only use cluster = (1, 1) or (2, 1)
235
+ assert params.cluster_shape_mn[1] == 1, "Only cluster_shape_mn[1] == 1 is supported"
236
+ if const_expr(params.use_cluster_idx):
237
+ # Grid must have num_block * cluster_m physical blocks so that there are num_block clusters
238
+ grid_x = params.num_block * params.cluster_shape_mn[0]
239
+ else:
240
+ grid_x = cute.round_up(params.num_block, params.cluster_shape_mn[0])
241
+ return (
242
+ grid_x,
243
+ params.num_head * params.num_splits,
244
+ params.num_batch,
245
+ )
246
+
247
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
248
+ block_idx, head_idx, batch_idx = self._blk_coord
249
+ if const_expr(self.params.is_split_kv):
250
+ head_idx, split_idx = divmod(head_idx, self.params.num_splits_divmod)
251
+ else:
252
+ split_idx = Int32(0)
253
+ return WorkTileInfo(
254
+ (block_idx, head_idx, batch_idx, split_idx),
255
+ self._is_first_block,
256
+ )
257
+
258
+ def initial_work_tile_info(self, *, loc=None, ip=None):
259
+ return self.get_current_work(loc=loc, ip=ip)
260
+
261
+ def prefetch_next_work(self, *, loc=None, ip=None):
262
+ pass
263
+
264
+ def advance_to_next_work(self, *, loc=None, ip=None):
265
+ self._is_first_block = False
266
+ return self.get_current_work()
267
+
268
+ def producer_tail(self, *, loc=None, ip=None):
269
+ pass
270
+
271
+ def __extract_mlir_values__(self):
272
+ values, self._values_pos = [], []
273
+ for obj in [self.params, self._blk_coord]:
274
+ obj_values = cutlass.extract_mlir_values(obj)
275
+ values += obj_values
276
+ self._values_pos.append(len(obj_values))
277
+ return values
278
+
279
+ def __new_from_mlir_values__(self, values):
280
+ obj_list = []
281
+ for obj, n_items in zip([self.params, self._blk_coord], self._values_pos):
282
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
283
+ values = values[n_items:]
284
+ return SingleTileScheduler(*(tuple(obj_list)), loc=self._loc)
285
+
286
+
287
+ class StaticPersistentTileScheduler:
288
+ @dataclass
289
+ class Params(ParamsBase):
290
+ num_block_cluster_divmod: FastDivmodDivisor
291
+ num_head_divmod: FastDivmodDivisor
292
+ total_blocks_cluster: Int32
293
+ cluster_shape_m: cutlass.Constexpr[int] = 1
294
+
295
+ @staticmethod
296
+ def create(
297
+ args: TileSchedulerArguments, *, loc=None, ip=None
298
+ ) -> "StaticPersistentTileScheduler.Params":
299
+ num_block_cluster = cute.ceil_div(args.num_block, cute.size(args.cluster_shape_mn))
300
+ total_blocks_cluster = num_block_cluster * args.num_head * args.num_batch
301
+ return StaticPersistentTileScheduler.Params(
302
+ FastDivmodDivisor(num_block_cluster),
303
+ FastDivmodDivisor(args.num_head),
304
+ total_blocks_cluster,
305
+ cluster_shape_m=args.cluster_shape_mn[0],
306
+ )
307
+
308
+ def __init__(self, params: Params, tile_idx: Int32, *, loc=None, ip=None):
309
+ self.params = params
310
+ self._tile_idx = tile_idx
311
+ self._loc = loc
312
+ self._ip = ip
313
+
314
+ @staticmethod
315
+ def to_underlying_arguments(
316
+ args: TileSchedulerArguments,
317
+ *,
318
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
319
+ loc=None,
320
+ ip=None,
321
+ ) -> Params:
322
+ assert scheduling_mode == SchedulingMode.STATIC, (
323
+ f"StaticPersistentTileScheduler only supports STATIC, got {scheduling_mode!r}"
324
+ )
325
+ return StaticPersistentTileScheduler.Params.create(args, loc=loc, ip=ip)
326
+
327
+ @staticmethod
328
+ def create(
329
+ params: Params, clc: ClcState | None = None, *, loc=None, ip=None
330
+ ) -> "StaticPersistentTileScheduler":
331
+ if const_expr(cute.size(params.cluster_shape_m) == 1):
332
+ tile_idx = cute.arch.block_idx()[0]
333
+ else:
334
+ tile_idx = cute.arch.cluster_idx()[0]
335
+ return StaticPersistentTileScheduler(params, tile_idx, loc=loc, ip=ip)
336
+
337
+ @staticmethod
338
+ def get_grid_shape(
339
+ params: Params,
340
+ *,
341
+ loc=None,
342
+ ip=None,
343
+ ) -> Tuple[Int32, Int32, Int32]:
344
+ hardware_info = cutlass.utils.HardwareInfo()
345
+ sm_count = hardware_info.get_device_multiprocessor_count()
346
+ max_ctas = (sm_count // params.cluster_shape_m) * params.cluster_shape_m
347
+ grid_x = cutlass.min(max_ctas, params.total_blocks_cluster * params.cluster_shape_m)
348
+ return (grid_x, Int32(1), Int32(1))
349
+
350
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
351
+ hn_idx, block_idx = divmod(self._tile_idx, self.params.num_block_cluster_divmod)
352
+ batch_idx, head_idx = divmod(hn_idx, self.params.num_head_divmod)
353
+ is_valid = self._tile_idx < self.params.total_blocks_cluster
354
+ return WorkTileInfo(
355
+ (Int32(block_idx), Int32(head_idx), Int32(batch_idx), Int32(0)), is_valid
356
+ )
357
+
358
+ def initial_work_tile_info(self, *, loc=None, ip=None):
359
+ return self.get_current_work(loc=loc, ip=ip)
360
+
361
+ def prefetch_next_work(self, *, loc=None, ip=None):
362
+ pass
363
+
364
+ def advance_to_next_work(self, *, loc=None, ip=None):
365
+ if const_expr(self.params.cluster_shape_m == 1):
366
+ self._tile_idx += cute.arch.grid_dim()[0]
367
+ else:
368
+ self._tile_idx += cute.arch.cluster_dim()[0]
369
+ return self.get_current_work()
370
+
371
+ def producer_tail(self, *, loc=None, ip=None):
372
+ pass
373
+
374
+ def __extract_mlir_values__(self):
375
+ values, self._values_pos = [], []
376
+ for obj in [self.params, self._tile_idx]:
377
+ obj_values = cutlass.extract_mlir_values(obj)
378
+ values += obj_values
379
+ self._values_pos.append(len(obj_values))
380
+ return values
381
+
382
+ def __new_from_mlir_values__(self, values):
383
+ obj_list = []
384
+ for obj, n_items in zip(
385
+ [self.params, self._tile_idx],
386
+ self._values_pos,
387
+ ):
388
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
389
+ values = values[n_items:]
390
+ return StaticPersistentTileScheduler(*(tuple(obj_list)), loc=self._loc)
391
+
392
+
393
+ class SingleTileLPTScheduler:
394
+ @dataclass
395
+ class Params(ParamsBase):
396
+ total_blocks: Int32
397
+ num_splits: Int32
398
+ num_block: Int32
399
+ num_head: Int32
400
+ num_batch: Int32
401
+ l2_minor: Int32
402
+ num_head_divmod: FastDivmodDivisor
403
+ l2_minor_divmod: FastDivmodDivisor
404
+ l2_major_divmod: FastDivmodDivisor
405
+ l2_minor_residual_divmod: FastDivmodDivisor
406
+ num_hb_quotient: Int32
407
+ num_splits_divmod: FastDivmodDivisor
408
+ is_split_kv: cutlass.Constexpr[bool] = False
409
+ cluster_shape_m: cutlass.Constexpr[int] = 1
410
+ scheduling_mode: cutlass.Constexpr[SchedulingMode] = SchedulingMode.STATIC
411
+ lpt: cutlass.Constexpr[bool] = True
412
+ use_cluster_idx: cutlass.Constexpr[bool] = True
413
+
414
+ @staticmethod
415
+ @cute.jit
416
+ def create(
417
+ args: TileSchedulerArguments,
418
+ *,
419
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
420
+ loc=None,
421
+ ip=None,
422
+ ) -> "SingleTileLPTScheduler.Params":
423
+ assert scheduling_mode in (SchedulingMode.STATIC, SchedulingMode.CLC), (
424
+ f"Only STATIC and CLC are supported, got {scheduling_mode!r}"
425
+ )
426
+ size_one_kv_head = args.seqlen_k * (args.headdim + args.headdim_v) * args.element_size
427
+ size_one_head = size_one_kv_head
428
+ size_l2 = 50 * 1024 * 1024 # 40 MB for K & V
429
+ # Swizzle is the size of each "section". Round swizzle to a power of 2
430
+ # Need to be careful about the case where only one head will fit
431
+ # swizzle is how many heads can fit in L2
432
+ # Seems faster if swizzle is a power of 2
433
+ log2_floor = lambda n: 31 - clz(n)
434
+ swizzle = 1 if size_l2 < size_one_head else (1 << log2_floor(size_l2 // size_one_head))
435
+ # If we're in the last section (called residual), we don't want to divide by
436
+ # swizzle. Instead we want to divide by the remainder.
437
+ num_hb_quotient = (args.num_head * args.num_batch) // swizzle
438
+ num_hb_remainder = (args.num_head * args.num_batch) % swizzle
439
+ return SingleTileLPTScheduler.Params(
440
+ total_blocks=args.num_block * args.num_head * args.num_batch,
441
+ num_block=args.num_block,
442
+ num_head=args.num_head,
443
+ num_batch=args.num_batch,
444
+ l2_minor=Int32(swizzle),
445
+ num_head_divmod=FastDivmodDivisor(args.num_head),
446
+ l2_minor_divmod=FastDivmodDivisor(swizzle),
447
+ l2_major_divmod=FastDivmodDivisor(swizzle * args.num_block),
448
+ l2_minor_residual_divmod=FastDivmodDivisor(max(num_hb_remainder, 1)),
449
+ num_hb_quotient=Int32(num_hb_quotient),
450
+ num_splits=args.num_splits,
451
+ num_splits_divmod=FastDivmodDivisor(args.num_splits),
452
+ is_split_kv=args.is_split_kv,
453
+ cluster_shape_m=args.cluster_shape_mn[0],
454
+ scheduling_mode=scheduling_mode,
455
+ lpt=args.lpt,
456
+ use_cluster_idx=args.use_cluster_idx,
457
+ )
458
+
459
+ def __init__(
460
+ self,
461
+ params: Params,
462
+ tile_idx: Int32,
463
+ split_idx: Int32,
464
+ clc: ClcState | None = None,
465
+ *,
466
+ loc=None,
467
+ ip=None,
468
+ ):
469
+ self.params = params
470
+ self._tile_idx = tile_idx
471
+ self._split_idx = split_idx
472
+ self.clc = clc
473
+ self._loc = loc
474
+ self._ip = ip
475
+
476
+ @staticmethod
477
+ def to_underlying_arguments(
478
+ args: TileSchedulerArguments,
479
+ *,
480
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
481
+ loc=None,
482
+ ip=None,
483
+ ) -> Params:
484
+ return SingleTileLPTScheduler.Params.create(
485
+ args, scheduling_mode=scheduling_mode, loc=loc, ip=ip
486
+ )
487
+
488
+ @staticmethod
489
+ def _clc_grid_shape(params: Params):
490
+ num_batch_splits = (
491
+ params.num_batch * params.num_splits
492
+ if const_expr(params.is_split_kv)
493
+ else params.num_batch
494
+ )
495
+ return (
496
+ cute.round_up(params.num_block, params.cluster_shape_m),
497
+ params.num_head,
498
+ num_batch_splits,
499
+ )
500
+
501
+ @staticmethod
502
+ @cute.jit
503
+ def clc_problem_shape(params: Params):
504
+ return ClcDynamicPersistentTileSchedulerParams(
505
+ problem_shape_ntile_mnl=SingleTileLPTScheduler._clc_grid_shape(params),
506
+ cluster_shape_mnk=(params.cluster_shape_m, 1, 1),
507
+ )
508
+
509
+ @staticmethod
510
+ @cute.jit
511
+ def create(
512
+ params: Params, clc: ClcState | None = None, *, loc=None, ip=None
513
+ ) -> "SingleTileLPTScheduler":
514
+ if const_expr(params.scheduling_mode == SchedulingMode.CLC):
515
+ return SingleTileLPTScheduler(
516
+ params, cute.arch.block_idx()[0], Int32(0), clc, loc=loc, ip=ip
517
+ )
518
+ tile_idx, split_idx, _ = cute.arch.block_idx()
519
+ return SingleTileLPTScheduler(params, tile_idx, split_idx, loc=loc, ip=ip)
520
+
521
+ @staticmethod
522
+ def get_grid_shape(
523
+ params: Params,
524
+ *,
525
+ loc=None,
526
+ ip=None,
527
+ ) -> Tuple[Int32, Int32, Int32]:
528
+ if const_expr(params.scheduling_mode == SchedulingMode.CLC):
529
+ return SingleTileLPTScheduler._clc_grid_shape(params)
530
+ return (params.total_blocks, params.num_splits, Int32(1))
531
+
532
+ @cute.jit
533
+ def clc_work_to_coords(self, work) -> WorkTileInfo:
534
+ """Convert CLC response (block, head, batch_split) to WorkTileInfo.
535
+
536
+ CLC returns raw grid coordinates — no L2 swizzle (hardware decides order).
537
+ We only apply cluster division, optional LPT block reversal, and split_kv unpacking.
538
+ """
539
+ block_idx = work.tile_idx[0]
540
+ if const_expr(self.params.cluster_shape_m > 1):
541
+ block_idx = block_idx // self.params.cluster_shape_m
542
+ if const_expr(self.params.lpt):
543
+ # Longest-processing-time-first: reverse block order
544
+ if const_expr(self.params.cluster_shape_m > 1 and not self.params.use_cluster_idx):
545
+ num_block = self.params.num_block // self.params.cluster_shape_m
546
+ else:
547
+ num_block = self.params.num_block
548
+ block_idx = num_block - 1 - block_idx
549
+ split_idx = Int32(0)
550
+ if const_expr(self.params.is_split_kv):
551
+ batch_idx, split_idx = divmod(work.tile_idx[2], self.params.num_splits_divmod)
552
+ else:
553
+ batch_idx = work.tile_idx[2]
554
+ if const_expr(self.params.cluster_shape_m > 1 and not self.params.use_cluster_idx):
555
+ bidx_in_cluster = cute.arch.block_in_cluster_idx()
556
+ block_idx = block_idx * self.params.cluster_shape_m + bidx_in_cluster[0]
557
+ return WorkTileInfo(
558
+ (Int32(block_idx), Int32(work.tile_idx[1]), Int32(batch_idx), Int32(split_idx)),
559
+ work.is_valid_tile,
560
+ )
561
+
562
+ @cute.jit
563
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
564
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
565
+ work = self.clc.get_current_work()
566
+ self._tile_idx = work.tile_idx[0]
567
+ return self.clc_work_to_coords(work)
568
+ # Static path: L2-swizzled coordinate mapping
569
+ params = self.params
570
+ # Implement LPT scheduling coordinate calculation
571
+ bidhb, l2_mod = divmod(self._tile_idx, params.l2_major_divmod)
572
+ # If we're in the last section (called residual), we don't want to divide by
573
+ # swizzle. Instead we want to divide by the remainder.
574
+ block, bidhb_residual = 0, 0
575
+ if bidhb < params.num_hb_quotient:
576
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_divmod)
577
+ else:
578
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_residual_divmod)
579
+ bidhb_actual = bidhb * params.l2_minor + bidhb_residual
580
+ batch_idx, head_idx = divmod(bidhb_actual, params.num_head_divmod)
581
+ # Longest-processing-time-first
582
+ if const_expr(params.lpt):
583
+ block = params.num_block - 1 - block
584
+ is_valid = self._tile_idx < params.total_blocks
585
+ return WorkTileInfo(
586
+ (Int32(block), Int32(head_idx), Int32(batch_idx), Int32(self._split_idx)), is_valid
587
+ )
588
+
589
+ @cute.jit
590
+ def initial_work_tile_info(self, *, loc=None, ip=None):
591
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
592
+ work = self.clc.initial_work_tile_info()
593
+ self._tile_idx = work.tile_idx[0]
594
+ return self.clc_work_to_coords(work)
595
+ return self.get_current_work(loc=loc, ip=ip)
596
+
597
+ def prefetch_next_work(self, *, loc=None, ip=None):
598
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
599
+ self.clc.prefetch_next_work(loc=loc, ip=ip)
600
+
601
+ def advance_to_next_work(self, *, loc=None, ip=None):
602
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
603
+ self.clc.consumer_wait(loc=loc, ip=ip)
604
+ work = self.get_current_work()
605
+ self.clc.consumer_release(loc=loc, ip=ip)
606
+ return work
607
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
608
+ self._tile_idx = self.params.total_blocks
609
+ return self.get_current_work()
610
+
611
+ def producer_tail(self, *, loc=None, ip=None):
612
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
613
+ self.clc.producer_tail(loc=loc, ip=ip)
614
+
615
+ def __extract_mlir_values__(self):
616
+ values, self._values_pos = [], []
617
+ objs = [self.params, self._tile_idx, self._split_idx]
618
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
619
+ objs += [self.clc]
620
+ for obj in objs:
621
+ obj_values = cutlass.extract_mlir_values(obj)
622
+ values += obj_values
623
+ self._values_pos.append(len(obj_values))
624
+ return values
625
+
626
+ def __new_from_mlir_values__(self, values):
627
+ obj_list = []
628
+ objs = [self.params, self._tile_idx, self._split_idx]
629
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
630
+ objs += [self.clc]
631
+ for obj, n_items in zip(objs, self._values_pos):
632
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
633
+ values = values[n_items:]
634
+ return self.__class__(*obj_list, loc=self._loc)
635
+
636
+
637
+ class SingleTileLPTBwdScheduler:
638
+ @dataclass
639
+ class Params(ParamsBase):
640
+ total_blocks: Int32
641
+ num_block: Int32
642
+ l2_minor: Int32
643
+ num_head_divmod: FastDivmodDivisor
644
+ l2_minor_divmod: FastDivmodDivisor
645
+ l2_major_divmod: FastDivmodDivisor
646
+ l2_minor_residual_divmod: FastDivmodDivisor
647
+ num_hb_quotient: Int32
648
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
649
+ spt: cutlass.Constexpr[bool] = True
650
+
651
+ @staticmethod
652
+ @cute.jit
653
+ def create(
654
+ args: TileSchedulerArguments, *, loc=None, ip=None
655
+ ) -> "SingleTileLPTBwdScheduler.Params":
656
+ size_l2 = 50 * 1024 * 1024
657
+ size_one_qdo_head = args.seqlen_k * (args.headdim + args.headdim_v) * args.element_size
658
+ size_one_dqaccum_head = args.seqlen_k * (args.headdim) * 4
659
+ # size_one_dqaccum_head = 0
660
+ size_one_head = size_one_qdo_head + size_one_dqaccum_head
661
+ log2_floor = lambda n: 31 - clz(n)
662
+ swizzle = 1 if size_l2 < size_one_head else (1 << log2_floor(size_l2 // size_one_head))
663
+ # swizzle = 8
664
+ # If we're in the last section (called residual), we don't want to divide by
665
+ # swizzle. Instead we want to divide by the remainder.
666
+ num_hb_quotient = (args.num_head * args.num_batch) // swizzle
667
+ num_hb_remainder = (args.num_head * args.num_batch) % swizzle
668
+ num_block = cute.ceil_div(args.num_block, args.cluster_shape_mn[0])
669
+ return SingleTileLPTBwdScheduler.Params(
670
+ total_blocks=(num_block * args.cluster_shape_mn[0])
671
+ * args.num_head
672
+ * args.num_batch,
673
+ num_block=num_block,
674
+ l2_minor=Int32(swizzle),
675
+ num_head_divmod=FastDivmodDivisor(args.num_head),
676
+ l2_minor_divmod=FastDivmodDivisor(swizzle),
677
+ l2_major_divmod=FastDivmodDivisor(swizzle * num_block),
678
+ l2_minor_residual_divmod=FastDivmodDivisor(
679
+ max(num_hb_remainder, 1)
680
+ ), # don't divide by 0
681
+ num_hb_quotient=Int32(num_hb_quotient),
682
+ cluster_shape_mn=args.cluster_shape_mn,
683
+ spt=args.lpt,
684
+ )
685
+
686
+ def __init__(self, params: Params, tile_idx: Int32, *, loc=None, ip=None):
687
+ self.params = params
688
+ self._tile_idx = tile_idx
689
+ self._loc = loc
690
+ self._ip = ip
691
+
692
+ @staticmethod
693
+ def to_underlying_arguments(
694
+ args: TileSchedulerArguments,
695
+ *,
696
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
697
+ loc=None,
698
+ ip=None,
699
+ ) -> Params:
700
+ assert scheduling_mode == SchedulingMode.STATIC, (
701
+ f"SingleTileLPTBwdScheduler only supports STATIC, got {scheduling_mode!r}"
702
+ )
703
+ return SingleTileLPTBwdScheduler.Params.create(args, loc=loc, ip=ip)
704
+
705
+ @staticmethod
706
+ @cute.jit
707
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileLPTBwdScheduler":
708
+ tile_idx = cute.arch.block_idx()[0]
709
+ return SingleTileLPTBwdScheduler(params, tile_idx, loc=loc, ip=ip)
710
+
711
+ # called by host
712
+ @staticmethod
713
+ def get_grid_shape(
714
+ params: Params,
715
+ *,
716
+ loc=None,
717
+ ip=None,
718
+ ) -> Tuple[Int32, Int32, Int32]:
719
+ return (params.total_blocks, Int32(1), Int32(1))
720
+
721
+ @cute.jit
722
+ def get_current_work(self, *, loc=None, ip=None) -> cutlass.utils.WorkTileInfo:
723
+ cluster_idx = self._tile_idx // self.params.cluster_shape_mn[0]
724
+ params = self.params
725
+ # Implement LPT scheduling coordinate calculation
726
+ bidhb, l2_mod = divmod(cluster_idx, params.l2_major_divmod)
727
+ # If we're in the last section (called residual), we don't want to divide by
728
+ # swizzle. Instead we want to divide by the remainder.
729
+ block, bidhb_residual = 0, 0
730
+ if bidhb < params.num_hb_quotient:
731
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_divmod)
732
+ else:
733
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_residual_divmod)
734
+ bidhb_actual = bidhb * params.l2_minor + bidhb_residual
735
+ batch_idx, head_idx = divmod(bidhb_actual, params.num_head_divmod)
736
+ if cutlass.const_expr(params.spt):
737
+ block = params.num_block - 1 - block
738
+ if cutlass.const_expr(params.cluster_shape_mn[0] > 1):
739
+ bidx_in_cluster = cute.arch.block_in_cluster_idx()
740
+ block = block * params.cluster_shape_mn[0] + bidx_in_cluster[0]
741
+ is_valid = self._tile_idx < params.total_blocks
742
+ return WorkTileInfo((Int32(block), Int32(head_idx), Int32(batch_idx), Int32(0)), is_valid)
743
+
744
+ def initial_work_tile_info(self, *, loc=None, ip=None):
745
+ return self.get_current_work(loc=loc, ip=ip)
746
+
747
+ def prefetch_next_work(self, *, loc=None, ip=None):
748
+ pass
749
+
750
+ def advance_to_next_work(self, *, loc=None, ip=None):
751
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
752
+ self._tile_idx = self.params.total_blocks
753
+ return self.get_current_work()
754
+
755
+ def __extract_mlir_values__(self):
756
+ values, self._values_pos = [], []
757
+ for obj in [self.params, self._tile_idx]:
758
+ obj_values = cutlass.extract_mlir_values(obj)
759
+ values += obj_values
760
+ self._values_pos.append(len(obj_values))
761
+ return values
762
+
763
+ def __new_from_mlir_values__(self, values):
764
+ obj_list = []
765
+ for obj, n_items in zip([self.params, self._tile_idx], self._values_pos):
766
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
767
+ values = values[n_items:]
768
+ return self.__class__(*(tuple(obj_list)), loc=self._loc)
769
+
770
+
771
+ class SingleTileVarlenScheduler:
772
+ @dataclass
773
+ class Params(ParamsBase):
774
+ num_head: Int32
775
+ num_batch: Int32
776
+ total_q: Int32
777
+ num_splits: Int32
778
+ max_kvblock_in_l2: Int32
779
+ tile_shape_mn: cutlass.Constexpr[Tuple[int, int]]
780
+ mCuSeqlensQ: Optional[cute.Tensor] = None
781
+ mSeqUsedQ: Optional[cute.Tensor] = None
782
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
783
+ lpt: cutlass.Constexpr[bool] = False
784
+ is_split_kv: cutlass.Constexpr[bool] = False
785
+ head_swizzle: cutlass.Constexpr[bool] = False
786
+ cluster_shape_m: cutlass.Constexpr[int] = 1
787
+ scheduling_mode: cutlass.Constexpr[SchedulingMode] = SchedulingMode.STATIC
788
+
789
+ @staticmethod
790
+ @cute.jit
791
+ def create(
792
+ args: TileSchedulerArguments,
793
+ *,
794
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
795
+ loc=None,
796
+ ip=None,
797
+ ) -> "SingleTileVarlenScheduler.Params":
798
+ assert scheduling_mode in (SchedulingMode.STATIC, SchedulingMode.CLC), (
799
+ f"Only STATIC and CLC are supported, got {scheduling_mode!r}"
800
+ )
801
+ size_l2 = 50 * 1024 * 1024 # 50 MB for K & V
802
+ # if backward, this is qdo block size
803
+ kv_block_size = (
804
+ (args.headdim + args.headdim_v) * args.element_size * args.tile_shape_mn[1]
805
+ )
806
+ # if backward, add dqaccum block size to calculate swizzle
807
+ if args.head_swizzle:
808
+ kv_block_size += args.headdim * 4 * args.tile_shape_mn[1]
809
+ max_kvblock_in_l2 = size_l2 // kv_block_size
810
+ assert args.mCuSeqlensQ is not None or args.mSeqUsedQ is not None, (
811
+ "At least one of mCuSeqlensQ or mSeqUsedQ must be provided"
812
+ )
813
+ assert args.cluster_shape_mn[1] == 1, "Only cluster_shape_mn[1] == 1 is supported"
814
+ # TODO: Support varlen CLC with cluster_shape_m > 1 by refactoring the
815
+ # flattened-tile decode so cluster unpacking semantics are explicit.
816
+ assert scheduling_mode != SchedulingMode.CLC or args.cluster_shape_mn[0] == 1, (
817
+ "Varlen CLC currently requires cluster_shape_mn[0] == 1"
818
+ )
819
+ return SingleTileVarlenScheduler.Params(
820
+ num_head=args.num_head,
821
+ num_batch=args.num_batch,
822
+ total_q=args.total_q,
823
+ num_splits=args.num_splits,
824
+ max_kvblock_in_l2=max_kvblock_in_l2,
825
+ tile_shape_mn=args.tile_shape_mn,
826
+ mCuSeqlensQ=args.mCuSeqlensQ,
827
+ mSeqUsedQ=args.mSeqUsedQ,
828
+ qhead_per_kvhead_packgqa=args.qhead_per_kvhead_packgqa,
829
+ lpt=args.lpt,
830
+ is_split_kv=args.is_split_kv,
831
+ head_swizzle=args.head_swizzle,
832
+ cluster_shape_m=args.cluster_shape_mn[0],
833
+ scheduling_mode=scheduling_mode,
834
+ )
835
+
836
+ def __init__(
837
+ self,
838
+ params: Params,
839
+ tile_idx: Int32,
840
+ split_idx: Int32,
841
+ clc: ClcState | None = None,
842
+ *,
843
+ loc=None,
844
+ ip=None,
845
+ ):
846
+ self.params = params
847
+ self._tile_idx = tile_idx
848
+ self._split_idx = split_idx
849
+ self._is_first_block = True
850
+ self.clc = clc
851
+ self._loc = loc
852
+ self._ip = ip
853
+
854
+ @staticmethod
855
+ def to_underlying_arguments(
856
+ args: TileSchedulerArguments,
857
+ *,
858
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
859
+ loc=None,
860
+ ip=None,
861
+ ) -> Params:
862
+ return SingleTileVarlenScheduler.Params.create(
863
+ args, scheduling_mode=scheduling_mode, loc=loc, ip=ip
864
+ )
865
+
866
+ @staticmethod
867
+ @cute.jit
868
+ def clc_problem_shape(params: Params):
869
+ return ClcDynamicPersistentTileSchedulerParams(
870
+ problem_shape_ntile_mnl=SingleTileVarlenScheduler.get_grid_shape(params),
871
+ cluster_shape_mnk=(1, 1, 1),
872
+ )
873
+
874
+ @staticmethod
875
+ @cute.jit
876
+ def create(
877
+ params: Params, clc: ClcState | None = None, *, loc=None, ip=None
878
+ ) -> "SingleTileVarlenScheduler":
879
+ if const_expr(params.scheduling_mode == SchedulingMode.CLC):
880
+ block_idx = cute.arch.block_idx()
881
+ split_idx = Int32(0)
882
+ if const_expr(params.is_split_kv):
883
+ split_idx = block_idx[1]
884
+ return SingleTileVarlenScheduler(
885
+ params,
886
+ block_idx[0],
887
+ split_idx,
888
+ clc,
889
+ loc=loc,
890
+ ip=ip,
891
+ )
892
+ tile_idx, split_idx, _ = cute.arch.block_idx()
893
+ return SingleTileVarlenScheduler(params, tile_idx, split_idx, loc=loc, ip=ip)
894
+
895
+ # called by host
896
+ @staticmethod
897
+ def get_grid_shape(
898
+ params: Params,
899
+ *,
900
+ loc=None,
901
+ ip=None,
902
+ ) -> Tuple[Int32, Int32, Int32]:
903
+ total_blocks_max = (
904
+ params.total_q
905
+ + params.num_batch * (params.cluster_shape_m * params.tile_shape_mn[0] - 1)
906
+ ) // params.tile_shape_mn[0]
907
+ # Round down to nearest multiple of cluster since odd excess is always padding.
908
+ total_blocks_max = total_blocks_max // params.cluster_shape_m * params.cluster_shape_m
909
+ return (total_blocks_max * params.num_head, params.num_splits, Int32(1))
910
+
911
+ @cute.jit
912
+ def _get_num_m_blocks(self, lane: Int32, bidb_start: Int32) -> Int32:
913
+ params = self.params
914
+ batch_idx = lane + bidb_start
915
+ if cutlass.const_expr(params.mSeqUsedQ is not None):
916
+ seqlen = Int32(0)
917
+ if batch_idx < params.num_batch:
918
+ seqlen = params.mSeqUsedQ[batch_idx]
919
+ else:
920
+ assert params.mCuSeqlensQ is not None
921
+ cur_cu_seqlen = Int32(0)
922
+ if batch_idx <= params.num_batch:
923
+ cur_cu_seqlen = params.mCuSeqlensQ[batch_idx]
924
+ next_cu_seqlen = cute.arch.shuffle_sync_down(cur_cu_seqlen, offset=1)
925
+ seqlen = next_cu_seqlen - cur_cu_seqlen
926
+ if cutlass.const_expr(params.qhead_per_kvhead_packgqa > 1):
927
+ seqlen *= params.qhead_per_kvhead_packgqa
928
+ return (
929
+ cute.ceil_div(cute.ceil_div(seqlen, params.tile_shape_mn[0]), params.cluster_shape_m)
930
+ if batch_idx < params.num_batch and lane < cute.arch.WARP_SIZE - 1
931
+ else Int32(0)
932
+ )
933
+
934
+ @cute.jit
935
+ def _varlen_coord_map(self) -> WorkTileInfo:
936
+ """Map self._tile_idx to (block, head, batch) via warp-level prefix sums."""
937
+ params = self.params
938
+ lane_idx = cute.arch.lane_idx()
939
+ num_m_blocks = self._get_num_m_blocks(lane_idx, bidb_start=0)
940
+ num_m_blocks_cumulative = utils.warp_prefix_sum(num_m_blocks, lane_idx)
941
+ # Total number of blocks for the next 31 batches
942
+ m_blocks_in_group = cute.arch.shuffle_sync(num_m_blocks_cumulative, cute.arch.WARP_SIZE - 1)
943
+ # Same for all lanes
944
+ group_end_tile = m_blocks_in_group * params.num_head
945
+ # if cute.arch.thread_idx()[0] == 128 + 31: cute.printf("SingleTileVarlenScheduler: tile_idx=%d, group_end_tile = %d, num_m_blocks=%d, num_m_blocks_cumulative = %d, m_blocks_in_group = %d", self._tile_idx, group_end_tile, num_m_blocks, num_m_blocks_cumulative, m_blocks_in_group)
946
+ block, head_idx, batch_idx = Int32(0), Int32(0), Int32(0)
947
+ next_tile_idx = self._tile_idx // params.cluster_shape_m
948
+ while group_end_tile <= next_tile_idx:
949
+ batch_idx += cute.arch.WARP_SIZE - 1
950
+ if batch_idx >= params.num_batch:
951
+ batch_idx = Int32(params.num_batch)
952
+ group_end_tile = next_tile_idx + 1
953
+ else:
954
+ num_m_blocks = self._get_num_m_blocks(lane_idx, bidb_start=batch_idx)
955
+ num_m_blocks_cumulative = utils.warp_prefix_sum(num_m_blocks, lane_idx)
956
+ m_blocks_in_group = cute.arch.shuffle_sync(
957
+ num_m_blocks_cumulative, cute.arch.WARP_SIZE - 1
958
+ )
959
+ group_end_tile += m_blocks_in_group * params.num_head
960
+ is_valid = False
961
+ if batch_idx >= params.num_batch:
962
+ block, head_idx, batch_idx = Int32(0), Int32(0), Int32(params.num_batch)
963
+ else:
964
+ group_start_tile = group_end_tile - m_blocks_in_group * params.num_head
965
+ # if cute.arch.thread_idx()[0] == 128 + 31: cute.printf("SingleTileVarlenScheduler: tile_idx=%d, group_end_tile = %d, num_m_blocks=%d, batch_idx = %d", self._tile_idx, group_end_tile, num_m_blocks, batch_idx)
966
+ # The next problem to process is the first one that does not have ending tile position
967
+ # that is greater than or equal to tile index.
968
+ batch_idx_in_group = cute.arch.popc(
969
+ cute.arch.vote_ballot_sync(
970
+ group_start_tile + num_m_blocks_cumulative * params.num_head <= next_tile_idx
971
+ )
972
+ )
973
+ batch_idx += batch_idx_in_group
974
+ num_m_blocks_prev_lane = (
975
+ 0
976
+ if batch_idx_in_group == 0
977
+ else cute.arch.shuffle_sync(num_m_blocks_cumulative, batch_idx_in_group - 1)
978
+ )
979
+ num_m_blocks = cute.arch.shuffle_sync(num_m_blocks, batch_idx_in_group)
980
+ mh_block = next_tile_idx - group_start_tile - num_m_blocks_prev_lane * params.num_head
981
+ if cutlass.const_expr(params.lpt or params.head_swizzle):
982
+ # This is a version of the SingleTileLPTScheduler, complicated by the fact that
983
+ # the seqlen can vary per batch.
984
+ # TODO: is there any case where num_m_blocks is 0?
985
+ # TODO: by right we should read the seqlen_kv but we're assuming seqlen_q == seqlen_k here
986
+ num_n_blocks = (
987
+ num_m_blocks
988
+ * params.tile_shape_mn[0]
989
+ * params.cluster_shape_m
990
+ // params.qhead_per_kvhead_packgqa
991
+ // params.tile_shape_mn[1]
992
+ )
993
+ # nheads_in_l2 = min(max(self.max_kvblock_in_l2 // num_n_blocks, 1), self.num_head)
994
+ # Seems faster to have this be a power of 2
995
+ nheads_in_l2 = (
996
+ 16
997
+ if num_n_blocks * 16 <= params.max_kvblock_in_l2
998
+ else (
999
+ 8
1000
+ if num_n_blocks * 8 <= params.max_kvblock_in_l2
1001
+ else (
1002
+ 4
1003
+ if num_n_blocks * 4 <= params.max_kvblock_in_l2
1004
+ else (2 if num_n_blocks * 2 <= params.max_kvblock_in_l2 else 1)
1005
+ )
1006
+ )
1007
+ )
1008
+ nheads_in_l2 = min(nheads_in_l2, params.num_head)
1009
+ mh_in_l2 = nheads_in_l2 * num_m_blocks
1010
+ section_idx = mh_block // mh_in_l2
1011
+ l2_mod = mh_block - section_idx * mh_in_l2
1012
+ # Deal with tail section
1013
+ nheads_in_this_section = (
1014
+ nheads_in_l2
1015
+ if nheads_in_l2 * (section_idx + 1) <= params.num_head
1016
+ else params.num_head - section_idx * nheads_in_l2
1017
+ )
1018
+ block = l2_mod // nheads_in_this_section
1019
+ head_idx_residual = l2_mod - block * nheads_in_this_section
1020
+ head_idx = section_idx * nheads_in_l2 + head_idx_residual
1021
+ if cutlass.const_expr(params.lpt):
1022
+ block = num_m_blocks - 1 - block
1023
+ else:
1024
+ head_idx = mh_block // num_m_blocks
1025
+ block = mh_block - head_idx * num_m_blocks
1026
+ is_valid = self._is_first_block and batch_idx < params.num_batch
1027
+ if cutlass.const_expr(params.cluster_shape_m > 1):
1028
+ bidx_in_cluster = cute.arch.block_in_cluster_idx()
1029
+ block = block * params.cluster_shape_m + bidx_in_cluster[0]
1030
+ # if cute.arch.thread_idx()[0] == 128: cute.printf("SingleTileVarlenScheduler: tile_idx=%d, batch_idx=%d, head_idx=%d, block=%d, is_valid = %d", self._tile_idx, batch_idx, head_idx, block, is_valid)
1031
+ split_idx = self._split_idx if const_expr(params.is_split_kv) else Int32(0)
1032
+ return WorkTileInfo((Int32(block), Int32(head_idx), Int32(batch_idx), split_idx), is_valid)
1033
+
1034
+ @cute.jit
1035
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
1036
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1037
+ clc_work = self.clc.get_current_work()
1038
+ # Default to grid_dim (one past last valid flat index) so _varlen_coord_map
1039
+ # returns is_valid=False when CLC is exhausted. CLC tile_idx is garbage when
1040
+ # invalid, so we can't trust it. Local-then-assign avoids CuTe DSL structural
1041
+ # mismatch on self inside the runtime if.
1042
+ new_tile_idx = cute.arch.grid_dim()[0]
1043
+ new_split_idx = Int32(0)
1044
+ if clc_work.is_valid_tile:
1045
+ new_tile_idx = clc_work.tile_idx[0]
1046
+ if const_expr(self.params.is_split_kv):
1047
+ new_split_idx = clc_work.tile_idx[1]
1048
+ self._tile_idx = new_tile_idx
1049
+ self._split_idx = new_split_idx
1050
+ return self._varlen_coord_map()
1051
+
1052
+ @cute.jit
1053
+ def initial_work_tile_info(self, *, loc=None, ip=None):
1054
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1055
+ clc_work = self.clc.initial_work_tile_info()
1056
+ # See get_current_work for why grid_dim and local-then-assign.
1057
+ new_tile_idx = cute.arch.grid_dim()[0]
1058
+ new_split_idx = Int32(0)
1059
+ if clc_work.is_valid_tile:
1060
+ new_tile_idx = clc_work.tile_idx[0]
1061
+ if const_expr(self.params.is_split_kv):
1062
+ new_split_idx = clc_work.tile_idx[1]
1063
+ self._tile_idx = new_tile_idx
1064
+ self._split_idx = new_split_idx
1065
+ return self._varlen_coord_map()
1066
+
1067
+ def prefetch_next_work(self, *, loc=None, ip=None):
1068
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1069
+ self.clc.prefetch_next_work(loc=loc, ip=ip)
1070
+
1071
+ def advance_to_next_work(self, *, loc=None, ip=None):
1072
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1073
+ self.clc.consumer_wait(loc=loc, ip=ip)
1074
+ work = self.get_current_work()
1075
+ self.clc.consumer_release(loc=loc, ip=ip)
1076
+ return work
1077
+ self._is_first_block = False
1078
+ return self.get_current_work()
1079
+
1080
+ def producer_tail(self, *, loc=None, ip=None):
1081
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1082
+ self.clc.producer_tail(loc=loc, ip=ip)
1083
+
1084
+ def __extract_mlir_values__(self):
1085
+ values, self._values_pos = [], []
1086
+ objs = [self.params, self._tile_idx, self._split_idx]
1087
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1088
+ objs += [self.clc]
1089
+ for obj in objs:
1090
+ obj_values = cutlass.extract_mlir_values(obj)
1091
+ values += obj_values
1092
+ self._values_pos.append(len(obj_values))
1093
+ return values
1094
+
1095
+ def __new_from_mlir_values__(self, values):
1096
+ obj_list = []
1097
+ objs = [self.params, self._tile_idx, self._split_idx]
1098
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1099
+ objs += [self.clc]
1100
+ for obj, n_items in zip(objs, self._values_pos):
1101
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
1102
+ values = values[n_items:]
1103
+ return self.__class__(*obj_list, loc=self._loc)
1104
+
1105
+
1106
+ # -----------------------------------------------------------------------------
1107
+ # SM100 FMHA-specific schedulers (kept separate from generic schedulers).
1108
+ # -----------------------------------------------------------------------------
1109
+
1110
+
1111
+ class Sm100FmhaStaticTileSchedulerParams:
1112
+ """A class to represent parameters for the FMHA (Fused Multi-Head Attention) static tile scheduler.
1113
+
1114
+ This class holds the configuration parameters needed to initialize and configure
1115
+ the tile scheduler for FMHA operations.
1116
+
1117
+ :ivar is_persistent: Whether to use persistent kernel mode.
1118
+ :type is_persistent: bool
1119
+ :ivar problem_shape_mbh: Problem shape in (M, B, H) format.
1120
+ :type problem_shape_mbh: cute.Shape
1121
+ """
1122
+
1123
+ def __init__(
1124
+ self,
1125
+ is_persistent: bool,
1126
+ problem_shape_mbh: cute.Shape,
1127
+ *,
1128
+ loc=None,
1129
+ ip=None,
1130
+ ):
1131
+ """
1132
+ Initializes the Sm100FmhaStaticTileSchedulerParams with the given parameters.
1133
+
1134
+ :param is_persistent: Whether to use persistent kernel mode.
1135
+ :type is_persistent: bool
1136
+ :param problem_shape_mbh: Problem shape in (M, B, H) format.
1137
+ :type problem_shape_mbh: cute.Shape
1138
+ """
1139
+ self.is_persistent = is_persistent
1140
+ self.problem_shape_mbh = problem_shape_mbh
1141
+ self._loc = loc
1142
+ self._ip = ip
1143
+
1144
+ def __extract_mlir_values__(self):
1145
+ values, self._values_pos = [], []
1146
+ for obj in [self.problem_shape_mbh]:
1147
+ obj_values = extract_mlir_values(obj)
1148
+ values += obj_values
1149
+ self._values_pos.append(len(obj_values))
1150
+ return values
1151
+
1152
+ def __new_from_mlir_values__(self, values):
1153
+ obj_list = []
1154
+ for obj, n_items in zip([self.problem_shape_mbh], self._values_pos):
1155
+ obj_list.append(new_from_mlir_values(obj, values[:n_items]))
1156
+ values = values[n_items:]
1157
+ return Sm100FmhaStaticTileSchedulerParams(
1158
+ self.is_persistent, *(tuple(obj_list)), loc=self._loc
1159
+ )
1160
+
1161
+
1162
+ class Sm100FmhaStaticTileScheduler:
1163
+ """A static tile scheduler for FMHA (Fused Multi-Head Attention) operations.
1164
+
1165
+ This class manages the scheduling of work tiles for FMHA kernels, supporting
1166
+ both persistent and non-persistent kernel modes. It tracks the current work
1167
+ position and advances through the problem space efficiently.
1168
+
1169
+ :ivar _params: Scheduler parameters.
1170
+ :type _params: Sm100FmhaStaticTileSchedulerParams
1171
+ :ivar _blk_coord: Block coordinates.
1172
+ :type _blk_coord: cute.Coord
1173
+ :ivar _grid_shape: Grid shape for the kernel.
1174
+ :type _grid_shape: cute.Shape
1175
+ :ivar _is_persistent: Whether to use persistent kernel mode.
1176
+ :type _is_persistent: bool
1177
+ :ivar _current_work_linear_idx: Current linear work index.
1178
+ :type _current_work_linear_idx: Int32
1179
+ :ivar _problem_shape_mbh: Problem shape in (M, B, H) format.
1180
+ :type _problem_shape_mbh: cute.Layout
1181
+ :ivar _num_blocks: Number of blocks in the problem.
1182
+ :type _num_blocks: Int32
1183
+ :ivar _is_first_block: Whether this is the first block.
1184
+ :type _is_first_block: bool
1185
+ :ivar num_persistent_sm: Number of persistent SMs.
1186
+ :type num_persistent_sm: Int32
1187
+ """
1188
+
1189
+ def __init__(
1190
+ self,
1191
+ params: Sm100FmhaStaticTileSchedulerParams,
1192
+ current_work_linear_idx: Int32,
1193
+ blk_coord: cute.Coord,
1194
+ grid_shape: cute.Shape,
1195
+ *,
1196
+ loc=None,
1197
+ ip=None,
1198
+ ):
1199
+ """
1200
+ Initializes the Sm100FmhaStaticTileScheduler with the given parameters.
1201
+
1202
+ :param params: Scheduler parameters.
1203
+ :type params: Sm100FmhaStaticTileSchedulerParams
1204
+ :param current_work_linear_idx: Current linear work index.
1205
+ :type current_work_linear_idx: Int32
1206
+ :param blk_coord: Block coordinates.
1207
+ :type blk_coord: cute.Coord
1208
+ :param grid_shape: Grid shape for the kernel.
1209
+ :type grid_shape: cute.Shape
1210
+ """
1211
+ self._params = params
1212
+ self._blk_coord = blk_coord
1213
+ self._grid_shape = grid_shape
1214
+ self._is_persistent = params.is_persistent
1215
+ self._current_work_linear_idx = current_work_linear_idx
1216
+ self._problem_shape_mbh = cute.make_layout(params.problem_shape_mbh, loc=loc, ip=ip)
1217
+ self._num_blocks = cute.size(self._problem_shape_mbh, loc=loc, ip=ip)
1218
+ self._is_first_block = True
1219
+ self.num_persistent_sm = cute.size(grid_shape, loc=loc, ip=ip)
1220
+ self._loc = loc
1221
+ self._ip = ip
1222
+
1223
+ # called by host
1224
+ @staticmethod
1225
+ def get_grid_shape(
1226
+ params: Sm100FmhaStaticTileSchedulerParams,
1227
+ *,
1228
+ loc=None,
1229
+ ip=None,
1230
+ ) -> cute.Shape:
1231
+ """
1232
+ Determine the grid shape for the FMHA kernel.
1233
+
1234
+ For persistent kernels, the grid shape is limited by the number of SMs
1235
+ (Streaming Multiprocessors) available on the device. For non-persistent
1236
+ kernels, the grid shape matches the problem shape.
1237
+
1238
+ :param params: Scheduler parameters.
1239
+ :type params: Sm100FmhaStaticTileSchedulerParams
1240
+
1241
+ :return: Grid shape as (M, B, H) tuple.
1242
+ :rtype: cute.Shape
1243
+ """
1244
+ if params.is_persistent:
1245
+ hardware_info = HardwareInfo()
1246
+ sm_count = hardware_info.get_device_multiprocessor_count()
1247
+ return (
1248
+ dsl_min(sm_count, cute.size(params.problem_shape_mbh, loc=loc, ip=ip)),
1249
+ 1,
1250
+ 1,
1251
+ )
1252
+ else:
1253
+ return params.problem_shape_mbh
1254
+
1255
+ @staticmethod
1256
+ def check_valid_work_for_seqlen_q(
1257
+ q_tiler: int,
1258
+ current_idx: Int32,
1259
+ seqlen_q: Int32,
1260
+ ) -> Boolean:
1261
+ """
1262
+ Check if the current work index is valid for the given query sequence length.
1263
+
1264
+ This method verifies that the current work tile index multiplied by the
1265
+ query tiler size is within the bounds of the query sequence length.
1266
+
1267
+ :param q_tiler: Query tiler size.
1268
+ :type q_tiler: int
1269
+ :param current_idx: Current work index.
1270
+ :type current_idx: Int32
1271
+ :param seqlen_q: Query sequence length.
1272
+ :type seqlen_q: Int32
1273
+
1274
+ :return: True if the work is valid, False otherwise.
1275
+ :rtype: Boolean
1276
+ """
1277
+ return current_idx * q_tiler < seqlen_q
1278
+
1279
+ def get_current_work(self, *, loc=None, ip=None) -> cutlass.utils.WorkTileInfo:
1280
+ """
1281
+ Get information about the current work tile.
1282
+
1283
+ Determines if the current work is valid and computes the tile coordinates
1284
+ based on whether the kernel is persistent or non-persistent.
1285
+
1286
+ :return: WorkTileInfo containing tile coordinates and validity flag.
1287
+ :rtype: WorkTileInfo
1288
+ """
1289
+ is_valid = (
1290
+ self._current_work_linear_idx < self._num_blocks
1291
+ if self._is_persistent
1292
+ else self._is_first_block
1293
+ )
1294
+
1295
+ blk_coord = (0, 0, 0)
1296
+ if self._is_persistent:
1297
+ blk_coord = self._problem_shape_mbh.get_hier_coord(
1298
+ self._current_work_linear_idx, loc=loc, ip=ip
1299
+ )
1300
+ else:
1301
+ blk_coord = self._blk_coord
1302
+
1303
+ # cur_tile_coord is (mid, 0, (bid, hid))
1304
+ cur_tile_coord = (
1305
+ blk_coord[0],
1306
+ 0,
1307
+ (blk_coord[1], blk_coord[2]),
1308
+ )
1309
+
1310
+ return cutlass.utils.WorkTileInfo(cur_tile_coord, is_valid)
1311
+
1312
+ def initial_work_tile_info(self, *, loc=None, ip=None):
1313
+ """
1314
+ Get the initial work tile information.
1315
+
1316
+ :return: Initial WorkTileInfo.
1317
+ :rtype: WorkTileInfo
1318
+ """
1319
+ return self.get_current_work(loc=loc, ip=ip)
1320
+
1321
+ def advance_to_next_work(self, *, advance_count=1, loc=None, ip=None):
1322
+ """
1323
+ Advance to the next work tile and return it.
1324
+
1325
+ For persistent kernels, advances by the number of persistent SMs.
1326
+ For non-persistent kernels, marks that the first block has been processed.
1327
+ """
1328
+ if self._is_persistent:
1329
+ self._current_work_linear_idx += advance_count * self.num_persistent_sm
1330
+ self._is_first_block = False
1331
+ return self.get_current_work()
1332
+
1333
+ def prefetch_next_work(self, *, loc=None, ip=None):
1334
+ """No-op for static scheduler."""
1335
+ pass
1336
+
1337
+ def producer_tail(self, *, loc=None, ip=None):
1338
+ """No-op for static scheduler."""
1339
+ pass
1340
+
1341
+ def __extract_mlir_values__(self):
1342
+ values = extract_mlir_values(self._params)
1343
+ values.extend(extract_mlir_values(self._current_work_linear_idx))
1344
+ values.extend(extract_mlir_values(self._blk_coord))
1345
+ values.extend(extract_mlir_values(self._grid_shape))
1346
+ return values
1347
+
1348
+ def __new_from_mlir_values__(self, values):
1349
+ assert len(values) == 10
1350
+ new_params = new_from_mlir_values(self._params, values[0:3])
1351
+ new_current_work_linear_idx = new_from_mlir_values(
1352
+ self._current_work_linear_idx, [values[3]]
1353
+ )
1354
+ new_blk_coord = new_from_mlir_values(self._blk_coord, values[4:7])
1355
+ new_grid_shape = new_from_mlir_values(self._grid_shape, values[7:])
1356
+ return Sm100FmhaStaticTileScheduler(
1357
+ new_params, new_current_work_linear_idx, new_blk_coord, new_grid_shape
1358
+ )
1359
+
1360
+
1361
+ def compute_sm100_fmha_grid(
1362
+ o_shape: cute.Shape,
1363
+ cta_tiler: Tuple[int, int, int],
1364
+ is_persistent: bool,
1365
+ ) -> Tuple[Sm100FmhaStaticTileSchedulerParams, Tuple[int, int, int]]:
1366
+ """Compute grid parameters for FMHA (static scheduler).
1367
+
1368
+ The output tensor o has shape (s, d, ((h_r, h_k), b)).
1369
+ """
1370
+ tile_sched_params = Sm100FmhaStaticTileSchedulerParams(
1371
+ is_persistent,
1372
+ (
1373
+ cute.ceil_div(cute.size(o_shape[0]), cta_tiler[0]),
1374
+ cute.size(o_shape[2][0]),
1375
+ cute.size(o_shape[2][1]),
1376
+ ),
1377
+ )
1378
+ grid = Sm100FmhaStaticTileScheduler.get_grid_shape(tile_sched_params)
1379
+ return tile_sched_params, grid
1380
+
1381
+
1382
+ ##############################################################################
1383
+ # Fmha CLC dynamic tile scheduler
1384
+ ##############################################################################
1385
+
1386
+
1387
+ class Sm100FmhaClcDynamicTileSchedulerParams:
1388
+ """Parameters for FMHA CLC dynamic persistent tile scheduler.
1389
+
1390
+ This class manages the layout of tiles for CLC (Cluster Launch Control)
1391
+ based dynamic scheduling, adapted for FMHA's (M, B, H) problem shape.
1392
+
1393
+ :ivar problem_shape_mbh: Problem shape in (M, B, H) format.
1394
+ :type problem_shape_mbh: cute.Shape
1395
+ :ivar cluster_shape_mnk: Cluster shape in (M, N, K) format.
1396
+ :type cluster_shape_mnk: cute.Shape
1397
+ """
1398
+
1399
+ def __init__(
1400
+ self,
1401
+ problem_shape_mbh: cute.Shape,
1402
+ cluster_shape_mnk: cute.Shape,
1403
+ *,
1404
+ loc=None,
1405
+ ip=None,
1406
+ ):
1407
+ self.problem_shape_mbh = problem_shape_mbh
1408
+ self._cluster_shape_mnk = cluster_shape_mnk
1409
+ self.cluster_shape_mn = cluster_shape_mnk[:2]
1410
+ self._loc = loc
1411
+ self._ip = ip
1412
+
1413
+ # FMHA uses linear indexing over (M, B, H), convert to (M, N, L) style
1414
+ # For FMHA: M dim is tile count along sequence, N=1, L=(B*H)
1415
+ self.problem_shape_ntile_mnl = (
1416
+ problem_shape_mbh[0], # M tiles
1417
+ 1, # N tiles (always 1 for FMHA)
1418
+ problem_shape_mbh[1] * problem_shape_mbh[2], # L = B * H
1419
+ )
1420
+
1421
+ # Create layout for cluster-to-tile mapping
1422
+ self.problem_layout_ncluster_mnl = cute.make_layout(
1423
+ cute.ceil_div(self.problem_shape_ntile_mnl, cluster_shape_mnk[:2]),
1424
+ loc=loc,
1425
+ ip=ip,
1426
+ )
1427
+
1428
+ def __extract_mlir_values__(self):
1429
+ values, self._values_pos = [], []
1430
+ for obj in [
1431
+ self.problem_shape_mbh,
1432
+ self._cluster_shape_mnk,
1433
+ ]:
1434
+ obj_values = extract_mlir_values(obj)
1435
+ values += obj_values
1436
+ self._values_pos.append(len(obj_values))
1437
+ return values
1438
+
1439
+ def __new_from_mlir_values__(self, values):
1440
+ obj_list = []
1441
+ values_copy = list(values)
1442
+ for obj, n_items in zip(
1443
+ [self.problem_shape_mbh, self._cluster_shape_mnk],
1444
+ self._values_pos,
1445
+ ):
1446
+ obj_list.append(new_from_mlir_values(obj, values_copy[:n_items]))
1447
+ values_copy = values_copy[n_items:]
1448
+ return Sm100FmhaClcDynamicTileSchedulerParams(*(tuple(obj_list)), loc=self._loc)
1449
+
1450
+ def get_grid_shape(self, *, loc=None, ip=None) -> Tuple[int, int, int]:
1451
+ """Compute grid shape aligned with cluster shape."""
1452
+ return cute.round_up(self.problem_shape_ntile_mnl, self._cluster_shape_mnk)
1453
+
1454
+ def clc_hw_params(self) -> ClcDynamicPersistentTileSchedulerParams:
1455
+ """Return params for the upstream CLC hardware scheduler."""
1456
+ return ClcDynamicPersistentTileSchedulerParams(
1457
+ problem_shape_ntile_mnl=self.problem_shape_ntile_mnl,
1458
+ cluster_shape_mnk=self._cluster_shape_mnk,
1459
+ )
1460
+
1461
+
1462
+ class Sm100FmhaClcDynamicTileScheduler:
1463
+ """CLC dynamic persistent tile scheduler for FMHA.
1464
+
1465
+ This scheduler uses Blackwell's Cluster Launch Control hardware mechanism
1466
+ for dynamic tile distribution, providing automatic load balancing.
1467
+ Adapted for FMHA's (M, B, H) problem shape.
1468
+ """
1469
+
1470
+ def __init__(
1471
+ self,
1472
+ params: Sm100FmhaClcDynamicTileSchedulerParams,
1473
+ cta_id_in_cluster: cute.Coord,
1474
+ num_tiles_executed: Int32,
1475
+ clc_response_ptr: cute.Pointer,
1476
+ block_idx: Tuple,
1477
+ clc: ClcState = None,
1478
+ *,
1479
+ loc=None,
1480
+ ip=None,
1481
+ ):
1482
+ self.params = params
1483
+ self.cta_id_in_cluster = cta_id_in_cluster
1484
+ self._num_tiles_executed = num_tiles_executed
1485
+ self._clc_response_ptr = clc_response_ptr
1486
+ self._block_idx = block_idx
1487
+ self.clc = clc
1488
+ self._loc = loc
1489
+ self._ip = ip
1490
+
1491
+ def __extract_mlir_values__(self):
1492
+ values = extract_mlir_values(self.cta_id_in_cluster)
1493
+ values.extend(extract_mlir_values(self._num_tiles_executed))
1494
+ values.extend(extract_mlir_values(self._clc_response_ptr))
1495
+ values.extend(extract_mlir_values(self._block_idx))
1496
+ if self.clc is not None:
1497
+ values.extend(extract_mlir_values(self.clc))
1498
+ return values
1499
+
1500
+ def __new_from_mlir_values__(self, values):
1501
+ new_cta_id_in_cluster = new_from_mlir_values(self.cta_id_in_cluster, values[0:3])
1502
+ new_num_tiles_executed = new_from_mlir_values(self._num_tiles_executed, [values[3]])
1503
+ new_clc_response_ptr = new_from_mlir_values(self._clc_response_ptr, [values[4]])
1504
+ new_block_idx = new_from_mlir_values(self._block_idx, values[5:8])
1505
+ new_clc = None
1506
+ if self.clc is not None:
1507
+ new_clc = new_from_mlir_values(self.clc, values[8:])
1508
+ return Sm100FmhaClcDynamicTileScheduler(
1509
+ self.params,
1510
+ new_cta_id_in_cluster,
1511
+ new_num_tiles_executed,
1512
+ new_clc_response_ptr,
1513
+ new_block_idx,
1514
+ new_clc,
1515
+ )
1516
+
1517
+ @staticmethod
1518
+ def create(
1519
+ params: Sm100FmhaClcDynamicTileSchedulerParams,
1520
+ block_idx: Tuple,
1521
+ grid_dim: Tuple,
1522
+ clc_response_ptr: cute.Pointer,
1523
+ clc: ClcState = None,
1524
+ *,
1525
+ loc=None,
1526
+ ip=None,
1527
+ ):
1528
+ """Create a CLC dynamic tile scheduler instance."""
1529
+ bidx, bidy, bidz = block_idx
1530
+
1531
+ # CTA id in cluster
1532
+ cta_id_in_cluster = (
1533
+ Int32(bidx % params.cluster_shape_mn[0]),
1534
+ Int32(bidy % params.cluster_shape_mn[1]),
1535
+ Int32(0),
1536
+ )
1537
+
1538
+ num_tiles_executed = Int32(0)
1539
+
1540
+ return Sm100FmhaClcDynamicTileScheduler(
1541
+ params,
1542
+ cta_id_in_cluster,
1543
+ num_tiles_executed,
1544
+ clc_response_ptr,
1545
+ block_idx,
1546
+ clc,
1547
+ )
1548
+
1549
+ @staticmethod
1550
+ def get_grid_shape(
1551
+ params: Sm100FmhaClcDynamicTileSchedulerParams,
1552
+ *,
1553
+ loc=None,
1554
+ ip=None,
1555
+ ) -> Tuple[int, int, int]:
1556
+ """Get grid shape for kernel launch."""
1557
+ return params.get_grid_shape(loc=loc, ip=ip)
1558
+
1559
+ def work_tile_info_from_clc_response(self, result_addr: cute.Pointer, *, loc=None, ip=None):
1560
+ """Parse CLC response and convert to FMHA tile coordinates."""
1561
+ m_idx, n_idx, l_idx, vld = cute.arch.clc_response(result_addr, loc=loc, ip=ip)
1562
+ cute.arch.fence_proxy("async.shared", space="cta")
1563
+
1564
+ # CLC returns first CTA coordinates: m_idx=x, l_idx=z
1565
+ # l_idx is the L (batch) dimension; decode to (bid, hid)
1566
+ hid = l_idx % self.params.problem_shape_mbh[2]
1567
+ bid = l_idx // self.params.problem_shape_mbh[2]
1568
+
1569
+ cta_idx_in_cluster, cta_idy_in_cluster, _ = self.cta_id_in_cluster
1570
+ cur_tile_coord = (
1571
+ m_idx + cta_idx_in_cluster, # M dimension
1572
+ 0, # N always 0 for FMHA
1573
+ (bid, hid), # (B, H) packed
1574
+ )
1575
+
1576
+ return cutlass.utils.WorkTileInfo(cur_tile_coord, vld)
1577
+
1578
+ def get_current_work(self, *, loc=None, ip=None):
1579
+ """Get current work tile from CLC response."""
1580
+ return self.work_tile_info_from_clc_response(self._clc_response_ptr, loc=loc, ip=ip)
1581
+
1582
+ def initial_work_tile_info(self, *, loc=None, ip=None):
1583
+ """Get initial work tile based on block index."""
1584
+ bidx, bidy, bidz = self._block_idx
1585
+ # bidz is the L (batch) dimension; decode to (bid, hid)
1586
+ hid = bidz % self.params.problem_shape_mbh[2]
1587
+ bid = bidz // self.params.problem_shape_mbh[2]
1588
+ return cutlass.utils.WorkTileInfo((bidx, 0, (bid, hid)), True)
1589
+
1590
+ def advance_to_next_work(self, *, loc=None, ip=None):
1591
+ """Consumer-side advance: wait for next tile, read coordinates, release."""
1592
+ self.clc.consumer_wait(loc=loc, ip=ip)
1593
+ work = self.get_current_work(loc=loc, ip=ip)
1594
+ self.clc.consumer_release(loc=loc, ip=ip)
1595
+ self._num_tiles_executed += Int32(1)
1596
+ return work
1597
+
1598
+ def prefetch_next_work(self, *, loc=None, ip=None):
1599
+ """Producer-side: issue CLC query for next tile."""
1600
+ self.clc.prefetch_next_work(loc=loc, ip=ip)
1601
+
1602
+ def producer_tail(self, *, loc=None, ip=None):
1603
+ """Producer-side cleanup after last tile."""
1604
+ self.clc.producer_tail(loc=loc, ip=ip)
1605
+
1606
+ @property
1607
+ def num_tiles_executed(self) -> Int32:
1608
+ return self._num_tiles_executed
1609
+
1610
+
1611
+ def compute_sm100_fmha_grid_clc(
1612
+ o_shape: cute.Shape,
1613
+ cta_tiler: Tuple[int, int, int],
1614
+ cluster_shape_mnk: Tuple[int, int, int],
1615
+ ) -> Tuple[Sm100FmhaClcDynamicTileSchedulerParams, Tuple[int, int, int]]:
1616
+ """Compute grid parameters for FMHA with CLC dynamic scheduling."""
1617
+ problem_shape_mbh = (
1618
+ cute.ceil_div(cute.size(o_shape[0]), cta_tiler[0]),
1619
+ cute.size(o_shape[2][0]),
1620
+ cute.size(o_shape[2][1]),
1621
+ )
1622
+ tile_sched_params = Sm100FmhaClcDynamicTileSchedulerParams(problem_shape_mbh, cluster_shape_mnk)
1623
+ grid = Sm100FmhaClcDynamicTileScheduler.get_grid_shape(tile_sched_params)
1624
+ return tile_sched_params, grid
1625
+
1626
+
1627
+ ##############################################################################
1628
+ # Fused Mask
1629
+ ##############################################################################
1630
+
1631
+
1632
+ def make_sm100_thread_cooperative_group(size: int):
1633
+ return cutlass.pipeline.CooperativeGroup(cutlass.pipeline.Agent.Thread, size)
1634
+
1635
+
1636
+ SM100_TMEM_CAPACITY_COLUMNS = 512
build/torch-cuda/flashrt_fa4/cute/utils.py ADDED
@@ -0,0 +1,948 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import math
4
+ import hashlib
5
+ import inspect
6
+ import os
7
+ from functools import partial
8
+ from typing import Type, Callable, Optional, Tuple, overload
9
+
10
+ import cutlass
11
+ import cutlass.cute as cute
12
+
13
+ from cutlass import Float32, Int32, const_expr
14
+ from cutlass.cute import FastDivmodDivisor
15
+ from cutlass.cutlass_dsl import T, dsl_user_op
16
+ from cutlass._mlir.dialects import nvvm, llvm
17
+ from cutlass.cute.runtime import from_dlpack
18
+
19
+
20
+ from fa4_cute_runtime.quack import activation as quack_activation
21
+
22
+ _MIXER_ATTRS = ("__vec_size__",)
23
+
24
+ # Obtained from sollya:
25
+ # fpminimax(exp(x * log(2.0)), 1, [|1,24...|],[0;1],relative);
26
+ POLY_EX2 = {
27
+ 0: (1.0),
28
+ 1: (
29
+ 1.0,
30
+ 0.922497093677520751953125,
31
+ ),
32
+ 2: (
33
+ 1.0,
34
+ 0.6657850742340087890625,
35
+ 0.330107033252716064453125,
36
+ ),
37
+ 3: (
38
+ 1.0,
39
+ 0.695146143436431884765625,
40
+ 0.227564394474029541015625,
41
+ 0.077119089663028717041015625,
42
+ ),
43
+ 4: (
44
+ 1.0,
45
+ 0.693042695522308349609375,
46
+ 0.2412912547588348388671875,
47
+ 5.2225358784198760986328125e-2,
48
+ 1.3434938155114650726318359375e-2,
49
+ ),
50
+ 5: (
51
+ 1.0,
52
+ 0.693151414394378662109375,
53
+ 0.24016360938549041748046875,
54
+ 5.5802188813686370849609375e-2,
55
+ 9.01452265679836273193359375e-3,
56
+ 1.86810153536498546600341796875e-3,
57
+ ),
58
+ }
59
+
60
+ _fa_clc_enabled: bool = os.environ.get("FA_CLC", "0") == "1"
61
+ _fa_disable_2cta_enabled: bool = os.environ.get("FA_DISABLE_2CTA", "0") == "1"
62
+
63
+
64
+ def _is_cuda_12() -> bool:
65
+ """Check if the CUDA toolkit version is 12.x.
66
+
67
+ 2CTA forward non-causal has a codegen regression on CUDA 12 that causes
68
+ ~18% slowdown compared to 1CTA. This is fixed in CUDA 13.x.
69
+ """
70
+ try:
71
+ import torch
72
+
73
+ cuda_version = torch.version.cuda
74
+ if cuda_version is not None:
75
+ major = cuda_version.split(".")[0]
76
+ return int(major) == 12
77
+ except Exception:
78
+ pass
79
+ return False
80
+
81
+
82
+ _fa_disable_2cta_cuda12: bool = _is_cuda_12()
83
+
84
+
85
+ def _get_use_clc_scheduler_default() -> bool:
86
+ return _fa_clc_enabled
87
+
88
+
89
+ def _get_disable_2cta_default() -> bool:
90
+ return _fa_disable_2cta_enabled or _fa_disable_2cta_cuda12
91
+
92
+
93
+ def _compute_base_hash(func: Callable) -> str:
94
+ """Compute hash from source code or bytecode and closure values."""
95
+ try:
96
+ data = inspect.getsource(func).encode()
97
+ except (OSError, TypeError):
98
+ if hasattr(func, "__code__") and func.__code__ is not None:
99
+ data = func.__code__.co_code
100
+ else:
101
+ data = repr(func).encode()
102
+
103
+ hasher = hashlib.sha256(data)
104
+
105
+ if hasattr(func, "__closure__") and func.__closure__ is not None:
106
+ for cell in func.__closure__:
107
+ hasher.update(repr(cell.cell_contents).encode())
108
+
109
+ return hasher.hexdigest()
110
+
111
+
112
+ def hash_callable(
113
+ func: Callable, mixer_attrs: Tuple[str] = _MIXER_ATTRS, set_cute_hash: bool = True
114
+ ) -> str:
115
+ """Hash a callable based on the source code or bytecode and closure values.
116
+ Fast-path: if the callable (or its __wrapped__ base) has a ``__cute_hash__``
117
+ attribute, that value is returned immediately as the base hash, then
118
+ metadata dunders are mixed in to produce the final dict-key hash.
119
+ set_cute_hash: whether or not to set func.__cute_hash__
120
+ """
121
+ # Resolve base hash
122
+ if hasattr(func, "__cute_hash__"):
123
+ base_hash = func.__cute_hash__
124
+ else:
125
+ # Unwrap decorated functions (e.g., cute.jit wrappers).
126
+ base_func = getattr(func, "__wrapped__", func)
127
+
128
+ if hasattr(base_func, "__cute_hash__"):
129
+ base_hash = base_func.__cute_hash__
130
+ else:
131
+ base_hash = _compute_base_hash(base_func)
132
+
133
+ if set_cute_hash:
134
+ base_func.__cute_hash__ = base_hash
135
+
136
+ # Mix in mutable metadata dunders
137
+ mixer_values = tuple(getattr(func, attr, None) for attr in mixer_attrs)
138
+
139
+ if all(v is None for v in mixer_values):
140
+ return base_hash
141
+
142
+ hasher = hashlib.sha256(base_hash.encode())
143
+
144
+ for attr, val in zip(_MIXER_ATTRS, mixer_values):
145
+ hasher.update(f"{attr}={val!r}".encode())
146
+
147
+ return hasher.hexdigest()
148
+
149
+
150
+ def create_softcap_scoremod(softcap_val):
151
+ @cute.jit
152
+ def scoremod_premask_fn(
153
+ acc_S_SSA, batch_idx, head_idx, q_idx, kv_idx, seqlen_info, aux_tensors
154
+ ):
155
+ scores = acc_S_SSA / softcap_val
156
+ return softcap_val * cute.math.tanh(scores, fastmath=True)
157
+
158
+ return scoremod_premask_fn
159
+
160
+
161
+ def create_softcap_scoremod_bwd(softcap_val):
162
+ @cute.jit
163
+ def scoremod_bwd_fn(
164
+ grad_out_SSA, score_SSA, batch_idx, head_idx, q_idx, kv_idx, seqlen_info, aux_tensors
165
+ ):
166
+ scores = score_SSA / softcap_val
167
+ tanh_scores = cute.math.tanh(scores, fastmath=True)
168
+ return grad_out_SSA * (1.0 - tanh_scores * tanh_scores)
169
+
170
+ return scoremod_bwd_fn
171
+
172
+
173
+ LOG2_E = math.log2(math.e)
174
+
175
+
176
+ def compute_softmax_scale_log2(softmax_scale, score_mod):
177
+ """Compute softmax_scale_log2 and adjusted softmax_scale based on whether score_mod is used.
178
+
179
+ When score_mod is None, fold the log2(e) factor into softmax_scale_log2 and set softmax_scale
180
+ to None. When score_mod is present, keep softmax_scale separate so it can be applied before
181
+ the score_mod, and set softmax_scale_log2 to just the change-of-base constant.
182
+
183
+ Returns (softmax_scale_log2, softmax_scale).
184
+ """
185
+ if const_expr(score_mod is None):
186
+ return softmax_scale * LOG2_E, None
187
+ else:
188
+ return LOG2_E, softmax_scale
189
+
190
+
191
+ def compute_fastdiv_mods(mQ, mK, qhead_per_kvhead, pack_gqa, aux_tensors, mPageTable=None):
192
+ """Compute FastDivmodDivisor pairs for aux_tensors index computation.
193
+
194
+ Returns a (seqlen_q_divmod, seqlen_k_divmod) tuple, or None if aux_tensors is None.
195
+ """
196
+ if const_expr(aux_tensors is None):
197
+ return None
198
+ seqlen_q = cute.size(mQ.shape[0]) // (qhead_per_kvhead if const_expr(pack_gqa) else 1)
199
+ seqlen_k = (
200
+ cute.size(mK.shape[0])
201
+ if const_expr(mPageTable is None)
202
+ else mK.shape[0] * mPageTable.shape[1]
203
+ )
204
+ return (FastDivmodDivisor(seqlen_q), FastDivmodDivisor(seqlen_k))
205
+
206
+
207
+ def convert_from_dlpack(x, leading_dim, alignment=16, divisibility=1) -> cute.Tensor:
208
+ return (
209
+ from_dlpack(x, assumed_align=alignment)
210
+ .mark_layout_dynamic(leading_dim=leading_dim)
211
+ .mark_compact_shape_dynamic(
212
+ mode=leading_dim, stride_order=x.dim_order(), divisibility=divisibility
213
+ )
214
+ )
215
+
216
+
217
+ def convert_from_dlpack_compact_dynamic(
218
+ x,
219
+ *,
220
+ dynamic_modes: tuple[int, ...],
221
+ alignment: int = 16,
222
+ stride_order=None,
223
+ divisibility: int = 1,
224
+ enable_tvm_ffi: bool = False,
225
+ ) -> cute.Tensor:
226
+ """Convert via DLPack and mark selected compact dimensions as dynamic."""
227
+ if isinstance(dynamic_modes, int):
228
+ dynamic_modes = (dynamic_modes,)
229
+ if stride_order is None:
230
+ stride_order = x.dim_order()
231
+ t = (
232
+ from_dlpack(x, assumed_align=alignment, enable_tvm_ffi=True)
233
+ if enable_tvm_ffi
234
+ else from_dlpack(x, assumed_align=alignment)
235
+ )
236
+ for mode in dynamic_modes:
237
+ t = t.mark_compact_shape_dynamic(
238
+ mode=mode,
239
+ stride_order=stride_order,
240
+ divisibility=divisibility,
241
+ )
242
+ return t
243
+
244
+
245
+ def convert_from_dlpack_leading_static(
246
+ x, leading_dim, alignment=16, static_modes=None, stride_order=None
247
+ ) -> cute.Tensor:
248
+ if stride_order is None:
249
+ stride_order = x.dim_order()
250
+ x_ = from_dlpack(x, assumed_align=alignment)
251
+ for i in range(x.ndim):
252
+ if i != leading_dim and (static_modes is None or i not in static_modes):
253
+ x_ = x_.mark_compact_shape_dynamic(mode=i, stride_order=stride_order)
254
+ return x_
255
+
256
+
257
+ def make_tiled_copy_A(
258
+ copy_atom: cute.CopyAtom, tiled_mma: cute.TiledMma, swapAB: cutlass.Constexpr[bool] = False
259
+ ) -> cute.TiledCopy:
260
+ if const_expr(swapAB):
261
+ return cute.make_tiled_copy_B(copy_atom, tiled_mma)
262
+ else:
263
+ return cute.make_tiled_copy_A(copy_atom, tiled_mma)
264
+
265
+
266
+ def make_tiled_copy_B(
267
+ copy_atom: cute.CopyAtom, tiled_mma: cute.TiledMma, swapAB: cutlass.Constexpr[bool] = False
268
+ ) -> cute.TiledCopy:
269
+ if const_expr(swapAB):
270
+ return cute.make_tiled_copy_A(copy_atom, tiled_mma)
271
+ else:
272
+ return cute.make_tiled_copy_B(copy_atom, tiled_mma)
273
+
274
+
275
+ def mma_make_fragment_A(
276
+ smem: cute.Tensor, thr_mma: cute.core.ThrMma, swapAB: cutlass.Constexpr[bool] = False
277
+ ) -> cute.Tensor:
278
+ if const_expr(swapAB):
279
+ return mma_make_fragment_B(smem, thr_mma)
280
+ else:
281
+ return thr_mma.make_fragment_A(thr_mma.partition_A(smem))
282
+
283
+
284
+ def mma_make_fragment_B(
285
+ smem: cute.Tensor, thr_mma: cute.core.ThrMma, swapAB: cutlass.Constexpr[bool] = False
286
+ ) -> cute.Tensor:
287
+ if const_expr(swapAB):
288
+ return mma_make_fragment_A(smem, thr_mma)
289
+ else:
290
+ return thr_mma.make_fragment_B(thr_mma.partition_B(smem))
291
+
292
+
293
+ def get_smem_store_atom(
294
+ arch: cutlass.Constexpr[int], element_type: Type[cute.Numeric], transpose: bool = False
295
+ ) -> cute.CopyAtom:
296
+ if const_expr(arch < 90 or element_type.width != 16):
297
+ return cute.make_copy_atom(
298
+ cute.nvgpu.CopyUniversalOp(),
299
+ element_type,
300
+ num_bits_per_copy=2 * element_type.width,
301
+ )
302
+ else:
303
+ return cute.make_copy_atom(
304
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=transpose, num_matrices=4),
305
+ element_type,
306
+ )
307
+
308
+
309
+ @cute.jit
310
+ def warp_reduce(
311
+ val: cute.TensorSSA | cute.Numeric,
312
+ op: Callable,
313
+ width: cutlass.Constexpr[int] = cute.arch.WARP_SIZE,
314
+ ) -> cute.TensorSSA | cute.Numeric:
315
+ if const_expr(isinstance(val, cute.TensorSSA)):
316
+ res = cute.make_fragment(val.shape, val.dtype)
317
+ res.store(val)
318
+ for i in cutlass.range_constexpr(cute.size(val.shape)):
319
+ res[i] = warp_reduce(res[i], op, width)
320
+ return res.load()
321
+ else:
322
+ for i in cutlass.range_constexpr(int(math.log2(width))):
323
+ val = op(val, cute.arch.shuffle_sync_bfly(val, offset=1 << i))
324
+ return val
325
+
326
+
327
+ @dsl_user_op
328
+ def smid(*, loc=None, ip=None) -> Int32:
329
+ return Int32(
330
+ llvm.inline_asm(
331
+ T.i32(),
332
+ [],
333
+ "mov.u32 $0, %smid;",
334
+ "=r",
335
+ has_side_effects=False,
336
+ is_align_stack=False,
337
+ asm_dialect=llvm.AsmDialect.AD_ATT,
338
+ )
339
+ )
340
+
341
+
342
+ @dsl_user_op
343
+ def fmax(
344
+ a: float | Float32, b: float | Float32, c: float | Float32 | None = None, *, loc=None, ip=None
345
+ ) -> Float32:
346
+ from cutlass import CUDA_VERSION
347
+
348
+ # * NVVM call based on nvvm version
349
+ if CUDA_VERSION.major == 12 and CUDA_VERSION.minor == 9:
350
+ # Old API: requires explicit result type as first positional argument
351
+ return Float32(
352
+ nvvm.fmax(
353
+ T.f32(),
354
+ Float32(a).ir_value(loc=loc, ip=ip),
355
+ Float32(b).ir_value(loc=loc, ip=ip),
356
+ c=Float32(c).ir_value(loc=loc, ip=ip) if c is not None else None,
357
+ loc=loc,
358
+ ip=ip,
359
+ )
360
+ )
361
+ else:
362
+ # New API: infers result type automatically
363
+ return Float32(
364
+ nvvm.fmax(
365
+ Float32(a).ir_value(loc=loc, ip=ip),
366
+ Float32(b).ir_value(loc=loc, ip=ip),
367
+ c=Float32(c).ir_value(loc=loc, ip=ip) if c is not None else None,
368
+ loc=loc,
369
+ ip=ip,
370
+ )
371
+ )
372
+
373
+
374
+ @cute.jit
375
+ def fmax_reduce(
376
+ x: cute.TensorSSA, init_val: float | Float32 | None = None, arch: cutlass.Constexpr[int] = 80
377
+ ) -> Float32:
378
+ if const_expr(arch < 100 or cute.size(x.shape) % 8 != 0):
379
+ # if const_expr(init_val is None):
380
+ # init_val = -cutlass.Float32.if
381
+ # return x.reduce(cute.ReductionOp.MAX, init_val, 0)
382
+ res = cute.make_fragment(x.shape, Float32)
383
+ res.store(x)
384
+ # local_max = [res[0], res[1]]
385
+ # for i in cutlass.range_constexpr(2, cute.size(x.shape), 2):
386
+ # local_max[0] = fmax(local_max[0], res[i + 0])
387
+ # local_max[1] = fmax(local_max[1], res[i + 1])
388
+ # local_max[0] = fmax(local_max[0], local_max[1])
389
+ # return local_max[0] if const_expr(init_val is None) else fmax(local_max[0], init_val)
390
+ local_max = [res[0], res[1], res[2], res[3]]
391
+ for i in cutlass.range_constexpr(4, cute.size(x.shape), 4):
392
+ local_max[0] = fmax(local_max[0], res[i + 0])
393
+ local_max[1] = fmax(local_max[1], res[i + 1])
394
+ local_max[2] = fmax(local_max[2], res[i + 2])
395
+ local_max[3] = fmax(local_max[3], res[i + 3])
396
+ local_max[0] = fmax(local_max[0], local_max[1])
397
+ local_max[2] = fmax(local_max[2], local_max[3])
398
+ local_max[0] = fmax(local_max[0], local_max[2])
399
+ return local_max[0] if const_expr(init_val is None) else fmax(local_max[0], init_val)
400
+ else:
401
+ # [2025-06-15] x.reduce only seems to use 50% 3-input max and 50% 2-input max
402
+ # We instead force the 3-input max.
403
+ res = cute.make_fragment(x.shape, Float32)
404
+ res.store(x)
405
+ local_max_0 = (
406
+ fmax(init_val, res[0], res[1])
407
+ if const_expr(init_val is not None)
408
+ else fmax(res[0], res[1])
409
+ )
410
+ local_max = [
411
+ local_max_0,
412
+ fmax(res[2], res[3]),
413
+ fmax(res[4], res[5]),
414
+ fmax(res[6], res[7]),
415
+ ]
416
+ for i in cutlass.range_constexpr(8, cute.size(x.shape), 8):
417
+ local_max[0] = fmax(local_max[0], res[i], res[i + 1])
418
+ local_max[1] = fmax(local_max[1], res[i + 2], res[i + 3])
419
+ local_max[2] = fmax(local_max[2], res[i + 4], res[i + 5])
420
+ local_max[3] = fmax(local_max[3], res[i + 6], res[i + 7])
421
+ local_max[0] = fmax(local_max[0], local_max[1])
422
+ return fmax(local_max[0], local_max[2], local_max[3])
423
+
424
+
425
+ @cute.jit
426
+ def fadd_reduce(
427
+ x: cute.TensorSSA, init_val: float | Float32 | None = None, arch: cutlass.Constexpr[int] = 80
428
+ ) -> Float32:
429
+ if const_expr(arch < 100 or cute.size(x.shape) % 8 != 0):
430
+ if const_expr(init_val is None):
431
+ init_val = Float32.zero
432
+ return x.reduce(cute.ReductionOp.ADD, init_val, 0)
433
+ # res = cute.make_fragment(x.shape, Float32)
434
+ # res.store(x)
435
+ # local_sum = [res[0], res[1], res[2], res[3]]
436
+ # for i in cutlass.range_constexpr(4, cute.size(x.shape), 4):
437
+ # local_sum[0] += res[i + 0]
438
+ # local_sum[1] += res[i + 1]
439
+ # local_sum[2] += res[i + 2]
440
+ # local_sum[3] += res[i + 3]
441
+ # local_sum[0] += local_sum[1]
442
+ # local_sum[2] += local_sum[3]
443
+ # local_sum[0] += local_sum[2]
444
+ # return local_sum[0] if const_expr(init_val is None) else local_sum[0] + init_val
445
+ else:
446
+ res = cute.make_fragment(x.shape, Float32)
447
+ res.store(x)
448
+ local_sum_0 = (
449
+ cute.arch.add_packed_f32x2((init_val, 0.0), (res[0], res[1]))
450
+ # cute.arch.add_packed_f32x2((init_val / 2, init_val / 2), (res[0], res[1]))
451
+ if const_expr(init_val is not None)
452
+ else (res[0], res[1])
453
+ )
454
+ local_sum = [local_sum_0, (res[2], res[3]), (res[4], res[5]), (res[6], res[7])]
455
+ for i in cutlass.range_constexpr(8, cute.size(x.shape), 8):
456
+ local_sum[0] = cute.arch.add_packed_f32x2(local_sum[0], (res[i + 0], res[i + 1]))
457
+ local_sum[1] = cute.arch.add_packed_f32x2(local_sum[1], (res[i + 2], res[i + 3]))
458
+ local_sum[2] = cute.arch.add_packed_f32x2(local_sum[2], (res[i + 4], res[i + 5]))
459
+ local_sum[3] = cute.arch.add_packed_f32x2(local_sum[3], (res[i + 6], res[i + 7]))
460
+ local_sum[0] = cute.arch.add_packed_f32x2(local_sum[0], local_sum[1])
461
+ local_sum[2] = cute.arch.add_packed_f32x2(local_sum[2], local_sum[3])
462
+ local_sum[0] = cute.arch.add_packed_f32x2(local_sum[0], local_sum[2])
463
+ return local_sum[0][0] + local_sum[0][1]
464
+
465
+
466
+ @dsl_user_op
467
+ def atomic_add_fp32(a: float | Float32, gmem_ptr: cute.Pointer, *, loc=None, ip=None) -> None:
468
+ # gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
469
+ # # cache_hint = cutlass.Int64(0x12F0000000000000)
470
+ # llvm.inline_asm(
471
+ # None,
472
+ # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip)],
473
+ # # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip), cache_hint.ir_value()],
474
+ # "red.global.add.f32 [$0], $1;",
475
+ # # "red.global.add.L2::cache_hint.f32 [$0], $1, 0x12F0000000000000;",
476
+ # # "red.global.add.L2::cache_hint.f32 [$0], $1, $2;",
477
+ # "l,f",
478
+ # # "l,f,l",
479
+ # has_side_effects=True,
480
+ # is_align_stack=False,
481
+ # asm_dialect=llvm.AsmDialect.AD_ATT,
482
+ # )
483
+ nvvm.atomicrmw(
484
+ res=T.f32(), op=nvvm.AtomicOpKind.FADD, ptr=gmem_ptr.llvm_ptr, a=Float32(a).ir_value()
485
+ )
486
+
487
+
488
+ @dsl_user_op
489
+ def elem_pointer(x: cute.Tensor, coord: cute.Coord, *, loc=None, ip=None) -> cute.Pointer:
490
+ return x.iterator + cute.crd2idx(coord, x.layout, loc=loc, ip=ip)
491
+
492
+
493
+ @cute.jit
494
+ def predicate_k(tAcA: cute.Tensor, limit: cutlass.Int32) -> cute.Tensor:
495
+ # Only compute predicates for the "k" dimension. For the mn dimension, we will use "if"
496
+ tApA = cute.make_fragment(
497
+ cute.make_layout(
498
+ (cute.size(tAcA, mode=[0, 1]), cute.size(tAcA, mode=[1]), cute.size(tAcA, mode=[2])),
499
+ stride=(cute.size(tAcA, mode=[2]), 0, 1),
500
+ ),
501
+ cutlass.Boolean,
502
+ )
503
+ for rest_v in cutlass.range_constexpr(tApA.shape[0]):
504
+ for rest_k in cutlass.range_constexpr(tApA.shape[2]):
505
+ tApA[rest_v, 0, rest_k] = cute.elem_less(tAcA[(0, rest_v), 0, rest_k][1], limit)
506
+ return tApA
507
+
508
+
509
+ def canonical_warp_group_idx(sync: bool = True) -> cutlass.Int32:
510
+ warp_group_idx = cute.arch.thread_idx()[0] // 128
511
+ if const_expr(sync):
512
+ warp_group_idx = cute.arch.make_warp_uniform(warp_group_idx)
513
+ return warp_group_idx
514
+
515
+
516
+ # @dsl_user_op
517
+ # def warp_vote_any_lt(a: float | Float32, b: float | Float32, *, loc=None, ip=None) -> cutlass.Boolean:
518
+ # mask = cutlass.Int32(-1)
519
+ # return cutlass.Boolean(
520
+ # llvm.inline_asm(
521
+ # T.i32(),
522
+ # [Float32(a).ir_value(loc=loc, ip=ip), Float32(b).ir_value(loc=loc, ip=ip), mask.ir_value(loc=loc, ip=ip)],
523
+ # ".pred p1, p2;\n"
524
+ # "setp.lt.f32 p1, $1, $2;\n"
525
+ # "vote.sync.any.pred p2, p1, $3;\n"
526
+ # "selp.u32 $0, 1, 0, p2;",
527
+ # # "selp.u32 $0, 1, 0, p1;",
528
+ # "=r,f,f,r",
529
+ # has_side_effects=False,
530
+ # is_align_stack=False,
531
+ # asm_dialect=llvm.AsmDialect.AD_ATT,
532
+ # )
533
+ # )
534
+
535
+
536
+ @cute.jit
537
+ def shuffle_sync(
538
+ value: cute.Numeric,
539
+ offset: cute.typing.Int,
540
+ width: cutlass.Constexpr[int] = cute.arch.WARP_SIZE,
541
+ ) -> cute.Numeric:
542
+ assert value.width % 32 == 0, "value type must be a multiple of 32 bits"
543
+ # 1 -> 0b11111, 2 -> 0b11110, 4 -> 0b11100, 8 -> 0b11000, 16 -> 0b10000, 32 -> 0b00000
544
+ mask = cute.arch.WARP_SIZE - width
545
+ clamp = cute.arch.WARP_SIZE - 1
546
+ mask_and_clamp = mask << 8 | clamp
547
+ # important: need stride 1 and not 0 for recast_tensor to work
548
+ val = cute.make_rmem_tensor(cute.make_layout((1,), stride=(1,)), type(value))
549
+ val[0] = value
550
+ val_i32 = cute.recast_tensor(val, cutlass.Int32)
551
+ for i in cutlass.range_constexpr(cute.size(val_i32)):
552
+ val_i32[i] = cute.arch.shuffle_sync(val_i32[i], offset, mask_and_clamp=mask_and_clamp)
553
+ return val[0]
554
+
555
+
556
+ @dsl_user_op
557
+ def shl_u32(val: cutlass.Uint32, shift: cutlass.Uint32, *, loc=None, ip=None) -> cutlass.Uint32:
558
+ """
559
+ Left-shift val by shift bits using PTX shl.b32 (sign-agnostic).
560
+
561
+ Named ``shl_u32`` (not ``shl_b32``) because python type annotations
562
+ distinguish signed/unsigned.
563
+
564
+ PTX semantics (§9.7.8.8): "Shift amounts greater than the register width N
565
+ are clamped to N." So ``shl.b32 d, a, 32`` is well-defined and yields 0.
566
+
567
+ This differs from C/C++ and LLVM IR, where shifting by >= the type width is
568
+ undefined behavior. CuTeDSL compiles through MLIR -> LLVM IR, so a plain
569
+ Python-level ``Uint32(x) << Uint32(n)`` inherits LLVM's UB: the optimizer
570
+ may treat the result as poison and eliminate dependent code. Inline PTX
571
+ bypasses the LLVM IR shift entirely — the instruction is emitted verbatim
572
+ into PTX where clamping makes it safe for all shift amounts.
573
+ """
574
+ return cutlass.Uint32(
575
+ llvm.inline_asm(
576
+ T.i32(),
577
+ [
578
+ cutlass.Uint32(val).ir_value(loc=loc, ip=ip),
579
+ cutlass.Uint32(shift).ir_value(loc=loc, ip=ip),
580
+ ],
581
+ "shl.b32 $0, $1, $2;",
582
+ "=r,r,r",
583
+ has_side_effects=False,
584
+ is_align_stack=False,
585
+ asm_dialect=llvm.AsmDialect.AD_ATT,
586
+ )
587
+ )
588
+
589
+
590
+ @dsl_user_op
591
+ def shr_u32(val: cutlass.Uint32, shift: cutlass.Uint32, *, loc=None, ip=None) -> cutlass.Uint32:
592
+ """
593
+ Unsigned right-shift val by shift bits using PTX shr.u32 (zero-fills).
594
+
595
+ See ``shl_u32`` docstring for why inline PTX is used instead of plain
596
+ CuTeDSL shift operators (LLVM shift-by-type-width UB).
597
+ """
598
+ return cutlass.Uint32(
599
+ llvm.inline_asm(
600
+ T.i32(),
601
+ [
602
+ cutlass.Uint32(val).ir_value(loc=loc, ip=ip),
603
+ cutlass.Uint32(shift).ir_value(loc=loc, ip=ip),
604
+ ],
605
+ "shr.u32 $0, $1, $2;",
606
+ "=r,r,r",
607
+ has_side_effects=False,
608
+ is_align_stack=False,
609
+ asm_dialect=llvm.AsmDialect.AD_ATT,
610
+ )
611
+ )
612
+
613
+
614
+ @cute.jit
615
+ def warp_prefix_sum(val: cutlass.Int32, lane: Optional[cutlass.Int32] = None) -> cutlass.Int32:
616
+ if const_expr(lane is None):
617
+ lane = cute.arch.lane_idx()
618
+ # if cute.arch.thread_idx()[0] >= 128 and cute.arch.thread_idx()[0] < 128 + 32 and cute.arch.block_idx()[0] == 0: cute.printf("tidx = %d, val = %d", cute.arch.thread_idx()[0] % 32, val)
619
+ for i in cutlass.range_constexpr(int(math.log2(cute.arch.WARP_SIZE))):
620
+ offset = 1 << i
621
+ # Very important that we set mask_and_clamp to 0
622
+ partial_sum = cute.arch.shuffle_sync_up(val, offset=offset, mask_and_clamp=0)
623
+ if lane >= offset:
624
+ val += partial_sum
625
+ # if cute.arch.thread_idx()[0] >= 128 and cute.arch.thread_idx()[0] < 128 + 32 and cute.arch.block_idx()[0] == 0: cute.printf("tidx = %d, partial_sum = %d, val = %d", cute.arch.thread_idx()[0] % 32, partial_sum, val)
626
+ return val
627
+
628
+
629
+ @dsl_user_op
630
+ def cvt_f16x2_f32(
631
+ a: float | Float32, b: float | Float32, to_dtype: Type, *, loc=None, ip=None
632
+ ) -> cutlass.Int32:
633
+ assert to_dtype in [cutlass.BFloat16, cutlass.Float16], "to_dtype must be BFloat16 or Float16"
634
+ return cutlass.Int32(
635
+ llvm.inline_asm(
636
+ T.i32(),
637
+ [Float32(a).ir_value(loc=loc, ip=ip), Float32(b).ir_value(loc=loc, ip=ip)],
638
+ f"cvt.rn.{'bf16x2' if to_dtype is cutlass.BFloat16 else 'f16x2'}.f32 $0, $2, $1;",
639
+ "=r,f,f",
640
+ has_side_effects=False,
641
+ is_align_stack=False,
642
+ asm_dialect=llvm.AsmDialect.AD_ATT,
643
+ )
644
+ )
645
+
646
+
647
+ @overload
648
+ def cvt_f16(src: cute.Tensor, dst: cute.Tensor) -> None: ...
649
+
650
+
651
+ @overload
652
+ def cvt_f16(src: cute.Tensor, dtype: Type[cute.Numeric]) -> cute.Tensor: ...
653
+
654
+
655
+ @cute.jit
656
+ def cvt_f16(src: cute.Tensor, dst_or_dtype):
657
+ """Convert Float32 tensor to Float16/BFloat16.
658
+
659
+ Args:
660
+ src: Source tensor with Float32 element type
661
+ dst_or_dtype: Either a destination tensor or a dtype (Float16/BFloat16)
662
+
663
+ Returns:
664
+ None if dst is a tensor, or a new tensor if dtype is provided
665
+ """
666
+ if const_expr(isinstance(dst_or_dtype, type)):
667
+ # dtype variant: create new tensor and call the tensor variant
668
+ dtype = dst_or_dtype
669
+ dst = cute.make_fragment(src.shape, dtype)
670
+ cvt_f16(src, dst)
671
+ return dst
672
+ else:
673
+ # tensor variant: write to dst
674
+ dst = dst_or_dtype
675
+ assert cute.size(dst.shape) == cute.size(src.shape), "dst and src must have the same size"
676
+ assert cute.size(src.shape) % 2 == 0, "src must have an even number of elements"
677
+ assert dst.element_type in [cutlass.BFloat16, cutlass.Float16], (
678
+ "dst must be BFloat16 or Float16"
679
+ )
680
+ assert src.element_type is Float32, "src must be Float32"
681
+ dst_i32 = cute.recast_tensor(dst, cutlass.Int32)
682
+ assert cute.size(dst_i32.shape) * 2 == cute.size(src.shape)
683
+ for i in cutlass.range_constexpr(cute.size(dst_i32)):
684
+ dst_i32[i] = cvt_f16x2_f32(src[2 * i], src[2 * i + 1], dst.element_type)
685
+
686
+
687
+ @dsl_user_op
688
+ @cute.jit
689
+ def evaluate_polynomial(x: Float32, poly: Tuple[Float32, ...], *, loc=None, ip=None) -> Float32:
690
+ deg = len(poly) - 1
691
+ out = poly[deg]
692
+ for i in cutlass.range_constexpr(deg - 1, -1, -1):
693
+ out = out * x + poly[i]
694
+ return out
695
+
696
+
697
+ @dsl_user_op
698
+ @cute.jit
699
+ def evaluate_polynomial_2(
700
+ x: Float32, y: Float32, poly: Tuple[Float32, ...], *, loc=None, ip=None
701
+ ) -> Tuple[Float32, Float32]:
702
+ deg = len(poly) - 1
703
+ out = (poly[deg], poly[deg])
704
+ for i in cutlass.range_constexpr(deg - 1, -1, -1):
705
+ out = cute.arch.fma_packed_f32x2(out, (x, y), (poly[i], poly[i]))
706
+ return out
707
+
708
+
709
+ @dsl_user_op
710
+ def add_round_down(x: float | Float32, y: float | Float32, *, loc=None, ip=None) -> Float32:
711
+ # There's probably a way to call llvm or nvvm to do this instead of ptx
712
+ return cutlass.Float32(
713
+ llvm.inline_asm(
714
+ T.f32(),
715
+ [Float32(x).ir_value(loc=loc, ip=ip), Float32(y).ir_value(loc=loc, ip=ip)],
716
+ "add.rm.ftz.f32 $0, $1, $2;",
717
+ "=f,f,f",
718
+ has_side_effects=False,
719
+ is_align_stack=False,
720
+ asm_dialect=llvm.AsmDialect.AD_ATT,
721
+ )
722
+ )
723
+
724
+
725
+ @dsl_user_op
726
+ def combine_int_frac_ex2(x_rounded: Float32, frac_ex2: Float32, *, loc=None, ip=None) -> Float32:
727
+ return cutlass.Float32(
728
+ llvm.inline_asm(
729
+ T.f32(),
730
+ [
731
+ Float32(x_rounded).ir_value(loc=loc, ip=ip),
732
+ Float32(frac_ex2).ir_value(loc=loc, ip=ip),
733
+ ],
734
+ "{\n\t"
735
+ ".reg .s32 x_rounded_i, frac_ex_i, x_rounded_e, out_i;\n\t"
736
+ "mov.b32 x_rounded_i, $1;\n\t"
737
+ "mov.b32 frac_ex_i, $2;\n\t"
738
+ "shl.b32 x_rounded_e, x_rounded_i, 23;\n\t"
739
+ # add.u32 generates IMAD instruction and add.s32 generates LEA instruction
740
+ # IMAD uses the FMA pipeline and LEA uses the ALU pipeline, afaik
741
+ "add.s32 out_i, x_rounded_e, frac_ex_i;\n\t"
742
+ "mov.b32 $0, out_i;\n\t"
743
+ "}\n",
744
+ "=f,f,f",
745
+ has_side_effects=False,
746
+ is_align_stack=False,
747
+ asm_dialect=llvm.AsmDialect.AD_ATT,
748
+ )
749
+ )
750
+
751
+
752
+ @dsl_user_op
753
+ def ex2_emulation(x: Float32, *, poly_degree: int = 3, loc=None, ip=None) -> Float32:
754
+ assert poly_degree in POLY_EX2, f"Polynomial degree {poly_degree} not supported"
755
+ # We assume x <= 127.0
756
+ fp32_round_int = float(2**23 + 2**22)
757
+ x_clamped = cute.arch.fmax(x, -127.0)
758
+ # We want to round down here, so that the fractional part is in [0, 1)
759
+ x_rounded = add_round_down(x_clamped, fp32_round_int, loc=loc, ip=ip)
760
+ # The integer floor of x is now in the last 8 bits of x_rounded
761
+ # We assume the next 2 ops round to nearest even. The rounding mode is important.
762
+ x_rounded_back = x_rounded - fp32_round_int
763
+ x_frac = x_clamped - x_rounded_back
764
+ x_frac_ex2 = evaluate_polynomial(x_frac, POLY_EX2[poly_degree], loc=loc, ip=ip)
765
+ return combine_int_frac_ex2(x_rounded, x_frac_ex2, loc=loc, ip=ip)
766
+
767
+
768
+ # TODO: check that the ex2_emulation_2 produces the same SASS as the ptx version
769
+ @dsl_user_op
770
+ def ex2_emulation_2(
771
+ x: Float32, y: Float32, *, poly_degree: int = 3, loc=None, ip=None
772
+ ) -> Tuple[Float32, Float32]:
773
+ # We assume x <= 127.0 and y <= 127.0
774
+ fp32_round_int = float(2**23 + 2**22)
775
+ xy_clamped = (cute.arch.fmax(x, -127.0), cute.arch.fmax(y, -127.0))
776
+ # We want to round down here, so that the fractional part is in [0, 1)
777
+ xy_rounded = cute.arch.add_packed_f32x2(xy_clamped, (fp32_round_int, fp32_round_int), rnd="rm")
778
+ # The integer floor of x & y are now in the last 8 bits of xy_rounded
779
+ # We want the next 2 ops to round to nearest even. The rounding mode is important.
780
+ xy_rounded_back = quack_activation.sub_packed_f32x2(
781
+ xy_rounded, (fp32_round_int, fp32_round_int)
782
+ )
783
+ xy_frac = quack_activation.sub_packed_f32x2(xy_clamped, xy_rounded_back)
784
+ xy_frac_ex2 = evaluate_polynomial_2(*xy_frac, POLY_EX2[poly_degree], loc=loc, ip=ip)
785
+ x_out = combine_int_frac_ex2(xy_rounded[0], xy_frac_ex2[0], loc=loc, ip=ip)
786
+ y_out = combine_int_frac_ex2(xy_rounded[1], xy_frac_ex2[1], loc=loc, ip=ip)
787
+ return x_out, y_out
788
+
789
+
790
+ @dsl_user_op
791
+ def e2e_asm2(x: Float32, y: Float32, *, loc=None, ip=None) -> Tuple[Float32, Float32]:
792
+ out_f32x2 = llvm.inline_asm(
793
+ llvm.StructType.get_literal([T.f32(), T.f32()]),
794
+ [Float32(x).ir_value(loc=loc, ip=ip), Float32(y, loc=loc, ip=ip).ir_value()],
795
+ "{\n\t"
796
+ ".reg .f32 f1, f2, f3, f4, f5, f6, f7;\n\t"
797
+ ".reg .b64 l1, l2, l3, l4, l5, l6, l7, l8, l9, l10;\n\t"
798
+ ".reg .s32 r1, r2, r3, r4, r5, r6, r7, r8;\n\t"
799
+ "max.ftz.f32 f1, $2, 0fC2FE0000;\n\t"
800
+ "max.ftz.f32 f2, $3, 0fC2FE0000;\n\t"
801
+ "mov.b64 l1, {f1, f2};\n\t"
802
+ "mov.f32 f3, 0f4B400000;\n\t"
803
+ "mov.b64 l2, {f3, f3};\n\t"
804
+ "add.rm.ftz.f32x2 l7, l1, l2;\n\t"
805
+ "sub.rn.ftz.f32x2 l8, l7, l2;\n\t"
806
+ "sub.rn.ftz.f32x2 l9, l1, l8;\n\t"
807
+ "mov.f32 f7, 0f3D9DF09D;\n\t"
808
+ "mov.b64 l6, {f7, f7};\n\t"
809
+ "mov.f32 f6, 0f3E6906A4;\n\t"
810
+ "mov.b64 l5, {f6, f6};\n\t"
811
+ "mov.f32 f5, 0f3F31F519;\n\t"
812
+ "mov.b64 l4, {f5, f5};\n\t"
813
+ "mov.f32 f4, 0f3F800000;\n\t"
814
+ "mov.b64 l3, {f4, f4};\n\t"
815
+ "fma.rn.ftz.f32x2 l10, l9, l6, l5;\n\t"
816
+ "fma.rn.ftz.f32x2 l10, l10, l9, l4;\n\t"
817
+ "fma.rn.ftz.f32x2 l10, l10, l9, l3;\n\t"
818
+ "mov.b64 {r1, r2}, l7;\n\t"
819
+ "mov.b64 {r3, r4}, l10;\n\t"
820
+ "shl.b32 r5, r1, 23;\n\t"
821
+ "add.s32 r7, r5, r3;\n\t"
822
+ "shl.b32 r6, r2, 23;\n\t"
823
+ "add.s32 r8, r6, r4;\n\t"
824
+ "mov.b32 $0, r7;\n\t"
825
+ "mov.b32 $1, r8;\n\t"
826
+ "}\n",
827
+ "=r,=r,f,f",
828
+ has_side_effects=False,
829
+ is_align_stack=False,
830
+ asm_dialect=llvm.AsmDialect.AD_ATT,
831
+ )
832
+ out0 = Float32(llvm.extractvalue(T.f32(), out_f32x2, [0], loc=loc, ip=ip))
833
+ out1 = Float32(llvm.extractvalue(T.f32(), out_f32x2, [1], loc=loc, ip=ip))
834
+ return out0, out1
835
+
836
+
837
+ @dsl_user_op
838
+ def domain_offset_aligned(
839
+ coord: cute.Coord, tensor: cute.Tensor, *, loc=None, ip=None
840
+ ) -> cute.Tensor:
841
+ assert isinstance(tensor.iterator, cute.Pointer)
842
+ # We assume that applying the offset does not change the pointer alignment
843
+ new_ptr = cute.make_ptr(
844
+ tensor.element_type,
845
+ elem_pointer(tensor, coord).toint(),
846
+ tensor.memspace,
847
+ assumed_align=tensor.iterator.alignment,
848
+ )
849
+ return cute.make_tensor(new_ptr, tensor.layout)
850
+
851
+
852
+ @dsl_user_op
853
+ def warp_reduction(
854
+ val: cute.Numeric, op: Callable, *, threads_in_group: int = 32, loc=None, ip=None
855
+ ) -> cute.Numeric:
856
+ """Warp-wide reduction helper for a custom binary op."""
857
+ offset = threads_in_group // 2
858
+ while offset > 0:
859
+ val = op(
860
+ val,
861
+ cute.arch.shuffle_sync_bfly(
862
+ val, offset=offset, mask=-1, mask_and_clamp=31, loc=loc, ip=ip
863
+ ),
864
+ )
865
+ offset //= 2
866
+ return val
867
+
868
+
869
+ warp_reduction_max = partial(
870
+ warp_reduction, op=lambda x, y: fmax(x, y) if isinstance(x, Float32) else max(x, y)
871
+ )
872
+ warp_reduction_sum = partial(warp_reduction, op=lambda x, y: x + y) # noqa: FURB118
873
+
874
+
875
+ @dsl_user_op
876
+ def make_cotiled_copy(
877
+ atom: cute.CopyAtom, atom_layout_tv: cute.Layout, data_layout: cute.Layout, *, loc=None, ip=None
878
+ ) -> cute.TiledCopy:
879
+ """Compatibility wrapper for deprecated CuTeDSL `make_cotiled_copy`."""
880
+ assert cute.is_static(atom_layout_tv.type), "atom_layout_tv must be static"
881
+ assert cute.is_static(data_layout.type), "data_layout must be static"
882
+
883
+ inv_layout_ = cute.left_inverse(data_layout, loc=loc, ip=ip)
884
+ inv_data_layout = cute.make_layout(
885
+ (inv_layout_.shape, (1)), stride=(inv_layout_.stride, (0)), loc=loc, ip=ip
886
+ )
887
+ layout_tv_data = cute.composition(inv_data_layout, atom_layout_tv, loc=loc, ip=ip)
888
+
889
+ atom_layout_v_to_check = cute.coalesce(
890
+ cute.make_layout(atom_layout_tv.shape[1], stride=atom_layout_tv.stride[1], loc=loc, ip=ip),
891
+ loc=loc,
892
+ ip=ip,
893
+ )
894
+ data_layout_v_to_check = cute.coalesce(
895
+ cute.composition(
896
+ data_layout,
897
+ cute.make_layout(
898
+ layout_tv_data.shape[1], stride=layout_tv_data.stride[1], loc=loc, ip=ip
899
+ ),
900
+ loc=loc,
901
+ ip=ip,
902
+ ),
903
+ loc=loc,
904
+ ip=ip,
905
+ )
906
+ assert data_layout_v_to_check == atom_layout_v_to_check, (
907
+ "the memory pointed to by atom_layout_tv does not exist in the data_layout."
908
+ )
909
+
910
+ flat_data_shape = cute.product_each(data_layout.shape, loc=loc, ip=ip)
911
+ tiler = tuple(
912
+ cute.filter(
913
+ cute.composition(
914
+ cute.make_layout(
915
+ flat_data_shape,
916
+ stride=tuple(0 if j != i else 1 for j in range(cute.rank(flat_data_shape))),
917
+ loc=loc,
918
+ ip=ip,
919
+ ),
920
+ layout_tv_data,
921
+ loc=loc,
922
+ ip=ip,
923
+ ),
924
+ loc=loc,
925
+ ip=ip,
926
+ )
927
+ for i in range(cute.rank(flat_data_shape))
928
+ )
929
+ tile2data = cute.composition(
930
+ cute.make_layout(flat_data_shape, loc=loc, ip=ip), tiler, loc=loc, ip=ip
931
+ )
932
+ layout_tv = cute.composition(
933
+ cute.left_inverse(tile2data, loc=loc, ip=ip), layout_tv_data, loc=loc, ip=ip
934
+ )
935
+ return cute.make_tiled_copy(atom, layout_tv, tiler, loc=loc, ip=ip)
936
+
937
+
938
+ @cute.jit
939
+ def scalar_to_ssa(a: cute.Numeric, dtype) -> cute.TensorSSA:
940
+ """Convert a scalar to a cute TensorSSA of shape (1,) and given dtype"""
941
+ vec = cute.make_fragment(1, dtype)
942
+ vec[0] = a
943
+ return vec.load()
944
+
945
+
946
+ def ssa_to_scalar(val):
947
+ """Could inline but nice for reflecting the above api"""
948
+ return val[0]
build/torch-cuda/metadata.json ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "fa4-cute-runtime",
3
+ "id": "_fa4_cute_runtime_cuda_8aeea60",
4
+ "version": 1,
5
+ "license": "BSD-3-Clause",
6
+ "python-depends": [
7
+ "einops",
8
+ "tvm-ffi",
9
+ "nvidia-cutlass-dsl"
10
+ ],
11
+ "backend": {
12
+ "type": "cuda"
13
+ },
14
+ "digest": {
15
+ "algorithm": "sha256",
16
+ "files": {
17
+ "__init__.py": "wgqRL1xQbZK6DKTDEQYqQNAIg06tQIZskiJbTjx+8PQ=",
18
+ "_ops.py": "NL8H1HgBavEWrAJ2FyNptJaExyt1DvqNuv+zUEHq22g=",
19
+ "flashrt_fa4/__init__.py": "QDJyS5jtJnGFsSuOx43ZvLBCOrHImm8NrZk5f9URWdk=",
20
+ "flashrt_fa4/cute/.flake8": "jLOWNT+97fgCh5KqNCiEnnvZxySm6klT2jJzeGBXDsk=",
21
+ "flashrt_fa4/cute/AUTHORS": "RieEHCBsm/mQ03zC7L+neKYy2FcxqvtMy1kjgzTSgh0=",
22
+ "flashrt_fa4/cute/LICENSE": "jJzLlsBl5wYTW2y60nm3IdphVuUfOl8nxrMymvlBbXM=",
23
+ "flashrt_fa4/cute/__init__.py": "zQElR2Z4rodSQX+AQTGxyRqAbw6Nq+r2SpzYnktxKDA=",
24
+ "flashrt_fa4/cute/ampere_helpers.py": "ytrBcKfaN54nw20EQ2BIDXEM+ojQgtrSud7NWOSEcRE=",
25
+ "flashrt_fa4/cute/barrier.py": "dnr4B6p1IbjRXubJU09HRaetIiSw2iuBbMScy56mwRk=",
26
+ "flashrt_fa4/cute/blackwell_helpers.py": "Ulor2QDzVaUocBxnbECUXZgLz/Vey7gg4DJX4HwTSzA=",
27
+ "flashrt_fa4/cute/block_info.py": "7bH6QXtV1EgXfCpWNeK50J2gil55Lha8qP8cmahzQlI=",
28
+ "flashrt_fa4/cute/block_sparse_utils.py": "7TVkMb87s86l1hcAJT9pRpEdox13kYBllA4Ul+ZbH+M=",
29
+ "flashrt_fa4/cute/block_sparsity.py": "RF+myNEB3zvIcweIbtcFW5LQSTGbifwoGk8jwO9mfGY=",
30
+ "flashrt_fa4/cute/cache_utils.py": "csDsYBIK4bM5ldSyW1qsi97ZL7jraeRluRIuBY17lCQ=",
31
+ "flashrt_fa4/cute/copy_utils.py": "D1Y+7rZlRgLRVB9yAre4g4bE29vSStawglg01l5QjAk=",
32
+ "flashrt_fa4/cute/cute_dsl_ptxas.py": "IfBnTJ9amdfDOQkuSdWCLTh7CkZziIvs/xrAc8taxhk=",
33
+ "flashrt_fa4/cute/cute_dsl_utils.py": "6CSKoO3Fk72y6QOXKBqRsFR7szO6PGcsRePOQu1GsHo=",
34
+ "flashrt_fa4/cute/fa_logging.py": "RwCKGmzRI+2251TAGvQll1GVIrKXzqG+AyhXy+yhmKo=",
35
+ "flashrt_fa4/cute/fast_math.py": "fl+CK/TQujaWdVjzrpoUCLQQy3JsMSIs1hcDVp0b5zg=",
36
+ "flashrt_fa4/cute/flash_fwd_combine.py": "nB6loyKvlPBSWANvzucQ1BthoQk9Cvse5SAc8jXunzI=",
37
+ "flashrt_fa4/cute/flash_fwd_sm100.py": "1X8zTB7D897uSJyKG9R7vjw0taY3EM7/4+0DnMABORc=",
38
+ "flashrt_fa4/cute/interface_fwd_sm100.py": "m7lfZi9qA3AGZEKuo/njgg/cmW6jbFuOUsqOnSbL1ns=",
39
+ "flashrt_fa4/cute/mask.py": "Lc/ZVBCHqw9CJTmyrrATbkA57SszRGNKrPOWa45vVeU=",
40
+ "flashrt_fa4/cute/mma_sm100_desc.py": "rG7Ciy8aS3l8ORp4B7iPg7QwRS1BgmLD36ew81FuOfk=",
41
+ "flashrt_fa4/cute/named_barrier.py": "6eJoAiSva6gmDjaq5Lsv3PWJt1vQ0HBQ33xMWeHHFt0=",
42
+ "flashrt_fa4/cute/pack_gqa.py": "NKgz4fVByUXspujafNnz1e6P1pcZ2vJj3uU6F+V4BPU=",
43
+ "flashrt_fa4/cute/paged_kv.py": "fhPZXtc8KsT0tO0fS1ze1NNr8r7jIksNS0Q0I7m2shU=",
44
+ "flashrt_fa4/cute/pipeline.py": "hJvOkccr4KXHRYl54QyCkPReBSU6g2AOMPuVQMYe1Wo=",
45
+ "flashrt_fa4/cute/seqlen_info.py": "QhiL+UP8MldON58Ita7+UdvHgxWpYAHTFxIUxQT01J8=",
46
+ "flashrt_fa4/cute/sm100_hd256_2cta_fmha_forward.py": "M4HeDqxnkI+MAIwn/AzJIHuE2VIR7OjbMJ3yQpxtgtA=",
47
+ "flashrt_fa4/cute/softmax.py": "x7+7+qmjdZF0/iXYo9MMfKTPLdT8/yLauG/JaNDWaKo=",
48
+ "flashrt_fa4/cute/testing.py": "IpK1idwE0qaCLGWqs6WI+u2MejB/VOE0hQBRmuEweW8=",
49
+ "flashrt_fa4/cute/tile_scheduler.py": "ykdClNE/ZwwP6NrzgOshiUOxEAThIU8tYBLqiqnaIbM=",
50
+ "flashrt_fa4/cute/utils.py": "SFlf4ThdwaKvvn7ocEEH3JjBtusgQ4ahHYKlS1WiZ1U=",
51
+ "quack/__init__.py": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
52
+ "quack/activation.py": "D7ODhhhoBPOTIlyul3IhxaaZyYgy8qRZxSRVKhOgUnA=",
53
+ "quack/compile_utils.py": "qJ3oTsDlbAiddrJHtEO7LPYVqn/s+neNfiw+/KvfXZU=",
54
+ "quack/copy_utils.py": "Em283YfONKRESV+TF+4vJ4663cm9G7PuBdrTsGvQkSA=",
55
+ "quack/cute_dsl_utils.py": "blyzks11vGX0tCvNIFQKIHaa8dpHE0cpdnQxQJePJLc=",
56
+ "quack/layout_utils.py": "Fb3wDjm6B5lU5E54Ub0qCBlshIKzj84IaTQA8JxoIsg=",
57
+ "quack/sm90_utils.py": "V9+7RlVPvS4huOX2zBj6sD+hSlzOdwUR3UwaRPiIfIQ=",
58
+ "quack/utils.py": "CobF28mmgrsAauAH07dyZ8bBQoJrs4HuFE6KPRjM+6k="
59
+ }
60
+ },
61
+ "provenance": {
62
+ "kernel-builder": {
63
+ "version": "0.17.0-dev0",
64
+ "sha": "81f55ea30fd8f819dcf93a3c934dd584c895bd2f",
65
+ "dirty": false
66
+ },
67
+ "kernel": {
68
+ "sha": "8aeea60b17c970bc431c7001c6f5008d1010ed6f",
69
+ "dirty": false
70
+ }
71
+ }
72
+ }
build/torch-cuda/quack/__init__.py ADDED
File without changes
build/torch-cuda/quack/activation.py ADDED
@@ -0,0 +1,568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import math
4
+ from typing import Tuple
5
+ from functools import partial
6
+
7
+ import cutlass.cute as cute
8
+ from cutlass import Float32, Boolean, const_expr
9
+ from cutlass.cutlass_dsl import T, dsl_user_op
10
+ from cutlass._mlir.dialects import llvm, nvvm
11
+
12
+
13
+ F32_or_F32x2 = Float32 | Tuple[Float32, Float32]
14
+
15
+
16
+ sub_packed_f32x2 = partial(
17
+ cute.arch.calc_packed_f32x2_op,
18
+ src_c=None,
19
+ calc_func=nvvm.sub_packed_f32x2,
20
+ )
21
+
22
+
23
+ @dsl_user_op
24
+ def tanh(a: float | Float32, *, loc=None, ip=None) -> Float32:
25
+ return Float32(
26
+ llvm.inline_asm(
27
+ T.f32(),
28
+ [Float32(a).ir_value(loc=loc, ip=ip)],
29
+ "tanh.approx.f32 $0, $1;",
30
+ "=f,f",
31
+ has_side_effects=False,
32
+ is_align_stack=False,
33
+ asm_dialect=llvm.AsmDialect.AD_ATT,
34
+ )
35
+ )
36
+
37
+
38
+ @dsl_user_op
39
+ def sigmoid(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
40
+ if const_expr(not isinstance(x, tuple)):
41
+ # return 0.5 + 0.5 * cute.math.tanh(0.5 * x, fastmath=True)
42
+ return 0.5 + 0.5 * tanh(0.5 * x)
43
+ else:
44
+ x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x)
45
+ tanh_x_half = (tanh(x_half[0]), tanh(x_half[1]))
46
+ return cute.arch.fma_packed_f32x2(tanh_x_half, (0.5, 0.5), (0.5, 0.5))
47
+
48
+
49
+ @dsl_user_op
50
+ def dsigmoid_from_output(out: Float32, dout: Float32, *, loc=None, ip=None) -> Float32:
51
+ # return dout * out * (1.0 - out)
52
+ return dout * (out - out * out)
53
+
54
+
55
+ @dsl_user_op
56
+ def relu(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
57
+ if const_expr(not isinstance(x, tuple)):
58
+ return cute.arch.fmax(x, Float32(0.0))
59
+ else:
60
+ return cute.arch.fmax(x[0], Float32(0.0)), cute.arch.fmax(x[1], Float32(0.0))
61
+
62
+
63
+ @dsl_user_op
64
+ @cute.jit
65
+ def drelu(
66
+ x: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
67
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
68
+ if const_expr(not isinstance(x, tuple)):
69
+ x_pos = Boolean(x > 0)
70
+ return dout if x_pos else Float32(0.0), cute.arch.fmax(x, Float32(0.0))
71
+ else:
72
+ x0_pos = Boolean(x[0] > 0)
73
+ x1_pos = Boolean(x[1] > 0)
74
+ dx = (dout[0] if x0_pos else Float32(0.0), dout[1] if x1_pos else Float32(0.0))
75
+ return dx, relu(x)
76
+
77
+
78
+ @dsl_user_op
79
+ def relu_sq(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
80
+ if const_expr(not isinstance(x, tuple)):
81
+ return cute.arch.fmax(x, Float32(0.0)) * x
82
+ else:
83
+ relu_x = (cute.arch.fmax(x[0], Float32(0.0)), cute.arch.fmax(x[1], Float32(0.0)))
84
+ return cute.arch.mul_packed_f32x2(relu_x, x)
85
+
86
+
87
+ @dsl_user_op
88
+ @cute.jit
89
+ def drelu_sq(
90
+ x: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
91
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
92
+ """
93
+ ReLU squared backward pass: computes gradient w.r.t. x and recomputes forward
94
+ Given: relu_sq_out = max(x, 0) * x, and dout = grad w.r.t. relu_sq_out
95
+ Returns: (dx, relu_sq_out) where:
96
+ - dx = dout * 2 * x if x > 0, else 0
97
+ - relu_sq_out = max(x, 0) * x
98
+ """
99
+ if const_expr(not isinstance(x, tuple)):
100
+ relu_x = relu(x)
101
+ relu_sq_out = relu_x * x
102
+ # Derivative: d/dx[max(x,0) * x] = 2*x if x > 0, else 0
103
+ dx = 2.0 * (dout * relu_x)
104
+ return dx, relu_sq_out
105
+ else:
106
+ relu_x = relu(x)
107
+ relu_sq_out = cute.arch.mul_packed_f32x2(relu_x, x)
108
+ dx = cute.arch.mul_packed_f32x2((2.0, 2.0), cute.arch.mul_packed_f32x2(dout, relu_x))
109
+ return dx, relu_sq_out
110
+
111
+
112
+ @dsl_user_op
113
+ def gelu_tanh_approx(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
114
+ """
115
+ gelu(x) = 0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715 * x^3)))
116
+ = 0.5 * x * (1 + tanh(x * (0.797885 + 0.0356774 * x * x)))
117
+ """
118
+ sqrt_2_over_pi = math.sqrt(2 / math.pi) # ~0.797885
119
+ sqrt_2_over_pi_coeff = 0.044715 * sqrt_2_over_pi # ~0.0356774
120
+ if const_expr(not isinstance(x, tuple)):
121
+ return 0.5 * (
122
+ x
123
+ # Currently cute.math.tanh(x, fastmath=True) generates very slow code
124
+ # * (1 + cute.math.tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * (x * x)), fastmath=True))
125
+ * (1.0 + tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * (x * x))))
126
+ )
127
+ else:
128
+ x_sq = cute.arch.mul_packed_f32x2(x, x)
129
+ x_sq_scaled = cute.arch.fma_packed_f32x2(
130
+ x_sq, (sqrt_2_over_pi_coeff, sqrt_2_over_pi_coeff), (sqrt_2_over_pi, sqrt_2_over_pi)
131
+ )
132
+ z = cute.arch.mul_packed_f32x2(x, x_sq_scaled)
133
+ tanh_z = (tanh(z[0]), tanh(z[1]))
134
+ x_tanh_z = cute.arch.fma_packed_f32x2(tanh_z, x, x)
135
+ return cute.arch.mul_packed_f32x2((0.5, 0.5), x_tanh_z)
136
+
137
+
138
+ @dsl_user_op
139
+ def dgelu_tanh_approx(
140
+ x: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
141
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2]:
142
+ """
143
+ GELU tanh approximation backward pass: computes gradient w.r.t. x and recomputes forward
144
+ Given: gelu_out = 0.5 * x * (1 + tanh(x * (c1 + c2 * x^2))), and dout = grad w.r.t. gelu_out
145
+ Returns: (dx, gelu_out)
146
+
147
+ Derivative uses the chain rule:
148
+ d/dx[gelu(x)] = 0.5 * (1 + tanh(z)) + 0.5 * x * sech^2(z) * dz/dx
149
+ where z = x * (c1 + c2 * x^2), dz/dx = c1 + 3 * c2 * x^2
150
+ and sech^2(z) = 1 - tanh^2(z)
151
+ """
152
+ sqrt_2_over_pi = math.sqrt(2 / math.pi) # c1 ~0.797885
153
+ sqrt_2_over_pi_coeff = 0.044715 * sqrt_2_over_pi # c2 ~0.0356774
154
+ sqrt_2_over_pi_coeff_3 = 3.0 * sqrt_2_over_pi_coeff # c3 ~0.01070322
155
+
156
+ if const_expr(not isinstance(x, tuple)):
157
+ # Compute z = x * (c1 + c2 * x^2)
158
+ x_sq = x * x
159
+ # tanh_z = cute.math.tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * x_sq), fastmath=True)
160
+ tanh_z = tanh(x * (sqrt_2_over_pi + sqrt_2_over_pi_coeff * x_sq))
161
+ half_tanh_z_plus_one = 0.5 + 0.5 * tanh_z
162
+ gelu_out = x * half_tanh_z_plus_one
163
+
164
+ # Compute gradient
165
+ # sech^2(z) = 1 - tanh^2(z)
166
+ sech2_z = 1 - tanh_z * tanh_z
167
+ # dz/dx = c1 + 3 * c2 * x^2
168
+ dz_dx = sqrt_2_over_pi + sqrt_2_over_pi_coeff_3 * x_sq
169
+ # d/dx[gelu(x)] = 0.5 * (1 + tanh(z)) + 0.5 * x * sech^2(z) * dz/dx
170
+ dgelu = half_tanh_z_plus_one + x * (0.5 * (sech2_z * dz_dx))
171
+
172
+ dx = dout * dgelu
173
+ return dx, gelu_out
174
+ else:
175
+ # Compute z = x * (c1 + c2 * x^2)
176
+ x_sq = cute.arch.mul_packed_f32x2(x, x)
177
+ x_sq_scaled = cute.arch.fma_packed_f32x2(
178
+ x_sq, (sqrt_2_over_pi_coeff, sqrt_2_over_pi_coeff), (sqrt_2_over_pi, sqrt_2_over_pi)
179
+ )
180
+ z = cute.arch.mul_packed_f32x2(x, x_sq_scaled)
181
+ tanh_z = (tanh(z[0]), tanh(z[1]))
182
+ half_tanh_z_plus_one = cute.arch.fma_packed_f32x2(tanh_z, (0.5, 0.5), (0.5, 0.5))
183
+ gelu_out = cute.arch.mul_packed_f32x2(x, half_tanh_z_plus_one)
184
+
185
+ # Compute gradient
186
+ # sech^2(z) = 1 - tanh^2(z)
187
+ sech2_z = cute.arch.fma_packed_f32x2(tanh_z, (-tanh_z[0], -tanh_z[1]), (1.0, 1.0))
188
+ # dz/dx = c1 + 3 * c2 * x^2
189
+ dz_dx = cute.arch.fma_packed_f32x2(
190
+ x_sq, (sqrt_2_over_pi_coeff_3, sqrt_2_over_pi_coeff_3), (sqrt_2_over_pi, sqrt_2_over_pi)
191
+ )
192
+ # d/dx[gelu(x)] = 0.5 * (1 + tanh(z)) + 0.5 * x * sech^2(z) * dz/dx
193
+ sech2_dz_dx = cute.arch.mul_packed_f32x2(sech2_z, dz_dx)
194
+ x_sech2_dz_dx = cute.arch.mul_packed_f32x2(x, sech2_dz_dx)
195
+ dgelu = cute.arch.fma_packed_f32x2(x_sech2_dz_dx, (0.5, 0.5), half_tanh_z_plus_one)
196
+
197
+ dx = cute.arch.mul_packed_f32x2(dout, dgelu)
198
+ return dx, gelu_out
199
+
200
+
201
+ @dsl_user_op
202
+ @cute.jit
203
+ def softplus(x: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
204
+ if const_expr(not isinstance(x, tuple)):
205
+ use_linear = Boolean(x > 20.0)
206
+ return (
207
+ cute.math.log(Float32(cute.math.exp(x, fastmath=True)) + 1.0, fastmath=True)
208
+ if not use_linear
209
+ else x
210
+ )
211
+ else:
212
+ log2_e = math.log2(math.e)
213
+ x_log2e = cute.arch.mul_packed_f32x2(x, (log2_e, log2_e))
214
+ x_exp = (cute.math.exp(x_log2e[0], fastmath=True), cute.math.exp(x_log2e[1], fastmath=True))
215
+ x_exp_p1 = cute.arch.add_packed_f32x2(x_exp, (1.0, 1.0))
216
+ log_x_exp_p1 = (
217
+ cute.math.log2(x_exp_p1[0], fastmath=True),
218
+ cute.math.log2(x_exp_p1[1], fastmath=True),
219
+ )
220
+ ln2 = math.log(2.0)
221
+ softplus_x = cute.arch.mul_packed_f32x2(log_x_exp_p1, (ln2, ln2))
222
+ use_linear_0 = Boolean(x[0] > 20.0)
223
+ use_linear_1 = Boolean(x[1] > 20.0)
224
+ return (
225
+ softplus_x[0] if not use_linear_0 else x[0],
226
+ softplus_x[1] if not use_linear_1 else x[1],
227
+ )
228
+
229
+
230
+ @dsl_user_op
231
+ @cute.jit
232
+ def dsoftplus_from_output(out: Float32, dout: Float32, *, loc=None, ip=None) -> Float32:
233
+ use_linear = Boolean(out > 20.0)
234
+ # dx = dout * (1.0 - cute.math.exp(-out, fastmath=True)) if not use_linear else dout
235
+ dx = dout - dout * cute.math.exp(-out, fastmath=True)
236
+ return dx if not use_linear else dout
237
+
238
+
239
+ @dsl_user_op
240
+ def silu(x: F32_or_F32x2, *, already_halved: bool = False, loc=None, ip=None) -> F32_or_F32x2:
241
+ """
242
+ silu(x) = x * sigmoid(x) = x * (1 + tanh(x / 2)) / 2 = (0.5 * x) * tanh(0.5 * x) + (0.5 * x)
243
+ This compiles down to 3 SASS instructions: FMUL to get 0.5 * x, MUFU.TANH, and FFMA.
244
+ """
245
+ if const_expr(not isinstance(x, tuple)):
246
+ x_half = 0.5 * x if const_expr(not already_halved) else x
247
+ # return x_half * cute.math.tanh(x_half, fastmath=True) + x_half
248
+ return x_half * tanh(x_half) + x_half
249
+ else:
250
+ x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x) if const_expr(not already_halved) else x
251
+ tanh_x_half = (tanh(x_half[0]), tanh(x_half[1]))
252
+ return cute.arch.fma_packed_f32x2(x_half, tanh_x_half, x_half)
253
+
254
+
255
+ @dsl_user_op
256
+ def swiglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
257
+ if const_expr(not isinstance(x, tuple)):
258
+ return silu(x) * y
259
+ else:
260
+ return cute.arch.mul_packed_f32x2(silu(x), y)
261
+
262
+
263
+ @dsl_user_op
264
+ def dswiglu(
265
+ x: F32_or_F32x2,
266
+ y: F32_or_F32x2,
267
+ dout: F32_or_F32x2,
268
+ *,
269
+ already_halved: bool = False,
270
+ loc=None,
271
+ ip=None,
272
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
273
+ """
274
+ SwiGLU backward pass: computes gradients w.r.t. x (gate) and y (up projection)
275
+ Given: swiglu_out = silu(x) * y, and dout = grad w.r.t. swiglu_out
276
+ Returns: (dx, dy, swiglu_out) where dx = dout * y * d_silu(x), dy = dout * silu(x)
277
+
278
+ d_silu(x) = sigmoid(x) * (1 + x * (1 - sigmoid(x)))
279
+
280
+ This has been optimized to use fewer instructions (i.e. we expand things out
281
+ to use FFMA instead of FADD and FMUL).
282
+ """
283
+ if const_expr(not isinstance(x, tuple)):
284
+ # Compute sigmoid(x) using tanh: sigmoid(x) = 0.5 * (1 + tanh(0.5 * x))
285
+ # FMUL, MUFU.TANH, then FFMA
286
+ if const_expr(not already_halved):
287
+ sigmoid_x = sigmoid(x)
288
+ silu_x = x * sigmoid_x # FMUL
289
+ else:
290
+ tanh_x = tanh(x) # MUFU.TANH
291
+ sigmoid_x = 0.5 * tanh_x + 0.5 # FFMA
292
+ silu_x = x * tanh_x + x # FFMA
293
+ silu_x_dout = silu_x * dout # FMUL
294
+ # d_silu(x) * dout
295
+ # = sigmoid_x * (1 + x * (1 - sigmoid_x)) * dout
296
+ # = (sigmoid_x + sigmoid_x * x * (1 - sigmoid_x)) * dout
297
+ # = (sigmoid_x + silu_x * (1 - sigmoid_x)) * dout
298
+ # = (sigmoid_x + silu_x - silu_x * sigmoid_x) * dout
299
+ # = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x * dout
300
+ d_silu_x_dout = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x_dout # FFMA, FFMA
301
+ dx = d_silu_x_dout * y # FMUL
302
+ dy = silu_x_dout
303
+ swiglu_out = silu_x * y # FMUL
304
+ # Overall it's 1 MUFU.TANH, 5 FMUL, 3 FFMA
305
+ return dx, dy, swiglu_out
306
+ else:
307
+ # Compute sigmoid(x) and silu(x)
308
+ if const_expr(not already_halved):
309
+ sigmoid_x = sigmoid(x)
310
+ silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_x)
311
+ else:
312
+ tanh_x = (tanh(x[0]), tanh(x[1]))
313
+ sigmoid_x = cute.arch.fma_packed_f32x2(tanh_x, (0.5, 0.5), (0.5, 0.5))
314
+ silu_x = cute.arch.fma_packed_f32x2(x, tanh_x, x)
315
+ silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
316
+ # d_silu(x) * dout = (sigmoid_x - silu_x * sigmoid_x) * dout + silu_x * dout
317
+ sigmoid_x_minus_silu_x_sigmoid_x = cute.arch.fma_packed_f32x2(
318
+ sigmoid_x, (-silu_x[0], -silu_x[1]), sigmoid_x
319
+ )
320
+ d_silu_x_dout = cute.arch.fma_packed_f32x2(
321
+ sigmoid_x_minus_silu_x_sigmoid_x, dout, silu_x_dout
322
+ )
323
+ dx = cute.arch.mul_packed_f32x2(d_silu_x_dout, y)
324
+ dy = silu_x_dout
325
+ swiglu_out = cute.arch.mul_packed_f32x2(silu_x, y)
326
+ return dx, dy, swiglu_out
327
+
328
+
329
+ @dsl_user_op
330
+ def swiglu_oai(
331
+ x: F32_or_F32x2, y: F32_or_F32x2, alpha: float = 1.702, *, loc=None, ip=None
332
+ ) -> F32_or_F32x2:
333
+ """The swiglu variant used in gpt-oss, which has a scaling factor on x and bias of 1 to y.
334
+ https://github.com/openai/gpt-oss/blob/7be9334950053a888e24887a57dac797a17d6e00/gpt_oss/torch/model.py#L249
335
+ x * sigmoid(alpha * x) * (y + 1)
336
+ Compile down to FMUL, FMUL, TANH, FFMA, FFMA
337
+ """
338
+ # Compute sigmoid(alpha * x) using tanh: sigmoid(z) = 0.5 * (1 + tanh(z/2))
339
+ if const_expr(not isinstance(x, tuple)):
340
+ x_half = 0.5 * x
341
+ # silu_x = x_half * cute.math.tanh(alpha * x_half, fastmath=True) + x_half
342
+ silu_x = x_half * tanh(alpha * x_half) + x_half
343
+ return silu_x * y + silu_x
344
+ else:
345
+ x_half = cute.arch.mul_packed_f32x2((0.5, 0.5), x)
346
+ alpha_x_half = cute.arch.mul_packed_f32x2((alpha, alpha), x_half)
347
+ tanh_alpha_x_half = (tanh(alpha_x_half[0]), tanh(alpha_x_half[1]))
348
+ silu_x = cute.arch.fma_packed_f32x2(x_half, tanh_alpha_x_half, x_half)
349
+ return cute.arch.fma_packed_f32x2(silu_x, y, silu_x)
350
+
351
+
352
+ @dsl_user_op
353
+ def dswiglu_oai(
354
+ x: F32_or_F32x2, y: F32_or_F32x2, dout: F32_or_F32x2, alpha: float = 1.702, *, loc=None, ip=None
355
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
356
+ """
357
+ Swiglu OAI backward pass: computes gradients w.r.t. x and y
358
+ Given: swiglu_oai_out = x * sigmoid(alpha * x) * (y + 1), and dout = grad w.r.t. swiglu_oai_out
359
+ Returns: (dx, dy, swiglu_oai_out)
360
+
361
+ Derivative of x * sigmoid(alpha * x) w.r.t. x:
362
+ d/dx[x * sigmoid(alpha * x)] = sigmoid(alpha * x) + alpha * x * sigmoid(alpha * x) * (1 - sigmoid(alpha * x))
363
+ """
364
+ if const_expr(not isinstance(x, tuple)):
365
+ # Compute sigmoid(alpha * x) using tanh: sigmoid(z) = 0.5 * (1 + tanh(z/2))
366
+ alpha_x_half = (0.5 * alpha) * x # FMUL
367
+ # MUFU.TANH, then FFMA
368
+ # sigmoid_alpha_x = 0.5 + 0.5 * cute.math.tanh(alpha_x_half, fastmath=True)
369
+ sigmoid_alpha_x = 0.5 + 0.5 * tanh(alpha_x_half)
370
+ silu_x = x * sigmoid_alpha_x # FMUL
371
+ silu_x_dout = silu_x * dout # FMUL
372
+ # FFMA, FFMA, FMUL
373
+ d_silu_x_dout = (sigmoid_alpha_x + alpha * (silu_x - silu_x * sigmoid_alpha_x)) * dout
374
+ dx = d_silu_x_dout * y + d_silu_x_dout # FFMA, instead of multiply by y + 1
375
+ dy = silu_x_dout
376
+ swiglu_out = silu_x * y + silu_x # FFMA, instead of multiply by y + 1
377
+ # Overall it's 1 MUFU.TANH, 4 FMUL, 5 FFMA
378
+ return dx, dy, swiglu_out
379
+ else:
380
+ # Compute sigmoid(alpha * x)
381
+ alpha_x_half = cute.arch.mul_packed_f32x2(((0.5 * alpha), (0.5 * alpha)), x)
382
+ tanh_alpha_x_half = (tanh(alpha_x_half[0]), tanh(alpha_x_half[1]))
383
+ sigmoid_alpha_x = cute.arch.fma_packed_f32x2(tanh_alpha_x_half, (0.5, 0.5), (0.5, 0.5))
384
+ silu_x = cute.arch.mul_packed_f32x2(x, sigmoid_alpha_x)
385
+ silu_x_dout = cute.arch.mul_packed_f32x2(silu_x, dout)
386
+ # d_silu_x_dout = (sigmoid_alpha_x + alpha * (silu_x - silu_x * sigmoid_alpha_x)) * dout
387
+ silu_x_minus_product = cute.arch.fma_packed_f32x2(
388
+ silu_x, (-sigmoid_alpha_x[0], -sigmoid_alpha_x[1]), silu_x
389
+ )
390
+ sigmoid_plus_alpha_diff = cute.arch.fma_packed_f32x2(
391
+ (alpha, alpha), silu_x_minus_product, sigmoid_alpha_x
392
+ )
393
+ d_silu_x_dout = cute.arch.mul_packed_f32x2(sigmoid_plus_alpha_diff, dout)
394
+ dx = cute.arch.fma_packed_f32x2(d_silu_x_dout, y, d_silu_x_dout)
395
+ dy = silu_x_dout
396
+ swiglu_out = cute.arch.fma_packed_f32x2(silu_x, y, silu_x)
397
+ return dx, dy, swiglu_out
398
+
399
+
400
+ @dsl_user_op
401
+ def glu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
402
+ """GLU: Gated Linear Unit
403
+ glu(x, y) = sigmoid(x) * y
404
+ Using tanh to compute sigmoid: sigmoid(x) = 0.5 * (1 + tanh(x/2))
405
+ """
406
+ if const_expr(not isinstance(x, tuple)):
407
+ sigmoid_x = sigmoid(x) # FMUL, MUFU.TANH, then FFMA
408
+ return sigmoid_x * y # FMUL
409
+ else:
410
+ sigmoid_x = sigmoid(x)
411
+ return cute.arch.mul_packed_f32x2(sigmoid_x, y)
412
+
413
+
414
+ @dsl_user_op
415
+ def dglu(
416
+ x: F32_or_F32x2, y: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
417
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
418
+ """
419
+ GLU backward pass: computes gradients w.r.t. x (gate) and y (up projection)
420
+ Given: glu_out = sigmoid(x) * y, and dout = grad w.r.t. glu_out
421
+ Returns: (dx, dy, glu_out) where:
422
+ - dx = dout * y * sigmoid(x) * (1 - sigmoid(x))
423
+ - dy = dout * sigmoid(x)
424
+ - glu_out = sigmoid(x) * y
425
+ """
426
+ if const_expr(not isinstance(x, tuple)):
427
+ # Compute sigmoid(x) using tanh: sigmoid(x) = 0.5 * (1 + tanh(x/2))
428
+ sigmoid_x = sigmoid(x) # FMUL, MUFU.TANH, then FFMA
429
+ sigmoid_x_dout = sigmoid_x * dout # FMUL
430
+ glu_out = sigmoid_x * y # FMUL
431
+ # dx = y * sigmoid(x) * (1 - sigmoid(x)) * dout
432
+ # = y * (1 - sigmoid(x)) * sigmoid_x_dout
433
+ # = (y - y * sigmoid(x)) * sigmoid_x_dout
434
+ # = (y - glu_out) * sigmoid_x_dout
435
+ dx = (y - glu_out) * sigmoid_x_dout # FADD, FMUL
436
+ dy = sigmoid_x_dout
437
+ # Total: 1 MUFU.TANH, 4 FMUL, 1 FADD, 1 FFMA
438
+ return dx, dy, glu_out
439
+ else:
440
+ sigmoid_x = sigmoid(x)
441
+ sigmoid_x_dout = cute.arch.mul_packed_f32x2(sigmoid_x, dout)
442
+ glu_out = cute.arch.mul_packed_f32x2(sigmoid_x, y)
443
+ # dx = (y - glu_out) * sigmoid_x_dout
444
+ y_minus_glu_out = sub_packed_f32x2(y, glu_out)
445
+ dx = cute.arch.mul_packed_f32x2(y_minus_glu_out, sigmoid_x_dout)
446
+ dy = sigmoid_x_dout
447
+ return dx, dy, glu_out
448
+
449
+
450
+ @dsl_user_op
451
+ def reglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
452
+ """ReGLU: ReLU Gated Linear Unit
453
+ reglu(x, y) = relu(x) * y = max(x, 0) * y
454
+ """
455
+ if const_expr(not isinstance(x, tuple)):
456
+ return cute.arch.fmax(x, Float32(0.0)) * y
457
+ else:
458
+ relu_x = relu(x)
459
+ return cute.arch.mul_packed_f32x2(relu_x, y)
460
+
461
+
462
+ @dsl_user_op
463
+ @cute.jit
464
+ def dreglu(
465
+ x: F32_or_F32x2, y: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
466
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
467
+ """
468
+ ReGLU backward pass: computes gradients w.r.t. x (gate) and y (up projection)
469
+ Given: reglu_out = relu(x) * y, and dout = grad w.r.t. reglu_out
470
+ Returns: (dx, dy, reglu_out) where:
471
+ - dx = dout * y if x > 0, else 0
472
+ - dy = dout * relu(x)
473
+ - reglu_out = relu(x) * y
474
+ """
475
+ if const_expr(not isinstance(x, tuple)):
476
+ x_pos = Boolean(x > 0)
477
+ relu_x = cute.arch.fmax(x, Float32(0.0))
478
+ dx = (dout * y) if x_pos else Float32(0.0)
479
+ dy = dout * relu_x
480
+ reglu_out = relu_x * y
481
+ return dx, dy, reglu_out
482
+ else:
483
+ x0_pos = Boolean(x[0] > 0)
484
+ x1_pos = Boolean(x[1] > 0)
485
+ relu_x = relu(x)
486
+ dout_y = cute.arch.mul_packed_f32x2(dout, y)
487
+ dx = ((dout_y[0] if x0_pos else Float32(0.0)), (dout_y[1] if x1_pos else Float32(0.0)))
488
+ dy = cute.arch.mul_packed_f32x2(dout, relu_x)
489
+ reglu_out = cute.arch.mul_packed_f32x2(relu_x, y)
490
+ return dx, dy, reglu_out
491
+
492
+
493
+ @dsl_user_op
494
+ def geglu(x: F32_or_F32x2, y: F32_or_F32x2, *, loc=None, ip=None) -> F32_or_F32x2:
495
+ """GeGLU: GELU Gated Linear Unit
496
+ geglu(x, y) = gelu(x) * y
497
+ Uses the tanh approximation of GELU
498
+ """
499
+ if const_expr(not isinstance(x, tuple)):
500
+ return gelu_tanh_approx(x) * y
501
+ else:
502
+ return cute.arch.mul_packed_f32x2(gelu_tanh_approx(x), y)
503
+
504
+
505
+ @dsl_user_op
506
+ def dgeglu(
507
+ x: F32_or_F32x2, y: F32_or_F32x2, dout: F32_or_F32x2, *, loc=None, ip=None
508
+ ) -> Tuple[F32_or_F32x2, F32_or_F32x2, F32_or_F32x2]:
509
+ """
510
+ GeGLU backward pass: computes gradients w.r.t. x (gate) and y (up projection)
511
+ Given: geglu_out = gelu(x) * y, and dout = grad w.r.t. geglu_out
512
+ Returns: (dx, dy, geglu_out) where:
513
+ - dx = dout * y * d_gelu(x)
514
+ - dy = dout * gelu(x)
515
+ - geglu_out = gelu(x) * y
516
+ """
517
+ if const_expr(not isinstance(x, tuple)):
518
+ # Reuse dgelu_tanh_approx to compute d_gelu(x) * dout and gelu(x)
519
+ dgelu_x_dout, gelu_x = dgelu_tanh_approx(x, dout)
520
+ # Compute gradients for geglu
521
+ dx = dgelu_x_dout * y
522
+ dy = gelu_x * dout
523
+ geglu_out = gelu_x * y
524
+ return dx, dy, geglu_out
525
+ else:
526
+ # Reuse dgelu_tanh_approx to compute d_gelu(x) * dout and gelu(x)
527
+ dgelu_x_dout, gelu_x = dgelu_tanh_approx(x, dout)
528
+ # Compute gradients for geglu
529
+ dx = cute.arch.mul_packed_f32x2(dgelu_x_dout, y)
530
+ dy = cute.arch.mul_packed_f32x2(gelu_x, dout)
531
+ geglu_out = cute.arch.mul_packed_f32x2(gelu_x, y)
532
+ return dx, dy, geglu_out
533
+
534
+
535
+ # ============================================================================
536
+ # Activation name -> function maps
537
+ # ============================================================================
538
+
539
+ act_fn_map = {
540
+ None: None,
541
+ "silu": silu,
542
+ "relu": relu,
543
+ "relu_sq": relu_sq,
544
+ "gelu_tanh_approx": gelu_tanh_approx,
545
+ }
546
+
547
+ dact_fn_map = {
548
+ None: None,
549
+ "relu": drelu,
550
+ "relu_sq": drelu_sq,
551
+ "gelu_tanh_approx": dgelu_tanh_approx,
552
+ }
553
+
554
+ gate_fn_map = {
555
+ "swiglu": swiglu,
556
+ "swiglu_oai": swiglu_oai,
557
+ "reglu": reglu,
558
+ "geglu": geglu,
559
+ "glu": glu,
560
+ }
561
+
562
+ dgate_fn_map = {
563
+ "swiglu": dswiglu,
564
+ "swiglu_oai": dswiglu_oai,
565
+ "reglu": dreglu,
566
+ "geglu": dgeglu,
567
+ "glu": dglu,
568
+ }
build/torch-cuda/quack/compile_utils.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+
3
+ from typing import Optional
4
+
5
+ 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))
16
+ )
17
+ return cute.runtime.make_fake_tensor(
18
+ dtype, shape, stride=stride, assumed_align=divisibility * dtype.width // 8
19
+ )
build/torch-cuda/quack/copy_utils.py ADDED
@@ -0,0 +1,1185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+
3
+ from typing import Optional, Type, Tuple, Callable, Sequence
4
+ from functools import partial
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+
9
+ from cutlass import Int32, Int16, Boolean, const_expr
10
+ from cutlass.cute.nvgpu import cpasync, warp, warpgroup
11
+ from cutlass.cute.nvgpu.tcgen05.mma import CtaGroup # noqa
12
+ from cutlass.cutlass_dsl import dsl_user_op
13
+ import cutlass.pipeline
14
+ from cutlass._mlir.dialects import llvm
15
+ from cutlass._mlir import ir
16
+ from cutlass._mlir.dialects import cute_nvgpu as _cute_nvgpu_ir
17
+
18
+ from . import layout_utils
19
+ from .utils import make_vector
20
+
21
+
22
+ Sm100MmaPeerBitMask = 0xFEFFFFFF
23
+
24
+
25
+ @dsl_user_op
26
+ def cvt_copy(
27
+ tiled_copy: cute.TiledCopy,
28
+ src: cute.Tensor,
29
+ dst: cute.Tensor,
30
+ *,
31
+ pred: Optional[cute.Tensor] = None,
32
+ retile: bool = False,
33
+ loc=None,
34
+ ip=None,
35
+ **kwargs,
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)
45
+
46
+
47
+ @dsl_user_op
48
+ def sr_cvt_copy(
49
+ tiled_copy: cute.TiledCopy,
50
+ src: cute.Tensor,
51
+ dst: cute.Tensor,
52
+ seed: Int32,
53
+ tidx: Int32,
54
+ *,
55
+ loc=None,
56
+ ip=None,
57
+ ) -> None:
58
+ """Like cvt_copy but uses stochastic rounding for FP32 -> BF16 conversion."""
59
+ assert isinstance(src.iterator, cute.Pointer) and src.memspace == cute.AddressSpace.rmem
60
+ from .rounding import convert_f32_to_bf16_sr
61
+ from cutlass.cute.tensor import TensorSSA
62
+
63
+ src_cvt = cute.make_rmem_tensor_like(src, dst.element_type)
64
+ src_vec = src.load()
65
+ raw_vec = convert_f32_to_bf16_sr(src_vec, seed, tidx, loc=loc, ip=ip)
66
+ src_cvt.store(TensorSSA(raw_vec, src_vec.shape, dst.element_type))
67
+ src = src_cvt
68
+ cute.copy(tiled_copy, src, dst, loc=loc, ip=ip)
69
+
70
+
71
+ @dsl_user_op
72
+ def load_s2r(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
73
+ dst = cute.make_rmem_tensor_like(src, src.element_type, loc=loc, ip=ip)
74
+ cute.autovec_copy(src, dst, loc=loc, ip=ip)
75
+ return dst
76
+
77
+
78
+ @dsl_user_op
79
+ def load_s2r_retile(
80
+ tiled_copy: cute.TiledCopy,
81
+ src: cute.Tensor,
82
+ dst_shape: cute.Tensor | cute.Shape,
83
+ *,
84
+ loc=None,
85
+ ip=None,
86
+ ) -> cute.Tensor:
87
+ # Will also accept dst_shape being a tensor, in which case we write into that tensor
88
+ if const_expr(not isinstance(dst_shape, cute.Tensor)):
89
+ dst = cute.make_rmem_tensor(dst_shape, src.element_type, loc=loc, ip=ip)
90
+ else:
91
+ dst = dst_shape
92
+ cute.copy(tiled_copy, src, tiled_copy.retile(dst), loc=loc, ip=ip)
93
+ return dst
94
+
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
+
106
+ @dsl_user_op
107
+ def get_copy_atom(
108
+ dtype: Type[cutlass.Numeric], num_copy_elems: int, is_async: bool = False, *, loc=None, ip=None
109
+ ) -> cute.CopyAtom:
110
+ num_copy_bits = const_expr(min(128, num_copy_elems * dtype.width))
111
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
112
+ return cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
113
+
114
+
115
+ @dsl_user_op
116
+ def copy(
117
+ src: cute.Tensor,
118
+ dst: cute.Tensor,
119
+ *,
120
+ pred: Optional[cute.Tensor] = None,
121
+ is_async: bool = False,
122
+ loc=None,
123
+ ip=None,
124
+ **kwargs,
125
+ ) -> None:
126
+ num_copy_elems = src.shape[0][0]
127
+ copy_atom = get_copy_atom(src.element_type, num_copy_elems, is_async)
128
+ cute.copy(copy_atom, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
129
+
130
+
131
+ def tiled_copy_1d(
132
+ dtype: Type[cutlass.Numeric], num_threads: int, num_copy_elems: int = 1, is_async: bool = False
133
+ ) -> cute.TiledCopy:
134
+ num_copy_bits = num_copy_elems * dtype.width
135
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
136
+ copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
137
+ thr_layout = cute.make_layout(num_threads)
138
+ val_layout = cute.make_layout(num_copy_elems)
139
+ return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
140
+
141
+
142
+ def tiled_copy_2d(
143
+ dtype: Type[cutlass.Numeric],
144
+ threads_per_row: int,
145
+ num_threads: int,
146
+ num_copy_elems: int = 1,
147
+ is_async: bool = False,
148
+ ) -> cute.TiledCopy:
149
+ num_copy_bits = num_copy_elems * dtype.width
150
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
151
+ copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
152
+ assert num_threads % threads_per_row == 0
153
+ thr_layout = cute.make_ordered_layout(
154
+ (num_threads // threads_per_row, threads_per_row),
155
+ order=(1, 0),
156
+ )
157
+ val_layout = cute.make_layout((1, num_copy_elems))
158
+ return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
159
+
160
+
161
+ @cute.jit
162
+ def predicate_k(tAcA: cute.Tensor, limit: Int32) -> cute.Tensor:
163
+ # Only compute predicates for the "k" dimension. For the mn dimension, we will use "if"
164
+ tApA = cute.make_rmem_tensor(
165
+ cute.make_layout(
166
+ (cute.size(tAcA, mode=[0, 1]), cute.size(tAcA, mode=[1]), cute.size(tAcA, mode=[2])),
167
+ stride=(cute.size(tAcA, mode=[2]), 0, 1),
168
+ ),
169
+ Boolean,
170
+ )
171
+ for rest_v in cutlass.range_constexpr(tApA.shape[0]):
172
+ for rest_k in cutlass.range_constexpr(tApA.shape[2]):
173
+ tApA[rest_v, 0, rest_k] = cute.elem_less(tAcA[(0, rest_v), 0, rest_k][1], limit)
174
+ return tApA
175
+
176
+
177
+ # def tiled_copy_2d(
178
+ # dtype: Type[cutlass.Numeric], major_mode_size: int, num_threads: int, is_async: bool = False
179
+ # ) -> cute.TiledCopy:
180
+ # num_copy_bits = math.gcd(major_mode_size, 128 // dtype.width) * dtype.width
181
+ # copy_elems = num_copy_bits // dtype.width
182
+ # copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
183
+ # copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
184
+ # gmem_threads_per_row = major_mode_size // copy_elems
185
+ # assert num_threads % gmem_threads_per_row == 0
186
+ # thr_layout = cute.make_ordered_layout(
187
+ # (num_threads // gmem_threads_per_row, gmem_threads_per_row),
188
+ # order=(1, 0),
189
+ # )
190
+ # val_layout = cute.make_layout((1, copy_elems))
191
+ # return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
192
+
193
+
194
+ # Ragged tensor trick for TMA: encodes variable-length sequences into a higher-rank
195
+ # tensor so that TMA's out-of-bounds checking handles sequence boundaries.
196
+ #
197
+ # Given a tensor T with a ragged dimension (variable-length across batches), we create
198
+ # a higher-rank tensor where the ragged dim is replaced with a fixed size `big_int`, and
199
+ # extra dim(s) are appended. When indexing into a specific sequence at (offset, length),
200
+ # `offset_ragged_tensor` computes coordinates such that:
201
+ # ragged_coord = big_int - length (OOB check clamps reads past the sequence end)
202
+ # extra_coord(s) = f(offset, length) (selects the correct memory region)
203
+ #
204
+ # ptr_shift=True: 1-extra-dim approach (adds 1 dim, supports up to 4D input):
205
+ # Shape: (*before, big_int, *after, max_int)
206
+ # Stride: (*original_strides, stride_r) where stride_r = T.stride[ragged_dim]
207
+ # Pointer shifted backward by big_int * stride_r elements.
208
+ # Address for coords (big_int - length) in ragged dim, (offset + length) in extra dim:
209
+ # addr = (base - big_int * s_r) + (big_int - length) * s_r + (offset + length) * s_r
210
+ # = base + offset * s_r [correct]
211
+ # Works for epilogue TMA store. Does NOT work for TMA load with large big_int
212
+ # — the shifted pointer must land in physically mapped GPU memory.
213
+ #
214
+ # ptr_shift=False: 2-extra-dim approach (adds 2 dims, supports up to 3D input):
215
+ # Shape: (*before, big_int, *after, max_int, max_int)
216
+ # Stride: (*before_strides, stride_r, *after_strides, 2^34 - stride_r, stride_r)
217
+ # No pointer shift. Uses 64-bit address wraparound to cancel the ragged offset.
218
+ # Let W = 2^34 - stride_r. Address for coords (big_int - length) in ragged dim,
219
+ # big_int in extra dim 0, (offset + length) in extra dim 1:
220
+ # addr = base + (big_int - length) * s_r + big_int * W + (offset + length) * s_r
221
+ # = base + big_int * (s_r + W) - length * s_r + (offset + length) * s_r
222
+ # = base + big_int * 2^34 + offset * s_r
223
+ # Since big_int = 2^30: big_int * 2^34 = 2^64 ≡ 0 (mod 2^64), so:
224
+ # addr = base + offset * s_r [correct]
225
+ # Works for all TMA paths since the base pointer is never shifted.
226
+ #
227
+ # Ragged tensor was adapted from the implementation from Triton, but here we have an option that
228
+ # only needs 1 extra dimension instead of 2.
229
+ # https://github.com/triton-lang/triton/blob/main/python/triton/tools/ragged_tma.py
230
+ BIG_INT = 2**30
231
+ MAX_INT = 2**31 - 1
232
+ BIG_INT_INV = 2**64 // BIG_INT
233
+
234
+
235
+ @dsl_user_op
236
+ def create_ragged_tensor_for_tma(
237
+ T: cute.Tensor,
238
+ ragged_dim: int = 0,
239
+ ptr_shift: bool = False,
240
+ *,
241
+ loc=None,
242
+ ip=None,
243
+ ) -> cute.Tensor:
244
+ rank = cute.rank(T)
245
+ if ragged_dim < 0:
246
+ ragged_dim += rank
247
+ if ptr_shift:
248
+ assert rank <= 4, "ptr_shift ragged tensor only supports up to 4 dimensions"
249
+ new_shape = T.shape[:ragged_dim] + (BIG_INT,) + T.shape[ragged_dim + 1 :] + (MAX_INT,)
250
+ new_stride = T.stride + (T.stride[ragged_dim],)
251
+ ptr_offset = (None,) * ragged_dim + (-BIG_INT,) + (None,) * (rank - ragged_dim - 1)
252
+ new_ptr = cute.domain_offset(ptr_offset, T).iterator
253
+ return cute.make_tensor(new_ptr, cute.make_layout(new_shape, stride=new_stride))
254
+ else:
255
+ assert rank <= 3, "non-ptr_shift ragged tensor only supports up to 3 dimensions"
256
+ stride_r = T.stride[ragged_dim]
257
+ new_shape = (
258
+ T.shape[:ragged_dim] + (BIG_INT,) + T.shape[ragged_dim + 1 :] + (MAX_INT, MAX_INT)
259
+ )
260
+ new_stride = (
261
+ T.stride[:ragged_dim]
262
+ + (stride_r,)
263
+ + T.stride[ragged_dim + 1 :]
264
+ + (BIG_INT_INV - stride_r, stride_r)
265
+ )
266
+ return cute.make_tensor(T.iterator, cute.make_layout(new_shape, stride=new_stride))
267
+
268
+
269
+ @dsl_user_op
270
+ def offset_ragged_tensor(
271
+ T: cute.Tensor,
272
+ offset: Int32,
273
+ length: Int32,
274
+ ragged_dim: int = 0,
275
+ ptr_shift: bool = False,
276
+ *,
277
+ loc=None,
278
+ ip=None,
279
+ ) -> cute.Tensor:
280
+ rank = cute.rank(T)
281
+ if ragged_dim < 0:
282
+ ragged_dim += rank
283
+ big_int = cute.size(T, mode=[ragged_dim])
284
+ offset_val = big_int - length
285
+ if ptr_shift:
286
+ # 1-extra-dim: rank = original_rank + 1
287
+ assert rank >= ragged_dim + 2
288
+ offset_tuple = (None,) * ragged_dim + (offset_val,) + (None,) * (rank - ragged_dim - 2)
289
+ index_tuple = (None,) * (rank - 1) + (offset + length,)
290
+ else:
291
+ # 2-extra-dim: rank = original_rank + 2, last 2 modes are the wraparound dims
292
+ assert rank >= ragged_dim + 3
293
+ offset_tuple = (None,) * ragged_dim + (offset_val,) + (None,) * (rank - ragged_dim - 3)
294
+ index_tuple = (None,) * (rank - 2) + (big_int, offset + length)
295
+ return cute.domain_offset(offset_tuple, T[index_tuple])
296
+
297
+
298
+ def swizzle_int(ptr_int: Int32, b: int, m: int, s: int) -> Int32:
299
+ bit_msk = (1 << b) - 1
300
+ yyy_msk = bit_msk << (m + s)
301
+ return ptr_int ^ ((ptr_int & yyy_msk) >> s)
302
+
303
+
304
+ def swizzle_ptr(ptr: cute.Pointer):
305
+ swz = ptr.type.swizzle_type
306
+ ptr_int = swizzle_int(ptr.toint(), swz.num_bits, swz.num_base, swz.num_shift)
307
+ return cute.make_ptr(ptr.dtype, ptr_int, ptr.memspace, assumed_align=ptr.alignment)
308
+
309
+
310
+ def as_position_independent_swizzle_tensor(tensor: cute.Tensor) -> cute.Tensor:
311
+ outer = tensor.layout
312
+ width = tensor.element_type.width
313
+ swizzle_type = tensor.iterator.type.swizzle_type
314
+ inner = cute.make_swizzle(swizzle_type.num_bits, swizzle_type.num_base, swizzle_type.num_shift)
315
+ # Need to recast the swizzle from byte (e.g. <3, 4, 3> to element units (e.g. <3, 3, 3> for
316
+ # for 16 bits and <3, 2, 3> for 32 bits)
317
+ new_layout = cute.recast_layout(
318
+ width, 8, cute.make_composed_layout(inner, 0, cute.recast_layout(8, width, outer))
319
+ )
320
+ # recast_ptr to remove the pointer swizzle
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,
339
+ )
340
+
341
+
342
+ @dsl_user_op
343
+ def sm90_get_smem_load_op(
344
+ layout_c: cutlass.utils.LayoutEnum,
345
+ elem_ty_c: Type[cutlass.Numeric],
346
+ *,
347
+ loc=None,
348
+ ip=None,
349
+ ) -> cute.CopyAtom:
350
+ """
351
+ Selects the largest vectorized smem load atom available subject to constraint of gmem layout.
352
+
353
+ Parameters:
354
+ -----------
355
+ layout_c : LayoutEnum
356
+ The layout enum of the output tensor D.
357
+
358
+ elem_ty_c : Type[Numeric]
359
+ The element type for output tensor D.
360
+
361
+ Returns:
362
+ --------
363
+ Either SmemLoadMatrix or SimtSyncCopy, based on the input parameters.
364
+ """
365
+
366
+ if not isinstance(elem_ty_c, cutlass.cutlass_dsl.NumericMeta):
367
+ raise TypeError(f"elem_ty_c must be a Numeric, but got {elem_ty_c}")
368
+ is_m_major = layout_c.is_m_major_c()
369
+ if elem_ty_c.width == 16:
370
+ return cute.make_copy_atom(warp.LdMatrix8x8x16bOp(is_m_major, 4), elem_ty_c, loc=loc, ip=ip)
371
+ else:
372
+ return cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), elem_ty_c, loc=loc, ip=ip)
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,
385
+ num_bits_per_copy=(2 if not transpose else 1) * element_type.width,
386
+ )
387
+ else:
388
+ num_matrices = (
389
+ 4
390
+ if major_mode_size is None or major_mode_size % 16 == 0
391
+ else (2 if major_mode_size % 8 == 0 else 1)
392
+ )
393
+ return cute.make_copy_atom(
394
+ warp.StMatrix8x8x16bOp(transpose=transpose, num_matrices=num_matrices),
395
+ element_type,
396
+ )
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,
409
+ num_bits_per_copy=(2 if not transpose else 1) * element_type.width,
410
+ )
411
+ else:
412
+ num_matrices = (
413
+ 4
414
+ if major_mode_size is None or major_mode_size % 16 == 0
415
+ else (2 if major_mode_size % 8 == 0 else 1)
416
+ )
417
+ return cute.make_copy_atom(
418
+ warp.LdMatrix8x8x16bOp(transpose=transpose, num_matrices=num_matrices),
419
+ element_type,
420
+ )
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
+
447
+
448
+ 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
+
468
+ def copy_fn(src_idx: Optional[Int32] = None, **new_kwargs):
469
+ src_tensor = tSR_sC if const_expr(src_idx is None) else tSR_sC[None, None, None, src_idx]
470
+ return load_s2r_retile(tiled_copy, src_tensor, dst_shape=tRS_shape, **new_kwargs)
471
+
472
+ return copy_fn, thr_copy, tSR_sC
473
+
474
+
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
+
485
+
486
+ def get_smem_store_epi(
487
+ tiled_mma: cute.TiledMma,
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
501
+ if const_expr(sC is not None):
502
+ if const_expr(not position_independent):
503
+ tRS_sC = thr_copy.partition_D(sC)
504
+ else:
505
+ tRS_sC = partition_D_position_independent(thr_copy, sC)
506
+ sC_shape = sC.shape[:2] if sC is not None else epi_tile
507
+ # (R2S, R2S_M, R2S_N, PIPE_C)
508
+ tRS_rC_shape = thr_copy.partition_S(cute.make_identity_tensor(sC_shape)).shape
509
+ tRS_rC = cute.make_rmem_tensor(tRS_rC_shape, tiled_mma.op.acc_dtype)
510
+
511
+ def copy_fn(src: cute.Tensor, dst_idx: Int32, **new_kwargs):
512
+ cvt_copy(tiled_copy, src, tRS_sC[None, None, None, dst_idx], **new_kwargs)
513
+
514
+ return copy_fn if const_expr(sC is not None) else None, thr_copy, tRS_sC, tRS_rC
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):
526
+ tRS_sA = thr_copy.partition_D(sA)
527
+ else:
528
+ tRS_sA = partition_D_position_independent(thr_copy, sA)
529
+
530
+ def copy_fn(src: cute.Tensor, dst_idx: Int32, **new_kwargs):
531
+ cvt_copy(tiled_copy, src, tRS_sA[None, None, None, dst_idx], retile=True, **new_kwargs)
532
+
533
+ return copy_fn, thr_copy, tRS_sA
534
+
535
+
536
+ 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):
550
+ tSR_sA = thr_copy.partition_S(sA)
551
+ else:
552
+ tSR_sA = partition_S_position_independent(thr_copy, sA)
553
+ tRS_shape = tiled_mma.partition_shape_A(sA.shape[:2])
554
+
555
+ def copy_fn(src_idx: Int32, **new_kwargs):
556
+ return load_s2r_retile(
557
+ tiled_copy, tSR_sA[None, None, None, src_idx], dst_shape=tRS_shape, **new_kwargs
558
+ )
559
+
560
+ def copy_fn_w_dst_tensor(src_idx: Int32, dst: cute.Tensor, **new_kwargs):
561
+ return load_s2r_retile(tiled_copy, tSR_sA[None, None, None, src_idx], dst, **new_kwargs)
562
+
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,
569
+ gmem_ptr: cute.Pointer,
570
+ store_bytes: int | Int32,
571
+ *,
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
+ asm_dialect=llvm.AsmDialect.AD_ATT,
588
+ )
589
+
590
+
591
+ @dsl_user_op
592
+ def get_tma_desc_addr(tma_atom: cute.CopyAtom, *, loc=None, ip=None) -> cute.Pointer:
593
+ """
594
+ Get the address of the TMA descriptor embedded in a TMA Copy Atom.
595
+
596
+ Extracts the constant memory address of the TMA descriptor for use with
597
+ custom PTX instructions.
598
+
599
+ :param tma_atom: TMA Copy Atom from make_tiled_tma_atom
600
+ :return: Pointer to TMA descriptor in constant memory
601
+
602
+ Example:
603
+ >>> desc_ptr = get_tma_descriptor_address(tma_atom)
604
+ """
605
+ exec_atom = _cute_nvgpu_ir.atom_make_exec_tma(tma_atom._trait.value, loc=loc, ip=ip)
606
+ tma_desc_ptr_type = ir.Type.parse(
607
+ "!cute.ptr<!cute_nvgpu.tma_descriptor_tiled, generic, align<128>>"
608
+ )
609
+ return _cute_nvgpu_ir.get_tma_desc_addr(tma_desc_ptr_type, exec_atom, loc=loc, ip=ip)
610
+
611
+
612
+ @dsl_user_op
613
+ def tma_gather4_load(
614
+ tma_desc_ptr: cute.Pointer,
615
+ dst_smem_ptr: cute.Pointer,
616
+ mbarrier_ptr: cute.Pointer,
617
+ col_idx: Int32,
618
+ row_indices: Sequence[Int32],
619
+ *,
620
+ num_cta: int = 1,
621
+ multicast_mask=None,
622
+ loc=None,
623
+ ip=None,
624
+ ) -> None:
625
+ """
626
+ Perform TMA gather4 load from global memory to shared memory.
627
+
628
+ Issues PTX instruction:
629
+ cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes
630
+ [dstMem], [tensorMap, {col_idx, row0, row1, row2, row3}], [smem_bar];
631
+
632
+ This loads 4 rows (specified by row_indices) from a 2D tensor at the given
633
+ column index into shared memory, using the TMA descriptor.
634
+
635
+ :param tma_desc_ptr: Pointer to TMA descriptor in constant memory (128-byte aligned)
636
+ :type tma_desc_ptr: Pointer
637
+ :param dst_smem_ptr: Destination address in shared memory
638
+ :type dst_smem_ptr: Pointer
639
+ :param mbarrier_ptr: Pointer to mbarrier in shared memory for completion tracking
640
+ :type mbarrier_ptr: Pointer
641
+ :param col_idx: Column index
642
+ :type col_idx: Int32
643
+ :param row_indices: Sequence of exactly 4 row indices
644
+ :type row_indices: Sequence[Int32]
645
+ :param num_cta: Number of CTAs participating (default: 1)
646
+ :type num_cta: int
647
+ :param multicast_mask: Optional multicast mask
648
+ :type multicast_mask: Int16
649
+
650
+ Requirements:
651
+ - row_indices must contain exactly 4 elements
652
+ - Compute capability >= SM_100 (Blackwell)
653
+ - TMA descriptor must be properly initialized for 2D tensor
654
+
655
+ Example:
656
+ >>> from cutlass.cute.nvgpu import cpasync
657
+ >>> from cutlass.cute import core
658
+ >>>
659
+ >>> # Create TMA descriptor
660
+ >>> tma_atom, tma_tensor = cpasync.make_tiled_tma_atom(...)
661
+ >>> tma_desc_ptr = get_tma_descriptor_address(tma_atom)
662
+ >>>
663
+ >>> # Compute indices (typically from kernel logic)
664
+ >>> col_idx = core.get(...) or 5 # Int32 value
665
+ >>> row_indices = [core.get(...) for _ in range(4)] # 4 Int32 values
666
+ >>>
667
+ >>> # Gather 4 rows at computed column
668
+ >>> tma_gather4_load(
669
+ ... tma_desc_ptr=tma_desc_ptr,
670
+ ... dst_smem_ptr=smem_ptr,
671
+ ... mbarrier_ptr=barrier_ptr,
672
+ ... col_idx=col_idx,
673
+ ... row_indices=row_indices
674
+ ... )
675
+ """
676
+ if len(row_indices) != 4:
677
+ raise ValueError(f"gather4 requires exactly 4 row indices, got {len(row_indices)}")
678
+ col_val = Int32(col_idx).ir_value()
679
+ row_vals = [Int32(row_idx).ir_value() for row_idx in row_indices]
680
+ # Convert pointers to integer addresses
681
+ desc_addr = tma_desc_ptr.toint(loc=loc, ip=ip).ir_value()
682
+ dst_addr = dst_smem_ptr.toint(loc=loc, ip=ip).ir_value()
683
+ mbar_addr = mbarrier_ptr.toint(loc=loc, ip=ip)
684
+ if num_cta > 1:
685
+ # Executed by both CTAs. Set peer bit to 0 so that the
686
+ # transaction bytes will update CTA0's barrier.
687
+ mbar_addr = mbar_addr & Sm100MmaPeerBitMask
688
+ mbar_addr = mbar_addr.ir_value()
689
+ # Handle multicast_mask - may already be ir.Value or Python int
690
+ multicast_mask_val = None
691
+ if multicast_mask is not None:
692
+ multicast_mask_val = Int16(multicast_mask).ir_value()
693
+ assert multicast_mask_val is None, "multicast is not supported yet"
694
+ # Emit inline PTX for TMA gather4
695
+ # PTX: cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes
696
+ # [dstMem], [tensorMap, {col, row0, row1, row2, row3}], [smem_bar];
697
+ ptx = (
698
+ f"cp.async.bulk.tensor.2d.shared::cta.global.tile::gather4.mbarrier::complete_tx::bytes.cta_group::{num_cta} "
699
+ "[$0], [$1, {$2, $3, $4, $5, $6}], [$7];"
700
+ )
701
+
702
+ llvm.inline_asm(
703
+ None,
704
+ [
705
+ dst_addr,
706
+ desc_addr,
707
+ col_val,
708
+ row_vals[0],
709
+ row_vals[1],
710
+ row_vals[2],
711
+ row_vals[3],
712
+ mbar_addr,
713
+ ],
714
+ ptx,
715
+ "r,l,r,r,r,r,r,r", # constraints: register, long, 6x register
716
+ has_side_effects=True,
717
+ is_align_stack=False,
718
+ asm_dialect=llvm.AsmDialect.AD_ATT,
719
+ loc=loc,
720
+ ip=ip,
721
+ )
722
+
723
+
724
+ def cpasync_bulk_get_copy_fn(
725
+ src_tensor: cute.Tensor,
726
+ dst_tensor: cute.Tensor,
727
+ single_stage: bool = False,
728
+ **kwargs,
729
+ ) -> Callable:
730
+ group_rank_src = const_expr(cute.rank(src_tensor) - (1 if not single_stage else 0))
731
+ group_rank_dst = const_expr(cute.rank(dst_tensor) - (1 if not single_stage else 0))
732
+ # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
733
+ src = cute.group_modes(src_tensor, 0, group_rank_src)
734
+ dst = cute.group_modes(dst_tensor, 0, group_rank_dst)
735
+
736
+ def copy_bulk(src_idx, dst_idx, tma_bar_ptr: cute.Pointer, **new_kwargs):
737
+ atom = cute.make_copy_atom(cpasync.CopyBulkG2SOp(), src.element_type)
738
+ with cute.arch.elect_one():
739
+ cute.copy(
740
+ atom,
741
+ src[None, src_idx],
742
+ dst[None, dst_idx],
743
+ mbar_ptr=tma_bar_ptr,
744
+ **new_kwargs,
745
+ **kwargs,
746
+ )
747
+
748
+ def copy_bulk_single_stage(tma_bar_ptr: cute.Pointer, **new_kwargs):
749
+ atom = cute.make_copy_atom(cpasync.CopyBulkG2SOp(), src.element_type)
750
+ with cute.arch.elect_one():
751
+ cute.copy(atom, src, dst, mbar_ptr=tma_bar_ptr, **new_kwargs, **kwargs)
752
+
753
+ return copy_bulk if const_expr(not single_stage) else copy_bulk_single_stage
754
+
755
+
756
+ @dsl_user_op
757
+ def tma_get_copy_fn(
758
+ atom: cute.CopyAtom,
759
+ cta_coord: cute.Coord,
760
+ cta_layout: cute.Layout,
761
+ src_tensor: cute.Tensor,
762
+ dst_tensor: cute.Tensor,
763
+ filter_zeros: bool = False,
764
+ single_stage: bool = False,
765
+ *,
766
+ loc=None,
767
+ ip=None,
768
+ **kwargs,
769
+ ) -> Callable:
770
+ src_is_smem = const_expr(
771
+ isinstance(src_tensor.iterator, cute.Pointer)
772
+ and src_tensor.memspace == cute.AddressSpace.smem
773
+ )
774
+ smem_tensor, gmem_tensor = (src_tensor, dst_tensor) if src_is_smem else (dst_tensor, src_tensor)
775
+ group_rank_smem = const_expr(cute.rank(smem_tensor) - (1 if not single_stage else 0))
776
+ group_rank_gmem = const_expr(cute.rank(gmem_tensor) - (1 if not single_stage else 0))
777
+ # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
778
+ s, g = cpasync.tma_partition(
779
+ atom,
780
+ cta_coord,
781
+ cta_layout,
782
+ cute.group_modes(smem_tensor, 0, group_rank_smem),
783
+ cute.group_modes(gmem_tensor, 0, group_rank_gmem),
784
+ loc=loc,
785
+ ip=ip,
786
+ )
787
+ if const_expr(filter_zeros):
788
+ s = cute.filter_zeros(s)
789
+ g = cute.filter_zeros(g)
790
+ src, dst = (s, g) if src_is_smem else (g, s)
791
+
792
+ @dsl_user_op
793
+ def copy_tma(src_idx, dst_idx, *, loc=None, ip=None, **new_kwargs):
794
+ cute.copy(
795
+ atom, src[None, src_idx], dst[None, dst_idx], **new_kwargs, **kwargs, loc=loc, ip=ip
796
+ )
797
+
798
+ @dsl_user_op
799
+ def copy_tma_single_stage(*, loc=None, ip=None, **new_kwargs):
800
+ cute.copy(atom, src, dst, **new_kwargs, **kwargs, loc=loc, ip=ip)
801
+
802
+ return (copy_tma if const_expr(not single_stage) else copy_tma_single_stage), s, g
803
+
804
+
805
+ def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsync):
806
+ def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
807
+ copy(
808
+ src_idx=src_idx,
809
+ dst_idx=producer_state.index,
810
+ tma_bar_ptr=pipeline.producer_get_barrier(producer_state),
811
+ **new_kwargs,
812
+ )
813
+
814
+ return copy_fn
815
+
816
+
817
+ @cute.jit
818
+ def gather_m_get_copy_fn(
819
+ thr_copy_A: cute.ThrCopy,
820
+ mA: cute.Tensor, # (whatever, K)
821
+ sA: cute.Tensor, # (tile_M, tile_K, STAGE)
822
+ gsAIdx: cute.Tensor, # (tile_M), either gmem or smem
823
+ limit_m: Int32,
824
+ limit_k: Int32,
825
+ ) -> Callable:
826
+ tile_M, tile_K = cute.size(sA, mode=[0]), cute.size(sA, mode=[1])
827
+ tAsA = partition_D_position_independent(thr_copy_A, sA)
828
+ # k-major
829
+ assert tAsA.shape[2] == 1
830
+ tAsA = cute.group_modes(cute.slice_(tAsA, (None, None, 0, None)), 0, 2)
831
+
832
+ is_even_m_smem = tile_M % thr_copy_A.tiler_mn[0].shape == 0
833
+ if const_expr(not is_even_m_smem):
834
+ limit_m = min(limit_m, tile_M)
835
+ elems_per_load = cute.size(tAsA.shape[0][0])
836
+ cA = cute.make_identity_tensor((tile_M, tile_K))
837
+ tAcA = thr_copy_A.partition_S(cA)
838
+ t0AcA = thr_copy_A.get_slice(0).partition_S(cA)
839
+ # Instead of comparing tAcA to limit_m, we instead compare t0AcA to limit_m - tAcA[0][0]
840
+ # since we know that tAcA[m][0] = t0AcA[m][0] + tAcA[0][0].
841
+ # This is so that when we do the comparison, t0AcA is known at compile time.
842
+ limit_m = limit_m - tAcA[0][0]
843
+ limit_k = limit_k - tAcA[0][1]
844
+ # Read and cache indices for A
845
+ rows_per_thread = const_expr(cute.size(tAcA.shape, mode=[1]))
846
+ cols_per_thread = const_expr(cute.size(tAcA.shape, mode=[2]))
847
+ tApA_m = cute.make_rmem_tensor(rows_per_thread, Boolean)
848
+ for m in cutlass.range(rows_per_thread, unroll_full=True):
849
+ tApA_m[m] = t0AcA[0, m, 0][0] < limit_m
850
+ m_idx = cute.make_rmem_tensor(rows_per_thread, Int32)
851
+ for m in cutlass.range(rows_per_thread, unroll_full=True):
852
+ row_idx = tAcA[0, m, 0][0]
853
+ if tApA_m[m]:
854
+ m_idx[m] = gsAIdx[row_idx]
855
+ else:
856
+ m_idx[m] = 0 # It's ok to load row 0 in the case of OOB
857
+
858
+ mA_k = cute.logical_divide(mA, (None, tile_K))
859
+
860
+ def copy_fn(src_idx, dst_idx, pred: bool = False):
861
+ tApA_k = None
862
+ if const_expr(pred):
863
+ tApA_k = cute.make_rmem_tensor(cols_per_thread, Boolean)
864
+ limit_k_cur = limit_k - src_idx * tile_K
865
+ for k in cutlass.range(cols_per_thread, unroll_full=True):
866
+ tApA_k[k] = t0AcA[0, 0, k][1] < limit_k_cur
867
+ mA_cur = mA_k[None, (None, src_idx)]
868
+ for m in cutlass.range_constexpr(tAcA.shape[1]):
869
+ # cute.tiled_divide(mA_cur[m_idx[m], None], (elems_per_load,)) would give shape
870
+ # ((elems_per_load), thread_per_row)
871
+ # But we actually want shape ((elems_per_load, 1), thread_per_row) to match tAsA
872
+ # So we append 1s to the last dimension and then do tiled_divide, then slice.
873
+ mA_row = cute.tiled_divide(
874
+ cute.append_ones(mA_cur[m_idx[m], None], up_to_rank=2), (elems_per_load, 1)
875
+ )[None, None, 0]
876
+ if const_expr(is_even_m_smem) or tApA_m[m]:
877
+ # There's only 1 load per row
878
+ assert cute.size(tAcA.shape, mode=[2]) == 1
879
+ ki = tAcA[0, 0, 0][1] // elems_per_load
880
+ cute.copy(thr_copy_A, mA_row[None, ki], tAsA[(None, m), dst_idx], pred=tApA_k)
881
+
882
+ return copy_fn
883
+
884
+
885
+ @cute.jit
886
+ def gather_k_get_copy_fn(
887
+ thr_copy_A: cute.ThrCopy,
888
+ mA: cute.Tensor, # (tile_M, whatever)
889
+ sA: cute.Tensor, # (tile_M, tile_K, STAGE)
890
+ gsAIdx: cute.Tensor, # (tile_K, RestK), either gmem or smem
891
+ limit_m: Int32,
892
+ limit_k: Int32,
893
+ ) -> Callable:
894
+ gAIdx, sAIdx = None, None
895
+ if const_expr(gsAIdx.memspace == cute.AddressSpace.gmem):
896
+ gAIdx = gsAIdx
897
+ else:
898
+ assert gsAIdx.memspace == cute.AddressSpace.smem
899
+ sAIdx = gsAIdx
900
+ tile_shape_mk = (cute.size(sA, mode=[0]), cute.size(sA, mode=[1]))
901
+ # (atom_v, CPY_M, 1, STAGE)
902
+ tAsA = thr_copy_A.partition_D(sA)
903
+ # m-major
904
+ tAsA = cute.group_modes(tAsA, 0, 3)
905
+
906
+ is_even_m_smem = tile_shape_mk[0] % thr_copy_A.tiler_mn[0].shape == 0
907
+ if const_expr(not is_even_m_smem):
908
+ limit_m = min(limit_m, tile_shape_mk[0])
909
+ elems_per_load = cute.size(tAsA.shape[0][0])
910
+ cA = cute.make_identity_tensor(tile_shape_mk)
911
+ tAcA = thr_copy_A.partition_S(cA)
912
+ t0AcA = thr_copy_A.get_slice(0).partition_S(cA)
913
+ # Instead of comparing tAcA to limit_m, we instead compare t0AcA to limit_m - tAcA[0][0]
914
+ # since we know that tAcA[m][0] = t0AcA[m][0] + tAcA[0][0].
915
+ # This is so that when we do the comparison, t0AcA is known at compile time.
916
+ limit_m = limit_m - tAcA[0][0]
917
+ limit_k = limit_k - tAcA[0][1]
918
+ # Read and cache indices for A
919
+ rows_per_thread = const_expr(cute.size(tAcA.shape, mode=[1]))
920
+ cols_per_thread = const_expr(cute.size(tAcA.shape, mode=[2]))
921
+ tApA_m = cute.make_rmem_tensor(rows_per_thread, Boolean)
922
+ for m in cutlass.range(rows_per_thread, unroll_full=True):
923
+ tApA_m[m] = t0AcA[0, m, 0][0] < limit_m
924
+ threads_per_col = const_expr(thr_copy_A.tiler_mn[0].shape // elems_per_load)
925
+ # This is very convoluted but idk a better way
926
+ # for tile_M=128, flat_divide gives (8, 16, K),
927
+ # then logical_divide gives ((8, 1), (8, 2), K).
928
+ tidx = thr_copy_A.thr_idx
929
+ tAmA = cute.logical_divide(
930
+ cute.flat_divide(mA, (elems_per_load,)), (elems_per_load, threads_per_col)
931
+ )[None, (tidx % threads_per_col, None), None] # ((8, 1), 2, K)
932
+
933
+ def prefetch_from_gmem_fn(src_idx, pred: bool = False) -> Tuple[cute.Tensor, cute.Tensor]:
934
+ # Prefetch mAIdx early, even before smem is free
935
+ tApA_k = None
936
+ if const_expr(pred):
937
+ tApA_k = cute.make_rmem_tensor(cols_per_thread, Boolean)
938
+ limit_k_cur = limit_k - src_idx * tile_shape_mk[1]
939
+ for k in cutlass.range(cols_per_thread, unroll_full=True):
940
+ tApA_k[k] = t0AcA[0, 0, k][1] < limit_k_cur
941
+ gAIdx_cur = gAIdx[None, src_idx]
942
+ k_idx = cute.make_rmem_tensor(cols_per_thread, Int32)
943
+ for k in cutlass.range(cols_per_thread):
944
+ col_idx = tAcA[0, 0, k][1]
945
+ if const_expr(not pred):
946
+ k_idx[k] = gAIdx_cur[col_idx]
947
+ else:
948
+ if tApA_k[k]:
949
+ k_idx[k] = gAIdx_cur[col_idx]
950
+ else:
951
+ k_idx[k] = -1
952
+ return k_idx, tApA_k
953
+
954
+ def prefetch_from_smem_fn(
955
+ a_prefetch_pipeline, src_idx, dst_idx, a_prefetch_consumer_state, pred: bool = False
956
+ ) -> Tuple[cute.Tensor, cute.Tensor]:
957
+ tApA_k = None
958
+ if const_expr(pred):
959
+ tApA_k = cute.make_rmem_tensor(cols_per_thread, Boolean)
960
+ limit_k_cur = limit_k - src_idx * tile_shape_mk[1]
961
+ for k in cutlass.range(cols_per_thread, unroll_full=True):
962
+ tApA_k[k] = t0AcA[0, 0, k][1] < limit_k_cur
963
+ a_prefetch_pipeline.consumer_wait(a_prefetch_consumer_state)
964
+ sAIdx_cur = sAIdx[None, dst_idx]
965
+ k_idx = cute.make_rmem_tensor(cols_per_thread, Int32)
966
+ for k in cutlass.range(cols_per_thread):
967
+ col_idx = tAcA[0, 0, k][1]
968
+ k_idx[k] = sAIdx_cur[col_idx]
969
+ cute.arch.sync_warp()
970
+ with cute.arch.elect_one():
971
+ a_prefetch_pipeline.consumer_release(a_prefetch_consumer_state)
972
+ return k_idx, tApA_k
973
+
974
+ def copy_fn(
975
+ src_idx, dst_idx, k_idx_tApA_k: Tuple[cute.Tensor, cute.Tensor], pred: bool = False
976
+ ):
977
+ k_idx, tApA_k = k_idx_tApA_k
978
+ tApA_k_pred = None
979
+ if const_expr(pred):
980
+ tApA_k_pred = cute.prepend_ones(tApA_k, up_to_rank=2) # (1, cols_per_thread)
981
+ for k in cutlass.range_constexpr(tAcA.shape[2]):
982
+ # copy_A(tAmA[None, None, k_idx[k]], tAsA[(None, None, k), smem_idx], pred=cute.prepend_ones(tApA_m, up_to_rank=2))
983
+ for m in cutlass.range_constexpr(tAcA.shape[1]):
984
+ if tApA_m[m]:
985
+ cute.copy(
986
+ thr_copy_A,
987
+ tAmA[None, m, k_idx[k]],
988
+ tAsA[(None, m, k), dst_idx],
989
+ pred=None if const_expr(tApA_k_pred is None) else tApA_k_pred[None, k],
990
+ )
991
+
992
+ return copy_fn, prefetch_from_gmem_fn if const_expr(
993
+ gAIdx is not None
994
+ ) else prefetch_from_smem_fn
995
+
996
+
997
+ @cute.jit
998
+ def gather_m_get_tma_copy_fn(
999
+ tma_atom: cute.CopyAtom,
1000
+ mA: cute.Tensor, # (whatever, K)
1001
+ sA: cute.Tensor, # ((4, 32), (64, 1), STAGE)
1002
+ sAIdx: cute.Tensor, # (tile_M),
1003
+ warp_idx: Int32,
1004
+ num_warps: int,
1005
+ num_cta: int = 1,
1006
+ ) -> Callable:
1007
+ tile_M = cute.size(sAIdx, mode=[0])
1008
+ tile_K = cute.size(sA[None, None, 0]) // tile_M
1009
+ assert tile_M % 4 == 0
1010
+ # cta_group = 1 if tma_atom.op.cta_group == CtaGroup.ONE else 2
1011
+ cta_group = num_cta # Somehow all tma_atom has CtaGroup.ONE inside the kernel
1012
+
1013
+ copy_AIdx_s2r = cute.make_tiled_copy_tv(
1014
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Int32, num_bits_per_copy=128),
1015
+ cute.make_layout(num_warps), # thr_layout
1016
+ cute.make_layout(4), # val_layout
1017
+ )
1018
+ warp_copy_AIdx_s2r = copy_AIdx_s2r.get_slice(warp_idx)
1019
+ tSR_sAIdx = warp_copy_AIdx_s2r.partition_S(sAIdx)
1020
+ # ((4, 1), 8, (64, 1), STAGE)
1021
+ tSR_sA = warp_copy_AIdx_s2r.partition_S(sA)
1022
+ tSR_rAIdx = load_s2r(tSR_sAIdx)
1023
+ tma_desc_ptr = get_tma_desc_addr(tma_atom)
1024
+ tma_gather4_load_fn = partial(tma_gather4_load, tma_desc_ptr, num_cta=cta_group)
1025
+
1026
+ def copy_fn(src_idx, dst_idx, tma_bar_ptr: cute.Pointer):
1027
+ tSR_sA_cur = tSR_sA[None, None, None, dst_idx]
1028
+ col_idx = tile_K * src_idx
1029
+ for m in cutlass.range(cute.size(tSR_rAIdx, mode=[1]), unroll_full=True):
1030
+ row_indices = [tSR_rAIdx[v, m] for v in range(4)]
1031
+ smem_ptr = tSR_sA_cur[None, m, None].iterator
1032
+ with cute.arch.elect_one():
1033
+ tma_gather4_load_fn(smem_ptr, tma_bar_ptr, col_idx, row_indices)
1034
+
1035
+ return copy_fn
1036
+
1037
+
1038
+ @cute.jit
1039
+ def gather_k_get_tma_copy_fn(
1040
+ tma_atom: cute.CopyAtom,
1041
+ sA: cute.Tensor, # ((4, tile_K/4), (tile_M,), STAGE) — K-grouped load layout
1042
+ sAIdx: cute.Tensor, # (tile_K, a_prefetch_stage) — K indices in smem
1043
+ col_idx: Int32, # M offset in global tensor (contiguous dim for M-major)
1044
+ warp_idx: Int32,
1045
+ num_warps: int,
1046
+ num_cta: int = 1,
1047
+ ) -> Tuple[Callable, Callable]:
1048
+ """Build a copy function for TMA gather4 in K dimension (M-major A).
1049
+
1050
+ Each gather4 instruction loads 4 K-columns × tile_M contiguous M-elements.
1051
+ col_idx is the absolute M position in the global tensor.
1052
+ K indices come from sAIdx (prefetched to smem by the scheduler warp).
1053
+
1054
+ Returns copy_fn(src_idx, dst_idx, tma_bar_ptr) which:
1055
+ Issues gather4 calls with those K indices as row_indices
1056
+ """
1057
+ tile_K = cute.size(sAIdx, mode=[0])
1058
+ assert tile_K % 4 == 0
1059
+ cta_group = num_cta
1060
+
1061
+ # Tiled copy for loading K indices from smem to registers (4 per vector, across warps)
1062
+ copy_AIdx_s2r = cute.make_tiled_copy_tv(
1063
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Int32, num_bits_per_copy=128),
1064
+ cute.make_layout(num_warps), # thr_layout
1065
+ cute.make_layout(4), # val_layout — 4 K indices per gather4
1066
+ )
1067
+ warp_idx = cute.arch.make_warp_uniform(warp_idx)
1068
+ warp_copy_AIdx_s2r = copy_AIdx_s2r.get_slice(warp_idx)
1069
+ tSR_sAIdx = warp_copy_AIdx_s2r.partition_S(sAIdx) # (((4,1),4,4))
1070
+ # ((4,1),4,(64,2),(1,4)):((64,0),1024,(1,4096),(0,8192))
1071
+ tSR_sA = warp_copy_AIdx_s2r.partition_S(layout_utils.transpose_view(sA))
1072
+ tma_desc_ptr = get_tma_desc_addr(tma_atom)
1073
+ tma_gather4_load_fn = partial(tma_gather4_load, tma_desc_ptr, num_cta=cta_group)
1074
+
1075
+ def prefetch_from_smem_fn(
1076
+ a_prefetch_pipeline,
1077
+ src_idx,
1078
+ dst_idx,
1079
+ a_prefetch_consumer_state,
1080
+ ) -> cute.Tensor:
1081
+ a_prefetch_pipeline.consumer_wait(a_prefetch_consumer_state)
1082
+ tSR_rAIdx = load_s2r(tSR_sAIdx[None, None, dst_idx])
1083
+ cute.arch.sync_warp()
1084
+ with cute.arch.elect_one():
1085
+ a_prefetch_pipeline.consumer_release(a_prefetch_consumer_state)
1086
+ return tSR_rAIdx
1087
+
1088
+ def copy_fn(src_idx, dst_idx, tSR_rAIdx, tma_bar_ptr: cute.Pointer):
1089
+ # Issue gather4: col_idx = M position, row_indices = 4 K positions
1090
+ tSR_sA_cur = tSR_sA[None, None, None, dst_idx]
1091
+ gather_dim = cute.size(tSR_sA_cur, mode=[2, 0]) # Typically 64
1092
+ for k in cutlass.range(cute.size(tSR_rAIdx, mode=[1]), unroll_full=True):
1093
+ row_indices = [tSR_rAIdx[v, k] for v in range(4)]
1094
+ for m in cutlass.range(cute.size(tSR_sA_cur, mode=[2, 1]), unroll_full=True):
1095
+ smem_ptr = tSR_sA_cur[None, k, (None, m)].iterator
1096
+ with cute.arch.elect_one():
1097
+ tma_gather4_load_fn(
1098
+ smem_ptr, tma_bar_ptr, col_idx + m * gather_dim, row_indices
1099
+ )
1100
+
1101
+ return copy_fn, prefetch_from_smem_fn
1102
+
1103
+
1104
+ # ---------------------------------------------------------------------------
1105
+ # Store helpers
1106
+ # ---------------------------------------------------------------------------
1107
+
1108
+
1109
+ @dsl_user_op
1110
+ @cute.jit
1111
+ def store(
1112
+ ptr: cute.Pointer,
1113
+ val,
1114
+ pred: Optional[Boolean] = None,
1115
+ cop: cutlass.Constexpr = None,
1116
+ *,
1117
+ loc=None,
1118
+ ip=None,
1119
+ ):
1120
+ """Store a scalar value via cute.arch.store.
1121
+
1122
+ ptr: cute.Pointer (any address space).
1123
+ val: DSL Numeric value.
1124
+ pred: None → unconditional. DSL Boolean → skipped when pred == 0.
1125
+ cop: Cache operator — "wb" (default), "cg", "cs" (streaming), "wt".
1126
+ """
1127
+ if const_expr(pred is None):
1128
+ cute.arch.store(ptr.llvm_ptr, type(val)(val), cop=cop, loc=loc, ip=ip)
1129
+ else:
1130
+ if pred:
1131
+ cute.arch.store(ptr.llvm_ptr, type(val)(val), cop=cop, loc=loc, ip=ip)
1132
+
1133
+
1134
+ @dsl_user_op
1135
+ @cute.jit
1136
+ def store_v2(
1137
+ ptr: cute.Pointer,
1138
+ v0,
1139
+ v1,
1140
+ pred: Optional[Boolean] = None,
1141
+ cop: cutlass.Constexpr = None,
1142
+ *,
1143
+ loc=None,
1144
+ ip=None,
1145
+ ):
1146
+ """Vectorized store of 2 elements via cute.arch.store.
1147
+
1148
+ Packs v0, v1 into an MLIR <2 x T> vector.
1149
+ ptr: cute.Pointer (any address space, must be aligned for vector width).
1150
+ cop: Cache operator — "wb" (default), "cg", "cs" (streaming), "wt".
1151
+ """
1152
+ vec = make_vector(type(v0), v0, v1, loc=loc, ip=ip)
1153
+ if const_expr(pred is None):
1154
+ cute.arch.store(ptr.llvm_ptr, vec, cop=cop, loc=loc, ip=ip)
1155
+ else:
1156
+ if pred:
1157
+ cute.arch.store(ptr.llvm_ptr, vec, cop=cop, loc=loc, ip=ip)
1158
+
1159
+
1160
+ @dsl_user_op
1161
+ @cute.jit
1162
+ def store_v4(
1163
+ ptr: cute.Pointer,
1164
+ v0,
1165
+ v1,
1166
+ v2,
1167
+ v3,
1168
+ pred: Optional[Boolean] = None,
1169
+ cop: cutlass.Constexpr = None,
1170
+ *,
1171
+ loc=None,
1172
+ ip=None,
1173
+ ):
1174
+ """Vectorized store of 4 elements via cute.arch.store.
1175
+
1176
+ Packs v0–v3 into an MLIR <4 x T> vector.
1177
+ ptr: cute.Pointer (any address space, must be aligned for vector width).
1178
+ cop: Cache operator — "wb" (default), "cg", "cs" (streaming), "wt".
1179
+ """
1180
+ vec = make_vector(type(v0), v0, v1, v2, v3, loc=loc, ip=ip)
1181
+ if const_expr(pred is None):
1182
+ cute.arch.store(ptr.llvm_ptr, vec, cop=cop, loc=loc, ip=ip)
1183
+ else:
1184
+ if pred:
1185
+ cute.arch.store(ptr.llvm_ptr, vec, cop=cop, loc=loc, ip=ip)
build/torch-cuda/quack/cute_dsl_utils.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Tuple, get_origin
4
+ from functools import lru_cache
5
+ from dataclasses import dataclass, fields
6
+
7
+ import os
8
+ import re
9
+
10
+ import torch
11
+
12
+ try:
13
+ from triton.tools.disasm import extract
14
+ except ImportError:
15
+ extract = None
16
+
17
+ import cutlass
18
+ import cutlass.cute as cute
19
+ from cutlass import Int32, Int64, Float16, BFloat16, Float32
20
+ from cutlass.base_dsl.tvm_ffi_builder import spec
21
+ from cutlass.cutlass_dsl import NumericMeta
22
+
23
+
24
+ StaticTypes = (cutlass.Constexpr, NumericMeta, int, bool, str, float, type(None))
25
+
26
+
27
+ load_cubin_module_data_og = cutlass.base_dsl.runtime.cuda.load_cubin_module_data
28
+ cute_compile_og = cute.compile
29
+
30
+
31
+ # Patch TVM-FFI converter to handle Constexpr type annotations as compile-time constants.
32
+ # Fields annotated with cutlass.Constexpr[T] are emitted as ConstNone (not runtime args).
33
+ # At call time, pass None for these fields; the compile-time value is baked in.
34
+ import cutlass.cute._tvm_ffi_args_spec_converter as _converter_module # noqa
35
+
36
+ _original_convert_single_arg = _converter_module._convert_single_arg
37
+
38
+
39
+ def _patched_convert_single_arg(arg, arg_name, arg_type, ctx):
40
+ if arg_type is not None and get_origin(arg_type) is cutlass.Constexpr:
41
+ return spec.ConstNone(arg_name)
42
+ # If arg is a NamedTuple but arg_type doesn't have _fields (e.g. annotated as tuple),
43
+ # redirect so the converter uses the NamedTuple's own type hints.
44
+ if (
45
+ isinstance(arg, tuple)
46
+ and hasattr(type(arg), "_fields")
47
+ and (arg_type is None or not hasattr(arg_type, "_fields"))
48
+ ):
49
+ return _original_convert_single_arg(arg, arg_name, type(arg), ctx)
50
+ return _original_convert_single_arg(arg, arg_name, arg_type, ctx)
51
+
52
+
53
+ _converter_module._convert_single_arg = _patched_convert_single_arg
54
+
55
+
56
+ torch2cute_dtype_map = {
57
+ torch.float16: Float16,
58
+ torch.bfloat16: BFloat16,
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]:
71
+ """Parse arch string (e.g. 'sm_90', 'sm90', '90', 'sm_100a') to (major, minor) tuple."""
72
+ match = re.match(r"^(?:sm_?)?(\d+)(\d)([af]?)$", arch_str.strip(), re.IGNORECASE)
73
+ if not match:
74
+ raise ValueError(f"Invalid QUACK_ARCH format: {arch_str!r} (expected e.g. '90', 'sm_90')")
75
+ major, minor, _ = match.groups()
76
+ return int(major), int(minor)
77
+
78
+
79
+ @lru_cache
80
+ def get_device_capacity(device: torch.device = None) -> Tuple[int, int]:
81
+ """Return (major, minor) device capability.
82
+
83
+ Override with QUACK_ARCH (e.g. 'sm_90' or '90') for CPU-only compilation
84
+ without a GPU present.
85
+ """
86
+ arch_override = os.environ.get("QUACK_ARCH")
87
+ if arch_override is not None:
88
+ return _parse_arch_str(arch_override)
89
+ return torch.cuda.get_device_capability(device)
90
+
91
+
92
+ def _partition_fields(obj):
93
+ """Split dataclass fields into (constexpr_dict, non_constexpr_dict) by type."""
94
+ all_fields = {field.name: getattr(obj, field.name) for field in fields(obj)}
95
+ constexpr = {n: f for n, f in all_fields.items() if isinstance(f, StaticTypes)}
96
+ non_constexpr = {n: f for n, f in all_fields.items() if not isinstance(f, StaticTypes)}
97
+ return constexpr, non_constexpr
98
+
99
+
100
+ def _new_from_mlir_values(self, values):
101
+ constexpr_fields, non_constexpr_fields = _partition_fields(self)
102
+ for (name, field), n_items in zip(non_constexpr_fields.items(), self._values_pos):
103
+ non_constexpr_fields[name] = cutlass.new_from_mlir_values(field, values[:n_items])
104
+ values = values[n_items:]
105
+ return self.__class__(**non_constexpr_fields, **constexpr_fields)
106
+
107
+
108
+ def _namedtuple_new_from_mlir_values(self, values):
109
+ """Generic __new_from_mlir_values__ for NamedTuples.
110
+
111
+ Applied to NamedTuple classes via the ``@mlir_namedtuple`` decorator.
112
+
113
+ Fields that are None or Constexpr (StaticTypes) are preserved from ``self`` (the compile-time
114
+ template). Only non-static fields consume MLIR values. Multi-value fields (e.g. cute.Tensor)
115
+ consume the correct number of values via ``cutlass.new_from_mlir_values``.
116
+
117
+ Constexpr fields (annotated ``cutlass.Constexpr[T]``) are baked into the compiled kernel via
118
+ a converter patch (see above). At call time, pass None for these fields.
119
+ """
120
+ from cutlass.base_dsl.typing import get_mlir_types
121
+
122
+ values = list(values)
123
+ new_fields = []
124
+ for field_val in self:
125
+ if field_val is None or isinstance(field_val, StaticTypes):
126
+ new_fields.append(field_val)
127
+ else:
128
+ n_items = len(get_mlir_types(field_val))
129
+ new_fields.append(cutlass.new_from_mlir_values(field_val, values[:n_items]))
130
+ values = values[n_items:]
131
+ return self.__class__(*new_fields)
132
+
133
+
134
+ def mlir_namedtuple(cls):
135
+ """Decorator that adds MLIR value reconstruction to a NamedTuple class.
136
+
137
+ Usage::
138
+
139
+ @mlir_namedtuple
140
+ class MyArgs(NamedTuple):
141
+ tensor_arg: cute.Tensor
142
+ const_arg: cutlass.Constexpr[int] = 0
143
+ """
144
+ cls.__new_from_mlir_values__ = _namedtuple_new_from_mlir_values
145
+ return cls
146
+
147
+
148
+ @dataclass
149
+ class ParamsBase:
150
+ def __extract_mlir_values__(self):
151
+ _, non_constexpr_fields = _partition_fields(self)
152
+ values, self._values_pos = [], []
153
+ for obj in non_constexpr_fields.values():
154
+ obj_values = cutlass.extract_mlir_values(obj)
155
+ values += obj_values
156
+ self._values_pos.append(len(obj_values))
157
+ return values
158
+
159
+ __new_from_mlir_values__ = _new_from_mlir_values
build/torch-cuda/quack/layout_utils.py ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+
7
+ from cutlass import Int32, const_expr
8
+
9
+
10
+ def transpose_view(a: cute.Tensor) -> cute.Tensor:
11
+ """Transpose the first two dimensions of a tensor on smem."""
12
+ shape = (a.shape[1], a.shape[0], *a.shape[2:])
13
+ order = (1, 0, *range(2, cute.rank(a)))
14
+ return cute.composition(a, cute.make_ordered_layout(shape, order=order))
15
+
16
+
17
+ def select(a: cute.Tensor, mode: list[int]) -> cute.Tensor:
18
+ return cute.make_tensor(a.iterator, cute.select(a.layout, mode))
19
+
20
+
21
+ def expand(a: cute.Tensor, dim: int, size: Int32 | int) -> cute.Tensor:
22
+ shape = (*a.shape[:dim], size, *a.shape[dim:])
23
+ stride = (*a.layout.stride[:dim], 0, *a.layout.stride[dim:])
24
+ return cute.make_tensor(a.iterator, cute.make_layout(shape, stride=stride))
25
+
26
+
27
+ @cute.jit
28
+ def permute_gated_Cregs_b16(t: cute.Tensor) -> None:
29
+ assert t.element_type.width == 16
30
+ assert cute.size(t.shape) % 4 == 0, "Tensor size must be a multiple of 4 for b16 permutation"
31
+ t_u32 = cute.recast_tensor(t, Int32)
32
+
33
+ quad_idx = cute.arch.lane_idx() % 4
34
+ lane_03 = quad_idx == 0 or quad_idx == 3
35
+ selector_upper = Int32(0x5410) if lane_03 else Int32(0x1054)
36
+ selector_lower = Int32(0x7632) if lane_03 else Int32(0x3276)
37
+ # upper_map = [0, 3, 1, 2]
38
+ # lower_map = [1, 2, 0, 3]
39
+ # upper_idx = upper_map[quad_idx]
40
+ # indexing isn't supported so we have to do arithmetic
41
+ upper_idx = quad_idx // 2 if quad_idx % 2 == 0 else 3 - quad_idx // 2
42
+ lower_idx = upper_idx ^ 1
43
+
44
+ # 1 -> 0b11111, 2 -> 0b11110, 4 -> 0b11100, 8 -> 0b11000, 16 -> 0b10000, 32 -> 0b00000
45
+ width = 4
46
+ mask = cute.arch.WARP_SIZE - width
47
+ clamp = cute.arch.WARP_SIZE - 1
48
+ mask_and_clamp = mask << 8 | clamp
49
+
50
+ for i in cutlass.range(cute.size(t_u32.shape) // 2, unroll_full=True):
51
+ upper, lower = t_u32[i * 2 + 0], t_u32[i * 2 + 1]
52
+ upper0 = upper if lane_03 else lower
53
+ lower0 = lower if lane_03 else upper
54
+ upper0 = cute.arch.shuffle_sync(upper0, offset=upper_idx, mask_and_clamp=mask_and_clamp)
55
+ lower0 = cute.arch.shuffle_sync(lower0, offset=lower_idx, mask_and_clamp=mask_and_clamp)
56
+ t_u32[i * 2 + 0] = cute.arch.prmt(upper0, lower0, selector_upper)
57
+ t_u32[i * 2 + 1] = cute.arch.prmt(upper0, lower0, selector_lower)
58
+
59
+
60
+ @cute.jit
61
+ def permute_Cregs_b32_for_stsm(t: cute.Tensor) -> None:
62
+ """Permute and shuffle within 4 threads to change the layout from
63
+ T0 | T1 | T2 | T3
64
+ a b | c d | e f | g h
65
+ to
66
+ T0 | T1 | T2 | T3 | T0 | T1 | T2 | T3
67
+ a | b | c | d | e | f | g | h
68
+ This is so that we can use STSM (instead of STS.64) to store C registers without bank conflict.
69
+ """
70
+
71
+ assert t.element_type.width == 32
72
+ assert cute.size(t.shape) % 4 == 0, "Tensor size must be a multiple of 4 for b32 permutation"
73
+
74
+ quad_idx = cute.arch.lane_idx() % 4
75
+ # left_map = [0, 2, 1, 3]
76
+ # right_map = [2, 0, 3, 1]
77
+ # indexing isn't supported so we have to do arithmetic
78
+ left_idx = quad_idx // 2 if quad_idx % 2 == 0 else 2 + quad_idx // 2
79
+ right_idx = left_idx ^ 0b10
80
+
81
+ # 1 -> 0b11111, 2 -> 0b11110, 4 -> 0b11100, 8 -> 0b11000, 16 -> 0b10000, 32 -> 0b00000
82
+ width = 4
83
+ mask = cute.arch.WARP_SIZE - width
84
+ clamp = cute.arch.WARP_SIZE - 1
85
+ mask_and_clamp = mask << 8 | clamp
86
+
87
+ for i in cutlass.range(cute.size(t.shape) // 4, unroll_full=True):
88
+ for r in cutlass.range(2, unroll_full=True):
89
+ left, right = t[i * 4 + r * 2 + 0], t[i * 4 + r * 2 + 1]
90
+ # a b | c d | e f | g h -> a b | c d | f e | h g
91
+ left0 = left if quad_idx < 2 else right
92
+ right0 = right if quad_idx < 2 else left
93
+ # a b | c d | f e | h g -> a b | f d | c e | h g
94
+ left0 = cute.arch.shuffle_sync(left0, offset=left_idx, mask_and_clamp=mask_and_clamp)
95
+ # a b | f d | c e | h g -> a e | f b | c g | h d
96
+ right0 = cute.arch.shuffle_sync(right0, offset=right_idx, mask_and_clamp=mask_and_clamp)
97
+ # a e | f b | c g | h d -> a e | b f | c g | d h
98
+ t[i * 4 + r * 2 + 0] = left0 if quad_idx % 2 == 0 else right0
99
+ t[i * 4 + r * 2 + 1] = right0 if quad_idx % 2 == 0 else left0
100
+ t[i * 4 + 1], t[i * 4 + 2] = t[i * 4 + 2], t[i * 4 + 1]
101
+
102
+
103
+ @cute.jit
104
+ def permute_Cregs_b32_for_ldsm(t: cute.Tensor) -> None:
105
+ """Permute and shuffle within 4 threads to change the layout from
106
+ T0 | T1 | T2 | T3 | T0 | T1 | T2 | T3
107
+ a | b | c | d | e | f | g | h
108
+ to
109
+ T0 | T1 | T2 | T3
110
+ a b | c d | e f | g h
111
+ This is so that we can use LDSM (instead of LDS.64) to store C registers without bank conflict.
112
+ """
113
+
114
+ assert t.element_type.width == 32
115
+ assert cute.size(t.shape) % 4 == 0, "Tensor size must be a multiple of 4 for b32 permutation"
116
+
117
+ quad_idx = cute.arch.lane_idx() % 4
118
+ # left_map = [0, 2, 1, 3]
119
+ # right_map = [1, 3, 0, 2]
120
+ # indexing isn't supported so we have to do arithmetic
121
+ left_idx = quad_idx // 2 if quad_idx % 2 == 0 else 2 + quad_idx // 2
122
+ right_idx = left_idx ^ 0b01
123
+
124
+ # 1 -> 0b11111, 2 -> 0b11110, 4 -> 0b11100, 8 -> 0b11000, 16 -> 0b10000, 32 -> 0b00000
125
+ width = 4
126
+ mask = cute.arch.WARP_SIZE - width
127
+ clamp = cute.arch.WARP_SIZE - 1
128
+ mask_and_clamp = mask << 8 | clamp
129
+
130
+ # This is just the inverse of permute_Cregs_b32_for_stsm
131
+ for i in cutlass.range(cute.size(t.shape) // 4, unroll_full=True):
132
+ t[i * 4 + 1], t[i * 4 + 2] = t[i * 4 + 2], t[i * 4 + 1]
133
+ for r in cutlass.range(2, unroll_full=True):
134
+ left, right = t[i * 4 + r * 2 + 0], t[i * 4 + r * 2 + 1]
135
+ # a e | b f | c g | d h -> a e | f b | c g | h d
136
+ left0 = left if quad_idx % 2 == 0 else right
137
+ right0 = right if quad_idx % 2 == 0 else left
138
+ # a e | f b | c g | h d -> a b | f d | c e | h g
139
+ right0 = cute.arch.shuffle_sync(right0, offset=right_idx, mask_and_clamp=mask_and_clamp)
140
+ # a b | f d | c e | h g -> a b | c d | f e | h g
141
+ left0 = cute.arch.shuffle_sync(left0, offset=left_idx, mask_and_clamp=mask_and_clamp)
142
+ # a b | c d | f e | h g -> a b | c d | e f | g h
143
+ t[i * 4 + r * 2 + 0] = left0 if quad_idx < 2 else right0
144
+ t[i * 4 + r * 2 + 1] = right0 if quad_idx < 2 else left0
145
+
146
+
147
+ @cute.jit
148
+ def concat_layout(*layouts: cute.Layout) -> cute.Layout:
149
+ return cute.make_layout(
150
+ tuple(l.shape for l in layouts),
151
+ stride=tuple(l.stride for l in layouts),
152
+ )
153
+
154
+
155
+ def convert_layout_acc_mn(acc_layout: cute.Layout, transpose: bool = False) -> cute.Layout:
156
+ """
157
+ For Sm80, convert ((2, 2), MMA_M, MMA_N, ...) to ((2, MMA_M), (2, MMA_N), ...).
158
+ For Sm90, convert ((2, 2, V), MMA_M, MMA_N, ...) to ((2, MMA_M), (2, V, MMA_N), ...).
159
+ """
160
+ acc_layout_col_major = cute.make_layout(acc_layout.shape)
161
+ shape = (
162
+ (acc_layout_col_major.shape[0][1], acc_layout_col_major.shape[1]), # MMA_M
163
+ (
164
+ acc_layout_col_major.shape[0][0],
165
+ *acc_layout_col_major.shape[0][2:],
166
+ acc_layout_col_major.shape[2],
167
+ ), # MMA_N
168
+ *acc_layout_col_major.shape[3:],
169
+ )
170
+ stride = (
171
+ (acc_layout_col_major.stride[0][1], acc_layout_col_major.stride[1]), # MMA_M
172
+ (
173
+ acc_layout_col_major.stride[0][0],
174
+ *acc_layout_col_major.stride[0][2:],
175
+ acc_layout_col_major.stride[2],
176
+ ), # MMA_N
177
+ *acc_layout_col_major.stride[3:],
178
+ )
179
+ if const_expr(transpose):
180
+ shape = (shape[1], shape[0], *shape[2:])
181
+ stride = (stride[1], stride[0], *stride[2:])
182
+ acc_layout_mn = cute.make_layout(shape, stride=stride)
183
+ return cute.composition(acc_layout, acc_layout_mn)
184
+
185
+
186
+ def make_acc_tensor_mn_view(acc: cute.Tensor, transpose: bool = False) -> cute.Tensor:
187
+ return cute.make_tensor(acc.iterator, convert_layout_acc_mn(acc.layout, transpose=transpose))
188
+
189
+
190
+ def reshape_acc_to_mn(acc: cute.Tensor, transpose: bool = False) -> cute.Tensor:
191
+ return cute.make_tensor(acc.iterator, convert_layout_acc_mn(acc.layout, transpose=transpose))
192
+
193
+
194
+ @cute.jit
195
+ def convert_layout_acc_frgA(acc_layout: cute.Layout) -> cute.Layout:
196
+ # For back to back gemm, convert layout of acc0 to gemm 1 accept layout.
197
+ # For Sm80, as the mma instruction shape is 16x8x16, we need to convert from (4, MMA_M, MMA_N) to ((4, 2), MMA_M, MMA_N / 2)
198
+ # For Sm90, FP16/BF16, convert acc_layout from ((2, 2, N / 8), MMA_M, MMA_N) to ((2, 2, 2), MMA_M, (N / 16, MMA_N))
199
+ # If N / 8 is odd, we'll convert to ((2, 2, 1), MMA_M, N / 8, MMA_N).
200
+ # TODO: Sm90 FP8
201
+ if const_expr(cute.rank(acc_layout.shape[0]) == 3): # Sm90
202
+ div = 2 if const_expr(acc_layout.shape[0][2] % 2 == 0) else 1
203
+ l = cute.logical_divide(
204
+ acc_layout, ((None, None, div), None, None)
205
+ ) # ((2, 2, (2, N / 16)), MMA_M, MMA_N)
206
+ rA_mma_view = cute.make_layout(
207
+ (
208
+ (l.shape[0][0], l.shape[0][1], l.shape[0][2][0]),
209
+ l.shape[1],
210
+ (l.shape[0][2][1], l.shape[2]),
211
+ ),
212
+ stride=(
213
+ (l.stride[0][0], l.stride[0][1], l.stride[0][2][0]),
214
+ l.stride[1],
215
+ (l.stride[0][2][1], l.stride[2]),
216
+ ),
217
+ )
218
+ else: # Sm80
219
+ # (4, MMA_M, MMA_N) -> (4, MMA_M, (2, MMA_N / 2))
220
+ l = cute.logical_divide(acc_layout, (None, None, 2))
221
+ rA_mma_view = cute.make_layout(
222
+ (
223
+ (l.shape[0], l.shape[2][0]),
224
+ l.shape[1],
225
+ l.shape[2][1],
226
+ ),
227
+ stride=(
228
+ (l.stride[0], l.stride[2][0]),
229
+ l.stride[1],
230
+ l.stride[2][1],
231
+ ),
232
+ )
233
+ return rA_mma_view
234
+
235
+
236
+ def reshape_acc_to_frgA(acc: cute.Tensor) -> cute.Tensor:
237
+ return cute.make_tensor(acc.iterator, convert_layout_acc_frgA(acc.layout))
238
+
239
+
240
+ def convert_layout_zero_stride(
241
+ input: cute.Tensor | cute.Layout, ref_layout: cute.Layout
242
+ ) -> cute.Layout:
243
+ layout = input.layout if const_expr(isinstance(input, cute.Tensor)) else input
244
+ # Group the modes with non-zero stride in the ref_layout together,
245
+ # and the modes with zero stride together
246
+ layout_flat = cute.flatten(layout)
247
+ ref_layout_flat = cute.flatten(ref_layout)
248
+ nonzero_modes = [i for i in range(cute.rank(layout_flat)) if ref_layout_flat[i].stride != 0]
249
+ zero_modes = [i for i in range(cute.rank(layout_flat)) if ref_layout_flat[i].stride == 0]
250
+ # There's an edge case when all modes are zero stride
251
+ new_shape = (
252
+ tuple(layout_flat[i].shape for i in nonzero_modes) if len(nonzero_modes) > 0 else (1,),
253
+ tuple(layout_flat[i].shape for i in zero_modes),
254
+ )
255
+ new_stride = (
256
+ tuple(layout_flat[i].stride for i in nonzero_modes) if len(nonzero_modes) > 0 else (0,),
257
+ tuple(layout_flat[i].stride for i in zero_modes),
258
+ )
259
+ out_layout = cute.make_layout(new_shape, stride=new_stride)
260
+ if const_expr(isinstance(input, cute.Tensor)):
261
+ return cute.make_tensor(input.iterator, out_layout)
262
+ else:
263
+ return out_layout
264
+
265
+
266
+ def mma_partition_C_vec(
267
+ sVec: cute.Tensor, thr_mma: cute.core.ThrMma, expand_shape: int, is_colvec: bool
268
+ ) -> cute.Tensor:
269
+ assert cute.rank(sVec) == 2
270
+ assert sVec.stride[0] == 1
271
+ stage = sVec.shape[1]
272
+ shape = (
273
+ (sVec.shape[0], expand_shape, stage)
274
+ if const_expr(is_colvec)
275
+ else (expand_shape, sVec.shape[0], stage)
276
+ )
277
+ stride = (1, 0, sVec.stride[1]) if const_expr(is_colvec) else (0, 1, sVec.stride[1])
278
+ sVec_mma = cute.make_tensor(sVec.iterator, cute.make_layout(shape, stride=stride))
279
+ tC_sVec = make_acc_tensor_mn_view(thr_mma.partition_C(sVec_mma))
280
+ return tC_sVec[None, 0, None] if const_expr(is_colvec) else tC_sVec[0, None, None]
281
+
282
+
283
+ def mma_partition_A_vec(
284
+ sVec: cute.Tensor, thr_mma: cute.core.ThrMma, expand_shape: int, is_colvec: bool
285
+ ) -> cute.Tensor:
286
+ assert cute.rank(sVec) == 2
287
+ assert sVec.stride[0] == 1
288
+ stage = sVec.shape[1]
289
+ shape = (
290
+ (sVec.shape[0], expand_shape, stage)
291
+ if const_expr(is_colvec)
292
+ else (expand_shape, sVec.shape[0], stage)
293
+ )
294
+ stride = (1, 0, sVec.stride[1]) if const_expr(is_colvec) else (0, 1, sVec.stride[1])
295
+ sVec_mma = cute.make_tensor(sVec.iterator, cute.make_layout(shape, stride=stride))
296
+ tC_sVec = make_acc_tensor_mn_view(thr_mma.partition_A(sVec_mma))
297
+ return tC_sVec[None, 0, None] if const_expr(is_colvec) else tC_sVec[0, None, None]
298
+
299
+
300
+ def copy_partition_S_vec(
301
+ sVec: cute.Tensor, thr_copy: cute.core.ThrCopy, expand_shape: int, is_colvec: bool
302
+ ) -> cute.Tensor:
303
+ assert cute.rank(sVec) == 2
304
+ assert sVec.stride[0] == 1
305
+ stage = sVec.shape[1]
306
+ shape = (
307
+ (sVec.shape[0], expand_shape, stage)
308
+ if const_expr(is_colvec)
309
+ else (expand_shape, sVec.shape[0], stage)
310
+ )
311
+ stride = (1, 0, sVec.stride[1]) if const_expr(is_colvec) else (0, 1, sVec.stride[1])
312
+ sVec_thr = cute.make_tensor(sVec.iterator, cute.make_layout(shape, stride=stride))
313
+ tC_sVec = reshape_acc_to_mn(thr_copy.partition_S(sVec_thr))
314
+ return tC_sVec[None, 0, None] if const_expr(is_colvec) else tC_sVec[0, None, None]
315
+
316
+
317
+ def copy_partition_D_vec(
318
+ sVec: cute.Tensor, thr_copy: cute.core.ThrCopy, expand_shape: int, is_colvec: bool
319
+ ) -> cute.Tensor:
320
+ assert cute.rank(sVec) == 2
321
+ assert sVec.stride[0] == 1
322
+ stage = sVec.shape[1]
323
+ shape = (
324
+ (sVec.shape[0], expand_shape, stage)
325
+ if const_expr(is_colvec)
326
+ else (expand_shape, sVec.shape[0], stage)
327
+ )
328
+ stride = (1, 0, sVec.stride[1]) if const_expr(is_colvec) else (0, 1, sVec.stride[1])
329
+ sVec_thr = cute.make_tensor(sVec.iterator, cute.make_layout(shape, stride=stride))
330
+ tC_sVec = reshape_acc_to_mn(thr_copy.partition_D(sVec_thr))
331
+ return tC_sVec[None, 0, None] if const_expr(is_colvec) else tC_sVec[0, None, None]
build/torch-cuda/quack/sm90_utils.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Type, Union, Optional
4
+
5
+ import cutlass
6
+ import cutlass.cute as cute
7
+ import cutlass.utils.hopper_helpers as sm90_utils_og
8
+ from cutlass.cute.nvgpu import warpgroup
9
+ from cutlass.cutlass_dsl import Numeric, dsl_user_op
10
+ from cutlass import Float32, Int32, Boolean, const_expr
11
+ from cutlass.utils import LayoutEnum
12
+
13
+
14
+ @dsl_user_op
15
+ def make_smem_layout(
16
+ dtype: Type[Numeric],
17
+ layout: LayoutEnum,
18
+ tile: cute.Tile,
19
+ stage: Optional[int] = None,
20
+ major_mode_size: Optional[int] = None,
21
+ *,
22
+ loc=None,
23
+ ip=None,
24
+ ) -> Union[cute.Layout, cute.ComposedLayout]:
25
+ shape = cute.product_each(cute.shape(tile, loc=loc, ip=ip), loc=loc, ip=ip)
26
+ if const_expr(major_mode_size is None):
27
+ major_mode_size = shape[1] if layout.is_n_major_c() else shape[0]
28
+ smem_layout_atom = warpgroup.make_smem_layout_atom(
29
+ sm90_utils_og.get_smem_layout_atom(layout, dtype, major_mode_size),
30
+ dtype,
31
+ )
32
+ order = (1, 0, 2) if const_expr(layout.is_m_major_c()) else (0, 1, 2)
33
+ smem_layout_staged = cute.tile_to_shape(
34
+ smem_layout_atom,
35
+ cute.append(shape, stage) if const_expr(stage is not None) else shape,
36
+ order=order if const_expr(stage is not None) else order[:2],
37
+ )
38
+ return smem_layout_staged
39
+
40
+
41
+ # For compatibility with blackwell_helpers.py
42
+ make_smem_layout_epi = make_smem_layout
43
+
44
+
45
+ @dsl_user_op
46
+ def partition_for_epilogue(
47
+ cT: cute.Tensor,
48
+ epi_tile: cute.Tile,
49
+ tiled_copy: cute.TiledCopy,
50
+ tidx: Int32,
51
+ reference_src: bool, # do register tensors reference the src or dst layout of the tiled copy
52
+ *,
53
+ loc=None,
54
+ ip=None,
55
+ ) -> cute.Tensor:
56
+ thr_copy = tiled_copy.get_slice(tidx)
57
+ cT_epi = cute.flat_divide(cT, epi_tile)
58
+ # (CPY, CPY_M, CPY_N, EPI_M, EPI_N)
59
+ if const_expr(reference_src):
60
+ return thr_copy.partition_S(cT_epi, loc=loc, ip=ip)
61
+ else:
62
+ return thr_copy.partition_D(cT_epi, loc=loc, ip=ip)
63
+
64
+
65
+ @cute.jit
66
+ def gemm(
67
+ tiled_mma: cute.TiledMma,
68
+ acc: cute.Tensor,
69
+ tCrA: cute.Tensor,
70
+ tCrB: cute.Tensor,
71
+ zero_init: cutlass.Constexpr[bool] = False,
72
+ wg_wait: cutlass.Constexpr[int] = 0,
73
+ # A_in_regs: cutlass.Constexpr[bool] = False,
74
+ swap_AB: cutlass.Constexpr[bool] = False,
75
+ ) -> None:
76
+ if const_expr(swap_AB):
77
+ gemm(tiled_mma, acc, tCrB, tCrA, zero_init=zero_init, wg_wait=wg_wait, swap_AB=False)
78
+ else:
79
+ warpgroup.fence()
80
+ # We make a new mma_atom since we'll be modifying its attribute (accumulate).
81
+ # Otherwise the compiler complains "operand #0 does not dominate this use"
82
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
83
+ mma_atom.set(warpgroup.Field.ACCUMULATE, not zero_init)
84
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
85
+ cute.gemm(mma_atom, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
86
+ mma_atom.set(warpgroup.Field.ACCUMULATE, True)
87
+ warpgroup.commit_group()
88
+ if const_expr(wg_wait >= 0):
89
+ warpgroup.wait_group(wg_wait)
90
+
91
+
92
+ def gemm_zero_init(
93
+ tiled_mma: cute.TiledMma,
94
+ shape: cute.Shape,
95
+ tCrA: cute.Tensor,
96
+ tCrB: cute.Tensor,
97
+ A_idx: Optional[Int32] = None,
98
+ B_idx: Optional[Int32] = None,
99
+ wg_wait: int = -1,
100
+ swap_AB: bool = False,
101
+ ) -> cute.Tensor:
102
+ if const_expr(swap_AB):
103
+ return gemm_zero_init(
104
+ tiled_mma, shape[::-1], tCrB, tCrA, B_idx, A_idx, wg_wait, swap_AB=False
105
+ )
106
+ else:
107
+ acc = cute.make_rmem_tensor(tiled_mma.partition_shape_C(shape), Float32)
108
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
109
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
110
+ gemm(tiled_mma, acc, rA, rB, zero_init=True, wg_wait=wg_wait)
111
+ return acc
112
+
113
+
114
+ def gemm_w_idx(
115
+ tiled_mma: cute.TiledMma,
116
+ acc: cute.Tensor,
117
+ tCrA: cute.Tensor,
118
+ tCrB: cute.Tensor,
119
+ zero_init: Boolean,
120
+ A_idx: Optional[Int32] = None,
121
+ B_idx: Optional[Int32] = None,
122
+ wg_wait: int = -1,
123
+ swap_AB: bool = False,
124
+ ) -> None:
125
+ if const_expr(swap_AB):
126
+ gemm_w_idx(tiled_mma, acc, tCrB, tCrA, zero_init, B_idx, A_idx, wg_wait, swap_AB=False)
127
+ else:
128
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
129
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
130
+ gemm(tiled_mma, acc, rA, rB, zero_init=zero_init, wg_wait=wg_wait)
131
+
132
+
133
+ def partition_fragment_ABC(
134
+ thr_mma: cute.ThrMma,
135
+ shape_mnk: cute.Shape,
136
+ sA: Optional[cute.Tensor],
137
+ sB: Optional[cute.Tensor],
138
+ swap_AB: bool = False,
139
+ ):
140
+ is_rs = thr_mma.op.a_src == warpgroup.OperandSource.RMEM
141
+ if const_expr(not swap_AB):
142
+ acc = cute.make_rmem_tensor(thr_mma.partition_shape_C(shape_mnk[:2]), Float32)
143
+ if const_expr(not is_rs):
144
+ assert sA is not None
145
+ tCrA = thr_mma.make_fragment_A(thr_mma.partition_A(sA))
146
+ else:
147
+ tCrA = thr_mma.make_fragment_A(thr_mma.partition_shape_A((shape_mnk[0], shape_mnk[2])))
148
+ assert sB is not None
149
+ tCrB = thr_mma.make_fragment_B(thr_mma.partition_B(sB))
150
+ else:
151
+ acc = cute.make_rmem_tensor(
152
+ thr_mma.partition_shape_C((shape_mnk[1], shape_mnk[0])), Float32
153
+ )
154
+ if const_expr(not is_rs):
155
+ assert sB is not None
156
+ tCrB = thr_mma.make_fragment_A(thr_mma.partition_A(sB))
157
+ else: # B in rmem
158
+ tCrB = thr_mma.make_fragment_A(thr_mma.partition_shape_A((shape_mnk[1], shape_mnk[2])))
159
+ assert sA is not None
160
+ tCrA = thr_mma.make_fragment_B(thr_mma.partition_B(sA))
161
+ return acc, tCrA, tCrB
build/torch-cuda/quack/utils.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+
3
+ import math
4
+ from typing import Optional, Tuple, Union
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+
9
+ from cutlass import Float32, Int32, const_expr
10
+ from cutlass._mlir.dialects import arith as _arith
11
+ from cutlass._mlir.dialects import llvm, nvvm, vector
12
+ from cutlass.cutlass_dsl import T, dsl_user_op
13
+
14
+
15
+ @dsl_user_op
16
+ def elem_pointer(x: cute.Tensor, coord: cute.Coord, *, loc=None, ip=None) -> cute.Pointer:
17
+ return x.iterator + cute.crd2idx(coord, x.layout, loc=loc, ip=ip)
18
+
19
+
20
+ @cute.jit
21
+ def load_scalar_or_pointer(x, dtype=Float32):
22
+ if const_expr(isinstance(x, cute.Pointer)):
23
+ return dtype(cute.make_tensor(x, cute.make_layout(1))[0])
24
+ else:
25
+ return x
26
+
27
+
28
+ @dsl_user_op
29
+ def set_block_rank(
30
+ smem_ptr: cute.Pointer, peer_cta_rank_in_cluster: Int32, *, loc=None, ip=None
31
+ ) -> Int32:
32
+ """Map the given smem pointer to the address at another CTA rank in the cluster."""
33
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
34
+ return Int32(
35
+ llvm.inline_asm(
36
+ T.i32(),
37
+ [smem_ptr_i32, peer_cta_rank_in_cluster.ir_value()],
38
+ "mapa.shared::cluster.u32 $0, $1, $2;",
39
+ "=r,r,r",
40
+ has_side_effects=False,
41
+ is_align_stack=False,
42
+ asm_dialect=llvm.AsmDialect.AD_ATT,
43
+ )
44
+ )
45
+
46
+
47
+ @dsl_user_op
48
+ def store_shared_remote(
49
+ val: float | Float32 | Int32 | cutlass.Int64,
50
+ smem_ptr: cute.Pointer,
51
+ mbar_ptr: cute.Pointer,
52
+ peer_cta_rank_in_cluster: cute.typing.Int,
53
+ *,
54
+ loc=None,
55
+ ip=None,
56
+ ) -> None:
57
+ remote_smem_ptr_i32 = set_block_rank(
58
+ smem_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
59
+ ).ir_value()
60
+ remote_mbar_ptr_i32 = set_block_rank(
61
+ mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
62
+ ).ir_value()
63
+ if const_expr(isinstance(val, float)):
64
+ val = Float32(val)
65
+ assert isinstance(val, (Float32, Int32, cutlass.Int64)), "val must be Float32, Int32, or Int64"
66
+ suffix = {Float32: "f32", Int32: "s32", cutlass.Int64: "s64"}[type(val)]
67
+ constraint = {Float32: "f", Int32: "r", cutlass.Int64: "l"}[type(val)]
68
+ llvm.inline_asm(
69
+ None,
70
+ [remote_smem_ptr_i32, val.ir_value(loc=loc, ip=ip), remote_mbar_ptr_i32],
71
+ f"st.async.shared::cluster.mbarrier::complete_tx::bytes.{suffix} [$0], $1, [$2];",
72
+ f"r,{constraint},r",
73
+ has_side_effects=True,
74
+ is_align_stack=False,
75
+ asm_dialect=llvm.AsmDialect.AD_ATT,
76
+ )
77
+
78
+
79
+ @dsl_user_op
80
+ def store_shared_remote_x4(
81
+ val0: Float32 | Int32,
82
+ val1: Float32 | Int32,
83
+ val2: Float32 | Int32,
84
+ val3: Float32 | Int32,
85
+ smem_ptr: cute.Pointer,
86
+ mbar_ptr: cute.Pointer,
87
+ peer_cta_rank_in_cluster: cute.typing.Int,
88
+ *,
89
+ loc=None,
90
+ ip=None,
91
+ ) -> None:
92
+ remote_smem_ptr_i32 = set_block_rank(
93
+ smem_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
94
+ ).ir_value()
95
+ remote_mbar_ptr_i32 = set_block_rank(
96
+ mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
97
+ ).ir_value()
98
+ assert isinstance(val0, (Float32, Int32)), "val must be Float32, or Int32"
99
+ dtype = Float32 if isinstance(val0, Float32) else Int32
100
+ suffix = {Float32: "f32", Int32: "s32"}[dtype]
101
+ constraint = {Float32: "f", Int32: "r"}[dtype]
102
+ llvm.inline_asm(
103
+ None,
104
+ [
105
+ remote_smem_ptr_i32,
106
+ remote_mbar_ptr_i32,
107
+ dtype(val0).ir_value(loc=loc, ip=ip),
108
+ dtype(val1).ir_value(loc=loc, ip=ip),
109
+ dtype(val2).ir_value(loc=loc, ip=ip),
110
+ dtype(val3).ir_value(loc=loc, ip=ip),
111
+ ],
112
+ "{\n\t"
113
+ f".reg .v4 .{suffix} abcd;\n\t"
114
+ f"mov.{suffix} abcd.x, $2;\n\t"
115
+ f"mov.{suffix} abcd.y, $3;\n\t"
116
+ f"mov.{suffix} abcd.z, $4;\n\t"
117
+ f"mov.{suffix} abcd.w, $5;\n\t"
118
+ f"st.async.shared::cluster.mbarrier::complete_tx::bytes.v4.{suffix} [$0], abcd, [$1];\n\t"
119
+ "}\n",
120
+ f"r,r,{constraint},{constraint},{constraint},{constraint}",
121
+ has_side_effects=True,
122
+ is_align_stack=False,
123
+ asm_dialect=llvm.AsmDialect.AD_ATT,
124
+ )
125
+
126
+
127
+ @dsl_user_op
128
+ def fmin(a: Union[float, Float32], b: Union[float, Float32], *, loc=None, ip=None) -> Float32:
129
+ if cutlass.const_expr(cutlass.CUDA_VERSION.major) == 12:
130
+ return Float32(
131
+ nvvm.fmin(
132
+ T.f32(),
133
+ Float32(a).ir_value(loc=loc, ip=ip),
134
+ Float32(b).ir_value(loc=loc, ip=ip),
135
+ loc=loc,
136
+ ip=ip,
137
+ )
138
+ )
139
+ return Float32(
140
+ nvvm.fmin(
141
+ Float32(a).ir_value(loc=loc, ip=ip),
142
+ Float32(b).ir_value(loc=loc, ip=ip),
143
+ loc=loc,
144
+ ip=ip,
145
+ )
146
+ )
147
+
148
+
149
+ @dsl_user_op
150
+ def sqrt(a: float | Float32, *, loc=None, ip=None) -> Float32:
151
+ return Float32(
152
+ llvm.inline_asm(
153
+ T.f32(),
154
+ [Float32(a).ir_value(loc=loc, ip=ip)],
155
+ "sqrt.approx.f32 $0, $1;",
156
+ "=f,f",
157
+ has_side_effects=False,
158
+ is_align_stack=False,
159
+ asm_dialect=llvm.AsmDialect.AD_ATT,
160
+ )
161
+ )
162
+
163
+
164
+ @dsl_user_op
165
+ def ceil(a: float | Float32, *, loc=None, ip=None) -> Int32:
166
+ return Int32(
167
+ llvm.inline_asm(
168
+ T.i32(),
169
+ [Float32(a).ir_value(loc=loc, ip=ip)],
170
+ "cvt.rpi.ftz.s32.f32 $0, $1;",
171
+ "=r,f",
172
+ has_side_effects=False,
173
+ is_align_stack=False,
174
+ asm_dialect=llvm.AsmDialect.AD_ATT,
175
+ )
176
+ )
177
+
178
+
179
+ @cute.jit
180
+ def fill_oob(tXsX: cute.Tensor, tXpX: Optional[cute.Tensor], fill_value: cute.Numeric) -> None:
181
+ """Fill out-of-bounds values in shared memory tensor.
182
+
183
+ Args:
184
+ tXsX: Shared memory tensor to fill
185
+ tXpX: Predicate tensor indicating valid elements
186
+ fill_value: Value to fill OOB locations with
187
+ """
188
+ tXrX_fill = cute.make_rmem_tensor_like(tXsX[(None, 0), None, 0])
189
+ tXrX_fill.fill(fill_value)
190
+ for rest_v in cutlass.range_constexpr(tXsX.shape[0][1]):
191
+ for rest_k in cutlass.range_constexpr(tXsX.shape[2]):
192
+ if const_expr(tXpX is not None):
193
+ if not tXpX[rest_v, 0, rest_k]:
194
+ cute.autovec_copy(tXrX_fill, tXsX[(None, rest_v), None, rest_k])
195
+ else:
196
+ cute.autovec_copy(tXrX_fill, tXsX[(None, rest_v), None, rest_k])
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # General-purpose DSL store / vector helpers
201
+ # ---------------------------------------------------------------------------
202
+
203
+
204
+ @dsl_user_op
205
+ def make_vector(elem_type, *values, loc=None, ip=None):
206
+ """Build an MLIR vector <N x elem_type> from N scalar DSL values.
207
+
208
+ Example: make_vector(cutlass.Uint32, v0, v1) -> <2 x i32> MLIR vector
209
+ """
210
+ from cutlass._mlir import ir
211
+
212
+ n = len(values)
213
+ mlir_ty = elem_type.mlir_type
214
+ vec_ty = ir.VectorType.get([n], mlir_ty)
215
+ vec = llvm.mlir_undef(vec_ty, loc=loc, ip=ip)
216
+ for i, v in enumerate(values):
217
+ vec = vector.insertelement(
218
+ elem_type(v).ir_value(loc=loc, ip=ip),
219
+ vec,
220
+ position=_arith.constant(T.i32(), i, loc=loc, ip=ip),
221
+ loc=loc,
222
+ ip=ip,
223
+ )
224
+ return vec
225
+
226
+
227
+ @dsl_user_op
228
+ def f32x2_to_i64(a: Float32, b: Float32, *, loc=None, ip=None) -> cutlass.Int64:
229
+ vec_f32x2 = vector.from_elements(
230
+ T.vector(2, T.f32()), (a.ir_value(), b.ir_value()), loc=loc, ip=ip
231
+ )
232
+ vec_i64x1 = vector.bitcast(T.vector(1, T.i64()), vec_f32x2)
233
+ res = cutlass.Int64(
234
+ vector.extract(vec_i64x1, dynamic_position=[], static_position=[0], loc=loc, ip=ip)
235
+ )
236
+ return res
237
+
238
+
239
+ @dsl_user_op
240
+ def i64_to_f32x2(c: cutlass.Int64, *, loc=None, ip=None) -> Tuple[Float32, Float32]:
241
+ vec_i64x1 = vector.from_elements(T.vector(1, T.i64()), (c.ir_value(),), loc=loc, ip=ip)
242
+ vec_f32x2 = vector.bitcast(T.vector(2, T.f32()), vec_i64x1)
243
+ res0 = Float32(
244
+ vector.extract(vec_f32x2, dynamic_position=[], static_position=[0], loc=loc, ip=ip)
245
+ )
246
+ res1 = Float32(
247
+ vector.extract(vec_f32x2, dynamic_position=[], static_position=[1], loc=loc, ip=ip)
248
+ )
249
+ return res0, res1
250
+
251
+
252
+ @cute.jit
253
+ def warp_prefix_sum(val: Int32, lane: Optional[Int32] = None) -> Int32:
254
+ if const_expr(lane is None):
255
+ lane = cute.arch.lane_idx()
256
+ for i in cutlass.range_constexpr(int(math.log2(cute.arch.WARP_SIZE))):
257
+ offset = 1 << i
258
+ # Very important that we set mask_and_clamp to 0
259
+ partial_sum = cute.arch.shuffle_sync_up(val, offset=offset, mask_and_clamp=0)
260
+ if lane >= offset:
261
+ val += partial_sum
262
+ return val
263
+
264
+
265
+ @dsl_user_op
266
+ def atomic_inc_i32(a: int | Int32, gmem_ptr: cute.Pointer, *, loc=None, ip=None) -> Int32:
267
+ from cutlass import CUDA_VERSION
268
+
269
+ # * NVVM call based on nvvm version
270
+ if CUDA_VERSION.major == 12 and CUDA_VERSION.minor == 9:
271
+ # Old API: requires explicit result type as first positional argument
272
+ return nvvm.atomicrmw(
273
+ res=T.i32(), op=nvvm.AtomicOpKind.INC, ptr=gmem_ptr.llvm_ptr, a=Int32(a).ir_value()
274
+ )
275
+ else:
276
+ # New API: infers result type automatically
277
+ return nvvm.atomicrmw(
278
+ op=nvvm.AtomicOpKind.INC, ptr=gmem_ptr.llvm_ptr, a=Int32(a).ir_value()
279
+ )
280
+
281
+
282
+ @dsl_user_op
283
+ def atomic_add_i32(a: int | Int32, gmem_ptr: cute.Pointer, *, loc=None, ip=None) -> Int32:
284
+ from cutlass import CUDA_VERSION
285
+
286
+ # * NVVM call based on nvvm version
287
+ if CUDA_VERSION.major == 12 and CUDA_VERSION.minor == 9:
288
+ # Old API: requires explicit result type as first positional argument
289
+ return nvvm.atomicrmw(
290
+ res=T.i32(), op=nvvm.AtomicOpKind.ADD, ptr=gmem_ptr.llvm_ptr, a=Int32(a).ir_value()
291
+ )
292
+ else:
293
+ # New API: infers result type automatically
294
+ return nvvm.atomicrmw(
295
+ op=nvvm.AtomicOpKind.ADD, ptr=gmem_ptr.llvm_ptr, a=Int32(a).ir_value()
296
+ )
297
+
298
+
299
+ @dsl_user_op
300
+ def issue_clc_query_nomulticast(
301
+ mbar_ptr: cute.Pointer,
302
+ clc_response_ptr: cute.Pointer,
303
+ loc=None,
304
+ ip=None,
305
+ ) -> None:
306
+ """
307
+ The clusterlaunchcontrol.try_cancel instruction requests atomically cancelling the launch
308
+ of a cluster that has not started running yet. It asynchronously writes an opaque response
309
+ to shared memory indicating whether the operation succeeded or failed. On success, the
310
+ opaque response contains the ctaid of the first CTA of the canceled cluster.
311
+
312
+ :param mbar_ptr: A pointer to the mbarrier address in SMEM
313
+ :type mbar_ptr: Pointer
314
+ :param clc_response_ptr: A pointer to the cluster launch control response address in SMEM
315
+ :type clc_response_ptr: Pointer
316
+ """
317
+ mbar_llvm_ptr = mbar_ptr.llvm_ptr
318
+ clc_response_llvm_ptr = clc_response_ptr.llvm_ptr
319
+ nvvm.clusterlaunchcontrol_try_cancel(
320
+ clc_response_llvm_ptr,
321
+ mbar_llvm_ptr,
322
+ loc=loc,
323
+ ip=ip,
324
+ )