hp-l33 commited on
Commit
8e9f35a
·
verified ·
1 Parent(s): 7b82f70

Add Sol-Attn Kernel Builder source

Browse files

Pinned to NVlabs/Sana commit 8a26fb0ec9e353125ead798cb2e312d5ce48cded; packaging changes are limited to version-isolated relative imports.

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +4 -0
  2. CARD.md +57 -0
  3. README.md +40 -0
  4. SOURCE.md +23 -0
  5. benchmarks/benchmark.py +58 -0
  6. build.toml +21 -0
  7. example.py +18 -0
  8. flake.lock +117 -0
  9. flake.nix +17 -0
  10. tests/test_sol_attn.py +89 -0
  11. tools/verify_upstream.py +115 -0
  12. torch-ext/sol_attn/THIRD_PARTY_NOTICES.md +15 -0
  13. torch-ext/sol_attn/__init__.py +5 -0
  14. torch-ext/sol_attn/_vendor/__init__.py +1 -0
  15. torch-ext/sol_attn/_vendor/flash_attn/__init__.py +1 -0
  16. torch-ext/sol_attn/_vendor/flash_attn/cute/__init__.py +1 -0
  17. torch-ext/sol_attn/_vendor/flash_attn/cute/ampere_helpers.py +103 -0
  18. torch-ext/sol_attn/_vendor/flash_attn/cute/block_info.py +139 -0
  19. torch-ext/sol_attn/_vendor/flash_attn/cute/block_sparsity.py +463 -0
  20. torch-ext/sol_attn/_vendor/flash_attn/cute/cute_dsl_utils.py +129 -0
  21. torch-ext/sol_attn/_vendor/flash_attn/cute/fast_math.py +21 -0
  22. torch-ext/sol_attn/_vendor/flash_attn/cute/flash_fwd.py +1218 -0
  23. torch-ext/sol_attn/_vendor/flash_attn/cute/mask.py +712 -0
  24. torch-ext/sol_attn/_vendor/flash_attn/cute/named_barrier.py +47 -0
  25. torch-ext/sol_attn/_vendor/flash_attn/cute/pack_gqa.py +263 -0
  26. torch-ext/sol_attn/_vendor/flash_attn/cute/pipeline.py +402 -0
  27. torch-ext/sol_attn/_vendor/flash_attn/cute/seqlen_info.py +290 -0
  28. torch-ext/sol_attn/_vendor/flash_attn/cute/softmax.py +639 -0
  29. torch-ext/sol_attn/_vendor/flash_attn/cute/tile_scheduler.py +1087 -0
  30. torch-ext/sol_attn/_vendor/flash_attn/cute/utils.py +800 -0
  31. torch-ext/sol_attn/common/__init__.py +5 -0
  32. torch-ext/sol_attn/common/layout_utils.py +130 -0
  33. torch-ext/sol_attn/common/runtime.py +14 -0
  34. torch-ext/sol_attn/common/selector.py +169 -0
  35. torch-ext/sol_attn/interface.py +399 -0
  36. torch-ext/sol_attn/preprocess.py +463 -0
  37. torch-ext/sol_attn/sm100/LICENSE.flash-attention +29 -0
  38. torch-ext/sol_attn/sm100/__init__.py +5 -0
  39. torch-ext/sol_attn/sm100/kernel.py +5 -0
  40. torch-ext/sol_attn/sm100/mainloop.py +1762 -0
  41. torch-ext/sol_attn/sm100/math.py +29 -0
  42. torch-ext/sol_attn/sm100/softmax.py +156 -0
  43. torch-ext/sol_attn/sm100/tmem.py +138 -0
  44. torch-ext/sol_attn/sm120/__init__.py +5 -0
  45. torch-ext/sol_attn/sm120/kernel.py +19 -0
  46. torch-ext/sol_attn/sm120/mainloop.py +1172 -0
  47. torch-ext/sol_attn/sm90/__init__.py +5 -0
  48. torch-ext/sol_attn/sm90/_compat/__init__.py +1 -0
  49. torch-ext/sol_attn/sm90/_compat/activation.py +5 -0
  50. torch-ext/sol_attn/sm90/_compat/copy_utils.py +169 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .hf-venv/
2
+ build/
3
+ __pycache__/
4
+ *.pyc
CARD.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: kernels
3
+ {% if license %}license: {{ license }}
4
+ {% endif %}tags:
5
+ - kernels
6
+ - cuda
7
+ - attention
8
+ - triton
9
+ - cute-dsl
10
+ ---
11
+
12
+ # Sol-Attn
13
+
14
+ Sol-Attn accelerates image and video generation with on-the-fly attention
15
+ sparsification. The public API dispatches to CuTe DSL kernels on SM90, SM100,
16
+ and SM120, and to Triton on SM80 and SM89 or when CuTe DSL is unavailable.
17
+
18
+ ## Usage
19
+
20
+ ```python
21
+ from kernels import get_kernel
22
+
23
+ kernel = get_kernel("{{ repo_id }}", version={{ version }})
24
+
25
+ out = kernel.sol_attn(
26
+ q, # Contiguous BF16 CUDA tensor [batch, tokens, heads, 128].
27
+ k, # Same shape, dtype, layout, and device as q.
28
+ v, # Same shape, dtype, layout, and device as q.
29
+ tau=1.0,
30
+ thresh_type="exact",
31
+ )
32
+ ```
33
+
34
+ The released implementation is noncausal and forward-only. Q/K/V must have
35
+ the same BTHD shape. An optional exact KV sink is available through
36
+ `sink_start` and `sink_tokens`.
37
+
38
+ ## Backends
39
+
40
+ | Architecture | Example GPU | Backend |
41
+ |---|---|---|
42
+ | SM90 | H100 | CuTe DSL |
43
+ | SM100 | GB200 | CuTe DSL |
44
+ | SM120 | RTX 5090 | CuTe DSL |
45
+ | SM80 / SM89 | A100 / RTX 4090 | Triton |
46
+
47
+ ## Paper
48
+
49
+ [Accelerating Video Generation Inference via On-the-Fly Attention
50
+ Sparsification](https://arxiv.org/abs/2607.24027)
51
+
52
+ ## Source
53
+
54
+ The implementation is maintained in
55
+ [`NVlabs/Sana`](https://github.com/NVlabs/Sana/tree/sol-engine/techniques/sparse_backends/sol_attn).
56
+ This release is pinned to commit
57
+ [`8a26fb0`](https://github.com/NVlabs/Sana/commit/8a26fb0ec9e353125ead798cb2e312d5ce48cded).
README.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tags:
4
+ - kernels
5
+ - cuda
6
+ - attention
7
+ - triton
8
+ - cute-dsl
9
+ ---
10
+
11
+ # Sol-Attn Kernel Builder Source
12
+
13
+ This repository contains the Hugging Face Kernel Builder packaging for
14
+ [Sol-Attn](https://github.com/NVlabs/Sana/tree/sol-engine/techniques/sparse_backends/sol_attn).
15
+ The kernel implementation is pinned to NVIDIA's `NVlabs/Sana` commit
16
+ [`8a26fb0`](https://github.com/NVlabs/Sana/commit/8a26fb0ec9e353125ead798cb2e312d5ce48cded).
17
+
18
+ The packaged Python sources preserve that implementation. The only source
19
+ relocation required by Kernel Hub is converting internal `sol_attn.*` imports
20
+ to package-relative imports, so the kernel remains loadable under the
21
+ version-isolated module name assigned by `kernels.get_kernel(...)`.
22
+
23
+ Published builds are loaded from
24
+ [`Efficient-Large-Model/Sol-Attn`](https://huggingface.co/Efficient-Large-Model/Sol-Attn):
25
+
26
+ ```python
27
+ from kernels import get_kernel
28
+
29
+ kernel = get_kernel("Efficient-Large-Model/Sol-Attn", version=1)
30
+
31
+ out = kernel.sol_attn(
32
+ q, # Contiguous BF16 CUDA tensor [batch, tokens, heads, 128].
33
+ k, # Same shape, dtype, layout, and device as q.
34
+ v, # Same shape, dtype, layout, and device as q.
35
+ tau=1.0,
36
+ thresh_type="exact",
37
+ )
38
+ ```
39
+
40
+ See [SOURCE.md](SOURCE.md) for provenance and the verification command.
SOURCE.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Source provenance
2
+
3
+ - Upstream repository: <https://github.com/NVlabs/Sana>
4
+ - Upstream branch: `sol-engine`
5
+ - Upstream commit: `8a26fb0ec9e353125ead798cb2e312d5ce48cded`
6
+ - Upstream path: `techniques/sparse_backends/sol_attn`
7
+ - Kernel Hub target: `Efficient-Large-Model/Sol-Attn`
8
+
9
+ `torch-ext/sol_attn` contains the complete upstream package at the pinned
10
+ commit. Its only packaging-level changes are semantically equivalent
11
+ absolute-to-relative internal imports. Kernel Hub loads each kernel version
12
+ under an isolated module name, so hard-coded `sol_attn.*` package imports
13
+ would otherwise escape that namespace.
14
+
15
+ Given a checkout of the pinned Sana commit, verify the file inventory, all
16
+ non-import source text, and import semantics with:
17
+
18
+ ```bash
19
+ python tools/verify_upstream.py /path/to/Sana
20
+ ```
21
+
22
+ The publishing process does not add files, workflows, or commits to
23
+ `NVlabs/Sana`.
benchmarks/benchmark.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+
3
+ import torch
4
+ from kernels.benchmark import Benchmark
5
+
6
+
7
+ class SolAttnBenchmark(Benchmark):
8
+ seed: int = 42
9
+
10
+ def _setup(self, tokens, heads):
11
+ self.q = torch.randn(
12
+ 1,
13
+ tokens,
14
+ heads,
15
+ 128,
16
+ device=self.device,
17
+ dtype=torch.bfloat16,
18
+ )
19
+ self.k = torch.randn_like(self.q)
20
+ self.v = torch.randn_like(self.q)
21
+ module = importlib.import_module(f"{self.kernel.__name__}.triton_ref")
22
+ self.triton_sol_attn = module.sol_attn
23
+
24
+ def _run(self):
25
+ self.out = self.kernel.sol_attn(
26
+ self.q,
27
+ self.k,
28
+ self.v,
29
+ tau=1.0,
30
+ thresh_type="exact",
31
+ )
32
+
33
+ def _reference(self):
34
+ return self.triton_sol_attn(
35
+ self.q,
36
+ self.k,
37
+ self.v,
38
+ tau=1.0,
39
+ thresh_type="exact",
40
+ )
41
+
42
+ def setup_base(self):
43
+ self._setup(tokens=4096, heads=8)
44
+
45
+ def benchmark_base(self):
46
+ self._run()
47
+
48
+ def verify_base(self):
49
+ return self._reference()
50
+
51
+ def setup_video(self):
52
+ self._setup(tokens=16384, heads=16)
53
+
54
+ def benchmark_video(self):
55
+ self._run()
56
+
57
+ def verify_video(self):
58
+ return self._reference()
build.toml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [general]
2
+ name = "sol-attn"
3
+ version = 1
4
+ edition = 5
5
+ license = "Apache-2.0"
6
+ backends = ["cuda"]
7
+ python-depends = ["tvm-ffi"]
8
+ upstream = "https://github.com/NVlabs/Sana.git"
9
+ source = "https://huggingface.co/Efficient-Large-Model/Sol-Attn-Kernel-Source"
10
+
11
+ [general.cuda]
12
+ minver = "12.8"
13
+ python-depends = ["nvidia-cutlass-dsl"]
14
+
15
+ [general.hub]
16
+ repo-id = "Efficient-Large-Model/Sol-Attn"
17
+
18
+ [torch-noarch]
19
+ cuda-capabilities = ["8.0", "8.9", "9.0", "10.0", "12.0"]
20
+
21
+ [kernel]
example.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from kernels import get_kernel
3
+
4
+
5
+ kernel = get_kernel("Efficient-Large-Model/Sol-Attn", version=1)
6
+
7
+ q = torch.randn(1, 4096, 16, 128, device="cuda", dtype=torch.bfloat16)
8
+ k = torch.randn_like(q)
9
+ v = torch.randn_like(q)
10
+
11
+ out = kernel.sol_attn(
12
+ q,
13
+ k,
14
+ v,
15
+ tau=1.0,
16
+ thresh_type="exact",
17
+ )
18
+ print(out.shape)
flake.lock ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nodes": {
3
+ "flake-compat": {
4
+ "locked": {
5
+ "lastModified": 1767039857,
6
+ "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
7
+ "owner": "edolstra",
8
+ "repo": "flake-compat",
9
+ "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
10
+ "type": "github"
11
+ },
12
+ "original": {
13
+ "owner": "edolstra",
14
+ "repo": "flake-compat",
15
+ "type": "github"
16
+ }
17
+ },
18
+ "flake-utils": {
19
+ "inputs": {
20
+ "systems": "systems"
21
+ },
22
+ "locked": {
23
+ "lastModified": 1731533236,
24
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
25
+ "owner": "numtide",
26
+ "repo": "flake-utils",
27
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
28
+ "type": "github"
29
+ },
30
+ "original": {
31
+ "owner": "numtide",
32
+ "repo": "flake-utils",
33
+ "type": "github"
34
+ }
35
+ },
36
+ "kernel-builder": {
37
+ "inputs": {
38
+ "flake-compat": "flake-compat",
39
+ "flake-utils": "flake-utils",
40
+ "nixpkgs": "nixpkgs",
41
+ "rust-overlay": "rust-overlay"
42
+ },
43
+ "locked": {
44
+ "lastModified": 1785782967,
45
+ "narHash": "sha256-5Esv3PsgzKfP6I3Te1n4Ge4kQG69WQETPJoxGB9WKpk=",
46
+ "owner": "huggingface",
47
+ "repo": "kernels",
48
+ "rev": "a6564d1f481adcbd3273099c0f4432e7b833e846",
49
+ "type": "github"
50
+ },
51
+ "original": {
52
+ "owner": "huggingface",
53
+ "repo": "kernels",
54
+ "type": "github"
55
+ }
56
+ },
57
+ "nixpkgs": {
58
+ "locked": {
59
+ "lastModified": 1783284758,
60
+ "narHash": "sha256-tiQ8/qi8I45OOaBBYlVbXoAVkeQzvvTQOv5I45rMw5o=",
61
+ "owner": "NixOS",
62
+ "repo": "nixpkgs",
63
+ "rev": "ec1a11210589d294f0ac99d3290a27e6c73dfa1d",
64
+ "type": "github"
65
+ },
66
+ "original": {
67
+ "owner": "NixOS",
68
+ "repo": "nixpkgs",
69
+ "rev": "ec1a11210589d294f0ac99d3290a27e6c73dfa1d",
70
+ "type": "github"
71
+ }
72
+ },
73
+ "root": {
74
+ "inputs": {
75
+ "kernel-builder": "kernel-builder"
76
+ }
77
+ },
78
+ "rust-overlay": {
79
+ "inputs": {
80
+ "nixpkgs": [
81
+ "kernel-builder",
82
+ "nixpkgs"
83
+ ]
84
+ },
85
+ "locked": {
86
+ "lastModified": 1783320166,
87
+ "narHash": "sha256-l7C/OsjcnWDOk2K3ssj+SBduwL67LashjBqis9+t468=",
88
+ "owner": "oxalica",
89
+ "repo": "rust-overlay",
90
+ "rev": "20ee15370c9256669d66968b89ee20a4b0a4e673",
91
+ "type": "github"
92
+ },
93
+ "original": {
94
+ "owner": "oxalica",
95
+ "repo": "rust-overlay",
96
+ "type": "github"
97
+ }
98
+ },
99
+ "systems": {
100
+ "locked": {
101
+ "lastModified": 1681028828,
102
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
103
+ "owner": "nix-systems",
104
+ "repo": "default",
105
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
106
+ "type": "github"
107
+ },
108
+ "original": {
109
+ "owner": "nix-systems",
110
+ "repo": "default",
111
+ "type": "github"
112
+ }
113
+ }
114
+ },
115
+ "root": "root",
116
+ "version": 7
117
+ }
flake.nix ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ description = "Flake for Sol-Attn kernels";
3
+
4
+ inputs = {
5
+ kernel-builder.url = "github:huggingface/kernels";
6
+ };
7
+
8
+ outputs =
9
+ {
10
+ self,
11
+ kernel-builder,
12
+ }:
13
+ kernel-builder.lib.genKernelFlakeOutputs {
14
+ inherit self;
15
+ path = ./.;
16
+ };
17
+ }
tests/test_sol_attn.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib
2
+
3
+ import pytest
4
+ import torch
5
+ import torch.nn.functional as F
6
+ from kernels import get_kernel
7
+
8
+
9
+ kernel = get_kernel("Efficient-Large-Model/Sol-Attn", version=1)
10
+
11
+
12
+ def _inputs(tokens=256, heads=4):
13
+ torch.manual_seed(42)
14
+ q = torch.randn(
15
+ 1,
16
+ tokens,
17
+ heads,
18
+ 128,
19
+ device="cuda",
20
+ dtype=torch.bfloat16,
21
+ )
22
+ return q, torch.randn_like(q), torch.randn_like(q)
23
+
24
+
25
+ @pytest.mark.kernels_ci
26
+ def test_backend_dispatch_contract():
27
+ interface = importlib.import_module(f"{kernel.__name__}.interface")
28
+
29
+ assert interface._backend_for_arch((8, 0), cute_available=True) == "triton"
30
+ assert interface._backend_for_arch((8, 9), cute_available=True) == "triton"
31
+ assert interface._backend_for_arch((9, 0), cute_available=True) == "cute_sm90"
32
+ assert interface._backend_for_arch((10, 0), cute_available=True) == "cute_sm100"
33
+ assert interface._backend_for_arch((12, 0), cute_available=True) == "cute_sm120"
34
+ assert interface._backend_for_arch((9, 0), cute_available=False) == "triton"
35
+ assert interface._backend_for_arch((10, 0), cute_available=False) == "triton"
36
+ assert interface._backend_for_arch((12, 0), cute_available=False) == "triton"
37
+
38
+
39
+ @pytest.mark.kernels_ci
40
+ @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required")
41
+ def test_full_sink_matches_sdpa():
42
+ capability = torch.cuda.get_device_capability()
43
+ if capability[0] < 8:
44
+ pytest.skip("Sol-Attn requires compute capability 8.0 or newer")
45
+
46
+ q, k, v = _inputs()
47
+ expected = F.scaled_dot_product_attention(
48
+ q.transpose(1, 2),
49
+ k.transpose(1, 2),
50
+ v.transpose(1, 2),
51
+ ).transpose(1, 2)
52
+ actual = kernel.sol_attn(
53
+ q,
54
+ k,
55
+ v,
56
+ tau=1.0,
57
+ thresh_type="exact",
58
+ sink_start=0,
59
+ sink_tokens=q.shape[1],
60
+ )
61
+
62
+ torch.testing.assert_close(actual, expected, atol=2e-2, rtol=3e-2)
63
+
64
+
65
+ @pytest.mark.kernels_ci
66
+ @pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA is required")
67
+ def test_selected_backend_matches_triton_reference():
68
+ capability = torch.cuda.get_device_capability()
69
+ if capability[0] < 8:
70
+ pytest.skip("Sol-Attn requires compute capability 8.0 or newer")
71
+
72
+ triton_ref = importlib.import_module(f"{kernel.__name__}.triton_ref")
73
+ q, k, v = _inputs()
74
+ expected = triton_ref.sol_attn(
75
+ q,
76
+ k,
77
+ v,
78
+ tau=1.0,
79
+ thresh_type="exact",
80
+ )
81
+ actual = kernel.sol_attn(
82
+ q,
83
+ k,
84
+ v,
85
+ tau=1.0,
86
+ thresh_type="exact",
87
+ )
88
+
89
+ torch.testing.assert_close(actual, expected, atol=2e-2, rtol=3e-2)
tools/verify_upstream.py ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Verify that the packaged sources only relocate internal imports."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import ast
7
+ import importlib.util
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+
13
+ PINNED_COMMIT = "8a26fb0ec9e353125ead798cb2e312d5ce48cded"
14
+ UPSTREAM_SUBDIR = Path("techniques/sparse_backends/sol_attn")
15
+ IGNORED_PARTS = {"__pycache__"}
16
+
17
+
18
+ def source_files(root: Path) -> dict[Path, Path]:
19
+ return {
20
+ path.relative_to(root): path
21
+ for path in root.rglob("*")
22
+ if path.is_file() and not (set(path.parts) & IGNORED_PARTS)
23
+ }
24
+
25
+
26
+ def module_package(relative_path: Path) -> str:
27
+ parent_parts = relative_path.with_suffix("").parts[:-1]
28
+ return ".".join(("sol_attn", *parent_parts))
29
+
30
+
31
+ def normalized_imports(tree: ast.AST, package: str):
32
+ imports = []
33
+ for node in ast.walk(tree):
34
+ if isinstance(node, ast.Import):
35
+ imports.extend(
36
+ (alias.name, alias.asname or alias.name.split(".")[0])
37
+ for alias in node.names
38
+ )
39
+ elif isinstance(node, ast.ImportFrom):
40
+ module = node.module or ""
41
+ if node.level:
42
+ relative = "." * node.level + module
43
+ module = importlib.util.resolve_name(relative, package)
44
+ imports.extend(
45
+ (
46
+ f"{module}.{alias.name}" if module else alias.name,
47
+ alias.asname or alias.name,
48
+ )
49
+ for alias in node.names
50
+ )
51
+ return sorted(imports)
52
+
53
+
54
+ def without_imports(text: str) -> str:
55
+ tree = ast.parse(text)
56
+ lines = text.splitlines(keepends=True)
57
+ for node in ast.walk(tree):
58
+ if isinstance(node, (ast.Import, ast.ImportFrom)):
59
+ for index in range(node.lineno - 1, node.end_lineno):
60
+ lines[index] = "\n" if lines[index].endswith("\n") else ""
61
+ return "".join(lines)
62
+
63
+
64
+ def main() -> int:
65
+ if len(sys.argv) != 2:
66
+ print("usage: verify_upstream.py /path/to/Sana", file=sys.stderr)
67
+ return 2
68
+
69
+ checkout = Path(sys.argv[1]).resolve()
70
+ commit = subprocess.check_output(
71
+ ["git", "-C", str(checkout), "rev-parse", "HEAD"],
72
+ text=True,
73
+ ).strip()
74
+ if commit != PINNED_COMMIT:
75
+ raise SystemExit(f"expected Sana {PINNED_COMMIT}, found {commit}")
76
+
77
+ upstream_root = checkout / UPSTREAM_SUBDIR
78
+ packaged_root = Path(__file__).resolve().parents[1] / "torch-ext/sol_attn"
79
+ upstream = source_files(upstream_root)
80
+ packaged = source_files(packaged_root)
81
+ if upstream.keys() != packaged.keys():
82
+ missing = sorted(upstream.keys() - packaged.keys())
83
+ extra = sorted(packaged.keys() - upstream.keys())
84
+ raise SystemExit(f"source inventory mismatch; missing={missing}, extra={extra}")
85
+
86
+ relocated = []
87
+ for relative_path in sorted(upstream):
88
+ upstream_bytes = upstream[relative_path].read_bytes()
89
+ packaged_bytes = packaged[relative_path].read_bytes()
90
+ if upstream_bytes == packaged_bytes:
91
+ continue
92
+ if relative_path.suffix != ".py":
93
+ raise SystemExit(f"non-Python source differs: {relative_path}")
94
+
95
+ upstream_text = upstream_bytes.decode()
96
+ packaged_text = packaged_bytes.decode()
97
+ if without_imports(upstream_text) != without_imports(packaged_text):
98
+ raise SystemExit(f"non-import source differs: {relative_path}")
99
+
100
+ package = module_package(relative_path)
101
+ upstream_imports = normalized_imports(ast.parse(upstream_text), package)
102
+ packaged_imports = normalized_imports(ast.parse(packaged_text), package)
103
+ if upstream_imports != packaged_imports:
104
+ raise SystemExit(f"import semantics differ: {relative_path}")
105
+ relocated.append(relative_path)
106
+
107
+ print(
108
+ "Verified pinned NVlabs/Sana source; only semantically equivalent "
109
+ f"import relocation is present in {len(relocated)} files."
110
+ )
111
+ return 0
112
+
113
+
114
+ if __name__ == "__main__":
115
+ raise SystemExit(main())
torch-ext/sol_attn/THIRD_PARTY_NOTICES.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Third-party notices
2
+
3
+ The files under `sol_attn/_vendor/flash_attn/cute/` and portions of the SM90
4
+ and SM100 design scaffold derive from the FlashAttention project. Its
5
+ BSD-3-Clause license is included at
6
+ `sol_attn/sm100/LICENSE.flash-attention`.
7
+
8
+ The runtime also depends on NVIDIA CUTLASS / CuTe DSL, cuda-python, PyTorch,
9
+ and Triton. Those dependencies are not redistributed by this repository and
10
+ remain subject to their respective licenses.
11
+
12
+ The SM120 warp-MMA/TMA execution skeleton and online-softmax helpers are
13
+ adapted from NVIDIA cuDNN Frontend's block-sparse-attention reference at commit
14
+ `74785165de2da954a2c879a5e3e6f95411c2292d`. That source is licensed under the
15
+ Apache License 2.0; adapted files retain the corresponding SPDX header.
torch-ext/sol_attn/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Sol-Attn."""
2
+
3
+ from .interface import sol_attn
4
+
5
+ __all__ = ["sol_attn"]
torch-ext/sol_attn/_vendor/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Private source dependencies bundled with Sol-Attn."""
torch-ext/sol_attn/_vendor/flash_attn/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Local FlashAttention Cute shim for the SOL_ATTN SM90 release."""
torch-ext/sol_attn/_vendor/flash_attn/cute/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Vendored FlashAttention Cute Python helpers used by SOL_ATTN SM90."""
torch-ext/sol_attn/_vendor/flash_attn/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()
torch-ext/sol_attn/_vendor/flash_attn/cute/block_info.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ...._vendor.flash_attn.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))
torch-ext/sol_attn/_vendor/flash_attn/cute/block_sparsity.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ...._vendor.flash_attn.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
+
23
+ def __new_from_mlir_values__(self, values):
24
+ if len(values) == 2:
25
+ values = (*values, None, None)
26
+ return BlockSparseTensors(*values)
27
+
28
+
29
+ class BlockSparseTensorsTorch(NamedTuple):
30
+ mask_block_cnt: torch.Tensor
31
+ mask_block_idx: torch.Tensor
32
+ full_block_cnt: torch.Tensor | None = None
33
+ full_block_idx: torch.Tensor | None = None
34
+ block_size: tuple[int, int] | None = None
35
+
36
+
37
+ def get_sparse_q_block_size(
38
+ tensors: BlockSparseTensorsTorch | None,
39
+ seqlen_q: int,
40
+ ) -> int | None:
41
+ """Return the Q sparse block size, or None when sparsity is unset or ambiguous."""
42
+ if tensors is None:
43
+ return None
44
+ if tensors.block_size is not None:
45
+ return tensors.block_size[0]
46
+ num_m_blocks = tensors.mask_block_idx.shape[2]
47
+ min_block_size = ceildiv(seqlen_q, num_m_blocks)
48
+ max_block_size = seqlen_q if num_m_blocks == 1 else (seqlen_q - 1) // (num_m_blocks - 1)
49
+ if min_block_size != max_block_size:
50
+ return None
51
+ return min_block_size
52
+
53
+
54
+ def _expand_sparsity_tensor(
55
+ tensor: torch.Tensor,
56
+ expected_shape: Tuple[int, ...],
57
+ tensor_name: str,
58
+ context: str | None,
59
+ hint: str | Callable[[], str] | None,
60
+ ) -> torch.Tensor:
61
+ """Check if we need to expand the tensor to expected shape, and do so if possible."""
62
+ needs_expand = tensor.shape != expected_shape
63
+ if not needs_expand:
64
+ return tensor
65
+ can_expand = all(map(lambda cur, tgt: cur == tgt or cur == 1, tensor.shape, expected_shape))
66
+ if not can_expand:
67
+ context_clause = f" ({context})" if context else ""
68
+ resolved_hint = hint() if callable(hint) else hint
69
+ hint_clause = f" Hint: {resolved_hint}" if resolved_hint else ""
70
+ raise ValueError(
71
+ f"{tensor_name}{context_clause} with shape {tensor.shape} cannot be expanded to expected shape {expected_shape}."
72
+ f"{hint_clause}"
73
+ )
74
+ return tensor.expand(*expected_shape)
75
+
76
+
77
+ def _check_and_expand_block(
78
+ name: str,
79
+ cnt: torch.Tensor | None,
80
+ idx: torch.Tensor | None,
81
+ expected_count_shape: Tuple[int, int, int],
82
+ expected_index_shape: Tuple[int, int, int, int],
83
+ context: str | None,
84
+ hint: str | Callable[[], str] | None,
85
+ ) -> Tuple[torch.Tensor | None, torch.Tensor | None]:
86
+ if (cnt is None) != (idx is None):
87
+ raise ValueError(
88
+ f"{name}_block_cnt and {name}_block_idx must both be provided or both be None"
89
+ )
90
+ if cnt is None or idx is None:
91
+ return None, None
92
+ if cnt.dtype != torch.int32 or idx.dtype != torch.int32:
93
+ raise ValueError(f"{name}_block tensors must have dtype torch.int32")
94
+ if cnt.device != idx.device:
95
+ raise ValueError(f"{name}_block_cnt and {name}_block_idx must be on the same device")
96
+ if not cnt.is_cuda or not idx.is_cuda:
97
+ raise ValueError(f"{name}_block tensors must live on CUDA")
98
+ expanded_cnt = _expand_sparsity_tensor(
99
+ cnt, expected_count_shape, f"{name}_block_cnt", context, hint
100
+ )
101
+ # [Note] Allow Compact block sparse indices
102
+ # Allow the last dimension (n_blocks) of idx to be <= expected, since
103
+ # FA4 only accesses indices 0..cnt-1 per query tile. This enables compact
104
+ # index tensors that avoid O(N^2) memory at long sequence lengths.
105
+ if idx.ndim == 4 and idx.shape[3] <= expected_index_shape[3]:
106
+ expected_index_shape = (*expected_index_shape[:3], idx.shape[3])
107
+ expanded_idx = _expand_sparsity_tensor(
108
+ idx, expected_index_shape, f"{name}_block_idx", context, hint
109
+ )
110
+ return expanded_cnt, expanded_idx
111
+
112
+
113
+ def get_block_sparse_expected_shapes(
114
+ batch_size: int,
115
+ num_head: int,
116
+ seqlen_q: int,
117
+ seqlen_k: int,
118
+ m_block_size: int,
119
+ n_block_size: int,
120
+ q_stage: int,
121
+ ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
122
+ """Return (expected_count_shape, expected_index_shape) for block sparse normalization."""
123
+ m_block_size_effective = q_stage * m_block_size
124
+ expected_m_blocks = ceildiv(seqlen_q, m_block_size_effective)
125
+ expected_n_blocks = ceildiv(seqlen_k, n_block_size)
126
+ expected_count_shape = (batch_size, num_head, expected_m_blocks)
127
+ expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
128
+ return expected_count_shape, expected_index_shape
129
+
130
+
131
+ def infer_block_sparse_expected_shapes(
132
+ tensors: BlockSparseTensorsTorch,
133
+ *,
134
+ batch_size: int,
135
+ num_head: int,
136
+ seqlen_q: int,
137
+ seqlen_k: int,
138
+ m_block_size: int,
139
+ n_block_size: int,
140
+ q_stage: int,
141
+ context: str,
142
+ sparse_block_size_q: int | None = None,
143
+ sparse_block_size_kv: int | None = None,
144
+ ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int], int]:
145
+ """Infer shapes and scaling for block-sparse tensors.
146
+
147
+ Expectations:
148
+ - mask_block_cnt is (B, H, M) and mask_block_idx is (B, H, M, N).
149
+ - Batch/head dims may be 1 for broadcast, or match the requested sizes.
150
+ - sparse_block_size_kv must match tile_n.
151
+ - sparse_block_size_q must be a multiple of q_stage * tile_m.
152
+ - If sparse_block_size_q is omitted and seqlen_q/num_m_blocks is ambiguous,
153
+ the caller must provide block_size to disambiguate. TODO will make this required in a future PR.
154
+ """
155
+ base_m_block = q_stage * m_block_size
156
+ base_n_block = n_block_size
157
+ if sparse_block_size_kv is None:
158
+ sparse_block_size_kv = base_n_block
159
+ if sparse_block_size_kv != base_n_block:
160
+ raise ValueError(f"Block sparse tensors{context} require BLOCK_SIZE_KV={base_n_block}.")
161
+ if tensors.mask_block_idx is None:
162
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
163
+ num_m_blocks = tensors.mask_block_idx.shape[2]
164
+
165
+ if sparse_block_size_q is None:
166
+ sparse_block_size_q = get_sparse_q_block_size(tensors, seqlen_q)
167
+ if sparse_block_size_q is None and base_m_block != 1:
168
+ raise ValueError(
169
+ f"Block sparse tensors{context} require explicit sparse_block_size[0] "
170
+ f"to disambiguate block size for seqlen_q={seqlen_q} and num_m_blocks={num_m_blocks}."
171
+ )
172
+ if sparse_block_size_q is None:
173
+ sparse_block_size_q = ceildiv(seqlen_q, num_m_blocks)
174
+
175
+ if sparse_block_size_q % base_m_block != 0:
176
+ raise ValueError(
177
+ f"Block sparse tensors{context} have block size {sparse_block_size_q}, "
178
+ f"which must be a multiple of {base_m_block}."
179
+ )
180
+
181
+ expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
182
+ expected_n_blocks = ceildiv(seqlen_k, sparse_block_size_kv)
183
+ q_subtile_factor = sparse_block_size_q // base_m_block
184
+ expected_count_shape = (batch_size, num_head, expected_m_blocks)
185
+ expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
186
+
187
+ mask_block_cnt = tensors.mask_block_cnt
188
+ mask_block_idx = tensors.mask_block_idx
189
+ if mask_block_cnt is None or mask_block_idx is None:
190
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
191
+ if mask_block_cnt.ndim != 3 or mask_block_idx.ndim != 4:
192
+ raise ValueError(
193
+ f"Block sparse tensors{context} must have shapes (B, H, M) and (B, H, M, N)."
194
+ )
195
+ for dim_name, cur, tgt in (
196
+ ("batch", mask_block_cnt.shape[0], expected_count_shape[0]),
197
+ ("head", mask_block_cnt.shape[1], expected_count_shape[1]),
198
+ ):
199
+ if cur != tgt and cur != 1:
200
+ raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
201
+ for dim_name, cur, tgt in (
202
+ ("batch", mask_block_idx.shape[0], expected_index_shape[0]),
203
+ ("head", mask_block_idx.shape[1], expected_index_shape[1]),
204
+ ):
205
+ if cur != tgt and cur != 1:
206
+ raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
207
+ if mask_block_cnt.shape[2] != mask_block_idx.shape[2]:
208
+ raise ValueError(f"Block sparse tensors{context} must share the same m-block dimension.")
209
+ # [Note] Allow Compact block sparse indices: FA4 only accesses indices 0..cnt-1
210
+ # per query tile, so idx.shape[3] can be <= expected_n_blocks.
211
+ if mask_block_idx.shape[3] > expected_n_blocks:
212
+ raise ValueError(
213
+ f"Block sparse tensors{context} n-block dimension must be <= {expected_n_blocks}."
214
+ )
215
+ if expected_m_blocks != num_m_blocks:
216
+ raise ValueError(
217
+ f"Block sparse tensors{context} m-block dimension {num_m_blocks} does not match "
218
+ f"sparse_block_size_q={sparse_block_size_q}. "
219
+ f"Set BlockSparseTensorsTorch.block_size to match the BlockMask BLOCK_SIZE."
220
+ )
221
+ return expected_count_shape, expected_index_shape, q_subtile_factor
222
+
223
+
224
+ def get_block_sparse_expected_shapes_bwd(
225
+ batch_size: int,
226
+ num_head: int,
227
+ seqlen_q: int,
228
+ seqlen_k: int,
229
+ m_block_size: int,
230
+ n_block_size: int,
231
+ subtile_factor: int,
232
+ ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
233
+ """Return (expected_count_shape, expected_index_shape) for backward block sparse normalization.
234
+
235
+ Backward uses Q-direction indexing (transposed from forward), where shapes are
236
+ indexed by N-blocks first, then M-blocks. The sparse_block_size_q is determined
237
+ by subtile_factor * m_block_size.
238
+ """
239
+ sparse_block_size_q = subtile_factor * m_block_size
240
+ expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
241
+ expected_n_blocks = ceildiv(seqlen_k, n_block_size)
242
+ expected_count_shape = (batch_size, num_head, expected_n_blocks)
243
+ expected_index_shape = (batch_size, num_head, expected_n_blocks, expected_m_blocks)
244
+ return expected_count_shape, expected_index_shape
245
+
246
+
247
+ def normalize_block_sparse_tensors(
248
+ tensors: BlockSparseTensorsTorch,
249
+ *,
250
+ expected_count_shape: Tuple[int, int, int],
251
+ expected_index_shape: Tuple[int, int, int, int],
252
+ context: str | None = None,
253
+ hint: str | Callable[[], str] | None = None,
254
+ ) -> BlockSparseTensorsTorch:
255
+ if tensors.mask_block_cnt is None or tensors.mask_block_idx is None:
256
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
257
+
258
+ mask_cnt, mask_idx = _check_and_expand_block(
259
+ "mask",
260
+ tensors.mask_block_cnt,
261
+ tensors.mask_block_idx,
262
+ expected_count_shape,
263
+ expected_index_shape,
264
+ context,
265
+ hint,
266
+ )
267
+ if mask_cnt is None or mask_idx is None:
268
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
269
+
270
+ full_cnt, full_idx = _check_and_expand_block(
271
+ "full",
272
+ tensors.full_block_cnt,
273
+ tensors.full_block_idx,
274
+ expected_count_shape,
275
+ expected_index_shape,
276
+ context,
277
+ hint,
278
+ )
279
+ if full_cnt is not None and mask_cnt.device != full_cnt.device:
280
+ raise ValueError("All block sparse tensors must be on the same device")
281
+
282
+ return BlockSparseTensorsTorch(
283
+ mask_block_cnt=mask_cnt,
284
+ mask_block_idx=mask_idx,
285
+ full_block_cnt=full_cnt,
286
+ full_block_idx=full_idx,
287
+ block_size=tensors.block_size,
288
+ )
289
+
290
+
291
+ def is_block_sparsity_enabled(tensors: BlockSparseTensorsTorch) -> bool:
292
+ return any(t is not None for t in (tensors.full_block_cnt, tensors.mask_block_cnt))
293
+
294
+
295
+ def get_block_sparse_broadcast_pattern(
296
+ tensors: BlockSparseTensorsTorch,
297
+ ) -> Tuple[Tuple[bool, ...], ...] | None:
298
+ """Return broadcast pattern for block sparse tensors by checking actual strides.
299
+
300
+ Returns a tuple of broadcast patterns (one per tensor) where each pattern
301
+ is a tuple of bools indicating which dims have stride=0.
302
+ This is used in compile keys to ensure kernels are recompiled when
303
+ broadcast patterns change, since CuTe's mark_layout_dynamic() keeps
304
+ stride=0 as static.
305
+
306
+ The tensors should already be expanded/normalized before calling this function.
307
+
308
+ Returns None if block sparsity is not enabled.
309
+ """
310
+ if not is_block_sparsity_enabled(tensors):
311
+ return None
312
+
313
+ patterns = []
314
+ for tensor in (
315
+ tensors.mask_block_cnt,
316
+ tensors.mask_block_idx,
317
+ tensors.full_block_cnt,
318
+ tensors.full_block_idx,
319
+ ):
320
+ if tensor is not None:
321
+ patterns.append(get_broadcast_dims(tensor))
322
+ else:
323
+ patterns.append(None)
324
+ return tuple(patterns)
325
+
326
+
327
+ def normalize_block_sparse_config(
328
+ tensors: BlockSparseTensorsTorch,
329
+ *,
330
+ batch_size: int,
331
+ num_head: int,
332
+ seqlen_q: int,
333
+ seqlen_k: int,
334
+ block_size: tuple[int, int],
335
+ q_stage: int,
336
+ ) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None, int]:
337
+ m_block_size, n_block_size = block_size
338
+ if tensors.block_size is None:
339
+ sparse_block_size_q, sparse_block_size_kv = None, n_block_size
340
+ else:
341
+ sparse_block_size_q, sparse_block_size_kv = tensors.block_size
342
+ if sparse_block_size_kv != n_block_size:
343
+ raise ValueError(
344
+ f"Block sparsity requires sparse_block_size[1]={n_block_size} to match tile_n."
345
+ )
346
+ expected_count_shape, expected_index_shape, q_subtile_factor = (
347
+ infer_block_sparse_expected_shapes(
348
+ tensors,
349
+ batch_size=batch_size,
350
+ num_head=num_head,
351
+ seqlen_q=seqlen_q,
352
+ seqlen_k=seqlen_k,
353
+ m_block_size=m_block_size,
354
+ n_block_size=n_block_size,
355
+ q_stage=q_stage,
356
+ context="forward",
357
+ sparse_block_size_q=sparse_block_size_q,
358
+ sparse_block_size_kv=sparse_block_size_kv,
359
+ )
360
+ )
361
+ normalized_tensors = normalize_block_sparse_tensors(
362
+ tensors,
363
+ expected_count_shape=expected_count_shape,
364
+ expected_index_shape=expected_index_shape,
365
+ )
366
+ return (
367
+ normalized_tensors,
368
+ get_block_sparse_broadcast_pattern(normalized_tensors),
369
+ q_subtile_factor,
370
+ )
371
+
372
+
373
+ def normalize_block_sparse_config_bwd(
374
+ tensors: BlockSparseTensorsTorch,
375
+ *,
376
+ batch_size: int,
377
+ num_head: int,
378
+ seqlen_q: int,
379
+ seqlen_k: int,
380
+ block_size: tuple[int, int],
381
+ subtile_factor: int,
382
+ ) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None]:
383
+ m_block_size, n_block_size = block_size
384
+ if tensors.block_size is None:
385
+ sparse_block_size_q, sparse_block_size_kv = subtile_factor * m_block_size, n_block_size
386
+ else:
387
+ sparse_block_size_q, sparse_block_size_kv = tensors.block_size
388
+ if sparse_block_size_q != subtile_factor * m_block_size:
389
+ raise ValueError(
390
+ f"Block sparsity expects sparse_block_size_q={subtile_factor * m_block_size} "
391
+ f"for subtile_factor={subtile_factor}."
392
+ )
393
+ if sparse_block_size_kv != n_block_size:
394
+ raise ValueError(
395
+ f"Block sparsity expects sparse_block_size[1]={n_block_size} to match tile_n."
396
+ )
397
+ expected_count_shape, expected_index_shape = get_block_sparse_expected_shapes_bwd(
398
+ batch_size,
399
+ num_head,
400
+ seqlen_q,
401
+ seqlen_k,
402
+ m_block_size,
403
+ n_block_size,
404
+ subtile_factor,
405
+ )
406
+ normalized_tensors = normalize_block_sparse_tensors(
407
+ tensors,
408
+ expected_count_shape=expected_count_shape,
409
+ expected_index_shape=expected_index_shape,
410
+ context="_flash_attn_bwd",
411
+ hint=lambda: (
412
+ f"Backward expects Q-direction block-sparse tensors (q_mask_cnt/q_mask_idx, "
413
+ f"and optionally full_q_cnt/full_q_idx). Regenerate the backward BlockMask with "
414
+ f"BLOCK_SIZE=({subtile_factor * m_block_size}, {n_block_size})."
415
+ ),
416
+ )
417
+ return normalized_tensors, get_block_sparse_broadcast_pattern(normalized_tensors)
418
+
419
+
420
+ def to_cute_block_sparse_tensors(
421
+ tensors: BlockSparseTensorsTorch, enable_tvm_ffi: bool = True
422
+ ) -> BlockSparseTensors | None:
423
+ """Convert torch block sparsity tensors to CuTe tensors, optionally for tvm ffi"""
424
+ if not is_block_sparsity_enabled(tensors):
425
+ return None
426
+
427
+ (
428
+ mask_block_cnt,
429
+ mask_block_idx,
430
+ full_block_cnt,
431
+ full_block_idx,
432
+ *_,
433
+ ) = tensors
434
+
435
+ (
436
+ mask_block_cnt_tensor,
437
+ mask_block_idx_tensor,
438
+ ) = [
439
+ to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
440
+ for t in (mask_block_cnt, mask_block_idx)
441
+ ]
442
+ (
443
+ full_block_cnt_tensor,
444
+ full_block_idx_tensor,
445
+ ) = [
446
+ to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
447
+ if t is not None
448
+ else None
449
+ for t in (full_block_cnt, full_block_idx)
450
+ ]
451
+
452
+ return BlockSparseTensors(
453
+ mask_block_cnt_tensor,
454
+ mask_block_idx_tensor,
455
+ full_block_cnt_tensor,
456
+ full_block_idx_tensor,
457
+ )
458
+
459
+
460
+ def fast_sampling(mask_mod):
461
+ """Convenience decorator to mark mask_mod as safe for 5-point fast sampling"""
462
+ mask_mod.use_fast_sampling = True
463
+ return mask_mod
torch-ext/sol_attn/_vendor/flash_attn/cute/cute_dsl_utils.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import os
4
+ import pathlib
5
+ from typing import Tuple
6
+ from functools import partial, lru_cache
7
+
8
+ import torch
9
+
10
+ try:
11
+ from triton.tools.disasm import extract
12
+ except ImportError:
13
+ extract = None
14
+
15
+ import cutlass
16
+ import cutlass.cute as cute
17
+ from cutlass.cutlass_dsl import NumericMeta
18
+ from cutlass.cute.runtime import from_dlpack
19
+
20
+ StaticTypes = (cutlass.Constexpr, NumericMeta, int, bool, str, float, type(None))
21
+
22
+
23
+ load_cubin_module_data_og = cutlass.base_dsl.runtime.cuda.load_cubin_module_data
24
+ cute_compile_og = cute.compile
25
+
26
+
27
+ torch2cute_dtype_map = {
28
+ torch.float16: cutlass.Float16,
29
+ torch.bfloat16: cutlass.BFloat16,
30
+ torch.float32: cutlass.Float32,
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 load_cubin_module_data_patched(cubin_data, filepath):
45
+ pathlib.Path(filepath).write_bytes(cubin_data)
46
+ return load_cubin_module_data_og(cubin_data)
47
+
48
+
49
+ def cute_compile_patched(*args, **kwargs):
50
+ """A patched version of cute.compile that dump the SASS to a file if CUTE_CUBIN_PATH is set."""
51
+ cubin_path = os.getenv("CUTE_CUBIN_PATH", None)
52
+ if cubin_path is not None:
53
+ cutlass.base_dsl.runtime.cuda.load_cubin_module_data = partial(
54
+ load_cubin_module_data_patched, filepath=cubin_path
55
+ )
56
+ output = cute_compile_og(*args, **kwargs)
57
+ if cubin_path is not None:
58
+ cutlass.base_dsl.runtime.cuda.load_cubin_module_data = load_cubin_module_data_og
59
+ if extract is not None:
60
+ sass = extract(cubin_path, None)
61
+ pathlib.Path(cubin_path).with_suffix(".annotated.sass").write_text(sass)
62
+ return output
63
+
64
+
65
+ def assume_strides_aligned(t):
66
+ """Assume all strides except the last are divisible by 128 bits.
67
+
68
+ Python int strides (e.g., stride=0 from GQA expand) are kept as-is
69
+ since they're static and don't need alignment assumptions.
70
+ """
71
+ divby = 128 // t.element_type.width
72
+ strides = tuple(s if isinstance(s, int) else cute.assume(s, divby=divby) for s in t.stride[:-1])
73
+ return (*strides, t.stride[-1])
74
+
75
+
76
+ def assume_tensor_aligned(t):
77
+ """Rebuild a tensor with 128-bit aligned stride assumptions. Passes through None."""
78
+ if t is None:
79
+ return None
80
+ return cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=assume_strides_aligned(t)))
81
+
82
+
83
+ def to_cute_tensor(t, assumed_align=16, leading_dim=-1, fully_dynamic=False, enable_tvm_ffi=True):
84
+ """Convert torch tensor to cute tensor for TVM FFI. leading_dim=-1 defaults to t.ndim-1."""
85
+ tensor = from_dlpack(t.detach(), assumed_align=assumed_align, enable_tvm_ffi=enable_tvm_ffi)
86
+ if fully_dynamic:
87
+ return tensor.mark_layout_dynamic()
88
+ if leading_dim == -1:
89
+ leading_dim = t.ndim - 1
90
+ return tensor.mark_layout_dynamic(leading_dim=leading_dim)
91
+
92
+
93
+ def to_cute_aux_tensor(t, enable_tvm_ffi=True):
94
+ """Convert torch tensor to cute tensor for TVM FFI, tailored to FlexAttention aux tensors.
95
+ This allows the user to specify alignment and leading dimension for aux tensors used in
96
+ custom score_mod callables.
97
+ """
98
+ assumed_align: int = getattr(t, "__assumed_align__", None)
99
+ leading_dim: int = getattr(t, "__leading_dim__", None)
100
+ fully_dynamic: bool = leading_dim is None
101
+
102
+ return to_cute_tensor(
103
+ t,
104
+ assumed_align=assumed_align,
105
+ leading_dim=leading_dim,
106
+ fully_dynamic=fully_dynamic,
107
+ enable_tvm_ffi=enable_tvm_ffi,
108
+ )
109
+
110
+
111
+ def get_aux_tensor_metadata(aux_tensors):
112
+ return tuple(
113
+ (
114
+ getattr(t, "__assumed_align__", 0),
115
+ getattr(t, "__leading_dim__", -1),
116
+ hasattr(t, "__leading_dim__"),
117
+ )
118
+ for t in aux_tensors
119
+ )
120
+
121
+
122
+ def get_broadcast_dims(tensor: torch.Tensor) -> Tuple[bool, ...]:
123
+ """Return tuple of bools indicating which dims have stride=0 (broadcast).
124
+
125
+ This is useful for compile keys since CuTe's mark_layout_dynamic() keeps
126
+ stride=0 as static, meaning kernels compiled with different broadcast
127
+ patterns are not interchangeable.
128
+ """
129
+ return tuple(s == 0 for s in tensor.stride())
torch-ext/sol_attn/_vendor/flash_attn/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
torch-ext/sol_attn/_vendor/flash_attn/cute/flash_fwd.py ADDED
@@ -0,0 +1,1218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of
3
+ # https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_kernel_sm80.h
4
+ # and https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_kernel_sm90.h
5
+ # from Cutlass C++ to Cute-DSL.
6
+ # Built on Cute-DSL example: https://github.com/NVIDIA/cutlass/blob/main/examples/python/CuTeDSL/ampere/flash_attention_v2.py
7
+
8
+ import math
9
+ from types import SimpleNamespace
10
+ from typing import Type, Callable, Optional, List
11
+ from functools import partial
12
+
13
+ import cuda.bindings.driver as cuda
14
+
15
+ import cutlass
16
+ import cutlass.cute as cute
17
+ from cutlass import Constexpr, Float32, Int32, const_expr, Boolean
18
+ from cutlass.cute.nvgpu import cpasync, warp
19
+ import cutlass.utils as utils_basic
20
+ from cutlass.base_dsl.arch import Arch
21
+ from cutlass.cutlass_dsl import BaseDSL
22
+
23
+ from ....sm90._compat import copy_utils
24
+ from ....sm90._compat import layout_utils
25
+
26
+ from ...._vendor.flash_attn.cute import ampere_helpers as sm80_utils
27
+ from ...._vendor.flash_attn.cute.cute_dsl_utils import assume_tensor_aligned
28
+ from ...._vendor.flash_attn.cute import utils
29
+ from ...._vendor.flash_attn.cute.mask import AttentionMask
30
+ from ...._vendor.flash_attn.cute.softmax import Softmax
31
+ from ...._vendor.flash_attn.cute.seqlen_info import SeqlenInfoQK
32
+ from ...._vendor.flash_attn.cute.block_info import BlockInfo
33
+ from ...._vendor.flash_attn.cute.pack_gqa import PackGQA
34
+ from ...._vendor.flash_attn.cute.named_barrier import NamedBarrierFwd
35
+ from ...._vendor.flash_attn.cute.block_sparsity import BlockSparseTensors
36
+ from ...._vendor.flash_attn.cute.tile_scheduler import (
37
+ SingleTileScheduler,
38
+ SingleTileVarlenScheduler,
39
+ TileSchedulerArguments,
40
+ )
41
+
42
+
43
+ class FlashAttentionForwardBase:
44
+
45
+ def __init__(
46
+ self,
47
+ dtype: Type[cutlass.Numeric],
48
+ head_dim: int,
49
+ head_dim_v: Optional[int] = None,
50
+ qhead_per_kvhead: int = 1,
51
+ is_causal: bool = False,
52
+ is_local: bool = False,
53
+ pack_gqa: bool = True,
54
+ tile_m: int = 128,
55
+ tile_n: int = 128,
56
+ num_stages: int = 1,
57
+ num_threads: int = 128,
58
+ Q_in_regs: bool = False,
59
+ score_mod: Optional[cutlass.Constexpr] = None,
60
+ mask_mod: Optional[cutlass.Constexpr] = None,
61
+ has_aux_tensors: bool = False,
62
+ q_subtile_factor: int | None = None,
63
+ ):
64
+ """Initializes the configuration for a flash attention kernel.
65
+
66
+ All contiguous dimensions must be at least 16 bytes aligned, which means that the head dimension
67
+ should be a multiple of 8.
68
+
69
+ :param head_dim: head dimension
70
+ :type head_dim: int
71
+ :param tile_m: m block size
72
+ :type tile_m: int
73
+ :param tile_n: n block size
74
+ :type tile_n: int
75
+ :param num_threads: number of threads
76
+ :type num_threads: int
77
+ :param is_causal: is causal
78
+ :param score_mod: A callable that takes the attention scores and applies a modification.
79
+ Callable signature: ``score_mod(scores, batch_idx, head_idx, q_idx, kv_idx, aux_tensors) -> Any``
80
+ :param mask_mod: A callable that takes the attention scores and returns a boolean representing whether that score should be masked.
81
+ Callable signature: ``mask_mod(batch_idx, head_idx, q_idx, kv_idx, aux_tensors) -> Boolean``
82
+ """
83
+ self.dtype = dtype
84
+ # padding head_dim to a multiple of 16 as k_block_size
85
+ hdim_multiple_of = 16
86
+ self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
87
+ head_dim_v = head_dim_v if head_dim_v is not None else head_dim
88
+ self.same_hdim_kv = head_dim == head_dim_v
89
+ self.tile_hdimv = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
90
+ # Can save registers (and hence be faster) if we don't have to check hdim predication
91
+ self.check_hdim_oob = head_dim != self.tile_hdim
92
+ self.check_hdim_v_oob = head_dim_v != self.tile_hdimv
93
+ self.qhead_per_kvhead = qhead_per_kvhead
94
+ self.is_causal = is_causal
95
+ self.is_local = is_local
96
+ self.pack_gqa = pack_gqa
97
+ self.tile_m = tile_m
98
+ self.tile_n = tile_n
99
+ self.num_threads = num_threads
100
+ self.num_stages = num_stages
101
+ self.q_subtile_factor = q_subtile_factor
102
+ self.Q_in_regs = Q_in_regs
103
+ self.score_mod = score_mod
104
+ self.mask_mod = mask_mod
105
+ self.qk_acc_dtype = Float32
106
+ self.vec_size: cutlass.Constexpr = getattr(
107
+ score_mod, "__vec_size__", 1 if cutlass.const_expr(has_aux_tensors) else 2
108
+ )
109
+ if self.vec_size > 2:
110
+ raise ValueError(
111
+ f"score_mod vec_size {self.vec_size} not supported on Sm80/90/120 "
112
+ "due to accumulator thread ownership pattern."
113
+ )
114
+ self.arch = BaseDSL._get_dsl().get_arch_enum()
115
+
116
+ @staticmethod
117
+ def can_implement(
118
+ dtype,
119
+ head_dim,
120
+ head_dim_v,
121
+ tile_m,
122
+ tile_n,
123
+ num_stages,
124
+ num_threads,
125
+ is_causal,
126
+ Q_in_regs=False,
127
+ ) -> bool:
128
+ """Check if the kernel can be implemented with the given parameters.
129
+
130
+ :param dtype: data type
131
+ :type dtype: cutlass.Numeric
132
+ :param head_dim: head dimension
133
+ :type head_dim: int
134
+ :param tile_m: m block size
135
+ :type tile_m: int
136
+ :param tile_n: n block size
137
+ :type tile_n: int
138
+ :param num_threads: number of threads
139
+ :type num_threads: int
140
+ :param is_causal: is causal
141
+ :type is_causal: bool
142
+
143
+ :return: True if the kernel can be implemented, False otherwise
144
+ :rtype: bool
145
+ """
146
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
147
+ return False
148
+ if head_dim % 8 != 0:
149
+ return False
150
+ if head_dim_v % 8 != 0:
151
+ return False
152
+ if tile_n % 16 != 0:
153
+ return False
154
+ if num_threads % 32 != 0:
155
+ return False
156
+ # Check if block size setting is out of shared memory capacity
157
+ # Shared memory usage: Q tile + (K tile + V tile) where K and V use the same tile size
158
+ smem_usage_Q = tile_m * head_dim * 2
159
+ smem_usage_K = tile_n * head_dim * num_stages * 2
160
+ smem_usage_V = tile_n * head_dim_v * num_stages * 2
161
+ smem_usage_QV = (
162
+ (smem_usage_Q + smem_usage_V) if not Q_in_regs else max(smem_usage_Q, smem_usage_V)
163
+ )
164
+ smem_usage = smem_usage_QV + smem_usage_K
165
+ # TODO: sm86 and sm89
166
+ smem_capacity = utils_basic.get_smem_capacity_in_bytes("sm_80")
167
+ if smem_usage > smem_capacity:
168
+ return False
169
+ # Check if twice the block size is divisible by the number of threads
170
+ if (tile_m * 2) % num_threads != 0:
171
+ return False
172
+ return True
173
+
174
+ def _check_type(
175
+ self,
176
+ mQ_type: Type[cutlass.Numeric],
177
+ mK_type: Type[cutlass.Numeric],
178
+ mV_type: Type[cutlass.Numeric],
179
+ mO_type: Type[cutlass.Numeric],
180
+ mLSE_type: Type[cutlass.Numeric] | None,
181
+ mCuSeqlensQ_type: Type[cutlass.Numeric] | None,
182
+ mCuSeqlensK_type: Type[cutlass.Numeric] | None,
183
+ mSeqUsedQ_type: Type[cutlass.Numeric] | None,
184
+ mSeqUsedK_type: Type[cutlass.Numeric] | None,
185
+ ):
186
+ # Get the data type and check if it is fp16 or bf16
187
+ if const_expr(not (mQ_type == mK_type == mV_type == mO_type)):
188
+ raise TypeError("All tensors must have the same data type")
189
+ if const_expr(mQ_type not in [cutlass.Float16, cutlass.BFloat16]):
190
+ raise TypeError("Only Float16 or BFloat16 is supported")
191
+ if const_expr(mLSE_type not in [None, Float32]):
192
+ raise TypeError("LSE tensor must be Float32")
193
+ if const_expr(mCuSeqlensQ_type not in [None, Int32]):
194
+ raise TypeError("cu_seqlens_q tensor must be Int32")
195
+ if const_expr(mCuSeqlensK_type not in [None, Int32]):
196
+ raise TypeError("cu_seqlens_k tensor must be Int32")
197
+ if const_expr(mSeqUsedQ_type not in [None, Int32]):
198
+ raise TypeError("seqused_q tensor must be Int32")
199
+ if const_expr(mSeqUsedK_type not in [None, Int32]):
200
+ raise TypeError("seqused_k tensor must be Int32")
201
+ assert mQ_type == self.dtype
202
+
203
+ def _setup_attributes(self):
204
+ # ///////////////////////////////////////////////////////////////////////////////
205
+ # Shared memory layout: Q/K/V
206
+ # ///////////////////////////////////////////////////////////////////////////////
207
+ sQ_layout_atom, sK_layout_atom, sV_layout_atom, sO_layout_atom, sP_layout_atom = (
208
+ self._get_smem_layout_atom()
209
+ )
210
+ self.sQ_layout = cute.tile_to_shape(
211
+ sQ_layout_atom,
212
+ (self.tile_m, self.tile_hdim),
213
+ (0, 1),
214
+ )
215
+ self.sK_layout = cute.tile_to_shape(
216
+ sK_layout_atom,
217
+ (self.tile_n, self.tile_hdim, self.num_stages),
218
+ (0, 1, 2),
219
+ )
220
+ self.sV_layout = cute.tile_to_shape(
221
+ sV_layout_atom,
222
+ (self.tile_n, self.tile_hdimv, self.num_stages),
223
+ (0, 1, 2),
224
+ )
225
+ self.sO_layout = cute.tile_to_shape(
226
+ sO_layout_atom,
227
+ (self.tile_m, self.tile_hdimv),
228
+ (0, 1),
229
+ )
230
+ if const_expr(sP_layout_atom is not None):
231
+ self.sP_layout = cute.tile_to_shape(
232
+ sP_layout_atom,
233
+ (self.tile_m, self.tile_n),
234
+ (0, 1),
235
+ )
236
+ else:
237
+ self.sP_layout = None
238
+
239
+ # ///////////////////////////////////////////////////////////////////////////////
240
+ # GMEM Tiled copy:
241
+ # ///////////////////////////////////////////////////////////////////////////////
242
+ # Thread layouts for copies
243
+ universal_copy_bits = 128
244
+ async_copy_elems = universal_copy_bits // self.dtype.width
245
+ # atom_async_copy: async copy atom for QKV load
246
+ atom_async_copy = cute.make_copy_atom(
247
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
248
+ self.dtype,
249
+ num_bits_per_copy=universal_copy_bits,
250
+ )
251
+ # atom_universal_copy: universal copy atom for O store
252
+ atom_universal_copy = cute.make_copy_atom(
253
+ cute.nvgpu.CopyUniversalOp(),
254
+ self.dtype,
255
+ num_bits_per_copy=universal_copy_bits,
256
+ )
257
+ # tQ_layout and tK_layout: thread layout for QK load
258
+ tQK_shape_dim_1 = sQ_layout_atom.outer.shape[1] // async_copy_elems
259
+ assert self.num_Q_load_threads % tQK_shape_dim_1 == 0, (
260
+ "num_threads must be divisible by tQK_shape_dim_1"
261
+ )
262
+ assert self.num_producer_threads % tQK_shape_dim_1 == 0, (
263
+ "num_threads must be divisible by tQK_shape_dim_1"
264
+ )
265
+ tQ_layout = cute.make_ordered_layout(
266
+ (self.num_Q_load_threads // tQK_shape_dim_1, tQK_shape_dim_1),
267
+ order=(1, 0),
268
+ )
269
+ tK_layout = cute.make_ordered_layout(
270
+ (self.num_producer_threads // tQK_shape_dim_1, tQK_shape_dim_1),
271
+ order=(1, 0),
272
+ )
273
+ # So that we don't have to check if we overshoot kBlockM when we load Q
274
+ assert self.tile_m % tQ_layout.shape[0] == 0
275
+ tV_shape_dim_1 = sV_layout_atom.outer.shape[1] // async_copy_elems
276
+ tV_layout = cute.make_ordered_layout(
277
+ (self.num_producer_threads // tV_shape_dim_1, tV_shape_dim_1),
278
+ order=(1, 0),
279
+ )
280
+ # TODO: need a different layout for O if O dtype is not the same as V dtype
281
+ # tO_layout: thread layout for O store
282
+ tO_layout = cute.make_ordered_layout(
283
+ (self.num_epilogue_threads // tV_shape_dim_1, tV_shape_dim_1),
284
+ order=(1, 0),
285
+ )
286
+ # So that we don't have to check if we overshoot kBlockM when we store O
287
+ assert self.tile_m % tO_layout.shape[0] == 0
288
+
289
+ # Value layouts for copies
290
+ vQKV_layout = cute.make_layout((1, async_copy_elems))
291
+ vO_layout = vQKV_layout
292
+
293
+ self.gmem_tiled_copy_Q = cute.make_tiled_copy_tv(atom_async_copy, tQ_layout, vQKV_layout)
294
+ self.gmem_tiled_copy_K = cute.make_tiled_copy_tv(atom_async_copy, tK_layout, vQKV_layout)
295
+ self.gmem_tiled_copy_V = cute.make_tiled_copy_tv(atom_async_copy, tV_layout, vQKV_layout)
296
+ # gmem_tiled_copy_O: tiled copy for O store
297
+ self.gmem_tiled_copy_O = cute.make_tiled_copy_tv(atom_universal_copy, tO_layout, vO_layout)
298
+
299
+ def _get_smem_layout_atom(self):
300
+ raise NotImplementedError()
301
+
302
+ def _get_tiled_mma(self):
303
+ raise NotImplementedError()
304
+
305
+ def _get_shared_storage_cls(self):
306
+ raise NotImplementedError()
307
+
308
+ @cute.jit
309
+ def __call__(
310
+ self,
311
+ mQ: cute.Tensor,
312
+ mK: cute.Tensor,
313
+ mV: cute.Tensor,
314
+ mO: cute.Tensor,
315
+ mLSE: Optional[cute.Tensor],
316
+ softmax_scale: Float32,
317
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
318
+ stream: cuda.CUstream = None,
319
+ ):
320
+ """Configures and launches the flash attention kernel.
321
+
322
+ mQ/mK/mV/mO has same data types(supports fp16 and bf16) and same layout:
323
+ (batch_size, seqlen_q, num_head, head_dim):(_, _, _, 1)
324
+ """
325
+ raise NotImplementedError()
326
+
327
+ @cute.jit
328
+ def epilogue(
329
+ self,
330
+ acc_O: cute.Tensor,
331
+ lse: cute.Tensor,
332
+ mO: cute.Tensor,
333
+ mLSE: Optional[cute.Tensor],
334
+ sO: cute.Tensor,
335
+ seqlen: SeqlenInfoQK,
336
+ gmem_tiled_copy_O: cute.TiledCopy,
337
+ tma_atom_O: Optional[cute.CopyAtom],
338
+ tiled_mma: cute.TiledMma,
339
+ tidx: Int32,
340
+ m_block: Int32,
341
+ head_idx: Int32,
342
+ batch_idx: Int32,
343
+ output_scale: Optional[cute.Tensor] = None,
344
+ ):
345
+ # store acc_O
346
+ rO = cute.make_fragment_like(acc_O, self.dtype)
347
+ if const_expr(output_scale is None):
348
+ rO.store(acc_O.load().to(self.dtype))
349
+ else:
350
+ # Fuse the final row normalization with the FP32 -> output dtype
351
+ # conversion. This avoids a separate full traversal of acc_O.
352
+ acc_O_mn = layout_utils.reshape_acc_to_mn(acc_O)
353
+ rO_mn = layout_utils.reshape_acc_to_mn(rO)
354
+ assert cute.size(output_scale) == cute.size(acc_O_mn, mode=[0])
355
+ for r in cutlass.range(cute.size(output_scale), unroll_full=True):
356
+ rO_mn[r, None].store(
357
+ (acc_O_mn[r, None].load() * output_scale[r]).to(self.dtype)
358
+ )
359
+ # Make sure all threads have finished reading V
360
+ cute.arch.barrier(
361
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_epilogue_threads
362
+ )
363
+ smem_copy_atom_O = utils.get_smem_store_atom(self.arch.major * 10 + self.arch.minor, self.dtype)
364
+ smem_thr_copy_O = cute.make_tiled_copy_C(smem_copy_atom_O, tiled_mma).get_slice(tidx)
365
+ taccOrO = smem_thr_copy_O.retile(rO)
366
+ taccOsO = smem_thr_copy_O.partition_D(sO)
367
+ # taccOsO = copy_utils.partition_D_position_independent(smem_thr_copy_O, sO)
368
+ # copy acc O from rmem to smem with the smem copy atom
369
+ cute.copy(smem_copy_atom_O, taccOrO, taccOsO)
370
+
371
+ cO = cute.make_identity_tensor((self.tile_m, self.tile_hdimv))
372
+ pack_gqa = PackGQA(
373
+ self.tile_m, self.tile_hdimv, self.check_hdim_v_oob, self.qhead_per_kvhead
374
+ )
375
+
376
+ # Write LSE from rmem -> gmem
377
+ if const_expr(mLSE is not None):
378
+ mLSE_cur = seqlen.offset_batch_Q(mLSE, batch_idx, dim=2)[None, head_idx]
379
+ if const_expr(not self.pack_gqa):
380
+ gLSE = cute.local_tile(mLSE_cur, (self.tile_m,), (m_block,))
381
+ gLSE_expanded_layout = cute.append(
382
+ gLSE.layout, cute.make_layout((self.tile_hdimv,), stride=(0,))
383
+ )
384
+ gLSE_expanded = cute.make_tensor(gLSE.iterator, gLSE_expanded_layout)
385
+ thr_mma = tiled_mma.get_slice(tidx)
386
+ taccOgLSE = layout_utils.reshape_acc_to_mn(thr_mma.partition_C(gLSE_expanded))
387
+ assert cute.size(taccOgLSE, mode=[0]) == cute.size(lse)
388
+ taccOcO = layout_utils.reshape_acc_to_mn(thr_mma.partition_C(cO))
389
+ t0accOcO = layout_utils.reshape_acc_to_mn(thr_mma.get_slice(0).partition_C(cO))
390
+ # Only the thread corresponding to column 0 writes out the lse to gmem
391
+ if taccOcO[0][1] == 0:
392
+ for m in cutlass.range(cute.size(taccOgLSE.shape[1]), unroll_full=True):
393
+ if (
394
+ t0accOcO[m, 0][0]
395
+ < seqlen.seqlen_q - m_block * self.tile_m - taccOcO[0][0]
396
+ ):
397
+ taccOgLSE[m, 0] = lse[m]
398
+ else:
399
+ pack_gqa.store_LSE(mLSE_cur, lse, tiled_mma, tidx, m_block, seqlen.seqlen_q)
400
+
401
+ ragged = self.use_tma_O and (seqlen.has_cu_seqlens_q or seqlen.has_seqused_q)
402
+ mO_cur = seqlen.offset_batch_Q(mO, batch_idx, dim=3, ragged=ragged)[None, None, head_idx]
403
+ # thr_mma = tiled_mma.get_slice(tidx)
404
+ # taccOgO = thr_mma.partition_C(gO)
405
+ # cute.autovec_copy(rO, taccOgO)
406
+ # sync to make sure all smem stores are done
407
+ if const_expr(self.use_tma_O):
408
+ # ensure smem writes are visible to TMA
409
+ cute.arch.fence_view_async_shared()
410
+ cute.arch.barrier_arrive(
411
+ barrier_id=int(NamedBarrierFwd.Epilogue),
412
+ number_of_threads=self.num_epilogue_threads + cute.arch.WARP_SIZE,
413
+ )
414
+ gO = cute.local_tile(mO_cur, (self.tile_m, self.tile_hdimv), (m_block, 0))
415
+ store_O, _, _ = copy_utils.tma_get_copy_fn(
416
+ tma_atom_O, 0, cute.make_layout(1), sO, gO, single_stage=True
417
+ )
418
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
419
+ if warp_idx == 4:
420
+ cute.arch.barrier(
421
+ barrier_id=int(NamedBarrierFwd.Epilogue),
422
+ number_of_threads=self.num_epilogue_threads + cute.arch.WARP_SIZE,
423
+ )
424
+ store_O()
425
+ cute.arch.cp_async_bulk_commit_group()
426
+ cute.arch.cp_async_bulk_wait_group(0, read=True)
427
+ else:
428
+ cute.arch.barrier(
429
+ barrier_id=int(NamedBarrierFwd.Epilogue),
430
+ number_of_threads=self.num_epilogue_threads,
431
+ )
432
+ gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
433
+ tOsO = gmem_thr_copy_O.partition_S(sO)
434
+ tOrO = cute.make_fragment_like(tOsO, self.dtype)
435
+ # load acc O from smem to rmem for wider vectorization
436
+ cute.autovec_copy(tOsO, tOrO)
437
+ if const_expr(not self.pack_gqa):
438
+ gO = cute.local_tile(mO_cur, (self.tile_m, self.tile_hdimv), (m_block, 0))
439
+ tOgO = gmem_thr_copy_O.partition_D(gO)
440
+ tOcO = gmem_thr_copy_O.partition_S(cO)
441
+ t0OcO = gmem_tiled_copy_O.get_slice(0).partition_S(cO)
442
+ tOpO = utils.predicate_k(tOcO, limit=mO.shape[1])
443
+ # copy acc O from rmem to gmem
444
+ for rest_m in cutlass.range_constexpr(cute.size(tOrO.shape[1])):
445
+ if (
446
+ t0OcO[0, rest_m, 0][0]
447
+ < seqlen.seqlen_q - m_block * self.tile_m - tOcO[0][0]
448
+ ):
449
+ cute.copy(
450
+ gmem_tiled_copy_O,
451
+ tOrO[None, rest_m, None],
452
+ tOgO[None, rest_m, None],
453
+ pred=tOpO[None, rest_m, None]
454
+ if const_expr(self.check_hdim_v_oob)
455
+ else None,
456
+ )
457
+ else:
458
+ pack_gqa.store_O(mO_cur, tOrO, gmem_tiled_copy_O, tidx, m_block, seqlen.seqlen_q)
459
+
460
+ @cute.jit
461
+ def advance_pipeline(self, pipeline_index):
462
+ return pipeline_index + 1 if pipeline_index < self.num_stages - 1 else 0
463
+
464
+ @cute.jit
465
+ def load_Q(
466
+ self,
467
+ gmem_thr_copy: cute.TiledCopy,
468
+ gQ: cute.Tensor,
469
+ sQ: cute.Tensor,
470
+ block: Int32,
471
+ seqlen: Int32,
472
+ headdim: Int32,
473
+ ):
474
+ tQsQ, tQgQ = gmem_thr_copy.partition_D(sQ), gmem_thr_copy.partition_S(gQ)
475
+ cQ = cute.make_identity_tensor((self.tile_m, self.tile_hdim))
476
+ tQcQ = gmem_thr_copy.partition_S(cQ)
477
+ t0QcQ = gmem_thr_copy.get_slice(0).partition_S(cQ)
478
+ tQpQ = utils.predicate_k(tQcQ, limit=headdim)
479
+ for m in cutlass.range_constexpr(cute.size(tQsQ.shape[1])):
480
+ # Instead of using tQcQ, we using t0QcQ and subtract the offset from the limit
481
+ # (seqlen - block * kBlockM). This is because the entries of t0QcQ are known at compile time.
482
+ if t0QcQ[0, m, 0][0] < seqlen - block * self.tile_m - tQcQ[0][0]:
483
+ cute.copy(
484
+ gmem_thr_copy,
485
+ tQgQ[None, m, None],
486
+ tQsQ[None, m, None],
487
+ pred=tQpQ[None, m, None] if const_expr(self.check_hdim_oob) else None,
488
+ )
489
+ # We don't need to clear the sQ smem tiles since we'll only write out the valid outputs
490
+
491
+ @cute.jit
492
+ def load_K(
493
+ self,
494
+ gmem_tiled_copy: cute.TiledCopy,
495
+ tKgK: cute.Tensor,
496
+ tKsK: cute.Tensor,
497
+ tKcK: cute.Tensor,
498
+ t0KcK: cute.Tensor,
499
+ tKpK: cute.Tensor,
500
+ block: Int32,
501
+ smem_pipe_write: Int32,
502
+ seqlen: Int32,
503
+ need_predicates: cutlass.Constexpr,
504
+ ):
505
+ # Do we need to check if we overshoot kBlockN when we load K?
506
+ is_even_n_smem_k = self.tile_n % gmem_tiled_copy.tiler_mn[0].shape == 0
507
+ if const_expr(need_predicates or not is_even_n_smem_k):
508
+ # Instead of using tKcK, we using t0KcK and subtract the offset from the limit
509
+ # (seqlen - block * kBlockN). This is because the entries of t0KcK are known at compile time.
510
+ if const_expr(is_even_n_smem_k):
511
+ seqlen_limit = seqlen - block * self.tile_n
512
+ else:
513
+ if const_expr(not need_predicates):
514
+ seqlen_limit = self.tile_n
515
+ else:
516
+ seqlen_limit = cutlass.min(seqlen - block * self.tile_n, self.tile_n)
517
+ seqlen_limit -= tKcK[0][0]
518
+ for n in cutlass.range_constexpr(cute.size(tKsK.shape[1])):
519
+ if t0KcK[0, n, 0][0] < seqlen_limit:
520
+ cute.copy(
521
+ gmem_tiled_copy,
522
+ tKgK[None, n, None, block],
523
+ tKsK[
524
+ None, n, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0
525
+ ],
526
+ pred=tKpK[None, n, None] if const_expr(self.check_hdim_oob) else None,
527
+ )
528
+ # We don't need to clear the sK smem tiles since we'll mask out the scores anyway.
529
+ else:
530
+ cute.copy(
531
+ gmem_tiled_copy,
532
+ tKgK[None, None, None, block],
533
+ tKsK[None, None, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0],
534
+ pred=tKpK if const_expr(self.check_hdim_oob) else None,
535
+ )
536
+
537
+ @cute.jit
538
+ def load_V(
539
+ self,
540
+ gmem_tiled_copy: cute.TiledCopy,
541
+ tVgV: cute.Tensor,
542
+ tVsV: cute.Tensor,
543
+ tVcV: cute.Tensor,
544
+ t0VcV: cute.Tensor,
545
+ tVpV: cute.Tensor,
546
+ block: Int32,
547
+ smem_pipe_write: Int32,
548
+ seqlen: Int32,
549
+ need_predicates: cutlass.Constexpr,
550
+ ):
551
+ # Do we need to check if we overshoot kBlockN when we load V?
552
+ is_even_n_smem_v = self.tile_n % gmem_tiled_copy.tiler_mn[0].shape == 0
553
+ if const_expr(need_predicates or not is_even_n_smem_v):
554
+ for n in cutlass.range_constexpr(cute.size(tVsV.shape[1])):
555
+ # If kBlockN doesn't evenly divide the tiled copy, only the last `n` needs to be checked
556
+ if (
557
+ is_even_n_smem_v
558
+ or n < cute.size(tVsV.shape[1]) - 1
559
+ or tVcV[0, n, 0][0] < self.tile_n
560
+ ):
561
+ predicate = tVpV[None, n, None] if const_expr(self.check_hdim_v_oob) else None
562
+ if const_expr(need_predicates):
563
+ seqlen_limit = seqlen - block * self.tile_n - tVcV[0][0]
564
+ predicate_n = t0VcV[0, n, 0][0] < seqlen_limit
565
+ predicate = cute.make_fragment_like(tVpV[None, 0, None])
566
+ for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
567
+ for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
568
+ predicate[i, k] = (
569
+ tVpV[i, n, k] if const_expr(self.check_hdim_v_oob) else True
570
+ ) and predicate_n
571
+ cute.copy(
572
+ gmem_tiled_copy,
573
+ tVgV[None, n, None, block],
574
+ tVsV[
575
+ None, n, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0
576
+ ],
577
+ pred=predicate,
578
+ )
579
+ else:
580
+ cute.copy(
581
+ gmem_tiled_copy,
582
+ tVgV[None, None, None, block],
583
+ tVsV[None, None, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0],
584
+ pred=tVpV if const_expr(self.check_hdim_v_oob) else None,
585
+ )
586
+
587
+
588
+ class FlashAttentionForwardSm80(FlashAttentionForwardBase):
589
+ def _get_smem_layout_atom(self):
590
+ sQ_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.tile_hdim)
591
+ sK_layout_atom = sQ_layout_atom
592
+ sV_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.tile_hdimv)
593
+ sO_layout_atom = sV_layout_atom
594
+ sP_layout_atom = None
595
+ return sQ_layout_atom, sK_layout_atom, sV_layout_atom, sO_layout_atom, sP_layout_atom
596
+
597
+ def _get_tiled_mma(self):
598
+ tiled_mma_qk = cute.make_tiled_mma(
599
+ warp.MmaF16BF16Op(self.dtype, Float32, (16, 8, 16)),
600
+ (self.num_threads // 32, 1, 1),
601
+ permutation_mnk=(self.num_threads // 32 * 16, 16, 16),
602
+ )
603
+ tiled_mma_pv = cute.make_tiled_mma(
604
+ warp.MmaF16BF16Op(self.dtype, Float32, (16, 8, 16)),
605
+ (self.num_threads // 32, 1, 1),
606
+ permutation_mnk=(self.num_threads // 32 * 16, 16, 16),
607
+ )
608
+ return tiled_mma_qk, tiled_mma_pv
609
+
610
+ def _get_shared_storage_cls(self):
611
+ sQ_struct, sK_struct, sV_struct = [
612
+ cute.struct.Align[cute.struct.MemRange[self.dtype, cute.cosize(layout)], 1024]
613
+ for layout in (self.sQ_layout, self.sK_layout, self.sV_layout)
614
+ ]
615
+ cosize_sQV = max(cute.cosize(self.sQ_layout), cute.cosize(self.sV_layout))
616
+ sQV_struct = cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sQV], 1024]
617
+
618
+ @cute.struct
619
+ class SharedStorageQKV:
620
+ sV: sV_struct
621
+ sQ: sQ_struct
622
+ sK: sK_struct
623
+
624
+ @cute.struct
625
+ class SharedStorageSharedQV:
626
+ sQ: sQV_struct
627
+ sK: sK_struct
628
+
629
+ return SharedStorageQKV if const_expr(not self.Q_in_regs) else SharedStorageSharedQV
630
+
631
+ @cute.jit
632
+ def __call__(
633
+ self,
634
+ mQ: cute.Tensor,
635
+ mK: cute.Tensor,
636
+ mV: cute.Tensor,
637
+ mO: cute.Tensor,
638
+ mLSE: Optional[cute.Tensor],
639
+ softmax_scale: Float32,
640
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
641
+ mCuSeqlensK: Optional[cute.Tensor] = None,
642
+ mSeqUsedQ: Optional[cute.Tensor] = None,
643
+ mSeqUsedK: Optional[cute.Tensor] = None,
644
+ mPageTable: Optional[cute.Tensor] = None,
645
+ window_size_left: Optional[Int32] = None,
646
+ window_size_right: Optional[Int32] = None,
647
+ learnable_sink: Optional[cute.Tensor] = None,
648
+ blocksparse_tensors: Optional[BlockSparseTensors] = None,
649
+ aux_tensors=None,
650
+ # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
651
+ stream: cuda.CUstream = None,
652
+ ):
653
+ """Configures and launches the flash attention kernel.
654
+
655
+ mQ/mK/mV/mO has same data types(supports fp16 and bf16) and same layout:
656
+ (batch_size, seqlen_q, num_head, head_dim):(_, _, _, 1)
657
+ """
658
+ assert learnable_sink is None, "Learnable sink is not supported in this kernel"
659
+ self._check_type(
660
+ *(t.element_type if t is not None else None for t in (mQ, mK, mV, mO, mLSE, mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK))
661
+ )
662
+ tiled_mma_qk, tiled_mma_pv = self._get_tiled_mma()
663
+ self.num_mma_threads = tiled_mma_pv.size
664
+ self.num_producer_threads = self.num_threads
665
+ self.num_Q_load_threads = self.num_threads
666
+ self.num_epilogue_threads = self.num_threads
667
+ # This synchronous warp-MMA implementation only constructs the
668
+ # vectorized register/SMEM output copy. When compiled for SM100 as a
669
+ # SOL_ATTN scaffold, selecting TMA solely from the target arch leaves the
670
+ # TMA store atom unset and fails IR verification.
671
+ self.use_tma_O = False
672
+ self._setup_attributes()
673
+ SharedStorage = self._get_shared_storage_cls()
674
+ mQ, mK, mV, mO = [assume_tensor_aligned(t) for t in (mQ, mK, mV, mO)]
675
+ # Layout permutation: 4D non-varlen vs 3D varlen
676
+ QO_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 2, 1]
677
+ KV_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensK is None) else [0, 2, 1]
678
+ mQ, mO = [
679
+ cute.make_tensor(t.iterator, cute.select(t.layout, mode=QO_layout_transpose))
680
+ for t in (mQ, mO)
681
+ ]
682
+ mK, mV = [
683
+ cute.make_tensor(t.iterator, cute.select(t.layout, mode=KV_layout_transpose))
684
+ for t in (mK, mV)
685
+ ]
686
+ if const_expr(mLSE is not None):
687
+ LSE_layout_transpose = [2, 1, 0] if const_expr(mCuSeqlensQ is None) else [1, 0]
688
+ mLSE = cute.make_tensor(mLSE.iterator, cute.select(mLSE.layout, mode=LSE_layout_transpose))
689
+ # TileScheduler for varlen, simple grid for non-varlen
690
+ if const_expr(mCuSeqlensQ is not None or mSeqUsedQ is not None):
691
+ TileScheduler = SingleTileVarlenScheduler
692
+ else:
693
+ TileScheduler = SingleTileScheduler
694
+ num_batch = (
695
+ mCuSeqlensQ.shape[0] - 1
696
+ if const_expr(mCuSeqlensQ is not None)
697
+ else mQ.shape[3]
698
+ )
699
+ tile_sched_args = TileSchedulerArguments(
700
+ num_block=cute.ceil_div(mQ.shape[0], self.tile_m),
701
+ num_head=cute.size(mQ.shape[2]),
702
+ num_batch=num_batch,
703
+ num_splits=getattr(self, "sol_attn_v_splits", 1),
704
+ seqlen_k=0,
705
+ headdim=mQ.shape[1],
706
+ headdim_v=mV.shape[1],
707
+ total_q=cute.size(mQ.shape[0])
708
+ if const_expr(mCuSeqlensQ is not None)
709
+ else cute.size(mQ.shape[0]) * cute.size(mQ.shape[3]),
710
+ tile_shape_mn=(self.tile_m, self.tile_n),
711
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
712
+ mCuSeqlensQ=mCuSeqlensQ,
713
+ mSeqUsedQ=mSeqUsedQ,
714
+ is_split_kv=getattr(self, "sol_attn_v_split_d64", False),
715
+ )
716
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
717
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
718
+ softmax_scale_log2, softmax_scale = utils.compute_softmax_scale_log2(softmax_scale, self.score_mod)
719
+ fastdiv_mods = utils.compute_fastdiv_mods(mQ, mK, self.qhead_per_kvhead, self.pack_gqa, aux_tensors)
720
+
721
+ kernel_args = (
722
+ mQ,
723
+ mK,
724
+ mV,
725
+ mO,
726
+ mLSE,
727
+ mCuSeqlensQ,
728
+ mCuSeqlensK,
729
+ mSeqUsedQ,
730
+ mSeqUsedK,
731
+ softmax_scale_log2,
732
+ softmax_scale,
733
+ window_size_left,
734
+ window_size_right,
735
+ self.sQ_layout,
736
+ self.sK_layout,
737
+ self.sV_layout,
738
+ self.sO_layout,
739
+ self.sP_layout,
740
+ self.gmem_tiled_copy_Q,
741
+ self.gmem_tiled_copy_K,
742
+ self.gmem_tiled_copy_V,
743
+ self.gmem_tiled_copy_O,
744
+ tiled_mma_qk,
745
+ tiled_mma_pv,
746
+ )
747
+ kernel_tail = (
748
+ SharedStorage,
749
+ tile_sched_params,
750
+ TileScheduler,
751
+ aux_tensors,
752
+ fastdiv_mods,
753
+ )
754
+ kernel = self.kernel(*kernel_args, *kernel_tail)
755
+ kernel.launch(
756
+ grid=grid_dim,
757
+ block=[self.num_threads, 1, 1],
758
+ smem=SharedStorage.size_in_bytes(),
759
+ min_blocks_per_mp=getattr(self, "sol_attn_min_blocks_per_mp", 0),
760
+ stream=stream,
761
+ )
762
+
763
+ @cute.kernel
764
+ def kernel(
765
+ self,
766
+ mQ: cute.Tensor,
767
+ mK: cute.Tensor,
768
+ mV: cute.Tensor,
769
+ mO: cute.Tensor,
770
+ mLSE: Optional[cute.Tensor],
771
+ mCuSeqlensQ: Optional[cute.Tensor],
772
+ mCuSeqlensK: Optional[cute.Tensor],
773
+ mSeqUsedQ: Optional[cute.Tensor],
774
+ mSeqUsedK: Optional[cute.Tensor],
775
+ softmax_scale_log2: Float32,
776
+ softmax_scale: Optional[Float32],
777
+ window_size_left: Optional[Int32],
778
+ window_size_right: Optional[Int32],
779
+ sQ_layout: cute.ComposedLayout,
780
+ sK_layout: cute.ComposedLayout,
781
+ sV_layout: cute.ComposedLayout,
782
+ sO_layout: cute.ComposedLayout,
783
+ sP_layout: cute.ComposedLayout | None,
784
+ gmem_tiled_copy_Q: cute.TiledCopy,
785
+ gmem_tiled_copy_K: cute.TiledCopy,
786
+ gmem_tiled_copy_V: cute.TiledCopy,
787
+ gmem_tiled_copy_O: cute.TiledCopy,
788
+ tiled_mma_qk: cute.TiledMma,
789
+ tiled_mma_pv: cute.TiledMma,
790
+ SharedStorage: cutlass.Constexpr,
791
+ tile_sched_params,
792
+ TileScheduler: cutlass.Constexpr[Callable],
793
+ aux_tensors=None,
794
+ fastdiv_mods=None,
795
+ ):
796
+ # Thread index, block index
797
+ tidx, _, _ = cute.arch.thread_idx()
798
+
799
+ tile_scheduler = TileScheduler.create(tile_sched_params)
800
+ work_tile = tile_scheduler.initial_work_tile_info()
801
+ m_block, num_head, batch_size, _ = work_tile.tile_idx
802
+
803
+ block_info = BlockInfo(
804
+ self.tile_m,
805
+ self.tile_n,
806
+ self.is_causal,
807
+ self.is_local,
808
+ False, # is_split_kv
809
+ window_size_left,
810
+ window_size_right,
811
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
812
+ )
813
+ seqlen = SeqlenInfoQK.create(
814
+ batch_idx=batch_size,
815
+ seqlen_q_static=mQ.shape[0],
816
+ seqlen_k_static=mK.shape[0],
817
+ mCuSeqlensQ=mCuSeqlensQ,
818
+ mCuSeqlensK=mCuSeqlensK,
819
+ mSeqUsedQ=mSeqUsedQ,
820
+ mSeqUsedK=mSeqUsedK,
821
+ )
822
+ n_block_min, n_block_max = block_info.get_n_block_min_max(seqlen, m_block)
823
+ # For varlen, wasted grid tiles (where batch_idx >= num_batch) will have
824
+ # seqlen_q=seqlen_k=0 and n_block_max=0. Clamp to 0 so we don't use a
825
+ # negative block index for K/V loads; the load/store predicates already
826
+ # guard all memory accesses when seqlen is 0.
827
+ n_block = cutlass.max(n_block_max - 1, 0)
828
+
829
+ # ///////////////////////////////////////////////////////////////////////////////
830
+ # Get the appropriate tiles for this thread block.
831
+ # ///////////////////////////////////////////////////////////////////////////////
832
+ blkQ_shape = (self.tile_m, self.tile_hdim)
833
+ blkK_shape = (self.tile_n, self.tile_hdim)
834
+ blkV_shape = (self.tile_n, self.tile_hdimv)
835
+ num_head_kv = num_head // self.qhead_per_kvhead
836
+ if const_expr(not seqlen.has_cu_seqlens_q):
837
+ mQ_cur = mQ[None, None, num_head, batch_size]
838
+ else:
839
+ mQ_cur = cute.domain_offset((seqlen.offset_q, 0), mQ[None, None, num_head])
840
+ if const_expr(not seqlen.has_cu_seqlens_k):
841
+ mK_cur = mK[None, None, num_head_kv, batch_size]
842
+ mV_cur = mV[None, None, num_head_kv, batch_size]
843
+ else:
844
+ mK_cur = cute.domain_offset((seqlen.offset_k, 0), mK[None, None, num_head_kv])
845
+ mV_cur = cute.domain_offset((seqlen.offset_k, 0), mV[None, None, num_head_kv])
846
+ gQ = cute.local_tile(mQ_cur, blkQ_shape, (m_block, 0))
847
+ gK = cute.local_tile(mK_cur, blkK_shape, (None, 0))
848
+ gV = cute.local_tile(mV_cur, blkV_shape, (None, 0))
849
+
850
+ # ///////////////////////////////////////////////////////////////////////////////
851
+ # Get shared memory buffer
852
+ # ///////////////////////////////////////////////////////////////////////////////
853
+ smem = cutlass.utils.SmemAllocator()
854
+ storage = smem.allocate(SharedStorage)
855
+ sQ = storage.sQ.get_tensor(sQ_layout)
856
+ sK = storage.sK.get_tensor(sK_layout)
857
+ if const_expr(not self.Q_in_regs):
858
+ sV = storage.sV.get_tensor(sV_layout)
859
+ else:
860
+ sV = cute.make_tensor(cute.recast_ptr(sQ.iterator, dtype=self.dtype), sV_layout)
861
+ # Transpose view of V to tensor with layout (head_dim_v, tile_n) for tiled mma
862
+ sVt = layout_utils.transpose_view(sV)
863
+
864
+ gmem_thr_copy_K = gmem_tiled_copy_K.get_slice(tidx)
865
+ gmem_thr_copy_V = gmem_tiled_copy_V.get_slice(tidx)
866
+ # (CPY_Atom, CPY_N, CPY_K, n_block)
867
+ tKsK, tKgK = gmem_thr_copy_K.partition_D(sK), gmem_thr_copy_K.partition_S(gK)
868
+ # (CPY_Atom, CPY_N, CPY_K, n_block)
869
+ tVsV, tVgV = gmem_thr_copy_V.partition_D(sV), gmem_thr_copy_V.partition_S(gV)
870
+
871
+ # ///////////////////////////////////////////////////////////////////////////////
872
+ # Tile MMA compute thread partitions and allocate accumulators
873
+ # ///////////////////////////////////////////////////////////////////////////////
874
+ thr_mma_qk = tiled_mma_qk.get_slice(tidx)
875
+ thr_mma_pv = tiled_mma_pv.get_slice(tidx)
876
+ tSrQ = thr_mma_qk.make_fragment_A(thr_mma_qk.partition_A(sQ))
877
+ tSrK = thr_mma_qk.make_fragment_B(thr_mma_qk.partition_B(sK[None, None, 0]))
878
+ tOrVt = thr_mma_pv.make_fragment_B(thr_mma_pv.partition_B(sVt[None, None, 0]))
879
+ acc_shape_O = thr_mma_pv.partition_shape_C((self.tile_m, self.tile_hdimv))
880
+ acc_O = cute.make_rmem_tensor(acc_shape_O, Float32)
881
+ acc_O.fill(0.0)
882
+
883
+ # ///////////////////////////////////////////////////////////////////////////////
884
+ # Smem copy atom tiling
885
+ # ///////////////////////////////////////////////////////////////////////////////
886
+ smem_copy_atom_QK = cute.make_copy_atom(
887
+ warp.LdMatrix8x8x16bOp(transpose=False, num_matrices=4),
888
+ self.dtype,
889
+ )
890
+ smem_copy_atom_V = cute.make_copy_atom(
891
+ warp.LdMatrix8x8x16bOp(transpose=True, num_matrices=4),
892
+ self.dtype,
893
+ )
894
+ smem_thr_copy_Q = utils.make_tiled_copy_A(smem_copy_atom_QK, tiled_mma_qk).get_slice(tidx)
895
+ smem_thr_copy_K = utils.make_tiled_copy_B(smem_copy_atom_QK, tiled_mma_qk).get_slice(tidx)
896
+ smem_thr_copy_V = utils.make_tiled_copy_B(smem_copy_atom_V, tiled_mma_pv).get_slice(tidx)
897
+
898
+ tSsQ = smem_thr_copy_Q.partition_S(sQ)
899
+ tSsK = smem_thr_copy_K.partition_S(sK)
900
+ tOsVt = smem_thr_copy_V.partition_S(sVt)
901
+
902
+ # ///////////////////////////////////////////////////////////////////////////////
903
+ # Predicate: Mark indices that need to copy when problem_shape isn't a multiple
904
+ # of tile_shape
905
+ # ///////////////////////////////////////////////////////////////////////////////
906
+ # Construct identity layout for KV
907
+ cK = cute.make_identity_tensor((self.tile_n, self.tile_hdim))
908
+ tKcK = gmem_thr_copy_K.partition_S(cK)
909
+ t0KcK = gmem_thr_copy_K.get_slice(0).partition_S(cK)
910
+ if const_expr(self.tile_hdim == self.tile_hdimv):
911
+ tVcV = tKcK
912
+ t0VcV = t0KcK
913
+ else:
914
+ cV = cute.make_identity_tensor((self.tile_n, self.tile_hdimv))
915
+ tVcV = gmem_thr_copy_V.partition_S(cV)
916
+ t0VcV = gmem_thr_copy_V.get_slice(0).partition_S(cV)
917
+ # Allocate predicate tensors for m and n, here we only allocate the tile of k, and
918
+ # use "if" on the mn dimension.
919
+ # This is to reduce register pressure and gets 2-3% performance gain.
920
+ tKpK = utils.predicate_k(tKcK, limit=mK.shape[1])
921
+ if const_expr(self.same_hdim_kv):
922
+ tVpV = tKpK
923
+ else:
924
+ tVpV = utils.predicate_k(tVcV, limit=mV.shape[1])
925
+
926
+ # shape: (atom_v_m * rest_m)
927
+ softmax = Softmax.create(
928
+ softmax_scale_log2,
929
+ num_rows=acc_O.shape[0][0] * acc_O.shape[1],
930
+ softmax_scale=softmax_scale,
931
+ )
932
+ softmax.reset()
933
+
934
+ # group parameters for compute_one_n_block
935
+ mma_params = SimpleNamespace(
936
+ thr_mma_qk=thr_mma_qk,
937
+ thr_mma_pv=thr_mma_pv,
938
+ tSrQ=tSrQ,
939
+ tSrK=tSrK,
940
+ tOrVt=tOrVt,
941
+ acc_O=acc_O,
942
+ )
943
+ smem_copy_params = SimpleNamespace(
944
+ smem_thr_copy_Q=smem_thr_copy_Q,
945
+ smem_thr_copy_K=smem_thr_copy_K,
946
+ smem_thr_copy_V=smem_thr_copy_V,
947
+ tSsQ=tSsQ,
948
+ tSsK=tSsK,
949
+ tOsVt=tOsVt,
950
+ )
951
+ load_K = partial(
952
+ self.load_K, gmem_tiled_copy_K, tKgK, tKsK, tKcK, t0KcK, tKpK, seqlen=seqlen.seqlen_k
953
+ )
954
+ load_V = partial(
955
+ self.load_V, gmem_tiled_copy_V, tVgV, tVsV, tVcV, t0VcV, tVpV, seqlen=seqlen.seqlen_k
956
+ )
957
+
958
+ compute_one_n_block = partial(
959
+ self.compute_one_n_block,
960
+ mma_params=mma_params,
961
+ smem_copy_params=smem_copy_params,
962
+ softmax=softmax,
963
+ load_K=load_K,
964
+ load_V=load_V,
965
+ score_mod=self.score_mod,
966
+ batch_idx=batch_size,
967
+ head_idx=num_head,
968
+ m_block=m_block,
969
+ aux_tensors=aux_tensors,
970
+ fastdiv_mods=fastdiv_mods,
971
+ )
972
+
973
+ # ///////////////////////////////////////////////////////////////////////////////
974
+ # Prologue
975
+ # ///////////////////////////////////////////////////////////////////////////////
976
+ # Start async loads of the last mn-tile, where we take care of the mn residue
977
+ gmem_thr_copy_Q = gmem_tiled_copy_Q.get_slice(tidx)
978
+ self.load_Q(gmem_thr_copy_Q, gQ, sQ, m_block, seqlen=seqlen.seqlen_q, headdim=mQ.shape[1])
979
+ cute.arch.cp_async_commit_group()
980
+
981
+ def preprocess_Q():
982
+ cute.arch.cp_async_wait_group(self.num_stages * 2 - 1)
983
+ if const_expr(self.Q_in_regs):
984
+ cute.arch.barrier()
985
+ tSrQ_copy_view = smem_thr_copy_Q.retile(tSrQ)
986
+ cute.copy(smem_thr_copy_Q, tSsQ, tSrQ_copy_view)
987
+
988
+ # If Q_in_regs, we load Q, then load 1 stage of K, then (optionally) rotate Q and
989
+ # read from smem_q to registers, then load V.
990
+ # If !Q_in_regs, we load Q, load all stages of K & V, then (optionally) rotate Q.
991
+ if const_expr(self.Q_in_regs):
992
+ load_K(n_block, smem_pipe_write=0, need_predicates=True)
993
+ cute.arch.cp_async_commit_group()
994
+ preprocess_Q()
995
+ cute.arch.barrier() # Make sure all threads have read smem_q before loading V
996
+
997
+ for stage in cutlass.range_constexpr(self.num_stages):
998
+ if const_expr(not self.Q_in_regs or stage > 0):
999
+ if stage == 0 or n_block - stage >= 0:
1000
+ load_K(n_block - stage, smem_pipe_write=stage, need_predicates=stage == 0)
1001
+ cute.arch.cp_async_commit_group()
1002
+ if const_expr(stage < self.num_stages - 1):
1003
+ if stage == 0 or n_block - stage >= 0:
1004
+ load_V(n_block - stage, smem_pipe_write=stage, need_predicates=stage == 0)
1005
+ cute.arch.cp_async_commit_group()
1006
+ if const_expr(not self.Q_in_regs):
1007
+ preprocess_Q()
1008
+
1009
+ # ///////////////////////////////////////////////////////////////////////////////
1010
+ # Mainloop
1011
+ # ///////////////////////////////////////////////////////////////////////////////
1012
+ # Start processing of the first n-block.
1013
+ # For performance reason, we separate out two kinds of iterations:
1014
+ # those that need masking on S, and those that don't.
1015
+ # We need masking on S for the very last block when K and V has length not multiple of tile_n.
1016
+ # We also need masking on S if it's causal, for the last several blocks.
1017
+ mask = AttentionMask(
1018
+ self.tile_m,
1019
+ self.tile_n,
1020
+ seqlen,
1021
+ window_size_left,
1022
+ window_size_right,
1023
+ self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
1024
+ )
1025
+ mask_fn = partial(
1026
+ mask.apply_mask,
1027
+ batch_idx=batch_size,
1028
+ head_idx=num_head,
1029
+ m_block=m_block,
1030
+ thr_mma=thr_mma_qk,
1031
+ mask_causal=self.is_causal,
1032
+ mask_local=self.is_local,
1033
+ aux_tensors=aux_tensors,
1034
+ fastdiv_mods=fastdiv_mods if const_expr(self.mask_mod is not None) else None,
1035
+ )
1036
+
1037
+ # First iteration with seqlen masking
1038
+ smem_pipe_read = Int32(0)
1039
+ smem_pipe_write = Int32(self.num_stages - 1)
1040
+ compute_one_n_block(
1041
+ n_block,
1042
+ smem_pipe_read,
1043
+ smem_pipe_write,
1044
+ is_first_n_block=True,
1045
+ seqlen=seqlen,
1046
+ mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=True),
1047
+ )
1048
+ smem_pipe_read = self.advance_pipeline(smem_pipe_read)
1049
+ smem_pipe_write = self.advance_pipeline(smem_pipe_write)
1050
+ # Next couple of iterations with causal masking
1051
+ if const_expr(self.is_causal or self.is_local):
1052
+ n_block_min_causal_local_mask = block_info.get_n_block_min_causal_local_mask(
1053
+ seqlen, m_block, n_block_min
1054
+ )
1055
+ for n_tile in cutlass.range(n_block_max - 1 - n_block_min_causal_local_mask, unroll=1):
1056
+ n_block = n_block_max - 2 - n_tile
1057
+ compute_one_n_block(
1058
+ n_block,
1059
+ smem_pipe_read,
1060
+ smem_pipe_write,
1061
+ seqlen=seqlen,
1062
+ mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=True),
1063
+ )
1064
+ smem_pipe_read = self.advance_pipeline(smem_pipe_read)
1065
+ smem_pipe_write = self.advance_pipeline(smem_pipe_write)
1066
+ # The remaining iterations have no masking
1067
+ for n_tile in cutlass.range(n_block, unroll=1):
1068
+ compute_one_n_block(
1069
+ n_block - n_tile - 1, smem_pipe_read, smem_pipe_write,
1070
+ seqlen=seqlen, is_first_n_block=False,
1071
+ mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=False)
1072
+ )
1073
+ smem_pipe_read = self.advance_pipeline(smem_pipe_read)
1074
+ smem_pipe_write = self.advance_pipeline(smem_pipe_write)
1075
+ # TODO: local
1076
+
1077
+ # normalize acc_O by row_sum and calculate the lse
1078
+ row_scale = softmax.finalize()
1079
+ softmax.rescale_O(acc_O, row_scale)
1080
+
1081
+ # ///////////////////////////////////////////////////////////////////////////////
1082
+ # Epilogue
1083
+ # ///////////////////////////////////////////////////////////////////////////////
1084
+ # reuse sQ's data iterator
1085
+ sO = cute.make_tensor(sQ.iterator, sO_layout)
1086
+ self.epilogue(
1087
+ acc_O,
1088
+ softmax.row_sum,
1089
+ mO,
1090
+ mLSE,
1091
+ sO,
1092
+ seqlen,
1093
+ gmem_tiled_copy_O,
1094
+ None,
1095
+ tiled_mma_pv,
1096
+ tidx,
1097
+ m_block,
1098
+ num_head,
1099
+ batch_size,
1100
+ )
1101
+
1102
+ @cute.jit
1103
+ def compute_one_n_block(
1104
+ self,
1105
+ n_block: Int32,
1106
+ smem_pipe_read: Int32,
1107
+ smem_pipe_write: Int32,
1108
+ mma_params: SimpleNamespace,
1109
+ smem_copy_params: SimpleNamespace,
1110
+ softmax: Softmax,
1111
+ load_K: Callable,
1112
+ load_V: Callable,
1113
+ score_mod: Callable | None,
1114
+ batch_idx: cutlass.Int32,
1115
+ head_idx: cutlass.Int32,
1116
+ m_block: cutlass.Int32,
1117
+ seqlen: SeqlenInfoQK,
1118
+ aux_tensors=None,
1119
+ fastdiv_mods=None,
1120
+ mask_fn: Optional[Callable] = None,
1121
+ is_first_n_block: cutlass.Constexpr = False,
1122
+ check_inf: cutlass.Constexpr = True,
1123
+ ):
1124
+ """Compute one n_block of S/O.
1125
+
1126
+ This function provides different variants for processing the first n block versus
1127
+ subsequent blocks.
1128
+ """
1129
+
1130
+ def sync():
1131
+ cute.arch.cp_async_wait_group(self.num_stages * 2 - 2)
1132
+ cute.arch.barrier()
1133
+
1134
+ acc_shape_S = mma_params.thr_mma_qk.partition_shape_C((self.tile_m, self.tile_n))
1135
+ acc_S = cute.make_rmem_tensor(acc_shape_S, Float32)
1136
+ acc_S.fill(0.0)
1137
+ # wait for smem tile QK before mma calculation for S
1138
+ sync()
1139
+
1140
+ # need predicates for the first tile
1141
+ def load_V_next():
1142
+ if self.num_stages == 1 or n_block - self.num_stages + 1 >= 0:
1143
+ load_V(
1144
+ n_block - self.num_stages + 1,
1145
+ smem_pipe_write,
1146
+ need_predicates=is_first_n_block and self.num_stages == 1,
1147
+ )
1148
+ cute.arch.cp_async_commit_group()
1149
+
1150
+ load_V_next()
1151
+ sm80_utils.gemm(
1152
+ mma_params.thr_mma_qk,
1153
+ acc_S,
1154
+ mma_params.tSrQ,
1155
+ mma_params.tSrK,
1156
+ smem_copy_params.tSsQ,
1157
+ smem_copy_params.tSsK[
1158
+ None, None, None, smem_pipe_read if const_expr(self.num_stages > 1) else 0
1159
+ ],
1160
+ smem_copy_params.smem_thr_copy_Q,
1161
+ smem_copy_params.smem_thr_copy_K,
1162
+ # hook_fn=load_V_next,
1163
+ A_in_regs=self.Q_in_regs,
1164
+ )
1165
+ if const_expr(score_mod is not None):
1166
+ self.apply_score_mod(
1167
+ mma_params.thr_mma_qk,
1168
+ batch_idx,
1169
+ head_idx,
1170
+ m_block,
1171
+ acc_S,
1172
+ n_block,
1173
+ seqlen,
1174
+ softmax_scale=softmax.softmax_scale,
1175
+ aux_tensors=aux_tensors,
1176
+ fastdiv_mods=fastdiv_mods,
1177
+ )
1178
+
1179
+ smem_pipe_write = self.advance_pipeline(smem_pipe_write)
1180
+
1181
+ def load_K_next():
1182
+ if n_block - self.num_stages >= 0:
1183
+ load_K(n_block - self.num_stages, smem_pipe_write, need_predicates=False)
1184
+ cute.arch.cp_async_commit_group()
1185
+
1186
+ # wait for smem tile V for O
1187
+ if const_expr(self.num_stages == 1):
1188
+ sync()
1189
+ load_K_next()
1190
+ if const_expr(mask_fn is not None):
1191
+ mask_fn(acc_S, n_block=n_block)
1192
+ row_scale = softmax.online_softmax(acc_S, is_first=is_first_n_block, check_inf=check_inf)
1193
+ softmax.rescale_O(mma_params.acc_O, row_scale)
1194
+ rP = cute.make_fragment_like(acc_S, self.dtype)
1195
+ rP.store(acc_S.load().to(self.dtype))
1196
+ tOrP = layout_utils.reshape_acc_to_frgA(rP)
1197
+ if const_expr(self.num_stages > 1):
1198
+ sync()
1199
+ load_K_next()
1200
+ sm80_utils.gemm_rs(
1201
+ mma_params.thr_mma_pv,
1202
+ mma_params.acc_O,
1203
+ tOrP,
1204
+ mma_params.tOrVt,
1205
+ smem_copy_params.tOsVt[
1206
+ None, None, None, smem_pipe_read if const_expr(self.num_stages > 1) else 0
1207
+ ],
1208
+ smem_copy_params.smem_thr_copy_V,
1209
+ # hook_fn=load_K_next,
1210
+ )
1211
+ # if const_expr(self.num_stages > 1):
1212
+ # load_K_next()
1213
+
1214
+
1215
+ def __getattr__(name):
1216
+ if name == "FlashAttentionForwardSm90":
1217
+ raise AttributeError("FlashAttentionForwardSm90 is not vendored in the SOL_ATTN release")
1218
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
torch-ext/sol_attn/_vendor/flash_attn/cute/mask.py ADDED
@@ -0,0 +1,712 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Optional, Callable, TypeAlias
4
+ from dataclasses import dataclass
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+ from cutlass import Float32, Int32, Uint32, const_expr
9
+
10
+ from ....sm90._compat import layout_utils
11
+ from . import utils
12
+ from ...._vendor.flash_attn.cute.seqlen_info import SeqlenInfoQK
13
+
14
+ MaskGenFn: TypeAlias = Callable[[int], Uint32]
15
+ MASK_R2P_CHUNK_SIZE: int = 32
16
+
17
+
18
+ @cute.jit
19
+ def r2p_bitmask_below(limit: Int32, s: int) -> Uint32:
20
+ """32-bit R2P bitmask keeping positions < limit (exclusive upper bound).
21
+
22
+ Positions 0..limit-1 in chunk `s` get bit=1 (keep), the rest bit=0 (mask).
23
+ Uses inline PTX to avoid shift-by-type-width UB.
24
+ """
25
+ m = max((s + 1) * MASK_R2P_CHUNK_SIZE - limit, 0)
26
+ return utils.shr_u32(Uint32(0xFFFFFFFF), Uint32(m))
27
+
28
+
29
+ @cute.jit
30
+ def r2p_bitmask_above(limit: Int32, s: int) -> Uint32:
31
+ """32-bit R2P bitmask keeping positions >= limit (inclusive lower bound).
32
+
33
+ Positions limit..31 in chunk `s` get bit=1 (keep), the rest bit=0 (mask).
34
+ Uses inline PTX to avoid shift-by-type-width UB.
35
+ """
36
+ n = max(limit - s * MASK_R2P_CHUNK_SIZE, 0)
37
+ return utils.shl_u32(Uint32(0xFFFFFFFF), Uint32(n))
38
+
39
+
40
+ @cute.jit
41
+ def mask_r2p_lambda(
42
+ X: cute.Tensor,
43
+ mask_gen_fn: cutlass.Constexpr[MaskGenFn],
44
+ rank1: bool = False,
45
+ ) -> None:
46
+ """Apply R2P masking with a custom bitmask generator.
47
+
48
+ mask_gen_fn(chunk_idx: constexpr int) -> Uint32:
49
+ Returns a 32-bit bitmask for the chunk. Bit i set means column
50
+ chunk_idx * chunk_size + i is KEPT; bit i clear means masked to -inf.
51
+ """
52
+ ncol = const_expr(cute.size(X.shape[cute.rank(X) - 1]) if not rank1 else cute.size(X.shape))
53
+ # 32-column chunks. The mask_gen_fn returns a Uint32 bitmask (1=keep).
54
+ CHUNK_SIZE = MASK_R2P_CHUNK_SIZE
55
+ for s in cutlass.range_constexpr(cute.ceil_div(ncol, CHUNK_SIZE)):
56
+ mask = mask_gen_fn(s)
57
+ # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
58
+ for i in cutlass.range_constexpr(min(CHUNK_SIZE, ncol - s * CHUNK_SIZE)):
59
+ in_bound = cutlass.Boolean(mask & (Uint32(1) << i))
60
+ c = s * CHUNK_SIZE + i
61
+ if const_expr(rank1):
62
+ X[c] = X[c] if in_bound else -Float32.inf
63
+ else:
64
+ for r in cutlass.range_constexpr(cute.size(X.shape[0])):
65
+ X[r, c] = X[r, c] if in_bound else -Float32.inf
66
+
67
+
68
+ @cute.jit
69
+ def sm90_col_to_r2p_idx(col_limit: Int32) -> Int32:
70
+ """Transform SM90 MMA column coordinate to R2P element index.
71
+
72
+ SM90 MMA accumulator column indices are non-contiguous: 0, 1, 8, 9, 16, 17, ...
73
+ Element indices are contiguous: 0, 1, 2, 3, 4, 5, ...
74
+ This converts a column-space threshold to element-space for r2p_bitmask_below/above.
75
+ """
76
+ return col_limit // 8 * 2 + min(col_limit % 8, 2)
77
+
78
+
79
+ @cute.jit
80
+ def row_to_r2p_idx(x: Int32, num_rep: int, num_wg: int) -> Int32:
81
+ """Convert a row coordinate to an R2P element index in the warp-group interleaved layout.
82
+
83
+ In the SM100 backward pass, 2 warp groups share TMEM. The TMEM load atom
84
+ distributes rows in an interleaved pattern: elements 0..num_rep-1 map to
85
+ rows 0..num_rep-1 (warp group 0), elements num_rep..2*num_rep-1 map to
86
+ rows num_rep*num_wg..num_rep*num_wg+num_rep-1 (warp group 1), and so on.
87
+ Row-coordinate thresholds (causal limits, window bounds, uih_len) must be
88
+ converted to element indices before use with r2p_bitmask_above/below.
89
+
90
+ Rows not owned by this thread (in the gap between warp groups) are clamped
91
+ to the boundary element index, which is safe because R2P thresholds are
92
+ monotonic.
93
+
94
+ Example with num_rep=16, num_wg=2:
95
+ row 0 -> elem 0, row 15 -> elem 15,
96
+ row 16 -> elem 16 (clamped), row 31 -> elem 16 (clamped),
97
+ row 32 -> elem 16, row 33 -> elem 17, row 47 -> elem 31.
98
+ """
99
+ return x // (num_rep * num_wg) * num_rep + min(x % (num_rep * num_wg), num_rep)
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class AttentionMask:
104
+ tile_m: cutlass.Constexpr[int]
105
+ tile_n: cutlass.Constexpr[int]
106
+ seqlen_info: SeqlenInfoQK
107
+ window_size_left: Optional[Int32] = None
108
+ window_size_right: Optional[Int32] = None
109
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1 # only pass in if we're doing PackGQA
110
+ swap_AB: cutlass.Constexpr[bool] = False
111
+
112
+ @property
113
+ def seqlen_q(self) -> Int32:
114
+ return self.seqlen_info.seqlen_q
115
+
116
+ @property
117
+ def seqlen_k(self) -> Int32:
118
+ return self.seqlen_info.seqlen_k
119
+
120
+ @cute.jit
121
+ def apply_mask(
122
+ self,
123
+ acc_S: cute.Tensor,
124
+ batch_idx: cutlass.Int32,
125
+ head_idx: cutlass.Int32,
126
+ m_block: cutlass.Int32,
127
+ n_block: cutlass.Int32,
128
+ thr_mma: cute.TiledMma,
129
+ mask_seqlen: cutlass.Constexpr[bool],
130
+ mask_causal: cutlass.Constexpr[bool],
131
+ mask_local: cutlass.Constexpr[bool] = False,
132
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
133
+ aux_tensors: Optional[list] = None,
134
+ fastdiv_mods=(None, None),
135
+ ) -> None:
136
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
137
+ acc_S_mn = layout_utils.reshape_acc_to_mn(acc_S, transpose=self.swap_AB)
138
+ acc_shape = (self.tile_m, self.tile_n)
139
+ cS = cute.make_identity_tensor(acc_shape if not self.swap_AB else acc_shape[::-1])
140
+ tScS_mn = layout_utils.reshape_acc_to_mn(thr_mma.partition_C(cS), transpose=self.swap_AB)
141
+ # We use t0ScS as these indices are known at compile time. We then must subtract the
142
+ # column limit by the thread column offset.
143
+ t0ScS_mn = layout_utils.reshape_acc_to_mn(
144
+ thr_mma.get_slice(0).partition_C(cS), transpose=self.swap_AB
145
+ )
146
+ ROW = 0 if const_expr(not self.swap_AB) else 1
147
+ COL = 1 if const_expr(not self.swap_AB) else 0
148
+ thr_col_offset = tScS_mn[0][COL]
149
+ # To handle edge cases of completely masked out rows where n_block_max = 0,
150
+ # we treat negative n_blocks as 0th n_block
151
+ # TODO: find more transparent solution
152
+ if n_block < 0:
153
+ n_block = 0
154
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
155
+ if const_expr(not mask_causal and not mask_local and mask_mod is None):
156
+ if const_expr(mask_seqlen):
157
+ r2p = const_expr(not self.swap_AB)
158
+ if const_expr(not r2p):
159
+ # traverse column index.
160
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
161
+ oob = t0ScS_mn[0, c][COL] >= seqlenk_col_limit
162
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
163
+ acc_S_mn[r, c] = -Float32.inf if oob else acc_S_mn[r, c]
164
+ else:
165
+ seqlenk_col_limit_r2p = sm90_col_to_r2p_idx(seqlenk_col_limit)
166
+ mask_r2p_lambda(acc_S_mn, lambda s: r2p_bitmask_below(seqlenk_col_limit_r2p, s))
167
+
168
+ elif const_expr(
169
+ not mask_causal and not mask_local and mask_mod is not None
170
+ ): # FlexAttention mask mod
171
+ nrow = const_expr(cute.size(tScS_mn.shape[0]))
172
+ ncol = const_expr(cute.size(tScS_mn.shape[1]))
173
+ has_fastdiv = const_expr(
174
+ fastdiv_mods is not None
175
+ and fastdiv_mods[0] is not None
176
+ and fastdiv_mods[1] is not None
177
+ )
178
+ wrap_aux_indices = const_expr(
179
+ has_fastdiv and mask_seqlen and const_expr(aux_tensors is not None)
180
+ )
181
+
182
+ for r in cutlass.range_constexpr(nrow):
183
+ # Respect swap_AB: ROW/COL determine which coordinate component corresponds to Q/KV.
184
+ local_row = tScS_mn[r, 0][ROW]
185
+ global_row_idx = local_row + m_block * self.tile_m
186
+ row_for_mod = global_row_idx
187
+ head_idx_for_mod = head_idx
188
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
189
+ head_offset = global_row_idx % self.qhead_per_kvhead_packgqa
190
+ head_idx_for_mod = head_idx * self.qhead_per_kvhead_packgqa + head_offset
191
+ row_for_mod = global_row_idx // self.qhead_per_kvhead_packgqa
192
+ row_for_seqlen = row_for_mod
193
+ if const_expr(wrap_aux_indices):
194
+ _, row_for_mod = divmod(row_for_mod, fastdiv_mods[0])
195
+
196
+ for col in cutlass.range_constexpr(ncol):
197
+ col_idx_local = t0ScS_mn[0, col][COL]
198
+ # Convert to absolute column index
199
+ global_col_idx = thr_col_offset + col_idx_local + n_block * self.tile_n
200
+ col_for_mod = global_col_idx
201
+ if const_expr(wrap_aux_indices):
202
+ _, col_for_mod = divmod(global_col_idx, fastdiv_mods[1])
203
+
204
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
205
+ head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
206
+ q_idx_ssa = utils.scalar_to_ssa(row_for_mod, cutlass.Int32)
207
+ kv_idx_ssa = utils.scalar_to_ssa(col_for_mod, cutlass.Int32)
208
+ mask_value = mask_mod(
209
+ batch_idx_ssa,
210
+ head_idx_ssa,
211
+ q_idx_ssa,
212
+ kv_idx_ssa,
213
+ self.seqlen_info,
214
+ aux_tensors,
215
+ )
216
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
217
+ if const_expr(mask_seqlen):
218
+ out_of_bounds = (row_for_seqlen >= self.seqlen_q) or (
219
+ global_col_idx >= self.seqlen_k
220
+ )
221
+ if out_of_bounds:
222
+ acc_S_mn[r, col] = -cutlass.Float32.inf
223
+ else:
224
+ acc_S_mn[r, col] = acc_S_mn[r, col] if cond else -cutlass.Float32.inf
225
+ else:
226
+ acc_S_mn[r, col] = acc_S_mn[r, col] if cond else -cutlass.Float32.inf
227
+
228
+ else: # Causal or local
229
+ if const_expr(not self.swap_AB):
230
+ # If PackGQA, we split the work of compute divmod among threads in the same row
231
+ threads_per_row = thr_mma.tv_layout_C.shape[0][0]
232
+ mma_m_idx = None
233
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
234
+ assert not self.swap_AB, "swap_AB with PackGQA not supported yet"
235
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, (
236
+ "threads_per_row must divide WARP_SIZE"
237
+ )
238
+ assert cute.size(acc_S_mn.shape[0]) <= threads_per_row
239
+ tidx = thr_mma.thr_idx
240
+ mma_m_idx = (
241
+ m_block * self.tile_m + tScS_mn[tidx % threads_per_row, 0][0]
242
+ ) // self.qhead_per_kvhead_packgqa
243
+ causal_row_offset = (
244
+ 1 + self.seqlen_k - n_block * self.tile_n - self.seqlen_q - thr_col_offset
245
+ )
246
+ if const_expr(mask_causal):
247
+ r2p = const_expr(not self.swap_AB) # R2P trick, see apply_mask_sm100
248
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
249
+ # get the column index limit based on current row. Only consider the row index, so the column index sets to 0.
250
+ if const_expr(self.qhead_per_kvhead_packgqa == 1):
251
+ row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
252
+ else:
253
+ row_idx = utils.shuffle_sync(
254
+ mma_m_idx, r % threads_per_row, width=threads_per_row
255
+ )
256
+ col_limit_right = row_idx + causal_row_offset
257
+ if const_expr(mask_seqlen):
258
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
259
+ if const_expr(not r2p):
260
+ # traverse column index.
261
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
262
+ acc_S_mn[r, c] = (
263
+ -Float32.inf
264
+ if t0ScS_mn[0, c][1] >= col_limit_right
265
+ else acc_S_mn[r, c]
266
+ )
267
+ else:
268
+ col_limit_r2p = sm90_col_to_r2p_idx(col_limit_right)
269
+ mask_r2p_lambda(
270
+ acc_S_mn[r, None],
271
+ lambda s: r2p_bitmask_below(col_limit_r2p, s),
272
+ rank1=True,
273
+ )
274
+ else: # Local
275
+ local_row_offset_right = (
276
+ causal_row_offset + self.window_size_right
277
+ if const_expr(self.window_size_right is not None)
278
+ else None
279
+ )
280
+ local_row_offset_left = (
281
+ causal_row_offset - 1 - self.window_size_left
282
+ if const_expr(self.window_size_left is not None)
283
+ else None
284
+ )
285
+ r2p_local = const_expr(not self.swap_AB)
286
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
287
+ if const_expr(self.qhead_per_kvhead_packgqa == 1):
288
+ row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
289
+ else:
290
+ row_idx = utils.shuffle_sync(
291
+ mma_m_idx, r % threads_per_row, width=threads_per_row
292
+ )
293
+ if const_expr(self.window_size_right is not None):
294
+ col_limit_right = row_idx + local_row_offset_right
295
+ else:
296
+ col_limit_right = self.tile_n
297
+ if const_expr(mask_seqlen):
298
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
299
+ col_limit_left = (
300
+ row_idx + local_row_offset_left
301
+ if const_expr(self.window_size_left is not None)
302
+ else 0
303
+ )
304
+ if const_expr(not r2p_local):
305
+ # traverse column index.
306
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
307
+ col_idx = t0ScS_mn[0, c][1]
308
+ if col_idx >= col_limit_right or col_idx < col_limit_left:
309
+ acc_S_mn[r, c] = -Float32.inf
310
+ else:
311
+ col_limit_right_r2p = sm90_col_to_r2p_idx(col_limit_right)
312
+ col_limit_left_r2p = sm90_col_to_r2p_idx(col_limit_left)
313
+
314
+ def mask_gen_fn(s: int) -> Uint32:
315
+ return r2p_bitmask_below(
316
+ col_limit_right_r2p, s
317
+ ) & r2p_bitmask_above(col_limit_left_r2p, s)
318
+
319
+ mask_r2p_lambda(acc_S_mn[r, None], mask_gen_fn, rank1=True)
320
+ else: # swap_AB
321
+ assert self.qhead_per_kvhead_packgqa == 1
322
+ thr_row_offset = tScS_mn[0][ROW]
323
+ causal_row_offset = (
324
+ seqlenk_col_limit - self.seqlen_q + m_block * self.tile_m + thr_row_offset
325
+ )
326
+ if const_expr(mask_causal):
327
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
328
+ col0 = t0ScS_mn[0, c][COL]
329
+ # If col0 is beyond the column limit, we want to mask out the entire
330
+ # column, by setting row limit to be self.tile_m.
331
+ row_limit_top = (
332
+ self.tile_m
333
+ if col0 >= seqlenk_col_limit and mask_seqlen
334
+ else col0 - causal_row_offset
335
+ )
336
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
337
+ acc_S_mn[r, c] = (
338
+ -Float32.inf
339
+ if t0ScS_mn[r, 0][ROW] < row_limit_top
340
+ else acc_S_mn[r, c]
341
+ )
342
+ else:
343
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
344
+ col0 = t0ScS_mn[0, c][COL]
345
+ # If col0 is beyond the column limit, we want to mask out the entire
346
+ # column, by setting row limit to be self.tile_m.
347
+ row_limit_top = (
348
+ self.tile_m
349
+ if col0 >= seqlenk_col_limit and mask_seqlen
350
+ else (
351
+ col0 - causal_row_offset - self.window_size_right
352
+ if const_expr(self.window_size_right is not None)
353
+ else 0
354
+ )
355
+ )
356
+ row_limit_bot = (
357
+ col0 - causal_row_offset + self.window_size_left
358
+ if const_expr(self.window_size_left is not None)
359
+ else self.tile_m
360
+ )
361
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
362
+ row_idx = t0ScS_mn[r, 0][ROW]
363
+ acc_S_mn[r, c] = (
364
+ -Float32.inf
365
+ if row_idx < row_limit_top or row_idx > row_limit_bot
366
+ else acc_S_mn[r, c]
367
+ )
368
+
369
+ @cute.jit
370
+ def apply_mask_sm100(
371
+ self,
372
+ acc_S: cute.Tensor,
373
+ m_block: Int32,
374
+ n_block: Int32,
375
+ thr_mma: cute.TiledMma,
376
+ thr_tmem_load: cute.TiledCopy,
377
+ mask_seqlen: cutlass.Constexpr[bool],
378
+ mask_causal: cutlass.Constexpr[bool],
379
+ mask_local: cutlass.Constexpr[bool] = False,
380
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
381
+ batch_idx: Int32 = None,
382
+ head_idx: Int32 = None,
383
+ aux_tensors: Optional[list] = None,
384
+ fastdiv_mods=(None, None),
385
+ head_divmod=None,
386
+ check_q_boundary: bool = False,
387
+ vec_size: cutlass.Constexpr[int] = 1,
388
+ ) -> None:
389
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
390
+ acc_shape = (self.tile_m, self.tile_n)
391
+ cS = cute.make_identity_tensor(acc_shape if not self.swap_AB else acc_shape[::-1])
392
+ tScS = thr_mma.partition_C(cS)
393
+ tScS = tScS[(None, None), 0, 0]
394
+ tScS_t2r = thr_tmem_load.partition_D(tScS)
395
+ # To handle edge cases of completely masked out rows where n_block_max = 0,
396
+ # we treat negative n_blocks as 0th n_block
397
+ # TODO: find more transparent solution
398
+ if n_block < 0:
399
+ n_block = 0
400
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n
401
+ r2p = True
402
+ if const_expr(not mask_causal and not mask_local and mask_mod is None):
403
+ if const_expr(mask_seqlen):
404
+ if const_expr(not r2p):
405
+ for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
406
+ # if tScS_t2r[i][1] >= seqlenk_col_limit:
407
+ # acc_S[i] = -Float32.inf
408
+ # For some reason the 2 lines above generate really bad SASS
409
+ acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= seqlenk_col_limit else acc_S[i]
410
+ else:
411
+ mask_r2p_lambda(
412
+ acc_S,
413
+ lambda s: r2p_bitmask_below(seqlenk_col_limit, s),
414
+ rank1=True,
415
+ )
416
+
417
+ elif const_expr(not mask_causal and not mask_local and mask_mod is not None):
418
+ # Block sparse case w/ mask_mod
419
+ has_fastdiv = const_expr(
420
+ fastdiv_mods is not None
421
+ and fastdiv_mods[0] is not None
422
+ and fastdiv_mods[1] is not None
423
+ )
424
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
425
+
426
+ ncol = const_expr(cute.size(tScS_t2r.shape))
427
+ for i in cutlass.range_constexpr(ncol):
428
+ row_coord = tScS_t2r[i][0] if not self.swap_AB else tScS_t2r[i][1]
429
+ col_coord = tScS_t2r[i][1] if not self.swap_AB else tScS_t2r[i][0]
430
+ global_row = row_coord + m_block * self.tile_m
431
+ global_col = col_coord + n_block * self.tile_n
432
+
433
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
434
+ assert head_divmod is not None
435
+ mask_row, head_offset = divmod(global_row, head_divmod)
436
+ head_idx_for_mod = head_idx * self.qhead_per_kvhead_packgqa + head_offset
437
+ else:
438
+ head_idx_for_mod = head_idx
439
+ mask_row = global_row
440
+
441
+ mask_row_for_mod = mask_row
442
+ if const_expr(has_fastdiv and aux_tensors is not None):
443
+ if check_q_boundary:
444
+ _, mask_row_for_mod = divmod(mask_row, fastdiv_mods[0])
445
+ global_col_for_mod = global_col
446
+ if const_expr(has_fastdiv and mask_seqlen and aux_tensors is not None):
447
+ _, global_col_for_mod = divmod(global_col, fastdiv_mods[1])
448
+
449
+ head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
450
+ mask_row_ssa = utils.scalar_to_ssa(mask_row_for_mod, cutlass.Int32)
451
+ kv_idx_ssa = utils.scalar_to_ssa(global_col_for_mod, cutlass.Int32)
452
+ mask_value = mask_mod(
453
+ batch_idx_ssa,
454
+ head_idx_ssa,
455
+ mask_row_ssa,
456
+ kv_idx_ssa,
457
+ self.seqlen_info,
458
+ aux_tensors,
459
+ )
460
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
461
+ acc_S[i] = acc_S[i] if cond else -Float32.inf
462
+ if const_expr(mask_seqlen):
463
+ acc_S[i] = -Float32.inf if global_col >= self.seqlen_k else acc_S[i]
464
+ if check_q_boundary:
465
+ acc_S[i] = -Float32.inf if mask_row >= self.seqlen_q else acc_S[i]
466
+
467
+ else: # Causal or local
468
+ causal_row_offset = self.seqlen_k - n_block * self.tile_n - self.seqlen_q
469
+ row_idx = tScS_t2r[0][0] + m_block * self.tile_m
470
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
471
+ row_idx = row_idx // self.qhead_per_kvhead_packgqa
472
+ if const_expr(mask_causal):
473
+ col_limit_right = row_idx + causal_row_offset + 1
474
+ if const_expr(mask_seqlen):
475
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
476
+ # if cute.arch.thread_idx()[0] % 32 == 0:
477
+ # 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)
478
+ ncol = const_expr(cute.size(tScS_t2r.shape))
479
+ if const_expr(not r2p):
480
+ for i in cutlass.range(ncol, unroll_full=True):
481
+ acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= col_limit_right else acc_S[i]
482
+ else:
483
+ mask_r2p_lambda(
484
+ acc_S,
485
+ lambda s: r2p_bitmask_below(col_limit_right, s),
486
+ rank1=True,
487
+ )
488
+ else:
489
+ local_row_offset_right = (
490
+ causal_row_offset + 1 + self.window_size_right
491
+ if const_expr(self.window_size_right is not None)
492
+ else None
493
+ )
494
+ local_row_offset_left = (
495
+ causal_row_offset - self.window_size_left
496
+ if const_expr(self.window_size_left is not None)
497
+ else None
498
+ )
499
+ if const_expr(self.window_size_right is not None):
500
+ col_limit_right = row_idx + local_row_offset_right
501
+ else:
502
+ col_limit_right = self.tile_n
503
+ if const_expr(mask_seqlen):
504
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
505
+ col_limit_left = (
506
+ row_idx + local_row_offset_left
507
+ if const_expr(self.window_size_left is not None)
508
+ else 0
509
+ )
510
+ if const_expr(not r2p):
511
+ # 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)
512
+ for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
513
+ col_idx = tScS_t2r[i][1]
514
+ acc_S[i] = (
515
+ -Float32.inf
516
+ if col_idx >= col_limit_right or col_idx < col_limit_left
517
+ else acc_S[i]
518
+ )
519
+ else:
520
+ # Dual-bound R2P masking for SM100.
521
+ # Masks elements where: NOT (col_limit_left <= col < col_limit_right)
522
+
523
+ def mask_gen_fn(s: int) -> Uint32:
524
+ return r2p_bitmask_below(col_limit_right, s) & r2p_bitmask_above(
525
+ col_limit_left, s
526
+ )
527
+
528
+ mask_r2p_lambda(acc_S, mask_gen_fn, rank1=True)
529
+
530
+ @cute.jit
531
+ def apply_mask_sm100_transposed(
532
+ self,
533
+ acc_S: cute.Tensor,
534
+ tScS_t2r: cute.Tensor,
535
+ t0ScS_t2r: cute.Tensor,
536
+ m_block: cutlass.Int32,
537
+ n_block: cutlass.Int32,
538
+ mask_seqlen: cutlass.Constexpr,
539
+ mask_causal: cutlass.Constexpr,
540
+ mask_local: cutlass.Constexpr,
541
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
542
+ batch_idx: Int32 = None,
543
+ head_idx: Int32 = None,
544
+ aux_tensors: Optional[list] = None,
545
+ fastdiv_mods=(None, None),
546
+ is_full_block: bool = False,
547
+ check_m_boundary: bool = True,
548
+ ) -> None:
549
+ """
550
+ Backward pass: mask S = K @ Q.T where n_block tiles seqlen_k and m_block tiles seqlen_q.
551
+
552
+ Coordinate conventio:
553
+ - ROW corresponds to Q (m_block)
554
+ - COL corresponds to KV (n_block)
555
+
556
+ is_full_block: If True, skip mask_mod (all elements valid). Only apply seqlen masking.
557
+ check_m_boundary: If False, skip seqlen_q boundary check (optimization for non-boundary m_blocks).
558
+ When iterating m_blocks in forward order, only the last m_block may be partial.
559
+ """
560
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
561
+ ROW = 0 if const_expr(not self.swap_AB) else 1
562
+ COL = 1 if const_expr(not self.swap_AB) else 0
563
+ # assert t0ScS_t2r[0][COL] == 0, "col0 == 0" # tmp comment for 2-cta bwd
564
+ thr_col_offset = tScS_t2r[0][COL]
565
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
566
+
567
+ if const_expr(not mask_causal and not mask_local and mask_mod is not None):
568
+ # Block sparse case with mask_mod (backward)
569
+ #
570
+ # Coordinate convention: ROW → Q (m_block), COL → KV (n_block).
571
+ # These already account for swap_AB.
572
+ #
573
+ # FULL blocks: mask_mod returns True for all elements, so skip it.
574
+ # Still need seqlen bounds check (elements may be OOB on last m_block).
575
+ # PARTIAL blocks: apply mask_mod element-wise, then seqlen bounds.
576
+ if is_full_block:
577
+ if const_expr(mask_seqlen):
578
+ if seqlenk_col_limit <= 0:
579
+ # Entire tile is OOB for K
580
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
581
+ acc_S[i] = -cutlass.Float32.inf
582
+ elif check_m_boundary:
583
+ # Last m_block: check Q and K boundaries
584
+ ncol = const_expr(cute.size(tScS_t2r.shape))
585
+ for i in cutlass.range_constexpr(ncol):
586
+ row_coord = tScS_t2r[i][ROW]
587
+ col_coord = tScS_t2r[i][COL]
588
+ global_q = row_coord + m_block * self.tile_m
589
+ global_kv = col_coord + n_block * self.tile_n
590
+ q_out_of_bounds = global_q >= self.seqlen_q
591
+ kv_out_of_bounds = global_kv >= self.seqlen_k
592
+ out_of_bounds = q_out_of_bounds or kv_out_of_bounds
593
+ acc_S[i] = -cutlass.Float32.inf if out_of_bounds else acc_S[i]
594
+ else:
595
+ # Partial block
596
+ has_fastdiv = const_expr(
597
+ fastdiv_mods is not None
598
+ and fastdiv_mods[0] is not None
599
+ and fastdiv_mods[1] is not None
600
+ )
601
+ wrap_aux_indices = const_expr(
602
+ has_fastdiv and mask_seqlen and const_expr(aux_tensors is not None)
603
+ )
604
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
605
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32)
606
+
607
+ ncol = const_expr(cute.size(tScS_t2r.shape))
608
+ for i in cutlass.range_constexpr(ncol):
609
+ row_coord = tScS_t2r[i][ROW]
610
+ col_coord = tScS_t2r[i][COL]
611
+ global_q = row_coord + m_block * self.tile_m
612
+ global_kv = col_coord + n_block * self.tile_n
613
+
614
+ q_idx_for_mod = global_q
615
+ kv_idx_for_mod = global_kv
616
+ if const_expr(wrap_aux_indices):
617
+ _, q_idx_for_mod = divmod(global_q, fastdiv_mods[0])
618
+ _, kv_idx_for_mod = divmod(global_kv, fastdiv_mods[1])
619
+
620
+ q_idx_ssa = utils.scalar_to_ssa(q_idx_for_mod, cutlass.Int32)
621
+ kv_idx_ssa = utils.scalar_to_ssa(kv_idx_for_mod, cutlass.Int32)
622
+
623
+ mask_value = mask_mod(
624
+ batch_idx_ssa,
625
+ head_idx_ssa,
626
+ q_idx_ssa,
627
+ kv_idx_ssa,
628
+ self.seqlen_info,
629
+ aux_tensors,
630
+ )
631
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
632
+ acc_S[i] = acc_S[i] if cond else -cutlass.Float32.inf
633
+
634
+ if const_expr(mask_seqlen):
635
+ # check_m_boundary=False skips q check for non-boundary m_blocks
636
+ q_out_of_bounds = check_m_boundary and (global_q >= self.seqlen_q)
637
+ kv_out_of_bounds = global_kv >= self.seqlen_k
638
+ out_of_bounds = q_out_of_bounds or kv_out_of_bounds
639
+ acc_S[i] = -cutlass.Float32.inf if out_of_bounds else acc_S[i]
640
+
641
+ elif const_expr(not mask_causal and not mask_local):
642
+ if const_expr(mask_seqlen):
643
+ if seqlenk_col_limit <= 0:
644
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
645
+ acc_S[i] = -cutlass.Float32.inf
646
+ else: # Causal or local
647
+ thr_row_offset = tScS_t2r[0][ROW]
648
+ seqlenq_row_limit = self.seqlen_q - m_block * self.tile_m - thr_row_offset
649
+ causal_offset = seqlenq_row_limit - seqlenk_col_limit
650
+ if const_expr(mask_causal):
651
+ # tidx = cute.arch.thread_idx()[0] % 256
652
+ # if tidx < 32:
653
+ # cute.printf("tidx = {}, {} {}, {} {}", tidx, tScS_t2r[0][0], tScS_t2r[0][1], tScS_t2r[1][0], tScS_t2r[1][1])
654
+ row_limit_top = causal_offset
655
+ if const_expr(mask_seqlen):
656
+ # If col is beyond the column limit, we want to mask out the entire
657
+ # column, by setting row limit to be self.tile_m.
658
+ if seqlenk_col_limit <= 0:
659
+ row_limit_top = self.tile_m
660
+ r2p = True
661
+ if const_expr(not r2p):
662
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
663
+ acc_S[i] = (
664
+ -cutlass.Float32.inf if t0ScS_t2r[i][ROW] < row_limit_top else acc_S[i]
665
+ )
666
+ else:
667
+ num_rep = cute.size(tScS_t2r, mode=[0]) # 16 or 32
668
+ num_wg = 2
669
+ row_limit = row_to_r2p_idx(row_limit_top, num_rep, num_wg)
670
+ mask_r2p_lambda(
671
+ acc_S,
672
+ lambda s: r2p_bitmask_above(row_limit, s),
673
+ rank1=True,
674
+ )
675
+ else:
676
+ if const_expr(self.window_size_right is not None):
677
+ row_limit_top = causal_offset - self.window_size_right
678
+ else:
679
+ row_limit_top = 0
680
+ if const_expr(self.window_size_left is not None):
681
+ row_limit_bot = causal_offset + self.window_size_left
682
+ if const_expr(mask_seqlen):
683
+ if seqlenk_col_limit <= 0:
684
+ row_limit_top = self.tile_m
685
+ r2p = True
686
+ if const_expr(not r2p):
687
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
688
+ row_idx = t0ScS_t2r[i][ROW]
689
+ local_mask = row_idx < row_limit_top
690
+ if const_expr(self.window_size_left is not None):
691
+ local_mask |= row_idx > row_limit_bot
692
+ acc_S[i] = -cutlass.Float32.inf if local_mask else acc_S[i]
693
+ else:
694
+
695
+ def mask_gen_fn(s: int) -> Uint32:
696
+ num_rep = cute.size(tScS_t2r, mode=[0])
697
+ num_wg = 2
698
+
699
+ row_limit = row_to_r2p_idx(row_limit_top, num_rep, num_wg)
700
+ mask = r2p_bitmask_above(row_limit, s)
701
+
702
+ if const_expr(self.window_size_left is not None):
703
+ row_limit_bottom = row_to_r2p_idx(row_limit_bot + 1, num_rep, num_wg)
704
+ mask = mask & r2p_bitmask_below(row_limit_bottom, s)
705
+
706
+ return mask
707
+
708
+ mask_r2p_lambda(
709
+ acc_S,
710
+ mask_gen_fn,
711
+ rank1=True,
712
+ )
torch-ext/sol_attn/_vendor/flash_attn/cute/named_barrier.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
torch-ext/sol_attn/_vendor/flash_attn/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 ....sm90._compat import layout_utils
12
+ from . import 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_rmem_tensor(num_ptr_per_thread, cutlass.Int64)
134
+ for i in cutlass.range_constexpr(num_ptr_per_thread):
135
+ row = i * num_threads + cRows[tidx % threads_per_row][0]
136
+ idx = block * self.m_block_size + row
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
+ )
torch-ext/sol_attn/_vendor/flash_attn/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)
torch-ext/sol_attn/_vendor/flash_attn/cute/seqlen_info.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ....sm90._compat 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
+ mCuTotalMBlocks: Optional[cute.Tensor] = None,
89
+ mCuBlockIdxOffsets: Optional[cute.Tensor] = None,
90
+ tile_m: cutlass.Constexpr[Int32] = 128,
91
+ tile_n: cutlass.Constexpr[Int32] = 128,
92
+ ):
93
+ del mCuTotalMBlocks, mCuBlockIdxOffsets
94
+ offset_q = 0 if const_expr(mCuSeqlensQ is None) else mCuSeqlensQ[batch_idx]
95
+ offset_k = 0 if const_expr(mCuSeqlensK is None) else mCuSeqlensK[batch_idx]
96
+ padded_offset_q = (
97
+ 0
98
+ if const_expr(mCuSeqlensQ is None)
99
+ else cute.assume((offset_q + batch_idx * tile_m) // tile_m * tile_m, divby=tile_m)
100
+ )
101
+ padded_offset_k = (
102
+ 0
103
+ if const_expr(mCuSeqlensK is None)
104
+ else cute.assume((offset_k + batch_idx * tile_n) // tile_n * tile_n, divby=tile_n)
105
+ )
106
+ if const_expr(mSeqUsedQ is not None):
107
+ seqlen_q = mSeqUsedQ[batch_idx]
108
+ else:
109
+ seqlen_q = (
110
+ seqlen_q_static
111
+ if const_expr(mCuSeqlensQ is None)
112
+ else mCuSeqlensQ[batch_idx + 1] - offset_q
113
+ )
114
+ if const_expr(mSeqUsedK is not None):
115
+ seqlen_k = mSeqUsedK[batch_idx]
116
+ else:
117
+ seqlen_k = (
118
+ seqlen_k_static
119
+ if const_expr(mCuSeqlensK is None)
120
+ else mCuSeqlensK[batch_idx + 1] - offset_k
121
+ )
122
+ return SeqlenInfoQK(
123
+ offset_q,
124
+ offset_k,
125
+ padded_offset_q,
126
+ padded_offset_k,
127
+ seqlen_q,
128
+ seqlen_k,
129
+ has_cu_seqlens_q=mCuSeqlensQ is not None,
130
+ has_cu_seqlens_k=mCuSeqlensK is not None,
131
+ has_seqused_q=mSeqUsedQ is not None,
132
+ has_seqused_k=mSeqUsedK is not None,
133
+ )
134
+
135
+ def offset_batch_Q(
136
+ self,
137
+ mQ: cute.Tensor,
138
+ batch_idx: Int32,
139
+ dim: int,
140
+ padded: cutlass.Constexpr[bool] = False,
141
+ ragged: cutlass.Constexpr[bool] = False,
142
+ ) -> cute.Tensor:
143
+ """Seqlen must be the first dimension of mQ"""
144
+ if const_expr(not ragged):
145
+ if const_expr(not self.has_cu_seqlens_q):
146
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mQ) - 1 - dim)
147
+ return mQ[idx]
148
+ else:
149
+ offset_q = self.offset_q if const_expr(not padded) else self.padded_offset_q
150
+ offset_q = offset_q if const_expr(cute.rank(mQ.shape[0]) == 1) else (None, offset_q)
151
+ idx = (offset_q,) + (None,) * (cute.rank(mQ) - 1)
152
+ return cute.domain_offset(idx, mQ)
153
+ else:
154
+ if const_expr(not self.has_cu_seqlens_q):
155
+ offset_q = 0
156
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mQ) - 1 - dim)
157
+ mQ = mQ[idx]
158
+ else:
159
+ offset_q = self.offset_q if const_expr(not padded) else self.padded_offset_q
160
+ if const_expr(cute.rank(mQ.shape[0]) == 1):
161
+ return copy_utils.offset_ragged_tensor(
162
+ mQ, offset_q, self.seqlen_q, ragged_dim=0, ptr_shift=True
163
+ )
164
+ else: # PackGQA
165
+ assert cute.rank(mQ.shape[0]) == 2
166
+ # Unpack before calling offset_ragged_tensor, then pack
167
+ idx = ((None, None),) + (None,) * (cute.rank(mQ) - 1)
168
+ mQ = mQ[idx]
169
+ mQ = copy_utils.offset_ragged_tensor(
170
+ mQ, offset_q, self.seqlen_q, ragged_dim=1, ptr_shift=True
171
+ )
172
+ return cute.group_modes(mQ, 0, 2)
173
+
174
+ def offset_batch_K(
175
+ self,
176
+ mK: cute.Tensor,
177
+ batch_idx: Int32,
178
+ dim: int,
179
+ padded: cutlass.Constexpr[bool] = False,
180
+ ragged: cutlass.Constexpr[bool] = False,
181
+ multiple: int = 1,
182
+ ) -> cute.Tensor:
183
+ """Seqlen must be the first dimension of mK"""
184
+ if const_expr(not ragged):
185
+ if const_expr(not self.has_cu_seqlens_k):
186
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mK) - 1 - dim)
187
+ return mK[idx]
188
+ else:
189
+ offset_k = self.offset_k if const_expr(not padded) else self.padded_offset_k
190
+ offset_k *= multiple
191
+ idx = (offset_k,) + (None,) * (cute.rank(mK) - 1)
192
+ return cute.domain_offset(idx, mK)
193
+ else:
194
+ if const_expr(not self.has_cu_seqlens_k):
195
+ offset_k = 0
196
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mK) - 1 - dim)
197
+ mK = mK[idx]
198
+ else:
199
+ offset_k = self.offset_k if const_expr(not padded) else self.padded_offset_k
200
+ offset_k *= multiple
201
+ return copy_utils.offset_ragged_tensor(
202
+ mK, offset_k, self.seqlen_k, ragged_dim=0, ptr_shift=True
203
+ )
204
+
205
+
206
+ @dataclass(frozen=True)
207
+ class SeqlenInfoQKNewK:
208
+ """Sequence length info for append-KV with left-padding and new K support.
209
+
210
+ Extends SeqlenInfoQK with:
211
+ - leftpad_k: left padding for K (tokens to skip at the start of the KV cache)
212
+ - offset_k_new: offset into the new K tensor
213
+ - seqlen_k_og: original K length (before appending new K), excluding leftpad
214
+ - seqlen_k_new: length of new K to append
215
+ - seqlen_k: total K length (seqlen_k_og + seqlen_k_new)
216
+ - seqlen_rotary: position for rotary embedding computation
217
+ """
218
+
219
+ leftpad_k: Int32
220
+ offset_q: Int32
221
+ offset_k: Int32
222
+ offset_k_new: Int32
223
+ seqlen_q: Int32
224
+ seqlen_k_og: Int32
225
+ seqlen_k_new: Int32
226
+ seqlen_k: Int32
227
+ seqlen_rotary: Int32
228
+
229
+ @staticmethod
230
+ def create(
231
+ batch_idx: Int32,
232
+ seqlen_q_static: Int32,
233
+ seqlen_k_static: Int32,
234
+ shape_K_new_0: Int32,
235
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
236
+ mCuSeqlensK: Optional[cute.Tensor] = None,
237
+ mCuSeqlensKNew: Optional[cute.Tensor] = None,
238
+ mSeqUsedQ: Optional[cute.Tensor] = None,
239
+ mSeqUsedK: Optional[cute.Tensor] = None,
240
+ mLeftpadK: Optional[cute.Tensor] = None,
241
+ mSeqlensRotary: Optional[cute.Tensor] = None,
242
+ ):
243
+ leftpad_k = 0 if const_expr(mLeftpadK is None) else mLeftpadK[batch_idx]
244
+ offset_q = 0 if const_expr(mCuSeqlensQ is None) else mCuSeqlensQ[batch_idx]
245
+ if const_expr(mCuSeqlensK is not None):
246
+ offset_k = mCuSeqlensK[batch_idx] + leftpad_k
247
+ else:
248
+ offset_k = leftpad_k if const_expr(mCuSeqlensQ is not None) else 0
249
+ offset_k_new = 0 if const_expr(mCuSeqlensKNew is None) else mCuSeqlensKNew[batch_idx]
250
+ # seqlen_q
251
+ if const_expr(mSeqUsedQ is not None):
252
+ seqlen_q = mSeqUsedQ[batch_idx]
253
+ elif const_expr(mCuSeqlensQ is not None):
254
+ seqlen_q = mCuSeqlensQ[batch_idx + 1] - mCuSeqlensQ[batch_idx]
255
+ else:
256
+ seqlen_q = seqlen_q_static
257
+ # seqlen_k_og: original K length (excluding leftpad)
258
+ if const_expr(mSeqUsedK is not None):
259
+ seqlen_k_og = mSeqUsedK[batch_idx] - leftpad_k
260
+ elif const_expr(mCuSeqlensK is not None):
261
+ seqlen_k_og = mCuSeqlensK[batch_idx + 1] - mCuSeqlensK[batch_idx] - leftpad_k
262
+ else:
263
+ seqlen_k_og = (
264
+ seqlen_k_static - leftpad_k
265
+ if const_expr(mCuSeqlensQ is not None)
266
+ else seqlen_k_static
267
+ )
268
+ # seqlen_k_new
269
+ if const_expr(mCuSeqlensKNew is None):
270
+ seqlen_k_new = 0 if const_expr(mCuSeqlensQ is None) else shape_K_new_0
271
+ else:
272
+ seqlen_k_new = mCuSeqlensKNew[batch_idx + 1] - mCuSeqlensKNew[batch_idx]
273
+ seqlen_k = seqlen_k_og if const_expr(mCuSeqlensQ is None) else seqlen_k_og + seqlen_k_new
274
+
275
+ # seqlen_rotary: defaults to seqlen_k_og + leftpad_k unless explicitly provided
276
+ if const_expr(mSeqlensRotary is not None):
277
+ seqlen_rotary = mSeqlensRotary[batch_idx]
278
+ else:
279
+ seqlen_rotary = seqlen_k_og + leftpad_k
280
+ return SeqlenInfoQKNewK(
281
+ leftpad_k,
282
+ offset_q,
283
+ offset_k,
284
+ offset_k_new,
285
+ seqlen_q,
286
+ seqlen_k_og,
287
+ seqlen_k_new,
288
+ seqlen_k,
289
+ seqlen_rotary,
290
+ )
torch-ext/sol_attn/_vendor/flash_attn/cute/softmax.py ADDED
@@ -0,0 +1,639 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import math
4
+ import operator
5
+ from typing import Tuple
6
+ from dataclasses import dataclass
7
+
8
+ import cutlass
9
+ import cutlass.cute as cute
10
+ from cutlass import Float32
11
+
12
+ from ....sm90._compat import layout_utils
13
+ from . import utils
14
+ from ....sm90._compat.cute_dsl_utils import ParamsBase
15
+ from ...._vendor.flash_attn.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
+ row_ref_max: cute.Tensor
25
+ arch: cutlass.Constexpr[int] = 80
26
+ softmax_scale: Float32 | None = None
27
+
28
+ @staticmethod
29
+ def create(
30
+ scale_log2: Float32,
31
+ num_rows: cutlass.Constexpr[int],
32
+ arch: cutlass.Constexpr[int] = 80,
33
+ softmax_scale: Float32 | None = None,
34
+ ):
35
+ row_max = cute.make_rmem_tensor(num_rows, Float32)
36
+ row_sum = cute.make_rmem_tensor(num_rows, Float32)
37
+ row_ref_max = cute.make_rmem_tensor(num_rows, Float32)
38
+ return Softmax(
39
+ scale_log2,
40
+ num_rows,
41
+ row_max,
42
+ row_sum,
43
+ row_ref_max,
44
+ arch,
45
+ softmax_scale,
46
+ )
47
+
48
+ def reset(self) -> None:
49
+ self.row_max.fill(-Float32.inf)
50
+ self.row_sum.fill(0.0)
51
+ self.row_ref_max.fill(-Float32.inf)
52
+
53
+ def _compute_row_max(
54
+ self, acc_S_row: cute.TensorSSA, init_val: float | Float32 | None = None
55
+ ) -> Float32:
56
+ return utils.fmax_reduce(acc_S_row, init_val, arch=self.arch)
57
+
58
+ def _compute_row_sum(
59
+ self, acc_S_row_exp: cute.TensorSSA, init_val: float | Float32 | None = None
60
+ ) -> Float32:
61
+ return utils.fadd_reduce(acc_S_row_exp, init_val, arch=self.arch)
62
+
63
+ @cute.jit
64
+ def online_softmax(
65
+ self,
66
+ acc_S: cute.Tensor,
67
+ is_first: cutlass.Constexpr[bool] = False,
68
+ check_inf: cutlass.Constexpr[bool] = True,
69
+ ) -> cute.Tensor:
70
+ """Apply online softmax and return the row_scale to rescale O.
71
+
72
+ :param acc_S: acc_S tensor
73
+ :type acc_S: cute.Tensor
74
+ :param is_first: is first n_block
75
+ :type is_first: cutlass.Constexpr
76
+ """
77
+ # Change acc_S to M,N layout view.
78
+ acc_S_mn = layout_utils.reshape_acc_to_mn(acc_S)
79
+ row_scale = cute.make_fragment_like(self.row_max, Float32)
80
+
81
+ row_max = self.row_max
82
+ row_sum = self.row_sum
83
+ scale_log2 = self.scale_log2
84
+ arch = self.arch
85
+
86
+ # Each iteration processes one row of acc_S
87
+ for r in cutlass.range(cute.size(row_max), unroll_full=True):
88
+ acc_S_row = acc_S_mn[r, None].load() # (n_block_size)
89
+
90
+ row_max_cur = utils.fmax_reduce(
91
+ acc_S_row,
92
+ init_val=row_max[r] if cutlass.const_expr(not is_first) else None,
93
+ arch=arch,
94
+ )
95
+
96
+ row_max_cur = cute.arch.warp_reduction_max(row_max_cur, threads_in_group=4)
97
+ # Update row_max before changing row_max_cur to safe value for -inf
98
+ row_max_prev = row_max[r]
99
+ row_max[r] = row_max_cur
100
+
101
+ if cutlass.const_expr(check_inf):
102
+ row_max_cur = 0.0 if row_max_cur == -Float32.inf else row_max_cur
103
+
104
+ if cutlass.const_expr(is_first):
105
+ row_max_cur_scaled = row_max_cur * scale_log2
106
+ acc_S_row_exp = cute.math.exp2(
107
+ acc_S_row * scale_log2 - row_max_cur_scaled, fastmath=True
108
+ )
109
+ acc_S_row_sum = utils.fadd_reduce(acc_S_row_exp, init_val=None, arch=arch)
110
+ row_scale[r] = 1.0
111
+ else:
112
+ row_max_cur_scaled = row_max_cur * scale_log2
113
+ acc_S_row_exp = cute.math.exp2(
114
+ acc_S_row * scale_log2 - row_max_cur_scaled, fastmath=True
115
+ )
116
+ # row_scale[r] = cute.math.exp2(row_max_prev * self.scale_log2 - row_max_cur_scaled)
117
+ row_scale[r] = cute.math.exp2(
118
+ (row_max_prev - row_max_cur) * scale_log2, fastmath=True
119
+ )
120
+ acc_S_row_sum = utils.fadd_reduce(
121
+ acc_S_row_exp, init_val=row_sum[r] * row_scale[r], arch=arch
122
+ )
123
+
124
+ row_sum[r] = acc_S_row_sum
125
+ acc_S_mn[r, None].store(acc_S_row_exp)
126
+
127
+ return row_scale
128
+
129
+ @cute.jit
130
+ def finalize(
131
+ self, final_scale: Float32 = 1.0, sink_val: Float32 | cute.Tensor | None = None
132
+ ) -> cute.Tensor:
133
+ """Finalize the online softmax by computing the scale and logsumexp."""
134
+ if cutlass.const_expr(sink_val is not None and isinstance(sink_val, cute.Tensor)):
135
+ assert cute.size(sink_val) == cute.size(self.row_sum)
136
+ row_sum = self.row_sum
137
+ row_max = self.row_max
138
+ scale_log2 = self.scale_log2
139
+
140
+ # quad reduction for row_sum as we didn't do it during each iteration of online softmax
141
+ row_sum.store(utils.warp_reduce(row_sum.load(), operator.add, width=4))
142
+ row_scale = cute.make_fragment_like(row_max, Float32)
143
+
144
+ for r in cutlass.range(cute.size(row_sum), unroll_full=True):
145
+ if cutlass.const_expr(sink_val is not None):
146
+ sink_val_cur = sink_val if not isinstance(sink_val, cute.Tensor) else sink_val[r]
147
+ LOG2_E = math.log2(math.e)
148
+ row_sum[r] += cute.math.exp2(
149
+ sink_val_cur * LOG2_E - row_max[r] * scale_log2, fastmath=True
150
+ )
151
+
152
+ # if row_sum is zero or nan, set acc_O_mn_row to 1.0
153
+ acc_O_mn_row_is_zero_or_nan = row_sum[r] == 0.0 or row_sum[r] != row_sum[r]
154
+ row_scale[r] = (
155
+ cute.arch.rcp_approx(row_sum[r] if not acc_O_mn_row_is_zero_or_nan else 1.0)
156
+ ) * final_scale
157
+ row_sum_cur = row_sum[r]
158
+ LN2 = math.log(2.0)
159
+ row_sum[r] = (
160
+ (row_max[r] * scale_log2 + cute.math.log2(row_sum_cur, fastmath=True)) * LN2
161
+ if not acc_O_mn_row_is_zero_or_nan
162
+ else -Float32.inf
163
+ )
164
+ return row_scale
165
+
166
+ @cute.jit
167
+ def rescale_O(self, acc_O: cute.Tensor, row_scale: cute.Tensor) -> None:
168
+ """Scale each row of acc_O by the given scale tensor.
169
+ :param acc_O: input tensor
170
+ :type acc_O: cute.Tensor
171
+ :param row_scale: row_scale tensor
172
+ :type row_scale: cute.Tensor
173
+ """
174
+ acc_O_mn = layout_utils.reshape_acc_to_mn(acc_O)
175
+ assert cute.size(row_scale) == cute.size(acc_O_mn, mode=[0])
176
+ for r in cutlass.range(cute.size(row_scale), unroll_full=True):
177
+ acc_O_mn[r, None].store(acc_O_mn[r, None].load() * row_scale[r])
178
+
179
+
180
+ @dataclass
181
+ class SoftmaxSm100(Softmax):
182
+ rescale_threshold: cutlass.Constexpr[float] = 0.0
183
+ max_offset: cutlass.Constexpr[int] = 0
184
+
185
+ @staticmethod
186
+ def create(
187
+ scale_log2: Float32,
188
+ rescale_threshold: cutlass.Constexpr[float] = 0.0,
189
+ softmax_scale: Float32 | None = None,
190
+ max_offset: cutlass.Constexpr[int] = 0,
191
+ ):
192
+ num_rows = 1
193
+ arch = 100
194
+ row_max = cute.make_rmem_tensor(num_rows, Float32)
195
+ row_sum = cute.make_rmem_tensor(num_rows, Float32)
196
+ row_ref_max = cute.make_rmem_tensor(num_rows, Float32)
197
+ return SoftmaxSm100(
198
+ scale_log2,
199
+ num_rows,
200
+ row_max,
201
+ row_sum,
202
+ row_ref_max,
203
+ arch,
204
+ softmax_scale,
205
+ rescale_threshold=rescale_threshold,
206
+ max_offset=max_offset,
207
+ )
208
+
209
+ @cute.jit
210
+ def compute_row_max_local(self, acc_S_row: cute.TensorSSA, is_first: int) -> Float32:
211
+ if cutlass.const_expr(is_first):
212
+ row_max_new = self._compute_row_max(acc_S_row)
213
+ else:
214
+ row_max_old = self.row_max[0]
215
+ row_max_new = self._compute_row_max(acc_S_row, init_val=row_max_old)
216
+ return row_max_new
217
+
218
+ @cute.jit
219
+ def update_row_max_from_local(
220
+ self,
221
+ row_max_new: Float32,
222
+ is_first: int,
223
+ ) -> Tuple[Float32, Float32]:
224
+ if cutlass.const_expr(is_first):
225
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
226
+ acc_scale = 0.0
227
+ else:
228
+ row_max_old = self.row_max[0]
229
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
230
+ acc_scale_ = (row_max_old - row_max_safe) * self.scale_log2
231
+ acc_scale = cute.math.exp2(acc_scale_, fastmath=True)
232
+ if cutlass.const_expr(self.rescale_threshold > 0.0):
233
+ if acc_scale_ >= -self.rescale_threshold:
234
+ row_max_new = row_max_old
235
+ row_max_safe = row_max_old
236
+ acc_scale = 1.0
237
+ self.row_max[0] = row_max_new
238
+ return row_max_safe, acc_scale
239
+
240
+ @cute.jit
241
+ def update_row_max(self, acc_S_row: cute.TensorSSA, is_first: int) -> Tuple[Float32, Float32]:
242
+ if cutlass.const_expr(is_first):
243
+ row_max_new = self._compute_row_max(acc_S_row)
244
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
245
+ acc_scale = 0.0
246
+ else:
247
+ row_max_old = self.row_max[0]
248
+ row_max_new = self._compute_row_max(acc_S_row, init_val=row_max_old)
249
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
250
+ acc_scale_ = (row_max_old - row_max_safe) * self.scale_log2
251
+ acc_scale = cute.math.exp2(acc_scale_, fastmath=True)
252
+ if cutlass.const_expr(self.rescale_threshold > 0.0):
253
+ if acc_scale_ >= -self.rescale_threshold:
254
+ row_max_new = row_max_old
255
+ row_max_safe = row_max_old
256
+ acc_scale = 1.0
257
+ self.row_max[0] = row_max_new
258
+ return row_max_safe, acc_scale
259
+
260
+ def update_row_sum(
261
+ self, acc_S_row_exp: cute.TensorSSA, row_scale: Float32, is_first: int = False
262
+ ) -> None:
263
+ init_val = self.row_sum[0] * row_scale if cutlass.const_expr(not is_first) else None
264
+ # self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=self.row_sum[0] * row_scale)
265
+ self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=init_val)
266
+ # tmp = self._compute_row_sum(acc_S_row_exp)
267
+ # self.row_sum[0] = self.row_sum[0] * row_scale + tmp
268
+
269
+ @cute.jit
270
+ def scale_subtract_rowmax(
271
+ self,
272
+ acc_S_row: cute.Tensor,
273
+ row_max: Float32,
274
+ ):
275
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
276
+ row_max_scaled = row_max * self.scale_log2
277
+ for i in cutlass.range(0, cute.size(acc_S_row.shape), 2, unroll_full=True):
278
+ acc_S_row[i], acc_S_row[i + 1] = cute.arch.fma_packed_f32x2(
279
+ (acc_S_row[i], acc_S_row[i + 1]),
280
+ (self.scale_log2, self.scale_log2),
281
+ (-row_max_scaled, -row_max_scaled),
282
+ )
283
+
284
+ @cute.jit
285
+ def apply_exp2_convert(
286
+ self,
287
+ acc_S_row: cute.Tensor,
288
+ acc_S_row_converted: cute.Tensor,
289
+ ex2_emu_freq: cutlass.Constexpr[int] = 0,
290
+ ex2_emu_res: cutlass.Constexpr[int] = 4,
291
+ ex2_emu_start_frg: cutlass.Constexpr[int] = 0,
292
+ ):
293
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
294
+ frg_tile = 32
295
+ assert frg_tile % 2 == 0
296
+ frg_cnt = cute.size(acc_S_row) // frg_tile
297
+ assert cute.size(acc_S_row) % frg_tile == 0
298
+ acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
299
+ acc_S_row_converted_frg = cute.logical_divide(
300
+ acc_S_row_converted, cute.make_layout(frg_tile)
301
+ )
302
+ for j in cutlass.range_constexpr(frg_cnt):
303
+ for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
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(acc_S_row_frg[k + 1, j], fastmath=True)
306
+ if cutlass.const_expr(ex2_emu_freq == 0):
307
+ acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
308
+ acc_S_row_frg[k + 1, j] = cute.math.exp2(acc_S_row_frg[k + 1, j], fastmath=True)
309
+ else:
310
+ if cutlass.const_expr(
311
+ k % ex2_emu_freq < ex2_emu_freq - ex2_emu_res
312
+ or j >= frg_cnt - 1
313
+ or j < ex2_emu_start_frg
314
+ ):
315
+ acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
316
+ acc_S_row_frg[k + 1, j] = cute.math.exp2(
317
+ acc_S_row_frg[k + 1, j], fastmath=True
318
+ )
319
+ else:
320
+ # 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])
321
+ acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = utils.ex2_emulation_2(
322
+ acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]
323
+ )
324
+ acc_S_row_converted_frg[None, j].store(
325
+ acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
326
+ )
327
+
328
+ @cute.jit
329
+ def scale_apply_exp2_convert(
330
+ self,
331
+ acc_S_row: cute.Tensor,
332
+ row_max: Float32,
333
+ acc_S_row_converted: cute.Tensor,
334
+ ):
335
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
336
+ minus_row_max_scaled = -row_max * self.scale_log2
337
+ for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
338
+ acc_S_row[i], acc_S_row[i + 1] = cute.arch.fma_packed_f32x2(
339
+ (acc_S_row[i], acc_S_row[i + 1]),
340
+ (self.scale_log2, self.scale_log2),
341
+ (minus_row_max_scaled, minus_row_max_scaled),
342
+ )
343
+
344
+ # for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
345
+ # acc_S_row[i], acc_S_row[i + 1] = cute.arch.fma_packed_f32x2(
346
+ # (acc_S_row[i], acc_S_row[i + 1]),
347
+ # (self.scale_log2, self.scale_log2),
348
+ # (minus_row_max_scaled, minus_row_max_scaled),
349
+ # )
350
+ # acc_S_row[i] = cute.math.exp2(acc_S_row[i], fastmath=True)
351
+ # acc_S_row[i + 1] = cute.math.exp2(acc_S_row[i + 1], fastmath=True)
352
+
353
+ frg_tile = 32
354
+ assert frg_tile % 2 == 0
355
+ frg_cnt = cute.size(acc_S_row) // frg_tile
356
+ assert cute.size(acc_S_row) % frg_tile == 0
357
+ acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
358
+ acc_S_row_converted_frg = cute.logical_divide(
359
+ acc_S_row_converted, cute.make_layout(frg_tile)
360
+ )
361
+ for j in cutlass.range_constexpr(frg_cnt):
362
+ for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
363
+ # acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = (
364
+ # cute.arch.fma_packed_f32x2(
365
+ # (acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]),
366
+ # (self.scale_log2, self.scale_log2),
367
+ # (minus_row_max_scaled, minus_row_max_scaled),
368
+ # )
369
+ # )
370
+ # acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
371
+ # acc_S_row_frg[k + 1, j] = cute.math.exp2(acc_S_row_frg[k + 1, j], fastmath=True)
372
+ acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
373
+ acc_S_row_frg[k + 1, j] = cute.math.exp2(acc_S_row_frg[k + 1, j], fastmath=True)
374
+ acc_S_row_converted_frg[None, j].store(
375
+ acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
376
+ )
377
+
378
+
379
+ @cute.jit
380
+ def floor_if_packed(
381
+ q_idx,
382
+ qhead_per_kvhead: cutlass.Constexpr[int],
383
+ ) -> cute.Tensor:
384
+ """Convert q_idx to packed format for Pack-GQA."""
385
+ if cutlass.const_expr(qhead_per_kvhead == 1):
386
+ return q_idx
387
+ return q_idx // qhead_per_kvhead
388
+
389
+
390
+ @cute.jit
391
+ def apply_score_mod_inner(
392
+ score_tensor,
393
+ index_tensor,
394
+ score_mod: cutlass.Constexpr,
395
+ batch_idx,
396
+ head_idx,
397
+ softmax_scale,
398
+ vec_size: cutlass.Constexpr,
399
+ qk_acc_dtype: cutlass.Constexpr,
400
+ aux_tensors,
401
+ fastdiv_mods,
402
+ seqlen_info: SeqlenInfoQK,
403
+ constant_q_idx: cutlass.Constexpr,
404
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
405
+ transpose_indices: cutlass.Constexpr[bool] = False,
406
+ ):
407
+ """Shared implementation for applying score modification.
408
+
409
+ Args:
410
+ score_tensor: The scores to modify (acc_S for flash_fwd, tSrS_t2r for sm100)
411
+ index_tensor: Index positions (tScS for flash_fwd, tScS_t2r for sm100)
412
+ score_mod: The score modification function to apply
413
+ batch_idx: Batch index
414
+ head_idx: Head index
415
+ softmax_scale: Scale to apply
416
+ vec_size: Vector size for processing elements
417
+ qk_acc_dtype: Data type for accumulator
418
+ aux_tensors: Optional aux_tensors for FlexAttention
419
+ fastdiv_mods: Tuple of (seqlen_q_divmod, seqlen_k_divmod) for wrapping
420
+ seqlen_info: Sequence length info
421
+ constant_q_idx: If provided, use this constant for all q_idx values
422
+ If None, compute q_idx per-element
423
+ qhead_per_kvhead_packgqa: Pack-GQA replication factor. Divide q_idx by this
424
+ when greater than 1 so score mods see logical heads.
425
+ transpose_indices: If True, swap q_idx/kv_idx in index_tensor (for bwd kernel where S is transposed)
426
+ """
427
+ # Index positions in the index_tensor tuple
428
+ # Forward: index_tensor[...][0] = q_idx, index_tensor[...][1] = kv_idx
429
+ # Backward (transposed): index_tensor[...][0] = kv_idx, index_tensor[...][1] = q_idx
430
+ if cutlass.const_expr(transpose_indices):
431
+ q_idx_pos = cutlass.const_expr(1)
432
+ kv_idx_pos = cutlass.const_expr(0)
433
+ else:
434
+ q_idx_pos = cutlass.const_expr(0)
435
+ kv_idx_pos = cutlass.const_expr(1)
436
+
437
+ n_vals = cutlass.const_expr(cute.size(score_tensor.shape))
438
+ score_vec = cute.make_rmem_tensor(vec_size, qk_acc_dtype)
439
+ kv_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
440
+
441
+ # SSA values for batch (constant across all elements)
442
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32).broadcast_to((vec_size,))
443
+
444
+ # Handle q_idx based on whether it's constant
445
+ q_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
446
+
447
+ # For Pack-GQA with non-constant q_idx, we need per-element head indices
448
+ # since a thread my process multiple query head indices
449
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
450
+ head_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
451
+
452
+ for i in cutlass.range(0, n_vals, vec_size, unroll_full=True):
453
+ for j in cutlass.range(vec_size, unroll_full=True):
454
+ score_vec[j] = score_tensor[i + j] * softmax_scale
455
+
456
+ # Extract head offset from packed q_idx for Pack-GQA
457
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
458
+ q_idx_packed = index_tensor[i + j][q_idx_pos]
459
+ # Building up the logical q_head idx: final_q_head = kv_head * qhead_per_kvhead + (q_physical % qhead_per_kvhead)
460
+ q_idx_logical = q_idx_packed // qhead_per_kvhead
461
+ head_offset = q_idx_packed - q_idx_logical * qhead_per_kvhead
462
+ head_idx_vec[j] = head_idx * qhead_per_kvhead + head_offset
463
+
464
+ # If we will do loads we mod, in order to not read OOB
465
+ if cutlass.const_expr(aux_tensors is not None and fastdiv_mods is not None):
466
+ if cutlass.const_expr(constant_q_idx is None):
467
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
468
+ q_idx_floored = floor_if_packed(
469
+ index_tensor[i + j][q_idx_pos], qhead_per_kvhead
470
+ )
471
+ _, q_idx_wrapped = divmod(q_idx_floored, seqlen_q_divmod)
472
+ q_idx_vec[j] = q_idx_wrapped
473
+ else:
474
+ _, seqlen_k_divmod = fastdiv_mods
475
+
476
+ _, kv_idx_wrapped = divmod(index_tensor[i + j][kv_idx_pos], seqlen_k_divmod)
477
+ kv_idx_vec[j] = kv_idx_wrapped
478
+ else:
479
+ # No bounds checking - direct indexing
480
+ if constant_q_idx is None:
481
+ q_idx_vec[j] = floor_if_packed(index_tensor[i + j][q_idx_pos], qhead_per_kvhead)
482
+ kv_idx_vec[j] = index_tensor[i + j][kv_idx_pos]
483
+
484
+ # Convert to SSA for score_mod call
485
+ score_ssa = score_vec.load()
486
+ kv_idx_ssa = kv_idx_vec.load()
487
+ if cutlass.const_expr(constant_q_idx is None):
488
+ q_idx_ssa = q_idx_vec.load()
489
+ else:
490
+ # NB we do not apply Pack-GQA division here, as constant_q_idx is assumed to already be logical
491
+ q_idx_const = constant_q_idx
492
+ q_idx_ssa = utils.scalar_to_ssa(q_idx_const, cutlass.Int32).broadcast_to((vec_size,))
493
+
494
+ # Compute head_idx_ssa: per-element for Pack-GQA with non-constant q_idx, constant otherwise
495
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
496
+ head_idx_ssa = head_idx_vec.load()
497
+ else:
498
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32).broadcast_to((vec_size,))
499
+
500
+ aux_args = []
501
+ if cutlass.const_expr(aux_tensors is not None):
502
+ aux_args = aux_tensors
503
+
504
+ post_mod_scores = score_mod(
505
+ score_ssa,
506
+ batch_idx_ssa,
507
+ head_idx_ssa,
508
+ q_idx=q_idx_ssa,
509
+ kv_idx=kv_idx_ssa,
510
+ seqlen_info=seqlen_info,
511
+ aux_tensors=aux_args,
512
+ )
513
+
514
+ # Write back modified scores
515
+ score_vec.store(post_mod_scores)
516
+ for j in cutlass.range(vec_size, unroll_full=True):
517
+ score_tensor[i + j] = score_vec[j]
518
+
519
+
520
+ @cute.jit
521
+ def apply_score_mod_bwd_inner(
522
+ grad_tensor,
523
+ score_tensor,
524
+ index_tensor,
525
+ score_mod_bwd: cutlass.Constexpr,
526
+ batch_idx,
527
+ head_idx,
528
+ softmax_scale,
529
+ vec_size: cutlass.Constexpr,
530
+ qk_acc_dtype: cutlass.Constexpr,
531
+ aux_tensors,
532
+ fastdiv_mods,
533
+ seqlen_info,
534
+ constant_q_idx: cutlass.Constexpr,
535
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
536
+ transpose_indices: cutlass.Constexpr[bool] = False,
537
+ ):
538
+ """Apply backward score modification (joint graph).
539
+
540
+ Args:
541
+ grad_tensor: in/out: dlogits rewritten in-place with d(scaled_scores)
542
+ score_tensor: pre-mod scores (unscaled QK tile), scaled by softmax_scale internally
543
+ index_tensor: Index positions (same as forward)
544
+ score_mod_bwd: The backward score modification function (joint graph)
545
+ batch_idx: Batch index
546
+ head_idx: Head index
547
+ softmax_scale: Scale to apply to score_tensor
548
+ vec_size: Vector size for processing elements
549
+ qk_acc_dtype: Data type for accumulator
550
+ aux_tensors: Optional aux_tensors for FlexAttention
551
+ fastdiv_mods: Tuple of (seqlen_q_divmod, seqlen_k_divmod) for wrapping
552
+ seqlen_info: Sequence length info
553
+ constant_q_idx: If provided, use this constant for all q_idx values
554
+ qhead_per_kvhead: Pack-GQA replication factor
555
+ transpose_indices: If True, swap q_idx/kv_idx in index_tensor
556
+ """
557
+ # Index positions in the index_tensor tuple
558
+ # Forward: index_tensor[...][0] = q_idx, index_tensor[...][1] = kv_idx
559
+ # Backward (transposed): index_tensor[...][0] = kv_idx, index_tensor[...][1] = q_idx
560
+ if cutlass.const_expr(transpose_indices):
561
+ q_idx_pos = cutlass.const_expr(1)
562
+ kv_idx_pos = cutlass.const_expr(0)
563
+ else:
564
+ q_idx_pos = cutlass.const_expr(0)
565
+ kv_idx_pos = cutlass.const_expr(1)
566
+ n_vals = cutlass.const_expr(cute.size(grad_tensor.shape))
567
+ grad_vec = cute.make_rmem_tensor(vec_size, qk_acc_dtype)
568
+ score_vec = cute.make_rmem_tensor(vec_size, qk_acc_dtype)
569
+ kv_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
570
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32).broadcast_to((vec_size,))
571
+ q_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
572
+
573
+ # For Pack-GQA with non-constant q_idx, we need per-element head indices
574
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
575
+ head_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
576
+
577
+ for i in cutlass.range(0, n_vals, vec_size, unroll_full=True):
578
+ for j in cutlass.range(vec_size, unroll_full=True):
579
+ grad_vec[j] = grad_tensor[i + j]
580
+ # Scale score so joint graph sees same value as forward score_mod
581
+ score_vec[j] = score_tensor[i + j] * softmax_scale
582
+
583
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
584
+ q_idx_packed = index_tensor[i + j][q_idx_pos]
585
+ q_idx_logical = q_idx_packed // qhead_per_kvhead
586
+ head_offset = q_idx_packed - q_idx_logical * qhead_per_kvhead
587
+ head_idx_vec[j] = head_idx * qhead_per_kvhead + head_offset
588
+
589
+ if cutlass.const_expr(aux_tensors is not None and fastdiv_mods is not None):
590
+ if cutlass.const_expr(constant_q_idx is None):
591
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
592
+ q_idx_floored = floor_if_packed(
593
+ index_tensor[i + j][q_idx_pos], qhead_per_kvhead
594
+ )
595
+ _, q_idx_wrapped = divmod(q_idx_floored, seqlen_q_divmod)
596
+ q_idx_vec[j] = q_idx_wrapped
597
+ else:
598
+ _, seqlen_k_divmod = fastdiv_mods
599
+
600
+ _, kv_idx_wrapped = divmod(index_tensor[i + j][kv_idx_pos], seqlen_k_divmod)
601
+ kv_idx_vec[j] = kv_idx_wrapped
602
+ else:
603
+ # No bounds checking - direct indexing
604
+ if constant_q_idx is None:
605
+ q_idx_vec[j] = floor_if_packed(index_tensor[i + j][q_idx_pos], qhead_per_kvhead)
606
+ kv_idx_vec[j] = index_tensor[i + j][kv_idx_pos]
607
+
608
+ grad_ssa = grad_vec.load()
609
+ score_ssa = score_vec.load()
610
+ kv_idx_ssa = kv_idx_vec.load()
611
+
612
+ if cutlass.const_expr(constant_q_idx is None):
613
+ q_idx_ssa = q_idx_vec.load()
614
+ else:
615
+ q_idx_ssa = utils.scalar_to_ssa(constant_q_idx, cutlass.Int32).broadcast_to((vec_size,))
616
+
617
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
618
+ head_idx_ssa = head_idx_vec.load()
619
+ else:
620
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32).broadcast_to((vec_size,))
621
+
622
+ aux_args = []
623
+ if cutlass.const_expr(aux_tensors is not None):
624
+ aux_args = aux_tensors
625
+
626
+ grad_out_ssa = score_mod_bwd(
627
+ grad_ssa,
628
+ score_ssa,
629
+ batch_idx_ssa,
630
+ head_idx_ssa,
631
+ q_idx=q_idx_ssa,
632
+ kv_idx=kv_idx_ssa,
633
+ seqlen_info=seqlen_info,
634
+ aux_tensors=aux_args,
635
+ )
636
+
637
+ grad_vec.store(grad_out_ssa)
638
+ for j in cutlass.range(vec_size, unroll_full=True):
639
+ grad_tensor[i + j] = grad_vec[j]
torch-ext/sol_attn/_vendor/flash_attn/cute/tile_scheduler.py ADDED
@@ -0,0 +1,1087 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
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
+
20
+ from ....sm90._compat.cute_dsl_utils import ParamsBase
21
+
22
+ from . import utils
23
+ from ...._vendor.flash_attn.cute.fast_math import clz
24
+
25
+
26
+ class SchedulingMode(IntEnum):
27
+ NONE = auto()
28
+ STATIC = auto()
29
+ DYNAMIC = auto()
30
+ CLC = auto()
31
+
32
+
33
+ @dataclass
34
+ class ClcState(ParamsBase):
35
+ """Owns the runtime state shared by CLC-capable tile schedulers.
36
+
37
+ `FlashAttentionForwardSm100` constructs this state because it owns the CLC
38
+ response buffer, mbarrier storage, and launch geometry needed to initialize
39
+ the hardware scheduler and async pipeline. Individual tile schedulers then
40
+ consume this state and map the returned hardware work tiles into their own
41
+ logical `WorkTileInfo` coordinates.
42
+
43
+ To add CLC support to a scheduler:
44
+ - implement `clc_problem_shape(params)` so the kernel can create the hardware scheduler
45
+ - accept `clc: ClcState | None` in `create(...)` / `__init__`
46
+ - map `clc.initial_work_tile_info()` and `clc.get_current_work()` into scheduler coordinates
47
+ """
48
+
49
+ _hw_scheduler: ClcDynamicPersistentTileScheduler
50
+ _pipeline: PipelineClcFetchAsync
51
+ _consumer_state: PipelineState
52
+ _producer_state: PipelineState
53
+
54
+ @staticmethod
55
+ def create(
56
+ *,
57
+ hw_scheduler: ClcDynamicPersistentTileScheduler,
58
+ pipeline: PipelineClcFetchAsync,
59
+ consumer_state: PipelineState,
60
+ producer_state: PipelineState,
61
+ ) -> "ClcState":
62
+ return ClcState(hw_scheduler, pipeline, consumer_state, producer_state)
63
+
64
+ def initial_work_tile_info(self):
65
+ return self._hw_scheduler.initial_work_tile_info()
66
+
67
+ def get_current_work(self):
68
+ return self._hw_scheduler.get_current_work()
69
+
70
+ def prefetch_next_work(self, *, loc=None, ip=None):
71
+ self._pipeline.producer_acquire(self._producer_state, loc=loc, ip=ip)
72
+ mbarrier_addr = self._pipeline.producer_get_barrier(self._producer_state, loc=loc, ip=ip)
73
+ self._hw_scheduler.advance_to_next_work(mbarrier_addr, loc=loc, ip=ip)
74
+ self._producer_state.advance(loc=loc, ip=ip)
75
+
76
+ def consumer_wait(self, *, loc=None, ip=None):
77
+ self._pipeline.consumer_wait(self._consumer_state, loc=loc, ip=ip)
78
+
79
+ def consumer_release(self, *, loc=None, ip=None):
80
+ self._pipeline.consumer_release(self._consumer_state, loc=loc, ip=ip)
81
+ self._consumer_state.advance(loc=loc, ip=ip)
82
+
83
+ def producer_tail(self, *, loc=None, ip=None):
84
+ self._pipeline.producer_tail(self._producer_state, loc=loc, ip=ip)
85
+
86
+
87
+ class WorkTileInfo(cutlass.utils.WorkTileInfo):
88
+ """Altered WorkTileInfo which includes four axes: (block, head, batch, split)"""
89
+
90
+ @override
91
+ def __new_from_mlir_values__(self, values: list[ir.Value]) -> "WorkTileInfo":
92
+ assert len(values) == 5
93
+ new_tile_idx = cutlass.new_from_mlir_values(self._tile_idx, values[:-1])
94
+ new_is_valid_tile = cutlass.new_from_mlir_values(self._is_valid_tile, [values[-1]])
95
+ return WorkTileInfo(new_tile_idx, new_is_valid_tile)
96
+
97
+
98
+ @runtime_checkable
99
+ class TileSchedulerProtocol(Protocol):
100
+ """Protocol defining the interface all tile schedulers must implement.
101
+
102
+ Schedulers are responsible for:
103
+ 1. Coordinate mapping: linear tile index -> (m_block, head, batch, split)
104
+ 2. Work distribution: how to get the next tile (static grid-stride vs CLC dynamic)
105
+ """
106
+
107
+ def get_current_work(self) -> WorkTileInfo:
108
+ """Get the current work tile coordinates."""
109
+ ...
110
+
111
+ def initial_work_tile_info(self) -> WorkTileInfo:
112
+ """Get the initial work tile for this CTA."""
113
+ ...
114
+
115
+ def advance_to_next_work(self, *, loc=None, ip=None):
116
+ """Consumer-side advance: move to next tile and return it.
117
+
118
+ For static schedulers: grid-stride increment + get_current_work.
119
+ For CLC schedulers: consumer wait + get_current_work + consumer release + state advance.
120
+ """
121
+ ...
122
+
123
+ def prefetch_next_work(self, *, loc=None, ip=None) -> None:
124
+ """Producer-side prefetch of next work tile (no-op for static schedulers).
125
+
126
+ For CLC schedulers: producer acquire + issue CLC query + producer state advance.
127
+ Only called by the scheduler warp.
128
+ """
129
+ ...
130
+
131
+ def producer_tail(self, *, loc=None, ip=None) -> None:
132
+ """Producer-side cleanup after the last tile.
133
+
134
+ No-op for static schedulers. For CLC schedulers: pipeline producer_tail.
135
+ """
136
+ ...
137
+
138
+
139
+ @dataclass
140
+ class TileSchedulerArguments(ParamsBase):
141
+ num_block: Int32
142
+ num_head: Int32
143
+ num_batch: Int32
144
+ num_splits: Int32
145
+ seqlen_k: Int32
146
+ headdim: Int32
147
+ headdim_v: Int32
148
+ total_q: Int32
149
+ tile_shape_mn: cutlass.Constexpr[Tuple[int, int]]
150
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
151
+ mCuSeqlensQ: Optional[cute.Tensor] = None
152
+ mSeqUsedQ: Optional[cute.Tensor] = None
153
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
154
+ element_size: cutlass.Constexpr[int] = 2
155
+ is_persistent: cutlass.Constexpr[bool] = False
156
+ lpt: cutlass.Constexpr[bool] = False
157
+ is_split_kv: cutlass.Constexpr[bool] = False
158
+ head_swizzle: cutlass.Constexpr[bool] = False
159
+ use_cluster_idx: cutlass.Constexpr[bool] = False
160
+
161
+
162
+ class SingleTileScheduler:
163
+ @dataclass
164
+ class Params(ParamsBase):
165
+ num_block: Int32
166
+ num_head: Int32
167
+ num_batch: Int32
168
+ num_splits: Int32
169
+ num_splits_divmod: FastDivmodDivisor
170
+ is_split_kv: cutlass.Constexpr[bool] = False
171
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
172
+ use_cluster_idx: cutlass.Constexpr[bool] = False
173
+
174
+ @staticmethod
175
+ def create(
176
+ args: TileSchedulerArguments, *, loc=None, ip=None
177
+ ) -> "SingleTileScheduler.Params":
178
+ return SingleTileScheduler.Params(
179
+ args.num_block,
180
+ args.num_head,
181
+ args.num_batch,
182
+ args.num_splits,
183
+ FastDivmodDivisor(args.num_splits),
184
+ args.is_split_kv,
185
+ args.cluster_shape_mn,
186
+ args.use_cluster_idx,
187
+ )
188
+
189
+ def __init__(self, params: Params, blk_coord: cute.Coord, *, loc=None, ip=None):
190
+ self.params = params
191
+ self._blk_coord = blk_coord
192
+ self._is_first_block = True
193
+ self._loc = loc
194
+ self._ip = ip
195
+
196
+ @staticmethod
197
+ def to_underlying_arguments(
198
+ args: TileSchedulerArguments,
199
+ *,
200
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
201
+ loc=None,
202
+ ip=None,
203
+ ) -> Params:
204
+ assert scheduling_mode == SchedulingMode.STATIC, (
205
+ f"SingleTileScheduler only supports STATIC, got {scheduling_mode!r}"
206
+ )
207
+ return SingleTileScheduler.Params.create(args, loc=loc, ip=ip)
208
+
209
+ @staticmethod
210
+ def create(
211
+ params: Params, clc: ClcState | None = None, *, loc=None, ip=None
212
+ ) -> "SingleTileScheduler":
213
+ if const_expr(cute.size(params.cluster_shape_mn) == 1 or not params.use_cluster_idx):
214
+ blk_coord = cute.arch.block_idx()
215
+ else:
216
+ blk_coord = cute.arch.cluster_idx()
217
+ return SingleTileScheduler(params, blk_coord, loc=loc, ip=ip)
218
+
219
+ # called by host
220
+ @staticmethod
221
+ def get_grid_shape(
222
+ params: Params,
223
+ *,
224
+ loc=None,
225
+ ip=None,
226
+ ) -> Tuple[Int32, Int32, Int32]:
227
+ # TODO: this hard-codes the fact that we only use cluster = (1, 1) or (2, 1)
228
+ assert params.cluster_shape_mn[1] == 1, "Only cluster_shape_mn[1] == 1 is supported"
229
+ if const_expr(params.use_cluster_idx):
230
+ # Grid must have num_block * cluster_m physical blocks so that there are num_block clusters
231
+ grid_x = params.num_block * params.cluster_shape_mn[0]
232
+ else:
233
+ grid_x = cute.round_up(params.num_block, params.cluster_shape_mn[0])
234
+ return (
235
+ grid_x,
236
+ params.num_head * params.num_splits,
237
+ params.num_batch,
238
+ )
239
+
240
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
241
+ block_idx, head_idx, batch_idx = self._blk_coord
242
+ if const_expr(self.params.is_split_kv):
243
+ head_idx, split_idx = divmod(head_idx, self.params.num_splits_divmod)
244
+ else:
245
+ split_idx = Int32(0)
246
+ return WorkTileInfo(
247
+ (block_idx, head_idx, batch_idx, split_idx),
248
+ self._is_first_block,
249
+ )
250
+
251
+ def initial_work_tile_info(self, *, loc=None, ip=None):
252
+ return self.get_current_work(loc=loc, ip=ip)
253
+
254
+ def prefetch_next_work(self, *, loc=None, ip=None):
255
+ pass
256
+
257
+ def advance_to_next_work(self, *, loc=None, ip=None):
258
+ self._is_first_block = False
259
+ return self.get_current_work()
260
+
261
+ def producer_tail(self, *, loc=None, ip=None):
262
+ pass
263
+
264
+ def __extract_mlir_values__(self):
265
+ values, self._values_pos = [], []
266
+ for obj in [self.params, self._blk_coord]:
267
+ obj_values = cutlass.extract_mlir_values(obj)
268
+ values += obj_values
269
+ self._values_pos.append(len(obj_values))
270
+ return values
271
+
272
+ def __new_from_mlir_values__(self, values):
273
+ obj_list = []
274
+ for obj, n_items in zip([self.params, self._blk_coord], self._values_pos):
275
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
276
+ values = values[n_items:]
277
+ return SingleTileScheduler(*(tuple(obj_list)), loc=self._loc)
278
+
279
+
280
+ class StaticPersistentTileScheduler:
281
+ @dataclass
282
+ class Params(ParamsBase):
283
+ num_block_cluster_divmod: FastDivmodDivisor
284
+ num_head_divmod: FastDivmodDivisor
285
+ total_blocks_cluster: Int32
286
+ cluster_shape_m: cutlass.Constexpr[int] = 1
287
+
288
+ @staticmethod
289
+ def create(
290
+ args: TileSchedulerArguments, *, loc=None, ip=None
291
+ ) -> "StaticPersistentTileScheduler.Params":
292
+ num_block_cluster = cute.ceil_div(args.num_block, cute.size(args.cluster_shape_mn))
293
+ total_blocks_cluster = num_block_cluster * args.num_head * args.num_batch
294
+ return StaticPersistentTileScheduler.Params(
295
+ FastDivmodDivisor(num_block_cluster),
296
+ FastDivmodDivisor(args.num_head),
297
+ total_blocks_cluster,
298
+ cluster_shape_m=args.cluster_shape_mn[0],
299
+ )
300
+
301
+ def __init__(self, params: Params, tile_idx: Int32, *, loc=None, ip=None):
302
+ self.params = params
303
+ self._tile_idx = tile_idx
304
+ self._loc = loc
305
+ self._ip = ip
306
+
307
+ @staticmethod
308
+ def to_underlying_arguments(
309
+ args: TileSchedulerArguments,
310
+ *,
311
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
312
+ loc=None,
313
+ ip=None,
314
+ ) -> Params:
315
+ assert scheduling_mode == SchedulingMode.STATIC, (
316
+ f"StaticPersistentTileScheduler only supports STATIC, got {scheduling_mode!r}"
317
+ )
318
+ return StaticPersistentTileScheduler.Params.create(args, loc=loc, ip=ip)
319
+
320
+ @staticmethod
321
+ def create(
322
+ params: Params, clc: ClcState | None = None, *, loc=None, ip=None
323
+ ) -> "StaticPersistentTileScheduler":
324
+ if const_expr(cute.size(params.cluster_shape_m) == 1):
325
+ tile_idx = cute.arch.block_idx()[0]
326
+ else:
327
+ tile_idx = cute.arch.cluster_idx()[0]
328
+ return StaticPersistentTileScheduler(params, tile_idx, loc=loc, ip=ip)
329
+
330
+ @staticmethod
331
+ def get_grid_shape(
332
+ params: Params,
333
+ *,
334
+ loc=None,
335
+ ip=None,
336
+ ) -> Tuple[Int32, Int32, Int32]:
337
+ hardware_info = cutlass.utils.HardwareInfo()
338
+ sm_count = hardware_info.get_device_multiprocessor_count()
339
+ max_ctas = (sm_count // params.cluster_shape_m) * params.cluster_shape_m
340
+ grid_x = cutlass.min(max_ctas, params.total_blocks_cluster * params.cluster_shape_m)
341
+ return (grid_x, Int32(1), Int32(1))
342
+
343
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
344
+ hn_idx, block_idx = divmod(self._tile_idx, self.params.num_block_cluster_divmod)
345
+ batch_idx, head_idx = divmod(hn_idx, self.params.num_head_divmod)
346
+ is_valid = self._tile_idx < self.params.total_blocks_cluster
347
+ return WorkTileInfo(
348
+ (Int32(block_idx), Int32(head_idx), Int32(batch_idx), Int32(0)), is_valid
349
+ )
350
+
351
+ def initial_work_tile_info(self, *, loc=None, ip=None):
352
+ return self.get_current_work(loc=loc, ip=ip)
353
+
354
+ def prefetch_next_work(self, *, loc=None, ip=None):
355
+ pass
356
+
357
+ def advance_to_next_work(self, *, loc=None, ip=None):
358
+ if const_expr(self.params.cluster_shape_m == 1):
359
+ self._tile_idx += cute.arch.grid_dim()[0]
360
+ else:
361
+ self._tile_idx += cute.arch.cluster_dim()[0]
362
+ return self.get_current_work()
363
+
364
+ def producer_tail(self, *, loc=None, ip=None):
365
+ pass
366
+
367
+ def __extract_mlir_values__(self):
368
+ values, self._values_pos = [], []
369
+ for obj in [self.params, self._tile_idx]:
370
+ obj_values = cutlass.extract_mlir_values(obj)
371
+ values += obj_values
372
+ self._values_pos.append(len(obj_values))
373
+ return values
374
+
375
+ def __new_from_mlir_values__(self, values):
376
+ obj_list = []
377
+ for obj, n_items in zip(
378
+ [self.params, self._tile_idx],
379
+ self._values_pos,
380
+ ):
381
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
382
+ values = values[n_items:]
383
+ return StaticPersistentTileScheduler(*(tuple(obj_list)), loc=self._loc)
384
+
385
+
386
+ class SingleTileLPTScheduler:
387
+ @dataclass
388
+ class Params(ParamsBase):
389
+ total_blocks: Int32
390
+ num_splits: Int32
391
+ num_block: Int32
392
+ num_head: Int32
393
+ num_batch: Int32
394
+ l2_minor: Int32
395
+ num_head_divmod: FastDivmodDivisor
396
+ l2_minor_divmod: FastDivmodDivisor
397
+ l2_major_divmod: FastDivmodDivisor
398
+ l2_minor_residual_divmod: FastDivmodDivisor
399
+ num_hb_quotient: Int32
400
+ num_splits_divmod: FastDivmodDivisor
401
+ is_split_kv: cutlass.Constexpr[bool] = False
402
+ cluster_shape_m: cutlass.Constexpr[int] = 1
403
+ scheduling_mode: cutlass.Constexpr[SchedulingMode] = SchedulingMode.STATIC
404
+ lpt: cutlass.Constexpr[bool] = True
405
+
406
+ @staticmethod
407
+ @cute.jit
408
+ def create(
409
+ args: TileSchedulerArguments,
410
+ *,
411
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
412
+ loc=None,
413
+ ip=None,
414
+ ) -> "SingleTileLPTScheduler.Params":
415
+ assert scheduling_mode in (SchedulingMode.STATIC, SchedulingMode.CLC), (
416
+ f"Only STATIC and CLC are supported, got {scheduling_mode!r}"
417
+ )
418
+ size_one_kv_head = args.seqlen_k * (args.headdim + args.headdim_v) * args.element_size
419
+ size_one_head = size_one_kv_head
420
+ size_l2 = 50 * 1024 * 1024 # 40 MB for K & V
421
+ # Swizzle is the size of each "section". Round swizzle to a power of 2
422
+ # Need to be careful about the case where only one head will fit
423
+ # swizzle is how many heads can fit in L2
424
+ # Seems faster if swizzle is a power of 2
425
+ log2_floor = lambda n: 31 - clz(n)
426
+ swizzle = 1 if size_l2 < size_one_head else (1 << log2_floor(size_l2 // size_one_head))
427
+ # If we're in the last section (called residual), we don't want to divide by
428
+ # swizzle. Instead we want to divide by the remainder.
429
+ num_hb_quotient = (args.num_head * args.num_batch) // swizzle
430
+ num_hb_remainder = (args.num_head * args.num_batch) % swizzle
431
+ return SingleTileLPTScheduler.Params(
432
+ total_blocks=args.num_block * args.num_head * args.num_batch,
433
+ num_block=args.num_block,
434
+ num_head=args.num_head,
435
+ num_batch=args.num_batch,
436
+ l2_minor=Int32(swizzle),
437
+ num_head_divmod=FastDivmodDivisor(args.num_head),
438
+ l2_minor_divmod=FastDivmodDivisor(swizzle),
439
+ l2_major_divmod=FastDivmodDivisor(swizzle * args.num_block),
440
+ l2_minor_residual_divmod=FastDivmodDivisor(max(num_hb_remainder, 1)),
441
+ num_hb_quotient=Int32(num_hb_quotient),
442
+ num_splits=args.num_splits,
443
+ num_splits_divmod=FastDivmodDivisor(args.num_splits),
444
+ is_split_kv=args.is_split_kv,
445
+ cluster_shape_m=args.cluster_shape_mn[0],
446
+ scheduling_mode=scheduling_mode,
447
+ lpt=args.lpt,
448
+ )
449
+
450
+ def __init__(
451
+ self,
452
+ params: Params,
453
+ tile_idx: Int32,
454
+ split_idx: Int32,
455
+ clc: ClcState | None = None,
456
+ *,
457
+ loc=None,
458
+ ip=None,
459
+ ):
460
+ self.params = params
461
+ self._tile_idx = tile_idx
462
+ self._split_idx = split_idx
463
+ self.clc = clc
464
+ self._loc = loc
465
+ self._ip = ip
466
+
467
+ @staticmethod
468
+ def to_underlying_arguments(
469
+ args: TileSchedulerArguments,
470
+ *,
471
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
472
+ loc=None,
473
+ ip=None,
474
+ ) -> Params:
475
+ return SingleTileLPTScheduler.Params.create(
476
+ args, scheduling_mode=scheduling_mode, loc=loc, ip=ip
477
+ )
478
+
479
+ @staticmethod
480
+ def _clc_grid_shape(params: Params):
481
+ num_batch_splits = (
482
+ params.num_batch * params.num_splits
483
+ if const_expr(params.is_split_kv)
484
+ else params.num_batch
485
+ )
486
+ return (
487
+ cute.round_up(params.num_block, params.cluster_shape_m),
488
+ params.num_head,
489
+ num_batch_splits,
490
+ )
491
+
492
+ @staticmethod
493
+ @cute.jit
494
+ def clc_problem_shape(params: Params):
495
+ return ClcDynamicPersistentTileSchedulerParams(
496
+ problem_shape_ntile_mnl=SingleTileLPTScheduler._clc_grid_shape(params),
497
+ cluster_shape_mnk=(params.cluster_shape_m, 1, 1),
498
+ )
499
+
500
+ @staticmethod
501
+ @cute.jit
502
+ def create(
503
+ params: Params, clc: ClcState | None = None, *, loc=None, ip=None
504
+ ) -> "SingleTileLPTScheduler":
505
+ if const_expr(params.scheduling_mode == SchedulingMode.CLC):
506
+ return SingleTileLPTScheduler(
507
+ params, cute.arch.block_idx()[0], Int32(0), clc, loc=loc, ip=ip
508
+ )
509
+ tile_idx, split_idx, _ = cute.arch.block_idx()
510
+ return SingleTileLPTScheduler(params, tile_idx, split_idx, loc=loc, ip=ip)
511
+
512
+ @staticmethod
513
+ def get_grid_shape(
514
+ params: Params,
515
+ *,
516
+ loc=None,
517
+ ip=None,
518
+ ) -> Tuple[Int32, Int32, Int32]:
519
+ if const_expr(params.scheduling_mode == SchedulingMode.CLC):
520
+ return SingleTileLPTScheduler._clc_grid_shape(params)
521
+ return (params.total_blocks, params.num_splits, Int32(1))
522
+
523
+ @cute.jit
524
+ def clc_work_to_coords(self, work) -> WorkTileInfo:
525
+ """Convert CLC response (block, head, batch_split) to WorkTileInfo.
526
+
527
+ CLC returns raw grid coordinates — no L2 swizzle (hardware decides order).
528
+ We only apply cluster division, optional LPT block reversal, and split_kv unpacking.
529
+ """
530
+ block_idx = work.tile_idx[0]
531
+ if const_expr(self.params.cluster_shape_m > 1):
532
+ block_idx = block_idx // self.params.cluster_shape_m
533
+ if const_expr(self.params.lpt):
534
+ # Longest-processing-time-first: reverse block order
535
+ block_idx = self.params.num_block - 1 - block_idx
536
+ split_idx = Int32(0)
537
+ if const_expr(self.params.is_split_kv):
538
+ batch_idx, split_idx = divmod(work.tile_idx[2], self.params.num_splits_divmod)
539
+ else:
540
+ batch_idx = work.tile_idx[2]
541
+ return WorkTileInfo(
542
+ (Int32(block_idx), Int32(work.tile_idx[1]), Int32(batch_idx), Int32(split_idx)),
543
+ work.is_valid_tile,
544
+ )
545
+
546
+ @cute.jit
547
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
548
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
549
+ work = self.clc.get_current_work()
550
+ self._tile_idx = work.tile_idx[0]
551
+ return self.clc_work_to_coords(work)
552
+ # Static path: L2-swizzled coordinate mapping
553
+ params = self.params
554
+ # Implement LPT scheduling coordinate calculation
555
+ bidhb, l2_mod = divmod(self._tile_idx, params.l2_major_divmod)
556
+ # If we're in the last section (called residual), we don't want to divide by
557
+ # swizzle. Instead we want to divide by the remainder.
558
+ block, bidhb_residual = 0, 0
559
+ if bidhb < params.num_hb_quotient:
560
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_divmod)
561
+ else:
562
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_residual_divmod)
563
+ bidhb_actual = bidhb * params.l2_minor + bidhb_residual
564
+ batch_idx, head_idx = divmod(bidhb_actual, params.num_head_divmod)
565
+ # Longest-processing-time-first
566
+ if const_expr(params.lpt):
567
+ block = params.num_block - 1 - block
568
+ is_valid = self._tile_idx < params.total_blocks
569
+ return WorkTileInfo(
570
+ (Int32(block), Int32(head_idx), Int32(batch_idx), Int32(self._split_idx)), is_valid
571
+ )
572
+
573
+ @cute.jit
574
+ def initial_work_tile_info(self, *, loc=None, ip=None):
575
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
576
+ work = self.clc.initial_work_tile_info()
577
+ self._tile_idx = work.tile_idx[0]
578
+ return self.clc_work_to_coords(work)
579
+ return self.get_current_work(loc=loc, ip=ip)
580
+
581
+ def prefetch_next_work(self, *, loc=None, ip=None):
582
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
583
+ self.clc.prefetch_next_work(loc=loc, ip=ip)
584
+
585
+ def advance_to_next_work(self, *, loc=None, ip=None):
586
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
587
+ self.clc.consumer_wait(loc=loc, ip=ip)
588
+ work = self.get_current_work()
589
+ self.clc.consumer_release(loc=loc, ip=ip)
590
+ return work
591
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
592
+ self._tile_idx = self.params.total_blocks
593
+ return self.get_current_work()
594
+
595
+ def producer_tail(self, *, loc=None, ip=None):
596
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
597
+ self.clc.producer_tail(loc=loc, ip=ip)
598
+
599
+ def __extract_mlir_values__(self):
600
+ values, self._values_pos = [], []
601
+ objs = [self.params, self._tile_idx, self._split_idx]
602
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
603
+ objs += [self.clc]
604
+ for obj in objs:
605
+ obj_values = cutlass.extract_mlir_values(obj)
606
+ values += obj_values
607
+ self._values_pos.append(len(obj_values))
608
+ return values
609
+
610
+ def __new_from_mlir_values__(self, values):
611
+ obj_list = []
612
+ objs = [self.params, self._tile_idx, self._split_idx]
613
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
614
+ objs += [self.clc]
615
+ for obj, n_items in zip(objs, self._values_pos):
616
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
617
+ values = values[n_items:]
618
+ return self.__class__(*obj_list, loc=self._loc)
619
+
620
+
621
+ class SingleTileLPTBwdScheduler:
622
+ @dataclass
623
+ class Params(ParamsBase):
624
+ total_blocks: Int32
625
+ num_block: Int32
626
+ l2_minor: Int32
627
+ num_head_divmod: FastDivmodDivisor
628
+ l2_minor_divmod: FastDivmodDivisor
629
+ l2_major_divmod: FastDivmodDivisor
630
+ l2_minor_residual_divmod: FastDivmodDivisor
631
+ num_hb_quotient: Int32
632
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
633
+ spt: cutlass.Constexpr[bool] = True
634
+
635
+ @staticmethod
636
+ @cute.jit
637
+ def create(
638
+ args: TileSchedulerArguments, *, loc=None, ip=None
639
+ ) -> "SingleTileLPTBwdScheduler.Params":
640
+ size_l2 = 50 * 1024 * 1024
641
+ size_one_qdo_head = args.seqlen_k * (args.headdim + args.headdim_v) * args.element_size
642
+ size_one_dqaccum_head = args.seqlen_k * (args.headdim) * 4
643
+ # size_one_dqaccum_head = 0
644
+ size_one_head = size_one_qdo_head + size_one_dqaccum_head
645
+ log2_floor = lambda n: 31 - clz(n)
646
+ swizzle = 1 if size_l2 < size_one_head else (1 << log2_floor(size_l2 // size_one_head))
647
+ # swizzle = 8
648
+ # If we're in the last section (called residual), we don't want to divide by
649
+ # swizzle. Instead we want to divide by the remainder.
650
+ num_hb_quotient = (args.num_head * args.num_batch) // swizzle
651
+ num_hb_remainder = (args.num_head * args.num_batch) % swizzle
652
+ num_block = cute.ceil_div(args.num_block, args.cluster_shape_mn[0])
653
+ return SingleTileLPTBwdScheduler.Params(
654
+ total_blocks=(num_block * args.cluster_shape_mn[0])
655
+ * args.num_head
656
+ * args.num_batch,
657
+ num_block=num_block,
658
+ l2_minor=Int32(swizzle),
659
+ num_head_divmod=FastDivmodDivisor(args.num_head),
660
+ l2_minor_divmod=FastDivmodDivisor(swizzle),
661
+ l2_major_divmod=FastDivmodDivisor(swizzle * num_block),
662
+ l2_minor_residual_divmod=FastDivmodDivisor(
663
+ max(num_hb_remainder, 1)
664
+ ), # don't divide by 0
665
+ num_hb_quotient=Int32(num_hb_quotient),
666
+ cluster_shape_mn=args.cluster_shape_mn,
667
+ spt=args.lpt,
668
+ )
669
+
670
+ def __init__(self, params: Params, tile_idx: Int32, *, loc=None, ip=None):
671
+ self.params = params
672
+ self._tile_idx = tile_idx
673
+ self._loc = loc
674
+ self._ip = ip
675
+
676
+ @staticmethod
677
+ def to_underlying_arguments(
678
+ args: TileSchedulerArguments,
679
+ *,
680
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
681
+ loc=None,
682
+ ip=None,
683
+ ) -> Params:
684
+ assert scheduling_mode == SchedulingMode.STATIC, (
685
+ f"SingleTileLPTBwdScheduler only supports STATIC, got {scheduling_mode!r}"
686
+ )
687
+ return SingleTileLPTBwdScheduler.Params.create(args, loc=loc, ip=ip)
688
+
689
+ @staticmethod
690
+ @cute.jit
691
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileLPTBwdScheduler":
692
+ tile_idx = cute.arch.block_idx()[0]
693
+ return SingleTileLPTBwdScheduler(params, tile_idx, loc=loc, ip=ip)
694
+
695
+ # called by host
696
+ @staticmethod
697
+ def get_grid_shape(
698
+ params: Params,
699
+ *,
700
+ loc=None,
701
+ ip=None,
702
+ ) -> Tuple[Int32, Int32, Int32]:
703
+ return (params.total_blocks, Int32(1), Int32(1))
704
+
705
+ @cute.jit
706
+ def get_current_work(self, *, loc=None, ip=None) -> cutlass.utils.WorkTileInfo:
707
+ cluster_idx = self._tile_idx // self.params.cluster_shape_mn[0]
708
+ params = self.params
709
+ # Implement LPT scheduling coordinate calculation
710
+ bidhb, l2_mod = divmod(cluster_idx, params.l2_major_divmod)
711
+ # If we're in the last section (called residual), we don't want to divide by
712
+ # swizzle. Instead we want to divide by the remainder.
713
+ block, bidhb_residual = 0, 0
714
+ if bidhb < params.num_hb_quotient:
715
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_divmod)
716
+ else:
717
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_residual_divmod)
718
+ bidhb_actual = bidhb * params.l2_minor + bidhb_residual
719
+ batch_idx, head_idx = divmod(bidhb_actual, params.num_head_divmod)
720
+ if cutlass.const_expr(params.spt):
721
+ block = params.num_block - 1 - block
722
+ if cutlass.const_expr(params.cluster_shape_mn[0] > 1):
723
+ bidx_in_cluster = cute.arch.block_in_cluster_idx()
724
+ block = block * params.cluster_shape_mn[0] + bidx_in_cluster[0]
725
+ is_valid = self._tile_idx < params.total_blocks
726
+ return WorkTileInfo((Int32(block), Int32(head_idx), Int32(batch_idx), Int32(0)), is_valid)
727
+
728
+ def initial_work_tile_info(self, *, loc=None, ip=None):
729
+ return self.get_current_work(loc=loc, ip=ip)
730
+
731
+ def prefetch_next_work(self, *, loc=None, ip=None):
732
+ pass
733
+
734
+ def advance_to_next_work(self, *, loc=None, ip=None):
735
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
736
+ self._tile_idx = self.params.total_blocks
737
+ return self.get_current_work()
738
+
739
+ def __extract_mlir_values__(self):
740
+ values, self._values_pos = [], []
741
+ for obj in [self.params, self._tile_idx]:
742
+ obj_values = cutlass.extract_mlir_values(obj)
743
+ values += obj_values
744
+ self._values_pos.append(len(obj_values))
745
+ return values
746
+
747
+ def __new_from_mlir_values__(self, values):
748
+ obj_list = []
749
+ for obj, n_items in zip([self.params, self._tile_idx], self._values_pos):
750
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
751
+ values = values[n_items:]
752
+ return self.__class__(*(tuple(obj_list)), loc=self._loc)
753
+
754
+
755
+ class SingleTileVarlenScheduler:
756
+ @dataclass
757
+ class Params(ParamsBase):
758
+ num_head: Int32
759
+ num_batch: Int32
760
+ total_q: Int32
761
+ num_splits: Int32
762
+ max_kvblock_in_l2: Int32
763
+ tile_shape_mn: cutlass.Constexpr[Tuple[int, int]]
764
+ mCuSeqlensQ: Optional[cute.Tensor] = None
765
+ mSeqUsedQ: Optional[cute.Tensor] = None
766
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
767
+ lpt: cutlass.Constexpr[bool] = False
768
+ is_split_kv: cutlass.Constexpr[bool] = False
769
+ head_swizzle: cutlass.Constexpr[bool] = False
770
+ cluster_shape_m: cutlass.Constexpr[int] = 1
771
+ scheduling_mode: cutlass.Constexpr[SchedulingMode] = SchedulingMode.STATIC
772
+
773
+ @staticmethod
774
+ @cute.jit
775
+ def create(
776
+ args: TileSchedulerArguments,
777
+ *,
778
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
779
+ loc=None,
780
+ ip=None,
781
+ ) -> "SingleTileVarlenScheduler.Params":
782
+ assert scheduling_mode in (SchedulingMode.STATIC, SchedulingMode.CLC), (
783
+ f"Only STATIC and CLC are supported, got {scheduling_mode!r}"
784
+ )
785
+ size_l2 = 50 * 1024 * 1024 # 50 MB for K & V
786
+ # if backward, this is qdo block size
787
+ kv_block_size = (
788
+ (args.headdim + args.headdim_v) * args.element_size * args.tile_shape_mn[1]
789
+ )
790
+ # if backward, add dqaccum block size to calculate swizzle
791
+ if args.head_swizzle:
792
+ kv_block_size += args.headdim * 4 * args.tile_shape_mn[1]
793
+ max_kvblock_in_l2 = size_l2 // kv_block_size
794
+ assert args.mCuSeqlensQ is not None or args.mSeqUsedQ is not None, (
795
+ "At least one of mCuSeqlensQ or mSeqUsedQ must be provided"
796
+ )
797
+ assert args.cluster_shape_mn[1] == 1, "Only cluster_shape_mn[1] == 1 is supported"
798
+ # TODO: Support varlen CLC with cluster_shape_m > 1 by refactoring the
799
+ # flattened-tile decode so cluster unpacking semantics are explicit.
800
+ assert scheduling_mode != SchedulingMode.CLC or args.cluster_shape_mn[0] == 1, (
801
+ "Varlen CLC currently requires cluster_shape_mn[0] == 1"
802
+ )
803
+ return SingleTileVarlenScheduler.Params(
804
+ num_head=args.num_head,
805
+ num_batch=args.num_batch,
806
+ total_q=args.total_q,
807
+ num_splits=args.num_splits,
808
+ max_kvblock_in_l2=max_kvblock_in_l2,
809
+ tile_shape_mn=args.tile_shape_mn,
810
+ mCuSeqlensQ=args.mCuSeqlensQ,
811
+ mSeqUsedQ=args.mSeqUsedQ,
812
+ qhead_per_kvhead_packgqa=args.qhead_per_kvhead_packgqa,
813
+ lpt=args.lpt,
814
+ is_split_kv=args.is_split_kv,
815
+ head_swizzle=args.head_swizzle,
816
+ cluster_shape_m=args.cluster_shape_mn[0],
817
+ scheduling_mode=scheduling_mode,
818
+ )
819
+
820
+ def __init__(
821
+ self,
822
+ params: Params,
823
+ tile_idx: Int32,
824
+ split_idx: Int32,
825
+ clc: ClcState | None = None,
826
+ *,
827
+ loc=None,
828
+ ip=None,
829
+ ):
830
+ self.params = params
831
+ self._tile_idx = tile_idx
832
+ self._split_idx = split_idx
833
+ self._is_first_block = True
834
+ self.clc = clc
835
+ self._loc = loc
836
+ self._ip = ip
837
+
838
+ @staticmethod
839
+ def to_underlying_arguments(
840
+ args: TileSchedulerArguments,
841
+ *,
842
+ scheduling_mode: SchedulingMode = SchedulingMode.STATIC,
843
+ loc=None,
844
+ ip=None,
845
+ ) -> Params:
846
+ return SingleTileVarlenScheduler.Params.create(
847
+ args, scheduling_mode=scheduling_mode, loc=loc, ip=ip
848
+ )
849
+
850
+ @staticmethod
851
+ @cute.jit
852
+ def clc_problem_shape(params: Params):
853
+ return ClcDynamicPersistentTileSchedulerParams(
854
+ problem_shape_ntile_mnl=SingleTileVarlenScheduler.get_grid_shape(params),
855
+ cluster_shape_mnk=(1, 1, 1),
856
+ )
857
+
858
+ @staticmethod
859
+ @cute.jit
860
+ def create(
861
+ params: Params, clc: ClcState | None = None, *, loc=None, ip=None
862
+ ) -> "SingleTileVarlenScheduler":
863
+ if const_expr(params.scheduling_mode == SchedulingMode.CLC):
864
+ block_idx = cute.arch.block_idx()
865
+ split_idx = Int32(0)
866
+ if const_expr(params.is_split_kv):
867
+ split_idx = block_idx[1]
868
+ return SingleTileVarlenScheduler(
869
+ params,
870
+ block_idx[0],
871
+ split_idx,
872
+ clc,
873
+ loc=loc,
874
+ ip=ip,
875
+ )
876
+ tile_idx, split_idx, _ = cute.arch.block_idx()
877
+ return SingleTileVarlenScheduler(params, tile_idx, split_idx, loc=loc, ip=ip)
878
+
879
+ # called by host
880
+ @staticmethod
881
+ def get_grid_shape(
882
+ params: Params,
883
+ *,
884
+ loc=None,
885
+ ip=None,
886
+ ) -> Tuple[Int32, Int32, Int32]:
887
+ total_blocks_max = (
888
+ params.total_q
889
+ + params.num_batch * (params.cluster_shape_m * params.tile_shape_mn[0] - 1)
890
+ ) // params.tile_shape_mn[0]
891
+ # Round down to nearest multiple of cluster since odd excess is always padding.
892
+ total_blocks_max = total_blocks_max // params.cluster_shape_m * params.cluster_shape_m
893
+ return (total_blocks_max * params.num_head, params.num_splits, Int32(1))
894
+
895
+ @cute.jit
896
+ def _get_num_m_blocks(self, lane: Int32, bidb_start: Int32) -> Int32:
897
+ params = self.params
898
+ batch_idx = lane + bidb_start
899
+ if cutlass.const_expr(params.mSeqUsedQ is not None):
900
+ seqlen = Int32(0)
901
+ if batch_idx < params.num_batch:
902
+ seqlen = params.mSeqUsedQ[batch_idx]
903
+ else:
904
+ assert params.mCuSeqlensQ is not None
905
+ cur_cu_seqlen = Int32(0)
906
+ if batch_idx <= params.num_batch:
907
+ cur_cu_seqlen = params.mCuSeqlensQ[batch_idx]
908
+ next_cu_seqlen = cute.arch.shuffle_sync_down(cur_cu_seqlen, offset=1)
909
+ seqlen = next_cu_seqlen - cur_cu_seqlen
910
+ if cutlass.const_expr(params.qhead_per_kvhead_packgqa > 1):
911
+ seqlen *= params.qhead_per_kvhead_packgqa
912
+ return (
913
+ cute.ceil_div(cute.ceil_div(seqlen, params.tile_shape_mn[0]), params.cluster_shape_m)
914
+ if batch_idx < params.num_batch and lane < cute.arch.WARP_SIZE - 1
915
+ else Int32(0)
916
+ )
917
+
918
+ @cute.jit
919
+ def _varlen_coord_map(self) -> WorkTileInfo:
920
+ """Map self._tile_idx to (block, head, batch) via warp-level prefix sums."""
921
+ params = self.params
922
+ lane_idx = cute.arch.lane_idx()
923
+ num_m_blocks = self._get_num_m_blocks(lane_idx, bidb_start=0)
924
+ num_m_blocks_cumulative = utils.warp_prefix_sum(num_m_blocks, lane_idx)
925
+ # Total number of blocks for the next 31 batches
926
+ m_blocks_in_group = cute.arch.shuffle_sync(num_m_blocks_cumulative, cute.arch.WARP_SIZE - 1)
927
+ # Same for all lanes
928
+ group_end_tile = m_blocks_in_group * params.num_head
929
+ # 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)
930
+ block, head_idx, batch_idx = Int32(0), Int32(0), Int32(0)
931
+ next_tile_idx = self._tile_idx // params.cluster_shape_m
932
+ while group_end_tile <= next_tile_idx:
933
+ batch_idx += cute.arch.WARP_SIZE - 1
934
+ if batch_idx >= params.num_batch:
935
+ batch_idx = Int32(params.num_batch)
936
+ group_end_tile = next_tile_idx + 1
937
+ else:
938
+ num_m_blocks = self._get_num_m_blocks(lane_idx, bidb_start=batch_idx)
939
+ num_m_blocks_cumulative = utils.warp_prefix_sum(num_m_blocks, lane_idx)
940
+ m_blocks_in_group = cute.arch.shuffle_sync(
941
+ num_m_blocks_cumulative, cute.arch.WARP_SIZE - 1
942
+ )
943
+ group_end_tile += m_blocks_in_group * params.num_head
944
+ is_valid = False
945
+ if batch_idx >= params.num_batch:
946
+ block, head_idx, batch_idx = Int32(0), Int32(0), Int32(params.num_batch)
947
+ else:
948
+ group_start_tile = group_end_tile - m_blocks_in_group * params.num_head
949
+ # 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)
950
+ # The next problem to process is the first one that does not have ending tile position
951
+ # that is greater than or equal to tile index.
952
+ batch_idx_in_group = cute.arch.popc(
953
+ cute.arch.vote_ballot_sync(
954
+ group_start_tile + num_m_blocks_cumulative * params.num_head <= next_tile_idx
955
+ )
956
+ )
957
+ batch_idx += batch_idx_in_group
958
+ num_m_blocks_prev_lane = (
959
+ 0
960
+ if batch_idx_in_group == 0
961
+ else cute.arch.shuffle_sync(num_m_blocks_cumulative, batch_idx_in_group - 1)
962
+ )
963
+ num_m_blocks = cute.arch.shuffle_sync(num_m_blocks, batch_idx_in_group)
964
+ mh_block = next_tile_idx - group_start_tile - num_m_blocks_prev_lane * params.num_head
965
+ if cutlass.const_expr(params.lpt or params.head_swizzle):
966
+ # This is a version of the SingleTileLPTScheduler, complicated by the fact that
967
+ # the seqlen can vary per batch.
968
+ # TODO: is there any case where num_m_blocks is 0?
969
+ # TODO: by right we should read the seqlen_kv but we're assuming seqlen_q == seqlen_k here
970
+ num_n_blocks = (
971
+ num_m_blocks
972
+ * params.tile_shape_mn[0]
973
+ * params.cluster_shape_m
974
+ // params.qhead_per_kvhead_packgqa
975
+ // params.tile_shape_mn[1]
976
+ )
977
+ # nheads_in_l2 = min(max(self.max_kvblock_in_l2 // num_n_blocks, 1), self.num_head)
978
+ # Seems faster to have this be a power of 2
979
+ nheads_in_l2 = (
980
+ 16
981
+ if num_n_blocks * 16 <= params.max_kvblock_in_l2
982
+ else (
983
+ 8
984
+ if num_n_blocks * 8 <= params.max_kvblock_in_l2
985
+ else (
986
+ 4
987
+ if num_n_blocks * 4 <= params.max_kvblock_in_l2
988
+ else (2 if num_n_blocks * 2 <= params.max_kvblock_in_l2 else 1)
989
+ )
990
+ )
991
+ )
992
+ nheads_in_l2 = min(nheads_in_l2, params.num_head)
993
+ mh_in_l2 = nheads_in_l2 * num_m_blocks
994
+ section_idx = mh_block // mh_in_l2
995
+ l2_mod = mh_block - section_idx * mh_in_l2
996
+ # Deal with tail section
997
+ nheads_in_this_section = (
998
+ nheads_in_l2
999
+ if nheads_in_l2 * (section_idx + 1) <= params.num_head
1000
+ else params.num_head - section_idx * nheads_in_l2
1001
+ )
1002
+ block = l2_mod // nheads_in_this_section
1003
+ head_idx_residual = l2_mod - block * nheads_in_this_section
1004
+ head_idx = section_idx * nheads_in_l2 + head_idx_residual
1005
+ if cutlass.const_expr(params.lpt):
1006
+ block = num_m_blocks - 1 - block
1007
+ else:
1008
+ head_idx = mh_block // num_m_blocks
1009
+ block = mh_block - head_idx * num_m_blocks
1010
+ is_valid = self._is_first_block and batch_idx < params.num_batch
1011
+ if cutlass.const_expr(params.cluster_shape_m > 1):
1012
+ bidx_in_cluster = cute.arch.block_in_cluster_idx()
1013
+ block = block * params.cluster_shape_m + bidx_in_cluster[0]
1014
+ # 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)
1015
+ split_idx = self._split_idx if const_expr(params.is_split_kv) else Int32(0)
1016
+ return WorkTileInfo((Int32(block), Int32(head_idx), Int32(batch_idx), split_idx), is_valid)
1017
+
1018
+ @cute.jit
1019
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
1020
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1021
+ clc_work = self.clc.get_current_work()
1022
+ # Default to grid_dim (one past last valid flat index) so _varlen_coord_map
1023
+ # returns is_valid=False when CLC is exhausted. CLC tile_idx is garbage when
1024
+ # invalid, so we can't trust it. Local-then-assign avoids CuTe DSL structural
1025
+ # mismatch on self inside the runtime if.
1026
+ new_tile_idx = cute.arch.grid_dim()[0]
1027
+ new_split_idx = Int32(0)
1028
+ if clc_work.is_valid_tile:
1029
+ new_tile_idx = clc_work.tile_idx[0]
1030
+ if const_expr(self.params.is_split_kv):
1031
+ new_split_idx = clc_work.tile_idx[1]
1032
+ self._tile_idx = new_tile_idx
1033
+ self._split_idx = new_split_idx
1034
+ return self._varlen_coord_map()
1035
+
1036
+ @cute.jit
1037
+ def initial_work_tile_info(self, *, loc=None, ip=None):
1038
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1039
+ clc_work = self.clc.initial_work_tile_info()
1040
+ # See get_current_work for why grid_dim and local-then-assign.
1041
+ new_tile_idx = cute.arch.grid_dim()[0]
1042
+ new_split_idx = Int32(0)
1043
+ if clc_work.is_valid_tile:
1044
+ new_tile_idx = clc_work.tile_idx[0]
1045
+ if const_expr(self.params.is_split_kv):
1046
+ new_split_idx = clc_work.tile_idx[1]
1047
+ self._tile_idx = new_tile_idx
1048
+ self._split_idx = new_split_idx
1049
+ return self._varlen_coord_map()
1050
+
1051
+ def prefetch_next_work(self, *, loc=None, ip=None):
1052
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1053
+ self.clc.prefetch_next_work(loc=loc, ip=ip)
1054
+
1055
+ def advance_to_next_work(self, *, loc=None, ip=None):
1056
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1057
+ self.clc.consumer_wait(loc=loc, ip=ip)
1058
+ work = self.get_current_work()
1059
+ self.clc.consumer_release(loc=loc, ip=ip)
1060
+ return work
1061
+ self._is_first_block = False
1062
+ return self.get_current_work()
1063
+
1064
+ def producer_tail(self, *, loc=None, ip=None):
1065
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1066
+ self.clc.producer_tail(loc=loc, ip=ip)
1067
+
1068
+ def __extract_mlir_values__(self):
1069
+ values, self._values_pos = [], []
1070
+ objs = [self.params, self._tile_idx, self._split_idx]
1071
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1072
+ objs += [self.clc]
1073
+ for obj in objs:
1074
+ obj_values = cutlass.extract_mlir_values(obj)
1075
+ values += obj_values
1076
+ self._values_pos.append(len(obj_values))
1077
+ return values
1078
+
1079
+ def __new_from_mlir_values__(self, values):
1080
+ obj_list = []
1081
+ objs = [self.params, self._tile_idx, self._split_idx]
1082
+ if const_expr(self.params.scheduling_mode == SchedulingMode.CLC):
1083
+ objs += [self.clc]
1084
+ for obj, n_items in zip(objs, self._values_pos):
1085
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
1086
+ values = values[n_items:]
1087
+ return self.__class__(*obj_list, loc=self._loc)
torch-ext/sol_attn/_vendor/flash_attn/cute/utils.py ADDED
@@ -0,0 +1,800 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import math
4
+ import hashlib
5
+ import inspect
6
+ import os
7
+ from typing import Type, Callable, Optional, Tuple, overload
8
+
9
+ import cutlass
10
+ import cutlass.cute as cute
11
+
12
+ from cutlass import Float32, Int32, const_expr
13
+ from cutlass.cute import FastDivmodDivisor
14
+ from cutlass.cutlass_dsl import T, dsl_user_op
15
+ from cutlass._mlir.dialects import nvvm, llvm
16
+ from cutlass.cute.runtime import from_dlpack
17
+
18
+
19
+ from ....sm90._compat import activation
20
+
21
+ _MIXER_ATTRS = ("__vec_size__",)
22
+
23
+ # Obtained from sollya:
24
+ # fpminimax(exp(x * log(2.0)), 1, [|1,24...|],[0;1],relative);
25
+ POLY_EX2 = {
26
+ 0: (1.0),
27
+ 1: (
28
+ 1.0,
29
+ 0.922497093677520751953125,
30
+ ),
31
+ 2: (
32
+ 1.0,
33
+ 0.6657850742340087890625,
34
+ 0.330107033252716064453125,
35
+ ),
36
+ 3: (
37
+ 1.0,
38
+ 0.695146143436431884765625,
39
+ 0.227564394474029541015625,
40
+ 0.077119089663028717041015625,
41
+ ),
42
+ 4: (
43
+ 1.0,
44
+ 0.693042695522308349609375,
45
+ 0.2412912547588348388671875,
46
+ 5.2225358784198760986328125e-2,
47
+ 1.3434938155114650726318359375e-2,
48
+ ),
49
+ 5: (
50
+ 1.0,
51
+ 0.693151414394378662109375,
52
+ 0.24016360938549041748046875,
53
+ 5.5802188813686370849609375e-2,
54
+ 9.01452265679836273193359375e-3,
55
+ 1.86810153536498546600341796875e-3,
56
+ ),
57
+ }
58
+
59
+ _fa_clc_enabled: bool = os.environ.get("FA_CLC", "0") == "1"
60
+ _fa_disable_2cta_enabled: bool = os.environ.get("FA_DISABLE_2CTA", "0") == "1"
61
+
62
+
63
+ def _get_use_clc_scheduler_default() -> bool:
64
+ return _fa_clc_enabled
65
+
66
+
67
+ def _get_disable_2cta_default() -> bool:
68
+ return _fa_disable_2cta_enabled
69
+
70
+
71
+ def _compute_base_hash(func: Callable) -> str:
72
+ """Compute hash from source code or bytecode and closure values."""
73
+ try:
74
+ data = inspect.getsource(func).encode()
75
+ except (OSError, TypeError):
76
+ if hasattr(func, "__code__") and func.__code__ is not None:
77
+ data = func.__code__.co_code
78
+ else:
79
+ data = repr(func).encode()
80
+
81
+ hasher = hashlib.sha256(data)
82
+
83
+ if hasattr(func, "__closure__") and func.__closure__ is not None:
84
+ for cell in func.__closure__:
85
+ hasher.update(repr(cell.cell_contents).encode())
86
+
87
+ return hasher.hexdigest()
88
+
89
+
90
+ def hash_callable(
91
+ func: Callable, mixer_attrs: Tuple[str] = _MIXER_ATTRS, set_cute_hash: bool = True
92
+ ) -> str:
93
+ """Hash a callable based on the source code or bytecode and closure values.
94
+ Fast-path: if the callable (or its __wrapped__ base) has a ``__cute_hash__``
95
+ attribute, that value is returned immediately as the base hash, then
96
+ metadata dunders are mixed in to produce the final dict-key hash.
97
+ set_cute_hash: whether or not to set func.__cute_hash__
98
+ """
99
+ # Resolve base hash
100
+ if hasattr(func, "__cute_hash__"):
101
+ base_hash = func.__cute_hash__
102
+ else:
103
+ # Unwrap decorated functions (e.g., cute.jit wrappers).
104
+ base_func = getattr(func, "__wrapped__", func)
105
+
106
+ if hasattr(base_func, "__cute_hash__"):
107
+ base_hash = base_func.__cute_hash__
108
+ else:
109
+ base_hash = _compute_base_hash(base_func)
110
+
111
+ if set_cute_hash:
112
+ base_func.__cute_hash__ = base_hash
113
+
114
+ # Mix in mutable metadata dunders
115
+ mixer_values = tuple(getattr(func, attr, None) for attr in mixer_attrs)
116
+
117
+ if all(v is None for v in mixer_values):
118
+ return base_hash
119
+
120
+ hasher = hashlib.sha256(base_hash.encode())
121
+
122
+ for attr, val in zip(_MIXER_ATTRS, mixer_values):
123
+ hasher.update(f"{attr}={val!r}".encode())
124
+
125
+ return hasher.hexdigest()
126
+
127
+
128
+ def create_softcap_scoremod(softcap_val):
129
+ inv_softcap = 1.0 / softcap_val
130
+
131
+ @cute.jit
132
+ def scoremod_premask_fn(acc_S_SSA, batch_idx, head_idx, q_idx, kv_idx, aux_tensors):
133
+ scores = acc_S_SSA * inv_softcap
134
+ return scores * cute.math.tanh(scores, fastmath=True)
135
+
136
+ return scoremod_premask_fn
137
+
138
+
139
+ LOG2_E = math.log2(math.e)
140
+
141
+
142
+ def compute_softmax_scale_log2(softmax_scale, score_mod):
143
+ """Compute softmax_scale_log2 and adjusted softmax_scale based on whether score_mod is used.
144
+
145
+ When score_mod is None, fold the log2(e) factor into softmax_scale_log2 and set softmax_scale
146
+ to None. When score_mod is present, keep softmax_scale separate so it can be applied before
147
+ the score_mod, and set softmax_scale_log2 to just the change-of-base constant.
148
+
149
+ Returns (softmax_scale_log2, softmax_scale).
150
+ """
151
+ if const_expr(score_mod is None):
152
+ return softmax_scale * LOG2_E, None
153
+ else:
154
+ return LOG2_E, softmax_scale
155
+
156
+
157
+ def compute_fastdiv_mods(mQ, mK, qhead_per_kvhead, pack_gqa, aux_tensors, mPageTable=None):
158
+ """Compute FastDivmodDivisor pairs for aux_tensors index computation.
159
+
160
+ Returns a (seqlen_q_divmod, seqlen_k_divmod) tuple, or None if aux_tensors is None.
161
+ """
162
+ if const_expr(aux_tensors is None):
163
+ return None
164
+ seqlen_q = cute.size(mQ.shape[0]) // (qhead_per_kvhead if const_expr(pack_gqa) else 1)
165
+ seqlen_k = (
166
+ cute.size(mK.shape[0])
167
+ if const_expr(mPageTable is None)
168
+ else mK.shape[0] * mPageTable.shape[1]
169
+ )
170
+ return (FastDivmodDivisor(seqlen_q), FastDivmodDivisor(seqlen_k))
171
+
172
+
173
+ def convert_from_dlpack(x, leading_dim, alignment=16, divisibility=1) -> cute.Tensor:
174
+ return (
175
+ from_dlpack(x, assumed_align=alignment)
176
+ .mark_layout_dynamic(leading_dim=leading_dim)
177
+ .mark_compact_shape_dynamic(
178
+ mode=leading_dim, stride_order=x.dim_order(), divisibility=divisibility
179
+ )
180
+ )
181
+
182
+
183
+ def convert_from_dlpack_leading_static(
184
+ x, leading_dim, alignment=16, static_modes=None, stride_order=None
185
+ ) -> cute.Tensor:
186
+ if stride_order is None:
187
+ stride_order = x.dim_order()
188
+ x_ = from_dlpack(x, assumed_align=alignment)
189
+ for i in range(x.ndim):
190
+ if i != leading_dim and (static_modes is None or i not in static_modes):
191
+ x_ = x_.mark_compact_shape_dynamic(mode=i, stride_order=stride_order)
192
+ return x_
193
+
194
+
195
+ def make_tiled_copy_A(
196
+ copy_atom: cute.CopyAtom, tiled_mma: cute.TiledMma, swapAB: cutlass.Constexpr[bool] = False
197
+ ) -> cute.TiledCopy:
198
+ if const_expr(swapAB):
199
+ return cute.make_tiled_copy_B(copy_atom, tiled_mma)
200
+ else:
201
+ return cute.make_tiled_copy_A(copy_atom, tiled_mma)
202
+
203
+
204
+ def make_tiled_copy_B(
205
+ copy_atom: cute.CopyAtom, tiled_mma: cute.TiledMma, swapAB: cutlass.Constexpr[bool] = False
206
+ ) -> cute.TiledCopy:
207
+ if const_expr(swapAB):
208
+ return cute.make_tiled_copy_A(copy_atom, tiled_mma)
209
+ else:
210
+ return cute.make_tiled_copy_B(copy_atom, tiled_mma)
211
+
212
+
213
+ def mma_make_fragment_A(
214
+ smem: cute.Tensor, thr_mma: cute.ThrMma, swapAB: cutlass.Constexpr[bool] = False
215
+ ) -> cute.Tensor:
216
+ if const_expr(swapAB):
217
+ return mma_make_fragment_B(smem, thr_mma)
218
+ else:
219
+ return thr_mma.make_fragment_A(thr_mma.partition_A(smem))
220
+
221
+
222
+ def mma_make_fragment_B(
223
+ smem: cute.Tensor, thr_mma: cute.ThrMma, swapAB: cutlass.Constexpr[bool] = False
224
+ ) -> cute.Tensor:
225
+ if const_expr(swapAB):
226
+ return mma_make_fragment_A(smem, thr_mma)
227
+ else:
228
+ return thr_mma.make_fragment_B(thr_mma.partition_B(smem))
229
+
230
+
231
+ def get_smem_store_atom(
232
+ arch: cutlass.Constexpr[int], element_type: Type[cute.Numeric], transpose: bool = False
233
+ ) -> cute.CopyAtom:
234
+ if const_expr(arch < 90 or element_type.width != 16):
235
+ return cute.make_copy_atom(
236
+ cute.nvgpu.CopyUniversalOp(),
237
+ element_type,
238
+ num_bits_per_copy=2 * element_type.width,
239
+ )
240
+ else:
241
+ return cute.make_copy_atom(
242
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=transpose, num_matrices=4),
243
+ element_type,
244
+ )
245
+
246
+
247
+ @cute.jit
248
+ def warp_reduce(
249
+ val: cute.TensorSSA | cute.Numeric,
250
+ op: Callable,
251
+ width: cutlass.Constexpr[int] = cute.arch.WARP_SIZE,
252
+ ) -> cute.TensorSSA | cute.Numeric:
253
+ if const_expr(isinstance(val, cute.TensorSSA)):
254
+ res = cute.make_rmem_tensor(val.shape, val.dtype)
255
+ res.store(val)
256
+ for i in cutlass.range_constexpr(cute.size(val.shape)):
257
+ res[i] = warp_reduce(res[i], op, width)
258
+ return res.load()
259
+ else:
260
+ for i in cutlass.range_constexpr(int(math.log2(width))):
261
+ val = op(val, cute.arch.shuffle_sync_bfly(val, offset=1 << i))
262
+ return val
263
+
264
+
265
+ @dsl_user_op
266
+ def smid(*, loc=None, ip=None) -> Int32:
267
+ return Int32(
268
+ llvm.inline_asm(
269
+ T.i32(),
270
+ [],
271
+ "mov.u32 $0, %smid;",
272
+ "=r",
273
+ has_side_effects=False,
274
+ is_align_stack=False,
275
+ asm_dialect=llvm.AsmDialect.AD_ATT,
276
+ )
277
+ )
278
+
279
+
280
+ @dsl_user_op
281
+ def fmax(
282
+ a: float | Float32, b: float | Float32, c: float | Float32 | None = None, *, loc=None, ip=None
283
+ ) -> Float32:
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 Float32(
290
+ nvvm.fmax(
291
+ T.f32(),
292
+ Float32(a).ir_value(loc=loc, ip=ip),
293
+ Float32(b).ir_value(loc=loc, ip=ip),
294
+ c=Float32(c).ir_value(loc=loc, ip=ip) if c is not None else None,
295
+ loc=loc,
296
+ ip=ip,
297
+ )
298
+ )
299
+ else:
300
+ # New API: infers result type automatically
301
+ return Float32(
302
+ nvvm.fmax(
303
+ Float32(a).ir_value(loc=loc, ip=ip),
304
+ Float32(b).ir_value(loc=loc, ip=ip),
305
+ c=Float32(c).ir_value(loc=loc, ip=ip) if c is not None else None,
306
+ loc=loc,
307
+ ip=ip,
308
+ )
309
+ )
310
+
311
+
312
+ @cute.jit
313
+ def fmax_reduce(
314
+ x: cute.TensorSSA, init_val: float | Float32 | None = None, arch: cutlass.Constexpr[int] = 80
315
+ ) -> Float32:
316
+ if const_expr(arch < 100 or cute.size(x.shape) % 8 != 0):
317
+ # if const_expr(init_val is None):
318
+ # init_val = -cutlass.Float32.if
319
+ # return x.reduce(cute.ReductionOp.MAX, init_val, 0)
320
+ res = cute.make_rmem_tensor(x.shape, Float32)
321
+ res.store(x)
322
+ # local_max = [res[0], res[1]]
323
+ # for i in cutlass.range_constexpr(2, cute.size(x.shape), 2):
324
+ # local_max[0] = fmax(local_max[0], res[i + 0])
325
+ # local_max[1] = fmax(local_max[1], res[i + 1])
326
+ # local_max[0] = fmax(local_max[0], local_max[1])
327
+ # return local_max[0] if const_expr(init_val is None) else fmax(local_max[0], init_val)
328
+ local_max = [res[0], res[1], res[2], res[3]]
329
+ for i in cutlass.range_constexpr(4, cute.size(x.shape), 4):
330
+ local_max[0] = fmax(local_max[0], res[i + 0])
331
+ local_max[1] = fmax(local_max[1], res[i + 1])
332
+ local_max[2] = fmax(local_max[2], res[i + 2])
333
+ local_max[3] = fmax(local_max[3], res[i + 3])
334
+ local_max[0] = fmax(local_max[0], local_max[1])
335
+ local_max[2] = fmax(local_max[2], local_max[3])
336
+ local_max[0] = fmax(local_max[0], local_max[2])
337
+ return local_max[0] if const_expr(init_val is None) else fmax(local_max[0], init_val)
338
+ else:
339
+ # [2025-06-15] x.reduce only seems to use 50% 3-input max and 50% 2-input max
340
+ # We instead force the 3-input max.
341
+ res = cute.make_rmem_tensor(x.shape, Float32)
342
+ res.store(x)
343
+ local_max_0 = (
344
+ fmax(init_val, res[0], res[1])
345
+ if const_expr(init_val is not None)
346
+ else fmax(res[0], res[1])
347
+ )
348
+ local_max = [
349
+ local_max_0,
350
+ fmax(res[2], res[3]),
351
+ fmax(res[4], res[5]),
352
+ fmax(res[6], res[7]),
353
+ ]
354
+ for i in cutlass.range_constexpr(8, cute.size(x.shape), 8):
355
+ local_max[0] = fmax(local_max[0], res[i], res[i + 1])
356
+ local_max[1] = fmax(local_max[1], res[i + 2], res[i + 3])
357
+ local_max[2] = fmax(local_max[2], res[i + 4], res[i + 5])
358
+ local_max[3] = fmax(local_max[3], res[i + 6], res[i + 7])
359
+ local_max[0] = fmax(local_max[0], local_max[1])
360
+ return fmax(local_max[0], local_max[2], local_max[3])
361
+
362
+
363
+ @cute.jit
364
+ def fadd_reduce(
365
+ x: cute.TensorSSA, init_val: float | Float32 | None = None, arch: cutlass.Constexpr[int] = 80
366
+ ) -> Float32:
367
+ if const_expr(arch < 100 or cute.size(x.shape) % 8 != 0):
368
+ if const_expr(init_val is None):
369
+ init_val = Float32.zero
370
+ return x.reduce(cute.ReductionOp.ADD, init_val, 0)
371
+ # res = cute.make_rmem_tensor(x.shape, Float32)
372
+ # res.store(x)
373
+ # local_sum = [res[0], res[1], res[2], res[3]]
374
+ # for i in cutlass.range_constexpr(4, cute.size(x.shape), 4):
375
+ # local_sum[0] += res[i + 0]
376
+ # local_sum[1] += res[i + 1]
377
+ # local_sum[2] += res[i + 2]
378
+ # local_sum[3] += res[i + 3]
379
+ # local_sum[0] += local_sum[1]
380
+ # local_sum[2] += local_sum[3]
381
+ # local_sum[0] += local_sum[2]
382
+ # return local_sum[0] if const_expr(init_val is None) else local_sum[0] + init_val
383
+ else:
384
+ res = cute.make_rmem_tensor(x.shape, Float32)
385
+ res.store(x)
386
+ local_sum_0 = (
387
+ cute.arch.add_packed_f32x2((init_val, 0.0), (res[0], res[1]))
388
+ # cute.arch.add_packed_f32x2((init_val / 2, init_val / 2), (res[0], res[1]))
389
+ if const_expr(init_val is not None)
390
+ else (res[0], res[1])
391
+ )
392
+ local_sum = [local_sum_0, (res[2], res[3]), (res[4], res[5]), (res[6], res[7])]
393
+ for i in cutlass.range_constexpr(8, cute.size(x.shape), 8):
394
+ local_sum[0] = cute.arch.add_packed_f32x2(local_sum[0], (res[i + 0], res[i + 1]))
395
+ local_sum[1] = cute.arch.add_packed_f32x2(local_sum[1], (res[i + 2], res[i + 3]))
396
+ local_sum[2] = cute.arch.add_packed_f32x2(local_sum[2], (res[i + 4], res[i + 5]))
397
+ local_sum[3] = cute.arch.add_packed_f32x2(local_sum[3], (res[i + 6], res[i + 7]))
398
+ local_sum[0] = cute.arch.add_packed_f32x2(local_sum[0], local_sum[1])
399
+ local_sum[2] = cute.arch.add_packed_f32x2(local_sum[2], local_sum[3])
400
+ local_sum[0] = cute.arch.add_packed_f32x2(local_sum[0], local_sum[2])
401
+ return local_sum[0][0] + local_sum[0][1]
402
+
403
+
404
+ @dsl_user_op
405
+ def atomic_add_fp32(a: float | Float32, gmem_ptr: cute.Pointer, *, loc=None, ip=None) -> None:
406
+ # gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
407
+ # # cache_hint = cutlass.Int64(0x12F0000000000000)
408
+ # llvm.inline_asm(
409
+ # None,
410
+ # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip)],
411
+ # # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip), cache_hint.ir_value()],
412
+ # "red.global.add.f32 [$0], $1;",
413
+ # # "red.global.add.L2::cache_hint.f32 [$0], $1, 0x12F0000000000000;",
414
+ # # "red.global.add.L2::cache_hint.f32 [$0], $1, $2;",
415
+ # "l,f",
416
+ # # "l,f,l",
417
+ # has_side_effects=True,
418
+ # is_align_stack=False,
419
+ # asm_dialect=llvm.AsmDialect.AD_ATT,
420
+ # )
421
+ nvvm.atomicrmw(
422
+ res=T.f32(), op=nvvm.AtomicOpKind.FADD, ptr=gmem_ptr.llvm_ptr, a=Float32(a).ir_value()
423
+ )
424
+
425
+
426
+ @dsl_user_op
427
+ def elem_pointer(x: cute.Tensor, coord: cute.Coord, *, loc=None, ip=None) -> cute.Pointer:
428
+ return x.iterator + cute.crd2idx(coord, x.layout, loc=loc, ip=ip)
429
+
430
+
431
+ @cute.jit
432
+ def predicate_k(tAcA: cute.Tensor, limit: cutlass.Int32) -> cute.Tensor:
433
+ # Only compute predicates for the "k" dimension. For the mn dimension, we will use "if"
434
+ tApA = cute.make_rmem_tensor(
435
+ cute.make_layout(
436
+ (cute.size(tAcA, mode=[0, 1]), cute.size(tAcA, mode=[1]), cute.size(tAcA, mode=[2])),
437
+ stride=(cute.size(tAcA, mode=[2]), 0, 1),
438
+ ),
439
+ cutlass.Boolean,
440
+ )
441
+ for rest_v in cutlass.range_constexpr(tApA.shape[0]):
442
+ for rest_k in cutlass.range_constexpr(tApA.shape[2]):
443
+ tApA[rest_v, 0, rest_k] = cute.elem_less(tAcA[(0, rest_v), 0, rest_k][1], limit)
444
+ return tApA
445
+
446
+
447
+ def canonical_warp_group_idx(sync: bool = True) -> cutlass.Int32:
448
+ warp_group_idx = cute.arch.thread_idx()[0] // 128
449
+ if const_expr(sync):
450
+ warp_group_idx = cute.arch.make_warp_uniform(warp_group_idx)
451
+ return warp_group_idx
452
+
453
+
454
+ # @dsl_user_op
455
+ # def warp_vote_any_lt(a: float | Float32, b: float | Float32, *, loc=None, ip=None) -> cutlass.Boolean:
456
+ # mask = cutlass.Int32(-1)
457
+ # return cutlass.Boolean(
458
+ # llvm.inline_asm(
459
+ # T.i32(),
460
+ # [Float32(a).ir_value(loc=loc, ip=ip), Float32(b).ir_value(loc=loc, ip=ip), mask.ir_value(loc=loc, ip=ip)],
461
+ # ".pred p1, p2;\n"
462
+ # "setp.lt.f32 p1, $1, $2;\n"
463
+ # "vote.sync.any.pred p2, p1, $3;\n"
464
+ # "selp.u32 $0, 1, 0, p2;",
465
+ # # "selp.u32 $0, 1, 0, p1;",
466
+ # "=r,f,f,r",
467
+ # has_side_effects=False,
468
+ # is_align_stack=False,
469
+ # asm_dialect=llvm.AsmDialect.AD_ATT,
470
+ # )
471
+ # )
472
+
473
+
474
+ @cute.jit
475
+ def shuffle_sync(
476
+ value: cute.Numeric,
477
+ offset: cute.typing.Int,
478
+ width: cutlass.Constexpr[int] = cute.arch.WARP_SIZE,
479
+ ) -> cute.Numeric:
480
+ assert value.width % 32 == 0, "value type must be a multiple of 32 bits"
481
+ # 1 -> 0b11111, 2 -> 0b11110, 4 -> 0b11100, 8 -> 0b11000, 16 -> 0b10000, 32 -> 0b00000
482
+ mask = cute.arch.WARP_SIZE - width
483
+ clamp = cute.arch.WARP_SIZE - 1
484
+ mask_and_clamp = mask << 8 | clamp
485
+ # important: need stride 1 and not 0 for recast_tensor to work
486
+ val = cute.make_rmem_tensor(cute.make_layout((1,), stride=(1,)), type(value))
487
+ val[0] = value
488
+ val_i32 = cute.recast_tensor(val, cutlass.Int32)
489
+ for i in cutlass.range_constexpr(cute.size(val_i32)):
490
+ val_i32[i] = cute.arch.shuffle_sync(val_i32[i], offset, mask_and_clamp=mask_and_clamp)
491
+ return val[0]
492
+
493
+
494
+ @dsl_user_op
495
+ def shl_u32(val: cutlass.Uint32, shift: cutlass.Uint32, *, loc=None, ip=None) -> cutlass.Uint32:
496
+ """
497
+ Left-shift val by shift bits using PTX shl.b32 (sign-agnostic).
498
+
499
+ Named ``shl_u32`` (not ``shl_b32``) because python type annotations
500
+ distinguish signed/unsigned.
501
+
502
+ PTX semantics (§9.7.8.8): "Shift amounts greater than the register width N
503
+ are clamped to N." So ``shl.b32 d, a, 32`` is well-defined and yields 0.
504
+
505
+ This differs from C/C++ and LLVM IR, where shifting by >= the type width is
506
+ undefined behavior. CuTeDSL compiles through MLIR -> LLVM IR, so a plain
507
+ Python-level ``Uint32(x) << Uint32(n)`` inherits LLVM's UB: the optimizer
508
+ may treat the result as poison and eliminate dependent code. Inline PTX
509
+ bypasses the LLVM IR shift entirely — the instruction is emitted verbatim
510
+ into PTX where clamping makes it safe for all shift amounts.
511
+ """
512
+ return cutlass.Uint32(
513
+ llvm.inline_asm(
514
+ T.i32(),
515
+ [
516
+ cutlass.Uint32(val).ir_value(loc=loc, ip=ip),
517
+ cutlass.Uint32(shift).ir_value(loc=loc, ip=ip),
518
+ ],
519
+ "shl.b32 $0, $1, $2;",
520
+ "=r,r,r",
521
+ has_side_effects=False,
522
+ is_align_stack=False,
523
+ asm_dialect=llvm.AsmDialect.AD_ATT,
524
+ )
525
+ )
526
+
527
+
528
+ @dsl_user_op
529
+ def shr_u32(val: cutlass.Uint32, shift: cutlass.Uint32, *, loc=None, ip=None) -> cutlass.Uint32:
530
+ """
531
+ Unsigned right-shift val by shift bits using PTX shr.u32 (zero-fills).
532
+
533
+ See ``shl_u32`` docstring for why inline PTX is used instead of plain
534
+ CuTeDSL shift operators (LLVM shift-by-type-width UB).
535
+ """
536
+ return cutlass.Uint32(
537
+ llvm.inline_asm(
538
+ T.i32(),
539
+ [
540
+ cutlass.Uint32(val).ir_value(loc=loc, ip=ip),
541
+ cutlass.Uint32(shift).ir_value(loc=loc, ip=ip),
542
+ ],
543
+ "shr.u32 $0, $1, $2;",
544
+ "=r,r,r",
545
+ has_side_effects=False,
546
+ is_align_stack=False,
547
+ asm_dialect=llvm.AsmDialect.AD_ATT,
548
+ )
549
+ )
550
+
551
+
552
+ @cute.jit
553
+ def warp_prefix_sum(val: cutlass.Int32, lane: Optional[cutlass.Int32] = None) -> cutlass.Int32:
554
+ if const_expr(lane is None):
555
+ lane = cute.arch.lane_idx()
556
+ # 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)
557
+ for i in cutlass.range_constexpr(int(math.log2(cute.arch.WARP_SIZE))):
558
+ offset = 1 << i
559
+ # Very important that we set mask_and_clamp to 0
560
+ partial_sum = cute.arch.shuffle_sync_up(val, offset=offset, mask_and_clamp=0)
561
+ if lane >= offset:
562
+ val += partial_sum
563
+ # 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)
564
+ return val
565
+
566
+
567
+ @dsl_user_op
568
+ def cvt_f16x2_f32(
569
+ a: float | Float32, b: float | Float32, to_dtype: Type, *, loc=None, ip=None
570
+ ) -> cutlass.Int32:
571
+ assert to_dtype in [cutlass.BFloat16, cutlass.Float16], "to_dtype must be BFloat16 or Float16"
572
+ return cutlass.Int32(
573
+ llvm.inline_asm(
574
+ T.i32(),
575
+ [Float32(a).ir_value(loc=loc, ip=ip), Float32(b).ir_value(loc=loc, ip=ip)],
576
+ f"cvt.rn.{'bf16x2' if to_dtype is cutlass.BFloat16 else 'f16x2'}.f32 $0, $2, $1;",
577
+ "=r,f,f",
578
+ has_side_effects=False,
579
+ is_align_stack=False,
580
+ asm_dialect=llvm.AsmDialect.AD_ATT,
581
+ )
582
+ )
583
+
584
+
585
+ @overload
586
+ def cvt_f16(src: cute.Tensor, dst: cute.Tensor) -> None: ...
587
+
588
+
589
+ @overload
590
+ def cvt_f16(src: cute.Tensor, dtype: Type[cute.Numeric]) -> cute.Tensor: ...
591
+
592
+
593
+ @cute.jit
594
+ def cvt_f16(src: cute.Tensor, dst_or_dtype):
595
+ """Convert Float32 tensor to Float16/BFloat16.
596
+
597
+ Args:
598
+ src: Source tensor with Float32 element type
599
+ dst_or_dtype: Either a destination tensor or a dtype (Float16/BFloat16)
600
+
601
+ Returns:
602
+ None if dst is a tensor, or a new tensor if dtype is provided
603
+ """
604
+ if const_expr(isinstance(dst_or_dtype, type)):
605
+ # dtype variant: create new tensor and call the tensor variant
606
+ dtype = dst_or_dtype
607
+ dst = cute.make_rmem_tensor(src.shape, dtype)
608
+ cvt_f16(src, dst)
609
+ return dst
610
+ else:
611
+ # tensor variant: write to dst
612
+ dst = dst_or_dtype
613
+ assert cute.size(dst.shape) == cute.size(src.shape), "dst and src must have the same size"
614
+ assert cute.size(src.shape) % 2 == 0, "src must have an even number of elements"
615
+ assert dst.element_type in [cutlass.BFloat16, cutlass.Float16], (
616
+ "dst must be BFloat16 or Float16"
617
+ )
618
+ assert src.element_type is Float32, "src must be Float32"
619
+ dst_i32 = cute.recast_tensor(dst, cutlass.Int32)
620
+ assert cute.size(dst_i32.shape) * 2 == cute.size(src.shape)
621
+ for i in cutlass.range_constexpr(cute.size(dst_i32)):
622
+ dst_i32[i] = cvt_f16x2_f32(src[2 * i], src[2 * i + 1], dst.element_type)
623
+
624
+
625
+ @dsl_user_op
626
+ @cute.jit
627
+ def evaluate_polynomial(x: Float32, poly: Tuple[Float32, ...], *, loc=None, ip=None) -> Float32:
628
+ deg = len(poly) - 1
629
+ out = poly[deg]
630
+ for i in cutlass.range_constexpr(deg - 1, -1, -1):
631
+ out = out * x + poly[i]
632
+ return out
633
+
634
+
635
+ @dsl_user_op
636
+ @cute.jit
637
+ def evaluate_polynomial_2(
638
+ x: Float32, y: Float32, poly: Tuple[Float32, ...], *, loc=None, ip=None
639
+ ) -> Tuple[Float32, Float32]:
640
+ deg = len(poly) - 1
641
+ out = (poly[deg], poly[deg])
642
+ for i in cutlass.range_constexpr(deg - 1, -1, -1):
643
+ out = cute.arch.fma_packed_f32x2(out, (x, y), (poly[i], poly[i]))
644
+ return out
645
+
646
+
647
+ @dsl_user_op
648
+ def add_round_down(x: float | Float32, y: float | Float32, *, loc=None, ip=None) -> Float32:
649
+ # There's probably a way to call llvm or nvvm to do this instead of ptx
650
+ return cutlass.Float32(
651
+ llvm.inline_asm(
652
+ T.f32(),
653
+ [Float32(x).ir_value(loc=loc, ip=ip), Float32(y).ir_value(loc=loc, ip=ip)],
654
+ "add.rm.ftz.f32 $0, $1, $2;",
655
+ "=f,f,f",
656
+ has_side_effects=False,
657
+ is_align_stack=False,
658
+ asm_dialect=llvm.AsmDialect.AD_ATT,
659
+ )
660
+ )
661
+
662
+
663
+ @dsl_user_op
664
+ def combine_int_frac_ex2(x_rounded: Float32, frac_ex2: Float32, *, loc=None, ip=None) -> Float32:
665
+ return cutlass.Float32(
666
+ llvm.inline_asm(
667
+ T.f32(),
668
+ [
669
+ Float32(x_rounded).ir_value(loc=loc, ip=ip),
670
+ Float32(frac_ex2).ir_value(loc=loc, ip=ip),
671
+ ],
672
+ "{\n\t"
673
+ ".reg .s32 x_rounded_i, frac_ex_i, x_rounded_e, out_i;\n\t"
674
+ "mov.b32 x_rounded_i, $1;\n\t"
675
+ "mov.b32 frac_ex_i, $2;\n\t"
676
+ "shl.b32 x_rounded_e, x_rounded_i, 23;\n\t"
677
+ # add.u32 generates IMAD instruction and add.s32 generates LEA instruction
678
+ # IMAD uses the FMA pipeline and LEA uses the ALU pipeline, afaik
679
+ "add.s32 out_i, x_rounded_e, frac_ex_i;\n\t"
680
+ "mov.b32 $0, out_i;\n\t"
681
+ "}\n",
682
+ "=f,f,f",
683
+ has_side_effects=False,
684
+ is_align_stack=False,
685
+ asm_dialect=llvm.AsmDialect.AD_ATT,
686
+ )
687
+ )
688
+
689
+
690
+ @dsl_user_op
691
+ def ex2_emulation(x: Float32, *, poly_degree: int = 3, loc=None, ip=None) -> Float32:
692
+ assert poly_degree in POLY_EX2, f"Polynomial degree {poly_degree} not supported"
693
+ # We assume x <= 127.0
694
+ fp32_round_int = float(2**23 + 2**22)
695
+ x_clamped = cute.arch.fmax(x, -127.0)
696
+ # We want to round down here, so that the fractional part is in [0, 1)
697
+ x_rounded = add_round_down(x_clamped, fp32_round_int, loc=loc, ip=ip)
698
+ # The integer floor of x is now in the last 8 bits of x_rounded
699
+ # We assume the next 2 ops round to nearest even. The rounding mode is important.
700
+ x_rounded_back = x_rounded - fp32_round_int
701
+ x_frac = x_clamped - x_rounded_back
702
+ x_frac_ex2 = evaluate_polynomial(x_frac, POLY_EX2[poly_degree], loc=loc, ip=ip)
703
+ return combine_int_frac_ex2(x_rounded, x_frac_ex2, loc=loc, ip=ip)
704
+
705
+
706
+ # TODO: check that the ex2_emulation_2 produces the same SASS as the ptx version
707
+ @dsl_user_op
708
+ def ex2_emulation_2(
709
+ x: Float32, y: Float32, *, poly_degree: int = 3, loc=None, ip=None
710
+ ) -> Tuple[Float32, Float32]:
711
+ # We assume x <= 127.0 and y <= 127.0
712
+ fp32_round_int = float(2**23 + 2**22)
713
+ xy_clamped = (cute.arch.fmax(x, -127.0), cute.arch.fmax(y, -127.0))
714
+ # We want to round down here, so that the fractional part is in [0, 1)
715
+ xy_rounded = cute.arch.add_packed_f32x2(xy_clamped, (fp32_round_int, fp32_round_int), rnd="rm")
716
+ # The integer floor of x & y are now in the last 8 bits of xy_rounded
717
+ # We want the next 2 ops to round to nearest even. The rounding mode is important.
718
+ xy_rounded_back = activation.sub_packed_f32x2(
719
+ xy_rounded, (fp32_round_int, fp32_round_int)
720
+ )
721
+ xy_frac = activation.sub_packed_f32x2(xy_clamped, xy_rounded_back)
722
+ xy_frac_ex2 = evaluate_polynomial_2(*xy_frac, POLY_EX2[poly_degree], loc=loc, ip=ip)
723
+ x_out = combine_int_frac_ex2(xy_rounded[0], xy_frac_ex2[0], loc=loc, ip=ip)
724
+ y_out = combine_int_frac_ex2(xy_rounded[1], xy_frac_ex2[1], loc=loc, ip=ip)
725
+ return x_out, y_out
726
+
727
+
728
+ @dsl_user_op
729
+ def e2e_asm2(x: Float32, y: Float32, *, loc=None, ip=None) -> Tuple[Float32, Float32]:
730
+ out_f32x2 = llvm.inline_asm(
731
+ llvm.StructType.get_literal([T.f32(), T.f32()]),
732
+ [Float32(x).ir_value(loc=loc, ip=ip), Float32(y, loc=loc, ip=ip).ir_value()],
733
+ "{\n\t"
734
+ ".reg .f32 f1, f2, f3, f4, f5, f6, f7;\n\t"
735
+ ".reg .b64 l1, l2, l3, l4, l5, l6, l7, l8, l9, l10;\n\t"
736
+ ".reg .s32 r1, r2, r3, r4, r5, r6, r7, r8;\n\t"
737
+ "max.ftz.f32 f1, $2, 0fC2FE0000;\n\t"
738
+ "max.ftz.f32 f2, $3, 0fC2FE0000;\n\t"
739
+ "mov.b64 l1, {f1, f2};\n\t"
740
+ "mov.f32 f3, 0f4B400000;\n\t"
741
+ "mov.b64 l2, {f3, f3};\n\t"
742
+ "add.rm.ftz.f32x2 l7, l1, l2;\n\t"
743
+ "sub.rn.ftz.f32x2 l8, l7, l2;\n\t"
744
+ "sub.rn.ftz.f32x2 l9, l1, l8;\n\t"
745
+ "mov.f32 f7, 0f3D9DF09D;\n\t"
746
+ "mov.b64 l6, {f7, f7};\n\t"
747
+ "mov.f32 f6, 0f3E6906A4;\n\t"
748
+ "mov.b64 l5, {f6, f6};\n\t"
749
+ "mov.f32 f5, 0f3F31F519;\n\t"
750
+ "mov.b64 l4, {f5, f5};\n\t"
751
+ "mov.f32 f4, 0f3F800000;\n\t"
752
+ "mov.b64 l3, {f4, f4};\n\t"
753
+ "fma.rn.ftz.f32x2 l10, l9, l6, l5;\n\t"
754
+ "fma.rn.ftz.f32x2 l10, l10, l9, l4;\n\t"
755
+ "fma.rn.ftz.f32x2 l10, l10, l9, l3;\n\t"
756
+ "mov.b64 {r1, r2}, l7;\n\t"
757
+ "mov.b64 {r3, r4}, l10;\n\t"
758
+ "shl.b32 r5, r1, 23;\n\t"
759
+ "add.s32 r7, r5, r3;\n\t"
760
+ "shl.b32 r6, r2, 23;\n\t"
761
+ "add.s32 r8, r6, r4;\n\t"
762
+ "mov.b32 $0, r7;\n\t"
763
+ "mov.b32 $1, r8;\n\t"
764
+ "}\n",
765
+ "=r,=r,f,f",
766
+ has_side_effects=False,
767
+ is_align_stack=False,
768
+ asm_dialect=llvm.AsmDialect.AD_ATT,
769
+ )
770
+ out0 = Float32(llvm.extractvalue(T.f32(), out_f32x2, [0], loc=loc, ip=ip))
771
+ out1 = Float32(llvm.extractvalue(T.f32(), out_f32x2, [1], loc=loc, ip=ip))
772
+ return out0, out1
773
+
774
+
775
+ @dsl_user_op
776
+ def domain_offset_aligned(
777
+ coord: cute.Coord, tensor: cute.Tensor, *, loc=None, ip=None
778
+ ) -> cute.Tensor:
779
+ assert isinstance(tensor.iterator, cute.Pointer)
780
+ # We assume that applying the offset does not change the pointer alignment
781
+ new_ptr = cute.make_ptr(
782
+ tensor.element_type,
783
+ elem_pointer(tensor, coord).toint(),
784
+ tensor.memspace,
785
+ assumed_align=tensor.iterator.alignment,
786
+ )
787
+ return cute.make_tensor(new_ptr, tensor.layout)
788
+
789
+
790
+ @cute.jit
791
+ def scalar_to_ssa(a: cute.Numeric, dtype) -> cute.TensorSSA:
792
+ """Convert a scalar to a cute TensorSSA of shape (1,) and given dtype"""
793
+ vec = cute.make_rmem_tensor(1, dtype)
794
+ vec[0] = a
795
+ return vec.load()
796
+
797
+
798
+ def ssa_to_scalar(val):
799
+ """Could inline but nice for reflecting the above api"""
800
+ return val[0]
torch-ext/sol_attn/common/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Internal helpers shared by the architecture backends."""
2
+
3
+ from .runtime import to_cute_tensor
4
+
5
+ __all__ = ["to_cute_tensor"]
torch-ext/sol_attn/common/layout_utils.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tensor-layout helpers shared by the two CuTe kernels."""
2
+
3
+ import cutlass.cute as cute
4
+ from cutlass import const_expr
5
+
6
+
7
+ def transpose_view(tensor: cute.Tensor) -> cute.Tensor:
8
+ shape = (tensor.shape[1], tensor.shape[0], *tensor.shape[2:])
9
+ order = (1, 0, *range(2, cute.rank(tensor)))
10
+ return cute.composition(
11
+ tensor,
12
+ cute.make_ordered_layout(shape, order=order),
13
+ )
14
+
15
+
16
+ def select(tensor: cute.Tensor, modes: list[int]) -> cute.Tensor:
17
+ return cute.make_tensor(
18
+ tensor.iterator,
19
+ cute.select(tensor.layout, modes),
20
+ )
21
+
22
+
23
+ def _accumulator_mn_layout(
24
+ layout: cute.Layout,
25
+ transpose: bool = False,
26
+ ) -> cute.Layout:
27
+ column_major = cute.make_layout(layout.shape)
28
+ shape = (
29
+ (column_major.shape[0][1], column_major.shape[1]),
30
+ (
31
+ column_major.shape[0][0],
32
+ *column_major.shape[0][2:],
33
+ column_major.shape[2],
34
+ ),
35
+ *column_major.shape[3:],
36
+ )
37
+ stride = (
38
+ (column_major.stride[0][1], column_major.stride[1]),
39
+ (
40
+ column_major.stride[0][0],
41
+ *column_major.stride[0][2:],
42
+ column_major.stride[2],
43
+ ),
44
+ *column_major.stride[3:],
45
+ )
46
+ if const_expr(transpose):
47
+ shape = (shape[1], shape[0], *shape[2:])
48
+ stride = (stride[1], stride[0], *stride[2:])
49
+ return cute.composition(
50
+ layout,
51
+ cute.make_layout(shape, stride=stride),
52
+ )
53
+
54
+
55
+ def reshape_acc_to_mn(
56
+ accumulator: cute.Tensor,
57
+ transpose: bool = False,
58
+ ) -> cute.Tensor:
59
+ return cute.make_tensor(
60
+ accumulator.iterator,
61
+ _accumulator_mn_layout(accumulator.layout, transpose),
62
+ )
63
+
64
+
65
+ @cute.jit
66
+ def _accumulator_frga_layout(layout: cute.Layout) -> cute.Layout:
67
+ if const_expr(cute.rank(layout.shape[0]) == 3):
68
+ divisor = 2 if const_expr(layout.shape[0][2] % 2 == 0) else 1
69
+ divided = cute.logical_divide(
70
+ layout,
71
+ ((None, None, divisor), None, None),
72
+ )
73
+ return cute.make_layout(
74
+ (
75
+ (
76
+ divided.shape[0][0],
77
+ divided.shape[0][1],
78
+ divided.shape[0][2][0],
79
+ ),
80
+ divided.shape[1],
81
+ (divided.shape[0][2][1], divided.shape[2]),
82
+ ),
83
+ stride=(
84
+ (
85
+ divided.stride[0][0],
86
+ divided.stride[0][1],
87
+ divided.stride[0][2][0],
88
+ ),
89
+ divided.stride[1],
90
+ (divided.stride[0][2][1], divided.stride[2]),
91
+ ),
92
+ )
93
+
94
+ assert layout.shape[2] % 2 == 0
95
+ divided = cute.logical_divide(layout, (None, None, 2))
96
+ return cute.make_layout(
97
+ (
98
+ (
99
+ divided.shape[0][0],
100
+ divided.shape[0][1],
101
+ divided.shape[2][0],
102
+ ),
103
+ divided.shape[1],
104
+ divided.shape[2][1],
105
+ ),
106
+ stride=(
107
+ (
108
+ divided.stride[0][0],
109
+ divided.stride[0][1],
110
+ divided.stride[2][0],
111
+ ),
112
+ divided.stride[1],
113
+ divided.stride[2][1],
114
+ ),
115
+ )
116
+
117
+
118
+ def reshape_acc_to_frgA(accumulator: cute.Tensor) -> cute.Tensor:
119
+ return cute.make_tensor(
120
+ accumulator.iterator,
121
+ _accumulator_frga_layout(accumulator.layout),
122
+ )
123
+
124
+
125
+ __all__ = [
126
+ "reshape_acc_to_frgA",
127
+ "reshape_acc_to_mn",
128
+ "select",
129
+ "transpose_view",
130
+ ]
torch-ext/sol_attn/common/runtime.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small host helpers shared by the architecture backends."""
2
+
3
+ from cutlass.cute.runtime import from_dlpack
4
+
5
+
6
+ def to_cute_tensor(tensor):
7
+ return from_dlpack(
8
+ tensor,
9
+ assumed_align=16,
10
+ enable_tvm_ffi=True,
11
+ ).mark_layout_dynamic(leading_dim=tensor.ndim - 1)
12
+
13
+
14
+ __all__ = ["to_cute_tensor"]
torch-ext/sol_attn/common/selector.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CTA-local routing-mask helpers shared by the CuTe architecture backends."""
2
+
3
+ import cutlass
4
+ import cutlass.cute as cute
5
+ from cutlass import Float32, Int32, const_expr
6
+ from cutlass._mlir.dialects import llvm
7
+ from cutlass.cutlass_dsl import T, dsl_user_op
8
+
9
+
10
+ @dsl_user_op
11
+ def sol_attn_bfind_b32(
12
+ value: Int32,
13
+ *,
14
+ loc=None,
15
+ ip=None,
16
+ ) -> Int32:
17
+ return Int32(
18
+ llvm.inline_asm(
19
+ T.i32(),
20
+ [Int32(value).ir_value(loc=loc, ip=ip)],
21
+ "bfind.u32 $0, $1;",
22
+ "=r,r",
23
+ has_side_effects=False,
24
+ is_align_stack=False,
25
+ )
26
+ )
27
+
28
+
29
+ @dsl_user_op
30
+ def sol_attn_popc_b32(
31
+ value: Int32,
32
+ *,
33
+ loc=None,
34
+ ip=None,
35
+ ) -> Int32:
36
+ return Int32(
37
+ llvm.inline_asm(
38
+ T.i32(),
39
+ [Int32(value).ir_value(loc=loc, ip=ip)],
40
+ "popc.b32 $0, $1;",
41
+ "=r,r",
42
+ has_side_effects=False,
43
+ is_align_stack=False,
44
+ )
45
+ )
46
+
47
+
48
+ @cute.jit
49
+ def _mask_word(
50
+ mask0: Int32,
51
+ mask1: Int32,
52
+ mask2: Int32,
53
+ mask3: Int32,
54
+ word: Int32,
55
+ ) -> Int32:
56
+ result = mask0
57
+ if word == Int32(1):
58
+ result = mask1
59
+ if word == Int32(2):
60
+ result = mask2
61
+ if word == Int32(3):
62
+ result = mask3
63
+ return result
64
+
65
+
66
+ @cute.jit
67
+ def _test_exact_bit(
68
+ mask0: Int32,
69
+ mask1: Int32,
70
+ mask2: Int32,
71
+ mask3: Int32,
72
+ offset: Int32,
73
+ ) -> cutlass.Boolean:
74
+ word = offset // Int32(32)
75
+ bit = offset - word * Int32(32)
76
+ return (
77
+ _mask_word(mask0, mask1, mask2, mask3, word)
78
+ & (Int32(1) << bit)
79
+ ) != Int32(0)
80
+
81
+
82
+ @cute.jit
83
+ def sol_attn_test_exact_bit_limited_words(
84
+ mask0: Int32,
85
+ mask1: Int32,
86
+ mask2: Int32,
87
+ mask3: Int32,
88
+ offset: Int32,
89
+ group_words: cutlass.Constexpr[int],
90
+ ) -> cutlass.Boolean:
91
+ bit = offset & Int32(31)
92
+ if const_expr(group_words == 1):
93
+ return (mask0 & (Int32(1) << bit)) != Int32(0)
94
+ if const_expr(group_words == 2):
95
+ word = mask0
96
+ if offset >= Int32(32):
97
+ word = mask1
98
+ return (word & (Int32(1) << bit)) != Int32(0)
99
+ if const_expr(group_words == 3):
100
+ index = offset // Int32(32)
101
+ word = mask0
102
+ if index == Int32(1):
103
+ word = mask1
104
+ if index == Int32(2):
105
+ word = mask2
106
+ return (word & (Int32(1) << bit)) != Int32(0)
107
+ return _test_exact_bit(mask0, mask1, mask2, mask3, offset)
108
+
109
+
110
+ @cute.jit
111
+ def sol_attn_set_exact_bit(
112
+ mask0: Int32,
113
+ mask1: Int32,
114
+ mask2: Int32,
115
+ mask3: Int32,
116
+ offset: Int32,
117
+ ):
118
+ word = offset // Int32(32)
119
+ bit_value = Int32(1) << (offset - word * Int32(32))
120
+ if word == Int32(0):
121
+ mask0 = mask0 | bit_value
122
+ if word == Int32(1):
123
+ mask1 = mask1 | bit_value
124
+ if word == Int32(2):
125
+ mask2 = mask2 | bit_value
126
+ if word == Int32(3):
127
+ mask3 = mask3 | bit_value
128
+ return mask0, mask1, mask2, mask3
129
+
130
+
131
+ @cute.jit
132
+ def sol_attn_route_is_exact(
133
+ q_block: Int32,
134
+ kv_block: Int32,
135
+ column_mean: Float32,
136
+ threshold: Float32,
137
+ valid: cutlass.Boolean,
138
+ ) -> cutlass.Boolean:
139
+ distance = q_block - kv_block
140
+ if distance < Int32(0):
141
+ distance = Int32(0) - distance
142
+ return ((column_mean > threshold) or distance <= Int32(1)) and valid
143
+
144
+
145
+ @cute.jit
146
+ def sol_attn_mask_word_constexpr(
147
+ mask0: Int32,
148
+ mask1: Int32,
149
+ mask2: Int32,
150
+ mask3: Int32,
151
+ word: cutlass.Constexpr[int],
152
+ ) -> Int32:
153
+ if const_expr(word == 0):
154
+ return mask0
155
+ if const_expr(word == 1):
156
+ return mask1
157
+ if const_expr(word == 2):
158
+ return mask2
159
+ return mask3
160
+
161
+
162
+ __all__ = [
163
+ "sol_attn_bfind_b32",
164
+ "sol_attn_mask_word_constexpr",
165
+ "sol_attn_popc_b32",
166
+ "sol_attn_route_is_exact",
167
+ "sol_attn_set_exact_bit",
168
+ "sol_attn_test_exact_bit_limited_words",
169
+ ]
torch-ext/sol_attn/interface.py ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Public Sol-Attn interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+
7
+ import torch
8
+
9
+ BLOCK_SIZE = 64
10
+ _CUTE_BACKENDS = {
11
+ (9, 0): "cute_sm90",
12
+ (10, 0): "cute_sm100",
13
+ (12, 0): "cute_sm120",
14
+ }
15
+ _compiled = {}
16
+
17
+
18
+ def _validate_inputs(
19
+ q,
20
+ k,
21
+ v,
22
+ thresh_type,
23
+ sink_tokens=0,
24
+ sink_start=None,
25
+ ):
26
+ if q.ndim != 4 or q.shape != k.shape or q.shape != v.shape:
27
+ raise ValueError("q, k, and v must share shape [B, T, H, 128]")
28
+ if q.shape[1] == 0 or q.shape[3] != 128:
29
+ raise ValueError("Sol-Attn requires T > 0 and head dimension 128")
30
+ if any(x.dtype != torch.bfloat16 for x in (q, k, v)):
31
+ raise TypeError("q, k, and v must use torch.bfloat16")
32
+ if q.device.type != "cuda" or k.device != q.device or v.device != q.device:
33
+ raise ValueError("q, k, and v must be on the same CUDA device")
34
+ if not (q.is_contiguous() and k.is_contiguous() and v.is_contiguous()):
35
+ raise ValueError("q, k, and v must be contiguous BTHD tensors")
36
+ if thresh_type not in ("diag", "exact"):
37
+ raise ValueError("thresh_type must be 'diag' or 'exact'")
38
+ if not isinstance(sink_tokens, int):
39
+ raise TypeError("sink_tokens must be an integer")
40
+ if not 0 <= sink_tokens <= q.shape[1]:
41
+ raise ValueError("sink_tokens must be in [0, T]")
42
+ if sink_start is not None:
43
+ if not isinstance(sink_start, int):
44
+ raise TypeError("sink_start must be an integer or None")
45
+ if not 0 <= sink_start <= q.shape[1]:
46
+ raise ValueError("sink_start must be in [0, T]")
47
+ if sink_start + sink_tokens > q.shape[1]:
48
+ raise ValueError("sink_start + sink_tokens must be <= T")
49
+
50
+ return tuple(torch.cuda.get_device_capability(q.device))
51
+
52
+
53
+ @functools.lru_cache(maxsize=1)
54
+ def _cute_runtime_available() -> bool:
55
+ """Whether the optional CuTe DSL runtime can be imported."""
56
+
57
+ try:
58
+ import cuda.bindings.driver # noqa: F401
59
+ import cutlass.cute # noqa: F401
60
+ except ImportError:
61
+ return False
62
+ return True
63
+
64
+
65
+ def _backend_for_arch(
66
+ arch: tuple[int, int],
67
+ *,
68
+ cute_available: bool | None = None,
69
+ ) -> str:
70
+ """Select CuTe when specialized and available, otherwise Triton."""
71
+
72
+ if arch[0] < 8:
73
+ raise RuntimeError(
74
+ "Sol-Attn requires an NVIDIA GPU with compute capability >= 8.0; "
75
+ f"got SM{arch[0]}{arch[1]}"
76
+ )
77
+ cute_backend = _CUTE_BACKENDS.get(arch)
78
+ if cute_backend is not None:
79
+ available = (
80
+ _cute_runtime_available()
81
+ if cute_available is None
82
+ else cute_available
83
+ )
84
+ if available:
85
+ return cute_backend
86
+ return "triton"
87
+
88
+
89
+ def _validate_cute(arch, tokens, kv_splits):
90
+ if arch != (9, 0) and kv_splits != 1:
91
+ raise ValueError("kv_splits=2/4 is currently available on SM90 only")
92
+ route_groups = ((tokens + 63) // 64 + 63) // 64
93
+ if kv_splits > route_groups:
94
+ raise ValueError("each KV split must contain at least one N64 route group")
95
+
96
+
97
+ def _stream(device):
98
+ import cuda.bindings.driver as cuda
99
+
100
+ return cuda.CUstream(torch.cuda.current_stream(device).cuda_stream)
101
+
102
+
103
+ def _to_cute_tensors(tensors):
104
+ from .common import to_cute_tensor
105
+
106
+ return [to_cute_tensor(x) for x in tensors]
107
+
108
+
109
+ def _sink_block_range(tokens, sink_start, sink_tokens):
110
+ blocks = (tokens + BLOCK_SIZE - 1) // BLOCK_SIZE
111
+ if not sink_tokens:
112
+ return blocks, blocks
113
+ start = tokens - sink_tokens if sink_start is None else sink_start
114
+ return (
115
+ start // BLOCK_SIZE,
116
+ (start + sink_tokens + BLOCK_SIZE - 1) // BLOCK_SIZE,
117
+ )
118
+
119
+
120
+ def _compile_sm90(
121
+ key,
122
+ tensors,
123
+ scale,
124
+ tokens,
125
+ kv_splits,
126
+ sink_range,
127
+ stream,
128
+ ):
129
+ import cutlass.cute as cute
130
+
131
+ from .sm90 import make_kernel
132
+
133
+ operator = make_kernel(tokens, kv_splits)
134
+ args = _to_cute_tensors(tensors)
135
+ compiled = cute.compile(
136
+ operator,
137
+ *args,
138
+ scale,
139
+ sink_range,
140
+ stream=stream,
141
+ options="--enable-tvm-ffi",
142
+ )
143
+ _compiled[key] = compiled
144
+ return compiled, args
145
+
146
+
147
+ def _compile_sm100(
148
+ key,
149
+ tensors,
150
+ scale,
151
+ sink_start_block,
152
+ sink_end_block,
153
+ stream,
154
+ ):
155
+ import cutlass.cute as cute
156
+
157
+ from .sm100 import forward
158
+
159
+ args = _to_cute_tensors(tensors)
160
+ compiled = cute.compile(
161
+ forward,
162
+ *args,
163
+ scale,
164
+ sink_start_block,
165
+ sink_end_block,
166
+ stream=stream,
167
+ options="--enable-tvm-ffi",
168
+ )
169
+ _compiled[key] = compiled
170
+ return compiled, args
171
+
172
+
173
+ def _compile_sm120(
174
+ key,
175
+ tensors,
176
+ scale,
177
+ sink_start_block,
178
+ sink_end_block,
179
+ stream,
180
+ ):
181
+ import cutlass.cute as cute
182
+
183
+ from .sm120 import make_kernel
184
+
185
+ operator = make_kernel()
186
+ args = _to_cute_tensors(tensors)
187
+ compiled = cute.compile(
188
+ operator,
189
+ *args,
190
+ scale,
191
+ sink_start_block,
192
+ sink_end_block,
193
+ stream=stream,
194
+ options="--enable-tvm-ffi",
195
+ )
196
+ _compiled[key] = compiled
197
+ return compiled, args
198
+
199
+
200
+ def _sol_attn_cute(
201
+ q,
202
+ k,
203
+ v,
204
+ *,
205
+ arch,
206
+ scale,
207
+ tau,
208
+ thresh_type,
209
+ kv_splits,
210
+ sink_tokens,
211
+ sink_start,
212
+ ):
213
+ from .preprocess import prepare
214
+
215
+ batch, tokens, heads, _ = q.shape
216
+
217
+ with torch.cuda.device(q.device):
218
+ kc, vc, threshold = prepare(
219
+ q,
220
+ k,
221
+ v,
222
+ scale=scale,
223
+ tau=tau,
224
+ thresh_type=thresh_type,
225
+ )
226
+ output = torch.empty_like(v)
227
+ lse = torch.empty(
228
+ (batch, tokens, heads),
229
+ device=q.device,
230
+ dtype=torch.float32,
231
+ )
232
+ stream = _stream(q.device)
233
+ key = (q.device.index, arch, batch, tokens, heads, kv_splits)
234
+
235
+ if arch == (9, 0):
236
+ if sink_tokens:
237
+ sink_start_block, sink_end_block = _sink_block_range(
238
+ tokens,
239
+ sink_start,
240
+ sink_tokens,
241
+ )
242
+ sink_range = sink_start_block | (sink_end_block << 16)
243
+ else:
244
+ sink_range = 0
245
+ tensors = [q, k, v, output, kc, vc, threshold, lse]
246
+ if kv_splits > 1:
247
+ tensors.extend(
248
+ [
249
+ torch.empty(
250
+ (batch, tokens, kv_splits * heads, 128),
251
+ device=q.device,
252
+ dtype=torch.bfloat16,
253
+ ),
254
+ torch.empty(
255
+ (batch, tokens, kv_splits * heads),
256
+ device=q.device,
257
+ dtype=torch.float32,
258
+ ),
259
+ ]
260
+ )
261
+ compiled = _compiled.get(key)
262
+ if compiled is None:
263
+ compiled, args = _compile_sm90(
264
+ key,
265
+ tensors,
266
+ scale,
267
+ tokens,
268
+ kv_splits,
269
+ sink_range,
270
+ stream,
271
+ )
272
+ else:
273
+ args = _to_cute_tensors(tensors)
274
+ compiled(
275
+ *args,
276
+ scale,
277
+ sink_range,
278
+ stream=stream,
279
+ )
280
+ elif arch == (10, 0):
281
+ sink_start_block, sink_end_block = _sink_block_range(
282
+ tokens,
283
+ sink_start,
284
+ sink_tokens,
285
+ )
286
+ tensors = [q, k, v, output, kc, vc, threshold, lse]
287
+ compiled = _compiled.get(key)
288
+ if compiled is None:
289
+ compiled, args = _compile_sm100(
290
+ key,
291
+ tensors,
292
+ scale,
293
+ sink_start_block,
294
+ sink_end_block,
295
+ stream,
296
+ )
297
+ else:
298
+ args = _to_cute_tensors(tensors)
299
+ compiled(
300
+ *args,
301
+ scale,
302
+ sink_start_block,
303
+ sink_end_block,
304
+ stream=stream,
305
+ )
306
+ else:
307
+ sink_start_block, sink_end_block = _sink_block_range(
308
+ tokens,
309
+ sink_start,
310
+ sink_tokens,
311
+ )
312
+ tensors = [q, k, v, output, kc, vc, threshold, lse]
313
+ compiled = _compiled.get(key)
314
+ if compiled is None:
315
+ compiled, args = _compile_sm120(
316
+ key,
317
+ tensors,
318
+ scale,
319
+ sink_start_block,
320
+ sink_end_block,
321
+ stream,
322
+ )
323
+ else:
324
+ args = _to_cute_tensors(tensors)
325
+ compiled(
326
+ *args,
327
+ scale,
328
+ sink_start_block,
329
+ sink_end_block,
330
+ stream=stream,
331
+ )
332
+ return output
333
+
334
+
335
+ def sol_attn(
336
+ q: torch.Tensor,
337
+ k: torch.Tensor,
338
+ v: torch.Tensor,
339
+ *,
340
+ scale: float | None = None,
341
+ tau: float = 1.0,
342
+ thresh_type: str = "diag",
343
+ kv_splits: int = 1,
344
+ sink_tokens: int = 0,
345
+ sink_start: int | None = None,
346
+ ) -> torch.Tensor:
347
+ """Compute noncausal Sol-Attn for contiguous BF16 BTHD tensors.
348
+
349
+ ``sink_start`` and ``sink_tokens`` keep every KV block overlapping the
350
+ corresponding contiguous token range exact for all queries. Omitting
351
+ ``sink_start`` places the range at the token suffix.
352
+ """
353
+
354
+ arch = _validate_inputs(
355
+ q,
356
+ k,
357
+ v,
358
+ thresh_type,
359
+ sink_tokens,
360
+ sink_start,
361
+ )
362
+ if kv_splits not in (1, 2, 4):
363
+ raise ValueError("kv_splits must be 1, 2, or 4")
364
+ backend = _backend_for_arch(arch)
365
+ scale = q.shape[-1] ** -0.5 if scale is None else float(scale)
366
+ tau = float(tau)
367
+
368
+ if backend == "triton":
369
+ if kv_splits != 1:
370
+ raise ValueError("kv_splits=2/4 is currently available on SM90 only")
371
+ from .triton_ref import sol_attn as triton_sol_attn
372
+
373
+ return triton_sol_attn(
374
+ q,
375
+ k,
376
+ v,
377
+ scale=scale,
378
+ tau=tau,
379
+ thresh_type=thresh_type,
380
+ sink_tokens=sink_tokens,
381
+ sink_start=sink_start,
382
+ )
383
+
384
+ _validate_cute(arch, q.shape[1], kv_splits)
385
+ return _sol_attn_cute(
386
+ q,
387
+ k,
388
+ v,
389
+ arch=arch,
390
+ scale=scale,
391
+ tau=tau,
392
+ thresh_type=thresh_type,
393
+ kv_splits=kv_splits,
394
+ sink_tokens=sink_tokens,
395
+ sink_start=sink_start,
396
+ )
397
+
398
+
399
+ __all__ = ["sol_attn"]
torch-ext/sol_attn/preprocess.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Block summaries and routing thresholds shared by both CuTe kernels."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ import triton
7
+ import triton.language as tl
8
+ from triton.tools.tensor_descriptor import TensorDescriptor
9
+
10
+
11
+ BLOCK_SIZE = 64
12
+ HEAD_DIM = 128
13
+ THRESHOLD_GROUP_SIZE = 64
14
+
15
+
16
+ @triton.autotune(
17
+ configs=[
18
+ triton.Config({}, num_warps=warps, num_stages=stages)
19
+ for warps in (4, 8)
20
+ for stages in (1, 2, 3, 4)
21
+ ],
22
+ key=["T"],
23
+ )
24
+ @triton.jit
25
+ def _reduce_kc_kernel(
26
+ k_desc,
27
+ kc,
28
+ T,
29
+ H: tl.constexpr,
30
+ N: tl.constexpr,
31
+ D: tl.constexpr,
32
+ BLOCK: tl.constexpr,
33
+ TILE_D: tl.constexpr,
34
+ ):
35
+ d_tile, block, batch_head = (
36
+ tl.program_id(0),
37
+ tl.program_id(1),
38
+ tl.program_id(2),
39
+ )
40
+ batch, head = batch_head // H, batch_head % H
41
+ block_len = tl.minimum(BLOCK, T - block * BLOCK)
42
+ values = k_desc.load(
43
+ [batch, block * BLOCK, head, d_tile * TILE_D]
44
+ ).reshape([BLOCK, TILE_D])
45
+ summary = tl.sum(values, axis=0) / block_len
46
+ offsets = d_tile * TILE_D + tl.arange(0, TILE_D)
47
+ tl.store(
48
+ kc + ((batch * N + block) * H + head) * D + offsets,
49
+ summary,
50
+ mask=offsets < D,
51
+ )
52
+
53
+
54
+ @triton.autotune(
55
+ configs=[
56
+ triton.Config({}, num_warps=warps, num_stages=stages)
57
+ for warps in (4, 8)
58
+ for stages in (1, 2, 3, 4)
59
+ ],
60
+ key=["T"],
61
+ )
62
+ @triton.jit
63
+ def _reduce_vc_kernel(
64
+ v_desc,
65
+ vc,
66
+ T,
67
+ H: tl.constexpr,
68
+ N: tl.constexpr,
69
+ D: tl.constexpr,
70
+ BLOCK: tl.constexpr,
71
+ TILE_D: tl.constexpr,
72
+ ):
73
+ d_tile, block, batch_head = (
74
+ tl.program_id(0),
75
+ tl.program_id(1),
76
+ tl.program_id(2),
77
+ )
78
+ batch, head = batch_head // H, batch_head % H
79
+ values = v_desc.load(
80
+ [batch, block * BLOCK, head, d_tile * TILE_D]
81
+ ).reshape([BLOCK, TILE_D])
82
+ summary = tl.sum(values, axis=0)
83
+ offsets = d_tile * TILE_D + tl.arange(0, TILE_D)
84
+ tl.store(
85
+ vc + ((batch * N + block) * H + head) * D + offsets,
86
+ summary,
87
+ mask=offsets < D,
88
+ )
89
+
90
+
91
+ @triton.autotune(
92
+ configs=[triton.Config({}, num_warps=4, num_stages=2)],
93
+ key=["N"],
94
+ )
95
+ @triton.jit
96
+ def _reduce_kc_stats_kernel(
97
+ kc_desc,
98
+ kc_mean,
99
+ kc_var_diag,
100
+ H: tl.constexpr,
101
+ N: tl.constexpr,
102
+ D: tl.constexpr,
103
+ TILE_D: tl.constexpr,
104
+ GROUP: tl.constexpr,
105
+ ):
106
+ d_tile, batch_head = tl.program_id(0), tl.program_id(1)
107
+ batch, head = batch_head // H, batch_head % H
108
+ block_offsets = tl.arange(0, GROUP)
109
+ block_offsets = tl.max_contiguous(block_offsets, GROUP)
110
+ d_offsets = d_tile * TILE_D + tl.arange(0, TILE_D)
111
+ total = tl.zeros((TILE_D,), dtype=tl.float32)
112
+ total_sq = tl.zeros((TILE_D,), dtype=tl.float32)
113
+ count = tl.full((), 0.0, dtype=tl.float32)
114
+ for start in range(0, N, GROUP):
115
+ valid = start + block_offsets < N
116
+ values = kc_desc.load(
117
+ [batch, start, head, d_tile * TILE_D]
118
+ ).reshape([GROUP, TILE_D]).to(tl.float32)
119
+ values = tl.where(valid[:, None], values, 0.0)
120
+ total += tl.sum(values, axis=0)
121
+ total_sq += tl.sum(values * values, axis=0)
122
+ count += tl.sum(valid.to(tl.float32), axis=0)
123
+ mean = total / count
124
+ variance = tl.maximum(total_sq / count - mean * mean, 0.0)
125
+ valid_d = d_offsets < D
126
+ tl.store(
127
+ kc_mean + batch_head * D + d_offsets,
128
+ mean,
129
+ mask=valid_d,
130
+ )
131
+ tl.store(
132
+ kc_var_diag + batch_head * D + d_offsets,
133
+ variance,
134
+ mask=valid_d,
135
+ )
136
+
137
+
138
+ @triton.autotune(
139
+ configs=[triton.Config({}, num_warps=4, num_stages=2)],
140
+ key=["T"],
141
+ )
142
+ @triton.jit
143
+ def _diag_threshold_kernel(
144
+ q_desc,
145
+ kc_mean,
146
+ kc_var_diag,
147
+ global_threshold,
148
+ softmax_scale,
149
+ T,
150
+ H: tl.constexpr,
151
+ N: tl.constexpr,
152
+ D: tl.constexpr,
153
+ BLOCK: tl.constexpr,
154
+ TILE_D: tl.constexpr,
155
+ TAU: tl.constexpr,
156
+ ):
157
+ q_block, batch_head = tl.program_id(0), tl.program_id(1)
158
+ batch, head = batch_head // H, batch_head % H
159
+ q_start = q_block * BLOCK
160
+ q_len = tl.minimum(BLOCK, T - q_start).to(tl.float32)
161
+ d_offsets = tl.arange(0, TILE_D)
162
+ valid_d = d_offsets < D
163
+ q_values = q_desc.load(
164
+ [batch, q_start, head, 0]
165
+ ).reshape([BLOCK, TILE_D])
166
+ q_centroid = tl.sum(q_values.to(tl.float32), axis=0) / q_len
167
+ mean_kc = tl.load(
168
+ kc_mean + batch_head * D + d_offsets,
169
+ mask=valid_d,
170
+ other=0.0,
171
+ )
172
+ var_kc = tl.load(
173
+ kc_var_diag + batch_head * D + d_offsets,
174
+ mask=valid_d,
175
+ other=0.0,
176
+ )
177
+ log2_scale = softmax_scale * 1.4426950408889634
178
+ mean = tl.sum(q_centroid * mean_kc, axis=0) * log2_scale
179
+ variance = tl.sum(
180
+ q_centroid * q_centroid * var_kc, axis=0
181
+ ) * (log2_scale * log2_scale)
182
+ std = tl.sqrt(tl.maximum(variance, 0.0) + 1.0e-6)
183
+ tl.store(
184
+ global_threshold + (batch * N + q_block) * H + head,
185
+ mean + TAU * std,
186
+ )
187
+
188
+
189
+ @triton.jit
190
+ def _pool_query_kernel(
191
+ q_desc,
192
+ q_bar,
193
+ T,
194
+ H: tl.constexpr,
195
+ N: tl.constexpr,
196
+ D: tl.constexpr,
197
+ BLOCK: tl.constexpr,
198
+ TILE_D: tl.constexpr,
199
+ ):
200
+ q_block, batch_head = tl.program_id(0), tl.program_id(1)
201
+ batch, head = batch_head // H, batch_head % H
202
+ q_start = q_block * BLOCK
203
+ q_len = tl.minimum(BLOCK, T - q_start).to(tl.float32)
204
+ offsets = tl.arange(0, TILE_D)
205
+ values = q_desc.load([batch, q_start, head, 0]).reshape(
206
+ [BLOCK, TILE_D]
207
+ )
208
+ centroid = tl.sum(values.to(tl.float32), axis=0) / q_len
209
+ tl.store(
210
+ q_bar + (batch_head * N + q_block) * D + offsets,
211
+ centroid,
212
+ mask=offsets < D,
213
+ )
214
+
215
+
216
+ @triton.jit
217
+ def _exact_fused_threshold_kernel(
218
+ q_bar,
219
+ kc_mean,
220
+ kc_second_moment,
221
+ global_threshold,
222
+ softmax_scale,
223
+ H: tl.constexpr,
224
+ N: tl.constexpr,
225
+ D: tl.constexpr,
226
+ BLOCK_M: tl.constexpr,
227
+ TILE_D: tl.constexpr,
228
+ TAU: tl.constexpr,
229
+ ):
230
+ row_tile, batch_head = tl.program_id(0), tl.program_id(1)
231
+ rows = row_tile * BLOCK_M + tl.arange(0, BLOCK_M)
232
+ offsets = tl.arange(0, TILE_D)
233
+ valid_rows = rows < N
234
+ valid_d = offsets < D
235
+
236
+ q_centroid = tl.load(
237
+ q_bar + (batch_head * N + rows[:, None]) * D + offsets[None, :],
238
+ mask=valid_rows[:, None] & valid_d[None, :],
239
+ other=0.0,
240
+ )
241
+ mean_kc = tl.load(
242
+ kc_mean + batch_head * D + offsets,
243
+ mask=valid_d,
244
+ other=0.0,
245
+ )
246
+ second_moment = tl.load(
247
+ kc_second_moment
248
+ + batch_head * D * D
249
+ + offsets[:, None] * D
250
+ + offsets[None, :],
251
+ mask=valid_d[:, None] & valid_d[None, :],
252
+ other=0.0,
253
+ )
254
+
255
+ raw_mean = tl.sum(q_centroid.to(tl.float32) * mean_kc[None, :], axis=1)
256
+ projected = tl.dot(
257
+ q_centroid,
258
+ second_moment,
259
+ out_dtype=tl.float32,
260
+ )
261
+ raw_second_moment = tl.sum(
262
+ projected * q_centroid.to(tl.float32),
263
+ axis=1,
264
+ )
265
+ log2_scale = softmax_scale * 1.4426950408889634
266
+ mean = raw_mean * log2_scale
267
+ variance = tl.maximum(
268
+ raw_second_moment - raw_mean * raw_mean,
269
+ 0.0,
270
+ ) * (log2_scale * log2_scale)
271
+ threshold = mean + TAU * tl.sqrt(variance + 1.0e-6)
272
+ batch, head = batch_head // H, batch_head % H
273
+ tl.store(
274
+ global_threshold + (batch * N + rows) * H + head,
275
+ threshold,
276
+ mask=valid_rows,
277
+ )
278
+
279
+
280
+ def _reduce_kv(
281
+ k: torch.Tensor,
282
+ v: torch.Tensor,
283
+ ) -> tuple[torch.Tensor, torch.Tensor]:
284
+ batch, tokens, heads, head_dim = k.shape
285
+ blocks = triton.cdiv(tokens, BLOCK_SIZE)
286
+ tile_d = min(128, triton.next_power_of_2(head_dim))
287
+ kc = torch.empty(
288
+ (batch, blocks, heads, head_dim),
289
+ device=k.device,
290
+ dtype=torch.bfloat16,
291
+ )
292
+ vc = torch.empty_like(kc)
293
+ k_desc = TensorDescriptor.from_tensor(
294
+ k,
295
+ [1, BLOCK_SIZE, 1, tile_d],
296
+ )
297
+ v_desc = TensorDescriptor.from_tensor(
298
+ v,
299
+ [1, BLOCK_SIZE, 1, tile_d],
300
+ )
301
+ grid = (triton.cdiv(head_dim, tile_d), blocks, batch * heads)
302
+ _reduce_kc_kernel[grid](
303
+ k_desc,
304
+ kc,
305
+ tokens,
306
+ heads,
307
+ blocks,
308
+ head_dim,
309
+ BLOCK_SIZE,
310
+ tile_d,
311
+ )
312
+ _reduce_vc_kernel[grid](
313
+ v_desc,
314
+ vc,
315
+ tokens,
316
+ heads,
317
+ blocks,
318
+ head_dim,
319
+ BLOCK_SIZE,
320
+ tile_d,
321
+ )
322
+ return kc, vc
323
+
324
+
325
+ def _compute_diag_threshold(
326
+ q: torch.Tensor,
327
+ kc: torch.Tensor,
328
+ *,
329
+ tau: float,
330
+ scale: float,
331
+ ) -> torch.Tensor:
332
+ batch, tokens, heads, head_dim = q.shape
333
+ blocks = triton.cdiv(tokens, BLOCK_SIZE)
334
+ tile_d = min(128, triton.next_power_of_2(head_dim))
335
+ kc_mean = torch.empty(
336
+ (batch, heads, head_dim),
337
+ device=q.device,
338
+ dtype=torch.float32,
339
+ )
340
+ kc_var_diag = torch.empty_like(kc_mean)
341
+ global_threshold = torch.empty(
342
+ (batch, blocks, heads),
343
+ device=q.device,
344
+ dtype=torch.float32,
345
+ )
346
+ q_desc = TensorDescriptor.from_tensor(
347
+ q,
348
+ [1, BLOCK_SIZE, 1, tile_d],
349
+ )
350
+ kc_desc = TensorDescriptor.from_tensor(
351
+ kc,
352
+ [1, THRESHOLD_GROUP_SIZE, 1, tile_d],
353
+ )
354
+ _reduce_kc_stats_kernel[
355
+ (triton.cdiv(head_dim, tile_d), batch * heads)
356
+ ](
357
+ kc_desc,
358
+ kc_mean,
359
+ kc_var_diag,
360
+ heads,
361
+ blocks,
362
+ head_dim,
363
+ tile_d,
364
+ THRESHOLD_GROUP_SIZE,
365
+ )
366
+ _diag_threshold_kernel[(blocks, batch * heads)](
367
+ q_desc,
368
+ kc_mean,
369
+ kc_var_diag,
370
+ global_threshold,
371
+ scale,
372
+ tokens,
373
+ heads,
374
+ blocks,
375
+ head_dim,
376
+ BLOCK_SIZE,
377
+ tile_d,
378
+ tau,
379
+ )
380
+ return global_threshold
381
+
382
+
383
+ def _compute_exact_threshold(
384
+ q: torch.Tensor,
385
+ kc: torch.Tensor,
386
+ *,
387
+ tau: float,
388
+ scale: float,
389
+ ) -> torch.Tensor:
390
+ batch, tokens, heads, head_dim = q.shape
391
+ blocks = triton.cdiv(tokens, BLOCK_SIZE)
392
+ tile_d = min(128, triton.next_power_of_2(head_dim))
393
+ batch_heads = batch * heads
394
+ kc_bh = kc.permute(0, 2, 1, 3)
395
+ kc_mean = kc_bh.mean(dim=2, dtype=torch.float32)
396
+ kc_second_moment = torch.matmul(
397
+ kc_bh.transpose(-1, -2),
398
+ kc_bh,
399
+ )
400
+ kc_second_moment.div_(blocks)
401
+ q_bar = torch.empty(
402
+ (batch_heads, blocks, head_dim),
403
+ device=q.device,
404
+ dtype=torch.bfloat16,
405
+ )
406
+ global_threshold = torch.empty(
407
+ (batch, blocks, heads),
408
+ device=q.device,
409
+ dtype=torch.float32,
410
+ )
411
+ q_desc = TensorDescriptor.from_tensor(
412
+ q,
413
+ [1, BLOCK_SIZE, 1, tile_d],
414
+ )
415
+ _pool_query_kernel[(blocks, batch_heads)](
416
+ q_desc,
417
+ q_bar,
418
+ tokens,
419
+ heads,
420
+ blocks,
421
+ head_dim,
422
+ BLOCK_SIZE,
423
+ tile_d,
424
+ num_warps=4,
425
+ num_stages=1,
426
+ )
427
+ block_m = 64
428
+ _exact_fused_threshold_kernel[(triton.cdiv(blocks, block_m), batch_heads)](
429
+ q_bar,
430
+ kc_mean,
431
+ kc_second_moment,
432
+ global_threshold,
433
+ scale,
434
+ heads,
435
+ blocks,
436
+ head_dim,
437
+ block_m,
438
+ tile_d,
439
+ tau,
440
+ num_warps=4,
441
+ num_stages=1,
442
+ )
443
+ return global_threshold
444
+
445
+
446
+ def prepare(
447
+ q: torch.Tensor,
448
+ k: torch.Tensor,
449
+ v: torch.Tensor,
450
+ *,
451
+ tau: float,
452
+ scale: float,
453
+ thresh_type: str = "diag",
454
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
455
+ kc, vc = _reduce_kv(k, v)
456
+ if thresh_type == "exact":
457
+ threshold = _compute_exact_threshold(q, kc, tau=tau, scale=scale)
458
+ else:
459
+ threshold = _compute_diag_threshold(q, kc, tau=tau, scale=scale)
460
+ return kc, vc, threshold
461
+
462
+
463
+ __all__ = ["prepare"]
torch-ext/sol_attn/sm100/LICENSE.flash-attention 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.
torch-ext/sol_attn/sm100/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Blackwell backend."""
2
+
3
+ from .kernel import forward
4
+
5
+ __all__ = ["forward"]
torch-ext/sol_attn/sm100/kernel.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Blackwell kernel entry."""
2
+
3
+ from .mainloop import forward
4
+
5
+ __all__ = ["forward"]
torch-ext/sol_attn/sm100/mainloop.py ADDED
@@ -0,0 +1,1762 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sol-Attn forward kernel for Blackwell SM100.
2
+
3
+ The kernel routes two physical N64 halves at a time and accumulates their exact
4
+ indices into one logical G256 stream. Per-column additive masks are built once
5
+ in shared memory and reused by the approximate and exact score paths.
6
+ """
7
+
8
+ import math
9
+ import cuda.bindings.driver as cuda
10
+ import cutlass
11
+ import cutlass.cute as cute
12
+ import cutlass.pipeline as pipeline
13
+ import cutlass.utils as utils
14
+ import cutlass.utils.blackwell_helpers as sm100_utils
15
+ from .._vendor.flash_attn.cute import pipeline as fa_pipeline
16
+ from .._vendor.flash_attn.cute import utils as fa_utils
17
+ from cutlass import BFloat16, Float32, Int32
18
+ from cutlass._mlir.dialects import llvm
19
+ from cutlass.cute.nvgpu import cpasync, tcgen05
20
+ from cutlass.cutlass_dsl import T, dsl_user_op
21
+ from .._vendor.flash_attn.cute.cute_dsl_utils import assume_tensor_aligned
22
+
23
+ from .softmax import (
24
+ _load_m64_n128_score as _load_pair_score,
25
+ _online_update_one_half as _online_update_pair,
26
+ _rescale_m64_partial_o as _rescale_pair_o,
27
+ )
28
+ from . import math as mma_utils
29
+
30
+ from ..common import layout_utils
31
+ from ..common.selector import (
32
+ sol_attn_popc_b32,
33
+ sol_attn_route_is_exact,
34
+ )
35
+ from .tmem import (
36
+ _add_physical_tmem_base,
37
+ _zero_based_tmem_tensor,
38
+ load_m64_o_fp32_256b,
39
+ tcgen05_wait_st,
40
+ )
41
+
42
+
43
+ M = 64
44
+ N_MEMBER = 64
45
+ N_PACK_HALF = 128
46
+ D = 128
47
+ DV = 128
48
+ THREADS = 192
49
+ PAIR_STAGES = 1
50
+ TMEM_COLS = 256
51
+ PAIR_SCORE_OFFSET = 0
52
+ PAIR_P_OFFSET = 64
53
+ O_OFFSET = 128
54
+ PACK_QK_INST = (M, N_PACK_HALF, 16)
55
+ PACK_QK_TILE = (M, N_PACK_HALF, D)
56
+ PACK_PV_INST = (M, DV, 16)
57
+ PACK_PV_TILE = (M, DV, N_PACK_HALF)
58
+ PACK_QK_QUARTER_INST = (M, N_MEMBER, 16)
59
+ PACK_PV_QUARTER_INST = (M, 64, 16)
60
+ PACK_QK_GATHER_TILE = (M, N_MEMBER, 64)
61
+ PACK_PV_GATHER_TILE = (M, 64, 64)
62
+ LOG2E = math.log2(math.e)
63
+ LN2 = math.log(2.0)
64
+ SEMANTIC_ROW_OFFSET = 16
65
+ LOGICAL_GROUP_SIZE = 256
66
+ ROUTE_TILE_SIZE = 128
67
+ ROUTE_HALVES_PER_GROUP = LOGICAL_GROUP_SIZE // ROUTE_TILE_SIZE
68
+ ROUTE_MASK_WORDS = 4
69
+ # masks[0:4], current-half exact count, append base, cumulative exact count,
70
+ # logical-terminal-half flag
71
+ PACKET_WORDS = 8
72
+ ROUTE_INDEX_CAPACITY = LOGICAL_GROUP_SIZE
73
+ PAIR_P_CHUNKS = 4
74
+ PAIR_P_CHUNK_PACKED_COLUMNS = (N_PACK_HALF // 2) // PAIR_P_CHUNKS
75
+ PAIR_P_PACKED_REGISTERS_PER_THREAD_PER_CHUNK = 8
76
+ O_PACKED_STORE_VALUES_PER_WORD = 2
77
+ O_PACKED_STORE_ALIGNMENT_BYTES = 4
78
+ O_PACKED_STORE_WRITER_THREADS = 4 * 32
79
+ O_ROWS_PER_OWNER_THREAD = 2
80
+ O_PACKED_WORDS_PER_ROW_PER_THREAD = 16
81
+ O_PACKED_COLUMN_STRIDE = 8
82
+
83
+ @dsl_user_op
84
+ def _cvt_bf16x2_f32(
85
+ hi: Float32,
86
+ lo: Float32,
87
+ *,
88
+ loc=None,
89
+ ip=None,
90
+ ) -> Int32:
91
+ """Round two FP32 values and pack them as ``{lo, hi}`` BF16 bits."""
92
+
93
+ return Int32(
94
+ llvm.inline_asm(
95
+ T.i32(),
96
+ [
97
+ Float32(hi).ir_value(loc=loc, ip=ip),
98
+ Float32(lo).ir_value(loc=loc, ip=ip),
99
+ ],
100
+ "cvt.rn.bf16x2.f32 $0, $1, $2;",
101
+ "=r,f,f",
102
+ has_side_effects=False,
103
+ is_align_stack=False,
104
+ asm_dialect=llvm.AsmDialect.AD_ATT,
105
+ )
106
+ )
107
+
108
+
109
+ @dsl_user_op
110
+ def _store_global_u32_inline(
111
+ ptr: cute.Pointer,
112
+ value: Int32,
113
+ *,
114
+ loc=None,
115
+ ip=None,
116
+ ) -> None:
117
+ """Store one aligned same-row BF16 pair as a single 32-bit word."""
118
+
119
+ llvm.inline_asm(
120
+ None,
121
+ [
122
+ ptr.toint().ir_value(),
123
+ Int32(value).ir_value(loc=loc, ip=ip),
124
+ ],
125
+ "st.global.u32 [$0], $1;",
126
+ "l,r",
127
+ has_side_effects=True,
128
+ is_align_stack=False,
129
+ asm_dialect=llvm.AsmDialect.AD_ATT,
130
+ )
131
+
132
+
133
+ @dsl_user_op
134
+ def _prmt_b32(
135
+ a: Int32,
136
+ b: Int32,
137
+ sel: Int32,
138
+ *,
139
+ loc=None,
140
+ ip=None,
141
+ ) -> Int32:
142
+ """Select four bytes from packed words ``a`` and ``b``."""
143
+
144
+ return Int32(
145
+ llvm.inline_asm(
146
+ T.i32(),
147
+ [
148
+ Int32(a).ir_value(loc=loc, ip=ip),
149
+ Int32(b).ir_value(loc=loc, ip=ip),
150
+ Int32(sel).ir_value(loc=loc, ip=ip),
151
+ ],
152
+ "prmt.b32 $0, $1, $2, $3;",
153
+ "=r,r,r,r",
154
+ has_side_effects=False,
155
+ is_align_stack=False,
156
+ asm_dialect=llvm.AsmDialect.AD_ATT,
157
+ )
158
+ )
159
+
160
+
161
+ @cute.jit
162
+ def _store_pair_probability_chunked_tmemp(
163
+ o_template: cute.Tensor,
164
+ probabilities: cute.Tensor,
165
+ tmem_base: Int32,
166
+ p_offset: Int32,
167
+ owner_tidx: Int32,
168
+ ):
169
+ """Store M64xN128 BF16 P as four live-range-bounded x8 chunks.
170
+
171
+ Probabilities remain FP32 until each x8 fragment is converted, and every
172
+ chunk waits for its St16x64b store before the fragment goes out of scope.
173
+ """
174
+
175
+ assert o_template.element_type == Float32
176
+ assert cute.size(o_template) == M * DV
177
+ p_chunk_layout = cute.composition(
178
+ o_template.layout,
179
+ cute.make_layout((M, PAIR_P_CHUNK_PACKED_COLUMNS)),
180
+ )
181
+ relative_chunk = _zero_based_tmem_tensor(Float32, p_chunk_layout)
182
+ store_atom = cute.make_copy_atom(
183
+ tcgen05.copy.St16x64bOp(tcgen05.copy.Repetition(8)),
184
+ Float32,
185
+ )
186
+ tiled_store = tcgen05.make_tmem_copy(store_atom, relative_chunk)
187
+ thread_store = tiled_store.get_slice(owner_tidx)
188
+ destination_relative = thread_store.partition_D(relative_chunk)
189
+ destination = _add_physical_tmem_base(
190
+ destination_relative, tmem_base + p_offset
191
+ )
192
+ p_store_coordinates = thread_store.partition_S(
193
+ cute.make_identity_tensor((M, PAIR_P_CHUNK_PACKED_COLUMNS))
194
+ )
195
+ lane = owner_tidx % Int32(32)
196
+
197
+ for chunk_idx in cutlass.range_constexpr(PAIR_P_CHUNKS):
198
+ p_store_registers = cute.make_rmem_tensor(
199
+ p_store_coordinates.shape, Float32
200
+ )
201
+ assert (
202
+ cute.size(p_store_registers)
203
+ == PAIR_P_PACKED_REGISTERS_PER_THREAD_PER_CHUNK
204
+ )
205
+ assert (
206
+ cute.size(probabilities)
207
+ == 2 * cute.size(p_store_registers) * PAIR_P_CHUNKS
208
+ )
209
+ p_store_words = cute.make_tensor(
210
+ cute.recast_ptr(p_store_registers.iterator, dtype=Int32),
211
+ p_store_registers.layout,
212
+ )
213
+ probability_base = chunk_idx * (2 * cute.size(p_store_registers))
214
+ for i in cutlass.range(
215
+ cute.size(p_store_registers), unroll_full=True
216
+ ):
217
+ low = probability_base + i * 2
218
+ high = low + 1
219
+ own = _cvt_bf16x2_f32(
220
+ Float32(probabilities[high]),
221
+ Float32(probabilities[low]),
222
+ )
223
+ peer = cute.arch.shuffle_sync_bfly(own, offset=2)
224
+ if (lane & Int32(2)) == Int32(0):
225
+ p_store_words[i] = _prmt_b32(
226
+ own, peer, Int32(0x5410)
227
+ )
228
+ else:
229
+ p_store_words[i] = _prmt_b32(
230
+ own, peer, Int32(0x3276)
231
+ )
232
+
233
+ destination_chunk = cute.make_tensor(
234
+ destination.iterator
235
+ + chunk_idx * PAIR_P_CHUNK_PACKED_COLUMNS,
236
+ destination.layout,
237
+ )
238
+ cute.copy(tiled_store, p_store_registers, destination_chunk)
239
+ tcgen05_wait_st()
240
+
241
+ cute.arch.fence_view_async_tmem_store()
242
+
243
+
244
+ @cute.jit
245
+ def _load_pack_k_half(
246
+ tma_atom_pack_k: cute.CopyAtom,
247
+ tPackKgK: cute.Tensor,
248
+ tPackKsK: cute.Tensor,
249
+ block0: Int32,
250
+ block1: Int32,
251
+ quarter0: Int32,
252
+ barrier,
253
+ ):
254
+ """Gather one canonical N128 K tile as K0/N0,K0/N1,K1/N0,K1/N1."""
255
+
256
+ cute.copy(
257
+ tma_atom_pack_k,
258
+ tPackKgK[(None, block0, Int32(0))],
259
+ tPackKsK[(None, quarter0)],
260
+ tma_bar_ptr=barrier,
261
+ )
262
+ cute.copy(
263
+ tma_atom_pack_k,
264
+ tPackKgK[(None, block1, Int32(0))],
265
+ tPackKsK[(None, quarter0 + Int32(1))],
266
+ tma_bar_ptr=barrier,
267
+ )
268
+ cute.copy(
269
+ tma_atom_pack_k,
270
+ tPackKgK[(None, block0, Int32(1))],
271
+ tPackKsK[(None, quarter0 + Int32(2))],
272
+ tma_bar_ptr=barrier,
273
+ )
274
+ cute.copy(
275
+ tma_atom_pack_k,
276
+ tPackKgK[(None, block1, Int32(1))],
277
+ tPackKsK[(None, quarter0 + Int32(3))],
278
+ tma_bar_ptr=barrier,
279
+ )
280
+
281
+
282
+ @cute.jit
283
+ def _load_pack_v_half(
284
+ tma_atom_pack_v: cute.CopyAtom,
285
+ tPackVgV: cute.Tensor,
286
+ tPackVsV: cute.Tensor,
287
+ block0: Int32,
288
+ block1: Int32,
289
+ quarter0: Int32,
290
+ barrier,
291
+ ):
292
+ """Gather one canonical N128 V tile as D0/N0,D0/N1,D1/N0,D1/N1."""
293
+
294
+ cute.copy(
295
+ tma_atom_pack_v,
296
+ tPackVgV[(None, Int32(0), block0)],
297
+ tPackVsV[(None, quarter0)],
298
+ tma_bar_ptr=barrier,
299
+ )
300
+ cute.copy(
301
+ tma_atom_pack_v,
302
+ tPackVgV[(None, Int32(0), block1)],
303
+ tPackVsV[(None, quarter0 + Int32(1))],
304
+ tma_bar_ptr=barrier,
305
+ )
306
+ cute.copy(
307
+ tma_atom_pack_v,
308
+ tPackVgV[(None, Int32(1), block0)],
309
+ tPackVsV[(None, quarter0 + Int32(2))],
310
+ tma_bar_ptr=barrier,
311
+ )
312
+ cute.copy(
313
+ tma_atom_pack_v,
314
+ tPackVgV[(None, Int32(1), block1)],
315
+ tPackVsV[(None, quarter0 + Int32(3))],
316
+ tma_bar_ptr=barrier,
317
+ )
318
+
319
+
320
+ @cute.struct
321
+ class SharedStorage:
322
+ q_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2]
323
+ pack_k_mbar_ptr: cute.struct.MemRange[
324
+ cutlass.Int64, PAIR_STAGES * 2
325
+ ]
326
+ pack_v_mbar_ptr: cute.struct.MemRange[
327
+ cutlass.Int64, PAIR_STAGES * 2
328
+ ]
329
+ pair_score_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2]
330
+ pair_o_mbar_ptr: cute.struct.MemRange[cutlass.Int64, 2]
331
+ final_stats: cute.struct.Align[
332
+ cute.struct.MemRange[Float32, M * 2], 128
333
+ ]
334
+ route_partial: cute.struct.Align[
335
+ cute.struct.MemRange[Float32, 4 * ROUTE_TILE_SIZE], 16
336
+ ]
337
+ column_masks: cute.struct.Align[
338
+ cute.struct.MemRange[Float32, ROUTE_TILE_SIZE], 16
339
+ ]
340
+ route_packet: cute.struct.Align[
341
+ cute.struct.MemRange[Int32, PACKET_WORDS], 16
342
+ ]
343
+ tmem_holding_buf: Int32
344
+ # Owner-warp 0 lane 0 appends both N128 route masks. The full-CTA
345
+ # pre-exact join publishes the completed list to warp 5; no HBM indices.
346
+ route_indices: cute.struct.Align[
347
+ cute.struct.MemRange[Int32, ROUTE_INDEX_CAPACITY], 16
348
+ ]
349
+
350
+
351
+ @cute.kernel
352
+ def _sol_attn_sm100_bf16_kernel(
353
+ tiled_pack_qk: cute.TiledMma,
354
+ tiled_pack_pv: cute.TiledMma,
355
+ tma_atom_q: cute.CopyAtom,
356
+ mQ_mkl: cute.Tensor,
357
+ tma_atom_pack_k: cute.CopyAtom,
358
+ mPackK_nkl: cute.Tensor,
359
+ tma_atom_pack_v: cute.CopyAtom,
360
+ mPackV_nkl: cute.Tensor,
361
+ tma_atom_kc: cute.CopyAtom,
362
+ mKC_nkl: cute.Tensor,
363
+ tma_atom_vc: cute.CopyAtom,
364
+ mVC_nkl: cute.Tensor,
365
+ mThreshold_bnh: cute.Tensor,
366
+ mO_bthd: cute.Tensor,
367
+ mLSE_bth: cute.Tensor,
368
+ token_count: Int32,
369
+ route_valid_total: Int32,
370
+ num_route_tiles: Int32,
371
+ softmax_scale: Float32,
372
+ sink_start_block: Int32,
373
+ sink_end_block: Int32,
374
+ q_layout: cute.ComposedLayout,
375
+ pack_k_layout: cute.ComposedLayout,
376
+ pack_k_gather_layout: cute.ComposedLayout,
377
+ pack_p_layout: cute.ComposedLayout,
378
+ pack_v_layout: cute.ComposedLayout,
379
+ pack_v_gather_layout: cute.ComposedLayout,
380
+ route_k_layout: cute.ComposedLayout,
381
+ route_v_layout: cute.ComposedLayout,
382
+ ):
383
+ tidx, _, _ = cute.arch.thread_idx()
384
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
385
+ q_block_idx_raw, head_idx_raw, batch_idx_raw = cute.arch.block_idx()
386
+ q_block_idx = Int32(q_block_idx_raw)
387
+ head_idx = Int32(head_idx_raw)
388
+ batch_idx = Int32(batch_idx_raw)
389
+ softmax_scale_log2 = softmax_scale * Float32(LOG2E)
390
+
391
+ smem = utils.SmemAllocator()
392
+ storage = smem.allocate(SharedStorage)
393
+ sFinalStats = storage.final_stats.get_tensor(
394
+ cute.make_layout((M, 2))
395
+ )
396
+ route_partial = storage.route_partial.get_tensor(
397
+ cute.make_layout((4, ROUTE_TILE_SIZE))
398
+ )
399
+ column_masks = storage.column_masks.get_tensor(
400
+ cute.make_layout((ROUTE_TILE_SIZE,))
401
+ )
402
+ route_packet = storage.route_packet.get_tensor(
403
+ cute.make_layout((PACKET_WORDS,))
404
+ )
405
+ route_indices = storage.route_indices.get_tensor(
406
+ cute.make_layout((ROUTE_INDEX_CAPACITY,))
407
+ )
408
+ sQ = smem.allocate_tensor(
409
+ element_type=BFloat16,
410
+ layout=q_layout.outer,
411
+ byte_alignment=128,
412
+ swizzle=q_layout.inner,
413
+ )
414
+ sPackK = smem.allocate_tensor(
415
+ element_type=BFloat16,
416
+ layout=pack_k_layout.outer,
417
+ byte_alignment=128,
418
+ swizzle=pack_k_layout.inner,
419
+ )
420
+ sPackV = smem.allocate_tensor(
421
+ element_type=BFloat16,
422
+ layout=pack_v_layout.outer,
423
+ byte_alignment=128,
424
+ swizzle=pack_v_layout.inner,
425
+ )
426
+ # One independent physical N128 K stage and one N128 V stage. Every
427
+ # runtime route/exact transaction stays in this completion domain.
428
+ sPackKGather = cute.make_tensor(
429
+ cute.recast_ptr(
430
+ sPackK.iterator, pack_k_gather_layout.inner, BFloat16
431
+ ),
432
+ pack_k_gather_layout.outer,
433
+ )
434
+ sPackVGather = cute.make_tensor(
435
+ cute.recast_ptr(
436
+ sPackV.iterator, pack_v_gather_layout.inner, BFloat16
437
+ ),
438
+ pack_v_gather_layout.outer,
439
+ )
440
+ # KC/VC and exact K/V have disjoint lifetimes within each runtime group.
441
+ # They reuse the same independent N128 K and V allocations without a
442
+ # cross-operand alias barrier.
443
+ sKC = cute.make_tensor(
444
+ cute.recast_ptr(sPackK.iterator, route_k_layout.inner, BFloat16),
445
+ route_k_layout.outer,
446
+ )
447
+ sVC = cute.make_tensor(
448
+ cute.recast_ptr(sPackV.iterator, route_v_layout.inner, BFloat16),
449
+ route_v_layout.outer,
450
+ )
451
+
452
+ tmem_barrier = pipeline.NamedBarrier(barrier_id=1, num_threads=THREADS)
453
+ score_loaded_barrier = pipeline.NamedBarrier(
454
+ barrier_id=2, num_threads=4 * 32
455
+ )
456
+ final_stats_ready_barrier = pipeline.NamedBarrier(
457
+ barrier_id=3, num_threads=4 * 32
458
+ )
459
+ pack_score_loaded_barrier = pipeline.NamedBarrier(
460
+ barrier_id=4, num_threads=4 * 32
461
+ )
462
+ route_packet_ready_barrier = pipeline.NamedBarrier(
463
+ barrier_id=5, num_threads=5 * 32
464
+ )
465
+ exact_pair_p_ready_barrier = pipeline.NamedBarrier(
466
+ barrier_id=6, num_threads=5 * 32
467
+ )
468
+ tmem = utils.TmemAllocator(
469
+ storage.tmem_holding_buf.ptr,
470
+ barrier_for_retrieve=tmem_barrier,
471
+ )
472
+ tmem.allocate(TMEM_COLS)
473
+
474
+ one_thread = pipeline.CooperativeGroup(pipeline.Agent.Thread, 1)
475
+ pack_owner_threads = pipeline.CooperativeGroup(
476
+ pipeline.Agent.Thread, 4 * 32
477
+ )
478
+ q_bytes = cute.size_in_bytes(
479
+ BFloat16, cute.select(q_layout, mode=[0, 1, 2])
480
+ )
481
+ route_k_bytes = cute.size_in_bytes(
482
+ BFloat16, cute.select(route_k_layout, mode=[0, 1, 2])
483
+ )
484
+ route_v_bytes = cute.size_in_bytes(
485
+ BFloat16, cute.select(route_v_layout, mode=[0, 1, 2])
486
+ )
487
+ pack_k_bytes = cute.size_in_bytes(
488
+ BFloat16, cute.select(pack_k_layout, mode=[0, 1, 2])
489
+ )
490
+ pack_v_bytes = cute.size_in_bytes(
491
+ BFloat16, cute.select(pack_v_layout, mode=[0, 1, 2])
492
+ )
493
+ assert route_k_bytes == pack_k_bytes
494
+ assert route_v_bytes == pack_v_bytes
495
+ q_pipe = fa_pipeline.PipelineTmaUmma.create(
496
+ num_stages=1,
497
+ producer_group=one_thread,
498
+ consumer_group=one_thread,
499
+ tx_count=q_bytes,
500
+ barrier_storage=storage.q_mbar_ptr.data_ptr(),
501
+ )
502
+ pack_k_pipe = fa_pipeline.PipelineTmaUmma.create(
503
+ num_stages=PAIR_STAGES,
504
+ producer_group=one_thread,
505
+ consumer_group=one_thread,
506
+ tx_count=pack_k_bytes,
507
+ barrier_storage=storage.pack_k_mbar_ptr.data_ptr(),
508
+ )
509
+ pack_v_pipe = fa_pipeline.PipelineTmaUmma.create(
510
+ num_stages=PAIR_STAGES,
511
+ producer_group=one_thread,
512
+ consumer_group=one_thread,
513
+ tx_count=pack_v_bytes,
514
+ barrier_storage=storage.pack_v_mbar_ptr.data_ptr(),
515
+ )
516
+ pair_score_pipe = fa_pipeline.PipelineUmmaAsync.create(
517
+ num_stages=1,
518
+ producer_group=one_thread,
519
+ consumer_group=pack_owner_threads,
520
+ barrier_storage=storage.pair_score_mbar_ptr.data_ptr(),
521
+ )
522
+ pair_o_pipe = fa_pipeline.PipelineUmmaAsync.create(
523
+ num_stages=1,
524
+ producer_group=one_thread,
525
+ consumer_group=pack_owner_threads,
526
+ barrier_storage=storage.pair_o_mbar_ptr.data_ptr(),
527
+ )
528
+
529
+ mQ_cur = mQ_mkl[None, None, head_idx, batch_idx]
530
+ mPackK_cur = mPackK_nkl[None, None, head_idx, batch_idx]
531
+ mPackV_cur = mPackV_nkl[None, None, head_idx, batch_idx]
532
+ mKC_cur = mKC_nkl[None, None, head_idx, batch_idx]
533
+ mVC_cur = mVC_nkl[None, None, head_idx, batch_idx]
534
+ gQ = cute.local_tile(mQ_cur, (M, D), (None, 0))
535
+ gPackK = cute.local_tile(
536
+ mPackK_cur, (N_MEMBER, 64), (None, None)
537
+ )
538
+ gPackV = cute.local_tile(
539
+ mPackV_cur, (64, N_MEMBER), (None, None)
540
+ )
541
+ gKC = cute.local_tile(mKC_cur, (N_PACK_HALF, D), (None, 0))
542
+ gVC = cute.local_tile(mVC_cur, (DV, N_PACK_HALF), (0, None))
543
+ thr_pack_qk = tiled_pack_qk.get_slice(0)
544
+ thr_pack_pv = tiled_pack_pv.get_slice(0)
545
+ tCgQ = thr_pack_qk.partition_A(gQ)
546
+ tCgKC = thr_pack_qk.partition_B(gKC)
547
+ tCgVC = thr_pack_pv.partition_B(gVC)
548
+ tCrKC = tiled_pack_qk.make_fragment_B(sKC)
549
+ tCrVC = tiled_pack_pv.make_fragment_B(sVC)
550
+ tCrPackQ = tiled_pack_qk.make_fragment_A(sQ)
551
+ tCrPackK = tiled_pack_qk.make_fragment_B(sPackK)
552
+ tCrPackV = tiled_pack_pv.make_fragment_B(sPackV)
553
+
554
+ tQsQ, tQgQ = cpasync.tma_partition(
555
+ tma_atom_q,
556
+ 0,
557
+ cute.make_layout(1),
558
+ cute.group_modes(sQ, 0, 3),
559
+ cute.group_modes(tCgQ, 0, 3),
560
+ )
561
+ tPackKsK, tPackKgK = cpasync.tma_partition(
562
+ tma_atom_pack_k,
563
+ 0,
564
+ cute.make_layout(1),
565
+ cute.group_modes(sPackKGather, 0, 3),
566
+ cute.group_modes(gPackK, 0, 2),
567
+ )
568
+ tPackVsV, tPackVgV = cpasync.tma_partition(
569
+ tma_atom_pack_v,
570
+ 0,
571
+ cute.make_layout(1),
572
+ cute.group_modes(sPackVGather, 0, 3),
573
+ cute.group_modes(gPackV, 0, 2),
574
+ )
575
+ tKCsKC, tKCgKC = cpasync.tma_partition(
576
+ tma_atom_kc,
577
+ 0,
578
+ cute.make_layout(1),
579
+ cute.group_modes(sKC, 0, 3),
580
+ cute.group_modes(tCgKC, 0, 3),
581
+ )
582
+ tVCsVC, tVCgVC = cpasync.tma_partition(
583
+ tma_atom_vc,
584
+ 0,
585
+ cute.make_layout(1),
586
+ cute.group_modes(sVC, 0, 3),
587
+ cute.group_modes(tCgVC, 0, 3),
588
+ )
589
+
590
+ pack_score_shape = tiled_pack_qk.partition_shape_C(
591
+ PACK_QK_TILE[:2]
592
+ )
593
+ pack_score_template = tiled_pack_qk.make_fragment_C(pack_score_shape)
594
+ pack_o_shape = tiled_pack_pv.partition_shape_C(PACK_PV_TILE[:2])
595
+ pack_o_template = tiled_pack_pv.make_fragment_C(pack_o_shape)
596
+
597
+ tmem.wait_for_alloc()
598
+ tmem_ptr = tmem.retrieve_ptr(Float32)
599
+ # The 256-column allocation leaves the second half of SM TMEM available to
600
+ # another CTA. The live allocation remains owned after permit release.
601
+ tmem.relinquish_alloc_permit()
602
+ tmem_base = tmem_ptr.toint()
603
+ pair_tScore = cute.make_tensor(
604
+ cute.make_ptr(
605
+ Float32,
606
+ tmem_base + Int32(PAIR_SCORE_OFFSET),
607
+ cute.AddressSpace.tmem,
608
+ assumed_align=16,
609
+ ),
610
+ pack_score_template.layout,
611
+ )
612
+ pair_tO = cute.make_tensor(
613
+ cute.make_ptr(
614
+ Float32,
615
+ tmem_base + Int32(O_OFFSET),
616
+ cute.AddressSpace.tmem,
617
+ assumed_align=16,
618
+ ),
619
+ pack_o_template.layout,
620
+ )
621
+ # make_fragment_A drops the physical TMEM allocation base and addresses
622
+ # packed BF16 columns in half-column units. Restore both facts so
623
+ # 2*tmem_base + 2*PAIR_P_OFFSET names columns 64..127.
624
+ pair_tP_storage = cute.make_tensor(
625
+ pair_tScore.iterator, pack_p_layout.outer
626
+ )
627
+ pair_tP_base = tiled_pack_pv.make_fragment_A(pair_tP_storage)[
628
+ None, None, None, 0
629
+ ]
630
+ pair_tP = cute.make_tensor(
631
+ pair_tP_base.iterator
632
+ + tmem_base
633
+ + tmem_base
634
+ + Int32(PAIR_P_OFFSET * 2),
635
+ pair_tP_base.layout,
636
+ )
637
+ q_producer = fa_pipeline.make_pipeline_state(
638
+ pipeline.PipelineUserType.Producer, 1
639
+ )
640
+ q_consumer = fa_pipeline.make_pipeline_state(
641
+ pipeline.PipelineUserType.Consumer, 1
642
+ )
643
+ pack_k_producer = pipeline.make_pipeline_state(
644
+ pipeline.PipelineUserType.Producer, PAIR_STAGES
645
+ )
646
+ pack_k_consumer = pipeline.make_pipeline_state(
647
+ pipeline.PipelineUserType.Consumer, PAIR_STAGES
648
+ )
649
+ pack_v_producer = pipeline.make_pipeline_state(
650
+ pipeline.PipelineUserType.Producer, PAIR_STAGES
651
+ )
652
+ pack_v_consumer = pipeline.make_pipeline_state(
653
+ pipeline.PipelineUserType.Consumer, PAIR_STAGES
654
+ )
655
+ pair_score_producer = fa_pipeline.make_pipeline_state(
656
+ pipeline.PipelineUserType.Producer, 1
657
+ )
658
+ pair_score_consumer = fa_pipeline.make_pipeline_state(
659
+ pipeline.PipelineUserType.Consumer, 1
660
+ )
661
+ pair_o_producer = fa_pipeline.make_pipeline_state(
662
+ pipeline.PipelineUserType.Producer, 1
663
+ )
664
+ pair_o_consumer = fa_pipeline.make_pipeline_state(
665
+ pipeline.PipelineUserType.Consumer, 1
666
+ )
667
+ route_start_base = Int32(0)
668
+ q_len = token_count - q_block_idx * Int32(M)
669
+ if q_len > Int32(M):
670
+ q_len = Int32(M)
671
+ threshold = Float32(
672
+ mThreshold_bnh[batch_idx, q_block_idx, head_idx]
673
+ )
674
+
675
+ if warp_idx == Int32(5):
676
+ cpasync.prefetch_descriptor(tma_atom_q)
677
+ cpasync.prefetch_descriptor(tma_atom_pack_k)
678
+ cpasync.prefetch_descriptor(tma_atom_pack_v)
679
+ cpasync.prefetch_descriptor(tma_atom_kc)
680
+ cpasync.prefetch_descriptor(tma_atom_vc)
681
+
682
+ q_pipe.producer_acquire(q_producer)
683
+ q_barrier = q_pipe.producer_get_barrier(q_producer)
684
+ cute.copy(
685
+ tma_atom_q,
686
+ tQgQ[(None, q_block_idx)],
687
+ tQsQ[(None, q_producer.index)],
688
+ tma_bar_ptr=q_barrier,
689
+ )
690
+ q_producer.advance()
691
+
692
+ is_owner = warp_idx >= Int32(1) and warp_idx <= Int32(4)
693
+ is_score_consumer = warp_idx <= Int32(4)
694
+ owner_tidx = tidx - Int32(32)
695
+
696
+ # One register-resident online state and one TMEM-O initialization bit span
697
+ # every route/exact transaction in every runtime group.
698
+ running_max = -Float32.inf
699
+ running_sum = Float32(0.0)
700
+ owner_o_initialized = Int32(0)
701
+ mma_o_initialized = Int32(0)
702
+
703
+ if warp_idx == Int32(0):
704
+ q_pipe.consumer_wait(q_consumer)
705
+
706
+ # The outer loop owns one logical G256 exact-index lifetime. The inner
707
+ # loop consumes each physical score/PV half immediately; it appends only
708
+ # integer indices, never a second score or probability fragment.
709
+ num_logical_groups = (
710
+ num_route_tiles + Int32(ROUTE_HALVES_PER_GROUP - 1)
711
+ ) // Int32(ROUTE_HALVES_PER_GROUP)
712
+ # BEGIN_G256_CURSOR_UNIFORM_INDUCTION
713
+ # arch_make_warp_uniform is a lowering hint, not a value broadcast. Both
714
+ # values are CTA-invariant integer scalars before the hint.
715
+ logical_group_idx = cute.arch.make_warp_uniform(Int32(0))
716
+ remaining_group_tiles = cute.arch.make_warp_uniform(num_route_tiles)
717
+ while logical_group_idx < num_logical_groups:
718
+ is_final_logical_group = (
719
+ logical_group_idx + Int32(1) == num_logical_groups
720
+ )
721
+ group_route_tile_base = logical_group_idx * Int32(
722
+ ROUTE_HALVES_PER_GROUP
723
+ )
724
+ physical_halves_this_group = remaining_group_tiles
725
+ if physical_halves_this_group > Int32(ROUTE_HALVES_PER_GROUP):
726
+ physical_halves_this_group = Int32(ROUTE_HALVES_PER_GROUP)
727
+
728
+ for half_idx in cutlass.range(
729
+ physical_halves_this_group, unroll=1
730
+ ):
731
+ route_tile_idx = cute.arch.make_warp_uniform(
732
+ group_route_tile_base + half_idx
733
+ )
734
+ is_final_route_tile = (
735
+ route_tile_idx + Int32(1) == num_route_tiles
736
+ )
737
+ is_logical_terminal_half = (
738
+ half_idx + Int32(1) == physical_halves_this_group
739
+ )
740
+ route_start = cute.arch.make_warp_uniform(
741
+ route_start_base
742
+ + route_tile_idx * Int32(ROUTE_TILE_SIZE)
743
+ )
744
+ remaining_route_count = cute.arch.make_warp_uniform(
745
+ route_valid_total
746
+ - route_tile_idx * Int32(ROUTE_TILE_SIZE)
747
+ )
748
+ valid_route_count = remaining_route_count
749
+ if valid_route_count > Int32(ROUTE_TILE_SIZE):
750
+ valid_route_count = Int32(ROUTE_TILE_SIZE)
751
+ if valid_route_count < Int32(0):
752
+ valid_route_count = Int32(0)
753
+
754
+ # One native N128 route transaction shares the independent K/V stages
755
+ # with the exact-pair engine. Route and exact are separated by
756
+ # a full-CTA phase boundary, so no K<->V alias handoff is required.
757
+ if warp_idx == Int32(5):
758
+ pack_k_pipe.producer_acquire(pack_k_producer)
759
+ route_k_barrier = pack_k_pipe.producer_get_barrier(
760
+ pack_k_producer
761
+ )
762
+ cute.copy(
763
+ tma_atom_kc,
764
+ tKCgKC[(None, route_tile_idx)],
765
+ tKCsKC[(None, pack_k_producer.index)],
766
+ tma_bar_ptr=route_k_barrier,
767
+ )
768
+ pack_k_producer.advance()
769
+
770
+ pack_v_pipe.producer_acquire(pack_v_producer)
771
+ route_v_barrier = pack_v_pipe.producer_get_barrier(
772
+ pack_v_producer
773
+ )
774
+ cute.copy(
775
+ tma_atom_vc,
776
+ tVCgVC[(None, route_tile_idx)],
777
+ tVCsVC[(None, pack_v_producer.index)],
778
+ tma_bar_ptr=route_v_barrier,
779
+ )
780
+ pack_v_producer.advance()
781
+
782
+ if warp_idx == Int32(0):
783
+ pack_k_pipe.consumer_wait(pack_k_consumer)
784
+ pair_score_pipe.producer_acquire(pair_score_producer)
785
+ mma_utils.gemm(
786
+ tiled_pack_qk,
787
+ pair_tScore,
788
+ tCrPackQ[None, None, None, q_consumer.index],
789
+ tCrKC[None, None, None, pack_k_consumer.index],
790
+ zero_init=True,
791
+ )
792
+ pair_score_pipe.producer_commit(pair_score_producer)
793
+ pair_score_producer.advance()
794
+ pack_k_pipe.consumer_release(pack_k_consumer)
795
+ pack_k_consumer.advance()
796
+
797
+ # BEGIN_RUNTIME_GROUP_BODY
798
+
799
+ # Route generation: four physical owner warps reduce the native N128
800
+ # score tile into one four-word mask. HBM receives only the diagnostic
801
+ # copy; the compacted exact stream remains resident in SMEM.
802
+ if is_owner:
803
+ pair_score_pipe.consumer_wait(pair_score_consumer)
804
+ score_raw, score_coords = _load_pair_score(
805
+ pack_score_template,
806
+ thr_pack_qk,
807
+ tmem_base,
808
+ Int32(PAIR_SCORE_OFFSET),
809
+ owner_tidx,
810
+ )
811
+ pack_score_loaded_barrier.arrive_and_wait()
812
+ pair_score_pipe.consumer_release(pair_score_consumer)
813
+ pair_score_consumer.advance()
814
+ owner_warp = owner_tidx // Int32(32)
815
+ lane = owner_tidx % Int32(32)
816
+ semantic_row = (
817
+ score_coords[0][0] + Int32(SEMANTIC_ROW_OFFSET)
818
+ ) & Int32(M - 1)
819
+ row_valid = semantic_row < q_len
820
+ lane_col_parity = (lane // Int32(2)) % Int32(2)
821
+ # Column-pair reduction: parity-0 lanes carry column 2*pair and
822
+ # parity-1 lanes carry column 2*pair+1. The XOR-1/16/8/4
823
+ # butterfly tree never crosses lane column-parity classes
824
+ # ((l^k)//2 keeps (l//2)%2 for k in {1,16,8,4}), so one tree
825
+ # reduces both columns at once; every surviving addition chain
826
+ # sees the same zero-padded operand streams, and the removed
827
+ # chains only ever accumulated 0.0. Writer lanes 0 and 2 equal
828
+ # 2*(col%2).
829
+ for pair_idx in cutlass.range_constexpr(
830
+ 0, ROUTE_TILE_SIZE // 2, 2
831
+ ):
832
+ my_col0 = Int32(2 * pair_idx) + lane_col_parity
833
+ partial0 = Float32(0.0)
834
+ if row_valid and my_col0 < valid_route_count:
835
+ partial0 = Float32(score_raw[pair_idx])
836
+ my_col1 = Int32(2 * (pair_idx + 1)) + lane_col_parity
837
+ partial1 = Float32(0.0)
838
+ if row_valid and my_col1 < valid_route_count:
839
+ partial1 = Float32(score_raw[pair_idx + 1])
840
+
841
+ raw_partial0 = partial0
842
+ raw_partial1 = partial1
843
+ scaled0, scaled1 = cute.arch.mul_packed_f32x2(
844
+ (raw_partial0, raw_partial1),
845
+ (softmax_scale_log2, softmax_scale_log2),
846
+ )
847
+ peer_scaled0 = cute.arch.shuffle_sync_bfly(
848
+ scaled0, offset=1
849
+ )
850
+ peer_scaled1 = cute.arch.shuffle_sync_bfly(
851
+ scaled1, offset=1
852
+ )
853
+ partial0, partial1 = cute.arch.fma_packed_f32x2(
854
+ (raw_partial0, raw_partial1),
855
+ (softmax_scale_log2, softmax_scale_log2),
856
+ (peer_scaled0, peer_scaled1),
857
+ )
858
+ peer0 = cute.arch.shuffle_sync_bfly(
859
+ partial0, offset=16
860
+ )
861
+ peer1 = cute.arch.shuffle_sync_bfly(
862
+ partial1, offset=16
863
+ )
864
+ partial0, partial1 = cute.arch.add_packed_f32x2(
865
+ (partial0, partial1), (peer0, peer1)
866
+ )
867
+ peer0 = cute.arch.shuffle_sync_bfly(
868
+ partial0, offset=8
869
+ )
870
+ peer1 = cute.arch.shuffle_sync_bfly(
871
+ partial1, offset=8
872
+ )
873
+ partial0, partial1 = cute.arch.add_packed_f32x2(
874
+ (partial0, partial1), (peer0, peer1)
875
+ )
876
+ peer0 = cute.arch.shuffle_sync_bfly(
877
+ partial0, offset=4
878
+ )
879
+ peer1 = cute.arch.shuffle_sync_bfly(
880
+ partial1, offset=4
881
+ )
882
+ partial0, partial1 = cute.arch.add_packed_f32x2(
883
+ (partial0, partial1), (peer0, peer1)
884
+ )
885
+ if lane == Int32(0):
886
+ route_partial[owner_warp, 2 * pair_idx] = partial0
887
+ route_partial[owner_warp, 2 * (pair_idx + 1)] = (
888
+ partial1
889
+ )
890
+ if lane == Int32(2):
891
+ route_partial[owner_warp, 2 * pair_idx + 1] = partial0
892
+ route_partial[
893
+ owner_warp, 2 * (pair_idx + 1) + 1
894
+ ] = partial1
895
+
896
+ cute.arch.fence_view_async_shared()
897
+ score_loaded_barrier.arrive_and_wait()
898
+ if owner_warp == Int32(0):
899
+ mask0 = Int32(0)
900
+ mask1 = Int32(0)
901
+ mask2 = Int32(0)
902
+ mask3 = Int32(0)
903
+
904
+ # Half 0 starts a fresh G256 stream and half 1 appends to
905
+ # lane 0's cumulative packet word. The preceding packet
906
+ # barrier makes the base warp-uniform before the vote.
907
+ append_base = Int32(0)
908
+ if half_idx != Int32(0):
909
+ append_base = Int32(route_packet[6])
910
+
911
+ # A positive signed shift avoids materializing 1<<31:
912
+ # lane 0 gets zero and lane 31 gets 0x7fffffff.
913
+ lane_mask_lt = Int32(0x7FFFFFFF) >> (
914
+ Int32(31) - lane
915
+ )
916
+ preceding_word_count = Int32(0)
917
+ for word in cutlass.range_constexpr(ROUTE_MASK_WORDS):
918
+ off = Int32(word * 32) + lane
919
+ valid = off < valid_route_count
920
+ exact_pred = False
921
+ if valid:
922
+ pair_02 = Float32(route_partial[0, off]) + Float32(
923
+ route_partial[2, off]
924
+ )
925
+ pair_13 = Float32(route_partial[1, off]) + Float32(
926
+ route_partial[3, off]
927
+ )
928
+ col_mean = (pair_02 + pair_13) / Float32(q_len)
929
+ exact_pred = sol_attn_route_is_exact(
930
+ q_block_idx,
931
+ route_start + off,
932
+ col_mean,
933
+ threshold,
934
+ valid,
935
+ )
936
+ # Sink is a KV-only contract. Text queries remain
937
+ # a caller-side dense operation in MMDiT models.
938
+ exact_pred = (
939
+ exact_pred
940
+ or (
941
+ route_start + off >= sink_start_block
942
+ and route_start + off < sink_end_block
943
+ )
944
+ )
945
+ word_mask = Int32(
946
+ cute.arch.vote_ballot_sync(exact_pred)
947
+ )
948
+ # Site 2: preserve the route decision and its four
949
+ # ordered ballots, but materialize the resulting
950
+ # approximate-column mask exactly once. Dedicated
951
+ # SMEM holds the two N64 mask halves so the reduction
952
+ # scratch remains non-aliasing for ptxas scheduling.
953
+ # The existing shared fence and owner barrier below
954
+ # publish them to every score owner.
955
+ if valid and not exact_pred:
956
+ column_masks[off] = Float32(0.0)
957
+ else:
958
+ column_masks[off] = -Float32.inf
959
+ lane_rank = (
960
+ append_base
961
+ + preceding_word_count
962
+ + sol_attn_popc_b32(word_mask & lane_mask_lt)
963
+ )
964
+ if exact_pred:
965
+ route_indices[lane_rank] = route_start + off
966
+ if cutlass.const_expr(word == 0):
967
+ mask0 = word_mask
968
+ elif cutlass.const_expr(word == 1):
969
+ mask1 = word_mask
970
+ elif cutlass.const_expr(word == 2):
971
+ mask2 = word_mask
972
+ else:
973
+ mask3 = word_mask
974
+ preceding_word_count = (
975
+ preceding_word_count
976
+ + sol_attn_popc_b32(word_mask)
977
+ )
978
+
979
+ # Every selected lane has a unique rank; lane 0 publishes
980
+ # the packet after reconvergence.
981
+ exact_count = preceding_word_count
982
+ if lane == Int32(0):
983
+ route_rank = append_base + exact_count
984
+
985
+ route_packet[0] = mask0
986
+ route_packet[1] = mask1
987
+ route_packet[2] = mask2
988
+ route_packet[3] = mask3
989
+ route_packet[4] = exact_count
990
+ route_packet[5] = append_base
991
+ route_packet[6] = route_rank
992
+ terminal_half_word = Int32(0)
993
+ if is_logical_terminal_half:
994
+ terminal_half_word = Int32(1)
995
+ route_packet[7] = terminal_half_word
996
+ cute.arch.fence_view_async_shared()
997
+
998
+ # The selector packet is now immutable. Reuse the already resident
999
+ # route scores for the non-exact transaction; no offset list or second
1000
+ # route-score load is introduced.
1001
+ score_loaded_barrier.arrive_and_wait()
1002
+ route_exact_count = Int32(route_packet[4])
1003
+ has_route_approx = route_exact_count < valid_route_count
1004
+ if has_route_approx:
1005
+ row_mask = -Float32.inf
1006
+ if row_valid:
1007
+ row_mask = Float32(0.0)
1008
+ # Route generation has consumed every raw score. Apply
1009
+ # the shared mask in place so raw and masked N128
1010
+ # fragments never overlap in registers; the same object
1011
+ # remains available for the later route-mass scratch.
1012
+ route_scores = score_raw
1013
+ assert cute.size(score_raw) % 2 == 0
1014
+ for i in cutlass.range_constexpr(
1015
+ 0, cute.size(score_raw), 2
1016
+ ):
1017
+ group_col0 = score_coords[i][1]
1018
+ group_col1 = score_coords[i + 1][1]
1019
+ mask0 = Float32(column_masks[group_col0])
1020
+ mask1 = Float32(column_masks[group_col1])
1021
+ mask0, mask1 = cute.arch.add_packed_f32x2(
1022
+ (mask0, mask1), (row_mask, row_mask)
1023
+ )
1024
+ mask0, mask1 = cute.arch.add_packed_f32x2(
1025
+ (
1026
+ Float32(score_raw[i]),
1027
+ Float32(score_raw[i + 1]),
1028
+ ),
1029
+ (mask0, mask1),
1030
+ )
1031
+ route_scores[i] = mask0
1032
+ route_scores[i + 1] = mask1
1033
+
1034
+ local_max = fa_utils.fmax_reduce(
1035
+ route_scores.load(), arch=100
1036
+ )
1037
+ local_max = Float32(local_max) * softmax_scale
1038
+ peer_max = cute.arch.shuffle_sync_bfly(local_max, offset=2)
1039
+ pair_max = local_max
1040
+ if peer_max > pair_max:
1041
+ pair_max = peer_max
1042
+
1043
+ old_max = running_max
1044
+ old_sum = running_sum
1045
+ new_max = old_max
1046
+ if old_max == -Float32.inf or pair_max > old_max:
1047
+ new_max = pair_max
1048
+ row_alpha = Float32(0.0)
1049
+ if old_max != -Float32.inf:
1050
+ row_alpha = cute.math.exp2(
1051
+ (old_max - new_max) * Float32(LOG2E),
1052
+ fastmath=True,
1053
+ )
1054
+
1055
+ route_probabilities = cute.make_rmem_tensor(
1056
+ route_scores.shape, Float32
1057
+ )
1058
+ if new_max == -Float32.inf:
1059
+ for i in cutlass.range(
1060
+ cute.size(route_scores), unroll_full=True
1061
+ ):
1062
+ route_probabilities[i] = Float32(0.0)
1063
+ else:
1064
+ for i in cutlass.range(
1065
+ cute.size(route_scores), unroll_full=True
1066
+ ):
1067
+ route_probabilities[i] = cute.math.exp2(
1068
+ Float32(route_scores[i]) * softmax_scale_log2
1069
+ - new_max * Float32(LOG2E),
1070
+ fastmath=True,
1071
+ )
1072
+ # ``route_scores`` is dead after the exponentials above. Use
1073
+ # it as mass scratch so the compiler does not need a second
1074
+ # full N128-shaped fragment while probabilities remain live
1075
+ # for the chunked TMEM-P store below. Keeping the same shape,
1076
+ # index order, and fadd_reduce preserves floating-point
1077
+ # reduction order and every phase edge.
1078
+ assert cute.size(route_probabilities) % 2 == 0
1079
+ for i in cutlass.range_constexpr(
1080
+ 0, cute.size(route_probabilities), 2
1081
+ ):
1082
+ block_idx0 = route_start + score_coords[i][1]
1083
+ raw_length0 = (
1084
+ token_count - block_idx0 * Int32(N_MEMBER)
1085
+ )
1086
+ block_length0 = max(
1087
+ Int32(0), min(raw_length0, Int32(N_MEMBER))
1088
+ )
1089
+ block_idx1 = route_start + score_coords[i + 1][1]
1090
+ raw_length1 = (
1091
+ token_count - block_idx1 * Int32(N_MEMBER)
1092
+ )
1093
+ block_length1 = max(
1094
+ Int32(0), min(raw_length1, Int32(N_MEMBER))
1095
+ )
1096
+ mass0, mass1 = cute.arch.mul_packed_f32x2(
1097
+ (
1098
+ Float32(route_probabilities[i]),
1099
+ Float32(route_probabilities[i + 1]),
1100
+ ),
1101
+ (
1102
+ Float32(block_length0),
1103
+ Float32(block_length1),
1104
+ ),
1105
+ )
1106
+ route_scores[i] = mass0
1107
+ route_scores[i + 1] = mass1
1108
+ current_sum = fa_utils.fadd_reduce(
1109
+ route_scores.load(), arch=100
1110
+ )
1111
+ current_sum += cute.arch.shuffle_sync_bfly(
1112
+ current_sum, offset=2
1113
+ )
1114
+ # KC is a block mean and VC a valid-token sum. Route mass uses
1115
+ # the true block length while PV still consumes p*VC once.
1116
+ running_sum = old_sum * row_alpha + current_sum
1117
+ running_max = new_max
1118
+ if owner_o_initialized != Int32(0):
1119
+ _rescale_pair_o(
1120
+ pack_o_template,
1121
+ thr_pack_pv,
1122
+ tmem_base,
1123
+ Int32(O_OFFSET),
1124
+ owner_tidx,
1125
+ row_alpha,
1126
+ )
1127
+ _store_pair_probability_chunked_tmemp(
1128
+ pack_o_template,
1129
+ route_probabilities,
1130
+ tmem_base,
1131
+ Int32(PAIR_P_OFFSET),
1132
+ owner_tidx,
1133
+ )
1134
+ owner_o_initialized = Int32(1)
1135
+ # Publish the mask/P decision to warp 0. The route PV is deliberately
1136
+ # drained before exact work so all-exact, all-approx, odd, and
1137
+ # partial-tail paths share one phase boundary.
1138
+ if is_score_consumer:
1139
+ route_packet_ready_barrier.arrive_and_wait()
1140
+ if warp_idx == Int32(0):
1141
+ route_exact_count = Int32(route_packet[4])
1142
+ route_has_approx = route_exact_count < valid_route_count
1143
+ pack_v_pipe.consumer_wait(pack_v_consumer)
1144
+ if route_has_approx:
1145
+ mma_utils.gemm(
1146
+ tiled_pack_pv,
1147
+ pair_tO,
1148
+ pair_tP,
1149
+ tCrVC[None, None, None, pack_v_consumer.index],
1150
+ zero_init=mma_o_initialized == Int32(0),
1151
+ )
1152
+ # Half 0 is followed by half-1 route QK. The terminal
1153
+ # route half is followed by exact QK0 whenever the fused
1154
+ # G256 index stream is nonempty. Those score completions
1155
+ # prove this PV complete; only a final route-only CTA needs
1156
+ # an explicit O completion here.
1157
+ if (
1158
+ is_final_route_tile
1159
+ and Int32(route_packet[6]) == Int32(0)
1160
+ ):
1161
+ pair_o_pipe.producer_commit(pair_o_producer)
1162
+ mma_o_initialized = Int32(1)
1163
+ pack_v_pipe.consumer_release(pack_v_consumer)
1164
+ pack_v_consumer.advance()
1165
+ if is_owner:
1166
+ cumulative_exact_count = Int32(route_packet[6])
1167
+ if (
1168
+ is_final_route_tile
1169
+ and cumulative_exact_count == Int32(0)
1170
+ ):
1171
+ pair_o_pipe.consumer_wait(pair_o_consumer)
1172
+
1173
+ # route_packet may be reused by the next physical half without a
1174
+ # CTA join. Warp 0 reads this half's packet before it can issue
1175
+ # next-half QK; owner-warp 0 cannot overwrite the packet until
1176
+ # that QK's pair-score completion has released all owners.
1177
+
1178
+ # Both route halves have published their packet/index data and drained
1179
+ # approximate PV. This is the only CTA-wide pre-exact join in the
1180
+ # logical G256 group; it publishes the combined list to warp 5.
1181
+ cute.arch.barrier()
1182
+ # The cumulative count covers half 0 followed by half 1. Pairing this
1183
+ # one ordered stream removes cross-half odd padding without retaining
1184
+ # either physical score fragment.
1185
+ exact_block_count = Int32(route_packet[6])
1186
+ exact_pair_count = (exact_block_count + Int32(1)) // Int32(2)
1187
+ pair_count = exact_pair_count
1188
+ has_pair_exact = exact_block_count > Int32(0)
1189
+
1190
+ # BEGIN_GENERAL_N128_PAIR
1191
+ # Every executable exact count, including a logical-group terminal
1192
+ # exact1, stays in the N128 domain.
1193
+
1194
+ # Warp 5 streams one physical N128 K stage and one physical N128 V
1195
+ # stage. A missing odd peer duplicates block0 only for the physical
1196
+ # transaction; owners mask all upper-64 scores before softmax.
1197
+ if warp_idx == Int32(5) and has_pair_exact:
1198
+ for pair_idx in cutlass.range(pair_count, unroll=1):
1199
+ ordinal0 = pair_idx * Int32(2)
1200
+ block0 = Int32(route_indices[ordinal0])
1201
+ block1 = block0
1202
+ if ordinal0 + Int32(1) < exact_block_count:
1203
+ block1 = Int32(route_indices[ordinal0 + Int32(1)])
1204
+
1205
+ pack_k_pipe.producer_acquire(pack_k_producer)
1206
+ pair_k_barrier = pack_k_pipe.producer_get_barrier(
1207
+ pack_k_producer
1208
+ )
1209
+ _load_pack_k_half(
1210
+ tma_atom_pack_k,
1211
+ tPackKgK,
1212
+ tPackKsK,
1213
+ block0,
1214
+ block1,
1215
+ pack_k_producer.index * Int32(4),
1216
+ pair_k_barrier,
1217
+ )
1218
+ pack_k_producer.advance()
1219
+
1220
+ pack_v_pipe.producer_acquire(pack_v_producer)
1221
+ pair_v_barrier = pack_v_pipe.producer_get_barrier(
1222
+ pack_v_producer
1223
+ )
1224
+ _load_pack_v_half(
1225
+ tma_atom_pack_v,
1226
+ tPackVgV,
1227
+ tPackVsV,
1228
+ block0,
1229
+ block1,
1230
+ pack_v_producer.index * Int32(4),
1231
+ pair_v_barrier,
1232
+ )
1233
+ pack_v_producer.advance()
1234
+
1235
+ if warp_idx == Int32(0) and has_pair_exact:
1236
+ # QK0 prologue. K and score cursors advance exactly once per QK;
1237
+ # neither V nor O state is touched until the steady-state PV path.
1238
+ pack_k_pipe.consumer_wait(pack_k_consumer)
1239
+ pair_score_pipe.producer_acquire(pair_score_producer)
1240
+ mma_utils.gemm(
1241
+ tiled_pack_qk,
1242
+ pair_tScore,
1243
+ tCrPackQ[None, None, None, q_consumer.index],
1244
+ tCrPackK[None, None, None, pack_k_consumer.index],
1245
+ zero_init=True,
1246
+ )
1247
+ pair_score_pipe.producer_commit(pair_score_producer)
1248
+ pair_score_producer.advance()
1249
+ # PipelineTmaUmma release is tcgen05-completion-backed.
1250
+ pack_k_pipe.consumer_release(pack_k_consumer)
1251
+ pack_k_consumer.advance()
1252
+
1253
+ for pair_idx in cutlass.range(pair_count, unroll=1):
1254
+ # P aliases the drained upper half of S. PV must therefore be
1255
+ # issued before QK(i+1) overwrites S. Both instructions are
1256
+ # emitted back-to-back by warp 0, retaining the full-G128
1257
+ # tcgen05 dependency order without its K/V alias barriers.
1258
+ pack_v_pipe.consumer_wait(pack_v_consumer)
1259
+ # All four owners have completed their synchronous chunked
1260
+ # TMEM stores and the helper's TMEM store fence before this
1261
+ # five-warp rendezvous releases the single MMA warp.
1262
+ exact_pair_p_ready_barrier.arrive_and_wait()
1263
+ mma_utils.gemm(
1264
+ tiled_pack_pv,
1265
+ pair_tO,
1266
+ pair_tP,
1267
+ tCrPackV[None, None, None, pack_v_consumer.index],
1268
+ zero_init=mma_o_initialized == Int32(0),
1269
+ )
1270
+ # QK(i+1) completion dominates PV(i) completion for every
1271
+ # nonterminal transaction on this tcgen05 issuer. Commit one
1272
+ # explicit O-full generation only for the CTA's final PV.
1273
+ if (
1274
+ is_final_logical_group
1275
+ and pair_idx + Int32(1) == pair_count
1276
+ ):
1277
+ pair_o_pipe.producer_commit(pair_o_producer)
1278
+ mma_o_initialized = Int32(1)
1279
+ pack_v_pipe.consumer_release(pack_v_consumer)
1280
+ pack_v_consumer.advance()
1281
+
1282
+ if pair_idx + Int32(1) < pair_count:
1283
+ pack_k_pipe.consumer_wait(pack_k_consumer)
1284
+ pair_score_pipe.producer_acquire(pair_score_producer)
1285
+ mma_utils.gemm(
1286
+ tiled_pack_qk,
1287
+ pair_tScore,
1288
+ tCrPackQ[None, None, None, q_consumer.index],
1289
+ tCrPackK[
1290
+ None, None, None, pack_k_consumer.index
1291
+ ],
1292
+ zero_init=True,
1293
+ )
1294
+ pair_score_pipe.producer_commit(pair_score_producer)
1295
+ pair_score_producer.advance()
1296
+ pack_k_pipe.consumer_release(pack_k_consumer)
1297
+ pack_k_consumer.advance()
1298
+
1299
+ if is_owner and has_pair_exact:
1300
+ exact_owner_warp = owner_tidx // Int32(32)
1301
+ exact_lane = owner_tidx % Int32(32)
1302
+ for pair_idx in cutlass.range(pair_count, unroll=1):
1303
+ ordinal0 = pair_idx * Int32(2)
1304
+ block0 = Int32(route_indices[ordinal0])
1305
+ has_peer = ordinal0 + Int32(1) < exact_block_count
1306
+ block1 = block0
1307
+ if has_peer:
1308
+ block1 = Int32(route_indices[ordinal0 + Int32(1)])
1309
+ valid0 = token_count - block0 * Int32(N_MEMBER)
1310
+ valid1 = Int32(0)
1311
+ if has_peer:
1312
+ valid1 = token_count - block1 * Int32(N_MEMBER)
1313
+ # Keep packed-select integer min/max lowering and exact-pair
1314
+ # bookkeeping unchanged.
1315
+ valid0 = max(Int32(0), min(valid0, Int32(N_MEMBER)))
1316
+ valid1 = max(Int32(0), min(valid1, Int32(N_MEMBER)))
1317
+
1318
+ # Site 1: owner warp 0 builds two 64-column gates once for
1319
+ # this exact N128 pair. The existing score-load barrier below
1320
+ # both protects the S/P alias and publishes these stores; no
1321
+ # barrier or shared allocation is added.
1322
+ if exact_owner_warp == Int32(0):
1323
+ for cohort in cutlass.range_constexpr(4):
1324
+ column = Int32(cohort * 32) + exact_lane
1325
+ if cutlass.const_expr(cohort < 2):
1326
+ if column >= valid0:
1327
+ column_masks[column] = -Float32.inf
1328
+ else:
1329
+ column_masks[column] = Float32(0.0)
1330
+ else:
1331
+ if column - Int32(N_MEMBER) >= valid1:
1332
+ column_masks[column] = -Float32.inf
1333
+ else:
1334
+ column_masks[column] = Float32(0.0)
1335
+ cute.arch.fence_view_async_shared()
1336
+
1337
+ pair_score_pipe.consumer_wait(pair_score_consumer)
1338
+ # Keep the exact ae9 score-load helper and fragment scope.
1339
+ pair_scores, pair_coords = _load_pair_score(
1340
+ pack_score_template,
1341
+ thr_pack_qk,
1342
+ tmem_base,
1343
+ Int32(PAIR_SCORE_OFFSET),
1344
+ owner_tidx,
1345
+ )
1346
+ # Every owner retires the complete score load before the
1347
+ # packed P store aliases columns 64..127 of S.
1348
+ pack_score_loaded_barrier.arrive_and_wait()
1349
+ pair_score_pipe.consumer_release(pair_score_consumer)
1350
+ pair_score_consumer.advance()
1351
+
1352
+ semantic_row = (
1353
+ pair_coords[0][0] + Int32(SEMANTIC_ROW_OFFSET)
1354
+ ) & Int32(M - 1)
1355
+ row_valid = semantic_row < q_len
1356
+ row_mask = -Float32.inf
1357
+ if row_valid:
1358
+ row_mask = Float32(0.0)
1359
+ assert cute.size(pair_scores) % 2 == 0
1360
+ for i in cutlass.range_constexpr(
1361
+ 0, cute.size(pair_scores), 2
1362
+ ):
1363
+ column0 = pair_coords[i][1]
1364
+ column1 = pair_coords[i + 1][1]
1365
+ mask0 = Float32(column_masks[column0])
1366
+ mask1 = Float32(column_masks[column1])
1367
+ mask0, mask1 = cute.arch.add_packed_f32x2(
1368
+ (mask0, mask1), (row_mask, row_mask)
1369
+ )
1370
+ mask0, mask1 = cute.arch.add_packed_f32x2(
1371
+ (
1372
+ Float32(pair_scores[i]),
1373
+ Float32(pair_scores[i + 1]),
1374
+ ),
1375
+ (mask0, mask1),
1376
+ )
1377
+ pair_scores[i] = mask0
1378
+ pair_scores[i + 1] = mask1
1379
+
1380
+ probabilities, next_max, next_sum, row_alpha = (
1381
+ _online_update_pair(
1382
+ pair_scores,
1383
+ running_max,
1384
+ running_sum,
1385
+ softmax_scale,
1386
+ )
1387
+ )
1388
+ # For i>0, pair-score completion comes from QK(i), issued
1389
+ # after PV(i-1) on the same tcgen05 issuer. The score wait and
1390
+ # load above therefore retire PV(i-1) before this O rescale.
1391
+ # Pair0 similarly follows either route QK or route PV->QK0.
1392
+ if owner_o_initialized != Int32(0):
1393
+ _rescale_pair_o(
1394
+ pack_o_template,
1395
+ thr_pack_pv,
1396
+ tmem_base,
1397
+ Int32(O_OFFSET),
1398
+ owner_tidx,
1399
+ row_alpha,
1400
+ )
1401
+ # The one TMEM P image is free once PV(i-1) completes. Keep
1402
+ # probabilities FP32 until the live-range-bounded chunked R2T.
1403
+ _store_pair_probability_chunked_tmemp(
1404
+ pack_o_template,
1405
+ probabilities,
1406
+ tmem_base,
1407
+ Int32(PAIR_P_OFFSET),
1408
+ owner_tidx,
1409
+ )
1410
+ # The preceding helper performs tcgen05.wait::st for every
1411
+ # chunk and a TMEM-store fence. Publish P to warp 0 with one
1412
+ # uniform generation shared by warps 0-4; warp 5 is excluded.
1413
+ exact_pair_p_ready_barrier.arrive_and_wait()
1414
+ running_max = next_max
1415
+ running_sum = next_sum
1416
+ owner_o_initialized = Int32(1)
1417
+
1418
+ # There is no successor QK after the CTA's final exact PV. Keep
1419
+ # exactly one completion-backed wait before the epilogue; all
1420
+ # earlier groups flow into a successor route QK completion.
1421
+ if is_final_logical_group and pair_count > Int32(0):
1422
+ pair_o_pipe.consumer_wait(pair_o_consumer)
1423
+
1424
+ # route_indices reuse HB proof for the next logical group:
1425
+ # (1) warp 5 reads both indices before producing each pair's K/V, and
1426
+ # final-pair score completion therefore dominates its last read;
1427
+ # (2) all owner index reads precede the final exact-P NamedBarrier;
1428
+ # (3) owner-warp0/lane0 is the sole next-group writer and reaches it
1429
+ # only after that same exact loop. For exact_count==0 there are no
1430
+ # readers. Therefore no group-tail CTA barrier is required.
1431
+
1432
+ # Cross-group progress is carried by the existing K/V buffer-free
1433
+ # phases and pair-score ready phase. There is no CTA-wide group-tail
1434
+ # join: the next producer acquire cannot overwrite a live K/V stage,
1435
+ # and the next owner score load cannot precede QK completion.
1436
+ # END_GENERAL_N128_PAIR
1437
+
1438
+ logical_group_idx = cute.arch.make_warp_uniform(
1439
+ logical_group_idx + Int32(1)
1440
+ )
1441
+ remaining_group_tiles = cute.arch.make_warp_uniform(
1442
+ remaining_group_tiles - Int32(ROUTE_HALVES_PER_GROUP)
1443
+ )
1444
+ # END_RUNTIME_GROUP_BODY
1445
+ # END_G256_CURSOR_UNIFORM_INDUCTION
1446
+
1447
+ if warp_idx == Int32(0):
1448
+ q_pipe.consumer_release(q_consumer)
1449
+ q_consumer.advance()
1450
+
1451
+ if is_owner:
1452
+ lane = owner_tidx % Int32(32)
1453
+ owner_warp = owner_tidx // Int32(32)
1454
+ owner_row = (
1455
+ owner_warp * Int32(16)
1456
+ + lane // Int32(4)
1457
+ + (lane % Int32(2)) * Int32(8)
1458
+ + Int32(SEMANTIC_ROW_OFFSET)
1459
+ ) & Int32(M - 1)
1460
+ # Register state remains owner-local for the entire exact stream. It
1461
+ # is published only once here because the final Ld16x256b epilogue
1462
+ # remaps rows differently from the Ld16x64b xor-2 score ownership.
1463
+ if (lane & Int32(2)) == Int32(0):
1464
+ sFinalStats[owner_row, 0] = running_sum
1465
+ sFinalStats[owner_row, 1] = running_max
1466
+ cute.arch.fence_view_async_shared()
1467
+ final_stats_ready_barrier.arrive_and_wait()
1468
+
1469
+ o_regs, o_coords = load_m64_o_fp32_256b(
1470
+ pack_o_template,
1471
+ thr_pack_pv,
1472
+ tmem_base,
1473
+ owner_tidx,
1474
+ )
1475
+ assert cute.size(o_regs) == 64
1476
+ assert cute.size(o_coords) == 64
1477
+
1478
+ # B7's device inversion proves that 4*w/4*w+1 belong to one
1479
+ # semantic row and 4*w+2/4*w+3 to its row-plus-eight peer. Hoist
1480
+ # validity, final-sum LDS, reciprocal, and row base once per stratum.
1481
+ semantic_row0 = (
1482
+ owner_warp * Int32(16)
1483
+ + lane // Int32(4)
1484
+ + Int32(SEMANTIC_ROW_OFFSET)
1485
+ ) & Int32(M - 1)
1486
+ semantic_row1 = (semantic_row0 + Int32(8)) & Int32(M - 1)
1487
+ even_col_base = (lane % Int32(4)) * Int32(2)
1488
+
1489
+ if semantic_row0 < q_len:
1490
+ inv_sum0 = cute.arch.rcp_approx(
1491
+ Float32(sFinalStats[semantic_row0, 0])
1492
+ )
1493
+ query_idx0 = q_block_idx * Int32(M) + semantic_row0
1494
+ destination_row0 = cute.domain_offset(
1495
+ (batch_idx, query_idx0, head_idx, Int32(0)), mO_bthd
1496
+ )
1497
+ for word_i in cutlass.range(
1498
+ O_PACKED_WORDS_PER_ROW_PER_THREAD, unroll_full=True
1499
+ ):
1500
+ even_i = word_i * 4
1501
+ odd_i = even_i + 1
1502
+ even_value = Float32(o_regs[even_i]) * inv_sum0
1503
+ odd_value = Float32(o_regs[odd_i]) * inv_sum0
1504
+ packed_word = _cvt_bf16x2_f32(
1505
+ Float32(odd_value), Float32(even_value)
1506
+ )
1507
+ even_col = (
1508
+ even_col_base + word_i * O_PACKED_COLUMN_STRIDE
1509
+ )
1510
+ _store_global_u32_inline(
1511
+ destination_row0.iterator + even_col, packed_word
1512
+ )
1513
+
1514
+ if semantic_row1 < q_len:
1515
+ inv_sum1 = cute.arch.rcp_approx(
1516
+ Float32(sFinalStats[semantic_row1, 0])
1517
+ )
1518
+ query_idx1 = q_block_idx * Int32(M) + semantic_row1
1519
+ destination_row1 = cute.domain_offset(
1520
+ (batch_idx, query_idx1, head_idx, Int32(0)), mO_bthd
1521
+ )
1522
+ for word_i in cutlass.range(
1523
+ O_PACKED_WORDS_PER_ROW_PER_THREAD, unroll_full=True
1524
+ ):
1525
+ even_i = word_i * 4 + 2
1526
+ odd_i = even_i + 1
1527
+ even_value = Float32(o_regs[even_i]) * inv_sum1
1528
+ odd_value = Float32(o_regs[odd_i]) * inv_sum1
1529
+ packed_word = _cvt_bf16x2_f32(
1530
+ Float32(odd_value), Float32(even_value)
1531
+ )
1532
+ even_col = (
1533
+ even_col_base + word_i * O_PACKED_COLUMN_STRIDE
1534
+ )
1535
+ _store_global_u32_inline(
1536
+ destination_row1.iterator + even_col, packed_word
1537
+ )
1538
+
1539
+ if (lane & Int32(2)) == Int32(0) and owner_row < q_len:
1540
+ query_idx = q_block_idx * Int32(M) + owner_row
1541
+ mLSE_bth[batch_idx, query_idx, head_idx] = (
1542
+ running_max
1543
+ + cute.math.log2(running_sum, fastmath=True) * Float32(LN2)
1544
+ )
1545
+
1546
+ cute.arch.barrier()
1547
+ tmem.free(tmem_ptr)
1548
+
1549
+
1550
+ @cute.jit
1551
+ def _sol_attn_sm100_bf16_host(
1552
+ q: cute.Tensor,
1553
+ k: cute.Tensor,
1554
+ v: cute.Tensor,
1555
+ o: cute.Tensor,
1556
+ kc: cute.Tensor,
1557
+ vc: cute.Tensor,
1558
+ threshold: cute.Tensor,
1559
+ lse: cute.Tensor,
1560
+ softmax_scale: Float32,
1561
+ sink_start_block: Int32,
1562
+ sink_end_block: Int32,
1563
+ stream: cuda.CUstream = None,
1564
+ ):
1565
+ q, k, v, o, kc, vc = tuple(
1566
+ assume_tensor_aligned(t) for t in (q, k, v, o, kc, vc)
1567
+ )
1568
+ q_mkl, k_nkl, kc_nkl = [
1569
+ layout_utils.select(t, [1, 3, 2, 0]) for t in (q, k, kc)
1570
+ ]
1571
+ v_nkl, vc_nkl = [
1572
+ layout_utils.select(t, [3, 1, 2, 0]) for t in (v, vc)
1573
+ ]
1574
+ token_count = cute.size(q_mkl.shape[0])
1575
+ num_blocks = cute.size(kc_nkl.shape[0])
1576
+ num_heads = cute.size(q_mkl.shape[2])
1577
+ num_batches = cute.size(q_mkl.shape[3])
1578
+ num_route_tiles = cute.ceil_div(num_blocks, ROUTE_TILE_SIZE)
1579
+ pack_qk_op = tcgen05.MmaF16BF16Op(
1580
+ BFloat16,
1581
+ Float32,
1582
+ PACK_QK_INST,
1583
+ tcgen05.CtaGroup.ONE,
1584
+ tcgen05.OperandSource.SMEM,
1585
+ cute.nvgpu.OperandMajorMode.K,
1586
+ cute.nvgpu.OperandMajorMode.K,
1587
+ )
1588
+ tiled_pack_qk = cute.make_tiled_mma(pack_qk_op)
1589
+ pack_pv_op = tcgen05.MmaF16BF16Op(
1590
+ BFloat16,
1591
+ Float32,
1592
+ PACK_PV_INST,
1593
+ tcgen05.CtaGroup.ONE,
1594
+ tcgen05.OperandSource.TMEM,
1595
+ cute.nvgpu.OperandMajorMode.K,
1596
+ cute.nvgpu.OperandMajorMode.MN,
1597
+ )
1598
+ tiled_pack_pv = cute.make_tiled_mma(pack_pv_op)
1599
+ pack_qk_quarter_op = tcgen05.MmaF16BF16Op(
1600
+ BFloat16,
1601
+ Float32,
1602
+ PACK_QK_QUARTER_INST,
1603
+ tcgen05.CtaGroup.ONE,
1604
+ tcgen05.OperandSource.SMEM,
1605
+ cute.nvgpu.OperandMajorMode.K,
1606
+ cute.nvgpu.OperandMajorMode.K,
1607
+ )
1608
+ tiled_pack_qk_gather = cute.make_tiled_mma(pack_qk_quarter_op)
1609
+ pack_pv_quarter_op = tcgen05.MmaF16BF16Op(
1610
+ BFloat16,
1611
+ Float32,
1612
+ PACK_PV_QUARTER_INST,
1613
+ tcgen05.CtaGroup.ONE,
1614
+ tcgen05.OperandSource.TMEM,
1615
+ cute.nvgpu.OperandMajorMode.K,
1616
+ cute.nvgpu.OperandMajorMode.MN,
1617
+ )
1618
+ tiled_pack_pv_gather = cute.make_tiled_mma(pack_pv_quarter_op)
1619
+ q_layout = sm100_utils.make_smem_layout_a(
1620
+ tiled_pack_qk, PACK_QK_TILE, BFloat16, 1
1621
+ )
1622
+ pack_k_layout = sm100_utils.make_smem_layout_b(
1623
+ tiled_pack_qk, PACK_QK_TILE, BFloat16, PAIR_STAGES
1624
+ )
1625
+ pack_v_layout = sm100_utils.make_smem_layout_b(
1626
+ tiled_pack_pv, PACK_PV_TILE, BFloat16, PAIR_STAGES
1627
+ )
1628
+ pack_k_gather_layout = sm100_utils.make_smem_layout_b(
1629
+ tiled_pack_qk_gather,
1630
+ PACK_QK_GATHER_TILE,
1631
+ BFloat16,
1632
+ PAIR_STAGES * 4,
1633
+ )
1634
+ pack_v_gather_layout = sm100_utils.make_smem_layout_b(
1635
+ tiled_pack_pv_gather,
1636
+ PACK_PV_GATHER_TILE,
1637
+ BFloat16,
1638
+ PAIR_STAGES * 4,
1639
+ )
1640
+ pack_p_layout = sm100_utils.make_smem_layout_a(
1641
+ tiled_pack_pv, PACK_PV_TILE, BFloat16, 1
1642
+ )
1643
+ route_k_layout = sm100_utils.make_smem_layout_b(
1644
+ tiled_pack_qk, PACK_QK_TILE, BFloat16, PAIR_STAGES
1645
+ )
1646
+ route_v_layout = sm100_utils.make_smem_layout_b(
1647
+ tiled_pack_pv, PACK_PV_TILE, BFloat16, PAIR_STAGES
1648
+ )
1649
+ copy_op = cpasync.CopyBulkTensorTileG2SOp(tcgen05.CtaGroup.ONE)
1650
+ q_tma_atom, q_tma_tensor = cute.nvgpu.make_tiled_tma_atom_A(
1651
+ copy_op,
1652
+ q_mkl,
1653
+ cute.select(q_layout, mode=[0, 1, 2]),
1654
+ PACK_QK_TILE,
1655
+ tiled_pack_qk,
1656
+ )
1657
+ pack_k_tma_layout = cute.make_composed_layout(
1658
+ pack_k_gather_layout.inner,
1659
+ 0,
1660
+ cute.make_layout((64, 64), stride=(64, 1)),
1661
+ )
1662
+ pack_k_tma_atom, pack_k_tma_tensor = cpasync.make_tiled_tma_atom(
1663
+ copy_op,
1664
+ k_nkl,
1665
+ pack_k_tma_layout,
1666
+ (64, 64),
1667
+ )
1668
+ pack_v_tma_layout = cute.make_composed_layout(
1669
+ pack_v_gather_layout.inner,
1670
+ 0,
1671
+ cute.make_layout((64, 64), stride=(1, 64)),
1672
+ )
1673
+ pack_v_tma_atom, pack_v_tma_tensor = cpasync.make_tiled_tma_atom(
1674
+ copy_op,
1675
+ v_nkl,
1676
+ pack_v_tma_layout,
1677
+ (64, 64),
1678
+ )
1679
+ kc_tma_atom, kc_tma_tensor = cute.nvgpu.make_tiled_tma_atom_B(
1680
+ copy_op,
1681
+ kc_nkl,
1682
+ cute.select(route_k_layout, mode=[0, 1, 2]),
1683
+ PACK_QK_TILE,
1684
+ tiled_pack_qk,
1685
+ )
1686
+ vc_tma_atom, vc_tma_tensor = cute.nvgpu.make_tiled_tma_atom_B(
1687
+ copy_op,
1688
+ vc_nkl,
1689
+ cute.select(route_v_layout, mode=[0, 1, 2]),
1690
+ PACK_PV_TILE,
1691
+ tiled_pack_pv,
1692
+ )
1693
+ _sol_attn_sm100_bf16_kernel(
1694
+ tiled_pack_qk,
1695
+ tiled_pack_pv,
1696
+ q_tma_atom,
1697
+ q_tma_tensor,
1698
+ pack_k_tma_atom,
1699
+ pack_k_tma_tensor,
1700
+ pack_v_tma_atom,
1701
+ pack_v_tma_tensor,
1702
+ kc_tma_atom,
1703
+ kc_tma_tensor,
1704
+ vc_tma_atom,
1705
+ vc_tma_tensor,
1706
+ threshold,
1707
+ o,
1708
+ lse,
1709
+ Int32(token_count),
1710
+ Int32(num_blocks),
1711
+ Int32(num_route_tiles),
1712
+ softmax_scale,
1713
+ sink_start_block,
1714
+ sink_end_block,
1715
+ q_layout,
1716
+ pack_k_layout,
1717
+ pack_k_gather_layout,
1718
+ pack_p_layout,
1719
+ pack_v_layout,
1720
+ pack_v_gather_layout,
1721
+ route_k_layout,
1722
+ route_v_layout,
1723
+ ).launch(
1724
+ grid=(num_blocks, num_heads, num_batches),
1725
+ block=(THREADS, 1, 1),
1726
+ stream=stream,
1727
+ min_blocks_per_mp=2,
1728
+ )
1729
+
1730
+
1731
+ @cute.jit
1732
+ def forward(
1733
+ q: cute.Tensor,
1734
+ k: cute.Tensor,
1735
+ v: cute.Tensor,
1736
+ o: cute.Tensor,
1737
+ kc: cute.Tensor,
1738
+ vc: cute.Tensor,
1739
+ threshold: cute.Tensor,
1740
+ lse: cute.Tensor,
1741
+ softmax_scale: Float32,
1742
+ sink_start_block: Int32,
1743
+ sink_end_block: Int32,
1744
+ stream: cuda.CUstream = None,
1745
+ ):
1746
+ return _sol_attn_sm100_bf16_host(
1747
+ q,
1748
+ k,
1749
+ v,
1750
+ o,
1751
+ kc,
1752
+ vc,
1753
+ threshold,
1754
+ lse,
1755
+ softmax_scale,
1756
+ sink_start_block,
1757
+ sink_end_block,
1758
+ stream,
1759
+ )
1760
+
1761
+
1762
+ __all__ = ["forward"]
torch-ext/sol_attn/sm100/math.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Small tensor-core helpers used by the Blackwell mainloop."""
2
+
3
+ import cutlass
4
+ import cutlass.cute as cute
5
+ from cutlass import Boolean
6
+ from cutlass.cute.nvgpu import tcgen05
7
+
8
+
9
+ @cute.jit
10
+ def gemm(
11
+ tiled_mma: cute.TiledMma,
12
+ accumulator: cute.Tensor,
13
+ a: cute.Tensor,
14
+ b: cute.Tensor,
15
+ zero_init: bool | Boolean = False,
16
+ ) -> None:
17
+ mma = cute.make_mma_atom(tiled_mma.op)
18
+ for k in cutlass.range_constexpr(cute.size(a.shape[2])):
19
+ mma.set(tcgen05.Field.ACCUMULATE, not zero_init or k != 0)
20
+ cute.gemm(
21
+ mma,
22
+ accumulator,
23
+ a[None, None, k],
24
+ b[None, None, k],
25
+ accumulator,
26
+ )
27
+
28
+
29
+ __all__ = ["gemm"]
torch-ext/sol_attn/sm100/softmax.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Online-softmax helpers for the Blackwell mainloop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import cutlass
6
+ import cutlass.cute as cute
7
+ from cutlass import Float32, Int32
8
+ from cutlass.cute.nvgpu import tcgen05
9
+
10
+ from .._vendor.flash_attn.cute import utils as fa_utils
11
+
12
+ from .tmem import (
13
+ _add_physical_tmem_base,
14
+ _zero_based_tmem_tensor,
15
+ tcgen05_wait_ld,
16
+ tcgen05_wait_st,
17
+ )
18
+
19
+
20
+ M = 64
21
+ N_HALF = 128
22
+ DV = 128
23
+ LOG2E = 1.4426950408889634
24
+
25
+
26
+ @cute.jit
27
+ def _load_m64_n128_score(
28
+ score_template: cute.Tensor,
29
+ thr_mma_qk: cute.ThrMma,
30
+ tmem_base: Int32,
31
+ score_offset: Int32,
32
+ owner_tidx: Int32,
33
+ ):
34
+ """Load one M64xN128 FP32 score tile from TMEM."""
35
+
36
+ relative_score = _zero_based_tmem_tensor(Float32, score_template.layout)
37
+ load_atom = cute.make_copy_atom(
38
+ tcgen05.copy.Ld16x64bOp(tcgen05.copy.Repetition(64)),
39
+ Float32,
40
+ )
41
+ tiled_load = tcgen05.make_tmem_copy(load_atom, relative_score)
42
+ thread_load = tiled_load.get_slice(owner_tidx)
43
+ source_relative = thread_load.partition_S(relative_score)
44
+ source = _add_physical_tmem_base(
45
+ source_relative, tmem_base + score_offset
46
+ )
47
+ coordinates = thread_load.partition_D(
48
+ thr_mma_qk.partition_C(cute.make_identity_tensor((M, N_HALF)))
49
+ )
50
+ scores = cute.make_rmem_tensor(coordinates.shape, Float32)
51
+ cute.copy(tiled_load, source, scores)
52
+ tcgen05_wait_ld()
53
+ cute.arch.fence_view_async_tmem_load()
54
+ return scores, coordinates
55
+
56
+
57
+ @cute.jit
58
+ def _rescale_m64_partial_o(
59
+ o_template: cute.Tensor,
60
+ thr_mma_pv: cute.ThrMma,
61
+ tmem_base: Int32,
62
+ o_offset: Int32,
63
+ owner_tidx: Int32,
64
+ alpha: Float32,
65
+ ):
66
+ """Rescale the prior M64 output accumulator before its next PV update."""
67
+
68
+ relative_o = _zero_based_tmem_tensor(Float32, o_template.layout)
69
+ correction_width = 16
70
+ relative_fragment = cute.composition(
71
+ relative_o, cute.make_layout((M, correction_width))
72
+ )
73
+ load_atom = cute.make_copy_atom(
74
+ tcgen05.copy.Ld16x64bOp(tcgen05.copy.Repetition(8)), Float32
75
+ )
76
+ store_atom = cute.make_copy_atom(
77
+ tcgen05.copy.St16x64bOp(tcgen05.copy.Repetition(8)), Float32
78
+ )
79
+ thread_load = tcgen05.make_tmem_copy(
80
+ load_atom, relative_fragment
81
+ ).get_slice(owner_tidx)
82
+ thread_store = tcgen05.make_tmem_copy(
83
+ store_atom, relative_fragment
84
+ ).get_slice(owner_tidx)
85
+ source = _add_physical_tmem_base(
86
+ thread_load.partition_S(relative_fragment), tmem_base + o_offset
87
+ )
88
+ destination = _add_physical_tmem_base(
89
+ thread_store.partition_D(relative_fragment), tmem_base + o_offset
90
+ )
91
+ for fragment_idx in cutlass.range_constexpr(DV // correction_width):
92
+ registers = cute.make_rmem_tensor(
93
+ thread_load.partition_D(relative_fragment).shape, Float32
94
+ )
95
+ source_i = cute.make_tensor(
96
+ source.iterator + fragment_idx * correction_width, source.layout
97
+ )
98
+ cute.copy(thread_load, source_i, registers)
99
+ tcgen05_wait_ld()
100
+ cute.arch.fence_view_async_tmem_load()
101
+ for i in cutlass.range(cute.size(registers), unroll_full=True):
102
+ registers[i] = Float32(registers[i]) * Float32(alpha)
103
+ destination_i = cute.make_tensor(
104
+ destination.iterator + fragment_idx * correction_width,
105
+ destination.layout,
106
+ )
107
+ cute.copy(thread_store, registers, destination_i)
108
+ tcgen05_wait_st()
109
+ cute.arch.fence_view_async_tmem_store()
110
+
111
+
112
+ @cute.jit
113
+ def _online_update_one_half(
114
+ scores: cute.Tensor,
115
+ running_max: Float32,
116
+ running_sum: Float32,
117
+ softmax_scale: Float32,
118
+ ):
119
+ """Apply one FP32 online-softmax update to an M64xN128 score tile."""
120
+
121
+ local_max = fa_utils.fmax_reduce(scores.load(), arch=100)
122
+ local_max = Float32(local_max) * softmax_scale
123
+ peer_max = cute.arch.shuffle_sync_bfly(local_max, offset=2)
124
+ transaction_max = local_max
125
+ if peer_max > transaction_max:
126
+ transaction_max = peer_max
127
+ new_max = running_max
128
+ if running_max == -Float32.inf or transaction_max > running_max:
129
+ new_max = transaction_max
130
+ alpha = Float32(0.0)
131
+ if running_max != -Float32.inf:
132
+ alpha = cute.math.exp2(
133
+ (running_max - new_max) * Float32(LOG2E), fastmath=True
134
+ )
135
+ probabilities = cute.make_rmem_tensor(scores.shape, Float32)
136
+ for i in cutlass.range(cute.size(scores), unroll_full=True):
137
+ probabilities[i] = cute.math.exp2(
138
+ Float32(scores[i]) * softmax_scale * Float32(LOG2E)
139
+ - new_max * Float32(LOG2E),
140
+ fastmath=True,
141
+ )
142
+ transaction_sum = fa_utils.fadd_reduce(
143
+ probabilities.load(), arch=100
144
+ )
145
+ transaction_sum += cute.arch.shuffle_sync_bfly(
146
+ transaction_sum, offset=2
147
+ )
148
+ new_sum = running_sum * alpha + transaction_sum
149
+ return probabilities, new_max, new_sum, alpha
150
+
151
+
152
+ __all__ = [
153
+ "_load_m64_n128_score",
154
+ "_online_update_one_half",
155
+ "_rescale_m64_partial_o",
156
+ ]
torch-ext/sol_attn/sm100/tmem.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TMEM load helpers used by the SM100 mainloop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import cutlass.cute as cute
6
+ import cutlass.cute.nvgpu.tcgen05 as tcgen05
7
+ from cutlass import Float32, Int32
8
+ from cutlass._mlir.dialects import llvm
9
+
10
+
11
+ M = 64
12
+ D = 128
13
+ O_OFFSET = 128
14
+
15
+
16
+ @cute.jit
17
+ def tcgen05_wait_ld() -> None:
18
+ llvm.inline_asm(
19
+ None,
20
+ [],
21
+ "tcgen05.wait::ld.sync.aligned;",
22
+ "",
23
+ has_side_effects=True,
24
+ is_align_stack=False,
25
+ asm_dialect=llvm.AsmDialect.AD_ATT,
26
+ )
27
+
28
+
29
+ @cute.jit
30
+ def tcgen05_wait_st() -> None:
31
+ llvm.inline_asm(
32
+ None,
33
+ [],
34
+ "tcgen05.wait::st.sync.aligned;",
35
+ "",
36
+ has_side_effects=True,
37
+ is_align_stack=False,
38
+ asm_dialect=llvm.AsmDialect.AD_ATT,
39
+ )
40
+
41
+
42
+ @cute.jit
43
+ def _zero_based_tmem_tensor(element_type, layout):
44
+ return cute.make_tensor(
45
+ cute.make_ptr(
46
+ element_type,
47
+ Int32(0),
48
+ cute.AddressSpace.tmem,
49
+ assumed_align=16,
50
+ ),
51
+ layout,
52
+ )
53
+
54
+
55
+ @cute.jit
56
+ def _add_physical_tmem_base(
57
+ relative: cute.Tensor,
58
+ physical_address: Int32,
59
+ ):
60
+ return cute.make_tensor(
61
+ cute.make_ptr(
62
+ relative.element_type,
63
+ physical_address + relative.iterator.toint(),
64
+ cute.AddressSpace.tmem,
65
+ assumed_align=16,
66
+ ),
67
+ relative.layout,
68
+ )
69
+
70
+
71
+ @cute.jit
72
+ def _o_copy_views(
73
+ o_template: cute.Tensor,
74
+ pv_thread: cute.ThrMma,
75
+ ):
76
+ assert o_template.element_type == Float32
77
+ assert cute.size(o_template) == M * D
78
+ relative = _zero_based_tmem_tensor(Float32, o_template.layout)
79
+ coordinates = pv_thread.partition_C(
80
+ cute.make_identity_tensor((M, D))
81
+ )
82
+ tiler = (
83
+ (
84
+ cute.size(relative, mode=[0, 0]),
85
+ cute.size(relative, mode=[0, 1]),
86
+ ),
87
+ )
88
+ return (
89
+ cute.zipped_divide(relative, tiler),
90
+ cute.zipped_divide(coordinates, tiler),
91
+ )
92
+
93
+
94
+ @cute.jit
95
+ def load_m64_o_fp32_256b(
96
+ o_template: cute.Tensor,
97
+ pv_thread: cute.ThrMma,
98
+ physical_tmem_base: Int32,
99
+ thread_idx: Int32,
100
+ ):
101
+ relative, coordinates = _o_copy_views(o_template, pv_thread)
102
+ atom = cute.make_copy_atom(
103
+ tcgen05.Ld16x256bOp(tcgen05.Repetition.x8),
104
+ Float32,
105
+ )
106
+ tiled_copy = tcgen05.make_tmem_copy(
107
+ atom,
108
+ relative[None, Int32(0)],
109
+ )
110
+ thread_copy = tiled_copy.get_slice(thread_idx)
111
+ source = _add_physical_tmem_base(
112
+ thread_copy.partition_S(relative),
113
+ physical_tmem_base + Int32(O_OFFSET),
114
+ )
115
+ register_coordinates = thread_copy.partition_D(coordinates)[
116
+ None, None, Int32(0)
117
+ ]
118
+ registers = cute.make_rmem_tensor(
119
+ register_coordinates.shape,
120
+ Float32,
121
+ )
122
+ cute.copy(
123
+ tiled_copy,
124
+ source[None, None, Int32(0)],
125
+ registers,
126
+ )
127
+ tcgen05_wait_ld()
128
+ cute.arch.fence_view_async_tmem_load()
129
+ return registers, register_coordinates
130
+
131
+
132
+ __all__ = [
133
+ "_add_physical_tmem_base",
134
+ "_zero_based_tmem_tensor",
135
+ "load_m64_o_fp32_256b",
136
+ "tcgen05_wait_ld",
137
+ "tcgen05_wait_st",
138
+ ]
torch-ext/sol_attn/sm120/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """GeForce Blackwell (SM120) backend."""
2
+
3
+ from .kernel import make_kernel
4
+
5
+ __all__ = ["make_kernel"]
torch-ext/sol_attn/sm120/kernel.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SM120 kernel recipe."""
2
+
3
+ from .mainloop import SolAttnForwardSm120
4
+
5
+
6
+ def make_kernel(
7
+ *,
8
+ debug_route_trace: bool = False,
9
+ prefetch_first_exact_k: bool = True,
10
+ prefetch_next_route_k: bool = True,
11
+ ):
12
+ return SolAttnForwardSm120(
13
+ debug_route_trace=debug_route_trace,
14
+ prefetch_first_exact_k=prefetch_first_exact_k,
15
+ prefetch_next_route_k=prefetch_next_route_k,
16
+ )
17
+
18
+
19
+ __all__ = ["make_kernel"]
torch-ext/sol_attn/sm120/mainloop.py ADDED
@@ -0,0 +1,1172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """Fused Sol-Attn forward kernel for GeForce Blackwell SM120.
4
+
5
+ The warp-MMA/TMA execution skeleton and online-softmax helpers are adapted
6
+ from NVIDIA cuDNN Frontend's SM120 block-sparse-attention kernel. Sol-specific
7
+ routing, CTA-local exact-index compaction, approximate block mass, and the
8
+ mixed approximate/exact mainloop are implemented here.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import operator
14
+
15
+ import cuda.bindings.driver as cuda
16
+ import cutlass
17
+ import cutlass.cute as cute
18
+ import cutlass.pipeline as pipeline
19
+ import cutlass.utils as utils
20
+ import cutlass.utils.hopper_helpers as sm90_utils
21
+
22
+ from .._vendor.flash_attn.cute import utils as kernel_utils
23
+ from ..common import layout_utils
24
+ from ..common.selector import (
25
+ sol_attn_popc_b32,
26
+ sol_attn_route_is_exact,
27
+ )
28
+
29
+
30
+ M = 64
31
+ N = 64
32
+ D = 128
33
+ DV = 128
34
+ THREADS = 128
35
+ STAGES = 1
36
+
37
+
38
+ class SolAttnForwardSm120:
39
+ """M64/N64 warp-MMA Sol-Attn kernel for BF16 D128 inputs."""
40
+
41
+ def __init__(
42
+ self,
43
+ *,
44
+ debug_route_trace: bool = False,
45
+ prefetch_first_exact_k: bool = True,
46
+ prefetch_next_route_k: bool = True,
47
+ ):
48
+ self.dtype = cutlass.BFloat16
49
+ self.acc_dtype = cutlass.Float32
50
+ self.tile_shape_qk = (M, N, D)
51
+ self.tile_shape_pv = (M, DV, N)
52
+ self.num_threads = THREADS
53
+ self.q_stage = 1
54
+ self.kv_stage = STAGES
55
+ self.debug_route_trace = debug_route_trace
56
+ self.prefetch_first_exact_k = prefetch_first_exact_k
57
+ self.prefetch_next_route_k = prefetch_next_route_k
58
+
59
+ @cute.kernel
60
+ def kernel(
61
+ self,
62
+ mQ: cute.Tensor,
63
+ mK: cute.Tensor,
64
+ mV: cute.Tensor,
65
+ mO: cute.Tensor,
66
+ mKC: cute.Tensor,
67
+ mVC: cute.Tensor,
68
+ mThreshold: cute.Tensor,
69
+ mLSE: cute.Tensor,
70
+ tma_atom_Q: cute.CopyAtom,
71
+ tma_atom_K: cute.CopyAtom,
72
+ tma_atom_V: cute.CopyAtom,
73
+ tma_atom_KC: cute.CopyAtom,
74
+ tma_atom_VC: cute.CopyAtom,
75
+ tma_atom_O: cute.CopyAtom,
76
+ tiled_mma_qk: cute.TiledMma,
77
+ tiled_mma_pv: cute.TiledMma,
78
+ Q_smem_layout: cute.ComposedLayout,
79
+ K_smem_layout: cute.ComposedLayout,
80
+ V_smem_layout: cute.ComposedLayout,
81
+ O_smem_layout: cute.ComposedLayout,
82
+ scale_softmax_log2e: cutlass.Float32,
83
+ sink_start_block: cutlass.Int32,
84
+ sink_end_block: cutlass.Int32,
85
+ ):
86
+ tidx, _, _ = cute.arch.thread_idx()
87
+ lane = cute.arch.lane_idx()
88
+ warp = cute.arch.make_warp_uniform(cute.arch.warp_idx())
89
+ q_tile_idx, head_idx, batch_idx = cute.arch.block_idx()
90
+ q_tile_idx = cute.arch.make_warp_uniform(q_tile_idx)
91
+ head_idx = cute.arch.make_warp_uniform(head_idx)
92
+ batch_idx = cute.arch.make_warp_uniform(batch_idx)
93
+
94
+ token_count = mK.shape[0]
95
+ num_blocks = mKC.shape[0]
96
+ num_route_groups = cute.ceil_div(num_blocks, N)
97
+ q_start = q_tile_idx * M
98
+ q_len = token_count - q_start
99
+ if q_len > M:
100
+ q_len = cutlass.Int32(M)
101
+ threshold = cutlass.Float32(
102
+ mThreshold[batch_idx, q_tile_idx, head_idx]
103
+ )
104
+
105
+ storage = cutlass.utils.SmemAllocator().allocate(self.shared_storage_t)
106
+ if warp == 0 and lane == 0:
107
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_Q)
108
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_K)
109
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_V)
110
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_KC)
111
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_VC)
112
+ cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_O)
113
+
114
+ cg = pipeline.CooperativeGroup(pipeline.Agent.Thread)
115
+ consumer_group = pipeline.CooperativeGroup(
116
+ pipeline.Agent.Thread, self.num_threads // 32
117
+ )
118
+ cta_layout_vmnk = cute.make_layout((1, 1, 1, 1))
119
+ Q_pipeline = pipeline.PipelineTmaAsync.create(
120
+ num_stages=self.q_stage,
121
+ producer_group=cg,
122
+ consumer_group=consumer_group,
123
+ tx_count=cute.size_in_bytes(
124
+ self.Q_dtype, cute.select(Q_smem_layout, mode=[0, 1])
125
+ ),
126
+ barrier_storage=storage.Q_barrier.data_ptr(),
127
+ cta_layout_vmnk=cta_layout_vmnk,
128
+ )
129
+ K_pipeline = pipeline.PipelineTmaAsync.create(
130
+ num_stages=self.kv_stage,
131
+ producer_group=cg,
132
+ consumer_group=consumer_group,
133
+ tx_count=cute.size_in_bytes(
134
+ self.K_dtype, cute.select(K_smem_layout, mode=[0, 1])
135
+ ),
136
+ barrier_storage=storage.K_barrier.data_ptr(),
137
+ cta_layout_vmnk=cta_layout_vmnk,
138
+ )
139
+ V_pipeline = pipeline.PipelineTmaAsync.create(
140
+ num_stages=self.kv_stage,
141
+ producer_group=cg,
142
+ consumer_group=consumer_group,
143
+ tx_count=cute.size_in_bytes(
144
+ self.V_dtype, cute.select(V_smem_layout, mode=[0, 1])
145
+ ),
146
+ barrier_storage=storage.V_barrier.data_ptr(),
147
+ cta_layout_vmnk=cta_layout_vmnk,
148
+ )
149
+ Q_producer = pipeline.make_pipeline_state(
150
+ pipeline.PipelineUserType.Producer, self.q_stage
151
+ )
152
+ Q_consumer = pipeline.make_pipeline_state(
153
+ pipeline.PipelineUserType.Consumer, self.q_stage
154
+ )
155
+ K_producer = pipeline.make_pipeline_state(
156
+ pipeline.PipelineUserType.Producer, self.kv_stage
157
+ )
158
+ K_consumer = pipeline.make_pipeline_state(
159
+ pipeline.PipelineUserType.Consumer, self.kv_stage
160
+ )
161
+ V_producer = pipeline.make_pipeline_state(
162
+ pipeline.PipelineUserType.Producer, self.kv_stage
163
+ )
164
+ V_consumer = pipeline.make_pipeline_state(
165
+ pipeline.PipelineUserType.Consumer, self.kv_stage
166
+ )
167
+
168
+ sQ = storage.Q_smem.get_tensor(
169
+ Q_smem_layout.outer, swizzle=Q_smem_layout.inner
170
+ )
171
+ sK = storage.K_smem.get_tensor(
172
+ K_smem_layout.outer, swizzle=K_smem_layout.inner
173
+ )
174
+ sV = storage.V_smem.get_tensor(
175
+ V_smem_layout.outer, swizzle=V_smem_layout.inner
176
+ )
177
+ # Q is register-resident after the prologue. Reuse its 16 KiB SMEM
178
+ # allocation for route scratch until the same allocation becomes sO
179
+ # in the epilogue. This drops the CTA below the 2-block/SM threshold
180
+ # on SM120 without changing any route reduction or synchronization.
181
+ route_f32_ptr = cute.recast_ptr(
182
+ storage.Q_smem.data_ptr(), dtype=cutlass.Float32
183
+ )
184
+ route_i32_ptr = cute.recast_ptr(
185
+ storage.Q_smem.data_ptr(), dtype=cutlass.Int32
186
+ )
187
+ route_sums = cute.make_tensor(
188
+ route_f32_ptr, cute.make_layout((4, N))
189
+ )
190
+ column_masks = cute.make_tensor(
191
+ route_f32_ptr + 4 * N, cute.make_layout(N)
192
+ )
193
+ route_indices = cute.make_tensor(
194
+ route_i32_ptr + 5 * N, cute.make_layout(N)
195
+ )
196
+ route_meta = cute.make_tensor(
197
+ route_i32_ptr + 6 * N, cute.make_layout(2)
198
+ )
199
+
200
+ mQ_slice = mQ[None, None, head_idx, batch_idx]
201
+ mK_slice = mK[None, None, head_idx, batch_idx]
202
+ mV_slice = mV[None, None, head_idx, batch_idx]
203
+ mO_slice = mO[None, None, head_idx, batch_idx]
204
+ mKC_slice = mKC[None, None, head_idx, batch_idx]
205
+ mVC_slice = mVC[None, None, head_idx, batch_idx]
206
+ if cutlass.const_expr(not self.debug_route_trace):
207
+ mLSE_slice = mLSE[None, head_idx, batch_idx]
208
+
209
+ gQ = cute.local_tile(
210
+ mQ_slice, (M, D), coord=(q_tile_idx, 0)
211
+ )
212
+ gK = cute.local_tile(mK_slice, (N, D), coord=(None, 0))
213
+ gV = cute.local_tile(mV_slice, (DV, N), coord=(0, None))
214
+ gKC = cute.local_tile(mKC_slice, (N, D), coord=(None, 0))
215
+ gVC = cute.local_tile(mVC_slice, (DV, N), coord=(0, None))
216
+ gO = cute.local_tile(
217
+ mO_slice, (M, DV), coord=(q_tile_idx, 0)
218
+ )
219
+
220
+ cta_coord_layout = (0, cute.make_layout(1))
221
+ tQsQ, tQgQ = cute.nvgpu.cpasync.tma_partition(
222
+ tma_atom_Q,
223
+ *cta_coord_layout,
224
+ cute.group_modes(sQ, 0, 2),
225
+ cute.group_modes(gQ, 0, 2),
226
+ )
227
+ tKsK, tKgK = cute.nvgpu.cpasync.tma_partition(
228
+ tma_atom_K,
229
+ *cta_coord_layout,
230
+ cute.group_modes(sK, 0, 2),
231
+ cute.group_modes(gK, 0, 2),
232
+ )
233
+ tVsV, tVgV = cute.nvgpu.cpasync.tma_partition(
234
+ tma_atom_V,
235
+ *cta_coord_layout,
236
+ cute.group_modes(sV, 0, 2),
237
+ cute.group_modes(gV, 0, 2),
238
+ )
239
+ tKCsK, tKCgKC = cute.nvgpu.cpasync.tma_partition(
240
+ tma_atom_KC,
241
+ *cta_coord_layout,
242
+ cute.group_modes(sK, 0, 2),
243
+ cute.group_modes(gKC, 0, 2),
244
+ )
245
+ tVCsV, tVCgVC = cute.nvgpu.cpasync.tma_partition(
246
+ tma_atom_VC,
247
+ *cta_coord_layout,
248
+ cute.group_modes(sV, 0, 2),
249
+ cute.group_modes(gVC, 0, 2),
250
+ )
251
+
252
+ cS = cute.make_identity_tensor(self.tile_shape_qk[:2])
253
+ thr_mma_qk = tiled_mma_qk.get_slice(tidx)
254
+ tSsQ = thr_mma_qk.partition_A(sQ)
255
+ tSsK = thr_mma_qk.partition_B(sK)
256
+ tSrQ = tiled_mma_qk.make_fragment_A(tSsQ[None, None, None, 0])
257
+ tSrK = tiled_mma_qk.make_fragment_B(tSsK[None, None, None, 0])
258
+ tSrS = cute.make_rmem_tensor(
259
+ thr_mma_qk.partition_shape_C((M, N)), self.acc_dtype
260
+ )
261
+ tScS = thr_mma_qk.partition_C(cS)
262
+
263
+ thr_mma_pv = tiled_mma_pv.get_slice(tidx)
264
+ tOsV = thr_mma_pv.partition_B(sV)
265
+ tOrV = tiled_mma_pv.make_fragment_B(tOsV[None, None, None, 0])
266
+ tOrO = cute.make_rmem_tensor(
267
+ thr_mma_pv.partition_shape_C((M, DV)), self.acc_dtype
268
+ )
269
+
270
+ atom_copy_Q = cute.make_copy_atom(
271
+ cute.nvgpu.warp.LdMatrix8x8x16bOp(
272
+ self.Q_layout.is_m_major_a(), 4
273
+ ),
274
+ self.Q_dtype,
275
+ )
276
+ atom_copy_K = cute.make_copy_atom(
277
+ cute.nvgpu.warp.LdMatrix8x8x16bOp(
278
+ self.K_layout.is_n_major_b(), 4
279
+ ),
280
+ self.K_dtype,
281
+ )
282
+ atom_copy_V = cute.make_copy_atom(
283
+ cute.nvgpu.warp.LdMatrix8x8x16bOp(
284
+ self.V_layout.is_n_major_b(), 4
285
+ ),
286
+ self.V_dtype,
287
+ )
288
+ smem_copy_Q = cute.make_tiled_copy_A(atom_copy_Q, tiled_mma_qk)
289
+ smem_copy_K = cute.make_tiled_copy_B(atom_copy_K, tiled_mma_qk)
290
+ smem_copy_V = cute.make_tiled_copy_B(atom_copy_V, tiled_mma_pv)
291
+ thr_copy_Q = smem_copy_Q.get_slice(tidx)
292
+ thr_copy_K = smem_copy_K.get_slice(tidx)
293
+ thr_copy_V = smem_copy_V.get_slice(tidx)
294
+ tSsQ_copy = thr_copy_Q.partition_S(sQ)
295
+ tSrQ_copy = thr_copy_Q.retile(tSrQ)
296
+ tSsK_copy = thr_copy_K.partition_S(sK)
297
+ tOsV_copy = thr_copy_V.partition_S(sV)
298
+
299
+ max_m_layout = cute.make_layout(
300
+ cute.size(
301
+ layout_utils.reshape_acc_to_mn(tOrO).layout,
302
+ mode=[0],
303
+ )
304
+ )
305
+ max_m = cute.make_rmem_tensor_like(max_m_layout, cutlass.Float32)
306
+ sum_m = cute.make_rmem_tensor_like(max_m, cutlass.Float32)
307
+ tOrO.store(cute.full_like(tOrO, 0.0, self.acc_dtype))
308
+ max_m.store(cute.full_like(max_m, float("-inf"), cutlass.Float32))
309
+ sum_m.store(cute.full_like(sum_m, 0.0, cutlass.Float32))
310
+
311
+ if warp == 0:
312
+ Q_pipeline.producer_acquire(Q_producer)
313
+ cute.copy(
314
+ tma_atom_Q,
315
+ tQgQ,
316
+ tQsQ[None, Q_producer.index],
317
+ tma_bar_ptr=Q_pipeline.producer_get_barrier(Q_producer),
318
+ )
319
+ Q_pipeline.producer_commit(Q_producer)
320
+ Q_producer.advance()
321
+ cute.arch.sync_threads()
322
+ q_wait = Q_pipeline.consumer_try_wait(Q_consumer)
323
+ Q_pipeline.consumer_wait(Q_consumer, q_wait)
324
+ q_stage = Q_consumer.index
325
+ for k_block in cutlass.range_constexpr(cute.size(tSrQ, mode=[2])):
326
+ cute.copy(
327
+ smem_copy_Q,
328
+ tSsQ_copy[None, None, k_block, q_stage],
329
+ tSrQ_copy[None, None, k_block],
330
+ )
331
+ Q_pipeline.consumer_release(Q_consumer)
332
+ Q_consumer.advance()
333
+
334
+ for route_group in cutlass.range(
335
+ 0, num_route_groups, 1, unroll=1
336
+ ):
337
+ group_start = route_group * cutlass.Int32(N)
338
+ valid_blocks = num_blocks - group_start
339
+ if valid_blocks > N:
340
+ valid_blocks = cutlass.Int32(N)
341
+
342
+ if warp == 0:
343
+ if cutlass.const_expr(self.prefetch_next_route_k):
344
+ # P19-style terminal handoff: when the previous route
345
+ # group had an exact block, its final exact QK already
346
+ # refilled this K stage with the current group's KC.
347
+ if route_group == 0:
348
+ K_pipeline.producer_acquire(K_producer)
349
+ cute.copy(
350
+ tma_atom_KC,
351
+ tKCgKC[None, route_group],
352
+ tKCsK[None, K_producer.index],
353
+ tma_bar_ptr=K_pipeline.producer_get_barrier(
354
+ K_producer
355
+ ),
356
+ )
357
+ K_pipeline.producer_commit(K_producer)
358
+ K_producer.advance()
359
+ else:
360
+ previous_group_exact_count = cutlass.Int32(
361
+ route_meta[0]
362
+ )
363
+ if previous_group_exact_count == 0:
364
+ K_pipeline.producer_acquire(K_producer)
365
+ cute.copy(
366
+ tma_atom_KC,
367
+ tKCgKC[None, route_group],
368
+ tKCsK[None, K_producer.index],
369
+ tma_bar_ptr=K_pipeline.producer_get_barrier(
370
+ K_producer
371
+ ),
372
+ )
373
+ K_pipeline.producer_commit(K_producer)
374
+ K_producer.advance()
375
+ else:
376
+ K_pipeline.producer_acquire(K_producer)
377
+ cute.copy(
378
+ tma_atom_KC,
379
+ tKCgKC[None, route_group],
380
+ tKCsK[None, K_producer.index],
381
+ tma_bar_ptr=K_pipeline.producer_get_barrier(
382
+ K_producer
383
+ ),
384
+ )
385
+ K_pipeline.producer_commit(K_producer)
386
+ K_producer.advance()
387
+ V_pipeline.producer_acquire(V_producer)
388
+ cute.copy(
389
+ tma_atom_VC,
390
+ tVCgVC[None, route_group],
391
+ tVCsV[None, V_producer.index],
392
+ tma_bar_ptr=V_pipeline.producer_get_barrier(V_producer),
393
+ )
394
+ V_pipeline.producer_commit(V_producer)
395
+ V_producer.advance()
396
+
397
+ k_wait = K_pipeline.consumer_try_wait(K_consumer)
398
+ K_pipeline.consumer_wait(K_consumer, k_wait)
399
+ gemm_smem_zero_acc(
400
+ tiled_mma_qk,
401
+ tSrS,
402
+ tSrQ,
403
+ tSrK,
404
+ tSsK_copy[None, None, None, K_consumer.index],
405
+ smem_copy_K,
406
+ )
407
+ K_pipeline.consumer_release(K_consumer)
408
+ K_consumer.advance()
409
+
410
+ reduce_route_columns(
411
+ tSrS,
412
+ tScS,
413
+ route_sums,
414
+ warp,
415
+ lane,
416
+ q_len,
417
+ )
418
+ cute.arch.fence_view_async_shared()
419
+ cute.arch.sync_threads()
420
+
421
+ if warp == 0:
422
+ preceding = cutlass.Int32(0)
423
+ lane_mask_lt = cutlass.Int32(0x7FFFFFFF) >> (
424
+ cutlass.Int32(31) - lane
425
+ )
426
+ for word in cutlass.range_constexpr(2):
427
+ off = cutlass.Int32(word * 32) + lane
428
+ valid = off < valid_blocks
429
+ exact = False
430
+ if valid:
431
+ col_sum = (
432
+ cutlass.Float32(route_sums[0, off])
433
+ + cutlass.Float32(route_sums[1, off])
434
+ + cutlass.Float32(route_sums[2, off])
435
+ + cutlass.Float32(route_sums[3, off])
436
+ )
437
+ col_mean = (
438
+ col_sum
439
+ * scale_softmax_log2e
440
+ / cutlass.Float32(q_len)
441
+ )
442
+ kv_block = group_start + off
443
+ exact = sol_attn_route_is_exact(
444
+ q_tile_idx,
445
+ kv_block,
446
+ col_mean,
447
+ threshold,
448
+ valid,
449
+ )
450
+ exact = exact or (
451
+ kv_block >= sink_start_block
452
+ and kv_block < sink_end_block
453
+ )
454
+ ballot = cutlass.Int32(
455
+ cute.arch.vote_ballot_sync(exact)
456
+ )
457
+ column_masks[off] = (
458
+ -cutlass.Float32.inf
459
+ if (exact or not valid)
460
+ else cutlass.Float32(0.0)
461
+ )
462
+ rank = preceding + sol_attn_popc_b32(
463
+ ballot & lane_mask_lt
464
+ )
465
+ if exact:
466
+ route_indices[rank] = group_start + off
467
+ preceding += sol_attn_popc_b32(ballot)
468
+ if cutlass.const_expr(self.debug_route_trace):
469
+ if lane == 0:
470
+ mLSE[
471
+ batch_idx,
472
+ q_tile_idx,
473
+ head_idx,
474
+ route_group,
475
+ word,
476
+ ] = ballot
477
+ if lane == 0:
478
+ route_meta[0] = preceding
479
+ route_meta[1] = valid_blocks
480
+ cute.arch.fence_view_async_shared()
481
+ cute.arch.sync_threads()
482
+
483
+ exact_count = cutlass.Int32(route_meta[0])
484
+ has_approx = exact_count < valid_blocks
485
+ if cutlass.const_expr(self.prefetch_first_exact_k):
486
+ # Once routing identifies the first exact block, the route KC
487
+ # stage is free. Refill it before the approximate softmax/PV
488
+ # so the first exact K transfer overlaps that work.
489
+ if warp == 0 and exact_count > 0:
490
+ first_exact = cutlass.Int32(route_indices[0])
491
+ K_pipeline.producer_acquire(K_producer)
492
+ cute.copy(
493
+ tma_atom_K,
494
+ tKgK[None, first_exact],
495
+ tKsK[None, K_producer.index],
496
+ tma_bar_ptr=K_pipeline.producer_get_barrier(
497
+ K_producer
498
+ ),
499
+ )
500
+ K_pipeline.producer_commit(K_producer)
501
+ K_producer.advance()
502
+ v_wait = V_pipeline.consumer_try_wait(V_consumer)
503
+ V_pipeline.consumer_wait(V_consumer, v_wait)
504
+ if has_approx:
505
+ apply_route_mask(tSrS, tScS, column_masks, q_len)
506
+ row_scale = online_softmax_route(
507
+ tSrS,
508
+ tScS,
509
+ max_m,
510
+ sum_m,
511
+ scale_softmax_log2e,
512
+ group_start,
513
+ token_count,
514
+ )
515
+ rescale_o_for_next_acc(tOrO, row_scale)
516
+ tOrP_frg = cute.make_rmem_tensor_like(
517
+ tSrS, self.K_dtype
518
+ )
519
+ tOrP_frg.store(tSrS.load().to(self.K_dtype))
520
+ tOrP = layout_utils.reshape_acc_to_frgA(tOrP_frg)
521
+ gemm_rs_smem(
522
+ tiled_mma_pv,
523
+ tOrO,
524
+ tOrP,
525
+ tOrV,
526
+ tOsV_copy[None, None, None, V_consumer.index],
527
+ smem_copy_V,
528
+ )
529
+ V_pipeline.consumer_release(V_consumer)
530
+ V_consumer.advance()
531
+
532
+ if warp == 0 and exact_count > 0:
533
+ first_exact = cutlass.Int32(route_indices[0])
534
+ if cutlass.const_expr(not self.prefetch_first_exact_k):
535
+ K_pipeline.producer_acquire(K_producer)
536
+ cute.copy(
537
+ tma_atom_K,
538
+ tKgK[None, first_exact],
539
+ tKsK[None, K_producer.index],
540
+ tma_bar_ptr=K_pipeline.producer_get_barrier(
541
+ K_producer
542
+ ),
543
+ )
544
+ K_pipeline.producer_commit(K_producer)
545
+ K_producer.advance()
546
+ V_pipeline.producer_acquire(V_producer)
547
+ cute.copy(
548
+ tma_atom_V,
549
+ tVgV[None, first_exact],
550
+ tVsV[None, V_producer.index],
551
+ tma_bar_ptr=V_pipeline.producer_get_barrier(V_producer),
552
+ )
553
+ V_pipeline.producer_commit(V_producer)
554
+ V_producer.advance()
555
+
556
+ for ordinal in cutlass.range(0, exact_count, 1, unroll=1):
557
+ exact_block = cutlass.Int32(route_indices[ordinal])
558
+ k_wait = K_pipeline.consumer_try_wait(K_consumer)
559
+ K_pipeline.consumer_wait(K_consumer, k_wait)
560
+ gemm_smem_zero_acc(
561
+ tiled_mma_qk,
562
+ tSrS,
563
+ tSrQ,
564
+ tSrK,
565
+ tSsK_copy[None, None, None, K_consumer.index],
566
+ smem_copy_K,
567
+ )
568
+ K_pipeline.consumer_release(K_consumer)
569
+ K_consumer.advance()
570
+ next_ordinal = ordinal + cutlass.Int32(1)
571
+ if warp == 0:
572
+ if next_ordinal < exact_count:
573
+ next_exact = cutlass.Int32(
574
+ route_indices[next_ordinal]
575
+ )
576
+ K_pipeline.producer_acquire(K_producer)
577
+ cute.copy(
578
+ tma_atom_K,
579
+ tKgK[None, next_exact],
580
+ tKsK[None, K_producer.index],
581
+ tma_bar_ptr=K_pipeline.producer_get_barrier(
582
+ K_producer
583
+ ),
584
+ )
585
+ K_pipeline.producer_commit(K_producer)
586
+ K_producer.advance()
587
+ else:
588
+ if cutlass.const_expr(
589
+ self.prefetch_next_route_k
590
+ ):
591
+ next_route_group = route_group + cutlass.Int32(1)
592
+ if next_route_group < num_route_groups:
593
+ # Reuse the K stage released by the final
594
+ # exact QK. The next outer prologue supplies
595
+ # VC, matching the SM90 P19 partial handoff.
596
+ K_pipeline.producer_acquire(K_producer)
597
+ cute.copy(
598
+ tma_atom_KC,
599
+ tKCgKC[None, next_route_group],
600
+ tKCsK[None, K_producer.index],
601
+ tma_bar_ptr=(
602
+ K_pipeline.producer_get_barrier(
603
+ K_producer
604
+ )
605
+ ),
606
+ )
607
+ K_pipeline.producer_commit(K_producer)
608
+ K_producer.advance()
609
+ block_len = token_count - exact_block * cutlass.Int32(N)
610
+ if block_len > N:
611
+ block_len = cutlass.Int32(N)
612
+ mask_exact_scores(tSrS, tScS, block_len, q_len)
613
+ row_scale = online_softmax(
614
+ tSrS, max_m, sum_m, scale_softmax_log2e
615
+ )
616
+ rescale_o_for_next_acc(tOrO, row_scale)
617
+ tOrP_frg = cute.make_rmem_tensor_like(
618
+ tSrS, self.K_dtype
619
+ )
620
+ tOrP_frg.store(tSrS.load().to(self.K_dtype))
621
+ tOrP = layout_utils.reshape_acc_to_frgA(tOrP_frg)
622
+
623
+ v_wait = V_pipeline.consumer_try_wait(V_consumer)
624
+ V_pipeline.consumer_wait(V_consumer, v_wait)
625
+ gemm_rs_smem(
626
+ tiled_mma_pv,
627
+ tOrO,
628
+ tOrP,
629
+ tOrV,
630
+ tOsV_copy[None, None, None, V_consumer.index],
631
+ smem_copy_V,
632
+ )
633
+ V_pipeline.consumer_release(V_consumer)
634
+ V_consumer.advance()
635
+ if warp == 0 and next_ordinal < exact_count:
636
+ next_exact = cutlass.Int32(route_indices[next_ordinal])
637
+ V_pipeline.producer_acquire(V_producer)
638
+ cute.copy(
639
+ tma_atom_V,
640
+ tVgV[None, next_exact],
641
+ tVsV[None, V_producer.index],
642
+ tma_bar_ptr=V_pipeline.producer_get_barrier(
643
+ V_producer
644
+ ),
645
+ )
646
+ V_pipeline.producer_commit(V_producer)
647
+ V_producer.advance()
648
+
649
+ final_ratio, lse = finalize_softmax(
650
+ max_m, sum_m, scale_softmax_log2e
651
+ )
652
+ rescale_o_for_next_acc(tOrO, final_ratio)
653
+ if cutlass.const_expr(not self.debug_route_trace):
654
+ tScS_mn = layout_utils.reshape_acc_to_mn(tScS)
655
+ for m in cutlass.range_constexpr(cute.size(lse)):
656
+ row = tScS_mn[m, 0][0]
657
+ if tScS_mn[m, 0][1] == 0 and row < q_len:
658
+ mLSE_slice[q_start + row] = lse[m]
659
+
660
+ tOrO_cvt = cute.make_rmem_tensor_like(tOrO, self.O_dtype)
661
+ tOrO_cvt.store(tOrO.load().to(self.O_dtype))
662
+ sO = storage.Q_smem.get_tensor(
663
+ O_smem_layout.outer, swizzle=O_smem_layout.inner
664
+ )
665
+ tiled_copy_O = cute.make_tiled_copy_C(
666
+ cute.make_copy_atom(
667
+ cute.nvgpu.warp.StMatrix8x8x16bOp(
668
+ self.O_layout.is_m_major_c(), 4
669
+ ),
670
+ self.O_dtype,
671
+ ),
672
+ tiled_mma_pv,
673
+ )
674
+ tOrO_cv = tiled_copy_O.retile(tOrO_cvt)
675
+ tOsO = tiled_copy_O.get_slice(tidx).partition_D(sO)
676
+ cute.copy(tiled_copy_O, tOrO_cv, tOsO)
677
+ cute.arch.fence_view_async_shared()
678
+ cute.arch.sync_threads()
679
+ tOsO, tOgO = cute.nvgpu.cpasync.tma_partition(
680
+ tma_atom_O,
681
+ *cta_coord_layout,
682
+ cute.group_modes(sO, 0, 2),
683
+ cute.group_modes(gO, 0, 2),
684
+ )
685
+ if warp == 0:
686
+ cute.copy(tma_atom_O, tOsO, tOgO)
687
+ cute.arch.cp_async_bulk_commit_group()
688
+ cute.arch.cp_async_bulk_wait_group(0, read=True)
689
+
690
+ @cute.jit
691
+ def __call__(
692
+ self,
693
+ q: cute.Tensor,
694
+ k: cute.Tensor,
695
+ v: cute.Tensor,
696
+ o: cute.Tensor,
697
+ kc: cute.Tensor,
698
+ vc: cute.Tensor,
699
+ threshold: cute.Tensor,
700
+ lse: cute.Tensor,
701
+ softmax_scale: cutlass.Float32,
702
+ sink_start_block: cutlass.Int32,
703
+ sink_end_block: cutlass.Int32,
704
+ stream: cuda.CUstream,
705
+ ):
706
+ q_mkl, k_nkl, kc_nkl = [
707
+ layout_utils.select(t, [1, 3, 2, 0])
708
+ for t in (q, k, kc)
709
+ ]
710
+ v_nkl, vc_nkl = [
711
+ layout_utils.select(t, [3, 1, 2, 0]) for t in (v, vc)
712
+ ]
713
+ o_mkl = layout_utils.select(o, [1, 3, 2, 0])
714
+ if cutlass.const_expr(self.debug_route_trace):
715
+ lse_target = lse
716
+ else:
717
+ lse_target = layout_utils.select(lse, [1, 2, 0])
718
+
719
+ self.Q_dtype = q_mkl.element_type
720
+ self.K_dtype = k_nkl.element_type
721
+ self.V_dtype = v_nkl.element_type
722
+ self.O_dtype = o_mkl.element_type
723
+ self.Q_layout = utils.LayoutEnum.from_tensor(q_mkl)
724
+ self.K_layout = utils.LayoutEnum.from_tensor(k_nkl)
725
+ self.V_layout = utils.LayoutEnum.from_tensor(v_nkl)
726
+ self.O_layout = utils.LayoutEnum.from_tensor(o_mkl)
727
+ assert self.Q_dtype == cutlass.BFloat16
728
+ assert self.K_dtype == cutlass.BFloat16
729
+ assert self.V_dtype == cutlass.BFloat16
730
+
731
+ self.Q_smem_layout = sm90_utils.make_smem_layout_a(
732
+ self.Q_layout,
733
+ self.tile_shape_qk,
734
+ self.Q_dtype,
735
+ self.q_stage,
736
+ )
737
+ self.K_smem_layout = sm90_utils.make_smem_layout_b(
738
+ self.K_layout,
739
+ self.tile_shape_qk,
740
+ self.K_dtype,
741
+ self.kv_stage,
742
+ )
743
+ self.V_smem_layout = sm90_utils.make_smem_layout_b(
744
+ self.V_layout,
745
+ self.tile_shape_pv,
746
+ self.V_dtype,
747
+ self.kv_stage,
748
+ )
749
+ O_smem_layout_staged = sm90_utils.make_smem_layout_epi(
750
+ self.O_dtype,
751
+ self.O_layout,
752
+ self.tile_shape_pv[:2],
753
+ 1,
754
+ )
755
+ self.O_smem_layout = cute.select(
756
+ O_smem_layout_staged, mode=[0, 1]
757
+ )
758
+
759
+ @cute.struct
760
+ class SharedStorage:
761
+ Q_barrier: cute.struct.MemRange[
762
+ cutlass.Int64, self.q_stage * 2
763
+ ]
764
+ K_barrier: cute.struct.MemRange[
765
+ cutlass.Int64, self.kv_stage * 2
766
+ ]
767
+ V_barrier: cute.struct.MemRange[
768
+ cutlass.Int64, self.kv_stage * 2
769
+ ]
770
+ Q_smem: cute.struct.Align[
771
+ cute.struct.MemRange[
772
+ self.Q_dtype, cute.cosize(self.Q_smem_layout)
773
+ ],
774
+ 128,
775
+ ]
776
+ K_smem: cute.struct.Align[
777
+ cute.struct.MemRange[
778
+ self.K_dtype, cute.cosize(self.K_smem_layout)
779
+ ],
780
+ 128,
781
+ ]
782
+ V_smem: cute.struct.Align[
783
+ cute.struct.MemRange[
784
+ self.V_dtype, cute.cosize(self.V_smem_layout)
785
+ ],
786
+ 128,
787
+ ]
788
+ self.shared_storage_t = SharedStorage
789
+
790
+ tiled_mma_qk = cute.make_tiled_mma(
791
+ cute.nvgpu.warp.MmaF16BF16Op(
792
+ self.Q_dtype,
793
+ self.acc_dtype,
794
+ (16, 8, 16),
795
+ ),
796
+ cute.make_layout((4, 1, 1)),
797
+ permutation_mnk=(64, 16, 16),
798
+ )
799
+ tiled_mma_pv = cute.make_tiled_mma(
800
+ cute.nvgpu.warp.MmaF16BF16Op(
801
+ self.K_dtype,
802
+ self.acc_dtype,
803
+ (16, 8, 16),
804
+ ),
805
+ cute.make_layout((4, 1, 1)),
806
+ permutation_mnk=(64, 16, 16),
807
+ )
808
+
809
+ g2s_op = cute.nvgpu.cpasync.CopyBulkTensorTileG2SOp()
810
+ tma_atom_Q, tma_tensor_Q = (
811
+ cute.nvgpu.cpasync.make_tiled_tma_atom(
812
+ g2s_op,
813
+ q_mkl,
814
+ self.Q_smem_layout,
815
+ (M, D),
816
+ num_multicast=1,
817
+ )
818
+ )
819
+ tma_atom_K, tma_tensor_K = (
820
+ cute.nvgpu.cpasync.make_tiled_tma_atom(
821
+ g2s_op,
822
+ k_nkl,
823
+ self.K_smem_layout,
824
+ (N, D),
825
+ num_multicast=1,
826
+ )
827
+ )
828
+ tma_atom_V, tma_tensor_V = (
829
+ cute.nvgpu.cpasync.make_tiled_tma_atom(
830
+ g2s_op,
831
+ v_nkl,
832
+ self.V_smem_layout,
833
+ (DV, N),
834
+ num_multicast=1,
835
+ )
836
+ )
837
+ tma_atom_KC, tma_tensor_KC = (
838
+ cute.nvgpu.cpasync.make_tiled_tma_atom(
839
+ g2s_op,
840
+ kc_nkl,
841
+ self.K_smem_layout,
842
+ (N, D),
843
+ num_multicast=1,
844
+ )
845
+ )
846
+ tma_atom_VC, tma_tensor_VC = (
847
+ cute.nvgpu.cpasync.make_tiled_tma_atom(
848
+ g2s_op,
849
+ vc_nkl,
850
+ self.V_smem_layout,
851
+ (DV, N),
852
+ num_multicast=1,
853
+ )
854
+ )
855
+ s2g_op = cute.nvgpu.cpasync.CopyBulkTensorTileS2GOp()
856
+ tma_atom_O, tma_tensor_O = (
857
+ cute.nvgpu.cpasync.make_tiled_tma_atom(
858
+ s2g_op,
859
+ o_mkl,
860
+ self.O_smem_layout,
861
+ (M, DV),
862
+ num_multicast=1,
863
+ )
864
+ )
865
+
866
+ self.kernel(
867
+ tma_tensor_Q,
868
+ tma_tensor_K,
869
+ tma_tensor_V,
870
+ tma_tensor_O,
871
+ tma_tensor_KC,
872
+ tma_tensor_VC,
873
+ threshold,
874
+ lse_target,
875
+ tma_atom_Q,
876
+ tma_atom_K,
877
+ tma_atom_V,
878
+ tma_atom_KC,
879
+ tma_atom_VC,
880
+ tma_atom_O,
881
+ tiled_mma_qk,
882
+ tiled_mma_pv,
883
+ self.Q_smem_layout,
884
+ self.K_smem_layout,
885
+ self.V_smem_layout,
886
+ self.O_smem_layout,
887
+ softmax_scale * 1.4426950408889634,
888
+ sink_start_block,
889
+ sink_end_block,
890
+ ).launch(
891
+ grid=(cute.ceil_div(q_mkl.shape[0], M), q_mkl.shape[2], q_mkl.shape[3]),
892
+ block=(self.num_threads, 1, 1),
893
+ cluster=(1, 1, 1),
894
+ smem=self.shared_storage_t.size_in_bytes(),
895
+ stream=stream,
896
+ min_blocks_per_mp=1,
897
+ )
898
+
899
+
900
+ @cute.jit
901
+ def gemm_smem_zero_acc(
902
+ tiled_mma: cute.TiledMma,
903
+ acc: cute.Tensor,
904
+ tCrA: cute.Tensor,
905
+ tCrB: cute.Tensor,
906
+ tCsB: cute.Tensor,
907
+ smem_tiled_copy_B: cute.TiledCopy,
908
+ ):
909
+ acc.fill(0.0)
910
+ tCrB_copy = smem_tiled_copy_B.retile(tCrB)
911
+ cute.copy(
912
+ smem_tiled_copy_B,
913
+ tCsB[None, None, 0],
914
+ tCrB_copy[None, None, 0],
915
+ )
916
+ for k_block in cutlass.range_constexpr(cute.size(tCsB.shape[2])):
917
+ if k_block < cute.size(tCsB.shape[2]) - 1:
918
+ cute.copy(
919
+ smem_tiled_copy_B,
920
+ tCsB[None, None, k_block + 1],
921
+ tCrB_copy[None, None, k_block + 1],
922
+ )
923
+ cute.gemm(
924
+ tiled_mma,
925
+ acc,
926
+ tCrA[None, None, k_block],
927
+ tCrB[None, None, k_block],
928
+ acc,
929
+ )
930
+
931
+
932
+ @cute.jit
933
+ def gemm_rs_smem(
934
+ tiled_mma: cute.TiledMma,
935
+ acc: cute.Tensor,
936
+ tCrA: cute.Tensor,
937
+ tCrB: cute.Tensor,
938
+ tCsB: cute.Tensor,
939
+ smem_tiled_copy_B: cute.TiledCopy,
940
+ ):
941
+ tCrB_copy = smem_tiled_copy_B.retile(tCrB)
942
+ cute.copy(
943
+ smem_tiled_copy_B,
944
+ tCsB[None, None, 0],
945
+ tCrB_copy[None, None, 0],
946
+ )
947
+ for k_block in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
948
+ if k_block < cute.size(tCrA.shape[2]) - 1:
949
+ cute.copy(
950
+ smem_tiled_copy_B,
951
+ tCsB[None, None, k_block + 1],
952
+ tCrB_copy[None, None, k_block + 1],
953
+ )
954
+ cute.gemm(
955
+ tiled_mma,
956
+ acc,
957
+ tCrA[None, None, k_block],
958
+ tCrB[None, None, k_block],
959
+ acc,
960
+ )
961
+
962
+
963
+ @cute.jit
964
+ def reduce_route_columns(
965
+ scores: cute.Tensor,
966
+ coords: cute.Tensor,
967
+ route_sums: cute.Tensor,
968
+ warp: cutlass.Int32,
969
+ lane: cutlass.Int32,
970
+ q_len: cutlass.Int32,
971
+ ):
972
+ """Reduce M64 score columns using the measured SM120 lane layout."""
973
+
974
+ scores_mn = layout_utils.reshape_acc_to_mn(scores)
975
+ coords_mn = layout_utils.reshape_acc_to_mn(coords)
976
+ row0 = coords_mn[0, 0][0]
977
+ row1 = coords_mn[1, 0][0]
978
+ valid0 = row0 < q_len
979
+ valid1 = row1 < q_len
980
+ for group in cutlass.range_constexpr(8):
981
+ n0 = group * 2
982
+ partial0 = cutlass.Float32(0.0)
983
+ partial1 = cutlass.Float32(0.0)
984
+ if valid0:
985
+ partial0 += cutlass.Float32(scores_mn[0, n0])
986
+ partial1 += cutlass.Float32(scores_mn[0, n0 + 1])
987
+ if valid1:
988
+ partial0 += cutlass.Float32(scores_mn[1, n0])
989
+ partial1 += cutlass.Float32(scores_mn[1, n0 + 1])
990
+ for offset in (4, 8, 16):
991
+ partial0 += cute.arch.shuffle_sync_bfly(partial0, offset=offset)
992
+ partial1 += cute.arch.shuffle_sync_bfly(partial1, offset=offset)
993
+ if lane < 4:
994
+ column = cutlass.Int32(group * 8) + lane * cutlass.Int32(2)
995
+ route_sums[warp, column] = partial0
996
+ route_sums[warp, column + 1] = partial1
997
+
998
+
999
+ @cute.jit
1000
+ def apply_route_mask(
1001
+ scores: cute.Tensor,
1002
+ coords: cute.Tensor,
1003
+ column_masks: cute.Tensor,
1004
+ q_len: cutlass.Int32,
1005
+ ):
1006
+ scores_mn = layout_utils.reshape_acc_to_mn(scores)
1007
+ coords_mn = layout_utils.reshape_acc_to_mn(coords)
1008
+ for m in cutlass.range_constexpr(cute.size(scores_mn, mode=[0])):
1009
+ valid_row = coords_mn[m, 0][0] < q_len
1010
+ for n in cutlass.range_constexpr(cute.size(scores_mn, mode=[1])):
1011
+ column = coords_mn[m, n][1]
1012
+ scores_mn[m, n] = (
1013
+ cutlass.Float32(scores_mn[m, n])
1014
+ + cutlass.Float32(column_masks[column])
1015
+ if valid_row
1016
+ else -cutlass.Float32.inf
1017
+ )
1018
+
1019
+
1020
+ @cute.jit
1021
+ def mask_exact_scores(
1022
+ scores: cute.Tensor,
1023
+ coords: cute.Tensor,
1024
+ block_len: cutlass.Int32,
1025
+ q_len: cutlass.Int32,
1026
+ ):
1027
+ scores_mn = layout_utils.reshape_acc_to_mn(scores)
1028
+ coords_mn = layout_utils.reshape_acc_to_mn(coords)
1029
+ for m in cutlass.range_constexpr(cute.size(scores_mn, mode=[0])):
1030
+ valid_row = coords_mn[m, 0][0] < q_len
1031
+ for n in cutlass.range_constexpr(cute.size(scores_mn, mode=[1])):
1032
+ if (not valid_row) or coords_mn[m, n][1] >= block_len:
1033
+ scores_mn[m, n] = -cutlass.Float32.inf
1034
+
1035
+
1036
+ @cute.jit
1037
+ def online_softmax(
1038
+ scores: cute.Tensor,
1039
+ row_max: cute.Tensor,
1040
+ row_sum: cute.Tensor,
1041
+ scale_log2e: cutlass.Float32,
1042
+ ):
1043
+ scores_mn = layout_utils.reshape_acc_to_mn(scores)
1044
+ row_scale = cute.make_rmem_tensor_like(row_max, cutlass.Float32)
1045
+ for m in cutlass.range_constexpr(cute.size(row_max)):
1046
+ score_row = scores_mn[m, None].load()
1047
+ current_max = kernel_utils.fmax_reduce(
1048
+ score_row, init_val=row_max[m], arch=80
1049
+ )
1050
+ current_max = cute.arch.warp_reduction_max(
1051
+ current_max, threads_in_group=4
1052
+ )
1053
+ previous_max = row_max[m]
1054
+ row_max[m] = current_max
1055
+ safe_max = (
1056
+ cutlass.Float32(0.0)
1057
+ if current_max == -cutlass.Float32.inf
1058
+ else current_max
1059
+ )
1060
+ scaled_max = safe_max * scale_log2e
1061
+ probabilities = cute.math.exp2(
1062
+ score_row * scale_log2e - scaled_max, fastmath=True
1063
+ )
1064
+ row_scale[m] = cute.math.exp2(
1065
+ (previous_max - safe_max) * scale_log2e, fastmath=True
1066
+ )
1067
+ row_sum[m] = kernel_utils.fadd_reduce(
1068
+ probabilities,
1069
+ init_val=row_sum[m] * row_scale[m],
1070
+ arch=80,
1071
+ )
1072
+ scores_mn[m, None].store(probabilities)
1073
+ return row_scale
1074
+
1075
+
1076
+ @cute.jit
1077
+ def online_softmax_route(
1078
+ scores: cute.Tensor,
1079
+ coords: cute.Tensor,
1080
+ row_max: cute.Tensor,
1081
+ row_sum: cute.Tensor,
1082
+ scale_log2e: cutlass.Float32,
1083
+ group_start: cutlass.Int32,
1084
+ token_count: cutlass.Int32,
1085
+ ):
1086
+ scores_mn = layout_utils.reshape_acc_to_mn(scores)
1087
+ coords_mn = layout_utils.reshape_acc_to_mn(coords)
1088
+ row_scale = cute.make_rmem_tensor_like(row_max, cutlass.Float32)
1089
+ for m in cutlass.range_constexpr(cute.size(row_max)):
1090
+ score_row = scores_mn[m, None].load()
1091
+ current_max = kernel_utils.fmax_reduce(
1092
+ score_row, init_val=row_max[m], arch=80
1093
+ )
1094
+ current_max = cute.arch.warp_reduction_max(
1095
+ current_max, threads_in_group=4
1096
+ )
1097
+ previous_max = row_max[m]
1098
+ row_max[m] = current_max
1099
+ safe_max = (
1100
+ cutlass.Float32(0.0)
1101
+ if current_max == -cutlass.Float32.inf
1102
+ else current_max
1103
+ )
1104
+ probabilities = cute.math.exp2(
1105
+ score_row * scale_log2e - safe_max * scale_log2e,
1106
+ fastmath=True,
1107
+ )
1108
+ row_scale[m] = cute.math.exp2(
1109
+ (previous_max - safe_max) * scale_log2e, fastmath=True
1110
+ )
1111
+ masses = cute.make_rmem_tensor_like(
1112
+ scores_mn[m, None], cutlass.Float32
1113
+ )
1114
+ for n in cutlass.range_constexpr(cute.size(masses)):
1115
+ block = group_start + coords_mn[m, n][1]
1116
+ length = token_count - block * cutlass.Int32(N)
1117
+ if length > N:
1118
+ length = cutlass.Int32(N)
1119
+ if length < 0:
1120
+ length = cutlass.Int32(0)
1121
+ masses[n] = cutlass.Float32(probabilities[n]) * cutlass.Float32(
1122
+ length
1123
+ )
1124
+ row_sum[m] = kernel_utils.fadd_reduce(
1125
+ masses.load(),
1126
+ init_val=row_sum[m] * row_scale[m],
1127
+ arch=80,
1128
+ )
1129
+ scores_mn[m, None].store(probabilities)
1130
+ return row_scale
1131
+
1132
+
1133
+ @cute.jit
1134
+ def finalize_softmax(
1135
+ row_max: cute.Tensor,
1136
+ row_sum: cute.Tensor,
1137
+ scale_log2e: cutlass.Float32,
1138
+ ):
1139
+ row_sum.store(
1140
+ kernel_utils.warp_reduce(row_sum.load(), operator.add, width=4)
1141
+ )
1142
+ ratio = cute.make_rmem_tensor_like(row_sum, cutlass.Float32)
1143
+ lse = cute.make_rmem_tensor_like(row_sum, cutlass.Float32)
1144
+ for m in cutlass.range_constexpr(cute.size(row_sum)):
1145
+ total = row_sum[m]
1146
+ invalid = total == 0.0 or total != total
1147
+ ratio[m] = cute.arch.rcp_approx(total if not invalid else 1.0)
1148
+ lse[m] = (
1149
+ -cutlass.Float32.inf
1150
+ if invalid
1151
+ else (
1152
+ row_max[m] * scale_log2e
1153
+ + cute.math.log2(total, fastmath=True)
1154
+ )
1155
+ * 0.6931471805599453
1156
+ )
1157
+ return ratio, lse
1158
+
1159
+
1160
+ @cute.jit
1161
+ def rescale_o_for_next_acc(
1162
+ output: cute.Tensor,
1163
+ row_scale: cute.Tensor,
1164
+ ):
1165
+ output_mn = layout_utils.reshape_acc_to_mn(output)
1166
+ for m in cutlass.range_constexpr(cute.size(row_scale)):
1167
+ output_mn[m, None].store(
1168
+ output_mn[m, None].load() * row_scale[m]
1169
+ )
1170
+
1171
+
1172
+ __all__ = ["SolAttnForwardSm120"]
torch-ext/sol_attn/sm90/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Hopper backend."""
2
+
3
+ from .kernel import make_kernel
4
+
5
+ __all__ = ["make_kernel"]
torch-ext/sol_attn/sm90/_compat/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Local CuteDSL helper compatibility layer for the SOL_ATTN SM90 kernel."""
torch-ext/sol_attn/sm90/_compat/activation.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import cutlass.cute as cute
2
+
3
+
4
+ def sub_packed_f32x2(a, b):
5
+ return cute.arch.add_packed_f32x2(a, (-b[0], -b[1]))
torch-ext/sol_attn/sm90/_compat/copy_utils.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CuTe copy helpers used by the Hopper mainloop."""
2
+
3
+ from typing import Callable
4
+
5
+ import cutlass
6
+ import cutlass.cute as cute
7
+ from cutlass import const_expr
8
+ from cutlass import pipeline
9
+ from cutlass.cute.nvgpu import cpasync
10
+ from cutlass.cutlass_dsl import dsl_user_op
11
+
12
+
13
+ _RAGGED_BASE = 2**30
14
+ _RAGGED_LIMIT = 2**31 - 1
15
+ _RAGGED_WRAP_STRIDE = 2**64 // _RAGGED_BASE
16
+
17
+
18
+ @dsl_user_op
19
+ def create_ragged_tensor_for_tma(
20
+ tensor: cute.Tensor,
21
+ ragged_dim: int = 0,
22
+ ptr_shift: bool = False,
23
+ *,
24
+ loc=None,
25
+ ip=None,
26
+ ) -> cute.Tensor:
27
+ rank = cute.rank(tensor)
28
+ if ragged_dim < 0:
29
+ ragged_dim += rank
30
+ if ptr_shift:
31
+ shape = (
32
+ tensor.shape[:ragged_dim]
33
+ + (_RAGGED_BASE,)
34
+ + tensor.shape[ragged_dim + 1 :]
35
+ + (_RAGGED_LIMIT,)
36
+ )
37
+ stride = tensor.stride + (tensor.stride[ragged_dim],)
38
+ offset = (
39
+ (None,) * ragged_dim
40
+ + (-_RAGGED_BASE,)
41
+ + (None,) * (rank - ragged_dim - 1)
42
+ )
43
+ pointer = cute.domain_offset(offset, tensor).iterator
44
+ return cute.make_tensor(
45
+ pointer,
46
+ cute.make_layout(shape, stride=stride),
47
+ )
48
+
49
+ ragged_stride = tensor.stride[ragged_dim]
50
+ shape = (
51
+ tensor.shape[:ragged_dim]
52
+ + (_RAGGED_BASE,)
53
+ + tensor.shape[ragged_dim + 1 :]
54
+ + (_RAGGED_LIMIT, _RAGGED_LIMIT)
55
+ )
56
+ stride = (
57
+ tensor.stride[:ragged_dim]
58
+ + (ragged_stride,)
59
+ + tensor.stride[ragged_dim + 1 :]
60
+ + (_RAGGED_WRAP_STRIDE - ragged_stride, ragged_stride)
61
+ )
62
+ return cute.make_tensor(
63
+ tensor.iterator,
64
+ cute.make_layout(shape, stride=stride),
65
+ )
66
+
67
+
68
+ def tma_get_copy_fn(
69
+ atom: cute.CopyAtom,
70
+ cta_coord: cute.Coord,
71
+ cta_layout: cute.Layout,
72
+ src_tensor: cute.Tensor,
73
+ dst_tensor: cute.Tensor,
74
+ filter_zeros: bool = False,
75
+ single_stage: bool = False,
76
+ *,
77
+ loc=None,
78
+ ip=None,
79
+ **kwargs,
80
+ ) -> Callable:
81
+ source_is_smem = const_expr(
82
+ isinstance(src_tensor.iterator, cute.Pointer)
83
+ and src_tensor.memspace == cute.AddressSpace.smem
84
+ )
85
+ smem, gmem = (
86
+ (src_tensor, dst_tensor)
87
+ if source_is_smem
88
+ else (dst_tensor, src_tensor)
89
+ )
90
+ smem_rank = const_expr(cute.rank(smem) - (0 if single_stage else 1))
91
+ gmem_rank = const_expr(cute.rank(gmem) - (0 if single_stage else 1))
92
+ smem, gmem = cpasync.tma_partition(
93
+ atom,
94
+ cta_coord,
95
+ cta_layout,
96
+ cute.group_modes(smem, 0, smem_rank),
97
+ cute.group_modes(gmem, 0, gmem_rank),
98
+ loc=loc,
99
+ ip=ip,
100
+ )
101
+ if const_expr(filter_zeros):
102
+ smem = cute.filter_zeros(smem)
103
+ gmem = cute.filter_zeros(gmem)
104
+ source, destination = (
105
+ (smem, gmem) if source_is_smem else (gmem, smem)
106
+ )
107
+
108
+ @dsl_user_op
109
+ def copy_tma(
110
+ src_idx,
111
+ dst_idx,
112
+ *,
113
+ loc=None,
114
+ ip=None,
115
+ **call_kwargs,
116
+ ):
117
+ cute.copy(
118
+ atom,
119
+ source[None, src_idx],
120
+ destination[None, dst_idx],
121
+ **call_kwargs,
122
+ **kwargs,
123
+ loc=loc,
124
+ ip=ip,
125
+ )
126
+
127
+ @dsl_user_op
128
+ def copy_single_stage(*, loc=None, ip=None, **call_kwargs):
129
+ cute.copy(
130
+ atom,
131
+ source,
132
+ destination,
133
+ **call_kwargs,
134
+ **kwargs,
135
+ loc=loc,
136
+ ip=ip,
137
+ )
138
+
139
+ return (
140
+ copy_tma if const_expr(not single_stage) else copy_single_stage,
141
+ smem,
142
+ gmem,
143
+ )
144
+
145
+
146
+ def tma_producer_copy_fn(
147
+ copy: Callable,
148
+ copy_pipeline: pipeline.PipelineAsync,
149
+ ):
150
+ def copy_fn(
151
+ src_idx,
152
+ producer_state: pipeline.PipelineState,
153
+ **kwargs,
154
+ ):
155
+ copy(
156
+ src_idx=src_idx,
157
+ dst_idx=producer_state.index,
158
+ tma_bar_ptr=copy_pipeline.producer_get_barrier(producer_state),
159
+ **kwargs,
160
+ )
161
+
162
+ return copy_fn
163
+
164
+
165
+ __all__ = [
166
+ "create_ragged_tensor_for_tma",
167
+ "tma_get_copy_fn",
168
+ "tma_producer_copy_fn",
169
+ ]