OpenOneRec commited on
Commit
03c3dff
·
verified ·
1 Parent(s): 74f28be

Release KSA-V2-1.9B-base weights and inference runtime

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -34
  2. LICENSE +201 -0
  3. README.md +227 -0
  4. README_zh.md +212 -0
  5. assets/figures/ksa_v2_compression.png +3 -0
  6. assets/figures/ksa_v2_overview.png +3 -0
  7. assets/figures/ksa_v2_results.png +3 -0
  8. assets/figures/ksa_v2_sparse.png +3 -0
  9. cache_ring_buffer.py +892 -0
  10. config.json +79 -0
  11. elementwise_fusion.py +75 -0
  12. generate.py +76 -0
  13. generation_config.json +9 -0
  14. hierarchical_summary_attention.py +381 -0
  15. indexer_step.py +40 -0
  16. kernel/deps/flash_attn/AUTHORS +5 -0
  17. kernel/deps/flash_attn/LICENSE +29 -0
  18. kernel/deps/flash_attn/__init__.py +4 -0
  19. kernel/deps/flash_attn/cute/__init__.py +21 -0
  20. kernel/deps/flash_attn/cute/ampere_helpers.py +103 -0
  21. kernel/deps/flash_attn/cute/barrier.py +71 -0
  22. kernel/deps/flash_attn/cute/blackwell_helpers.py +753 -0
  23. kernel/deps/flash_attn/cute/block_info.py +108 -0
  24. kernel/deps/flash_attn/cute/block_sparse_utils.py +1448 -0
  25. kernel/deps/flash_attn/cute/block_sparsity.py +440 -0
  26. kernel/deps/flash_attn/cute/compute_block_sparsity.py +378 -0
  27. kernel/deps/flash_attn/cute/copy_utils.py +340 -0
  28. kernel/deps/flash_attn/cute/cute_dsl_utils.py +144 -0
  29. kernel/deps/flash_attn/cute/fast_math.py +21 -0
  30. kernel/deps/flash_attn/cute/flash_bwd.py +1262 -0
  31. kernel/deps/flash_attn/cute/flash_bwd_postprocess.py +463 -0
  32. kernel/deps/flash_attn/cute/flash_bwd_preprocess.py +365 -0
  33. kernel/deps/flash_attn/cute/flash_bwd_sm100.py +0 -0
  34. kernel/deps/flash_attn/cute/flash_bwd_sm90.py +1708 -0
  35. kernel/deps/flash_attn/cute/flash_fwd.py +0 -0
  36. kernel/deps/flash_attn/cute/flash_fwd_combine.py +704 -0
  37. kernel/deps/flash_attn/cute/flash_fwd_sm100.py +0 -0
  38. kernel/deps/flash_attn/cute/hopper_helpers.py +101 -0
  39. kernel/deps/flash_attn/cute/interface.py +1762 -0
  40. kernel/deps/flash_attn/cute/mask.py +651 -0
  41. kernel/deps/flash_attn/cute/mma_sm100_desc.py +291 -0
  42. kernel/deps/flash_attn/cute/named_barrier.py +31 -0
  43. kernel/deps/flash_attn/cute/pack_gqa.py +164 -0
  44. kernel/deps/flash_attn/cute/paged_kv.py +214 -0
  45. kernel/deps/flash_attn/cute/pipeline.py +272 -0
  46. kernel/deps/flash_attn/cute/seqlen_info.py +138 -0
  47. kernel/deps/flash_attn/cute/softmax.py +582 -0
  48. kernel/deps/flash_attn/cute/tile_scheduler.py +719 -0
  49. kernel/deps/flash_attn/cute/utils.py +859 -0
  50. kernel/ksa_attn/__init__.py +7 -0
.gitattributes CHANGED
@@ -1,35 +1,5 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
  *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  *.safetensors filter=lfs diff=lfs merge=lfs -text
2
+ assets/figures/ksa_v2_compression.png filter=lfs diff=lfs merge=lfs -text
3
+ assets/figures/ksa_v2_overview.png filter=lfs diff=lfs merge=lfs -text
4
+ assets/figures/ksa_v2_results.png filter=lfs diff=lfs merge=lfs -text
5
+ assets/figures/ksa_v2_sparse.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ - zh
6
+ library_name: transformers
7
+ pipeline_tag: text-generation
8
+ tags:
9
+ - ksa
10
+ - ksa-v2
11
+ - custom_code
12
+ - long-context
13
+ ---
14
+
15
+
16
+
17
+ <div align="center">
18
+ <h1>KSA V2 1.9B</h1>
19
+ <p><strong>Summary tokens as long-range memory and sparse indexes</strong></p>
20
+ <p>
21
+ <a href="https://arxiv.org/abs/2610.18654"><img alt="KSA V2 Paper" src="https://img.shields.io/badge/KSA_V2-arXiv%3A2610.18654-b31b1b?logo=arxiv" /></a>
22
+ <a href="https://arxiv.org/abs/2604.24432"><img alt="KSA Paper" src="https://img.shields.io/badge/KSA-arXiv%3A2604.24432-b31b1b?logo=arxiv" /></a>
23
+ <a href="https://github.com/Kuaishou-OneRec/KSA"><img alt="GitHub" src="https://img.shields.io/badge/GitHub-Kuaishou--OneRec-black?logo=github" /></a>
24
+ <a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-green" /></a>
25
+ </p>
26
+ <p><a href="README.md">English</a> | <a href="README_zh.md">中文</a></p>
27
+ </div>
28
+
29
+ ## 📖 Introduction
30
+
31
+ **Kwai Summary Attention (KSA)** is our attention architecture for efficient
32
+ long-context modeling. Learnable Summary tokens at fixed chunk boundaries
33
+ preserve a compressed representation of distant history.
34
+
35
+ **KSA V2** gives these Summaries two complementary roles:
36
+
37
+ - **Compressed KSA (C-KSA)** aggregates Summary KV into compact Meta KV for
38
+ more efficient long-range memory.
39
+ - **Sparse KSA (S-KSA)** uses Summaries to build block-level routing keys.
40
+ A lightweight Indexer selects relevant blocks, whose original KV is read
41
+ directly by the main attention.
42
+
43
+ We retain Hybrid KSA's 3:1 layer layout, replacing KSA layers with C-KSA and
44
+ Full layers with S-KSA.
45
+
46
+ <p align="center"><img src="assets/figures/ksa_v2_overview.png" width="100%" alt="From Hybrid KSA to KSA V2: the same Summary layout, with C-KSA compression and S-KSA block routing." /></p>
47
+ <p align="center"><em>From Hybrid KSA to KSA V2: shared Summaries, layer-local KV compression and sparse original-token access.</em></p>
48
+
49
+ This base model builds on our **1.9B Hybrid KSA backbone trained from scratch**. It is not an instruction-tuned model.
50
+
51
+ ## 🔥 News
52
+
53
+ - **2026-10-13** — [KSA V2 technical report](https://arxiv.org/abs/2610.18654) released.
54
+ - **2026-05-08** — [KSA-4B-base](https://huggingface.co/OpenOneRec/KSA-4B-base) weights released.
55
+ - **2026-04-28** — [KSA technical report](https://arxiv.org/abs/2604.24432) and training code released.
56
+
57
+ ## ✨ Method
58
+
59
+ **C-KSA: Summary → Meta KV.** Learnable dimension-wise pooling compresses groups
60
+ of Summary KV within each attention layer. Summary tokens continue through
61
+ the backbone at their original chunk granularity; stronger KV compression does
62
+ not require a new large-chunk backbone. Each query attends to remote Meta,
63
+ boundary Summary and local text KV. Meta entries remain local to the layer
64
+ that constructs them; they are not propagated as new tokens.
65
+
66
+ <p align="center"><img src="assets/figures/ksa_v2_compression.png" width="80%" alt="C-KSA compresses complete remote Summary groups into Meta KV while retaining boundary Summaries and local text KV." /></p>
67
+ <p align="center"><em>C-KSA hierarchical compression, illustrated with chunk size 4 and two Summaries per Meta entry.</em></p>
68
+
69
+ **S-KSA: Summary → block index.** A Summary query aggregates each text block
70
+ into a representation for low-dimensional routing. Main attention reads the
71
+ selected blocks' original KV together with sinks and local context. A
72
+ blockwise-max dense-attention teacher supervises the Indexer.
73
+
74
+ <p align="center"><img src="assets/figures/ksa_v2_sparse.png" width="80%" alt="S-KSA uses Summary-guided block aggregation and an Indexer to select remote text blocks, then attends to their original KV together with sinks and local context." /></p>
75
+ <p align="center"><em>S-KSA block routing, illustrated with chunk size 4 and block size 2. Released model configurations are listed below.</em></p>
76
+
77
+ **Three adaptation stages.** Compression-module warmup → full-parameter C-KSA
78
+ CPT → S-KSA aggregation and Indexer warmup. The final stage freezes inherited
79
+ parameters; its checkpoint is exported directly for sparse inference.
80
+
81
+ ## 🤖 Model configuration
82
+
83
+ This directory contains **KSA-v2-1.9B**.
84
+
85
+ | Configuration | KSA-v2-1.9B | KSA-v2-4B |
86
+ | :--- | :--- | :--- |
87
+ | Number of layers | 24 | 36 |
88
+ | Hidden size | 2048 | 2560 |
89
+ | Intermediate size | 6144 | 9728 |
90
+ | Attention heads (Q / KV) | 16 / 16 (MHA) | 32 / 8 (GQA) |
91
+ | Head dimension | 128 | 128 |
92
+ | Hybrid ratio (C-KSA : S-KSA) | 3 : 1 | 3 : 1 |
93
+ | Maximum context (text tokens) | 131072 | 131072 |
94
+ | Summary chunk size | 8 | 8 |
95
+ | C-KSA sliding chunk number | 128 | 128 |
96
+ | C-KSA group size | 128 | 32 |
97
+ | S-KSA block size | 4 | 4 |
98
+ | S-KSA remote token budget | 4096 | 4096 |
99
+ | Sink tokens | 8 | 8 |
100
+ | S-KSA local window (text tokens) | 1024 | 1024 |
101
+ | Indexer heads × head dimension | 8 × 128 | 8 × 128 |
102
+ | Indexer query rank | 512 | 512 |
103
+ | Indexer RoPE dimension | 64 | 64 |
104
+ | Indexer Q/K RMSNorm | False | True |
105
+ | Teacher pooling | max | max |
106
+ | Tied embeddings | False | True |
107
+
108
+ The remote budget includes eight sink tokens; the local window is additional.
109
+
110
+ Weights are stored in FP32 and loaded in BF16.
111
+
112
+ ## 📈 Results
113
+
114
+ <p align="center"><img src="assets/figures/ksa_v2_results.png" width="85%" alt="The technical report's first-page 1.9B results: long-context retrieval and general capabilities, and theoretical per-token inference FLOPs and KV cache size." /></p>
115
+ <p align="center"><em>1.9B results: long-context and general capabilities (left), theoretical per-token inference FLOPs and KV cache size (right).</em></p>
116
+
117
+ We evaluate KSA V2 on two Hybrid KSA backbones: a 1.9B model trained from
118
+ scratch and a 4B model continually pretrained from Qwen3-4B. Both undergo
119
+ three-stage adaptation; original baseline results are reused from KSA V1.
120
+
121
+ ### 1.9B: Long-context retrieval
122
+
123
+ C-KSA uses group128 and S-KSA uses block4. Hybrid KSA is the parent model before adaptation.
124
+
125
+ | Model | 4K | 8K | 16K | 32K | 64K | 128K | Mean |
126
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
127
+ | Full | 76.08 | 72.85 | 73.24 | 69.06 | 65.32 | 48.75 | 67.55 |
128
+ | Hybrid SWA | 74.54 | 71.69 | 69.54 | 67.86 | 63.03 | 56.64 | 67.22 |
129
+ | Hybrid SCA | 77.72 | 75.22 | 72.55 | 67.74 | 63.54 | 58.01 | 69.13 |
130
+ | Hybrid GDN | 79.83 | 76.01 | 74.04 | 70.41 | 69.39 | 59.87 | 71.59 |
131
+ | Pure KSA | 70.44 | 65.91 | 66.74 | 62.54 | 57.13 | 39.29 | 60.34 |
132
+ | Hybrid KSA | 80.65 | 73.35 | 74.07 | 72.30 | 69.95 | 65.35 | 72.61 |
133
+ | C-KSA + Full | 81.52 | 72.24 | 74.44 | 71.93 | 69.61 | 66.31 | 72.68 |
134
+ | **C-KSA + S-KSA** | 81.48 | 72.42 | 75.67 | 70.78 | 68.99 | 65.01 | 72.39 |
135
+
136
+ ### 4B: Scaling to a GQA backbone
137
+
138
+ C-KSA uses group32 and S-KSA uses block4, preserving the backbone's GQA structure.
139
+
140
+ | Model | 4K | 8K | 16K | 32K | 64K | 128K | Mean |
141
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
142
+ | Full | 92.88 | 91.38 | 89.12 | 84.74 | 78.16 | 65.86 | 83.69 |
143
+ | Hybrid SWA | 91.30 | 88.03 | 82.87 | 78.94 | 73.88 | 66.27 | 80.22 |
144
+ | Hybrid SCA | 86.02 | 84.28 | 80.67 | 76.89 | 68.88 | 60.94 | 76.28 |
145
+ | Hybrid Linear | 86.39 | 83.86 | 78.06 | 76.48 | 73.50 | 67.98 | 77.71 |
146
+ | Pure KSA | 91.55 | 86.78 | 84.78 | 80.30 | 76.09 | 66.81 | 81.05 |
147
+ | Hybrid KSA | 92.97 | 90.53 | 88.86 | 86.65 | 76.04 | 71.67 | 84.45 |
148
+ | Hybrid KSA + CPT | 92.22 | 89.85 | 87.20 | 83.09 | 74.74 | 72.03 | 83.19 |
149
+ | C-KSA + Full | 92.77 | 88.12 | 86.27 | 82.68 | 78.58 | 73.92 | 83.72 |
150
+ | **C-KSA + S-KSA** | 93.38 | 87.78 | 85.45 | 83.60 | 80.28 | 74.41 | 84.15 |
151
+
152
+ Hybrid KSA + CPT is a continuation control with no architectural changes.
153
+
154
+ See the technical report for further ablations and inference efficiency.
155
+
156
+ ## Quick start
157
+
158
+ Linux, Python 3.12 and H100/H800. Download the **entire model directory**,
159
+ including `kernel/`, and run from it:
160
+
161
+ ```bash
162
+ python -m pip install torch==2.6.0 --index-url https://download.pytorch.org/whl/cu126
163
+ python -m pip install -r requirements.txt
164
+
165
+ CUDA_VISIBLE_DEVICES=0 python generate.py \
166
+ --prompt "The main advantage of long-context language models is" \
167
+ --max-new-tokens 128
168
+ ```
169
+
170
+ Omit `--prompt` for interactive completion. The optional `--apply-chat-template`
171
+ only applies the tokenizer template; it does not add instruction tuning.
172
+
173
+ Transformers 5.0.0 and the bundled `ksa_attn` / FlashAttention CuTe sources are
174
+ used. No training checkout, kernel build or FlashAttention 2 CUDA extension
175
+ is required.
176
+
177
+ ### Transformers usage
178
+
179
+ Set the bundled kernel paths before importing Transformers:
180
+
181
+ ```bash
182
+ export PYTHONPATH="$PWD/kernel:$PWD/kernel/deps${PYTHONPATH:+:$PYTHONPATH}"
183
+ ```
184
+
185
+ ```python
186
+ import torch
187
+ from transformers import AutoModelForCausalLM, AutoTokenizer
188
+
189
+ tokenizer = AutoTokenizer.from_pretrained(".", local_files_only=True)
190
+ model = AutoModelForCausalLM.from_pretrained(
191
+ ".",
192
+ trust_remote_code=True,
193
+ local_files_only=True,
194
+ dtype=torch.bfloat16,
195
+ device_map={"": "cuda:0"},
196
+ attn_implementation="sdpa",
197
+ ).eval()
198
+
199
+ inputs = tokenizer("The main advantage of long-context models is", return_tensors="pt")
200
+ inputs = inputs.to(model.device)
201
+ with torch.inference_mode():
202
+ output = model.generate(
203
+ **inputs,
204
+ max_new_tokens=128,
205
+ do_sample=False,
206
+ pad_token_id=tokenizer.eos_token_id,
207
+ )
208
+ print(tokenizer.decode(output[0, inputs.input_ids.shape[1]:], skip_special_tokens=True))
209
+ ```
210
+
211
+ ## License and citation
212
+
213
+ Weights and our runtime: [Apache-2.0](LICENSE). Qwen-derived files retain their
214
+ copyright notices. Bundled FlashAttention CuTe retains its
215
+ [BSD-3-Clause license](kernel/deps/flash_attn/LICENSE) and
216
+ [AUTHORS](kernel/deps/flash_attn/AUTHORS); its initializer exposes CuTe without
217
+ the separate FlashAttention 2 extension.
218
+
219
+ ```bibtex
220
+ @techreport{kwai2026ksav2,
221
+ title = {Kwai Summary Attention V2 Technical Report},
222
+ author = {OneRec Team},
223
+ year = {2026},
224
+ institution = {Kuaishou Technology},
225
+ url = {https://arxiv.org/abs/2610.18654}
226
+ }
227
+ ```
README_zh.md ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ - zh
6
+ library_name: transformers
7
+ pipeline_tag: text-generation
8
+ tags:
9
+ - ksa
10
+ - ksa-v2
11
+ - custom_code
12
+ - long-context
13
+ ---
14
+
15
+
16
+
17
+ <div align="center">
18
+ <h1>KSA V2 1.9B</h1>
19
+ <p><strong>用 Summary 同时构建长程记忆与稀疏索引</strong></p>
20
+ <p>
21
+ <a href="https://arxiv.org/abs/2610.18654"><img alt="KSA V2 Paper" src="https://img.shields.io/badge/KSA_V2-arXiv%3A2610.18654-b31b1b?logo=arxiv" /></a>
22
+ <a href="https://arxiv.org/abs/2604.24432"><img alt="KSA Paper" src="https://img.shields.io/badge/KSA-arXiv%3A2604.24432-b31b1b?logo=arxiv" /></a>
23
+ <a href="https://github.com/Kuaishou-OneRec/KSA"><img alt="GitHub" src="https://img.shields.io/badge/GitHub-Kuaishou--OneRec-black?logo=github" /></a>
24
+ <a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-green" /></a>
25
+ </p>
26
+ <p><a href="README.md">English</a> | <a href="README_zh.md">中文</a></p>
27
+ </div>
28
+
29
+ ## 📖 简介
30
+
31
+ **Kwai Summary Attention(KSA)** 是我们面向长上下文建模的高效注意力机制。
32
+ KSA 在固定 chunk 边界插入可学习的 Summary Token,以压缩后的状态保留对远端历史的访问。
33
+
34
+ **KSA V2** 进一步让 Summary 承担两个角色:
35
+
36
+ - **Compressed KSA(C-KSA)**:将多份 Summary KV 聚合为 Meta KV,进一步降低长程记忆的存储开销。
37
+ - **Sparse KSA(S-KSA)**:以 Summary 聚合原文块的选路表示,用轻量 Indexer 选择相关块,直接读取这些块的原始 KV。
38
+
39
+ 我们保留 Hybrid KSA 的 3:1 层间布局,将 KSA 层替换为 C-KSA、Full 层替换为 S-KSA,
40
+ 分别优化长程记忆和原文读取。
41
+
42
+ <p align="center"><img src="assets/figures/ksa_v2_overview.png" width="100%" alt="从 Hybrid KSA 到 KSA V2:保持 Summary 布局,引入 C-KSA 压缩与 S-KSA 原文块选路。" /></p>
43
+ <p align="center"><em>从 Hybrid KSA 到 KSA V2:复用 Summary,在注意力层内压缩 KV,并稀疏读取原文。</em></p>
44
+
45
+ 本模型基于**从零训练的 1.9B Hybrid KSA 主干**进行适配,是未经过指令微调的基础模型。
46
+
47
+ ## 🔥 新闻
48
+
49
+ - **2026-10-13** —— [KSA V2 技术报告](https://arxiv.org/abs/2610.18654)发布。
50
+ - **2026-05-08** —— [KSA-4B-base](https://huggingface.co/OpenOneRec/KSA-4B-base) 权重发布。
51
+ - **2026-04-28** —— [KSA 技术报告](https://arxiv.org/abs/2604.24432)与训练代码发布。
52
+
53
+ ## ✨ 方法
54
+
55
+ **C-KSA:Summary → Meta KV。** 在注意力层内进行可学习的逐维聚合。
56
+ 每组 Summary 生成一份更紧凑的 Meta KV;Summary Token 本身仍按原 chunk 粒度传递,
57
+ 不必为增加压缩率重新训练更大 chunk 的主干。查询同时访问远程 Meta、边界 Summary
58
+ 和局部原文 KV;Meta 仅在当前层内使用,不作为新 token 传递。
59
+
60
+ <p align="center"><img src="assets/figures/ksa_v2_compression.png" width="80%" alt="C-KSA:将完整的远程 Summary 分组压缩为 Meta KV,保留边界 Summary 与局部原文 KV。" /></p>
61
+ <p align="center"><em>C-KSA 两级压缩示意,图中 chunk 大小为 4,每 2 个 Summary 聚合为一个 Meta。</em></p>
62
+
63
+ **S-KSA:Summary → 块级索引。** 以 Summary 为 query 聚合原文块,再映射到低维索引空间。
64
+ 主注意力读取选中块的原始 KV,并保留 sink 与局部窗口。Indexer 用按块取 max 的
65
+ dense attention teacher 监督训练。
66
+
67
+ <p align="center"><img src="assets/figures/ksa_v2_sparse.png" width="80%" alt="S-KSA:Summary 引导块内聚合,Indexer 选择远程原文块,主注意力读取其原始 KV 以及 sink 和局部窗口。" /></p>
68
+ <p align="center"><em>S-KSA 块级选路示意,图中 chunk 大小为 4、block 大小为 2;发布模型的实际配置见下表。</em></p>
69
+
70
+ **三个适配阶段。** 压缩模块 warmup → C-KSA 全参数 CPT → S-KSA 聚合模块与 Indexer warmup。
71
+ 最后一阶段冻结继承参数,完成后直接导出用于稀疏推理。
72
+
73
+ ## 🤖 模型配置
74
+
75
+ 当前目录为 **KSA-v2-1.9B**。
76
+
77
+ | 配置项 | KSA-v2-1.9B | KSA-v2-4B |
78
+ | :--- | :--- | :--- |
79
+ | 层数 | 24 | 36 |
80
+ | Hidden size | 2048 | 2560 |
81
+ | Intermediate size | 6144 | 9728 |
82
+ | Attention heads(Q / KV) | 16 / 16(MHA) | 32 / 8(GQA) |
83
+ | Head dimension | 128 | 128 |
84
+ | Hybrid 比例(C-KSA : S-KSA) | 3 : 1 | 3 : 1 |
85
+ | 最大上下文(原文 token) | 131072 | 131072 |
86
+ | Summary chunk size | 8 | 8 |
87
+ | C-KSA sliding chunk number | 128 | 128 |
88
+ | C-KSA group size | 128 | 32 |
89
+ | S-KSA block size | 4 | 4 |
90
+ | S-KSA 远程 token 预算 | 4096 | 4096 |
91
+ | Sink tokens | 8 | 8 |
92
+ | S-KSA 局部窗口(原文 token) | 1024 | 1024 |
93
+ | Indexer heads × head dimension | 8 × 128 | 8 × 128 |
94
+ | Indexer query rank | 512 | 512 |
95
+ | Indexer RoPE dimension | 64 | 64 |
96
+ | Indexer Q/K RMSNorm | False | True |
97
+ | Teacher 聚合 | max | max |
98
+ | Tied embeddings | False | True |
99
+
100
+ 远程预算包含 8 个 sink token,局部窗口另计。
101
+
102
+ 权重以 FP32 保存,推理时以 BF16 加载。
103
+
104
+ ## 📈 实验结果
105
+
106
+ <p align="center"><img src="assets/figures/ksa_v2_results.png" width="85%" alt="技术报告第一页的 1.9B 主结果图:长上下文检索与通用能力,以及单 token 推理 FLOPs 和 KV cache 大小的理论值。" /></p>
107
+ <p align="center"><em>1.9B 主结果:长上下文与通用能力(左),单 token 推理 FLOPs 与 KV cache 大小的理论值(右)。</em></p>
108
+
109
+ 我们在两种 Hybrid KSA 主干上验证 KSA V2:从零训练的 1.9B,以及基于 Qwen3-4B
110
+ 继续预训练的 4B。两者均经过三阶段适配,原始 baseline 结果沿用 KSA V1。
111
+
112
+ ### 1.9B:长上下文检索
113
+
114
+ C-KSA 使用 group128,S-KSA 使用 block4;Hybrid KSA 是适配前的父模型。
115
+
116
+ | 模型 | 4K | 8K | 16K | 32K | 64K | 128K | Mean |
117
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
118
+ | Full | 76.08 | 72.85 | 73.24 | 69.06 | 65.32 | 48.75 | 67.55 |
119
+ | Hybrid SWA | 74.54 | 71.69 | 69.54 | 67.86 | 63.03 | 56.64 | 67.22 |
120
+ | Hybrid SCA | 77.72 | 75.22 | 72.55 | 67.74 | 63.54 | 58.01 | 69.13 |
121
+ | Hybrid GDN | 79.83 | 76.01 | 74.04 | 70.41 | 69.39 | 59.87 | 71.59 |
122
+ | Pure KSA | 70.44 | 65.91 | 66.74 | 62.54 | 57.13 | 39.29 | 60.34 |
123
+ | Hybrid KSA | 80.65 | 73.35 | 74.07 | 72.30 | 69.95 | 65.35 | 72.61 |
124
+ | C-KSA + Full | 81.52 | 72.24 | 74.44 | 71.93 | 69.61 | 66.31 | 72.68 |
125
+ | **C-KSA + S-KSA** | 81.48 | 72.42 | 75.67 | 70.78 | 68.99 | 65.01 | 72.39 |
126
+
127
+ ### 4B:扩展到 GQA 主干
128
+
129
+ C-KSA 使用 group32,S-KSA 使用 block4,保留主干的 GQA 结构。
130
+
131
+ | 模型 | 4K | 8K | 16K | 32K | 64K | 128K | Mean |
132
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
133
+ | Full | 92.88 | 91.38 | 89.12 | 84.74 | 78.16 | 65.86 | 83.69 |
134
+ | Hybrid SWA | 91.30 | 88.03 | 82.87 | 78.94 | 73.88 | 66.27 | 80.22 |
135
+ | Hybrid SCA | 86.02 | 84.28 | 80.67 | 76.89 | 68.88 | 60.94 | 76.28 |
136
+ | Hybrid Linear | 86.39 | 83.86 | 78.06 | 76.48 | 73.50 | 67.98 | 77.71 |
137
+ | Pure KSA | 91.55 | 86.78 | 84.78 | 80.30 | 76.09 | 66.81 | 81.05 |
138
+ | Hybrid KSA | 92.97 | 90.53 | 88.86 | 86.65 | 76.04 | 71.67 | 84.45 |
139
+ | Hybrid KSA + CPT | 92.22 | 89.85 | 87.20 | 83.09 | 74.74 | 72.03 | 83.19 |
140
+ | C-KSA + Full | 92.77 | 88.12 | 86.27 | 82.68 | 78.58 | 73.92 | 83.72 |
141
+ | **C-KSA + S-KSA** | 93.38 | 87.78 | 85.45 | 83.60 | 80.28 | 74.41 | 84.15 |
142
+
143
+ Hybrid KSA + CPT 是不改模型结构的续训对照。
144
+
145
+ 更多消融实验和推理效率分析见技术报告。
146
+
147
+ ## 快速开始
148
+
149
+ Linux、Python 3.12、H100/H800。下载**完整模型目录**(包含 `kernel/`),并在该目录下执行:
150
+
151
+ ```bash
152
+ python -m pip install torch==2.6.0 --index-url https://download.pytorch.org/whl/cu126
153
+ python -m pip install -r requirements.txt
154
+
155
+ CUDA_VISIBLE_DEVICES=0 python generate.py \
156
+ --prompt "The main advantage of long-context language models is" \
157
+ --max-new-tokens 128
158
+ ```
159
+
160
+ 省略 `--prompt` 可进行交互式文本续写。`--apply-chat-template` 只应用 tokenizer 中的对话模板,不代表模型经过指令微调。
161
+
162
+ 使用 Transformers 5.0.0 和随包提供的 `ksa_attn`/FlashAttention CuTe 源码,无需训练仓库、单独编译内核或安装 FlashAttention 2 CUDA 扩展。
163
+
164
+ ### Transformers 用法
165
+
166
+ 导入 Transformers 前设置随包内核路径:
167
+
168
+ ```bash
169
+ export PYTHONPATH="$PWD/kernel:$PWD/kernel/deps${PYTHONPATH:+:$PYTHONPATH}"
170
+ ```
171
+
172
+ ```python
173
+ import torch
174
+ from transformers import AutoModelForCausalLM, AutoTokenizer
175
+
176
+ tokenizer = AutoTokenizer.from_pretrained(".", local_files_only=True)
177
+ model = AutoModelForCausalLM.from_pretrained(
178
+ ".",
179
+ trust_remote_code=True,
180
+ local_files_only=True,
181
+ dtype=torch.bfloat16,
182
+ device_map={"": "cuda:0"},
183
+ attn_implementation="sdpa",
184
+ ).eval()
185
+
186
+ inputs = tokenizer("The main advantage of long-context models is", return_tensors="pt")
187
+ inputs = inputs.to(model.device)
188
+ with torch.inference_mode():
189
+ output = model.generate(
190
+ **inputs,
191
+ max_new_tokens=128,
192
+ do_sample=False,
193
+ pad_token_id=tokenizer.eos_token_id,
194
+ )
195
+ print(tokenizer.decode(output[0, inputs.input_ids.shape[1]:], skip_special_tokens=True))
196
+ ```
197
+
198
+ ## 许可证与引用
199
+
200
+ 模型权重及我们的推理代码采用 [Apache-2.0](LICENSE)。基于 Qwen 的文件保留原版权声明。
201
+ 随包的 FlashAttention CuTe 保留其 [BSD-3-Clause 许可证](kernel/deps/flash_attn/LICENSE)
202
+ 与 [AUTHORS](kernel/deps/flash_attn/AUTHORS);入口仅加载 CuTe,不依赖单独的 FlashAttention 2 扩展。
203
+
204
+ ```bibtex
205
+ @techreport{kwai2026ksav2,
206
+ title = {Kwai Summary Attention V2 Technical Report},
207
+ author = {OneRec Team},
208
+ year = {2026},
209
+ institution = {Kuaishou Technology},
210
+ url = {https://arxiv.org/abs/2610.18654}
211
+ }
212
+ ```
assets/figures/ksa_v2_compression.png ADDED

Git LFS Details

  • SHA256: ff04e1cb08f2cab60f3095980042806c5fbcd9b6a77b3934fa0d379595469085
  • Pointer size: 131 Bytes
  • Size of remote file: 105 kB
assets/figures/ksa_v2_overview.png ADDED

Git LFS Details

  • SHA256: 7508b6a895f43d88fd8bc45d55e81bcb05ad7e02e19a9b69bdc50b746cf16eef
  • Pointer size: 131 Bytes
  • Size of remote file: 133 kB
assets/figures/ksa_v2_results.png ADDED

Git LFS Details

  • SHA256: 1d9602c8998e8c391dc81d5b9ab60ec20d6424267e3ebdfa76921bbc1372468e
  • Pointer size: 131 Bytes
  • Size of remote file: 152 kB
assets/figures/ksa_v2_sparse.png ADDED

Git LFS Details

  • SHA256: 7c0059cf52966b500b865700c0866174cfebbb7024559aa8b129ec6e00a0724d
  • Pointer size: 131 Bytes
  • Size of remote file: 127 kB
cache_ring_buffer.py ADDED
@@ -0,0 +1,892 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2025 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ from typing import Any, Callable, Optional, Union
18
+ import torch
19
+ from transformers.models.qwen3.configuration_qwen3 import Qwen3Config
20
+ from .runtime_config import _get_shared_batch_mask, _parse_config_pattern
21
+ from .layer_norm_rope import rotate_half
22
+
23
+
24
+ class Qwen3RingBufferCache:
25
+ """
26
+ Ring buffer KV cache with summary support.
27
+
28
+ Two strategies based on per-layer sliding_chunk_num:
29
+ - Large window layers (is_large_window=True): append-only buffer storing only text KV.
30
+ Summary KV is NOT stored since text tokens attend to all text KV directly.
31
+ - Small window layers (is_large_window=False):
32
+ Three buffers:
33
+ 1. key_cache: [ring(ws) | old_summaries(growing) | chunk_mirror(≤C)]
34
+ → attention input, steady state is a single contiguous slice
35
+ 2. new_summary_buf: ring buffer of size scn, stores summaries whose text
36
+ is still in the window (not needed for attention)
37
+ 3. chunk_buf: size C, holds current chunk's text KV
38
+
39
+ RoPE position information is baked into KV, so physical order doesn't matter.
40
+ """
41
+
42
+ is_compileable = False
43
+ _SUMMARY_INIT_CAP = 512
44
+ _APPEND_HEADROOM = 1024
45
+
46
+ def __init__(self, config: Qwen3Config, sliding_chunk_nums: list[int]):
47
+ super().__init__()
48
+ self.summary_chunk_size = getattr(config, "summary_chunk_size", 0)
49
+ self.summary_token_num = getattr(config, "summary_token_num", 0)
50
+ self.num_hidden_layers = config.num_hidden_layers
51
+
52
+ self.sliding_chunk_nums = sliding_chunk_nums
53
+ large_window_threshold = min(sliding_chunk_nums) * self.summary_chunk_size
54
+ self.is_large_window = [sv * self.summary_chunk_size > large_window_threshold for sv in sliding_chunk_nums]
55
+ self.window_sizes = [sv * self.summary_chunk_size for sv in sliding_chunk_nums]
56
+ pool_mode = getattr(config, "summary_meta_pool_mode", "none") or "none"
57
+ pool_groups = _parse_config_pattern(
58
+ getattr(config, "summary_meta_pool_group_size", 0) or 0
59
+ )
60
+ if not isinstance(pool_groups, list):
61
+ pool_groups = [int(pool_groups)] * config.num_hidden_layers
62
+ self.meta_pool_group_sizes = [int(v) for v in pool_groups]
63
+ self.meta_pool_enabled = [
64
+ pool_mode in ("mean", "scalar", "dimension_wise")
65
+ and group_size > 1
66
+ for group_size in self.meta_pool_group_sizes
67
+ ]
68
+ self.meta_pool_mode = pool_mode
69
+
70
+ self.key_cache = [None for _ in range(config.num_hidden_layers)]
71
+ self.value_cache = [None for _ in range(config.num_hidden_layers)]
72
+ # Full-layer Lightning Indexer keys are not main-attention K/V. The
73
+ # summary-indexer variant stores one key per raw chunk; token-indexer
74
+ # mode stores one key per raw text token.
75
+ self._sksa_index_keys = [None for _ in range(config.num_hidden_layers)]
76
+ self._sparse_gather_storage = {}
77
+ # Independent chunk-Q/K mode needs the auxiliary K states for the
78
+ # currently accumulating raw chunk. They are separate from the
79
+ # backbone attention cache and are discarded once its summary token is
80
+ # emitted.
81
+ self._sksa_chunk_k = [None for _ in range(config.num_hidden_layers)]
82
+ self._sksa_chunk_v = [None for _ in range(config.num_hidden_layers)]
83
+
84
+ # Large window: append-only
85
+ self._text_len = [0] * config.num_hidden_layers
86
+ self._capacity = [0] * config.num_hidden_layers
87
+
88
+ # Small window: ring buffer + summary
89
+ self._window_write_ptr = [0] * config.num_hidden_layers
90
+ self._n_valid_window = [0] * config.num_hidden_layers
91
+ self._old_summary_len = [0] * config.num_hidden_layers # old summaries in key_cache
92
+ self._old_summary_cap = [0] * config.num_hidden_layers
93
+ self._old_summary_total = [0] * config.num_hidden_layers
94
+
95
+ # Hierarchical old-summary state. Complete groups are stored as post-RoPE
96
+ # meta KV; fewer than group_size evicted summaries remain residual KV.
97
+ self._meta_key_cache = [None for _ in range(config.num_hidden_layers)]
98
+ self._meta_value_cache = [None for _ in range(config.num_hidden_layers)]
99
+ self._meta_len = [0] * config.num_hidden_layers
100
+ self._meta_cap = [0] * config.num_hidden_layers
101
+ self._residual_pre_key_buf = [None for _ in range(config.num_hidden_layers)]
102
+ self._residual_score_buf = [None for _ in range(config.num_hidden_layers)]
103
+ self._residual_cos_buf = [None for _ in range(config.num_hidden_layers)]
104
+ self._residual_sin_buf = [None for _ in range(config.num_hidden_layers)]
105
+ self._residual_position_buf = [None for _ in range(config.num_hidden_layers)]
106
+
107
+ # New summary ring buffer (small window only): summaries whose text is still in window
108
+ self._new_sum_key_buf = [None for _ in range(config.num_hidden_layers)]
109
+ self._new_sum_value_buf = [None for _ in range(config.num_hidden_layers)]
110
+ self._new_sum_pre_key_buf = [None for _ in range(config.num_hidden_layers)]
111
+ self._new_sum_score_buf = [None for _ in range(config.num_hidden_layers)]
112
+ self._new_sum_cos_buf = [None for _ in range(config.num_hidden_layers)]
113
+ self._new_sum_sin_buf = [None for _ in range(config.num_hidden_layers)]
114
+ self._new_sum_position_buf = [None for _ in range(config.num_hidden_layers)]
115
+ self._new_sum_len = [0] * config.num_hidden_layers # how many filled (≤ scn)
116
+ self._new_sum_write_ptr = [0] * config.num_hidden_layers # ring write pointer
117
+
118
+ # Mirror region length (small window only): partial chunk text KV count in key_cache
119
+ self._chunk_buf_len = [0] * config.num_hidden_layers
120
+
121
+ # Common
122
+ self.cur_chunk_sizes = [0] * config.num_hidden_layers
123
+ self.true_tokens = [0] * config.num_hidden_layers
124
+ self._total_chunks = [0] * config.num_hidden_layers # completed chunks count
125
+ self._reorganized = [False] * config.num_hidden_layers
126
+ self._prefill_text_positions = None
127
+ self._prefill_summary_positions = None
128
+
129
+
130
+
131
+
132
+ def __len__(self):
133
+ return self.num_hidden_layers
134
+
135
+ def get_seq_length(self, layer_idx: Optional[int] = 0) -> int:
136
+ """Returns nonzero only when this layer's cache is in post-reorganize state."""
137
+ if layer_idx >= self.num_hidden_layers:
138
+ return 0
139
+ if not self._reorganized[layer_idx]:
140
+ return 0
141
+ if self.is_large_window[layer_idx]:
142
+ return self._text_len[layer_idx]
143
+ else:
144
+ return (self._n_valid_window[layer_idx] + self._chunk_buf_len[layer_idx]
145
+ + self._old_summary_len[layer_idx] + self._new_sum_len[layer_idx])
146
+
147
+ def get_cur_chunk_size(self, layer_idx: Optional[int] = None) -> int:
148
+ if layer_idx is None:
149
+ layer_idx = self.num_hidden_layers - 1
150
+ return self.cur_chunk_sizes[layer_idx]
151
+
152
+ def get_true_token_num(self, layer_idx: Optional[int] = None) -> int:
153
+ if layer_idx is None:
154
+ layer_idx = self.num_hidden_layers - 1
155
+ return self.true_tokens[layer_idx]
156
+
157
+ def set_sksa_index_keys(
158
+ self, layer_idx: int, index_keys: torch.Tensor
159
+ ) -> None:
160
+ if index_keys.ndim != 3:
161
+ raise ValueError("S-KSA index keys must use [B,M,D] layout")
162
+ self._sksa_index_keys[layer_idx] = index_keys.contiguous()
163
+
164
+ def append_sksa_index_keys(
165
+ self, layer_idx: int, index_keys: torch.Tensor
166
+ ) -> None:
167
+ if index_keys.ndim != 3:
168
+ raise ValueError("S-KSA index keys must use [B,M,D] layout")
169
+ current = self._sksa_index_keys[layer_idx]
170
+ if current is None:
171
+ self._sksa_index_keys[layer_idx] = index_keys.contiguous()
172
+ else:
173
+ if current.shape[0] != index_keys.shape[0] or current.shape[2] != index_keys.shape[2]:
174
+ raise ValueError("S-KSA index key cache geometry changed during decode")
175
+ self._sksa_index_keys[layer_idx] = torch.cat(
176
+ (current, index_keys), dim=1
177
+ )
178
+
179
+ def get_sksa_index_keys(self, layer_idx: int) -> torch.Tensor:
180
+ index_keys = self._sksa_index_keys[layer_idx]
181
+ if index_keys is None:
182
+ raise RuntimeError(f"S-KSA index key cache for layer {layer_idx} is uninitialized")
183
+ return index_keys
184
+
185
+ def set_sksa_chunk_k_from_prefill(
186
+ self,
187
+ layer_idx: int,
188
+ chunk_key_states: Optional[torch.Tensor],
189
+ summary_mask: torch.Tensor,
190
+ ) -> None:
191
+ """Retain auxiliary K for the incomplete raw chunk after prefill."""
192
+ if chunk_key_states is None:
193
+ self._sksa_chunk_k[layer_idx] = None
194
+ return
195
+ if chunk_key_states.ndim != 4:
196
+ raise ValueError("S-KSA chunk K must use [B,H,S,D] layout")
197
+ shared = _get_shared_batch_mask(summary_mask, "summary_mask")
198
+ positions = self._prefill_text_positions
199
+ text_key = (chunk_key_states.index_select(2, positions) if positions is not None
200
+ else chunk_key_states[:, :, ~shared, :])
201
+ partial = text_key.shape[2] % self.summary_chunk_size
202
+ if partial:
203
+ self._sksa_chunk_k[layer_idx] = text_key[:, :, -partial:, :].contiguous()
204
+ else:
205
+ self._sksa_chunk_k[layer_idx] = None
206
+
207
+ def append_sksa_chunk_k(
208
+ self, layer_idx: int, chunk_key_states: Optional[torch.Tensor]
209
+ ) -> None:
210
+ """Append one decoded raw token's independent auxiliary K state."""
211
+ if chunk_key_states is None:
212
+ return
213
+ if chunk_key_states.ndim != 4 or chunk_key_states.shape[2] != 1:
214
+ raise ValueError("decoded S-KSA chunk K must use [B,H,1,D] layout")
215
+ current = self._sksa_chunk_k[layer_idx]
216
+ self._sksa_chunk_k[layer_idx] = (
217
+ chunk_key_states.contiguous()
218
+ if current is None
219
+ else torch.cat((current, chunk_key_states), dim=2).contiguous()
220
+ )
221
+
222
+ def get_current_sksa_chunk_k(self, layer_idx: int) -> Optional[torch.Tensor]:
223
+ return self._sksa_chunk_k[layer_idx]
224
+
225
+ def clear_sksa_chunk_k(self, layer_idx: int) -> None:
226
+ self._sksa_chunk_k[layer_idx] = None
227
+
228
+ def set_sksa_chunk_v_from_prefill(
229
+ self,
230
+ layer_idx: int,
231
+ chunk_value_states: Optional[torch.Tensor],
232
+ summary_mask: torch.Tensor,
233
+ ) -> None:
234
+ """Retain auxiliary V for the incomplete raw chunk after prefill."""
235
+ if chunk_value_states is None:
236
+ self._sksa_chunk_v[layer_idx] = None
237
+ return
238
+ if chunk_value_states.ndim != 4:
239
+ raise ValueError("S-KSA chunk V must use [B,H,S,D] layout")
240
+ shared = _get_shared_batch_mask(summary_mask, "summary_mask")
241
+ positions = self._prefill_text_positions
242
+ text_value = (chunk_value_states.index_select(2, positions) if positions is not None
243
+ else chunk_value_states[:, :, ~shared, :])
244
+ partial = text_value.shape[2] % self.summary_chunk_size
245
+ if partial:
246
+ self._sksa_chunk_v[layer_idx] = text_value[:, :, -partial:, :].contiguous()
247
+ else:
248
+ self._sksa_chunk_v[layer_idx] = None
249
+
250
+ def append_sksa_chunk_v(
251
+ self, layer_idx: int, chunk_value_states: Optional[torch.Tensor]
252
+ ) -> None:
253
+ """Append one decoded raw token's independent auxiliary V state."""
254
+ if chunk_value_states is None:
255
+ return
256
+ if chunk_value_states.ndim != 4 or chunk_value_states.shape[2] != 1:
257
+ raise ValueError("decoded S-KSA chunk V must use [B,H,1,D] layout")
258
+ current = self._sksa_chunk_v[layer_idx]
259
+ self._sksa_chunk_v[layer_idx] = (
260
+ chunk_value_states.contiguous()
261
+ if current is None
262
+ else torch.cat((current, chunk_value_states), dim=2).contiguous()
263
+ )
264
+
265
+ def get_current_sksa_chunk_v(self, layer_idx: int) -> Optional[torch.Tensor]:
266
+ return self._sksa_chunk_v[layer_idx]
267
+
268
+ def clear_sksa_chunk_v(self, layer_idx: int) -> None:
269
+ self._sksa_chunk_v[layer_idx] = None
270
+
271
+ # ── Prefill: per-layer one-shot init into ring buffer layout ──
272
+
273
+ def update(
274
+ self,
275
+ key_states: torch.Tensor,
276
+ value_states: torch.Tensor,
277
+ layer_idx: int,
278
+ cache_kwargs: Optional[dict[str, Any]] = None,
279
+ ) -> tuple[torch.Tensor, torch.Tensor]:
280
+ """Deprecated for prefill — use init_layer_from_prefill instead.
281
+
282
+ Kept only to surface accidental calls; the prefill path now skips this
283
+ intermediate full-length buffer entirely.
284
+ """
285
+ raise NotImplementedError(
286
+ "Qwen3RingBufferCache.update() is no longer used; prefill goes through "
287
+ "init_layer_from_prefill, decode goes through update_text/update_summary."
288
+ )
289
+
290
+ def prepare_prefill(self, text_positions, summary_positions):
291
+ self._prefill_text_positions = text_positions
292
+ self._prefill_summary_positions = summary_positions
293
+
294
+ def init_layer_from_prefill(
295
+ self,
296
+ key_states: torch.Tensor,
297
+ value_states: torch.Tensor,
298
+ summary_mask: torch.Tensor,
299
+ layer_idx: int,
300
+ *,
301
+ pre_rope_key_states: Optional[torch.Tensor] = None,
302
+ summary_scores: Optional[torch.Tensor] = None,
303
+ summary_cos: Optional[torch.Tensor] = None,
304
+ summary_sin: Optional[torch.Tensor] = None,
305
+ summary_position_ids: Optional[torch.Tensor] = None,
306
+ meta_key_states: Optional[torch.Tensor] = None,
307
+ meta_value_states: Optional[torch.Tensor] = None,
308
+ sksa_chunk_key_states: Optional[torch.Tensor] = None,
309
+ ):
310
+ """Allocate this layer's cache directly into the post-reorganize ring buffer
311
+ layout from prefill K/V (already projected + RoPE applied), with NO intermediate
312
+ full-length buffer. Replaces the old update() + reorganize_after_prefill() pair
313
+ for the prefill path.
314
+
315
+ key_states / value_states: [bsz, kv_heads, prefill_len_expanded, head_dim]
316
+ summary_mask: [bsz, prefill_len_expanded] (bool, batch-uniform)
317
+ """
318
+ assert not self._reorganized[layer_idx], f"layer {layer_idx} already reorganized"
319
+
320
+ prefill_k = key_states
321
+ prefill_v = value_states
322
+ bsz, heads, prefill_len, head_dim = prefill_k.shape
323
+ device, dtype = prefill_k.device, prefill_k.dtype
324
+
325
+ sm = _get_shared_batch_mask(summary_mask, "summary_mask")
326
+ text_mask = ~sm
327
+ text_positions = self._prefill_text_positions
328
+ summary_positions = self._prefill_summary_positions
329
+ if text_positions is None:
330
+ text_positions = torch.nonzero(text_mask, as_tuple=False).flatten()
331
+ summary_positions = torch.nonzero(sm, as_tuple=False).flatten()
332
+ n_text = text_positions.numel()
333
+
334
+ block = self.summary_chunk_size + self.summary_token_num
335
+ # Match what update() + reorganize_after_prefill() would have set:
336
+ # update(): cur_chunk_sizes += prefill_len; true_tokens += n_text
337
+ # reorganize: cur_chunk_sizes %= block
338
+ self.cur_chunk_sizes[layer_idx] = prefill_len % block
339
+ self.true_tokens[layer_idx] = n_text
340
+
341
+ if self.is_large_window[layer_idx]:
342
+ text_k = prefill_k.index_select(2, text_positions)
343
+ text_v = prefill_v.index_select(2, text_positions)
344
+ cap = n_text + self._APPEND_HEADROOM
345
+ new_k = torch.empty(bsz, heads, cap, head_dim, dtype=dtype, device=device)
346
+ new_v = torch.empty(bsz, heads, cap, head_dim, dtype=dtype, device=device)
347
+ if n_text > 0:
348
+ new_k[:, :, :n_text, :].copy_(text_k)
349
+ new_v[:, :, :n_text, :].copy_(text_v)
350
+ self.key_cache[layer_idx] = new_k
351
+ self.value_cache[layer_idx] = new_v
352
+ self._text_len[layer_idx] = n_text
353
+ self._capacity[layer_idx] = cap
354
+ self._total_chunks[layer_idx] = n_text // self.summary_chunk_size
355
+ else:
356
+ all_summary_k = prefill_k.index_select(2, summary_positions)
357
+ all_summary_v = prefill_v.index_select(2, summary_positions)
358
+ n_summary = all_summary_k.shape[2]
359
+
360
+ C = self.summary_chunk_size
361
+ ws = self.window_sizes[layer_idx]
362
+ scn = self.sliding_chunk_nums[layer_idx]
363
+
364
+ n_complete_chunks = n_text // C
365
+ n_partial = n_text % C
366
+ n_complete_text = n_complete_chunks * C
367
+
368
+ n_window_chunks = min(scn, n_complete_chunks)
369
+ n_window_text = n_window_chunks * C
370
+ window_start = n_complete_text - n_window_text
371
+ # Only retain the live window and partial chunk, avoiding a
372
+ # full-context text KV temporary on compressed layers.
373
+ live_positions = text_positions[window_start:]
374
+ text_k = prefill_k.index_select(2, live_positions)
375
+ text_v = prefill_v.index_select(2, live_positions)
376
+
377
+ n_old_total = max(0, n_summary - n_window_chunks)
378
+ n_new = n_summary - n_old_total
379
+ hierarchical = self.meta_pool_enabled[layer_idx]
380
+ group_size = self.meta_pool_group_sizes[layer_idx]
381
+ n_meta = n_old_total // group_size if hierarchical else 0
382
+ n_old = n_old_total % group_size if hierarchical else n_old_total
383
+ old_begin = n_meta * group_size if hierarchical else 0
384
+
385
+ old_s_cap = max(self._SUMMARY_INIT_CAP, (n_old + 1) * 2)
386
+ total_cap = ws + old_s_cap + C
387
+ new_k = torch.empty(bsz, heads, total_cap, head_dim, dtype=dtype, device=device)
388
+ new_v = torch.empty(bsz, heads, total_cap, head_dim, dtype=dtype, device=device)
389
+
390
+ if n_window_text > 0:
391
+ new_k[:, :, :n_window_text, :].copy_(text_k[:, :, :n_window_text, :])
392
+ new_v[:, :, :n_window_text, :].copy_(text_v[:, :, :n_window_text, :])
393
+ self._n_valid_window[layer_idx] = n_window_text
394
+ self._window_write_ptr[layer_idx] = n_window_text % ws
395
+
396
+ if n_old > 0:
397
+ new_k[:, :, ws:ws + n_old, :].copy_(
398
+ all_summary_k[:, :, old_begin:n_old_total, :]
399
+ )
400
+ new_v[:, :, ws:ws + n_old, :].copy_(
401
+ all_summary_v[:, :, old_begin:n_old_total, :]
402
+ )
403
+ self._old_summary_len[layer_idx] = n_old
404
+ self._old_summary_total[layer_idx] = n_old_total
405
+ self._old_summary_cap[layer_idx] = old_s_cap
406
+
407
+ if hierarchical:
408
+ required = (
409
+ pre_rope_key_states,
410
+ summary_scores,
411
+ summary_cos,
412
+ summary_sin,
413
+ summary_position_ids,
414
+ meta_key_states,
415
+ meta_value_states,
416
+ )
417
+ if any(item is None for item in required):
418
+ raise ValueError("hierarchical prefill cache requires pre-RoPE and meta state")
419
+ all_pre_k = pre_rope_key_states.index_select(2, summary_positions)
420
+ meta_cap = max(self._SUMMARY_INIT_CAP, (n_meta + 1) * 2)
421
+ meta_k_buf = torch.empty(
422
+ bsz, heads, meta_cap, head_dim, dtype=dtype, device=device
423
+ )
424
+ meta_v_buf = torch.empty_like(meta_k_buf)
425
+ if n_meta:
426
+ meta_k_buf[:, :, :n_meta, :].copy_(meta_key_states[:, :, :n_meta, :])
427
+ meta_v_buf[:, :, :n_meta, :].copy_(meta_value_states[:, :, :n_meta, :])
428
+ self._meta_key_cache[layer_idx] = meta_k_buf
429
+ self._meta_value_cache[layer_idx] = meta_v_buf
430
+ self._meta_len[layer_idx] = n_meta
431
+ self._meta_cap[layer_idx] = meta_cap
432
+
433
+ residual_pre = torch.empty(
434
+ bsz, heads, group_size, head_dim, dtype=dtype, device=device
435
+ )
436
+ score_width = (
437
+ heads * head_dim
438
+ if self.meta_pool_mode == "dimension_wise"
439
+ else 1
440
+ )
441
+ residual_score = torch.empty(
442
+ bsz,
443
+ group_size,
444
+ score_width,
445
+ dtype=summary_scores.dtype,
446
+ device=device,
447
+ )
448
+ residual_cos = torch.empty(
449
+ bsz, group_size, head_dim, dtype=summary_cos.dtype, device=device
450
+ )
451
+ residual_sin = torch.empty_like(residual_cos)
452
+ residual_position = torch.empty(
453
+ bsz, group_size, dtype=torch.long, device=device
454
+ )
455
+ if n_old:
456
+ residual_pre[:, :, :n_old, :].copy_(
457
+ all_pre_k[:, :, old_begin:n_old_total, :]
458
+ )
459
+ residual_score[:, :n_old, :].copy_(
460
+ summary_scores[:, old_begin:n_old_total]
461
+ )
462
+ residual_cos[:, :n_old, :].copy_(
463
+ summary_cos[:, old_begin:n_old_total, :]
464
+ )
465
+ residual_sin[:, :n_old, :].copy_(
466
+ summary_sin[:, old_begin:n_old_total, :]
467
+ )
468
+ residual_position[:, :n_old].copy_(
469
+ summary_position_ids[:, old_begin:n_old_total]
470
+ )
471
+ self._residual_pre_key_buf[layer_idx] = residual_pre
472
+ self._residual_score_buf[layer_idx] = residual_score
473
+ self._residual_cos_buf[layer_idx] = residual_cos
474
+ self._residual_sin_buf[layer_idx] = residual_sin
475
+ self._residual_position_buf[layer_idx] = residual_position
476
+
477
+ if n_partial > 0:
478
+ mirror_start = ws + n_old
479
+ new_k[:, :, mirror_start:mirror_start + n_partial, :].copy_(
480
+ text_k[:, :, n_window_text:, :])
481
+ new_v[:, :, mirror_start:mirror_start + n_partial, :].copy_(
482
+ text_v[:, :, n_window_text:, :])
483
+
484
+ self.key_cache[layer_idx] = new_k
485
+ self.value_cache[layer_idx] = new_v
486
+ self._capacity[layer_idx] = total_cap
487
+ self._text_len[layer_idx] = 0
488
+
489
+ ns_buf_k = torch.empty(bsz, heads, scn, head_dim, dtype=dtype, device=device)
490
+ ns_buf_v = torch.empty(bsz, heads, scn, head_dim, dtype=dtype, device=device)
491
+ if n_new > 0:
492
+ ns_buf_k[:, :, :n_new, :].copy_(all_summary_k[:, :, n_old_total:, :])
493
+ ns_buf_v[:, :, :n_new, :].copy_(all_summary_v[:, :, n_old_total:, :])
494
+ self._new_sum_key_buf[layer_idx] = ns_buf_k
495
+ self._new_sum_value_buf[layer_idx] = ns_buf_v
496
+ if hierarchical:
497
+ ns_pre = torch.empty(bsz, heads, scn, head_dim, dtype=dtype, device=device)
498
+ score_width = (
499
+ heads * head_dim
500
+ if self.meta_pool_mode == "dimension_wise"
501
+ else 1
502
+ )
503
+ ns_score = torch.empty(
504
+ bsz,
505
+ scn,
506
+ score_width,
507
+ dtype=summary_scores.dtype,
508
+ device=device,
509
+ )
510
+ ns_cos = torch.empty(bsz, scn, head_dim, dtype=summary_cos.dtype, device=device)
511
+ ns_sin = torch.empty_like(ns_cos)
512
+ ns_position = torch.empty(
513
+ bsz, scn, dtype=torch.long, device=device
514
+ )
515
+ if n_new:
516
+ ns_pre[:, :, :n_new, :].copy_(all_pre_k[:, :, n_old_total:, :])
517
+ ns_score[:, :n_new, :].copy_(summary_scores[:, n_old_total:])
518
+ ns_cos[:, :n_new, :].copy_(summary_cos[:, n_old_total:, :])
519
+ ns_sin[:, :n_new, :].copy_(summary_sin[:, n_old_total:, :])
520
+ ns_position[:, :n_new].copy_(
521
+ summary_position_ids[:, n_old_total:]
522
+ )
523
+ self._new_sum_pre_key_buf[layer_idx] = ns_pre
524
+ self._new_sum_score_buf[layer_idx] = ns_score
525
+ self._new_sum_cos_buf[layer_idx] = ns_cos
526
+ self._new_sum_sin_buf[layer_idx] = ns_sin
527
+ self._new_sum_position_buf[layer_idx] = ns_position
528
+ self._new_sum_len[layer_idx] = n_new
529
+ self._new_sum_write_ptr[layer_idx] = n_new % scn
530
+
531
+ self._chunk_buf_len[layer_idx] = n_partial
532
+ self._total_chunks[layer_idx] = n_old_total + n_new
533
+
534
+ self._reorganized[layer_idx] = True
535
+ self.set_sksa_chunk_k_from_prefill(
536
+ layer_idx, sksa_chunk_key_states, summary_mask
537
+ )
538
+
539
+ # ── Reorganize after prefill ──
540
+
541
+ def reorganize_after_prefill(self, summary_mask: torch.Tensor):
542
+ """No-op sanity check.
543
+
544
+ Prefill path now writes each layer directly into the ring buffer layout via
545
+ init_layer_from_prefill, so by the time Qwen3Model.forward calls this,
546
+ all layers must already be reorganized.
547
+ """
548
+ for layer_idx in range(self.num_hidden_layers):
549
+ assert self._reorganized[layer_idx], (
550
+ f"layer {layer_idx} was not initialized via init_layer_from_prefill"
551
+ )
552
+
553
+ self._prefill_text_positions = None
554
+ self._prefill_summary_positions = None
555
+
556
+ # ── Decode: text token update ──
557
+
558
+ def update_text(
559
+ self,
560
+ key_states: torch.Tensor,
561
+ value_states: torch.Tensor,
562
+ layer_idx: int,
563
+ *,
564
+ sksa_chunk_key_states: Optional[torch.Tensor] = None,
565
+ ):
566
+ """Write a single text token KV during decode."""
567
+ if self.is_large_window[layer_idx]:
568
+ cur = self._text_len[layer_idx]
569
+ new_len = cur + 1
570
+ if new_len > self._capacity[layer_idx]:
571
+ cap = max(new_len + self._APPEND_HEADROOM, self._capacity[layer_idx] * 2)
572
+ old_k, old_v = self.key_cache[layer_idx], self.value_cache[layer_idx]
573
+ bsz, heads, _, head_dim = old_k.shape
574
+ new_k = torch.empty(bsz, heads, cap, head_dim, dtype=old_k.dtype, device=old_k.device)
575
+ new_v = torch.empty(bsz, heads, cap, head_dim, dtype=old_v.dtype, device=old_v.device)
576
+ new_k[:, :, :cur, :].copy_(old_k[:, :, :cur, :])
577
+ new_v[:, :, :cur, :].copy_(old_v[:, :, :cur, :])
578
+ self.key_cache[layer_idx] = new_k
579
+ self.value_cache[layer_idx] = new_v
580
+ self._capacity[layer_idx] = cap
581
+ self.key_cache[layer_idx][:, :, cur:new_len, :].copy_(key_states)
582
+ self.value_cache[layer_idx][:, :, cur:new_len, :].copy_(value_states)
583
+ self._text_len[layer_idx] = new_len
584
+ else:
585
+ # Write only to key_cache mirror region (chunk_buf eliminated)
586
+ ws = self.window_sizes[layer_idx]
587
+ n_old = self._old_summary_len[layer_idx]
588
+ pos = self._chunk_buf_len[layer_idx]
589
+ mirror_pos = ws + n_old + pos
590
+ self.key_cache[layer_idx][:, :, mirror_pos:mirror_pos+1, :].copy_(key_states)
591
+ self.value_cache[layer_idx][:, :, mirror_pos:mirror_pos+1, :].copy_(value_states)
592
+ self._chunk_buf_len[layer_idx] = pos + 1
593
+
594
+ self.cur_chunk_sizes[layer_idx] += 1
595
+ self.true_tokens[layer_idx] += 1
596
+ self.append_sksa_chunk_k(layer_idx, sksa_chunk_key_states)
597
+
598
+ # ── Decode: summary token update ──
599
+
600
+ def update_summary(
601
+ self,
602
+ key_states: torch.Tensor,
603
+ value_states: torch.Tensor,
604
+ layer_idx: int,
605
+ *,
606
+ pre_rope_key_states: Optional[torch.Tensor] = None,
607
+ summary_scores: Optional[torch.Tensor] = None,
608
+ summary_cos: Optional[torch.Tensor] = None,
609
+ summary_sin: Optional[torch.Tensor] = None,
610
+ summary_position_ids: Optional[torch.Tensor] = None,
611
+ position_bias: Optional[torch.Tensor] = None,
612
+ ):
613
+ """Write summary token KV during decode (chunk boundary).
614
+
615
+ Large window: skip.
616
+ Small window (order matters — flush mirror before evict to avoid clobbering):
617
+ 1. Flush mirror region → ring
618
+ 2. Evict oldest new_summary → old_summary in key_cache (if full)
619
+ 3. Write new summary → new_summary_buf
620
+ """
621
+ n_summary = key_states.shape[2]
622
+
623
+ if self.is_large_window[layer_idx]:
624
+ self.cur_chunk_sizes[layer_idx] += n_summary
625
+ self._total_chunks[layer_idx] += n_summary
626
+ return
627
+
628
+ C = self.summary_chunk_size
629
+ ws = self.window_sizes[layer_idx]
630
+ scn = self.sliding_chunk_nums[layer_idx]
631
+ hierarchical = self.meta_pool_enabled[layer_idx]
632
+ group_size = self.meta_pool_group_sizes[layer_idx]
633
+ cbl = self._chunk_buf_len[layer_idx]
634
+ ptr = self._window_write_ptr[layer_idx]
635
+ n_old = self._old_summary_len[layer_idx]
636
+
637
+ # Step 1: Flush mirror region → ring (must happen before evict touches mirror[0])
638
+ mirror_start = ws + n_old
639
+ if ptr + cbl <= ws:
640
+ self.key_cache[layer_idx][:, :, ptr:ptr + cbl, :].copy_(
641
+ self.key_cache[layer_idx][:, :, mirror_start:mirror_start + cbl, :])
642
+ self.value_cache[layer_idx][:, :, ptr:ptr + cbl, :].copy_(
643
+ self.value_cache[layer_idx][:, :, mirror_start:mirror_start + cbl, :])
644
+ else:
645
+ first = ws - ptr
646
+ self.key_cache[layer_idx][:, :, ptr:ws, :].copy_(
647
+ self.key_cache[layer_idx][:, :, mirror_start:mirror_start + first, :])
648
+ self.value_cache[layer_idx][:, :, ptr:ws, :].copy_(
649
+ self.value_cache[layer_idx][:, :, mirror_start:mirror_start + first, :])
650
+ rest = cbl - first
651
+ self.key_cache[layer_idx][:, :, :rest, :].copy_(
652
+ self.key_cache[layer_idx][:, :, mirror_start + first:mirror_start + cbl, :])
653
+ self.value_cache[layer_idx][:, :, :rest, :].copy_(
654
+ self.value_cache[layer_idx][:, :, mirror_start + first:mirror_start + cbl, :])
655
+
656
+ self._window_write_ptr[layer_idx] = (ptr + cbl) % ws
657
+ if self._n_valid_window[layer_idx] < ws:
658
+ self._n_valid_window[layer_idx] = min(ws, self._n_valid_window[layer_idx] + cbl)
659
+ self._chunk_buf_len[layer_idx] = 0
660
+
661
+ # Step 2: Evict oldest new_summary → old_summary (now safe — mirror already flushed)
662
+ if self._new_sum_len[layer_idx] >= scn:
663
+ read_ptr = self._new_sum_write_ptr[layer_idx]
664
+ old_dst = ws + n_old # == mirror_start, but mirror data is already in ring
665
+
666
+ # Check capacity for old_summary growth
667
+ needed = old_dst + 1 + C # +1 for new old_sum, +C for future chunk mirror
668
+ if needed > self._capacity[layer_idx]:
669
+ new_s_cap = max(self._old_summary_cap[layer_idx] * 2, n_old + self._SUMMARY_INIT_CAP)
670
+ new_total = ws + new_s_cap + C
671
+ old_k, old_v = self.key_cache[layer_idx], self.value_cache[layer_idx]
672
+ bsz, heads, _, head_dim = old_k.shape
673
+ nk = torch.empty(bsz, heads, new_total, head_dim, dtype=old_k.dtype, device=old_k.device)
674
+ nv = torch.empty(bsz, heads, new_total, head_dim, dtype=old_v.dtype, device=old_v.device)
675
+ copy_len = ws + n_old
676
+ nk[:, :, :copy_len, :].copy_(old_k[:, :, :copy_len, :])
677
+ nv[:, :, :copy_len, :].copy_(old_v[:, :, :copy_len, :])
678
+ self.key_cache[layer_idx] = nk
679
+ self.value_cache[layer_idx] = nv
680
+ self._old_summary_cap[layer_idx] = new_s_cap
681
+ self._capacity[layer_idx] = new_total
682
+
683
+ self.key_cache[layer_idx][:, :, old_dst:old_dst+1, :].copy_(
684
+ self._new_sum_key_buf[layer_idx][:, :, read_ptr:read_ptr+1, :])
685
+ self.value_cache[layer_idx][:, :, old_dst:old_dst+1, :].copy_(
686
+ self._new_sum_value_buf[layer_idx][:, :, read_ptr:read_ptr+1, :])
687
+ n_old += 1
688
+ self._old_summary_total[layer_idx] += 1
689
+
690
+ if hierarchical:
691
+ self._residual_pre_key_buf[layer_idx][:, :, n_old-1:n_old, :].copy_(
692
+ self._new_sum_pre_key_buf[layer_idx][:, :, read_ptr:read_ptr+1, :]
693
+ )
694
+ self._residual_score_buf[layer_idx][:, n_old-1:n_old, :].copy_(
695
+ self._new_sum_score_buf[layer_idx][:, read_ptr:read_ptr+1, :]
696
+ )
697
+ self._residual_cos_buf[layer_idx][:, n_old-1:n_old, :].copy_(
698
+ self._new_sum_cos_buf[layer_idx][:, read_ptr:read_ptr+1, :]
699
+ )
700
+ self._residual_sin_buf[layer_idx][:, n_old-1:n_old, :].copy_(
701
+ self._new_sum_sin_buf[layer_idx][:, read_ptr:read_ptr+1, :]
702
+ )
703
+ self._residual_position_buf[layer_idx][:, n_old-1:n_old].copy_(
704
+ self._new_sum_position_buf[layer_idx][
705
+ :, read_ptr:read_ptr+1
706
+ ]
707
+ )
708
+
709
+ if n_old == group_size:
710
+ scores = self._residual_score_buf[layer_idx][
711
+ :, :group_size, :
712
+ ]
713
+ if self.meta_pool_mode == "dimension_wise":
714
+ if position_bias is None:
715
+ weights = torch.full_like(scores, 1.0 / group_size)
716
+ else:
717
+ weights = torch.softmax(
718
+ (
719
+ scores
720
+ + position_bias.view(
721
+ 1, group_size, scores.shape[-1]
722
+ )
723
+ ).float(),
724
+ dim=1,
725
+ ).to(key_states.dtype)
726
+ bsz, heads, _, head_dim = self._residual_pre_key_buf[
727
+ layer_idx
728
+ ].shape
729
+ weights = weights.view(
730
+ bsz, group_size, heads, head_dim
731
+ ).permute(0, 2, 1, 3)
732
+ else:
733
+ scalar_scores = scores.squeeze(-1)
734
+ if position_bias is None:
735
+ scalar_weights = torch.full_like(
736
+ scalar_scores, 1.0 / group_size
737
+ )
738
+ else:
739
+ scalar_weights = torch.softmax(
740
+ (
741
+ scalar_scores
742
+ + position_bias.view(1, group_size)
743
+ ).float(),
744
+ dim=1,
745
+ ).to(key_states.dtype)
746
+ weights = scalar_weights[:, None, :, None]
747
+ meta_pre_k = (
748
+ self._residual_pre_key_buf[layer_idx][:, :, :group_size, :]
749
+ * weights
750
+ ).sum(dim=2)
751
+ meta_v = (
752
+ self.value_cache[layer_idx][:, :, ws:ws + group_size, :]
753
+ * weights
754
+ ).sum(dim=2)
755
+ last_cos = self._residual_cos_buf[layer_idx][:, group_size - 1, :]
756
+ last_sin = self._residual_sin_buf[layer_idx][:, group_size - 1, :]
757
+ meta_k = (
758
+ meta_pre_k * last_cos[:, None, :]
759
+ + rotate_half(meta_pre_k) * last_sin[:, None, :]
760
+ )
761
+
762
+ meta_dst = self._meta_len[layer_idx]
763
+ if meta_dst >= self._meta_cap[layer_idx]:
764
+ old_meta_k = self._meta_key_cache[layer_idx]
765
+ old_meta_v = self._meta_value_cache[layer_idx]
766
+ new_cap = max(meta_dst + self._SUMMARY_INIT_CAP, self._meta_cap[layer_idx] * 2)
767
+ bsz, heads, _, head_dim = old_meta_k.shape
768
+ new_meta_k = torch.empty(
769
+ bsz, heads, new_cap, head_dim,
770
+ dtype=old_meta_k.dtype, device=old_meta_k.device,
771
+ )
772
+ new_meta_v = torch.empty_like(new_meta_k)
773
+ new_meta_k[:, :, :meta_dst, :].copy_(old_meta_k[:, :, :meta_dst, :])
774
+ new_meta_v[:, :, :meta_dst, :].copy_(old_meta_v[:, :, :meta_dst, :])
775
+ self._meta_key_cache[layer_idx] = new_meta_k
776
+ self._meta_value_cache[layer_idx] = new_meta_v
777
+ self._meta_cap[layer_idx] = new_cap
778
+ self._meta_key_cache[layer_idx][:, :, meta_dst:meta_dst+1, :].copy_(
779
+ meta_k.unsqueeze(2)
780
+ )
781
+ self._meta_value_cache[layer_idx][:, :, meta_dst:meta_dst+1, :].copy_(
782
+ meta_v.unsqueeze(2)
783
+ )
784
+ self._meta_len[layer_idx] += 1
785
+ n_old = 0
786
+
787
+ self._old_summary_len[layer_idx] = n_old
788
+
789
+ # Step 3: Write new summary to new_summary_buf (overwrite oldest slot)
790
+ w_ptr = self._new_sum_write_ptr[layer_idx]
791
+ self._new_sum_key_buf[layer_idx][:, :, w_ptr:w_ptr+1, :].copy_(key_states)
792
+ self._new_sum_value_buf[layer_idx][:, :, w_ptr:w_ptr+1, :].copy_(value_states)
793
+ if hierarchical:
794
+ if any(
795
+ item is None
796
+ for item in (
797
+ pre_rope_key_states,
798
+ summary_scores,
799
+ summary_cos,
800
+ summary_sin,
801
+ summary_position_ids,
802
+ )
803
+ ):
804
+ raise ValueError("hierarchical decode requires pre-RoPE summary state")
805
+ self._new_sum_pre_key_buf[layer_idx][:, :, w_ptr:w_ptr+1, :].copy_(
806
+ pre_rope_key_states
807
+ )
808
+ self._new_sum_score_buf[layer_idx][:, w_ptr:w_ptr+1, :].copy_(
809
+ summary_scores
810
+ )
811
+ self._new_sum_cos_buf[layer_idx][:, w_ptr:w_ptr+1, :].copy_(summary_cos)
812
+ self._new_sum_sin_buf[layer_idx][:, w_ptr:w_ptr+1, :].copy_(summary_sin)
813
+ self._new_sum_position_buf[layer_idx][
814
+ :, w_ptr:w_ptr+1
815
+ ].copy_(summary_position_ids)
816
+ self._new_sum_write_ptr[layer_idx] = (w_ptr + 1) % scn
817
+ if self._new_sum_len[layer_idx] < scn:
818
+ self._new_sum_len[layer_idx] += 1
819
+
820
+ self.cur_chunk_sizes[layer_idx] += n_summary
821
+ self._total_chunks[layer_idx] += n_summary
822
+
823
+ # ── Decode: get KV for attention ──
824
+
825
+ def get_attention_kv(self, layer_idx: int) -> tuple[torch.Tensor, torch.Tensor, int]:
826
+ """Get full KV for text token attention.
827
+
828
+ Large window: buffer[:text_len]
829
+ Small window (steady state): key_cache[:ws + n_old + cbl] — single slice, zero cat
830
+ """
831
+ if self.is_large_window[layer_idx]:
832
+ tl = self._text_len[layer_idx]
833
+ return (self.key_cache[layer_idx][:, :, :tl, :],
834
+ self.value_cache[layer_idx][:, :, :tl, :], 0)
835
+
836
+ ws = self.window_sizes[layer_idx]
837
+ nv = self._n_valid_window[layer_idx]
838
+ cbl = self._chunk_buf_len[layer_idx]
839
+ n_old = self._old_summary_len[layer_idx]
840
+
841
+ # Steady state: ring full → [ring(ws) | old_sums(n_old) | chunk_mirror(cbl)] contiguous
842
+ if nv == ws:
843
+ end = ws + n_old + cbl
844
+ base_k = self.key_cache[layer_idx][:, :, :end, :]
845
+ base_v = self.value_cache[layer_idx][:, :, :end, :]
846
+ else:
847
+ # Warmup: ring not full, [nv:ws] is gap → cat
848
+ parts_k, parts_v = [], []
849
+ if nv > 0:
850
+ parts_k.append(self.key_cache[layer_idx][:, :, :nv, :])
851
+ parts_v.append(self.value_cache[layer_idx][:, :, :nv, :])
852
+ if cbl > 0:
853
+ mirror_start = ws + n_old
854
+ parts_k.append(self.key_cache[layer_idx][:, :, mirror_start:mirror_start + cbl, :])
855
+ parts_v.append(self.value_cache[layer_idx][:, :, mirror_start:mirror_start + cbl, :])
856
+ if n_old > 0:
857
+ parts_k.append(self.key_cache[layer_idx][:, :, ws:ws + n_old, :])
858
+ parts_v.append(self.value_cache[layer_idx][:, :, ws:ws + n_old, :])
859
+ base_k = parts_k[0] if len(parts_k) == 1 else torch.cat(parts_k, dim=2)
860
+ base_v = parts_v[0] if len(parts_v) == 1 else torch.cat(parts_v, dim=2)
861
+
862
+ meta_len = self._meta_len[layer_idx]
863
+ if meta_len:
864
+ base_k = torch.cat(
865
+ (base_k, self._meta_key_cache[layer_idx][:, :, :meta_len, :]), dim=2
866
+ )
867
+ base_v = torch.cat(
868
+ (base_v, self._meta_value_cache[layer_idx][:, :, :meta_len, :]), dim=2
869
+ )
870
+ return base_k, base_v, meta_len
871
+
872
+ def get_current_chunk_kv(self, layer_idx: int) -> tuple[torch.Tensor, torch.Tensor]:
873
+ """Get KV of the current chunk's C text tokens for summary token attention."""
874
+ C = self.summary_chunk_size
875
+ if self.is_large_window[layer_idx]:
876
+ tl = self._text_len[layer_idx]
877
+ return (self.key_cache[layer_idx][:, :, tl - C:tl, :],
878
+ self.value_cache[layer_idx][:, :, tl - C:tl, :])
879
+ else:
880
+ ws = self.window_sizes[layer_idx]
881
+ n_old = self._old_summary_len[layer_idx]
882
+ cbl = self._chunk_buf_len[layer_idx]
883
+ mirror_start = ws + n_old
884
+ return (self.key_cache[layer_idx][:, :, mirror_start:mirror_start + cbl, :],
885
+ self.value_cache[layer_idx][:, :, mirror_start:mirror_start + cbl, :])
886
+
887
+ def reset_chunk_counter(self):
888
+ """Reset chunk counters after a chunk boundary step completes."""
889
+ block = self.summary_chunk_size + self.summary_token_num
890
+ for layer_idx in range(self.num_hidden_layers):
891
+ if self.cur_chunk_sizes[layer_idx] >= block:
892
+ self.cur_chunk_sizes[layer_idx] %= block
config.json ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen3ForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "auto_map": {
8
+ "AutoModel": "modeling_qwen3.Qwen3Model",
9
+ "AutoModelForCausalLM": "modeling_qwen3.Qwen3ForCausalLM"
10
+ },
11
+ "bos_token_id": 151643,
12
+ "eos_token_id": 151643,
13
+ "head_dim": 128,
14
+ "hidden_act": "silu",
15
+ "hidden_size": 2048,
16
+ "initializer_range": 0.02,
17
+ "intermediate_size": 6144,
18
+ "max_position_embeddings": 131072,
19
+ "max_window_layers": 24,
20
+ "mix_coeff": 0.0,
21
+ "model_type": "qwen3",
22
+ "num_attention_heads": 16,
23
+ "num_hidden_layers": 24,
24
+ "num_key_value_heads": 16,
25
+ "rms_norm_eps": 1e-06,
26
+ "rope_scaling": null,
27
+ "rope_theta": 1000000,
28
+ "sksa_apply_sparse_attention": true,
29
+ "sksa_enabled": true,
30
+ "sksa_indexer_block_size": 4,
31
+ "sksa_indexer_chunk_attention_include_summary": false,
32
+ "sksa_indexer_chunk_attention_qk_source": "independent",
33
+ "sksa_indexer_chunk_attention_v_source": "independent",
34
+ "sksa_indexer_force_sink": true,
35
+ "sksa_indexer_head_dim": 128,
36
+ "sksa_indexer_heads": 8,
37
+ "sksa_indexer_qk_norm": false,
38
+ "sksa_indexer_query_block_size": 512,
39
+ "sksa_indexer_query_rank": 512,
40
+ "sksa_indexer_rope_dim": 64,
41
+ "sksa_indexer_scorer_backend": "fused",
42
+ "sksa_indexer_summary_key_source": "chunk_attention",
43
+ "sksa_indexer_teacher_pooling": "max",
44
+ "sksa_indexer_token_budget": 4096,
45
+ "sksa_local_window_tokens": 1024,
46
+ "sksa_routing_mode": "summary_indexer",
47
+ "sksa_sparse_full_layers": [
48
+ 3,
49
+ 7,
50
+ 11,
51
+ 15,
52
+ 19,
53
+ 23
54
+ ],
55
+ "sliding_window": null,
56
+ "summary_chunk_position_ids_type": "origin",
57
+ "summary_chunk_size": 8,
58
+ "summary_independent_attention_layernorm": false,
59
+ "summary_independent_parameters": true,
60
+ "summary_layer_freq": "([1]*4)*6",
61
+ "summary_meta_pool_block_size": 512,
62
+ "summary_meta_pool_group_size": "([128]*1+[128]*1+[128]*1+[0]*1)*6",
63
+ "summary_meta_pool_mass_correction": "log_group_size",
64
+ "summary_meta_pool_mode": "dimension_wise",
65
+ "summary_meta_pool_position": "group_last",
66
+ "summary_partial_chunk_policy": "defer",
67
+ "summary_sliding_chunk_num": "([128]*3+[16384]*1)*6",
68
+ "summary_token_begin": 151936,
69
+ "summary_token_num": 1,
70
+ "summary_token_position_ids_type": "last_chunk_slice_right",
71
+ "tie_word_embeddings": false,
72
+ "torch_dtype": "bfloat16",
73
+ "transformers_version": "5.0.0",
74
+ "truncate_predict_nums": 151936,
75
+ "use_cache": true,
76
+ "use_sliding_window": false,
77
+ "use_summary_attention": true,
78
+ "vocab_size": 151937
79
+ }
elementwise_fusion.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """BF16 inference fusion; FMA disabled, original cast points explicit.
2
+
3
+ Torch pow/mean is retained verbatim: no alternate RMS reduction is substituted.
4
+ The Q/K path materializes normalized pre-RoPE keys because meta pooling uses them.
5
+ """
6
+ import torch
7
+ import triton as tr
8
+ import triton.language as tl
9
+ import triton.language.extra.cuda.libdevice as lib
10
+
11
+
12
+ @tr.jit
13
+ def norm_tail(X,V,W,Y,N:tl.constexpr,D:tl.constexpr,EPS:tl.constexpr,BLOCK:tl.constexpr):
14
+ i=tl.program_id(0)*BLOCK+tl.arange(0,BLOCK)
15
+ x=tl.load(X+i,i<N,0).to(tl.float32)
16
+ v=tl.load(V+i//D,i<N,0)
17
+ scale=lib.rsqrt(v+EPS)
18
+ n=(x*scale).to(tl.bfloat16).to(tl.float32)
19
+ w=tl.load(W+i%D,i<N,0).to(tl.float32)
20
+ tl.store(Y+i,n*w,i<N)
21
+
22
+ _norm_launchers={}
23
+ _rope_launchers={}
24
+
25
+ def tail(x,variance,weight,eps,fast=False):
26
+ y=torch.empty_like(x);n=x.numel()
27
+ key=(x.device.index,n,x.shape[-1],eps)
28
+ if fast and key in _norm_launchers:
29
+ _norm_launchers[key](x,variance,weight,y)
30
+ else:
31
+ compiled=norm_tail[(tr.cdiv(n,256),)](x,variance,weight,y,n,x.shape[-1],eps,256,enable_fp_fusion=False)
32
+ if fast:_norm_launchers[key]=compiled[(tr.cdiv(n,256),1,1)]
33
+ return y
34
+
35
+ def rms(x,weight,eps):
36
+ # EXACT original FP32 pow + torch.mean dispatch and shape.
37
+ f=x.to(torch.float32);variance=f.pow(2).mean(-1,keepdim=True)
38
+ return tail(x,variance,weight,eps,fast=True)
39
+
40
+
41
+ @tr.jit
42
+ def rope_kernel(X,C,S,Y,N:tl.constexpr,H:tl.constexpr,T:tl.constexpr,D:tl.constexpr,ST0:tl.constexpr,ST1:tl.constexpr,ST2:tl.constexpr,ST3:tl.constexpr,SY0:tl.constexpr,SY1:tl.constexpr,SY2:tl.constexpr,SY3:tl.constexpr,BLOCK:tl.constexpr):
43
+ i=tl.program_id(0)*BLOCK+tl.arange(0,BLOCK);d=i%D;t=i//D%T;h=i//(D*T)%H;b=i//(D*T*H)
44
+ ptr=b*ST0+h*ST1+t*ST2+d*ST3
45
+ rd=tl.where(d<D//2,d+D//2,d-D//2)
46
+ x=tl.load(X+ptr,i<N,0).to(tl.float32)
47
+ rot=tl.load(X+b*ST0+h*ST1+t*ST2+rd*ST3,i<N,0).to(tl.float32)
48
+ rot=tl.where(d<D//2,-rot,rot)
49
+ c=tl.load(C+(b*T+t)*D+d,i<N,0).to(tl.float32);s=tl.load(S+(b*T+t)*D+d,i<N,0).to(tl.float32)
50
+ a=(x*c).to(tl.bfloat16).to(tl.float32);z=(rot*s).to(tl.bfloat16).to(tl.float32)
51
+ tl.store(Y+b*SY0+h*SY1+t*SY2+d*SY3,a+z,i<N)
52
+
53
+ def rope_one(x,cos,sin):
54
+ b,h,t,d=x.shape
55
+ # TensorIterator canonicalizes dense degenerate dimensions (e.g. T=1),
56
+ # while preserving the physical order of non-contiguous dense inputs.
57
+ y=torch.empty(x.shape,device=x.device,dtype=x.dtype) if x.is_contiguous() else torch.empty_like(x)
58
+ n=x.numel()
59
+ key=(x.device.index,n,h,t,d,x.stride(),y.stride())
60
+ if key in _rope_launchers:
61
+ _rope_launchers[key](x,cos,sin,y)
62
+ else:
63
+ compiled=rope_kernel[(tr.cdiv(n,256),)](x,cos,sin,y,n,h,t,d,*x.stride(),*y.stride(),256,enable_fp_fusion=False)
64
+ _rope_launchers[key]=compiled[(tr.cdiv(n,256),1,1)]
65
+ return y
66
+
67
+ def rope(q,k,cos,sin,position_ids=None,unsqueeze_dim=1):
68
+ assert unsqueeze_dim==1 and cos.is_contiguous() and sin.is_contiguous()
69
+ return rope_one(q,cos,sin),rope_one(k,cos,sin)
70
+
71
+
72
+
73
+
74
+ def enabled(x):
75
+ return x.is_cuda and x.device.index == torch.cuda.current_device() and x.dtype == torch.bfloat16 and x.ndim >= 2 and x.shape[0] == 1 and not torch.is_grad_enabled()
generate.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """Generate text with a local KSA V2 export and its bundled kernels."""
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+ import sys
8
+
9
+
10
+ def parse_args() -> argparse.Namespace:
11
+ parser = argparse.ArgumentParser(description="Interactively generate text with a local model.")
12
+ parser.add_argument("--model", type=Path, default=Path(__file__).resolve().parent)
13
+ parser.add_argument("--prompt", help="Omit for interactive text completion.")
14
+ parser.add_argument("--max-new-tokens", type=int, default=256)
15
+ parser.add_argument("--apply-chat-template", action="store_true")
16
+ return parser.parse_args()
17
+
18
+
19
+ def main() -> None:
20
+ args = parse_args()
21
+ model_dir = args.model.resolve()
22
+ # Select the export's kernels before importing either runtime or Transformers.
23
+ for directory in (model_dir / "kernel/deps", model_dir / "kernel"):
24
+ sys.path.insert(0, str(directory))
25
+
26
+ import torch
27
+ from transformers import AutoModelForCausalLM, AutoTokenizer
28
+
29
+ tokenizer = AutoTokenizer.from_pretrained(model_dir, local_files_only=True)
30
+ model = AutoModelForCausalLM.from_pretrained(
31
+ model_dir,
32
+ trust_remote_code=True,
33
+ local_files_only=True,
34
+ dtype=torch.bfloat16,
35
+ device_map={"": "cuda:0"},
36
+ attn_implementation="sdpa",
37
+ ).eval()
38
+
39
+ def complete(prompt: str) -> str:
40
+ if args.apply_chat_template:
41
+ prompt = tokenizer.apply_chat_template(
42
+ [{"role": "user", "content": prompt}],
43
+ tokenize=False,
44
+ add_generation_prompt=True,
45
+ )
46
+
47
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
48
+ with torch.inference_mode():
49
+ output_ids = model.generate(
50
+ **inputs,
51
+ max_new_tokens=args.max_new_tokens,
52
+ do_sample=False,
53
+ pad_token_id=tokenizer.eos_token_id,
54
+ )
55
+
56
+ new_tokens = output_ids[0, inputs["input_ids"].shape[1] :]
57
+ return tokenizer.decode(new_tokens, skip_special_tokens=True)
58
+
59
+ if args.prompt is not None:
60
+ print(complete(args.prompt))
61
+ return
62
+ print("Enter a prompt, or type 'exit' to stop.")
63
+ while True:
64
+ try:
65
+ prompt = input("Prompt: ").strip()
66
+ except (EOFError, KeyboardInterrupt):
67
+ print()
68
+ break
69
+ if prompt.lower() in {"exit", "quit"}:
70
+ break
71
+ if prompt:
72
+ print(complete(prompt))
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
generation_config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 151643,
4
+ "eos_token_id": 151643,
5
+ "output_attentions": false,
6
+ "output_hidden_states": false,
7
+ "transformers_version": "5.0.0",
8
+ "use_cache": true
9
+ }
hierarchical_summary_attention.py ADDED
@@ -0,0 +1,381 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Reference prefill attention for hierarchical KSA summary pooling.
2
+
3
+ This module intentionally uses PyTorch SDPA over compact per-query-block KV sets.
4
+ It is the correctness path used before the fused CUDA kernel grows support for
5
+ meta-summary KV entries and their additive logit bias.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from typing import Dict, Tuple
12
+
13
+ import torch
14
+ import torch.nn.functional as F
15
+
16
+
17
+ _LAYOUT_CACHE: Dict[tuple, Tuple[torch.Tensor, torch.Tensor]] = {}
18
+ _SUMMARY_LAYOUT_CACHE: Dict[tuple, Tuple[torch.Tensor, torch.Tensor]] = {}
19
+
20
+
21
+ def _summary_layout(
22
+ *,
23
+ seq_len: int,
24
+ q_start: int,
25
+ q_end: int,
26
+ chunk_size: int,
27
+ sliding_chunk_num: int,
28
+ device: torch.device,
29
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
30
+ """Return compact base-KV positions and the original-KSA block mask."""
31
+ block = chunk_size + 1
32
+ q_pos = torch.arange(q_start, q_end, device=device, dtype=torch.long)
33
+ q_chunk = torch.div(q_pos, block, rounding_mode="floor")
34
+ q_offset = q_pos.remainder(block)
35
+ q_is_text = q_offset < chunk_size
36
+ window_start = torch.clamp(q_chunk - sliding_chunk_num, min=0)
37
+
38
+ candidates = []
39
+ if bool(q_is_text.any()):
40
+ old_summary_count = int(window_start[q_is_text].max().item())
41
+ if old_summary_count:
42
+ candidates.append(
43
+ torch.arange(old_summary_count, device=device) * block + chunk_size
44
+ )
45
+ first_text_chunk = int(window_start[q_is_text].min().item())
46
+ else:
47
+ first_text_chunk = int(q_chunk.min().item())
48
+
49
+ last_text_chunk = int(q_chunk.max().item())
50
+ text_chunks = torch.arange(
51
+ first_text_chunk,
52
+ last_text_chunk + 1,
53
+ device=device,
54
+ dtype=torch.long,
55
+ )
56
+ text_offsets = torch.arange(chunk_size, device=device, dtype=torch.long)
57
+ text_candidates = (text_chunks[:, None] * block + text_offsets).reshape(-1)
58
+ candidates.append(text_candidates[text_candidates < seq_len])
59
+
60
+ if bool((~q_is_text).any()):
61
+ current_summary_chunks = torch.unique(q_chunk[~q_is_text], sorted=True)
62
+ candidates.append(current_summary_chunks * block + chunk_size)
63
+
64
+ base_pos = torch.unique(torch.cat(candidates), sorted=True)
65
+ k_chunk = torch.div(base_pos, block, rounding_mode="floor")
66
+ k_offset = base_pos.remainder(block)
67
+ k_is_text = k_offset < chunk_size
68
+ k_is_summary = k_offset == chunk_size
69
+
70
+ qc = q_chunk[:, None]
71
+ qp = q_pos[:, None]
72
+ ws = window_start[:, None]
73
+ kc = k_chunk[None, :]
74
+ kp = base_pos[None, :]
75
+ kt = k_is_text[None, :]
76
+ ks = k_is_summary[None, :]
77
+ text_allowed = (ks & (kc < ws)) | (
78
+ kt & (((kc >= ws) & (kc < qc)) | ((kc == qc) & (kp <= qp)))
79
+ )
80
+ summary_allowed = (kc == qc) & (kt | (ks & (kp <= qp)))
81
+ allowed = torch.where(q_is_text[:, None], text_allowed, summary_allowed)
82
+ used = allowed.any(dim=0)
83
+ return base_pos[used], allowed[:, used]
84
+
85
+
86
+ def summary_attention_prefill(
87
+ query_states: torch.Tensor,
88
+ key_states: torch.Tensor,
89
+ value_states: torch.Tensor,
90
+ summary_mask: torch.Tensor,
91
+ *,
92
+ chunk_size: int,
93
+ sliding_chunk_num: int,
94
+ block_size: int = 512,
95
+ ) -> torch.Tensor:
96
+ """Pure-PyTorch batch-uniform prefill for original (unpooled) KSA.
97
+
98
+ This is the portable correctness path used when the optional ``summary_attn``
99
+ package is unavailable or explicitly disabled. Inputs use
100
+ ``[batch, heads, sequence, head_dim]`` layout; the output uses
101
+ ``[batch, sequence, query_heads, head_dim]``.
102
+ """
103
+ if chunk_size <= 0 or sliding_chunk_num <= 0 or block_size <= 0:
104
+ raise ValueError("chunk_size, sliding_chunk_num, and block_size must be positive")
105
+ if query_states.ndim != 4 or key_states.ndim != 4 or value_states.ndim != 4:
106
+ raise ValueError("query/key/value states must be rank-4 tensors")
107
+
108
+ batch, query_heads, seq_len, head_dim = query_states.shape
109
+ remainder = seq_len % (chunk_size + 1)
110
+ if remainder > chunk_size:
111
+ raise ValueError("invalid deferred partial-chunk layout")
112
+ if key_states.shape[:1] != (batch,) or key_states.shape[2:] != (seq_len, head_dim):
113
+ raise ValueError("key_states shape is incompatible with query_states")
114
+ if value_states.shape != key_states.shape:
115
+ raise ValueError("key_states and value_states must have identical shapes")
116
+ kv_heads = key_states.shape[1]
117
+ if query_heads % kv_heads:
118
+ raise ValueError("query head count must be divisible by KV head count")
119
+
120
+ if summary_mask.ndim == 2:
121
+ if summary_mask.shape != (batch, seq_len):
122
+ raise ValueError("summary_mask shape is incompatible with query_states")
123
+ if not torch.all(summary_mask == summary_mask[:1]):
124
+ raise ValueError("reference prefill requires a batch-uniform summary mask")
125
+ shared_summary_mask = summary_mask[0]
126
+ elif summary_mask.ndim == 1 and summary_mask.shape[0] == seq_len:
127
+ shared_summary_mask = summary_mask
128
+ else:
129
+ raise ValueError("summary_mask must have shape [sequence] or [batch, sequence]")
130
+ expected_summary_mask = torch.zeros(seq_len, dtype=torch.bool, device=query_states.device)
131
+ expected_summary_mask[chunk_size:: chunk_size + 1] = True
132
+ if not torch.equal(shared_summary_mask.to(dtype=torch.bool), expected_summary_mask):
133
+ raise ValueError("summary_mask does not match the configured chunk layout")
134
+
135
+ repeat_factor = query_heads // kv_heads
136
+ output = torch.empty_like(query_states)
137
+ for q_start in range(0, seq_len, block_size):
138
+ q_end = min(seq_len, q_start + block_size)
139
+ cache_key = (
140
+ seq_len,
141
+ q_start,
142
+ q_end,
143
+ chunk_size,
144
+ sliding_chunk_num,
145
+ device_key(query_states.device),
146
+ )
147
+ cached = _SUMMARY_LAYOUT_CACHE.get(cache_key)
148
+ if cached is None:
149
+ base_pos, allowed = _summary_layout(
150
+ seq_len=seq_len,
151
+ q_start=q_start,
152
+ q_end=q_end,
153
+ chunk_size=chunk_size,
154
+ sliding_chunk_num=sliding_chunk_num,
155
+ device=query_states.device,
156
+ )
157
+ _SUMMARY_LAYOUT_CACHE[cache_key] = (base_pos, allowed)
158
+ else:
159
+ base_pos, allowed = cached
160
+
161
+ compact_k = key_states[:, :, base_pos, :]
162
+ compact_v = value_states[:, :, base_pos, :]
163
+ if repeat_factor != 1:
164
+ compact_k = compact_k.repeat_interleave(repeat_factor, dim=1)
165
+ compact_v = compact_v.repeat_interleave(repeat_factor, dim=1)
166
+ output[:, :, q_start:q_end, :] = F.scaled_dot_product_attention(
167
+ query_states[:, :, q_start:q_end, :],
168
+ compact_k,
169
+ compact_v,
170
+ attn_mask=allowed,
171
+ dropout_p=0.0,
172
+ is_causal=False,
173
+ )
174
+ return output.transpose(1, 2).contiguous()
175
+
176
+
177
+ def _compact_layout(
178
+ *,
179
+ seq_len: int,
180
+ q_start: int,
181
+ q_end: int,
182
+ chunk_size: int,
183
+ sliding_chunk_num: int,
184
+ group_size: int,
185
+ device: torch.device,
186
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
187
+ """Return compact base-KV positions and an additive [Q, meta+base] mask."""
188
+ block = chunk_size + 1
189
+ q_pos = torch.arange(q_start, q_end, device=device, dtype=torch.long)
190
+ q_chunk = torch.div(q_pos, block, rounding_mode="floor")
191
+ q_offset = q_pos.remainder(block)
192
+ q_is_text = q_offset < chunk_size
193
+ window_start = torch.clamp(q_chunk - sliding_chunk_num, min=0)
194
+ complete_group_count = torch.div(window_start, group_size, rounding_mode="floor")
195
+ complete_end = complete_group_count * group_size
196
+
197
+ # Only a local interval of original K/V can remain visible. Older history is
198
+ # represented exclusively by meta KV, so it is omitted from the compact set.
199
+ if bool(q_is_text.any()):
200
+ first_chunk = int(complete_end[q_is_text].min().item())
201
+ else:
202
+ first_chunk = int(q_chunk.min().item())
203
+ base_start = first_chunk * block
204
+ base_pos = torch.arange(base_start, q_end, device=device, dtype=torch.long)
205
+ k_chunk = torch.div(base_pos, block, rounding_mode="floor")
206
+ k_offset = base_pos.remainder(block)
207
+ k_is_text = k_offset < chunk_size
208
+ k_is_summary = k_offset == chunk_size
209
+
210
+ qc = q_chunk[:, None]
211
+ qp = q_pos[:, None]
212
+ ws = window_start[:, None]
213
+ ce = complete_end[:, None]
214
+ kc = k_chunk[None, :]
215
+ kp = base_pos[None, :]
216
+ kt = k_is_text[None, :]
217
+ ks = k_is_summary[None, :]
218
+
219
+ text_to_raw = kt & (((kc >= ws) & (kc < qc)) | ((kc == qc) & (kp <= qp)))
220
+ text_to_residual = ks & (kc >= ce) & (kc < ws)
221
+ summary_to_current = (kc == qc) & (kt | (ks & (kp <= qp)))
222
+ base_allowed = torch.where(
223
+ q_is_text[:, None],
224
+ text_to_raw | text_to_residual,
225
+ summary_to_current,
226
+ )
227
+
228
+ max_meta = int(complete_group_count[q_is_text].max().item()) if bool(q_is_text.any()) else 0
229
+ if max_meta:
230
+ meta_idx = torch.arange(max_meta, device=device, dtype=torch.long)
231
+ meta_allowed = q_is_text[:, None] & (
232
+ meta_idx[None, :] < complete_group_count[:, None]
233
+ )
234
+ allowed = torch.cat((meta_allowed, base_allowed), dim=1)
235
+ else:
236
+ meta_idx = torch.empty(0, device=device, dtype=torch.long)
237
+ allowed = base_allowed
238
+
239
+ return torch.cat((meta_idx, base_pos)), allowed
240
+
241
+
242
+ def hierarchical_summary_attention_prefill(
243
+ query_states: torch.Tensor,
244
+ key_states: torch.Tensor,
245
+ value_states: torch.Tensor,
246
+ meta_key_states: torch.Tensor,
247
+ meta_value_states: torch.Tensor,
248
+ *,
249
+ chunk_size: int,
250
+ sliding_chunk_num: int,
251
+ group_size: int,
252
+ block_size: int = 512,
253
+ mass_correction: bool = True,
254
+ return_lse: bool = False,
255
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
256
+ """Compute hierarchical KSA attention for a batch-uniform prefill.
257
+
258
+ Args:
259
+ query_states: [batch, query_heads, seq, head_dim]
260
+ key_states/value_states: [batch, kv_heads, seq, head_dim]
261
+ meta_key_states/meta_value_states: [batch, kv_heads, groups, head_dim]
262
+
263
+ Returns:
264
+ Attention output in [batch, seq, query_heads, head_dim] layout. When
265
+ ``return_lse`` is true, also returns fp32 LSE in [batch, heads, seq].
266
+ """
267
+ if group_size <= 1:
268
+ raise ValueError(f"group_size must be greater than one, got {group_size}")
269
+ if chunk_size <= 0 or sliding_chunk_num <= 0 or block_size <= 0:
270
+ raise ValueError("chunk_size, sliding_chunk_num, and block_size must be positive")
271
+ if query_states.ndim != 4 or key_states.ndim != 4 or value_states.ndim != 4:
272
+ raise ValueError("query/key/value states must be rank-4 tensors")
273
+
274
+ batch, query_heads, seq_len, head_dim = query_states.shape
275
+ if key_states.shape[:1] != (batch,) or key_states.shape[2:] != (seq_len, head_dim):
276
+ raise ValueError("key_states shape is incompatible with query_states")
277
+ if value_states.shape != key_states.shape:
278
+ raise ValueError("key_states and value_states must have identical shapes")
279
+ kv_heads = key_states.shape[1]
280
+ if query_heads % kv_heads:
281
+ raise ValueError("query head count must be divisible by KV head count")
282
+ if meta_key_states.shape != meta_value_states.shape:
283
+ raise ValueError("meta key/value shapes must match")
284
+ if meta_key_states.shape[:2] != (batch, kv_heads) or meta_key_states.shape[3] != head_dim:
285
+ raise ValueError("meta key/value shape is incompatible with base key/value")
286
+
287
+ repeat_factor = query_heads // kv_heads
288
+ output = torch.empty_like(query_states)
289
+ softmax_lse = (
290
+ torch.empty(
291
+ batch, query_heads, seq_len,
292
+ dtype=torch.float32, device=query_states.device,
293
+ )
294
+ if return_lse else None
295
+ )
296
+ log_group_size = math.log(group_size) if mass_correction else 0.0
297
+
298
+ for q_start in range(0, seq_len, block_size):
299
+ q_end = min(seq_len, q_start + block_size)
300
+ cache_key = (
301
+ seq_len,
302
+ q_start,
303
+ q_end,
304
+ chunk_size,
305
+ sliding_chunk_num,
306
+ group_size,
307
+ device_key(query_states.device),
308
+ )
309
+ cached = _LAYOUT_CACHE.get(cache_key)
310
+ if cached is None:
311
+ packed_pos, allowed = _compact_layout(
312
+ seq_len=seq_len,
313
+ q_start=q_start,
314
+ q_end=q_end,
315
+ chunk_size=chunk_size,
316
+ sliding_chunk_num=sliding_chunk_num,
317
+ group_size=group_size,
318
+ device=query_states.device,
319
+ )
320
+ _LAYOUT_CACHE[cache_key] = (packed_pos, allowed)
321
+ else:
322
+ packed_pos, allowed = cached
323
+
324
+ # packed_pos stores meta indices first and sequence positions second.
325
+ q_pos = torch.arange(q_start, q_end, device=query_states.device)
326
+ q_chunk = torch.div(q_pos, chunk_size + 1, rounding_mode="floor")
327
+ q_is_text = q_pos.remainder(chunk_size + 1) < chunk_size
328
+ window_start = torch.clamp(q_chunk - sliding_chunk_num, min=0)
329
+ meta_len = (
330
+ int(torch.div(window_start[q_is_text], group_size, rounding_mode="floor").max().item())
331
+ if bool(q_is_text.any())
332
+ else 0
333
+ )
334
+ meta_idx = packed_pos[:meta_len]
335
+ base_pos = packed_pos[meta_len:]
336
+
337
+ base_k = key_states[:, :, base_pos, :]
338
+ base_v = value_states[:, :, base_pos, :]
339
+ if meta_len:
340
+ compact_k = torch.cat((meta_key_states[:, :, meta_idx, :], base_k), dim=2)
341
+ compact_v = torch.cat((meta_value_states[:, :, meta_idx, :], base_v), dim=2)
342
+ else:
343
+ compact_k, compact_v = base_k, base_v
344
+ if repeat_factor != 1:
345
+ compact_k = compact_k.repeat_interleave(repeat_factor, dim=1)
346
+ compact_v = compact_v.repeat_interleave(repeat_factor, dim=1)
347
+
348
+ additive_mask = torch.full(
349
+ allowed.shape,
350
+ float("-inf"),
351
+ dtype=query_states.dtype,
352
+ device=query_states.device,
353
+ )
354
+ additive_mask.masked_fill_(allowed, 0.0)
355
+ if meta_len and log_group_size:
356
+ additive_mask[:, :meta_len].masked_fill_(allowed[:, :meta_len], log_group_size)
357
+
358
+ output[:, :, q_start:q_end, :] = F.scaled_dot_product_attention(
359
+ query_states[:, :, q_start:q_end, :],
360
+ compact_k,
361
+ compact_v,
362
+ attn_mask=additive_mask,
363
+ dropout_p=0.0,
364
+ is_causal=False,
365
+ )
366
+ if return_lse:
367
+ logits = torch.einsum(
368
+ "bhqd,bhkd->bhqk",
369
+ query_states[:, :, q_start:q_end, :],
370
+ compact_k,
371
+ ).float()
372
+ logits.mul_(head_dim ** -0.5)
373
+ logits.add_(additive_mask.float()[None, None])
374
+ softmax_lse[:, :, q_start:q_end] = torch.logsumexp(logits, dim=-1)
375
+
376
+ output = output.transpose(1, 2).contiguous()
377
+ return (output, softmax_lse) if return_lse else output
378
+
379
+
380
+ def device_key(device: torch.device) -> tuple:
381
+ return device.type, device.index
indexer_step.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextvars import ContextVar
2
+ from functools import wraps
3
+
4
+ _INDEXER_STEP = ContextVar("indexer_step", default=None)
5
+
6
+
7
+ def shared_indexer_step(forward):
8
+ @wraps(forward)
9
+ def scoped(*args, **kwargs):
10
+ token = _INDEXER_STEP.set({"positions": {}, "bounds": {}})
11
+ try:
12
+ return forward(*args, **kwargs)
13
+ finally:
14
+ _INDEXER_STEP.reset(token)
15
+ return scoped
16
+
17
+
18
+ def shared_indexer_positions(source, kind, factory, *layout):
19
+ scope = _INDEXER_STEP.get()
20
+ if scope is None:
21
+ return factory()
22
+ key = (id(source), kind, *layout)
23
+ if key not in scope["positions"]:
24
+ # Retain the source, so tensor identity cannot be recycled in this step.
25
+ scope["positions"][key] = (source, factory())
26
+ return scope["positions"][key][1]
27
+
28
+
29
+ def _validated_position_bounds(position_ids):
30
+ scope = _INDEXER_STEP.get()
31
+ key = id(position_ids)
32
+ if scope is not None and key in scope["bounds"]:
33
+ return scope["bounds"][key][1]
34
+ low = int(position_ids.min())
35
+ if low < 0:
36
+ raise ValueError("indexer position IDs must be non-negative")
37
+ bounds = (low, int(position_ids.max()))
38
+ if scope is not None:
39
+ scope["bounds"][key] = (position_ids, bounds)
40
+ return bounds
kernel/deps/flash_attn/AUTHORS ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Tri Dao, tri@tridao.me
2
+ Jay Shah
3
+ Ted Zadouri
4
+ Markus Hoehnerbach
5
+ Vijay Thakkar
kernel/deps/flash_attn/LICENSE ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2022, the respective contributors, as shown by the AUTHORS file.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
kernel/deps/flash_attn/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ """Bundled FlashAttention CuTe kernels for KSA V2 inference.
2
+
3
+ This package does not require the separate FlashAttention 2 extension.
4
+ """
kernel/deps/flash_attn/cute/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flash Attention CUTE (CUDA Template Engine) implementation."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ import cutlass.cute as cute
6
+
7
+ from .interface import (
8
+ flash_attn_func,
9
+ flash_attn_varlen_func,
10
+ )
11
+
12
+ from flash_attn.cute.cute_dsl_utils import cute_compile_patched
13
+
14
+ # Patch cute.compile to optionally dump SASS
15
+ cute.compile = cute_compile_patched
16
+
17
+
18
+ __all__ = [
19
+ "flash_attn_func",
20
+ "flash_attn_varlen_func",
21
+ ]
kernel/deps/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()
kernel/deps/flash_attn/cute/barrier.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cutlass
2
+ import cutlass.cute as cute
3
+ from cutlass import Int32
4
+ from cutlass.cutlass_dsl import T, dsl_user_op
5
+ from cutlass._mlir.dialects import llvm
6
+
7
+
8
+ @dsl_user_op
9
+ def ld_acquire(lock_ptr: cute.Pointer, *, loc=None, ip=None) -> cutlass.Int32:
10
+ lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
11
+ state = llvm.inline_asm(
12
+ T.i32(),
13
+ [lock_ptr_i64],
14
+ "ld.global.acquire.gpu.b32 $0, [$1];",
15
+ "=r,l",
16
+ has_side_effects=True,
17
+ is_align_stack=False,
18
+ asm_dialect=llvm.AsmDialect.AD_ATT,
19
+ )
20
+ return cutlass.Int32(state)
21
+
22
+
23
+ @dsl_user_op
24
+ def red_relaxed(
25
+ lock_ptr: cute.Pointer, val: cutlass.Constexpr[Int32], *, loc=None, ip=None
26
+ ) -> None:
27
+ lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
28
+ llvm.inline_asm(
29
+ None,
30
+ [lock_ptr_i64, Int32(val).ir_value(loc=loc, ip=ip)],
31
+ "red.relaxed.gpu.global.add.s32 [$0], $1;",
32
+ "l,r",
33
+ has_side_effects=True,
34
+ is_align_stack=False,
35
+ asm_dialect=llvm.AsmDialect.AD_ATT,
36
+ )
37
+
38
+
39
+ @dsl_user_op
40
+ def red_release(
41
+ lock_ptr: cute.Pointer, val: cutlass.Constexpr[Int32], *, loc=None, ip=None
42
+ ) -> None:
43
+ lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
44
+ llvm.inline_asm(
45
+ None,
46
+ [lock_ptr_i64, Int32(val).ir_value(loc=loc, ip=ip)],
47
+ "red.release.gpu.global.add.s32 [$0], $1;",
48
+ "l,r",
49
+ has_side_effects=True,
50
+ is_align_stack=False,
51
+ asm_dialect=llvm.AsmDialect.AD_ATT,
52
+ )
53
+
54
+
55
+ @cute.jit
56
+ def wait_eq(lock_ptr: cute.Pointer, thread_idx: int | Int32, flag_offset: int, val: Int32) -> None:
57
+ flag_ptr = lock_ptr + flag_offset
58
+ if thread_idx == 0:
59
+ read_val = Int32(0)
60
+ while read_val != val:
61
+ read_val = ld_acquire(flag_ptr)
62
+
63
+
64
+ @cute.jit
65
+ def arrive_inc(
66
+ lock_ptr: cute.Pointer, thread_idx: int | Int32, flag_offset: int, val: cutlass.Constexpr[Int32]
67
+ ) -> None:
68
+ flag_ptr = lock_ptr + flag_offset
69
+ if thread_idx == 0:
70
+ red_release(flag_ptr, val)
71
+ # red_relaxed(flag_ptr, val)
kernel/deps/flash_attn/cute/blackwell_helpers.py ADDED
@@ -0,0 +1,753 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ from typing import Optional, Tuple
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+ from cutlass import Int32, Boolean, const_expr
7
+ from cutlass.cute.nvgpu import tcgen05
8
+ from cutlass._mlir.dialects import llvm
9
+
10
+ import flash_attn.cute.mma_sm100_desc as sm100_desc
11
+ from flash_attn.cute.utils import parse_swizzle_from_pointer
12
+
13
+
14
+ @cute.jit
15
+ def gemm_w_idx(
16
+ tiled_mma: cute.TiledMma,
17
+ acc: cute.Tensor,
18
+ tCrA: cute.Tensor,
19
+ tCrB: cute.Tensor,
20
+ A_idx: Optional[Int32] = None,
21
+ B_idx: Optional[Int32] = None,
22
+ zero_init: bool | Boolean = False,
23
+ swap_AB: bool = False,
24
+ ) -> None:
25
+ if const_expr(swap_AB):
26
+ return gemm_w_idx(
27
+ tiled_mma, acc, tCrB, tCrA, B_idx, A_idx, zero_init=zero_init, swap_AB=False
28
+ )
29
+ else:
30
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
31
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
32
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
33
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
34
+ mma_atom.set(tcgen05.Field.ACCUMULATE, not zero_init or k != 0)
35
+ cute.gemm(mma_atom, acc, rA[None, None, k], rB[None, None, k], acc)
36
+
37
+
38
+ @cute.jit
39
+ def gemm_ptx_w_idx(
40
+ tiled_mma: cute.TiledMma,
41
+ acc: cute.Tensor,
42
+ tCrA: cute.Tensor,
43
+ tCrB: cute.Tensor,
44
+ sA: Optional[cute.Tensor],
45
+ sB: cute.Tensor,
46
+ A_idx: Optional[Int32] = None,
47
+ B_idx: Optional[Int32] = None,
48
+ zero_init: bool | Boolean = False,
49
+ **kwargs,
50
+ ) -> None:
51
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
52
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
53
+ sA_cur = None
54
+ if const_expr(sA is not None):
55
+ sA_cur = sA if const_expr(A_idx is None) else sA[None, None, None, A_idx]
56
+ sB_cur = sB if const_expr(B_idx is None) else sB[None, None, None, B_idx]
57
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
58
+ acc_tmem_addr = acc.iterator.toint()
59
+ gemm_ptx_partial(
60
+ mma_atom.op, acc_tmem_addr, rA, rB, sA_cur, sB_cur, zero_init=zero_init, **kwargs
61
+ )
62
+
63
+
64
+ @cute.jit
65
+ def gemm(
66
+ tiled_mma: cute.TiledMma,
67
+ acc: cute.Tensor,
68
+ tCrA: cute.Tensor,
69
+ tCrB: cute.Tensor,
70
+ zero_init: bool | Boolean = False,
71
+ ) -> cute.TiledMma:
72
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
73
+ tiled_mma.set(tcgen05.Field.ACCUMULATE, not zero_init or k != 0)
74
+ cute.gemm(tiled_mma, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
75
+ return tiled_mma
76
+
77
+
78
+ def i64_to_i32x2(i: int) -> Tuple[int, int]:
79
+ """Convert a 64-bit integer to a tuple of two 32-bit integers."""
80
+ return i & 0xFFFF_FFFF, (i >> 32) & 0xFFFF_FFFF
81
+
82
+
83
+ @cute.jit
84
+ def gemm_ptx(
85
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
86
+ acc: cute.Tensor,
87
+ tCrA: cute.Tensor,
88
+ tCrB: cute.Tensor,
89
+ sA: Optional[cute.Tensor],
90
+ sB: cute.Tensor,
91
+ zero_init: bool | Boolean = False,
92
+ ) -> None:
93
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
94
+ if const_expr(not is_ts):
95
+ assert sA is not None, "sA must be provided when a_src is not TMEM"
96
+ sA_layout = sA.layout if sA is not None else None
97
+ sB_layout = sB.layout
98
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
99
+ if const_expr(not is_ts):
100
+ sA_swizzle = parse_swizzle_from_pointer(sA.iterator)
101
+ smem_desc_base_a: int = const_expr(
102
+ sm100_desc.make_smem_desc_base(
103
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
104
+ sA_swizzle,
105
+ sm100_desc.Major.K
106
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
107
+ else sm100_desc.Major.MN,
108
+ )
109
+ )
110
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
111
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
112
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
113
+ else:
114
+ smem_desc_base_a = None
115
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
116
+ sB_swizzle = parse_swizzle_from_pointer(sB.iterator)
117
+ smem_desc_base_b: int = const_expr(
118
+ sm100_desc.make_smem_desc_base(
119
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
120
+ sB_swizzle,
121
+ sm100_desc.Major.K
122
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
123
+ else sm100_desc.Major.MN,
124
+ )
125
+ )
126
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
127
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
128
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
129
+
130
+ if const_expr(not is_ts):
131
+ smem_desc_start_a_lo = Int32(smem_desc_base_a_lo) | sm100_desc.make_smem_desc_start_addr(
132
+ sA[None, None, 0].iterator
133
+ )
134
+ else:
135
+ smem_desc_start_a_lo = None
136
+ smem_desc_start_b_lo = Int32(smem_desc_base_b_lo) | sm100_desc.make_smem_desc_start_addr(
137
+ sB[None, None, 0].iterator
138
+ )
139
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
140
+ if const_expr(not is_ts):
141
+ smem_desc_a_lo = smem_desc_start_a_lo + (
142
+ (cute.crd2idx((0, 0, k), sA_layout) * sA.element_type.width // 8) >> 4
143
+ )
144
+ smem_desc_b_lo = smem_desc_start_b_lo + (
145
+ (cute.crd2idx((0, 0, k), sB_layout) * sB.element_type.width // 8) >> 4
146
+ )
147
+ # with cute.arch.elect_one():
148
+ # cute.printf("smem_desc_a_lo = {}, smem_desc_b_lo = {}", smem_desc_a_lo, smem_desc_b_lo)
149
+ # cute.printf("smem_desc_a_lo_correct = {}, smem_desc_b_lo_correct = {}", smem_desc_a_lo_correct, smem_desc_b_lo_correct)
150
+ with cute.arch.elect_one():
151
+ if const_expr(not is_ts):
152
+ llvm.inline_asm(
153
+ None,
154
+ [
155
+ acc.iterator.toint().ir_value(),
156
+ smem_desc_a_lo.ir_value(),
157
+ smem_desc_b_lo.ir_value(),
158
+ Int32(not zero_init or k != 0).ir_value(),
159
+ ],
160
+ "{\n\t"
161
+ ".reg .pred p;\n\t"
162
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
163
+ ".reg .b32 idesc;\n\t"
164
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
165
+ f"mov.b64 smem_desc_a, {{$1, {hex(smem_desc_a_hi)}}};\n\t"
166
+ f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
167
+ "setp.ne.b32 p, $3, 0;\n\t"
168
+ f"tcgen05.mma.cta_group::1.kind::f16 [$0], smem_desc_a, smem_desc_b, idesc, p;\n\t"
169
+ "}\n",
170
+ "r,r,r,r",
171
+ has_side_effects=True,
172
+ is_align_stack=False,
173
+ asm_dialect=llvm.AsmDialect.AD_ATT,
174
+ )
175
+ else:
176
+ llvm.inline_asm(
177
+ None,
178
+ [
179
+ acc.iterator.toint().ir_value(),
180
+ tCrA[None, None, k].iterator.toint().ir_value(),
181
+ smem_desc_b_lo.ir_value(),
182
+ Int32(not zero_init or k != 0).ir_value(),
183
+ ],
184
+ "{\n\t"
185
+ ".reg .pred p;\n\t"
186
+ ".reg .b64 smem_desc_b;\n\t"
187
+ f"mov.b64 smem_desc_b, {{$2, {hex(smem_desc_b_hi)}}};\n\t"
188
+ "setp.ne.b32 p, $3, 0;\n\t"
189
+ f"tcgen05.mma.cta_group::1.kind::f16 [$0], [$1], smem_desc_b, {hex(idesc)}, p;\n\t"
190
+ "}\n",
191
+ "r,r,r,r",
192
+ has_side_effects=True,
193
+ is_align_stack=False,
194
+ asm_dialect=llvm.AsmDialect.AD_ATT,
195
+ )
196
+
197
+
198
+ @cute.jit
199
+ def gemm_ptx_loop(
200
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
201
+ acc: cute.Tensor,
202
+ tCrA: cute.Tensor,
203
+ tCrB: cute.Tensor,
204
+ sA: Optional[cute.Tensor],
205
+ sB: cute.Tensor,
206
+ zero_init: bool | Boolean = False,
207
+ ) -> None:
208
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
209
+ if const_expr(not is_ts):
210
+ assert sA is not None, "sA must be provided when a_src is not TMEM"
211
+ sA_layout = sA.layout if sA is not None else tCrA.layout
212
+ sB_layout = sB.layout
213
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
214
+ if const_expr(not is_ts):
215
+ sA_swizzle = parse_swizzle_from_pointer(sA.iterator)
216
+ smem_desc_base_a: int = const_expr(
217
+ sm100_desc.make_smem_desc_base(
218
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
219
+ sA_swizzle,
220
+ sm100_desc.Major.K
221
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
222
+ else sm100_desc.Major.MN,
223
+ )
224
+ )
225
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
226
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
227
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
228
+ else:
229
+ smem_desc_base_a = None
230
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
231
+ sB_swizzle = parse_swizzle_from_pointer(sB.iterator)
232
+ smem_desc_base_b: int = const_expr(
233
+ sm100_desc.make_smem_desc_base(
234
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
235
+ sB_swizzle,
236
+ sm100_desc.Major.K
237
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
238
+ else sm100_desc.Major.MN,
239
+ )
240
+ )
241
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
242
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
243
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
244
+
245
+ if const_expr(not is_ts):
246
+ offset_a = [
247
+ (cute.crd2idx((0, 0, k), sA_layout) * sA.element_type.width // 8) >> 4
248
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2]))
249
+ ]
250
+ else:
251
+ offset_a = [
252
+ cute.crd2idx((0, 0, k), sA_layout) * op.a_dtype.width // 32
253
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2]))
254
+ ]
255
+ offset_a_diff = [
256
+ offset_a[k] - offset_a[k - 1] for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
257
+ ]
258
+ offset_b = [
259
+ (cute.crd2idx((0, 0, k), sB_layout) * sB.element_type.width // 8) >> 4
260
+ for k in cutlass.range_constexpr(cute.size(tCrB.shape[2]))
261
+ ]
262
+ offset_b_diff = [
263
+ offset_b[k] - offset_b[k - 1] for k in cutlass.range_constexpr(1, cute.size(tCrB.shape[2]))
264
+ ]
265
+
266
+ if const_expr(not is_ts):
267
+ smem_desc_start_a_lo = Int32(
268
+ smem_desc_base_a_lo | sm100_desc.make_smem_desc_start_addr(sA[None, None, 0].iterator)
269
+ )
270
+ else:
271
+ smem_desc_start_a_lo = None
272
+ smem_desc_start_b_lo = Int32(
273
+ smem_desc_base_b_lo | sm100_desc.make_smem_desc_start_addr(sB[None, None, 0].iterator)
274
+ )
275
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
276
+ if const_expr(not is_ts):
277
+ llvm.inline_asm(
278
+ None,
279
+ [
280
+ acc.iterator.toint().ir_value(),
281
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
282
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
283
+ Int32(not zero_init).ir_value(),
284
+ ],
285
+ "{\n\t"
286
+ ".reg .pred leader_thread;\n\t"
287
+ ".reg .pred p;\n\t"
288
+ ".reg .b32 idesc;\n\t"
289
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
290
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
291
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
292
+ "elect.sync _|leader_thread, -1;\n\t"
293
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
294
+ "mov.b32 smem_desc_a_lo, $1;\n\t"
295
+ "mov.b32 smem_desc_b_lo, $2;\n\t"
296
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
297
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
298
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
299
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
300
+ "setp.ne.b32 p, $3, 0;\n\t"
301
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
302
+ + "".join(
303
+ (
304
+ f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
305
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
306
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
307
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
308
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
309
+ )
310
+ for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
311
+ )
312
+ + "}\n",
313
+ "r,r,r,r",
314
+ has_side_effects=True,
315
+ is_align_stack=False,
316
+ asm_dialect=llvm.AsmDialect.AD_ATT,
317
+ )
318
+ else:
319
+ llvm.inline_asm(
320
+ None,
321
+ [
322
+ acc.iterator.toint().ir_value(),
323
+ Int32(tCrA[None, None, 0].iterator.toint()).ir_value(),
324
+ Int32(smem_desc_start_b_lo).ir_value(),
325
+ Int32(not zero_init).ir_value(),
326
+ ],
327
+ "{\n\t"
328
+ ".reg .pred leader_thread;\n\t"
329
+ ".reg .pred p;\n\t"
330
+ ".reg .b32 idesc;\n\t"
331
+ ".reg .b32 tmem_a;\n\t"
332
+ ".reg .b32 smem_desc_b_lo;\n\t"
333
+ ".reg .b32 smem_desc_b_hi;\n\t"
334
+ ".reg .b64 smem_desc_b;\n\t"
335
+ "elect.sync _|leader_thread, -1;\n\t"
336
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
337
+ "mov.b32 tmem_a, $1;\n\t"
338
+ "mov.b32 smem_desc_b_lo, $2;\n\t"
339
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
340
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
341
+ "setp.ne.b32 p, $3, 0;\n\t"
342
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
343
+ + "".join(
344
+ (
345
+ # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
346
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
347
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
348
+ # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, 1;\n\t"
349
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
350
+ )
351
+ for k in cutlass.range_constexpr(1, cute.size(tCrA.shape[2]))
352
+ )
353
+ + "}\n",
354
+ "r,r,r,r",
355
+ has_side_effects=True,
356
+ is_align_stack=False,
357
+ asm_dialect=llvm.AsmDialect.AD_ATT,
358
+ )
359
+
360
+
361
+ @cute.jit
362
+ def gemm_ptx_partial(
363
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
364
+ acc_tmem_addr: Int32,
365
+ tCrA: cute.Tensor,
366
+ tCrB: cute.Tensor,
367
+ sA: Optional[cute.Tensor],
368
+ sB: cute.Tensor,
369
+ mbar_ptr: Optional[cutlass.Pointer] = None,
370
+ mbar_phase: Optional[Int32] = None,
371
+ zero_init: bool | Boolean = False,
372
+ # sA_offset: Int32 = 0,
373
+ # acc_offset: Int32 = 0,
374
+ tA_addr: Optional[Int32] = None,
375
+ ) -> None:
376
+ # acc_tmem_addr += acc_offset
377
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
378
+ if const_expr(not is_ts):
379
+ assert sA is not None, "sA must be provided when a_src is not TMEM"
380
+ sA_layout = sA.layout if sA is not None else tCrA.layout
381
+ sB_layout = sB.layout
382
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
383
+ if const_expr(not is_ts):
384
+ sA_swizzle = parse_swizzle_from_pointer(sA.iterator)
385
+ smem_desc_base_a: int = const_expr(
386
+ sm100_desc.make_smem_desc_base(
387
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
388
+ sA_swizzle,
389
+ sm100_desc.Major.K
390
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
391
+ else sm100_desc.Major.MN,
392
+ )
393
+ )
394
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
395
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
396
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
397
+ else:
398
+ smem_desc_base_a = None
399
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
400
+ sB_swizzle = parse_swizzle_from_pointer(sB.iterator)
401
+ smem_desc_base_b: int = const_expr(
402
+ sm100_desc.make_smem_desc_base(
403
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
404
+ sB_swizzle,
405
+ sm100_desc.Major.K
406
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
407
+ else sm100_desc.Major.MN,
408
+ )
409
+ )
410
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
411
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
412
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
413
+
414
+ tCrA_layout = (
415
+ tCrA.layout
416
+ if const_expr(not is_ts)
417
+ else cute.recast_layout(32, tCrA.element_type.width, tCrA.layout)
418
+ )
419
+ offset_a = [cute.crd2idx((0, 0, k), tCrA_layout) for k in range(cute.size(tCrA.shape[2]))]
420
+ offset_a_diff = [offset_a[k] - offset_a[k - 1] for k in range(1, cute.size(tCrA.shape[2]))]
421
+ offset_b = [cute.crd2idx((0, 0, k), tCrB.layout) for k in range(cute.size(tCrB.shape[2]))]
422
+ offset_b_diff = [offset_b[k] - offset_b[k - 1] for k in range(1, cute.size(tCrB.shape[2]))]
423
+
424
+ if const_expr(not is_ts):
425
+ smem_desc_start_a_lo = Int32(
426
+ smem_desc_base_a_lo | sm100_desc.make_smem_desc_start_addr(sA[None, None, 0].iterator)
427
+ )
428
+ # ) + sA_offset
429
+ else:
430
+ smem_desc_start_a_lo = None
431
+ smem_desc_start_b_lo = Int32(
432
+ smem_desc_base_b_lo | sm100_desc.make_smem_desc_start_addr(sB[None, None, 0].iterator)
433
+ )
434
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
435
+ if const_expr(not is_ts):
436
+ assert mbar_ptr is None, "mbar_ptr must be None when a_src is not TMEM"
437
+ llvm.inline_asm(
438
+ None,
439
+ [
440
+ # acc.iterator.toint().ir_value(),
441
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
442
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
443
+ Int32(not zero_init).ir_value(),
444
+ Int32(cute.arch.make_warp_uniform(acc_tmem_addr)).ir_value(),
445
+ ],
446
+ "{\n\t"
447
+ ".reg .pred leader_thread;\n\t"
448
+ ".reg .pred p;\n\t"
449
+ ".reg .b32 idesc;\n\t"
450
+ ".reg .b32 tmem_acc;\n\t"
451
+ ".reg .b32 smem_desc_a_lo_start, smem_desc_b_lo_start;\n\t"
452
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
453
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
454
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
455
+ "elect.sync _|leader_thread, -1;\n\t"
456
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
457
+ # f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
458
+ f"mov.b32 tmem_acc, $3;\n\t"
459
+ "mov.b32 smem_desc_a_lo_start, $0;\n\t"
460
+ "mov.b32 smem_desc_b_lo_start, $1;\n\t"
461
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
462
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
463
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo_start, smem_desc_a_hi}};\n\t"
464
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
465
+ "setp.ne.b32 p, $2, 0;\n\t"
466
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, {pred_str};\n\t"
467
+ + "".join(
468
+ (
469
+ # f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
470
+ # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
471
+ f"add.u32 smem_desc_a_lo, smem_desc_a_lo_start, {hex(offset_a[k])};\n\t"
472
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
473
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
474
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
475
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, 1;\n\t"
476
+ )
477
+ for k in range(1, cute.size(tCrA.shape[2]))
478
+ )
479
+ + "}\n",
480
+ # "r,r,r",
481
+ "r,r,r,r",
482
+ has_side_effects=True,
483
+ is_align_stack=False,
484
+ asm_dialect=llvm.AsmDialect.AD_ATT,
485
+ )
486
+ else:
487
+ # For TS gemm, somehow tCrA.iterator.toint() returns 0 no matter what, so we need to
488
+ # explicitly pass in the tA_addr for correctness.
489
+ tA_addr = tCrA[None, None, 0].iterator.toint() if tA_addr is None else tA_addr
490
+ input_args = [
491
+ # Int32(cute.arch.make_warp_uniform(tCrA[None, None, 0].iterator.toint())).ir_value(),
492
+ Int32(cute.arch.make_warp_uniform(tA_addr)).ir_value(),
493
+ Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
494
+ Int32(not zero_init).ir_value(),
495
+ Int32(cute.arch.make_warp_uniform(acc_tmem_addr)).ir_value(),
496
+ ]
497
+ if const_expr(mbar_ptr is not None):
498
+ assert mbar_phase is not None, "mbar_phase must be provided when mbar_ptr is not None"
499
+ input_args.append(mbar_ptr.toint().ir_value())
500
+ input_args.append(Int32(mbar_phase).ir_value())
501
+ mbar_wait_str = (
502
+ ".reg .pred P1; \n\t"
503
+ "LAB_WAIT: \n\t"
504
+ "mbarrier.try_wait.parity.shared::cta.b64 P1, [$4], $5, 10000000; \n\t"
505
+ "@P1 bra DONE; \n\t"
506
+ "bra LAB_WAIT; \n\t"
507
+ "DONE: \n\t"
508
+ )
509
+ else:
510
+ mbar_wait_str = ""
511
+ llvm.inline_asm(
512
+ None,
513
+ # [
514
+ # # acc.iterator.toint().ir_value(),
515
+ # Int32(tCrA[None, None, 0].iterator.toint()).ir_value(),
516
+ # Int32(smem_desc_start_b_lo).ir_value(),
517
+ # Int32(not zero_init).ir_value(),
518
+ # ],
519
+ input_args,
520
+ "{\n\t"
521
+ ".reg .pred leader_thread;\n\t"
522
+ ".reg .pred p;\n\t"
523
+ ".reg .b32 idesc;\n\t"
524
+ ".reg .b32 tmem_acc;\n\t"
525
+ ".reg .b32 tmem_a;\n\t"
526
+ ".reg .b32 smem_desc_b_lo_start;\n\t"
527
+ ".reg .b32 smem_desc_b_lo;\n\t"
528
+ ".reg .b32 smem_desc_b_hi;\n\t"
529
+ ".reg .b64 smem_desc_b;\n\t"
530
+ "elect.sync _|leader_thread, -1;\n\t"
531
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
532
+ # f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
533
+ f"mov.b32 tmem_acc, $3;\n\t"
534
+ f"mov.b32 tmem_a, $0;\n\t"
535
+ f"mov.b32 smem_desc_b_lo_start, $1;\n\t"
536
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
537
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo_start, smem_desc_b_hi}};\n\t"
538
+ "setp.ne.b32 p, $2, 0;\n\t"
539
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, {pred_str};\n\t"
540
+ + "".join(
541
+ (
542
+ # f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
543
+ # f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
544
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo_start, {hex(offset_b[k])};\n\t"
545
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
546
+ # f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a], smem_desc_b, idesc, 1;\n\t"
547
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
548
+ )
549
+ for k in range(
550
+ 1,
551
+ cute.size(tCrA.shape[2])
552
+ if const_expr(mbar_ptr is None)
553
+ else cute.size(tCrA.shape[2]) // 4 * 3,
554
+ )
555
+ )
556
+ + mbar_wait_str
557
+ + (
558
+ "".join(
559
+ (
560
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
561
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
562
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], [tmem_a + {hex(offset_a[k])}], smem_desc_b, idesc, 1;\n\t"
563
+ )
564
+ for k in range(cute.size(tCrA.shape[2]) // 4 * 3, cute.size(tCrA.shape[2]))
565
+ )
566
+ if const_expr(mbar_ptr is not None)
567
+ else ""
568
+ )
569
+ + "}\n",
570
+ "r,r,r,r" if const_expr(mbar_ptr is None) else "r,r,r,r,r,r",
571
+ has_side_effects=True,
572
+ is_align_stack=False,
573
+ asm_dialect=llvm.AsmDialect.AD_ATT,
574
+ )
575
+
576
+
577
+ @cute.jit
578
+ def gemm_ptx_partial1(
579
+ op: cute.nvgpu.tcgen05.mma.MmaOp,
580
+ acc_tmem_addr: cutlass.Constexpr[int],
581
+ tCrA: cute.Tensor,
582
+ tCrB: cute.Tensor,
583
+ sA_base_addr_for_desc: Int32,
584
+ sA_addr_offset_for_desc: cutlass.Constexpr[int],
585
+ sA_stage: Int32,
586
+ sB_base_addr_for_desc: Int32,
587
+ sB_addr_offset_for_desc: cutlass.Constexpr[int],
588
+ sB_stage: Int32,
589
+ sA_layout: Optional[cute.Layout],
590
+ sB_layout: Optional[cute.Layout],
591
+ sA_swizzle: Optional[cute.Swizzle],
592
+ sB_swizzle: cute.Swizzle,
593
+ zero_init: bool | Boolean = False,
594
+ ) -> None:
595
+ is_ts = op.a_src == cute.nvgpu.tcgen05.OperandSource.TMEM
596
+ if const_expr(not is_ts):
597
+ assert sA_layout is not None, "sA_layout must be provided when a_src is not TMEM"
598
+ assert sA_swizzle is not None, "sA_swizzle must be provided when a_src is not TMEM"
599
+ idesc: int = const_expr(sm100_desc.mma_op_to_idesc(op))
600
+ if const_expr(not is_ts):
601
+ smem_desc_base_a: int = const_expr(
602
+ sm100_desc.make_smem_desc_base(
603
+ cute.recast_layout(128, op.a_dtype.width, sA_layout[0]),
604
+ sA_swizzle,
605
+ sm100_desc.Major.K
606
+ if const_expr(op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
607
+ else sm100_desc.Major.MN,
608
+ )
609
+ )
610
+ smem_desc_base_a_lo, smem_desc_a_hi = i64_to_i32x2(smem_desc_base_a)
611
+ smem_desc_base_a_lo = const_expr(smem_desc_base_a_lo)
612
+ smem_desc_a_hi = const_expr(smem_desc_a_hi)
613
+ else:
614
+ smem_desc_base_a = None
615
+ smem_desc_base_a_lo, smem_desc_a_hi = None, None
616
+ smem_desc_base_b: int = const_expr(
617
+ sm100_desc.make_smem_desc_base(
618
+ cute.recast_layout(128, op.b_dtype.width, sB_layout[0]),
619
+ sB_swizzle,
620
+ sm100_desc.Major.K
621
+ if const_expr(op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K)
622
+ else sm100_desc.Major.MN,
623
+ )
624
+ )
625
+ smem_desc_base_b_lo, smem_desc_b_hi = i64_to_i32x2(smem_desc_base_b)
626
+ smem_desc_base_b_lo = const_expr(smem_desc_base_b_lo)
627
+ smem_desc_b_hi = const_expr(smem_desc_b_hi)
628
+ mask = [Int32(0)] * 4
629
+
630
+ if const_expr(not is_ts):
631
+ offset_a = [
632
+ (cute.crd2idx((0, 0, k), sA_layout) * op.a_dtype.width // 8) >> 4
633
+ for k in range(cute.size(tCrA.shape[2]))
634
+ ]
635
+ else:
636
+ offset_a = [
637
+ cute.crd2idx((0, 0, k), sA_layout) * op.a_dtype.width // 32
638
+ for k in range(cute.size(tCrA.shape[2]))
639
+ ]
640
+ offset_a_diff = [offset_a[k] - offset_a[k - 1] for k in range(1, cute.size(tCrA.shape[2]))]
641
+ offset_b = [
642
+ (cute.crd2idx((0, 0, k), sB_layout) * op.b_dtype.width // 8) >> 4
643
+ for k in range(cute.size(tCrB.shape[2]))
644
+ ]
645
+ offset_b_diff = [offset_b[k] - offset_b[k - 1] for k in range(1, cute.size(tCrB.shape[2]))]
646
+
647
+ if const_expr(not is_ts):
648
+ # smem_desc_start_a_lo = Int32(smem_desc_base_a_lo | sm100_desc.make_smem_desc_start_addr(sA[None, None, 0].iterator))
649
+ smem_desc_start_a_lo = const_expr(smem_desc_base_a_lo)
650
+ else:
651
+ smem_desc_start_a_lo = None
652
+ # smem_desc_start_b_lo = Int32(smem_desc_base_b_lo | sm100_desc.make_smem_desc_start_addr(sB[None, None, 0].iterator))
653
+ smem_desc_start_b_lo = const_expr(smem_desc_base_b_lo)
654
+ pred_str = "p" if isinstance(zero_init, Boolean) else "0" if zero_init else "1"
655
+ if const_expr(not is_ts):
656
+ llvm.inline_asm(
657
+ None,
658
+ [
659
+ # acc.iterator.toint().ir_value(),
660
+ # Int32(cute.arch.make_warp_uniform(smem_desc_start_a_lo)).ir_value(),
661
+ Int32(sA_base_addr_for_desc).ir_value(),
662
+ Int32(sA_stage).ir_value(),
663
+ # Int32(cute.arch.make_warp_uniform(smem_desc_start_b_lo)).ir_value(),
664
+ Int32(sB_base_addr_for_desc).ir_value(),
665
+ Int32(sB_stage).ir_value(),
666
+ Int32(not zero_init).ir_value(),
667
+ mask[0].ir_value(),
668
+ mask[1].ir_value(),
669
+ mask[2].ir_value(),
670
+ mask[3].ir_value(),
671
+ ],
672
+ "{\n\t"
673
+ ".reg .pred leader_thread;\n\t"
674
+ ".reg .pred p;\n\t"
675
+ ".reg .b32 idesc;\n\t"
676
+ ".reg .b32 tmem_acc;\n\t"
677
+ ".reg .b32 smem_desc_a_lo, smem_desc_b_lo;\n\t"
678
+ ".reg .b32 smem_desc_a_hi, smem_desc_b_hi;\n\t"
679
+ ".reg .b64 smem_desc_a, smem_desc_b;\n\t"
680
+ "elect.sync _|leader_thread, -1;\n\t"
681
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
682
+ f"mov.b32 tmem_acc, {hex(acc_tmem_addr)};\n\t"
683
+ # "mov.b32 smem_desc_a_lo, $0;\n\t"
684
+ # f"add.u32 smem_desc_a_lo, $0, {hex(smem_desc_start_a_lo)};\n\t"
685
+ f"mad.lo.u32 smem_desc_a_lo, $1, {hex(sA_addr_offset_for_desc)}, $0;\n\t"
686
+ # "mov.b32 smem_desc_b_lo, $2;\n\t"
687
+ f"mad.lo.u32 smem_desc_b_lo, $3, {hex(sB_addr_offset_for_desc)}, $2;\n\t"
688
+ f"mov.b32 smem_desc_a_hi, {hex(smem_desc_a_hi)};\n\t"
689
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
690
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
691
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
692
+ "setp.ne.b32 p, $4, 0;\n\t"
693
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, {{$5, $6, $7, $8}}, {pred_str};\n\t"
694
+ + "".join(
695
+ (
696
+ f"add.u32 smem_desc_a_lo, smem_desc_a_lo, {hex(offset_a_diff[k - 1])};\n\t"
697
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
698
+ f"mov.b64 smem_desc_a, {{smem_desc_a_lo, smem_desc_a_hi}};\n\t"
699
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
700
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [tmem_acc], smem_desc_a, smem_desc_b, idesc, {{$5, $6, $7, $8}}, 1;\n\t"
701
+ )
702
+ for k in range(1, cute.size(tCrA.shape[2]))
703
+ )
704
+ + "}\n",
705
+ "r,r,r,r,r,r,r,r,r",
706
+ has_side_effects=True,
707
+ is_align_stack=False,
708
+ asm_dialect=llvm.AsmDialect.AD_ATT,
709
+ )
710
+ else:
711
+ llvm.inline_asm(
712
+ None,
713
+ [
714
+ # acc.iterator.toint().ir_value(),
715
+ Int32(tCrA[None, None, 0].iterator.toint()).ir_value(),
716
+ Int32(smem_desc_start_b_lo).ir_value(),
717
+ Int32(not zero_init).ir_value(),
718
+ mask[0].ir_value(),
719
+ mask[1].ir_value(),
720
+ mask[2].ir_value(),
721
+ mask[3].ir_value(),
722
+ ],
723
+ "{\n\t"
724
+ ".reg .pred leader_thread;\n\t"
725
+ ".reg .pred p;\n\t"
726
+ ".reg .b32 idesc;\n\t"
727
+ ".reg .b32 tmem_a;\n\t"
728
+ ".reg .b32 smem_desc_b_lo;\n\t"
729
+ ".reg .b32 smem_desc_b_hi;\n\t"
730
+ ".reg .b64 smem_desc_b;\n\t"
731
+ "elect.sync _|leader_thread, -1;\n\t"
732
+ f"mov.b32 idesc, {hex(idesc)};\n\t"
733
+ f"mov.b32 tmem_a, $1;\n\t"
734
+ f"mov.b32 smem_desc_b_lo, $2;\n\t"
735
+ f"mov.b32 smem_desc_b_hi, {hex(smem_desc_b_hi)};\n\t"
736
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
737
+ "setp.ne.b32 p, $3, 0;\n\t"
738
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, {{$4, $5, $6, $7}}, {pred_str};\n\t"
739
+ + "".join(
740
+ (
741
+ f"add.u32 tmem_a, tmem_a, {hex(offset_a_diff[k - 1])};\n\t"
742
+ f"add.u32 smem_desc_b_lo, smem_desc_b_lo, {hex(offset_b_diff[k - 1])};\n\t"
743
+ f"mov.b64 smem_desc_b, {{smem_desc_b_lo, smem_desc_b_hi}};\n\t"
744
+ f"@leader_thread tcgen05.mma.cta_group::1.kind::f16 [$0], [tmem_a], smem_desc_b, idesc, {{$4, $5, $6, $7}}, 1;\n\t"
745
+ )
746
+ for k in range(1, cute.size(tCrA.shape[2]))
747
+ )
748
+ + "}\n",
749
+ "r,r,r,r,r,r,r,r",
750
+ has_side_effects=True,
751
+ is_align_stack=False,
752
+ asm_dialect=llvm.AsmDialect.AD_ATT,
753
+ )
kernel/deps/flash_attn/cute/block_info.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 flash_attn.cute.seqlen_info import SeqlenInfoQK
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: cutlass.Int32 = 0,
29
+ num_splits: cutlass.Int32 = 1,
30
+ ) -> Tuple[Int32, Int32]:
31
+ n_block_max = cute.ceil_div(seqlen_info.seqlen_k, self.tile_n)
32
+ if const_expr(self.is_causal or (self.is_local and self.window_size_right is not None)):
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
+ cutlass.Int32(0)
50
+ if n_block_max <= n_block_min
51
+ else (n_block_max - n_block_min + num_splits - 1) // num_splits
52
+ )
53
+ n_block_min = n_block_min + split_idx * num_n_blocks_per_split
54
+ n_block_max = cutlass.min(n_block_min + num_n_blocks_per_split, n_block_max)
55
+ return n_block_min, n_block_max
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_min_causal_local_mask(
75
+ self,
76
+ seqlen_info: SeqlenInfoQK,
77
+ m_block: Int32,
78
+ n_block_min: Int32,
79
+ ) -> Int32:
80
+ """If we have separate iterations with causal or local masking at the start, where do we stop"""
81
+ m_idx_min = m_block * self.tile_m
82
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
83
+ m_idx_min = m_idx_min // self.qhead_per_kvhead_packgqa
84
+ n_idx = m_idx_min + seqlen_info.seqlen_k - seqlen_info.seqlen_q
85
+ n_idx_right = (
86
+ n_idx
87
+ if const_expr(not self.is_local or self.window_size_right is None)
88
+ else n_idx + self.window_size_right
89
+ )
90
+ return cutlass.max(n_block_min, n_idx_right // self.tile_n)
91
+
92
+ @cute.jit
93
+ def get_n_block_min_before_local_mask(
94
+ self,
95
+ seqlen_info: SeqlenInfoQK,
96
+ m_block: Int32,
97
+ n_block_min: Int32,
98
+ ) -> Int32:
99
+ """If we have separate iterations with local masking at the end, where do we stop the non-masked iterations"""
100
+ if const_expr(not self.is_local or self.window_size_left is None):
101
+ return n_block_min
102
+ else:
103
+ m_idx_max = (m_block + 1) * self.tile_m
104
+ if const_expr(self.qhead_per_kvhead_packgqa > 1):
105
+ m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
106
+ n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
107
+ n_idx_left = n_idx - self.window_size_left
108
+ return cutlass.max(n_block_min, cute.ceil_div(n_idx_left, self.tile_n))
kernel/deps/flash_attn/cute/block_sparse_utils.py ADDED
@@ -0,0 +1,1448 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Block-sparse runtime utilities for CUTE DSL kernels.
3
+
4
+ This module contains runtime execution functions for block-sparse attention kernels.
5
+ These utilities are used by CUTE DSL kernels to produce and consume block-sparse loads.
6
+ """
7
+
8
+ from typing import Callable, Optional
9
+ from functools import partial
10
+ import math
11
+ import cutlass
12
+ import cutlass.cute as cute
13
+ from cutlass import Float32, Int32, const_expr
14
+
15
+ # Import data structures from block_sparsity
16
+ from flash_attn.cute.block_sparsity import BlockSparseTensors
17
+ from flash_attn.cute import utils
18
+ from flash_attn.cute import copy_utils
19
+ from flash_attn.cute.named_barrier import NamedBarrierBwd
20
+
21
+
22
+ @cute.jit
23
+ def load_block_list(
24
+ block_indices: cute.Tensor,
25
+ block_count,
26
+ load_q_with_first: cutlass.Constexpr,
27
+ first_block_preloaded: cutlass.Constexpr,
28
+ kv_producer_state,
29
+ load_Q,
30
+ load_K,
31
+ load_V,
32
+ pipeline_k,
33
+ pipeline_v,
34
+ use_tma_q: cutlass.Constexpr,
35
+ tma_q_bytes: cutlass.Constexpr,
36
+ intra_wg_overlap: cutlass.Constexpr,
37
+ ):
38
+ """Iterate over the sparse blocks and load K, V (and Q) into the pipeline.
39
+ for the intra_wg_overlap case, we overlap the loads of K and V. And this
40
+ means we need to pipeline the last V load from the partial block case,
41
+ with the loads for the full blocks. Set first_block_preloaded when the
42
+ caller has already issued the first K load for the list.
43
+
44
+ Note:
45
+ we iterate along the block_n indices in reverse.
46
+
47
+ Returns:
48
+ Updated kv_producer_state after processing the block list.
49
+
50
+ """
51
+ if block_count > 0:
52
+ if const_expr(not intra_wg_overlap):
53
+ # Peel first iteration: the first block may need to load Q alongside K,
54
+ # Parameters are already Constexpr, so no need to wrap in const_expr()
55
+ n_block_first = block_indices[block_count - 1]
56
+ extra_tx = tma_q_bytes if const_expr(load_q_with_first) and const_expr(use_tma_q) else 0
57
+ pipeline_k.producer_acquire(kv_producer_state, extra_tx_count=extra_tx)
58
+
59
+ if const_expr(load_q_with_first and use_tma_q):
60
+ load_Q(tma_bar_ptr=pipeline_k.producer_get_barrier(kv_producer_state))
61
+
62
+ load_K(src_idx=n_block_first, producer_state=kv_producer_state)
63
+ pipeline_v.producer_acquire(kv_producer_state)
64
+ load_V(src_idx=n_block_first, producer_state=kv_producer_state)
65
+ kv_producer_state.advance()
66
+
67
+ for offset in cutlass.range(1, block_count):
68
+ n_block = block_indices[block_count - 1 - offset]
69
+ pipeline_k.producer_acquire(kv_producer_state)
70
+ load_K(src_idx=n_block, producer_state=kv_producer_state)
71
+ pipeline_v.producer_acquire(kv_producer_state)
72
+ load_V(src_idx=n_block, producer_state=kv_producer_state)
73
+ kv_producer_state.advance()
74
+ else:
75
+ n_block_first = block_indices[block_count - 1]
76
+ if const_expr(not first_block_preloaded):
77
+ extra_tx = (
78
+ tma_q_bytes if const_expr(load_q_with_first) and const_expr(use_tma_q) else 0
79
+ )
80
+ pipeline_k.producer_acquire(kv_producer_state, extra_tx_count=extra_tx)
81
+
82
+ if const_expr(load_q_with_first and use_tma_q):
83
+ load_Q(tma_bar_ptr=pipeline_k.producer_get_barrier(kv_producer_state))
84
+
85
+ load_K(src_idx=n_block_first, producer_state=kv_producer_state)
86
+
87
+ for idx in cutlass.range(block_count - 1, unroll=1):
88
+ n_block_prev = block_indices[block_count - 1 - idx]
89
+ n_block = block_indices[block_count - 2 - idx]
90
+ kv_producer_state_prev = kv_producer_state.clone()
91
+ kv_producer_state.advance()
92
+ pipeline_k.producer_acquire(kv_producer_state)
93
+ load_K(src_idx=n_block, producer_state=kv_producer_state)
94
+ pipeline_v.producer_acquire(kv_producer_state_prev)
95
+ load_V(src_idx=n_block_prev, producer_state=kv_producer_state_prev)
96
+
97
+ return kv_producer_state
98
+
99
+
100
+ @cute.jit
101
+ def finish_overlap_v_load(
102
+ block_indices: cute.Tensor,
103
+ block_count,
104
+ load_V,
105
+ pipeline_v,
106
+ kv_producer_state,
107
+ ):
108
+ """Load the final V block after overlapped K/V loads."""
109
+ if block_count > 0:
110
+ n_block_last = block_indices[0]
111
+ pipeline_v.producer_acquire(kv_producer_state)
112
+ load_V(src_idx=n_block_last, producer_state=kv_producer_state)
113
+ kv_producer_state.advance()
114
+
115
+ return kv_producer_state
116
+
117
+
118
+ @cute.jit
119
+ def sparse_tensor_m_block(
120
+ m_block,
121
+ qhead_per_kvhead: cutlass.Constexpr[int],
122
+ q_subtile_factor: cutlass.Constexpr[int],
123
+ ):
124
+ """Map packed m_block indices to block-sparse tensor indices."""
125
+ block = m_block
126
+ if const_expr(qhead_per_kvhead != 1):
127
+ block = block // qhead_per_kvhead
128
+ if const_expr(q_subtile_factor != 1):
129
+ block = block // q_subtile_factor
130
+ return block
131
+
132
+
133
+ @cute.jit
134
+ def produce_block_sparse_loads(
135
+ blocksparse_tensors: BlockSparseTensors,
136
+ batch_idx,
137
+ head_idx,
138
+ m_block,
139
+ kv_producer_state,
140
+ load_Q,
141
+ load_K,
142
+ load_V,
143
+ pipeline_k,
144
+ pipeline_v,
145
+ use_tma_q: cutlass.Constexpr,
146
+ tma_q_bytes: cutlass.Constexpr,
147
+ intra_wg_overlap: cutlass.Constexpr,
148
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
149
+ q_subtile_factor: cutlass.Constexpr[int] = 1,
150
+ ):
151
+ """Iterate over the mask and full block lists for a single tile.
152
+
153
+ The masked (partial) list may leave the last V load pending when intra-warp-group
154
+ overlap is enabled. The first full block must consume that pending V while
155
+ issuing its own K load on the next pipeline stage.
156
+
157
+ In the intra-wg-overlap path, the last masked block leaves its V copy in flight
158
+ while we advance the producer state to start the next full K. Either the full list
159
+ overlaps that pending V load, or, if no full blocks exist, we explicitly drain it.
160
+
161
+ Args:
162
+ qhead_per_kvhead: Pack-GQA factor. When > 1, m_block is in packed space and
163
+ must be converted to unpacked for sparse tensor indexing.
164
+ """
165
+
166
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
167
+
168
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
169
+
170
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
171
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
172
+
173
+ if const_expr(full_block_cnt is not None):
174
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
175
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
176
+ else:
177
+ curr_full_block_cnt = Int32(0)
178
+ curr_full_block_idx = None
179
+
180
+ mask_empty = curr_mask_block_cnt == 0
181
+ full_empty = curr_full_block_cnt == 0
182
+
183
+ if mask_empty:
184
+ # No masked blocks: the full list owns the initial Q+K load.
185
+ kv_producer_state = load_block_list(
186
+ curr_full_block_idx,
187
+ curr_full_block_cnt,
188
+ load_q_with_first=True,
189
+ first_block_preloaded=False,
190
+ kv_producer_state=kv_producer_state,
191
+ load_Q=load_Q,
192
+ load_K=load_K,
193
+ load_V=load_V,
194
+ pipeline_k=pipeline_k,
195
+ pipeline_v=pipeline_v,
196
+ use_tma_q=use_tma_q,
197
+ tma_q_bytes=tma_q_bytes,
198
+ intra_wg_overlap=intra_wg_overlap,
199
+ )
200
+
201
+ if const_expr(intra_wg_overlap) and curr_full_block_cnt > 0:
202
+ kv_producer_state = finish_overlap_v_load(
203
+ curr_full_block_idx,
204
+ curr_full_block_cnt,
205
+ load_V,
206
+ pipeline_v,
207
+ kv_producer_state,
208
+ )
209
+ else:
210
+ # Masked blocks present: load Q together with the first masked K so consumers can
211
+ # start immediately. When overlap is disabled this fully drains the list.
212
+ kv_producer_state = load_block_list(
213
+ curr_mask_block_idx,
214
+ curr_mask_block_cnt,
215
+ load_q_with_first=True,
216
+ first_block_preloaded=False,
217
+ kv_producer_state=kv_producer_state,
218
+ load_Q=load_Q,
219
+ load_K=load_K,
220
+ load_V=load_V,
221
+ pipeline_k=pipeline_k,
222
+ pipeline_v=pipeline_v,
223
+ use_tma_q=use_tma_q,
224
+ tma_q_bytes=tma_q_bytes,
225
+ intra_wg_overlap=intra_wg_overlap,
226
+ )
227
+
228
+ if full_empty:
229
+ if const_expr(intra_wg_overlap):
230
+ kv_producer_state = finish_overlap_v_load(
231
+ curr_mask_block_idx,
232
+ curr_mask_block_cnt,
233
+ load_V,
234
+ pipeline_v,
235
+ kv_producer_state,
236
+ )
237
+ else:
238
+ if const_expr(intra_wg_overlap):
239
+ # Bridge the masked list to the full list by overlapping the pending masked V
240
+ # with the first full K load.
241
+ n_block_mask_last = curr_mask_block_idx[0]
242
+ n_block_full_first = curr_full_block_idx[curr_full_block_cnt - 1]
243
+ kv_producer_state_prev = kv_producer_state.clone()
244
+ kv_producer_state.advance()
245
+ pipeline_k.producer_acquire(kv_producer_state)
246
+ load_K(src_idx=n_block_full_first, producer_state=kv_producer_state)
247
+ pipeline_v.producer_acquire(kv_producer_state_prev)
248
+ load_V(src_idx=n_block_mask_last, producer_state=kv_producer_state_prev)
249
+
250
+ kv_producer_state = load_block_list(
251
+ curr_full_block_idx,
252
+ curr_full_block_cnt,
253
+ load_q_with_first=False,
254
+ first_block_preloaded=True,
255
+ kv_producer_state=kv_producer_state,
256
+ load_Q=load_Q,
257
+ load_K=load_K,
258
+ load_V=load_V,
259
+ pipeline_k=pipeline_k,
260
+ pipeline_v=pipeline_v,
261
+ use_tma_q=use_tma_q,
262
+ tma_q_bytes=tma_q_bytes,
263
+ intra_wg_overlap=intra_wg_overlap,
264
+ )
265
+
266
+ kv_producer_state = finish_overlap_v_load(
267
+ curr_full_block_idx,
268
+ curr_full_block_cnt,
269
+ load_V,
270
+ pipeline_v,
271
+ kv_producer_state,
272
+ )
273
+ else:
274
+ # Non-overlap path with both lists: run the full list normally (skipping the Q
275
+ # reload because the masked list already issued it).
276
+ kv_producer_state = load_block_list(
277
+ curr_full_block_idx,
278
+ curr_full_block_cnt,
279
+ load_q_with_first=False,
280
+ first_block_preloaded=False,
281
+ kv_producer_state=kv_producer_state,
282
+ load_Q=load_Q,
283
+ load_K=load_K,
284
+ load_V=load_V,
285
+ pipeline_k=pipeline_k,
286
+ pipeline_v=pipeline_v,
287
+ use_tma_q=use_tma_q,
288
+ tma_q_bytes=tma_q_bytes,
289
+ intra_wg_overlap=intra_wg_overlap,
290
+ )
291
+
292
+ return kv_producer_state
293
+
294
+
295
+ @cute.jit
296
+ def consume_block_sparse_loads(
297
+ blocksparse_tensors: BlockSparseTensors,
298
+ batch_idx,
299
+ head_idx,
300
+ m_block,
301
+ seqlen,
302
+ kv_consumer_state,
303
+ mma_pv_fn,
304
+ mma_one_n_block,
305
+ process_first_half_block,
306
+ process_last_half_block,
307
+ mask_fn,
308
+ score_mod_fn,
309
+ O_should_accumulate,
310
+ mask_mod,
311
+ fastdiv_mods,
312
+ intra_wg_overlap: cutlass.Constexpr,
313
+ warp_scheduler_barrier_sync: Callable,
314
+ warp_scheduler_barrier_arrive: Callable,
315
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
316
+ q_subtile_factor: cutlass.Constexpr[int] = 1,
317
+ ):
318
+ """Consume the mask and full block lists for a single tile on the consumer side.
319
+
320
+ Mirrors `produce_block_sparse_loads` so that the consumer pipeline uses
321
+ the same sparse tensor indexing.
322
+
323
+ Args:
324
+ qhead_per_kvhead: Pack-GQA factor. When > 1, m_block is in packed space and
325
+ must be converted to unpacked for sparse tensor indexing.
326
+ """
327
+
328
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
329
+
330
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
331
+
332
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
333
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
334
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
335
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
336
+
337
+ processed_any = curr_mask_block_cnt + curr_full_block_cnt > 0
338
+
339
+ if const_expr(not intra_wg_overlap):
340
+ if curr_mask_block_cnt > 0:
341
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
342
+ warp_scheduler_barrier_sync()
343
+ kv_consumer_state = mma_one_n_block(
344
+ kv_consumer_state,
345
+ n_block=mask_n_block,
346
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
347
+ mask_fn=partial(
348
+ mask_fn,
349
+ mask_mod=mask_mod,
350
+ mask_seqlen=True,
351
+ fastdiv_mods=fastdiv_mods if cutlass.const_expr(mask_mod is not None) else None,
352
+ ),
353
+ is_first_n_block=True,
354
+ )
355
+ O_should_accumulate = True
356
+ for i in cutlass.range(1, curr_mask_block_cnt):
357
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1 - i]
358
+ kv_consumer_state = mma_one_n_block(
359
+ kv_consumer_state,
360
+ n_block=mask_n_block,
361
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
362
+ mask_fn=partial(mask_fn, mask_mod=mask_mod, mask_seqlen=False),
363
+ is_first_n_block=False,
364
+ )
365
+ O_should_accumulate = True
366
+ if curr_full_block_cnt == 0:
367
+ warp_scheduler_barrier_arrive()
368
+
369
+ if curr_full_block_cnt > 0:
370
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1]
371
+ if curr_mask_block_cnt == 0:
372
+ warp_scheduler_barrier_sync()
373
+ kv_consumer_state = mma_one_n_block(
374
+ kv_consumer_state,
375
+ n_block=full_n_block,
376
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
377
+ mask_fn=partial(mask_fn, mask_seqlen=True),
378
+ is_first_n_block=True,
379
+ )
380
+ O_should_accumulate = True
381
+ for i in cutlass.range(1, curr_full_block_cnt):
382
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
383
+ kv_consumer_state = mma_one_n_block(
384
+ kv_consumer_state,
385
+ n_block=full_n_block,
386
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
387
+ mask_fn=partial(mask_fn, mask_seqlen=False),
388
+ is_first_n_block=False,
389
+ )
390
+ O_should_accumulate = True
391
+ else:
392
+ kv_consumer_state = mma_one_n_block(
393
+ kv_consumer_state,
394
+ n_block=full_n_block,
395
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
396
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
397
+ is_first_n_block=False,
398
+ )
399
+ O_should_accumulate = True
400
+ for i in cutlass.range(1, curr_full_block_cnt):
401
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
402
+ kv_consumer_state = mma_one_n_block(
403
+ kv_consumer_state,
404
+ n_block=full_n_block,
405
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
406
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=False),
407
+ is_first_n_block=False,
408
+ )
409
+ O_should_accumulate = True
410
+ warp_scheduler_barrier_arrive()
411
+ else:
412
+ if curr_mask_block_cnt > 0:
413
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
414
+ kv_consumer_state = process_first_half_block(
415
+ n_block=mask_n_block,
416
+ seqlen=seqlen,
417
+ kv_consumer_state=kv_consumer_state,
418
+ mask_fn=partial(
419
+ mask_fn,
420
+ mask_mod=mask_mod,
421
+ mask_seqlen=True,
422
+ fastdiv_mods=fastdiv_mods if cutlass.const_expr(mask_mod is not None) else None,
423
+ ),
424
+ score_mod_fn=score_mod_fn,
425
+ is_first_block=True,
426
+ )
427
+ for i in cutlass.range(1, curr_mask_block_cnt):
428
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1 - i]
429
+ kv_consumer_state = mma_one_n_block(
430
+ kv_consumer_state,
431
+ n_block=mask_n_block,
432
+ seqlen=seqlen,
433
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
434
+ mask_fn=partial(mask_fn, mask_mod=mask_mod, mask_seqlen=False),
435
+ )
436
+ O_should_accumulate = True
437
+
438
+ if curr_full_block_cnt > 0:
439
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1]
440
+ if curr_mask_block_cnt == 0:
441
+ kv_consumer_state = process_first_half_block(
442
+ n_block=full_n_block,
443
+ seqlen=seqlen,
444
+ kv_consumer_state=kv_consumer_state,
445
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
446
+ score_mod_fn=score_mod_fn,
447
+ is_first_block=True,
448
+ )
449
+ else:
450
+ kv_consumer_state = mma_one_n_block(
451
+ kv_consumer_state,
452
+ n_block=full_n_block,
453
+ seqlen=seqlen,
454
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
455
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=True),
456
+ )
457
+ O_should_accumulate = True
458
+ for i in cutlass.range(1, curr_full_block_cnt):
459
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
460
+ kv_consumer_state = mma_one_n_block(
461
+ kv_consumer_state,
462
+ n_block=full_n_block,
463
+ seqlen=seqlen,
464
+ mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
465
+ mask_fn=partial(mask_fn, mask_mod=None, mask_seqlen=False),
466
+ )
467
+ O_should_accumulate = True
468
+
469
+ if curr_mask_block_cnt + curr_full_block_cnt > 0:
470
+ kv_consumer_state = process_last_half_block(
471
+ kv_consumer_state=kv_consumer_state,
472
+ zero_init=not O_should_accumulate,
473
+ )
474
+ O_should_accumulate = True
475
+
476
+ return kv_consumer_state, O_should_accumulate, processed_any
477
+
478
+
479
+ @cute.jit
480
+ def load_block_list_sm100(
481
+ block_indices: cute.Tensor,
482
+ block_count,
483
+ load_q_with_first: cutlass.Constexpr,
484
+ m_block,
485
+ q_stage: cutlass.Constexpr,
486
+ kv_producer_state,
487
+ load_Q,
488
+ load_K,
489
+ load_V,
490
+ pipeline_kv,
491
+ ):
492
+ """SM100 version of load_block_list (no intra_wg_overlap, no extra_tx_count)."""
493
+ if block_count > 0:
494
+ # First iteration: load Q alongside K if requested
495
+ n_block_first = block_indices[block_count - 1]
496
+
497
+ if const_expr(load_q_with_first):
498
+ # SM100 loads Q0 and optionally Q1
499
+ load_Q(block=q_stage * m_block + 0, stage=0)
500
+ if const_expr(q_stage == 2):
501
+ load_Q(block=q_stage * m_block + 1, stage=1)
502
+
503
+ # SM100 doesn't use producer_acquire for pipeline_kv in load path
504
+ # The pipeline barriers are handled inside load_KV
505
+ load_K(block=n_block_first, producer_state=kv_producer_state, page_idx=None)
506
+ kv_producer_state.advance()
507
+ load_V(block=n_block_first, producer_state=kv_producer_state, page_idx=None)
508
+ kv_producer_state.advance()
509
+
510
+ # Remaining blocks
511
+ for offset in cutlass.range(1, block_count):
512
+ n_block = block_indices[block_count - 1 - offset]
513
+ load_K(block=n_block, producer_state=kv_producer_state, page_idx=None)
514
+ kv_producer_state.advance()
515
+ load_V(block=n_block, producer_state=kv_producer_state, page_idx=None)
516
+ kv_producer_state.advance()
517
+
518
+ return kv_producer_state
519
+
520
+
521
+ # SM100-specific tile processor using SM100 helpers
522
+ @cute.jit
523
+ def produce_block_sparse_loads_sm100(
524
+ blocksparse_tensors: BlockSparseTensors,
525
+ batch_idx,
526
+ head_idx,
527
+ m_block,
528
+ kv_producer_state,
529
+ load_Q,
530
+ load_K,
531
+ load_V,
532
+ pipeline_kv,
533
+ q_stage: cutlass.Constexpr,
534
+ q_producer_phase: Int32,
535
+ qhead_per_kvhead: cutlass.Constexpr,
536
+ q_subtile_factor: cutlass.Constexpr,
537
+ ):
538
+ """SM100 entry point for sparse block iteration.
539
+
540
+ SM100 uses PipelineTmaUmma which doesn't support extra_tx_count, so we use
541
+ simplified block processing that just calls producer_acquire without extras.
542
+
543
+ Args:
544
+ m_block: which tile of m we are processing
545
+ qhead_per_kvhead: Constexpr pack factor
546
+ """
547
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
548
+
549
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
550
+
551
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
552
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
553
+
554
+ if const_expr(full_block_cnt is not None):
555
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
556
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
557
+ else:
558
+ curr_full_block_cnt = Int32(0)
559
+ curr_full_block_idx = None
560
+
561
+ mask_empty = curr_mask_block_cnt == 0
562
+ full_empty = curr_full_block_cnt == 0
563
+
564
+ q_phase_flipped = False
565
+
566
+ if mask_empty:
567
+ # No masked blocks: process full list with Q loading
568
+ kv_producer_state = load_block_list_sm100(
569
+ curr_full_block_idx,
570
+ curr_full_block_cnt,
571
+ load_q_with_first=True,
572
+ m_block=m_block,
573
+ q_stage=q_stage,
574
+ kv_producer_state=kv_producer_state,
575
+ load_Q=load_Q,
576
+ load_K=load_K,
577
+ load_V=load_V,
578
+ pipeline_kv=pipeline_kv,
579
+ )
580
+ q_phase_flipped = not full_empty
581
+ else:
582
+ # Process masked blocks with Q loading
583
+ kv_producer_state = load_block_list_sm100(
584
+ curr_mask_block_idx,
585
+ curr_mask_block_cnt,
586
+ load_q_with_first=True,
587
+ m_block=m_block,
588
+ q_stage=q_stage,
589
+ kv_producer_state=kv_producer_state,
590
+ load_Q=load_Q,
591
+ load_K=load_K,
592
+ load_V=load_V,
593
+ pipeline_kv=pipeline_kv,
594
+ )
595
+ q_phase_flipped = True
596
+
597
+ if not full_empty:
598
+ # Process full blocks without Q loading
599
+ kv_producer_state = load_block_list_sm100(
600
+ curr_full_block_idx,
601
+ curr_full_block_cnt,
602
+ load_q_with_first=False,
603
+ m_block=m_block,
604
+ q_stage=q_stage,
605
+ kv_producer_state=kv_producer_state,
606
+ load_Q=load_Q,
607
+ load_K=load_K,
608
+ load_V=load_V,
609
+ pipeline_kv=pipeline_kv,
610
+ )
611
+
612
+ if q_phase_flipped:
613
+ q_producer_phase ^= 1
614
+
615
+ return kv_producer_state, q_producer_phase
616
+
617
+
618
+ @cute.jit
619
+ def get_total_block_count(
620
+ blocksparse_tensors: BlockSparseTensors,
621
+ batch_idx,
622
+ head_idx,
623
+ m_block,
624
+ qhead_per_kvhead: cutlass.Constexpr,
625
+ q_subtile_factor: cutlass.Constexpr,
626
+ ):
627
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
628
+
629
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
630
+ if const_expr(full_block_cnt is not None):
631
+ return (
632
+ mask_block_cnt[batch_idx, head_idx, m_block_sparse]
633
+ + full_block_cnt[batch_idx, head_idx, m_block_sparse]
634
+ )
635
+ else:
636
+ return mask_block_cnt[batch_idx, head_idx, m_block_sparse]
637
+
638
+
639
+ @cute.jit
640
+ def handle_block_sparse_empty_tile_correction_sm100(
641
+ tidx: Int32,
642
+ q_stage: cutlass.Constexpr,
643
+ m_block_size: cutlass.Constexpr,
644
+ qhead_per_kvhead,
645
+ pack_gqa: cutlass.Constexpr,
646
+ is_split_kv: cutlass.Constexpr,
647
+ learnable_sink,
648
+ mLSE,
649
+ seqlen,
650
+ m_block: Int32,
651
+ head_idx: Int32,
652
+ batch_idx: Int32,
653
+ split_idx: Int32,
654
+ sScale: cute.Tensor,
655
+ stats: list,
656
+ correction_epilogue: Callable,
657
+ thr_mma_pv: cute.core.ThrMma,
658
+ tOtOs: tuple[cute.Tensor],
659
+ sO: cute.Tensor,
660
+ mbar_ptr,
661
+ mbar_softmax_corr_full_offset: Int32,
662
+ mbar_softmax_corr_empty_offset: Int32,
663
+ mbar_P_full_O_rescaled_offset: Int32,
664
+ mbar_P_full_2_offset: Int32,
665
+ mbar_corr_epi_full_offset: Int32,
666
+ mbar_corr_epi_empty_offset: Int32,
667
+ softmax_corr_consumer_phase: Int32,
668
+ o_corr_consumer_phase: Int32,
669
+ corr_epi_producer_phase: Int32,
670
+ softmax_scale_log2: Float32,
671
+ mO_cur: Optional[cute.Tensor] = None,
672
+ gO: Optional[cute.Tensor] = None,
673
+ gmem_tiled_copy_O: Optional[cute.TiledCopy] = None,
674
+ ):
675
+ """Handle the block-sparse case where a tile is fully masked:
676
+ * zero staged results
677
+ * seed stats
678
+ * satisfy the usual barrier protocol so downstream warps continue to make progress.
679
+ """
680
+ LOG2_E = Float32(math.log2(math.e))
681
+
682
+ for stage in cutlass.range_constexpr(q_stage):
683
+ row_sum_value = Float32(1.0)
684
+ row_max_value = (
685
+ -Float32.inf if const_expr(mLSE is not None or learnable_sink is not None) else None
686
+ )
687
+ if const_expr(learnable_sink is not None):
688
+ sink_val = -Float32.inf
689
+ if const_expr(not pack_gqa):
690
+ sink_val = Float32(learnable_sink[head_idx])
691
+ elif tidx < m_block_size:
692
+ q_head_idx = (
693
+ (q_stage * m_block + stage) * m_block_size + tidx
694
+ ) % qhead_per_kvhead + head_idx * qhead_per_kvhead
695
+ sink_val = Float32(learnable_sink[q_head_idx])
696
+ if sink_val != -Float32.inf and (const_expr(not is_split_kv) or split_idx == 0):
697
+ if row_max_value == -Float32.inf:
698
+ row_max_value = sink_val * (LOG2_E / softmax_scale_log2)
699
+ row_sum_value = Float32(1.0)
700
+ else:
701
+ row_sum_value = row_sum_value + utils.exp2f(
702
+ sink_val * LOG2_E - row_max_value * softmax_scale_log2
703
+ )
704
+ if tidx < m_block_size:
705
+ scale_row_idx = tidx + stage * m_block_size
706
+ sScale[scale_row_idx] = row_sum_value
707
+ if const_expr(mLSE is not None or learnable_sink is not None):
708
+ sScale[scale_row_idx + m_block_size * 2] = row_max_value
709
+ acc_flag = row_sum_value == Float32(0.0) or row_sum_value != row_sum_value
710
+ stats[stage] = (row_sum_value, row_max_value, acc_flag)
711
+
712
+ cute.arch.mbarrier_wait(
713
+ mbar_ptr + mbar_softmax_corr_full_offset + stage,
714
+ softmax_corr_consumer_phase,
715
+ )
716
+ cute.arch.mbarrier_arrive(mbar_ptr + mbar_softmax_corr_empty_offset + stage)
717
+
718
+ if const_expr(gmem_tiled_copy_O is None):
719
+ cute.arch.mbarrier_wait(
720
+ mbar_ptr + mbar_corr_epi_empty_offset + stage,
721
+ corr_epi_producer_phase,
722
+ )
723
+ correction_epilogue(
724
+ thr_mma_pv,
725
+ tOtOs[stage],
726
+ tidx,
727
+ stage,
728
+ m_block,
729
+ seqlen.seqlen_q,
730
+ Float32(0.0), # zero scale ensures empty tile writes zeros into staged outputs
731
+ sO[None, None, stage],
732
+ mO_cur,
733
+ gO,
734
+ gmem_tiled_copy_O,
735
+ )
736
+ if const_expr(gmem_tiled_copy_O is None):
737
+ cute.arch.mbarrier_arrive(mbar_ptr + mbar_corr_epi_full_offset + stage)
738
+ cute.arch.mbarrier_arrive(mbar_ptr + mbar_P_full_O_rescaled_offset + stage)
739
+ cute.arch.mbarrier_arrive(mbar_ptr + mbar_P_full_2_offset + stage)
740
+
741
+ softmax_corr_consumer_phase ^= 1
742
+ o_corr_consumer_phase ^= 1
743
+ corr_epi_producer_phase ^= 1
744
+
745
+ return (
746
+ softmax_corr_consumer_phase,
747
+ o_corr_consumer_phase,
748
+ corr_epi_producer_phase,
749
+ )
750
+
751
+
752
+ @cute.jit
753
+ def softmax_block_sparse_sm100(
754
+ blocksparse_tensors: BlockSparseTensors,
755
+ batch_idx,
756
+ head_idx,
757
+ m_block,
758
+ softmax_step: Callable,
759
+ mask_fn: Callable,
760
+ mask_fn_none: Callable,
761
+ mma_si_consumer_phase: Int32,
762
+ si_corr_producer_phase: Int32,
763
+ s0_s1_sequence_phase: Int32,
764
+ mbar_ptr,
765
+ mbar_softmax_corr_full_offset: Int32,
766
+ mbar_softmax_corr_empty_offset: Int32,
767
+ mbar_P_full_O_rescaled_offset: Int32,
768
+ mbar_P_full_2_offset: Int32,
769
+ q_stage: cutlass.Constexpr,
770
+ stage_idx: Int32,
771
+ check_m_boundary: bool,
772
+ qhead_per_kvhead: cutlass.Constexpr,
773
+ q_subtile_factor: cutlass.Constexpr[int] = 1,
774
+ ):
775
+ m_block_sparse = sparse_tensor_m_block(m_block, qhead_per_kvhead, q_subtile_factor)
776
+
777
+ mask_block_cnt, mask_block_idx, full_block_cnt, full_block_idx = blocksparse_tensors
778
+
779
+ curr_mask_block_cnt = mask_block_cnt[batch_idx, head_idx, m_block_sparse]
780
+ curr_mask_block_idx = mask_block_idx[batch_idx, head_idx, m_block_sparse, None]
781
+
782
+ if const_expr(full_block_cnt is not None):
783
+ curr_full_block_cnt = full_block_cnt[batch_idx, head_idx, m_block_sparse]
784
+ curr_full_block_idx = full_block_idx[batch_idx, head_idx, m_block_sparse, None]
785
+ else:
786
+ curr_full_block_cnt = Int32(0)
787
+ curr_full_block_idx = None
788
+
789
+ total_block_cnt = curr_mask_block_cnt + curr_full_block_cnt
790
+
791
+ if total_block_cnt == 0:
792
+ cute.arch.mbarrier_arrive(mbar_ptr + mbar_softmax_corr_full_offset + stage_idx)
793
+ cute.arch.mbarrier_arrive(mbar_ptr + mbar_P_full_O_rescaled_offset + stage_idx)
794
+ cute.arch.mbarrier_arrive(mbar_ptr + mbar_P_full_2_offset + stage_idx)
795
+ cute.arch.mbarrier_arrive(mbar_ptr + mbar_softmax_corr_empty_offset + stage_idx)
796
+ else:
797
+ if curr_mask_block_cnt > 0:
798
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1]
799
+ (
800
+ mma_si_consumer_phase,
801
+ si_corr_producer_phase,
802
+ s0_s1_sequence_phase,
803
+ ) = softmax_step(
804
+ mma_si_consumer_phase,
805
+ si_corr_producer_phase,
806
+ s0_s1_sequence_phase,
807
+ mask_n_block,
808
+ is_first=True,
809
+ mask_fn=partial(mask_fn, mask_seqlen=True, check_q_boundary=check_m_boundary),
810
+ )
811
+ for i in cutlass.range(1, curr_mask_block_cnt):
812
+ mask_n_block = curr_mask_block_idx[curr_mask_block_cnt - 1 - i]
813
+ (
814
+ mma_si_consumer_phase,
815
+ si_corr_producer_phase,
816
+ s0_s1_sequence_phase,
817
+ ) = softmax_step(
818
+ mma_si_consumer_phase,
819
+ si_corr_producer_phase,
820
+ s0_s1_sequence_phase,
821
+ mask_n_block,
822
+ mask_fn=partial(mask_fn, mask_seqlen=False, check_q_boundary=check_m_boundary),
823
+ )
824
+
825
+ if curr_full_block_cnt > 0:
826
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1]
827
+ if curr_mask_block_cnt == 0:
828
+ (
829
+ mma_si_consumer_phase,
830
+ si_corr_producer_phase,
831
+ s0_s1_sequence_phase,
832
+ ) = softmax_step(
833
+ mma_si_consumer_phase,
834
+ si_corr_producer_phase,
835
+ s0_s1_sequence_phase,
836
+ full_n_block,
837
+ is_first=True,
838
+ mask_fn=partial(
839
+ mask_fn_none, mask_seqlen=True, check_q_boundary=check_m_boundary
840
+ ),
841
+ )
842
+ else:
843
+ (
844
+ mma_si_consumer_phase,
845
+ si_corr_producer_phase,
846
+ s0_s1_sequence_phase,
847
+ ) = softmax_step(
848
+ mma_si_consumer_phase,
849
+ si_corr_producer_phase,
850
+ s0_s1_sequence_phase,
851
+ full_n_block,
852
+ is_first=False,
853
+ mask_fn=partial(
854
+ mask_fn_none, mask_seqlen=False, check_q_boundary=check_m_boundary
855
+ ),
856
+ )
857
+ for i in cutlass.range(1, curr_full_block_cnt):
858
+ full_n_block = curr_full_block_idx[curr_full_block_cnt - 1 - i]
859
+ (
860
+ mma_si_consumer_phase,
861
+ si_corr_producer_phase,
862
+ s0_s1_sequence_phase,
863
+ ) = softmax_step(
864
+ mma_si_consumer_phase,
865
+ si_corr_producer_phase,
866
+ s0_s1_sequence_phase,
867
+ full_n_block,
868
+ mask_fn=partial(
869
+ mask_fn_none, mask_seqlen=False, check_q_boundary=check_m_boundary
870
+ ),
871
+ )
872
+
873
+ return (
874
+ mma_si_consumer_phase,
875
+ si_corr_producer_phase,
876
+ s0_s1_sequence_phase,
877
+ total_block_cnt == 0,
878
+ )
879
+
880
+
881
+ # =============================================================================
882
+ # Backward-specific block-sparse helpers (SM100)
883
+ # =============================================================================
884
+ #
885
+ # In backward, iteration is transposed compared to forward:
886
+ # - Forward: outer loop over m_blocks (Q tiles), inner loop over n_blocks (KV tiles)
887
+ # - Backward: outer loop over n_blocks (KV tiles), inner loop over m_blocks (Q tiles)
888
+ #
889
+ # The backward block-sparse tensors use "Q direction" indexing:
890
+ # - q_block_cnt[batch, head, n_block] → count of m_blocks to process for this KV tile
891
+ # - q_block_idx[batch, head, n_block, :] → indices of m_blocks to process
892
+ #
893
+
894
+
895
+ @cute.jit
896
+ def get_total_q_block_count_bwd(
897
+ blocksparse_tensors: BlockSparseTensors,
898
+ batch_idx,
899
+ head_idx,
900
+ n_block,
901
+ subtile_factor: cutlass.Constexpr = 1,
902
+ m_block_max: int = 0,
903
+ ):
904
+ """Count total tile iterations for given n_block (KV tile) in backward."""
905
+ q_block_cnt, _, full_block_cnt, _ = blocksparse_tensors
906
+ total = q_block_cnt[batch_idx, head_idx, n_block]
907
+ if const_expr(full_block_cnt is not None):
908
+ total = total + full_block_cnt[batch_idx, head_idx, n_block]
909
+ return total * subtile_factor
910
+
911
+
912
+ @cute.jit
913
+ def produce_block_sparse_q_loads_bwd_sm100(
914
+ blocksparse_tensors: BlockSparseTensors,
915
+ batch_idx,
916
+ head_idx,
917
+ n_block,
918
+ # Pipeline states (will be returned after advancing)
919
+ producer_state_Q_LSE,
920
+ producer_state_dO_dPsum,
921
+ # Pipelines
922
+ pipeline_Q,
923
+ pipeline_LSE,
924
+ pipeline_dO,
925
+ pipeline_dPsum,
926
+ # Load functions
927
+ load_K,
928
+ load_V,
929
+ load_Q,
930
+ load_dO,
931
+ copy_stats,
932
+ # Global tensors for LSE/dPsum
933
+ gLSE,
934
+ sLSE,
935
+ gdPsum,
936
+ sdPsum,
937
+ # TMA copy bytes for extra_tx_count
938
+ tma_copy_bytes_K,
939
+ tma_copy_bytes_V,
940
+ # Flags for which loads to perform
941
+ should_load_Q: cutlass.Constexpr,
942
+ should_load_dO: cutlass.Constexpr,
943
+ # Subtiling factor and bounds
944
+ subtile_factor: cutlass.Constexpr = 1,
945
+ m_block_max: int = 0,
946
+ ):
947
+ """SM100 backward block sparse loading with subtiling.
948
+
949
+ Returns updated (producer_state_Q_LSE, producer_state_dO_dPsum).
950
+ First iteration loads K/V alongside Q/dO; subsequent iterations load only Q/dO.
951
+ """
952
+ (
953
+ curr_q_cnt,
954
+ curr_q_idx,
955
+ curr_full_cnt,
956
+ curr_full_idx,
957
+ loop_count,
958
+ ) = get_block_sparse_iteration_info_bwd(
959
+ blocksparse_tensors, batch_idx, head_idx, n_block, subtile_factor, m_block_max
960
+ )
961
+
962
+ for iter_idx in cutlass.range(loop_count, unroll=1):
963
+ m_block, _ = get_m_block_from_iter_bwd(
964
+ iter_idx,
965
+ curr_q_cnt,
966
+ curr_q_idx,
967
+ curr_full_cnt,
968
+ curr_full_idx,
969
+ subtile_factor,
970
+ m_block_max,
971
+ )
972
+ m_block_safe = m_block
973
+ if m_block_max > 0:
974
+ m_block_safe = cutlass.min(m_block, m_block_max - 1)
975
+
976
+ if iter_idx == 0:
977
+ # First block: load K/V alongside Q/dO
978
+ if const_expr(should_load_Q):
979
+ pipeline_Q.producer_acquire(producer_state_Q_LSE, extra_tx_count=tma_copy_bytes_K)
980
+ load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q_LSE))
981
+ load_Q(m_block_safe, producer_state=producer_state_Q_LSE)
982
+ pipeline_Q.producer_commit(producer_state_Q_LSE)
983
+ pipeline_LSE.producer_acquire(producer_state_Q_LSE)
984
+ with cute.arch.elect_one():
985
+ copy_stats(
986
+ gLSE[None, m_block_safe],
987
+ sLSE[None, producer_state_Q_LSE.index],
988
+ mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_Q_LSE),
989
+ )
990
+ producer_state_Q_LSE.advance()
991
+ if const_expr(should_load_dO):
992
+ pipeline_dO.producer_acquire(
993
+ producer_state_dO_dPsum, extra_tx_count=tma_copy_bytes_V
994
+ )
995
+ load_V(tma_bar_ptr=pipeline_dO.producer_get_barrier(producer_state_dO_dPsum))
996
+ load_dO(m_block_safe, producer_state=producer_state_dO_dPsum)
997
+ pipeline_dO.producer_commit(producer_state_dO_dPsum)
998
+ pipeline_dPsum.producer_acquire(producer_state_dO_dPsum)
999
+ with cute.arch.elect_one():
1000
+ copy_stats(
1001
+ gdPsum[None, m_block_safe],
1002
+ sdPsum[None, producer_state_dO_dPsum.index],
1003
+ mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dO_dPsum),
1004
+ )
1005
+ producer_state_dO_dPsum.advance()
1006
+ else:
1007
+ # Subsequent blocks: just load Q/dO (K/V already loaded)
1008
+ if const_expr(should_load_Q):
1009
+ pipeline_Q.producer_acquire(producer_state_Q_LSE)
1010
+ load_Q(m_block_safe, producer_state=producer_state_Q_LSE)
1011
+ pipeline_Q.producer_commit(producer_state_Q_LSE)
1012
+ pipeline_LSE.producer_acquire(producer_state_Q_LSE)
1013
+ with cute.arch.elect_one():
1014
+ copy_stats(
1015
+ gLSE[None, m_block_safe],
1016
+ sLSE[None, producer_state_Q_LSE.index],
1017
+ mbar_ptr=pipeline_LSE.producer_get_barrier(producer_state_Q_LSE),
1018
+ )
1019
+ producer_state_Q_LSE.advance()
1020
+ if const_expr(should_load_dO):
1021
+ pipeline_dO.producer_acquire(producer_state_dO_dPsum)
1022
+ load_dO(m_block_safe, producer_state=producer_state_dO_dPsum)
1023
+ pipeline_dO.producer_commit(producer_state_dO_dPsum)
1024
+ pipeline_dPsum.producer_acquire(producer_state_dO_dPsum)
1025
+ with cute.arch.elect_one():
1026
+ copy_stats(
1027
+ gdPsum[None, m_block_safe],
1028
+ sdPsum[None, producer_state_dO_dPsum.index],
1029
+ mbar_ptr=pipeline_dPsum.producer_get_barrier(producer_state_dO_dPsum),
1030
+ )
1031
+ producer_state_dO_dPsum.advance()
1032
+
1033
+ return producer_state_Q_LSE, producer_state_dO_dPsum
1034
+
1035
+
1036
+ @cute.jit
1037
+ def get_block_sparse_iteration_info_bwd(
1038
+ blocksparse_tensors: BlockSparseTensors,
1039
+ batch_idx,
1040
+ head_idx,
1041
+ n_block,
1042
+ subtile_factor: cutlass.Constexpr = 1,
1043
+ m_block_max: int = 0,
1044
+ ):
1045
+ """Extract block-sparse iteration info for backward pass.
1046
+
1047
+ Returns (curr_q_cnt, curr_q_idx, curr_full_cnt, curr_full_idx, total_count).
1048
+ """
1049
+ q_cnt, q_idx, full_cnt, full_idx = blocksparse_tensors
1050
+ curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1051
+ curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1052
+
1053
+ if const_expr(full_cnt is not None):
1054
+ curr_full_cnt = full_cnt[batch_idx, head_idx, n_block]
1055
+ curr_full_idx = full_idx[batch_idx, head_idx, n_block, None]
1056
+ else:
1057
+ curr_full_cnt = Int32(0)
1058
+ curr_full_idx = None
1059
+
1060
+ sparse_block_count = curr_q_cnt
1061
+ if const_expr(full_cnt is not None):
1062
+ sparse_block_count = sparse_block_count + curr_full_cnt
1063
+ total_count = sparse_block_count * subtile_factor
1064
+
1065
+ return curr_q_cnt, curr_q_idx, curr_full_cnt, curr_full_idx, total_count
1066
+
1067
+
1068
+ @cute.jit
1069
+ def get_m_block_from_iter_bwd(
1070
+ iter_idx,
1071
+ curr_q_cnt,
1072
+ curr_q_idx: cute.Tensor,
1073
+ curr_full_cnt,
1074
+ curr_full_idx: Optional[cute.Tensor],
1075
+ subtile_factor: cutlass.Constexpr = 1,
1076
+ m_block_max: int = 0,
1077
+ ):
1078
+ """Derive m_block index and is_full_block flag from iteration index.
1079
+
1080
+ Returns (m_block, is_full_block):
1081
+ - m_block: The actual Q-tile block index
1082
+ - is_full_block: True if this is a full block (no mask_mod needed)
1083
+ """
1084
+ sparse_iter_idx = iter_idx // subtile_factor
1085
+ subtile_offset = iter_idx % subtile_factor
1086
+
1087
+ sparse_m_block = Int32(0)
1088
+ is_full_block = False
1089
+ if const_expr(curr_full_idx is not None):
1090
+ if sparse_iter_idx < curr_q_cnt:
1091
+ sparse_m_block = curr_q_idx[sparse_iter_idx]
1092
+ else:
1093
+ sparse_m_block = curr_full_idx[sparse_iter_idx - curr_q_cnt]
1094
+ is_full_block = True
1095
+ else:
1096
+ sparse_m_block = curr_q_idx[sparse_iter_idx]
1097
+
1098
+ return sparse_m_block * subtile_factor + subtile_offset, is_full_block
1099
+
1100
+
1101
+ @cute.jit
1102
+ def _load_q_do_block_sm90(
1103
+ m_block,
1104
+ producer_state_Q,
1105
+ producer_state_dO,
1106
+ pipeline_Q,
1107
+ pipeline_dO,
1108
+ load_K,
1109
+ load_V,
1110
+ load_Q,
1111
+ load_dO,
1112
+ load_LSE,
1113
+ load_dPsum,
1114
+ tma_copy_bytes_K,
1115
+ tma_copy_bytes_V,
1116
+ Q_stage_eq_dO_stage: cutlass.Constexpr,
1117
+ load_kv: bool,
1118
+ ):
1119
+ """Load one Q/dO block, optionally loading K/V on first iteration."""
1120
+ if load_kv:
1121
+ pipeline_Q.producer_acquire(producer_state_Q, extra_tx_count=tma_copy_bytes_K)
1122
+ load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q))
1123
+ else:
1124
+ pipeline_Q.producer_acquire(producer_state_Q)
1125
+ load_Q(m_block, producer_state=producer_state_Q)
1126
+ with cute.arch.elect_one():
1127
+ load_LSE(m_block, producer_state=producer_state_Q)
1128
+
1129
+ producer_state_dO_cur = (
1130
+ producer_state_dO if const_expr(not Q_stage_eq_dO_stage) else producer_state_Q
1131
+ )
1132
+ if load_kv:
1133
+ pipeline_dO.producer_acquire(producer_state_dO_cur, extra_tx_count=tma_copy_bytes_V)
1134
+ load_V(tma_bar_ptr=pipeline_dO.producer_get_barrier(producer_state_dO_cur))
1135
+ else:
1136
+ pipeline_dO.producer_acquire(producer_state_dO_cur)
1137
+ load_dO(m_block, producer_state=producer_state_dO_cur)
1138
+ with cute.arch.elect_one():
1139
+ load_dPsum(m_block, producer_state=producer_state_dO_cur)
1140
+
1141
+ producer_state_Q.advance()
1142
+ producer_state_dO.advance()
1143
+ return producer_state_Q, producer_state_dO
1144
+
1145
+
1146
+ @cute.jit
1147
+ def produce_block_sparse_q_loads_bwd_sm90(
1148
+ blocksparse_tensors: BlockSparseTensors,
1149
+ batch_idx,
1150
+ head_idx,
1151
+ n_block,
1152
+ producer_state_Q,
1153
+ producer_state_dO,
1154
+ pipeline_Q,
1155
+ pipeline_dO,
1156
+ load_K,
1157
+ load_V,
1158
+ load_Q,
1159
+ load_dO,
1160
+ load_LSE,
1161
+ load_dPsum,
1162
+ tma_copy_bytes_K,
1163
+ tma_copy_bytes_V,
1164
+ Q_stage_eq_dO_stage: cutlass.Constexpr,
1165
+ subtile_factor: cutlass.Constexpr,
1166
+ m_block_max: int,
1167
+ ):
1168
+ """SM90 backward block sparse loading with separate partial/full loops.
1169
+
1170
+ K/V are loaded with the first valid block. Iterates partial blocks first,
1171
+ then full blocks, matching consumer order.
1172
+
1173
+ Returns updated (producer_state_Q, producer_state_dO).
1174
+ """
1175
+ q_cnt, q_idx, full_cnt, full_idx = blocksparse_tensors
1176
+ curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1177
+ curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1178
+
1179
+ if const_expr(full_cnt is not None):
1180
+ curr_full_cnt = full_cnt[batch_idx, head_idx, n_block]
1181
+ curr_full_idx = full_idx[batch_idx, head_idx, n_block, None]
1182
+ else:
1183
+ curr_full_cnt = Int32(0)
1184
+ curr_full_idx = None
1185
+
1186
+ kv_loaded = False
1187
+
1188
+ for iter_idx in cutlass.range(curr_q_cnt * subtile_factor, unroll=1):
1189
+ sparse_idx = iter_idx // subtile_factor
1190
+ subtile_offset = iter_idx % subtile_factor
1191
+ m_block = curr_q_idx[sparse_idx] * subtile_factor + subtile_offset
1192
+
1193
+ if m_block < m_block_max:
1194
+ producer_state_Q, producer_state_dO = _load_q_do_block_sm90(
1195
+ m_block,
1196
+ producer_state_Q,
1197
+ producer_state_dO,
1198
+ pipeline_Q,
1199
+ pipeline_dO,
1200
+ load_K,
1201
+ load_V,
1202
+ load_Q,
1203
+ load_dO,
1204
+ load_LSE,
1205
+ load_dPsum,
1206
+ tma_copy_bytes_K,
1207
+ tma_copy_bytes_V,
1208
+ Q_stage_eq_dO_stage,
1209
+ load_kv=not kv_loaded,
1210
+ )
1211
+ kv_loaded = True
1212
+
1213
+ if const_expr(full_cnt is not None):
1214
+ for iter_idx in cutlass.range(curr_full_cnt * subtile_factor, unroll=1):
1215
+ sparse_idx = iter_idx // subtile_factor
1216
+ subtile_offset = iter_idx % subtile_factor
1217
+ m_block = curr_full_idx[sparse_idx] * subtile_factor + subtile_offset
1218
+
1219
+ if m_block < m_block_max:
1220
+ producer_state_Q, producer_state_dO = _load_q_do_block_sm90(
1221
+ m_block,
1222
+ producer_state_Q,
1223
+ producer_state_dO,
1224
+ pipeline_Q,
1225
+ pipeline_dO,
1226
+ load_K,
1227
+ load_V,
1228
+ load_Q,
1229
+ load_dO,
1230
+ load_LSE,
1231
+ load_dPsum,
1232
+ tma_copy_bytes_K,
1233
+ tma_copy_bytes_V,
1234
+ Q_stage_eq_dO_stage,
1235
+ load_kv=not kv_loaded,
1236
+ )
1237
+ kv_loaded = True
1238
+
1239
+ return producer_state_Q, producer_state_dO
1240
+
1241
+
1242
+ @cute.jit
1243
+ def consume_block_sparse_mma_bwd_sm90(
1244
+ blocksparse_tensors: BlockSparseTensors,
1245
+ batch_idx,
1246
+ head_idx,
1247
+ n_block,
1248
+ consumer_state_Q,
1249
+ consumer_state_dO,
1250
+ mma_one_m_block_fn,
1251
+ mask,
1252
+ mask_mod,
1253
+ is_causal: cutlass.Constexpr,
1254
+ is_local: cutlass.Constexpr,
1255
+ thr_mma_SdP,
1256
+ softmax_scale,
1257
+ seqlen,
1258
+ subtile_factor: cutlass.Constexpr,
1259
+ m_block_max: int,
1260
+ aux_tensors=None,
1261
+ fastdiv_mods=(None, None),
1262
+ ):
1263
+ """SM90 backward block sparse MMA consumption with separate partial/full loops.
1264
+
1265
+ Partial blocks are processed first (with mask_mod applied), then full blocks
1266
+ (without mask_mod). This ensures mask_mod is only applied where needed.
1267
+
1268
+ Returns updated (consumer_state_Q, consumer_state_dO).
1269
+ """
1270
+ q_cnt, q_idx, full_cnt, full_idx = blocksparse_tensors
1271
+ curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1272
+ curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1273
+
1274
+ if const_expr(full_cnt is not None):
1275
+ curr_full_cnt = full_cnt[batch_idx, head_idx, n_block]
1276
+ curr_full_idx = full_idx[batch_idx, head_idx, n_block, None]
1277
+ else:
1278
+ curr_full_cnt = Int32(0)
1279
+ curr_full_idx = None
1280
+
1281
+ dKV_accumulate = False
1282
+
1283
+ mask_fn_partial = partial(
1284
+ mask.apply_mask,
1285
+ batch_idx=batch_idx,
1286
+ head_idx=head_idx,
1287
+ n_block=n_block,
1288
+ thr_mma=thr_mma_SdP,
1289
+ mask_seqlen=True,
1290
+ mask_causal=is_causal,
1291
+ mask_local=is_local,
1292
+ mask_mod=mask_mod,
1293
+ aux_tensors=aux_tensors,
1294
+ fastdiv_mods=fastdiv_mods,
1295
+ )
1296
+
1297
+ mask_fn_full = partial(
1298
+ mask.apply_mask,
1299
+ batch_idx=batch_idx,
1300
+ head_idx=head_idx,
1301
+ n_block=n_block,
1302
+ thr_mma=thr_mma_SdP,
1303
+ mask_seqlen=True,
1304
+ mask_causal=is_causal,
1305
+ mask_local=is_local,
1306
+ aux_tensors=aux_tensors,
1307
+ fastdiv_mods=fastdiv_mods,
1308
+ )
1309
+
1310
+ for iter_idx in cutlass.range(curr_q_cnt * subtile_factor, unroll=1):
1311
+ sparse_idx = iter_idx // subtile_factor
1312
+ subtile_offset = iter_idx % subtile_factor
1313
+ m_block = curr_q_idx[sparse_idx] * subtile_factor + subtile_offset
1314
+
1315
+ if m_block < m_block_max:
1316
+ consumer_state_Q, consumer_state_dO = mma_one_m_block_fn(
1317
+ m_block,
1318
+ consumer_state_Q,
1319
+ consumer_state_dO,
1320
+ mask_fn=mask_fn_partial,
1321
+ dKV_accumulate=dKV_accumulate,
1322
+ thr_mma_SdP=thr_mma_SdP,
1323
+ batch_idx=batch_idx,
1324
+ head_idx=head_idx,
1325
+ n_block=n_block,
1326
+ softmax_scale=softmax_scale,
1327
+ seqlen=seqlen,
1328
+ aux_tensors=aux_tensors,
1329
+ fastdiv_mods=fastdiv_mods,
1330
+ )
1331
+ dKV_accumulate = True
1332
+
1333
+ if const_expr(full_cnt is not None):
1334
+ for iter_idx in cutlass.range(curr_full_cnt * subtile_factor, unroll=1):
1335
+ sparse_idx = iter_idx // subtile_factor
1336
+ subtile_offset = iter_idx % subtile_factor
1337
+ m_block = curr_full_idx[sparse_idx] * subtile_factor + subtile_offset
1338
+
1339
+ if m_block < m_block_max:
1340
+ consumer_state_Q, consumer_state_dO = mma_one_m_block_fn(
1341
+ m_block,
1342
+ consumer_state_Q,
1343
+ consumer_state_dO,
1344
+ mask_fn=mask_fn_full,
1345
+ dKV_accumulate=dKV_accumulate,
1346
+ thr_mma_SdP=thr_mma_SdP,
1347
+ batch_idx=batch_idx,
1348
+ head_idx=head_idx,
1349
+ n_block=n_block,
1350
+ softmax_scale=softmax_scale,
1351
+ seqlen=seqlen,
1352
+ aux_tensors=aux_tensors,
1353
+ fastdiv_mods=fastdiv_mods,
1354
+ )
1355
+ dKV_accumulate = True
1356
+
1357
+ return consumer_state_Q, consumer_state_dO
1358
+
1359
+
1360
+ @cute.jit
1361
+ def _store_one_dQaccum_sm90(
1362
+ m_block,
1363
+ sdQaccum: cute.Tensor,
1364
+ gdQaccum: cute.Tensor,
1365
+ num_mma_warp_groups: cutlass.Constexpr,
1366
+ num_threads_per_warp_group: cutlass.Constexpr,
1367
+ tma_copy_bytes_dQ,
1368
+ ):
1369
+ """Store dQaccum for a single m_block."""
1370
+ for warp_group_idx in cutlass.range_constexpr(num_mma_warp_groups):
1371
+ cute.arch.barrier(
1372
+ barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1373
+ number_of_threads=num_threads_per_warp_group + cute.arch.WARP_SIZE,
1374
+ )
1375
+ with cute.arch.elect_one():
1376
+ copy_utils.cpasync_reduce_bulk_add_f32(
1377
+ sdQaccum[None, warp_group_idx].iterator,
1378
+ gdQaccum[None, warp_group_idx, m_block].iterator,
1379
+ tma_copy_bytes_dQ,
1380
+ )
1381
+ cute.arch.cp_async_bulk_commit_group()
1382
+ for warp_group_idx in cutlass.range_constexpr(num_mma_warp_groups):
1383
+ cute.arch.cp_async_bulk_wait_group(num_mma_warp_groups - 1 - warp_group_idx, read=True)
1384
+ cute.arch.barrier_arrive(
1385
+ barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1386
+ number_of_threads=num_threads_per_warp_group + cute.arch.WARP_SIZE,
1387
+ )
1388
+
1389
+
1390
+ @cute.jit
1391
+ def dQaccum_store_block_sparse_bwd_sm90(
1392
+ blocksparse_tensors: BlockSparseTensors,
1393
+ batch_idx,
1394
+ head_idx,
1395
+ n_block,
1396
+ sdQaccum: cute.Tensor,
1397
+ gdQaccum: cute.Tensor,
1398
+ subtile_factor: cutlass.Constexpr,
1399
+ m_block_max: int,
1400
+ num_mma_warp_groups: cutlass.Constexpr,
1401
+ num_threads_per_warp_group: cutlass.Constexpr,
1402
+ tma_copy_bytes_dQ,
1403
+ ):
1404
+ """SM90 backward block sparse dQaccum store with separate partial/full loops.
1405
+
1406
+ Iterates partial blocks first, then full blocks, matching producer/consumer order.
1407
+ """
1408
+ q_cnt, q_idx, full_cnt, full_idx = blocksparse_tensors
1409
+ curr_q_cnt = q_cnt[batch_idx, head_idx, n_block]
1410
+ curr_q_idx = q_idx[batch_idx, head_idx, n_block, None]
1411
+
1412
+ if const_expr(full_cnt is not None):
1413
+ curr_full_cnt = full_cnt[batch_idx, head_idx, n_block]
1414
+ curr_full_idx = full_idx[batch_idx, head_idx, n_block, None]
1415
+ else:
1416
+ curr_full_cnt = Int32(0)
1417
+ curr_full_idx = None
1418
+
1419
+ for iter_idx in cutlass.range(curr_q_cnt * subtile_factor, unroll=1):
1420
+ sparse_idx = iter_idx // subtile_factor
1421
+ subtile_offset = iter_idx % subtile_factor
1422
+ m_block = curr_q_idx[sparse_idx] * subtile_factor + subtile_offset
1423
+
1424
+ if m_block < m_block_max:
1425
+ _store_one_dQaccum_sm90(
1426
+ m_block,
1427
+ sdQaccum,
1428
+ gdQaccum,
1429
+ num_mma_warp_groups,
1430
+ num_threads_per_warp_group,
1431
+ tma_copy_bytes_dQ,
1432
+ )
1433
+
1434
+ if const_expr(full_cnt is not None):
1435
+ for iter_idx in cutlass.range(curr_full_cnt * subtile_factor, unroll=1):
1436
+ sparse_idx = iter_idx // subtile_factor
1437
+ subtile_offset = iter_idx % subtile_factor
1438
+ m_block = curr_full_idx[sparse_idx] * subtile_factor + subtile_offset
1439
+
1440
+ if m_block < m_block_max:
1441
+ _store_one_dQaccum_sm90(
1442
+ m_block,
1443
+ sdQaccum,
1444
+ gdQaccum,
1445
+ num_mma_warp_groups,
1446
+ num_threads_per_warp_group,
1447
+ tma_copy_bytes_dQ,
1448
+ )
kernel/deps/flash_attn/cute/block_sparsity.py ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 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 _expand_sparsity_tensor(
38
+ tensor: torch.Tensor,
39
+ expected_shape: Tuple[int, ...],
40
+ tensor_name: str,
41
+ context: str | None,
42
+ hint: str | Callable[[], str] | None,
43
+ ) -> torch.Tensor:
44
+ """Check if we need to expand the tensor to expected shape, and do so if possible."""
45
+ needs_expand = tensor.shape != expected_shape
46
+ if not needs_expand:
47
+ return tensor
48
+ can_expand = all(map(lambda cur, tgt: cur == tgt or cur == 1, tensor.shape, expected_shape))
49
+ if not can_expand:
50
+ context_clause = f" ({context})" if context else ""
51
+ resolved_hint = hint() if callable(hint) else hint
52
+ hint_clause = f" Hint: {resolved_hint}" if resolved_hint else ""
53
+ raise ValueError(
54
+ f"{tensor_name}{context_clause} with shape {tensor.shape} cannot be expanded to expected shape {expected_shape}."
55
+ f"{hint_clause}"
56
+ )
57
+ return tensor.expand(*expected_shape)
58
+
59
+
60
+ def _check_and_expand_block(
61
+ name: str,
62
+ cnt: torch.Tensor | None,
63
+ idx: torch.Tensor | None,
64
+ expected_count_shape: Tuple[int, int, int],
65
+ expected_index_shape: Tuple[int, int, int, int],
66
+ context: str | None,
67
+ hint: str | Callable[[], str] | None,
68
+ ) -> Tuple[torch.Tensor | None, torch.Tensor | None]:
69
+ if (cnt is None) != (idx is None):
70
+ raise ValueError(
71
+ f"{name}_block_cnt and {name}_block_idx must both be provided or both be None"
72
+ )
73
+ if cnt is None or idx is None:
74
+ return None, None
75
+ if cnt.dtype != torch.int32 or idx.dtype != torch.int32:
76
+ raise ValueError(f"{name}_block tensors must have dtype torch.int32")
77
+ if cnt.device != idx.device:
78
+ raise ValueError(f"{name}_block_cnt and {name}_block_idx must be on the same device")
79
+ if not cnt.is_cuda or not idx.is_cuda:
80
+ raise ValueError(f"{name}_block tensors must live on CUDA")
81
+ expanded_cnt = _expand_sparsity_tensor(
82
+ cnt, expected_count_shape, f"{name}_block_cnt", context, hint
83
+ )
84
+ expanded_idx = _expand_sparsity_tensor(
85
+ idx, expected_index_shape, f"{name}_block_idx", context, hint
86
+ )
87
+ return expanded_cnt, expanded_idx
88
+
89
+
90
+ def get_block_sparse_expected_shapes(
91
+ batch_size: int,
92
+ num_head: int,
93
+ seqlen_q: int,
94
+ seqlen_k: int,
95
+ m_block_size: int,
96
+ n_block_size: int,
97
+ q_stage: int,
98
+ ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
99
+ """Return (expected_count_shape, expected_index_shape) for block sparse normalization."""
100
+ m_block_size_effective = q_stage * m_block_size
101
+ expected_m_blocks = ceildiv(seqlen_q, m_block_size_effective)
102
+ expected_n_blocks = ceildiv(seqlen_k, n_block_size)
103
+ expected_count_shape = (batch_size, num_head, expected_m_blocks)
104
+ expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
105
+ return expected_count_shape, expected_index_shape
106
+
107
+
108
+ def infer_block_sparse_expected_shapes(
109
+ tensors: BlockSparseTensorsTorch,
110
+ *,
111
+ batch_size: int,
112
+ num_head: int,
113
+ seqlen_q: int,
114
+ seqlen_k: int,
115
+ m_block_size: int,
116
+ n_block_size: int,
117
+ q_stage: int,
118
+ context: str,
119
+ sparse_block_size_q: int | None = None,
120
+ sparse_block_size_kv: int | None = None,
121
+ ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int], int]:
122
+ """Infer shapes and scaling for block-sparse tensors.
123
+
124
+ Expectations:
125
+ - mask_block_cnt is (B, H, M) and mask_block_idx is (B, H, M, N).
126
+ - Batch/head dims may be 1 for broadcast, or match the requested sizes.
127
+ - sparse_block_size_kv must match tile_n.
128
+ - sparse_block_size_q must be a multiple of q_stage * tile_m.
129
+ - If sparse_block_size_q is omitted and seqlen_q/num_m_blocks is ambiguous,
130
+ the caller must provide block_size to disambiguate. TODO will make this required in a future PR.
131
+ """
132
+ base_m_block = q_stage * m_block_size
133
+ base_n_block = n_block_size
134
+ if sparse_block_size_kv is None:
135
+ sparse_block_size_kv = base_n_block
136
+ if sparse_block_size_kv != base_n_block:
137
+ raise ValueError(f"Block sparse tensors{context} require BLOCK_SIZE_KV={base_n_block}.")
138
+ if tensors.mask_block_idx is None:
139
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
140
+ num_m_blocks = tensors.mask_block_idx.shape[2]
141
+
142
+ if sparse_block_size_q is None:
143
+ min_block_size = ceildiv(seqlen_q, num_m_blocks)
144
+ if num_m_blocks == 1:
145
+ max_block_size = seqlen_q
146
+ else:
147
+ max_block_size = (seqlen_q - 1) // (num_m_blocks - 1)
148
+ if max_block_size != min_block_size and base_m_block != 1:
149
+ raise ValueError(
150
+ f"Block sparse tensors{context} require explicit sparse_block_size[0] "
151
+ f"to disambiguate block size for seqlen_q={seqlen_q} and num_m_blocks={num_m_blocks}."
152
+ )
153
+ sparse_block_size_q = min_block_size
154
+
155
+ if sparse_block_size_q % base_m_block != 0:
156
+ raise ValueError(
157
+ f"Block sparse tensors{context} have block size {sparse_block_size_q}, "
158
+ f"which must be a multiple of {base_m_block}."
159
+ )
160
+
161
+ expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
162
+ expected_n_blocks = ceildiv(seqlen_k, sparse_block_size_kv)
163
+ q_subtile_factor = sparse_block_size_q // base_m_block
164
+ expected_count_shape = (batch_size, num_head, expected_m_blocks)
165
+ expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
166
+
167
+ mask_block_cnt = tensors.mask_block_cnt
168
+ mask_block_idx = tensors.mask_block_idx
169
+ if mask_block_cnt is None or mask_block_idx is None:
170
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
171
+ if mask_block_cnt.ndim != 3 or mask_block_idx.ndim != 4:
172
+ raise ValueError(
173
+ f"Block sparse tensors{context} must have shapes (B, H, M) and (B, H, M, N)."
174
+ )
175
+ for dim_name, cur, tgt in (
176
+ ("batch", mask_block_cnt.shape[0], expected_count_shape[0]),
177
+ ("head", mask_block_cnt.shape[1], expected_count_shape[1]),
178
+ ):
179
+ if cur != tgt and cur != 1:
180
+ raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
181
+ for dim_name, cur, tgt in (
182
+ ("batch", mask_block_idx.shape[0], expected_index_shape[0]),
183
+ ("head", mask_block_idx.shape[1], expected_index_shape[1]),
184
+ ):
185
+ if cur != tgt and cur != 1:
186
+ raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
187
+ if mask_block_cnt.shape[2] != mask_block_idx.shape[2]:
188
+ raise ValueError(f"Block sparse tensors{context} must share the same m-block dimension.")
189
+ if mask_block_idx.shape[3] != expected_n_blocks:
190
+ raise ValueError(
191
+ f"Block sparse tensors{context} n-block dimension must be {expected_n_blocks}."
192
+ )
193
+ if expected_m_blocks != num_m_blocks:
194
+ raise ValueError(
195
+ f"Block sparse tensors{context} m-block dimension {num_m_blocks} does not match "
196
+ f"sparse_block_size_q={sparse_block_size_q}. "
197
+ f"Set BlockSparseTensorsTorch.block_size to match the BlockMask BLOCK_SIZE."
198
+ )
199
+ return expected_count_shape, expected_index_shape, q_subtile_factor
200
+
201
+
202
+ def get_block_sparse_expected_shapes_bwd(
203
+ batch_size: int,
204
+ num_head: int,
205
+ seqlen_q: int,
206
+ seqlen_k: int,
207
+ m_block_size: int,
208
+ n_block_size: int,
209
+ subtile_factor: int,
210
+ ) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
211
+ """Return (expected_count_shape, expected_index_shape) for backward block sparse normalization.
212
+
213
+ Backward uses Q-direction indexing (transposed from forward), where shapes are
214
+ indexed by N-blocks first, then M-blocks. The sparse_block_size_q is determined
215
+ by subtile_factor * m_block_size.
216
+ """
217
+ sparse_block_size_q = subtile_factor * m_block_size
218
+ expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
219
+ expected_n_blocks = ceildiv(seqlen_k, n_block_size)
220
+ expected_count_shape = (batch_size, num_head, expected_n_blocks)
221
+ expected_index_shape = (batch_size, num_head, expected_n_blocks, expected_m_blocks)
222
+ return expected_count_shape, expected_index_shape
223
+
224
+
225
+ def normalize_block_sparse_tensors(
226
+ tensors: BlockSparseTensorsTorch,
227
+ *,
228
+ expected_count_shape: Tuple[int, int, int],
229
+ expected_index_shape: Tuple[int, int, int, int],
230
+ context: str | None = None,
231
+ hint: str | Callable[[], str] | None = None,
232
+ ) -> BlockSparseTensorsTorch:
233
+ if tensors.mask_block_cnt is None or tensors.mask_block_idx is None:
234
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
235
+
236
+ mask_cnt, mask_idx = _check_and_expand_block(
237
+ "mask",
238
+ tensors.mask_block_cnt,
239
+ tensors.mask_block_idx,
240
+ expected_count_shape,
241
+ expected_index_shape,
242
+ context,
243
+ hint,
244
+ )
245
+ if mask_cnt is None or mask_idx is None:
246
+ raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
247
+
248
+ full_cnt, full_idx = _check_and_expand_block(
249
+ "full",
250
+ tensors.full_block_cnt,
251
+ tensors.full_block_idx,
252
+ expected_count_shape,
253
+ expected_index_shape,
254
+ context,
255
+ hint,
256
+ )
257
+ if full_cnt is not None and mask_cnt.device != full_cnt.device:
258
+ raise ValueError("All block sparse tensors must be on the same device")
259
+
260
+ return BlockSparseTensorsTorch(
261
+ mask_block_cnt=mask_cnt,
262
+ mask_block_idx=mask_idx,
263
+ full_block_cnt=full_cnt,
264
+ full_block_idx=full_idx,
265
+ block_size=tensors.block_size,
266
+ )
267
+
268
+
269
+ def is_block_sparsity_enabled(tensors: BlockSparseTensorsTorch) -> bool:
270
+ return any(t is not None for t in (tensors.full_block_cnt, tensors.mask_block_cnt))
271
+
272
+
273
+ def get_block_sparse_broadcast_pattern(
274
+ tensors: BlockSparseTensorsTorch,
275
+ ) -> Tuple[Tuple[bool, ...], ...] | None:
276
+ """Return broadcast pattern for block sparse tensors by checking actual strides.
277
+
278
+ Returns a tuple of broadcast patterns (one per tensor) where each pattern
279
+ is a tuple of bools indicating which dims have stride=0.
280
+ This is used in compile keys to ensure kernels are recompiled when
281
+ broadcast patterns change, since CuTe's mark_layout_dynamic() keeps
282
+ stride=0 as static.
283
+
284
+ The tensors should already be expanded/normalized before calling this function.
285
+
286
+ Returns None if block sparsity is not enabled.
287
+ """
288
+ if not is_block_sparsity_enabled(tensors):
289
+ return None
290
+
291
+ patterns = []
292
+ for tensor in (
293
+ tensors.mask_block_cnt,
294
+ tensors.mask_block_idx,
295
+ tensors.full_block_cnt,
296
+ tensors.full_block_idx,
297
+ ):
298
+ if tensor is not None:
299
+ patterns.append(get_broadcast_dims(tensor))
300
+ else:
301
+ patterns.append(None)
302
+ return tuple(patterns)
303
+
304
+
305
+ def normalize_block_sparse_config(
306
+ tensors: BlockSparseTensorsTorch,
307
+ *,
308
+ batch_size: int,
309
+ num_head: int,
310
+ seqlen_q: int,
311
+ seqlen_k: int,
312
+ block_size: tuple[int, int],
313
+ q_stage: int,
314
+ ) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None, int]:
315
+ m_block_size, n_block_size = block_size
316
+ if tensors.block_size is None:
317
+ sparse_block_size_q, sparse_block_size_kv = q_stage * m_block_size, n_block_size
318
+ else:
319
+ sparse_block_size_q, sparse_block_size_kv = tensors.block_size
320
+ if sparse_block_size_kv != n_block_size:
321
+ raise ValueError(
322
+ f"Block sparsity requires sparse_block_size[1]={n_block_size} to match tile_n."
323
+ )
324
+ expected_count_shape, expected_index_shape, q_subtile_factor = (
325
+ infer_block_sparse_expected_shapes(
326
+ tensors,
327
+ batch_size=batch_size,
328
+ num_head=num_head,
329
+ seqlen_q=seqlen_q,
330
+ seqlen_k=seqlen_k,
331
+ m_block_size=m_block_size,
332
+ n_block_size=n_block_size,
333
+ q_stage=q_stage,
334
+ context="forward",
335
+ sparse_block_size_q=sparse_block_size_q,
336
+ sparse_block_size_kv=sparse_block_size_kv,
337
+ )
338
+ )
339
+ normalized_tensors = normalize_block_sparse_tensors(
340
+ tensors,
341
+ expected_count_shape=expected_count_shape,
342
+ expected_index_shape=expected_index_shape,
343
+ )
344
+ return (
345
+ normalized_tensors,
346
+ get_block_sparse_broadcast_pattern(normalized_tensors),
347
+ q_subtile_factor,
348
+ )
349
+
350
+
351
+ def normalize_block_sparse_config_bwd(
352
+ tensors: BlockSparseTensorsTorch,
353
+ *,
354
+ batch_size: int,
355
+ num_head: int,
356
+ seqlen_q: int,
357
+ seqlen_k: int,
358
+ block_size: tuple[int, int],
359
+ subtile_factor: int,
360
+ ) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None]:
361
+ m_block_size, n_block_size = block_size
362
+ if tensors.block_size is None:
363
+ sparse_block_size_q, sparse_block_size_kv = subtile_factor * m_block_size, n_block_size
364
+ else:
365
+ sparse_block_size_q, sparse_block_size_kv = tensors.block_size
366
+ if sparse_block_size_q != subtile_factor * m_block_size:
367
+ raise ValueError(
368
+ f"Block sparsity expects sparse_block_size_q={subtile_factor * m_block_size} "
369
+ f"for subtile_factor={subtile_factor}."
370
+ )
371
+ if sparse_block_size_kv != n_block_size:
372
+ raise ValueError(
373
+ f"Block sparsity expects sparse_block_size[1]={n_block_size} to match tile_n."
374
+ )
375
+ expected_count_shape, expected_index_shape = get_block_sparse_expected_shapes_bwd(
376
+ batch_size,
377
+ num_head,
378
+ seqlen_q,
379
+ seqlen_k,
380
+ m_block_size,
381
+ n_block_size,
382
+ subtile_factor,
383
+ )
384
+ normalized_tensors = normalize_block_sparse_tensors(
385
+ tensors,
386
+ expected_count_shape=expected_count_shape,
387
+ expected_index_shape=expected_index_shape,
388
+ context="_flash_attn_bwd",
389
+ hint=lambda: (
390
+ f"Backward expects Q-direction block-sparse tensors (q_mask_cnt/q_mask_idx, "
391
+ f"and optionally full_q_cnt/full_q_idx). Regenerate the backward BlockMask with "
392
+ f"BLOCK_SIZE=({subtile_factor * m_block_size}, {n_block_size})."
393
+ ),
394
+ )
395
+ return normalized_tensors, get_block_sparse_broadcast_pattern(normalized_tensors)
396
+
397
+
398
+ def to_cute_block_sparse_tensors(
399
+ tensors: BlockSparseTensorsTorch, enable_tvm_ffi: bool = True
400
+ ) -> BlockSparseTensors | None:
401
+ """Convert torch block sparsity tensors to CuTe tensors, optionally for tvm ffi"""
402
+ if not is_block_sparsity_enabled(tensors):
403
+ return None
404
+ (
405
+ mask_block_cnt,
406
+ mask_block_idx,
407
+ full_block_cnt,
408
+ full_block_idx,
409
+ *_,
410
+ ) = tensors
411
+
412
+ (
413
+ mask_block_cnt_tensor,
414
+ mask_block_idx_tensor,
415
+ ) = [
416
+ to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
417
+ for t in (mask_block_cnt, mask_block_idx)
418
+ ]
419
+ (
420
+ full_block_cnt_tensor,
421
+ full_block_idx_tensor,
422
+ ) = [
423
+ to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
424
+ if t is not None
425
+ else None
426
+ for t in (full_block_cnt, full_block_idx)
427
+ ]
428
+
429
+ return BlockSparseTensors(
430
+ mask_block_cnt_tensor,
431
+ mask_block_idx_tensor,
432
+ full_block_cnt_tensor,
433
+ full_block_idx_tensor,
434
+ )
435
+
436
+
437
+ def fast_sampling(mask_mod):
438
+ """Convenience decorator to mark mask_mod as safe for 5-point fast sampling"""
439
+ mask_mod.use_fast_sampling = True
440
+ return mask_mod
kernel/deps/flash_attn/cute/compute_block_sparsity.py ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import partial
2
+ from typing import Callable, Optional, Tuple
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+ import torch
7
+ from cutlass import Boolean, Int8, Int32, const_expr
8
+
9
+ from flash_attn.cute.block_sparsity import (
10
+ BlockSparseTensors,
11
+ BlockSparseTensorsTorch,
12
+ to_cute_block_sparse_tensors,
13
+ )
14
+ from flash_attn.cute.utils import hash_callable, scalar_to_ssa, ssa_to_scalar
15
+ from flash_attn.cute.seqlen_info import SeqlenInfoQK
16
+
17
+
18
+ class BlockSparsityKernel:
19
+ """Block sparsity kernel for FlexAttention.
20
+
21
+ This kernel computes `mask_mod` for every token of each block
22
+ to determine if an n block is full, masked, or neither.
23
+
24
+ Writes block counts and indices to a BlockSparseTensors object.
25
+
26
+ When use_fast_sampling=True, uses 5-point sampling (4 corners + center)
27
+ which is much faster but only suitable for masks where this is sufficient.
28
+
29
+ TODO:
30
+ - optimize mask_mod evaluation
31
+ - varlen support
32
+ - transposed tensors for bwd pass
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ mask_mod: Callable,
38
+ tile_mn: Tuple[int, int],
39
+ compute_full_blocks: bool = True,
40
+ use_aux_tensors: bool = False,
41
+ use_fast_sampling: bool = False,
42
+ ):
43
+ self.mask_mod = mask_mod
44
+ self.tile_mn = tile_mn
45
+ self.compute_full_blocks = compute_full_blocks
46
+ self.use_aux_tensors = use_aux_tensors
47
+ self.use_fast_sampling = use_fast_sampling
48
+
49
+ @cute.jit
50
+ def __call__(
51
+ self,
52
+ blocksparse_tensors: BlockSparseTensors,
53
+ seqlen_q: Int32,
54
+ seqlen_k: Int32,
55
+ aux_tensors: Optional[list] = None,
56
+ ):
57
+ self.mask_cnt, self.mask_idx, self.full_cnt, self.full_idx = blocksparse_tensors
58
+
59
+ if const_expr(self.compute_full_blocks):
60
+ assert self.full_cnt is not None and self.full_idx is not None, (
61
+ "full block tensors must be provided when computing full blocks"
62
+ )
63
+
64
+ batch_size, num_heads, num_m_blocks, num_n_blocks = self.mask_idx.shape
65
+ # launch 1 CTA per m block
66
+ grid = [num_m_blocks, num_heads, batch_size]
67
+
68
+ if const_expr(self.use_fast_sampling):
69
+ num_threads = 5
70
+ self.num_warps = 1
71
+ else:
72
+ num_threads = self.tile_mn[0]
73
+ self.num_warps = (num_threads + 32 - 1) // 32
74
+
75
+ self.kernel(
76
+ self.mask_cnt,
77
+ self.mask_idx,
78
+ self.full_cnt,
79
+ self.full_idx,
80
+ num_n_blocks,
81
+ seqlen_q,
82
+ seqlen_k,
83
+ aux_tensors,
84
+ ).launch(grid=grid, block=[num_threads, 1, 1])
85
+
86
+ @cute.kernel
87
+ def kernel(
88
+ self,
89
+ mask_cnt: cute.Tensor,
90
+ mask_idx: cute.Tensor,
91
+ full_cnt: cute.Tensor,
92
+ full_idx: cute.Tensor,
93
+ num_n_blocks: Int32,
94
+ seqlen_q: Int32,
95
+ seqlen_k: Int32,
96
+ aux_tensors: Optional[list] = None,
97
+ ):
98
+ tidx, _, _ = cute.arch.thread_idx()
99
+ warp_idx = cute.arch.warp_idx()
100
+ lane_id = cute.arch.lane_idx()
101
+ m_block, head_idx, batch_idx = cute.arch.block_idx()
102
+
103
+ ssa = partial(scalar_to_ssa, dtype=Int32)
104
+
105
+ seqlen = SeqlenInfoQK.create(
106
+ batch_idx,
107
+ seqlen_q,
108
+ seqlen_k,
109
+ mCuSeqlensQ=None,
110
+ mCuSeqlensK=None,
111
+ mSeqUsedQ=None,
112
+ mSeqUsedK=None,
113
+ )
114
+
115
+ @cute.struct
116
+ class SharedStorage:
117
+ reduction_buffer_smem: cute.struct.Align[
118
+ cute.struct.MemRange[cutlass.Int8, 2 * self.num_warps], 1024
119
+ ]
120
+
121
+ smem = cutlass.utils.SmemAllocator()
122
+ storage = smem.allocate(SharedStorage, 16)
123
+
124
+ reduction_buffer = storage.reduction_buffer_smem.get_tensor(
125
+ cute.make_layout((self.num_warps, 2))
126
+ )
127
+
128
+ num_mask_blocks = Int32(0)
129
+ num_full_blocks = Int32(0)
130
+
131
+ for n_block in cutlass.range(num_n_blocks, unroll_full=True):
132
+ m_base = m_block * self.tile_mn[0]
133
+ n_base = n_block * self.tile_mn[1]
134
+
135
+ if const_expr(self.use_fast_sampling):
136
+ # Fast path: 5-point sampling (4 corners + center)
137
+ # Clamps OOB indices to nearest in bounds.
138
+ thread_result = Boolean(False)
139
+ thread_is_valid = Boolean(False)
140
+ q_idx = Int32(0)
141
+ kv_idx = Int32(0)
142
+
143
+ if tidx == 0:
144
+ # Top-left corner (0, 0); always in bounds
145
+ q_idx = m_base
146
+ kv_idx = n_base
147
+ elif tidx == 1:
148
+ # Top-right corner
149
+ q_idx = m_base
150
+ kv_idx = cutlass.min(n_base + self.tile_mn[1] - 1, seqlen_k - 1)
151
+ elif tidx == 2:
152
+ # Bottom-left corner
153
+ q_idx = cutlass.min(m_base + self.tile_mn[0] - 1, seqlen_q - 1)
154
+ kv_idx = n_base
155
+ elif tidx == 3:
156
+ # Bottom-right corner
157
+ q_idx = cutlass.min(m_base + self.tile_mn[0] - 1, seqlen_q - 1)
158
+ kv_idx = cutlass.min(n_base + self.tile_mn[1] - 1, seqlen_k - 1)
159
+ elif tidx == 4:
160
+ # Center point
161
+ q_idx = m_base + (cutlass.min(seqlen_q - m_base, self.tile_mn[0])) // 2
162
+ kv_idx = n_base + (cutlass.min(seqlen_k - n_base, self.tile_mn[1])) // 2
163
+ else:
164
+ thread_is_valid = Boolean(False)
165
+
166
+ # Check bounds and determine if this thread has a valid index pair
167
+ if tidx < 5 and q_idx < seqlen_q and kv_idx < seqlen_k:
168
+ thread_is_valid = Boolean(True)
169
+ q_idx_ssa = ssa(q_idx)
170
+ kv_idx_ssa = ssa(kv_idx)
171
+ thread_result = ssa_to_scalar(
172
+ self.mask_mod(
173
+ ssa(batch_idx),
174
+ ssa(head_idx),
175
+ q_idx_ssa,
176
+ kv_idx_ssa,
177
+ seqlen,
178
+ aux_tensors,
179
+ )
180
+ )
181
+ else:
182
+ thread_is_valid = Boolean(False)
183
+
184
+ # Use vote_any_sync to see if any valid thread found unmasked or masked
185
+ # Only count results from threads that checked valid indices
186
+ has_unmasked = cute.arch.vote_any_sync(thread_result & thread_is_valid)
187
+ has_masked = cute.arch.vote_any_sync((Boolean(not thread_result)) & thread_is_valid)
188
+
189
+ else:
190
+ # Full path: check all elements in the block
191
+ # Track if this thread's row has any masked or unmasked elements
192
+ thread_has_unmasked = Boolean(False)
193
+ thread_has_masked = Boolean(False)
194
+ thread_is_valid = Boolean(False)
195
+
196
+ # Each thread handles 1 row
197
+ q_idx = m_base + tidx
198
+ kv_idx = Int32(0)
199
+ if tidx < self.tile_mn[0] and q_idx < seqlen_q:
200
+ thread_is_valid = Boolean(True)
201
+ q_idx_ssa = ssa(q_idx)
202
+
203
+ # Loop over all columns in this row
204
+ for c in cutlass.range(self.tile_mn[1], unroll_full=True):
205
+ kv_idx = n_base + c
206
+ kv_idx_ssa = ssa(kv_idx)
207
+
208
+ # Only check elements within valid sequence bounds
209
+ if kv_idx < seqlen_k:
210
+ # Direct scalar call
211
+ mask_val = ssa_to_scalar(
212
+ self.mask_mod(
213
+ ssa(batch_idx),
214
+ ssa(head_idx),
215
+ q_idx_ssa,
216
+ kv_idx_ssa,
217
+ seqlen,
218
+ aux_tensors,
219
+ )
220
+ )
221
+
222
+ # Update tracking flags
223
+ if mask_val:
224
+ thread_has_unmasked = Boolean(True)
225
+ else:
226
+ thread_has_masked = Boolean(True)
227
+
228
+ # Block-level reduction to combine results across all threads
229
+ # Only count votes from threads that checked valid indices
230
+ warp_has_unmasked_mask = cute.arch.vote_any_sync(
231
+ thread_has_unmasked & thread_is_valid
232
+ )
233
+ warp_has_masked_mask = cute.arch.vote_any_sync(thread_has_masked & thread_is_valid)
234
+
235
+ # lane 0 writes the ballot mask to shared memory
236
+ lane_id = tidx % 32
237
+ if lane_id == 0:
238
+ # Store as Int8
239
+ reduction_buffer[warp_idx, 0] = Int8(1) if warp_has_unmasked_mask else Int8(0)
240
+ reduction_buffer[warp_idx, 1] = Int8(1) if warp_has_masked_mask else Int8(0)
241
+
242
+ cute.arch.sync_threads()
243
+
244
+ # Thread 0 ORs all warp results together
245
+ has_unmasked = Boolean(False)
246
+ has_masked = Boolean(False)
247
+ if tidx == 0:
248
+ for w in cutlass.range(self.num_warps):
249
+ if reduction_buffer[w, 0]:
250
+ has_unmasked = Boolean(True)
251
+ if reduction_buffer[w, 1]:
252
+ has_masked = Boolean(True)
253
+
254
+ # Only thread 0 updates the output arrays (common to both paths)
255
+ if tidx == 0:
256
+ # Block classification based on what we found:
257
+ # - If has_masked and has_unmasked: partial block (needs masking)
258
+ # - If only has_unmasked: full block (no masking needed)
259
+ # - If only has_masked: skip this block entirely
260
+ is_partial = Boolean(has_masked and has_unmasked)
261
+ is_full = Boolean(has_unmasked and (not has_masked))
262
+
263
+ if is_partial:
264
+ mask_idx[batch_idx, head_idx, m_block, num_mask_blocks] = n_block
265
+ num_mask_blocks += 1
266
+ elif is_full and const_expr(self.compute_full_blocks):
267
+ full_idx[batch_idx, head_idx, m_block, num_full_blocks] = n_block
268
+ num_full_blocks += 1
269
+
270
+ # Only thread 0 writes back the counts
271
+ if tidx == 0:
272
+ mask_cnt[batch_idx, head_idx, m_block] = num_mask_blocks
273
+ if const_expr(self.compute_full_blocks):
274
+ full_cnt[batch_idx, head_idx, m_block] = num_full_blocks
275
+
276
+
277
+ def compute_block_sparsity(
278
+ tile_m,
279
+ tile_n,
280
+ batch_size,
281
+ num_heads,
282
+ seqlen_q,
283
+ seqlen_k,
284
+ mask_mod: Callable,
285
+ aux_tensors: Optional[list], # list[cute.Tensor]
286
+ device,
287
+ compute_full_blocks: bool = True,
288
+ use_fast_sampling: bool = False,
289
+ ) -> Tuple[BlockSparseTensors, BlockSparseTensorsTorch]:
290
+ """
291
+ Computes block sparsity for a given `mask_mod`.
292
+
293
+ Args:
294
+ tile_m: The tile size for the m dimension.
295
+ tile_n: The tile size for the n dimension.
296
+ batch_size: The batch size.
297
+ num_heads: The number of heads.
298
+ seqlen_q: The sequence length for the query.
299
+ seqlen_k: The sequence length for the key.
300
+ mask_mod: The `mask_mod` callable to use.
301
+ aux_tensors: A list of auxiliary tensors.
302
+ device: The device to use.
303
+ compute_full_blocks: Whether to compute full blocks. If False, only partially-masked blocks are computed.
304
+ use_fast_sampling: Whether to use 5-point sampling (4 corners + center). This is much faster, but only suitable for masks where this check is sufficient.
305
+
306
+ Returns:
307
+ A tuple of `BlockSparseTensors` and `BlockSparseTensorsTorch`.
308
+ """
309
+ # Check if mask_mod is marked as suitable for 5-point fast sampling
310
+ use_fast_sampling = getattr(mask_mod, "use_fast_sampling", use_fast_sampling)
311
+
312
+ num_m_blocks = (seqlen_q + tile_m - 1) // tile_m
313
+ num_n_blocks = (seqlen_k + tile_n - 1) // tile_n
314
+
315
+ mask_block_cnt = torch.zeros(
316
+ (batch_size, num_heads, num_m_blocks), device=device, dtype=torch.int32
317
+ )
318
+ mask_block_idx = torch.zeros(
319
+ (batch_size, num_heads, num_m_blocks, num_n_blocks), device=device, dtype=torch.int32
320
+ )
321
+ full_block_cnt = (
322
+ torch.zeros((batch_size, num_heads, num_m_blocks), device=device, dtype=torch.int32)
323
+ if compute_full_blocks
324
+ else None
325
+ )
326
+ full_block_idx = (
327
+ torch.zeros(
328
+ (batch_size, num_heads, num_m_blocks, num_n_blocks), device=device, dtype=torch.int32
329
+ )
330
+ if compute_full_blocks
331
+ else None
332
+ )
333
+
334
+ blocksparse_tensors_torch = BlockSparseTensorsTorch(
335
+ mask_block_cnt=mask_block_cnt,
336
+ mask_block_idx=mask_block_idx,
337
+ full_block_cnt=full_block_cnt,
338
+ full_block_idx=full_block_idx,
339
+ block_size=(tile_m, tile_n),
340
+ )
341
+
342
+ mask_mod_hash = hash_callable(mask_mod)
343
+ blocksparse_tensors = to_cute_block_sparse_tensors(
344
+ blocksparse_tensors_torch, enable_tvm_ffi=True
345
+ )
346
+
347
+ compile_key = (
348
+ tile_m,
349
+ tile_n,
350
+ mask_mod_hash,
351
+ compute_full_blocks,
352
+ aux_tensors is not None,
353
+ use_fast_sampling,
354
+ )
355
+ if compile_key not in compute_block_sparsity.compile_cache:
356
+ kernel = BlockSparsityKernel(
357
+ mask_mod,
358
+ tile_mn=(tile_m, tile_n),
359
+ compute_full_blocks=compute_full_blocks,
360
+ use_aux_tensors=aux_tensors is not None,
361
+ use_fast_sampling=use_fast_sampling,
362
+ )
363
+
364
+ compute_block_sparsity.compile_cache[compile_key] = cute.compile(
365
+ kernel, blocksparse_tensors, seqlen_q, seqlen_k, aux_tensors, options="--enable-tvm-ffi"
366
+ )
367
+
368
+ compute_block_sparsity.compile_cache[compile_key](
369
+ blocksparse_tensors_torch[:4],
370
+ seqlen_q,
371
+ seqlen_k,
372
+ aux_tensors,
373
+ )
374
+
375
+ return blocksparse_tensors, blocksparse_tensors_torch
376
+
377
+
378
+ compute_block_sparsity.compile_cache = {}
kernel/deps/flash_attn/cute/copy_utils.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
2
+
3
+ import math
4
+ from typing import Optional, Type, Callable
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+ from cutlass import Float32, Int32, const_expr
9
+ from cutlass.cute.nvgpu import cpasync
10
+ import cutlass.utils.blackwell_helpers as sm100_utils
11
+ from cutlass.cutlass_dsl import T, dsl_user_op
12
+ from cutlass._mlir.dialects import llvm
13
+ import cutlass.pipeline
14
+
15
+
16
+ @dsl_user_op
17
+ def cvt_copy(
18
+ atom: cute.CopyAtom,
19
+ src: cute.Tensor,
20
+ dst: cute.Tensor,
21
+ *,
22
+ pred: Optional[cute.Tensor] = None,
23
+ loc=None,
24
+ ip=None,
25
+ **kwargs,
26
+ ) -> None:
27
+ assert isinstance(src.iterator, cute.Pointer) and src.memspace == cute.AddressSpace.rmem
28
+ if const_expr(src.element_type != dst.element_type):
29
+ src_cvt = cute.make_fragment_like(src, dst.element_type, loc=loc, ip=ip)
30
+ src_cvt.store(src.load().to(dst.element_type))
31
+ src = src_cvt
32
+ cute.copy(atom, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
33
+
34
+
35
+ @dsl_user_op
36
+ def load_s2r(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
37
+ dst = cute.make_fragment_like(src, src.element_type, loc=loc, ip=ip)
38
+ cute.autovec_copy(src, dst, loc=loc, ip=ip)
39
+ return dst
40
+
41
+
42
+ @dsl_user_op
43
+ def get_copy_atom(
44
+ dtype: Type[cutlass.Numeric], num_copy_elems: int, is_async: bool = False, *, loc=None, ip=None
45
+ ) -> cute.CopyAtom:
46
+ num_copy_bits = const_expr(min(128, num_copy_elems * dtype.width))
47
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
48
+ return cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
49
+
50
+
51
+ @dsl_user_op
52
+ def make_tmem_copy(
53
+ tmem_copy_atom: cute.CopyAtom, num_wg: int = 1, *, loc=None, ip=None
54
+ ) -> cute.CopyAtom:
55
+ num_dp, num_bits, num_rep, _ = sm100_utils.get_tmem_copy_properties(tmem_copy_atom)
56
+ assert num_dp == 32
57
+ assert num_bits == 32
58
+ tiler_mn = (cute.make_layout((128 * num_rep * num_wg // 32, 32), stride=(32, 1)),)
59
+ layout_tv = cute.make_layout(
60
+ ((32, 4, num_wg), (num_rep, 32)), stride=((0, 1, 4 * num_rep), (4, 4 * num_rep * num_wg))
61
+ )
62
+ return cute.make_tiled_copy(tmem_copy_atom, layout_tv, tiler_mn)
63
+
64
+
65
+ @dsl_user_op
66
+ def copy(
67
+ src: cute.Tensor,
68
+ dst: cute.Tensor,
69
+ *,
70
+ pred: Optional[cute.Tensor] = None,
71
+ num_copy_elems: int = 1,
72
+ is_async: bool = False,
73
+ loc=None,
74
+ ip=None,
75
+ **kwargs,
76
+ ) -> None:
77
+ copy_atom = get_copy_atom(src.element_type, num_copy_elems, is_async)
78
+ cute.copy(copy_atom, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
79
+
80
+
81
+ def tiled_copy_1d(
82
+ dtype: Type[cutlass.Numeric], num_threads: int, num_copy_elems: int = 1, is_async: bool = False
83
+ ) -> cute.TiledCopy:
84
+ num_copy_bits = num_copy_elems * dtype.width
85
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
86
+ copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
87
+ thr_layout = cute.make_layout(num_threads)
88
+ val_layout = cute.make_layout(num_copy_elems)
89
+ return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
90
+
91
+
92
+ def tiled_copy_2d(
93
+ dtype: Type[cutlass.Numeric], major_mode_size: int, num_threads: int, is_async: bool = False
94
+ ) -> cute.TiledCopy:
95
+ num_copy_bits = math.gcd(major_mode_size, 128 // dtype.width) * dtype.width
96
+ copy_elems = num_copy_bits // dtype.width
97
+ copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
98
+ copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
99
+ gmem_threads_per_row = major_mode_size // copy_elems
100
+ assert num_threads % gmem_threads_per_row == 0
101
+ thr_layout = cute.make_ordered_layout(
102
+ (num_threads // gmem_threads_per_row, gmem_threads_per_row),
103
+ order=(1, 0),
104
+ )
105
+ val_layout = cute.make_layout((1, copy_elems))
106
+ return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
107
+
108
+
109
+ @dsl_user_op
110
+ def atomic_add_fp32x4(
111
+ a: Float32, b: Float32, c: Float32, d: Float32, gmem_ptr: cute.Pointer, *, loc=None, ip=None
112
+ ) -> None:
113
+ gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
114
+ # cache_hint = cutlass.Int64(0x12F0000000000000)
115
+ llvm.inline_asm(
116
+ None,
117
+ [
118
+ gmem_ptr_i64,
119
+ Float32(a).ir_value(loc=loc, ip=ip),
120
+ Float32(b).ir_value(loc=loc, ip=ip),
121
+ Float32(c).ir_value(loc=loc, ip=ip),
122
+ Float32(d).ir_value(loc=loc, ip=ip),
123
+ ],
124
+ # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip), cache_hint.ir_value()],
125
+ "{\n\t"
126
+ # ".reg .b128 abcd;\n\t"
127
+ # "mov.b128 abcd, {$1, $2, $3, $4};\n\t"
128
+ ".reg .v4 .f32 abcd;\n\t"
129
+ # "mov.b128 abcd, {$1, $2, $3, $4};\n\t"
130
+ "mov.f32 abcd.x, $1;\n\t"
131
+ "mov.f32 abcd.y, $2;\n\t"
132
+ "mov.f32 abcd.z, $3;\n\t"
133
+ "mov.f32 abcd.w, $4;\n\t"
134
+ "red.global.add.v4.f32 [$0], abcd;\n\t"
135
+ # "red.global.add.L2::cache_hint.v4.f32 [$0], abcd, 0x14F0000000000000;\n\t"
136
+ "}\n",
137
+ # "red.global.add.L2::cache_hint.f32 [$0], $1, 0x12F0000000000000;",
138
+ # "red.global.add.L2::cache_hint.f32 [$0], $1, $2;",
139
+ "l,f,f,f,f",
140
+ # "l,f,l",
141
+ has_side_effects=True,
142
+ is_align_stack=False,
143
+ asm_dialect=llvm.AsmDialect.AD_ATT,
144
+ )
145
+
146
+
147
+ @dsl_user_op
148
+ def set_block_rank(
149
+ smem_ptr: cute.Pointer, peer_cta_rank_in_cluster: Int32, *, loc=None, ip=None
150
+ ) -> Int32:
151
+ """Map the given smem pointer to the address at another CTA rank in the cluster."""
152
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
153
+ return Int32(
154
+ llvm.inline_asm(
155
+ T.i32(),
156
+ [smem_ptr_i32, peer_cta_rank_in_cluster.ir_value()],
157
+ "mapa.shared::cluster.u32 $0, $1, $2;",
158
+ "=r,r,r",
159
+ has_side_effects=False,
160
+ is_align_stack=False,
161
+ asm_dialect=llvm.AsmDialect.AD_ATT,
162
+ )
163
+ )
164
+
165
+
166
+ @dsl_user_op
167
+ def store_shared_remote_fp32x4(
168
+ a: Float32,
169
+ b: Float32,
170
+ c: Float32,
171
+ d: Float32,
172
+ smem_ptr: cute.Pointer,
173
+ mbar_ptr: cute.Pointer,
174
+ peer_cta_rank_in_cluster: Int32,
175
+ *,
176
+ loc=None,
177
+ ip=None,
178
+ ) -> None:
179
+ remote_smem_ptr_i32 = set_block_rank(
180
+ smem_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
181
+ ).ir_value()
182
+ remote_mbar_ptr_i32 = set_block_rank(
183
+ mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
184
+ ).ir_value()
185
+ llvm.inline_asm(
186
+ None,
187
+ [
188
+ remote_smem_ptr_i32,
189
+ remote_mbar_ptr_i32,
190
+ Float32(a).ir_value(loc=loc, ip=ip),
191
+ Float32(b).ir_value(loc=loc, ip=ip),
192
+ Float32(c).ir_value(loc=loc, ip=ip),
193
+ Float32(d).ir_value(loc=loc, ip=ip),
194
+ ],
195
+ "{\n\t"
196
+ ".reg .v4 .f32 abcd;\n\t"
197
+ "mov.f32 abcd.x, $2;\n\t"
198
+ "mov.f32 abcd.y, $3;\n\t"
199
+ "mov.f32 abcd.z, $4;\n\t"
200
+ "mov.f32 abcd.w, $5;\n\t"
201
+ "st.async.shared::cluster.mbarrier::complete_tx::bytes.v4.f32 [$0], abcd, [$1];\n\t"
202
+ "}\n",
203
+ "r,r,f,f,f,f",
204
+ has_side_effects=True,
205
+ is_align_stack=False,
206
+ asm_dialect=llvm.AsmDialect.AD_ATT,
207
+ )
208
+
209
+
210
+ @dsl_user_op
211
+ def cpasync_bulk_g2s(
212
+ gmem_ptr: cute.Pointer,
213
+ smem_ptr: cute.Pointer,
214
+ tma_bar_ptr: cute.Pointer,
215
+ size: int | Int32,
216
+ *,
217
+ loc=None,
218
+ ip=None,
219
+ ):
220
+ gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
221
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
222
+ mbar_ptr_i32 = tma_bar_ptr.toint(loc=loc, ip=ip).ir_value()
223
+ llvm.inline_asm(
224
+ None,
225
+ [gmem_ptr_i64, smem_ptr_i32, mbar_ptr_i32, Int32(size).ir_value()],
226
+ "cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes [$1], [$0], $3, [$2];",
227
+ "l,r,r,r",
228
+ has_side_effects=True,
229
+ is_align_stack=False,
230
+ asm_dialect=llvm.AsmDialect.AD_ATT,
231
+ )
232
+
233
+
234
+ @dsl_user_op
235
+ def cpasync_reduce_bulk_add_f32(
236
+ smem_ptr: cute.Pointer,
237
+ gmem_ptr: cute.Pointer,
238
+ store_bytes: int | Int32,
239
+ *,
240
+ loc=None,
241
+ ip=None,
242
+ ):
243
+ smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
244
+ # cache_hint = cutlass.Int64(0x14F0000000000000) # EVICT_LAST
245
+ llvm.inline_asm(
246
+ None,
247
+ [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value()],
248
+ "cp.reduce.async.bulk.global.shared::cta.bulk_group.add.f32 [$0], [$1], $2;",
249
+ "l,r,r",
250
+ # [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value(), cache_hint.ir_value()],
251
+ # "cp.reduce.async.bulk.global.shared::cta.bulk_group.L2::cache_hint.add.f32 [$0], [$1], $2, $3;",
252
+ # "l,r,r,l",
253
+ has_side_effects=True,
254
+ is_align_stack=False,
255
+ asm_dialect=llvm.AsmDialect.AD_ATT,
256
+ )
257
+
258
+
259
+ def cpasync_bulk_get_copy_fn(
260
+ src_tensor: cute.Tensor,
261
+ dst_tensor: cute.Tensor,
262
+ single_stage: bool = False,
263
+ **kwargs,
264
+ ) -> Callable:
265
+ # src_is_smem = const_expr(
266
+ # isinstance(src_tensor.iterator, cute.Pointer)
267
+ # and src_tensor.memspace == cute.AddressSpace.smem
268
+ # )
269
+ group_rank_src = const_expr(cute.rank(src_tensor) - (1 if not single_stage else 0))
270
+ group_rank_dst = const_expr(cute.rank(dst_tensor) - (1 if not single_stage else 0))
271
+ # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
272
+ src = cute.group_modes(src_tensor, 0, group_rank_src)
273
+ dst = cute.group_modes(dst_tensor, 0, group_rank_dst)
274
+
275
+ def copy_bulk(src_idx, dst_idx, **new_kwargs):
276
+ size = const_expr(cute.size(src.shape[:-1]) * src.element_type.width // 8)
277
+ cpasync_bulk_g2s(
278
+ src[None, src_idx].iterator,
279
+ dst[None, dst_idx].iterator,
280
+ size=size,
281
+ **new_kwargs,
282
+ **kwargs,
283
+ )
284
+
285
+ def copy_bulk_single_stage(**new_kwargs):
286
+ size = const_expr(cute.size(src.shape) * src.element_type.width // 8)
287
+ cpasync_bulk_g2s(src.iterator, dst.iterator, size=size, **new_kwargs, **kwargs)
288
+
289
+ return copy_bulk if const_expr(not single_stage) else copy_bulk_single_stage
290
+
291
+
292
+ def tma_get_copy_fn(
293
+ atom: cute.CopyAtom,
294
+ cta_coord: cute.Coord,
295
+ cta_layout: cute.Layout,
296
+ src_tensor: cute.Tensor,
297
+ dst_tensor: cute.Tensor,
298
+ filter_zeros: bool = False,
299
+ single_stage: bool = False,
300
+ **kwargs,
301
+ ) -> Callable:
302
+ src_is_smem = const_expr(
303
+ isinstance(src_tensor.iterator, cute.Pointer)
304
+ and src_tensor.memspace == cute.AddressSpace.smem
305
+ )
306
+ smem_tensor, gmem_tensor = (src_tensor, dst_tensor) if src_is_smem else (dst_tensor, src_tensor)
307
+ group_rank_smem = const_expr(cute.rank(smem_tensor) - (1 if not single_stage else 0))
308
+ group_rank_gmem = const_expr(cute.rank(gmem_tensor) - (1 if not single_stage else 0))
309
+ # ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
310
+ s, g = cpasync.tma_partition(
311
+ atom,
312
+ cta_coord,
313
+ cta_layout,
314
+ cute.group_modes(smem_tensor, 0, group_rank_smem),
315
+ cute.group_modes(gmem_tensor, 0, group_rank_gmem),
316
+ )
317
+ if const_expr(filter_zeros):
318
+ s = cute.filter_zeros(s)
319
+ g = cute.filter_zeros(g)
320
+ src, dst = (s, g) if src_is_smem else (g, s)
321
+
322
+ def copy_tma(src_idx, dst_idx, **new_kwargs):
323
+ cute.copy(atom, src[None, src_idx], dst[None, dst_idx], **new_kwargs, **kwargs)
324
+
325
+ def copy_tma_single_stage(**new_kwargs):
326
+ cute.copy(atom, src, dst, **new_kwargs, **kwargs)
327
+
328
+ return (copy_tma if const_expr(not single_stage) else copy_tma_single_stage), s, g
329
+
330
+
331
+ def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsync):
332
+ def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
333
+ copy(
334
+ src_idx=src_idx,
335
+ dst_idx=producer_state.index,
336
+ tma_bar_ptr=pipeline.producer_get_barrier(producer_state),
337
+ **new_kwargs,
338
+ )
339
+
340
+ return copy_fn
kernel/deps/flash_attn/cute/cute_dsl_utils.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import os
4
+ import pathlib
5
+ from typing import Tuple
6
+ from functools import partial, lru_cache
7
+ from dataclasses import dataclass, fields
8
+
9
+ import torch
10
+
11
+ try:
12
+ from triton.tools.disasm import extract
13
+ except ImportError:
14
+ extract = None
15
+
16
+ import cutlass
17
+ import cutlass.cute as cute
18
+ from cutlass.base_dsl.typing import JitArgument
19
+ from cutlass.cutlass_dsl import NumericMeta
20
+ from cutlass.cute.runtime import from_dlpack
21
+
22
+ StaticTypes = (cutlass.Constexpr, NumericMeta, int, bool, str, float, type(None))
23
+
24
+
25
+ load_cubin_module_data_og = cutlass.base_dsl.runtime.cuda.load_cubin_module_data
26
+ cute_compile_og = cute.compile
27
+
28
+
29
+ torch2cute_dtype_map = {
30
+ torch.float16: cutlass.Float16,
31
+ torch.bfloat16: cutlass.BFloat16,
32
+ torch.float32: cutlass.Float32,
33
+ }
34
+
35
+
36
+ @lru_cache
37
+ def get_max_active_clusters(cluster_size):
38
+ return cutlass.utils.HardwareInfo().get_max_active_clusters(cluster_size=cluster_size)
39
+
40
+
41
+ @lru_cache
42
+ def get_device_capacity(device: torch.device = None) -> Tuple[int, int]:
43
+ return torch.cuda.get_device_capability(device)
44
+
45
+
46
+ @dataclass
47
+ class ParamsBase:
48
+ def __extract_mlir_values__(self):
49
+ all_fields = [getattr(self, field.name) for field in fields(self)]
50
+ non_constexpr_fields = [f for f in all_fields if not isinstance(f, StaticTypes)]
51
+ values, self._values_pos = [], []
52
+ for obj in non_constexpr_fields:
53
+ obj_values = cutlass.extract_mlir_values(obj)
54
+ values += obj_values
55
+ self._values_pos.append(len(obj_values))
56
+ return values
57
+
58
+ def __new_from_mlir_values__(self, values):
59
+ all_fields = {field.name: getattr(self, field.name) for field in fields(self)}
60
+ constexpr_fields = {n: f for n, f in all_fields.items() if isinstance(f, StaticTypes)}
61
+ non_constexpr_fields = {
62
+ n: f for n, f in all_fields.items() if not isinstance(f, StaticTypes)
63
+ }
64
+ for (name, field), n_items in zip(non_constexpr_fields.items(), self._values_pos):
65
+ non_constexpr_fields[name] = cutlass.new_from_mlir_values(field, values[:n_items])
66
+ values = values[n_items:]
67
+ return self.__class__(**non_constexpr_fields, **constexpr_fields)
68
+
69
+
70
+ @dataclass
71
+ class ArgumentsBase(JitArgument):
72
+ def __c_pointers__(self):
73
+ all_fields = [getattr(self, field.name) for field in fields(self)]
74
+ non_constexpr_fields = [f for f in all_fields if not isinstance(f, StaticTypes)]
75
+ c_ptrs = []
76
+ for obj in non_constexpr_fields:
77
+ if hasattr(obj, "__c_pointers__"):
78
+ c_ptrs.extend(obj.__c_pointers__())
79
+ return c_ptrs
80
+
81
+ def __get_mlir_types__(self):
82
+ all_fields = [getattr(self, field.name) for field in fields(self)]
83
+ non_constexpr_fields = [f for f in all_fields if not isinstance(f, StaticTypes)]
84
+ types, self._values_pos = [], []
85
+ for obj in non_constexpr_fields:
86
+ if hasattr(obj, "__get_mlir_types__"):
87
+ obj_types = obj.__get_mlir_types__()
88
+ types.extend(obj_types)
89
+ self._values_pos.append(len(obj_types))
90
+ else:
91
+ self._values_pos.append(0)
92
+ return types
93
+
94
+ def __new_from_mlir_values__(self, values):
95
+ all_fields = {field.name: getattr(self, field.name) for field in fields(self)}
96
+ constexpr_fields = {n: f for n, f in all_fields.items() if isinstance(f, StaticTypes)}
97
+ non_constexpr_fields = {
98
+ n: f for n, f in all_fields.items() if not isinstance(f, StaticTypes)
99
+ }
100
+ for (name, field), n_items in zip(non_constexpr_fields.items(), self._values_pos):
101
+ non_constexpr_fields[name] = cutlass.new_from_mlir_values(field, values[:n_items])
102
+ values = values[n_items:]
103
+ return self.__class__(**non_constexpr_fields, **constexpr_fields)
104
+
105
+
106
+ def load_cubin_module_data_patched(cubin_data, filepath):
107
+ pathlib.Path(filepath).write_bytes(cubin_data)
108
+ return load_cubin_module_data_og(cubin_data)
109
+
110
+
111
+ def cute_compile_patched(*args, **kwargs):
112
+ """A patched version of cute.compile that dump the SASS to a file if CUTE_CUBIN_PATH is set."""
113
+ cubin_path = os.getenv("CUTE_CUBIN_PATH", None)
114
+ if cubin_path is not None:
115
+ cutlass.base_dsl.runtime.cuda.load_cubin_module_data = partial(
116
+ load_cubin_module_data_patched, filepath=cubin_path
117
+ )
118
+ output = cute_compile_og(*args, **kwargs)
119
+ if cubin_path is not None:
120
+ cutlass.base_dsl.runtime.cuda.load_cubin_module_data = load_cubin_module_data_og
121
+ if extract is not None:
122
+ sass = extract(cubin_path, None)
123
+ pathlib.Path(cubin_path).with_suffix(".annotated.sass").write_text(sass)
124
+ return output
125
+
126
+
127
+ def to_cute_tensor(t, assumed_align=16, leading_dim=-1, fully_dynamic=False, enable_tvm_ffi=True):
128
+ """Convert torch tensor to cute tensor for TVM FFI. leading_dim=-1 defaults to t.ndim-1."""
129
+ tensor = from_dlpack(t.detach(), assumed_align=assumed_align, enable_tvm_ffi=enable_tvm_ffi)
130
+ if fully_dynamic:
131
+ return tensor.mark_layout_dynamic()
132
+ if leading_dim == -1:
133
+ leading_dim = t.ndim - 1
134
+ return tensor.mark_layout_dynamic(leading_dim=leading_dim)
135
+
136
+
137
+ def get_broadcast_dims(tensor: torch.Tensor) -> Tuple[bool, ...]:
138
+ """Return tuple of bools indicating which dims have stride=0 (broadcast).
139
+
140
+ This is useful for compile keys since CuTe's mark_layout_dynamic() keeps
141
+ stride=0 as static, meaning kernels compiled with different broadcast
142
+ patterns are not interchangeable.
143
+ """
144
+ return tuple(s == 0 for s in tensor.stride())
kernel/deps/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
kernel/deps/flash_attn/cute/flash_bwd.py ADDED
@@ -0,0 +1,1262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/mainloop_bwd_sm80.hpp
3
+ # from Cutlass C++ to Cute-DSL.
4
+ import math
5
+ from types import SimpleNamespace
6
+ from typing import Type, Callable, Optional
7
+ from functools import partial
8
+
9
+ import cuda.bindings.driver as cuda
10
+
11
+ import cutlass
12
+ import cutlass.cute as cute
13
+ from cutlass.cute.nvgpu import cpasync, warp
14
+ from cutlass import Float32, Int32
15
+ import cutlass.utils as utils_basic
16
+
17
+ from flash_attn.cute import ampere_helpers as sm80_utils
18
+ from flash_attn.cute import utils
19
+ from flash_attn.cute.mask import AttentionMask
20
+ from flash_attn.cute.seqlen_info import SeqlenInfoQK
21
+ from flash_attn.cute.tile_scheduler import ParamsBase, SingleTileScheduler, SingleTileVarlenScheduler, TileSchedulerArguments
22
+
23
+
24
+ class FlashAttentionBackwardSm80:
25
+ def __init__(
26
+ self,
27
+ dtype: Type[cutlass.Numeric],
28
+ head_dim: int,
29
+ head_dim_v: Optional[int] = None,
30
+ qhead_per_kvhead: int = 1,
31
+ m_block_size: int = 64,
32
+ n_block_size: int = 128,
33
+ num_stages_Q: int = 2,
34
+ num_stages_dO: int = 2,
35
+ num_threads: int = 256,
36
+ pack_gqa: bool = False,
37
+ is_causal: bool = False,
38
+ SdP_swapAB: bool = False,
39
+ dKV_swapAB: bool = False,
40
+ dQ_swapAB: bool = False,
41
+ AtomLayoutMSdP: int = 1,
42
+ AtomLayoutNdKV: int = 8,
43
+ AtomLayoutMdQ: int = 1,
44
+ V_in_regs: bool = False,
45
+ ):
46
+ """Initializes the configuration for a flash attention v2 kernel.
47
+
48
+ All contiguous dimensions must be at least 16 bytes aligned which indicates the head dimension
49
+ should be a multiple of 8.
50
+
51
+ :param head_dim: head dimension
52
+ :type head_dim: int
53
+ :param m_block_size: m block size
54
+ :type m_block_size: int
55
+ :param n_block_size: n block size
56
+ :type n_block_size: int
57
+ :param num_threads: number of threads
58
+ :type num_threads: int
59
+ :param is_causal: is causal
60
+ """
61
+ self.dtype = dtype
62
+ # padding head_dim to a multiple of 16 as k_block_size
63
+ hdim_multiple_of = 32
64
+ self.head_dim_padded = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
65
+ head_dim_v = head_dim_v if head_dim_v is not None else head_dim
66
+ self.same_hdim_kv = head_dim == head_dim_v
67
+ self.head_dim_v_padded = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
68
+ # Can save registers (and hence be faster) if we don't have to check hdim predication
69
+ self.check_hdim_oob = head_dim != self.head_dim_padded
70
+ self.check_hdim_v_oob = head_dim_v != self.head_dim_v_padded
71
+ self.qhead_per_kvhead = qhead_per_kvhead
72
+ self.m_block_size = m_block_size
73
+ self.n_block_size = n_block_size
74
+ self.num_threads = num_threads
75
+ self.pack_gqa = pack_gqa
76
+ self.is_causal = is_causal
77
+ self.num_stages_Q = num_stages_Q
78
+ self.num_stages_dO = num_stages_dO
79
+ self.SdP_swapAB = SdP_swapAB
80
+ self.dKV_swapAB = dKV_swapAB
81
+ self.dQ_swapAB = dQ_swapAB
82
+ self.AtomLayoutMSdP = AtomLayoutMSdP
83
+ self.AtomLayoutNdKV = AtomLayoutNdKV
84
+ self.AtomLayoutMdQ = AtomLayoutMdQ
85
+ num_mma_warps = self.num_threads // cute.arch.WARP_SIZE
86
+ self.Mma_dKV_is_RS = AtomLayoutMSdP == 1 and AtomLayoutNdKV == num_mma_warps and SdP_swapAB and not dKV_swapAB
87
+ self.V_in_regs = V_in_regs
88
+ self.share_QV_smem = V_in_regs
89
+
90
+ @staticmethod
91
+ def can_implement(
92
+ dtype, head_dim, head_dim_v, m_block_size, n_block_size, num_stages_Q, num_stages_dO,
93
+ num_threads, is_causal,
94
+ V_in_regs=False
95
+ ) -> bool:
96
+ """Check if the kernel can be implemented with the given parameters.
97
+
98
+ :param dtype: data type
99
+ :type dtype: cutlass.Numeric
100
+ :param head_dim: head dimension
101
+ :type head_dim: int
102
+ :param m_block_size: m block size
103
+ :type m_block_size: int
104
+ :param n_block_size: n block size
105
+ :type n_block_size: int
106
+ :param num_threads: number of threads
107
+ :type num_threads: int
108
+ :param is_causal: is causal
109
+ :type is_causal: bool
110
+
111
+ :return: True if the kernel can be implemented, False otherwise
112
+ :rtype: bool
113
+ """
114
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
115
+ return False
116
+ if head_dim % 8 != 0:
117
+ return False
118
+ if head_dim_v % 8 != 0:
119
+ return False
120
+ if n_block_size % 16 != 0:
121
+ return False
122
+ if num_threads % 32 != 0:
123
+ return False
124
+ # Check if block size setting is out of shared memory capacity
125
+ # Shared memory usage: Q tile + (K tile + V tile) where K and V use the same tile size
126
+ smem_usage_Q = m_block_size * head_dim * num_stages_Q * 2
127
+ smem_usage_dO = m_block_size * head_dim_v * num_stages_dO * 2
128
+ smem_usage_K = n_block_size * head_dim * 2
129
+ smem_usage_V = n_block_size * head_dim_v * 2
130
+ smem_usage_QV = (smem_usage_Q + smem_usage_V) if not V_in_regs else max(smem_usage_Q, smem_usage_V)
131
+ smem_usage = smem_usage_QV + smem_usage_dO + smem_usage_K
132
+ smem_capacity = utils_basic.get_smem_capacity_in_bytes("sm_80")
133
+ if smem_usage > smem_capacity:
134
+ return False
135
+ return True
136
+
137
+ def _check_type(
138
+ self,
139
+ mQ_type: Type[cutlass.Numeric],
140
+ mK_type: Type[cutlass.Numeric],
141
+ mV_type: Type[cutlass.Numeric],
142
+ mdO_type: Type[cutlass.Numeric],
143
+ mLSE_type: Type[cutlass.Numeric],
144
+ mdPsum_type: Type[cutlass.Numeric],
145
+ mdQaccum_type: Type[cutlass.Numeric],
146
+ mdK_type: Type[cutlass.Numeric],
147
+ mdV_type: Type[cutlass.Numeric],
148
+ mCuSeqlensQ_type: Type[cutlass.Numeric] | None,
149
+ mCuSeqlensK_type: Type[cutlass.Numeric] | None,
150
+ mSeqUsedQ_type: Type[cutlass.Numeric] | None,
151
+ mSeqUsedK_type: Type[cutlass.Numeric] | None,
152
+ ):
153
+ if cutlass.const_expr(not (mQ_type == mK_type == mV_type == mdO_type)):
154
+ raise TypeError("All tensors must have the same data type")
155
+ if cutlass.const_expr(self.qhead_per_kvhead == 1):
156
+ if cutlass.const_expr(not (mdK_type == mdV_type == mQ_type)):
157
+ raise TypeError("mdK and mdV tensors must have the same data type as mQ")
158
+ else:
159
+ if cutlass.const_expr(not (mdK_type == mdV_type == cutlass.Float32)):
160
+ raise TypeError("mdKaccum and mdVaccum tensors must have the data type Float32")
161
+ if cutlass.const_expr(not mQ_type in [cutlass.Float16, cutlass.BFloat16]):
162
+ raise TypeError("Only Float16 or BFloat16 is supported")
163
+ if cutlass.const_expr(not mLSE_type in [cutlass.Float32]):
164
+ raise TypeError("LSE tensor must be Float32")
165
+ if cutlass.const_expr(not mdPsum_type in [cutlass.Float32]):
166
+ raise TypeError("dPsum tensor must be Float32")
167
+ if cutlass.const_expr(not mdQaccum_type in [cutlass.Float32]):
168
+ raise TypeError("dQaccum tensor must be Float32")
169
+ if cutlass.const_expr(mCuSeqlensQ_type not in [None, cutlass.Int32]):
170
+ raise TypeError("cuSeqlensQ tensor must be Int32")
171
+ if cutlass.const_expr(mCuSeqlensK_type not in [None, cutlass.Int32]):
172
+ raise TypeError("cuSeqlensK tensor must be Int32")
173
+ if cutlass.const_expr(mSeqUsedQ_type not in [None, cutlass.Int32]):
174
+ raise TypeError("SeqUsedQ tensor must be Int32")
175
+ if cutlass.const_expr(mSeqUsedK_type not in [None, cutlass.Int32]):
176
+ raise TypeError("SeqUsedK tensor must be Int32")
177
+ assert mQ_type == self.dtype
178
+
179
+ def _setup_attributes(self):
180
+ # ///////////////////////////////////////////////////////////////////////////////
181
+ # Shared memory layout: Q/K/V
182
+ # ///////////////////////////////////////////////////////////////////////////////
183
+ sQ_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.head_dim_padded)
184
+ self.sQ_layout = cute.tile_to_shape(
185
+ sQ_layout_atom, (self.m_block_size, self.head_dim_padded, self.num_stages_Q), (0, 1, 2),
186
+ )
187
+ sK_layout_atom = sQ_layout_atom
188
+ self.sK_layout = cute.tile_to_shape(
189
+ sK_layout_atom, (self.n_block_size, self.head_dim_padded), (0, 1),
190
+ )
191
+ sV_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.head_dim_v_padded)
192
+ self.sV_layout = cute.tile_to_shape(
193
+ sV_layout_atom, (self.n_block_size, self.head_dim_v_padded), (0, 1),
194
+ )
195
+ sdO_layout_atom = sV_layout_atom
196
+ self.sdO_layout = cute.tile_to_shape(
197
+ sdO_layout_atom, (self.m_block_size, self.head_dim_v_padded, self.num_stages_dO), (0, 1, 2),
198
+ )
199
+ # TODO: do we set swizzle to be 3 here explicitly?
200
+ sPdS_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.n_block_size)
201
+ self.sPdS_layout = cute.tile_to_shape(
202
+ sPdS_layout_atom, (self.m_block_size, self.n_block_size), (0, 1),
203
+ )
204
+ # We set stride to be multiple of 64 so that if ShuffleLSE, even if threads read from sLSE but out of bounds,
205
+ # it's still a valid smem address.
206
+ self.sLSE_layout = cute.make_layout(
207
+ (self.m_block_size, self.num_stages_Q),
208
+ stride=(1, cute.round_up(self.m_block_size, 64)),
209
+ )
210
+ sLSEMma_layout = cute.make_layout(
211
+ (self.m_block_size, self.n_block_size, self.num_stages_Q),
212
+ stride=(1, 0, cute.round_up(self.m_block_size, 64)),
213
+ )
214
+ sLSEMma_layout_transposed = cute.make_layout(
215
+ (self.n_block_size, self.m_block_size, self.num_stages_Q),
216
+ stride=(0, 1, cute.round_up(self.m_block_size, 64)),
217
+ )
218
+ self.sLSEMma_layout = sLSEMma_layout if not self.SdP_swapAB else sLSEMma_layout_transposed
219
+
220
+ # ///////////////////////////////////////////////////////////////////////////////
221
+ # GMEM Tiled copy:
222
+ # ///////////////////////////////////////////////////////////////////////////////
223
+ # Thread layouts for copies
224
+ universal_copy_bits = 128
225
+ async_copy_elems = universal_copy_bits // self.dtype.width
226
+ # atom_async_copy: async copy atom for QKV load
227
+ atom_async_copy = cute.make_copy_atom(
228
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
229
+ self.dtype,
230
+ num_bits_per_copy=universal_copy_bits,
231
+ )
232
+ # atom_universal_copy: universal copy atom for O store
233
+ atom_universal_copy = cute.make_copy_atom(
234
+ cute.nvgpu.CopyUniversalOp(), self.dtype, num_bits_per_copy=universal_copy_bits,
235
+ )
236
+ # tQK_layout: thread layout for QK load
237
+ tQK_shape_dim_1 = sQ_layout_atom.outer.shape[1] // async_copy_elems
238
+ assert self.num_threads % tQK_shape_dim_1 == 0, "num_threads must be divisible by tQK_shape_dim_1"
239
+ tQK_layout = cute.make_ordered_layout(
240
+ (self.num_threads // tQK_shape_dim_1, tQK_shape_dim_1), order=(1, 0),
241
+ )
242
+ # Do we need to check if we overshot kBlockM when we load Q?
243
+ self.is_even_m_smem_q = self.m_block_size % tQK_layout.shape[0] == 0
244
+ # Do we need to check if we overshot kBlockN when we load K?
245
+ self.is_even_n_smem_k = self.n_block_size % tQK_layout.shape[0] == 0
246
+ tVdO_shape_dim_1 = sV_layout_atom.outer.shape[1] // async_copy_elems
247
+ assert self.num_threads % tVdO_shape_dim_1 == 0, "num_threads must be divisible by tVdO_shape_dim_1"
248
+ tVdO_layout = cute.make_ordered_layout(
249
+ (self.num_threads // tVdO_shape_dim_1, tVdO_shape_dim_1), order=(1, 0),
250
+ )
251
+ # Do we need to check if we overshot kBlockN when we load V?
252
+ self.is_even_n_smem_v = self.n_block_size % tVdO_layout.shape[0] == 0
253
+ self.is_even_m_smem_do = self.m_block_size % tVdO_layout.shape[0] == 0
254
+
255
+ # Value layouts for copies
256
+ vQKVdO_layout = cute.make_layout((1, async_copy_elems))
257
+
258
+ # gmem_tiled_copy_QK: tiled copy for QK load
259
+ self.gmem_tiled_copy_QK = cute.make_tiled_copy_tv(atom_async_copy, tQK_layout, vQKVdO_layout)
260
+ self.gmem_tiled_copy_VdO = cute.make_tiled_copy_tv(atom_async_copy, tVdO_layout, vQKVdO_layout)
261
+ self.gmem_tiled_copy_dK = cute.make_tiled_copy_tv(atom_universal_copy, tQK_layout, vQKVdO_layout)
262
+ self.gmem_tiled_copy_dV = cute.make_tiled_copy_tv(atom_universal_copy, tVdO_layout, vQKVdO_layout)
263
+ async_copy_elems_accum = universal_copy_bits // cutlass.Float32.width
264
+
265
+ # I think we wouldn't require this with smarter padding
266
+ if cutlass.const_expr(not self.varlen_q):
267
+ async_copy_elems_accum = universal_copy_bits // cutlass.Float32.width
268
+ atom_async_copy_accum = cute.make_copy_atom(
269
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
270
+ cutlass.Float32,
271
+ num_bits_per_copy=universal_copy_bits,
272
+ )
273
+ else:
274
+ async_copy_elems_accum = 1
275
+ atom_async_copy_accum = cute.make_copy_atom(
276
+ cute.nvgpu.CopyUniversalOp(),
277
+ cutlass.Float32,
278
+ num_bits_per_copy=cutlass.Float32.width,
279
+ )
280
+ self.gmem_tiled_copy_LSE = cute.make_tiled_copy_tv(
281
+ atom_async_copy_accum,
282
+ cute.make_layout(self.num_threads),
283
+ cute.make_layout(async_copy_elems_accum),
284
+ )
285
+ self.gmem_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
286
+ cute.make_copy_atom(
287
+ cute.nvgpu.CopyUniversalOp(), cutlass.Float32, num_bits_per_copy=cutlass.Float32.width
288
+ ),
289
+ cute.make_layout(self.num_threads),
290
+ cute.make_layout(1)
291
+ )
292
+ if cutlass.const_expr(self.qhead_per_kvhead > 1):
293
+ self.gmem_tiled_copy_dK = self.gmem_tiled_copy_dQaccum
294
+ self.gmem_tiled_copy_dV = self.gmem_tiled_copy_dQaccum
295
+
296
+ def _get_tiled_mma(self):
297
+ num_mma_warps = self.num_threads // 32
298
+ AtomLayoutSdP = (self.AtomLayoutMSdP, num_mma_warps // self.AtomLayoutMSdP, 1) if cutlass.const_expr(not self.SdP_swapAB) else (num_mma_warps // self.AtomLayoutMSdP, self.AtomLayoutMSdP, 1)
299
+ tiled_mma_sdp = cute.make_tiled_mma(
300
+ warp.MmaF16BF16Op(self.dtype, cutlass.Float32, (16, 8, 16)),
301
+ AtomLayoutSdP,
302
+ permutation_mnk=(AtomLayoutSdP[0] * 16, AtomLayoutSdP[1] * 16, 16),
303
+ )
304
+ AtomLayoutdKV = (self.AtomLayoutNdKV, num_mma_warps // self.AtomLayoutNdKV, 1) if cutlass.const_expr(not self.dKV_swapAB) else (num_mma_warps // self.AtomLayoutNdKV, self.AtomLayoutNdKV, 1)
305
+ tiled_mma_dkv = cute.make_tiled_mma(
306
+ warp.MmaF16BF16Op(self.dtype, cutlass.Float32, (16, 8, 16)),
307
+ AtomLayoutdKV,
308
+ permutation_mnk=(AtomLayoutdKV[0] * 16, AtomLayoutdKV[1] * 16, 16),
309
+ )
310
+ AtomLayoutdQ = (self.AtomLayoutMdQ, num_mma_warps // self.AtomLayoutMdQ, 1) if cutlass.const_expr(not self.dQ_swapAB) else (num_mma_warps // self.AtomLayoutMdQ, self.AtomLayoutMdQ, 1)
311
+ tiled_mma_dq = cute.make_tiled_mma(
312
+ warp.MmaF16BF16Op(self.dtype, cutlass.Float32, (16, 8, 16)),
313
+ AtomLayoutdQ,
314
+ permutation_mnk=(AtomLayoutdQ[0] * 16, AtomLayoutdQ[1] * 16, 16),
315
+ )
316
+ return tiled_mma_sdp, tiled_mma_dkv, tiled_mma_dq
317
+
318
+ def _get_shared_storage_cls(self):
319
+ sQ_struct, sK_struct, sV_struct, sdO_struct = [
320
+ cute.struct.Align[cute.struct.MemRange[self.dtype, cute.cosize(layout)], 1024]
321
+ for layout in (self.sQ_layout, self.sK_layout, self.sV_layout, self.sdO_layout)
322
+ ]
323
+ cosize_sQV = max(cute.cosize(self.sQ_layout), cute.cosize(self.sV_layout))
324
+ sQV_struct = cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sQV], 1024]
325
+ sLSE_struct, sdPsum_struct = [
326
+ cute.struct.Align[cute.struct.MemRange[cutlass.Float32, cute.cosize(layout)], 128]
327
+ for layout in (self.sLSE_layout, self.sLSE_layout)
328
+ ]
329
+ sP_struct, sdS_struct = [
330
+ cute.struct.Align[cute.struct.MemRange[self.dtype, cute.cosize(layout)], 128]
331
+ for layout in (self.sPdS_layout, self.sPdS_layout)
332
+ ]
333
+
334
+ @cute.struct
335
+ class SharedStorageSeparateQV:
336
+ sK: sK_struct
337
+ sV: sV_struct
338
+ sQ: sQ_struct
339
+ sdO: sdO_struct
340
+ sLSE: sLSE_struct
341
+ sdPsum: sdPsum_struct
342
+ sP: sP_struct
343
+ sdS: sdS_struct
344
+ # TODO: the case where there's no sP
345
+
346
+ @cute.struct
347
+ class SharedStorageSharedQV:
348
+ sK: sK_struct
349
+ sV: sV_struct
350
+ sQ: sQV_struct
351
+ sdO: sdO_struct
352
+ sLSE: sLSE_struct
353
+ sdPsum: sdPsum_struct
354
+ sP: sP_struct
355
+ sdS: sdS_struct
356
+
357
+ return SharedStorageSeparateQV if cutlass.const_expr(not self.share_QV_smem) else SharedStorageSharedQV
358
+
359
+ @cute.jit
360
+ def __call__(
361
+ self,
362
+ mQ: cute.Tensor,
363
+ mK: cute.Tensor,
364
+ mV: cute.Tensor,
365
+ mdO: cute.Tensor,
366
+ mLSE: cute.Tensor,
367
+ mdPsum: cute.Tensor,
368
+ mdQaccum: cute.Tensor,
369
+ mdK: cute.Tensor,
370
+ mdV: cute.Tensor,
371
+ softmax_scale: cutlass.Float32,
372
+ stream: cuda.CUstream,
373
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
374
+ mCuSeqlensK: Optional[cute.Tensor] = None,
375
+ mSeqUsedQ: Optional[cute.Tensor] = None,
376
+ mSeqUsedK: Optional[cute.Tensor] = None,
377
+ softcap: Float32 | float | None = None,
378
+ window_size_left: Int32 | int | None = None,
379
+ window_size_right: Int32 | int | None = None,
380
+ mdQ_semaphore: Optional[cute.Tensor] = None,
381
+ ):
382
+ assert mdQ_semaphore is None, "semaphore not supported yet"
383
+ # Get the data type and check if it is fp16 or bf16
384
+ self._check_type(*(t.element_type if t is not None else None
385
+ for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV, mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK)))
386
+ # Assume all strides are divisible by 128 bits except the last stride
387
+ # Skip cute.assume() for stride=0 (broadcast dims from expand() are Python ints)
388
+ new_stride = lambda t: (*(cute.assume(s, divby=128 // t.element_type.width) if not isinstance(s, int) or s != 0 else s for s in t.stride[:-1]), t.stride[-1])
389
+ mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV = [cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t))) if t is not None else None for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV)]
390
+ self.varlen_q = (mCuSeqlensQ is not None)
391
+ self._setup_attributes()
392
+ SharedStorage = self._get_shared_storage_cls()
393
+ tiled_mma_sdp, tiled_mma_dkv, tiled_mma_dq = self._get_tiled_mma()
394
+
395
+ num_head = mQ.shape[1] if cutlass.const_expr(mCuSeqlensQ is not None) else mQ.shape[2]
396
+
397
+ if cutlass.const_expr(mCuSeqlensK is not None):
398
+ TileScheduler = SingleTileVarlenScheduler
399
+ num_batch = mCuSeqlensK.shape[0] - 1
400
+ else:
401
+ TileScheduler = SingleTileScheduler
402
+ num_batch = mK.shape[0]
403
+
404
+ # Uses seqlen k, etc. since main bwd kernel's blocks are over n
405
+ tile_sched_args = TileSchedulerArguments(
406
+ num_block=cute.ceil_div(mK.shape[1], self.n_block_size),
407
+ num_head=num_head,
408
+ num_batch=num_batch,
409
+ num_splits=1,
410
+ seqlen_k=0,
411
+ headdim=mK.shape[2],
412
+ headdim_v=mV.shape[2],
413
+ total_q=mK.shape[0],
414
+ tile_shape_mn=(self.n_block_size, self.m_block_size),
415
+ qhead_per_kvhead_packgqa=self.qhead_per_kvhead if cutlass.const_expr(self.pack_gqa) else 1,
416
+ mCuSeqlensQ=mCuSeqlensK,
417
+ mSeqUsedQ=mSeqUsedK,
418
+ )
419
+
420
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
421
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
422
+
423
+ softmax_scale_log2 = softmax_scale * math.log2(math.e)
424
+ self.kernel(
425
+ mQ,
426
+ mK,
427
+ mV,
428
+ mdO,
429
+ mLSE,
430
+ mdPsum,
431
+ mdQaccum,
432
+ mdK,
433
+ mdV,
434
+ mCuSeqlensQ,
435
+ mCuSeqlensK,
436
+ mSeqUsedQ,
437
+ mSeqUsedK,
438
+ softmax_scale,
439
+ softmax_scale_log2,
440
+ self.sQ_layout,
441
+ self.sK_layout,
442
+ self.sV_layout,
443
+ self.sdO_layout,
444
+ self.sPdS_layout,
445
+ self.sLSE_layout,
446
+ self.sLSEMma_layout,
447
+ self.gmem_tiled_copy_QK,
448
+ self.gmem_tiled_copy_VdO,
449
+ self.gmem_tiled_copy_dK,
450
+ self.gmem_tiled_copy_dV,
451
+ self.gmem_tiled_copy_LSE,
452
+ self.gmem_tiled_copy_dQaccum,
453
+ tiled_mma_sdp,
454
+ tiled_mma_dkv,
455
+ tiled_mma_dq,
456
+ SharedStorage,
457
+ tile_sched_params,
458
+ TileScheduler,
459
+ ).launch(
460
+ grid=grid_dim,
461
+ block=[self.num_threads, 1, 1],
462
+ smem=SharedStorage.size_in_bytes(),
463
+ stream=stream,
464
+ )
465
+
466
+ @cute.kernel
467
+ def kernel(
468
+ self,
469
+ mQ: cute.Tensor,
470
+ mK: cute.Tensor,
471
+ mV: cute.Tensor,
472
+ mdO: cute.Tensor,
473
+ mLSE: cute.Tensor,
474
+ mdPsum: cute.Tensor,
475
+ mdQaccum: cute.Tensor,
476
+ mdK: cute.Tensor,
477
+ mdV: cute.Tensor,
478
+ mCuSeqlensQ: Optional[cute.Tensor],
479
+ mCuSeqlensK: Optional[cute.Tensor],
480
+ mSeqUsedQ: Optional[cute.Tensor],
481
+ mSeqUsedK: Optional[cute.Tensor],
482
+ softmax_scale: cutlass.Float32,
483
+ softmax_scale_log2: cutlass.Float32,
484
+ sQ_layout: cute.ComposedLayout,
485
+ sK_layout: cute.ComposedLayout,
486
+ sV_layout: cute.ComposedLayout,
487
+ sdO_layout: cute.ComposedLayout,
488
+ sPdS_layout: cute.ComposedLayout,
489
+ sLSE_layout: cute.Layout,
490
+ sLSEMma_layout: cute.Layout,
491
+ gmem_tiled_copy_QK: cute.TiledCopy,
492
+ gmem_tiled_copy_VdO: cute.TiledCopy,
493
+ gmem_tiled_copy_dK: cute.TiledCopy,
494
+ gmem_tiled_copy_dV: cute.TiledCopy,
495
+ gmem_tiled_copy_LSE: cute.TiledCopy,
496
+ gmem_tiled_copy_dQaccum: cute.TiledCopy,
497
+ tiled_mma_sdp: cute.TiledMma,
498
+ tiled_mma_dkv: cute.TiledMma,
499
+ tiled_mma_dq: cute.TiledMma,
500
+ SharedStorage: cutlass.Constexpr,
501
+ tile_sched_params: ParamsBase,
502
+ TileScheduler: cutlass.Constexpr[Callable],
503
+ ):
504
+ # Thread index, block index
505
+ tidx, _, _ = cute.arch.thread_idx()
506
+
507
+ tile_scheduler = TileScheduler.create(tile_sched_params)
508
+ work_tile = tile_scheduler.initial_work_tile_info()
509
+
510
+ n_block, head_idx, batch_idx, _ = work_tile.tile_idx
511
+
512
+ if work_tile.is_valid_tile:
513
+ seqlen = SeqlenInfoQK.create(batch_idx, mQ.shape[1], mK.shape[1], mCuSeqlensQ=mCuSeqlensQ, mCuSeqlensK=mCuSeqlensK, mSeqUsedQ=mSeqUsedQ, mSeqUsedK=mSeqUsedK)
514
+
515
+ m_block_max = cute.ceil_div(seqlen.seqlen_q, self.m_block_size)
516
+ m_block_min = 0
517
+ if cutlass.const_expr(self.is_causal):
518
+ m_block_min = max(
519
+ (n_block * self.n_block_size + seqlen.seqlen_q - seqlen.seqlen_k) // self.m_block_size,
520
+ m_block_min,
521
+ )
522
+ # TODO: return early if m_block_max == 0
523
+
524
+ # ///////////////////////////////////////////////////////////////////////////////
525
+ # Get the appropriate tiles for this thread block.
526
+ # ///////////////////////////////////////////////////////////////////////////////
527
+ blkQ_shape = (self.m_block_size, self.head_dim_padded)
528
+ blkK_shape = (self.n_block_size, self.head_dim_padded)
529
+ blkV_shape = (self.n_block_size, self.head_dim_v_padded)
530
+ blkdO_shape = (self.m_block_size, self.head_dim_v_padded)
531
+
532
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
533
+ mQ_cur = mQ[batch_idx, None, head_idx, None]
534
+ mLSE_cur = mLSE[batch_idx, head_idx, None]
535
+ mdO_cur = mdO[batch_idx, None, head_idx, None]
536
+ mdPsum_cur = mdPsum[batch_idx, head_idx, None]
537
+ mdQaccum_cur = mdQaccum[batch_idx, head_idx, None]
538
+ else:
539
+ padded_offset_q = seqlen.offset_q + batch_idx * self.m_block_size
540
+ mQ_cur = cute.domain_offset((seqlen.offset_q, 0), mQ[None, head_idx, None])
541
+ mLSE_cur = cute.domain_offset((padded_offset_q,), mLSE[head_idx, None])
542
+ mdO_cur = cute.domain_offset((seqlen.offset_q, 0), mdO[None, head_idx, None])
543
+ mdPsum_cur = cute.domain_offset((padded_offset_q,), mdPsum[head_idx, None])
544
+ mdQaccum_cur = cute.domain_offset((padded_offset_q * self.head_dim_padded,), mdQaccum[head_idx, None])
545
+ head_idx_kv = head_idx // self.qhead_per_kvhead if cutlass.const_expr(not self.pack_gqa) else head_idx
546
+
547
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_k):
548
+ mK_cur, mV_cur = [t[batch_idx, None, head_idx_kv, None] for t in (mK, mV)]
549
+ else:
550
+ mK_cur, mV_cur = [cute.domain_offset((seqlen.offset_k, 0), t[None, head_idx_kv, None]) for t in (mK, mV)]
551
+
552
+ # (m_block_size, head_dim, m_block)
553
+ gQ = cute.local_tile(mQ_cur, blkQ_shape, (None, 0))
554
+ # (n_block_size, head_dim)
555
+ gK = cute.local_tile(mK_cur, blkK_shape, (n_block, 0))
556
+ # (n_block_size, head_dim_v)
557
+ gV = cute.local_tile(mV_cur, blkV_shape, (n_block, 0))
558
+ # (m_block_size, head_dim_v, m_block)
559
+ gdO = cute.local_tile(mdO_cur, blkdO_shape, (None, 0))
560
+ gLSE = cute.local_tile(mLSE_cur, (self.m_block_size,), (None,))
561
+ gdPsum = cute.local_tile(mdPsum_cur, (self.m_block_size,), (None,))
562
+ gdQaccum = cute.local_tile(mdQaccum_cur, (self.m_block_size * self.head_dim_padded,), (None,))
563
+
564
+ # ///////////////////////////////////////////////////////////////////////////////
565
+ # Get shared memory buffer
566
+ # ///////////////////////////////////////////////////////////////////////////////
567
+ smem = cutlass.utils.SmemAllocator()
568
+ storage = smem.allocate(SharedStorage)
569
+ sQ = storage.sQ.get_tensor(sQ_layout)
570
+ sK = storage.sK.get_tensor(sK_layout)
571
+ if cutlass.const_expr(not self.share_QV_smem):
572
+ sV = storage.sV.get_tensor(sV_layout)
573
+ else:
574
+ sV = cute.make_tensor(cute.recast_ptr(sQ.iterator, dtype=self.dtype), sV_layout)
575
+ sdO = storage.sdO.get_tensor(sdO_layout)
576
+ sP = storage.sP.get_tensor(sPdS_layout)
577
+ sdS = storage.sdS.get_tensor(sPdS_layout)
578
+ sLSE = storage.sLSE.get_tensor(sLSE_layout)
579
+ sdPsum = storage.sdPsum.get_tensor(sLSE_layout)
580
+ sLSEMma = storage.sLSE.get_tensor(sLSEMma_layout)
581
+ sdPsumMma = storage.sdPsum.get_tensor(sLSEMma_layout)
582
+
583
+ # Transpose view of tensors for tiled mma
584
+ sQt, sdOt, sKt, sPt, sdSt = [utils.transpose_view(t) for t in (sQ, sdO, sK, sP, sdS)]
585
+
586
+ gmem_thr_copy_QK = gmem_tiled_copy_QK.get_slice(tidx)
587
+ gmem_thr_copy_VdO = gmem_tiled_copy_VdO.get_slice(tidx)
588
+ gmem_thr_copy_lse = gmem_tiled_copy_LSE.get_slice(tidx)
589
+ gmem_thr_copy_dQaccum = gmem_tiled_copy_dQaccum.get_slice(tidx)
590
+ # (CPY_Atom, CPY_M, CPY_K, m_block)
591
+ tQgQ = gmem_thr_copy_QK.partition_S(gQ)
592
+ tQsQ = gmem_thr_copy_QK.partition_D(sQ)
593
+ # (CPY_Atom, CPY_N, CPY_K)
594
+ tKgK = gmem_thr_copy_QK.partition_S(gK)
595
+ tKsK = gmem_thr_copy_QK.partition_D(sK)
596
+ # (CPY_Atom, CPY_N, CPY_K)
597
+ tVgV = gmem_thr_copy_VdO.partition_S(gV)
598
+ tVsV = gmem_thr_copy_VdO.partition_D(sV)
599
+ # (CPY_Atom, CPY_M, CPY_K, m_block)
600
+ tdOgdO = gmem_thr_copy_VdO.partition_S(gdO)
601
+ tdOsdO = gmem_thr_copy_VdO.partition_D(sdO)
602
+ tLSEgLSE = gmem_thr_copy_lse.partition_S(gLSE)
603
+ tLSEsLSE = gmem_thr_copy_lse.partition_D(sLSE)
604
+ tLSEgdPsum = gmem_thr_copy_lse.partition_S(gdPsum)
605
+ tLSEsdPsum = gmem_thr_copy_lse.partition_D(sdPsum)
606
+ tdQgdQaccum = gmem_thr_copy_dQaccum.partition_S(gdQaccum)
607
+
608
+ # ///////////////////////////////////////////////////////////////////////////////
609
+ # Tile MMA compute thread partitions and allocate accumulators
610
+ # ///////////////////////////////////////////////////////////////////////////////
611
+ thr_mma_sdp = tiled_mma_sdp.get_slice(tidx)
612
+ thr_mma_dkv = tiled_mma_dkv.get_slice(tidx)
613
+ thr_mma_dq = tiled_mma_dq.get_slice(tidx)
614
+ acc_shape_dK = thr_mma_dkv.partition_shape_C((self.n_block_size, self.head_dim_padded))
615
+ acc_shape_dV = thr_mma_dkv.partition_shape_C((self.n_block_size, self.head_dim_v_padded))
616
+ acc_dK = cute.make_fragment(acc_shape_dK, cutlass.Float32)
617
+ acc_dV = cute.make_fragment(acc_shape_dV, cutlass.Float32)
618
+ acc_dK.fill(0.0)
619
+ acc_dV.fill(0.0)
620
+
621
+ tSrQ = utils.mma_make_fragment_A(sQ[None, None, 0], thr_mma_sdp, swapAB=self.SdP_swapAB)
622
+ tSrK = utils.mma_make_fragment_B(sK, thr_mma_sdp, swapAB=self.SdP_swapAB)
623
+ tdPrdO = utils.mma_make_fragment_A(sdO[None, None, 0], thr_mma_sdp, swapAB=self.SdP_swapAB)
624
+ tdPrV = utils.mma_make_fragment_B(sV, thr_mma_sdp, swapAB=self.SdP_swapAB)
625
+ tdVrP = utils.mma_make_fragment_A(sPt, thr_mma_dkv, swapAB=self.dKV_swapAB)
626
+ tdVrdO = utils.mma_make_fragment_B(sdOt[None, None, 0], thr_mma_dkv, swapAB=self.dKV_swapAB)
627
+ tdKrdS = utils.mma_make_fragment_A(sdSt, thr_mma_dkv, swapAB=self.dKV_swapAB)
628
+ tdKrQ = utils.mma_make_fragment_B(sQt[None, None, 0], thr_mma_dkv, swapAB=self.dKV_swapAB)
629
+ tdQrdS = utils.mma_make_fragment_A(sdS, thr_mma_dq, swapAB=self.dQ_swapAB)
630
+ tdQrK = utils.mma_make_fragment_B(sKt, thr_mma_dq, swapAB=self.dQ_swapAB)
631
+
632
+ LSEslice = (None, 0, None) if cutlass.const_expr(not self.SdP_swapAB) else (0, None, None)
633
+ tSsLSEMma = utils.make_acc_tensor_mn_view(thr_mma_sdp.partition_C(sLSEMma))[LSEslice]
634
+ tSsdPsumMma = utils.make_acc_tensor_mn_view(thr_mma_sdp.partition_C(sdPsumMma))[LSEslice]
635
+
636
+ # ///////////////////////////////////////////////////////////////////////////////
637
+ # Smem copy atom tiling
638
+ # ///////////////////////////////////////////////////////////////////////////////
639
+ smem_copy_atom = cute.make_copy_atom(
640
+ warp.LdMatrix8x8x16bOp(transpose=False, num_matrices=4), self.dtype,
641
+ )
642
+ smem_copy_atom_transposed = cute.make_copy_atom(
643
+ warp.LdMatrix8x8x16bOp(transpose=True, num_matrices=4), self.dtype,
644
+ )
645
+ smem_thr_copy_QdO = utils.make_tiled_copy_A(
646
+ smem_copy_atom, tiled_mma_sdp, swapAB=self.SdP_swapAB
647
+ ).get_slice(tidx)
648
+ smem_thr_copy_KV = utils.make_tiled_copy_B(
649
+ smem_copy_atom, tiled_mma_sdp, swapAB=self.SdP_swapAB
650
+ ).get_slice(tidx)
651
+ # TODO: should this be smem_copy_atom_transposed?
652
+ smem_thr_copy_PdSt = utils.make_tiled_copy_A(
653
+ smem_copy_atom_transposed, tiled_mma_dkv, swapAB=self.dKV_swapAB
654
+ ).get_slice(tidx)
655
+ smem_thr_copy_QdOt = utils.make_tiled_copy_B(
656
+ smem_copy_atom_transposed, tiled_mma_dkv, swapAB=self.dKV_swapAB
657
+ ).get_slice(tidx)
658
+ smem_thr_copy_dS = utils.make_tiled_copy_A(
659
+ smem_copy_atom, tiled_mma_dq, swapAB=self.dQ_swapAB
660
+ ).get_slice(tidx)
661
+ smem_thr_copy_Kt = utils.make_tiled_copy_B(
662
+ smem_copy_atom_transposed, tiled_mma_dq, swapAB=self.dQ_swapAB
663
+ ).get_slice(tidx)
664
+ # TODO: what's the number of bits? What if SdP_swapAB
665
+ r2s_thr_copy_PdS = cute.make_tiled_copy_C(
666
+ cute.make_copy_atom(
667
+ cute.nvgpu.CopyUniversalOp(), self.dtype, num_bits_per_copy=2 * self.dtype.width
668
+ ),
669
+ tiled_mma_sdp,
670
+ ).get_slice(tidx)
671
+
672
+ tSsQ = smem_thr_copy_QdO.partition_S(sQ)
673
+ tdPsdO = smem_thr_copy_QdO.partition_S(sdO)
674
+ tSsK = smem_thr_copy_KV.partition_S(sK)
675
+ tdPsV = smem_thr_copy_KV.partition_S(sV)
676
+ tdVsPt = smem_thr_copy_PdSt.partition_S(sPt)
677
+ tdKsdSt = smem_thr_copy_PdSt.partition_S(sdSt)
678
+ tdVsdOt = smem_thr_copy_QdOt.partition_S(sdOt)
679
+ tdKsQt = smem_thr_copy_QdOt.partition_S(sQt)
680
+ tdQsdS = smem_thr_copy_dS.partition_S(sdS)
681
+ tdQsKt = smem_thr_copy_Kt.partition_S(sKt)
682
+ tPsP = r2s_thr_copy_PdS.partition_D(sP)
683
+ tdSsdS = r2s_thr_copy_PdS.partition_D(sdS)
684
+
685
+ # ///////////////////////////////////////////////////////////////////////////////
686
+ # Predicate: Mark indices that need to copy when problem_shape isn't a multiple
687
+ # of tile_shape
688
+ # ///////////////////////////////////////////////////////////////////////////////
689
+ # Construct identity layout for KV
690
+ cQ = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
691
+ tQcQ = gmem_thr_copy_QK.partition_S(cQ)
692
+ t0QcQ = gmem_thr_copy_QK.get_slice(0).partition_S(cQ)
693
+ if cutlass.const_expr(self.head_dim_padded == self.head_dim_v_padded):
694
+ tdOcdO = tQcQ
695
+ t0dOcdO = t0QcQ
696
+ else:
697
+ cdO = cute.make_identity_tensor((self.m_block_size, self.head_dim_v_padded))
698
+ tdOcdO = gmem_thr_copy_VdO.partition_S(cdO)
699
+ t0dOcdO = gmem_thr_copy_VdO.get_slice(0).partition_S(cdO)
700
+ cLSE = cute.make_identity_tensor((self.m_block_size,))
701
+ tLSEcLSE = gmem_thr_copy_lse.partition_S(cLSE)
702
+
703
+ # Allocate predicate tensors for m and n, here we only allocate the tile of k, and
704
+ # use "if" on the mn dimension.
705
+ # This is to reduce register pressure and gets 2-3% performance gain.
706
+
707
+ d_head = mQ.shape[cute.rank(mQ) - 1]
708
+ d_head_v = mdO.shape[cute.rank(mdO) - 1]
709
+
710
+ tQpQ = utils.predicate_k(tQcQ, limit=d_head)
711
+ if cutlass.const_expr(self.same_hdim_kv):
712
+ tdOpdO = tQpQ
713
+ else:
714
+ tdOpdO = utils.predicate_k(tdOcdO, limit=d_head_v)
715
+
716
+ # group parameters for compute_one_m_block
717
+ mma_params = SimpleNamespace(
718
+ thr_mma_sdp=thr_mma_sdp, thr_mma_dkv=thr_mma_dkv, thr_mma_dq=thr_mma_dq,
719
+ tSrQ=tSrQ, tSrK=tSrK, tdPrdO=tdPrdO, tdPrV=tdPrV,
720
+ tdVrP=tdVrP, tdVrdO=tdVrdO, tdKrdS=tdKrdS, tdKrQ=tdKrQ,
721
+ tdQrdS=tdQrdS, tdQrK=tdQrK,
722
+ acc_dK=acc_dK, acc_dV=acc_dV,
723
+ )
724
+ smem_copy_params = SimpleNamespace(
725
+ smem_thr_copy_QdO=smem_thr_copy_QdO,
726
+ smem_thr_copy_KV=smem_thr_copy_KV,
727
+ smem_thr_copy_PdSt=smem_thr_copy_PdSt,
728
+ smem_thr_copy_QdOt=smem_thr_copy_QdOt,
729
+ smem_thr_copy_dS=smem_thr_copy_dS,
730
+ smem_thr_copy_Kt=smem_thr_copy_Kt,
731
+ r2s_thr_copy_PdS=r2s_thr_copy_PdS,
732
+ tSsQ=tSsQ, tSsK=tSsK, tdPsdO=tdPsdO, tdPsV=tdPsV,
733
+ tSsLSEMma=tSsLSEMma, tSsdPsumMma=tSsdPsumMma,
734
+ tPsP=tPsP, tdSsdS=tdSsdS,
735
+ tdVsPt=tdVsPt, tdVsdOt=tdVsdOt, tdKsdSt=tdKsdSt, tdKsQt=tdKsQt,
736
+ tdQsdS=tdQsdS, tdQsKt=tdQsKt,
737
+ )
738
+ gmem_copy_params = SimpleNamespace(
739
+ gmem_thr_copy_dQaccum=gmem_thr_copy_dQaccum, tdQgdQaccum=tdQgdQaccum
740
+ )
741
+ load_Q_LSE = partial(
742
+ self.load_Q_LSE, gmem_tiled_copy_QK, gmem_tiled_copy_LSE,
743
+ tQgQ, tQsQ, tQcQ, t0QcQ, tQpQ,
744
+ tLSEgLSE, tLSEsLSE, tLSEcLSE, seqlen=seqlen.seqlen_q
745
+ )
746
+ load_dO_dPsum = partial(
747
+ self.load_dO_dPsum, gmem_tiled_copy_VdO, gmem_tiled_copy_LSE,
748
+ tdOgdO, tdOsdO, tdOcdO, t0dOcdO, tdOpdO,
749
+ tLSEgdPsum, tLSEsdPsum, tLSEcLSE, seqlen=seqlen.seqlen_q
750
+ )
751
+ compute_one_m_block = partial(
752
+ self.compute_one_m_block, mma_params=mma_params,
753
+ smem_copy_params=smem_copy_params, gmem_copy_params=gmem_copy_params,
754
+ load_Q_LSE=load_Q_LSE, load_dO_dPsum=load_dO_dPsum,
755
+ m_block_max=m_block_max,
756
+ softmax_scale_log2=softmax_scale_log2,
757
+ )
758
+
759
+ # ///////////////////////////////////////////////////////////////////////////////
760
+ # Prologue
761
+ # ///////////////////////////////////////////////////////////////////////////////
762
+ # Start async loads of the last mn-tile, where we take care of the mn residue
763
+ self.load_V(gmem_thr_copy_VdO, tVgV, tVsV, n_block, seqlen=seqlen.seqlen_k,
764
+ headdim=d_head_v)
765
+ if cutlass.const_expr(self.V_in_regs):
766
+ cute.arch.cp_async_commit_group()
767
+ self.load_K(gmem_thr_copy_QK, tKgK, tKsK, n_block, seqlen=seqlen.seqlen_k,
768
+ headdim=d_head)
769
+ cute.arch.cp_async_commit_group()
770
+
771
+ if cutlass.const_expr(self.V_in_regs):
772
+ cute.arch.cp_async_wait_group(1)
773
+ cute.arch.barrier()
774
+ tdPrV_copy_view = smem_thr_copy_KV.retile(tdPrV)
775
+ cute.copy(smem_thr_copy_KV, tdPsV, tdPrV_copy_view)
776
+ # Sync to avoid loading Q to smem_q, which overlaps with smem_v
777
+ cute.arch.barrier()
778
+
779
+ m_block = m_block_min
780
+ assert self.num_stages_Q >= self.num_stages_dO
781
+ for stage in cutlass.range_constexpr(self.num_stages_Q):
782
+ if cutlass.const_expr(self.num_stages_Q == 1 or stage < self.num_stages_Q - 1):
783
+ if stage == 0 or m_block + stage < m_block_max:
784
+ load_Q_LSE(m_block + stage, smem_pipe_write_q=stage)
785
+ cute.arch.cp_async_commit_group()
786
+ if cutlass.const_expr(stage < self.num_stages_dO):
787
+ if stage == 0 or m_block + stage < m_block_max:
788
+ load_dO_dPsum(m_block + stage, smem_pipe_write_q=stage)
789
+ cute.arch.cp_async_commit_group()
790
+
791
+ # ///////////////////////////////////////////////////////////////////////////////
792
+ # Mainloop
793
+ # ///////////////////////////////////////////////////////////////////////////////
794
+ # Start processing of the first n-block.
795
+ mask = AttentionMask(self.m_block_size, self.n_block_size, seqlen.seqlen_q, seqlen.seqlen_k)
796
+ mask_fn = partial(
797
+ mask.apply_mask, n_block=n_block, thr_mma=thr_mma_sdp,
798
+ mask_seqlen=True, mask_causal=self.is_causal
799
+ )
800
+ smem_pipe_read_q = cutlass.Int32(0)
801
+ smem_pipe_read_do = cutlass.Int32(0)
802
+ smem_pipe_write_q = cutlass.Int32(self.num_stages_Q - 1)
803
+ smem_pipe_write_do = cutlass.Int32(0)
804
+ for m_tile in cutlass.range(m_block_min, m_block_max, unroll=1):
805
+ compute_one_m_block(
806
+ m_tile, smem_pipe_read_q, smem_pipe_read_do, smem_pipe_write_q, smem_pipe_write_do,
807
+ mask_fn=mask_fn,
808
+ )
809
+ smem_pipe_read_q = self.advance_pipeline(smem_pipe_read_q, self.num_stages_Q)
810
+ smem_pipe_read_do = self.advance_pipeline(smem_pipe_read_do, self.num_stages_dO)
811
+ smem_pipe_write_q = self.advance_pipeline(smem_pipe_write_q, self.num_stages_Q)
812
+ smem_pipe_write_do = self.advance_pipeline(smem_pipe_write_do, self.num_stages_dO)
813
+
814
+ # ///////////////////////////////////////////////////////////////////////////////
815
+ # Epilogue
816
+ # ///////////////////////////////////////////////////////////////////////////////
817
+ # If GQA, we scale dK in the postprocessing kernel instead
818
+ if cutlass.const_expr(self.qhead_per_kvhead == 1):
819
+ acc_dK.store(acc_dK.load() * softmax_scale)
820
+ # reuse sK and sV data iterator
821
+ sdK = cute.make_tensor(sK.iterator, sK_layout)
822
+ sdV = cute.make_tensor(sV.iterator, sV_layout)
823
+ self.epilogue(
824
+ acc_dK, acc_dV, mdK, mdV, sdK, sdV,
825
+ gmem_tiled_copy_dK, gmem_tiled_copy_dV, tiled_mma_dkv,
826
+ tidx, n_block, head_idx, batch_idx, seqlen, d_head, d_head_v
827
+ )
828
+
829
+ @cute.jit
830
+ def compute_one_m_block(
831
+ self,
832
+ m_block: cutlass.Int32,
833
+ smem_pipe_read_q: cutlass.Int32,
834
+ smem_pipe_read_do: cutlass.Int32,
835
+ smem_pipe_write_q: cutlass.Int32,
836
+ smem_pipe_write_do: cutlass.Int32,
837
+ mma_params: SimpleNamespace,
838
+ smem_copy_params: SimpleNamespace,
839
+ gmem_copy_params: SimpleNamespace,
840
+ load_Q_LSE: Callable,
841
+ load_dO_dPsum: Callable,
842
+ m_block_max: cutlass.Int32,
843
+ softmax_scale_log2: cutlass.Float32,
844
+ mask_fn: Optional[Callable] = None,
845
+ ):
846
+ def load_Q_next():
847
+ m_block_next = m_block + (self.num_stages_Q - 1 if cutlass.const_expr(self.num_stages_Q > 1) else 1)
848
+ if m_block_next < m_block_max:
849
+ load_Q_LSE(m_block_next, smem_pipe_write_q)
850
+ cute.arch.cp_async_commit_group()
851
+
852
+ def load_dO_next():
853
+ if m_block + self.num_stages_dO < m_block_max:
854
+ load_dO_dPsum(m_block + self.num_stages_dO, smem_pipe_write_do)
855
+ cute.arch.cp_async_commit_group()
856
+
857
+ # MMA S
858
+ acc_shape_SdP = mma_params.thr_mma_sdp.partition_shape_C(
859
+ (self.m_block_size, self.n_block_size) if cutlass.const_expr(not self.SdP_swapAB) else (self.n_block_size, self.m_block_size)
860
+ )
861
+ acc_S = cute.make_fragment(acc_shape_SdP, cutlass.Float32)
862
+ acc_S.fill(0.0)
863
+ cute.arch.cp_async_wait_group(1 if cutlass.const_expr(self.num_stages_Q > 1) else 0)
864
+ cute.arch.barrier()
865
+ sm80_utils.gemm(
866
+ mma_params.thr_mma_sdp, acc_S, mma_params.tSrQ, mma_params.tSrK,
867
+ smem_copy_params.tSsQ[None, None, None, smem_pipe_read_q if cutlass.const_expr(self.num_stages_Q > 1) else 0],
868
+ smem_copy_params.tSsK,
869
+ smem_copy_params.smem_thr_copy_QdO, smem_copy_params.smem_thr_copy_KV,
870
+ swap_AB=self.SdP_swapAB,
871
+ )
872
+ tLSErLSE = cute.make_fragment_like(smem_copy_params.tSsLSEMma[None, 0])
873
+ cute.autovec_copy(
874
+ smem_copy_params.tSsLSEMma[None, smem_pipe_read_q if cutlass.const_expr(self.num_stages_Q > 1) else 0], tLSErLSE
875
+ )
876
+ if cutlass.const_expr(mask_fn is not None):
877
+ mask_fn(acc_S, m_block=m_block)
878
+ acc_S_mn = utils.make_acc_tensor_mn_view(acc_S)
879
+ bidx = 0
880
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_S_mn)
881
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == 1: cute.print_tensor(tLSErLSE)
882
+ assert cute.size(acc_S_mn, mode=[0]) == cute.size(tLSErLSE)
883
+ for r in cutlass.range(cute.size(acc_S_mn, mode=[0]), unroll_full=True):
884
+ acc_S_mn[r, None].store(utils.exp2f(acc_S_mn[r, None].load() * softmax_scale_log2 - tLSErLSE[r]))
885
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_S_mn)
886
+
887
+ # MMA dP
888
+ acc_dP = cute.make_fragment(acc_shape_SdP, cutlass.Float32)
889
+ acc_dP.fill(0.0)
890
+ cute.arch.cp_async_wait_group(1 if cutlass.const_expr(self.num_stages_dO > 1) else 0)
891
+ cute.arch.barrier()
892
+ sm80_utils.gemm(
893
+ mma_params.thr_mma_sdp, acc_dP, mma_params.tdPrdO, mma_params.tdPrV,
894
+ smem_copy_params.tdPsdO[None, None, None, smem_pipe_read_do if cutlass.const_expr(self.num_stages_dO > 1) else 0],
895
+ smem_copy_params.tdPsV,
896
+ smem_copy_params.smem_thr_copy_QdO, smem_copy_params.smem_thr_copy_KV,
897
+ hook_fn=load_Q_next if cutlass.const_expr(self.num_stages_Q > 1) else None,
898
+ swap_AB=self.SdP_swapAB,
899
+ )
900
+ tLSErdPsum = cute.make_fragment_like(smem_copy_params.tSsdPsumMma[None, 0])
901
+ cute.autovec_copy(
902
+ smem_copy_params.tSsdPsumMma[None, smem_pipe_read_do if cutlass.const_expr(self.num_stages_dO > 1) else 0], tLSErdPsum
903
+ )
904
+ acc_dP_mn = utils.make_acc_tensor_mn_view(acc_dP)
905
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dP_mn)
906
+ assert cute.size(acc_dP_mn, mode=[0]) == cute.size(tLSErdPsum)
907
+ for r in cutlass.range(cute.size(acc_dP_mn, mode=[0]), unroll_full=True):
908
+ acc_dP_mn[r, None].store(acc_S_mn[r, None].load() * (acc_dP_mn[r, None].load() - tLSErdPsum[r]))
909
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dP_mn)
910
+ rP = cute.make_fragment_like(acc_S, self.dtype)
911
+ rP.store(acc_S.load().to(self.dtype))
912
+ if cutlass.const_expr(not self.Mma_dKV_is_RS):
913
+ tPrP = smem_copy_params.r2s_thr_copy_PdS.retile(rP) # ((Atom,AtomNum), MMA_N, MMA_N)
914
+ cute.copy(smem_copy_params.r2s_thr_copy_PdS, tPrP, smem_copy_params.tPsP)
915
+ rdS = cute.make_fragment_like(acc_dP, self.dtype)
916
+ rdS.store(acc_dP.load().to(self.dtype))
917
+ if cutlass.const_expr(not self.Mma_dKV_is_RS):
918
+ cute.arch.barrier() # Make sure P is written
919
+ # For hdim 64, It's faster to write to smem_dS first before the dV gemm
920
+ if cutlass.const_expr(not self.Mma_dKV_is_RS):
921
+ tdSrdS = smem_copy_params.r2s_thr_copy_PdS.retile(rdS)
922
+ cute.copy(smem_copy_params.r2s_thr_copy_PdS, tdSrdS, smem_copy_params.tdSsdS)
923
+ if cutlass.const_expr(self.Mma_dKV_is_RS):
924
+ tdVrP = cute.make_tensor(rP.iterator, utils.convert_layout_acc_frgA(rP.layout))
925
+ else:
926
+ tdVrP = mma_params.tdVrP
927
+
928
+ # MMA dK
929
+ sm80_utils.gemm(
930
+ mma_params.thr_mma_dkv, mma_params.acc_dV, tdVrP, mma_params.tdVrdO,
931
+ smem_copy_params.tdVsPt,
932
+ smem_copy_params.tdVsdOt[None, None, None, smem_pipe_read_do if cutlass.const_expr(self.num_stages_dO > 1) else 0],
933
+ smem_copy_params.smem_thr_copy_PdSt, smem_copy_params.smem_thr_copy_QdOt,
934
+ A_in_regs=self.Mma_dKV_is_RS,
935
+ swap_AB=self.dKV_swapAB,
936
+ )
937
+ # if cute.arch.thread_idx()[0] == 0 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(mma_params.acc_dV)
938
+ cute.arch.barrier() # Make sure dS is written
939
+
940
+ # MMA dQ
941
+ def dQ_mma(hook_fn):
942
+ acc_shape_dQ = mma_params.thr_mma_dq.partition_shape_C(
943
+ (self.m_block_size, self.head_dim_padded) if cutlass.const_expr(not self.dQ_swapAB) else (self.head_dim_padded, self.m_block_size)
944
+ )
945
+ acc_dQ = cute.make_fragment(acc_shape_dQ, cutlass.Float32)
946
+ acc_dQ.fill(0.0)
947
+ sm80_utils.gemm(
948
+ mma_params.thr_mma_dq, acc_dQ, mma_params.tdQrdS, mma_params.tdQrK,
949
+ smem_copy_params.tdQsdS, smem_copy_params.tdQsKt,
950
+ smem_copy_params.smem_thr_copy_dS, smem_copy_params.smem_thr_copy_Kt,
951
+ swap_AB=self.dQ_swapAB,
952
+ hook_fn=hook_fn
953
+ )
954
+ # ((1, 1), num_elements)
955
+ acc_dQ_atomic = gmem_copy_params.gmem_thr_copy_dQaccum.retile(acc_dQ)
956
+ tdQgdQaccum_atomic = gmem_copy_params.tdQgdQaccum[None, None, m_block]
957
+ assert cute.size(acc_dQ_atomic) == cute.size(tdQgdQaccum_atomic)
958
+ for i in cutlass.range(cute.size(acc_dQ_atomic), unroll_full=True):
959
+ utils.atomic_add_fp32(acc_dQ_atomic[i], utils.elem_pointer(tdQgdQaccum_atomic, i))
960
+ # utils.atomic_add_fp32(acc_dQ[i], tdQgdQaccum_atomic.iterator + i * tdQgdQaccum_atomic.stride[1])
961
+ # if cute.arch.thread_idx()[0] == 64 and cute.arch.block_idx()[0] == bidx: cute.print_tensor(acc_dQ)
962
+
963
+ # If num_stages_Q == 1, we want to do Mma_dK first so we can start loading Q for the next iteration
964
+ if cutlass.const_expr(self.num_stages_Q > 1):
965
+ dQ_mma(load_dO_next)
966
+
967
+ # MMA dK
968
+ if cutlass.const_expr(self.Mma_dKV_is_RS):
969
+ tdKrdS = cute.make_tensor(rdS.iterator, utils.convert_layout_acc_frgA(rdS.layout))
970
+ else:
971
+ tdKrdS = mma_params.tdKrdS
972
+ sm80_utils.gemm(
973
+ mma_params.thr_mma_dkv, mma_params.acc_dK, tdKrdS, mma_params.tdKrQ,
974
+ smem_copy_params.tdKsdSt,
975
+ smem_copy_params.tdKsQt[None, None, None, smem_pipe_read_q if cutlass.const_expr(self.num_stages_Q > 1) else 0],
976
+ smem_copy_params.smem_thr_copy_PdSt, smem_copy_params.smem_thr_copy_QdOt,
977
+ A_in_regs=self.Mma_dKV_is_RS,
978
+ swap_AB=self.dKV_swapAB,
979
+ hook_fn=load_dO_next if cutlass.const_expr(self.num_stages_Q == 1) else None,
980
+ )
981
+ # if cute.arch.thread_idx()[0] == 0: cute.print_tensor(mma_params.acc_dK)
982
+ if cutlass.const_expr(self.num_stages_Q == 1):
983
+ cute.arch.barrier()
984
+ dQ_mma(load_Q_next)
985
+
986
+ @cute.jit
987
+ def epilogue(
988
+ self,
989
+ acc_dK: cute.Tensor,
990
+ acc_dV: cute.Tensor,
991
+ mdK: cute.Tensor,
992
+ mdV: cute.Tensor,
993
+ sdK: cute.Tensor,
994
+ sdV: cute.Tensor,
995
+ gmem_tiled_copy_dK: cute.TiledCopy,
996
+ gmem_tiled_copy_dV: cute.TiledCopy,
997
+ tiled_mma: cute.TiledMma,
998
+ tidx: cutlass.Int32,
999
+ n_block: cutlass.Int32,
1000
+ num_head: cutlass.Int32,
1001
+ batch_size: cutlass.Int32,
1002
+ seqlen: SeqlenInfoQK,
1003
+ d_head: cutlass.Int32,
1004
+ d_head_v: cutlass.Int32
1005
+ ):
1006
+ rdV = cute.make_fragment_like(acc_dV, self.dtype)
1007
+ rdV.store(acc_dV.load().to(self.dtype))
1008
+ rdK = cute.make_fragment_like(acc_dK, self.dtype)
1009
+ rdK.store(acc_dK.load().to(self.dtype))
1010
+ gmem_thr_copy_dK = gmem_tiled_copy_dK.get_slice(tidx)
1011
+ gmem_thr_copy_dV = gmem_tiled_copy_dV.get_slice(tidx)
1012
+
1013
+ batch_idx = batch_size
1014
+ head_idx_kv = num_head // self.qhead_per_kvhead if cutlass.const_expr(not self.pack_gqa) else num_head
1015
+
1016
+ if cutlass.const_expr(self.qhead_per_kvhead == 1):
1017
+ # Make sure all threads have finished reading K and V, otherwise we get racy dQ
1018
+ # because smem_q could be changed.
1019
+ cute.arch.barrier()
1020
+ # smem copy atom for dKV
1021
+ smem_copy_atom_dKV = cute.make_copy_atom(
1022
+ cute.nvgpu.CopyUniversalOp(), self.dtype, num_bits_per_copy=2 * self.dtype.width
1023
+ )
1024
+ smem_thr_copy_dKV = cute.make_tiled_copy_C(smem_copy_atom_dKV, tiled_mma).get_slice(tidx)
1025
+ taccdVrdV = smem_thr_copy_dKV.retile(rdV)
1026
+ taccdKrdK = smem_thr_copy_dKV.retile(rdK)
1027
+ taccdVsdV = smem_thr_copy_dKV.partition_D(sdV)
1028
+ taccdKsdK = smem_thr_copy_dKV.partition_D(sdK)
1029
+ # copy acc O from rmem to smem with the smem copy atom
1030
+ cute.copy(smem_copy_atom_dKV, taccdVrdV, taccdVsdV)
1031
+ cute.copy(smem_copy_atom_dKV, taccdKrdK, taccdKsdK)
1032
+
1033
+
1034
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_k):
1035
+ mdK_cur, mdV_cur = [t[batch_idx, None, head_idx_kv, None] for t in (mdK, mdV)]
1036
+ else:
1037
+ mdK_cur, mdV_cur = [cute.domain_offset((seqlen.offset_k, 0), t[None, head_idx_kv, None]) for t in (mdK, mdV)]
1038
+
1039
+ blkdK_shape = (self.n_block_size, self.head_dim_padded)
1040
+ blkdV_shape = (self.n_block_size, self.head_dim_v_padded)
1041
+ gdK = cute.local_tile(mdK_cur, blkdK_shape, (n_block, 0))
1042
+ gdV = cute.local_tile(mdV_cur, blkdV_shape, (n_block, 0))
1043
+ tdKsdK = gmem_thr_copy_dK.partition_S(sdK)
1044
+ tdKgdK = gmem_thr_copy_dK.partition_D(gdK)
1045
+ tdVsdV = gmem_thr_copy_dV.partition_S(sdV)
1046
+ tdVgdV = gmem_thr_copy_dV.partition_D(gdV)
1047
+ tdKrdK = cute.make_fragment_like(tdKgdK, self.dtype)
1048
+ tdVrdV = cute.make_fragment_like(tdVgdV, self.dtype)
1049
+ # sync before all smem stores are done.
1050
+ cute.arch.barrier()
1051
+ # load acc dK and dV from smem to rmem for wider vectorization
1052
+ # Need to check OOB when reading from smem if kBlockN isn't evenly tiled
1053
+ # TODO
1054
+ cute.autovec_copy(tdKsdK, tdKrdK)
1055
+ cute.autovec_copy(tdVsdV, tdVrdV)
1056
+
1057
+ cdK = cute.make_identity_tensor((self.n_block_size, self.head_dim_padded))
1058
+ tdKcdK = gmem_thr_copy_dK.partition_S(cdK)
1059
+ t0dKcdK = gmem_tiled_copy_dK.get_slice(0).partition_S(cdK)
1060
+ if cutlass.const_expr(self.head_dim_padded == self.head_dim_v_padded):
1061
+ tdVcdV = tdKcdK
1062
+ t0dVcdV = t0dKcdK
1063
+ else:
1064
+ cdV = cute.make_identity_tensor((self.n_block_size, self.head_dim_v_padded))
1065
+ tdVcdV = gmem_thr_copy_dV.partition_S(cdV)
1066
+ t0dVcdV = gmem_tiled_copy_dV.get_slice(0).partition_S(cdV)
1067
+ tdKpdK = utils.predicate_k(tdKcdK, limit=d_head)
1068
+ if cutlass.const_expr(self.same_hdim_kv):
1069
+ tdVpdV = tdKpdK
1070
+ else:
1071
+ tdVpdV = utils.predicate_k(tdVcdV, limit=d_head_v)
1072
+ # copy acc dK and acc_dV from rmem to gmem
1073
+ for rest_m in cutlass.range_constexpr(cute.size(tdKrdK.shape[1])):
1074
+ if t0dKcdK[0, rest_m, 0][0] < seqlen.seqlen_k - n_block * self.n_block_size - tdKcdK[0][0]:
1075
+ cute.copy(
1076
+ gmem_tiled_copy_dK,
1077
+ tdKrdK[None, rest_m, None],
1078
+ tdKgdK[None, rest_m, None],
1079
+ pred=tdKpdK[None, rest_m, None] if cutlass.const_expr(self.check_hdim_oob) else None,
1080
+ )
1081
+ for rest_m in cutlass.range_constexpr(cute.size(tdVrdV.shape[1])):
1082
+ if t0dVcdV[0, rest_m, 0][0] < seqlen.seqlen_k - n_block * self.n_block_size - tdVcdV[0][0]:
1083
+ cute.copy(
1084
+ gmem_tiled_copy_dV,
1085
+ tdVrdV[None, rest_m, None],
1086
+ tdVgdV[None, rest_m, None],
1087
+ pred=tdVpdV[None, rest_m, None] if cutlass.const_expr(self.check_hdim_v_oob) else None,
1088
+ )
1089
+
1090
+ else: # qhead_per_kvhead > 1, do atomic add
1091
+ # For Sm90, we need to sync to avoid racy writes to smem_q
1092
+ # For Sm80, we don't need to sync since we're not touching smem
1093
+ head_idx_kv = num_head // self.qhead_per_kvhead if cutlass.const_expr(not self.pack_gqa) else num_head
1094
+
1095
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_k):
1096
+ mdK_cur, mdV_cur = [t[batch_idx, head_idx_kv, None] for t in (mdK, mdV)]
1097
+ else:
1098
+ padded_offset_k = seqlen.offset_k + batch_idx * self.n_block_size
1099
+ mdK_cur = cute.domain_offset((padded_offset_k * self.head_dim_padded,), mdK[head_idx_kv, None])
1100
+ mdV_cur = cute.domain_offset((padded_offset_k * self.head_dim_v_padded,), mdV[head_idx_kv, None])
1101
+
1102
+ gdV = cute.local_tile(mdV_cur, (self.n_block_size * self.head_dim_v_padded,), (n_block,))
1103
+ gdK = cute.local_tile(mdK_cur, (self.n_block_size * self.head_dim_padded,), (n_block,))
1104
+ tdVgdVaccum = gmem_thr_copy_dV.partition_S(gdV)
1105
+ tdKgdKaccum = gmem_thr_copy_dK.partition_S(gdK)
1106
+ acc_dV_atomic = gmem_thr_copy_dV.retile(acc_dV)
1107
+ acc_dK_atomic = gmem_thr_copy_dK.retile(acc_dK)
1108
+ assert cute.size(acc_dV_atomic) == cute.size(tdVgdVaccum)
1109
+ assert cute.size(acc_dK_atomic) == cute.size(tdKgdKaccum)
1110
+ for i in cutlass.range(cute.size(acc_dV_atomic), unroll_full=True):
1111
+ utils.atomic_add_fp32(acc_dV_atomic[i], utils.elem_pointer(tdVgdVaccum, i))
1112
+ for i in cutlass.range(cute.size(acc_dK_atomic), unroll_full=True):
1113
+ utils.atomic_add_fp32(acc_dK_atomic[i], utils.elem_pointer(tdKgdKaccum, i))
1114
+
1115
+ @cute.jit
1116
+ def advance_pipeline(self, pipeline_index, num_stages: cutlass.Constexpr):
1117
+ return pipeline_index + 1 if pipeline_index < num_stages - 1 else 0
1118
+
1119
+ @cute.jit
1120
+ def load_K(
1121
+ self,
1122
+ gmem_thr_copy: cute.TiledCopy,
1123
+ tKgK: cute.Tensor,
1124
+ tKsK: cute.Tensor,
1125
+ block: cutlass.Int32,
1126
+ seqlen: cutlass.Int32,
1127
+ headdim: cutlass.Int32,
1128
+ ):
1129
+ cK = cute.make_identity_tensor((self.n_block_size, self.head_dim_padded))
1130
+ tKcK = gmem_thr_copy.partition_S(cK)
1131
+ t0KcK = gmem_thr_copy.get_slice(0).partition_S(cK)
1132
+ tKpK = utils.predicate_k(tKcK, limit=headdim)
1133
+ for n in cutlass.range_constexpr(cute.size(tKsK.shape[1])):
1134
+ # If kBlockN doesn't evenly divide the tiled copy, only the last `n` needs to be checked
1135
+ if self.is_even_n_smem_k or n < cute.size(tKsK.shape[1]) - 1 or tKcK[0, n, 0][0] < self.n_block_size:
1136
+ # Instead of using tKcK, we using t0KcK and subtract the offset from the limit
1137
+ # (seqlen - block * kBlockN). This is because the entries of t0KcK are known at compile time.
1138
+ predicate_n = t0KcK[0, n, 0][0] < seqlen - block * self.n_block_size - tKcK[0][0]
1139
+ predicate = cute.make_fragment_like(tKpK[None, 0, None])
1140
+ for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
1141
+ for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
1142
+ predicate[i, k] = (tKpK[i, n, k] if cutlass.const_expr(self.check_hdim_oob) else True) and predicate_n
1143
+ cute.copy(
1144
+ gmem_thr_copy, tKgK[None, n, None], tKsK[None, n, None], pred=predicate,
1145
+ )
1146
+ # We need to clear the sK smem tiles since we'll use sKt for mma_dq
1147
+
1148
+ @cute.jit
1149
+ def load_V(
1150
+ self,
1151
+ gmem_thr_copy: cute.TiledCopy,
1152
+ tVgV: cute.Tensor,
1153
+ tVsV: cute.Tensor,
1154
+ block: cutlass.Int32,
1155
+ seqlen: cutlass.Int32,
1156
+ headdim: cutlass.Int32,
1157
+ ):
1158
+ cV = cute.make_identity_tensor((self.n_block_size, self.head_dim_v_padded))
1159
+ tVcV = gmem_thr_copy.partition_S(cV)
1160
+ t0VcV = gmem_thr_copy.get_slice(0).partition_S(cV)
1161
+ tVpV = utils.predicate_k(tVcV, limit=headdim)
1162
+ for n in cutlass.range_constexpr(cute.size(tVsV.shape[1])):
1163
+ # If kBlockN doesn't evenly divide the tiled copy, only the last `n` needs to be checked
1164
+ if self.is_even_n_smem_v or n < cute.size(tVsV.shape[1]) - 1 or tVcV[0, n, 0][0] < self.n_block_size:
1165
+ # Instead of using tVcV, we using t0VcV and subtract the offset from the limit
1166
+ # (seqlen - block * kBlockN). This is because the entries of t0VcV are known at compile time.
1167
+ predicate_n = t0VcV[0, n, 0][0] < seqlen - block * self.n_block_size - tVcV[0][0]
1168
+ predicate = cute.make_fragment_like(tVpV[None, 0, None])
1169
+ for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
1170
+ for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
1171
+ predicate[i, k] = (tVpV[i, n, k] if cutlass.const_expr(self.check_hdim_oob) else True) and predicate_n
1172
+ cute.copy(
1173
+ gmem_thr_copy, tVgV[None, n, None], tVsV[None, n, None], pred=predicate,
1174
+ )
1175
+
1176
+ @cute.jit
1177
+ def load_Q_LSE(
1178
+ self,
1179
+ gmem_tiled_copy_Q: cute.TiledCopy,
1180
+ gmem_tiled_copy_LSE: cute.TiledCopy,
1181
+ tQgQ: cute.Tensor,
1182
+ tQsQ: cute.Tensor,
1183
+ tQcQ: cute.Tensor,
1184
+ t0QcQ: cute.Tensor,
1185
+ tQpQ: cute.Tensor,
1186
+ tLSEgLSE: cute.Tensor,
1187
+ tLSEsLSE: cute.Tensor,
1188
+ tLSEcLSE: cute.Tensor,
1189
+ block: cutlass.Int32,
1190
+ smem_pipe_write_q: cutlass.Int32,
1191
+ seqlen: cutlass.Int32,
1192
+ ):
1193
+ for m in cutlass.range_constexpr(cute.size(tQsQ.shape[1])):
1194
+ # If kBlockM doesn't evenly divide the tiled copy, only the last `m` needs to be checked
1195
+ if self.is_even_m_smem_q or m < cute.size(tQsQ.shape[1]) - 1 or tQcQ[0, m, 0][0] < self.m_block_size:
1196
+ # Instead of using tQcQ, we using t0QcQ and subtract the offset from the limit
1197
+ # (seqlen - block * kBlockM). This is because the entries of t0QcQ are known at compile time.
1198
+ predicate_m = t0QcQ[0, m, 0][0] < seqlen - block * self.m_block_size - tQcQ[0][0]
1199
+ predicate = cute.make_fragment_like(tQpQ[None, 0, None])
1200
+ for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
1201
+ for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
1202
+ predicate[i, k] = (tQpQ[i, m, k] if cutlass.const_expr(self.check_hdim_oob) else True) and predicate_m
1203
+ cute.copy(
1204
+ gmem_tiled_copy_Q,
1205
+ tQgQ[None, m, None, block],
1206
+ tQsQ[None, m, None, smem_pipe_write_q if cutlass.const_expr(self.num_stages_Q) > 1 else 0],
1207
+ pred=predicate,
1208
+ )
1209
+ # We need to clear the sQ smem tiles since we'll use sQt for mma_dK
1210
+ # We made sure LSE length is padded so we read `kBlockM` elements so that all
1211
+ # elements in sLSE are filled. Without this we might have uninitialized sLSE values.
1212
+ for m in cutlass.range_constexpr(cute.size(tLSEsLSE.shape[1])):
1213
+ if tLSEcLSE[0, m][0] < self.m_block_size:
1214
+ cute.copy(
1215
+ gmem_tiled_copy_LSE,
1216
+ tLSEgLSE[None, m, block],
1217
+ tLSEsLSE[None, m, smem_pipe_write_q if cutlass.const_expr(self.num_stages_Q > 1) else 0],
1218
+ )
1219
+
1220
+ @cute.jit
1221
+ def load_dO_dPsum(
1222
+ self,
1223
+ gmem_tiled_copy_dO: cute.TiledCopy,
1224
+ gmem_tiled_copy_dPsum: cute.TiledCopy,
1225
+ tdOgdO: cute.Tensor,
1226
+ tdOsdO: cute.Tensor,
1227
+ tdOcdO: cute.Tensor,
1228
+ t0dOcdO: cute.Tensor,
1229
+ tdOpdO: cute.Tensor,
1230
+ tdPsumgdPsum: cute.Tensor,
1231
+ tdPsumsdPsum: cute.Tensor,
1232
+ tdPsumcdPsum: cute.Tensor,
1233
+ block: cutlass.Int32,
1234
+ smem_pipe_write_q: cutlass.Int32,
1235
+ seqlen: cutlass.Int32,
1236
+ ):
1237
+ for m in cutlass.range_constexpr(cute.size(tdOsdO.shape[1])):
1238
+ # If kBlockM doesn't evenly divide the tiled copy, only the last `m` needs to be checked
1239
+ if self.is_even_m_smem_do or m < cute.size(tdOsdO.shape[1]) - 1 or tdOcdO[0, m, 0][0] < self.m_block_size:
1240
+ # Instead of using tdOcdO, we using t0dOcdO and subtract the offset from the limit
1241
+ # (seqlen - block * kBlockM). This is because the entries of t0dOcdO are known at compile time.
1242
+ predicate_m = t0dOcdO[0, m, 0][0] < seqlen - block * self.m_block_size - tdOcdO[0][0]
1243
+ predicate = cute.make_fragment_like(tdOpdO[None, 0, None])
1244
+ for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
1245
+ for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
1246
+ predicate[i, k] = (tdOpdO[i, m, k] if cutlass.const_expr(self.check_hdim_oob) else True) and predicate_m
1247
+ cute.copy(
1248
+ gmem_tiled_copy_dO,
1249
+ tdOgdO[None, m, None, block],
1250
+ tdOsdO[None, m, None, smem_pipe_write_q if cutlass.const_expr(self.num_stages_dO > 1) else 0],
1251
+ pred=predicate,
1252
+ )
1253
+ # We need to clear the sQ smem tiles since we'll use sQt for mma_dK
1254
+ # We made sure LSE length is padded so we read `kBlockM` elements so that all
1255
+ # elements in sLSE are filled. Without this we might have uninitialized sLSE values.
1256
+ for m in cutlass.range_constexpr(cute.size(tdPsumgdPsum.shape[1])):
1257
+ if tdPsumcdPsum[0, m][0] < self.m_block_size:
1258
+ cute.copy(
1259
+ gmem_tiled_copy_dPsum,
1260
+ tdPsumgdPsum[None, m, block],
1261
+ tdPsumsdPsum[None, m, smem_pipe_write_q if cutlass.const_expr(self.num_stages_dO > 1) else 0],
1262
+ )
kernel/deps/flash_attn/cute/flash_bwd_postprocess.py ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_bwd_postprocess_kernel.h
3
+ # from Cutlass C++ to Cute-DSL.
4
+ import math
5
+ from typing import Callable, Optional, Type, Literal
6
+
7
+ import cuda.bindings.driver as cuda
8
+
9
+ import cutlass
10
+ import cutlass.cute as cute
11
+ import cutlass.utils.hopper_helpers as sm90_utils_basic
12
+ import cutlass.utils.blackwell_helpers as sm100_utils_basic
13
+ from cutlass.cute.nvgpu import cpasync, warp, warpgroup
14
+ from cutlass import Float32, const_expr
15
+ from cutlass.utils import LayoutEnum
16
+
17
+ from flash_attn.cute import utils
18
+ from flash_attn.cute import copy_utils
19
+ from flash_attn.cute import ampere_helpers as sm80_utils
20
+ from flash_attn.cute import hopper_helpers as sm90_utils
21
+ from flash_attn.cute.seqlen_info import SeqlenInfoQK
22
+ import cutlass.cute.nvgpu.tcgen05 as tcgen05
23
+ from flash_attn.cute.tile_scheduler import (
24
+ ParamsBase,
25
+ SingleTileScheduler,
26
+ SingleTileVarlenScheduler,
27
+ TileSchedulerArguments,
28
+ )
29
+
30
+
31
+ class FlashAttentionBackwardPostprocess:
32
+ def __init__(
33
+ self,
34
+ dtype: Type[cutlass.Numeric],
35
+ head_dim: int,
36
+ arch: Literal[80, 90, 100],
37
+ tile_m: int = 128,
38
+ num_threads: int = 256,
39
+ AtomLayoutMdQ: int = 1,
40
+ dQ_swapAB: bool = False,
41
+ ):
42
+ """
43
+ :param head_dim: head dimension
44
+ :type head_dim: int
45
+ :param tile_m: m block size
46
+ :type tile_m: int
47
+ """
48
+ self.dtype = dtype
49
+ self.tile_m = tile_m
50
+ assert arch in [80, 90, 100], (
51
+ "Only Ampere (80), Hopper (90), and Blackwell (100) are supported"
52
+ )
53
+ self.arch = arch
54
+ # padding head_dim to a multiple of 32 as k_block_size
55
+ hdim_multiple_of = 32
56
+ self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
57
+ self.check_hdim_oob = head_dim != self.tile_hdim
58
+ self.num_threads = num_threads
59
+ self.AtomLayoutMdQ = AtomLayoutMdQ
60
+ self.dQ_swapAB = dQ_swapAB
61
+
62
+ @staticmethod
63
+ def can_implement(dtype, head_dim, tile_m, num_threads) -> bool:
64
+ """Check if the kernel can be implemented with the given parameters.
65
+
66
+ :param dtype: data type
67
+ :type dtype: cutlass.Numeric
68
+ :param head_dim: head dimension
69
+ :type head_dim: int
70
+ :param tile_m: m block size
71
+ :type tile_m: int
72
+
73
+ :return: True if the kernel can be implemented, False otherwise
74
+ :rtype: bool
75
+ """
76
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
77
+ return False
78
+ if head_dim % 8 != 0:
79
+ return False
80
+ if num_threads % 32 != 0:
81
+ return False
82
+ return True
83
+
84
+ def _get_tiled_mma(self):
85
+ if const_expr(self.arch == 80):
86
+ num_mma_warps = self.num_threads // 32
87
+ atom_layout_dQ = (
88
+ (self.AtomLayoutMdQ, num_mma_warps // self.AtomLayoutMdQ, 1)
89
+ if const_expr(not self.dQ_swapAB)
90
+ else (num_mma_warps // self.AtomLayoutMdQ, self.AtomLayoutMdQ, 1)
91
+ )
92
+ tiled_mma = cute.make_tiled_mma(
93
+ warp.MmaF16BF16Op(self.dtype, Float32, (16, 8, 16)),
94
+ atom_layout_dQ,
95
+ permutation_mnk=(atom_layout_dQ[0] * 16, atom_layout_dQ[1] * 16, 16),
96
+ )
97
+ elif const_expr(self.arch == 90):
98
+ num_mma_warp_groups = self.num_threads // 128
99
+ atom_layout_dQ = (self.AtomLayoutMdQ, num_mma_warp_groups // self.AtomLayoutMdQ)
100
+ tiler_mn_dQ = (self.tile_m // atom_layout_dQ[0], self.tile_hdim // atom_layout_dQ[1])
101
+ tiled_mma = sm90_utils_basic.make_trivial_tiled_mma(
102
+ self.dtype,
103
+ self.dtype,
104
+ warpgroup.OperandMajorMode.K, # These don't matter, we only care about the accum
105
+ warpgroup.OperandMajorMode.K,
106
+ Float32,
107
+ atom_layout_mnk=(atom_layout_dQ if not self.dQ_swapAB else atom_layout_dQ[::-1])
108
+ + (1,),
109
+ tiler_mn=tiler_mn_dQ if not self.dQ_swapAB else tiler_mn_dQ[::-1],
110
+ )
111
+ else:
112
+ cta_group = tcgen05.CtaGroup.ONE
113
+ tiled_mma = sm100_utils_basic.make_trivial_tiled_mma(
114
+ self.dtype,
115
+ tcgen05.OperandMajorMode.MN, # dS_major_mode
116
+ tcgen05.OperandMajorMode.MN, # Kt_major_mode
117
+ Float32,
118
+ cta_group,
119
+ (self.tile_m, self.tile_hdim),
120
+ )
121
+ if const_expr(self.arch in [80, 90]):
122
+ assert self.num_threads == tiled_mma.size
123
+ return tiled_mma
124
+
125
+ def _setup_attributes(self):
126
+ # ///////////////////////////////////////////////////////////////////////////////
127
+ # GMEM Tiled copy:
128
+ # ///////////////////////////////////////////////////////////////////////////////
129
+ # Thread layouts for copies
130
+ universal_copy_bits = 128
131
+ async_copy_elems_accum = universal_copy_bits // Float32.width
132
+ atom_async_copy_accum = cute.make_copy_atom(
133
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
134
+ Float32,
135
+ num_bits_per_copy=universal_copy_bits,
136
+ )
137
+ # We don't do bound checking for the gmem -> smem load so we just assert here.
138
+ assert (self.tile_m * self.tile_hdim // async_copy_elems_accum) % self.num_threads == 0
139
+ self.g2s_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
140
+ atom_async_copy_accum,
141
+ cute.make_layout(self.num_threads),
142
+ cute.make_layout(async_copy_elems_accum),
143
+ )
144
+ num_s2r_copy_elems = 1 if const_expr(self.arch == 80) else 4
145
+ if const_expr(self.arch == 80):
146
+ self.s2r_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
147
+ Float32, self.num_threads, num_s2r_copy_elems
148
+ )
149
+ self.sdQaccum_layout = cute.make_layout(self.tile_m * self.tile_hdim)
150
+ elif const_expr(self.arch == 90):
151
+ num_threads_per_warp_group = 128
152
+ num_mma_warp_groups = self.num_threads // 128
153
+ self.s2r_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
154
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
155
+ cute.make_layout((num_threads_per_warp_group, num_mma_warp_groups)), # thr_layout
156
+ cute.make_layout(128 // Float32.width), # val_layout
157
+ )
158
+ self.sdQaccum_layout = cute.make_layout(
159
+ (self.tile_m * self.tile_hdim // num_mma_warp_groups, num_mma_warp_groups)
160
+ )
161
+ else:
162
+ self.dQ_reduce_ncol = 32
163
+ dQaccum_reduce_stage = self.tile_hdim // self.dQ_reduce_ncol
164
+ assert self.num_threads == 128 # TODO: currently hard-coded
165
+ self.s2r_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
166
+ Float32, self.num_threads, num_s2r_copy_elems
167
+ )
168
+ self.sdQaccum_layout = cute.make_layout(
169
+ (self.tile_m * self.tile_hdim // dQaccum_reduce_stage, dQaccum_reduce_stage)
170
+ )
171
+
172
+ self.gmem_tiled_copy_dQ = copy_utils.tiled_copy_2d(
173
+ self.dtype, self.tile_hdim, self.num_threads
174
+ )
175
+ # ///////////////////////////////////////////////////////////////////////////////
176
+ # Shared memory layout: dQ
177
+ # ///////////////////////////////////////////////////////////////////////////////
178
+ # We can't just use kHeadDim here. E.g. if MMA shape is 64 x 96 but split across 2 WGs,
179
+ # then setting kBlockKSmem to 32 will cause "Static shape_div failure".
180
+ # We want to treat it as 64 x 48, so kBlockKSmem should be 16.
181
+ mma_shape_n = self.tiled_mma.get_tile_size(1)
182
+ if const_expr(self.arch == 80):
183
+ sdQ_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, mma_shape_n)
184
+ self.sdQ_layout = cute.tile_to_shape(
185
+ sdQ_layout_atom, (self.tile_m, self.tile_hdim), (0, 1)
186
+ )
187
+ elif const_expr(self.arch == 90):
188
+ self.sdQ_layout = sm90_utils.make_smem_layout(
189
+ self.dtype, LayoutEnum.ROW_MAJOR, (self.tile_m, self.tile_hdim)
190
+ )
191
+ else:
192
+ # TODO: this is hard-coded for hdim 128
193
+ self.sdQ_layout = sm100_utils_basic.make_smem_layout_epi(
194
+ self.dtype, LayoutEnum.ROW_MAJOR, (self.tile_m, self.tile_hdim), 1
195
+ )
196
+
197
+ @cute.jit
198
+ def __call__(
199
+ self,
200
+ mdQaccum: cute.Tensor,
201
+ mdQ: cute.Tensor,
202
+ scale: cutlass.Float32,
203
+ stream: cuda.CUstream,
204
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
205
+ mSeqUsedQ: Optional[cute.Tensor] = None,
206
+ ):
207
+ # Get the data type and check if it is fp16 or bf16
208
+ if const_expr(mdQ.element_type not in [cutlass.Float16, cutlass.BFloat16]):
209
+ raise TypeError("Only Float16 or BFloat16 is supported")
210
+ if const_expr(mdQaccum is not None):
211
+ if const_expr(mdQaccum.element_type not in [cutlass.Float32]):
212
+ raise TypeError("dQaccum tensor must be Float32")
213
+
214
+ # Assume all strides are divisible by 128 bits except the last stride
215
+ new_stride = lambda t: (
216
+ *(cute.assume(s, divby=128 // t.element_type.width) for s in t.stride[:-1]),
217
+ t.stride[-1],
218
+ )
219
+ mdQaccum, mdQ = [
220
+ cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
221
+ for t in (mdQaccum, mdQ)
222
+ ]
223
+
224
+ self.tiled_mma = self._get_tiled_mma()
225
+ self._setup_attributes()
226
+
227
+ smem_size = max(
228
+ cute.size_in_bytes(cutlass.Float32, self.sdQaccum_layout),
229
+ cute.size_in_bytes(self.dtype, self.sdQ_layout),
230
+ )
231
+
232
+ if const_expr(mCuSeqlensQ is not None):
233
+ TileScheduler = SingleTileVarlenScheduler
234
+ num_head = mdQ.shape[1]
235
+ num_batch = mCuSeqlensQ.shape[0] - 1
236
+ num_block = cute.ceil_div(mdQ.shape[0], self.tile_m)
237
+ else:
238
+ TileScheduler = SingleTileScheduler
239
+ num_head = mdQ.shape[2]
240
+ num_batch = mdQ.shape[0]
241
+ num_block = cute.ceil_div(mdQ.shape[1], self.tile_m)
242
+
243
+ tile_sched_args = TileSchedulerArguments(
244
+ num_block=num_block,
245
+ num_head=num_head,
246
+ num_batch=num_batch,
247
+ num_splits=1,
248
+ seqlen_k=0,
249
+ headdim=mdQ.shape[2],
250
+ headdim_v=0,
251
+ total_q=mdQ.shape[0],
252
+ tile_shape_mn=(self.tile_m, 1),
253
+ mCuSeqlensQ=mCuSeqlensQ,
254
+ mSeqUsedQ=mSeqUsedQ,
255
+ )
256
+
257
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
258
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
259
+
260
+ # grid_dim: (m_block, num_head, batch_size)
261
+ self.kernel(
262
+ mdQaccum,
263
+ mdQ,
264
+ mCuSeqlensQ,
265
+ mSeqUsedQ,
266
+ scale,
267
+ self.tiled_mma,
268
+ self.dQ_swapAB,
269
+ self.sdQaccum_layout,
270
+ self.sdQ_layout,
271
+ self.g2s_tiled_copy_dQaccum,
272
+ self.s2r_tiled_copy_dQaccum,
273
+ self.gmem_tiled_copy_dQ,
274
+ tile_sched_params,
275
+ TileScheduler,
276
+ ).launch(
277
+ grid=grid_dim,
278
+ block=[self.num_threads, 1, 1],
279
+ smem=smem_size,
280
+ stream=stream,
281
+ )
282
+
283
+ @cute.kernel
284
+ def kernel(
285
+ self,
286
+ mdQaccum: cute.Tensor,
287
+ mdQ: cute.Tensor,
288
+ mCuSeqlensQ: Optional[cute.Tensor],
289
+ mSeqUsedQ: Optional[cute.Tensor],
290
+ scale: cutlass.Float32,
291
+ tiled_mma: cute.TiledMma,
292
+ dQ_swapAB: cutlass.Constexpr,
293
+ sdQaccum_layout: cute.Layout,
294
+ sdQ_layout: cute.ComposedLayout,
295
+ g2s_tiled_copy_dQaccum: cute.TiledCopy,
296
+ s2r_tiled_copy_dQaccum: cute.TiledCopy,
297
+ gmem_tiled_copy_dQ: cute.TiledCopy,
298
+ tile_sched_params: ParamsBase,
299
+ TileScheduler: cutlass.Constexpr[Callable],
300
+ ):
301
+ # ///////////////////////////////////////////////////////////////////////////////
302
+ # Get shared memory buffer
303
+ # ///////////////////////////////////////////////////////////////////////////////
304
+ smem = cutlass.utils.SmemAllocator()
305
+ sdQaccum = smem.allocate_tensor(cutlass.Float32, sdQaccum_layout, byte_alignment=1024)
306
+ sdQaccum_flat = cute.make_tensor(sdQaccum.iterator, cute.make_layout(cute.size(sdQaccum)))
307
+ if const_expr(self.arch in [80, 90]):
308
+ sdQ = cute.make_tensor(cute.recast_ptr(sdQaccum.iterator, dtype=self.dtype), sdQ_layout)
309
+ else:
310
+ # extra stage dimension
311
+ sdQ = cute.make_tensor(
312
+ cute.recast_ptr(sdQaccum.iterator, sdQ_layout.inner, dtype=self.dtype),
313
+ sdQ_layout.outer,
314
+ )[None, None, 0]
315
+ sdQt = utils.transpose_view(sdQ)
316
+
317
+ # Thread index, block index
318
+ tidx, _, _ = cute.arch.thread_idx()
319
+
320
+ tile_scheduler = TileScheduler.create(tile_sched_params)
321
+ work_tile = tile_scheduler.initial_work_tile_info()
322
+
323
+ m_block, head_idx, batch_idx, _ = work_tile.tile_idx
324
+
325
+ if work_tile.is_valid_tile:
326
+ # ///////////////////////////////////////////////////////////////////////////////
327
+ # Get the appropriate tiles for this thread block.
328
+ # ///////////////////////////////////////////////////////////////////////////////
329
+
330
+ seqlen = SeqlenInfoQK.create(
331
+ batch_idx,
332
+ mdQ.shape[1],
333
+ 0,
334
+ mCuSeqlensQ=mCuSeqlensQ,
335
+ mCuSeqlensK=None,
336
+ mSeqUsedQ=mSeqUsedQ,
337
+ mSeqUsedK=None,
338
+ )
339
+ if const_expr(not seqlen.has_cu_seqlens_q):
340
+ mdQ_cur = mdQ[batch_idx, None, head_idx, None]
341
+ mdQaccum_cur = mdQaccum[batch_idx, head_idx, None]
342
+ head_dim = mdQ.shape[3]
343
+ else:
344
+ padded_offset_q = seqlen.offset_q + batch_idx * self.tile_m
345
+ if cutlass.const_expr(self.arch >= 90):
346
+ padded_offset_q = padded_offset_q // self.tile_m * self.tile_m
347
+ mdQ_cur = cute.domain_offset((seqlen.offset_q, 0), mdQ[None, head_idx, None])
348
+ mdQaccum_cur = cute.domain_offset(
349
+ (padded_offset_q * self.tile_hdim,), mdQaccum[head_idx, None]
350
+ )
351
+ head_dim = mdQ.shape[2]
352
+
353
+ # HACK: Compiler doesn't seem to recognize that padding
354
+ # by padded_offset_q * self.tile_hdim keeps alignment
355
+ # since statically divisible by 4
356
+
357
+ mdQaccum_cur_ptr = cute.make_ptr(
358
+ dtype=mdQaccum_cur.element_type,
359
+ value=mdQaccum_cur.iterator.toint(),
360
+ mem_space=mdQaccum_cur.iterator.memspace,
361
+ assumed_align=mdQaccum.iterator.alignment,
362
+ )
363
+ mdQaccum_cur = cute.make_tensor(mdQaccum_cur_ptr, mdQaccum_cur.layout)
364
+
365
+ gdQaccum = cute.local_tile(mdQaccum_cur, (self.tile_m * self.tile_hdim,), (m_block,))
366
+ gdQ = cute.local_tile(mdQ_cur, (self.tile_m, self.tile_hdim), (m_block, 0))
367
+
368
+ seqlen_q = seqlen.seqlen_q
369
+ seqlen_q_rounded = cute.round_up(seqlen_q, self.tile_m)
370
+
371
+ # Step 1: load dQaccum from gmem to smem
372
+ g2s_thr_copy_dQaccum = g2s_tiled_copy_dQaccum.get_slice(tidx)
373
+ tdQgdQaccum = g2s_thr_copy_dQaccum.partition_S(gdQaccum)
374
+ tdQsdQaccumg2s = g2s_thr_copy_dQaccum.partition_D(sdQaccum_flat)
375
+ cute.copy(g2s_tiled_copy_dQaccum, tdQgdQaccum, tdQsdQaccumg2s)
376
+ cute.arch.cp_async_commit_group()
377
+ cute.arch.cp_async_wait_group(0)
378
+ cute.arch.barrier()
379
+
380
+ # Step 2: load dQ from smem to rmem
381
+ s2r_thr_copy_dQaccum = s2r_tiled_copy_dQaccum.get_slice(tidx)
382
+ tdQsdQaccum = s2r_thr_copy_dQaccum.partition_S(sdQaccum)
383
+ tile_shape = (self.tile_m, self.tile_hdim)
384
+ acc = None
385
+ tiled_copy_t2r = None
386
+ if const_expr(self.arch in [80, 90]):
387
+ acc_shape = tiled_mma.partition_shape_C(
388
+ tile_shape if const_expr(not dQ_swapAB) else tile_shape[::-1]
389
+ )
390
+ acc = cute.make_fragment(acc_shape, cutlass.Float32)
391
+ assert cute.size(acc) == cute.size(tdQsdQaccum)
392
+ else:
393
+ thr_mma = tiled_mma.get_slice(0) # 1-CTA
394
+ dQacc_shape = tiled_mma.partition_shape_C((self.tile_m, self.tile_hdim))
395
+ tdQtdQ = tiled_mma.make_fragment_C(dQacc_shape)
396
+ tdQcdQ = thr_mma.partition_C(
397
+ cute.make_identity_tensor((self.tile_m, self.tile_hdim))
398
+ )
399
+ tmem_load_atom = cute.make_copy_atom(
400
+ tcgen05.copy.Ld32x32bOp(tcgen05.copy.Repetition(self.dQ_reduce_ncol)), Float32
401
+ )
402
+ tiled_copy_t2r = tcgen05.make_tmem_copy(tmem_load_atom, tdQtdQ)
403
+ thr_copy_t2r = tiled_copy_t2r.get_slice(tidx)
404
+ tdQrdQ_t2r_shape = thr_copy_t2r.partition_D(tdQcdQ).shape
405
+ acc = cute.make_fragment(tdQrdQ_t2r_shape, Float32)
406
+ tdQrdQaccum = cute.make_tensor(acc.iterator, cute.make_layout(tdQsdQaccum.shape))
407
+ cute.autovec_copy(tdQsdQaccum, tdQrdQaccum)
408
+ # Convert tdQrdQaccum from fp32 to fp16/bf16
409
+ rdQ = cute.make_fragment_like(acc, self.dtype)
410
+ rdQ.store((acc.load() * scale).to(self.dtype))
411
+
412
+ # Step 3: Copy dQ from register to smem
413
+ cute.arch.barrier() # make sure all threads have finished loading dQaccum
414
+ if const_expr(self.arch in [80, 90]):
415
+ copy_atom_r2s_dQ = utils.get_smem_store_atom(
416
+ self.arch, self.dtype, transpose=self.dQ_swapAB
417
+ )
418
+ tiled_copy_r2s_dQ = cute.make_tiled_copy_C(copy_atom_r2s_dQ, tiled_mma)
419
+ else:
420
+ # copy_atom_r2s_dQ = sm100_utils_basic.get_smem_store_op(
421
+ # LayoutEnum.ROW_MAJOR, self.dtype, Float32, tiled_copy_t2r,
422
+ # )
423
+ # tiled_copy_r2s_dQ = cute.make_tiled_copy_D(copy_atom_r2s_dQ, tiled_copy_t2r)
424
+ thr_layout_r2s_dQ = cute.make_layout((self.num_threads, 1)) # 128 threads
425
+ val_layout_r2s_dQ = cute.make_layout((1, 128 // self.dtype.width))
426
+ copy_atom_r2s_dQ = cute.make_copy_atom(
427
+ cute.nvgpu.CopyUniversalOp(),
428
+ self.dtype,
429
+ num_bits_per_copy=128,
430
+ )
431
+ tiled_copy_r2s_dQ = cute.make_tiled_copy_tv(
432
+ copy_atom_r2s_dQ, thr_layout_r2s_dQ, val_layout_r2s_dQ
433
+ )
434
+ thr_copy_r2s_dQ = tiled_copy_r2s_dQ.get_slice(tidx)
435
+ cdQ = cute.make_identity_tensor((self.tile_m, self.tile_hdim))
436
+ if const_expr(self.arch in [80, 90]):
437
+ taccdQrdQ = thr_copy_r2s_dQ.retile(rdQ)
438
+ else:
439
+ taccdQcdQ_shape = thr_copy_r2s_dQ.partition_S(cdQ).shape
440
+ taccdQrdQ = cute.make_tensor(rdQ.iterator, taccdQcdQ_shape)
441
+ taccdQsdQ = thr_copy_r2s_dQ.partition_D(sdQ if const_expr(not self.dQ_swapAB) else sdQt)
442
+ cute.copy(thr_copy_r2s_dQ, taccdQrdQ, taccdQsdQ)
443
+
444
+ # Step 4: Copy dQ from smem to register to prepare for coalesced write to gmem
445
+ cute.arch.barrier() # make sure all smem stores are done
446
+ gmem_thr_copy_dQ = gmem_tiled_copy_dQ.get_slice(tidx)
447
+ tdQgdQ = gmem_thr_copy_dQ.partition_S(gdQ)
448
+ tdQsdQ = gmem_thr_copy_dQ.partition_D(sdQ)
449
+ tdQrdQ = cute.make_fragment_like(tdQsdQ, self.dtype)
450
+ # TODO: check OOB when reading from smem if kBlockM isn't evenly tiled
451
+ cute.autovec_copy(tdQsdQ, tdQrdQ)
452
+
453
+ # Step 5: Copy dQ from register to gmem
454
+ tdQcdQ = gmem_thr_copy_dQ.partition_S(cdQ)
455
+ tdQpdQ = utils.predicate_k(tdQcdQ, limit=head_dim)
456
+ for rest_m in cutlass.range(cute.size(tdQrdQ.shape[1]), unroll_full=True):
457
+ if tdQcdQ[0, rest_m, 0][0] < seqlen_q - m_block * self.tile_m:
458
+ cute.copy(
459
+ gmem_tiled_copy_dQ,
460
+ tdQrdQ[None, rest_m, None],
461
+ tdQgdQ[None, rest_m, None],
462
+ pred=tdQpdQ[None, rest_m, None],
463
+ )
kernel/deps/flash_attn/cute/flash_bwd_preprocess.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_bwd_preprocess_kernel.h
3
+ # from Cutlass C++ to Cute-DSL.
4
+ import math
5
+ import operator
6
+ from typing import Callable, Type, Optional, Literal
7
+
8
+ import cuda.bindings.driver as cuda
9
+
10
+ import cutlass
11
+ import cutlass.cute as cute
12
+ from cutlass import Float32
13
+
14
+ from flash_attn.cute import utils
15
+ from flash_attn.cute import copy_utils
16
+ from flash_attn.cute.seqlen_info import SeqlenInfoQK
17
+ from flash_attn.cute.tile_scheduler import (
18
+ ParamsBase,
19
+ SingleTileScheduler,
20
+ SingleTileVarlenScheduler,
21
+ TileSchedulerArguments,
22
+ )
23
+
24
+
25
+ class FlashAttentionBackwardPreprocess:
26
+ def __init__(
27
+ self,
28
+ dtype: Type[cutlass.Numeric],
29
+ head_dim: int,
30
+ arch: Literal[80, 90, 100],
31
+ m_block_size: int = 128,
32
+ num_threads: int = 128,
33
+ ):
34
+ """
35
+ All contiguous dimensions must be at least 16 bytes aligned which indicates the head dimension
36
+ should be a multiple of 8.
37
+
38
+ :param head_dim: head dimension
39
+ :type head_dim: int
40
+ :param m_block_size: m block size
41
+ :type m_block_size: int
42
+ :param num_threads: number of threads
43
+ :type num_threads: int
44
+ """
45
+ self.dtype = dtype
46
+ self.m_block_size = m_block_size
47
+ self.arch = arch
48
+ # padding head_dim to a multiple of 32 as k_block_size
49
+ hdim_multiple_of = 32
50
+ self.head_dim_padded = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
51
+ self.check_hdim_oob = head_dim != self.head_dim_padded
52
+ self.num_threads = num_threads
53
+
54
+ @staticmethod
55
+ def can_implement(dtype, head_dim, m_block_size, num_threads) -> bool:
56
+ """Check if the kernel can be implemented with the given parameters.
57
+
58
+ :param dtype: data type
59
+ :type dtype: cutlass.Numeric
60
+ :param head_dim: head dimension
61
+ :type head_dim: int
62
+ :param m_block_size: m block size
63
+ :type m_block_size: int
64
+ :param num_threads: number of threads
65
+ :type num_threads: int
66
+
67
+ :return: True if the kernel can be implemented, False otherwise
68
+ :rtype: bool
69
+ """
70
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
71
+ return False
72
+ if head_dim % 8 != 0:
73
+ return False
74
+ if num_threads % 32 != 0:
75
+ return False
76
+ if num_threads < m_block_size: # For multiplying lse with log2
77
+ return False
78
+ return True
79
+
80
+ def _setup_attributes(self):
81
+ # ///////////////////////////////////////////////////////////////////////////////
82
+ # GMEM Tiled copy:
83
+ # ///////////////////////////////////////////////////////////////////////////////
84
+ # Thread layouts for copies
85
+ # We want kBlockKGmem to be a power of 2 so that when we do the summing,
86
+ # it's just between threads in the same warp
87
+ gmem_k_block_size = (
88
+ 128
89
+ if self.head_dim_padded % 128 == 0
90
+ else (
91
+ 64
92
+ if self.head_dim_padded % 64 == 0
93
+ else (32 if self.head_dim_padded % 32 == 0 else 16)
94
+ )
95
+ )
96
+ self.gmem_tiled_copy_O = copy_utils.tiled_copy_2d(
97
+ self.dtype, gmem_k_block_size, self.num_threads
98
+ )
99
+ universal_copy_bits = 128
100
+ num_copy_elems_dQaccum = universal_copy_bits // Float32.width
101
+ assert (
102
+ self.m_block_size * self.head_dim_padded // num_copy_elems_dQaccum
103
+ ) % self.num_threads == 0
104
+ self.gmem_tiled_copy_dQaccum = copy_utils.tiled_copy_1d(
105
+ Float32, self.num_threads, num_copy_elems_dQaccum
106
+ )
107
+
108
+ @cute.jit
109
+ def __call__(
110
+ self,
111
+ mO: cute.Tensor,
112
+ mdO: cute.Tensor,
113
+ mdPsum: cute.Tensor,
114
+ stream: cuda.CUstream,
115
+ mLSE: Optional[cute.Tensor] = None,
116
+ mLSElog2: Optional[cute.Tensor] = None,
117
+ mdQaccum: Optional[cute.Tensor] = None,
118
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
119
+ mSeqUsedQ: Optional[cute.Tensor] = None,
120
+ ):
121
+ # Get the data type and check if it is fp16 or bf16
122
+ if cutlass.const_expr(not (mO.element_type == mdO.element_type)):
123
+ raise TypeError("All tensors must have the same data type")
124
+ if cutlass.const_expr(mO.element_type not in [cutlass.Float16, cutlass.BFloat16]):
125
+ raise TypeError("Only Float16 or BFloat16 is supported")
126
+ if cutlass.const_expr(mdPsum.element_type not in [Float32]):
127
+ raise TypeError("dPsum tensor must be Float32")
128
+ if cutlass.const_expr(mdQaccum is not None):
129
+ if cutlass.const_expr(mdQaccum.element_type not in [Float32]):
130
+ raise TypeError("dQaccum tensor must be Float32")
131
+ if cutlass.const_expr(mLSE is not None):
132
+ assert mLSElog2 is not None, "If mLSE is provided, mLSElog2 must also be provided"
133
+ if cutlass.const_expr(mLSE.element_type not in [Float32]):
134
+ raise TypeError("LSE tensor must be Float32")
135
+ if cutlass.const_expr(mLSElog2.element_type not in [Float32]):
136
+ raise TypeError("LSElog2 tensor must be Float32")
137
+
138
+ # Assume all strides are divisible by 128 bits except the last stride
139
+ new_stride = lambda t: (
140
+ *(cute.assume(s, divby=128 // t.element_type.width) for s in t.stride[:-1]),
141
+ t.stride[-1],
142
+ )
143
+ mO, mdO, mdQaccum = [
144
+ cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
145
+ if t is not None
146
+ else None
147
+ for t in (mO, mdO, mdQaccum)
148
+ ]
149
+
150
+ self._setup_attributes()
151
+
152
+ if cutlass.const_expr(mCuSeqlensQ is not None):
153
+ TileScheduler = SingleTileVarlenScheduler
154
+ num_head = mO.shape[1]
155
+ num_batch = mCuSeqlensQ.shape[0] - 1
156
+ else:
157
+ TileScheduler = SingleTileScheduler
158
+ num_head = mO.shape[2]
159
+ num_batch = mO.shape[0]
160
+
161
+ tile_sched_args = TileSchedulerArguments(
162
+ num_block=cute.ceil_div(mO.shape[1], self.m_block_size),
163
+ num_head=num_head,
164
+ num_batch=num_batch,
165
+ num_splits=1,
166
+ seqlen_k=0,
167
+ headdim=0,
168
+ headdim_v=mO.shape[2],
169
+ total_q=mO.shape[0],
170
+ tile_shape_mn=(self.m_block_size, 1),
171
+ mCuSeqlensQ=mCuSeqlensQ,
172
+ mSeqUsedQ=mSeqUsedQ,
173
+ )
174
+
175
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
176
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
177
+
178
+ self.kernel(
179
+ mO,
180
+ mdO,
181
+ mdPsum,
182
+ mLSE,
183
+ mLSElog2,
184
+ mdQaccum,
185
+ mCuSeqlensQ,
186
+ mSeqUsedQ,
187
+ self.gmem_tiled_copy_O,
188
+ self.gmem_tiled_copy_dQaccum,
189
+ tile_sched_params,
190
+ TileScheduler,
191
+ ).launch(
192
+ grid=grid_dim,
193
+ block=[self.num_threads, 1, 1],
194
+ stream=stream,
195
+ )
196
+
197
+ @cute.kernel
198
+ def kernel(
199
+ self,
200
+ mO: cute.Tensor,
201
+ mdO: cute.Tensor,
202
+ mdPsum: cute.Tensor,
203
+ mLSE: Optional[cute.Tensor],
204
+ mLSElog2: Optional[cute.Tensor],
205
+ mdQaccum: Optional[cute.Tensor],
206
+ mCuSeqlensQ: Optional[cute.Tensor],
207
+ mSeqUsedQ: Optional[cute.Tensor],
208
+ gmem_tiled_copy_O: cute.TiledCopy,
209
+ gmem_tiled_copy_dQaccum: cute.TiledCopy,
210
+ tile_sched_params: ParamsBase,
211
+ TileScheduler: cutlass.Constexpr[Callable],
212
+ ):
213
+ # Thread index, block index
214
+ tidx, _, _ = cute.arch.thread_idx()
215
+
216
+ tile_scheduler = TileScheduler.create(tile_sched_params)
217
+ work_tile = tile_scheduler.initial_work_tile_info()
218
+ m_block, head_idx, batch_idx, _ = work_tile.tile_idx
219
+
220
+ if work_tile.is_valid_tile:
221
+ # ///////////////////////////////////////////////////////////////////////////////
222
+ # Get the appropriate tiles for this thread block.
223
+ # ///////////////////////////////////////////////////////////////////////////////
224
+ seqlen = SeqlenInfoQK.create(
225
+ batch_idx,
226
+ mO.shape[1],
227
+ 0,
228
+ mCuSeqlensQ=mCuSeqlensQ,
229
+ mCuSeqlensK=None,
230
+ mSeqUsedQ=mSeqUsedQ,
231
+ mSeqUsedK=None,
232
+ )
233
+
234
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
235
+ mO_cur = mO[batch_idx, None, head_idx, None]
236
+ mdO_cur = mdO[batch_idx, None, head_idx, None]
237
+ mdPsum_cur = mdPsum[batch_idx, head_idx, None]
238
+ headdim_v = mO.shape[3]
239
+ else:
240
+ mO_cur = cute.domain_offset((seqlen.offset_q, 0), mO[None, head_idx, None])
241
+ mdO_cur = cute.domain_offset((seqlen.offset_q, 0), mdO[None, head_idx, None])
242
+
243
+ padded_offset_q = seqlen.offset_q + batch_idx * self.m_block_size
244
+ if cutlass.const_expr(self.arch >= 90):
245
+ padded_offset_q = padded_offset_q // self.m_block_size * self.m_block_size
246
+ mdPsum_cur = cute.domain_offset((padded_offset_q,), mdPsum[head_idx, None])
247
+ headdim_v = mO.shape[2]
248
+
249
+ blkOdO_shape = (self.m_block_size, self.head_dim_padded)
250
+ # (m_block_size, head_dim)
251
+ gO = cute.local_tile(mO_cur, blkOdO_shape, (m_block, 0))
252
+ gdO = cute.local_tile(mdO_cur, blkOdO_shape, (m_block, 0))
253
+
254
+ gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
255
+ # (CPY_Atom, CPY_M, CPY_K)
256
+ tOgO = gmem_thr_copy_O.partition_S(gO)
257
+ tOgdO = gmem_thr_copy_O.partition_S(gdO)
258
+
259
+ # ///////////////////////////////////////////////////////////////////////////////
260
+ # Predicate: Mark indices that need to copy when problem_shape isn't a multiple
261
+ # of tile_shape
262
+ # ///////////////////////////////////////////////////////////////////////////////
263
+ # Construct identity layout for KV
264
+ cO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
265
+ tOcO = gmem_thr_copy_O.partition_S(cO)
266
+ t0OcO = gmem_thr_copy_O.get_slice(0).partition_S(cO)
267
+ tOpO = utils.predicate_k(tOcO, limit=headdim_v)
268
+ tOpdO = utils.predicate_k(tOcO, limit=headdim_v)
269
+
270
+ seqlen_q = seqlen.seqlen_q
271
+ seqlen_q_rounded = cute.round_up(seqlen_q, self.m_block_size)
272
+
273
+ if cutlass.const_expr(mLSE is not None):
274
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
275
+ mLSE_cur = mLSE[batch_idx, head_idx, None]
276
+ else:
277
+ mLSE_cur = cute.domain_offset((seqlen.offset_q,), mLSE[head_idx, None])
278
+
279
+ gLSE = cute.local_tile(mLSE_cur, (self.m_block_size,), (m_block,))
280
+ lse = Float32.inf
281
+ if tidx < seqlen_q - m_block * self.m_block_size:
282
+ lse = gLSE[tidx]
283
+
284
+ tOrO = cute.make_fragment_like(tOgO)
285
+ tOrdO = cute.make_fragment_like(tOgdO)
286
+ assert cute.size(tOgO, mode=[0]) == cute.size(tOgdO, mode=[0])
287
+ assert cute.size(tOgO, mode=[1]) == cute.size(tOgdO, mode=[1])
288
+ assert cute.size(tOgO, mode=[2]) == cute.size(tOgdO, mode=[2])
289
+ for m in cutlass.range(cute.size(tOrO.shape[1]), unroll_full=True):
290
+ # Instead of using tOcO, we using t0OcO and subtract the offset from the limit
291
+ # (seqlen_q - m_block * kBlockM). This is because the entries of t0OcO are known at compile time.
292
+ if t0OcO[0, m, 0][0] < seqlen_q - m_block * self.m_block_size - tOcO[0][0]:
293
+ cute.copy(
294
+ gmem_thr_copy_O,
295
+ tOgO[None, m, None],
296
+ tOrO[None, m, None],
297
+ pred=tOpO[None, m, None]
298
+ if cutlass.const_expr(self.check_hdim_oob)
299
+ else None,
300
+ )
301
+ cute.copy(
302
+ gmem_thr_copy_O,
303
+ tOgdO[None, m, None],
304
+ tOrdO[None, m, None],
305
+ pred=tOpdO[None, m, None]
306
+ if cutlass.const_expr(self.check_hdim_oob)
307
+ else None,
308
+ )
309
+ # Sum across the "k" dimension
310
+ dpsum = (tOrO.load().to(Float32) * tOrdO.load().to(Float32)).reduce(
311
+ cute.ReductionOp.ADD, init_val=0.0, reduction_profile=(0, None, 1)
312
+ )
313
+ threads_per_row = gmem_tiled_copy_O.layout_src_tv_tiled[0].shape[0]
314
+ assert cute.arch.WARP_SIZE % threads_per_row == 0
315
+ dpsum = utils.warp_reduce(dpsum, operator.add, width=threads_per_row)
316
+ dP_sum = cute.make_fragment(cute.size(tOrO, mode=[1]), Float32)
317
+ dP_sum.store(dpsum)
318
+
319
+ # Write dPsum from rmem -> gmem
320
+ gdPsum = cute.local_tile(mdPsum_cur, (self.m_block_size,), (m_block,))
321
+ # Only the thread corresponding to column 0 writes out the dPsum to gmem
322
+ if tOcO[0, 0, 0][1] == 0:
323
+ for m in cutlass.range(cute.size(dP_sum), unroll_full=True):
324
+ row = tOcO[0, m, 0][0]
325
+ gdPsum[row] = dP_sum[m] if row < seqlen_q - m_block * self.m_block_size else 0.0
326
+
327
+ # Clear dQaccum
328
+ if cutlass.const_expr(mdQaccum is not None):
329
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
330
+ mdQaccum_cur = mdQaccum[batch_idx, head_idx, None]
331
+ else:
332
+ mdQaccum_cur = cute.domain_offset(
333
+ (padded_offset_q * self.head_dim_padded,), mdQaccum[head_idx, None]
334
+ )
335
+
336
+ # HACK: Compiler doesn't seem to recognize that padding
337
+ # by padded_offset_q * self.head_dim_padded keeps alignment
338
+ # since statically divisible by 4
339
+
340
+ mdQaccum_cur_ptr = cute.make_ptr(
341
+ dtype=mdQaccum_cur.element_type,
342
+ value=mdQaccum_cur.iterator.toint(),
343
+ mem_space=mdQaccum_cur.iterator.memspace,
344
+ assumed_align=mdQaccum.iterator.alignment,
345
+ )
346
+ mdQaccum_cur = cute.make_tensor(mdQaccum_cur_ptr, mdQaccum_cur.layout)
347
+
348
+ blkdQaccum_shape = (self.m_block_size * self.head_dim_padded,)
349
+ gdQaccum = cute.local_tile(mdQaccum_cur, blkdQaccum_shape, (m_block,))
350
+ gmem_thr_copy_dQaccum = gmem_tiled_copy_dQaccum.get_slice(tidx)
351
+ tdQgdQaccum = gmem_thr_copy_dQaccum.partition_S(gdQaccum)
352
+ zero = cute.make_fragment_like(tdQgdQaccum)
353
+ zero.fill(0.0)
354
+ cute.copy(gmem_tiled_copy_dQaccum, zero, tdQgdQaccum)
355
+
356
+ if cutlass.const_expr(mLSE is not None):
357
+ if cutlass.const_expr(not seqlen.has_cu_seqlens_q):
358
+ mLSElog2_cur = mLSElog2[batch_idx, head_idx, None]
359
+ else:
360
+ mLSElog2_cur = cute.domain_offset((padded_offset_q,), mLSElog2[head_idx, None])
361
+
362
+ gLSElog2 = cute.local_tile(mLSElog2_cur, (self.m_block_size,), (m_block,))
363
+ LOG2_E = math.log2(math.e)
364
+ if tidx < seqlen_q_rounded - m_block * self.m_block_size:
365
+ gLSElog2[tidx] = lse * LOG2_E if lse != -Float32.inf else 0.0
kernel/deps/flash_attn/cute/flash_bwd_sm100.py ADDED
The diff for this file is too large to render. See raw diff
 
kernel/deps/flash_attn/cute/flash_bwd_sm90.py ADDED
@@ -0,0 +1,1708 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Callable, Optional, Type
3
+ from functools import partial
4
+
5
+ import cuda.bindings.driver as cuda
6
+
7
+ import cutlass
8
+ import cutlass.cute as cute
9
+ import cutlass.utils.hopper_helpers as sm90_utils_basic
10
+ from cutlass.cute.nvgpu import cpasync, warpgroup
11
+ from cutlass.cute.arch import ProxyKind, SharedSpace
12
+ from cutlass.cute import FastDivmodDivisor
13
+ from cutlass import Float32, Int32, Boolean, const_expr
14
+ from cutlass.utils import LayoutEnum
15
+
16
+ from flash_attn.cute import hopper_helpers as sm90_utils
17
+ from flash_attn.cute import utils
18
+ from flash_attn.cute import copy_utils
19
+ from flash_attn.cute.hopper_helpers import gemm_zero_init, gemm_w_idx
20
+ from flash_attn.cute.mask import AttentionMask
21
+ from flash_attn.cute.seqlen_info import SeqlenInfoQK
22
+ from flash_attn.cute.block_info import BlockInfo
23
+ from flash_attn.cute import pipeline
24
+ from flash_attn.cute.tile_scheduler import TileSchedulerArguments, SingleTileScheduler, ParamsBase
25
+ from flash_attn.cute.named_barrier import NamedBarrierFwd, NamedBarrierBwd
26
+ from flash_attn.cute.softmax import apply_score_mod_inner, apply_score_mod_bwd_inner
27
+ from flash_attn.cute.block_sparsity import BlockSparseTensors
28
+ from flash_attn.cute.block_sparse_utils import (
29
+ get_total_q_block_count_bwd,
30
+ produce_block_sparse_q_loads_bwd_sm90,
31
+ consume_block_sparse_mma_bwd_sm90,
32
+ dQaccum_store_block_sparse_bwd_sm90,
33
+ )
34
+
35
+
36
+ def mma_partition_fragment_AB(
37
+ thr_mma: cute.core.ThrMma, sA: Optional[cute.Tensor], sB: Optional[cute.Tensor], swap_AB: bool
38
+ ):
39
+ if const_expr(not swap_AB):
40
+ return (
41
+ thr_mma.make_fragment_A(thr_mma.partition_A(sA)) if sA is not None else None,
42
+ thr_mma.make_fragment_B(thr_mma.partition_B(sB)) if sB is not None else None,
43
+ )
44
+ else:
45
+ return (
46
+ thr_mma.make_fragment_B(thr_mma.partition_B(sA)) if sA is not None else None,
47
+ thr_mma.make_fragment_A(thr_mma.partition_A(sB)) if sB is not None else None,
48
+ )
49
+
50
+
51
+ class FlashAttentionBackwardSm90:
52
+ arch = 90
53
+
54
+ def __init__(
55
+ self,
56
+ dtype: Type[cutlass.Numeric],
57
+ head_dim: int,
58
+ head_dim_v: Optional[int] = None,
59
+ qhead_per_kvhead: int = 1,
60
+ is_causal: bool = False,
61
+ tile_m: int = 64,
62
+ tile_n: int = 128,
63
+ Q_stage: int = 2,
64
+ dO_stage: int = 2,
65
+ PdS_stage: int = 2,
66
+ SdP_swapAB: bool = False,
67
+ dKV_swapAB: bool = False,
68
+ dQ_swapAB: bool = False,
69
+ AtomLayoutMSdP: int = 1,
70
+ AtomLayoutNdKV: int = 2,
71
+ AtomLayoutMdQ: int = 1,
72
+ num_threads: int = 384,
73
+ V_in_regs: bool = False,
74
+ score_mod: cutlass.Constexpr | None = None,
75
+ score_mod_bwd: cutlass.Constexpr | None = None,
76
+ mask_mod: cutlass.Constexpr | None = None,
77
+ has_aux_tensors: cutlass.Constexpr = False,
78
+ subtile_factor: cutlass.Constexpr[int] = 1,
79
+ ):
80
+ self.dtype = dtype
81
+ # padding head_dim to a multiple of 16 as k_block_size
82
+ hdim_multiple_of = 16
83
+ self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
84
+ head_dim_v = head_dim_v if head_dim_v is not None else head_dim
85
+ self.same_hdim_kv = head_dim == head_dim_v
86
+ self.tile_hdimv = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
87
+ # Can save registers (and hence be faster) if we don't have to check hdim predication
88
+ self.check_hdim_oob = head_dim != self.tile_hdim
89
+ self.check_hdim_v_oob = head_dim_v != self.tile_hdimv
90
+ self.qhead_per_kvhead = qhead_per_kvhead
91
+ self.is_causal = is_causal
92
+ self.is_local = False
93
+ self.tile_m = tile_m
94
+ self.tile_n = tile_n
95
+ self.num_threads = num_threads
96
+ self.Q_stage = Q_stage
97
+ self.dO_stage = dO_stage
98
+ self.PdS_stage = PdS_stage
99
+ assert self.dO_stage in [1, self.Q_stage]
100
+ assert self.PdS_stage in [1, self.Q_stage]
101
+ self.SdP_swapAB = SdP_swapAB
102
+ self.dKV_swapAB = dKV_swapAB
103
+ self.dQ_swapAB = dQ_swapAB
104
+ self.AtomLayoutMSdP = AtomLayoutMSdP
105
+ self.AtomLayoutNdKV = AtomLayoutNdKV
106
+ self.AtomLayoutMdQ = AtomLayoutMdQ
107
+ self.num_mma_warp_groups = (self.num_threads // 128) - 1
108
+ self.mma_dkv_is_rs = (
109
+ AtomLayoutMSdP == 1
110
+ and AtomLayoutNdKV == self.num_mma_warp_groups
111
+ and SdP_swapAB
112
+ and not dKV_swapAB
113
+ )
114
+ self.V_in_regs = V_in_regs
115
+ if qhead_per_kvhead > 1:
116
+ assert self.same_hdim_kv, "GQA backward requires head_dim == head_dim_v"
117
+ assert self.num_mma_warp_groups == 2, "GQA backward assumes 2 warp groups"
118
+ # These are tuned for speed
119
+ # Do we keep the LSE and dPsum in each thread, or split them across 8 threads that share
120
+ # them and then shuffle to get the value whenever we need? This can reduce register
121
+ # pressure when SdP_swapAB, where each thread needs to keep statistics for (kBlockM / 4)
122
+ # rows. If !SdP_swapAB, each thread only needs to keep statistics for 2 rows.
123
+ # TODO: impl these for hdim 64
124
+ self.shuffle_LSE = self.SdP_swapAB and self.tile_hdim <= 64
125
+ self.shuffle_dPsum = self.SdP_swapAB and self.tile_hdim <= 64
126
+
127
+ self.score_mod = score_mod
128
+ self.score_mod_bwd = score_mod_bwd
129
+ self.mask_mod = mask_mod
130
+ self.has_aux_tensors = has_aux_tensors
131
+ self.subtile_factor = subtile_factor
132
+ if cutlass.const_expr(has_aux_tensors):
133
+ self.vec_size: cutlass.Constexpr = 1
134
+ else:
135
+ self.vec_size: cutlass.Constexpr = 4
136
+ self.qk_acc_dtype = Float32
137
+
138
+ @staticmethod
139
+ def can_implement(
140
+ dtype,
141
+ head_dim,
142
+ head_dim_v,
143
+ tile_m,
144
+ tile_n,
145
+ Q_stage,
146
+ num_threads,
147
+ V_in_regs=False,
148
+ ) -> bool:
149
+ if dtype not in [cutlass.Float16, cutlass.BFloat16]:
150
+ return False
151
+ if head_dim % 8 != 0:
152
+ return False
153
+ if head_dim_v % 8 != 0:
154
+ return False
155
+ if tile_n % 16 != 0:
156
+ return False
157
+ if num_threads % 32 != 0:
158
+ return False
159
+ if (tile_m * 2) % num_threads != 0:
160
+ return False
161
+ return True
162
+
163
+ def _check_type(
164
+ self,
165
+ mQ_type: Type[cutlass.Numeric],
166
+ mK_type: Type[cutlass.Numeric],
167
+ mV_type: Type[cutlass.Numeric],
168
+ mdO_type: Type[cutlass.Numeric],
169
+ mLSE_type: Type[cutlass.Numeric],
170
+ mdPsum_type: Type[cutlass.Numeric],
171
+ mdQaccum_type: Type[cutlass.Numeric],
172
+ mdK_type: Type[cutlass.Numeric],
173
+ mdV_type: Type[cutlass.Numeric],
174
+ ):
175
+ # Get the data type and check if it is fp16 or bf16
176
+ if const_expr(not (mQ_type == mK_type == mV_type == mdO_type)):
177
+ raise TypeError("All tensors must have the same data type")
178
+ if const_expr(mQ_type not in [cutlass.Float16, cutlass.BFloat16]):
179
+ raise TypeError("Only Float16 or BFloat16 is supported")
180
+ if const_expr(mLSE_type not in [Float32]):
181
+ raise TypeError("LSE tensor must be Float32")
182
+ if const_expr(mdPsum_type not in [Float32]):
183
+ raise TypeError("dPsum tensor must be Float32")
184
+ if const_expr(mdQaccum_type not in [Float32]):
185
+ raise TypeError("dQaccum tensor must be Float32")
186
+ if const_expr(self.qhead_per_kvhead == 1):
187
+ if const_expr(not (mdK_type == mdV_type == mQ_type)):
188
+ raise TypeError("mdK and mdV tensors must have the same data type as mQ")
189
+ else:
190
+ if const_expr(not (mdK_type == mdV_type == Float32)):
191
+ raise TypeError("mdKaccum and mdVaccum tensors must have the data type Float32")
192
+ assert mQ_type == self.dtype
193
+
194
+ def _setup_attributes(self):
195
+ self.sQ_layout, self.sK_layout, self.sV_layout, self.sdO_layout, self.sPdS_layout = [
196
+ sm90_utils.make_smem_layout(self.dtype, LayoutEnum.ROW_MAJOR, shape, stage)
197
+ for shape, stage in [
198
+ ((self.tile_m, self.tile_hdim), self.Q_stage),
199
+ ((self.tile_n, self.tile_hdim), None),
200
+ ((self.tile_n, self.tile_hdimv), None),
201
+ ((self.tile_m, self.tile_hdimv), self.dO_stage),
202
+ ((self.tile_m, self.tile_n), self.PdS_stage),
203
+ ]
204
+ ]
205
+ self.sdQaccum_layout = cute.make_layout(
206
+ (self.tile_m * self.tile_hdim // self.num_mma_warp_groups, self.num_mma_warp_groups)
207
+ )
208
+ # dQaccum R->S
209
+ self.r2s_tiled_copy_dQaccum = cute.make_tiled_copy_tv(
210
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
211
+ # thr_layout
212
+ cute.make_layout((self.num_threads_per_warp_group, self.num_mma_warp_groups)),
213
+ cute.make_layout(128 // Float32.width), # val_layout
214
+ )
215
+ # dKVaccum for GQA epilogue - reuses sV+sK memory recast as f32
216
+ self.sdKVaccum_layout = cute.make_layout(
217
+ (self.tile_n * self.tile_hdim // self.num_mma_warp_groups, self.num_mma_warp_groups)
218
+ )
219
+ # dKVaccum R->S (same pattern as dQaccum but sized for tile_n)
220
+ self.r2s_tiled_copy_dKVaccum = cute.make_tiled_copy_tv(
221
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32, num_bits_per_copy=128),
222
+ cute.make_layout((self.num_threads_per_warp_group, self.num_mma_warp_groups)),
223
+ cute.make_layout(128 // Float32.width),
224
+ )
225
+
226
+ def _get_tiled_mma(self):
227
+ # S = Q @ K.T, dP = dO @ V.T
228
+ atom_layout_SdP = (self.AtomLayoutMSdP, self.num_mma_warp_groups // self.AtomLayoutMSdP)
229
+ tiler_mn_SdP = (self.tile_m // atom_layout_SdP[0], self.tile_n // atom_layout_SdP[1])
230
+ tiled_mma_SdP = sm90_utils_basic.make_trivial_tiled_mma(
231
+ self.dtype,
232
+ self.dtype,
233
+ warpgroup.OperandMajorMode.K,
234
+ warpgroup.OperandMajorMode.K,
235
+ Float32,
236
+ atom_layout_mnk=(atom_layout_SdP if not self.SdP_swapAB else atom_layout_SdP[::-1])
237
+ + (1,),
238
+ tiler_mn=tiler_mn_SdP if not self.SdP_swapAB else tiler_mn_SdP[::-1],
239
+ )
240
+ # dV = P.T @ dO, dK = dS.T @ Q
241
+ atom_layout_dKV = (self.AtomLayoutNdKV, self.num_mma_warp_groups // self.AtomLayoutNdKV)
242
+ tiler_mn_dK = (self.tile_n // atom_layout_dKV[0], self.tile_hdim // atom_layout_dKV[1])
243
+ tiler_mn_dV = (self.tile_n // atom_layout_dKV[0], self.tile_hdimv // atom_layout_dKV[1])
244
+ tiled_mma_dK, tiled_mma_dV = [
245
+ sm90_utils_basic.make_trivial_tiled_mma(
246
+ self.dtype,
247
+ self.dtype,
248
+ warpgroup.OperandMajorMode.MN
249
+ if not self.mma_dkv_is_rs
250
+ else warpgroup.OperandMajorMode.K,
251
+ warpgroup.OperandMajorMode.MN,
252
+ Float32,
253
+ atom_layout_mnk=(atom_layout_dKV if not self.dKV_swapAB else atom_layout_dKV[::-1])
254
+ + (1,),
255
+ tiler_mn=tiler_mn_d if not self.dKV_swapAB else tiler_mn_d[::-1],
256
+ a_source=warpgroup.OperandSource.RMEM
257
+ if self.mma_dkv_is_rs
258
+ else warpgroup.OperandSource.SMEM,
259
+ )
260
+ for tiler_mn_d in (tiler_mn_dK, tiler_mn_dV)
261
+ ]
262
+ # dQ = dS @ K
263
+ atom_layout_dQ = (self.AtomLayoutMdQ, self.num_mma_warp_groups // self.AtomLayoutMdQ)
264
+ tiler_mn_dQ = (self.tile_m // atom_layout_dQ[0], self.tile_hdim // atom_layout_dQ[1])
265
+ tiled_mma_dQ = sm90_utils_basic.make_trivial_tiled_mma(
266
+ self.dtype,
267
+ self.dtype,
268
+ warpgroup.OperandMajorMode.K if not self.dQ_swapAB else warpgroup.OperandMajorMode.MN,
269
+ warpgroup.OperandMajorMode.MN if not self.dQ_swapAB else warpgroup.OperandMajorMode.K,
270
+ Float32,
271
+ atom_layout_mnk=(atom_layout_dQ if not self.dQ_swapAB else atom_layout_dQ[::-1]) + (1,),
272
+ tiler_mn=tiler_mn_dQ if not self.dQ_swapAB else tiler_mn_dQ[::-1],
273
+ )
274
+ return tiled_mma_SdP, tiled_mma_dK, tiled_mma_dV, tiled_mma_dQ
275
+
276
+ def _get_shared_storage_cls(self):
277
+ sQ_alignment = sK_alignment = sV_alighment = sdQaccum_alignment = sdO_alignment = 1024
278
+
279
+ sQ_struct, sK_struct, sV_struct, sdO_struct, sdQaccum_struct = [
280
+ cute.struct.Align[cute.struct.MemRange[type, cute.cosize(layout)], alignment]
281
+ for (layout, type, alignment) in [
282
+ (self.sQ_layout, self.dtype, sQ_alignment),
283
+ (self.sK_layout, self.dtype, sK_alignment),
284
+ (self.sV_layout, self.dtype, sV_alighment),
285
+ (self.sdO_layout, self.dtype, sdO_alignment),
286
+ (self.sdQaccum_layout, Float32, sdQaccum_alignment),
287
+ ]
288
+ ]
289
+
290
+ cosize_sdS = cute.cosize(self.sPdS_layout)
291
+ cosize_sP = cute.cosize(self.sPdS_layout) if const_expr(not self.mma_dkv_is_rs) else 0
292
+ sLSE_struct = cute.struct.Align[
293
+ cute.struct.MemRange[Float32, cute.round_up(self.tile_m, 64) * self.Q_stage], 128
294
+ ]
295
+ sdPsum_struct = cute.struct.Align[
296
+ cute.struct.MemRange[Float32, cute.round_up(self.tile_m, 64) * self.dO_stage], 128
297
+ ]
298
+
299
+ @cute.struct
300
+ class SharedStorageQKV:
301
+ mbar_ptr_Q: cute.struct.MemRange[cutlass.Int64, self.Q_stage * 2]
302
+ mbar_ptr_dO: cute.struct.MemRange[cutlass.Int64, self.dO_stage * 2]
303
+ sLSE: sLSE_struct
304
+ sdPsum: sdPsum_struct
305
+ sQ: sQ_struct
306
+ sV: sV_struct
307
+ sK: sK_struct
308
+ sdO: sdO_struct
309
+ sP: cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sP], 1024]
310
+ sdS: cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sdS], 1024]
311
+ sdQaccum: sdQaccum_struct
312
+
313
+ return SharedStorageQKV
314
+
315
+ @cute.jit
316
+ def __call__(
317
+ self,
318
+ mQ: cute.Tensor,
319
+ mK: cute.Tensor,
320
+ mV: cute.Tensor,
321
+ mdO: cute.Tensor,
322
+ mLSE: cute.Tensor,
323
+ mdPsum: cute.Tensor,
324
+ mdQaccum: cute.Tensor,
325
+ mdK: cute.Tensor,
326
+ mdV: cute.Tensor,
327
+ softmax_scale: Float32,
328
+ stream: cuda.CUstream,
329
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
330
+ mCuSeqlensK: Optional[cute.Tensor] = None,
331
+ mSeqUsedQ: Optional[cute.Tensor] = None,
332
+ mSeqUsedK: Optional[cute.Tensor] = None,
333
+ softcap: Float32 | float | None = None,
334
+ window_size_left: Int32 | int | None = None,
335
+ window_size_right: Int32 | int | None = None,
336
+ mdQ_semaphore: Optional[cute.Tensor] = None,
337
+ mdK_semaphore: Optional[cute.Tensor] = None,
338
+ mdV_semaphore: Optional[cute.Tensor] = None,
339
+ aux_tensors: Optional[list] = None,
340
+ blocksparse_tensors: Optional[BlockSparseTensors] = None,
341
+ ):
342
+ assert mdQ_semaphore is None and mdK_semaphore is None and mdV_semaphore is None, (
343
+ "determinism not supported yet for Sm90"
344
+ )
345
+
346
+ self._check_type(
347
+ *(
348
+ t.element_type if t is not None else None
349
+ for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV)
350
+ )
351
+ )
352
+
353
+ # Assume all strides are divisible by 128 bits except the last stride
354
+ # Skip cute.assume() for stride=0 (broadcast dims from expand() are Python ints)
355
+ new_stride = lambda t: (
356
+ *(
357
+ cute.assume(s, divby=128 // t.element_type.width)
358
+ if not isinstance(s, int) or s != 0
359
+ else s
360
+ for s in t.stride[:-1]
361
+ ),
362
+ t.stride[-1],
363
+ )
364
+ mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV = [
365
+ cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
366
+ if t is not None
367
+ else None
368
+ for t in (mQ, mK, mV, mdO, mLSE, mdPsum, mdQaccum, mdK, mdV)
369
+ ]
370
+
371
+ layout_transpose = [1, 3, 2, 0] # (b, s, n, h) --> (s, h, n, b)
372
+ mQ, mK, mV, mdO = [utils.select(t, layout_transpose) for t in (mQ, mK, mV, mdO)]
373
+ if const_expr(self.qhead_per_kvhead == 1):
374
+ mdK, mdV = [utils.select(t, layout_transpose) for t in (mdK, mdV)]
375
+ else:
376
+ accum_transpose = [2, 1, 0] # (b, n, s*h) -> (s*h, n, b)
377
+ mdK, mdV = [utils.select(t, accum_transpose) for t in (mdK, mdV)]
378
+ LSE_dPsum_dQaccum_transpose = [2, 1, 0] # (b, n, s) -> (s, n, b)
379
+ mLSE, mdPsum, mdQaccum = [
380
+ utils.select(t, LSE_dPsum_dQaccum_transpose) for t in (mLSE, mdPsum, mdQaccum)
381
+ ]
382
+
383
+ tiled_mma_SdP, tiled_mma_dK, tiled_mma_dV, tiled_mma_dQ = self._get_tiled_mma()
384
+
385
+ self.num_mma_threads = tiled_mma_SdP.size
386
+ assert self.num_mma_threads + 128 == self.num_threads
387
+
388
+ self.num_threads_per_warp_group = 128
389
+ self.num_producer_threads = 32
390
+
391
+ self.num_mma_regs = 240
392
+ self.num_producer_regs = 24
393
+ # self.num_mma_regs = 232
394
+ # self.num_producer_regs = 40
395
+
396
+ self._setup_attributes()
397
+ SharedStorage = self._get_shared_storage_cls()
398
+
399
+ self.tma_copy_bytes = {
400
+ name: cute.size_in_bytes(mX.element_type, cute.select(layout, mode=[0, 1]))
401
+ for name, mX, layout in [
402
+ ("Q", mQ, self.sQ_layout),
403
+ ("K", mK, self.sK_layout),
404
+ ("V", mV, self.sV_layout),
405
+ ("dO", mdO, self.sdO_layout),
406
+ ]
407
+ }
408
+ self.tma_copy_bytes["LSE"] = self.tile_m * Float32.width // 8
409
+ self.tma_copy_bytes["dPsum"] = self.tile_m * Float32.width // 8
410
+ self.tma_copy_bytes["dQ"] = (
411
+ self.tile_m * self.tile_hdim * Float32.width // 8 // self.num_mma_warp_groups
412
+ )
413
+ self.tma_copy_bytes["dKacc"] = self.tile_n * self.tile_hdim * Float32.width // 8
414
+ self.tma_copy_bytes["dVacc"] = self.tile_n * self.tile_hdimv * Float32.width // 8
415
+
416
+ tma_atom_Q, tma_tensor_Q = cpasync.make_tiled_tma_atom(
417
+ cpasync.CopyBulkTensorTileG2SOp(),
418
+ mQ,
419
+ cute.select(self.sQ_layout, mode=[0, 1]),
420
+ (self.tile_m, self.tile_hdim),
421
+ )
422
+ tma_atom_K, tma_tensor_K = cpasync.make_tiled_tma_atom(
423
+ cpasync.CopyBulkTensorTileG2SOp(),
424
+ mK,
425
+ cute.select(self.sK_layout, mode=[0, 1]),
426
+ (self.tile_n, self.tile_hdim),
427
+ )
428
+ tma_atom_V, tma_tensor_V = cpasync.make_tiled_tma_atom(
429
+ cpasync.CopyBulkTensorTileG2SOp(),
430
+ mV,
431
+ cute.select(self.sV_layout, mode=[0, 1]),
432
+ (self.tile_n, self.tile_hdimv),
433
+ )
434
+ tma_atom_dO, tma_tensor_dO = cpasync.make_tiled_tma_atom(
435
+ cpasync.CopyBulkTensorTileG2SOp(),
436
+ mdO,
437
+ cute.select(self.sdO_layout, mode=[0, 1]),
438
+ (self.tile_m, self.tile_hdimv),
439
+ )
440
+ if const_expr(self.qhead_per_kvhead == 1):
441
+ tma_atom_dK, tma_tensor_dK = cpasync.make_tiled_tma_atom(
442
+ cpasync.CopyBulkTensorTileS2GOp(),
443
+ mdK,
444
+ cute.select(self.sK_layout, mode=[0, 1]),
445
+ (self.tile_n, self.tile_hdim),
446
+ )
447
+ tma_atom_dV, tma_tensor_dV = cpasync.make_tiled_tma_atom(
448
+ cpasync.CopyBulkTensorTileS2GOp(),
449
+ mdV,
450
+ cute.select(self.sV_layout, mode=[0, 1]),
451
+ (self.tile_n, self.tile_hdimv),
452
+ )
453
+ else:
454
+ tma_atom_dK = tma_atom_dV = tma_tensor_dK = tma_tensor_dV = None
455
+
456
+ TileScheduler = SingleTileScheduler
457
+ tile_sched_args = TileSchedulerArguments(
458
+ cute.ceil_div(cute.size(mK.shape[0]), self.tile_n),
459
+ cute.size(mQ.shape[2]),
460
+ cute.size(mQ.shape[3]),
461
+ 1, # num_splits
462
+ cute.size(mK.shape[0]),
463
+ mQ.shape[1],
464
+ mV.shape[1],
465
+ total_q=cute.size(mQ.shape[0]) * cute.size(mQ.shape[3]),
466
+ tile_shape_mn=(self.tile_m, self.tile_n),
467
+ mCuSeqlensQ=None,
468
+ mSeqUsedQ=None,
469
+ qhead_per_kvhead_packgqa=1,
470
+ element_size=self.dtype.width // 8,
471
+ is_persistent=False,
472
+ lpt=False,
473
+ )
474
+
475
+ tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
476
+ grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
477
+
478
+ LOG2_E = math.log2(math.e)
479
+ if const_expr(self.score_mod is None):
480
+ softmax_scale_log2 = softmax_scale * LOG2_E
481
+ else:
482
+ softmax_scale_log2 = LOG2_E
483
+
484
+ fastdiv_mods = None
485
+ if const_expr(aux_tensors is not None):
486
+ seqlen_q = cute.size(mQ.shape[0])
487
+ seqlen_k = cute.size(mK.shape[0])
488
+ seqlen_q_divmod = FastDivmodDivisor(seqlen_q)
489
+ seqlen_k_divmod = FastDivmodDivisor(seqlen_k)
490
+ fastdiv_mods = (seqlen_q_divmod, seqlen_k_divmod)
491
+
492
+ qhead_per_kvhead_divmod = None
493
+ if const_expr(self.qhead_per_kvhead > 1):
494
+ qhead_per_kvhead_divmod = FastDivmodDivisor(self.qhead_per_kvhead)
495
+
496
+ self.use_block_sparsity = cutlass.const_expr(blocksparse_tensors is not None)
497
+
498
+ self.kernel(
499
+ tma_tensor_Q,
500
+ tma_tensor_K,
501
+ tma_tensor_V,
502
+ tma_tensor_dO,
503
+ tma_tensor_dK if const_expr(self.qhead_per_kvhead == 1) else mdK,
504
+ tma_tensor_dV if const_expr(self.qhead_per_kvhead == 1) else mdV,
505
+ tma_atom_Q,
506
+ tma_atom_K,
507
+ tma_atom_V,
508
+ tma_atom_dO,
509
+ tma_atom_dK,
510
+ tma_atom_dV,
511
+ mLSE,
512
+ mdPsum,
513
+ mdQaccum,
514
+ self.sQ_layout,
515
+ self.sK_layout,
516
+ self.sV_layout,
517
+ self.sPdS_layout,
518
+ self.sdO_layout,
519
+ self.sdQaccum_layout,
520
+ self.sdKVaccum_layout,
521
+ self.r2s_tiled_copy_dQaccum,
522
+ self.r2s_tiled_copy_dKVaccum,
523
+ tiled_mma_SdP,
524
+ tiled_mma_dK,
525
+ tiled_mma_dV,
526
+ tiled_mma_dQ,
527
+ softmax_scale_log2,
528
+ softmax_scale,
529
+ tile_sched_params,
530
+ TileScheduler,
531
+ SharedStorage,
532
+ aux_tensors,
533
+ fastdiv_mods,
534
+ blocksparse_tensors,
535
+ qhead_per_kvhead_divmod,
536
+ ).launch(
537
+ grid=grid_dim,
538
+ block=[self.num_threads, 1, 1],
539
+ smem=SharedStorage.size_in_bytes(),
540
+ stream=stream,
541
+ min_blocks_per_mp=1,
542
+ )
543
+
544
+ @cute.kernel
545
+ def kernel(
546
+ self,
547
+ mQ: cute.Tensor,
548
+ mK: cute.Tensor,
549
+ mV: cute.Tensor,
550
+ mdO: cute.Tensor,
551
+ mdK: cute.Tensor,
552
+ mdV: cute.Tensor,
553
+ tma_atom_Q: cute.CopyAtom,
554
+ tma_atom_K: cute.CopyAtom,
555
+ tma_atom_V: cute.CopyAtom,
556
+ tma_atom_dO: cute.CopyAtom,
557
+ tma_atom_dK: cute.CopyAtom,
558
+ tma_atom_dV: cute.CopyAtom,
559
+ mLSE: cute.Tensor,
560
+ mdPsum: cute.Tensor,
561
+ mdQaccum: cute.Tensor,
562
+ sQ_layout: cute.ComposedLayout,
563
+ sK_layout: cute.ComposedLayout,
564
+ sV_layout: cute.ComposedLayout,
565
+ sPdS_layout: cute.ComposedLayout,
566
+ sdO_layout: cute.ComposedLayout,
567
+ sdQaccum_layout: cute.Layout,
568
+ sdKVaccum_layout: cute.Layout,
569
+ r2s_tiled_copy_dQaccum: cute.TiledCopy,
570
+ r2s_tiled_copy_dKVaccum: cute.TiledCopy,
571
+ tiled_mma_SdP: cute.TiledMma,
572
+ tiled_mma_dK: cute.TiledMma,
573
+ tiled_mma_dV: cute.TiledMma,
574
+ tiled_mma_dQ: cute.TiledMma,
575
+ softmax_scale_log2,
576
+ softmax_scale,
577
+ tile_sched_params: ParamsBase,
578
+ TileScheduler: cutlass.Constexpr[Callable],
579
+ SharedStorage: cutlass.Constexpr[Callable],
580
+ aux_tensors: Optional[list] = None,
581
+ fastdiv_mods=(None, None),
582
+ blocksparse_tensors: Optional[BlockSparseTensors] = None,
583
+ qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
584
+ ):
585
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
586
+
587
+ # prefetch TMA descriptors
588
+ if warp_idx == 0:
589
+ cpasync.prefetch_descriptor(tma_atom_Q)
590
+ cpasync.prefetch_descriptor(tma_atom_K)
591
+ cpasync.prefetch_descriptor(tma_atom_V)
592
+ cpasync.prefetch_descriptor(tma_atom_dO)
593
+
594
+ smem = cutlass.utils.SmemAllocator()
595
+ storage = smem.allocate(SharedStorage)
596
+
597
+ pipeline_producer_group = cutlass.pipeline.CooperativeGroup(cutlass.pipeline.Agent.Thread)
598
+ pipeline_consumer_group = cutlass.pipeline.CooperativeGroup(
599
+ cutlass.pipeline.Agent.Thread, self.num_mma_threads // cute.arch.WARP_SIZE
600
+ )
601
+ pipeline_Q = pipeline.PipelineTmaAsync.create(
602
+ barrier_storage=storage.mbar_ptr_Q.data_ptr(),
603
+ num_stages=self.Q_stage,
604
+ producer_group=pipeline_producer_group,
605
+ consumer_group=pipeline_consumer_group,
606
+ tx_count=self.tma_copy_bytes["Q"] + self.tma_copy_bytes["LSE"],
607
+ defer_sync=True,
608
+ )
609
+ pipeline_dO = pipeline.PipelineTmaAsync.create(
610
+ barrier_storage=storage.mbar_ptr_dO.data_ptr(),
611
+ num_stages=self.dO_stage,
612
+ producer_group=pipeline_producer_group,
613
+ consumer_group=pipeline_consumer_group,
614
+ tx_count=self.tma_copy_bytes["dO"] + self.tma_copy_bytes["dPsum"],
615
+ defer_sync=False,
616
+ )
617
+
618
+ sQ = storage.sQ.get_tensor(sQ_layout.outer, swizzle=sQ_layout.inner)
619
+ sdO = storage.sdO.get_tensor(sdO_layout.outer, swizzle=sdO_layout.inner)
620
+ sK = storage.sK.get_tensor(sK_layout.outer, swizzle=sK_layout.inner)
621
+ sV = storage.sV.get_tensor(sV_layout.outer, swizzle=sV_layout.inner)
622
+ sP = None
623
+ if const_expr(not self.mma_dkv_is_rs):
624
+ sP = storage.sP.get_tensor(sPdS_layout.outer, swizzle=sPdS_layout.inner)
625
+ sdS = storage.sdS.get_tensor(sPdS_layout.outer, swizzle=sPdS_layout.inner)
626
+ sLSE = storage.sLSE.get_tensor(
627
+ cute.make_layout(
628
+ (self.tile_m, self.Q_stage),
629
+ stride=(1, cute.round_up(self.tile_m, 64)),
630
+ )
631
+ )
632
+ sdPsum = storage.sdPsum.get_tensor(
633
+ cute.make_layout(
634
+ (self.tile_m, self.dO_stage),
635
+ stride=(1, cute.round_up(self.tile_m, 64)),
636
+ )
637
+ )
638
+ sdQaccum = storage.sdQaccum.get_tensor(sdQaccum_layout)
639
+
640
+ block_info = BlockInfo(
641
+ self.tile_m,
642
+ self.tile_n,
643
+ self.is_causal,
644
+ self.is_local,
645
+ False, # is_split_kv
646
+ None,
647
+ None,
648
+ qhead_per_kvhead_packgqa=1,
649
+ )
650
+ SeqlenInfoCls = partial(
651
+ SeqlenInfoQK.create,
652
+ seqlen_q_static=mQ.shape[0],
653
+ seqlen_k_static=mK.shape[0],
654
+ mCuSeqlensQ=None,
655
+ mCuSeqlensK=None,
656
+ mSeqUsedQ=None,
657
+ mSeqUsedK=None,
658
+ )
659
+ AttentionMaskCls = partial(
660
+ AttentionMask,
661
+ self.tile_m,
662
+ self.tile_n,
663
+ window_size_left=None,
664
+ window_size_right=None,
665
+ swap_AB=self.SdP_swapAB,
666
+ )
667
+ TileSchedulerCls = partial(TileScheduler.create, tile_sched_params)
668
+
669
+ if warp_idx < 4:
670
+ cute.arch.warpgroup_reg_dealloc(self.num_producer_regs)
671
+ if warp_idx == 0:
672
+ self.load(
673
+ mQ,
674
+ mK,
675
+ mV,
676
+ mdO,
677
+ mLSE,
678
+ mdPsum,
679
+ sQ,
680
+ sK,
681
+ sV,
682
+ sdO,
683
+ sLSE,
684
+ sdPsum,
685
+ tma_atom_Q,
686
+ tma_atom_K,
687
+ tma_atom_V,
688
+ tma_atom_dO,
689
+ pipeline_Q,
690
+ pipeline_dO,
691
+ block_info,
692
+ SeqlenInfoCls,
693
+ TileSchedulerCls,
694
+ blocksparse_tensors,
695
+ qhead_per_kvhead_divmod,
696
+ )
697
+ if warp_idx == 1:
698
+ for warp_group_idx in cutlass.range(self.num_mma_warp_groups):
699
+ cute.arch.barrier_arrive(
700
+ barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
701
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
702
+ )
703
+ self.dQaccum_store(
704
+ mdQaccum,
705
+ sdQaccum,
706
+ block_info,
707
+ TileSchedulerCls,
708
+ SeqlenInfoCls,
709
+ blocksparse_tensors,
710
+ )
711
+ else:
712
+ cute.arch.warpgroup_reg_alloc(self.num_mma_regs)
713
+ tidx, _, _ = cute.arch.thread_idx()
714
+ tidx = tidx - 128
715
+ self.mma(
716
+ tiled_mma_SdP,
717
+ tiled_mma_dK,
718
+ tiled_mma_dV,
719
+ tiled_mma_dQ,
720
+ mdK,
721
+ mdV,
722
+ mdQaccum,
723
+ sQ,
724
+ sK,
725
+ sV,
726
+ sdO,
727
+ sP,
728
+ sdS,
729
+ sLSE,
730
+ sdPsum,
731
+ sdQaccum,
732
+ pipeline_Q,
733
+ pipeline_dO,
734
+ tidx,
735
+ tma_atom_dK,
736
+ tma_atom_dV,
737
+ r2s_tiled_copy_dQaccum,
738
+ r2s_tiled_copy_dKVaccum,
739
+ sdKVaccum_layout,
740
+ softmax_scale_log2,
741
+ softmax_scale,
742
+ block_info,
743
+ SeqlenInfoCls,
744
+ AttentionMaskCls,
745
+ TileSchedulerCls,
746
+ aux_tensors,
747
+ fastdiv_mods,
748
+ blocksparse_tensors,
749
+ qhead_per_kvhead_divmod,
750
+ )
751
+
752
+ @cute.jit
753
+ def load(
754
+ self,
755
+ mQ: cute.Tensor,
756
+ mK: cute.Tensor,
757
+ mV: cute.Tensor,
758
+ mdO: cute.Tensor,
759
+ mLSE: cute.Tensor,
760
+ mdPsum: cute.Tensor,
761
+ sQ: cute.Tensor,
762
+ sK: cute.Tensor,
763
+ sV: cute.Tensor,
764
+ sdO: cute.Tensor,
765
+ sLSE: cute.Tensor,
766
+ sdPsum: cute.Tensor,
767
+ tma_atom_Q: cute.CopyAtom,
768
+ tma_atom_K: cute.CopyAtom,
769
+ tma_atom_V: cute.CopyAtom,
770
+ tma_atom_dO: cute.CopyAtom,
771
+ pipeline_Q: cutlass.pipeline.PipelineAsync,
772
+ pipeline_dO: cutlass.pipeline.PipelineAsync,
773
+ block_info: BlockInfo,
774
+ SeqlenInfoCls: Callable,
775
+ TileSchedulerCls: Callable,
776
+ blocksparse_tensors: Optional[BlockSparseTensors] = None,
777
+ qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
778
+ ):
779
+ warp_idx_in_wg = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % 4
780
+
781
+ if warp_idx_in_wg == 0:
782
+ producer_state_Q = cutlass.pipeline.make_pipeline_state(
783
+ cutlass.pipeline.PipelineUserType.Producer, self.Q_stage
784
+ )
785
+ producer_state_dO = cutlass.pipeline.make_pipeline_state(
786
+ cutlass.pipeline.PipelineUserType.Producer, self.dO_stage
787
+ )
788
+ tile_scheduler = TileSchedulerCls()
789
+ work_tile = tile_scheduler.initial_work_tile_info()
790
+ while work_tile.is_valid_tile:
791
+ n_block, head_idx, batch_idx, _ = work_tile.tile_idx
792
+ seqlen = SeqlenInfoCls(batch_idx)
793
+ head_idx_kv = (
794
+ head_idx
795
+ if const_expr(self.qhead_per_kvhead == 1)
796
+ else head_idx // qhead_per_kvhead_divmod
797
+ )
798
+ mK_cur = mK[None, None, head_idx_kv, batch_idx]
799
+ gK = cute.local_tile(mK_cur, (self.tile_n, self.tile_hdim), (n_block, 0))
800
+ mV_cur = mV[None, None, head_idx_kv, batch_idx]
801
+ gV = cute.local_tile(mV_cur, (self.tile_n, self.tile_hdimv), (n_block, 0))
802
+
803
+ mQ_cur = mQ[None, None, head_idx, batch_idx]
804
+ gQ = cute.local_tile(mQ_cur, (self.tile_m, self.tile_hdim), (None, 0))
805
+ mdO_cur = mdO[None, None, head_idx, batch_idx]
806
+ gdO = cute.local_tile(mdO_cur, (self.tile_m, self.tile_hdimv), (None, 0))
807
+ mLSE_cur = mLSE[None, head_idx, batch_idx]
808
+ gLSE = cute.local_tile(mLSE_cur, (self.tile_m,), (None,))
809
+ mdPsum_cur = mdPsum[None, head_idx, batch_idx]
810
+ gdPsum = cute.local_tile(mdPsum_cur, (self.tile_m,), (None,))
811
+
812
+ load_K, _, _ = copy_utils.tma_get_copy_fn(
813
+ tma_atom_K, 0, cute.make_layout(1), gK, sK, single_stage=True
814
+ )
815
+ load_V, _, _ = copy_utils.tma_get_copy_fn(
816
+ tma_atom_V, 0, cute.make_layout(1), gV, sV, single_stage=True
817
+ )
818
+ load_Q, _, _ = copy_utils.tma_get_copy_fn(
819
+ tma_atom_Q, 0, cute.make_layout(1), gQ, sQ
820
+ )
821
+ load_Q = copy_utils.tma_producer_copy_fn(load_Q, pipeline_Q)
822
+ load_dO, _, _ = copy_utils.tma_get_copy_fn(
823
+ tma_atom_dO, 0, cute.make_layout(1), gdO, sdO
824
+ )
825
+ load_dO = copy_utils.tma_producer_copy_fn(load_dO, pipeline_dO)
826
+ load_LSE = copy_utils.cpasync_bulk_get_copy_fn(gLSE, sLSE)
827
+ load_LSE = copy_utils.tma_producer_copy_fn(load_LSE, pipeline_Q)
828
+ load_dPsum = copy_utils.cpasync_bulk_get_copy_fn(gdPsum, sdPsum)
829
+ load_dPsum = copy_utils.tma_producer_copy_fn(load_dPsum, pipeline_dO)
830
+
831
+ m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
832
+
833
+ if const_expr(not self.use_block_sparsity):
834
+ total_m_block_cnt = m_block_max - m_block_min
835
+ process_tile = const_expr(not self.is_local) or m_block_min < m_block_max
836
+ else:
837
+ total_m_block_cnt = get_total_q_block_count_bwd(
838
+ blocksparse_tensors,
839
+ batch_idx,
840
+ head_idx,
841
+ n_block,
842
+ subtile_factor=self.subtile_factor,
843
+ m_block_max=m_block_max,
844
+ )
845
+ process_tile = total_m_block_cnt > Int32(0)
846
+
847
+ if process_tile:
848
+ if const_expr(not self.use_block_sparsity):
849
+ first_m_block = m_block_min
850
+ pipeline_Q.producer_acquire(
851
+ producer_state_Q, extra_tx_count=self.tma_copy_bytes["K"]
852
+ )
853
+ load_K(tma_bar_ptr=pipeline_Q.producer_get_barrier(producer_state_Q))
854
+ load_Q(first_m_block, producer_state=producer_state_Q)
855
+ with cute.arch.elect_one():
856
+ load_LSE(first_m_block, producer_state=producer_state_Q)
857
+ producer_state_dO_cur = (
858
+ producer_state_dO
859
+ if const_expr(self.Q_stage != self.dO_stage)
860
+ else producer_state_Q
861
+ )
862
+ pipeline_dO.producer_acquire(
863
+ producer_state_dO_cur, extra_tx_count=self.tma_copy_bytes["V"]
864
+ )
865
+ load_V(tma_bar_ptr=pipeline_dO.producer_get_barrier(producer_state_dO_cur))
866
+ load_dO(first_m_block, producer_state=producer_state_dO_cur)
867
+ with cute.arch.elect_one():
868
+ load_dPsum(first_m_block, producer_state=producer_state_dO_cur)
869
+ producer_state_Q.advance()
870
+ producer_state_dO.advance()
871
+
872
+ for m_block in cutlass.range(m_block_min + 1, m_block_max, unroll=1):
873
+ pipeline_Q.producer_acquire(producer_state_Q)
874
+ load_Q(m_block, producer_state=producer_state_Q)
875
+ with cute.arch.elect_one():
876
+ load_LSE(m_block, producer_state=producer_state_Q)
877
+ producer_state_dO_cur = (
878
+ producer_state_dO
879
+ if const_expr(self.Q_stage != self.dO_stage)
880
+ else producer_state_Q
881
+ )
882
+ pipeline_dO.producer_acquire(producer_state_dO_cur)
883
+ load_dO(m_block, producer_state=producer_state_dO_cur)
884
+ with cute.arch.elect_one():
885
+ load_dPsum(m_block, producer_state=producer_state_dO_cur)
886
+ producer_state_Q.advance()
887
+ producer_state_dO.advance()
888
+ else:
889
+ producer_state_Q, producer_state_dO = produce_block_sparse_q_loads_bwd_sm90(
890
+ blocksparse_tensors,
891
+ batch_idx,
892
+ head_idx,
893
+ n_block,
894
+ producer_state_Q,
895
+ producer_state_dO,
896
+ pipeline_Q,
897
+ pipeline_dO,
898
+ load_K,
899
+ load_V,
900
+ load_Q,
901
+ load_dO,
902
+ load_LSE,
903
+ load_dPsum,
904
+ self.tma_copy_bytes["K"],
905
+ self.tma_copy_bytes["V"],
906
+ Q_stage_eq_dO_stage=(self.Q_stage == self.dO_stage),
907
+ subtile_factor=self.subtile_factor,
908
+ m_block_max=m_block_max,
909
+ )
910
+
911
+ tile_scheduler.prefetch_next_work()
912
+ tile_scheduler.advance_to_next_work()
913
+ work_tile = tile_scheduler.get_current_work()
914
+
915
+ @cute.jit
916
+ def apply_score_mod(
917
+ self,
918
+ acc_S: cute.Tensor,
919
+ thr_mma_SdP: cute.core.ThrMma,
920
+ batch_idx,
921
+ head_idx,
922
+ m_block,
923
+ n_block,
924
+ softmax_scale,
925
+ seqlen_info: SeqlenInfoQK,
926
+ aux_tensors=None,
927
+ fastdiv_mods=(None, None),
928
+ ):
929
+ # [NOTE] SdP_swapAB: swapAB transposes the tile, so use (n, m) indexing
930
+ cS = cute.make_identity_tensor(
931
+ (self.tile_n, self.tile_m) if self.SdP_swapAB else (self.tile_m, self.tile_n)
932
+ )
933
+ cS = cute.domain_offset(
934
+ (n_block * self.tile_n, m_block * self.tile_m)
935
+ if self.SdP_swapAB
936
+ else (m_block * self.tile_m, n_block * self.tile_n),
937
+ cS,
938
+ )
939
+ tScS = thr_mma_SdP.partition_C(cS)
940
+
941
+ apply_score_mod_inner(
942
+ acc_S,
943
+ tScS,
944
+ self.score_mod,
945
+ batch_idx,
946
+ head_idx,
947
+ softmax_scale,
948
+ self.vec_size,
949
+ self.qk_acc_dtype,
950
+ aux_tensors,
951
+ fastdiv_mods,
952
+ seqlen_info,
953
+ constant_q_idx=None,
954
+ qhead_per_kvhead=self.qhead_per_kvhead,
955
+ transpose_indices=self.SdP_swapAB,
956
+ )
957
+
958
+ @cute.jit
959
+ def apply_score_mod_bwd(
960
+ self,
961
+ grad_tensor: cute.Tensor,
962
+ score_tensor: cute.Tensor,
963
+ thr_mma_SdP: cute.core.ThrMma,
964
+ batch_idx,
965
+ head_idx,
966
+ m_block,
967
+ n_block,
968
+ softmax_scale,
969
+ seqlen_info: SeqlenInfoQK,
970
+ aux_tensors=None,
971
+ fastdiv_mods=(None, None),
972
+ ):
973
+ cS = cute.make_identity_tensor(
974
+ (self.tile_n, self.tile_m) if self.SdP_swapAB else (self.tile_m, self.tile_n)
975
+ )
976
+ cS = cute.domain_offset(
977
+ (n_block * self.tile_n, m_block * self.tile_m)
978
+ if self.SdP_swapAB
979
+ else (m_block * self.tile_m, n_block * self.tile_n),
980
+ cS,
981
+ )
982
+ tScS = thr_mma_SdP.partition_C(cS)
983
+
984
+ apply_score_mod_bwd_inner(
985
+ grad_tensor,
986
+ score_tensor,
987
+ tScS,
988
+ self.score_mod_bwd,
989
+ batch_idx,
990
+ head_idx,
991
+ softmax_scale,
992
+ self.vec_size,
993
+ self.qk_acc_dtype,
994
+ aux_tensors,
995
+ fastdiv_mods,
996
+ seqlen_info,
997
+ constant_q_idx=None,
998
+ qhead_per_kvhead=self.qhead_per_kvhead,
999
+ transpose_indices=self.SdP_swapAB,
1000
+ )
1001
+
1002
+ @cute.jit
1003
+ def mma(
1004
+ self,
1005
+ tiled_mma_SdP: cute.TiledMma,
1006
+ tiled_mma_dK: cute.TiledMma,
1007
+ tiled_mma_dV: cute.TiledMma,
1008
+ tiled_mma_dQ: cute.TiledMma,
1009
+ mdK: cute.Tensor,
1010
+ mdV: cute.Tensor,
1011
+ mdQaccum: cute.Tensor,
1012
+ sQ: cute.Tensor,
1013
+ sK: cute.Tensor,
1014
+ sV: cute.Tensor,
1015
+ sdO: cute.Tensor,
1016
+ sP: Optional[cute.Tensor],
1017
+ sdS: cute.Tensor,
1018
+ sLSE: cute.Tensor,
1019
+ sdPsum: cute.Tensor,
1020
+ sdQaccum: cute.Tensor,
1021
+ pipeline_Q: cutlass.pipeline.PipelineAsync,
1022
+ pipeline_dO: cutlass.pipeline.PipelineAsync,
1023
+ tidx: Int32,
1024
+ tma_atom_dK: cute.CopyAtom,
1025
+ tma_atom_dV: cute.CopyAtom,
1026
+ r2s_tiled_copy_dQaccum: cute.TiledCopy,
1027
+ r2s_tiled_copy_dKVaccum: cute.TiledCopy,
1028
+ sdKVaccum_layout: cute.Layout,
1029
+ softmax_scale_log2: Float32,
1030
+ softmax_scale: Float32,
1031
+ block_info: BlockInfo,
1032
+ SeqlenInfoCls: Callable,
1033
+ AttentionMaskCls: Callable,
1034
+ TileSchedulerCls: Callable,
1035
+ aux_tensors: Optional[list] = None,
1036
+ fastdiv_mods=(None, None),
1037
+ blocksparse_tensors: Optional[BlockSparseTensors] = None,
1038
+ qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
1039
+ ):
1040
+ warp_group_idx = cute.arch.make_warp_uniform(tidx // self.num_threads_per_warp_group)
1041
+ warp_group_thread_layout = cute.make_layout(
1042
+ self.num_mma_warp_groups, stride=self.num_threads_per_warp_group
1043
+ )
1044
+ thr_mma_SdP = tiled_mma_SdP.get_slice(tidx)
1045
+ wg_mma_SdP = tiled_mma_SdP.get_slice(warp_group_thread_layout(warp_group_idx))
1046
+ wg_mma_dK = tiled_mma_dK.get_slice(warp_group_thread_layout(warp_group_idx))
1047
+ wg_mma_dV = tiled_mma_dV.get_slice(warp_group_thread_layout(warp_group_idx))
1048
+ wg_mma_dQ = tiled_mma_dQ.get_slice(warp_group_thread_layout(warp_group_idx))
1049
+ # S = Q @ K.T
1050
+ tSrQ, tSrK = mma_partition_fragment_AB(wg_mma_SdP, sQ, sK, self.SdP_swapAB)
1051
+ # dP = dO @ V.T
1052
+ tdPrdO, tdPrV = mma_partition_fragment_AB(wg_mma_SdP, sdO, sV, self.SdP_swapAB)
1053
+ # dV += P.T @ dO
1054
+ sPt = utils.transpose_view(sP) if sP is not None else None
1055
+ sdOt = utils.transpose_view(sdO)
1056
+ tdVrPt, tdVrdOt = mma_partition_fragment_AB(wg_mma_dV, sPt, sdOt, self.dKV_swapAB)
1057
+ # dK += dS.T @ Q
1058
+ sdSt = utils.transpose_view(sdS)
1059
+ sQt = utils.transpose_view(sQ)
1060
+ tdKrdSt, tdKrQt = mma_partition_fragment_AB(wg_mma_dK, sdSt, sQt, self.dKV_swapAB)
1061
+ # dQ = dS @ K
1062
+ sKt = utils.transpose_view(sK)
1063
+ tdQrdS, tdQrKt = mma_partition_fragment_AB(wg_mma_dQ, sdS, sKt, self.dQ_swapAB)
1064
+
1065
+ # Smem copy atom tiling
1066
+ smem_copy_atom_PdS = utils.get_smem_store_atom(
1067
+ self.arch, self.dtype, transpose=self.SdP_swapAB
1068
+ )
1069
+ smem_thr_copy_PdS = cute.make_tiled_copy_C(smem_copy_atom_PdS, tiled_mma_SdP).get_slice(
1070
+ tidx
1071
+ )
1072
+ tPsP = None
1073
+ if const_expr(sP is not None):
1074
+ tPsP = smem_thr_copy_PdS.partition_D(sP if const_expr(not self.SdP_swapAB) else sPt)
1075
+ tdSsdS = smem_thr_copy_PdS.partition_D(sdS if const_expr(not self.SdP_swapAB) else sdSt)
1076
+
1077
+ sLSE_mma = cute.make_tensor(
1078
+ sLSE.iterator,
1079
+ cute.make_layout(
1080
+ (self.tile_m, self.tile_n, self.Q_stage),
1081
+ stride=(1, 0, cute.round_up(self.tile_m, 64)),
1082
+ ),
1083
+ )
1084
+ sdPsum_mma = cute.make_tensor(
1085
+ sdPsum.iterator,
1086
+ cute.make_layout(
1087
+ (self.tile_m, self.tile_n, self.dO_stage),
1088
+ stride=(1, 0, cute.round_up(self.tile_m, 64)),
1089
+ ),
1090
+ )
1091
+ if const_expr(self.SdP_swapAB):
1092
+ sLSE_mma = utils.transpose_view(sLSE_mma)
1093
+ sdPsum_mma = utils.transpose_view(sdPsum_mma)
1094
+ LSEslice = (None, 0, None) if const_expr(not self.SdP_swapAB) else (0, None, None)
1095
+ tLSEsLSE = utils.make_acc_tensor_mn_view(thr_mma_SdP.partition_C(sLSE_mma))[LSEslice]
1096
+ tLSEsdPsum = utils.make_acc_tensor_mn_view(thr_mma_SdP.partition_C(sdPsum_mma))[LSEslice]
1097
+
1098
+ smem_thr_copy_dQaccum = r2s_tiled_copy_dQaccum.get_slice(tidx)
1099
+ tdQsdQaccum = smem_thr_copy_dQaccum.partition_D(sdQaccum)
1100
+
1101
+ dV_shape = (self.tile_n, self.tile_hdimv)
1102
+ acc_dV = cute.make_fragment(
1103
+ tiled_mma_dV.partition_shape_C(dV_shape if not self.dKV_swapAB else dV_shape[::-1]),
1104
+ Float32,
1105
+ )
1106
+ dK_shape = (self.tile_n, self.tile_hdim)
1107
+ acc_dK = cute.make_fragment(
1108
+ tiled_mma_dK.partition_shape_C(dK_shape if not self.dKV_swapAB else dK_shape[::-1]),
1109
+ Float32,
1110
+ )
1111
+
1112
+ mma_qk_fn = partial(
1113
+ gemm_zero_init,
1114
+ tiled_mma_SdP,
1115
+ (self.tile_m, self.tile_n),
1116
+ tSrQ,
1117
+ tSrK,
1118
+ swap_AB=self.SdP_swapAB,
1119
+ )
1120
+ mma_dov_fn = partial(
1121
+ gemm_zero_init,
1122
+ tiled_mma_SdP,
1123
+ (self.tile_m, self.tile_n),
1124
+ tdPrdO,
1125
+ tdPrV,
1126
+ swap_AB=self.SdP_swapAB,
1127
+ )
1128
+ if const_expr(not self.mma_dkv_is_rs):
1129
+ mma_pdo_fn = partial(
1130
+ gemm_w_idx, tiled_mma_dV, acc_dV, tdVrPt, tdVrdOt, swap_AB=self.dKV_swapAB
1131
+ )
1132
+ mma_dsq_fn = partial(
1133
+ gemm_w_idx, tiled_mma_dK, acc_dK, tdKrdSt, tdKrQt, swap_AB=self.dKV_swapAB
1134
+ )
1135
+ else:
1136
+ assert not self.dKV_swapAB
1137
+ mma_pdo_fn = partial(gemm_w_idx, tiled_mma_dV, acc_dV, tCrB=tdVrdOt)
1138
+ mma_dsq_fn = partial(gemm_w_idx, tiled_mma_dK, acc_dK, tCrB=tdKrQt)
1139
+ mma_dsk_fn = partial(
1140
+ gemm_zero_init,
1141
+ tiled_mma_dQ,
1142
+ (self.tile_m, self.tile_hdim),
1143
+ tdQrdS,
1144
+ tdQrKt,
1145
+ swap_AB=self.dQ_swapAB,
1146
+ )
1147
+
1148
+ mma_one_m_block_all = partial(
1149
+ self.mma_one_m_block,
1150
+ warp_group_idx=warp_group_idx,
1151
+ mma_qk_fn=mma_qk_fn,
1152
+ mma_dov_fn=mma_dov_fn,
1153
+ mma_pdo_fn=mma_pdo_fn,
1154
+ mma_dsq_fn=mma_dsq_fn,
1155
+ mma_dsk_fn=mma_dsk_fn,
1156
+ pipeline_Q=pipeline_Q,
1157
+ pipeline_dO=pipeline_dO,
1158
+ tLSEsLSE=tLSEsLSE,
1159
+ tLSEsdPsum=tLSEsdPsum,
1160
+ tPsP=tPsP,
1161
+ tdSsdS=tdSsdS,
1162
+ tdQsdQaccum=tdQsdQaccum,
1163
+ smem_thr_copy_PdS=smem_thr_copy_PdS,
1164
+ smem_thr_copy_dQaccum=smem_thr_copy_dQaccum,
1165
+ softmax_scale_log2=softmax_scale_log2,
1166
+ # acc_dV=acc_dV,
1167
+ # acc_dK=acc_dK,
1168
+ )
1169
+
1170
+ consumer_state_Q = cutlass.pipeline.make_pipeline_state(
1171
+ cutlass.pipeline.PipelineUserType.Consumer, self.Q_stage
1172
+ )
1173
+ consumer_state_dO = cutlass.pipeline.make_pipeline_state(
1174
+ cutlass.pipeline.PipelineUserType.Consumer, self.dO_stage
1175
+ )
1176
+ tile_scheduler = TileSchedulerCls()
1177
+ work_tile = tile_scheduler.initial_work_tile_info()
1178
+ while work_tile.is_valid_tile:
1179
+ n_block, head_idx, batch_idx, _ = work_tile.tile_idx
1180
+ seqlen = SeqlenInfoCls(batch_idx)
1181
+ mask = AttentionMaskCls(seqlen)
1182
+ m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
1183
+
1184
+ if const_expr(not self.use_block_sparsity):
1185
+ process_tile = const_expr(not self.is_local) or m_block_min < m_block_max
1186
+ else:
1187
+ total_m_block_cnt = get_total_q_block_count_bwd(
1188
+ blocksparse_tensors,
1189
+ batch_idx,
1190
+ head_idx,
1191
+ n_block,
1192
+ subtile_factor=self.subtile_factor,
1193
+ m_block_max=m_block_max,
1194
+ )
1195
+ process_tile = total_m_block_cnt > Int32(0)
1196
+
1197
+ if process_tile:
1198
+ if const_expr(not self.use_block_sparsity):
1199
+ mask_fn = partial(
1200
+ mask.apply_mask,
1201
+ batch_idx=batch_idx,
1202
+ head_idx=head_idx,
1203
+ n_block=n_block,
1204
+ thr_mma=thr_mma_SdP,
1205
+ mask_seqlen=True,
1206
+ mask_causal=self.is_causal,
1207
+ mask_local=self.is_local,
1208
+ mask_mod=self.mask_mod,
1209
+ aux_tensors=aux_tensors,
1210
+ fastdiv_mods=fastdiv_mods,
1211
+ )
1212
+ dKV_accumulate = False
1213
+ for m_block in cutlass.range(m_block_min, m_block_max, unroll=1):
1214
+ consumer_state_Q, consumer_state_dO = mma_one_m_block_all(
1215
+ m_block,
1216
+ consumer_state_Q,
1217
+ consumer_state_dO,
1218
+ mask_fn=mask_fn,
1219
+ dKV_accumulate=dKV_accumulate,
1220
+ thr_mma_SdP=thr_mma_SdP,
1221
+ batch_idx=batch_idx,
1222
+ head_idx=head_idx,
1223
+ n_block=n_block,
1224
+ softmax_scale=softmax_scale,
1225
+ seqlen=seqlen,
1226
+ aux_tensors=aux_tensors,
1227
+ fastdiv_mods=fastdiv_mods,
1228
+ )
1229
+ dKV_accumulate = True
1230
+ else:
1231
+ consumer_state_Q, consumer_state_dO = consume_block_sparse_mma_bwd_sm90(
1232
+ blocksparse_tensors,
1233
+ batch_idx,
1234
+ head_idx,
1235
+ n_block,
1236
+ consumer_state_Q,
1237
+ consumer_state_dO,
1238
+ mma_one_m_block_all,
1239
+ mask,
1240
+ self.mask_mod,
1241
+ is_causal=self.is_causal,
1242
+ is_local=self.is_local,
1243
+ thr_mma_SdP=thr_mma_SdP,
1244
+ softmax_scale=softmax_scale,
1245
+ seqlen=seqlen,
1246
+ subtile_factor=self.subtile_factor,
1247
+ m_block_max=m_block_max,
1248
+ aux_tensors=aux_tensors,
1249
+ fastdiv_mods=fastdiv_mods,
1250
+ )
1251
+
1252
+ if const_expr(self.qhead_per_kvhead == 1):
1253
+ acc_dK.store(acc_dK.load() * softmax_scale)
1254
+ self.epilogue_dKV(
1255
+ acc_dV,
1256
+ mdV,
1257
+ sV,
1258
+ acc_dK,
1259
+ mdK,
1260
+ sK,
1261
+ seqlen,
1262
+ tma_atom_dK,
1263
+ tma_atom_dV,
1264
+ tiled_mma_dK,
1265
+ tiled_mma_dV,
1266
+ r2s_tiled_copy_dKVaccum,
1267
+ sdKVaccum_layout,
1268
+ tidx,
1269
+ n_block,
1270
+ head_idx,
1271
+ batch_idx,
1272
+ qhead_per_kvhead_divmod,
1273
+ )
1274
+ else:
1275
+ # Block sparsity: KV tile with zero Q blocks produces no dK/dV; write zeros.
1276
+ if const_expr(self.use_block_sparsity):
1277
+ acc_dK.fill(0.0)
1278
+ acc_dV.fill(0.0)
1279
+ self.epilogue_dKV(
1280
+ acc_dV,
1281
+ mdV,
1282
+ sV,
1283
+ acc_dK,
1284
+ mdK,
1285
+ sK,
1286
+ seqlen,
1287
+ tma_atom_dK,
1288
+ tma_atom_dV,
1289
+ tiled_mma_dK,
1290
+ tiled_mma_dV,
1291
+ r2s_tiled_copy_dKVaccum,
1292
+ sdKVaccum_layout,
1293
+ tidx,
1294
+ n_block,
1295
+ head_idx,
1296
+ batch_idx,
1297
+ qhead_per_kvhead_divmod,
1298
+ )
1299
+
1300
+ tile_scheduler.advance_to_next_work()
1301
+ work_tile = tile_scheduler.get_current_work()
1302
+
1303
+ @cute.jit
1304
+ def mma_one_m_block(
1305
+ self,
1306
+ m_block: Int32,
1307
+ consumer_state_Q: cutlass.pipeline.PipelineState | pipeline.PipelineStateSimple,
1308
+ consumer_state_dO: cutlass.pipeline.PipelineState | pipeline.PipelineStateSimple,
1309
+ warp_group_idx: Int32,
1310
+ mma_qk_fn: Callable,
1311
+ mma_dov_fn: Callable,
1312
+ mma_pdo_fn: Callable,
1313
+ mma_dsq_fn: Callable,
1314
+ mma_dsk_fn: Callable,
1315
+ pipeline_Q: cutlass.pipeline.PipelineAsync,
1316
+ pipeline_dO: cutlass.pipeline.PipelineAsync,
1317
+ tLSEsLSE: cute.Tensor,
1318
+ tLSEsdPsum: cute.Tensor,
1319
+ tPsP: Optional[cute.Tensor],
1320
+ tdSsdS: Optional[cute.Tensor],
1321
+ tdQsdQaccum: cute.Tensor,
1322
+ smem_thr_copy_PdS: cute.TiledCopy,
1323
+ smem_thr_copy_dQaccum: cute.TiledCopy,
1324
+ softmax_scale_log2: Float32,
1325
+ mask_fn: Optional[Callable] = None,
1326
+ dKV_accumulate: Boolean = True,
1327
+ thr_mma_SdP: Optional[cute.core.ThrMma] = None,
1328
+ batch_idx: Int32 = 0,
1329
+ head_idx: Int32 = 0,
1330
+ n_block: Int32 = 0,
1331
+ softmax_scale: Float32 = 1.0,
1332
+ seqlen: Optional[SeqlenInfoQK] = None,
1333
+ aux_tensors: Optional[list] = None,
1334
+ fastdiv_mods=(None, None),
1335
+ ):
1336
+ consumer_state_dO_cur = (
1337
+ consumer_state_dO if const_expr(self.Q_stage == self.dO_stage) else consumer_state_Q
1338
+ )
1339
+ smem_idx_Q = consumer_state_Q.index
1340
+ smem_idx_dO = consumer_state_dO_cur.index if const_expr(self.dO_stage > 1) else 0
1341
+ smem_idx_PdS = smem_idx_Q if const_expr(self.PdS_stage > 1) else 0
1342
+ # (1) [GEMM 1] S = Q @ K^T
1343
+ pipeline_Q.consumer_wait(consumer_state_Q, pipeline_Q.consumer_try_wait(consumer_state_Q))
1344
+ acc_S = mma_qk_fn(A_idx=smem_idx_Q, wg_wait=-1)
1345
+ tLSErLSE = copy_utils.load_s2r(tLSEsLSE[None, smem_idx_Q])
1346
+ # (2) [GEMM 2] dP = dO @ V.T
1347
+ pipeline_dO.consumer_wait(
1348
+ consumer_state_dO_cur, pipeline_dO.consumer_try_wait(consumer_state_dO_cur)
1349
+ )
1350
+ acc_dP = mma_dov_fn(A_idx=smem_idx_Q, wg_wait=1)
1351
+
1352
+ if const_expr(self.score_mod_bwd is not None):
1353
+ acc_S_pre = cute.make_fragment_like(acc_S)
1354
+ cute.autovec_copy(acc_S, acc_S_pre)
1355
+
1356
+ if const_expr(self.score_mod is not None):
1357
+ self.apply_score_mod(
1358
+ acc_S,
1359
+ thr_mma_SdP,
1360
+ batch_idx,
1361
+ head_idx,
1362
+ m_block,
1363
+ n_block,
1364
+ softmax_scale,
1365
+ seqlen,
1366
+ aux_tensors,
1367
+ fastdiv_mods,
1368
+ )
1369
+
1370
+ # (3) [Pointwise 1] P = exp(S - LSE)
1371
+ if cutlass.const_expr(mask_fn is not None):
1372
+ mask_fn(acc_S, m_block=m_block)
1373
+ acc_S_mn = utils.make_acc_tensor_mn_view(acc_S, transpose=self.SdP_swapAB)
1374
+ for r in cutlass.range_constexpr(cute.size(acc_S_mn, mode=[0])):
1375
+ for c in cutlass.range(cute.size(acc_S_mn, mode=[1]), unroll_full=True):
1376
+ acc_S_mn[r, c] = cute.math.exp2(
1377
+ acc_S_mn[r, c] * softmax_scale_log2 - tLSErLSE[r], fastmath=True
1378
+ )
1379
+ tLSErdPsum = copy_utils.load_s2r(tLSEsdPsum[None, smem_idx_dO])
1380
+
1381
+ # Convert P from f32 -> f16
1382
+ tdVrP = utils.cvt_f16(utils.make_acc_tensor_frgA_view(acc_S), self.dtype)
1383
+ # R2S for P
1384
+ if const_expr(not self.mma_dkv_is_rs):
1385
+ # sync to ensure P has already been used in the previous iteration before overwriting
1386
+ if const_expr(self.PdS_stage == 1):
1387
+ cute.arch.barrier(
1388
+ barrier_id=int(NamedBarrierBwd.PdS), number_of_threads=self.num_mma_threads
1389
+ )
1390
+ tPrP = smem_thr_copy_PdS.retile(tdVrP)
1391
+ cute.copy(smem_thr_copy_PdS, tPrP, tPsP[None, None, None, smem_idx_PdS])
1392
+
1393
+ # (4) [Pointwise 2] dS = P*(dP-dPsum)
1394
+ warpgroup.wait_group(0)
1395
+ acc_dP_mn = utils.make_acc_tensor_mn_view(acc_dP, transpose=self.SdP_swapAB)
1396
+ for r in cutlass.range_constexpr(cute.size(acc_dP_mn, mode=[0])):
1397
+ for c in cutlass.range(cute.size(acc_dP_mn, mode=[1]), unroll_full=True):
1398
+ acc_dP_mn[r, c] = acc_S_mn[r, c] * (acc_dP_mn[r, c] - tLSErdPsum[r])
1399
+
1400
+ if const_expr(self.score_mod_bwd is not None):
1401
+ self.apply_score_mod_bwd(
1402
+ acc_dP,
1403
+ acc_S_pre,
1404
+ thr_mma_SdP,
1405
+ batch_idx,
1406
+ head_idx,
1407
+ m_block,
1408
+ n_block,
1409
+ softmax_scale,
1410
+ seqlen,
1411
+ aux_tensors,
1412
+ fastdiv_mods,
1413
+ )
1414
+
1415
+ # Convert dS from f32 -> f16
1416
+ tdKrdS = utils.cvt_f16(utils.make_acc_tensor_frgA_view(acc_dP), self.dtype)
1417
+
1418
+ # If there's double buffering on dS, we don't need to sync here.
1419
+ # Otherwise we might have WG1 writing to dS before WG2 is done reading from it during MmadQ.
1420
+ # But because both WGs have to sync at the end of the loop and double buffering,
1421
+ # this race condition is not possible.
1422
+ # This sync is to ensure (1) P is written in case of !mma_dkv_is_rs and
1423
+ # (2) dS is already read by the Mma in the previous iteration in case of mma_dkv_is_rs.
1424
+ if const_expr(not self.mma_dkv_is_rs or (self.PdS_stage == 1 and self.mma_dkv_is_rs)):
1425
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1426
+ cute.arch.barrier(
1427
+ barrier_id=int(NamedBarrierBwd.PdS), number_of_threads=self.num_mma_threads
1428
+ )
1429
+
1430
+ # R2S for dS
1431
+ tdSrdS = smem_thr_copy_PdS.retile(tdKrdS)
1432
+ cute.copy(smem_thr_copy_PdS, tdSrdS, tdSsdS[None, None, None, smem_idx_PdS])
1433
+
1434
+ # (5) [GEMM 3] dV += P.T @ dO
1435
+ if const_expr(not self.mma_dkv_is_rs):
1436
+ mma_pdo_fn(
1437
+ A_idx=smem_idx_PdS, B_idx=smem_idx_dO, zero_init=not dKV_accumulate, wg_wait=-1
1438
+ )
1439
+ else:
1440
+ mma_pdo_fn(tCrA=tdVrP, B_idx=smem_idx_dO, zero_init=not dKV_accumulate, wg_wait=-1)
1441
+
1442
+ # smem fence to make sure sdS is written before it's read by WGMMA
1443
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1444
+ cute.arch.barrier(
1445
+ barrier_id=int(NamedBarrierBwd.PdS), number_of_threads=self.num_mma_threads
1446
+ )
1447
+ # (6) [GEMM 4] dQ = dS @ K
1448
+ acc_dQ = mma_dsk_fn(A_idx=smem_idx_PdS, wg_wait=1)
1449
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dV)
1450
+ pipeline_dO.consumer_release(consumer_state_dO_cur) # release dO as dV mma is done
1451
+
1452
+ # (7) [GEMM 5] dK += dS.T @ Q
1453
+ if const_expr(not self.mma_dkv_is_rs):
1454
+ mma_dsq_fn(
1455
+ A_idx=smem_idx_PdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1
1456
+ )
1457
+ else:
1458
+ mma_dsq_fn(tCrA=tdKrdS, B_idx=smem_idx_Q, zero_init=not dKV_accumulate, wg_wait=1)
1459
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dQ)
1460
+
1461
+ cute.arch.barrier(
1462
+ barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1463
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1464
+ )
1465
+ tdQrdQaccum_flat = cute.make_tensor(acc_dQ.iterator, cute.make_layout(tdQsdQaccum.shape))
1466
+ cute.autovec_copy(tdQrdQaccum_flat, tdQsdQaccum)
1467
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1468
+ cute.arch.barrier_arrive(
1469
+ barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1470
+ number_of_threads=self.num_threads_per_warp_group + cute.arch.WARP_SIZE,
1471
+ )
1472
+
1473
+ warpgroup.wait_group(0)
1474
+ # if cute.arch.thread_idx()[0] == 128: cute.print_tensor(acc_dK)
1475
+ pipeline_Q.consumer_release(consumer_state_Q)
1476
+ # if cute.arch.thread_idx()[0] % 32 == 0: cute.printf("tidx = {}, m_block = {}, after pipeline_Q consumer release", cute.arch.thread_idx()[0], m_block)
1477
+
1478
+ consumer_state_Q.advance()
1479
+ consumer_state_dO.advance()
1480
+ return consumer_state_Q, consumer_state_dO
1481
+
1482
+ @cute.jit
1483
+ def epilogue_dKV(
1484
+ self,
1485
+ acc_dV: cute.Tensor,
1486
+ mdV: cute.Tensor,
1487
+ sV: cute.Tensor,
1488
+ acc_dK: cute.Tensor,
1489
+ mdK: cute.Tensor,
1490
+ sK: cute.Tensor,
1491
+ seqlen: SeqlenInfoQK,
1492
+ tma_atom_dK: cute.CopyAtom,
1493
+ tma_atom_dV: cute.CopyAtom,
1494
+ tiled_mma_dK: cute.TiledMma,
1495
+ tiled_mma_dV: cute.TiledMma,
1496
+ r2s_tiled_copy_dKVaccum: cute.TiledCopy,
1497
+ sdKVaccum_layout: cute.Layout,
1498
+ tidx: Int32,
1499
+ n_block: Int32,
1500
+ head_idx: Int32,
1501
+ batch_idx: Int32,
1502
+ qhead_per_kvhead_divmod: Optional[FastDivmodDivisor] = None,
1503
+ ):
1504
+ warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
1505
+
1506
+ if const_expr(self.qhead_per_kvhead == 1):
1507
+ rdV = cute.make_fragment_like(acc_dV, self.dtype)
1508
+ rdV.store(acc_dV.load().to(self.dtype))
1509
+ rdK = utils.cvt_f16(acc_dK, self.dtype)
1510
+
1511
+ cute.arch.barrier(
1512
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1513
+ )
1514
+
1515
+ smem_copy_atom_dKV = cute.make_copy_atom(
1516
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=self.dKV_swapAB, num_matrices=4),
1517
+ self.dtype,
1518
+ )
1519
+ smem_thr_copy_dK = cute.make_tiled_copy_C(smem_copy_atom_dKV, tiled_mma_dK).get_slice(
1520
+ tidx
1521
+ )
1522
+ smem_thr_copy_dV = cute.make_tiled_copy_C(smem_copy_atom_dKV, tiled_mma_dV).get_slice(
1523
+ tidx
1524
+ )
1525
+ mdV_cur = mdV[None, None, head_idx, batch_idx]
1526
+ mdK_cur = mdK[None, None, head_idx, batch_idx]
1527
+ gdK = cute.local_tile(mdK_cur, (self.tile_n, self.tile_hdim), (n_block, 0))
1528
+ gdV = cute.local_tile(mdV_cur, (self.tile_n, self.tile_hdimv), (n_block, 0))
1529
+ store_dK, _, _ = copy_utils.tma_get_copy_fn(
1530
+ tma_atom_dK, 0, cute.make_layout(1), sK, gdK, single_stage=True
1531
+ )
1532
+ store_dV, _, _ = copy_utils.tma_get_copy_fn(
1533
+ tma_atom_dV, 0, cute.make_layout(1), sV, gdV, single_stage=True
1534
+ )
1535
+
1536
+ taccdVrdV = smem_thr_copy_dV.retile(rdV)
1537
+ sdV = sV if const_expr(not self.dKV_swapAB) else utils.transpose_view(sV)
1538
+ taccdVsdV = smem_thr_copy_dV.partition_D(sdV)
1539
+ cute.copy(smem_copy_atom_dKV, taccdVrdV, taccdVsdV)
1540
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1541
+ cute.arch.barrier(
1542
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1543
+ )
1544
+ if warp_idx == 4:
1545
+ store_dV()
1546
+ taccdKrdK = smem_thr_copy_dK.retile(rdK)
1547
+ sdK = sK if const_expr(not self.dKV_swapAB) else utils.transpose_view(sK)
1548
+ taccdKsdK = smem_thr_copy_dK.partition_D(sdK)
1549
+ cute.copy(smem_copy_atom_dKV, taccdKrdK, taccdKsdK)
1550
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1551
+ cute.arch.barrier(
1552
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1553
+ )
1554
+ if warp_idx == 4:
1555
+ store_dK()
1556
+ cute.arch.cp_async_bulk_commit_group()
1557
+ cute.arch.cp_async_bulk_wait_group(0, read=True)
1558
+ else:
1559
+ head_idx_kv = head_idx // qhead_per_kvhead_divmod
1560
+
1561
+ mdKaccum_cur = mdK[None, head_idx_kv, batch_idx]
1562
+ gdKaccum_ = cute.local_tile(mdKaccum_cur, (self.tile_n * self.tile_hdim,), (n_block,))
1563
+ gdKaccum = cute.flat_divide(
1564
+ gdKaccum_, (self.tile_n * self.tile_hdim // self.num_mma_warp_groups,)
1565
+ )
1566
+
1567
+ mdVaccum_cur = mdV[None, head_idx_kv, batch_idx]
1568
+ gdVaccum_ = cute.local_tile(mdVaccum_cur, (self.tile_n * self.tile_hdimv,), (n_block,))
1569
+ gdVaccum = cute.flat_divide(
1570
+ gdVaccum_, (self.tile_n * self.tile_hdimv // self.num_mma_warp_groups,)
1571
+ )
1572
+
1573
+ sdKVaccum = cute.make_tensor(
1574
+ cute.recast_ptr(sV.iterator, dtype=Float32),
1575
+ sdKVaccum_layout,
1576
+ )
1577
+
1578
+ smem_thr_copy_dKVaccum = r2s_tiled_copy_dKVaccum.get_slice(tidx)
1579
+ tdKsdKVaccum = smem_thr_copy_dKVaccum.partition_D(sdKVaccum)
1580
+
1581
+ cute.arch.barrier(
1582
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1583
+ )
1584
+
1585
+ tdKrdKaccum_flat = cute.make_tensor(
1586
+ acc_dK.iterator, cute.make_layout(tdKsdKVaccum.shape)
1587
+ )
1588
+ cute.autovec_copy(tdKrdKaccum_flat, tdKsdKVaccum)
1589
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1590
+ cute.arch.barrier(
1591
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1592
+ )
1593
+
1594
+ if warp_idx == 4:
1595
+ with cute.arch.elect_one():
1596
+ for wg_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
1597
+ copy_utils.cpasync_reduce_bulk_add_f32(
1598
+ sdKVaccum[None, wg_idx].iterator,
1599
+ gdKaccum[None, wg_idx].iterator,
1600
+ self.tma_copy_bytes["dKacc"] // self.num_mma_warp_groups,
1601
+ )
1602
+ cute.arch.cp_async_bulk_commit_group()
1603
+ cute.arch.cp_async_bulk_wait_group(0, read=True)
1604
+
1605
+ cute.arch.barrier(
1606
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1607
+ )
1608
+
1609
+ tdVrdVaccum_flat = cute.make_tensor(
1610
+ acc_dV.iterator, cute.make_layout(tdKsdKVaccum.shape)
1611
+ )
1612
+ cute.autovec_copy(tdVrdVaccum_flat, tdKsdKVaccum)
1613
+ cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
1614
+ cute.arch.barrier(
1615
+ barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_mma_threads
1616
+ )
1617
+
1618
+ if warp_idx == 4:
1619
+ with cute.arch.elect_one():
1620
+ for wg_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
1621
+ copy_utils.cpasync_reduce_bulk_add_f32(
1622
+ sdKVaccum[None, wg_idx].iterator,
1623
+ gdVaccum[None, wg_idx].iterator,
1624
+ self.tma_copy_bytes["dVacc"] // self.num_mma_warp_groups,
1625
+ )
1626
+ cute.arch.cp_async_bulk_commit_group()
1627
+ cute.arch.cp_async_bulk_wait_group(0, read=True)
1628
+
1629
+ @cute.jit
1630
+ def dQaccum_store(
1631
+ self,
1632
+ mdQaccum: cute.Tensor,
1633
+ sdQaccum: cute.Tensor,
1634
+ block_info: BlockInfo,
1635
+ TileSchedulerCls: cutlass.Constexpr[Callable],
1636
+ SeqlenInfoCls: cutlass.Constexpr[Callable],
1637
+ blocksparse_tensors: Optional[BlockSparseTensors] = None,
1638
+ ):
1639
+ tile_scheduler = TileSchedulerCls()
1640
+ work_tile = tile_scheduler.initial_work_tile_info()
1641
+ while work_tile.is_valid_tile:
1642
+ n_block, head_idx, batch_idx, _ = work_tile.tile_idx
1643
+ seqlen = SeqlenInfoCls(batch_idx)
1644
+ mdQaccum_cur = mdQaccum[None, head_idx, batch_idx]
1645
+ gdQaccum_ = cute.local_tile(mdQaccum_cur, (self.tile_m * self.tile_hdim,), (None,))
1646
+ # (M * K / WG, WG, _)
1647
+ gdQaccum = cute.flat_divide(
1648
+ gdQaccum_, (self.tile_m * self.tile_hdim // self.num_mma_warp_groups,)
1649
+ )
1650
+ m_block_min, m_block_max = block_info.get_m_block_min_max(seqlen, n_block)
1651
+ if const_expr(not self.use_block_sparsity):
1652
+ process_tile = const_expr(not self.is_local) or m_block_min < m_block_max
1653
+ loop_count = m_block_max - m_block_min
1654
+ else:
1655
+ total_block_cnt = get_total_q_block_count_bwd(
1656
+ blocksparse_tensors,
1657
+ batch_idx,
1658
+ head_idx,
1659
+ n_block,
1660
+ subtile_factor=self.subtile_factor,
1661
+ m_block_max=m_block_max,
1662
+ )
1663
+ process_tile = total_block_cnt > Int32(0)
1664
+
1665
+ if process_tile:
1666
+ if const_expr(not self.use_block_sparsity):
1667
+ for iter_idx in cutlass.range(loop_count, unroll=1):
1668
+ m_block = m_block_min + iter_idx
1669
+ m_block_safe = m_block
1670
+
1671
+ for warp_group_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
1672
+ cute.arch.barrier(
1673
+ barrier_id=int(NamedBarrierBwd.dQFullWG0) + warp_group_idx,
1674
+ number_of_threads=self.num_threads_per_warp_group
1675
+ + cute.arch.WARP_SIZE,
1676
+ )
1677
+ with cute.arch.elect_one():
1678
+ copy_utils.cpasync_reduce_bulk_add_f32(
1679
+ sdQaccum[None, warp_group_idx].iterator,
1680
+ gdQaccum[None, warp_group_idx, m_block_safe].iterator,
1681
+ self.tma_copy_bytes["dQ"],
1682
+ )
1683
+ cute.arch.cp_async_bulk_commit_group()
1684
+ for warp_group_idx in cutlass.range_constexpr(self.num_mma_warp_groups):
1685
+ cute.arch.cp_async_bulk_wait_group(
1686
+ self.num_mma_warp_groups - 1 - warp_group_idx, read=True
1687
+ )
1688
+ cute.arch.barrier_arrive(
1689
+ barrier_id=int(NamedBarrierBwd.dQEmptyWG0) + warp_group_idx,
1690
+ number_of_threads=self.num_threads_per_warp_group
1691
+ + cute.arch.WARP_SIZE,
1692
+ )
1693
+ else:
1694
+ dQaccum_store_block_sparse_bwd_sm90(
1695
+ blocksparse_tensors,
1696
+ batch_idx,
1697
+ head_idx,
1698
+ n_block,
1699
+ sdQaccum,
1700
+ gdQaccum,
1701
+ subtile_factor=self.subtile_factor,
1702
+ m_block_max=m_block_max,
1703
+ num_mma_warp_groups=self.num_mma_warp_groups,
1704
+ num_threads_per_warp_group=self.num_threads_per_warp_group,
1705
+ tma_copy_bytes_dQ=self.tma_copy_bytes["dQ"],
1706
+ )
1707
+ tile_scheduler.advance_to_next_work()
1708
+ work_tile = tile_scheduler.get_current_work()
kernel/deps/flash_attn/cute/flash_fwd.py ADDED
The diff for this file is too large to render. See raw diff
 
kernel/deps/flash_attn/cute/flash_fwd_combine.py ADDED
@@ -0,0 +1,704 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_combine_kernel.h
3
+ # from Cutlass C++ to Cute-DSL.
4
+ import math
5
+ import operator
6
+ from typing import Type, Optional
7
+ from functools import partial
8
+
9
+ import cuda.bindings.driver as cuda
10
+
11
+ import cutlass
12
+ import cutlass.cute as cute
13
+ from cutlass.cute.nvgpu import cpasync
14
+ from cutlass import Float32, Int32, const_expr
15
+
16
+ from flash_attn.cute import utils
17
+ from flash_attn.cute.seqlen_info import SeqlenInfo
18
+ from cutlass.cute import FastDivmodDivisor
19
+
20
+
21
+ class FlashAttentionForwardCombine:
22
+ def __init__(
23
+ self,
24
+ dtype: Type[cutlass.Numeric],
25
+ dtype_partial: Type[cutlass.Numeric],
26
+ head_dim: int,
27
+ m_block_size: int = 8,
28
+ k_block_size: int = 64,
29
+ log_max_splits: int = 4,
30
+ num_threads: int = 256,
31
+ stages: int = 4,
32
+ ):
33
+ """
34
+ Forward combine kernel for split attention computation.
35
+
36
+ :param dtype: output data type
37
+ :param dtype_partial: partial accumulation data type
38
+ :param head_dim: head dimension
39
+ :param m_block_size: m block size
40
+ :param k_block_size: k block size
41
+ :param log_max_splits: log2 of maximum splits
42
+ :param num_threads: number of threads
43
+ :param varlen: whether using variable length sequences
44
+ :param stages: number of pipeline stages
45
+ """
46
+ self.dtype = dtype
47
+ self.dtype_partial = dtype_partial
48
+ self.head_dim = head_dim
49
+ self.m_block_size = m_block_size
50
+ self.k_block_size = k_block_size
51
+ self.max_splits = 1 << log_max_splits
52
+ self.num_threads = num_threads
53
+ self.is_even_k = head_dim % k_block_size == 0
54
+ self.stages = stages
55
+
56
+ @staticmethod
57
+ def can_implement(
58
+ dtype,
59
+ dtype_partial,
60
+ head_dim,
61
+ m_block_size,
62
+ k_block_size,
63
+ log_max_splits,
64
+ num_threads,
65
+ ) -> bool:
66
+ """Check if the kernel can be implemented with the given parameters."""
67
+ if dtype not in [cutlass.Float16, cutlass.BFloat16, cutlass.Float32]:
68
+ return False
69
+ if dtype_partial not in [cutlass.Float16, cutlass.BFloat16, Float32]:
70
+ return False
71
+ if head_dim % 8 != 0:
72
+ return False
73
+ if num_threads % 32 != 0:
74
+ return False
75
+ if m_block_size % 8 != 0:
76
+ return False
77
+ max_splits = 1 << log_max_splits
78
+ if max_splits > 256:
79
+ return False
80
+ if (m_block_size * max_splits) % num_threads != 0:
81
+ return False
82
+ return True
83
+
84
+ def _setup_attributes(self):
85
+ # GMEM copy setup for O partial
86
+ universal_copy_bits = 128
87
+ async_copy_elems = universal_copy_bits // self.dtype_partial.width
88
+ assert self.k_block_size % async_copy_elems == 0
89
+
90
+ k_block_gmem = (
91
+ 128 if self.k_block_size % 128 == 0 else (64 if self.k_block_size % 64 == 0 else 32)
92
+ )
93
+ gmem_threads_per_row = k_block_gmem // async_copy_elems
94
+ assert self.num_threads % gmem_threads_per_row == 0
95
+
96
+ # Async copy atom for O partial load
97
+ atom_async_copy_partial = cute.make_copy_atom(
98
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
99
+ self.dtype_partial,
100
+ num_bits_per_copy=universal_copy_bits,
101
+ )
102
+ tOpartial_layout = cute.make_ordered_layout(
103
+ (self.num_threads // gmem_threads_per_row, gmem_threads_per_row),
104
+ order=(1, 0),
105
+ )
106
+ vOpartial_layout = cute.make_layout((1, async_copy_elems)) # 4 vals per load
107
+ self.gmem_tiled_copy_O_partial = cute.make_tiled_copy_tv(
108
+ atom_async_copy_partial, tOpartial_layout, vOpartial_layout
109
+ )
110
+
111
+ # GMEM copy setup for final O (use universal copy for store)
112
+ atom_universal_copy = cute.make_copy_atom(
113
+ cute.nvgpu.CopyUniversalOp(),
114
+ self.dtype,
115
+ num_bits_per_copy=async_copy_elems * self.dtype.width,
116
+ )
117
+ self.gmem_tiled_copy_O = cute.make_tiled_copy_tv(
118
+ atom_universal_copy,
119
+ tOpartial_layout,
120
+ vOpartial_layout, # 4 vals per store
121
+ )
122
+
123
+ # LSE copy setup with async copy (alignment = 1)
124
+ lse_copy_bits = Float32.width # 1 element per copy, width is in bits
125
+ m_block_smem = (
126
+ 128
127
+ if self.m_block_size % 128 == 0
128
+ else (
129
+ 64
130
+ if self.m_block_size % 64 == 0
131
+ else (
132
+ 32
133
+ if self.m_block_size % 32 == 0
134
+ else (16 if self.m_block_size % 16 == 0 else 8)
135
+ )
136
+ )
137
+ )
138
+ gmem_threads_per_row_lse = m_block_smem
139
+ assert self.num_threads % gmem_threads_per_row_lse == 0
140
+
141
+ # Async copy atom for LSE load
142
+ atom_async_copy_lse = cute.make_copy_atom(
143
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.ALWAYS),
144
+ Float32,
145
+ num_bits_per_copy=lse_copy_bits,
146
+ )
147
+ tLSE_layout = cute.make_ordered_layout(
148
+ (self.num_threads // gmem_threads_per_row_lse, gmem_threads_per_row_lse),
149
+ order=(1, 0),
150
+ )
151
+ vLSE_layout = cute.make_layout(1)
152
+ self.gmem_tiled_copy_LSE = cute.make_tiled_copy_tv(
153
+ atom_async_copy_lse, tLSE_layout, vLSE_layout
154
+ )
155
+
156
+ # ///////////////////////////////////////////////////////////////////////////////
157
+ # Shared memory
158
+ # ///////////////////////////////////////////////////////////////////////////////
159
+
160
+ # Shared memory to register copy for LSE
161
+ self.smem_threads_per_col_lse = self.num_threads // m_block_smem
162
+ assert 32 % self.smem_threads_per_col_lse == 0 # Must divide warp size
163
+
164
+ s2r_layout_atom_lse = cute.make_ordered_layout(
165
+ (self.smem_threads_per_col_lse, self.num_threads // self.smem_threads_per_col_lse),
166
+ order=(0, 1),
167
+ )
168
+ self.s2r_tiled_copy_LSE = cute.make_tiled_copy_tv(
169
+ cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32),
170
+ s2r_layout_atom_lse,
171
+ cute.make_layout(1),
172
+ )
173
+
174
+ # LSE shared memory layout with swizzling to avoid bank conflicts
175
+ # This works for kBlockMSmem = 8, 16, 32, 64, 128, no bank conflicts
176
+ if const_expr(m_block_smem == 8):
177
+ smem_lse_swizzle = cute.make_swizzle(5, 0, 5)
178
+ elif const_expr(m_block_smem == 16):
179
+ smem_lse_swizzle = cute.make_swizzle(4, 0, 4)
180
+ else:
181
+ smem_lse_swizzle = cute.make_swizzle(3, 2, 3)
182
+ smem_layout_atom_lse = cute.make_composed_layout(
183
+ smem_lse_swizzle, 0, cute.make_ordered_layout((8, m_block_smem), order=(1, 0))
184
+ )
185
+ self.smem_layout_lse = cute.tile_to_shape(
186
+ smem_layout_atom_lse, (self.max_splits, self.m_block_size), (0, 1)
187
+ )
188
+
189
+ # O partial shared memory layout (simple layout for pipeline stages)
190
+ self.smem_layout_o = cute.make_ordered_layout(
191
+ (self.m_block_size, self.k_block_size, self.stages), order=(1, 0, 2)
192
+ )
193
+
194
+ @cute.jit
195
+ def __call__(
196
+ self,
197
+ mO_partial: cute.Tensor,
198
+ mLSE_partial: cute.Tensor,
199
+ mO: cute.Tensor,
200
+ mLSE: Optional[cute.Tensor] = None,
201
+ cu_seqlens: Optional[cute.Tensor] = None,
202
+ seqused: Optional[cute.Tensor] = None,
203
+ num_splits_dynamic_ptr: Optional[cute.Tensor] = None,
204
+ semaphore_to_reset: Optional[cute.Tensor] = None,
205
+ stream: cuda.CUstream = None,
206
+ ):
207
+ # Type checking
208
+ if const_expr(not (mO_partial.element_type == self.dtype_partial)):
209
+ raise TypeError("O partial tensor must match dtype_partial")
210
+ if const_expr(not (mO.element_type == self.dtype)):
211
+ raise TypeError("O tensor must match dtype")
212
+ if const_expr(mLSE_partial.element_type not in [Float32]):
213
+ raise TypeError("LSE partial tensor must be Float32")
214
+ if const_expr(mLSE is not None and mLSE.element_type not in [Float32]):
215
+ raise TypeError("LSE tensor must be Float32")
216
+
217
+ # Shape validation - input tensors are in user format, need to be converted to kernel format
218
+ if const_expr(len(mO_partial.shape) not in [4, 5]):
219
+ raise ValueError(
220
+ "O partial tensor must have 4 or 5 dimensions: (num_splits, batch, seqlen, nheads, headdim) or (num_splits, total_q, nheads, headdim)"
221
+ )
222
+ if const_expr(len(mLSE_partial.shape) not in [3, 4]):
223
+ raise ValueError(
224
+ "LSE partial tensor must have 3 or 4 dimensions: (num_splits, batch, seqlen, nheads) or (num_splits, total_q, nheads)"
225
+ )
226
+ if const_expr(len(mO.shape) not in [3, 4]):
227
+ raise ValueError(
228
+ "O tensor must have 3 or 4 dimensions: (batch, seqlen, nheads, headdim) or (total_q, nheads, headdim)"
229
+ )
230
+ if const_expr(mLSE is not None and len(mLSE.shape) not in [2, 3]):
231
+ raise ValueError(
232
+ "LSE tensor must have 2 or 3 dimensions: (batch, seqlen, nheads) or (total_q, nheads)"
233
+ )
234
+
235
+ # Assume all strides are divisible by 128 bits except the last stride
236
+ new_stride = lambda t: (
237
+ *(cute.assume(s, divby=128 // t.element_type.width) for s in t.stride[:-1]),
238
+ t.stride[-1],
239
+ )
240
+ mO_partial, mO = [
241
+ cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
242
+ for t in (mO_partial, mO)
243
+ ]
244
+ # (num_splits, b, seqlen, h, d) -> (seqlen, d, num_splits, h, b)
245
+ # or (num_splits, total_q, h, d) -> (total_q, d, num_splits, h)
246
+ O_partial_layout_transpose = (
247
+ [2, 4, 0, 3, 1] if const_expr(cu_seqlens is None) else [1, 3, 0, 2]
248
+ )
249
+ # (b, seqlen, h, d) -> (seqlen, d, h, b) or (total_q, h, d) -> (total_q, d, h)
250
+ mO_partial = cute.make_tensor(
251
+ mO_partial.iterator, cute.select(mO_partial.layout, mode=O_partial_layout_transpose)
252
+ )
253
+ O_layout_transpose = [1, 3, 2, 0] if const_expr(cu_seqlens is None) else [0, 2, 1]
254
+ mO = cute.make_tensor(mO.iterator, cute.select(mO.layout, mode=O_layout_transpose))
255
+ # (num_splits, b, seqlen, h) -> (seqlen, num_splits, h, b)
256
+ # or (num_splits, total_q, h) -> (total_q, num_splits, h)
257
+ LSE_partial_layout_transpose = [2, 0, 3, 1] if const_expr(cu_seqlens is None) else [1, 0, 2]
258
+ mLSE_partial = cute.make_tensor(
259
+ mLSE_partial.iterator,
260
+ cute.select(mLSE_partial.layout, mode=LSE_partial_layout_transpose),
261
+ )
262
+ # (b, seqlen, h) -> (seqlen, h, b) or (total_q, h) -> (total_q, h)
263
+ LSE_layout_transpose = [1, 2, 0] if const_expr(cu_seqlens is None) else [0, 1]
264
+ mLSE = (
265
+ cute.make_tensor(mLSE.iterator, cute.select(mLSE.layout, mode=LSE_layout_transpose))
266
+ if mLSE is not None
267
+ else None
268
+ )
269
+
270
+ # Determine if we have variable length sequences
271
+ varlen = const_expr(cu_seqlens is not None or seqused is not None)
272
+
273
+ self._setup_attributes()
274
+
275
+ @cute.struct
276
+ class SharedStorage:
277
+ sLSE: cute.struct.Align[
278
+ cute.struct.MemRange[Float32, cute.cosize(self.smem_layout_lse)], 128
279
+ ]
280
+ sMaxValidSplit: cute.struct.Align[cute.struct.MemRange[Int32, self.m_block_size], 128]
281
+ sO: cute.struct.Align[
282
+ cute.struct.MemRange[self.dtype_partial, cute.cosize(self.smem_layout_o)], 128
283
+ ]
284
+
285
+ smem_size = SharedStorage.size_in_bytes()
286
+
287
+ # Grid dimensions: (ceil_div(seqlen, m_block), ceil_div(head_dim, k_block), num_head * batch)
288
+ seqlen = mO_partial.shape[0]
289
+ num_head = mO_partial.shape[3]
290
+ batch_size = (
291
+ mO_partial.shape[4]
292
+ if const_expr(cu_seqlens is None)
293
+ else Int32(cu_seqlens.shape[0] - 1)
294
+ )
295
+
296
+ # Create FastDivmodDivisor objects for efficient division
297
+ seqlen_divmod = FastDivmodDivisor(seqlen)
298
+ head_divmod = FastDivmodDivisor(num_head)
299
+
300
+ grid_dim = (
301
+ cute.ceil_div(seqlen * num_head, self.m_block_size),
302
+ cute.ceil_div(self.head_dim, self.k_block_size),
303
+ batch_size,
304
+ )
305
+
306
+ self.kernel(
307
+ mO_partial,
308
+ mLSE_partial,
309
+ mO,
310
+ mLSE,
311
+ cu_seqlens,
312
+ seqused,
313
+ num_splits_dynamic_ptr,
314
+ semaphore_to_reset,
315
+ SharedStorage,
316
+ self.smem_layout_lse,
317
+ self.smem_layout_o,
318
+ self.gmem_tiled_copy_O_partial,
319
+ self.gmem_tiled_copy_O,
320
+ self.gmem_tiled_copy_LSE,
321
+ self.s2r_tiled_copy_LSE,
322
+ seqlen_divmod,
323
+ head_divmod,
324
+ varlen,
325
+ ).launch(
326
+ grid=grid_dim,
327
+ block=[self.num_threads, 1, 1],
328
+ smem=smem_size,
329
+ stream=stream,
330
+ )
331
+
332
+ @cute.kernel
333
+ def kernel(
334
+ self,
335
+ mO_partial: cute.Tensor,
336
+ mLSE_partial: cute.Tensor,
337
+ mO: cute.Tensor,
338
+ mLSE: Optional[cute.Tensor],
339
+ cu_seqlens: Optional[cute.Tensor],
340
+ seqused: Optional[cute.Tensor],
341
+ num_splits_dynamic_ptr: Optional[cute.Tensor],
342
+ semaphore_to_reset: Optional[cute.Tensor],
343
+ SharedStorage: cutlass.Constexpr,
344
+ smem_layout_lse: cute.Layout | cute.ComposedLayout,
345
+ smem_layout_o: cute.Layout,
346
+ gmem_tiled_copy_O_partial: cute.TiledCopy,
347
+ gmem_tiled_copy_O: cute.TiledCopy,
348
+ gmem_tiled_copy_LSE: cute.TiledCopy,
349
+ s2r_tiled_copy_LSE: cute.TiledCopy,
350
+ seqlen_divmod: FastDivmodDivisor,
351
+ head_divmod: FastDivmodDivisor,
352
+ varlen: cutlass.Constexpr[bool],
353
+ ):
354
+ # Thread and block indices
355
+ tidx, _, _ = cute.arch.thread_idx()
356
+ m_block, k_block, batch_idx = cute.arch.block_idx()
357
+
358
+ # ///////////////////////////////////////////////////////////////////////////////
359
+ # Get shared memory buffer
360
+ # ///////////////////////////////////////////////////////////////////////////////
361
+ smem = cutlass.utils.SmemAllocator()
362
+ storage = smem.allocate(SharedStorage)
363
+ sLSE = storage.sLSE.get_tensor(smem_layout_lse)
364
+ sMaxValidSplit = storage.sMaxValidSplit.get_tensor((self.m_block_size,))
365
+ sO = storage.sO.get_tensor(smem_layout_o)
366
+
367
+ # Handle semaphore reset
368
+ if const_expr(semaphore_to_reset is not None):
369
+ if (
370
+ tidx == 0
371
+ and m_block == cute.arch.grid_dim()[0] - 1
372
+ and k_block == cute.arch.grid_dim()[1] - 1
373
+ and batch_idx == cute.arch.grid_dim()[2] - 1
374
+ ):
375
+ semaphore_to_reset[0] = 0
376
+
377
+ # Get number of splits
378
+ num_splits = (
379
+ num_splits_dynamic_ptr[batch_idx]
380
+ if const_expr(num_splits_dynamic_ptr is not None)
381
+ else mLSE_partial.shape[1]
382
+ )
383
+ # Handle variable length sequences using SeqlenInfo
384
+ seqlen_info = SeqlenInfo.create(
385
+ batch_idx=batch_idx,
386
+ seqlen_static=mO_partial.shape[0],
387
+ cu_seqlens=cu_seqlens,
388
+ seqused=seqused,
389
+ )
390
+ seqlen, offset = seqlen_info.seqlen, seqlen_info.offset
391
+
392
+ # Extract number of heads (head index will be determined dynamically)
393
+ num_head = mO_partial.shape[3]
394
+ max_idx = seqlen * num_head
395
+
396
+ # Early exit for single split if dynamic
397
+ if (const_expr(num_splits_dynamic_ptr is None) or num_splits > 1) and (
398
+ const_expr(not varlen) or m_block * self.m_block_size < max_idx
399
+ ):
400
+ # ===============================
401
+ # Step 1: Load LSE_partial from gmem to shared memory
402
+ # ===============================
403
+
404
+ if const_expr(cu_seqlens is None):
405
+ # mLSE_partial_cur = mLSE_partial[None, None, None, batch_idx]
406
+ mLSE_partial_cur = utils.coord_offset_i64(mLSE_partial, batch_idx, dim=3)
407
+ else:
408
+ # mLSE_partial_cur = cute.domain_offset((offset, 0, 0), mLSE_partial)
409
+ mLSE_partial_cur = utils.domain_offset_i64((offset, 0, 0), mLSE_partial)
410
+ mLSE_partial_copy = cute.tiled_divide(mLSE_partial_cur, (1,))
411
+
412
+ gmem_thr_copy_LSE = gmem_tiled_copy_LSE.get_slice(tidx)
413
+ tLSEsLSE = gmem_thr_copy_LSE.partition_D(sLSE)
414
+
415
+ # Create identity tensor for coordinate tracking
416
+ cLSE = cute.make_identity_tensor((self.max_splits, self.m_block_size))
417
+ tLSEcLSE = gmem_thr_copy_LSE.partition_S(cLSE)
418
+
419
+ # Load LSE partial values
420
+ for m in cutlass.range(cute.size(tLSEcLSE, mode=[2]), unroll_full=True):
421
+ mi = tLSEcLSE[0, 0, m][1] # Get m coordinate
422
+ idx = m_block * self.m_block_size + mi
423
+ if idx < max_idx:
424
+ # Calculate actual sequence position and head using FastDivmodDivisor
425
+ if const_expr(not varlen):
426
+ head_idx, m_idx = divmod(idx, seqlen_divmod)
427
+ else:
428
+ head_idx = idx // seqlen
429
+ m_idx = idx - head_idx * seqlen
430
+ mLSE_partial_cur_copy = mLSE_partial_copy[None, m_idx, None, head_idx]
431
+ for s in cutlass.range(cute.size(tLSEcLSE, mode=[1]), unroll_full=True):
432
+ si = tLSEcLSE[0, s, 0][0] # Get split coordinate
433
+ if si < num_splits:
434
+ cute.copy(
435
+ gmem_thr_copy_LSE,
436
+ mLSE_partial_cur_copy[None, si],
437
+ tLSEsLSE[None, s, m],
438
+ )
439
+ else:
440
+ tLSEsLSE[None, s, m].fill(-Float32.inf)
441
+ # Don't need to zero out the rest of the LSEs, as we will not write the output to gmem
442
+ cute.arch.cp_async_commit_group()
443
+
444
+ # ===============================
445
+ # Step 2: Load O_partial for pipeline stages
446
+ # ===============================
447
+
448
+ gmem_thr_copy_O_partial = gmem_tiled_copy_O_partial.get_slice(tidx)
449
+ cO = cute.make_identity_tensor((self.m_block_size, self.k_block_size))
450
+ tOcO = gmem_thr_copy_O_partial.partition_D(cO)
451
+ tOsO_partial = gmem_thr_copy_O_partial.partition_D(sO)
452
+ if const_expr(cu_seqlens is None):
453
+ # mO_partial_cur = mO_partial[None, None, None, None, batch_idx]
454
+ mO_partial_cur = utils.coord_offset_i64(mO_partial, batch_idx, dim=4)
455
+ else:
456
+ # mO_partial_cur = cute.domain_offset((offset, 0, 0, 0), mO_partial)
457
+ mO_partial_cur = utils.domain_offset_i64((offset, 0, 0, 0), mO_partial)
458
+
459
+ # Precompute these values to avoid recomputing them in the loop
460
+ num_rows = const_expr(cute.size(tOcO, mode=[1]))
461
+ tOmidx = cute.make_fragment(num_rows, cutlass.Int32)
462
+ tOhidx = cute.make_fragment(num_rows, cutlass.Int32)
463
+ tOrOptr = cute.make_fragment(num_rows, cutlass.Int64)
464
+ for m in cutlass.range(num_rows, unroll_full=True):
465
+ mi = tOcO[0, m, 0][0] # m coordinate
466
+ idx = m_block * self.m_block_size + mi
467
+ if const_expr(not varlen):
468
+ tOhidx[m], tOmidx[m] = divmod(idx, seqlen_divmod)
469
+ else:
470
+ tOhidx[m] = idx // seqlen
471
+ tOmidx[m] = idx - tOhidx[m] * seqlen
472
+ tOrOptr[m] = utils.elem_pointer_i64(
473
+ mO_partial_cur, (tOmidx[m], k_block * self.k_block_size, 0, tOhidx[m])
474
+ ).toint()
475
+ if idx >= max_idx:
476
+ tOhidx[m] = -1
477
+
478
+ tOpO = cute.make_fragment(cute.size(tOcO, [2]), cutlass.Boolean)
479
+ if const_expr(not self.is_even_k):
480
+ for k in cutlass.range(cute.size(tOpO), unroll_full=True):
481
+ tOpO[k] = tOcO[0, 0, k][1] < mO_partial.shape[1] - k_block * self.k_block_size
482
+ # if cute.arch.thread_idx()[0] == 0 and k_block == 1: cute.print_tensor(tOpO)
483
+
484
+ load_O_partial = partial(
485
+ self.load_O_partial,
486
+ gmem_tiled_copy_O_partial,
487
+ tOrOptr,
488
+ tOsO_partial,
489
+ tOhidx,
490
+ tOpO,
491
+ tOcO,
492
+ mO_partial_cur.layout,
493
+ )
494
+
495
+ # Load first few stages of O_partial
496
+ for stage in cutlass.range(self.stages - 1, unroll_full=True):
497
+ if stage < num_splits:
498
+ load_O_partial(stage, stage)
499
+ cute.arch.cp_async_commit_group()
500
+
501
+ # ===============================
502
+ # Step 3: Load and transpose LSE from smem to registers
503
+ # ===============================
504
+
505
+ # Wait for LSE and initial O partial stages to complete
506
+ cute.arch.cp_async_wait_group(self.stages - 1)
507
+ cute.arch.sync_threads()
508
+ # if cute.arch.thread_idx()[0] == 0:
509
+ # # cute.print_tensor(sLSE)
510
+ # for i in range(64):
511
+ # cute.printf("sLSE[%d, 0] = %f", i, sLSE[i, 0])
512
+ # cute.arch.sync_threads()
513
+
514
+ s2r_thr_copy_LSE = s2r_tiled_copy_LSE.get_slice(tidx)
515
+ ts2rsLSE = s2r_thr_copy_LSE.partition_S(sLSE)
516
+ ts2rrLSE = cute.make_fragment_like(ts2rsLSE)
517
+ cute.copy(s2r_tiled_copy_LSE, ts2rsLSE, ts2rrLSE)
518
+
519
+ # ===============================
520
+ # Step 4: Compute final LSE along split dimension
521
+ # ===============================
522
+
523
+ lse_sum = cute.make_fragment(cute.size(ts2rrLSE, mode=[2]), Float32)
524
+ ts2rcLSE = s2r_thr_copy_LSE.partition_D(cLSE)
525
+ # We compute the max valid split for each row to short-circuit the computation later
526
+ max_valid_split = cute.make_fragment(cute.size(ts2rrLSE, mode=[2]), Int32)
527
+ assert cute.size(ts2rrLSE, mode=[0]) == 1
528
+ # Compute max, scales, and final LSE for each row
529
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
530
+ # Find max LSE value across splits
531
+ threads_per_col = const_expr(self.smem_threads_per_col_lse)
532
+ lse_max = utils.warp_reduce(
533
+ ts2rrLSE[None, None, m]
534
+ .load()
535
+ .reduce(cute.ReductionOp.MAX, init_val=-Float32.inf, reduction_profile=0),
536
+ op=cute.arch.fmax,
537
+ width=threads_per_col,
538
+ )
539
+ # if cute.arch.thread_idx()[0] == 0: cute.printf(lse_max)
540
+ # Find max valid split index
541
+ max_valid_idx = -1
542
+ for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
543
+ if ts2rrLSE[0, s, m] != -Float32.inf:
544
+ max_valid_idx = ts2rcLSE[0, s, 0][0] # Get split coordinate
545
+ # if cute.arch.thread_idx()[0] < 32: cute.printf(max_valid_idx)
546
+ max_valid_split[m] = utils.warp_reduce(max_valid_idx, max, width=threads_per_col)
547
+ # Compute exp scales and sum
548
+ lse_max_cur = (
549
+ 0.0 if lse_max == -Float32.inf else lse_max
550
+ ) # In case all local LSEs are -inf
551
+ LOG2_E = math.log2(math.e)
552
+ lse_sum_cur = 0.0
553
+ for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
554
+ scale = utils.exp2f(ts2rrLSE[0, s, m] * LOG2_E - (lse_max_cur * LOG2_E))
555
+ lse_sum_cur += scale
556
+ ts2rrLSE[0, s, m] = scale # Store scale for later use
557
+ lse_sum_cur = utils.warp_reduce(lse_sum_cur, operator.add, width=threads_per_col)
558
+ lse_sum[m] = utils.logf(lse_sum_cur) + lse_max
559
+ # Normalize scales
560
+ inv_sum = (
561
+ 0.0 if (lse_sum_cur == 0.0 or lse_sum_cur != lse_sum_cur) else 1.0 / lse_sum_cur
562
+ )
563
+ ts2rrLSE[None, None, m].store(ts2rrLSE[None, None, m].load() * inv_sum)
564
+ # Store the scales exp(lse - lse_logsum) back to smem
565
+ cute.copy(s2r_tiled_copy_LSE, ts2rrLSE, ts2rsLSE)
566
+
567
+ # Store max valid split to smem
568
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
569
+ if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
570
+ mi = ts2rcLSE[0, 0, m][1]
571
+ if mi < self.m_block_size:
572
+ sMaxValidSplit[mi] = max_valid_split[m]
573
+
574
+ # ===============================
575
+ # Step 5: Store final LSE to gmem
576
+ # ===============================
577
+
578
+ if const_expr(mLSE is not None):
579
+ if const_expr(cu_seqlens is None):
580
+ # mLSE_cur = mLSE[None, None, batch_idx]
581
+ mLSE_cur = utils.coord_offset_i64(mLSE, batch_idx, dim=2)
582
+ else:
583
+ # mLSE_cur = cute.domain_offset((offset, 0), mLSE)
584
+ mLSE_cur = utils.domain_offset_i64((offset, 0), mLSE)
585
+ if k_block == 0: # Only first k_block writes LSE when mLSE is provided
586
+ for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
587
+ if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
588
+ mi = ts2rcLSE[0, 0, m][1]
589
+ idx = m_block * self.m_block_size + mi
590
+ if idx < max_idx:
591
+ if const_expr(not varlen):
592
+ head_idx, m_idx = divmod(idx, seqlen_divmod)
593
+ else:
594
+ head_idx = idx // seqlen
595
+ m_idx = idx - head_idx * seqlen
596
+ mLSE_cur[m_idx, head_idx] = lse_sum[m]
597
+
598
+ # ===============================
599
+ # Step 6: Read O_partial and accumulate final O
600
+ # ===============================
601
+
602
+ cute.arch.sync_threads()
603
+
604
+ # Get max valid split for this thread
605
+ thr_max_valid_split = sMaxValidSplit[tOcO[0, 0, 0][0]]
606
+ for m in cutlass.range(1, cute.size(tOcO, mode=[1])):
607
+ thr_max_valid_split = max(thr_max_valid_split, sMaxValidSplit[tOcO[0, m, 0][0]])
608
+
609
+ tOrO_partial = cute.make_fragment_like(tOsO_partial[None, None, None, 0])
610
+ tOrO = cute.make_fragment_like(tOrO_partial, Float32)
611
+ tOrO.fill(0.0)
612
+
613
+ stage_load = self.stages - 1
614
+ stage_compute = 0
615
+
616
+ # Main accumulation loop
617
+ for s in cutlass.range(thr_max_valid_split + 1, unroll=4):
618
+ # Get scales for this split
619
+ scale = cute.make_fragment(num_rows, Float32)
620
+ for m in cutlass.range(num_rows, unroll_full=True):
621
+ scale[m] = sLSE[s, tOcO[0, m, 0][0]] # Get scale from smem
622
+
623
+ # Load next stage if needed
624
+ split_to_load = s + self.stages - 1
625
+ if split_to_load <= thr_max_valid_split:
626
+ load_O_partial(split_to_load, stage_load)
627
+ cute.arch.cp_async_commit_group()
628
+ stage_load = 0 if stage_load == self.stages - 1 else stage_load + 1
629
+
630
+ # Wait for the current stage to be ready
631
+ cute.arch.cp_async_wait_group(self.stages - 1)
632
+ # We don't need __syncthreads() because each thread is just reading its own data from smem
633
+ # Copy from smem to registers
634
+ cute.autovec_copy(tOsO_partial[None, None, None, stage_compute], tOrO_partial)
635
+ stage_compute = 0 if stage_compute == self.stages - 1 else stage_compute + 1
636
+
637
+ # Accumulate scaled partial results
638
+ for m in cutlass.range(num_rows, unroll_full=True):
639
+ if tOhidx[m] >= 0 and scale[m] > 0.0:
640
+ tOrO[None, m, None].store(
641
+ tOrO[None, m, None].load()
642
+ + scale[m] * tOrO_partial[None, m, None].load().to(Float32)
643
+ )
644
+
645
+ # ===============================
646
+ # Step 7: Write final O to gmem
647
+ # ===============================
648
+
649
+ rO = cute.make_fragment_like(tOrO, self.dtype)
650
+ rO.store(tOrO.load().to(self.dtype))
651
+ if const_expr(cu_seqlens is None):
652
+ # mO_cur = mO[None, None, None, batch_idx]
653
+ mO_cur = utils.coord_offset_i64(mO, batch_idx, dim=3)
654
+ else:
655
+ # mO_cur = cute.domain_offset((offset, 0, 0), mO)
656
+ mO_cur = utils.domain_offset_i64((offset, 0, 0), mO)
657
+ mO_cur = utils.domain_offset_aligned((0, k_block * self.k_block_size, 0), mO_cur)
658
+ elems_per_store = const_expr(cute.size(gmem_tiled_copy_O.layout_tv_tiled[1]))
659
+ # mO_cur_copy = cute.tiled_divide(mO_cur, (1, elems_per_store,))
660
+ gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
661
+ # Write final results
662
+ for m in cutlass.range(num_rows, unroll_full=True):
663
+ if tOhidx[m] >= 0:
664
+ mO_cur_copy = cute.tiled_divide(
665
+ mO_cur[tOmidx[m], None, tOhidx[m]], (elems_per_store,)
666
+ )
667
+ for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
668
+ k_idx = tOcO[0, 0, k][1] // elems_per_store
669
+ if const_expr(self.is_even_k) or tOpO[k]:
670
+ cute.copy(gmem_thr_copy_O, rO[None, m, k], mO_cur_copy[None, k_idx])
671
+
672
+ @cute.jit
673
+ def load_O_partial(
674
+ self,
675
+ gmem_tiled_copy_O_partial: cute.TiledCopy,
676
+ tOrOptr: cute.Tensor,
677
+ tOsO_partial: cute.Tensor,
678
+ tOhidx: cute.Tensor,
679
+ tOpO: cute.Tensor,
680
+ tOcO: cute.Tensor,
681
+ mO_cur_partial_layout: cute.Layout,
682
+ split: Int32,
683
+ stage: Int32,
684
+ ) -> None:
685
+ elems_per_load = const_expr(cute.size(gmem_tiled_copy_O_partial.layout_tv_tiled[1]))
686
+ tOsO_partial_cur = tOsO_partial[None, None, None, stage]
687
+ for m in cutlass.range(cute.size(tOcO, [1]), unroll_full=True):
688
+ if tOhidx[m] >= 0:
689
+ o_gmem_ptr = cute.make_ptr(
690
+ tOsO_partial.element_type, tOrOptr[m], cute.AddressSpace.gmem, assumed_align=16
691
+ )
692
+ mO_partial_cur = cute.make_tensor(
693
+ o_gmem_ptr, cute.slice_(mO_cur_partial_layout, (0, None, None, 0))
694
+ )
695
+ mO_partial_cur_copy = cute.tiled_divide(mO_partial_cur, (elems_per_load,))
696
+ for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
697
+ k_idx = tOcO[0, 0, k][1] // elems_per_load
698
+ if const_expr(self.is_even_k) or tOpO[k]:
699
+ cute.copy(
700
+ gmem_tiled_copy_O_partial,
701
+ # mO_partial_cur_copy[None, k_idx, split],
702
+ utils.coord_offset_i64(mO_partial_cur_copy, split, dim=2)[None, k_idx],
703
+ tOsO_partial_cur[None, m, k],
704
+ )
kernel/deps/flash_attn/cute/flash_fwd_sm100.py ADDED
The diff for this file is too large to render. See raw diff
 
kernel/deps/flash_attn/cute/hopper_helpers.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ from typing import Type, Union, Optional
3
+ import cutlass
4
+ import cutlass.cute as cute
5
+ from cutlass import Int32, Float32, Boolean, const_expr
6
+ from cutlass.cute.nvgpu import warpgroup
7
+ from cutlass.cutlass_dsl import Numeric, dsl_user_op
8
+ from cutlass.utils import LayoutEnum
9
+ import cutlass.utils.hopper_helpers as sm90_utils_og
10
+
11
+
12
+ @cute.jit
13
+ def gemm(
14
+ tiled_mma: cute.TiledMma,
15
+ acc: cute.Tensor,
16
+ tCrA: cute.Tensor,
17
+ tCrB: cute.Tensor,
18
+ zero_init: cutlass.Constexpr[bool] = False,
19
+ wg_wait: cutlass.Constexpr[int] = 0,
20
+ # A_in_regs: cutlass.Constexpr[bool] = False,
21
+ swap_AB: cutlass.Constexpr[bool] = False,
22
+ ) -> None:
23
+ if const_expr(swap_AB):
24
+ gemm(tiled_mma, acc, tCrB, tCrA, zero_init=zero_init, wg_wait=wg_wait, swap_AB=False)
25
+ else:
26
+ warpgroup.fence()
27
+ # We make a new mma_atom since we'll be modifying its attribute (accumulate).
28
+ # Otherwise the compiler complains "operand #0 does not dominate this use"
29
+ mma_atom = cute.make_mma_atom(tiled_mma.op)
30
+ mma_atom.set(warpgroup.Field.ACCUMULATE, not zero_init)
31
+ for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
32
+ cute.gemm(mma_atom, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
33
+ mma_atom.set(warpgroup.Field.ACCUMULATE, True)
34
+ warpgroup.commit_group()
35
+ if const_expr(wg_wait >= 0):
36
+ warpgroup.wait_group(wg_wait)
37
+
38
+
39
+ def gemm_zero_init(
40
+ tiled_mma: cute.TiledMma,
41
+ shape: cute.Shape,
42
+ tCrA: cute.Tensor,
43
+ tCrB: cute.Tensor,
44
+ A_idx: Optional[Int32] = None,
45
+ B_idx: Optional[Int32] = None,
46
+ wg_wait: int = -1,
47
+ swap_AB: bool = False,
48
+ ) -> cute.Tensor:
49
+ if const_expr(swap_AB):
50
+ return gemm_zero_init(
51
+ tiled_mma, shape[::-1], tCrB, tCrA, B_idx, A_idx, wg_wait, swap_AB=False
52
+ )
53
+ else:
54
+ acc = cute.make_fragment(tiled_mma.partition_shape_C(shape), Float32)
55
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
56
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
57
+ gemm(tiled_mma, acc, rA, rB, zero_init=True, wg_wait=wg_wait)
58
+ return acc
59
+
60
+
61
+ def gemm_w_idx(
62
+ tiled_mma: cute.TiledMma,
63
+ acc: cute.Tensor,
64
+ tCrA: cute.Tensor,
65
+ tCrB: cute.Tensor,
66
+ zero_init: Boolean,
67
+ A_idx: Optional[Int32] = None,
68
+ B_idx: Optional[Int32] = None,
69
+ wg_wait: int = -1,
70
+ swap_AB: bool = False,
71
+ ) -> None:
72
+ if const_expr(swap_AB):
73
+ gemm_w_idx(tiled_mma, acc, tCrB, tCrA, zero_init, B_idx, A_idx, wg_wait, swap_AB=False)
74
+ else:
75
+ rA = tCrA if const_expr(A_idx is None) else tCrA[None, None, None, A_idx]
76
+ rB = tCrB if const_expr(B_idx is None) else tCrB[None, None, None, B_idx]
77
+ gemm(tiled_mma, acc, rA, rB, zero_init=zero_init, wg_wait=wg_wait)
78
+
79
+
80
+ @dsl_user_op
81
+ def make_smem_layout(
82
+ dtype: Type[Numeric],
83
+ layout: LayoutEnum,
84
+ shape: cute.Shape,
85
+ stage: Optional[int] = None,
86
+ *,
87
+ loc=None,
88
+ ip=None,
89
+ ) -> Union[cute.Layout, cute.ComposedLayout]:
90
+ major_mode_size = shape[1] if layout.is_n_major_c() else shape[0]
91
+ smem_layout_atom = warpgroup.make_smem_layout_atom(
92
+ sm90_utils_og.get_smem_layout_atom(layout, dtype, major_mode_size),
93
+ dtype,
94
+ )
95
+ order = (1, 0, 2) if const_expr(layout.is_m_major_c()) else (0, 1, 2)
96
+ smem_layout_staged = cute.tile_to_shape(
97
+ smem_layout_atom,
98
+ cute.append(shape, stage) if const_expr(stage is not None) else shape,
99
+ order=order if const_expr(stage is not None) else order[:2],
100
+ )
101
+ return smem_layout_staged
kernel/deps/flash_attn/cute/interface.py ADDED
@@ -0,0 +1,1762 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
+ # [2025-07-04] Version in Cute-DSL, for Hopper and Blackwell. You'll need install nvidia-cutlass-dsl==4.2.0.
3
+
4
+ # Supported features:
5
+ # - BF16 & FP16 dtype
6
+ # - noncausal & causal attention
7
+ # - MHA, GQA, MQA
8
+ # - hdim 64, 96, 128.
9
+ # - (hdim_qk, hdim_v) = (192, 128) for Blackwell (i.e. DeepSeek shape)
10
+ # - varlen
11
+ # - sliding window
12
+ # - bwd pass for Ampere (will also run on Hopper/Blackwell, but will be slow)
13
+
14
+ # Features not supported yet:
15
+ # - split (i.e. FlashDecoding)
16
+ # - tuned block sizes
17
+ # - paged KV
18
+ # - append KV to existing KV cache
19
+ # - FP8
20
+ # - bwd pass optimized for Hopper/Blackwell
21
+
22
+ import math
23
+ from functools import lru_cache
24
+ from typing import Optional, Tuple, Callable
25
+
26
+ import torch
27
+
28
+
29
+ import cuda.bindings.driver as cuda
30
+
31
+ import cutlass
32
+ import cutlass.cute as cute
33
+
34
+ from flash_attn.cute import utils
35
+ from flash_attn.cute.cute_dsl_utils import to_cute_tensor
36
+ from flash_attn.cute.flash_fwd import FlashAttentionForwardSm90
37
+ from flash_attn.cute.flash_fwd_sm100 import FlashAttentionForwardSm100
38
+ from flash_attn.cute.flash_bwd_preprocess import FlashAttentionBackwardPreprocess
39
+ from flash_attn.cute.flash_bwd import FlashAttentionBackwardSm80
40
+ from flash_attn.cute.flash_bwd_sm90 import FlashAttentionBackwardSm90
41
+ from flash_attn.cute.flash_bwd_sm100 import FlashAttentionBackwardSm100
42
+ from flash_attn.cute.flash_bwd_postprocess import FlashAttentionBackwardPostprocess
43
+ from flash_attn.cute.flash_fwd_combine import FlashAttentionForwardCombine
44
+
45
+ from flash_attn.cute.block_sparsity import (
46
+ BlockSparseTensorsTorch,
47
+ to_cute_block_sparse_tensors,
48
+ normalize_block_sparse_config,
49
+ normalize_block_sparse_config_bwd,
50
+ )
51
+
52
+ @lru_cache(maxsize=None)
53
+ def _get_device_capability():
54
+ """Cached device capability check."""
55
+ return torch.cuda.get_device_capability()[0]
56
+
57
+ def maybe_contiguous(x):
58
+ return x.contiguous() if x is not None and x.stride(-1) != 1 else x
59
+
60
+
61
+ def _validate_tensor(t, name, expected_shape, expected_dtype, expected_device):
62
+ assert t.shape == expected_shape, f"{name} shape {t.shape} != expected {expected_shape}"
63
+ assert t.dtype == expected_dtype, f"{name} dtype {t.dtype} != expected {expected_dtype}"
64
+ assert t.device == expected_device, f"{name} device {t.device} != expected {expected_device}"
65
+ assert t.is_cuda, f"{name} must be on CUDA"
66
+
67
+
68
+ torch2cute_dtype_map = {
69
+ torch.float16: cutlass.Float16,
70
+ torch.bfloat16: cutlass.BFloat16,
71
+ torch.float32: cutlass.Float32,
72
+ }
73
+
74
+
75
+ def num_splits_heuristic(total_mblocks, num_SMs, num_n_blocks, max_splits):
76
+ # If num_n_blocks is too small, use 1 split. For example, we never split for hdim = 128 and seqlen_k = 512.
77
+ if num_n_blocks <= 4:
78
+ return 1
79
+
80
+ # NOTE: We should revisit this heuristic after persistence is supported for split KV.
81
+ # Sometimes, it's ideal to over-schedule splits for better efficiency.
82
+ return min(num_SMs // total_mblocks, max_splits, num_n_blocks)
83
+
84
+
85
+ def _flash_attn_fwd(
86
+ q: torch.Tensor,
87
+ k: torch.Tensor,
88
+ v: torch.Tensor,
89
+ cu_seqlens_q: Optional[torch.Tensor] = None,
90
+ cu_seqlens_k: Optional[torch.Tensor] = None,
91
+ seqused_q: Optional[torch.Tensor] = None,
92
+ seqused_k: Optional[torch.Tensor] = None,
93
+ max_seqlen_q: Optional[int] = None,
94
+ max_seqlen_k: Optional[int] = None,
95
+ page_table: Optional[torch.Tensor] = None,
96
+ softmax_scale: Optional[float] = None,
97
+ causal: bool = False,
98
+ softcap: Optional[float] = None,
99
+ window_size_left: Optional[int] = None,
100
+ window_size_right: Optional[int] = None,
101
+ learnable_sink: Optional[torch.Tensor] = None,
102
+ # m_block_size: int = 128,
103
+ # n_block_size: int = 64,
104
+ # num_threads: int = 128,
105
+ m_block_size: int = 128,
106
+ n_block_size: int = 128,
107
+ num_threads: int = 384,
108
+ num_splits: int = 1,
109
+ pack_gqa: Optional[bool] = None,
110
+ _compute_capability: Optional[int] = None,
111
+ score_mod: Optional[Callable] = None,
112
+ mask_mod: Optional[Callable] = None,
113
+ block_sparse_tensors: Optional[BlockSparseTensorsTorch] = None,
114
+ return_lse: bool = False,
115
+ out: Optional[torch.Tensor] = None,
116
+ lse: Optional[torch.Tensor] = None,
117
+ aux_tensors: Optional[list[torch.Tensor]] = None,
118
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
119
+ """Forward pass for FlashAttention.
120
+
121
+ Args:
122
+ ...
123
+ score_mod: A callable that takes the attention scores and applies a modification.
124
+ mask_mod: A callable that takes token position information and selectively masks
125
+ block_sparse_tensors: A tuple of tensors used for block sparsity.
126
+ return_lse: Whether to return the log softmax of the attention scores. If set to True will always calculate
127
+ out: Optional pre-allocated output tensor. If None, will be allocated internally.
128
+ lse: Optional pre-allocated log-sum-exp tensor. If None, will be allocated when needed.
129
+ aux_tensors: Some score_mods will want to read from global aux_tensors. This is how we thread them through to the inner kernel.
130
+ """
131
+ q, k, v = [maybe_contiguous(t) for t in (q, k, v)]
132
+ num_head, head_dim = q.shape[-2:]
133
+ if cu_seqlens_q is None:
134
+ batch_size, seqlen_q = q.shape[:2]
135
+ total_q = batch_size * seqlen_q
136
+ else:
137
+ batch_size = cu_seqlens_q.shape[0] - 1
138
+ seqlen_q = None
139
+ total_q = q.shape[0]
140
+ if page_table is not None:
141
+ assert cu_seqlens_k is None, "page_table is not supported with cu_seqlens_k"
142
+ assert page_table.dtype == torch.int32, "page_table must be int32"
143
+ assert page_table.stride(-1) == 1, "page_table must be contiguous in the last dimension"
144
+ max_num_pages_per_seq = page_table.shape[1]
145
+ assert page_table.shape == (batch_size, max_num_pages_per_seq)
146
+ num_pages, page_size = k.shape[:2]
147
+ seqlen_k = num_pages * page_size
148
+ else:
149
+ num_pages, page_size = None, None
150
+ seqlen_k = k.shape[-3]
151
+ num_head_kv = k.shape[-2]
152
+ head_dim_v = v.shape[-1]
153
+ if cu_seqlens_k is None:
154
+ if page_table is None:
155
+ assert k.shape == (batch_size, seqlen_k, num_head_kv, head_dim)
156
+ assert v.shape == (batch_size, seqlen_k, num_head_kv, head_dim_v)
157
+ else:
158
+ assert k.shape == (num_pages, page_size, num_head_kv, head_dim)
159
+ assert v.shape == (num_pages, page_size, num_head_kv, head_dim_v)
160
+ else:
161
+ assert k.shape == (seqlen_k, num_head_kv, head_dim)
162
+ assert v.shape == (seqlen_k, num_head_kv, head_dim_v)
163
+ assert cu_seqlens_k.shape == (batch_size + 1,), (
164
+ "cu_seqlens_k must have shape (batch_size + 1,)"
165
+ )
166
+
167
+ if cu_seqlens_q is not None:
168
+ assert cu_seqlens_q.shape == (batch_size + 1,), (
169
+ "cu_seqlens_q must have shape (batch_size + 1,)"
170
+ )
171
+ assert seqused_q is None or seqused_q.shape == (batch_size,), (
172
+ "seqused_q must have shape (batch_size,)"
173
+ )
174
+ assert seqused_k is None or seqused_k.shape == (batch_size,), (
175
+ "seqused_k must have shape (batch_size,)"
176
+ )
177
+ assert q.dtype in [torch.float16, torch.bfloat16], "inputs must be float16 or bfloat16"
178
+ assert q.dtype == k.dtype == v.dtype, "inputs must have the same dtype"
179
+ for t in [cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k]:
180
+ if t is not None:
181
+ assert t.dtype == torch.int32, (
182
+ "cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k must be int32"
183
+ )
184
+ assert t.stride(0) == 1, (
185
+ "cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k must be contiguous"
186
+ )
187
+ if learnable_sink is not None:
188
+ assert learnable_sink.shape == (num_head,)
189
+ assert learnable_sink.dtype == torch.bfloat16, "learnable_sink must be bfloat16"
190
+
191
+ assert all(
192
+ t is None or t.is_cuda
193
+ for t in (
194
+ q,
195
+ k,
196
+ v,
197
+ cu_seqlens_q,
198
+ cu_seqlens_k,
199
+ seqused_q,
200
+ seqused_k,
201
+ page_table,
202
+ learnable_sink,
203
+ )
204
+ ), "inputs must be on CUDA device"
205
+ assert num_head % num_head_kv == 0, "num_head must be divisible by num_head_kv"
206
+ assert head_dim <= 256, "head_dim must be less than or equal to 256"
207
+ alignment = 16 // q.element_size()
208
+ assert head_dim % alignment == 0, f"head_dim must be divisible by {alignment}"
209
+ assert head_dim_v % alignment == 0, f"head_dim_v must be divisible by {alignment}"
210
+ if softmax_scale is None:
211
+ softmax_scale = 1.0 / math.sqrt(head_dim)
212
+ if softcap == 0.0:
213
+ softcap = None
214
+ qhead_per_kvhead = num_head // num_head_kv
215
+ if pack_gqa is None:
216
+ pack_gqa = qhead_per_kvhead > 1
217
+
218
+ out_torch_dtype = q.dtype
219
+ device = q.device
220
+ q_batch_seqlen_shape = (batch_size, seqlen_q) if cu_seqlens_q is None else (total_q,)
221
+ lse_shape = (batch_size, num_head, seqlen_q) if cu_seqlens_q is None else (num_head, total_q)
222
+ requires_grad = q.requires_grad or k.requires_grad or v.requires_grad
223
+
224
+ if out is None:
225
+ out = torch.empty(
226
+ *q_batch_seqlen_shape, num_head, head_dim_v, dtype=out_torch_dtype, device=device
227
+ )
228
+ else:
229
+ _validate_tensor(out, "out", (*q_batch_seqlen_shape, num_head, head_dim_v), out_torch_dtype, device)
230
+
231
+ if lse is None:
232
+ lse = (
233
+ torch.empty(lse_shape, dtype=torch.float32, device=device)
234
+ if requires_grad or return_lse
235
+ else None
236
+ )
237
+ elif lse is not None:
238
+ _validate_tensor(lse, "lse", lse_shape, torch.float32, device)
239
+
240
+ dtype = torch2cute_dtype_map[q.dtype]
241
+ compute_capability = (
242
+ _get_device_capability()
243
+ if _compute_capability is None
244
+ else _compute_capability
245
+ )
246
+
247
+ assert compute_capability in [9, 10, 11], "Unsupported compute capability. Supported: 9.x, 10.x, 11.x"
248
+
249
+ use_block_sparsity = block_sparse_tensors is not None
250
+
251
+ if mask_mod is None:
252
+ if causal:
253
+ window_size_right = 0
254
+ local = window_size_left is not None or window_size_right is not None
255
+ if window_size_left is not None or window_size_right is not None:
256
+ if window_size_left is None and window_size_right == 0:
257
+ causal, local = True, False
258
+ window_size_right = None
259
+ else:
260
+ causal, local = False, True
261
+ else:
262
+ causal, local = False, False
263
+
264
+ current_stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
265
+
266
+ if compute_capability == 9: # TODO: tune block size according to hdim.
267
+ if head_dim == head_dim_v == 128 and not causal and not local and not use_block_sparsity:
268
+ n_block_size = 192
269
+
270
+ if compute_capability in [10, 11]:
271
+ if (
272
+ pack_gqa
273
+ and (128 % qhead_per_kvhead != 0)
274
+ ):
275
+ pack_gqa = False
276
+ # TODO: fix GQA + SplitKV + non-varlen
277
+ if pack_gqa and num_splits != 1 and cu_seqlens_q is None:
278
+ pack_gqa = False
279
+
280
+ if max_seqlen_q is None:
281
+ max_seqlen_q = seqlen_q if cu_seqlens_q is None else total_q
282
+ if max_seqlen_k is None:
283
+ max_seqlen_k = seqlen_k
284
+ seqlen_q_packgqa = max_seqlen_q * qhead_per_kvhead
285
+ if compute_capability == 10:
286
+ q_stage = 2 if seqlen_q_packgqa > m_block_size else 1
287
+ else:
288
+ q_stage = 1
289
+
290
+ if num_splits < 1:
291
+ m_block_size_effective = q_stage * m_block_size
292
+ seqlen_k_loaded = max_seqlen_k if not local else max(0, min(max_seqlen_k, window_size_right + window_size_left + 1 + m_block_size))
293
+ num_n_blocks = (seqlen_k_loaded + n_block_size - 1) // n_block_size
294
+ num_m_blocks = (seqlen_q_packgqa + m_block_size_effective - 1) // m_block_size_effective
295
+ total_mblocks = batch_size * num_head_kv * num_m_blocks
296
+ num_splits = num_splits_heuristic(
297
+ total_mblocks,
298
+ torch.cuda.get_device_properties(device).multi_processor_count,
299
+ num_n_blocks,
300
+ 128,
301
+ )
302
+
303
+ is_split_kv = num_splits > 1
304
+ if is_split_kv:
305
+ out_partial = torch.empty(num_splits, *q_batch_seqlen_shape, num_head, head_dim_v, dtype=torch.float32, device=device)
306
+ lse_partial = torch.empty(num_splits, *lse_shape, dtype=torch.float32, device=device)
307
+
308
+ # hash score and mask mods for compile cache
309
+ score_mod_hash = utils.hash_callable(score_mod) if score_mod is not None else False
310
+ mask_mod_hash = utils.hash_callable(mask_mod) if mask_mod is not None else False
311
+
312
+ if softcap is not None:
313
+ assert score_mod is None, "softcap and score_mod cannot be used together"
314
+ score_mod = utils.create_softcap_scoremod(softcap)
315
+
316
+ is_varlen = (
317
+ cu_seqlens_q is not None
318
+ or cu_seqlens_k is not None
319
+ or seqused_q is not None
320
+ or seqused_k is not None
321
+ )
322
+
323
+ if mask_mod is not None:
324
+ if is_varlen:
325
+ raise NotImplementedError(
326
+ "mask_mod with aux_tensors is not yet supported for varlen sequences. This will be fixed in a future PR."
327
+ )
328
+
329
+ if use_block_sparsity:
330
+ if is_varlen:
331
+ raise NotImplementedError(
332
+ "Block sparsity is not yet supported for varlen sequences. This will be fixed in a future PR."
333
+ )
334
+ # NB: pack_gqa requires block sparse head dim == 1 (broadcasted)
335
+ if pack_gqa and block_sparse_tensors.mask_block_cnt.shape[1] != 1:
336
+ pack_gqa = False
337
+ if is_split_kv:
338
+ raise NotImplementedError(
339
+ "Block sparsity is not yet supported with SplitKV. TODO: partition sparse block lists per split."
340
+ )
341
+
342
+ # See get_broadcast_dims for why this is needed in compile key
343
+ block_sparse_broadcast_pattern = None
344
+ normalized_block_sparse_tensors = None
345
+ q_subtile_factor = None
346
+ if block_sparse_tensors is not None:
347
+ if seqlen_q is None:
348
+ raise ValueError("Block sparsity requires fixed-length sequences (seqlen_q must be known).")
349
+ (
350
+ normalized_block_sparse_tensors,
351
+ block_sparse_broadcast_pattern,
352
+ q_subtile_factor,
353
+ ) = normalize_block_sparse_config(
354
+ block_sparse_tensors,
355
+ batch_size=batch_size,
356
+ num_head=num_head,
357
+ seqlen_q=seqlen_q,
358
+ seqlen_k=seqlen_k,
359
+ block_size=(m_block_size, n_block_size),
360
+ q_stage=q_stage,
361
+ )
362
+
363
+ compile_key = (
364
+ dtype,
365
+ head_dim,
366
+ head_dim_v,
367
+ qhead_per_kvhead,
368
+ causal,
369
+ score_mod_hash,
370
+ mask_mod_hash,
371
+ use_block_sparsity,
372
+ block_sparse_broadcast_pattern,
373
+ len(aux_tensors) if aux_tensors is not None else 0,
374
+ lse is None,
375
+ cu_seqlens_q is None,
376
+ cu_seqlens_k is None,
377
+ seqused_q is None,
378
+ seqused_k is None,
379
+ page_table is not None,
380
+ window_size_left is not None,
381
+ window_size_right is not None,
382
+ learnable_sink is not None,
383
+ m_block_size,
384
+ n_block_size,
385
+ q_stage,
386
+ num_threads,
387
+ is_split_kv,
388
+ pack_gqa,
389
+ compute_capability,
390
+ page_size not in [None, 128], # paged KV non-TMA
391
+ q_subtile_factor,
392
+ )
393
+ if compile_key not in _flash_attn_fwd.compile_cache:
394
+ (
395
+ cu_seqlens_q_tensor,
396
+ cu_seqlens_k_tensor,
397
+ seqused_q_tensor,
398
+ seqused_k_tensor,
399
+ learnable_sink_tensor,
400
+ ) = [
401
+ to_cute_tensor(t, assumed_align=4, leading_dim=0)
402
+ if t is not None
403
+ else None
404
+ for t in (cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k, learnable_sink)
405
+ ]
406
+ page_table_tensor = (
407
+ to_cute_tensor(page_table, assumed_align=4, leading_dim=1)
408
+ if page_table is not None
409
+ else None
410
+ )
411
+ q_tensor, k_tensor, v_tensor, o_tensor = [
412
+ to_cute_tensor(t) for t in (q, k, v, out if not is_split_kv else out_partial)
413
+ ]
414
+ if is_split_kv:
415
+ lse_tensor = to_cute_tensor(lse_partial, assumed_align=4)
416
+ elif lse is not None:
417
+ lse_tensor = to_cute_tensor(lse, assumed_align=4)
418
+ else:
419
+ lse_tensor = None
420
+
421
+ sparse_tensors = None
422
+ if normalized_block_sparse_tensors is not None:
423
+ sparse_tensors = to_cute_block_sparse_tensors(normalized_block_sparse_tensors)
424
+
425
+ cute_aux_tensors = None
426
+ if aux_tensors is not None:
427
+ cute_aux_tensors = [to_cute_tensor(buf, assumed_align=None, fully_dynamic=True) for buf in aux_tensors]
428
+
429
+ if compute_capability == 9:
430
+ assert page_table is None, "paged KV not supported on SM 9.0"
431
+ assert not is_split_kv, "SplitKV not supported on SM 9.0"
432
+ # fa_fwd = FlashAttentionForwardSm80(
433
+ fa_fwd = FlashAttentionForwardSm90(
434
+ dtype,
435
+ head_dim,
436
+ head_dim_v,
437
+ qhead_per_kvhead,
438
+ is_causal=causal,
439
+ is_local=local,
440
+ pack_gqa=pack_gqa,
441
+ tile_m=m_block_size,
442
+ tile_n=n_block_size,
443
+ # num_stages=1,
444
+ num_stages=2,
445
+ num_threads=num_threads,
446
+ Q_in_regs=False,
447
+ intra_wg_overlap=True,
448
+ mma_pv_is_rs=True,
449
+ mask_mod=mask_mod,
450
+ score_mod=score_mod,
451
+ has_aux_tensors=aux_tensors is not None,
452
+ q_subtile_factor=q_subtile_factor,
453
+ )
454
+ elif compute_capability in [10, 11]:
455
+ fa_fwd = FlashAttentionForwardSm100(
456
+ head_dim,
457
+ head_dim_v,
458
+ qhead_per_kvhead=qhead_per_kvhead,
459
+ is_causal=causal,
460
+ is_local=local,
461
+ is_split_kv=is_split_kv,
462
+ pack_gqa=pack_gqa,
463
+ m_block_size=m_block_size,
464
+ n_block_size=n_block_size,
465
+ q_stage=q_stage,
466
+ is_persistent=not causal
467
+ and not local
468
+ and cu_seqlens_q is None
469
+ and seqused_q is None
470
+ and not is_split_kv,
471
+ score_mod=score_mod,
472
+ mask_mod=mask_mod,
473
+ has_aux_tensors=aux_tensors is not None,
474
+ paged_kv_non_tma=page_size not in [None, 128],
475
+ is_varlen_q=cu_seqlens_q is not None
476
+ or seqused_q is not None,
477
+ q_subtile_factor=q_subtile_factor,
478
+ )
479
+ else:
480
+ raise ValueError(
481
+ f"Unsupported compute capability: {compute_capability}. Supported: 9.x, 10.x, 11.x"
482
+ )
483
+ # TODO: check @can_implement
484
+ _flash_attn_fwd.compile_cache[compile_key] = cute.compile(
485
+ fa_fwd,
486
+ q_tensor,
487
+ k_tensor,
488
+ v_tensor,
489
+ o_tensor,
490
+ lse_tensor,
491
+ softmax_scale,
492
+ current_stream,
493
+ cu_seqlens_q_tensor,
494
+ cu_seqlens_k_tensor,
495
+ seqused_q_tensor,
496
+ seqused_k_tensor,
497
+ page_table_tensor,
498
+ window_size_left,
499
+ window_size_right,
500
+ learnable_sink_tensor,
501
+ sparse_tensors,
502
+ cute_aux_tensors,
503
+ options="--enable-tvm-ffi",
504
+ )
505
+
506
+ _flash_attn_fwd.compile_cache[compile_key](
507
+ q,
508
+ k,
509
+ v,
510
+ out if not is_split_kv else out_partial,
511
+ lse_partial if is_split_kv else lse,
512
+ softmax_scale,
513
+ current_stream,
514
+ cu_seqlens_q,
515
+ cu_seqlens_k,
516
+ seqused_q,
517
+ seqused_k,
518
+ page_table,
519
+ window_size_left,
520
+ window_size_right,
521
+ learnable_sink,
522
+ normalized_block_sparse_tensors[:4] if normalized_block_sparse_tensors is not None else None,
523
+ aux_tensors,
524
+ )
525
+ if is_split_kv:
526
+ _flash_attn_fwd_combine(
527
+ out_partial,
528
+ lse_partial.transpose(-1, -2),
529
+ out,
530
+ lse.transpose(-1, -2) if lse is not None else None,
531
+ cu_seqlens_q,
532
+ seqused_q,
533
+ )
534
+ return out, lse
535
+
536
+
537
+ _flash_attn_fwd.compile_cache = {}
538
+
539
+
540
+ def _flash_attn_bwd(
541
+ q: torch.Tensor,
542
+ k: torch.Tensor,
543
+ v: torch.Tensor,
544
+ out: torch.Tensor,
545
+ dout: torch.Tensor,
546
+ lse: torch.Tensor,
547
+ softmax_scale: Optional[float] = None,
548
+ causal: bool = False,
549
+ softcap: float = 0.0,
550
+ window_size_left: Optional[int] = None,
551
+ window_size_right: Optional[int] = None,
552
+ m_block_size: int = 64,
553
+ n_block_size: int = 128,
554
+ num_threads: int = 256,
555
+ pack_gqa: bool = False,
556
+ num_stages_Q: int = 2,
557
+ num_stages_dO: int = 2,
558
+ SdP_swapAB: bool = False,
559
+ dKV_swapAB: bool = False,
560
+ dQ_swapAB: bool = False,
561
+ AtomLayoutMSdP: int = 2,
562
+ AtomLayoutNdKV: int = 2,
563
+ AtomLayoutMdQ: int = 2,
564
+ V_in_regs: bool = False,
565
+ cu_seqlens_q: Optional[torch.Tensor] = None,
566
+ cu_seqlens_k: Optional[torch.Tensor] = None,
567
+ seqused_q: Optional[torch.Tensor] = None,
568
+ seqused_k: Optional[torch.Tensor] = None,
569
+ max_seqlen_q: Optional[int] = None,
570
+ max_seqlen_k: Optional[int] = None,
571
+ deterministic: bool = False,
572
+ dq: Optional[torch.Tensor] = None,
573
+ dk: Optional[torch.Tensor] = None,
574
+ dv: Optional[torch.Tensor] = None,
575
+ score_mod: Optional[Callable] = None,
576
+ score_mod_bwd: Optional[Callable] = None,
577
+ mask_mod: Optional[Callable] = None,
578
+ aux_tensors: Optional[list[torch.Tensor]] = None,
579
+ block_sparse_tensors: Optional[BlockSparseTensorsTorch] = None,
580
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
581
+ compute_capability = _get_device_capability()
582
+ assert compute_capability in [9, 10, 11], "Unsupported compute capability. Supported: 9.x, 10.x, 11.x"
583
+
584
+ if compute_capability == 9:
585
+ m_block_size = 80 if not causal else 64
586
+ # Keep the caller-selected KV tile. Upstream hard-codes 128 here,
587
+ # but SKSA's raw-8 routes already use a 64-token forward schedule and
588
+ # benefit from transposing that same schedule directly for dK/dV.
589
+ assert n_block_size in [64, 128], "SM90 backward supports 64 or 128 KV tiles"
590
+ num_stages_Q = 2
591
+ num_stages_dO = 2
592
+ num_stages_PdS = 2
593
+ SdP_swapAB = True
594
+ dKV_swapAB = False
595
+ dQ_swapAB = not causal
596
+ # Preserve the 128-thread logical coverage expected by the kernels:
597
+ # 64-wide KV tiles use two M atoms and one N atom, whereas the
598
+ # upstream 128-wide layout uses one M atom and two N atoms.
599
+ AtomLayoutMSdP = 1 if n_block_size == 128 else 2
600
+ AtomLayoutNdKV = 2 if n_block_size == 128 else 1
601
+ AtomLayoutMdQ = 1
602
+ cluster_size = 1
603
+ assert window_size_left is None and window_size_right is None, "local not supported yet on 9.x"
604
+ else:
605
+ m_block_size = 128
606
+ n_block_size = 128
607
+ dQ_swapAB = False
608
+ dKV_swapAB = False
609
+ AtomLayoutMdQ = 1
610
+ AtomLayoutNdKV = 1
611
+ # TODO: support cluster size 2
612
+ cluster_size = 1
613
+ q, k, v, out, dout, lse, cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k = [
614
+ maybe_contiguous(t)
615
+ for t in (q, k, v, out, dout, lse, cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k)
616
+ ]
617
+ num_head, head_dim = q.shape[-2:]
618
+ if cu_seqlens_q is None:
619
+ batch_size, seqlen_q = q.shape[:2]
620
+ total_q = batch_size * seqlen_q
621
+ else:
622
+ batch_size = cu_seqlens_q.shape[0] - 1
623
+ total_q = q.shape[0]
624
+ seqlen_q = max_seqlen_q if max_seqlen_q is not None else total_q
625
+
626
+ if cu_seqlens_k is None:
627
+ batch_size, seqlen_k = k.shape[:2]
628
+ total_k = batch_size * seqlen_k
629
+ else:
630
+ batch_size = cu_seqlens_k.shape[0] - 1
631
+ total_k = k.shape[0]
632
+ seqlen_k = max_seqlen_k if max_seqlen_k is not None else total_k
633
+
634
+ num_head_kv = k.shape[-2]
635
+ head_dim_v = v.shape[-1]
636
+
637
+ if causal:
638
+ window_size_right = 0
639
+ local = window_size_left is not None or window_size_right is not None
640
+ if local:
641
+ if window_size_left is None and window_size_right == 0:
642
+ causal, local = True, False
643
+ window_size_right = None
644
+ else:
645
+ causal, local = False, True
646
+
647
+ use_block_sparsity = block_sparse_tensors is not None
648
+
649
+ # SM90 block-sparse backward: tile_m=64 is the GCD between a m_block_size that fits,
650
+ # the base block_m of 128 from forward, and block-sparse size for subtiling.
651
+ if compute_capability == 9 and use_block_sparsity:
652
+ m_block_size = 64
653
+ # dQ_swapAB tuning: use False when m_block_size=64 (same as causal case)
654
+ dQ_swapAB = False
655
+
656
+ # NB: this could be derived from the block_sparse_tensors but for now we hardcode it to 2
657
+ subtile_factor = 2
658
+ seqlen_q_rounded = (seqlen_q + m_block_size - 1) // m_block_size * m_block_size
659
+ seqlen_k_rounded = (seqlen_k + n_block_size - 1) // n_block_size * n_block_size
660
+
661
+ if cu_seqlens_k is None:
662
+ assert k.shape == (batch_size, seqlen_k, num_head_kv, head_dim)
663
+ assert v.shape == (batch_size, seqlen_k, num_head_kv, head_dim_v)
664
+ else:
665
+ assert k.shape == (total_k, num_head_kv, head_dim)
666
+ assert v.shape == (total_k, num_head_kv, head_dim_v)
667
+ assert cu_seqlens_k.shape == (batch_size + 1,), (
668
+ "cu_seqlens_k must have shape (batch_size + 1,)"
669
+ )
670
+
671
+ if cu_seqlens_q is not None:
672
+ assert cu_seqlens_q.shape == (batch_size + 1,), (
673
+ "cu_seqlens_q must have shape (batch_size + 1,)"
674
+ )
675
+
676
+ assert out.shape == (total_q, num_head, head_dim_v)
677
+ assert dout.shape == (total_q, num_head, head_dim_v)
678
+ assert lse.shape == (num_head, total_q), "lse must have shape (num_head, total_q)"
679
+ else:
680
+ assert out.shape == (batch_size, seqlen_q, num_head, head_dim_v)
681
+ assert dout.shape == (batch_size, seqlen_q, num_head, head_dim_v)
682
+ assert lse.shape == (batch_size, num_head, seqlen_q), (
683
+ "lse must have shape (batch_size, num_head, seqlen_q)"
684
+ )
685
+
686
+ assert q.dtype in [torch.float16, torch.bfloat16], "inputs must be float16 or bfloat16"
687
+ assert q.dtype == k.dtype == v.dtype == out.dtype == dout.dtype, (
688
+ "inputs must have the same dtype"
689
+ )
690
+ for t in [cu_seqlens_q, cu_seqlens_k]:
691
+ if t is not None:
692
+ assert t.dtype == torch.int32, "cu_seqlens_q, cu_seqlens_k must be int32"
693
+ assert lse.dtype == torch.float32, "lse must be float32"
694
+ assert all(
695
+ t is None or t.is_cuda for t in (q, k, v, out, dout, lse, cu_seqlens_q, cu_seqlens_k)
696
+ ), "inputs must be on CUDA device"
697
+ assert num_head % num_head_kv == 0, "num_head must be divisible by num_head_kv"
698
+ assert head_dim <= 256, "head_dim must be less than or equal to 256"
699
+ alignment = 16 // q.element_size()
700
+ assert head_dim % alignment == 0, f"head_dim must be divisible by {alignment}"
701
+ assert head_dim_v % alignment == 0, f"head_dim_v must be divisible by {alignment}"
702
+ if softmax_scale is None:
703
+ softmax_scale = 1.0 / math.sqrt(head_dim)
704
+ qhead_per_kvhead = num_head // num_head_kv
705
+ if pack_gqa is None:
706
+ pack_gqa = qhead_per_kvhead > 1
707
+ # pack_gqa backward not yet supported in bwd
708
+ pack_gqa = False
709
+ if compute_capability not in [10, 11]:
710
+ assert deterministic is False, "bwd deterministic only supported for sm100/sm110 for now"
711
+
712
+ if score_mod is not None:
713
+ assert score_mod_bwd is not None, "score_mod_bwd is required when score_mod is provided"
714
+ assert softcap == 0.0, "softcap and score_mod are mutually exclusive (different log2 scaling)"
715
+ assert cu_seqlens_q is None and cu_seqlens_k is None, (
716
+ "varlen + score_mod not supported in bwd yet"
717
+ )
718
+
719
+ device = q.device
720
+ out_torch_dtype = q.dtype
721
+
722
+ if dq is None:
723
+ dq = torch.empty_like(q)
724
+ else:
725
+ _validate_tensor(dq, "dq", q.shape, out_torch_dtype, device)
726
+
727
+ if dk is None:
728
+ dk = torch.empty_like(k)
729
+ else:
730
+ _validate_tensor(dk, "dk", k.shape, out_torch_dtype, device)
731
+
732
+ if dv is None:
733
+ dv = torch.empty_like(v)
734
+ else:
735
+ _validate_tensor(dv, "dv", v.shape, out_torch_dtype, device)
736
+
737
+ head_dim_rounded = (head_dim + 32 - 1) // 32 * 32
738
+
739
+ if cu_seqlens_q is None:
740
+ dq_accum = torch.empty(
741
+ batch_size,
742
+ num_head,
743
+ seqlen_q_rounded * head_dim_rounded,
744
+ dtype=torch.float32,
745
+ device=device,
746
+ )
747
+ dpsum = torch.empty(
748
+ batch_size, num_head, seqlen_q_rounded, dtype=torch.float32, device=device
749
+ )
750
+ lse_log2 = torch.empty(
751
+ batch_size, num_head, seqlen_q_rounded, dtype=torch.float32, device=device
752
+ )
753
+ else:
754
+ total_q_rounded_padded = (
755
+ (total_q + cu_seqlens_q.shape[0] * m_block_size - 1) // m_block_size * m_block_size
756
+ )
757
+ dq_accum = torch.empty(
758
+ num_head, total_q_rounded_padded * head_dim_rounded, dtype=torch.float32, device=device
759
+ )
760
+ dpsum = torch.empty(num_head, total_q_rounded_padded, dtype=torch.float32, device=device)
761
+ lse_log2 = torch.empty(num_head, total_q_rounded_padded, dtype=torch.float32, device=device)
762
+
763
+ dKV_postprocess = qhead_per_kvhead > 1
764
+ if dKV_postprocess:
765
+ head_dim_v_rounded = (head_dim_v + 32 - 1) // 32 * 32
766
+ if cu_seqlens_k is None:
767
+ num_n_blocks = seqlen_k_rounded // n_block_size
768
+ if cluster_size == 2 and num_n_blocks % cluster_size != 0:
769
+ seqlen_k_rounded = seqlen_k_rounded + n_block_size
770
+ dk_accum = torch.zeros(
771
+ batch_size,
772
+ num_head_kv,
773
+ seqlen_k_rounded * head_dim_rounded,
774
+ dtype=torch.float32,
775
+ device=device,
776
+ )
777
+ dv_accum = torch.zeros(
778
+ batch_size,
779
+ num_head_kv,
780
+ seqlen_k_rounded * head_dim_v_rounded,
781
+ dtype=torch.float32,
782
+ device=device,
783
+ )
784
+ else:
785
+ total_k_rounded_padded = (
786
+ (total_k + cu_seqlens_k.shape[0] * n_block_size - 1) // n_block_size * n_block_size
787
+ )
788
+ num_n_blocks = total_k_rounded_padded // n_block_size
789
+ if cluster_size == 2 and num_n_blocks % cluster_size != 0:
790
+ total_k_rounded_padded = total_k_rounded_padded + n_block_size
791
+ dk_accum = torch.zeros(
792
+ num_head_kv,
793
+ total_k_rounded_padded * head_dim_rounded,
794
+ dtype=torch.float32,
795
+ device=device,
796
+ )
797
+ dv_accum = torch.zeros(
798
+ num_head_kv,
799
+ total_k_rounded_padded * head_dim_v_rounded,
800
+ dtype=torch.float32,
801
+ device=device,
802
+ )
803
+
804
+ dtype = torch2cute_dtype_map[q.dtype]
805
+ current_stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
806
+
807
+ if deterministic:
808
+ dQ_semaphore = torch.zeros(batch_size, num_head, seqlen_q_rounded // m_block_size, 1, dtype=torch.int32, device="cuda")
809
+ else:
810
+ dQ_semaphore = None
811
+
812
+ if deterministic and qhead_per_kvhead > 1:
813
+ dK_semaphore = torch.zeros(batch_size, num_head_kv, seqlen_k_rounded // n_block_size, 2, dtype=torch.int32, device="cuda")
814
+ dV_semaphore = torch.zeros(batch_size, num_head_kv, seqlen_k_rounded // n_block_size, 2, dtype=torch.int32, device="cuda")
815
+ else:
816
+ dK_semaphore = None
817
+ dV_semaphore = None
818
+
819
+ # Preprocess kernel: compute (o * dout).sum(dim=-1), lse * log2_e, and zero out dq_accum.
820
+ compile_key_pre = (
821
+ compute_capability,
822
+ dtype,
823
+ head_dim_v,
824
+ m_block_size,
825
+ num_threads,
826
+ cu_seqlens_q is None,
827
+ seqused_q is None,
828
+ )
829
+ if compile_key_pre not in _flash_attn_bwd.compile_cache_pre:
830
+ o_tensor, do_tensor = [to_cute_tensor(t) for t in (out, dout)]
831
+ dq_accum_tensor, dpsum_tensor, lse_log2_tensor = [
832
+ to_cute_tensor(t) for t in (dq_accum, dpsum, lse_log2)
833
+ ]
834
+ lse_tensor = to_cute_tensor(lse, assumed_align=4)
835
+ cu_seqlens_q_tensor, seqused_q_tensor = [
836
+ to_cute_tensor(t, assumed_align=4) if t is not None else None
837
+ for t in (cu_seqlens_q, seqused_q)
838
+ ]
839
+ arch = compute_capability * 10
840
+ fa_bwd_pre = FlashAttentionBackwardPreprocess(
841
+ dtype,
842
+ head_dim_v,
843
+ arch,
844
+ m_block_size,
845
+ num_threads=num_threads,
846
+ )
847
+ # TODO: check @can_implement
848
+ _flash_attn_bwd.compile_cache_pre[compile_key_pre] = cute.compile(
849
+ fa_bwd_pre,
850
+ o_tensor,
851
+ do_tensor,
852
+ dpsum_tensor,
853
+ current_stream,
854
+ lse_tensor,
855
+ lse_log2_tensor,
856
+ dq_accum_tensor,
857
+ cu_seqlens_q_tensor,
858
+ seqused_q_tensor,
859
+ options="--enable-tvm-ffi",
860
+ )
861
+ _flash_attn_bwd.compile_cache_pre[compile_key_pre](
862
+ out,
863
+ dout,
864
+ dpsum,
865
+ current_stream,
866
+ lse,
867
+ lse_log2,
868
+ dq_accum,
869
+ cu_seqlens_q,
870
+ seqused_q,
871
+ )
872
+
873
+ # NB num_threads application for 3 kernels
874
+ # There are pre, main, post processing kernels, currenlty num_threads is only actually
875
+ # used for the pre proc, and then we hard code to 384 for the main and post proc, and we do
876
+ # before cache key gen
877
+ num_threads = 384
878
+
879
+ # Backward kernel: compute dk, dv, dq_accum.
880
+ score_mod_hash = utils.hash_callable(score_mod) if score_mod else False
881
+ score_mod_bwd_hash = utils.hash_callable(score_mod_bwd) if score_mod_bwd else False
882
+ mask_mod_hash = utils.hash_callable(mask_mod) if mask_mod else False
883
+ num_aux_tensors = len(aux_tensors) if aux_tensors else 0
884
+ cute_aux_tensors = None
885
+ if aux_tensors is not None:
886
+ cute_aux_tensors = [to_cute_tensor(buf, assumed_align=None, fully_dynamic=True) for buf in aux_tensors]
887
+
888
+ block_sparse_broadcast_pattern = None
889
+ normalized_block_sparse_tensors = None
890
+ if block_sparse_tensors is not None:
891
+ (
892
+ normalized_block_sparse_tensors,
893
+ block_sparse_broadcast_pattern,
894
+ ) = normalize_block_sparse_config_bwd(
895
+ block_sparse_tensors,
896
+ batch_size=batch_size,
897
+ num_head=num_head,
898
+ seqlen_q=seqlen_q,
899
+ seqlen_k=seqlen_k,
900
+ block_size=(m_block_size, n_block_size),
901
+ subtile_factor=subtile_factor,
902
+ )
903
+
904
+ if compute_capability == 9:
905
+ compile_key = (
906
+ compute_capability,
907
+ dtype,
908
+ head_dim,
909
+ head_dim_v,
910
+ qhead_per_kvhead,
911
+ causal,
912
+ softcap != 0.0,
913
+ m_block_size,
914
+ n_block_size,
915
+ num_threads,
916
+ pack_gqa,
917
+ num_stages_Q,
918
+ num_stages_dO,
919
+ SdP_swapAB,
920
+ dKV_swapAB,
921
+ dQ_swapAB,
922
+ AtomLayoutMSdP,
923
+ AtomLayoutNdKV,
924
+ AtomLayoutMdQ,
925
+ V_in_regs,
926
+ cu_seqlens_q is None,
927
+ cu_seqlens_k is None,
928
+ seqused_q is None,
929
+ seqused_k is None,
930
+ score_mod_hash,
931
+ score_mod_bwd_hash,
932
+ mask_mod_hash,
933
+ num_aux_tensors,
934
+ use_block_sparsity,
935
+ block_sparse_broadcast_pattern,
936
+ )
937
+ else:
938
+ compile_key = (
939
+ compute_capability,
940
+ dtype,
941
+ head_dim,
942
+ head_dim_v,
943
+ qhead_per_kvhead,
944
+ causal,
945
+ window_size_left is not None,
946
+ window_size_right is not None,
947
+ softcap != 0.0,
948
+ m_block_size,
949
+ n_block_size,
950
+ num_threads,
951
+ pack_gqa,
952
+ cluster_size,
953
+ deterministic,
954
+ score_mod_hash,
955
+ score_mod_bwd_hash,
956
+ mask_mod_hash,
957
+ num_aux_tensors,
958
+ use_block_sparsity,
959
+ block_sparse_broadcast_pattern,
960
+ cu_seqlens_q is None,
961
+ cu_seqlens_k is None,
962
+ seqused_q is None,
963
+ seqused_k is None,
964
+ )
965
+ if compile_key not in _flash_attn_bwd.compile_cache:
966
+ q_tensor, k_tensor, v_tensor, do_tensor, dq_tensor, dk_tensor, dv_tensor = [
967
+ to_cute_tensor(t) for t in (q, k, v, dout, dq, dk, dv)
968
+ ]
969
+ dq_accum_tensor, dpsum_tensor, lse_log2_tensor = [
970
+ to_cute_tensor(t) for t in (dq_accum, dpsum, lse_log2)
971
+ ]
972
+ if dKV_postprocess:
973
+ dk_accum_tensor, dv_accum_tensor = [
974
+ to_cute_tensor(t) for t in (dk_accum, dv_accum)
975
+ ]
976
+ cu_seqlens_q_tensor, cu_seqlens_k_tensor, seqused_q_tensor, seqused_k_tensor = [
977
+ to_cute_tensor(t, assumed_align=4) if t is not None else None
978
+ for t in (cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k)
979
+ ]
980
+ dQ_semaphore_tensor, dK_semaphore_tensor, dV_semaphore_tensor = [
981
+ utils.convert_from_dlpack_leading_static(t.detach(), leading_dim=3, alignment=4, stride_order=t.dim_order())
982
+ if t is not None else None
983
+ for t in (dQ_semaphore, dK_semaphore, dV_semaphore)
984
+ ]
985
+ fa_bwd_sm80 = FlashAttentionBackwardSm80(
986
+ dtype,
987
+ head_dim,
988
+ head_dim_v,
989
+ qhead_per_kvhead,
990
+ m_block_size,
991
+ n_block_size,
992
+ num_stages_Q,
993
+ num_stages_dO,
994
+ num_threads,
995
+ pack_gqa,
996
+ causal,
997
+ SdP_swapAB,
998
+ dKV_swapAB,
999
+ dQ_swapAB,
1000
+ AtomLayoutMSdP,
1001
+ AtomLayoutNdKV,
1002
+ AtomLayoutMdQ,
1003
+ V_in_regs=V_in_regs,
1004
+ )
1005
+ if compute_capability == 9:
1006
+ fa_bwd_obj = FlashAttentionBackwardSm90(
1007
+ dtype,
1008
+ head_dim,
1009
+ head_dim_v,
1010
+ qhead_per_kvhead,
1011
+ causal,
1012
+ m_block_size,
1013
+ n_block_size,
1014
+ num_stages_Q,
1015
+ num_stages_dO,
1016
+ num_stages_PdS,
1017
+ SdP_swapAB,
1018
+ dKV_swapAB,
1019
+ dQ_swapAB,
1020
+ AtomLayoutMSdP,
1021
+ AtomLayoutNdKV,
1022
+ AtomLayoutMdQ,
1023
+ num_threads,
1024
+ V_in_regs=V_in_regs,
1025
+ score_mod=score_mod,
1026
+ score_mod_bwd=score_mod_bwd,
1027
+ mask_mod=mask_mod,
1028
+ has_aux_tensors=aux_tensors is not None,
1029
+ subtile_factor=subtile_factor,
1030
+ )
1031
+ else:
1032
+ fa_bwd_obj = FlashAttentionBackwardSm100(
1033
+ head_dim,
1034
+ head_dim_v,
1035
+ is_causal=causal,
1036
+ is_local=local,
1037
+ qhead_per_kvhead=qhead_per_kvhead,
1038
+ # tile_m=m_block_size,
1039
+ # tile_n=n_block_size,
1040
+ cluster_size=cluster_size,
1041
+ # cluster_size=1,
1042
+ deterministic=deterministic,
1043
+ score_mod=score_mod,
1044
+ score_mod_bwd=score_mod_bwd,
1045
+ mask_mod=mask_mod,
1046
+ has_aux_tensors=aux_tensors is not None,
1047
+ subtile_factor=subtile_factor,
1048
+ )
1049
+
1050
+ # Block sparse tensors for backward use Q-direction indexing (transposed from forward).
1051
+ sparse_tensors_compile = None
1052
+ if normalized_block_sparse_tensors is not None:
1053
+ sparse_tensors_compile = to_cute_block_sparse_tensors(normalized_block_sparse_tensors)
1054
+
1055
+ # TODO: check @can_implement
1056
+ _flash_attn_bwd.compile_cache[compile_key] = cute.compile(
1057
+ fa_bwd_obj,
1058
+ q_tensor,
1059
+ k_tensor,
1060
+ v_tensor,
1061
+ do_tensor,
1062
+ lse_log2_tensor,
1063
+ dpsum_tensor,
1064
+ dq_accum_tensor,
1065
+ dk_tensor if not dKV_postprocess else dk_accum_tensor,
1066
+ dv_tensor if not dKV_postprocess else dv_accum_tensor,
1067
+ softmax_scale,
1068
+ current_stream,
1069
+ cu_seqlens_q_tensor,
1070
+ cu_seqlens_k_tensor,
1071
+ seqused_q_tensor,
1072
+ seqused_k_tensor,
1073
+ None, # softcap - not yet supported in backward
1074
+ window_size_left,
1075
+ window_size_right,
1076
+ dQ_semaphore_tensor,
1077
+ dK_semaphore_tensor,
1078
+ dV_semaphore_tensor,
1079
+ cute_aux_tensors,
1080
+ sparse_tensors_compile,
1081
+ options="--enable-tvm-ffi",
1082
+ )
1083
+ _flash_attn_bwd.compile_cache[compile_key](
1084
+ q,
1085
+ k,
1086
+ v,
1087
+ dout,
1088
+ lse_log2,
1089
+ dpsum,
1090
+ dq_accum,
1091
+ dk if not dKV_postprocess else dk_accum,
1092
+ dv if not dKV_postprocess else dv_accum,
1093
+ softmax_scale,
1094
+ current_stream,
1095
+ cu_seqlens_q,
1096
+ cu_seqlens_k,
1097
+ seqused_q,
1098
+ seqused_k,
1099
+ None, # softcap - not yet supported in backward
1100
+ window_size_left,
1101
+ window_size_right,
1102
+ dQ_semaphore,
1103
+ dK_semaphore,
1104
+ dV_semaphore,
1105
+ aux_tensors,
1106
+ normalized_block_sparse_tensors[:4] if normalized_block_sparse_tensors is not None else None,
1107
+ )
1108
+
1109
+ num_threads = 256 if compute_capability == 9 else 128
1110
+ arch = compute_capability * 10
1111
+ # Postprocess kernel: convert dq_accum from float32 to dq in bf16/fp16
1112
+ compile_key_post = (
1113
+ compute_capability,
1114
+ dtype,
1115
+ head_dim,
1116
+ m_block_size,
1117
+ num_threads,
1118
+ AtomLayoutMdQ,
1119
+ dQ_swapAB,
1120
+ cu_seqlens_q is None,
1121
+ seqused_q is None,
1122
+ )
1123
+ if compile_key_post not in _flash_attn_bwd.compile_cache_post:
1124
+ dq_accum_tensor = to_cute_tensor(dq_accum)
1125
+ dq_tensor = to_cute_tensor(dq)
1126
+ cu_seqlens_q_tensor, seqused_q_tensor = [
1127
+ to_cute_tensor(t, assumed_align=4) if t is not None else None
1128
+ for t in (cu_seqlens_q, seqused_q)
1129
+ ]
1130
+ fa_bwd_post = FlashAttentionBackwardPostprocess(
1131
+ dtype, head_dim, arch, m_block_size, num_threads, AtomLayoutMdQ, dQ_swapAB
1132
+ )
1133
+ # TODO: check @can_implement
1134
+ _flash_attn_bwd.compile_cache_post[compile_key_post] = cute.compile(
1135
+ fa_bwd_post,
1136
+ dq_accum_tensor,
1137
+ dq_tensor,
1138
+ softmax_scale,
1139
+ current_stream,
1140
+ cu_seqlens_q_tensor,
1141
+ seqused_q_tensor,
1142
+ options="--enable-tvm-ffi",
1143
+ )
1144
+ _flash_attn_bwd.compile_cache_post[compile_key_post](
1145
+ dq_accum,
1146
+ dq,
1147
+ softmax_scale,
1148
+ current_stream,
1149
+ cu_seqlens_q,
1150
+ seqused_q,
1151
+ )
1152
+
1153
+ if dKV_postprocess:
1154
+ # Postprocess kernel: convert dk_accum & dv_accum from float32 to bf16/fp16
1155
+ compile_key_post = (
1156
+ compute_capability,
1157
+ dtype,
1158
+ head_dim,
1159
+ n_block_size,
1160
+ num_threads,
1161
+ AtomLayoutNdKV,
1162
+ dKV_swapAB,
1163
+ cu_seqlens_k is None,
1164
+ seqused_k is None,
1165
+ )
1166
+ if compile_key_post not in _flash_attn_bwd.compile_cache_post:
1167
+ dk_accum_tensor = to_cute_tensor(dk_accum)
1168
+ dk_tensor = to_cute_tensor(dk)
1169
+ cu_seqlens_k_tensor, seqused_k_tensor = [
1170
+ to_cute_tensor(t, assumed_align=4) if t is not None else None
1171
+ for t in (cu_seqlens_k, seqused_k)
1172
+ ]
1173
+ arch = compute_capability * 10
1174
+ fa_bwd_post = FlashAttentionBackwardPostprocess(
1175
+ dtype, head_dim, arch, n_block_size, num_threads, AtomLayoutNdKV, dKV_swapAB
1176
+ )
1177
+ # TODO: check @can_implement
1178
+ _flash_attn_bwd.compile_cache_post[compile_key_post] = cute.compile(
1179
+ fa_bwd_post,
1180
+ dk_accum_tensor,
1181
+ dk_tensor,
1182
+ softmax_scale,
1183
+ current_stream,
1184
+ cu_seqlens_k_tensor,
1185
+ seqused_k_tensor,
1186
+ options="--enable-tvm-ffi",
1187
+ )
1188
+ _flash_attn_bwd.compile_cache_post[compile_key_post](
1189
+ dk_accum,
1190
+ dk,
1191
+ softmax_scale,
1192
+ current_stream,
1193
+ cu_seqlens_k,
1194
+ seqused_k,
1195
+ )
1196
+ compile_key_post = (
1197
+ compute_capability,
1198
+ dtype,
1199
+ head_dim_v,
1200
+ n_block_size,
1201
+ num_threads,
1202
+ AtomLayoutNdKV,
1203
+ dKV_swapAB,
1204
+ cu_seqlens_k is None,
1205
+ seqused_k is None,
1206
+ )
1207
+ if compile_key_post not in _flash_attn_bwd.compile_cache_post:
1208
+ dv_accum_tensor = to_cute_tensor(dv_accum)
1209
+ dv_tensor = to_cute_tensor(dv)
1210
+ cu_seqlens_k_tensor, seqused_k_tensor = [
1211
+ to_cute_tensor(t, assumed_align=4) if t is not None else None
1212
+ for t in (cu_seqlens_k, seqused_k)
1213
+ ]
1214
+ arch = compute_capability * 10
1215
+ fa_bwd_post = FlashAttentionBackwardPostprocess(
1216
+ dtype, head_dim_v, arch, n_block_size, num_threads, AtomLayoutNdKV, dKV_swapAB
1217
+ )
1218
+ # TODO: check @can_implement
1219
+ _flash_attn_bwd.compile_cache_post[compile_key_post] = cute.compile(
1220
+ fa_bwd_post,
1221
+ dv_accum_tensor,
1222
+ dv_tensor,
1223
+ cutlass.Float32(1.0),
1224
+ current_stream,
1225
+ cu_seqlens_k_tensor,
1226
+ seqused_k_tensor,
1227
+ options="--enable-tvm-ffi",
1228
+ )
1229
+ _flash_attn_bwd.compile_cache_post[compile_key_post](
1230
+ dv_accum,
1231
+ dv,
1232
+ 1.0,
1233
+ current_stream,
1234
+ cu_seqlens_k,
1235
+ seqused_k,
1236
+ )
1237
+
1238
+ return dq, dk, dv
1239
+
1240
+
1241
+ _flash_attn_bwd.compile_cache_pre = {}
1242
+ _flash_attn_bwd.compile_cache = {}
1243
+ _flash_attn_bwd.compile_cache_post = {}
1244
+
1245
+
1246
+ class FlashAttnFunc(torch.autograd.Function):
1247
+ @staticmethod
1248
+ def forward(
1249
+ ctx,
1250
+ q: torch.Tensor,
1251
+ k: torch.Tensor,
1252
+ v: torch.Tensor,
1253
+ softmax_scale: Optional[float] = None,
1254
+ causal: bool = False,
1255
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1256
+ learnable_sink: Optional[torch.Tensor] = None,
1257
+ softcap: float = 0.0,
1258
+ num_splits: int = 1,
1259
+ pack_gqa: Optional[bool] = None,
1260
+ deterministic: bool = False,
1261
+ mask_mod: Optional[Callable] = None,
1262
+ full_block_cnt: Optional[torch.Tensor] = None,
1263
+ full_block_idx: Optional[torch.Tensor] = None,
1264
+ mask_block_cnt: Optional[torch.Tensor] = None,
1265
+ mask_block_idx: Optional[torch.Tensor] = None,
1266
+ block_size: Optional[Tuple[int, int]] = None,
1267
+ ):
1268
+ # Only create block sparse tensors if at least one block sparse parameter is provided
1269
+ block_sparse_tensors = None
1270
+ if any(t is not None for t in [full_block_cnt, full_block_idx, mask_block_cnt, mask_block_idx]):
1271
+ block_sparse_tensors = BlockSparseTensorsTorch(
1272
+ full_block_cnt=full_block_cnt,
1273
+ full_block_idx=full_block_idx,
1274
+ mask_block_cnt=mask_block_cnt,
1275
+ mask_block_idx=mask_block_idx,
1276
+ block_size=block_size,
1277
+ )
1278
+ out, lse = _flash_attn_fwd(
1279
+ q,
1280
+ k,
1281
+ v,
1282
+ softmax_scale=softmax_scale,
1283
+ causal=causal,
1284
+ window_size_left=window_size[0],
1285
+ window_size_right=window_size[1],
1286
+ learnable_sink=learnable_sink,
1287
+ softcap=softcap,
1288
+ num_splits=num_splits,
1289
+ pack_gqa=pack_gqa,
1290
+ mask_mod=mask_mod,
1291
+ block_sparse_tensors=block_sparse_tensors
1292
+ )
1293
+ ctx.save_for_backward(q, k, v, out, lse)
1294
+ ctx.softmax_scale = softmax_scale
1295
+ ctx.causal = causal
1296
+ ctx.window_size = window_size
1297
+ ctx.softcap = softcap
1298
+ ctx.deterministic = deterministic
1299
+ return out, lse
1300
+
1301
+ @staticmethod
1302
+ def backward(ctx, dout, *args):
1303
+ q, k, v, out, lse = ctx.saved_tensors
1304
+ dq, dk, dv = _flash_attn_bwd(
1305
+ q,
1306
+ k,
1307
+ v,
1308
+ out,
1309
+ dout,
1310
+ lse,
1311
+ ctx.softmax_scale,
1312
+ ctx.causal,
1313
+ ctx.softcap,
1314
+ window_size_left=ctx.window_size[0],
1315
+ window_size_right=ctx.window_size[1],
1316
+ deterministic=ctx.deterministic,
1317
+ )
1318
+ return dq, dk, dv, *((None,) * 20) # Extra Nones is fine
1319
+
1320
+
1321
+ class FlashAttnVarlenFunc(torch.autograd.Function):
1322
+ @staticmethod
1323
+ def forward(
1324
+ ctx,
1325
+ q: torch.Tensor,
1326
+ k: torch.Tensor,
1327
+ v: torch.Tensor,
1328
+ cu_seqlens_q: Optional[torch.Tensor],
1329
+ cu_seqlens_k: Optional[torch.Tensor],
1330
+ seqused_q: Optional[torch.Tensor] = None,
1331
+ seqused_k: Optional[torch.Tensor] = None,
1332
+ max_seqlen_q: Optional[int] = None,
1333
+ max_seqlen_k: Optional[int] = None,
1334
+ page_table: Optional[torch.Tensor] = None,
1335
+ softmax_scale: Optional[float] = None,
1336
+ causal: bool = False,
1337
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1338
+ learnable_sink: Optional[torch.Tensor] = None,
1339
+ softcap: float = 0.0,
1340
+ num_splits: int = 1,
1341
+ pack_gqa: Optional[bool] = None,
1342
+ deterministic: bool = False,
1343
+ score_mod: Optional[Callable] = None,
1344
+ aux_tensors: Optional[list] = None,
1345
+ ):
1346
+ out, lse = _flash_attn_fwd(
1347
+ q,
1348
+ k,
1349
+ v,
1350
+ cu_seqlens_q,
1351
+ cu_seqlens_k,
1352
+ seqused_q,
1353
+ seqused_k,
1354
+ max_seqlen_q=max_seqlen_q,
1355
+ max_seqlen_k=max_seqlen_k,
1356
+ page_table=page_table,
1357
+ softmax_scale=softmax_scale,
1358
+ causal=causal,
1359
+ window_size_left=window_size[0],
1360
+ window_size_right=window_size[1],
1361
+ learnable_sink=learnable_sink,
1362
+ softcap=softcap,
1363
+ num_splits=num_splits,
1364
+ pack_gqa=pack_gqa,
1365
+ score_mod=score_mod,
1366
+ aux_tensors=aux_tensors,
1367
+ )
1368
+ ctx.save_for_backward(q, k, v, out, lse, cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k)
1369
+ ctx.softmax_scale = softmax_scale
1370
+ ctx.causal = causal
1371
+ ctx.window_size = window_size
1372
+ ctx.softcap = softcap
1373
+ ctx.deterministic = deterministic
1374
+ ctx.max_seqlen_q = max_seqlen_q
1375
+ ctx.max_seqlen_k = max_seqlen_k
1376
+ return out, lse
1377
+
1378
+ @staticmethod
1379
+ def backward(ctx, dout, *args):
1380
+ q, k, v, out, lse, cu_seqlens_q, cu_seqlens_k, seqused_q, seqused_k = ctx.saved_tensors
1381
+ assert ctx.softcap == 0.0
1382
+ dq, dk, dv = _flash_attn_bwd(
1383
+ q,
1384
+ k,
1385
+ v,
1386
+ out,
1387
+ dout,
1388
+ lse,
1389
+ ctx.softmax_scale,
1390
+ ctx.causal,
1391
+ ctx.softcap,
1392
+ window_size_left=ctx.window_size[0],
1393
+ window_size_right=ctx.window_size[1],
1394
+ cu_seqlens_q=cu_seqlens_q,
1395
+ cu_seqlens_k=cu_seqlens_k,
1396
+ seqused_q=seqused_q,
1397
+ seqused_k=seqused_k,
1398
+ max_seqlen_q=ctx.max_seqlen_q,
1399
+ max_seqlen_k=ctx.max_seqlen_k,
1400
+ deterministic=ctx.deterministic,
1401
+ )
1402
+
1403
+ return dq, dk, dv, *((None,) * 20)
1404
+
1405
+
1406
+ def flash_attn_func(
1407
+ q: torch.Tensor,
1408
+ k: torch.Tensor,
1409
+ v: torch.Tensor,
1410
+ softmax_scale: Optional[float] = None,
1411
+ causal: bool = False,
1412
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1413
+ learnable_sink: Optional[torch.Tensor] = None,
1414
+ softcap: float = 0.0,
1415
+ num_splits: int = 1,
1416
+ pack_gqa: Optional[bool] = None,
1417
+ deterministic: bool = False,
1418
+ mask_mod: Optional[Callable] = None,
1419
+ full_block_cnt: Optional[torch.Tensor] = None,
1420
+ full_block_idx: Optional[torch.Tensor] = None,
1421
+ mask_block_cnt: Optional[torch.Tensor] = None,
1422
+ mask_block_idx: Optional[torch.Tensor] = None,
1423
+ block_size: Optional[Tuple[int, int]] = None,
1424
+ ):
1425
+ return FlashAttnFunc.apply(
1426
+ q,
1427
+ k,
1428
+ v,
1429
+ softmax_scale,
1430
+ causal,
1431
+ window_size,
1432
+ learnable_sink,
1433
+ softcap,
1434
+ num_splits,
1435
+ pack_gqa,
1436
+ deterministic,
1437
+ mask_mod,
1438
+ full_block_cnt,
1439
+ full_block_idx,
1440
+ mask_block_cnt,
1441
+ mask_block_idx,
1442
+ block_size,
1443
+ )
1444
+
1445
+
1446
+ def flash_attn_varlen_func(
1447
+ q: torch.Tensor,
1448
+ k: torch.Tensor,
1449
+ v: torch.Tensor,
1450
+ cu_seqlens_q: Optional[torch.Tensor] = None,
1451
+ cu_seqlens_k: Optional[torch.Tensor] = None,
1452
+ max_seqlen_q: Optional[int] = None,
1453
+ max_seqlen_k: Optional[int] = None,
1454
+ seqused_q: Optional[torch.Tensor] = None,
1455
+ seqused_k: Optional[torch.Tensor] = None,
1456
+ page_table: Optional[torch.Tensor] = None,
1457
+ softmax_scale: Optional[float] = None,
1458
+ causal: bool = False,
1459
+ window_size: Tuple[Optional[int], Optional[int]] = (None, None),
1460
+ learnable_sink: Optional[torch.Tensor] = None,
1461
+ softcap: float = 0.0,
1462
+ num_splits: int = 1,
1463
+ pack_gqa: Optional[bool] = None,
1464
+ deterministic: bool = False,
1465
+ score_mod: Optional[Callable] = None,
1466
+ aux_tensors: Optional[list] = None,
1467
+ ):
1468
+ return FlashAttnVarlenFunc.apply(
1469
+ q,
1470
+ k,
1471
+ v,
1472
+ cu_seqlens_q,
1473
+ cu_seqlens_k,
1474
+ seqused_q,
1475
+ seqused_k,
1476
+ max_seqlen_q,
1477
+ max_seqlen_k,
1478
+ page_table,
1479
+ softmax_scale,
1480
+ causal,
1481
+ window_size,
1482
+ learnable_sink,
1483
+ softcap,
1484
+ num_splits,
1485
+ pack_gqa,
1486
+ deterministic,
1487
+ score_mod,
1488
+ aux_tensors,
1489
+ )
1490
+
1491
+
1492
+ def _flash_attn_fwd_combine(
1493
+ out_partial: torch.Tensor,
1494
+ lse_partial: torch.Tensor,
1495
+ out: torch.Tensor,
1496
+ lse: Optional[torch.Tensor] = None,
1497
+ cu_seqlens: Optional[torch.Tensor] = None,
1498
+ seqused: Optional[torch.Tensor] = None,
1499
+ num_splits_dynamic_ptr: Optional[torch.Tensor] = None,
1500
+ semaphore_to_reset: Optional[torch.Tensor] = None,
1501
+ ) -> None:
1502
+ """Forward combine kernel for split attention computation.
1503
+
1504
+ Combines partial outputs and log-sum-exp values from multiple splits
1505
+ of attention computation into final outputs.
1506
+
1507
+ Args:
1508
+ out_partial: Partial outputs tensor (num_splits, batch, seqlen, nheads, headdim) or
1509
+ (num_splits, total_q, nheads, headdim) if there's cu_seqlens
1510
+ lse_partial: Partial LSE tensor (num_splits, batch, seqlen, nheads) or
1511
+ (num_splits, total_q, nheads) if there's cu_seqlens
1512
+ out: Output tensor (batch, seqlen, nheads, headdim) or (total_q, nheads, headdim) if there's cu_seqlens
1513
+ lse: Output LSE tensor (batch, seqlen, nheads) or (total_q, nheads) if there's cu_seqlens.
1514
+ cu_seqlens: Cumulative sequence lengths for variable length sequences
1515
+ seqused: Used sequence lengths for each batch
1516
+ num_splits_dynamic_ptr: Dynamic number of splits per batch
1517
+ semaphore_to_reset: Semaphore for synchronization
1518
+ k_block_size: Block size for head dimension
1519
+
1520
+ Returns:
1521
+ None
1522
+ """
1523
+ # Input validation
1524
+ assert out_partial.dim() in [4, 5], "out_partial must have 4 or 5 dimensions"
1525
+ assert lse_partial.dim() in [3, 4], "lse_partial must have 3 or 4 dimensions"
1526
+ assert out_partial.dtype in [torch.float16, torch.bfloat16, torch.float32], (
1527
+ "out_partial must be fp16, bf16, or fp32"
1528
+ )
1529
+ assert lse_partial.dtype == torch.float32, "lse_partial must be fp32"
1530
+ assert out_partial.is_cuda and lse_partial.is_cuda, "tensors must be on CUDA device"
1531
+ assert out_partial.stride(-1) == 1, "out_partial must be contiguous in the last dimension"
1532
+ assert lse_partial.stride(-2) == 1, "lse_partial must be contiguous in the seqlen dimension"
1533
+ assert lse_partial.shape == out_partial.shape[:-1]
1534
+
1535
+ # Determine if this is variable length based on dimensions
1536
+ is_varlen = out_partial.dim() == 4
1537
+
1538
+ # Validate output tensor shapes and types
1539
+ assert out.shape == out_partial.shape[1:], "out shape mismatch"
1540
+ if lse is not None:
1541
+ assert lse.shape == lse_partial.shape[1:], "lse shape mismatch"
1542
+ assert lse.dtype == torch.float32, "lse must be fp32"
1543
+
1544
+ # Validate optional tensors
1545
+ for t, name in [
1546
+ (cu_seqlens, "cu_seqlens"),
1547
+ (seqused, "seqused"),
1548
+ (num_splits_dynamic_ptr, "num_splits_dynamic_ptr"),
1549
+ ]:
1550
+ if t is not None:
1551
+ assert t.dtype == torch.int32, f"{name} must be int32"
1552
+ assert t.is_cuda, f"{name} must be on CUDA device"
1553
+ assert t.is_contiguous(), f"{name} must be contiguous"
1554
+
1555
+ head_dim = out_partial.shape[-1]
1556
+ num_splits = out_partial.shape[0]
1557
+ assert num_splits <= 256
1558
+ # If hdim is 96 or 192, it's faster to round them to 128 or 256 respectively
1559
+ # so that kBlockM is smaller and we have more parallelism.
1560
+ k_block_size = 64 if head_dim <= 64 else 128
1561
+ # We want kBlockM to be as small as possible to maximize parallelism.
1562
+ # E.g., if hdim is 64, we want kBlockM to be 16 so that we can use 256 threads, each reading 4 elements (floats).
1563
+ m_block_size = 8 if k_block_size % 128 == 0 else (16 if k_block_size % 64 == 0 else 32)
1564
+ log_max_splits = max(math.ceil(math.log2(num_splits)), 4)
1565
+ if m_block_size == 8:
1566
+ # If kBlockM == 8 then the minimum number of splits is 32.
1567
+ # TODO: we can deal w this by using 128 threads instead
1568
+ log_max_splits = max(log_max_splits, 5)
1569
+
1570
+ current_stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream)
1571
+
1572
+ # Create combine kernel configuration
1573
+ dtype = torch2cute_dtype_map[out.dtype]
1574
+ dtype_partial = torch2cute_dtype_map[out_partial.dtype]
1575
+
1576
+ compile_key = (
1577
+ dtype,
1578
+ dtype_partial,
1579
+ head_dim,
1580
+ m_block_size,
1581
+ k_block_size,
1582
+ log_max_splits,
1583
+ cu_seqlens is not None,
1584
+ seqused is not None,
1585
+ lse is not None,
1586
+ )
1587
+
1588
+ if compile_key not in _flash_attn_fwd_combine.compile_cache:
1589
+ out_partial_tensor = to_cute_tensor(
1590
+ out_partial, leading_dim=4 if not is_varlen else 3
1591
+ )
1592
+ lse_partial_tensor = to_cute_tensor(
1593
+ lse_partial, assumed_align=4, leading_dim=lse_partial.ndim - 2
1594
+ )
1595
+ out_tensor = to_cute_tensor(out, leading_dim=3 if not is_varlen else 2)
1596
+ lse_tensor = (
1597
+ to_cute_tensor(lse, assumed_align=4, leading_dim=lse.ndim - 2)
1598
+ if lse is not None
1599
+ else None
1600
+ )
1601
+
1602
+ optional_tensors = [
1603
+ to_cute_tensor(t, assumed_align=4, leading_dim=0)
1604
+ if t is not None
1605
+ else None
1606
+ for t in (cu_seqlens, seqused, num_splits_dynamic_ptr, semaphore_to_reset)
1607
+ ]
1608
+ cu_seqlens_tensor, seqused_tensor, num_splits_dynamic_tensor, semaphore_tensor = (
1609
+ optional_tensors
1610
+ )
1611
+ fa_combine = FlashAttentionForwardCombine(
1612
+ dtype=dtype,
1613
+ dtype_partial=dtype_partial,
1614
+ head_dim=head_dim,
1615
+ m_block_size=m_block_size,
1616
+ k_block_size=k_block_size,
1617
+ log_max_splits=log_max_splits,
1618
+ )
1619
+
1620
+ # Check if implementation is supported
1621
+ if not fa_combine.can_implement(
1622
+ dtype,
1623
+ dtype_partial,
1624
+ head_dim,
1625
+ m_block_size,
1626
+ k_block_size,
1627
+ log_max_splits,
1628
+ num_threads=256,
1629
+ ):
1630
+ raise RuntimeError(
1631
+ "FlashAttention combine kernel cannot be implemented with given parameters"
1632
+ )
1633
+
1634
+ _flash_attn_fwd_combine.compile_cache[compile_key] = cute.compile(
1635
+ fa_combine,
1636
+ out_partial_tensor,
1637
+ lse_partial_tensor,
1638
+ out_tensor,
1639
+ lse_tensor,
1640
+ cu_seqlens_tensor,
1641
+ seqused_tensor,
1642
+ num_splits_dynamic_tensor,
1643
+ semaphore_tensor,
1644
+ current_stream,
1645
+ options="--enable-tvm-ffi",
1646
+ )
1647
+ _flash_attn_fwd_combine.compile_cache[compile_key](
1648
+ out_partial,
1649
+ lse_partial,
1650
+ out,
1651
+ lse,
1652
+ cu_seqlens,
1653
+ seqused,
1654
+ num_splits_dynamic_ptr,
1655
+ semaphore_to_reset,
1656
+ current_stream,
1657
+ )
1658
+
1659
+
1660
+ _flash_attn_fwd_combine.compile_cache = {}
1661
+
1662
+
1663
+ def flash_attn_combine(
1664
+ out_partial: torch.Tensor,
1665
+ lse_partial: torch.Tensor,
1666
+ out: Optional[torch.Tensor] = None,
1667
+ out_dtype: Optional[torch.dtype] = None,
1668
+ cu_seqlens: Optional[torch.Tensor] = None,
1669
+ seqused: Optional[torch.Tensor] = None,
1670
+ return_lse: bool = True,
1671
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
1672
+ """Flash Attention combine function for split attention computation.
1673
+
1674
+ Combines partial outputs and log-sum-exp values from multiple splits
1675
+ of attention computation into final outputs. This is the main user-facing
1676
+ interface for the combine kernel.
1677
+
1678
+ Args:
1679
+ out_partial: Partial outputs tensor with shape:
1680
+ - (num_splits, batch_size, seqlen, num_heads, head_size) for regular batched input
1681
+ - (num_splits, total_q, num_heads, head_size) for variable length input
1682
+ lse_partial: Partial LSE tensor with shape:
1683
+ - (num_splits, batch_size, seqlen, num_heads) for regular batched input
1684
+ - (num_splits, total_q, num_heads) for variable length input
1685
+ out: Optional output tensor. If None, will be created automatically.
1686
+ out_dtype: Optional output dtype. If None, will use fp16/bf16 based on input.
1687
+ cu_seqlens: Cumulative sequence lengths for variable length sequences
1688
+ seqused: Used sequence lengths for each batch
1689
+ return_lse: Whether to return the combined LSE tensor. Default is True.
1690
+
1691
+ Returns:
1692
+ Tuple of (out, lse) where:
1693
+ - out: Combined output tensor with shape (batch_size, seqlen, num_heads, head_size)
1694
+ or (total_q, num_heads, head_size) for varlen
1695
+ - lse: Combined log-sum-exp tensor with shape (batch_size, seqlen, num_heads)
1696
+ or (total_q, num_heads) for varlen. None if return_lse=False
1697
+
1698
+ Note:
1699
+ This function expects the input tensors to be in the format produced by
1700
+ split attention computation, where the first dimension is num_splits.
1701
+ The permuting from user format to kernel format is now done inside the kernel.
1702
+ """
1703
+ # Input validation
1704
+ assert out_partial.dim() in [4, 5], "out_partial must have 4 or 5 dimensions"
1705
+ assert lse_partial.dim() in [3, 4], "lse_partial must have 3 or 4 dimensions"
1706
+ assert out_partial.dtype == torch.float32, "out_partial must be fp32 (from accumulation)"
1707
+ assert lse_partial.dtype == torch.float32, "lse_partial must be fp32"
1708
+
1709
+ # Determine if this is variable length based on dimensions
1710
+ is_varlen = out_partial.dim() == 4
1711
+
1712
+ if is_varlen:
1713
+ # Variable length: (num_splits, total_q, num_heads, head_size)
1714
+ num_splits, total_q, num_heads, head_size = out_partial.shape
1715
+ assert lse_partial.shape == (num_splits, total_q, num_heads), (
1716
+ "lse_partial shape mismatch for varlen"
1717
+ )
1718
+ batch_size = 1 # Treat as single batch for varlen
1719
+ seqlen = total_q
1720
+ else:
1721
+ # Regular batched: (num_splits, batch_size, seqlen, num_heads, head_size)
1722
+ num_splits, batch_size, seqlen, num_heads, head_size = out_partial.shape
1723
+ assert lse_partial.shape == (num_splits, batch_size, seqlen, num_heads), (
1724
+ "lse_partial shape mismatch"
1725
+ )
1726
+
1727
+ # Determine output dtype
1728
+ if out_dtype is None:
1729
+ out_dtype = out_partial.dtype
1730
+
1731
+ # Create output if not provided
1732
+ device = out_partial.device
1733
+ if out is None:
1734
+ if is_varlen:
1735
+ out = torch.empty(total_q, num_heads, head_size, dtype=out_dtype, device=device)
1736
+ else:
1737
+ out = torch.empty(
1738
+ batch_size, seqlen, num_heads, head_size, dtype=out_dtype, device=device
1739
+ )
1740
+
1741
+ # Create lse output only if requested
1742
+ if return_lse:
1743
+ if is_varlen:
1744
+ lse = torch.empty(num_heads, total_q, dtype=torch.float32, device=device).transpose(
1745
+ 0, 1
1746
+ )
1747
+ else:
1748
+ lse = torch.empty(
1749
+ batch_size, num_heads, seqlen, dtype=torch.float32, device=device
1750
+ ).transpose(1, 2)
1751
+ else:
1752
+ lse = None
1753
+
1754
+ _flash_attn_fwd_combine(
1755
+ out_partial,
1756
+ lse_partial,
1757
+ out,
1758
+ lse,
1759
+ cu_seqlens,
1760
+ seqused,
1761
+ )
1762
+ return out, lse
kernel/deps/flash_attn/cute/mask.py ADDED
@@ -0,0 +1,651 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Optional, Callable
4
+ from dataclasses import dataclass
5
+
6
+ import cutlass
7
+ import cutlass.cute as cute
8
+ from cutlass import Float32, Int32, const_expr
9
+
10
+ import flash_attn.cute.utils as utils
11
+ from flash_attn.cute.seqlen_info import SeqlenInfoQK
12
+
13
+
14
+ @cute.jit
15
+ def mask_r2p(X: cute.Tensor, col_limit: Int32, arch: int = 90, rank1: bool = False) -> None:
16
+ # Bit manipulation, compiles down to the R2P instruction
17
+ # For sm100: we know that tScS_t2r[i][1] == i, for the particular tmem copy atom we're using.
18
+ # For sm90: instead of comparing limit to 0, 1, 8, 9, 16, 17, ...,
19
+ # we compare a transformed version of limit to 0, 1, 2, 3, 4, 5, ...
20
+ if const_expr(arch == 90):
21
+ col_limit_transformed = col_limit // 8 * 2 + min(col_limit % 8, 2)
22
+ else:
23
+ col_limit_transformed = col_limit
24
+ ncol = const_expr(cute.size(X.shape[cute.rank(X) - 1]) if not rank1 else cute.size(X.shape))
25
+ # Ideally we'd move by 32 instead of 24, but mask >> i isn't correct for i == 31
26
+ for s in cutlass.range_constexpr(cute.ceil_div(ncol, 24)):
27
+ # Don't need to clamp to 32 since the shr.u32 instruction does that already
28
+ col_limit_right_s = max(col_limit_transformed - s * 24, 0)
29
+ # 0 -> 0b00...00, 1 -> 0b00...01, ..., 31 -> 0b01...11, 32 -> 0b11...11
30
+ mask = (1 << col_limit_right_s) - 1
31
+ # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
32
+ for i in cutlass.range_constexpr(min(24, ncol - s * 24)):
33
+ in_bound = cutlass.Boolean(mask & (1 << i))
34
+ c = s * 24 + i
35
+ if const_expr(rank1):
36
+ X[c] = X[c] if in_bound else -Float32.inf
37
+ # This is the equivalent of:
38
+ # X[s * 24 + i] = X[s * 24 + i] if col_limit_right_s <= i else -Float32.inf
39
+ else:
40
+ for r in cutlass.range_constexpr(cute.size(X.shape[0])):
41
+ X[r, c] = X[r, c] if in_bound else -Float32.inf
42
+
43
+
44
+ @cute.jit
45
+ def mask_r2p_transposed(X: cute.Tensor, row_limit_top: Int32, num_rep: int) -> None:
46
+ # Bit manipulation, compiles down to the R2P instruction
47
+ # For sm100: we know that tScS_t2r[i][0] has the form 0, 1, ..., 31, 64, ..., 127
48
+ # or 0, 1, ..., 15, 32, ..., 47, 64, ...
49
+ # We compare a transformed version of limit to 0, 1, 2, 3, 4, 5, ...
50
+ # Here we hardcode for the case of 2 warp groups.
51
+ num_wg = 2
52
+ row_limit_top_transformed = row_limit_top // (num_rep * num_wg) * num_rep + min(
53
+ row_limit_top % (num_rep * num_wg), num_rep
54
+ )
55
+ ncol = cute.size(X.shape)
56
+ # Ideally we'd move by 32 instead of 24, but mask >> i isn't correct for i == 31
57
+ for s in cutlass.range_constexpr(cute.ceil_div(ncol, 24)):
58
+ row_limit_top_s = max(row_limit_top_transformed - s * 24, 0)
59
+ # 0 -> 0b00...00, 1 -> 0b00...01, ..., 31 -> 0b01...11, 32 -> 0b11...11
60
+ mask = (1 << row_limit_top_s) - 1
61
+ # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
62
+ for i in cutlass.range_constexpr(min(24, ncol - s * 24)):
63
+ out_bound = cutlass.Boolean(mask & (1 << i))
64
+ c = s * 24 + i
65
+ X[c] = -Float32.inf if out_bound else X[c]
66
+ # tidx = cute.arch.thread_idx()[0] % 256
67
+ # if tidx == 128:
68
+ # cute.printf("tidx = {}, s = {}, i = {}, row_limit_top = {}, row_limit_top_s = {}, mask = {}, out_bound = {}", tidx, s, i, row_limit_top, row_limit_top_s, mask, out_bound)
69
+
70
+
71
+ @cute.jit
72
+ def mask_r2p_dual_bound(
73
+ X: cute.Tensor,
74
+ col_limit_left: Int32, # Inclusive lower bound
75
+ col_limit_right: Int32, # Exclusive upper bound
76
+ ) -> None:
77
+ """
78
+ Dual-bound masking using two bitmasks for SM100, following mask_r2p.
79
+ Masks elements where: NOT (col_limit_left <= col < col_limit_right)
80
+
81
+ Uses bit manipulation to create a range mask:
82
+ mask_right = (1 << right) - 1 -> bits (right-1)..0 are 1
83
+ mask_left = (1 << left) - 1 -> bits (left-1)..0 are 1
84
+ mask_range = mask_range = mask_right & ~ mask_left -> bits (right-1)..left are 1
85
+ """
86
+ ncol = const_expr(cute.size(X.shape))
87
+
88
+ for s in cutlass.range_constexpr(cute.ceil_div(ncol, 24)):
89
+ right_s = max(col_limit_right - s * 24, 0)
90
+ left_s = max(col_limit_left - s * 24, 0)
91
+
92
+ # otherwise cute dsl complains about python int too large to convert into c long
93
+ right_s = min(right_s, 24)
94
+ left_s = min(left_s, 24)
95
+
96
+ # bits (right-1)..left are 1
97
+ mask_right = (1 << right_s) - 1
98
+ mask_left = (1 << left_s) - 1
99
+ mask_range = mask_right & ~mask_left
100
+
101
+ # This needs to be range_constexpr, o/w the compiler can't generate the R2P instruction
102
+ for i in cutlass.range_constexpr(min(24, ncol - s * 24)):
103
+ in_bound = cutlass.Boolean(mask_range & (1 << i))
104
+ c = s * 24 + i
105
+ X[c] = X[c] if in_bound else -Float32.inf
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class AttentionMask:
110
+ tile_m: cutlass.Constexpr[int]
111
+ tile_n: cutlass.Constexpr[int]
112
+ seqlen_info: SeqlenInfoQK
113
+ window_size_left: Optional[Int32] = None
114
+ window_size_right: Optional[Int32] = None
115
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1 # only pass in if we're doing PackGQA
116
+ swap_AB: cutlass.Constexpr[bool] = False
117
+
118
+ @property
119
+ def seqlen_q(self) -> Int32:
120
+ return self.seqlen_info.seqlen_q
121
+
122
+ @property
123
+ def seqlen_k(self) -> Int32:
124
+ return self.seqlen_info.seqlen_k
125
+
126
+ @cute.jit
127
+ def apply_mask(
128
+ self,
129
+ acc_S: cute.Tensor,
130
+ batch_idx: cutlass.Int32,
131
+ head_idx: cutlass.Int32,
132
+ m_block: cutlass.Int32,
133
+ n_block: cutlass.Int32,
134
+ thr_mma: cute.TiledMma,
135
+ mask_seqlen: cutlass.Constexpr[bool],
136
+ mask_causal: cutlass.Constexpr[bool],
137
+ mask_local: cutlass.Constexpr[bool] = False,
138
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
139
+ aux_tensors: Optional[list] = None,
140
+ fastdiv_mods=(None, None),
141
+ ) -> None:
142
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
143
+ acc_S_mn = utils.make_acc_tensor_mn_view(acc_S, transpose=self.swap_AB)
144
+ acc_shape = (self.tile_m, self.tile_n)
145
+ cS = cute.make_identity_tensor(acc_shape if not self.swap_AB else acc_shape[::-1])
146
+ tScS_mn = utils.make_acc_tensor_mn_view(thr_mma.partition_C(cS), transpose=self.swap_AB)
147
+ # We use t0ScS as these indices are known at compile time. We then must subtract the
148
+ # column limit by the thread column offset.
149
+ t0ScS_mn = utils.make_acc_tensor_mn_view(
150
+ thr_mma.get_slice(0).partition_C(cS), transpose=self.swap_AB
151
+ )
152
+ ROW = 0 if const_expr(not self.swap_AB) else 1
153
+ COL = 1 if const_expr(not self.swap_AB) else 0
154
+ thr_col_offset = tScS_mn[0][COL]
155
+ # To handle edge cases of completely masked out rows where n_block_max = 0,
156
+ # we treat negative n_blocks as 0th n_block
157
+ # TODO: find more transparent solution
158
+ if n_block < 0:
159
+ n_block = 0
160
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
161
+ if const_expr(not mask_causal and not mask_local and mask_mod is None):
162
+ if const_expr(mask_seqlen):
163
+ # The compiler now choses not to use R2P
164
+ r2p = const_expr(False and not self.swap_AB)
165
+ if const_expr(not r2p):
166
+ # traverse column index.
167
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
168
+ oob = t0ScS_mn[0, c][COL] >= seqlenk_col_limit
169
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
170
+ acc_S_mn[r, c] = -Float32.inf if oob else acc_S_mn[r, c]
171
+ else:
172
+ mask_r2p(acc_S_mn, seqlenk_col_limit, arch=90)
173
+
174
+ elif const_expr(
175
+ not mask_causal and not mask_local and mask_mod is not None
176
+ ): # FlexAttention mask mod
177
+ nrow = const_expr(cute.size(tScS_mn.shape[0]))
178
+ ncol = const_expr(cute.size(tScS_mn.shape[1]))
179
+ has_fastdiv = const_expr(
180
+ fastdiv_mods is not None
181
+ and fastdiv_mods[0] is not None
182
+ and fastdiv_mods[1] is not None
183
+ )
184
+ wrap_aux_indices = const_expr(
185
+ has_fastdiv and mask_seqlen and const_expr(aux_tensors is not None)
186
+ )
187
+
188
+ for r in cutlass.range_constexpr(nrow):
189
+ # Respect swap_AB: ROW/COL determine which coordinate component corresponds to Q/KV.
190
+ local_row = tScS_mn[r, 0][ROW]
191
+ global_row_idx = local_row + m_block * self.tile_m
192
+ row_for_mod = global_row_idx
193
+ head_idx_for_mod = head_idx
194
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
195
+ head_offset = global_row_idx % self.qhead_per_kvhead_packgqa
196
+ head_idx_for_mod = head_idx * self.qhead_per_kvhead_packgqa + head_offset
197
+ row_for_mod = global_row_idx // self.qhead_per_kvhead_packgqa
198
+ row_for_seqlen = row_for_mod
199
+ if const_expr(wrap_aux_indices):
200
+ _, row_for_mod = divmod(row_for_mod, fastdiv_mods[0])
201
+
202
+ for col in cutlass.range_constexpr(ncol):
203
+ col_idx_local = t0ScS_mn[0, col][COL]
204
+ # Convert to absolute column index
205
+ global_col_idx = thr_col_offset + col_idx_local + n_block * self.tile_n
206
+ col_for_mod = global_col_idx
207
+ if const_expr(wrap_aux_indices):
208
+ _, col_for_mod = divmod(global_col_idx, fastdiv_mods[1])
209
+
210
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
211
+ head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
212
+ q_idx_ssa = utils.scalar_to_ssa(row_for_mod, cutlass.Int32)
213
+ kv_idx_ssa = utils.scalar_to_ssa(col_for_mod, cutlass.Int32)
214
+ mask_value = mask_mod(
215
+ batch_idx_ssa,
216
+ head_idx_ssa,
217
+ q_idx_ssa,
218
+ kv_idx_ssa,
219
+ self.seqlen_info,
220
+ aux_tensors,
221
+ )
222
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
223
+ if const_expr(mask_seqlen):
224
+ out_of_bounds = (row_for_seqlen >= self.seqlen_q) or (
225
+ global_col_idx >= self.seqlen_k
226
+ )
227
+ if out_of_bounds:
228
+ acc_S_mn[r, col] = -cutlass.Float32.inf
229
+ else:
230
+ acc_S_mn[r, col] = acc_S_mn[r, col] if cond else -cutlass.Float32.inf
231
+ else:
232
+ acc_S_mn[r, col] = acc_S_mn[r, col] if cond else -cutlass.Float32.inf
233
+
234
+ else: # Causal or local
235
+ if const_expr(not self.swap_AB):
236
+ # If PackGQA, we split the work of compute divmod among threads in the same row
237
+ threads_per_row = thr_mma.tv_layout_C.shape[0][0]
238
+ mma_m_idx = None
239
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
240
+ assert not self.swap_AB, "swap_AB with PackGQA not supported yet"
241
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, (
242
+ "threads_per_row must divide WARP_SIZE"
243
+ )
244
+ assert cute.size(acc_S_mn.shape[0]) <= threads_per_row
245
+ tidx = thr_mma.thr_idx
246
+ mma_m_idx = (
247
+ m_block * self.tile_m + tScS_mn[tidx % threads_per_row, 0][0]
248
+ ) // self.qhead_per_kvhead_packgqa
249
+ causal_row_offset = (
250
+ 1 + self.seqlen_k - n_block * self.tile_n - self.seqlen_q - thr_col_offset
251
+ )
252
+ if const_expr(mask_causal):
253
+ r2p = const_expr(not self.swap_AB) # R2P trick, see apply_mask_sm100
254
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
255
+ # get the column index limit based on current row. Only consider the row index, so the column index sets to 0.
256
+ if const_expr(self.qhead_per_kvhead_packgqa == 1):
257
+ row_idx = tScS_mn[r, 0][0] + m_block * self.tile_m
258
+ else:
259
+ row_idx = utils.shuffle_sync(
260
+ mma_m_idx, r % threads_per_row, width=threads_per_row
261
+ )
262
+ col_limit_right = row_idx + causal_row_offset
263
+ if const_expr(mask_seqlen):
264
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
265
+ if const_expr(not r2p):
266
+ # traverse column index.
267
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
268
+ acc_S_mn[r, c] = (
269
+ -Float32.inf
270
+ if t0ScS_mn[0, c][1] >= col_limit_right
271
+ else acc_S_mn[r, c]
272
+ )
273
+ else:
274
+ mask_r2p(acc_S_mn[r, None], col_limit_right, arch=90, rank1=True)
275
+ else: # Local
276
+ local_row_offset_right = (
277
+ causal_row_offset + self.window_size_right
278
+ if const_expr(self.window_size_right is not None)
279
+ else None
280
+ )
281
+ local_row_offset_left = (
282
+ causal_row_offset - 1 - self.window_size_left
283
+ if const_expr(self.window_size_left is not None)
284
+ else None
285
+ )
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 cute.arch.thread_idx()[0] == 128: cute.printf("n_block = {}, r = {}, row_idx = {}, causal_row_offset = {}, col_limit_right = {}, col_limit_left = {}", n_block, r, row_idx, causal_row_offset, col_limit_right, col_limit_left)
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
+ # only consider the column index, so the row index sets to 0.
309
+ if col_idx >= col_limit_right or col_idx < col_limit_left:
310
+ acc_S_mn[r, c] = -Float32.inf
311
+ else: # swap_AB
312
+ assert self.qhead_per_kvhead_packgqa == 1
313
+ thr_row_offset = tScS_mn[0][ROW]
314
+ causal_row_offset = (
315
+ seqlenk_col_limit - self.seqlen_q + m_block * self.tile_m + thr_row_offset
316
+ )
317
+ if const_expr(mask_causal):
318
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
319
+ col0 = t0ScS_mn[0, c][COL]
320
+ # If col0 is beyond the column limit, we want to mask out the entire
321
+ # column, by setting row limit to be self.tile_m.
322
+ row_limit_top = (
323
+ self.tile_m
324
+ if col0 >= seqlenk_col_limit and mask_seqlen
325
+ else col0 - causal_row_offset
326
+ )
327
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
328
+ acc_S_mn[r, c] = (
329
+ -Float32.inf
330
+ if t0ScS_mn[r, 0][ROW] < row_limit_top
331
+ else acc_S_mn[r, c]
332
+ )
333
+ else:
334
+ for c in cutlass.range(cute.size(tScS_mn.shape[1]), unroll_full=True):
335
+ col0 = t0ScS_mn[0, c][COL]
336
+ # If col0 is beyond the column limit, we want to mask out the entire
337
+ # column, by setting row limit to be self.tile_m.
338
+ row_limit_top = (
339
+ self.tile_m
340
+ if col0 >= seqlenk_col_limit
341
+ else col0 - causal_row_offset - self.window_size_right
342
+ )
343
+ # TODO: do we need col_limit_sink?
344
+ row_limit_bot = col0 - causal_row_offset + self.window_size_left
345
+ for r in cutlass.range(cute.size(tScS_mn.shape[0]), unroll_full=True):
346
+ row_idx = t0ScS_mn[r, 0][ROW]
347
+ acc_S_mn[r, c] = (
348
+ -Float32.inf
349
+ if row_idx < row_limit_top or row_idx > row_limit_bot
350
+ else acc_S_mn[r, c]
351
+ )
352
+
353
+ @cute.jit
354
+ def apply_mask_sm100(
355
+ self,
356
+ acc_S: cute.Tensor,
357
+ m_block: Int32,
358
+ n_block: Int32,
359
+ thr_mma: cute.TiledMma,
360
+ thr_tmem_load: cute.TiledCopy,
361
+ mask_seqlen: cutlass.Constexpr[bool],
362
+ mask_causal: cutlass.Constexpr[bool],
363
+ mask_local: cutlass.Constexpr[bool] = False,
364
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
365
+ batch_idx: Int32 = None,
366
+ head_idx: Int32 = None,
367
+ aux_tensors: Optional[list] = None,
368
+ fastdiv_mods=(None, None),
369
+ head_divmod=None,
370
+ check_q_boundary: bool = False,
371
+ ) -> None:
372
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
373
+ acc_shape = (self.tile_m, self.tile_n)
374
+ cS = cute.make_identity_tensor(acc_shape if not self.swap_AB else acc_shape[::-1])
375
+ tScS = thr_mma.partition_C(cS)
376
+ tScS_t2r = thr_tmem_load.partition_D(tScS)
377
+ # To handle edge cases of completely masked out rows where n_block_max = 0,
378
+ # we treat negative n_blocks as 0th n_block
379
+ # TODO: find more transparent solution
380
+ if n_block < 0:
381
+ n_block = 0
382
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n
383
+ r2p = True
384
+ if const_expr(not mask_causal and not mask_local and mask_mod is None):
385
+ if const_expr(mask_seqlen):
386
+ if const_expr(not r2p):
387
+ for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
388
+ # if tScS_t2r[i][1] >= seqlenk_col_limit:
389
+ # acc_S[i] = -Float32.inf
390
+ # For some reason the 2 lines above generate really bad SASS
391
+ acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= seqlenk_col_limit else acc_S[i]
392
+ else:
393
+ mask_r2p(acc_S, seqlenk_col_limit, arch=100, rank1=True)
394
+
395
+ elif const_expr(not mask_causal and not mask_local and mask_mod is not None):
396
+ # Block sparse case w/ mask_mod
397
+ has_fastdiv = const_expr(
398
+ fastdiv_mods is not None
399
+ and fastdiv_mods[0] is not None
400
+ and fastdiv_mods[1] is not None
401
+ )
402
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
403
+
404
+ ncol = const_expr(cute.size(tScS_t2r.shape))
405
+ for i in cutlass.range_constexpr(ncol):
406
+ row_coord = tScS_t2r[i][0] if not self.swap_AB else tScS_t2r[i][1]
407
+ col_coord = tScS_t2r[i][1] if not self.swap_AB else tScS_t2r[i][0]
408
+ global_row = row_coord + m_block * self.tile_m
409
+ global_col = col_coord + n_block * self.tile_n
410
+
411
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
412
+ assert head_divmod is not None
413
+ mask_row, head_offset = divmod(global_row, head_divmod)
414
+ head_idx_for_mod = head_idx * self.qhead_per_kvhead_packgqa + head_offset
415
+ else:
416
+ head_idx_for_mod = head_idx
417
+ mask_row = global_row
418
+
419
+ mask_row_for_mod = mask_row
420
+ if const_expr(has_fastdiv and aux_tensors is not None):
421
+ if check_q_boundary:
422
+ _, mask_row_for_mod = divmod(mask_row, fastdiv_mods[0])
423
+ global_col_for_mod = global_col
424
+ if const_expr(has_fastdiv and mask_seqlen and aux_tensors is not None):
425
+ _, global_col_for_mod = divmod(global_col, fastdiv_mods[1])
426
+
427
+ head_idx_ssa = utils.scalar_to_ssa(head_idx_for_mod, cutlass.Int32)
428
+ mask_row_ssa = utils.scalar_to_ssa(mask_row_for_mod, cutlass.Int32)
429
+ kv_idx_ssa = utils.scalar_to_ssa(global_col_for_mod, cutlass.Int32)
430
+ mask_value = mask_mod(
431
+ batch_idx_ssa,
432
+ head_idx_ssa,
433
+ mask_row_ssa,
434
+ kv_idx_ssa,
435
+ self.seqlen_info,
436
+ aux_tensors,
437
+ )
438
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
439
+ acc_S[i] = acc_S[i] if cond else -Float32.inf
440
+ if const_expr(mask_seqlen):
441
+ acc_S[i] = -Float32.inf if global_col >= self.seqlen_k else acc_S[i]
442
+ if check_q_boundary:
443
+ acc_S[i] = -Float32.inf if mask_row >= self.seqlen_q else acc_S[i]
444
+
445
+ else: # Causal or local
446
+ causal_row_offset = 1 + self.seqlen_k - n_block * self.tile_n - self.seqlen_q
447
+ row_idx = tScS_t2r[0][0] + m_block * self.tile_m
448
+ if const_expr(self.qhead_per_kvhead_packgqa != 1):
449
+ row_idx = row_idx // self.qhead_per_kvhead_packgqa
450
+ if const_expr(mask_causal):
451
+ col_limit_right = row_idx + causal_row_offset
452
+ if const_expr(mask_seqlen):
453
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
454
+ # if cute.arch.thread_idx()[0] % 32 == 0:
455
+ # 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)
456
+ ncol = const_expr(cute.size(tScS_t2r.shape))
457
+ if const_expr(not r2p):
458
+ for i in cutlass.range(ncol, unroll_full=True):
459
+ acc_S[i] = -Float32.inf if tScS_t2r[i][1] >= col_limit_right else acc_S[i]
460
+ else:
461
+ mask_r2p(acc_S, col_limit_right, arch=100, rank1=True)
462
+ else:
463
+ local_row_offset_right = (
464
+ causal_row_offset + self.window_size_right
465
+ if const_expr(self.window_size_right is not None)
466
+ else None
467
+ )
468
+ local_row_offset_left = (
469
+ causal_row_offset - 1 - self.window_size_left
470
+ if const_expr(self.window_size_left is not None)
471
+ else None
472
+ )
473
+ if const_expr(self.window_size_right is not None):
474
+ col_limit_right = row_idx + local_row_offset_right
475
+ else:
476
+ col_limit_right = self.tile_n
477
+ if const_expr(mask_seqlen):
478
+ col_limit_right = cutlass.min(col_limit_right, seqlenk_col_limit)
479
+ col_limit_left = (
480
+ row_idx + local_row_offset_left
481
+ if const_expr(self.window_size_left is not None)
482
+ else 0
483
+ )
484
+ if const_expr(not r2p):
485
+ # 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)
486
+ for i in cutlass.range(cute.size(tScS_t2r.shape), unroll_full=True):
487
+ col_idx = tScS_t2r[i][1]
488
+ acc_S[i] = (
489
+ -Float32.inf
490
+ if col_idx >= col_limit_right or col_idx < col_limit_left
491
+ else acc_S[i]
492
+ )
493
+ else:
494
+ # XOR-based R2P dual bound masking
495
+ mask_r2p_dual_bound(acc_S, col_limit_left, col_limit_right)
496
+
497
+ @cute.jit
498
+ def apply_mask_sm100_transposed(
499
+ self,
500
+ acc_S: cute.Tensor,
501
+ tScS_t2r: cute.Tensor,
502
+ t0ScS_t2r: cute.Tensor,
503
+ m_block: cutlass.Int32,
504
+ n_block: cutlass.Int32,
505
+ mask_seqlen: cutlass.Constexpr,
506
+ mask_causal: cutlass.Constexpr,
507
+ mask_local: cutlass.Constexpr,
508
+ mask_mod: cutlass.Constexpr[Optional[Callable]] = None,
509
+ batch_idx: Int32 = None,
510
+ head_idx: Int32 = None,
511
+ aux_tensors: Optional[list] = None,
512
+ fastdiv_mods=(None, None),
513
+ is_full_block: bool = False,
514
+ check_m_boundary: bool = True,
515
+ ) -> None:
516
+ """
517
+ Backward pass: mask S = K @ Q.T where n_block tiles seqlen_k and m_block tiles seqlen_q.
518
+
519
+ Coordinate conventio:
520
+ - ROW corresponds to Q (m_block)
521
+ - COL corresponds to KV (n_block)
522
+
523
+ is_full_block: If True, skip mask_mod (all elements valid). Only apply seqlen masking.
524
+ check_m_boundary: If False, skip seqlen_q boundary check (optimization for non-boundary m_blocks).
525
+ When iterating m_blocks in forward order, only the last m_block may be partial.
526
+ """
527
+ assert not (mask_causal and mask_local), "mask_causal and mask_local cannot be both True"
528
+ ROW = 0 if const_expr(not self.swap_AB) else 1
529
+ COL = 1 if const_expr(not self.swap_AB) else 0
530
+ assert t0ScS_t2r[0][COL] == 0, "col0 == 0"
531
+ thr_col_offset = tScS_t2r[0][COL]
532
+ seqlenk_col_limit = self.seqlen_k - n_block * self.tile_n - thr_col_offset
533
+
534
+ if const_expr(not mask_causal and not mask_local and mask_mod is not None):
535
+ # Block sparse case with mask_mod (backward)
536
+ #
537
+ # Coordinate convention: ROW → Q (m_block), COL → KV (n_block).
538
+ # These already account for swap_AB.
539
+ #
540
+ # FULL blocks: mask_mod returns True for all elements, so skip it.
541
+ # Still need seqlen bounds check (elements may be OOB on last m_block).
542
+ # PARTIAL blocks: apply mask_mod element-wise, then seqlen bounds.
543
+ if is_full_block:
544
+ if const_expr(mask_seqlen):
545
+ if seqlenk_col_limit <= 0:
546
+ # Entire tile is OOB for K
547
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
548
+ acc_S[i] = -cutlass.Float32.inf
549
+ elif check_m_boundary:
550
+ # Last m_block: check Q and K boundaries
551
+ ncol = const_expr(cute.size(tScS_t2r.shape))
552
+ for i in cutlass.range_constexpr(ncol):
553
+ row_coord = tScS_t2r[i][ROW]
554
+ col_coord = tScS_t2r[i][COL]
555
+ global_q = row_coord + m_block * self.tile_m
556
+ global_kv = col_coord + n_block * self.tile_n
557
+ q_out_of_bounds = global_q >= self.seqlen_q
558
+ kv_out_of_bounds = global_kv >= self.seqlen_k
559
+ out_of_bounds = q_out_of_bounds or kv_out_of_bounds
560
+ acc_S[i] = -cutlass.Float32.inf if out_of_bounds else acc_S[i]
561
+ else:
562
+ # Partial block
563
+ has_fastdiv = const_expr(
564
+ fastdiv_mods is not None
565
+ and fastdiv_mods[0] is not None
566
+ and fastdiv_mods[1] is not None
567
+ )
568
+ wrap_aux_indices = const_expr(
569
+ has_fastdiv and mask_seqlen and const_expr(aux_tensors is not None)
570
+ )
571
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32)
572
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32)
573
+
574
+ ncol = const_expr(cute.size(tScS_t2r.shape))
575
+ for i in cutlass.range_constexpr(ncol):
576
+ row_coord = tScS_t2r[i][ROW]
577
+ col_coord = tScS_t2r[i][COL]
578
+ global_q = row_coord + m_block * self.tile_m
579
+ global_kv = col_coord + n_block * self.tile_n
580
+
581
+ q_idx_for_mod = global_q
582
+ kv_idx_for_mod = global_kv
583
+ if const_expr(wrap_aux_indices):
584
+ _, q_idx_for_mod = divmod(global_q, fastdiv_mods[0])
585
+ _, kv_idx_for_mod = divmod(global_kv, fastdiv_mods[1])
586
+
587
+ q_idx_ssa = utils.scalar_to_ssa(q_idx_for_mod, cutlass.Int32)
588
+ kv_idx_ssa = utils.scalar_to_ssa(kv_idx_for_mod, cutlass.Int32)
589
+
590
+ mask_value = mask_mod(
591
+ batch_idx_ssa,
592
+ head_idx_ssa,
593
+ q_idx_ssa,
594
+ kv_idx_ssa,
595
+ self.seqlen_info,
596
+ aux_tensors,
597
+ )
598
+ cond = cutlass.Boolean(utils.ssa_to_scalar(mask_value))
599
+ acc_S[i] = acc_S[i] if cond else -cutlass.Float32.inf
600
+
601
+ if const_expr(mask_seqlen):
602
+ # check_m_boundary=False skips q check for non-boundary m_blocks
603
+ q_out_of_bounds = check_m_boundary and (global_q >= self.seqlen_q)
604
+ kv_out_of_bounds = global_kv >= self.seqlen_k
605
+ out_of_bounds = q_out_of_bounds or kv_out_of_bounds
606
+ acc_S[i] = -cutlass.Float32.inf if out_of_bounds else acc_S[i]
607
+
608
+ elif const_expr(not mask_causal and not mask_local):
609
+ if const_expr(mask_seqlen):
610
+ if seqlenk_col_limit <= 0:
611
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
612
+ acc_S[i] = -cutlass.Float32.inf
613
+ else: # Causal or local
614
+ thr_row_offset = tScS_t2r[0][ROW]
615
+ seqlenq_row_limit = self.seqlen_q - m_block * self.tile_m - thr_row_offset
616
+ causal_offset = seqlenq_row_limit - seqlenk_col_limit
617
+ if const_expr(mask_causal):
618
+ # tidx = cute.arch.thread_idx()[0] % 256
619
+ # if tidx < 32:
620
+ # cute.printf("tidx = {}, {} {}, {} {}", tidx, tScS_t2r[0][0], tScS_t2r[0][1], tScS_t2r[1][0], tScS_t2r[1][1])
621
+ row_limit_top = causal_offset
622
+ if const_expr(mask_seqlen):
623
+ # If col is beyond the column limit, we want to mask out the entire
624
+ # column, by setting row limit to be self.tile_m.
625
+ if seqlenk_col_limit <= 0:
626
+ row_limit_top = self.tile_m
627
+ r2p = True
628
+ if const_expr(not r2p):
629
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
630
+ acc_S[i] = (
631
+ -cutlass.Float32.inf if t0ScS_t2r[i][ROW] < row_limit_top else acc_S[i]
632
+ )
633
+ else:
634
+ num_rep = cute.size(tScS_t2r, mode=[0]) # 16 or 32
635
+ mask_r2p_transposed(acc_S, row_limit_top, num_rep)
636
+ else:
637
+ if const_expr(self.window_size_right is not None):
638
+ row_limit_top = causal_offset - self.window_size_right
639
+ else:
640
+ row_limit_top = 0
641
+ if const_expr(self.window_size_left is not None):
642
+ row_limit_bot = causal_offset + self.window_size_left
643
+ if const_expr(mask_seqlen):
644
+ if seqlenk_col_limit <= 0:
645
+ row_limit_top = self.tile_m
646
+ for i in cutlass.range(cute.size(acc_S.shape), unroll_full=True):
647
+ row_idx = t0ScS_t2r[i][ROW]
648
+ local_mask = row_idx < row_limit_top
649
+ if const_expr(self.window_size_left is not None):
650
+ local_mask |= row_idx > row_limit_bot
651
+ acc_S[i] = -cutlass.Float32.inf if local_mask else acc_S[i]
kernel/deps/flash_attn/cute/mma_sm100_desc.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+ # Ported Cutlass code from C++ to Python:
3
+ # https://github.com/NVIDIA/cutlass/blob/main/include/cute/arch/mma_sm100_desc.hpp
4
+ # https://github.com/NVIDIA/cutlass/blob/main/include/cute/atom/mma_traits_sm100.hpp
5
+
6
+ from enum import IntEnum
7
+
8
+ import cutlass
9
+ import cutlass.cute as cute
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Enumerations that match the HW encodings (values MUST stay identical)
13
+ # ---------------------------------------------------------------------------
14
+
15
+
16
+ class Major(IntEnum): # matrix “layout” in the ISA docs
17
+ K = 0
18
+ MN = 1
19
+
20
+
21
+ class ScaleIn(IntEnum): # negate flags
22
+ One = 0
23
+ Neg = 1
24
+
25
+
26
+ class Saturate(IntEnum):
27
+ False_ = 0
28
+ True_ = 1
29
+
30
+
31
+ class CFormat(IntEnum): # 2-bit field (bits 4-5)
32
+ F16 = 0
33
+ F32 = 1
34
+ S32 = 2
35
+
36
+
37
+ class F16F32Format(IntEnum): # 3-bit field (A/B element type)
38
+ F16 = 0
39
+ BF16 = 1
40
+ TF32 = 2
41
+
42
+
43
+ class S8Format(IntEnum):
44
+ UINT8 = 0
45
+ INT8 = 1
46
+
47
+
48
+ class MXF8F6F4Format(IntEnum):
49
+ E4M3 = 0
50
+ E5M2 = 1
51
+ E2M3 = 3
52
+ E3M2 = 4
53
+ E2M1 = 5
54
+
55
+
56
+ class MaxShift(IntEnum):
57
+ NoShift = 0
58
+ MaxShift8 = 1
59
+ MaxShift16 = 2
60
+ MaxShift32 = 3
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # CUTLASS-type → encoding helpers
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ def to_UMMA_format(cutlass_type) -> int:
69
+ """
70
+ Map a CUTLASS scalar class to the 3-bit encoding for Matrix A/B.
71
+ """
72
+ if cutlass_type is cutlass.Int8:
73
+ return S8Format.INT8
74
+ # Unsigned 8-bit (if available in your CUTLASS build)
75
+ if cutlass_type is cutlass.Uint8:
76
+ return S8Format.UINT8
77
+ # FP-16 / BF-16
78
+ if cutlass_type is cutlass.Float16:
79
+ return F16F32Format.F16
80
+ if cutlass_type is cutlass.BFloat16:
81
+ return F16F32Format.BF16
82
+ # TensorFloat-32 (8-bit exponent, 10-bit mantissa packed in 19 bits)
83
+ if cutlass_type is cutlass.TFloat32:
84
+ return F16F32Format.TF32
85
+ # Float-8 / Float-6 / Float-4 – add whenever CUTLASS exposes them
86
+ if cutlass_type is cutlass.FloatE4M3FN:
87
+ return MXF8F6F4Format.E4M3
88
+ if cutlass_type is cutlass.FloatE5M2:
89
+ return MXF8F6F4Format.E5M2
90
+ raise TypeError(f"Unsupported CUTLASS scalar type for A/B: {cutlass_type!r}")
91
+
92
+
93
+ def to_C_format(cutlass_type) -> int:
94
+ """
95
+ Map a CUTLASS scalar class to the 2-bit accumulator encoding.
96
+ """
97
+ if cutlass_type is cutlass.Float16:
98
+ return CFormat.F16
99
+ if cutlass_type is cutlass.Float32:
100
+ return CFormat.F32
101
+ if cutlass_type is cutlass.Int32:
102
+ return CFormat.S32
103
+ raise TypeError(f"Unsupported CUTLASS scalar type for accumulator: {cutlass_type!r}")
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # The constructor – accepts only CUTLASS scalar classes
108
+ # ---------------------------------------------------------------------------
109
+
110
+
111
+ def make_instr_desc(
112
+ a_type, # CUTLASS scalar class, e.g. cutlass.Int8
113
+ b_type,
114
+ c_type,
115
+ M: int, # 64, 128 or 256
116
+ N: int, # 8 … 256 (multiple of 8)
117
+ a_major: Major,
118
+ b_major: Major,
119
+ a_neg: ScaleIn = ScaleIn.One,
120
+ b_neg: ScaleIn = ScaleIn.One,
121
+ c_sat: Saturate = Saturate.False_,
122
+ is_sparse: bool = False,
123
+ max_shift: MaxShift = MaxShift.NoShift,
124
+ ) -> int:
125
+ """
126
+ Build the 32-bit instruction descriptor for Blackwell MMA.
127
+ All matrix/accumulator **types must be CUTLASS scalar classes** –
128
+ passing integers is forbidden.
129
+ """
130
+ # --- encode element formats -------------------------------------------------
131
+ a_fmt = int(to_UMMA_format(a_type))
132
+ b_fmt = int(to_UMMA_format(b_type))
133
+ c_fmt = int(to_C_format(c_type))
134
+
135
+ # --- range checks on M/N -----------------------------------------------------
136
+ if M not in (64, 128, 256):
137
+ raise ValueError("M must be 64, 128 or 256")
138
+ if N < 8 or N > 256 or (N & 7):
139
+ raise ValueError("N must be a multiple of 8 in the range 8…256")
140
+
141
+ m_dim = M >> 4 # 5-bit field
142
+ n_dim = N >> 3 # 6-bit field
143
+
144
+ # fmt: off
145
+ # --- pack the bit-fields -----------------------------------------------------
146
+ desc = 0
147
+ desc |= (0 & 0x3) << 0 # sparse_id2 (always 0 here)
148
+ desc |= (int(is_sparse) & 0x1) << 2 # sparse_flag
149
+ desc |= (int(c_sat) & 0x1) << 3 # saturate
150
+ desc |= (c_fmt & 0x3) << 4 # c_format
151
+ desc |= (a_fmt & 0x7) << 7 # a_format
152
+ desc |= (b_fmt & 0x7) << 10 # b_format
153
+ desc |= (int(a_neg) & 0x1) << 13 # a_negate
154
+ desc |= (int(b_neg) & 0x1) << 14 # b_negate
155
+ desc |= (int(a_major) & 0x1) << 15 # a_major
156
+ desc |= (int(b_major) & 0x1) << 16 # b_major
157
+ desc |= (n_dim & 0x3F) << 17 # n_dim (6 bits)
158
+ desc |= (m_dim & 0x1F) << 24 # m_dim (5 bits)
159
+ desc |= (int(max_shift) & 0x3) << 30 # max_shift (2 bits)
160
+ # fmt: on
161
+
162
+ return desc & 0xFFFF_FFFF # ensure 32-bit result
163
+
164
+
165
+ def mma_op_to_idesc(op: cute.nvgpu.tcgen05.mma.MmaOp):
166
+ return make_instr_desc(
167
+ op.a_dtype,
168
+ op.b_dtype,
169
+ op.acc_dtype,
170
+ op.shape_mnk[0],
171
+ op.shape_mnk[1],
172
+ Major.K if op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K else Major.MN,
173
+ Major.K if op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K else Major.MN,
174
+ )
175
+
176
+
177
+ class LayoutType(IntEnum): # occupies the top-3 bits [61:64)
178
+ SWIZZLE_NONE = 0 # (a.k.a. “INTERLEAVE” in older docs)
179
+ SWIZZLE_128B_BASE32B = 1
180
+ SWIZZLE_128B = 2
181
+ SWIZZLE_64B = 4
182
+ SWIZZLE_32B = 6
183
+ # values 3,5,7 are reserved / illegal for UMMA
184
+
185
+
186
+ # ---------------------------------------------------------------------------
187
+ # Helpers – figure out the SWIZZLE_* family from the tensor layout
188
+ # ---------------------------------------------------------------------------
189
+
190
+
191
+ def _layout_type(swizzle: cute.Swizzle) -> LayoutType:
192
+ # No idea what the right way to get B, M, S is – so we're just parsing it from the __str__
193
+ # Swizzle string has the form "S<B,M,S>"
194
+ swz_str = str(swizzle)
195
+ inside = swz_str[swz_str.index("<") + 1 : swz_str.index(">")] # '3,4,3'
196
+ B, M, S = [int(x) for x in inside.split(",")] # [3, 4, 3]
197
+
198
+ if M == 4: # Swizzle<*,4,3>
199
+ if S != 3:
200
+ raise ValueError("Unexpected swizzle shift – want S==3 for M==4")
201
+ return {
202
+ 0: LayoutType.SWIZZLE_NONE,
203
+ 1: LayoutType.SWIZZLE_32B,
204
+ 2: LayoutType.SWIZZLE_64B,
205
+ 3: LayoutType.SWIZZLE_128B,
206
+ }[B] # KeyError ⇒ invalid B→ raise
207
+ if M == 5: # Swizzle<2,5,2> (the only legal triple for M==5)
208
+ if (B, S) != (2, 2):
209
+ raise ValueError("Only Swizzle<2,5,2> supported for 128B_BASE32B")
210
+ return LayoutType.SWIZZLE_128B_BASE32B
211
+
212
+ # Any other (M,B,S) triple is not a UMMA-legal shared-memory layout
213
+ raise ValueError("Unsupported swizzle triple for UMMA smem descriptor")
214
+
215
+
216
+ def make_smem_desc_base(layout: cute.Layout, swizzle: cute.Swizzle, major: Major) -> int:
217
+ """
218
+ Convert a 2-D *shared-memory* Cute layout into the Blackwell 64-bit
219
+ smem-descriptor, without the smem start address.
220
+ layout must correspond to layout of an uint128 tensor.
221
+ """
222
+ # ------------------------------------------------------------------ meta
223
+ layout_type = _layout_type(swizzle) # resolve SWIZZLE_* family
224
+
225
+ VERSION = 1 # bits 46–47
226
+ LBO_MODE = 0 # bit 52
227
+ BASE_OFFSET = 0 # bits 49–51 (CUTLASS always 0)
228
+
229
+ # ---------------------------------------------------------- strides (units: uint128_t = 16 B)
230
+ swizzle_atom_mn_size = {
231
+ LayoutType.SWIZZLE_NONE: 1,
232
+ LayoutType.SWIZZLE_32B: 2,
233
+ LayoutType.SWIZZLE_64B: 4,
234
+ LayoutType.SWIZZLE_128B: 8,
235
+ LayoutType.SWIZZLE_128B_BASE32B: 8,
236
+ }[layout_type]
237
+
238
+ if major is Major.MN:
239
+ swizzle_atom_k_size = 4 if layout_type is LayoutType.SWIZZLE_128B_BASE32B else 8
240
+ canonical_layout = cute.logical_divide(layout, (swizzle_atom_mn_size, swizzle_atom_k_size))
241
+ if not cute.is_congruent(canonical_layout, ((1, 1), (1, 1))):
242
+ raise ValueError("Not a canonical UMMA_MN Layout: Expected profile failure.")
243
+ stride_00 = canonical_layout.stride[0][0]
244
+ if layout_type is not LayoutType.SWIZZLE_NONE and stride_00 != 1:
245
+ raise ValueError("Not a canonical UMMA_MN Layout: Expected stride failure.")
246
+ stride_10 = canonical_layout.stride[1][0]
247
+ if stride_10 != swizzle_atom_mn_size:
248
+ raise ValueError("Not a canonical UMMA_MN Layout: Expected stride failure.")
249
+ stride_01, stride_11 = canonical_layout.stride[0][1], canonical_layout.stride[1][1]
250
+ if layout_type is LayoutType.SWIZZLE_NONE:
251
+ stride_byte_offset, leading_byte_offset = stride_01, stride_11
252
+ else:
253
+ stride_byte_offset, leading_byte_offset = stride_11, stride_01
254
+ else:
255
+ if layout_type == LayoutType.SWIZZLE_128B_BASE32B:
256
+ raise ValueError("SWIZZLE_128B_BASE32B is invalid for Major-K")
257
+ if not cute.size(layout.shape[0]) % 8 == 0:
258
+ raise ValueError("Not a canonical UMMA_K Layout: Expected MN-size multiple of 8.")
259
+ canonical_layout = cute.logical_divide(layout, (8, 2))
260
+ if not cute.is_congruent(canonical_layout, ((1, 1), (1, 1))):
261
+ raise ValueError("Not a canonical UMMA_K Layout: Expected profile failure.")
262
+ stride_00 = canonical_layout.stride[0][0]
263
+ if stride_00 != swizzle_atom_mn_size:
264
+ raise ValueError("Not a canonical UMMA_K Layout: Expected stride failure.")
265
+ stride_10 = canonical_layout.stride[1][0]
266
+ if layout_type is not LayoutType.SWIZZLE_NONE and stride_10 != 1:
267
+ raise ValueError("Not a canonical UMMA_K Layout: Expected stride failure.")
268
+ stride_01 = canonical_layout.stride[0][1]
269
+ stride_byte_offset, leading_byte_offset = stride_01, stride_10
270
+
271
+ # ------------------------------------------------------------------ pack
272
+ desc = 0
273
+ # leading_byte_offset_ [16:30)
274
+ desc |= (leading_byte_offset & 0x3FFF) << 16
275
+ # stride_byte_offset_ [32:46)
276
+ desc |= (stride_byte_offset & 0x3FFF) << 32
277
+ # version_ [46:48)
278
+ desc |= (VERSION & 0x3) << 46
279
+ # base_offset_ [49:52)
280
+ desc |= (BASE_OFFSET & 0x7) << 49
281
+ # lbo_mode_ [52:53)
282
+ desc |= (LBO_MODE & 0x1) << 52
283
+ # layout_type_ [61:64)
284
+ desc |= (int(layout_type) & 0x7) << 61
285
+
286
+ return desc & 0xFFFF_FFFF_FFFF_FFFF # force 64-bit width
287
+
288
+
289
+ def make_smem_desc_start_addr(start_addr: cute.Pointer) -> cutlass.Int32:
290
+ # 14 bits, remove 4 LSB (bits 0-13 in desc)
291
+ return (start_addr.toint() & 0x3FFFF) >> 4
kernel/deps/flash_attn/cute/named_barrier.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 NamedBarrierBwd(enum.IntEnum):
16
+ Epilogue = enum.auto()
17
+ WarpSchedulerWG1 = enum.auto()
18
+ WarpSchedulerWG2 = enum.auto()
19
+ WarpSchedulerWG3 = enum.auto()
20
+ PdS = enum.auto()
21
+ dQFullWG0 = enum.auto()
22
+ dQFullWG1 = enum.auto()
23
+ dQEmptyWG0 = enum.auto()
24
+ dQEmptyWG1 = enum.auto()
25
+
26
+
27
+ class NamedBarrierBwdSm100(enum.IntEnum):
28
+ EpilogueWG1 = enum.auto()
29
+ EpilogueWG2 = enum.auto()
30
+ Compute = enum.auto()
31
+ dQaccReduce = enum.auto()
kernel/deps/flash_attn/cute/pack_gqa.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+
7
+ import flash_attn.cute.utils as utils
8
+
9
+
10
+ class PackGQA:
11
+ def __init__(
12
+ self,
13
+ m_block_size: cutlass.Constexpr[int],
14
+ head_dim_padded: cutlass.Constexpr[int],
15
+ check_hdim_oob: cutlass.Constexpr[bool],
16
+ qhead_per_kvhead: cutlass.Constexpr[bool],
17
+ ):
18
+ self.m_block_size = m_block_size
19
+ self.head_dim_padded = head_dim_padded
20
+ self.check_hdim_oob = check_hdim_oob
21
+ self.qhead_per_kvhead = qhead_per_kvhead
22
+
23
+ @cute.jit
24
+ def compute_ptr(
25
+ self,
26
+ tensor: cute.Tensor,
27
+ cRows: cute.Tensor,
28
+ tidx: cutlass.Int32,
29
+ block: cutlass.Int32,
30
+ threads_per_row: cutlass.Constexpr[int],
31
+ num_threads: cutlass.Constexpr[int],
32
+ ):
33
+ num_ptr_per_thread = cute.ceil_div(cute.size(cRows), threads_per_row)
34
+ tPrPtr = cute.make_fragment(num_ptr_per_thread, cutlass.Int64)
35
+ for i in cutlass.range_constexpr(num_ptr_per_thread):
36
+ row = i * num_threads + cRows[tidx % threads_per_row][0]
37
+ idx = block * self.m_block_size + row
38
+ m_idx = idx // self.qhead_per_kvhead
39
+ h_idx = idx - m_idx * self.qhead_per_kvhead
40
+ tPrPtr[i] = utils.elem_pointer(tensor, ((h_idx, m_idx),)).toint()
41
+ return tPrPtr
42
+
43
+ @cute.jit
44
+ def load_Q(
45
+ self,
46
+ mQ: cute.Tensor, # ((qhead_per_kvhead, seqlen_q), headdim)
47
+ sQ: cute.Tensor, # (m_block_size, head_dim_padded)
48
+ gmem_tiled_copy: cute.TiledCopy,
49
+ tidx: cutlass.Int32,
50
+ block: cutlass.Int32,
51
+ seqlen: cutlass.Int32,
52
+ ):
53
+ gmem_thr_copy = gmem_tiled_copy.get_slice(tidx)
54
+ cQ = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
55
+ tQsQ = gmem_thr_copy.partition_D(sQ)
56
+ tQcQ = gmem_thr_copy.partition_S(cQ)
57
+ t0QcQ = gmem_thr_copy.get_slice(0).partition_S(cQ)
58
+ tQpQ = utils.predicate_k(tQcQ, limit=mQ.shape[1])
59
+ tQcQ_row = tQcQ[0, None, 0]
60
+ threads_per_row = gmem_tiled_copy.layout_tv_tiled.shape[0][0]
61
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, "threads_per_row must divide WARP_SIZE"
62
+ num_threads = gmem_tiled_copy.size
63
+ tPrQPtr = self.compute_ptr(mQ[None, 0], tQcQ_row, tidx, block, threads_per_row, num_threads)
64
+ for m in cutlass.range_constexpr(cute.size(tQsQ.shape[1])):
65
+ q_ptr_i64 = utils.shuffle_sync(
66
+ tPrQPtr[m // threads_per_row], m % threads_per_row, width=threads_per_row
67
+ )
68
+ q_gmem_ptr = cute.make_ptr(
69
+ mQ.element_type, q_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
70
+ )
71
+ if (
72
+ t0QcQ[0, m, 0][0]
73
+ < seqlen * self.qhead_per_kvhead - block * self.m_block_size - tQcQ_row[0][0]
74
+ ):
75
+ mQ_cur = cute.make_tensor(q_gmem_ptr, (self.head_dim_padded,))
76
+ elems_per_load = cute.size(tQsQ.shape[0][0])
77
+ mQ_cur_copy = cute.tiled_divide(mQ_cur, (elems_per_load,))
78
+ for k in cutlass.range_constexpr(cute.size(tQsQ.shape[2])):
79
+ ki = tQcQ[0, 0, k][1] // elems_per_load
80
+ cute.copy(
81
+ gmem_thr_copy,
82
+ mQ_cur_copy[None, ki],
83
+ tQsQ[None, m, k],
84
+ pred=tQpQ[None, m, k] if cutlass.const_expr(self.check_hdim_oob) else None,
85
+ )
86
+ # We don't need to clear the sQ smem tiles since we'll only write out the valid outputs
87
+
88
+ @cute.jit
89
+ def store_LSE(
90
+ self,
91
+ mLSE: cute.Tensor, # (qhead_per_kvhead, seqlen_q)
92
+ tLSErLSE: cute.Tensor, # (m_block_size, head_dim_padded)
93
+ tiled_mma: cute.TiledMma,
94
+ tidx: cutlass.Int32,
95
+ block: cutlass.Int32,
96
+ seqlen: cutlass.Int32,
97
+ ):
98
+ thr_mma = tiled_mma.get_slice(tidx)
99
+ caccO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
100
+ taccOcO = thr_mma.partition_C(caccO)
101
+ taccOcO_row = utils.make_acc_tensor_mn_view(taccOcO)[None, 0]
102
+ assert cute.size(tLSErLSE) == cute.size(taccOcO_row)
103
+ threads_per_row = tiled_mma.tv_layout_C.shape[0][0]
104
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, "threads_per_row must divide WARP_SIZE"
105
+ assert cute.size(tLSErLSE) <= threads_per_row
106
+ num_threads = tiled_mma.size
107
+ tPrLSEPtr = self.compute_ptr(mLSE, taccOcO_row, tidx, block, threads_per_row, num_threads)
108
+ for m in cutlass.range_constexpr(cute.size(tLSErLSE)):
109
+ lse_ptr_i64 = utils.shuffle_sync(
110
+ tPrLSEPtr[m // threads_per_row],
111
+ m % threads_per_row,
112
+ width=threads_per_row,
113
+ )
114
+ lse_gmem_ptr = cute.make_ptr(
115
+ mLSE.element_type, lse_ptr_i64, cute.AddressSpace.gmem, assumed_align=4
116
+ )
117
+ row = block * self.m_block_size + taccOcO_row[m][0]
118
+ # Only the thread corresponding to column 0 writes out the lse to gmem
119
+ if taccOcO[0][1] == 0 and row < seqlen * self.qhead_per_kvhead:
120
+ mLSE_copy = cute.make_tensor(lse_gmem_ptr, (1,))
121
+ mLSE_copy[0] = tLSErLSE[m]
122
+
123
+ @cute.jit
124
+ def store_O(
125
+ self,
126
+ mO: cute.Tensor, # ((qhead_per_kvhead, seqlen_q), headdim)
127
+ tOrO: cute.Tensor, # (m_block_size, head_dim_padded) split across threads according to gmem_tiled_copy
128
+ gmem_tiled_copy: cute.TiledCopy,
129
+ tidx: cutlass.Int32,
130
+ block: cutlass.Int32,
131
+ seqlen: cutlass.Int32,
132
+ ):
133
+ gmem_thr_copy = gmem_tiled_copy.get_slice(tidx)
134
+ cO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
135
+ tOcO = gmem_thr_copy.partition_S(cO)
136
+ t0OcO = gmem_thr_copy.get_slice(0).partition_S(cO)
137
+ tOpO = utils.predicate_k(tOcO, limit=mO.shape[1])
138
+ tOcO_row = tOcO[0, None, 0]
139
+ threads_per_row = gmem_tiled_copy.layout_tv_tiled.shape[0][0]
140
+ assert cute.arch.WARP_SIZE % threads_per_row == 0, "threads_per_row must divide WARP_SIZE"
141
+ num_threads = gmem_tiled_copy.size
142
+ tPrOPtr = self.compute_ptr(mO[None, 0], tOcO_row, tidx, block, threads_per_row, num_threads)
143
+ for m in cutlass.range_constexpr(cute.size(tOrO.shape[1])):
144
+ o_ptr_i64 = utils.shuffle_sync(
145
+ tPrOPtr[m // threads_per_row], m % threads_per_row, width=threads_per_row
146
+ )
147
+ o_gmem_ptr = cute.make_ptr(
148
+ mO.element_type, o_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
149
+ )
150
+ if (
151
+ t0OcO[0, m, 0][0]
152
+ < seqlen * self.qhead_per_kvhead - block * self.m_block_size - tOcO_row[0][0]
153
+ ):
154
+ mO_cur = cute.make_tensor(o_gmem_ptr, (self.head_dim_padded,))
155
+ elems_per_load = cute.size(tOrO.shape[0][0])
156
+ mO_cur_copy = cute.tiled_divide(mO_cur, (elems_per_load,))
157
+ for k in cutlass.range_constexpr(cute.size(tOrO.shape[2])):
158
+ ki = tOcO[0, 0, k][1] // elems_per_load
159
+ cute.copy(
160
+ gmem_thr_copy,
161
+ tOrO[None, m, k],
162
+ mO_cur_copy[None, ki],
163
+ pred=tOpO[None, m, k] if cutlass.const_expr(self.check_hdim_oob) else None,
164
+ )
kernel/deps/flash_attn/cute/paged_kv.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Type
2
+ from dataclasses import dataclass
3
+
4
+ import cutlass
5
+ import cutlass.cute as cute
6
+ from cutlass.cute.nvgpu import cpasync
7
+ from cutlass import Int32, const_expr
8
+
9
+ from flash_attn.cute import utils
10
+ from flash_attn.cute.cute_dsl_utils import ParamsBase
11
+ from cutlass.cute import FastDivmodDivisor
12
+
13
+ import math
14
+
15
+
16
+ @dataclass
17
+ class PagedKVManager(ParamsBase):
18
+ mPageTable: cute.Tensor
19
+ mK_paged: cute.Tensor
20
+ mV_paged: cute.Tensor
21
+ thread_idx: Int32
22
+
23
+ page_size_divmod: FastDivmodDivisor
24
+ seqlen_k: Int32
25
+ leftpad_k: Int32
26
+ n_block_size: Int32
27
+ num_threads: cutlass.Constexpr[Int32]
28
+ head_dim_padded: cutlass.Constexpr[Int32]
29
+ head_dim_v_padded: cutlass.Constexpr[Int32]
30
+
31
+ gmem_threads_per_row: cutlass.Constexpr[Int32]
32
+ page_entry_per_thread: Int32
33
+ async_copy_elems: Int32
34
+
35
+ gmem_tiled_copy_KV: cute.TiledCopy
36
+ gmem_thr_copy_KV: cute.TiledCopy
37
+ tPrPage: cute.Tensor
38
+ tPrPageOffset: cute.Tensor
39
+ tKpK: cute.Tensor
40
+ tVpV: cute.Tensor
41
+
42
+ @staticmethod
43
+ def create(
44
+ mPageTable: cute.Tensor,
45
+ mK_paged: cute.Tensor,
46
+ mV_paged: cute.Tensor,
47
+ page_size_divmod: FastDivmodDivisor,
48
+ bidb: Int32,
49
+ bidh: Int32,
50
+ thread_idx: Int32,
51
+ seqlen_k: Int32,
52
+ leftpad_k: Int32,
53
+ n_block_size: cutlass.Constexpr[Int32],
54
+ head_dim_padded: cutlass.Constexpr[Int32],
55
+ head_dim_v_padded: cutlass.Constexpr[Int32],
56
+ num_threads: cutlass.Constexpr[Int32],
57
+ dtype: Type[cutlass.Numeric],
58
+ ):
59
+ universal_copy_bits = 128
60
+ async_copy_elems = universal_copy_bits // dtype.width
61
+ dtype_bytes = dtype.width // 8
62
+ gmem_k_block_size = math.gcd(
63
+ head_dim_padded,
64
+ head_dim_v_padded,
65
+ 128 // dtype_bytes,
66
+ )
67
+ assert gmem_k_block_size % async_copy_elems == 0
68
+ gmem_threads_per_row = gmem_k_block_size // async_copy_elems
69
+ assert cute.arch.WARP_SIZE % gmem_threads_per_row == 0
70
+ atom_async_copy = cute.make_copy_atom(
71
+ cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
72
+ dtype,
73
+ num_bits_per_copy=universal_copy_bits,
74
+ )
75
+ thr_layout = cute.make_ordered_layout(
76
+ (num_threads // gmem_threads_per_row, gmem_threads_per_row),
77
+ order=(1, 0),
78
+ )
79
+ val_layout = cute.make_layout((1, async_copy_elems))
80
+ gmem_tiled_copy_KV = cute.make_tiled_copy_tv(atom_async_copy, thr_layout, val_layout)
81
+ gmem_thr_copy_KV = gmem_tiled_copy_KV.get_slice(thread_idx)
82
+ page_entry_per_thread = n_block_size // num_threads
83
+
84
+ tPrPage = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
85
+ tPrPageOffset = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
86
+
87
+ mPageTable = mPageTable[bidb, None]
88
+ mK_paged = mK_paged[None, None, bidh, None]
89
+ mV_paged = mV_paged[None, None, bidh, None]
90
+
91
+ cK = cute.make_identity_tensor((n_block_size, head_dim_padded))
92
+ tKcK = gmem_thr_copy_KV.partition_S(cK)
93
+ tKpK = utils.predicate_k(tKcK, limit=mK_paged.shape[1])
94
+
95
+ if const_expr(head_dim_padded == head_dim_v_padded):
96
+ tVpV = tKpK
97
+ else:
98
+ cV = cute.make_identity_tensor((n_block_size, head_dim_v_padded))
99
+ tVcV = gmem_thr_copy_KV.partition_S(cV)
100
+ tVpV = utils.predicate_k(tVcV, limit=mV_paged.shape[0])
101
+
102
+ return PagedKVManager(
103
+ mPageTable,
104
+ mK_paged,
105
+ mV_paged,
106
+ thread_idx,
107
+ page_size_divmod,
108
+ seqlen_k,
109
+ leftpad_k,
110
+ n_block_size,
111
+ num_threads,
112
+ head_dim_padded,
113
+ head_dim_v_padded,
114
+ gmem_threads_per_row,
115
+ page_entry_per_thread,
116
+ async_copy_elems,
117
+ gmem_tiled_copy_KV,
118
+ gmem_thr_copy_KV,
119
+ tPrPage,
120
+ tPrPageOffset,
121
+ tKpK,
122
+ tVpV,
123
+ )
124
+
125
+ @cute.jit
126
+ def load_page_table(self, n_block: Int32):
127
+ for i in cutlass.range(self.page_entry_per_thread, unroll=1):
128
+ row = (
129
+ i * self.num_threads
130
+ + (self.thread_idx % self.gmem_threads_per_row)
131
+ * (self.num_threads // self.gmem_threads_per_row)
132
+ + (self.thread_idx // self.gmem_threads_per_row)
133
+ )
134
+ row_idx = n_block * self.n_block_size + row
135
+
136
+ page_idx, page_offset = divmod(row_idx + self.leftpad_k, self.page_size_divmod)
137
+
138
+ is_valid = (
139
+ (i + 1) * self.num_threads <= self.n_block_size or row < self.n_block_size
140
+ ) and row_idx < self.seqlen_k
141
+ page = self.mPageTable[page_idx] if is_valid else 0
142
+
143
+ self.tPrPage[i] = page
144
+ self.tPrPageOffset[i] = page_offset
145
+
146
+ @cute.jit
147
+ def compute_X_ptr(self, K_or_V: str):
148
+ tPrXPtr = cute.make_rmem_tensor((self.page_entry_per_thread,), cutlass.Int64)
149
+ for i in cutlass.range(self.page_entry_per_thread, unroll=1):
150
+ page = self.tPrPage[i]
151
+ page_offset = self.tPrPageOffset[i]
152
+ if const_expr(K_or_V == "K"):
153
+ tPrXPtr[i] = utils.elem_pointer(self.mK_paged, (page_offset, 0, page)).toint()
154
+ else:
155
+ tPrXPtr[i] = utils.elem_pointer(self.mV_paged, (0, page_offset, page)).toint()
156
+ return tPrXPtr
157
+
158
+ @cute.jit
159
+ def load_KV(self, n_block: Int32, sX: cute.Tensor, K_or_V: str):
160
+ assert K_or_V in ("K", "V")
161
+
162
+ tPrXPtr = self.compute_X_ptr(K_or_V)
163
+
164
+ # Finesse sX layout to be (M, N).
165
+ sX_pi = cute.make_tensor(
166
+ sX.iterator,
167
+ cute.make_layout(
168
+ (sX.shape[0][0], (sX.shape[0][1], sX.shape[2])),
169
+ stride=(sX.stride[0][0], (sX.stride[0][1], sX.stride[2])),
170
+ ),
171
+ )
172
+
173
+ if const_expr(K_or_V == "V"):
174
+ # Need to transpose V
175
+ sX_pi = cute.make_tensor(sX_pi.iterator, cute.select(sX_pi.layout, mode=[1, 0]))
176
+
177
+ head_dim = self.head_dim_v_padded if const_expr(K_or_V == "V") else self.head_dim_padded
178
+ cX = cute.make_identity_tensor((self.n_block_size, head_dim))
179
+ tXsX = self.gmem_thr_copy_KV.partition_D(sX_pi)
180
+ tXcX = self.gmem_thr_copy_KV.partition_S(cX)
181
+ tXc0X = self.gmem_thr_copy_KV.get_slice(0).partition_S(cX)
182
+
183
+ seqlenk_row_limit = (
184
+ self.seqlen_k - n_block * self.n_block_size - tXcX[0][0] if n_block >= 0 else 0
185
+ )
186
+ for m in cutlass.range_constexpr(cute.size(tXsX, mode=[1])):
187
+ row_valid = tXc0X[0, m, 0][0] < seqlenk_row_limit
188
+ should_load = cute.make_fragment_like(tXsX[(0, None), m, 0], cute.Boolean)
189
+ should_load.fill(row_valid)
190
+
191
+ x_ptr_i64 = utils.shuffle_sync(
192
+ tPrXPtr[m // self.gmem_threads_per_row],
193
+ m % self.gmem_threads_per_row,
194
+ width=self.gmem_threads_per_row,
195
+ )
196
+ x_gmem_ptr = cute.make_ptr(
197
+ self.mK_paged.element_type, x_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
198
+ )
199
+ mX_paged_cur = cute.make_tensor(x_gmem_ptr, cute.make_layout((head_dim,)))
200
+ mX_paged_cur_copy = cute.tiled_divide(mX_paged_cur, (self.async_copy_elems,))
201
+
202
+ for k in cutlass.range_constexpr(cute.size(tXsX, mode=[2])):
203
+ ki = tXcX[0, 0, k][1] // self.async_copy_elems
204
+ mX_paged_cur_copy_ki = mX_paged_cur_copy[None, ki]
205
+ tXsX_k = tXsX[None, m, k]
206
+ mX_paged_cur_copy_ki = cute.make_tensor(
207
+ mX_paged_cur_copy_ki.iterator, tXsX_k.layout
208
+ )
209
+ cute.copy(
210
+ self.gmem_tiled_copy_KV,
211
+ mX_paged_cur_copy_ki,
212
+ tXsX_k,
213
+ pred=should_load,
214
+ )
kernel/deps/flash_attn/cute/pipeline.py ADDED
@@ -0,0 +1,272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ # import math
4
+ from typing import Optional
5
+ from dataclasses import dataclass
6
+
7
+ import cutlass
8
+ import cutlass.cute as cute
9
+ from cutlass import Boolean, Int32, const_expr
10
+ from cutlass.cutlass_dsl import if_generate
11
+ from cutlass.pipeline import PipelineAsync, PipelineState, Agent, CooperativeGroup
12
+ from cutlass.pipeline import PipelineUserType, PipelineOp
13
+ from cutlass.pipeline import PipelineTmaAsync as PipelineTmaAsyncOg
14
+ from cutlass.pipeline import PipelineTmaUmma as PipelineTmaUmmaOg
15
+
16
+
17
+ # We deviate from cute-dsl implementation to use cute.arch.cluster_arrive_relaxed
18
+ def pipeline_init_wait(cta_layout_vmnk: Optional[cute.Layout] = None):
19
+ """
20
+ Fences the mbarrier init and syncs the threadblock or cluster
21
+ """
22
+ cute.arch.mbarrier_init_fence()
23
+
24
+ if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
25
+ # If not using clusters, sync the threadblock
26
+ _sync(Agent.ThreadBlock)
27
+ else:
28
+ # If using clusters, sync the cluster
29
+ _sync(Agent.ThreadBlockCluster)
30
+
31
+
32
+ def _sync(group: Agent):
33
+ """
34
+ Syncs all threads within an agent.
35
+ """
36
+ if group is Agent.Thread:
37
+ raise NotImplementedError("Error: Not supported.")
38
+ elif group is Agent.ThreadBlock:
39
+ cute.arch.sync_threads()
40
+ elif group is Agent.ThreadBlockCluster:
41
+ cute.arch.cluster_arrive_relaxed()
42
+ cute.arch.cluster_wait()
43
+ else:
44
+ assert False, (
45
+ "Error: No explicit sync instruction exists. Please use barriers (named / mbarrier) instead."
46
+ )
47
+
48
+
49
+ class PipelineStateSimple:
50
+ """
51
+ Pipeline state contains an index and phase bit corresponding to the current position in the circular buffer.
52
+ Use a single Int32 to store both the index and phase bit, then we use divmod to get the
53
+ index and phase. If stages is a power of 2, divmod turns into bit twiddling.
54
+ """
55
+
56
+ def __init__(self, stages: int, phase_index: Int32):
57
+ # assert stages < 2**16
58
+ # self._log_stages = int(math.log2(stages))
59
+ # assert 1 << self._log_stages == stages, "Number of stages must be a power of 2."
60
+ self._stages = stages
61
+ self._phase_index = phase_index
62
+
63
+ def clone(self) -> "PipelineStateSimple":
64
+ return PipelineStateSimple(self.stages, self._phase_index)
65
+
66
+ @property
67
+ def stages(self) -> int:
68
+ # return 1 << self._log_stages
69
+ return self._stages
70
+
71
+ @property
72
+ def index(self) -> Int32:
73
+ # return self._phase_index & 0xFFFF
74
+ # return self._phase_index & ((1 << self._log_stages) - 1)
75
+ if const_expr(self._stages == 1):
76
+ return Int32(0)
77
+ else:
78
+ return self._phase_index % self._stages
79
+
80
+ @property
81
+ def phase(self) -> Int32:
82
+ # return self._phase_index >> 16
83
+ # PTX docs say that the phase parity needs to be 0 or 1, so by right we need to
84
+ # take modulo 2. But in practice just passing the phase in without modulo works fine.
85
+ # return (self._phase_index >> self._log_stages) % 2
86
+ # return self._phase_index >> self._log_stages
87
+ if const_expr(self._stages == 1):
88
+ return self._phase_index
89
+ else:
90
+ return self._phase_index // self._stages
91
+
92
+ def advance(self):
93
+ if const_expr(self._stages == 1):
94
+ self._phase_index ^= 1
95
+ else:
96
+ self._phase_index += 1
97
+
98
+ # def then_body(phase_index):
99
+ # # XOR the phase bit and set the index to 0
100
+ # return (phase_index & 0xFFFF0000) ^ (1 << 16)
101
+
102
+ # def else_body(phase_index):
103
+ # return phase_index
104
+
105
+ # self._phase_index = if_generate(
106
+ # (self._phase_index & 0xFFFF) == self.stages,
107
+ # then_body,
108
+ # else_body,
109
+ # [self._phase_index],
110
+ # [Int32],
111
+ # )
112
+
113
+ def __extract_mlir_values__(self):
114
+ phase_index = self._phase_index
115
+ return [phase_index.ir_value()]
116
+
117
+ def __new_from_mlir_values__(self, values):
118
+ return PipelineStateSimple(self.stages, Int32(values[0]))
119
+
120
+
121
+ def make_pipeline_state(type: PipelineUserType, stages: int):
122
+ """
123
+ Creates a pipeline state. Producers are assumed to start with an empty buffer and have a flipped phase bit of 1.
124
+ """
125
+ if type is PipelineUserType.Producer:
126
+ # return PipelineStateSimple(stages, Int32(1 << 16))
127
+ return PipelineStateSimple(stages, Int32(stages))
128
+ elif type is PipelineUserType.Consumer:
129
+ return PipelineStateSimple(stages, Int32(0))
130
+ else:
131
+ assert False, "Error: invalid PipelineUserType specified for make_pipeline_state."
132
+
133
+
134
+ @dataclass(frozen=True)
135
+ class PipelineTmaAsync(PipelineTmaAsyncOg):
136
+ """
137
+ Override producer_acquire to take in extra_tx_count parameter.
138
+ """
139
+
140
+ @staticmethod
141
+ def create(*args, **kwargs):
142
+ obj = PipelineTmaAsyncOg.create(*args, **kwargs)
143
+ # Can't assign to __class__ directly since the dataclass is frozen
144
+ # obj.__class__ = PipelineTmaAsync
145
+ object.__setattr__(obj, "__class__", PipelineTmaAsync)
146
+ return obj
147
+
148
+ def producer_acquire(
149
+ self,
150
+ state: PipelineState,
151
+ try_acquire_token: Optional[Boolean] = None,
152
+ extra_tx_count: int = 0,
153
+ ):
154
+ """
155
+ TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
156
+ """
157
+ if_generate(
158
+ try_acquire_token is None or try_acquire_token == 0,
159
+ lambda: self.sync_object_empty.wait(state.index, state.phase),
160
+ )
161
+ if const_expr(extra_tx_count == 0):
162
+ self.sync_object_full.arrive(state.index, self.producer_mask)
163
+ else:
164
+ tx_count = self.sync_object_full.tx_count + extra_tx_count
165
+ self.sync_object_full.arrive_and_expect_tx(state.index, tx_count)
166
+
167
+
168
+ @dataclass(frozen=True)
169
+ class PipelineTmaUmma(PipelineTmaUmmaOg):
170
+ @staticmethod
171
+ def create(
172
+ *,
173
+ num_stages: int,
174
+ producer_group: CooperativeGroup,
175
+ consumer_group: CooperativeGroup,
176
+ tx_count: int,
177
+ barrier_storage: cute.Pointer = None,
178
+ cta_layout_vmnk: Optional[cute.Layout] = None,
179
+ mcast_mode_mn: tuple[int, int] = (1, 1),
180
+ init_wait: cutlass.Constexpr[bool] = True,
181
+ ):
182
+ """
183
+ This helper function computes any necessary attributes and returns an instance of PipelineTmaUmma.
184
+ :param barrier_storage: Pointer to the smem address for this pipeline's mbarriers
185
+ :type barrier_storage: cute.Pointer
186
+ :param num_stages: Number of buffer stages for this pipeline
187
+ :type num_stages: Int32
188
+ :param producer_group: `CooperativeGroup` for the producer agent
189
+ :type producer_group: CooperativeGroup
190
+ :param consumer_group: `CooperativeGroup` for the consumer agent
191
+ :type consumer_group: CooperativeGroup
192
+ :param tx_count: Number of bytes expected to be written to the transaction barrier for one stage
193
+ :type tx_count: int
194
+ :param cta_layout_vmnk: Layout of the cluster shape
195
+ :type cta_layout_vmnk: cute.Layout | None
196
+ :param mcast_mode_mn: Tuple of two integers, specifying whether mcast is enabled for the m and n modes. At least one of the two integers must be 1.
197
+ :type mcast_mode_mn: tuple[int, int]
198
+ """
199
+ if not isinstance(barrier_storage, cute.Pointer):
200
+ raise ValueError(
201
+ f"Expected barrier_storage to be a cute.Pointer, but got {type(barrier_storage)}"
202
+ )
203
+
204
+ producer_type = PipelineOp.TmaLoad
205
+ consumer_type = PipelineOp.TCGen05Mma
206
+
207
+ producer = (producer_type, producer_group)
208
+ consumer = (consumer_type, consumer_group)
209
+
210
+ sync_object_full = PipelineAsync._make_sync_object(
211
+ barrier_storage.align(min_align=8), num_stages, producer, tx_count
212
+ )
213
+ sync_object_empty = PipelineAsync._make_sync_object(
214
+ barrier_storage.align(min_align=8) + num_stages, num_stages, consumer
215
+ )
216
+
217
+ if cta_layout_vmnk is None or cute.size(cta_layout_vmnk) == 1:
218
+ # No mcast mask if not using clusters
219
+ producer_mask = None
220
+ # All threadblocks are leaders if not using clusters
221
+ is_leader_cta = True
222
+ else:
223
+ producer_mask = PipelineTmaUmma._compute_mcast_arrival_mask(
224
+ cta_layout_vmnk, mcast_mode_mn
225
+ )
226
+ is_leader_cta = PipelineTmaUmma._compute_is_leader_cta(cta_layout_vmnk)
227
+
228
+ cta_group = (
229
+ cute.nvgpu.tcgen05.CtaGroup.ONE
230
+ if cta_layout_vmnk is None or cute.size(cta_layout_vmnk, mode=[0]) == 1
231
+ else cute.nvgpu.tcgen05.CtaGroup.TWO
232
+ )
233
+
234
+ consumer_mask = producer_mask
235
+
236
+ if const_expr(init_wait):
237
+ pipeline_init_wait(cta_layout_vmnk)
238
+
239
+ return PipelineTmaUmma(
240
+ sync_object_full,
241
+ sync_object_empty,
242
+ num_stages,
243
+ producer_mask,
244
+ consumer_mask,
245
+ is_leader_cta,
246
+ cta_group,
247
+ )
248
+
249
+ def producer_acquire(
250
+ self,
251
+ state: PipelineState,
252
+ try_acquire_token: Optional[Boolean] = None,
253
+ extra_tx_count: int = 0,
254
+ ):
255
+ """
256
+ TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
257
+ """
258
+ if_generate(
259
+ try_acquire_token is None or try_acquire_token == 0,
260
+ lambda: self.sync_object_empty.wait(state.index, state.phase),
261
+ )
262
+ if const_expr(extra_tx_count == 0):
263
+ if_generate(
264
+ self.is_leader_cta,
265
+ lambda: self.sync_object_full.arrive(state.index, self.producer_mask),
266
+ )
267
+ else:
268
+ tx_count = self.sync_object_full.tx_count + extra_tx_count
269
+ if_generate(
270
+ self.is_leader_cta,
271
+ lambda: self.sync_object_full.arrive_and_expect_tx(state.index, tx_count),
272
+ )
kernel/deps/flash_attn/cute/seqlen_info.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """
9
+ This consolidates all the info related to sequence length. This is so that we can do all
10
+ the gmem reads once at the beginning of each tile, rather than having to repeat these reads
11
+ to compute various things like n_block_min, n_block_max, etc.
12
+ """
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class SeqlenInfo:
17
+ offset: cutlass.Int32
18
+ seqlen: cutlass.Int32
19
+
20
+ @staticmethod
21
+ def create(
22
+ batch_idx: cutlass.Int32,
23
+ seqlen_static: cutlass.Int32,
24
+ cu_seqlens: Optional[cute.Tensor] = None,
25
+ seqused: Optional[cute.Tensor] = None,
26
+ ):
27
+ offset = 0 if const_expr(cu_seqlens is None) else cu_seqlens[batch_idx]
28
+ if const_expr(seqused is not None):
29
+ seqlen = seqused[batch_idx]
30
+ elif const_expr(cu_seqlens is not None):
31
+ seqlen = cu_seqlens[batch_idx + 1] - cu_seqlens[batch_idx]
32
+ else:
33
+ seqlen = seqlen_static
34
+ return SeqlenInfo(offset, seqlen)
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class SeqlenInfoQK:
39
+ offset_q: cutlass.Int32
40
+ offset_k: cutlass.Int32
41
+ padded_offset_q: cutlass.Int32
42
+ padded_offset_k: cutlass.Int32
43
+ seqlen_q: cutlass.Int32
44
+ seqlen_k: cutlass.Int32
45
+ has_cu_seqlens_q: cutlass.Constexpr[bool]
46
+ has_cu_seqlens_k: cutlass.Constexpr[bool]
47
+ has_seqused_q: cutlass.Constexpr[bool]
48
+ has_seqused_k: cutlass.Constexpr[bool]
49
+
50
+ @staticmethod
51
+ def create(
52
+ batch_idx: cutlass.Int32,
53
+ seqlen_q_static: cutlass.Int32,
54
+ seqlen_k_static: cutlass.Int32,
55
+ mCuSeqlensQ: Optional[cute.Tensor] = None,
56
+ mCuSeqlensK: Optional[cute.Tensor] = None,
57
+ mSeqUsedQ: Optional[cute.Tensor] = None,
58
+ mSeqUsedK: Optional[cute.Tensor] = None,
59
+ tile_m: cutlass.Constexpr[cutlass.Int32] = 128,
60
+ tile_n: cutlass.Constexpr[cutlass.Int32] = 128,
61
+ ):
62
+ offset_q = 0 if const_expr(mCuSeqlensQ is None) else mCuSeqlensQ[batch_idx]
63
+ offset_k = 0 if const_expr(mCuSeqlensK is None) else mCuSeqlensK[batch_idx]
64
+ padded_offset_q = (
65
+ 0
66
+ if const_expr(mCuSeqlensQ is None)
67
+ else (offset_q + batch_idx * tile_m) // tile_m * tile_m
68
+ )
69
+ padded_offset_k = (
70
+ 0
71
+ if const_expr(mCuSeqlensK is None)
72
+ else (offset_k + batch_idx * tile_n) // tile_n * tile_n
73
+ )
74
+ if const_expr(mSeqUsedQ is not None):
75
+ seqlen_q = mSeqUsedQ[batch_idx]
76
+ else:
77
+ seqlen_q = (
78
+ seqlen_q_static
79
+ if const_expr(mCuSeqlensQ is None)
80
+ else mCuSeqlensQ[batch_idx + 1] - offset_q
81
+ )
82
+ if const_expr(mSeqUsedK is not None):
83
+ seqlen_k = mSeqUsedK[batch_idx]
84
+ else:
85
+ seqlen_k = (
86
+ seqlen_k_static
87
+ if const_expr(mCuSeqlensK is None)
88
+ else mCuSeqlensK[batch_idx + 1] - offset_k
89
+ )
90
+ has_cu_seqlens_q: int = mCuSeqlensQ is not None
91
+ has_cu_seqlens_k: int = mCuSeqlensK is not None
92
+ has_seqused_q: int = mSeqUsedQ is not None
93
+ has_seqused_k: int = mSeqUsedK is not None
94
+ return SeqlenInfoQK(
95
+ offset_q,
96
+ offset_k,
97
+ padded_offset_q,
98
+ padded_offset_k,
99
+ seqlen_q,
100
+ seqlen_k,
101
+ has_cu_seqlens_q,
102
+ has_cu_seqlens_k,
103
+ has_seqused_q,
104
+ has_seqused_k,
105
+ )
106
+
107
+ def offset_batch_Q(
108
+ self,
109
+ mQ: cute.Tensor,
110
+ batch_idx: Int32,
111
+ dim: int,
112
+ padded: cutlass.Constexpr[bool] = False,
113
+ ) -> cute.Tensor:
114
+ """Seqlen must be the first dimension of mQ"""
115
+ if const_expr(not self.has_cu_seqlens_q):
116
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mQ) - 1 - dim)
117
+ return mQ[idx]
118
+ else:
119
+ offset_q = self.offset_q if const_expr(not padded) else self.padded_offset_q
120
+ offset = offset_q if const_expr(cute.rank(mQ.shape[0]) == 1) else (0, offset_q)
121
+ idx = (offset,) + (0,) * (cute.rank(mQ) - 1)
122
+ return cute.domain_offset(idx, mQ)
123
+
124
+ def offset_batch_K(
125
+ self,
126
+ mK: cute.Tensor,
127
+ batch_idx: Int32,
128
+ dim: int,
129
+ padded: cutlass.Constexpr[bool] = False,
130
+ ) -> cute.Tensor:
131
+ """Seqlen must be the first dimension of mK"""
132
+ if const_expr(not self.has_cu_seqlens_k):
133
+ idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mK) - 1 - dim)
134
+ return mK[idx]
135
+ else:
136
+ offset_k = self.offset_k if const_expr(not padded) else self.padded_offset_k
137
+ idx = (offset_k,) + (0,) * (cute.rank(mK) - 1)
138
+ return cute.domain_offset(idx, mK)
kernel/deps/flash_attn/cute/softmax.py ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import flash_attn.cute.utils as utils
13
+ from flash_attn.cute.cute_dsl_utils import ParamsBase
14
+ from flash_attn.cute.seqlen_info import SeqlenInfoQK
15
+
16
+
17
+ @dataclass
18
+ class Softmax(ParamsBase):
19
+ scale_log2: Float32
20
+ num_rows: cutlass.Constexpr[int]
21
+ row_max: cute.Tensor
22
+ row_sum: cute.Tensor
23
+ arch: cutlass.Constexpr[int] = 80
24
+ softmax_scale: Float32 | None = None
25
+
26
+ @staticmethod
27
+ def create(
28
+ scale_log2: Float32,
29
+ num_rows: cutlass.Constexpr[int],
30
+ arch: cutlass.Constexpr[int] = 80,
31
+ softmax_scale: Float32 | None = None,
32
+ ):
33
+ row_max = cute.make_rmem_tensor(num_rows, Float32)
34
+ row_sum = cute.make_rmem_tensor(num_rows, Float32)
35
+ return Softmax(scale_log2, num_rows, row_max, row_sum, arch, softmax_scale)
36
+
37
+ def reset(self) -> None:
38
+ self.row_max.fill(-Float32.inf)
39
+ self.row_sum.fill(0.0)
40
+
41
+ def _compute_row_max(
42
+ self, acc_S_row: cute.TensorSSA, init_val: float | Float32 | None = None
43
+ ) -> Float32:
44
+ return utils.fmax_reduce(acc_S_row, init_val, arch=self.arch)
45
+
46
+ def _compute_row_sum(
47
+ self, acc_S_row_exp: cute.TensorSSA, init_val: float | Float32 | None = None
48
+ ) -> Float32:
49
+ return utils.fadd_reduce(acc_S_row_exp, init_val, arch=self.arch)
50
+
51
+ @cute.jit
52
+ def online_softmax(
53
+ self,
54
+ acc_S: cute.Tensor,
55
+ is_first: cutlass.Constexpr[bool] = False,
56
+ check_inf: cutlass.Constexpr[bool] = True,
57
+ ) -> cute.Tensor:
58
+ """Apply online softmax and return the row_scale to rescale O.
59
+
60
+ :param acc_S: acc_S tensor
61
+ :type acc_S: cute.Tensor
62
+ :param is_first: is first n_block
63
+ :type is_first: cutlass.Constexpr
64
+ """
65
+ # Change acc_S to M,N layout view.
66
+ acc_S_mn = utils.make_acc_tensor_mn_view(acc_S)
67
+ row_scale = cute.make_fragment_like(self.row_max, Float32)
68
+
69
+ row_max = self.row_max
70
+ row_sum = self.row_sum
71
+ scale_log2 = self.scale_log2
72
+ arch = self.arch
73
+
74
+ # Each iteration processes one row of acc_S
75
+ for r in cutlass.range(cute.size(row_max), unroll_full=True):
76
+ acc_S_row = acc_S_mn[r, None].load() # (n_block_size)
77
+
78
+ row_max_cur = utils.fmax_reduce(
79
+ acc_S_row,
80
+ init_val=row_max[r] if cutlass.const_expr(not is_first) else None,
81
+ arch=arch,
82
+ )
83
+
84
+ row_max_cur = utils.warp_reduce(row_max_cur, cute.arch.fmax, width=4)
85
+ # Update row_max before changing row_max_cur to safe value for -inf
86
+ row_max_prev = row_max[r]
87
+ row_max[r] = row_max_cur
88
+
89
+ if cutlass.const_expr(check_inf):
90
+ row_max_cur = 0.0 if row_max_cur == -Float32.inf else row_max_cur
91
+
92
+ if cutlass.const_expr(is_first):
93
+ row_max_cur_scaled = row_max_cur * scale_log2
94
+ acc_S_row_exp = utils.exp2f(acc_S_row * scale_log2 - row_max_cur_scaled)
95
+
96
+ acc_S_row_sum = utils.fadd_reduce(acc_S_row_exp, init_val=None, arch=arch)
97
+ row_scale[r] = 1.0
98
+ else:
99
+ row_max_cur_scaled = row_max_cur * scale_log2
100
+ acc_S_row_exp = utils.exp2f(acc_S_row * scale_log2 - row_max_cur_scaled)
101
+ # row_scale[r] = utils.exp2f(row_max_prev * self.scale_log2 - row_max_cur_scaled)
102
+ row_scale[r] = utils.exp2f((row_max_prev - row_max_cur) * scale_log2)
103
+
104
+ acc_S_row_sum = utils.fadd_reduce(
105
+ acc_S_row_exp, init_val=row_sum[r] * row_scale[r], arch=arch
106
+ )
107
+
108
+ row_sum[r] = acc_S_row_sum
109
+ acc_S_mn[r, None].store(acc_S_row_exp)
110
+
111
+ return row_scale
112
+
113
+ @cute.jit
114
+ def finalize(
115
+ self, final_scale: Float32 = 1.0, sink_val: Float32 | cute.Tensor | None = None
116
+ ) -> cute.Tensor:
117
+ """Finalize the online softmax by computing the scale and logsumexp."""
118
+ if cutlass.const_expr(sink_val is not None and isinstance(sink_val, cute.Tensor)):
119
+ assert cute.size(sink_val) == cute.size(self.row_sum)
120
+ row_sum = self.row_sum
121
+ row_max = self.row_max
122
+ scale_log2 = self.scale_log2
123
+
124
+ # quad reduction for row_sum as we didn't do it during each iteration of online softmax
125
+ row_sum.store(utils.warp_reduce(row_sum.load(), operator.add, width=4))
126
+ row_scale = cute.make_fragment_like(row_max, Float32)
127
+
128
+ for r in cutlass.range(cute.size(row_sum), unroll_full=True):
129
+ if cutlass.const_expr(sink_val is not None):
130
+ sink_val_cur = sink_val if not isinstance(sink_val, cute.Tensor) else sink_val[r]
131
+ LOG2_E = math.log2(math.e)
132
+ row_sum[r] += utils.exp2f(sink_val_cur * LOG2_E - row_max[r] * scale_log2)
133
+
134
+ # if row_sum is zero or nan, set acc_O_mn_row to 1.0
135
+ acc_O_mn_row_is_zero_or_nan = row_sum[r] == 0.0 or row_sum[r] != row_sum[r]
136
+ row_scale[r] = (
137
+ cute.arch.rcp_approx(row_sum[r] if not acc_O_mn_row_is_zero_or_nan else 1.0)
138
+ ) * final_scale
139
+ row_sum_cur = row_sum[r]
140
+ LN2 = math.log(2.0)
141
+ row_sum[r] = (
142
+ (row_max[r] * scale_log2 + utils.log2f(row_sum_cur)) * LN2
143
+ if not acc_O_mn_row_is_zero_or_nan
144
+ else -Float32.inf
145
+ )
146
+ return row_scale
147
+
148
+ @cute.jit
149
+ def rescale_O(self, acc_O: cute.Tensor, row_scale: cute.Tensor) -> None:
150
+ """Scale each row of acc_O by the given scale tensor.
151
+ :param acc_O: input tensor
152
+ :type acc_O: cute.Tensor
153
+ :param row_scale: row_scale tensor
154
+ :type row_scale: cute.Tensor
155
+ """
156
+ acc_O_mn = utils.make_acc_tensor_mn_view(acc_O)
157
+ assert cute.size(row_scale) == cute.size(acc_O_mn, mode=[0])
158
+ for r in cutlass.range(cute.size(row_scale), unroll_full=True):
159
+ acc_O_mn[r, None].store(acc_O_mn[r, None].load() * row_scale[r])
160
+
161
+
162
+ @dataclass
163
+ class SoftmaxSm100(Softmax):
164
+ rescale_threshold: cutlass.Constexpr[float] = 0.0
165
+
166
+ @staticmethod
167
+ def create(
168
+ scale_log2: Float32,
169
+ rescale_threshold: cutlass.Constexpr[float] = 0.0,
170
+ softmax_scale: Float32 | None = None,
171
+ ):
172
+ num_rows = 1
173
+ arch = 100
174
+ row_max = cute.make_rmem_tensor(num_rows, Float32)
175
+ row_sum = cute.make_rmem_tensor(num_rows, Float32)
176
+ return SoftmaxSm100(
177
+ scale_log2,
178
+ num_rows,
179
+ row_max,
180
+ row_sum,
181
+ arch,
182
+ softmax_scale,
183
+ rescale_threshold=rescale_threshold,
184
+ )
185
+
186
+ @cute.jit
187
+ def update_row_max(self, acc_S_row: cute.TensorSSA, is_first: int) -> Tuple[Float32, Float32]:
188
+ if cutlass.const_expr(is_first):
189
+ row_max_new = self._compute_row_max(acc_S_row)
190
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
191
+ acc_scale = 0.0
192
+ else:
193
+ row_max_old = self.row_max[0]
194
+ row_max_new = self._compute_row_max(acc_S_row, init_val=row_max_old)
195
+ row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
196
+ acc_scale_ = (row_max_old - row_max_safe) * self.scale_log2
197
+ acc_scale = utils.exp2f(acc_scale_)
198
+ if cutlass.const_expr(self.rescale_threshold > 0.0):
199
+ if acc_scale_ >= -self.rescale_threshold:
200
+ row_max_new = row_max_old
201
+ row_max_safe = row_max_old
202
+ acc_scale = 1.0
203
+ self.row_max[0] = row_max_new
204
+ return row_max_safe, acc_scale
205
+
206
+ def update_row_sum(
207
+ self, acc_S_row_exp: cute.TensorSSA, row_scale: Float32, is_first: int = False
208
+ ) -> None:
209
+ init_val = self.row_sum[0] * row_scale if cutlass.const_expr(not is_first) else None
210
+ # self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=self.row_sum[0] * row_scale)
211
+ self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=init_val)
212
+ # tmp = self._compute_row_sum(acc_S_row_exp)
213
+ # self.row_sum[0] = self.row_sum[0] * row_scale + tmp
214
+
215
+ @cute.jit
216
+ def scale_subtract_rowmax(
217
+ self,
218
+ acc_S_row: cute.Tensor,
219
+ row_max: Float32,
220
+ ):
221
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
222
+ row_max_scaled = row_max * self.scale_log2
223
+ for i in cutlass.range(0, cute.size(acc_S_row.shape), 2, unroll_full=True):
224
+ acc_S_row[i], acc_S_row[i + 1] = utils.fma_packed_f32x2(
225
+ (acc_S_row[i], acc_S_row[i + 1]),
226
+ (self.scale_log2, self.scale_log2),
227
+ (-row_max_scaled, -row_max_scaled),
228
+ )
229
+
230
+ @cute.jit
231
+ def apply_exp2_convert(
232
+ self,
233
+ acc_S_row: cute.Tensor,
234
+ acc_S_row_converted: cute.Tensor,
235
+ e2e: cutlass.Constexpr[bool] = False,
236
+ e2e_freq: cutlass.Constexpr[int] = 16,
237
+ e2e_res: cutlass.Constexpr[int] = 4,
238
+ e2e_frg_limit: cutlass.Constexpr[int] = 1,
239
+ ):
240
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
241
+ frg_tile = 32
242
+ assert frg_tile % 2 == 0
243
+ frg_cnt = cute.size(acc_S_row) // frg_tile
244
+ assert cute.size(acc_S_row) % frg_tile == 0
245
+ acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
246
+ acc_S_row_converted_frg = cute.logical_divide(
247
+ acc_S_row_converted, cute.make_layout(frg_tile)
248
+ )
249
+ for j in cutlass.range_constexpr(frg_cnt):
250
+ for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
251
+ # acc_S_row_frg[k, j] = utils.exp2f(acc_S_row_frg[k, j])
252
+ # acc_S_row_frg[k + 1, j] = utils.exp2f(acc_S_row_frg[k + 1, j])
253
+ if cutlass.const_expr(not e2e):
254
+ acc_S_row_frg[k, j] = cute.arch.exp2(acc_S_row_frg[k, j])
255
+ acc_S_row_frg[k + 1, j] = cute.arch.exp2(acc_S_row_frg[k + 1, j])
256
+ else:
257
+ if cutlass.const_expr(
258
+ k % e2e_freq < e2e_freq - e2e_res or j >= frg_cnt - e2e_frg_limit
259
+ ):
260
+ acc_S_row_frg[k, j] = cute.arch.exp2(acc_S_row_frg[k, j])
261
+ acc_S_row_frg[k + 1, j] = cute.arch.exp2(acc_S_row_frg[k + 1, j])
262
+ else:
263
+ # 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])
264
+ acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = utils.ex2_emulation_2(
265
+ acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]
266
+ )
267
+ acc_S_row_converted_frg[None, j].store(
268
+ acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
269
+ )
270
+
271
+ @cute.jit
272
+ def scale_apply_exp2_convert(
273
+ self,
274
+ acc_S_row: cute.Tensor,
275
+ row_max: Float32,
276
+ acc_S_row_converted: cute.Tensor,
277
+ ):
278
+ assert cute.size(acc_S_row.shape) % 2 == 0, "acc_S_row must have an even number of elements"
279
+ minus_row_max_scaled = -row_max * self.scale_log2
280
+ for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
281
+ acc_S_row[i], acc_S_row[i + 1] = utils.fma_packed_f32x2(
282
+ (acc_S_row[i], acc_S_row[i + 1]),
283
+ (self.scale_log2, self.scale_log2),
284
+ (minus_row_max_scaled, minus_row_max_scaled),
285
+ )
286
+
287
+ # for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
288
+ # acc_S_row[i], acc_S_row[i + 1] = utils.fma_packed_f32x2(
289
+ # (acc_S_row[i], acc_S_row[i + 1]),
290
+ # (self.scale_log2, self.scale_log2),
291
+ # (minus_row_max_scaled, minus_row_max_scaled),
292
+ # )
293
+ # acc_S_row[i] = cute.arch.exp2(acc_S_row[i])
294
+ # acc_S_row[i + 1] = cute.arch.exp2(acc_S_row[i + 1])
295
+
296
+ frg_tile = 32
297
+ assert frg_tile % 2 == 0
298
+ frg_cnt = cute.size(acc_S_row) // frg_tile
299
+ assert cute.size(acc_S_row) % frg_tile == 0
300
+ acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
301
+ acc_S_row_converted_frg = cute.logical_divide(
302
+ acc_S_row_converted, cute.make_layout(frg_tile)
303
+ )
304
+ for j in cutlass.range_constexpr(frg_cnt):
305
+ for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
306
+ # acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = (
307
+ # utils.fma_packed_f32x2(
308
+ # (acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]),
309
+ # (self.scale_log2, self.scale_log2),
310
+ # (minus_row_max_scaled, minus_row_max_scaled),
311
+ # )
312
+ # )
313
+ # acc_S_row_frg[k, j] = utils.exp2f(acc_S_row_frg[k, j])
314
+ # acc_S_row_frg[k + 1, j] = utils.exp2f(acc_S_row_frg[k + 1, j])
315
+ acc_S_row_frg[k, j] = cute.arch.exp2(acc_S_row_frg[k, j])
316
+ acc_S_row_frg[k + 1, j] = cute.arch.exp2(acc_S_row_frg[k + 1, j])
317
+ acc_S_row_converted_frg[None, j].store(
318
+ acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
319
+ )
320
+
321
+
322
+ @cute.jit
323
+ def floor_if_packed(
324
+ q_idx,
325
+ qhead_per_kvhead: cutlass.Constexpr[int],
326
+ ) -> cute.Tensor:
327
+ """Convert q_idx to packed format for Pack-GQA."""
328
+ if cutlass.const_expr(qhead_per_kvhead == 1):
329
+ return q_idx
330
+ return q_idx // qhead_per_kvhead
331
+
332
+
333
+ @cute.jit
334
+ def apply_score_mod_inner(
335
+ score_tensor,
336
+ index_tensor,
337
+ score_mod: cutlass.Constexpr,
338
+ batch_idx,
339
+ head_idx,
340
+ softmax_scale,
341
+ vec_size: cutlass.Constexpr,
342
+ qk_acc_dtype: cutlass.Constexpr,
343
+ aux_tensors,
344
+ fastdiv_mods,
345
+ seqlen_info: SeqlenInfoQK,
346
+ constant_q_idx: cutlass.Constexpr,
347
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
348
+ transpose_indices: cutlass.Constexpr[bool] = False,
349
+ ):
350
+ """Shared implementation for applying score modification.
351
+
352
+ Args:
353
+ score_tensor: The scores to modify (acc_S for flash_fwd, tSrS_t2r for sm100)
354
+ index_tensor: Index positions (tScS for flash_fwd, tScS_t2r for sm100)
355
+ score_mod: The score modification function to apply
356
+ batch_idx: Batch index
357
+ head_idx: Head index
358
+ softmax_scale: Scale to apply
359
+ vec_size: Vector size for processing elements
360
+ qk_acc_dtype: Data type for accumulator
361
+ aux_tensors: Optional aux_tensors for FlexAttention
362
+ fastdiv_mods: Tuple of (seqlen_q_divmod, seqlen_k_divmod) for wrapping
363
+ seqlen_info: Sequence length info
364
+ constant_q_idx: If provided, use this constant for all q_idx values
365
+ If None, compute q_idx per-element
366
+ qhead_per_kvhead_packgqa: Pack-GQA replication factor. Divide q_idx by this
367
+ when greater than 1 so score mods see logical heads.
368
+ transpose_indices: If True, swap q_idx/kv_idx in index_tensor (for bwd kernel where S is transposed)
369
+ """
370
+ # Index positions in the index_tensor tuple
371
+ # Forward: index_tensor[...][0] = q_idx, index_tensor[...][1] = kv_idx
372
+ # Backward (transposed): index_tensor[...][0] = kv_idx, index_tensor[...][1] = q_idx
373
+ if cutlass.const_expr(transpose_indices):
374
+ q_idx_pos = cutlass.const_expr(1)
375
+ kv_idx_pos = cutlass.const_expr(0)
376
+ else:
377
+ q_idx_pos = cutlass.const_expr(0)
378
+ kv_idx_pos = cutlass.const_expr(1)
379
+
380
+ n_vals = cutlass.const_expr(cute.size(score_tensor.shape))
381
+ score_vec = cute.make_rmem_tensor(vec_size, qk_acc_dtype)
382
+ kv_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
383
+
384
+ # SSA values for batch (constant across all elements)
385
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32).broadcast_to((vec_size,))
386
+
387
+ # Handle q_idx based on whether it's constant
388
+ q_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
389
+
390
+ # For Pack-GQA with non-constant q_idx, we need per-element head indices
391
+ # since a thread my process multiple query head indices
392
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
393
+ head_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
394
+
395
+ for i in cutlass.range(0, n_vals, vec_size, unroll_full=True):
396
+ for j in cutlass.range(vec_size, unroll_full=True):
397
+ score_vec[j] = score_tensor[i + j] * softmax_scale
398
+
399
+ # Extract head offset from packed q_idx for Pack-GQA
400
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
401
+ q_idx_packed = index_tensor[i + j][q_idx_pos]
402
+ # Building up the logical q_head idx: final_q_head = kv_head * qhead_per_kvhead + (q_physical % qhead_per_kvhead)
403
+ q_idx_logical = q_idx_packed // qhead_per_kvhead
404
+ head_offset = q_idx_packed - q_idx_logical * qhead_per_kvhead
405
+ head_idx_vec[j] = head_idx * qhead_per_kvhead + head_offset
406
+
407
+ # If we will do loads we mod, in order to not read OOB
408
+ if cutlass.const_expr(aux_tensors is not None and fastdiv_mods is not None):
409
+ if cutlass.const_expr(constant_q_idx is None):
410
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
411
+ q_idx_floored = floor_if_packed(
412
+ index_tensor[i + j][q_idx_pos], qhead_per_kvhead
413
+ )
414
+ _, q_idx_wrapped = divmod(q_idx_floored, seqlen_q_divmod)
415
+ q_idx_vec[j] = q_idx_wrapped
416
+ else:
417
+ _, seqlen_k_divmod = fastdiv_mods
418
+
419
+ _, kv_idx_wrapped = divmod(index_tensor[i + j][kv_idx_pos], seqlen_k_divmod)
420
+ kv_idx_vec[j] = kv_idx_wrapped
421
+ else:
422
+ # No bounds checking - direct indexing
423
+ if constant_q_idx is None:
424
+ q_idx_vec[j] = floor_if_packed(index_tensor[i + j][q_idx_pos], qhead_per_kvhead)
425
+ kv_idx_vec[j] = index_tensor[i + j][kv_idx_pos]
426
+
427
+ # Convert to SSA for score_mod call
428
+ score_ssa = score_vec.load()
429
+ kv_idx_ssa = kv_idx_vec.load()
430
+ if cutlass.const_expr(constant_q_idx is None):
431
+ q_idx_ssa = q_idx_vec.load()
432
+ else:
433
+ # NB we do not apply Pack-GQA division here, as constant_q_idx is assumed to already be logical
434
+ q_idx_const = constant_q_idx
435
+ q_idx_ssa = utils.scalar_to_ssa(q_idx_const, cutlass.Int32).broadcast_to((vec_size,))
436
+
437
+ # Compute head_idx_ssa: per-element for Pack-GQA with non-constant q_idx, constant otherwise
438
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
439
+ head_idx_ssa = head_idx_vec.load()
440
+ else:
441
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32).broadcast_to((vec_size,))
442
+
443
+ aux_args = []
444
+ if cutlass.const_expr(aux_tensors is not None):
445
+ aux_args = aux_tensors
446
+
447
+ post_mod_scores = score_mod(
448
+ score_ssa,
449
+ batch_idx_ssa,
450
+ head_idx_ssa,
451
+ q_idx=q_idx_ssa,
452
+ kv_idx=kv_idx_ssa,
453
+ seqlen_info=seqlen_info,
454
+ aux_tensors=aux_args,
455
+ )
456
+
457
+ # Write back modified scores
458
+ score_vec.store(post_mod_scores)
459
+ for j in cutlass.range(vec_size, unroll_full=True):
460
+ score_tensor[i + j] = score_vec[j]
461
+
462
+
463
+ @cute.jit
464
+ def apply_score_mod_bwd_inner(
465
+ grad_tensor,
466
+ score_tensor,
467
+ index_tensor,
468
+ score_mod_bwd: cutlass.Constexpr,
469
+ batch_idx,
470
+ head_idx,
471
+ softmax_scale,
472
+ vec_size: cutlass.Constexpr,
473
+ qk_acc_dtype: cutlass.Constexpr,
474
+ aux_tensors,
475
+ fastdiv_mods,
476
+ seqlen_info,
477
+ constant_q_idx: cutlass.Constexpr,
478
+ qhead_per_kvhead: cutlass.Constexpr[int] = 1,
479
+ transpose_indices: cutlass.Constexpr[bool] = False,
480
+ ):
481
+ """Apply backward score modification (joint graph).
482
+
483
+ Args:
484
+ grad_tensor: in/out: dlogits rewritten in-place with d(scaled_scores)
485
+ score_tensor: pre-mod scores (unscaled QK tile), scaled by softmax_scale internally
486
+ index_tensor: Index positions (same as forward)
487
+ score_mod_bwd: The backward score modification function (joint graph)
488
+ batch_idx: Batch index
489
+ head_idx: Head index
490
+ softmax_scale: Scale to apply to score_tensor
491
+ vec_size: Vector size for processing elements
492
+ qk_acc_dtype: Data type for accumulator
493
+ aux_tensors: Optional aux_tensors for FlexAttention
494
+ fastdiv_mods: Tuple of (seqlen_q_divmod, seqlen_k_divmod) for wrapping
495
+ seqlen_info: Sequence length info
496
+ constant_q_idx: If provided, use this constant for all q_idx values
497
+ qhead_per_kvhead: Pack-GQA replication factor
498
+ transpose_indices: If True, swap q_idx/kv_idx in index_tensor
499
+ """
500
+ # Index positions in the index_tensor tuple
501
+ # Forward: index_tensor[...][0] = q_idx, index_tensor[...][1] = kv_idx
502
+ # Backward (transposed): index_tensor[...][0] = kv_idx, index_tensor[...][1] = q_idx
503
+ if cutlass.const_expr(transpose_indices):
504
+ q_idx_pos = cutlass.const_expr(1)
505
+ kv_idx_pos = cutlass.const_expr(0)
506
+ else:
507
+ q_idx_pos = cutlass.const_expr(0)
508
+ kv_idx_pos = cutlass.const_expr(1)
509
+ n_vals = cutlass.const_expr(cute.size(grad_tensor.shape))
510
+ grad_vec = cute.make_fragment(vec_size, qk_acc_dtype)
511
+ score_vec = cute.make_fragment(vec_size, qk_acc_dtype)
512
+ kv_idx_vec = cute.make_fragment(vec_size, cutlass.Int32)
513
+ batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32).broadcast_to((vec_size,))
514
+ q_idx_vec = cute.make_fragment(vec_size, cutlass.Int32)
515
+
516
+ # For Pack-GQA with non-constant q_idx, we need per-element head indices
517
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
518
+ head_idx_vec = cute.make_fragment(vec_size, cutlass.Int32)
519
+
520
+ for i in cutlass.range(0, n_vals, vec_size, unroll_full=True):
521
+ for j in cutlass.range(vec_size, unroll_full=True):
522
+ grad_vec[j] = grad_tensor[i + j]
523
+ # Scale score so joint graph sees same value as forward score_mod
524
+ score_vec[j] = score_tensor[i + j] * softmax_scale
525
+
526
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
527
+ q_idx_packed = index_tensor[i + j][q_idx_pos]
528
+ q_idx_logical = q_idx_packed // qhead_per_kvhead
529
+ head_offset = q_idx_packed - q_idx_logical * qhead_per_kvhead
530
+ head_idx_vec[j] = head_idx * qhead_per_kvhead + head_offset
531
+
532
+ if cutlass.const_expr(aux_tensors is not None and fastdiv_mods is not None):
533
+ if cutlass.const_expr(constant_q_idx is None):
534
+ seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
535
+ q_idx_floored = floor_if_packed(
536
+ index_tensor[i + j][q_idx_pos], qhead_per_kvhead
537
+ )
538
+ _, q_idx_wrapped = divmod(q_idx_floored, seqlen_q_divmod)
539
+ q_idx_vec[j] = q_idx_wrapped
540
+ else:
541
+ _, seqlen_k_divmod = fastdiv_mods
542
+
543
+ _, kv_idx_wrapped = divmod(index_tensor[i + j][kv_idx_pos], seqlen_k_divmod)
544
+ kv_idx_vec[j] = kv_idx_wrapped
545
+ else:
546
+ # No bounds checking - direct indexing
547
+ if constant_q_idx is None:
548
+ q_idx_vec[j] = floor_if_packed(index_tensor[i + j][q_idx_pos], qhead_per_kvhead)
549
+ kv_idx_vec[j] = index_tensor[i + j][kv_idx_pos]
550
+
551
+ grad_ssa = grad_vec.load()
552
+ score_ssa = score_vec.load()
553
+ kv_idx_ssa = kv_idx_vec.load()
554
+
555
+ if cutlass.const_expr(constant_q_idx is None):
556
+ q_idx_ssa = q_idx_vec.load()
557
+ else:
558
+ q_idx_ssa = utils.scalar_to_ssa(constant_q_idx, cutlass.Int32).broadcast_to((vec_size,))
559
+
560
+ if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
561
+ head_idx_ssa = head_idx_vec.load()
562
+ else:
563
+ head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32).broadcast_to((vec_size,))
564
+
565
+ aux_args = []
566
+ if cutlass.const_expr(aux_tensors is not None):
567
+ aux_args = aux_tensors
568
+
569
+ grad_out_ssa = score_mod_bwd(
570
+ grad_ssa,
571
+ score_ssa,
572
+ batch_idx_ssa,
573
+ head_idx_ssa,
574
+ q_idx=q_idx_ssa,
575
+ kv_idx=kv_idx_ssa,
576
+ seqlen_info=seqlen_info,
577
+ aux_tensors=aux_args,
578
+ )
579
+
580
+ grad_vec.store(grad_out_ssa)
581
+ for j in cutlass.range(vec_size, unroll_full=True):
582
+ grad_tensor[i + j] = grad_vec[j]
kernel/deps/flash_attn/cute/tile_scheduler.py ADDED
@@ -0,0 +1,719 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ from typing import Optional, Tuple
4
+ from dataclasses import dataclass, fields
5
+
6
+ try:
7
+ from typing import override
8
+ except ImportError: # Python < 3.12
9
+ from typing_extensions import override
10
+
11
+ import cutlass
12
+ from cutlass._mlir import ir
13
+ import cutlass.cute as cute
14
+ from cutlass import Int32, const_expr
15
+
16
+ import flash_attn.cute.utils as utils
17
+ from flash_attn.cute.fast_math import clz
18
+ from cutlass.cute import FastDivmodDivisor
19
+
20
+
21
+ class WorkTileInfo(cutlass.utils.WorkTileInfo):
22
+ """Altered WorkTileInfo which includes four axes: (block, head, batch, split)"""
23
+
24
+ @override
25
+ def __new_from_mlir_values__(self, values: list[ir.Value]) -> "WorkTileInfo":
26
+ assert len(values) == 5
27
+ new_tile_idx = cutlass.new_from_mlir_values(self._tile_idx, values[:-1])
28
+ new_is_valid_tile = cutlass.new_from_mlir_values(self._is_valid_tile, [values[-1]])
29
+ return WorkTileInfo(new_tile_idx, new_is_valid_tile)
30
+
31
+
32
+ @dataclass
33
+ class ParamsBase:
34
+ def __extract_mlir_values__(self):
35
+ all_fields = [getattr(self, field.name) for field in fields(self)]
36
+ non_constexpr_fields = [f for f in all_fields if not isinstance(f, cutlass.Constexpr)]
37
+ values, self._values_pos = [], []
38
+ for obj in non_constexpr_fields:
39
+ obj_values = cutlass.extract_mlir_values(obj)
40
+ values += obj_values
41
+ self._values_pos.append(len(obj_values))
42
+ return values
43
+
44
+ def __new_from_mlir_values__(self, values):
45
+ all_fields = {field.name: getattr(self, field.name) for field in fields(self)}
46
+ constexpr_fields = {n: f for n, f in all_fields.items() if isinstance(f, cutlass.Constexpr)}
47
+ non_constexpr_fields = {
48
+ n: f for n, f in all_fields.items() if not isinstance(f, cutlass.Constexpr)
49
+ }
50
+ for (name, field), n_items in zip(non_constexpr_fields.items(), self._values_pos):
51
+ non_constexpr_fields[name] = cutlass.new_from_mlir_values(field, values[:n_items])
52
+ values = values[n_items:]
53
+ return self.__class__(**non_constexpr_fields, **constexpr_fields)
54
+
55
+
56
+ @dataclass
57
+ class TileSchedulerArguments(ParamsBase):
58
+ num_block: Int32
59
+ num_head: Int32
60
+ num_batch: Int32
61
+ num_splits: Int32
62
+ seqlen_k: Int32
63
+ headdim: Int32
64
+ headdim_v: Int32
65
+ total_q: Int32
66
+ tile_shape_mn: cutlass.Constexpr[Tuple[int, int]]
67
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
68
+ mCuSeqlensQ: Optional[cute.Tensor] = None
69
+ mSeqUsedQ: Optional[cute.Tensor] = None
70
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
71
+ element_size: cutlass.Constexpr[int] = 2
72
+ is_persistent: cutlass.Constexpr[bool] = False
73
+ lpt: cutlass.Constexpr[bool] = False
74
+ is_split_kv: cutlass.Constexpr[bool] = False
75
+ head_swizzle: cutlass.Constexpr[bool] = False
76
+
77
+
78
+ class SingleTileScheduler:
79
+ @dataclass
80
+ class Params(ParamsBase):
81
+ num_block: Int32
82
+ num_head: Int32
83
+ num_batch: Int32
84
+ num_splits: Int32
85
+ num_splits_divmod: FastDivmodDivisor
86
+ is_split_kv: cutlass.Constexpr[bool] = False
87
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
88
+
89
+ @staticmethod
90
+ def create(
91
+ args: TileSchedulerArguments, *, loc=None, ip=None
92
+ ) -> "SingleTileScheduler.Params":
93
+ return SingleTileScheduler.Params(
94
+ args.num_block,
95
+ args.num_head,
96
+ args.num_batch,
97
+ args.num_splits,
98
+ FastDivmodDivisor(args.num_splits),
99
+ args.is_split_kv,
100
+ args.cluster_shape_mn,
101
+ )
102
+
103
+ def __init__(self, params: Params, blk_coord: cute.Coord, *, loc=None, ip=None):
104
+ self.params = params
105
+ self._blk_coord = blk_coord
106
+ self._is_first_block = True
107
+ self._loc = loc
108
+ self._ip = ip
109
+
110
+ @staticmethod
111
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
112
+ return SingleTileScheduler.Params.create(args, loc=loc, ip=ip)
113
+
114
+ @staticmethod
115
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileScheduler":
116
+ blk_coord = cute.arch.block_idx()
117
+ return SingleTileScheduler(params, blk_coord, loc=loc, ip=ip)
118
+
119
+ # called by host
120
+ @staticmethod
121
+ def get_grid_shape(
122
+ params: Params,
123
+ *,
124
+ loc=None,
125
+ ip=None,
126
+ ) -> Tuple[Int32, Int32, Int32]:
127
+ # TODO: this hard-codes the fact that we only use cluster = (1, 1) or (2, 1)
128
+ assert params.cluster_shape_mn[1] == 1, "Only cluster_shape_mn[1] == 1 is supported"
129
+ return (
130
+ cute.round_up(params.num_block, params.cluster_shape_mn[0]),
131
+ params.num_head * params.num_splits,
132
+ params.num_batch,
133
+ )
134
+
135
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
136
+ block_idx, head_idx, batch_idx = self._blk_coord
137
+ if const_expr(self.params.is_split_kv):
138
+ head_idx, split_idx = divmod(head_idx, self.params.num_splits_divmod)
139
+ else:
140
+ split_idx = Int32(0)
141
+ return WorkTileInfo(
142
+ (block_idx, head_idx, batch_idx, split_idx),
143
+ self._is_first_block,
144
+ )
145
+
146
+ def initial_work_tile_info(self, *, loc=None, ip=None):
147
+ return self.get_current_work(loc=loc, ip=ip)
148
+
149
+ def prefetch_next_work(self, *, loc=None, ip=None):
150
+ pass
151
+
152
+ def advance_to_next_work(self, *, loc=None, ip=None):
153
+ self._is_first_block = False
154
+
155
+ def __extract_mlir_values__(self):
156
+ values, self._values_pos = [], []
157
+ for obj in [self.params, self._blk_coord]:
158
+ obj_values = cutlass.extract_mlir_values(obj)
159
+ values += obj_values
160
+ self._values_pos.append(len(obj_values))
161
+ return values
162
+
163
+ def __new_from_mlir_values__(self, values):
164
+ obj_list = []
165
+ for obj, n_items in zip([self.params, self._blk_coord], self._values_pos):
166
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
167
+ values = values[n_items:]
168
+ return SingleTileScheduler(*(tuple(obj_list)), loc=self._loc)
169
+
170
+
171
+ class StaticPersistentTileScheduler:
172
+ @dataclass
173
+ class Params(ParamsBase):
174
+ num_block_divmod: FastDivmodDivisor
175
+ num_head_divmod: FastDivmodDivisor
176
+ total_blocks: Int32
177
+
178
+ @staticmethod
179
+ def create(
180
+ args: TileSchedulerArguments, *, loc=None, ip=None
181
+ ) -> "StaticPersistentTileScheduler.Params":
182
+ total_blocks = args.num_block * args.num_head * args.num_batch
183
+ return StaticPersistentTileScheduler.Params(
184
+ FastDivmodDivisor(args.num_block), FastDivmodDivisor(args.num_head), total_blocks
185
+ )
186
+
187
+ def __init__(self, params: Params, tile_idx: Int32, *, loc=None, ip=None):
188
+ self.params = params
189
+ self._tile_idx = tile_idx
190
+ self._loc = loc
191
+ self._ip = ip
192
+
193
+ @staticmethod
194
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
195
+ return StaticPersistentTileScheduler.Params.create(args, loc=loc, ip=ip)
196
+
197
+ @staticmethod
198
+ def create(params: Params, *, loc=None, ip=None) -> "StaticPersistentTileScheduler":
199
+ tile_idx = cute.arch.block_idx()[0]
200
+ return StaticPersistentTileScheduler(params, tile_idx, loc=loc, ip=ip)
201
+
202
+ # called by host
203
+ @staticmethod
204
+ def get_grid_shape(
205
+ params: Params,
206
+ *,
207
+ loc=None,
208
+ ip=None,
209
+ ) -> Tuple[Int32, Int32, Int32]:
210
+ hardware_info = cutlass.utils.HardwareInfo()
211
+ sm_count = hardware_info.get_device_multiprocessor_count()
212
+ return (cutlass.min(sm_count, params.total_blocks), Int32(1), Int32(1))
213
+
214
+ # @cute.jit
215
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
216
+ hn_idx, block_idx = divmod(self._tile_idx, self.params.num_block_divmod)
217
+ batch_idx, head_idx = divmod(hn_idx, self.params.num_head_divmod)
218
+ is_valid = self._tile_idx < self.params.total_blocks
219
+ # if cute.arch.thread_idx()[0] == 0:
220
+ # cute.printf("TileScheduler: tile_idx=%d, hn_idx=%d, block_idx=%d, batch_idx=%d, head_idx=%d, is_valid=%d", self._tile_idx, hn_idx, block_idx, batch_idx, head_idx, is_valid)
221
+ return WorkTileInfo(
222
+ (Int32(block_idx), Int32(head_idx), Int32(batch_idx), Int32(0)), is_valid
223
+ )
224
+
225
+ def initial_work_tile_info(self, *, loc=None, ip=None):
226
+ return self.get_current_work(loc=loc, ip=ip)
227
+
228
+ def prefetch_next_work(self, *, loc=None, ip=None):
229
+ pass
230
+
231
+ def advance_to_next_work(self, *, loc=None, ip=None):
232
+ self._tile_idx += cute.arch.grid_dim()[0]
233
+
234
+ def __extract_mlir_values__(self):
235
+ values, self._values_pos = [], []
236
+ for obj in [self.params, self._tile_idx]:
237
+ obj_values = cutlass.extract_mlir_values(obj)
238
+ values += obj_values
239
+ self._values_pos.append(len(obj_values))
240
+ return values
241
+
242
+ def __new_from_mlir_values__(self, values):
243
+ obj_list = []
244
+ for obj, n_items in zip(
245
+ [self.params, self._tile_idx],
246
+ self._values_pos,
247
+ ):
248
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
249
+ values = values[n_items:]
250
+ return StaticPersistentTileScheduler(*(tuple(obj_list)), loc=self._loc)
251
+
252
+
253
+ class SingleTileLPTScheduler:
254
+ @dataclass
255
+ class Params(ParamsBase):
256
+ total_blocks: Int32
257
+ num_splits: Int32
258
+ num_block: Int32
259
+ l2_minor: Int32
260
+ num_block_divmod: FastDivmodDivisor
261
+ num_head_divmod: FastDivmodDivisor
262
+ l2_minor_divmod: FastDivmodDivisor
263
+ l2_major_divmod: FastDivmodDivisor
264
+ l2_minor_residual_divmod: FastDivmodDivisor
265
+ num_hb_quotient: Int32
266
+ is_split_kv: cutlass.Constexpr[bool] = False
267
+
268
+ @staticmethod
269
+ @cute.jit
270
+ def create(
271
+ args: TileSchedulerArguments, *, loc=None, ip=None
272
+ ) -> "SingleTileLPTScheduler.Params":
273
+ # cute.printf(args.num_block, args.num_head, args.num_batch, args.seqlen_k, args.headdim, args.headdim_v, args.total_q, args.tile_shape_mn, args.qhead_per_kvhead_packgqa, args.element_size)
274
+ size_one_kv_head = args.seqlen_k * (args.headdim + args.headdim_v) * args.element_size
275
+ size_one_head = size_one_kv_head
276
+ size_l2 = 50 * 1024 * 1024 # 40 MB for K & V
277
+ # Swizzle is the size of each "section". Round swizzle to a power of 2
278
+ # Need to be careful about the case where only one head will fit
279
+ # swizzle is how many heads can fit in L2
280
+ # swizzle = 1 if size_l2 < size_one_head else (size_l2 // size_one_head)
281
+ # Seems faster if swizzle if a power of 2
282
+ log2_floor = lambda n: 31 - clz(n)
283
+ swizzle = 1 if size_l2 < size_one_head else (1 << log2_floor(size_l2 // size_one_head))
284
+ # swizzle = 1 if size_l2 < size_one_head else (size_l2 // size_one_head)
285
+ # If we're in the last section (called residual), we don't want to divide by
286
+ # swizzle. Instead we want to divide by the remainder.
287
+ num_hb_quotient = (args.num_head * args.num_batch) // swizzle
288
+ num_hb_remainder = (args.num_head * args.num_batch) % swizzle
289
+ return SingleTileLPTScheduler.Params(
290
+ total_blocks=args.num_block * args.num_head * args.num_batch,
291
+ num_block=args.num_block,
292
+ l2_minor=Int32(swizzle),
293
+ num_block_divmod=FastDivmodDivisor(args.num_block),
294
+ num_head_divmod=FastDivmodDivisor(args.num_head),
295
+ l2_minor_divmod=FastDivmodDivisor(swizzle),
296
+ l2_major_divmod=FastDivmodDivisor(swizzle * args.num_block),
297
+ l2_minor_residual_divmod=FastDivmodDivisor(
298
+ max(num_hb_remainder, 1)
299
+ ), # don't divide by 0
300
+ num_hb_quotient=Int32(num_hb_quotient),
301
+ num_splits=args.num_splits,
302
+ is_split_kv=args.is_split_kv,
303
+ )
304
+
305
+ def __init__(self, params: Params, tile_idx: Int32, split_idx: Int32, *, loc=None, ip=None):
306
+ self.params = params
307
+ self._tile_idx = tile_idx
308
+ self._split_idx = split_idx
309
+ self._loc = loc
310
+ self._ip = ip
311
+
312
+ @staticmethod
313
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
314
+ return SingleTileLPTScheduler.Params.create(args, loc=loc, ip=ip)
315
+
316
+ @staticmethod
317
+ @cute.jit
318
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileLPTScheduler":
319
+ tile_idx, split_idx, _ = cute.arch.block_idx()
320
+ return SingleTileLPTScheduler(params, tile_idx, split_idx, loc=loc, ip=ip)
321
+
322
+ # called by host
323
+ @staticmethod
324
+ def get_grid_shape(
325
+ params: Params,
326
+ *,
327
+ loc=None,
328
+ ip=None,
329
+ ) -> Tuple[Int32, Int32, Int32]:
330
+ return (params.total_blocks, params.num_splits, Int32(1))
331
+
332
+ @cute.jit
333
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
334
+ params = self.params
335
+ # Implement LPT scheduling coordinate calculation
336
+ bidhb, l2_mod = divmod(self._tile_idx, params.l2_major_divmod)
337
+ # If we're in the last section (called residual), we don't want to divide by
338
+ # swizzle. Instead we want to divide by the remainder.
339
+ block, bidhb_residual = 0, 0
340
+ if bidhb < params.num_hb_quotient:
341
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_divmod)
342
+ else:
343
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_residual_divmod)
344
+ bidhb_actual = bidhb * params.l2_minor + bidhb_residual
345
+ batch_idx, head_idx = divmod(bidhb_actual, params.num_head_divmod)
346
+ # Longest-processing-time-first
347
+ block = params.num_block - 1 - block
348
+ is_valid = self._tile_idx < params.total_blocks
349
+ return WorkTileInfo(
350
+ (Int32(block), Int32(head_idx), Int32(batch_idx), Int32(self._split_idx)), is_valid
351
+ )
352
+
353
+ def initial_work_tile_info(self, *, loc=None, ip=None):
354
+ return self.get_current_work(loc=loc, ip=ip)
355
+
356
+ def prefetch_next_work(self, *, loc=None, ip=None):
357
+ pass
358
+
359
+ def advance_to_next_work(self, *, loc=None, ip=None):
360
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
361
+ self._tile_idx = self.params.total_blocks
362
+
363
+ def __extract_mlir_values__(self):
364
+ values, self._values_pos = [], []
365
+ for obj in [self.params, self._tile_idx, self._split_idx]:
366
+ obj_values = cutlass.extract_mlir_values(obj)
367
+ values += obj_values
368
+ self._values_pos.append(len(obj_values))
369
+ return values
370
+
371
+ def __new_from_mlir_values__(self, values):
372
+ obj_list = []
373
+ for obj, n_items in zip([self.params, self._tile_idx, self._split_idx], self._values_pos):
374
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
375
+ values = values[n_items:]
376
+ return self.__class__(*(tuple(obj_list)), loc=self._loc)
377
+
378
+
379
+ class SingleTileLPTBwdScheduler:
380
+ @dataclass
381
+ class Params(ParamsBase):
382
+ total_blocks: Int32
383
+ num_block: Int32
384
+ l2_minor: Int32
385
+ num_head_divmod: FastDivmodDivisor
386
+ l2_minor_divmod: FastDivmodDivisor
387
+ l2_major_divmod: FastDivmodDivisor
388
+ l2_minor_residual_divmod: FastDivmodDivisor
389
+ num_hb_quotient: Int32
390
+ cluster_shape_mn: cutlass.Constexpr[Tuple[int, int]] = (1, 1)
391
+ spt: cutlass.Constexpr[bool] = True
392
+
393
+ @staticmethod
394
+ @cute.jit
395
+ def create(
396
+ args: TileSchedulerArguments, *, loc=None, ip=None
397
+ ) -> "SingleTileLPTBwdScheduler.Params":
398
+ size_l2 = 50 * 1024 * 1024
399
+ size_one_qdo_head = args.seqlen_k * (args.headdim + args.headdim_v) * args.element_size
400
+ # size_one_dqaccum_head = args.seqlen_k * (args.headdim) * 4
401
+ size_one_dqaccum_head = 0
402
+ size_one_head = size_one_qdo_head + size_one_dqaccum_head
403
+ log2_floor = lambda n: 31 - clz(n)
404
+ swizzle = 1 if size_l2 < size_one_head else (1 << log2_floor(size_l2 // size_one_head))
405
+ # swizzle = 8
406
+ # If we're in the last section (called residual), we don't want to divide by
407
+ # swizzle. Instead we want to divide by the remainder.
408
+ num_hb_quotient = (args.num_head * args.num_batch) // swizzle
409
+ num_hb_remainder = (args.num_head * args.num_batch) % swizzle
410
+ num_block = cute.ceil_div(args.num_block, args.cluster_shape_mn[0])
411
+ return SingleTileLPTBwdScheduler.Params(
412
+ total_blocks=(num_block * args.cluster_shape_mn[0])
413
+ * args.num_head
414
+ * args.num_batch,
415
+ num_block=num_block,
416
+ l2_minor=Int32(swizzle),
417
+ num_head_divmod=FastDivmodDivisor(args.num_head),
418
+ l2_minor_divmod=FastDivmodDivisor(swizzle),
419
+ l2_major_divmod=FastDivmodDivisor(swizzle * num_block),
420
+ l2_minor_residual_divmod=FastDivmodDivisor(
421
+ max(num_hb_remainder, 1)
422
+ ), # don't divide by 0
423
+ num_hb_quotient=Int32(num_hb_quotient),
424
+ cluster_shape_mn=args.cluster_shape_mn,
425
+ spt=args.lpt,
426
+ )
427
+
428
+ def __init__(self, params: Params, tile_idx: Int32, *, loc=None, ip=None):
429
+ self.params = params
430
+ self._tile_idx = tile_idx
431
+ self._loc = loc
432
+ self._ip = ip
433
+
434
+ @staticmethod
435
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
436
+ return SingleTileLPTBwdScheduler.Params.create(args, loc=loc, ip=ip)
437
+
438
+ @staticmethod
439
+ @cute.jit
440
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileLPTBwdScheduler":
441
+ tile_idx = cute.arch.block_idx()[0]
442
+ return SingleTileLPTBwdScheduler(params, tile_idx, loc=loc, ip=ip)
443
+
444
+ # called by host
445
+ @staticmethod
446
+ def get_grid_shape(
447
+ params: Params,
448
+ *,
449
+ loc=None,
450
+ ip=None,
451
+ ) -> Tuple[Int32, Int32, Int32]:
452
+ return (params.total_blocks, Int32(1), Int32(1))
453
+
454
+ @cute.jit
455
+ def get_current_work(self, *, loc=None, ip=None) -> cutlass.utils.WorkTileInfo:
456
+ cluster_idx = self._tile_idx // self.params.cluster_shape_mn[0]
457
+ params = self.params
458
+ # Implement LPT scheduling coordinate calculation
459
+ bidhb, l2_mod = divmod(cluster_idx, params.l2_major_divmod)
460
+ # If we're in the last section (called residual), we don't want to divide by
461
+ # swizzle. Instead we want to divide by the remainder.
462
+ block, bidhb_residual = 0, 0
463
+ if bidhb < params.num_hb_quotient:
464
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_divmod)
465
+ else:
466
+ block, bidhb_residual = divmod(l2_mod, params.l2_minor_residual_divmod)
467
+ bidhb_actual = bidhb * params.l2_minor + bidhb_residual
468
+ batch_idx, head_idx = divmod(bidhb_actual, params.num_head_divmod)
469
+ is_valid = self._tile_idx < params.total_blocks
470
+ bidx_in_cluster = cute.arch.block_in_cluster_idx()
471
+ block = block * params.cluster_shape_mn[0] + bidx_in_cluster[0]
472
+ if cutlass.const_expr(params.spt):
473
+ block = params.num_block - 1 - block
474
+ return WorkTileInfo((Int32(block), Int32(head_idx), Int32(batch_idx), Int32(0)), is_valid)
475
+
476
+ def initial_work_tile_info(self, *, loc=None, ip=None):
477
+ return self.get_current_work(loc=loc, ip=ip)
478
+
479
+ def prefetch_next_work(self, *, loc=None, ip=None):
480
+ pass
481
+
482
+ def advance_to_next_work(self, *, loc=None, ip=None):
483
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
484
+ self._tile_idx = self.params.total_blocks
485
+
486
+ def __extract_mlir_values__(self):
487
+ values, self._values_pos = [], []
488
+ for obj in [self.params, self._tile_idx]:
489
+ obj_values = cutlass.extract_mlir_values(obj)
490
+ values += obj_values
491
+ self._values_pos.append(len(obj_values))
492
+ return values
493
+
494
+ def __new_from_mlir_values__(self, values):
495
+ obj_list = []
496
+ for obj, n_items in zip([self.params, self._tile_idx], self._values_pos):
497
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
498
+ values = values[n_items:]
499
+ return self.__class__(*(tuple(obj_list)), loc=self._loc)
500
+
501
+
502
+ class SingleTileVarlenScheduler:
503
+ @dataclass
504
+ class Params(ParamsBase):
505
+ num_head: Int32
506
+ num_batch: Int32
507
+ total_q: Int32
508
+ num_splits: Int32
509
+ max_kvblock_in_l2: Int32
510
+ tile_shape_mn: cutlass.Constexpr[Tuple[int, int]]
511
+ mCuSeqlensQ: Optional[cute.Tensor] = None
512
+ mSeqUsedQ: Optional[cute.Tensor] = None
513
+ qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
514
+ lpt: cutlass.Constexpr[bool] = False
515
+ is_split_kv: cutlass.Constexpr[bool] = False
516
+ head_swizzle: cutlass.Constexpr[bool] = False
517
+
518
+ @staticmethod
519
+ @cute.jit
520
+ def create(
521
+ args: TileSchedulerArguments, *, loc=None, ip=None
522
+ ) -> "SingleTileVarlenScheduler.Params":
523
+ size_l2 = 50 * 1024 * 1024 # 50 MB for K & V
524
+ max_kvblock_in_l2 = size_l2 // (
525
+ (args.headdim + args.headdim_v) * args.element_size * args.tile_shape_mn[1]
526
+ )
527
+ assert args.mCuSeqlensQ is not None or args.mSeqUsedQ is not None, (
528
+ "At least one of mCuSeqlensQ or mSeqUsedQ must be provided"
529
+ )
530
+ return SingleTileVarlenScheduler.Params(
531
+ num_head=args.num_head,
532
+ num_batch=args.num_batch,
533
+ total_q=args.total_q,
534
+ num_splits=args.num_splits,
535
+ max_kvblock_in_l2=max_kvblock_in_l2,
536
+ tile_shape_mn=args.tile_shape_mn,
537
+ mCuSeqlensQ=args.mCuSeqlensQ,
538
+ mSeqUsedQ=args.mSeqUsedQ,
539
+ qhead_per_kvhead_packgqa=args.qhead_per_kvhead_packgqa,
540
+ lpt=args.lpt,
541
+ is_split_kv=args.is_split_kv,
542
+ head_swizzle=args.head_swizzle,
543
+ )
544
+
545
+ def __init__(self, params: Params, tile_idx: Int32, split_idx: Int32, *, loc=None, ip=None):
546
+ self.params = params
547
+ self._tile_idx = tile_idx
548
+ self._split_idx = split_idx
549
+ self._is_first_block = True
550
+ self._loc = loc
551
+ self._ip = ip
552
+
553
+ @staticmethod
554
+ def to_underlying_arguments(args: TileSchedulerArguments, *, loc=None, ip=None) -> Params:
555
+ return SingleTileVarlenScheduler.Params.create(args, loc=loc, ip=ip)
556
+
557
+ @staticmethod
558
+ def create(params: Params, *, loc=None, ip=None) -> "SingleTileVarlenScheduler":
559
+ tile_idx, split_idx, _ = cute.arch.block_idx()
560
+ return SingleTileVarlenScheduler(params, tile_idx, split_idx, loc=loc, ip=ip)
561
+
562
+ # called by host
563
+ @staticmethod
564
+ def get_grid_shape(
565
+ params: Params,
566
+ *,
567
+ loc=None,
568
+ ip=None,
569
+ ) -> Tuple[Int32, Int32, Int32]:
570
+ total_blocks_max = (
571
+ params.total_q + params.num_batch * (params.tile_shape_mn[0] - 1)
572
+ ) // params.tile_shape_mn[0]
573
+ return (total_blocks_max * params.num_head, params.num_splits, Int32(1))
574
+
575
+ @cute.jit
576
+ def _get_num_m_blocks(self, lane: Int32, bidb_start: Int32) -> Int32:
577
+ params = self.params
578
+ batch_idx = lane + bidb_start
579
+ if cutlass.const_expr(params.mSeqUsedQ is not None):
580
+ seqlen = Int32(0)
581
+ if batch_idx < params.num_batch:
582
+ seqlen = params.mSeqUsedQ[batch_idx]
583
+ else:
584
+ assert params.mCuSeqlensQ is not None
585
+ cur_cu_seqlen = Int32(0)
586
+ if batch_idx <= params.num_batch:
587
+ cur_cu_seqlen = params.mCuSeqlensQ[batch_idx]
588
+ next_cu_seqlen = cute.arch.shuffle_sync_down(cur_cu_seqlen, offset=1)
589
+ seqlen = next_cu_seqlen - cur_cu_seqlen
590
+ if cutlass.const_expr(params.qhead_per_kvhead_packgqa > 1):
591
+ seqlen *= params.qhead_per_kvhead_packgqa
592
+ return (
593
+ cute.ceil_div(seqlen, params.tile_shape_mn[0])
594
+ if batch_idx < params.num_batch and lane < cute.arch.WARP_SIZE - 1
595
+ else Int32(0)
596
+ )
597
+
598
+ @cute.jit
599
+ def get_current_work(self, *, loc=None, ip=None) -> WorkTileInfo:
600
+ params = self.params
601
+ lane_idx = cute.arch.lane_idx()
602
+ num_m_blocks = self._get_num_m_blocks(lane_idx, bidb_start=0)
603
+ num_m_blocks_cumulative = utils.warp_prefix_sum(num_m_blocks, lane_idx)
604
+ # Total number of blocks for the next 31 batches
605
+ m_blocks_in_group = cute.arch.shuffle_sync(num_m_blocks_cumulative, cute.arch.WARP_SIZE - 1)
606
+ # Same for all lanes
607
+ group_end_tile = m_blocks_in_group * params.num_head
608
+ # 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)
609
+ block, head_idx, batch_idx = Int32(0), Int32(0), Int32(0)
610
+ next_tile_idx = self._tile_idx
611
+ while group_end_tile <= next_tile_idx:
612
+ batch_idx += cute.arch.WARP_SIZE - 1
613
+ if batch_idx >= params.num_batch:
614
+ batch_idx = Int32(params.num_batch)
615
+ group_end_tile = next_tile_idx + 1
616
+ else:
617
+ num_m_blocks = self._get_num_m_blocks(lane_idx, bidb_start=batch_idx)
618
+ num_m_blocks_cumulative = utils.warp_prefix_sum(num_m_blocks, lane_idx)
619
+ m_blocks_in_group = cute.arch.shuffle_sync(
620
+ num_m_blocks_cumulative, cute.arch.WARP_SIZE - 1
621
+ )
622
+ group_end_tile += m_blocks_in_group * params.num_head
623
+ is_valid = False
624
+ if batch_idx >= params.num_batch:
625
+ block, head_idx, batch_idx = Int32(0), Int32(0), Int32(params.num_batch)
626
+ else:
627
+ group_start_tile = group_end_tile - m_blocks_in_group * params.num_head
628
+ # 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)
629
+ # The next problem to process is the first one that does not have ending tile position
630
+ # that is greater than or equal to tile index.
631
+ batch_idx_in_group = cute.arch.popc(
632
+ cute.arch.vote_ballot_sync(
633
+ group_start_tile + num_m_blocks_cumulative * params.num_head <= next_tile_idx
634
+ )
635
+ )
636
+ batch_idx += batch_idx_in_group
637
+ num_m_blocks_prev_lane = (
638
+ 0
639
+ if batch_idx_in_group == 0
640
+ else cute.arch.shuffle_sync(num_m_blocks_cumulative, batch_idx_in_group - 1)
641
+ )
642
+ num_m_blocks = cute.arch.shuffle_sync(num_m_blocks, batch_idx_in_group)
643
+ mh_block = next_tile_idx - group_start_tile - num_m_blocks_prev_lane * params.num_head
644
+ if cutlass.const_expr(params.lpt or params.head_swizzle):
645
+ # This is a version of the SingleTileLPTScheduler, complicated by the fact that
646
+ # the seqlen can vary per batch.
647
+ # TODO: is there any case where num_m_blocks is 0?
648
+ # TODO: by right we should read the seqlen_kv but we're assuming seqlen_q == seqlen_k here
649
+ num_n_blocks = (
650
+ num_m_blocks
651
+ * params.tile_shape_mn[0]
652
+ // params.qhead_per_kvhead_packgqa
653
+ // params.tile_shape_mn[1]
654
+ )
655
+ # nheads_in_l2 = min(max(self.max_kvblock_in_l2 // num_n_blocks, 1), self.num_head)
656
+ # Seems faster to have this be a power of 2
657
+ nheads_in_l2 = (
658
+ 16
659
+ if num_n_blocks * 16 <= params.max_kvblock_in_l2
660
+ else (
661
+ 8
662
+ if num_n_blocks * 8 <= params.max_kvblock_in_l2
663
+ else (
664
+ 4
665
+ if num_n_blocks * 4 <= params.max_kvblock_in_l2
666
+ else (2 if num_n_blocks * 2 <= params.max_kvblock_in_l2 else 1)
667
+ )
668
+ )
669
+ )
670
+ nheads_in_l2 = min(nheads_in_l2, params.num_head)
671
+ mh_in_l2 = nheads_in_l2 * num_m_blocks
672
+ section_idx = mh_block // mh_in_l2
673
+ l2_mod = mh_block - section_idx * mh_in_l2
674
+ # Deal with tail section
675
+ nheads_in_this_section = (
676
+ nheads_in_l2
677
+ if nheads_in_l2 * (section_idx + 1) <= params.num_head
678
+ else params.num_head - section_idx * nheads_in_l2
679
+ )
680
+ block = l2_mod // nheads_in_this_section
681
+ head_idx_residual = l2_mod - block * nheads_in_this_section
682
+ head_idx = section_idx * nheads_in_l2 + head_idx_residual
683
+ if cutlass.const_expr(params.lpt):
684
+ block = num_m_blocks - 1 - block
685
+ else:
686
+ head_idx = mh_block // num_m_blocks
687
+ block = mh_block - head_idx * num_m_blocks
688
+ is_valid = self._is_first_block and batch_idx < params.num_batch
689
+ # 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)
690
+ split_idx = self._split_idx if const_expr(params.is_split_kv) else Int32(0)
691
+ return WorkTileInfo((Int32(block), Int32(head_idx), Int32(batch_idx), split_idx), is_valid)
692
+
693
+ def initial_work_tile_info(self, *, loc=None, ip=None):
694
+ return self.get_current_work(loc=loc, ip=ip)
695
+
696
+ def prefetch_next_work(self, *, loc=None, ip=None):
697
+ pass
698
+
699
+ def advance_to_next_work(self, *, loc=None, ip=None):
700
+ # Single tile scheduler - set to invalid tile_idx to indicate no more work
701
+ self._is_first_block = False
702
+
703
+ def __extract_mlir_values__(self):
704
+ values, self._values_pos = [], []
705
+ for obj in [self.params, self._tile_idx, self._split_idx]:
706
+ obj_values = cutlass.extract_mlir_values(obj)
707
+ values += obj_values
708
+ self._values_pos.append(len(obj_values))
709
+ return values
710
+
711
+ def __new_from_mlir_values__(self, values):
712
+ obj_list = []
713
+ for obj, n_items in zip(
714
+ [self.params, self._tile_idx, self._split_idx],
715
+ self._values_pos,
716
+ ):
717
+ obj_list.append(cutlass.new_from_mlir_values(obj, values[:n_items]))
718
+ values = values[n_items:]
719
+ return SingleTileVarlenScheduler(*(tuple(obj_list)), loc=self._loc)
kernel/deps/flash_attn/cute/utils.py ADDED
@@ -0,0 +1,859 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025, Tri Dao.
2
+
3
+ import math
4
+ import hashlib
5
+ import inspect
6
+ import re
7
+ from typing import Type, Callable, Optional, Tuple, overload
8
+ from functools import partial
9
+
10
+ import cutlass
11
+ import cutlass.cute as cute
12
+
13
+ from cutlass import Float32, const_expr
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
+ # cute.arch.{fma,mul,add}_packed_f32x2 uses RZ rounding mode by default
20
+ fma_packed_f32x2 = partial(cute.arch.fma_packed_f32x2, rnd=nvvm.RoundingModeKind.RN)
21
+ mul_packed_f32x2 = partial(cute.arch.mul_packed_f32x2, rnd=nvvm.RoundingModeKind.RN)
22
+ add_packed_f32x2 = partial(cute.arch.add_packed_f32x2, rnd=nvvm.RoundingModeKind.RN)
23
+ sub_packed_f32x2 = partial(
24
+ cute.arch.calc_packed_f32x2_op,
25
+ src_c=None,
26
+ calc_func=nvvm.sub_packed_f32x2,
27
+ rnd=nvvm.RoundingModeKind.RN,
28
+ )
29
+
30
+
31
+ def hash_callable(func: Callable, set_cute_hash=True) -> str:
32
+ """Hash a callable based on the source code or bytecode and closure values.
33
+
34
+ Fast-path: if the callable (or its __wrapped__ base) has a ``__cute_hash__``
35
+ attribute, that value is returned immediately. Code-generation backends such
36
+ as Inductor can set this attribute to avoid expensive runtime hashing.
37
+
38
+ set_cute_hash: whether or not to set func.__cute_hash__ if not present
39
+ """
40
+ if hasattr(func, "__cute_hash__"):
41
+ return func.__cute_hash__
42
+
43
+ # Unwrap decorated functions (e.g., cute.jit wrappers).
44
+ if hasattr(func, "__wrapped__"):
45
+ base_func = func.__wrapped__
46
+ if hasattr(base_func, "__cute_hash__"):
47
+ return base_func.__cute_hash__
48
+ func = base_func
49
+
50
+ try:
51
+ data = inspect.getsource(func).encode()
52
+ except (OSError, TypeError):
53
+ if hasattr(func, "__code__") and func.__code__ is not None:
54
+ data = func.__code__.co_code
55
+ else:
56
+ data = repr(func).encode()
57
+
58
+ hasher = hashlib.sha256(data)
59
+
60
+ if hasattr(func, "__closure__") and func.__closure__ is not None:
61
+ for idx, cell in enumerate(func.__closure__):
62
+ cell_value = cell.cell_contents
63
+ hasher.update(repr(cell_value).encode())
64
+
65
+ hash = hasher.hexdigest()
66
+
67
+ if set_cute_hash:
68
+ func.__cute_hash__ = hash
69
+
70
+ return hash
71
+
72
+
73
+ def create_softcap_scoremod(softcap_val):
74
+ inv_softcap = 1.0 / softcap_val
75
+
76
+ @cute.jit
77
+ def scoremod_premask_fn(acc_S_SSA, batch_idx, head_idx, q_idx, kv_idx, aux_tensors):
78
+ scores = acc_S_SSA * inv_softcap
79
+ return scores * cute.math.tanh(scores, fastmath=True)
80
+
81
+ return scoremod_premask_fn
82
+
83
+
84
+ def convert_from_dlpack(x, leading_dim, alignment=16, divisibility=1) -> cute.Tensor:
85
+ return (
86
+ from_dlpack(x, assumed_align=alignment)
87
+ .mark_layout_dynamic(leading_dim=leading_dim)
88
+ .mark_compact_shape_dynamic(
89
+ mode=leading_dim, stride_order=x.dim_order(), divisibility=divisibility
90
+ )
91
+ )
92
+
93
+
94
+ def convert_from_dlpack_leading_static(
95
+ x, leading_dim, alignment=16, static_modes=None, stride_order=None
96
+ ) -> cute.Tensor:
97
+ if stride_order is None:
98
+ stride_order = x.dim_order()
99
+ x_ = from_dlpack(x, assumed_align=alignment)
100
+ for i in range(x.ndim):
101
+ if i != leading_dim and (static_modes is None or i not in static_modes):
102
+ x_ = x_.mark_compact_shape_dynamic(mode=i, stride_order=stride_order)
103
+ return x_
104
+
105
+
106
+ def make_tiled_copy_A(
107
+ copy_atom: cute.CopyAtom, tiled_mma: cute.TiledMma, swapAB: cutlass.Constexpr[bool] = False
108
+ ) -> cute.TiledCopy:
109
+ if const_expr(swapAB):
110
+ return cute.make_tiled_copy_B(copy_atom, tiled_mma)
111
+ else:
112
+ return cute.make_tiled_copy_A(copy_atom, tiled_mma)
113
+
114
+
115
+ def make_tiled_copy_B(
116
+ copy_atom: cute.CopyAtom, tiled_mma: cute.TiledMma, swapAB: cutlass.Constexpr[bool] = False
117
+ ) -> cute.TiledCopy:
118
+ if const_expr(swapAB):
119
+ return cute.make_tiled_copy_A(copy_atom, tiled_mma)
120
+ else:
121
+ return cute.make_tiled_copy_B(copy_atom, tiled_mma)
122
+
123
+
124
+ def mma_make_fragment_A(
125
+ smem: cute.Tensor, thr_mma: cute.core.ThrMma, swapAB: cutlass.Constexpr[bool] = False
126
+ ) -> cute.Tensor:
127
+ if const_expr(swapAB):
128
+ return mma_make_fragment_B(smem, thr_mma)
129
+ else:
130
+ return thr_mma.make_fragment_A(thr_mma.partition_A(smem))
131
+
132
+
133
+ def mma_make_fragment_B(
134
+ smem: cute.Tensor, thr_mma: cute.core.ThrMma, swapAB: cutlass.Constexpr[bool] = False
135
+ ) -> cute.Tensor:
136
+ if const_expr(swapAB):
137
+ return mma_make_fragment_A(smem, thr_mma)
138
+ else:
139
+ return thr_mma.make_fragment_B(thr_mma.partition_B(smem))
140
+
141
+
142
+ def get_smem_store_atom(
143
+ arch: cutlass.Constexpr[int], element_type: Type[cute.Numeric], transpose: bool = False
144
+ ) -> cute.CopyAtom:
145
+ if const_expr(arch < 90 or element_type.width != 16):
146
+ return cute.make_copy_atom(
147
+ cute.nvgpu.CopyUniversalOp(),
148
+ element_type,
149
+ num_bits_per_copy=2 * element_type.width,
150
+ )
151
+ else:
152
+ return cute.make_copy_atom(
153
+ cute.nvgpu.warp.StMatrix8x8x16bOp(transpose=transpose, num_matrices=4),
154
+ element_type,
155
+ )
156
+
157
+
158
+ @cute.jit
159
+ def warp_reduce(
160
+ val: cute.TensorSSA | cute.Numeric,
161
+ op: Callable,
162
+ width: cutlass.Constexpr[int] = cute.arch.WARP_SIZE,
163
+ ) -> cute.TensorSSA | cute.Numeric:
164
+ if const_expr(isinstance(val, cute.TensorSSA)):
165
+ res = cute.make_fragment(val.shape, val.dtype)
166
+ res.store(val)
167
+ for i in cutlass.range_constexpr(cute.size(val.shape)):
168
+ res[i] = warp_reduce(res[i], op, width)
169
+ return res.load()
170
+ else:
171
+ for i in cutlass.range_constexpr(int(math.log2(width))):
172
+ val = op(val, cute.arch.shuffle_sync_bfly(val, offset=1 << i))
173
+ return val
174
+
175
+
176
+ def convert_layout_acc_mn(acc_layout: cute.Layout, transpose: bool = False) -> cute.Layout:
177
+ """
178
+ For Sm80, convert ((2, 2), MMA_M, MMA_N, ...) to ((2, MMA_M), (2, MMA_N), ...).
179
+ For Sm90, convert ((2, 2, V), MMA_M, MMA_N, ...) to ((2, MMA_M), (2, V, MMA_N), ...).
180
+ """
181
+ acc_layout_col_major = cute.make_layout(acc_layout.shape)
182
+ shape = (
183
+ (acc_layout_col_major.shape[0][1], acc_layout_col_major.shape[1]), # MMA_M
184
+ (
185
+ acc_layout_col_major.shape[0][0],
186
+ *acc_layout_col_major.shape[0][2:],
187
+ acc_layout_col_major.shape[2],
188
+ ), # MMA_N
189
+ *acc_layout_col_major.shape[3:],
190
+ )
191
+ stride = (
192
+ (acc_layout_col_major.stride[0][1], acc_layout_col_major.stride[1]), # MMA_M
193
+ (
194
+ acc_layout_col_major.stride[0][0],
195
+ *acc_layout_col_major.stride[0][2:],
196
+ acc_layout_col_major.stride[2],
197
+ ), # MMA_N
198
+ *acc_layout_col_major.stride[3:],
199
+ )
200
+ if const_expr(transpose):
201
+ shape = (shape[1], shape[0], *shape[2:])
202
+ stride = (stride[1], stride[0], *stride[2:])
203
+ acc_layout_mn = cute.make_layout(shape, stride=stride)
204
+ return cute.composition(acc_layout, acc_layout_mn)
205
+
206
+
207
+ def make_acc_tensor_mn_view(acc: cute.Tensor, transpose: bool = False) -> cute.Tensor:
208
+ return cute.make_tensor(acc.iterator, convert_layout_acc_mn(acc.layout, transpose=transpose))
209
+
210
+
211
+ @cute.jit
212
+ def convert_layout_acc_frgA(acc_layout: cute.Layout) -> cute.Layout:
213
+ # For back to back gemm, convert layout of acc0 to gemm 1 accept layout.
214
+ # For Sm80, as the mma instruction shape is 16x8x16, we need to convert from (4, MMA_M, MMA_N) to ((4, 2), MMA_M, MMA_N / 2)
215
+ # For Sm90, FP16/BF16, convert acc_layout from ((2, 2, N / 8), MMA_M, MMA_N) to ((2, 2, 2), MMA_M, (N / 16, MMA_N))
216
+ # TODO: Sm90 FP8
217
+ if const_expr(cute.rank(acc_layout.shape[0]) == 3): # Sm90
218
+ l = cute.logical_divide(
219
+ acc_layout, ((None, None, 2), None, None)
220
+ ) # ((2, 2, (2, N / 16)), MMA_M, MMA_N)
221
+ rA_mma_view = cute.make_layout(
222
+ (
223
+ (l.shape[0][0], l.shape[0][1], l.shape[0][2][0]),
224
+ l.shape[1],
225
+ (l.shape[0][2][1], l.shape[2]),
226
+ ),
227
+ stride=(
228
+ (l.stride[0][0], l.stride[0][1], l.stride[0][2][0]),
229
+ l.stride[1],
230
+ (l.stride[0][2][1], l.stride[2]),
231
+ ),
232
+ )
233
+ else: # Sm80
234
+ # (4, MMA_M, MMA_N) -> (4, MMA_M, (2, MMA_N / 2))
235
+ l = cute.logical_divide(acc_layout, (None, None, 2))
236
+ rA_mma_view = cute.make_layout(
237
+ (
238
+ (l.shape[0], l.shape[2][0]),
239
+ l.shape[1],
240
+ l.shape[2][1],
241
+ ),
242
+ stride=(
243
+ (l.stride[0], l.stride[2][0]),
244
+ l.stride[1],
245
+ l.stride[2][1],
246
+ ),
247
+ )
248
+ return rA_mma_view
249
+
250
+
251
+ def make_acc_tensor_frgA_view(acc: cute.Tensor) -> cute.Tensor:
252
+ return cute.make_tensor(acc.iterator, convert_layout_acc_frgA(acc.layout))
253
+
254
+
255
+ def select(a: cute.Tensor, mode: list[int]) -> cute.Tensor:
256
+ return cute.make_tensor(a.iterator, cute.select(a.layout, mode))
257
+
258
+
259
+ def transpose_view(a: cute.Tensor) -> cute.Tensor:
260
+ """Transpose the first two dimensions of a tensor on smem."""
261
+ shape = (a.shape[1], a.shape[0], *a.shape[2:])
262
+ order = (1, 0, *range(2, cute.rank(a)))
263
+ return cute.composition(a, cute.make_ordered_layout(shape, order=order))
264
+ # stride = (a.layout.stride[1], a.layout.stride[0], *a.layout.stride[2:])
265
+ # return cute.make_tensor(a.iterator, cute.make_layout(shape, stride=stride))
266
+
267
+
268
+ def parse_swizzle_from_pointer(ptr: cute.Pointer) -> cute.Swizzle:
269
+ """Extract swizzle parameters from a pointer's swizzle_type.
270
+
271
+ The swizzle_type string has the form '!cute.swizzle<"S<b,m,s>">' where
272
+ b, m, s are the swizzle parameters (bits, base, shift).
273
+
274
+ Returns:
275
+ A cute.Swizzle object constructed from the extracted parameters
276
+
277
+ Raises:
278
+ ValueError: If the swizzle_type string cannot be parsed
279
+ """
280
+ # Ideally there should be a better API to get swizzle parameters, but we'll just parse
281
+ # the string here.
282
+ swizzle_str = str(ptr.type.swizzle_type)
283
+ # Extract the inner part "S<b,m,s>"
284
+ match = re.search(r"S<(\d+),(\d+),(\d+)>", swizzle_str)
285
+ if match:
286
+ b, m, s = int(match.group(1)), int(match.group(2)), int(match.group(3))
287
+ return cute.make_swizzle(b, m, s)
288
+ else:
289
+ raise ValueError(f"Could not parse swizzle_type: {swizzle_str}")
290
+
291
+
292
+ @cute.jit
293
+ def exp2f(x: cute.TensorSSA | Float32) -> cute.TensorSSA | Float32:
294
+ """exp2f calculation for both vector and scalar.
295
+ :param x: input value
296
+ :type x: cute.TensorSSA or Float32
297
+ :return: exp2 value
298
+ :rtype: cute.TensorSSA or Float32
299
+ """
300
+ if const_expr(isinstance(x, cute.TensorSSA)):
301
+ res = cute.make_fragment(x.shape, Float32)
302
+ res.store(x)
303
+ for i in cutlass.range_constexpr(cute.size(x.shape)):
304
+ res[i] = cute.arch.exp2(res[i])
305
+ return res.load()
306
+ else:
307
+ return cute.arch.exp2(x)
308
+
309
+
310
+ @dsl_user_op
311
+ def log2f(a: float | Float32, *, loc=None, ip=None) -> Float32:
312
+ return Float32(
313
+ llvm.inline_asm(
314
+ T.f32(),
315
+ [Float32(a).ir_value(loc=loc, ip=ip)],
316
+ "lg2.approx.ftz.f32 $0, $1;",
317
+ "=f,f",
318
+ has_side_effects=False,
319
+ is_align_stack=False,
320
+ asm_dialect=llvm.AsmDialect.AD_ATT,
321
+ )
322
+ )
323
+
324
+
325
+ @dsl_user_op
326
+ def logf(a: float | Float32, *, loc=None, ip=None) -> Float32:
327
+ return log2f(a, loc=loc, ip=ip) * math.log(2.0)
328
+
329
+
330
+ @dsl_user_op
331
+ def fmax(
332
+ a: float | Float32, b: float | Float32, c: float | Float32 | None = None, *, loc=None, ip=None
333
+ ) -> Float32:
334
+ return Float32(
335
+ nvvm.fmax(
336
+ T.f32(),
337
+ Float32(a).ir_value(loc=loc, ip=ip),
338
+ Float32(b).ir_value(loc=loc, ip=ip),
339
+ c=Float32(c).ir_value(loc=loc, ip=ip) if c is not None else None,
340
+ loc=loc,
341
+ ip=ip,
342
+ )
343
+ )
344
+
345
+
346
+ @cute.jit
347
+ def fmax_reduce(
348
+ x: cute.TensorSSA, init_val: float | Float32 | None = None, arch: cutlass.Constexpr[int] = 80
349
+ ) -> Float32:
350
+ if const_expr(arch < 100 or cute.size(x.shape) % 8 != 0):
351
+ # if const_expr(init_val is None):
352
+ # init_val = -cutlass.Float32.if
353
+ # return x.reduce(cute.ReductionOp.MAX, init_val, 0)
354
+ res = cute.make_fragment(x.shape, Float32)
355
+ res.store(x)
356
+ # local_max = [res[0], res[1]]
357
+ # for i in cutlass.range_constexpr(2, cute.size(x.shape), 2):
358
+ # local_max[0] = fmax(local_max[0], res[i + 0])
359
+ # local_max[1] = fmax(local_max[1], res[i + 1])
360
+ # local_max[0] = fmax(local_max[0], local_max[1])
361
+ # return local_max[0] if const_expr(init_val is None) else fmax(local_max[0], init_val)
362
+ local_max = [res[0], res[1], res[2], res[3]]
363
+ for i in cutlass.range_constexpr(4, cute.size(x.shape), 4):
364
+ local_max[0] = fmax(local_max[0], res[i + 0])
365
+ local_max[1] = fmax(local_max[1], res[i + 1])
366
+ local_max[2] = fmax(local_max[2], res[i + 2])
367
+ local_max[3] = fmax(local_max[3], res[i + 3])
368
+ local_max[0] = fmax(local_max[0], local_max[1])
369
+ local_max[2] = fmax(local_max[2], local_max[3])
370
+ local_max[0] = fmax(local_max[0], local_max[2])
371
+ return local_max[0] if const_expr(init_val is None) else fmax(local_max[0], init_val)
372
+ else:
373
+ # [2025-06-15] x.reduce only seems to use 50% 3-input max and 50% 2-input max
374
+ # We instead force the 3-input max.
375
+ res = cute.make_fragment(x.shape, Float32)
376
+ res.store(x)
377
+ local_max_0 = (
378
+ fmax(init_val, res[0], res[1])
379
+ if const_expr(init_val is not None)
380
+ else fmax(res[0], res[1])
381
+ )
382
+ local_max = [
383
+ local_max_0,
384
+ fmax(res[2], res[3]),
385
+ fmax(res[4], res[5]),
386
+ fmax(res[6], res[7]),
387
+ ]
388
+ for i in cutlass.range_constexpr(8, cute.size(x.shape), 8):
389
+ local_max[0] = fmax(local_max[0], res[i], res[i + 1])
390
+ local_max[1] = fmax(local_max[1], res[i + 2], res[i + 3])
391
+ local_max[2] = fmax(local_max[2], res[i + 4], res[i + 5])
392
+ local_max[3] = fmax(local_max[3], res[i + 6], res[i + 7])
393
+ local_max[0] = fmax(local_max[0], local_max[1])
394
+ return fmax(local_max[0], local_max[2], local_max[3])
395
+
396
+
397
+ @cute.jit
398
+ def fadd_reduce(
399
+ x: cute.TensorSSA, init_val: float | Float32 | None = None, arch: cutlass.Constexpr[int] = 80
400
+ ) -> Float32:
401
+ if const_expr(arch < 100 or cute.size(x.shape) % 8 != 0):
402
+ if const_expr(init_val is None):
403
+ init_val = Float32.zero
404
+ return x.reduce(cute.ReductionOp.ADD, init_val, 0)
405
+ # res = cute.make_fragment(x.shape, Float32)
406
+ # res.store(x)
407
+ # local_sum = [res[0], res[1], res[2], res[3]]
408
+ # for i in cutlass.range_constexpr(4, cute.size(x.shape), 4):
409
+ # local_sum[0] += res[i + 0]
410
+ # local_sum[1] += res[i + 1]
411
+ # local_sum[2] += res[i + 2]
412
+ # local_sum[3] += res[i + 3]
413
+ # local_sum[0] += local_sum[1]
414
+ # local_sum[2] += local_sum[3]
415
+ # local_sum[0] += local_sum[2]
416
+ # return local_sum[0] if const_expr(init_val is None) else local_sum[0] + init_val
417
+ else:
418
+ res = cute.make_fragment(x.shape, Float32)
419
+ res.store(x)
420
+ local_sum_0 = (
421
+ add_packed_f32x2((init_val, 0.0), (res[0], res[1]))
422
+ # add_packed_f32x2((init_val / 2, init_val / 2), (res[0], res[1]))
423
+ if const_expr(init_val is not None)
424
+ else (res[0], res[1])
425
+ )
426
+ local_sum = [local_sum_0, (res[2], res[3]), (res[4], res[5]), (res[6], res[7])]
427
+ for i in cutlass.range_constexpr(8, cute.size(x.shape), 8):
428
+ local_sum[0] = add_packed_f32x2(local_sum[0], (res[i + 0], res[i + 1]))
429
+ local_sum[1] = add_packed_f32x2(local_sum[1], (res[i + 2], res[i + 3]))
430
+ local_sum[2] = add_packed_f32x2(local_sum[2], (res[i + 4], res[i + 5]))
431
+ local_sum[3] = add_packed_f32x2(local_sum[3], (res[i + 6], res[i + 7]))
432
+ local_sum[0] = add_packed_f32x2(local_sum[0], local_sum[1])
433
+ local_sum[2] = add_packed_f32x2(local_sum[2], local_sum[3])
434
+ local_sum[0] = add_packed_f32x2(local_sum[0], local_sum[2])
435
+ return local_sum[0][0] + local_sum[0][1]
436
+
437
+
438
+ @dsl_user_op
439
+ def atomic_add_fp32(a: float | Float32, gmem_ptr: cute.Pointer, *, loc=None, ip=None) -> None:
440
+ # gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
441
+ # # cache_hint = cutlass.Int64(0x12F0000000000000)
442
+ # llvm.inline_asm(
443
+ # None,
444
+ # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip)],
445
+ # # [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip), cache_hint.ir_value()],
446
+ # "red.global.add.f32 [$0], $1;",
447
+ # # "red.global.add.L2::cache_hint.f32 [$0], $1, 0x12F0000000000000;",
448
+ # # "red.global.add.L2::cache_hint.f32 [$0], $1, $2;",
449
+ # "l,f",
450
+ # # "l,f,l",
451
+ # has_side_effects=True,
452
+ # is_align_stack=False,
453
+ # asm_dialect=llvm.AsmDialect.AD_ATT,
454
+ # )
455
+ nvvm.atomicrmw(
456
+ res=T.f32(), op=nvvm.AtomicOpKind.FADD, ptr=gmem_ptr.llvm_ptr, a=Float32(a).ir_value()
457
+ )
458
+
459
+
460
+ @dsl_user_op
461
+ def elem_pointer(x: cute.Tensor, coord: cute.Coord, *, loc=None, ip=None) -> cute.Pointer:
462
+ return x.iterator + cute.crd2idx(coord, x.layout, loc=loc, ip=ip)
463
+
464
+
465
+ @dsl_user_op
466
+ def elem_pointer_i64(x: cute.Tensor, coord: cute.Coord, *, loc=None, ip=None) -> cute.Pointer:
467
+ flat_coord_i64 = tuple(cutlass.Int64(c) for c in cute.flatten(coord))
468
+ flat_stride = cute.flatten_to_tuple(x.stride)
469
+ assert len(flat_coord_i64) == len(flat_stride), (
470
+ "Coordinate and stride must have the same length"
471
+ )
472
+ offset = sum(c * s for c, s in zip(flat_coord_i64, flat_stride))
473
+ # HACK: we assume that applying the offset does not change the pointer alignment
474
+ byte_offset = offset * x.element_type.width // 8
475
+ return cute.make_ptr(
476
+ x.element_type,
477
+ x.iterator.toint() + byte_offset,
478
+ x.memspace,
479
+ assumed_align=x.iterator.alignment,
480
+ )
481
+
482
+
483
+ @cute.jit
484
+ def predicate_k(tAcA: cute.Tensor, limit: cutlass.Int32) -> cute.Tensor:
485
+ # Only compute predicates for the "k" dimension. For the mn dimension, we will use "if"
486
+ tApA = cute.make_fragment(
487
+ cute.make_layout(
488
+ (cute.size(tAcA, mode=[0, 1]), cute.size(tAcA, mode=[1]), cute.size(tAcA, mode=[2])),
489
+ stride=(cute.size(tAcA, mode=[2]), 0, 1),
490
+ ),
491
+ cutlass.Boolean,
492
+ )
493
+ for rest_v in cutlass.range_constexpr(tApA.shape[0]):
494
+ for rest_k in cutlass.range_constexpr(tApA.shape[2]):
495
+ tApA[rest_v, 0, rest_k] = cute.elem_less(tAcA[(0, rest_v), 0, rest_k][1], limit)
496
+ return tApA
497
+
498
+
499
+ def canonical_warp_group_idx(sync: bool = True) -> cutlass.Int32:
500
+ warp_group_idx = cute.arch.thread_idx()[0] // 128
501
+ if const_expr(sync):
502
+ warp_group_idx = cute.arch.make_warp_uniform(warp_group_idx)
503
+ return warp_group_idx
504
+
505
+
506
+ # @dsl_user_op
507
+ # def warp_vote_any_lt(a: float | Float32, b: float | Float32, *, loc=None, ip=None) -> cutlass.Boolean:
508
+ # mask = cutlass.Int32(-1)
509
+ # return cutlass.Boolean(
510
+ # llvm.inline_asm(
511
+ # T.i32(),
512
+ # [Float32(a).ir_value(loc=loc, ip=ip), Float32(b).ir_value(loc=loc, ip=ip), mask.ir_value(loc=loc, ip=ip)],
513
+ # ".pred p1, p2;\n"
514
+ # "setp.lt.f32 p1, $1, $2;\n"
515
+ # "vote.sync.any.pred p2, p1, $3;\n"
516
+ # "selp.u32 $0, 1, 0, p2;",
517
+ # # "selp.u32 $0, 1, 0, p1;",
518
+ # "=r,f,f,r",
519
+ # has_side_effects=False,
520
+ # is_align_stack=False,
521
+ # asm_dialect=llvm.AsmDialect.AD_ATT,
522
+ # )
523
+ # )
524
+
525
+
526
+ @cute.jit
527
+ def shuffle_sync(
528
+ value: cute.Numeric,
529
+ offset: cute.typing.Int,
530
+ width: cutlass.Constexpr[int] = cute.arch.WARP_SIZE,
531
+ ) -> cute.Numeric:
532
+ assert value.width % 32 == 0, "value type must be a multiple of 32 bits"
533
+ # 1 -> 0b11111, 2 -> 0b11110, 4 -> 0b11100, 8 -> 0b11000, 16 -> 0b10000, 32 -> 0b00000
534
+ mask = cute.arch.WARP_SIZE - width
535
+ clamp = cute.arch.WARP_SIZE - 1
536
+ mask_and_clamp = mask << 8 | clamp
537
+ # important: need stride 1 and not 0 for recast_tensor to work
538
+ val = cute.make_rmem_tensor(cute.make_layout((1,), stride=(1,)), type(value))
539
+ val[0] = value
540
+ val_i32 = cute.recast_tensor(val, cutlass.Int32)
541
+ for i in cutlass.range_constexpr(cute.size(val_i32)):
542
+ val_i32[i] = cute.arch.shuffle_sync(val_i32[i], offset, mask_and_clamp=mask_and_clamp)
543
+ return val[0]
544
+
545
+
546
+ @dsl_user_op
547
+ def shr_u32(val: cutlass.Uint32, shift: cutlass.Uint32, *, loc=None, ip=None) -> cutlass.Uint32:
548
+ return cutlass.Uint32(
549
+ llvm.inline_asm(
550
+ T.i32(),
551
+ [
552
+ cutlass.Uint32(val).ir_value(loc=loc, ip=ip),
553
+ cutlass.Uint32(shift).ir_value(loc=loc, ip=ip),
554
+ ],
555
+ "shr.s32 $0, $1, $2;",
556
+ "=r,r,r",
557
+ has_side_effects=False,
558
+ is_align_stack=False,
559
+ asm_dialect=llvm.AsmDialect.AD_ATT,
560
+ )
561
+ )
562
+
563
+
564
+ @cute.jit
565
+ def warp_prefix_sum(val: cutlass.Int32, lane: Optional[cutlass.Int32] = None) -> cutlass.Int32:
566
+ if const_expr(lane is None):
567
+ lane = cute.arch.lane_idx()
568
+ # 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)
569
+ for i in cutlass.range_constexpr(int(math.log2(cute.arch.WARP_SIZE))):
570
+ offset = 1 << i
571
+ # Very important that we set mask_and_clamp to 0
572
+ partial_sum = cute.arch.shuffle_sync_up(val, offset=offset, mask_and_clamp=0)
573
+ if lane >= offset:
574
+ val += partial_sum
575
+ # 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)
576
+ return val
577
+
578
+
579
+ @dsl_user_op
580
+ def cvt_f16x2_f32(
581
+ a: float | Float32, b: float | Float32, to_dtype: Type, *, loc=None, ip=None
582
+ ) -> cutlass.Int32:
583
+ assert to_dtype in [cutlass.BFloat16, cutlass.Float16], "to_dtype must be BFloat16 or Float16"
584
+ return cutlass.Int32(
585
+ llvm.inline_asm(
586
+ T.i32(),
587
+ [Float32(a).ir_value(loc=loc, ip=ip), Float32(b).ir_value(loc=loc, ip=ip)],
588
+ f"cvt.rn.{'bf16x2' if to_dtype is cutlass.BFloat16 else 'f16x2'}.f32 $0, $2, $1;",
589
+ "=r,f,f",
590
+ has_side_effects=False,
591
+ is_align_stack=False,
592
+ asm_dialect=llvm.AsmDialect.AD_ATT,
593
+ )
594
+ )
595
+
596
+
597
+ @overload
598
+ def cvt_f16(src: cute.Tensor, dst: cute.Tensor) -> None: ...
599
+
600
+
601
+ @overload
602
+ def cvt_f16(src: cute.Tensor, dtype: Type[cute.Numeric]) -> cute.Tensor: ...
603
+
604
+
605
+ @cute.jit
606
+ def cvt_f16(src: cute.Tensor, dst_or_dtype):
607
+ """Convert Float32 tensor to Float16/BFloat16.
608
+
609
+ Args:
610
+ src: Source tensor with Float32 element type
611
+ dst_or_dtype: Either a destination tensor or a dtype (Float16/BFloat16)
612
+
613
+ Returns:
614
+ None if dst is a tensor, or a new tensor if dtype is provided
615
+ """
616
+ if const_expr(isinstance(dst_or_dtype, type)):
617
+ # dtype variant: create new tensor and call the tensor variant
618
+ dtype = dst_or_dtype
619
+ dst = cute.make_fragment(src.shape, dtype)
620
+ cvt_f16(src, dst)
621
+ return dst
622
+ else:
623
+ # tensor variant: write to dst
624
+ dst = dst_or_dtype
625
+ assert cute.size(dst.shape) == cute.size(src.shape), "dst and src must have the same size"
626
+ assert cute.size(src.shape) % 2 == 0, "src must have an even number of elements"
627
+ assert dst.element_type in [cutlass.BFloat16, cutlass.Float16], (
628
+ "dst must be BFloat16 or Float16"
629
+ )
630
+ assert src.element_type is Float32, "src must be Float32"
631
+ dst_i32 = cute.recast_tensor(dst, cutlass.Int32)
632
+ assert cute.size(dst_i32.shape) * 2 == cute.size(src.shape)
633
+ for i in cutlass.range_constexpr(cute.size(dst_i32)):
634
+ dst_i32[i] = cvt_f16x2_f32(src[2 * i], src[2 * i + 1], dst.element_type)
635
+
636
+
637
+ @dsl_user_op
638
+ @cute.jit
639
+ def evaluate_polynomial(x: Float32, poly: Tuple[Float32, ...], *, loc=None, ip=None) -> Float32:
640
+ deg = len(poly) - 1
641
+ out = poly[deg]
642
+ for i in cutlass.range_constexpr(deg - 1, -1, -1):
643
+ out = out * x + poly[i]
644
+ return out
645
+
646
+
647
+ @dsl_user_op
648
+ @cute.jit
649
+ def evaluate_polynomial_2(
650
+ x: Float32, y: Float32, poly: Tuple[Float32, ...], *, loc=None, ip=None
651
+ ) -> Tuple[Float32, Float32]:
652
+ deg = len(poly) - 1
653
+ out = (poly[deg], poly[deg])
654
+ for i in cutlass.range_constexpr(deg - 1, -1, -1):
655
+ out = fma_packed_f32x2(out, (x, y), (poly[i], poly[i]))
656
+ return out
657
+
658
+
659
+ @dsl_user_op
660
+ def add_round_down(x: float | Float32, y: float | Float32, *, loc=None, ip=None) -> Float32:
661
+ # There's probably a way to call llvm or nvvm to do this instead of ptx
662
+ return cutlass.Float32(
663
+ llvm.inline_asm(
664
+ T.f32(),
665
+ [Float32(x).ir_value(loc=loc, ip=ip), Float32(y).ir_value(loc=loc, ip=ip)],
666
+ "add.rm.ftz.f32 $0, $1, $2;",
667
+ "=f,f,f",
668
+ has_side_effects=False,
669
+ is_align_stack=False,
670
+ asm_dialect=llvm.AsmDialect.AD_ATT,
671
+ )
672
+ )
673
+
674
+
675
+ @dsl_user_op
676
+ def combine_int_frac_ex2(x_rounded: Float32, frac_ex2: Float32, *, loc=None, ip=None) -> Float32:
677
+ return cutlass.Float32(
678
+ llvm.inline_asm(
679
+ T.f32(),
680
+ [
681
+ Float32(x_rounded).ir_value(loc=loc, ip=ip),
682
+ Float32(frac_ex2).ir_value(loc=loc, ip=ip),
683
+ ],
684
+ "{\n\t"
685
+ ".reg .s32 x_rounded_i, frac_ex_i, x_rounded_e, out_i;\n\t"
686
+ "mov.b32 x_rounded_i, $1;\n\t"
687
+ "mov.b32 frac_ex_i, $2;\n\t"
688
+ "shl.b32 x_rounded_e, x_rounded_i, 23;\n\t"
689
+ # add.u32 generates IMAD instruction and add.s32 generates LEA instruction
690
+ # IMAD uses the FMA pipeline and LEA uses the ALU pipeline, afaik
691
+ "add.s32 out_i, x_rounded_e, frac_ex_i;\n\t"
692
+ "mov.b32 $0, out_i;\n\t"
693
+ "}\n",
694
+ "=f,f,f",
695
+ has_side_effects=False,
696
+ is_align_stack=False,
697
+ asm_dialect=llvm.AsmDialect.AD_ATT,
698
+ )
699
+ )
700
+
701
+
702
+ @dsl_user_op
703
+ def ex2_emulation(x: Float32, *, loc=None, ip=None) -> Float32:
704
+ # We assume x <= 127.0
705
+ poly_ex2_deg3 = (
706
+ 1.0,
707
+ 0.695146143436431884765625,
708
+ 0.227564394474029541015625,
709
+ 0.077119089663028717041015625,
710
+ )
711
+ fp32_round_int = float(2**23 + 2**22)
712
+ x_clamped = cute.arch.fmax(x, -127.0)
713
+ # We want to round down here, so that the fractional part is in [0, 1)
714
+ x_rounded = add_round_down(x_clamped, fp32_round_int, loc=loc, ip=ip)
715
+ # The integer floor of x is now in the last 8 bits of x_rounded
716
+ # We assume the next 2 ops round to nearest even. The rounding mode is important.
717
+ x_rounded_back = x_rounded - fp32_round_int
718
+ x_frac = x_clamped - x_rounded_back
719
+ x_frac_ex2 = evaluate_polynomial(x_frac, poly_ex2_deg3, loc=loc, ip=ip)
720
+ return combine_int_frac_ex2(x_rounded, x_frac_ex2, loc=loc, ip=ip)
721
+
722
+
723
+ # TODO: check that the ex2_emulation_2 produces the same SASS as the ptx version
724
+ @dsl_user_op
725
+ def ex2_emulation_2(x: Float32, y: Float32, *, loc=None, ip=None) -> Tuple[Float32, Float32]:
726
+ # We assume x <= 127.0 and y <= 127.0
727
+ poly_ex2_deg3 = (
728
+ 1.0,
729
+ 0.695146143436431884765625,
730
+ 0.227564394474029541015625,
731
+ 0.077119089663028717041015625,
732
+ )
733
+ fp32_round_int = float(2**23 + 2**22)
734
+ xy_clamped = (cute.arch.fmax(x, -127.0), cute.arch.fmax(y, -127.0))
735
+ # We want to round down here, so that the fractional part is in [0, 1)
736
+ xy_rounded = cute.arch.add_packed_f32x2(
737
+ xy_clamped, (fp32_round_int, fp32_round_int), rnd=nvvm.RoundingModeKind.RM
738
+ )
739
+ # The integer floor of x & y are now in the last 8 bits of xy_rounded
740
+ # We want the next 2 ops to round to nearest even. The rounding mode is important.
741
+ xy_rounded_back = sub_packed_f32x2(xy_rounded, (fp32_round_int, fp32_round_int))
742
+ xy_frac = sub_packed_f32x2(xy_clamped, xy_rounded_back)
743
+ xy_frac_ex2 = evaluate_polynomial_2(*xy_frac, poly_ex2_deg3, loc=loc, ip=ip)
744
+ x_out = combine_int_frac_ex2(xy_rounded[0], xy_frac_ex2[0], loc=loc, ip=ip)
745
+ y_out = combine_int_frac_ex2(xy_rounded[1], xy_frac_ex2[1], loc=loc, ip=ip)
746
+ return x_out, y_out
747
+
748
+
749
+ @dsl_user_op
750
+ def e2e_asm2(x: Float32, y: Float32, *, loc=None, ip=None) -> Tuple[Float32, Float32]:
751
+ out_f32x2 = llvm.inline_asm(
752
+ llvm.StructType.get_literal([T.f32(), T.f32()]),
753
+ [Float32(x).ir_value(loc=loc, ip=ip), Float32(y, loc=loc, ip=ip).ir_value()],
754
+ "{\n\t"
755
+ ".reg .f32 f1, f2, f3, f4, f5, f6, f7;\n\t"
756
+ ".reg .b64 l1, l2, l3, l4, l5, l6, l7, l8, l9, l10;\n\t"
757
+ ".reg .s32 r1, r2, r3, r4, r5, r6, r7, r8;\n\t"
758
+ "max.ftz.f32 f1, $2, 0fC2FE0000;\n\t"
759
+ "max.ftz.f32 f2, $3, 0fC2FE0000;\n\t"
760
+ "mov.b64 l1, {f1, f2};\n\t"
761
+ "mov.f32 f3, 0f4B400000;\n\t"
762
+ "mov.b64 l2, {f3, f3};\n\t"
763
+ "add.rm.ftz.f32x2 l7, l1, l2;\n\t"
764
+ "sub.rn.ftz.f32x2 l8, l7, l2;\n\t"
765
+ "sub.rn.ftz.f32x2 l9, l1, l8;\n\t"
766
+ "mov.f32 f7, 0f3D9DF09D;\n\t"
767
+ "mov.b64 l6, {f7, f7};\n\t"
768
+ "mov.f32 f6, 0f3E6906A4;\n\t"
769
+ "mov.b64 l5, {f6, f6};\n\t"
770
+ "mov.f32 f5, 0f3F31F519;\n\t"
771
+ "mov.b64 l4, {f5, f5};\n\t"
772
+ "mov.f32 f4, 0f3F800000;\n\t"
773
+ "mov.b64 l3, {f4, f4};\n\t"
774
+ "fma.rn.ftz.f32x2 l10, l9, l6, l5;\n\t"
775
+ "fma.rn.ftz.f32x2 l10, l10, l9, l4;\n\t"
776
+ "fma.rn.ftz.f32x2 l10, l10, l9, l3;\n\t"
777
+ "mov.b64 {r1, r2}, l7;\n\t"
778
+ "mov.b64 {r3, r4}, l10;\n\t"
779
+ "shl.b32 r5, r1, 23;\n\t"
780
+ "add.s32 r7, r5, r3;\n\t"
781
+ "shl.b32 r6, r2, 23;\n\t"
782
+ "add.s32 r8, r6, r4;\n\t"
783
+ "mov.b32 $0, r7;\n\t"
784
+ "mov.b32 $1, r8;\n\t"
785
+ "}\n",
786
+ "=r,=r,f,f",
787
+ has_side_effects=False,
788
+ is_align_stack=False,
789
+ asm_dialect=llvm.AsmDialect.AD_ATT,
790
+ )
791
+ out0 = Float32(llvm.extractvalue(T.f32(), out_f32x2, [0], loc=loc, ip=ip))
792
+ out1 = Float32(llvm.extractvalue(T.f32(), out_f32x2, [1], loc=loc, ip=ip))
793
+ return out0, out1
794
+
795
+
796
+ @dsl_user_op
797
+ def domain_offset_aligned(
798
+ coord: cute.Coord, tensor: cute.Tensor, *, loc=None, ip=None
799
+ ) -> cute.Tensor:
800
+ assert isinstance(tensor.iterator, cute.Pointer)
801
+ # We assume that applying the offset does not change the pointer alignment
802
+ new_ptr = cute.make_ptr(
803
+ tensor.element_type,
804
+ elem_pointer(tensor, coord).toint(),
805
+ tensor.memspace,
806
+ assumed_align=tensor.iterator.alignment,
807
+ )
808
+ return cute.make_tensor(new_ptr, tensor.layout)
809
+
810
+
811
+ @dsl_user_op
812
+ def domain_offset_i64(coord: cute.Coord, tensor: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
813
+ flat_coord_i64 = tuple(cutlass.Int64(c) for c in cute.flatten(coord))
814
+ flat_stride = cute.flatten_to_tuple(tensor.stride)
815
+ assert len(flat_coord_i64) == len(flat_stride), (
816
+ "Coordinate and stride must have the same length"
817
+ )
818
+ offset = sum(c * s for c, s in zip(flat_coord_i64, flat_stride))
819
+ assert isinstance(tensor.iterator, cute.Pointer)
820
+ # HACK: we assume that applying the offset does not change the pointer alignment
821
+ new_ptr = cute.make_ptr(
822
+ tensor.element_type,
823
+ tensor.iterator.toint() + offset * tensor.element_type.width // 8,
824
+ tensor.memspace,
825
+ assumed_align=tensor.iterator.max_alignment,
826
+ )
827
+ return cute.make_tensor(new_ptr, tensor.layout)
828
+
829
+
830
+ @dsl_user_op
831
+ def coord_offset_i64(
832
+ tensor: cute.Tensor, idx: cute.typing.Int, dim: int, *, loc=None, ip=None
833
+ ) -> cute.Tensor:
834
+ offset = cutlass.Int64(idx) * cute.size(tensor.stride[dim])
835
+ assert isinstance(tensor.iterator, cute.Pointer)
836
+ # HACK: we assume that applying the offset does not change the pointer alignment
837
+ new_ptr = cute.make_ptr(
838
+ tensor.element_type,
839
+ tensor.iterator.toint() + offset * tensor.element_type.width // 8,
840
+ tensor.memspace,
841
+ assumed_align=tensor.iterator.max_alignment,
842
+ )
843
+ new_layout = cute.slice_(
844
+ tensor.layout, (*[None] * dim, 0, *[None] * (cute.rank(tensor) - dim - 1))
845
+ )
846
+ return cute.make_tensor(new_ptr, new_layout)
847
+
848
+
849
+ @cute.jit
850
+ def scalar_to_ssa(a: cute.Numeric, dtype) -> cute.TensorSSA:
851
+ """Convert a scalar to a cute TensorSSA of shape (1,) and given dtype"""
852
+ vec = cute.make_fragment(1, dtype)
853
+ vec[0] = a
854
+ return vec.load()
855
+
856
+
857
+ def ssa_to_scalar(val):
858
+ """Could inline but nice for reflecting the above api"""
859
+ return val[0]
kernel/ksa_attn/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """KSA V2 inference kernels."""
2
+ from .interface import (
3
+ hierarchical_summary_attn_func,
4
+ summary_attn_func,
5
+ summary_block_sparse_attn_func,
6
+ )
7
+ from .indexer_scorer import fused_indexer_candidate_scores