Instructions to use Efficient-Large-Model/Sol-Attn-Kernel-Source with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Kernels
How to use Efficient-Large-Model/Sol-Attn-Kernel-Source with Kernels:
# !pip install kernels from kernels import get_kernel kernel = get_kernel("Efficient-Large-Model/Sol-Attn-Kernel-Source") - Notebooks
- Google Colab
- Kaggle
Add Sol-Attn Kernel Builder source
Browse filesPinned 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
- .gitignore +4 -0
- CARD.md +57 -0
- README.md +40 -0
- SOURCE.md +23 -0
- benchmarks/benchmark.py +58 -0
- build.toml +21 -0
- example.py +18 -0
- flake.lock +117 -0
- flake.nix +17 -0
- tests/test_sol_attn.py +89 -0
- tools/verify_upstream.py +115 -0
- torch-ext/sol_attn/THIRD_PARTY_NOTICES.md +15 -0
- torch-ext/sol_attn/__init__.py +5 -0
- torch-ext/sol_attn/_vendor/__init__.py +1 -0
- torch-ext/sol_attn/_vendor/flash_attn/__init__.py +1 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/__init__.py +1 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/ampere_helpers.py +103 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/block_info.py +139 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/block_sparsity.py +463 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/cute_dsl_utils.py +129 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/fast_math.py +21 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/flash_fwd.py +1218 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/mask.py +712 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/named_barrier.py +47 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/pack_gqa.py +263 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/pipeline.py +402 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/seqlen_info.py +290 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/softmax.py +639 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/tile_scheduler.py +1087 -0
- torch-ext/sol_attn/_vendor/flash_attn/cute/utils.py +800 -0
- torch-ext/sol_attn/common/__init__.py +5 -0
- torch-ext/sol_attn/common/layout_utils.py +130 -0
- torch-ext/sol_attn/common/runtime.py +14 -0
- torch-ext/sol_attn/common/selector.py +169 -0
- torch-ext/sol_attn/interface.py +399 -0
- torch-ext/sol_attn/preprocess.py +463 -0
- torch-ext/sol_attn/sm100/LICENSE.flash-attention +29 -0
- torch-ext/sol_attn/sm100/__init__.py +5 -0
- torch-ext/sol_attn/sm100/kernel.py +5 -0
- torch-ext/sol_attn/sm100/mainloop.py +1762 -0
- torch-ext/sol_attn/sm100/math.py +29 -0
- torch-ext/sol_attn/sm100/softmax.py +156 -0
- torch-ext/sol_attn/sm100/tmem.py +138 -0
- torch-ext/sol_attn/sm120/__init__.py +5 -0
- torch-ext/sol_attn/sm120/kernel.py +19 -0
- torch-ext/sol_attn/sm120/mainloop.py +1172 -0
- torch-ext/sol_attn/sm90/__init__.py +5 -0
- torch-ext/sol_attn/sm90/_compat/__init__.py +1 -0
- torch-ext/sol_attn/sm90/_compat/activation.py +5 -0
- 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 |
+
]
|