hp-l33 commited on
Commit
6a8a33f
·
verified ·
1 Parent(s): 775b8fb

Generic sol_h3_vae package and sol_h3_vae.safetensors; GPU-specific FA4 guard moves to Sol-H3 in ComfyUI

Browse files
README.md CHANGED
@@ -29,10 +29,12 @@ as Sol-H3 runs it.
29
 
30
  ## Usage
31
 
32
- Requirements: PyTorch with CUDA, Triton, [Diffusers](https://github.com/huggingface/diffusers)
33
- with MiniMax-H3 support, [FlashAttention-4](https://github.com/Dao-AILab/flash-attention)
34
- (`flash_attn.cute`) and [QuACK](https://github.com/Dao-AILab/quack). Tested on RTX 5090 with
35
- PyTorch 2.12 (CUDA 13), Triton 3.7, Diffusers 0.40, FlashAttention-4 4.0.0b19 and QuACK 0.6.3.
 
 
36
 
37
  ```python
38
  import sys
@@ -40,10 +42,10 @@ from huggingface_hub import hf_hub_download, snapshot_download
40
 
41
  repo = snapshot_download("Efficient-Large-Model/Sol-H3-VAE")
42
  sys.path.insert(0, f"{repo}/code")
43
- from spark_decoder import SparkFastH3VAEDecoder
44
 
45
  vae_config = hf_hub_download("MiniMaxAI/MiniMax-H3", "vae/config.json")
46
- decoder = SparkFastH3VAEDecoder(vae_config, f"{repo}/fast_h3_vae_noncausal_24l.safetensors")
47
 
48
  # z: normalized H3 video latents, (1, T, 24, H/16, W/16); T = 0 starts a native 5-latent block
49
  pixels = decoder.decode(z) # (1, 3, F, H, W), the H3 VAE decoder's normalized pixel space
@@ -52,7 +54,12 @@ rgb = decoder.decode_rgb_uint8(z) # (F, H, W, 3) uint8 on the GPU
52
 
53
  The decoder needs only the H3 VAE `config.json`, not the H3 VAE weights. The first call for each
54
  latent shape compiles kernels and captures a CUDA graph (about 25 s on RTX 5090); later calls
55
- reuse it. Pass `engine="ref"` for the plain PyTorch decoder (slower).
 
 
 
 
 
56
 
57
  ## Limitations
58
 
@@ -64,7 +71,6 @@ reuse it. Pass `engine="ref"` for the plain PyTorch decoder (slower).
64
 
65
  ## License
66
 
67
- The code is released under Apache-2.0; `code/_spark_flash_fwd_guarded.py` is adapted from
68
- FlashAttention (BSD-3-Clause). The weights are distilled from the MiniMax-H3 VAE and are
69
  subject to the
70
  [MiniMax-H3 license](https://huggingface.co/MiniMaxAI/MiniMax-H3/blob/main/LICENSE).
 
29
 
30
  ## Usage
31
 
32
+ Requirements: PyTorch with CUDA and [Diffusers](https://github.com/huggingface/diffusers) with
33
+ MiniMax-H3 support. The fast engine (default) also needs Triton,
34
+ [FlashAttention-4](https://github.com/Dao-AILab/flash-attention) (`flash_attn.cute`) and
35
+ [QuACK](https://github.com/Dao-AILab/quack); `engine="ref"` runs in plain PyTorch without them.
36
+ Tested on RTX 5090 with PyTorch 2.12 (CUDA 13), Triton 3.7, Diffusers 0.40, FlashAttention-4
37
+ 4.0.0b19 and QuACK 0.6.3.
38
 
39
  ```python
40
  import sys
 
42
 
43
  repo = snapshot_download("Efficient-Large-Model/Sol-H3-VAE")
44
  sys.path.insert(0, f"{repo}/code")
45
+ from sol_h3_vae import SolH3VAEDecoder
46
 
47
  vae_config = hf_hub_download("MiniMaxAI/MiniMax-H3", "vae/config.json")
48
+ decoder = SolH3VAEDecoder(vae_config, f"{repo}/sol_h3_vae.safetensors")
49
 
50
  # z: normalized H3 video latents, (1, T, 24, H/16, W/16); T = 0 starts a native 5-latent block
51
  pixels = decoder.decode(z) # (1, 3, F, H, W), the H3 VAE decoder's normalized pixel space
 
54
 
55
  The decoder needs only the H3 VAE `config.json`, not the H3 VAE weights. The first call for each
56
  latent shape compiles kernels and captures a CUDA graph (about 25 s on RTX 5090); later calls
57
+ reuse it.
58
+
59
+ On SM120 GPUs (RTX 5090, DGX Spark), FlashAttention-4 4.0.0b19 to 4.0.0b22 has a varlen bug that
60
+ makes the fast engine hit an illegal memory access. Apply the guard from
61
+ [Sol-H3 in ComfyUI](https://github.com/NVlabs/Sana/blob/sol-engine/models/minimax_h3/Sol-H3-ComfyUI/sol_h3_engine/fa4_sm120.py)
62
+ before the first decode, or use `engine="ref"`.
63
 
64
  ## Limitations
65
 
 
71
 
72
  ## License
73
 
74
+ The code is released under Apache-2.0. The weights are distilled from the MiniMax-H3 VAE and are
 
75
  subject to the
76
  [MiniMax-H3 license](https://huggingface.co/MiniMaxAI/MiniMax-H3/blob/main/LICENSE).
code/LICENSE.flash-attention DELETED
@@ -1,29 +0,0 @@
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.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/_spark_flash_fwd_guarded.py DELETED
@@ -1,1246 +0,0 @@
1
- # Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
2
- # A reimplementation of
3
- # https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_kernel_sm80.h
4
- # and https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_kernel_sm90.h
5
- # from Cutlass C++ to Cute-DSL.
6
- # Built on Cute-DSL example: https://github.com/NVIDIA/cutlass/blob/main/examples/python/CuTeDSL/ampere/flash_attention_v2.py
7
-
8
- import math
9
- from types import SimpleNamespace
10
- from typing import Type, Callable, Optional
11
- from functools import partial
12
-
13
- import cuda.bindings.driver as cuda
14
-
15
- import cutlass
16
- import cutlass.cute as cute
17
- from cutlass import Float32, Int32, const_expr
18
- from cutlass.cute.nvgpu import cpasync, warp
19
- import cutlass.utils as utils_basic
20
- from cutlass.base_dsl.arch import Arch
21
- from cutlass.cutlass_dsl import BaseDSL
22
-
23
- from quack import copy_utils
24
- from quack import layout_utils
25
-
26
- from flash_attn.cute import ampere_helpers as sm80_utils
27
- from flash_attn.cute.cute_dsl_utils import assume_tensor_aligned
28
- from flash_attn.cute import utils
29
- from flash_attn.cute.mask import AttentionMask
30
- from flash_attn.cute.softmax import Softmax, apply_score_mod_inner
31
- from flash_attn.cute.seqlen_info import SeqlenInfoQK
32
- from flash_attn.cute.block_info import BlockInfo
33
- from flash_attn.cute.pack_gqa import PackGQA, pack_gqa_layout
34
- from flash_attn.cute.named_barrier import NamedBarrierFwd
35
- from flash_attn.cute.block_sparsity import BlockSparseTensors
36
- from flash_attn.cute.tile_scheduler import SingleTileScheduler, SingleTileVarlenScheduler, TileSchedulerArguments
37
- from flash_attn.cute.utils import AuxData
38
-
39
-
40
- class FlashAttentionForwardBase:
41
-
42
- def __init__(
43
- self,
44
- dtype: Type[cutlass.Numeric],
45
- head_dim: int,
46
- head_dim_v: Optional[int] = None,
47
- qhead_per_kvhead: int = 1,
48
- is_causal: bool = False,
49
- is_local: bool = False,
50
- pack_gqa: bool = True,
51
- tile_m: int = 128,
52
- tile_n: int = 128,
53
- num_stages: int = 1,
54
- num_threads: int = 128,
55
- Q_in_regs: bool = False,
56
- score_mod: Optional[cutlass.Constexpr] = None,
57
- mask_mod: Optional[cutlass.Constexpr] = None,
58
- has_aux_tensors: bool = False,
59
- q_subtile_factor: int = 1,
60
- ):
61
- """Initializes the configuration for a flash attention kernel.
62
-
63
- All contiguous dimensions must be at least 16 bytes aligned, which means that the head dimension
64
- should be a multiple of 8.
65
-
66
- :param head_dim: head dimension
67
- :type head_dim: int
68
- :param tile_m: m block size
69
- :type tile_m: int
70
- :param tile_n: n block size
71
- :type tile_n: int
72
- :param num_threads: number of threads
73
- :type num_threads: int
74
- :param is_causal: is causal
75
- :param score_mod: A callable that takes the attention scores and applies a modification.
76
- Callable signature: ``score_mod(scores, batch_idx, head_idx, q_idx, kv_idx, aux_tensors) -> Any``
77
- :param mask_mod: A callable that takes the attention scores and returns a boolean representing whether that score should be masked.
78
- Callable signature: ``mask_mod(batch_idx, head_idx, q_idx, kv_idx, aux_tensors) -> Boolean``
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 = is_local
93
- self.pack_gqa = pack_gqa
94
- self.tile_m = tile_m
95
- self.tile_n = tile_n
96
- self.num_threads = num_threads
97
- self.num_stages = num_stages
98
- self.q_subtile_factor = q_subtile_factor
99
- self.Q_in_regs = Q_in_regs
100
- self.score_mod = score_mod
101
- self.mask_mod = mask_mod
102
- self.qk_acc_dtype = Float32
103
- self.score_vec_size: cutlass.Constexpr = getattr(
104
- score_mod, "__vec_size__", 1 if cutlass.const_expr(has_aux_tensors) else 2
105
- )
106
- if self.score_vec_size > 2:
107
- raise ValueError(
108
- f"score_mod vec_size {self.score_vec_size} not supported on Sm80/90/120 "
109
- "due to accumulator thread ownership pattern."
110
- )
111
- self.mask_vec_size: cutlass.Constexpr = getattr(mask_mod, "__vec_size__", 1)
112
- if self.mask_vec_size > 1:
113
- raise ValueError(
114
- f"mask_mod vec_size {self.mask_vec_size} not supported on Sm80/90/120 "
115
- "due to accumulator thread ownership pattern."
116
- )
117
- self.arch = BaseDSL._get_dsl().get_arch_enum()
118
-
119
- @staticmethod
120
- def can_implement(
121
- dtype,
122
- head_dim,
123
- head_dim_v,
124
- tile_m,
125
- tile_n,
126
- num_stages,
127
- num_threads,
128
- is_causal,
129
- Q_in_regs=False,
130
- ) -> bool:
131
- """Check if the kernel can be implemented with the given parameters.
132
-
133
- :param dtype: data type
134
- :type dtype: cutlass.Numeric
135
- :param head_dim: head dimension
136
- :type head_dim: int
137
- :param tile_m: m block size
138
- :type tile_m: int
139
- :param tile_n: n block size
140
- :type tile_n: int
141
- :param num_threads: number of threads
142
- :type num_threads: int
143
- :param is_causal: is causal
144
- :type is_causal: bool
145
-
146
- :return: True if the kernel can be implemented, False otherwise
147
- :rtype: bool
148
- """
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
- # Check if block size setting is out of shared memory capacity
160
- # Shared memory usage: Q tile + (K tile + V tile) where K and V use the same tile size
161
- smem_usage_Q = tile_m * head_dim * 2
162
- smem_usage_K = tile_n * head_dim * num_stages * 2
163
- smem_usage_V = tile_n * head_dim_v * num_stages * 2
164
- smem_usage_QV = (
165
- (smem_usage_Q + smem_usage_V) if not Q_in_regs else max(smem_usage_Q, smem_usage_V)
166
- )
167
- smem_usage = smem_usage_QV + smem_usage_K
168
- # TODO: sm86 and sm89
169
- smem_capacity = utils_basic.get_smem_capacity_in_bytes("sm_80")
170
- if smem_usage > smem_capacity:
171
- return False
172
- # Check if twice the block size is divisible by the number of threads
173
- if (tile_m * 2) % num_threads != 0:
174
- return False
175
- return True
176
-
177
- def _check_type(
178
- self,
179
- mQ_type: Type[cutlass.Numeric],
180
- mK_type: Type[cutlass.Numeric],
181
- mV_type: Type[cutlass.Numeric],
182
- mO_type: Type[cutlass.Numeric],
183
- mLSE_type: Type[cutlass.Numeric] | None,
184
- mCuSeqlensQ_type: Type[cutlass.Numeric] | None,
185
- mCuSeqlensK_type: Type[cutlass.Numeric] | None,
186
- mSeqUsedQ_type: Type[cutlass.Numeric] | None,
187
- mSeqUsedK_type: Type[cutlass.Numeric] | None,
188
- ):
189
- # Get the data type and check if it is fp16 or bf16
190
- if const_expr(not (mQ_type == mK_type == mV_type == mO_type)):
191
- raise TypeError("All tensors must have the same data type")
192
- if const_expr(mQ_type not in [cutlass.Float16, cutlass.BFloat16]):
193
- raise TypeError("Only Float16 or BFloat16 is supported")
194
- if const_expr(mLSE_type not in [None, Float32]):
195
- raise TypeError("LSE tensor must be Float32")
196
- if const_expr(mCuSeqlensQ_type not in [None, Int32]):
197
- raise TypeError("cu_seqlens_q tensor must be Int32")
198
- if const_expr(mCuSeqlensK_type not in [None, Int32]):
199
- raise TypeError("cu_seqlens_k tensor must be Int32")
200
- if const_expr(mSeqUsedQ_type not in [None, Int32]):
201
- raise TypeError("seqused_q tensor must be Int32")
202
- if const_expr(mSeqUsedK_type not in [None, Int32]):
203
- raise TypeError("seqused_k tensor must be Int32")
204
- assert mQ_type == self.dtype
205
-
206
- def _setup_attributes(self):
207
- # ///////////////////////////////////////////////////////////////////////////////
208
- # Shared memory layout: Q/K/V
209
- # ///////////////////////////////////////////////////////////////////////////////
210
- sQ_layout_atom, sK_layout_atom, sV_layout_atom, sO_layout_atom, sP_layout_atom = (
211
- self._get_smem_layout_atom()
212
- )
213
- self.sQ_layout = cute.tile_to_shape(
214
- sQ_layout_atom,
215
- (self.tile_m, self.tile_hdim),
216
- (0, 1),
217
- )
218
- self.sK_layout = cute.tile_to_shape(
219
- sK_layout_atom,
220
- (self.tile_n, self.tile_hdim, self.num_stages),
221
- (0, 1, 2),
222
- )
223
- self.sV_layout = cute.tile_to_shape(
224
- sV_layout_atom,
225
- (self.tile_n, self.tile_hdimv, self.num_stages),
226
- (0, 1, 2),
227
- )
228
- self.sO_layout = cute.tile_to_shape(
229
- sO_layout_atom,
230
- (self.tile_m, self.tile_hdimv),
231
- (0, 1),
232
- )
233
- if const_expr(sP_layout_atom is not None):
234
- self.sP_layout = cute.tile_to_shape(
235
- sP_layout_atom,
236
- (self.tile_m, self.tile_n),
237
- (0, 1),
238
- )
239
- else:
240
- self.sP_layout = None
241
-
242
- # ///////////////////////////////////////////////////////////////////////////////
243
- # GMEM Tiled copy:
244
- # ///////////////////////////////////////////////////////////////////////////////
245
- # Thread layouts for copies
246
- universal_copy_bits = 128
247
- async_copy_elems = universal_copy_bits // self.dtype.width
248
- # atom_async_copy: async copy atom for QKV load
249
- atom_async_copy = cute.make_copy_atom(
250
- cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
251
- self.dtype,
252
- num_bits_per_copy=universal_copy_bits,
253
- )
254
- # atom_universal_copy: universal copy atom for O store
255
- atom_universal_copy = cute.make_copy_atom(
256
- cute.nvgpu.CopyUniversalOp(),
257
- self.dtype,
258
- num_bits_per_copy=universal_copy_bits,
259
- )
260
- # tQ_layout and tK_layout: thread layout for QK load
261
- tQK_shape_dim_1 = sQ_layout_atom.outer.shape[1] // async_copy_elems
262
- assert self.num_Q_load_threads % tQK_shape_dim_1 == 0, (
263
- "num_threads must be divisible by tQK_shape_dim_1"
264
- )
265
- assert self.num_producer_threads % tQK_shape_dim_1 == 0, (
266
- "num_threads must be divisible by tQK_shape_dim_1"
267
- )
268
- tQ_layout = cute.make_ordered_layout(
269
- (self.num_Q_load_threads // tQK_shape_dim_1, tQK_shape_dim_1),
270
- order=(1, 0),
271
- )
272
- tK_layout = cute.make_ordered_layout(
273
- (self.num_producer_threads // tQK_shape_dim_1, tQK_shape_dim_1),
274
- order=(1, 0),
275
- )
276
- # So that we don't have to check if we overshoot kBlockM when we load Q
277
- assert self.tile_m % tQ_layout.shape[0] == 0
278
- tV_shape_dim_1 = sV_layout_atom.outer.shape[1] // async_copy_elems
279
- tV_layout = cute.make_ordered_layout(
280
- (self.num_producer_threads // tV_shape_dim_1, tV_shape_dim_1),
281
- order=(1, 0),
282
- )
283
- # TODO: need a different layout for O if O dtype is not the same as V dtype
284
- # tO_layout: thread layout for O store
285
- tO_layout = cute.make_ordered_layout(
286
- (self.num_epilogue_threads // tV_shape_dim_1, tV_shape_dim_1),
287
- order=(1, 0),
288
- )
289
- # So that we don't have to check if we overshoot kBlockM when we store O
290
- assert self.tile_m % tO_layout.shape[0] == 0
291
-
292
- # Value layouts for copies
293
- vQKV_layout = cute.make_layout((1, async_copy_elems))
294
- vO_layout = vQKV_layout
295
-
296
- self.gmem_tiled_copy_Q = cute.make_tiled_copy_tv(atom_async_copy, tQ_layout, vQKV_layout)
297
- self.gmem_tiled_copy_K = cute.make_tiled_copy_tv(atom_async_copy, tK_layout, vQKV_layout)
298
- self.gmem_tiled_copy_V = cute.make_tiled_copy_tv(atom_async_copy, tV_layout, vQKV_layout)
299
- # gmem_tiled_copy_O: tiled copy for O store
300
- self.gmem_tiled_copy_O = cute.make_tiled_copy_tv(atom_universal_copy, tO_layout, vO_layout)
301
-
302
- def _get_smem_layout_atom(self):
303
- raise NotImplementedError()
304
-
305
- def _get_tiled_mma(self):
306
- raise NotImplementedError()
307
-
308
- def _get_shared_storage_cls(self):
309
- raise NotImplementedError()
310
-
311
- @cute.jit
312
- def __call__(
313
- self,
314
- mQ: cute.Tensor,
315
- mK: cute.Tensor,
316
- mV: cute.Tensor,
317
- mO: cute.Tensor,
318
- mLSE: Optional[cute.Tensor],
319
- softmax_scale: Float32,
320
- # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
321
- stream: cuda.CUstream = None,
322
- ):
323
- """Configures and launches the flash attention kernel.
324
-
325
- mQ/mK/mV/mO has same data types(supports fp16 and bf16) and same layout:
326
- (batch_size, seqlen_q, num_head, head_dim):(_, _, _, 1)
327
- """
328
- raise NotImplementedError()
329
-
330
- @cute.jit
331
- def epilogue(
332
- self,
333
- acc_O: cute.Tensor,
334
- lse: cute.Tensor,
335
- mO: cute.Tensor,
336
- mLSE: Optional[cute.Tensor],
337
- sO: cute.Tensor,
338
- seqlen: SeqlenInfoQK,
339
- gmem_tiled_copy_O: cute.TiledCopy,
340
- tma_atom_O: Optional[cute.CopyAtom],
341
- tiled_mma: cute.TiledMma,
342
- tidx: Int32,
343
- m_block: Int32,
344
- head_idx: Int32,
345
- batch_idx: Int32,
346
- ):
347
- # store acc_O
348
- rO = cute.make_fragment_like(acc_O, self.dtype)
349
- rO.store(acc_O.load().to(self.dtype))
350
- # Make sure all threads have finished reading V
351
- cute.arch.barrier(
352
- barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_epilogue_threads
353
- )
354
- smem_copy_atom_O = utils.get_smem_store_atom(self.arch.major * 10 + self.arch.minor, self.dtype)
355
- smem_thr_copy_O = cute.make_tiled_copy_C(smem_copy_atom_O, tiled_mma).get_slice(tidx)
356
- taccOrO = smem_thr_copy_O.retile(rO)
357
- taccOsO = smem_thr_copy_O.partition_D(sO)
358
- # taccOsO = copy_utils.partition_D_position_independent(smem_thr_copy_O, sO)
359
- # copy acc O from rmem to smem with the smem copy atom
360
- cute.copy(smem_copy_atom_O, taccOrO, taccOsO)
361
-
362
- cO = cute.make_identity_tensor((self.tile_m, self.tile_hdimv))
363
- pack_gqa = PackGQA(
364
- self.tile_m, self.tile_hdimv, self.check_hdim_v_oob, self.qhead_per_kvhead
365
- )
366
-
367
- # Write LSE from rmem -> gmem
368
- if const_expr(mLSE is not None):
369
- mLSE_cur = seqlen.offset_batch_Q(mLSE, batch_idx, dim=2)[None, head_idx]
370
- if const_expr(not self.pack_gqa):
371
- gLSE = cute.local_tile(mLSE_cur, (self.tile_m,), (m_block,))
372
- gLSE_expanded_layout = cute.append(
373
- gLSE.layout, cute.make_layout((self.tile_hdimv,), stride=(0,))
374
- )
375
- gLSE_expanded = cute.make_tensor(gLSE.iterator, gLSE_expanded_layout)
376
- thr_mma = tiled_mma.get_slice(tidx)
377
- taccOgLSE = layout_utils.reshape_acc_to_mn(thr_mma.partition_C(gLSE_expanded))
378
- assert cute.size(taccOgLSE, mode=[0]) == cute.size(lse)
379
- taccOcO = layout_utils.reshape_acc_to_mn(thr_mma.partition_C(cO))
380
- t0accOcO = layout_utils.reshape_acc_to_mn(thr_mma.get_slice(0).partition_C(cO))
381
- # Only the thread corresponding to column 0 writes out the lse to gmem
382
- if taccOcO[0][1] == 0:
383
- for m in cutlass.range(cute.size(taccOgLSE.shape[1]), unroll_full=True):
384
- if (
385
- t0accOcO[m, 0][0]
386
- < seqlen.seqlen_q - m_block * self.tile_m - taccOcO[0][0]
387
- ):
388
- taccOgLSE[m, 0] = lse[m]
389
- else:
390
- pack_gqa.store_LSE(mLSE_cur, lse, tiled_mma, tidx, m_block, seqlen.seqlen_q)
391
-
392
- ragged = self.use_tma_O and (seqlen.has_cu_seqlens_q or seqlen.has_seqused_q)
393
- mO_cur = seqlen.offset_batch_Q(mO, batch_idx, dim=3, ragged=ragged)[None, None, head_idx]
394
- # thr_mma = tiled_mma.get_slice(tidx)
395
- # taccOgO = thr_mma.partition_C(gO)
396
- # cute.autovec_copy(rO, taccOgO)
397
- # sync to make sure all smem stores are done
398
- if const_expr(self.use_tma_O):
399
- # ensure smem writes are visible to TMA
400
- cute.arch.fence_view_async_shared()
401
- cute.arch.barrier_arrive(
402
- barrier_id=int(NamedBarrierFwd.Epilogue),
403
- number_of_threads=self.num_epilogue_threads + cute.arch.WARP_SIZE,
404
- )
405
- gO = cute.local_tile(mO_cur, (self.tile_m, self.tile_hdimv), (m_block, 0))
406
- store_O, _, _ = copy_utils.tma_get_copy_fn(
407
- tma_atom_O, 0, cute.make_layout(1), sO, gO, single_stage=True
408
- )
409
- warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
410
- if warp_idx == 4:
411
- cute.arch.barrier(
412
- barrier_id=int(NamedBarrierFwd.Epilogue),
413
- number_of_threads=self.num_epilogue_threads + cute.arch.WARP_SIZE,
414
- )
415
- store_O()
416
- cute.arch.cp_async_bulk_commit_group()
417
- cute.arch.cp_async_bulk_wait_group(0, read=True)
418
- else:
419
- cute.arch.barrier(
420
- barrier_id=int(NamedBarrierFwd.Epilogue),
421
- number_of_threads=self.num_epilogue_threads,
422
- )
423
- gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
424
- tOsO = gmem_thr_copy_O.partition_S(sO)
425
- tOrO = cute.make_fragment_like(tOsO, self.dtype)
426
- # load acc O from smem to rmem for wider vectorization
427
- cute.autovec_copy(tOsO, tOrO)
428
- if const_expr(not self.pack_gqa):
429
- gO = cute.local_tile(mO_cur, (self.tile_m, self.tile_hdimv), (m_block, 0))
430
- tOgO = gmem_thr_copy_O.partition_D(gO)
431
- tOcO = gmem_thr_copy_O.partition_S(cO)
432
- t0OcO = gmem_tiled_copy_O.get_slice(0).partition_S(cO)
433
- tOpO = utils.predicate_k(tOcO, limit=mO.shape[1])
434
- # copy acc O from rmem to gmem
435
- for rest_m in cutlass.range_constexpr(cute.size(tOrO.shape[1])):
436
- if (
437
- t0OcO[0, rest_m, 0][0]
438
- < seqlen.seqlen_q - m_block * self.tile_m - tOcO[0][0]
439
- ):
440
- cute.copy(
441
- gmem_tiled_copy_O,
442
- tOrO[None, rest_m, None],
443
- tOgO[None, rest_m, None],
444
- pred=tOpO[None, rest_m, None]
445
- if const_expr(self.check_hdim_v_oob)
446
- else None,
447
- )
448
- else:
449
- pack_gqa.store_O(mO_cur, tOrO, gmem_tiled_copy_O, tidx, m_block, seqlen.seqlen_q)
450
-
451
- @cute.jit
452
- def advance_pipeline(self, pipeline_index):
453
- return pipeline_index + 1 if pipeline_index < self.num_stages - 1 else 0
454
-
455
- @cute.jit
456
- def load_Q(
457
- self,
458
- gmem_thr_copy: cute.TiledCopy,
459
- gQ: cute.Tensor,
460
- sQ: cute.Tensor,
461
- block: Int32,
462
- seqlen: Int32,
463
- headdim: Int32,
464
- ):
465
- tQsQ, tQgQ = gmem_thr_copy.partition_D(sQ), gmem_thr_copy.partition_S(gQ)
466
- cQ = cute.make_identity_tensor((self.tile_m, self.tile_hdim))
467
- tQcQ = gmem_thr_copy.partition_S(cQ)
468
- t0QcQ = gmem_thr_copy.get_slice(0).partition_S(cQ)
469
- tQpQ = utils.predicate_k(tQcQ, limit=headdim)
470
- for m in cutlass.range_constexpr(cute.size(tQsQ.shape[1])):
471
- # Instead of using tQcQ, we using t0QcQ and subtract the offset from the limit
472
- # (seqlen - block * kBlockM). This is because the entries of t0QcQ are known at compile time.
473
- if t0QcQ[0, m, 0][0] < seqlen - block * self.tile_m - tQcQ[0][0]:
474
- cute.copy(
475
- gmem_thr_copy,
476
- tQgQ[None, m, None],
477
- tQsQ[None, m, None],
478
- pred=tQpQ[None, m, None] if const_expr(self.check_hdim_oob) else None,
479
- )
480
- # We don't need to clear the sQ smem tiles since we'll only write out the valid outputs
481
-
482
- @cute.jit
483
- def load_K(
484
- self,
485
- gmem_tiled_copy: cute.TiledCopy,
486
- tKgK: cute.Tensor,
487
- tKsK: cute.Tensor,
488
- tKcK: cute.Tensor,
489
- t0KcK: cute.Tensor,
490
- tKpK: cute.Tensor,
491
- block: Int32,
492
- smem_pipe_write: Int32,
493
- seqlen: Int32,
494
- need_predicates: cutlass.Constexpr,
495
- ):
496
- # Do we need to check if we overshoot kBlockN when we load K?
497
- is_even_n_smem_k = self.tile_n % gmem_tiled_copy.tiler_mn[0].shape == 0
498
- if const_expr(need_predicates or not is_even_n_smem_k):
499
- # Instead of using tKcK, we using t0KcK and subtract the offset from the limit
500
- # (seqlen - block * kBlockN). This is because the entries of t0KcK are known at compile time.
501
- if const_expr(is_even_n_smem_k):
502
- seqlen_limit = seqlen - block * self.tile_n
503
- else:
504
- if const_expr(not need_predicates):
505
- seqlen_limit = self.tile_n
506
- else:
507
- seqlen_limit = cutlass.min(seqlen - block * self.tile_n, self.tile_n)
508
- seqlen_limit -= tKcK[0][0]
509
- for n in cutlass.range_constexpr(cute.size(tKsK.shape[1])):
510
- if t0KcK[0, n, 0][0] < seqlen_limit:
511
- cute.copy(
512
- gmem_tiled_copy,
513
- tKgK[None, n, None, block],
514
- tKsK[
515
- None, n, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0
516
- ],
517
- pred=tKpK[None, n, None] if const_expr(self.check_hdim_oob) else None,
518
- )
519
- # We don't need to clear the sK smem tiles since we'll mask out the scores anyway.
520
- else:
521
- cute.copy(
522
- gmem_tiled_copy,
523
- tKgK[None, None, None, block],
524
- tKsK[None, None, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0],
525
- pred=tKpK if const_expr(self.check_hdim_oob) else None,
526
- )
527
-
528
- @cute.jit
529
- def load_V(
530
- self,
531
- gmem_tiled_copy: cute.TiledCopy,
532
- tVgV: cute.Tensor,
533
- tVsV: cute.Tensor,
534
- tVcV: cute.Tensor,
535
- t0VcV: cute.Tensor,
536
- tVpV: cute.Tensor,
537
- block: Int32,
538
- smem_pipe_write: Int32,
539
- seqlen: Int32,
540
- need_predicates: cutlass.Constexpr,
541
- ):
542
- # Do we need to check if we overshoot kBlockN when we load V?
543
- is_even_n_smem_v = self.tile_n % gmem_tiled_copy.tiler_mn[0].shape == 0
544
- if const_expr(need_predicates or not is_even_n_smem_v):
545
- for n in cutlass.range_constexpr(cute.size(tVsV.shape[1])):
546
- # If kBlockN doesn't evenly divide the tiled copy, only the last `n` needs to be checked
547
- if (
548
- is_even_n_smem_v
549
- or n < cute.size(tVsV.shape[1]) - 1
550
- or tVcV[0, n, 0][0] < self.tile_n
551
- ):
552
- predicate = tVpV[None, n, None] if const_expr(self.check_hdim_v_oob) else None
553
- if const_expr(need_predicates):
554
- seqlen_limit = seqlen - block * self.tile_n - tVcV[0][0]
555
- predicate_n = t0VcV[0, n, 0][0] < seqlen_limit
556
- predicate = cute.make_fragment_like(tVpV[None, 0, None])
557
- for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
558
- for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
559
- predicate[i, k] = (
560
- tVpV[i, n, k] if const_expr(self.check_hdim_v_oob) else True
561
- ) and predicate_n
562
- cute.copy(
563
- gmem_tiled_copy,
564
- tVgV[None, n, None, block],
565
- tVsV[
566
- None, n, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0
567
- ],
568
- pred=predicate,
569
- )
570
- else:
571
- cute.copy(
572
- gmem_tiled_copy,
573
- tVgV[None, None, None, block],
574
- tVsV[None, None, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0],
575
- pred=tVpV if const_expr(self.check_hdim_v_oob) else None,
576
- )
577
-
578
-
579
- class FlashAttentionForwardSm80(FlashAttentionForwardBase):
580
- def _get_smem_layout_atom(self):
581
- sQ_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.tile_hdim)
582
- sK_layout_atom = sQ_layout_atom
583
- sV_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.tile_hdimv)
584
- sO_layout_atom = sV_layout_atom
585
- sP_layout_atom = None
586
- return sQ_layout_atom, sK_layout_atom, sV_layout_atom, sO_layout_atom, sP_layout_atom
587
-
588
- def _get_tiled_mma(self):
589
- tiled_mma_qk = cute.make_tiled_mma(
590
- warp.MmaF16BF16Op(self.dtype, Float32, (16, 8, 16)),
591
- (self.num_threads // 32, 1, 1),
592
- permutation_mnk=(self.num_threads // 32 * 16, 16, 16),
593
- )
594
- tiled_mma_pv = cute.make_tiled_mma(
595
- warp.MmaF16BF16Op(self.dtype, Float32, (16, 8, 16)),
596
- (self.num_threads // 32, 1, 1),
597
- permutation_mnk=(self.num_threads // 32 * 16, 16, 16),
598
- )
599
- return tiled_mma_qk, tiled_mma_pv
600
-
601
- def _get_shared_storage_cls(self):
602
- sQ_struct, sK_struct, sV_struct = [
603
- cute.struct.Align[cute.struct.MemRange[self.dtype, cute.cosize(layout)], 1024]
604
- for layout in (self.sQ_layout, self.sK_layout, self.sV_layout)
605
- ]
606
- cosize_sQV = max(cute.cosize(self.sQ_layout), cute.cosize(self.sV_layout))
607
- sQV_struct = cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sQV], 1024]
608
-
609
- @cute.struct
610
- class SharedStorageQKV:
611
- sV: sV_struct
612
- sQ: sQ_struct
613
- sK: sK_struct
614
-
615
- @cute.struct
616
- class SharedStorageSharedQV:
617
- sQ: sQV_struct
618
- sK: sK_struct
619
-
620
- return SharedStorageQKV if const_expr(not self.Q_in_regs) else SharedStorageSharedQV
621
-
622
- @cute.jit
623
- def __call__(
624
- self,
625
- mQ: cute.Tensor,
626
- mK: cute.Tensor,
627
- mV: cute.Tensor,
628
- mO: cute.Tensor,
629
- mLSE: Optional[cute.Tensor],
630
- softmax_scale: Float32,
631
- mCuSeqlensQ: Optional[cute.Tensor] = None,
632
- mCuSeqlensK: Optional[cute.Tensor] = None,
633
- mSeqUsedQ: Optional[cute.Tensor] = None,
634
- mSeqUsedK: Optional[cute.Tensor] = None,
635
- mPageTable: Optional[cute.Tensor] = None,
636
- window_size_left: Int32 | int | None = None,
637
- window_size_right: Int32 | int | None = None,
638
- learnable_sink: Optional[cute.Tensor] = None,
639
- blocksparse_tensors: Optional[BlockSparseTensors] = None,
640
- aux_data: AuxData = AuxData(),
641
- # Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
642
- stream: cuda.CUstream = None,
643
- ):
644
- """Configures and launches the flash attention kernel.
645
-
646
- mQ/mK/mV/mO has same data types(supports fp16 and bf16) and same layout:
647
- (batch_size, seqlen_q, num_head, head_dim):(_, _, _, 1)
648
- """
649
- assert learnable_sink is None, "Learnable sink is not supported in this kernel"
650
- self._check_type(
651
- *(t.element_type if t is not None else None for t in (mQ, mK, mV, mO, mLSE, mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK))
652
- )
653
- tiled_mma_qk, tiled_mma_pv = self._get_tiled_mma()
654
- self.num_mma_threads = tiled_mma_pv.size
655
- self.num_producer_threads = self.num_threads
656
- self.num_Q_load_threads = self.num_threads
657
- self.num_epilogue_threads = self.num_threads
658
- self.use_tma_O = Arch.sm_90 <= self.arch < Arch.sm_120
659
- self._setup_attributes()
660
- SharedStorage = self._get_shared_storage_cls()
661
- mQ, mK, mV, mO = [assume_tensor_aligned(t) for t in (mQ, mK, mV, mO)]
662
- # Layout permutation: 4D non-varlen vs 3D varlen
663
- QO_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 2, 1]
664
- KV_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensK is None) else [0, 2, 1]
665
- mQ, mO = [
666
- cute.make_tensor(t.iterator, cute.select(t.layout, mode=QO_layout_transpose))
667
- for t in (mQ, mO)
668
- ]
669
- mK, mV = [
670
- cute.make_tensor(t.iterator, cute.select(t.layout, mode=KV_layout_transpose))
671
- for t in (mK, mV)
672
- ]
673
- if const_expr(mLSE is not None):
674
- LSE_layout_transpose = [2, 1, 0] if const_expr(mCuSeqlensQ is None) else [1, 0]
675
- mLSE = cute.make_tensor(mLSE.iterator, cute.select(mLSE.layout, mode=LSE_layout_transpose))
676
- if const_expr(self.pack_gqa):
677
- nheads_kv = mK.shape[2]
678
- mQ = pack_gqa_layout(mQ, self.qhead_per_kvhead, nheads_kv, head_idx=2)
679
- mO = pack_gqa_layout(mO, self.qhead_per_kvhead, nheads_kv, head_idx=2)
680
- if const_expr(mLSE is not None):
681
- mLSE = pack_gqa_layout(mLSE, self.qhead_per_kvhead, nheads_kv, head_idx=1)
682
- # TileScheduler for varlen, simple grid for non-varlen
683
- if const_expr(mCuSeqlensQ is not None or mSeqUsedQ is not None):
684
- TileScheduler = SingleTileVarlenScheduler
685
- else:
686
- TileScheduler = SingleTileScheduler
687
- num_batch = (
688
- mCuSeqlensQ.shape[0] - 1
689
- if const_expr(mCuSeqlensQ is not None)
690
- else cute.size(mQ.shape[3])
691
- )
692
- tile_sched_args = TileSchedulerArguments(
693
- num_block=cute.ceil_div(cute.size(mQ.shape[0]), self.tile_m),
694
- num_head=cute.size(mQ.shape[2]),
695
- num_batch=num_batch,
696
- num_splits=1,
697
- seqlen_k=0,
698
- headdim=mQ.shape[1],
699
- headdim_v=mV.shape[1],
700
- total_q=cute.size(mQ.shape[0])
701
- if const_expr(mCuSeqlensQ is not None)
702
- else cute.size(mQ.shape[0]) * cute.size(mQ.shape[3]),
703
- tile_shape_mn=(self.tile_m, self.tile_n),
704
- qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
705
- mCuSeqlensQ=mCuSeqlensQ,
706
- mSeqUsedQ=mSeqUsedQ,
707
- )
708
- tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
709
- grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
710
- softmax_scale_log2, softmax_scale = utils.compute_softmax_scale_log2(softmax_scale, self.score_mod)
711
- fastdiv_mods = utils.compute_fastdiv_mods(mQ, mK, self.qhead_per_kvhead, self.pack_gqa, aux_data.tensors)
712
-
713
- self.kernel(
714
- mQ,
715
- mK,
716
- mV,
717
- mO,
718
- mLSE,
719
- mCuSeqlensQ,
720
- mCuSeqlensK,
721
- mSeqUsedQ,
722
- mSeqUsedK,
723
- softmax_scale_log2,
724
- softmax_scale,
725
- window_size_left,
726
- window_size_right,
727
- self.sQ_layout,
728
- self.sK_layout,
729
- self.sV_layout,
730
- self.sO_layout,
731
- self.sP_layout,
732
- self.gmem_tiled_copy_Q,
733
- self.gmem_tiled_copy_K,
734
- self.gmem_tiled_copy_V,
735
- self.gmem_tiled_copy_O,
736
- tiled_mma_qk,
737
- tiled_mma_pv,
738
- SharedStorage,
739
- tile_sched_params,
740
- TileScheduler,
741
- aux_data,
742
- fastdiv_mods,
743
- ).launch(
744
- grid=grid_dim,
745
- block=[self.num_threads, 1, 1],
746
- smem=SharedStorage.size_in_bytes(),
747
- stream=stream,
748
- )
749
-
750
- @cute.kernel
751
- def kernel(
752
- self,
753
- mQ: cute.Tensor,
754
- mK: cute.Tensor,
755
- mV: cute.Tensor,
756
- mO: cute.Tensor,
757
- mLSE: Optional[cute.Tensor],
758
- mCuSeqlensQ: Optional[cute.Tensor],
759
- mCuSeqlensK: Optional[cute.Tensor],
760
- mSeqUsedQ: Optional[cute.Tensor],
761
- mSeqUsedK: Optional[cute.Tensor],
762
- softmax_scale_log2: Float32,
763
- softmax_scale: Optional[Float32],
764
- window_size_left: Optional[Int32],
765
- window_size_right: Optional[Int32],
766
- sQ_layout: cute.ComposedLayout,
767
- sK_layout: cute.ComposedLayout,
768
- sV_layout: cute.ComposedLayout,
769
- sO_layout: cute.ComposedLayout,
770
- sP_layout: cute.ComposedLayout | None,
771
- gmem_tiled_copy_Q: cute.TiledCopy,
772
- gmem_tiled_copy_K: cute.TiledCopy,
773
- gmem_tiled_copy_V: cute.TiledCopy,
774
- gmem_tiled_copy_O: cute.TiledCopy,
775
- tiled_mma_qk: cute.TiledMma,
776
- tiled_mma_pv: cute.TiledMma,
777
- SharedStorage: cutlass.Constexpr,
778
- tile_sched_params,
779
- TileScheduler: cutlass.Constexpr[Callable],
780
- aux_data: AuxData = AuxData(),
781
- fastdiv_mods=None,
782
- ):
783
- # Thread index, block index
784
- tidx, _, _ = cute.arch.thread_idx()
785
-
786
- tile_scheduler = TileScheduler.create(tile_sched_params)
787
- work_tile = tile_scheduler.initial_work_tile_info()
788
- m_block, num_head, batch_size, _ = work_tile.tile_idx
789
-
790
- # Spark fix: surplus scheduler tiles must not read sequence metadata.
791
- if work_tile.is_valid_tile:
792
-
793
- block_info = BlockInfo(
794
- self.tile_m,
795
- self.tile_n,
796
- self.is_causal,
797
- self.is_local,
798
- False, # is_split_kv
799
- window_size_left,
800
- window_size_right,
801
- qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
802
- )
803
- seqlen = SeqlenInfoQK.create(
804
- batch_idx=batch_size,
805
- seqlen_q_static=mQ.shape[0] if const_expr(not self.pack_gqa) else mQ.shape[0][1],
806
- seqlen_k_static=mK.shape[0],
807
- mCuSeqlensQ=mCuSeqlensQ,
808
- mCuSeqlensK=mCuSeqlensK,
809
- mSeqUsedQ=mSeqUsedQ,
810
- mSeqUsedK=mSeqUsedK,
811
- )
812
- n_block_min, n_block_max = block_info.get_n_block_min_max(seqlen, m_block)
813
- # For varlen, wasted grid tiles (where batch_idx >= num_batch) will have
814
- # seqlen_q=seqlen_k=0 and n_block_max=0. Clamp to 0 so we don't use a
815
- # negative block index for K/V loads; the load/store predicates already
816
- # guard all memory accesses when seqlen is 0.
817
- n_block = cutlass.max(n_block_max - 1, 0)
818
-
819
- # ///////////////////////////////////////////////////////////////////////////////
820
- # Get the appropriate tiles for this thread block.
821
- # ///////////////////////////////////////////////////////////////////////////////
822
- blkQ_shape = (self.tile_m, self.tile_hdim)
823
- blkK_shape = (self.tile_n, self.tile_hdim)
824
- blkV_shape = (self.tile_n, self.tile_hdimv)
825
- num_head_kv = num_head if const_expr(self.pack_gqa) else num_head // self.qhead_per_kvhead
826
- mQ_cur = seqlen.offset_batch_Q(mQ, batch_size, dim=3)[None, None, num_head]
827
- if const_expr(not seqlen.has_cu_seqlens_k):
828
- mK_cur = mK[None, None, num_head_kv, batch_size]
829
- mV_cur = mV[None, None, num_head_kv, batch_size]
830
- else:
831
- mK_cur = cute.domain_offset((seqlen.offset_k, 0), mK[None, None, num_head_kv])
832
- mV_cur = cute.domain_offset((seqlen.offset_k, 0), mV[None, None, num_head_kv])
833
- if const_expr(not self.pack_gqa):
834
- gQ = cute.local_tile(mQ_cur, blkQ_shape, (m_block, 0))
835
- gK = cute.local_tile(mK_cur, blkK_shape, (None, 0))
836
- gV = cute.local_tile(mV_cur, blkV_shape, (None, 0))
837
-
838
- # ///////////////////////////////////////////////////////////////////////////////
839
- # Get shared memory buffer
840
- # ///////////////////////////////////////////////////////////////////////////////
841
- smem = cutlass.utils.SmemAllocator()
842
- storage = smem.allocate(SharedStorage)
843
- sQ = storage.sQ.get_tensor(sQ_layout)
844
- sK = storage.sK.get_tensor(sK_layout)
845
- if const_expr(not self.Q_in_regs):
846
- sV = storage.sV.get_tensor(sV_layout)
847
- else:
848
- sV = cute.make_tensor(cute.recast_ptr(sQ.iterator, dtype=self.dtype), sV_layout)
849
- # Transpose view of V to tensor with layout (head_dim_v, tile_n) for tiled mma
850
- sVt = layout_utils.transpose_view(sV)
851
-
852
- gmem_thr_copy_K = gmem_tiled_copy_K.get_slice(tidx)
853
- gmem_thr_copy_V = gmem_tiled_copy_V.get_slice(tidx)
854
- # (CPY_Atom, CPY_N, CPY_K, n_block)
855
- tKsK, tKgK = gmem_thr_copy_K.partition_D(sK), gmem_thr_copy_K.partition_S(gK)
856
- # (CPY_Atom, CPY_N, CPY_K, n_block)
857
- tVsV, tVgV = gmem_thr_copy_V.partition_D(sV), gmem_thr_copy_V.partition_S(gV)
858
-
859
- # ///////////////////////////////////////////////////////////////////////////////
860
- # Tile MMA compute thread partitions and allocate accumulators
861
- # ///////////////////////////////////////////////////////////////////////////////
862
- thr_mma_qk = tiled_mma_qk.get_slice(tidx)
863
- thr_mma_pv = tiled_mma_pv.get_slice(tidx)
864
- tSrQ = thr_mma_qk.make_fragment_A(thr_mma_qk.partition_A(sQ))
865
- tSrK = thr_mma_qk.make_fragment_B(thr_mma_qk.partition_B(sK[None, None, 0]))
866
- tOrVt = thr_mma_pv.make_fragment_B(thr_mma_pv.partition_B(sVt[None, None, 0]))
867
- acc_shape_O = thr_mma_pv.partition_shape_C((self.tile_m, self.tile_hdimv))
868
- acc_O = cute.make_rmem_tensor(acc_shape_O, Float32)
869
- acc_O.fill(0.0)
870
-
871
- # ///////////////////////////////////////////////////////////////////////////////
872
- # Smem copy atom tiling
873
- # ///////////////////////////////////////////////////////////////////////////////
874
- smem_copy_atom_QK = cute.make_copy_atom(
875
- warp.LdMatrix8x8x16bOp(transpose=False, num_matrices=4),
876
- self.dtype,
877
- )
878
- smem_copy_atom_V = cute.make_copy_atom(
879
- warp.LdMatrix8x8x16bOp(transpose=True, num_matrices=4),
880
- self.dtype,
881
- )
882
- smem_thr_copy_Q = utils.make_tiled_copy_A(smem_copy_atom_QK, tiled_mma_qk).get_slice(tidx)
883
- smem_thr_copy_K = utils.make_tiled_copy_B(smem_copy_atom_QK, tiled_mma_qk).get_slice(tidx)
884
- smem_thr_copy_V = utils.make_tiled_copy_B(smem_copy_atom_V, tiled_mma_pv).get_slice(tidx)
885
-
886
- tSsQ = smem_thr_copy_Q.partition_S(sQ)
887
- tSsK = smem_thr_copy_K.partition_S(sK)
888
- tOsVt = smem_thr_copy_V.partition_S(sVt)
889
-
890
- # ///////////////////////////////////////////////////////////////////////////////
891
- # Predicate: Mark indices that need to copy when problem_shape isn't a multiple
892
- # of tile_shape
893
- # ///////////////////////////////////////////////////////////////////////////////
894
- # Construct identity layout for KV
895
- cK = cute.make_identity_tensor((self.tile_n, self.tile_hdim))
896
- tKcK = gmem_thr_copy_K.partition_S(cK)
897
- t0KcK = gmem_thr_copy_K.get_slice(0).partition_S(cK)
898
- if const_expr(self.tile_hdim == self.tile_hdimv):
899
- tVcV = tKcK
900
- t0VcV = t0KcK
901
- else:
902
- cV = cute.make_identity_tensor((self.tile_n, self.tile_hdimv))
903
- tVcV = gmem_thr_copy_V.partition_S(cV)
904
- t0VcV = gmem_thr_copy_V.get_slice(0).partition_S(cV)
905
- # Allocate predicate tensors for m and n, here we only allocate the tile of k, and
906
- # use "if" on the mn dimension.
907
- # This is to reduce register pressure and gets 2-3% performance gain.
908
- tKpK = utils.predicate_k(tKcK, limit=mK.shape[1])
909
- if const_expr(self.same_hdim_kv):
910
- tVpV = tKpK
911
- else:
912
- tVpV = utils.predicate_k(tVcV, limit=mV.shape[1])
913
-
914
- # shape: (atom_v_m * rest_m)
915
- softmax = Softmax.create(
916
- softmax_scale_log2,
917
- num_rows=acc_O.shape[0][0] * acc_O.shape[1],
918
- softmax_scale=softmax_scale,
919
- )
920
- softmax.reset()
921
-
922
- # group parameters for compute_one_n_block
923
- mma_params = SimpleNamespace(
924
- thr_mma_qk=thr_mma_qk,
925
- thr_mma_pv=thr_mma_pv,
926
- tSrQ=tSrQ,
927
- tSrK=tSrK,
928
- tOrVt=tOrVt,
929
- acc_O=acc_O,
930
- )
931
- smem_copy_params = SimpleNamespace(
932
- smem_thr_copy_Q=smem_thr_copy_Q,
933
- smem_thr_copy_K=smem_thr_copy_K,
934
- smem_thr_copy_V=smem_thr_copy_V,
935
- tSsQ=tSsQ,
936
- tSsK=tSsK,
937
- tOsVt=tOsVt,
938
- )
939
- load_K = partial(
940
- self.load_K, gmem_tiled_copy_K, tKgK, tKsK, tKcK, t0KcK, tKpK, seqlen=seqlen.seqlen_k
941
- )
942
- load_V = partial(
943
- self.load_V, gmem_tiled_copy_V, tVgV, tVsV, tVcV, t0VcV, tVpV, seqlen=seqlen.seqlen_k
944
- )
945
-
946
- compute_one_n_block = partial(
947
- self.compute_one_n_block,
948
- mma_params=mma_params,
949
- smem_copy_params=smem_copy_params,
950
- softmax=softmax,
951
- load_K=load_K,
952
- load_V=load_V,
953
- score_mod=self.score_mod,
954
- batch_idx=batch_size,
955
- head_idx=num_head,
956
- m_block=m_block,
957
- aux_data=aux_data,
958
- fastdiv_mods=fastdiv_mods,
959
- )
960
-
961
- # ///////////////////////////////////////////////////////////////////////////////
962
- # Prologue
963
- # ///////////////////////////////////////////////////////////////////////////////
964
- # Start async loads of the last mn-tile, where we take care of the mn residue
965
- gmem_thr_copy_Q = gmem_tiled_copy_Q.get_slice(tidx)
966
- if const_expr(not self.pack_gqa):
967
- self.load_Q(gmem_thr_copy_Q, gQ, sQ, m_block, seqlen=seqlen.seqlen_q, headdim=mQ.shape[1])
968
- else:
969
- pack_gqa = PackGQA(self.tile_m, self.tile_hdim, self.check_hdim_oob, self.qhead_per_kvhead)
970
- pack_gqa.load_Q(mQ_cur, sQ, gmem_tiled_copy_Q, tidx, m_block, seqlen.seqlen_q)
971
- cute.arch.cp_async_commit_group()
972
-
973
- def preprocess_Q():
974
- cute.arch.cp_async_wait_group(self.num_stages * 2 - 1)
975
- if const_expr(self.Q_in_regs):
976
- cute.arch.barrier()
977
- tSrQ_copy_view = smem_thr_copy_Q.retile(tSrQ)
978
- cute.copy(smem_thr_copy_Q, tSsQ, tSrQ_copy_view)
979
-
980
- # If Q_in_regs, we load Q, then load 1 stage of K, then (optionally) rotate Q and
981
- # read from smem_q to registers, then load V.
982
- # If !Q_in_regs, we load Q, load all stages of K & V, then (optionally) rotate Q.
983
- if const_expr(self.Q_in_regs):
984
- load_K(n_block, smem_pipe_write=0, need_predicates=True)
985
- cute.arch.cp_async_commit_group()
986
- preprocess_Q()
987
- cute.arch.barrier() # Make sure all threads have read smem_q before loading V
988
-
989
- for stage in cutlass.range_constexpr(self.num_stages):
990
- if const_expr(not self.Q_in_regs or stage > 0):
991
- if stage == 0 or n_block - stage >= 0:
992
- load_K(n_block - stage, smem_pipe_write=stage, need_predicates=stage == 0)
993
- cute.arch.cp_async_commit_group()
994
- if const_expr(stage < self.num_stages - 1):
995
- if stage == 0 or n_block - stage >= 0:
996
- load_V(n_block - stage, smem_pipe_write=stage, need_predicates=stage == 0)
997
- cute.arch.cp_async_commit_group()
998
- if const_expr(not self.Q_in_regs):
999
- preprocess_Q()
1000
-
1001
- # ///////////////////////////////////////////////////////////////////////////////
1002
- # Mainloop
1003
- # ///////////////////////////////////////////////////////////////////////////////
1004
- # Start processing of the first n-block.
1005
- # For performance reason, we separate out two kinds of iterations:
1006
- # those that need masking on S, and those that don't.
1007
- # We need masking on S for the very last block when K and V has length not multiple of tile_n.
1008
- # We also need masking on S if it's causal, for the last several blocks.
1009
- mask = AttentionMask(
1010
- self.tile_m,
1011
- self.tile_n,
1012
- seqlen,
1013
- window_size_left,
1014
- window_size_right,
1015
- self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
1016
- )
1017
- mask_fn = partial(
1018
- mask.apply_mask,
1019
- batch_idx=batch_size,
1020
- head_idx=num_head,
1021
- m_block=m_block,
1022
- thr_mma=thr_mma_qk,
1023
- mask_causal=self.is_causal,
1024
- mask_local=self.is_local,
1025
- aux_data=aux_data,
1026
- fastdiv_mods=fastdiv_mods if const_expr(self.mask_mod is not None) else None,
1027
- )
1028
-
1029
- # First iteration with seqlen masking
1030
- smem_pipe_read = Int32(0)
1031
- smem_pipe_write = Int32(self.num_stages - 1)
1032
- compute_one_n_block(
1033
- n_block,
1034
- smem_pipe_read,
1035
- smem_pipe_write,
1036
- is_first_n_block=True,
1037
- seqlen=seqlen,
1038
- mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=True),
1039
- )
1040
- smem_pipe_read = self.advance_pipeline(smem_pipe_read)
1041
- smem_pipe_write = self.advance_pipeline(smem_pipe_write)
1042
- # Next couple of iterations with causal masking
1043
- if const_expr(self.is_causal or self.is_local):
1044
- n_block_min_causal_local_mask = block_info.get_n_block_min_causal_local_mask(
1045
- seqlen, m_block, n_block_min
1046
- )
1047
- for n_tile in cutlass.range(n_block_max - 1 - n_block_min_causal_local_mask, unroll=1):
1048
- n_block = n_block_max - 2 - n_tile
1049
- compute_one_n_block(
1050
- n_block,
1051
- smem_pipe_read,
1052
- smem_pipe_write,
1053
- seqlen=seqlen,
1054
- mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=True),
1055
- )
1056
- smem_pipe_read = self.advance_pipeline(smem_pipe_read)
1057
- smem_pipe_write = self.advance_pipeline(smem_pipe_write)
1058
- # The remaining iterations have no masking
1059
- for n_tile in cutlass.range(n_block, unroll=1):
1060
- compute_one_n_block(
1061
- n_block - n_tile - 1, smem_pipe_read, smem_pipe_write,
1062
- seqlen=seqlen, is_first_n_block=False,
1063
- mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=False)
1064
- )
1065
- smem_pipe_read = self.advance_pipeline(smem_pipe_read)
1066
- smem_pipe_write = self.advance_pipeline(smem_pipe_write)
1067
- # TODO: local
1068
-
1069
- # normalize acc_O by row_sum and calculate the lse
1070
- row_scale = softmax.finalize()
1071
- softmax.rescale_O(acc_O, row_scale)
1072
-
1073
- # ///////////////////////////////////////////////////////////////////////////////
1074
- # Epilogue
1075
- # ///////////////////////////////////////////////////////////////////////////////
1076
- # reuse sQ's data iterator
1077
- sO = cute.make_tensor(sQ.iterator, sO_layout)
1078
- self.epilogue(
1079
- acc_O,
1080
- softmax.row_sum,
1081
- mO,
1082
- mLSE,
1083
- sO,
1084
- seqlen,
1085
- gmem_tiled_copy_O,
1086
- None,
1087
- tiled_mma_pv,
1088
- tidx,
1089
- m_block,
1090
- num_head,
1091
- batch_size,
1092
- )
1093
-
1094
- @cute.jit
1095
- def compute_one_n_block(
1096
- self,
1097
- n_block: Int32,
1098
- smem_pipe_read: Int32,
1099
- smem_pipe_write: Int32,
1100
- mma_params: SimpleNamespace,
1101
- smem_copy_params: SimpleNamespace,
1102
- softmax: Softmax,
1103
- load_K: Callable,
1104
- load_V: Callable,
1105
- score_mod: Callable | None,
1106
- batch_idx: cutlass.Int32,
1107
- head_idx: cutlass.Int32,
1108
- m_block: cutlass.Int32,
1109
- seqlen: SeqlenInfoQK,
1110
- aux_data: AuxData = AuxData(),
1111
- fastdiv_mods=None,
1112
- mask_fn: Optional[Callable] = None,
1113
- is_first_n_block: cutlass.Constexpr = False,
1114
- check_inf: cutlass.Constexpr = True,
1115
- ):
1116
- """Compute one n_block of S/O.
1117
-
1118
- This function provides different variants for processing the first n block versus
1119
- subsequent blocks.
1120
- """
1121
-
1122
- def sync():
1123
- cute.arch.cp_async_wait_group(self.num_stages * 2 - 2)
1124
- cute.arch.barrier()
1125
-
1126
- acc_shape_S = mma_params.thr_mma_qk.partition_shape_C((self.tile_m, self.tile_n))
1127
- acc_S = cute.make_rmem_tensor(acc_shape_S, Float32)
1128
- acc_S.fill(0.0)
1129
- # wait for smem tile QK before mma calculation for S
1130
- sync()
1131
-
1132
- # need predicates for the first tile
1133
- def load_V_next():
1134
- if self.num_stages == 1 or n_block - self.num_stages + 1 >= 0:
1135
- load_V(
1136
- n_block - self.num_stages + 1,
1137
- smem_pipe_write,
1138
- need_predicates=is_first_n_block and self.num_stages == 1,
1139
- )
1140
- cute.arch.cp_async_commit_group()
1141
-
1142
- load_V_next()
1143
- sm80_utils.gemm(
1144
- mma_params.thr_mma_qk,
1145
- acc_S,
1146
- mma_params.tSrQ,
1147
- mma_params.tSrK,
1148
- smem_copy_params.tSsQ,
1149
- smem_copy_params.tSsK[
1150
- None, None, None, smem_pipe_read if const_expr(self.num_stages > 1) else 0
1151
- ],
1152
- smem_copy_params.smem_thr_copy_Q,
1153
- smem_copy_params.smem_thr_copy_K,
1154
- # hook_fn=load_V_next,
1155
- A_in_regs=self.Q_in_regs,
1156
- )
1157
- if const_expr(score_mod is not None):
1158
- self.apply_score_mod(
1159
- mma_params.thr_mma_qk,
1160
- batch_idx,
1161
- head_idx,
1162
- m_block,
1163
- acc_S,
1164
- n_block,
1165
- softmax_scale=softmax.softmax_scale,
1166
- seqlen=seqlen,
1167
- aux_data=aux_data,
1168
- fastdiv_mods=fastdiv_mods,
1169
- )
1170
-
1171
- smem_pipe_write = self.advance_pipeline(smem_pipe_write)
1172
-
1173
- def load_K_next():
1174
- if n_block - self.num_stages >= 0:
1175
- load_K(n_block - self.num_stages, smem_pipe_write, need_predicates=False)
1176
- cute.arch.cp_async_commit_group()
1177
-
1178
- # wait for smem tile V for O
1179
- if const_expr(self.num_stages == 1):
1180
- sync()
1181
- load_K_next()
1182
- if const_expr(mask_fn is not None):
1183
- mask_fn(acc_S, n_block=n_block)
1184
- row_scale = softmax.online_softmax(acc_S, is_first=is_first_n_block, check_inf=check_inf)
1185
- softmax.rescale_O(mma_params.acc_O, row_scale)
1186
- rP = cute.make_fragment_like(acc_S, self.dtype)
1187
- rP.store(acc_S.load().to(self.dtype))
1188
- tOrP = layout_utils.reshape_acc_to_frgA(rP)
1189
- if const_expr(self.num_stages > 1):
1190
- sync()
1191
- load_K_next()
1192
- sm80_utils.gemm_rs(
1193
- mma_params.thr_mma_pv,
1194
- mma_params.acc_O,
1195
- tOrP,
1196
- mma_params.tOrVt,
1197
- smem_copy_params.tOsVt[
1198
- None, None, None, smem_pipe_read if const_expr(self.num_stages > 1) else 0
1199
- ],
1200
- smem_copy_params.smem_thr_copy_V,
1201
- # hook_fn=load_K_next,
1202
- )
1203
- # if const_expr(self.num_stages > 1):
1204
- # load_K_next()
1205
- @cute.jit
1206
- def apply_score_mod(
1207
- self,
1208
- thr_mma_qk,
1209
- batch_idx,
1210
- head_idx,
1211
- m_block,
1212
- acc_S,
1213
- n_block,
1214
- softmax_scale,
1215
- seqlen,
1216
- aux_data: AuxData = AuxData(),
1217
- fastdiv_mods=None,
1218
- ):
1219
- # Prepare index tensor
1220
- cS = cute.make_identity_tensor((self.tile_m, self.tile_n))
1221
- cS = cute.domain_offset((m_block * self.tile_m, n_block * self.tile_n), cS)
1222
- tScS = thr_mma_qk.partition_C(cS)
1223
-
1224
- apply_score_mod_inner(
1225
- acc_S,
1226
- tScS,
1227
- self.score_mod,
1228
- batch_idx,
1229
- head_idx,
1230
- softmax_scale,
1231
- self.score_vec_size,
1232
- self.qk_acc_dtype,
1233
- aux_data,
1234
- fastdiv_mods,
1235
- seqlen_info=seqlen,
1236
- constant_q_idx=None,
1237
- qhead_per_kvhead=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
1238
- )
1239
-
1240
-
1241
- # SM90 forward pass moved to flash_fwd_sm90.py; re-export for backward compatibility
1242
- def __getattr__(name):
1243
- if name == "FlashAttentionForwardSm90":
1244
- from flash_attn.cute.flash_fwd_sm90 import FlashAttentionForwardSm90
1245
- return FlashAttentionForwardSm90
1246
- raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/sol_h3_vae/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ """Sol-H3-VAE: fast noncausal decoder for MiniMax-H3 video latents."""
2
+ from .decoder import SolH3VAEDecoder
3
+
4
+ __all__ = ["SolH3VAEDecoder"]
code/{fast_h3_vae.py → sol_h3_vae/decoder.py} RENAMED
@@ -1,41 +1,39 @@
1
- """Fast H3 VAE (non-causal): drop-in replacement for MiniMaxH3VideoVAE.decode_base on whole clips.
2
 
3
- from fast_h3_vae import FastH3VAEDecoder
4
- dec = FastH3VAEDecoder(vae, "fast_h3_vae_noncausal_24l.safetensors") # vae: the released H3 VAE (for its
5
- pixels = dec.decode(z) # z: normalized latents (B=1, T, 24, H, W) post_quant_conv / embedder / stats)
6
  rgb_u8 = dec.decode_rgb_uint8(z) # (F, 16H, 16W, 3) uint8 on the GPU
7
 
8
- * any clip length T (t = 0 must be the start of a native 5-latent block, as for the released decoder). The output has
9
- the same frames as decode_base: 17 frames per full block, the first latent of every block keeps 1 of its 4 frames
10
- (tested T = 3..15: frame count equal to decode_base, fast vs reference 60.7-61.4 dB). For T = 1 and 2 the released
11
- decode_base raises an error; this decoder returns 1 and 5 frames by the same rule;
12
  * engine="fast" (default): NonCausalFast3 (FlashAttention-4 varlen windows, fused Triton kernels, one CUDA graph per
13
- (T, H, W)); engine="ref": the plain PyTorch NonCausalViTDecoder (any GPU with SDPA);
14
  * the fast engine is built on first use of a shape and cached (`max_engines` most recent shapes).
15
  """
16
  from __future__ import annotations
17
 
 
18
  from collections import OrderedDict
19
 
20
  import torch
21
  from safetensors.torch import load_file
22
 
23
- from noncausal_vit_decoder import KEEP, NonCausalViTDecoder
 
24
 
25
 
26
- class FastH3VAEDecoder:
27
- def __init__(self, vae, weights: str, *, layers: int = 24, ffn_keep: int | None = None, time_norm: float = 5,
28
- engine: str = "fast", max_engines: int = 4, device="cuda", offload_vae: bool = True):
29
- """offload_vae: after building the decoder, move the released VAE (encoder + 36-layer decoder, ~10 GB) to the CPU;
30
- decoding only needs its processor (pixel de-normalisation), which works on GPU tensors."""
31
- self.vae = vae
32
- m = NonCausalViTDecoder(vae, KEEP[layers], time_norm=time_norm, ffn_keep=ffn_keep)
33
- r = m.load_state_dict(load_file(weights, device="cpu"), strict=False)
34
- assert not r.missing_keys and not r.unexpected_keys, r
35
- self.model = m.to(device).eval().requires_grad_(False)
36
- if offload_vae:
37
- vae.to("cpu")
38
- torch.cuda.empty_cache()
39
  self.engine, self.max_engines = engine, max_engines
40
  self._engines: OrderedDict = OrderedDict()
41
 
@@ -44,7 +42,7 @@ class FastH3VAEDecoder:
44
  if key in self._engines:
45
  self._engines.move_to_end(key)
46
  return self._engines[key]
47
- from noncausal_fast3 import NonCausalFast3
48
  eng = NonCausalFast3(self.model, T, H, W, cuda_graph=True)
49
  self._engines[key] = eng
50
  while len(self._engines) > self.max_engines:
@@ -54,7 +52,8 @@ class FastH3VAEDecoder:
54
 
55
  @torch.no_grad()
56
  def decode(self, z):
57
- """z: normalized latents (1, T, 24, H, W). Returns decode_base-space pixels (1, 3, F, 16H, 16W), fp32."""
 
58
  assert z.dim() == 5 and z.shape[0] == 1 and z.shape[2] == 24, z.shape
59
  if self.engine == "ref":
60
  with torch.autocast("cuda", dtype=torch.bfloat16):
@@ -64,6 +63,6 @@ class FastH3VAEDecoder:
64
 
65
  @torch.no_grad()
66
  def decode_rgb_uint8(self, z):
67
- """Same as decode, then the VAE's revert_tensor, clamp and uint8: (F, 16H, 16W, 3) on the GPU."""
68
  px = self.decode(z)
69
- return self.vae.processor.revert_tensor(px).clamp_(0, 1).mul_(255).round_().to(torch.uint8)[0].permute(1, 2, 3, 0)
 
1
+ """Sol-H3-VAE: a 24-layer noncausal decoder for MiniMax-H3 video latents.
2
 
3
+ from sol_h3_vae import SolH3VAEDecoder
4
+ dec = SolH3VAEDecoder("MiniMax-H3/vae/config.json", "sol_h3_vae.safetensors")
5
+ pixels = dec.decode(z) # z: normalized latents (1, T, 24, H, W) -> (1, 3, F, 16H, 16W)
6
  rgb_u8 = dec.decode_rgb_uint8(z) # (F, 16H, 16W, 3) uint8 on the GPU
7
 
8
+ * any clip length T (t = 0 must be the start of a native 5-latent block, as for the H3 VAE). The output has the same
9
+ frames as the H3 VAE decoder: 17 frames per full block, the first latent of every block keeps 1 of its 4 frames;
 
 
10
  * engine="fast" (default): NonCausalFast3 (FlashAttention-4 varlen windows, fused Triton kernels, one CUDA graph per
11
+ (T, H, W)); engine="ref": the plain PyTorch NonCausalViTDecoder (any GPU with SDPA, no FlashAttention-4 needed);
12
  * the fast engine is built on first use of a shape and cached (`max_engines` most recent shapes).
13
  """
14
  from __future__ import annotations
15
 
16
+ import json
17
  from collections import OrderedDict
18
 
19
  import torch
20
  from safetensors.torch import load_file
21
 
22
+ from .model import NonCausalViTDecoder
23
+ from .template import RGBProcessor, make_template
24
 
25
 
26
+ class SolH3VAEDecoder:
27
+ def __init__(self, vae_config, weights, *, engine: str = "fast", device="cuda", max_engines: int = 1):
28
+ """vae_config: the H3 VAE config.json (Diffusers layout); weights: sol_h3_vae.safetensors. The checkpoint holds
29
+ every decoder parameter, so the H3 VAE weights are not needed."""
30
+ with open(vae_config) as f:
31
+ config = json.load(f)
32
+ model = NonCausalViTDecoder(make_template(config), time_norm=5)
33
+ model.load_state_dict(load_file(str(weights), device="cpu"), strict=True, assign=True)
34
+ assert not any(t.is_meta for t in list(model.parameters()) + list(model.buffers()))
35
+ self.model = model.to(device).eval().requires_grad_(False)
36
+ self.processor = RGBProcessor()
 
 
37
  self.engine, self.max_engines = engine, max_engines
38
  self._engines: OrderedDict = OrderedDict()
39
 
 
42
  if key in self._engines:
43
  self._engines.move_to_end(key)
44
  return self._engines[key]
45
+ from .fast import NonCausalFast3
46
  eng = NonCausalFast3(self.model, T, H, W, cuda_graph=True)
47
  self._engines[key] = eng
48
  while len(self._engines) > self.max_engines:
 
52
 
53
  @torch.no_grad()
54
  def decode(self, z):
55
+ """z: normalized latents (1, T, 24, H, W). Returns the H3 VAE decoder's pixels (1, 3, F, 16H, 16W), fp32,
56
+ ImageNet-normalized like the H3 VAE output; `processor.revert_tensor` maps them to [0, 1]."""
57
  assert z.dim() == 5 and z.shape[0] == 1 and z.shape[2] == 24, z.shape
58
  if self.engine == "ref":
59
  with torch.autocast("cuda", dtype=torch.bfloat16):
 
63
 
64
  @torch.no_grad()
65
  def decode_rgb_uint8(self, z):
66
+ """Same as decode, then revert_tensor, clamp and uint8: (F, 16H, 16W, 3) on the GPU."""
67
  px = self.decode(z)
68
+ return self.processor.revert_tensor(px).clamp_(0, 1).mul_(255).round_().to(torch.uint8)[0].permute(1, 2, 3, 0)
code/{noncausal_fast3.py → sol_h3_vae/fast.py} RENAMED
@@ -10,7 +10,7 @@ import torch
10
  import triton
11
  import triton.language as tl
12
 
13
- from noncausal_fast import BF16, NonCausalFast, _resid_norm_kernel, qgemm, qgemm_act, _flash_attn_fwd, _nc_qkv_post_kernel
14
 
15
 
16
  @triton.jit
 
10
  import triton
11
  import triton.language as tl
12
 
13
+ from .fast_base import BF16, NonCausalFast, _resid_norm_kernel, qgemm, qgemm_act, _flash_attn_fwd, _nc_qkv_post_kernel
14
 
15
 
16
  @triton.jit
code/{noncausal_fast.py → sol_h3_vae/fast_base.py} RENAMED
@@ -6,7 +6,7 @@
6
  tokens of the block + its global tokens;
7
  * the 5 global rows of every block are appended to the token rows, so each linear layer is a single GEMM;
8
  * residual add + RMSNorm, SwiGLU in the up-projection GEMM (quack), fp32 head as one bf16 GEMM on a hi/lo split
9
- (fused_kernels.py);
10
  * the whole decode is one CUDA graph.
11
  """
12
  from __future__ import annotations
@@ -24,8 +24,8 @@ if "cutlass" not in sys.modules:
24
  from flash_attn.cute.interface import _flash_attn_fwd
25
  from quack.gemm_interface import gemm as qgemm, gemm_act as qgemm_act
26
 
27
- from fused_kernels import _interleave, _resid_ln_split_kernel, _resid_norm_kernel
28
- from noncausal_vit_decoder import OUT_CH, PATCH, PATCH_T, NonCausalViTDecoder
29
 
30
  BF16 = torch.bfloat16
31
  import triton
 
6
  tokens of the block + its global tokens;
7
  * the 5 global rows of every block are appended to the token rows, so each linear layer is a single GEMM;
8
  * residual add + RMSNorm, SwiGLU in the up-projection GEMM (quack), fp32 head as one bf16 GEMM on a hi/lo split
9
+ (kernels.py);
10
  * the whole decode is one CUDA graph.
11
  """
12
  from __future__ import annotations
 
24
  from flash_attn.cute.interface import _flash_attn_fwd
25
  from quack.gemm_interface import gemm as qgemm, gemm_act as qgemm_act
26
 
27
+ from .kernels import _interleave, _resid_ln_split_kernel, _resid_norm_kernel
28
+ from .model import OUT_CH, PATCH, PATCH_T, NonCausalViTDecoder
29
 
30
  BF16 = torch.bfloat16
31
  import triton
code/{fused_kernels.py → sol_h3_vae/kernels.py} RENAMED
File without changes
code/{noncausal_vit_decoder.py → sol_h3_vae/model.py} RENAMED
@@ -23,7 +23,7 @@ import torch
23
  from torch import nn
24
  from torch.nn import functional as F
25
 
26
- from spark_compat import _apply_qk_norm, _apply_rotary_pos_emb_impl
27
 
28
  PATCH, PATCH_T, OUT_CH = 16, 4, 3
29
  KEEP_24 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20, 21, 31, 32, 35]
 
23
  from torch import nn
24
  from torch.nn import functional as F
25
 
26
+ from .template import _apply_qk_norm, _apply_rotary_pos_emb_impl
27
 
28
  PATCH, PATCH_T, OUT_CH = 16, 4, 3
29
  KEEP_24 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20, 21, 31, 32, 35]
code/{spark_compat.py → sol_h3_vae/template.py} RENAMED
File without changes
code/spark_decoder.py DELETED
@@ -1,25 +0,0 @@
1
- """Build the decoder from the Diffusers H3 VAE config.json alone; decoding is FastH3VAEDecoder's."""
2
- import json
3
- from collections import OrderedDict
4
- from types import SimpleNamespace
5
-
6
- from safetensors.torch import load_file
7
-
8
- from fast_h3_vae import FastH3VAEDecoder
9
- from noncausal_vit_decoder import NonCausalViTDecoder
10
- from spark_compat import RGBProcessor, make_template
11
- from spark_fa4 import enable_sm120_guard
12
-
13
-
14
- class SparkFastH3VAEDecoder(FastH3VAEDecoder):
15
- def __init__(self, vae_config, weights, *, engine='fast', device='cuda', max_engines=1):
16
- enable_sm120_guard()
17
- with open(vae_config) as f:
18
- config = json.load(f)
19
- model = NonCausalViTDecoder(make_template(config), time_norm=5)
20
- model.load_state_dict(load_file(str(weights), device='cpu'), strict=True, assign=True)
21
- assert not any(t.is_meta for t in list(model.parameters()) + list(model.buffers()))
22
- self.model = model.to(device).eval().requires_grad_(False)
23
- self.vae = SimpleNamespace(processor=RGBProcessor())
24
- self.engine, self.max_engines = engine, max_engines
25
- self._engines = OrderedDict()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
code/spark_fa4.py DELETED
@@ -1,22 +0,0 @@
1
- """Process-local SM120 fix for FlashAttention-4 (4.0.0b19-b22) varlen forward.
2
-
3
- On SM120 (RTX 5090, DGX Spark) surplus scheduler work tiles could read sequence
4
- metadata past the batch. _spark_flash_fwd_guarded.py is FA4's flash_fwd.py with that
5
- guard added; the installed FA4 package and valid-tile computation stay unchanged.
6
- """
7
- from flash_attn.cute import interface
8
- from flash_attn.cute.cache_utils import JITCache
9
- from flash_attn.cute.flash_fwd_sm120 import FlashAttentionForwardSm120
10
- from _spark_flash_fwd_guarded import FlashAttentionForwardSm80
11
-
12
-
13
- class SparkFlashAttentionForwardSm120(FlashAttentionForwardSm120):
14
- kernel = FlashAttentionForwardSm80.kernel
15
-
16
-
17
- def enable_sm120_guard():
18
- if interface.FlashAttentionForwardSm120 is SparkFlashAttentionForwardSm120:
19
- return
20
- interface.FlashAttentionForwardSm120 = SparkFlashAttentionForwardSm120
21
- # Never reuse an unguarded binary from an existing in-memory/disk cache.
22
- interface._flash_attn_fwd.compile_cache = JITCache()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fast_h3_vae_noncausal_24l.safetensors → sol_h3_vae.safetensors RENAMED
File without changes