2486 lines
104 KiB
Python
2486 lines
104 KiB
Python
# Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
|
|
# A reimplementation of
|
|
# https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_kernel_sm80.h
|
|
# and https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_kernel_sm90.h
|
|
# from Cutlass C++ to Cute-DSL.
|
|
# Built on Cute-DSL example: https://github.com/NVIDIA/cutlass/blob/main/examples/python/CuTeDSL/ampere/flash_attention_v2.py
|
|
|
|
import math
|
|
from types import SimpleNamespace
|
|
from typing import Type, Callable, Optional, List
|
|
from functools import partial
|
|
|
|
import cuda.bindings.driver as cuda
|
|
|
|
import cutlass
|
|
import cutlass.cute as cute
|
|
from cutlass import Constexpr, Float32, Int32, const_expr, Boolean
|
|
from cutlass.cute.nvgpu import cpasync, warp, warpgroup
|
|
from cutlass.cute.arch import ProxyKind, SharedSpace
|
|
import cutlass.utils as utils_basic
|
|
from cutlass.utils import LayoutEnum
|
|
import cutlass.utils.hopper_helpers as sm90_utils_basic
|
|
|
|
from quack import copy_utils as quack_copy_utils
|
|
|
|
import sglang.jit_kernel.flash_attention.cute.ampere_helpers as sm80_utils
|
|
import sglang.jit_kernel.flash_attention.cute.hopper_helpers as sm90_utils
|
|
import sglang.jit_kernel.flash_attention.cute.utils as utils
|
|
import sglang.jit_kernel.flash_attention.cute.copy_utils as copy_utils
|
|
from .mask import AttentionMask
|
|
from .softmax import Softmax, apply_score_mod_inner
|
|
from .seqlen_info import SeqlenInfoQK
|
|
from .block_info import BlockInfo
|
|
from .block_sparsity import BlockSparseTensors
|
|
from .block_sparse_utils import (
|
|
produce_block_sparse_loads,
|
|
consume_block_sparse_loads,
|
|
)
|
|
import sglang.jit_kernel.flash_attention.cute.pipeline as pipeline
|
|
from .pack_gqa import PackGQA
|
|
from .named_barrier import NamedBarrierFwd
|
|
from .tile_scheduler import (
|
|
TileSchedulerArguments,
|
|
SingleTileScheduler,
|
|
SingleTileLPTScheduler,
|
|
SingleTileVarlenScheduler,
|
|
ParamsBase,
|
|
)
|
|
from cutlass.cute import FastDivmodDivisor
|
|
|
|
|
|
class FlashAttentionForwardBase:
|
|
arch: int = 80
|
|
|
|
def __init__(
|
|
self,
|
|
dtype: Type[cutlass.Numeric],
|
|
head_dim: int,
|
|
head_dim_v: Optional[int] = None,
|
|
qhead_per_kvhead: int = 1,
|
|
is_causal: bool = False,
|
|
is_local: bool = False,
|
|
pack_gqa: bool = True,
|
|
tile_m: int = 128,
|
|
tile_n: int = 128,
|
|
num_stages: int = 1,
|
|
num_threads: int = 128,
|
|
Q_in_regs: bool = False,
|
|
score_mod: Optional[cutlass.Constexpr] = None,
|
|
mask_mod: Optional[cutlass.Constexpr] = None,
|
|
has_aux_tensors: bool = False,
|
|
):
|
|
"""Initializes the configuration for a flash attention kernel.
|
|
|
|
All contiguous dimensions must be at least 16 bytes aligned, which means that the head dimension
|
|
should be a multiple of 8.
|
|
|
|
:param head_dim: head dimension
|
|
:type head_dim: int
|
|
:param tile_m: m block size
|
|
:type tile_m: int
|
|
:param tile_n: n block size
|
|
:type tile_n: int
|
|
:param num_threads: number of threads
|
|
:type num_threads: int
|
|
:param is_causal: is causal
|
|
:param score_mod: A callable that takes the attention scores and applies a modification.
|
|
Callable signature: ``score_mod(scores, batch_idx, head_idx, q_idx, kv_idx, aux_tensors) -> Any``
|
|
:param mask_mod: A callable that takes the attention scores and returns a boolean representing whether that score should be masked.
|
|
Callable signature: ``mask_mod(batch_idx, head_idx, q_idx, kv_idx, aux_tensors) -> Boolean``
|
|
"""
|
|
self.dtype = dtype
|
|
# padding head_dim to a multiple of 16 as k_block_size
|
|
hdim_multiple_of = 16
|
|
self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
|
|
head_dim_v = head_dim_v if head_dim_v is not None else head_dim
|
|
self.same_hdim_kv = head_dim == head_dim_v
|
|
self.tile_hdimv = int(math.ceil(head_dim_v / hdim_multiple_of) * hdim_multiple_of)
|
|
# Can save registers (and hence be faster) if we don't have to check hdim predication
|
|
self.check_hdim_oob = head_dim != self.tile_hdim
|
|
self.check_hdim_v_oob = head_dim_v != self.tile_hdimv
|
|
self.qhead_per_kvhead = qhead_per_kvhead
|
|
self.is_causal = is_causal
|
|
self.is_local = is_local
|
|
self.pack_gqa = pack_gqa
|
|
self.tile_m = tile_m
|
|
self.tile_n = tile_n
|
|
self.num_threads = num_threads
|
|
self.num_stages = num_stages
|
|
self.Q_in_regs = Q_in_regs
|
|
self.score_mod = score_mod
|
|
self.mask_mod = mask_mod
|
|
self.qk_acc_dtype = Float32
|
|
if const_expr(has_aux_tensors):
|
|
self.vec_size: cutlass.Constexpr = 1
|
|
else:
|
|
self.vec_size: cutlass.Constexpr = 2
|
|
|
|
@staticmethod
|
|
def can_implement(
|
|
dtype,
|
|
head_dim,
|
|
head_dim_v,
|
|
tile_m,
|
|
tile_n,
|
|
num_stages,
|
|
num_threads,
|
|
is_causal,
|
|
Q_in_regs=False,
|
|
) -> bool:
|
|
"""Check if the kernel can be implemented with the given parameters.
|
|
|
|
:param dtype: data type
|
|
:type dtype: cutlass.Numeric
|
|
:param head_dim: head dimension
|
|
:type head_dim: int
|
|
:param tile_m: m block size
|
|
:type tile_m: int
|
|
:param tile_n: n block size
|
|
:type tile_n: int
|
|
:param num_threads: number of threads
|
|
:type num_threads: int
|
|
:param is_causal: is causal
|
|
:type is_causal: bool
|
|
|
|
:return: True if the kernel can be implemented, False otherwise
|
|
:rtype: bool
|
|
"""
|
|
if dtype not in [cutlass.Float16, cutlass.BFloat16]:
|
|
return False
|
|
if head_dim % 8 != 0:
|
|
return False
|
|
if head_dim_v % 8 != 0:
|
|
return False
|
|
if tile_n % 16 != 0:
|
|
return False
|
|
if num_threads % 32 != 0:
|
|
return False
|
|
# Check if block size setting is out of shared memory capacity
|
|
# Shared memory usage: Q tile + (K tile + V tile) where K and V use the same tile size
|
|
smem_usage_Q = tile_m * head_dim * 2
|
|
smem_usage_K = tile_n * head_dim * num_stages * 2
|
|
smem_usage_V = tile_n * head_dim_v * num_stages * 2
|
|
smem_usage_QV = (
|
|
(smem_usage_Q + smem_usage_V) if not Q_in_regs else max(smem_usage_Q, smem_usage_V)
|
|
)
|
|
smem_usage = smem_usage_QV + smem_usage_K
|
|
# TODO: sm86 and sm89
|
|
smem_capacity = utils_basic.get_smem_capacity_in_bytes("sm_80")
|
|
if smem_usage > smem_capacity:
|
|
return False
|
|
# Check if twice the block size is divisible by the number of threads
|
|
if (tile_m * 2) % num_threads != 0:
|
|
return False
|
|
return True
|
|
|
|
def _check_type(
|
|
self,
|
|
mQ_type: Type[cutlass.Numeric],
|
|
mK_type: Type[cutlass.Numeric],
|
|
mV_type: Type[cutlass.Numeric],
|
|
mO_type: Type[cutlass.Numeric],
|
|
mLSE_type: Type[cutlass.Numeric] | None,
|
|
mCuSeqlensQ_type: Type[cutlass.Numeric] | None,
|
|
mCuSeqlensK_type: Type[cutlass.Numeric] | None,
|
|
mSeqUsedQ_type: Type[cutlass.Numeric] | None,
|
|
mSeqUsedK_type: Type[cutlass.Numeric] | None,
|
|
):
|
|
# Get the data type and check if it is fp16 or bf16
|
|
if const_expr(not (mQ_type == mK_type == mV_type == mO_type)):
|
|
raise TypeError("All tensors must have the same data type")
|
|
if const_expr(mQ_type not in [cutlass.Float16, cutlass.BFloat16]):
|
|
raise TypeError("Only Float16 or BFloat16 is supported")
|
|
if const_expr(mLSE_type not in [None, Float32]):
|
|
raise TypeError("LSE tensor must be Float32")
|
|
if const_expr(mCuSeqlensQ_type not in [None, Int32]):
|
|
raise TypeError("cu_seqlens_q tensor must be Int32")
|
|
if const_expr(mCuSeqlensK_type not in [None, Int32]):
|
|
raise TypeError("cu_seqlens_k tensor must be Int32")
|
|
if const_expr(mSeqUsedQ_type not in [None, Int32]):
|
|
raise TypeError("seqused_q tensor must be Int32")
|
|
if const_expr(mSeqUsedK_type not in [None, Int32]):
|
|
raise TypeError("seqused_k tensor must be Int32")
|
|
assert mQ_type == self.dtype
|
|
|
|
def _setup_attributes(self):
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Shared memory layout: Q/K/V
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
sQ_layout_atom, sK_layout_atom, sV_layout_atom, sO_layout_atom, sP_layout_atom = (
|
|
self._get_smem_layout_atom()
|
|
)
|
|
self.sQ_layout = cute.tile_to_shape(
|
|
sQ_layout_atom,
|
|
(self.tile_m, self.tile_hdim),
|
|
(0, 1),
|
|
)
|
|
self.sK_layout = cute.tile_to_shape(
|
|
sK_layout_atom,
|
|
(self.tile_n, self.tile_hdim, self.num_stages),
|
|
(0, 1, 2),
|
|
)
|
|
self.sV_layout = cute.tile_to_shape(
|
|
sV_layout_atom,
|
|
(self.tile_n, self.tile_hdimv, self.num_stages),
|
|
(0, 1, 2),
|
|
)
|
|
self.sO_layout = cute.tile_to_shape(
|
|
sO_layout_atom,
|
|
(self.tile_m, self.tile_hdimv),
|
|
(0, 1),
|
|
)
|
|
if const_expr(sP_layout_atom is not None):
|
|
self.sP_layout = cute.tile_to_shape(
|
|
sP_layout_atom,
|
|
(self.tile_m, self.tile_n),
|
|
(0, 1),
|
|
)
|
|
else:
|
|
self.sP_layout = None
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# GMEM Tiled copy:
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Thread layouts for copies
|
|
universal_copy_bits = 128
|
|
async_copy_elems = universal_copy_bits // self.dtype.width
|
|
# atom_async_copy: async copy atom for QKV load
|
|
atom_async_copy = cute.make_copy_atom(
|
|
cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
|
|
self.dtype,
|
|
num_bits_per_copy=universal_copy_bits,
|
|
)
|
|
# atom_universal_copy: universal copy atom for O store
|
|
atom_universal_copy = cute.make_copy_atom(
|
|
cute.nvgpu.CopyUniversalOp(),
|
|
self.dtype,
|
|
num_bits_per_copy=universal_copy_bits,
|
|
)
|
|
# tQ_layout and tK_layout: thread layout for QK load
|
|
tQK_shape_dim_1 = sQ_layout_atom.outer.shape[1] // async_copy_elems
|
|
assert self.num_Q_load_threads % tQK_shape_dim_1 == 0, (
|
|
"num_threads must be divisible by tQK_shape_dim_1"
|
|
)
|
|
assert self.num_producer_threads % tQK_shape_dim_1 == 0, (
|
|
"num_threads must be divisible by tQK_shape_dim_1"
|
|
)
|
|
tQ_layout = cute.make_ordered_layout(
|
|
(self.num_Q_load_threads // tQK_shape_dim_1, tQK_shape_dim_1),
|
|
order=(1, 0),
|
|
)
|
|
tK_layout = cute.make_ordered_layout(
|
|
(self.num_producer_threads // tQK_shape_dim_1, tQK_shape_dim_1),
|
|
order=(1, 0),
|
|
)
|
|
# So that we don't have to check if we overshoot kBlockM when we load Q
|
|
assert self.tile_m % tQ_layout.shape[0] == 0
|
|
tV_shape_dim_1 = sV_layout_atom.outer.shape[1] // async_copy_elems
|
|
tV_layout = cute.make_ordered_layout(
|
|
(self.num_producer_threads // tV_shape_dim_1, tV_shape_dim_1),
|
|
order=(1, 0),
|
|
)
|
|
# TODO: need a different layout for O if O dtype is not the same as V dtype
|
|
# tO_layout: thread layout for O store
|
|
tO_layout = cute.make_ordered_layout(
|
|
(self.num_epilogue_threads // tV_shape_dim_1, tV_shape_dim_1),
|
|
order=(1, 0),
|
|
)
|
|
# So that we don't have to check if we overshoot kBlockM when we store O
|
|
assert self.tile_m % tO_layout.shape[0] == 0
|
|
|
|
# Value layouts for copies
|
|
vQKV_layout = cute.make_layout((1, async_copy_elems))
|
|
vO_layout = vQKV_layout
|
|
|
|
self.gmem_tiled_copy_Q = cute.make_tiled_copy_tv(atom_async_copy, tQ_layout, vQKV_layout)
|
|
self.gmem_tiled_copy_K = cute.make_tiled_copy_tv(atom_async_copy, tK_layout, vQKV_layout)
|
|
self.gmem_tiled_copy_V = cute.make_tiled_copy_tv(atom_async_copy, tV_layout, vQKV_layout)
|
|
# gmem_tiled_copy_O: tiled copy for O store
|
|
self.gmem_tiled_copy_O = cute.make_tiled_copy_tv(atom_universal_copy, tO_layout, vO_layout)
|
|
|
|
def _get_smem_layout_atom(self):
|
|
raise NotImplementedError()
|
|
|
|
def _get_tiled_mma(self):
|
|
raise NotImplementedError()
|
|
|
|
def _get_shared_storage_cls(self):
|
|
raise NotImplementedError()
|
|
|
|
@cute.jit
|
|
def __call__(
|
|
self,
|
|
mQ: cute.Tensor,
|
|
mK: cute.Tensor,
|
|
mV: cute.Tensor,
|
|
mO: cute.Tensor,
|
|
mLSE: Optional[cute.Tensor],
|
|
softmax_scale: Float32,
|
|
stream: cuda.CUstream,
|
|
):
|
|
"""Configures and launches the flash attention kernel.
|
|
|
|
mQ/mK/mV/mO has same data types(supports fp16 and bf16) and same layout:
|
|
(batch_size, seqlen_q, num_head, head_dim):(_, _, _, 1)
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@cute.jit
|
|
def epilogue(
|
|
self,
|
|
acc_O: cute.Tensor,
|
|
lse: cute.Tensor,
|
|
mO: cute.Tensor,
|
|
mLSE: Optional[cute.Tensor],
|
|
sO: cute.Tensor,
|
|
seqlen: SeqlenInfoQK,
|
|
gmem_tiled_copy_O: cute.TiledCopy,
|
|
tma_atom_O: Optional[cute.CopyAtom],
|
|
tiled_mma: cute.TiledMma,
|
|
tidx: Int32,
|
|
m_block: Int32,
|
|
head_idx: Int32,
|
|
batch_idx: Int32,
|
|
):
|
|
# store acc_O
|
|
rO = cute.make_fragment_like(acc_O, self.dtype)
|
|
rO.store(acc_O.load().to(self.dtype))
|
|
# Make sure all threads have finished reading V
|
|
cute.arch.barrier(
|
|
barrier_id=int(NamedBarrierFwd.Epilogue), number_of_threads=self.num_epilogue_threads
|
|
)
|
|
smem_copy_atom_O = utils.get_smem_store_atom(self.arch, self.dtype)
|
|
smem_thr_copy_O = cute.make_tiled_copy_C(smem_copy_atom_O, tiled_mma).get_slice(tidx)
|
|
taccOrO = smem_thr_copy_O.retile(rO)
|
|
taccOsO = smem_thr_copy_O.partition_D(sO)
|
|
# taccOsO = quack_copy_utils.partition_D_position_independent(smem_thr_copy_O, sO)
|
|
# copy acc O from rmem to smem with the smem copy atom
|
|
cute.copy(smem_copy_atom_O, taccOrO, taccOsO)
|
|
|
|
cO = cute.make_identity_tensor((self.tile_m, self.tile_hdimv))
|
|
pack_gqa = PackGQA(
|
|
self.tile_m, self.tile_hdimv, self.check_hdim_v_oob, self.qhead_per_kvhead
|
|
)
|
|
|
|
# Write LSE from rmem -> gmem
|
|
if const_expr(mLSE is not None):
|
|
if const_expr(not seqlen.has_cu_seqlens_q):
|
|
mLSE_cur = mLSE[None, head_idx, batch_idx]
|
|
else:
|
|
offset = seqlen.offset_q if const_expr(not self.pack_gqa) else (0, seqlen.offset_q)
|
|
mLSE_cur = cute.domain_offset((offset,), mLSE[None, head_idx])
|
|
if const_expr(not self.pack_gqa):
|
|
gLSE = cute.local_tile(mLSE_cur, (self.tile_m,), (m_block,))
|
|
gLSE_expanded_layout = cute.append(
|
|
gLSE.layout, cute.make_layout((self.tile_hdimv,), stride=(0,))
|
|
)
|
|
gLSE_expanded = cute.make_tensor(gLSE.iterator, gLSE_expanded_layout)
|
|
thr_mma = tiled_mma.get_slice(tidx)
|
|
taccOgLSE = utils.make_acc_tensor_mn_view(thr_mma.partition_C(gLSE_expanded))
|
|
assert cute.size(taccOgLSE, mode=[0]) == cute.size(lse)
|
|
taccOcO = utils.make_acc_tensor_mn_view(thr_mma.partition_C(cO))
|
|
t0accOcO = utils.make_acc_tensor_mn_view(thr_mma.get_slice(0).partition_C(cO))
|
|
# Only the thread corresponding to column 0 writes out the lse to gmem
|
|
if taccOcO[0][1] == 0:
|
|
for m in cutlass.range_constexpr(cute.size(taccOgLSE.shape[1])):
|
|
if (
|
|
t0accOcO[m, 0][0]
|
|
< seqlen.seqlen_q - m_block * self.tile_m - taccOcO[0][0]
|
|
):
|
|
taccOgLSE[m, 0] = lse[m]
|
|
else:
|
|
pack_gqa.store_LSE(mLSE_cur, lse, tiled_mma, tidx, m_block, seqlen.seqlen_q)
|
|
|
|
if const_expr(not seqlen.has_cu_seqlens_q):
|
|
mO_cur = mO[None, None, head_idx, batch_idx]
|
|
else:
|
|
offset = seqlen.offset_q if const_expr(not self.pack_gqa) else (0, seqlen.offset_q)
|
|
mO_cur = cute.domain_offset((offset, 0), mO[None, None, head_idx])
|
|
# thr_mma = tiled_mma.get_slice(tidx)
|
|
# taccOgO = thr_mma.partition_C(gO)
|
|
# cute.autovec_copy(rO, taccOgO)
|
|
# sync to make sure all smem stores are done
|
|
if const_expr(self.use_tma_O):
|
|
# ensure smem writes are visible to TMA
|
|
cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
|
|
cute.arch.barrier_arrive(
|
|
barrier_id=int(NamedBarrierFwd.Epilogue),
|
|
number_of_threads=self.num_epilogue_threads + cute.arch.WARP_SIZE,
|
|
)
|
|
gO = cute.local_tile(mO_cur, (self.tile_m, self.tile_hdimv), (m_block, 0))
|
|
store_O, _, _ = copy_utils.tma_get_copy_fn(
|
|
tma_atom_O, 0, cute.make_layout(1), sO, gO, single_stage=True
|
|
)
|
|
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
|
|
if warp_idx == 4:
|
|
cute.arch.barrier(
|
|
barrier_id=int(NamedBarrierFwd.Epilogue),
|
|
number_of_threads=self.num_epilogue_threads + cute.arch.WARP_SIZE,
|
|
)
|
|
store_O()
|
|
cute.arch.cp_async_bulk_commit_group()
|
|
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
|
else:
|
|
cute.arch.barrier(
|
|
barrier_id=int(NamedBarrierFwd.Epilogue),
|
|
number_of_threads=self.num_epilogue_threads,
|
|
)
|
|
gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
|
|
tOsO = gmem_thr_copy_O.partition_S(sO)
|
|
tOrO = cute.make_fragment_like(tOsO, self.dtype)
|
|
# load acc O from smem to rmem for wider vectorization
|
|
cute.autovec_copy(tOsO, tOrO)
|
|
if const_expr(not self.pack_gqa):
|
|
gO = cute.local_tile(mO_cur, (self.tile_m, self.tile_hdimv), (m_block, 0))
|
|
tOgO = gmem_thr_copy_O.partition_D(gO)
|
|
tOcO = gmem_thr_copy_O.partition_S(cO)
|
|
t0OcO = gmem_tiled_copy_O.get_slice(0).partition_S(cO)
|
|
tOpO = utils.predicate_k(tOcO, limit=mO.shape[1])
|
|
# copy acc O from rmem to gmem
|
|
for rest_m in cutlass.range_constexpr(cute.size(tOrO.shape[1])):
|
|
if (
|
|
t0OcO[0, rest_m, 0][0]
|
|
< seqlen.seqlen_q - m_block * self.tile_m - tOcO[0][0]
|
|
):
|
|
cute.copy(
|
|
gmem_tiled_copy_O,
|
|
tOrO[None, rest_m, None],
|
|
tOgO[None, rest_m, None],
|
|
pred=tOpO[None, rest_m, None]
|
|
if const_expr(self.check_hdim_v_oob)
|
|
else None,
|
|
)
|
|
else:
|
|
pack_gqa.store_O(mO_cur, tOrO, gmem_tiled_copy_O, tidx, m_block, seqlen.seqlen_q)
|
|
|
|
@cute.jit
|
|
def advance_pipeline(self, pipeline_index):
|
|
return pipeline_index + 1 if pipeline_index < self.num_stages - 1 else 0
|
|
|
|
@cute.jit
|
|
def load_Q(
|
|
self,
|
|
gmem_thr_copy: cute.TiledCopy,
|
|
gQ: cute.Tensor,
|
|
sQ: cute.Tensor,
|
|
block: Int32,
|
|
seqlen: Int32,
|
|
headdim: Int32,
|
|
):
|
|
tQsQ, tQgQ = gmem_thr_copy.partition_D(sQ), gmem_thr_copy.partition_S(gQ)
|
|
cQ = cute.make_identity_tensor((self.tile_m, self.tile_hdim))
|
|
tQcQ = gmem_thr_copy.partition_S(cQ)
|
|
t0QcQ = gmem_thr_copy.get_slice(0).partition_S(cQ)
|
|
tQpQ = utils.predicate_k(tQcQ, limit=headdim)
|
|
for m in cutlass.range_constexpr(cute.size(tQsQ.shape[1])):
|
|
# Instead of using tQcQ, we using t0QcQ and subtract the offset from the limit
|
|
# (seqlen - block * kBlockM). This is because the entries of t0QcQ are known at compile time.
|
|
if t0QcQ[0, m, 0][0] < seqlen - block * self.tile_m - tQcQ[0][0]:
|
|
cute.copy(
|
|
gmem_thr_copy,
|
|
tQgQ[None, m, None],
|
|
tQsQ[None, m, None],
|
|
pred=tQpQ[None, m, None] if const_expr(self.check_hdim_oob) else None,
|
|
)
|
|
# We don't need to clear the sQ smem tiles since we'll only write out the valid outputs
|
|
|
|
@cute.jit
|
|
def load_K(
|
|
self,
|
|
gmem_tiled_copy: cute.TiledCopy,
|
|
tKgK: cute.Tensor,
|
|
tKsK: cute.Tensor,
|
|
tKcK: cute.Tensor,
|
|
t0KcK: cute.Tensor,
|
|
tKpK: cute.Tensor,
|
|
block: Int32,
|
|
smem_pipe_write: Int32,
|
|
seqlen: Int32,
|
|
need_predicates: cutlass.Constexpr,
|
|
):
|
|
# Do we need to check if we overshoot kBlockN when we load K?
|
|
is_even_n_smem_k = self.tile_n % gmem_tiled_copy.tiler_mn[0].shape == 0
|
|
if const_expr(need_predicates or not is_even_n_smem_k):
|
|
# Instead of using tKcK, we using t0KcK and subtract the offset from the limit
|
|
# (seqlen - block * kBlockN). This is because the entries of t0KcK are known at compile time.
|
|
if const_expr(is_even_n_smem_k):
|
|
seqlen_limit = seqlen - block * self.tile_n
|
|
else:
|
|
if const_expr(not need_predicates):
|
|
seqlen_limit = self.tile_n
|
|
else:
|
|
seqlen_limit = cutlass.min(seqlen - block * self.tile_n, self.tile_n)
|
|
seqlen_limit -= tKcK[0][0]
|
|
for n in cutlass.range_constexpr(cute.size(tKsK.shape[1])):
|
|
if t0KcK[0, n, 0][0] < seqlen_limit:
|
|
cute.copy(
|
|
gmem_tiled_copy,
|
|
tKgK[None, n, None, block],
|
|
tKsK[
|
|
None, n, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0
|
|
],
|
|
pred=tKpK[None, n, None] if const_expr(self.check_hdim_oob) else None,
|
|
)
|
|
# We don't need to clear the sK smem tiles since we'll mask out the scores anyway.
|
|
else:
|
|
cute.copy(
|
|
gmem_tiled_copy,
|
|
tKgK[None, None, None, block],
|
|
tKsK[None, None, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0],
|
|
pred=tKpK if const_expr(self.check_hdim_oob) else None,
|
|
)
|
|
|
|
@cute.jit
|
|
def load_V(
|
|
self,
|
|
gmem_tiled_copy: cute.TiledCopy,
|
|
tVgV: cute.Tensor,
|
|
tVsV: cute.Tensor,
|
|
tVcV: cute.Tensor,
|
|
t0VcV: cute.Tensor,
|
|
tVpV: cute.Tensor,
|
|
block: Int32,
|
|
smem_pipe_write: Int32,
|
|
seqlen: Int32,
|
|
need_predicates: cutlass.Constexpr,
|
|
):
|
|
# Do we need to check if we overshoot kBlockN when we load V?
|
|
is_even_n_smem_v = self.tile_n % gmem_tiled_copy.tiler_mn[0].shape == 0
|
|
if const_expr(need_predicates or not is_even_n_smem_v):
|
|
for n in cutlass.range_constexpr(cute.size(tVsV.shape[1])):
|
|
# If kBlockN doesn't evenly divide the tiled copy, only the last `n` needs to be checked
|
|
if (
|
|
is_even_n_smem_v
|
|
or n < cute.size(tVsV.shape[1]) - 1
|
|
or tVcV[0, n, 0][0] < self.tile_n
|
|
):
|
|
predicate = tVpV[None, n, None] if const_expr(self.check_hdim_v_oob) else None
|
|
if const_expr(need_predicates):
|
|
seqlen_limit = seqlen - block * self.tile_n - tVcV[0][0]
|
|
predicate_n = t0VcV[0, n, 0][0] < seqlen_limit
|
|
predicate = cute.make_fragment_like(tVpV[None, 0, None])
|
|
for k in cutlass.range_constexpr(cute.size(predicate.shape[1])):
|
|
for i in cutlass.range_constexpr(cute.size(predicate.shape[0])):
|
|
predicate[i, k] = (
|
|
tVpV[i, n, k] if const_expr(self.check_hdim_v_oob) else True
|
|
) and predicate_n
|
|
cute.copy(
|
|
gmem_tiled_copy,
|
|
tVgV[None, n, None, block],
|
|
tVsV[
|
|
None, n, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0
|
|
],
|
|
pred=predicate,
|
|
)
|
|
else:
|
|
cute.copy(
|
|
gmem_tiled_copy,
|
|
tVgV[None, None, None, block],
|
|
tVsV[None, None, None, smem_pipe_write if const_expr(self.num_stages > 1) else 0],
|
|
pred=tVpV if const_expr(self.check_hdim_v_oob) else None,
|
|
)
|
|
|
|
|
|
class FlashAttentionForwardSm80(FlashAttentionForwardBase):
|
|
def _get_smem_layout_atom(self):
|
|
sQ_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.tile_hdim)
|
|
sK_layout_atom = sQ_layout_atom
|
|
sV_layout_atom = sm80_utils.get_smem_layout_atom(self.dtype, self.tile_hdimv)
|
|
sO_layout_atom = sV_layout_atom
|
|
sP_layout_atom = None
|
|
return sQ_layout_atom, sK_layout_atom, sV_layout_atom, sO_layout_atom, sP_layout_atom
|
|
|
|
def _get_tiled_mma(self):
|
|
tiled_mma_qk = cute.make_tiled_mma(
|
|
warp.MmaF16BF16Op(self.dtype, Float32, (16, 8, 16)),
|
|
(self.num_threads // 32, 1, 1),
|
|
permutation_mnk=(self.num_threads // 32 * 16, 16, 16),
|
|
)
|
|
tiled_mma_pv = cute.make_tiled_mma(
|
|
warp.MmaF16BF16Op(self.dtype, Float32, (16, 8, 16)),
|
|
(self.num_threads // 32, 1, 1),
|
|
permutation_mnk=(self.num_threads // 32 * 16, 16, 16),
|
|
)
|
|
return tiled_mma_qk, tiled_mma_pv
|
|
|
|
def _get_shared_storage_cls(self):
|
|
sQ_struct, sK_struct, sV_struct = [
|
|
cute.struct.Align[cute.struct.MemRange[self.dtype, cute.cosize(layout)], 1024]
|
|
for layout in (self.sQ_layout, self.sK_layout, self.sV_layout)
|
|
]
|
|
cosize_sQV = max(cute.cosize(self.sQ_layout), cute.cosize(self.sV_layout))
|
|
sQV_struct = cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sQV], 1024]
|
|
|
|
@cute.struct
|
|
class SharedStorageQKV:
|
|
sV: sV_struct
|
|
sQ: sQ_struct
|
|
sK: sK_struct
|
|
|
|
@cute.struct
|
|
class SharedStorageSharedQV:
|
|
sQ: sQV_struct
|
|
sK: sK_struct
|
|
|
|
return SharedStorageQKV if const_expr(not self.Q_in_regs) else SharedStorageSharedQV
|
|
|
|
@cute.jit
|
|
def __call__(
|
|
self,
|
|
mQ: cute.Tensor,
|
|
mK: cute.Tensor,
|
|
mV: cute.Tensor,
|
|
mO: cute.Tensor,
|
|
mLSE: Optional[cute.Tensor],
|
|
stream: cuda.CUstream,
|
|
softmax_scale: Optional[Float32] = None,
|
|
window_size_left: Optional[Int32] = None,
|
|
window_size_right: Optional[Int32] = None,
|
|
learnable_sink: Optional[cute.Tensor] = None,
|
|
aux_tensors=None,
|
|
):
|
|
"""Configures and launches the flash attention kernel.
|
|
|
|
mQ/mK/mV/mO has same data types(supports fp16 and bf16) and same layout:
|
|
(batch_size, seqlen_q, num_head, head_dim):(_, _, _, 1)
|
|
"""
|
|
assert learnable_sink is None, "Learnable sink is not supported in this kernel"
|
|
self._check_type(
|
|
*(t.element_type if t is not None else None for t in (mQ, mK, mV, mO, mLSE))
|
|
)
|
|
tiled_mma_qk, tiled_mma_pv = self._get_tiled_mma()
|
|
self.num_mma_threads = tiled_mma_pv.size
|
|
self.num_producer_threads = self.num_threads
|
|
self.num_Q_load_threads = self.num_threads
|
|
self.num_epilogue_threads = self.num_threads
|
|
# self.use_tma_O = self.arch >= 90 and mCuSeqlensQ is None
|
|
self.use_tma_O = self.arch >= 90
|
|
self._setup_attributes()
|
|
SharedStorage = self._get_shared_storage_cls()
|
|
# Assume all strides are divisible by 128 bits except the last stride
|
|
# Skip cute.assume() for stride=0 (broadcast dims from expand() are Python ints)
|
|
new_stride = lambda t: (
|
|
*(
|
|
cute.assume(s, divby=128 // t.element_type.width)
|
|
if s != 0
|
|
else s
|
|
for s in t.stride[:-1]
|
|
),
|
|
t.stride[-1],
|
|
)
|
|
mQ, mK, mV, mO = [
|
|
cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
|
|
for t in (mQ, mK, mV, mO)
|
|
]
|
|
mQ, mK, mV, mO = [
|
|
cute.make_tensor(t.iterator, cute.select(t.layout, mode=[1, 3, 2, 0]))
|
|
for t in (mQ, mK, mV, mO)
|
|
]
|
|
mLSE = cute.make_tensor(mLSE.iterator, cute.select(mLSE.layout, mode=[2, 1, 0]))
|
|
# grid_dim: (m_block, num_head, batch_size)
|
|
grid_dim = (
|
|
cute.ceil_div(mQ.shape[0], self.tile_m),
|
|
cute.size(mQ.shape[2]),
|
|
cute.size(mQ.shape[3]),
|
|
)
|
|
LOG2_E = math.log2(math.e)
|
|
if const_expr(self.score_mod is None):
|
|
softmax_scale_log2 = Float32(softmax_scale * LOG2_E)
|
|
softmax_scale = None
|
|
else:
|
|
# NB: If a user passes in a score mod, we want to apply the score-mod in the sm_scaled qk
|
|
# But in the original base 10. We hijack softmax_scale_log2 to just be the change of base
|
|
# and correctly apply the softmax_scale prior to score_mod in the softmax step
|
|
softmax_scale_log2 = Float32(LOG2_E)
|
|
softmax_scale = Float32(softmax_scale)
|
|
|
|
fastdiv_mods = None
|
|
if const_expr(aux_tensors is not None):
|
|
seqlen_q = cute.size(mQ.shape[0]) // (
|
|
self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1
|
|
)
|
|
seqlen_k = cute.size(mK.shape[0])
|
|
seqlen_q_divmod = FastDivmodDivisor(seqlen_q)
|
|
seqlen_k_divmod = FastDivmodDivisor(seqlen_k)
|
|
fastdiv_mods = (seqlen_q_divmod, seqlen_k_divmod)
|
|
|
|
self.kernel(
|
|
mQ,
|
|
mK,
|
|
mV,
|
|
mO,
|
|
mLSE,
|
|
softmax_scale_log2,
|
|
softmax_scale,
|
|
window_size_left,
|
|
window_size_right,
|
|
self.sQ_layout,
|
|
self.sK_layout,
|
|
self.sV_layout,
|
|
self.sO_layout,
|
|
self.sP_layout,
|
|
self.gmem_tiled_copy_Q,
|
|
self.gmem_tiled_copy_K,
|
|
self.gmem_tiled_copy_V,
|
|
self.gmem_tiled_copy_O,
|
|
tiled_mma_qk,
|
|
tiled_mma_pv,
|
|
SharedStorage,
|
|
aux_tensors,
|
|
fastdiv_mods,
|
|
).launch(
|
|
grid=grid_dim,
|
|
block=[self.num_threads, 1, 1],
|
|
smem=SharedStorage.size_in_bytes(),
|
|
stream=stream,
|
|
)
|
|
|
|
@cute.kernel
|
|
def kernel(
|
|
self,
|
|
mQ: cute.Tensor,
|
|
mK: cute.Tensor,
|
|
mV: cute.Tensor,
|
|
mO: cute.Tensor,
|
|
mLSE: Optional[cute.Tensor],
|
|
softmax_scale_log2: Float32,
|
|
softmax_scale: Optional[Float32],
|
|
window_size_left: Optional[Int32],
|
|
window_size_right: Optional[Int32],
|
|
sQ_layout: cute.ComposedLayout,
|
|
sK_layout: cute.ComposedLayout,
|
|
sV_layout: cute.ComposedLayout,
|
|
sO_layout: cute.ComposedLayout,
|
|
sP_layout: cute.ComposedLayout | None,
|
|
gmem_tiled_copy_Q: cute.TiledCopy,
|
|
gmem_tiled_copy_K: cute.TiledCopy,
|
|
gmem_tiled_copy_V: cute.TiledCopy,
|
|
gmem_tiled_copy_O: cute.TiledCopy,
|
|
tiled_mma_qk: cute.TiledMma,
|
|
tiled_mma_pv: cute.TiledMma,
|
|
SharedStorage: cutlass.Constexpr,
|
|
aux_tensors=None,
|
|
fastdiv_mods=None,
|
|
):
|
|
# Thread index, block index
|
|
tidx, _, _ = cute.arch.thread_idx()
|
|
m_block, num_head, batch_size = cute.arch.block_idx()
|
|
|
|
block_info = BlockInfo(
|
|
self.tile_m,
|
|
self.tile_n,
|
|
self.is_causal,
|
|
self.is_local,
|
|
False, # is_split_kv
|
|
window_size_left,
|
|
window_size_right,
|
|
qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
|
|
)
|
|
seqlen = SeqlenInfoQK.create(seqlen_q_static=mQ.shape[0], seqlen_k_static=mK.shape[0])
|
|
n_block_min, n_block_max = block_info.get_n_block_min_max(seqlen, m_block)
|
|
# TODO: return early if n_block_max == 0
|
|
# if self.is_causal:
|
|
# if n_block_max <= 0:
|
|
# return
|
|
n_block = n_block_max - 1
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Get the appropriate tiles for this thread block.
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
blkQ_shape = (self.tile_m, self.tile_hdim)
|
|
blkK_shape = (self.tile_n, self.tile_hdim)
|
|
blkV_shape = (self.tile_n, self.tile_hdimv)
|
|
gQ = cute.local_tile(mQ[None, None, num_head, batch_size], blkQ_shape, (m_block, 0))
|
|
num_head_kv = num_head // self.qhead_per_kvhead
|
|
gK = cute.local_tile(mK[None, None, num_head_kv, batch_size], blkK_shape, (None, 0))
|
|
gV = cute.local_tile(mV[None, None, num_head_kv, batch_size], blkV_shape, (None, 0))
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Get shared memory buffer
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
smem = cutlass.utils.SmemAllocator()
|
|
storage = smem.allocate(SharedStorage)
|
|
sQ = storage.sQ.get_tensor(sQ_layout)
|
|
sK = storage.sK.get_tensor(sK_layout)
|
|
if const_expr(not self.Q_in_regs):
|
|
sV = storage.sV.get_tensor(sV_layout)
|
|
else:
|
|
sV = cute.make_tensor(cute.recast_ptr(sQ.iterator, dtype=self.dtype), sV_layout)
|
|
# Transpose view of V to tensor with layout (head_dim_v, tile_n) for tiled mma
|
|
sVt = utils.transpose_view(sV)
|
|
|
|
gmem_thr_copy_K = gmem_tiled_copy_K.get_slice(tidx)
|
|
gmem_thr_copy_V = gmem_tiled_copy_V.get_slice(tidx)
|
|
# (CPY_Atom, CPY_N, CPY_K, n_block)
|
|
tKsK, tKgK = gmem_thr_copy_K.partition_D(sK), gmem_thr_copy_K.partition_S(gK)
|
|
# (CPY_Atom, CPY_N, CPY_K, n_block)
|
|
tVsV, tVgV = gmem_thr_copy_V.partition_D(sV), gmem_thr_copy_V.partition_S(gV)
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Tile MMA compute thread partitions and allocate accumulators
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
thr_mma_qk = tiled_mma_qk.get_slice(tidx)
|
|
thr_mma_pv = tiled_mma_pv.get_slice(tidx)
|
|
tSrQ = thr_mma_qk.make_fragment_A(thr_mma_qk.partition_A(sQ))
|
|
tSrK = thr_mma_qk.make_fragment_B(thr_mma_qk.partition_B(sK[None, None, 0]))
|
|
tOrVt = thr_mma_pv.make_fragment_B(thr_mma_pv.partition_B(sVt[None, None, 0]))
|
|
acc_shape_O = thr_mma_pv.partition_shape_C((self.tile_m, self.tile_hdimv))
|
|
acc_O = cute.make_fragment(acc_shape_O, Float32)
|
|
acc_O.fill(0.0)
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Smem copy atom tiling
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
smem_copy_atom_QK = cute.make_copy_atom(
|
|
warp.LdMatrix8x8x16bOp(transpose=False, num_matrices=4),
|
|
self.dtype,
|
|
)
|
|
smem_copy_atom_V = cute.make_copy_atom(
|
|
warp.LdMatrix8x8x16bOp(transpose=True, num_matrices=4),
|
|
self.dtype,
|
|
)
|
|
smem_thr_copy_Q = utils.make_tiled_copy_A(smem_copy_atom_QK, tiled_mma_qk).get_slice(tidx)
|
|
smem_thr_copy_K = utils.make_tiled_copy_B(smem_copy_atom_QK, tiled_mma_qk).get_slice(tidx)
|
|
smem_thr_copy_V = utils.make_tiled_copy_B(smem_copy_atom_V, tiled_mma_pv).get_slice(tidx)
|
|
|
|
tSsQ = smem_thr_copy_Q.partition_S(sQ)
|
|
tSsK = smem_thr_copy_K.partition_S(sK)
|
|
tOsVt = smem_thr_copy_V.partition_S(sVt)
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Predicate: Mark indices that need to copy when problem_shape isn't a multiple
|
|
# of tile_shape
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Construct identity layout for KV
|
|
cK = cute.make_identity_tensor((self.tile_n, self.tile_hdim))
|
|
tKcK = gmem_thr_copy_K.partition_S(cK)
|
|
t0KcK = gmem_thr_copy_K.get_slice(0).partition_S(cK)
|
|
if const_expr(self.tile_hdim == self.tile_hdimv):
|
|
tVcV = tKcK
|
|
t0VcV = t0KcK
|
|
else:
|
|
cV = cute.make_identity_tensor((self.tile_n, self.tile_hdimv))
|
|
tVcV = gmem_thr_copy_V.partition_S(cV)
|
|
t0VcV = gmem_thr_copy_V.get_slice(0).partition_S(cV)
|
|
# Allocate predicate tensors for m and n, here we only allocate the tile of k, and
|
|
# use "if" on the mn dimension.
|
|
# This is to reduce register pressure and gets 2-3% performance gain.
|
|
tKpK = utils.predicate_k(tKcK, limit=mK.shape[1])
|
|
if const_expr(self.same_hdim_kv):
|
|
tVpV = tKpK
|
|
else:
|
|
tVpV = utils.predicate_k(tVcV, limit=mV.shape[1])
|
|
|
|
# shape: (atom_v_m * rest_m)
|
|
softmax = Softmax.create(
|
|
softmax_scale_log2,
|
|
num_rows=acc_O.shape[0][0] * acc_O.shape[1],
|
|
softmax_scale=softmax_scale,
|
|
)
|
|
softmax.reset()
|
|
|
|
# group parameters for compute_one_n_block
|
|
mma_params = SimpleNamespace(
|
|
thr_mma_qk=thr_mma_qk,
|
|
thr_mma_pv=thr_mma_pv,
|
|
tSrQ=tSrQ,
|
|
tSrK=tSrK,
|
|
tOrVt=tOrVt,
|
|
acc_O=acc_O,
|
|
)
|
|
smem_copy_params = SimpleNamespace(
|
|
smem_thr_copy_Q=smem_thr_copy_Q,
|
|
smem_thr_copy_K=smem_thr_copy_K,
|
|
smem_thr_copy_V=smem_thr_copy_V,
|
|
tSsQ=tSsQ,
|
|
tSsK=tSsK,
|
|
tOsVt=tOsVt,
|
|
)
|
|
load_K = partial(
|
|
self.load_K, gmem_tiled_copy_K, tKgK, tKsK, tKcK, t0KcK, tKpK, seqlen=seqlen.seqlen_k
|
|
)
|
|
load_V = partial(
|
|
self.load_V, gmem_tiled_copy_V, tVgV, tVsV, tVcV, t0VcV, tVpV, seqlen=seqlen.seqlen_k
|
|
)
|
|
|
|
compute_one_n_block = partial(
|
|
self.compute_one_n_block,
|
|
mma_params=mma_params,
|
|
smem_copy_params=smem_copy_params,
|
|
softmax=softmax,
|
|
load_K=load_K,
|
|
load_V=load_V,
|
|
score_mod=self.score_mod,
|
|
batch_idx=batch_size,
|
|
head_idx=num_head,
|
|
m_block=m_block,
|
|
aux_tensors=aux_tensors,
|
|
fastdiv_mods=fastdiv_mods,
|
|
)
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Prologue
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Start async loads of the last mn-tile, where we take care of the mn residue
|
|
gmem_thr_copy_Q = gmem_tiled_copy_Q.get_slice(tidx)
|
|
self.load_Q(gmem_thr_copy_Q, gQ, sQ, m_block, seqlen=seqlen.seqlen_q, headdim=mQ.shape[1])
|
|
cute.arch.cp_async_commit_group()
|
|
|
|
def preprocess_Q():
|
|
cute.arch.cp_async_wait_group(self.num_stages * 2 - 1)
|
|
if const_expr(self.Q_in_regs):
|
|
cute.arch.barrier()
|
|
tSrQ_copy_view = smem_thr_copy_Q.retile(tSrQ)
|
|
cute.copy(smem_thr_copy_Q, tSsQ, tSrQ_copy_view)
|
|
|
|
# If Q_in_regs, we load Q, then load 1 stage of K, then (optionally) rotate Q and
|
|
# read from smem_q to registers, then load V.
|
|
# If !Q_in_regs, we load Q, load all stages of K & V, then (optionally) rotate Q.
|
|
if const_expr(self.Q_in_regs):
|
|
load_K(n_block, smem_pipe_write=0, need_predicates=True)
|
|
cute.arch.cp_async_commit_group()
|
|
preprocess_Q()
|
|
cute.arch.barrier() # Make sure all threads have read smem_q before loading V
|
|
|
|
for stage in cutlass.range_constexpr(self.num_stages):
|
|
if const_expr(not self.Q_in_regs or stage > 0):
|
|
if stage == 0 or n_block - stage >= 0:
|
|
load_K(n_block - stage, smem_pipe_write=stage, need_predicates=stage == 0)
|
|
cute.arch.cp_async_commit_group()
|
|
if const_expr(stage < self.num_stages - 1):
|
|
if stage == 0 or n_block - stage >= 0:
|
|
load_V(n_block - stage, smem_pipe_write=stage, need_predicates=stage == 0)
|
|
cute.arch.cp_async_commit_group()
|
|
if const_expr(not self.Q_in_regs):
|
|
preprocess_Q()
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Mainloop
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Start processing of the first n-block.
|
|
# For performance reason, we separate out two kinds of iterations:
|
|
# those that need masking on S, and those that don't.
|
|
# We need masking on S for the very last block when K and V has length not multiple of tile_n.
|
|
# We also need masking on S if it's causal, for the last several blocks.
|
|
mask = AttentionMask(
|
|
self.tile_m,
|
|
self.tile_n,
|
|
seqlen.seqlen_q,
|
|
seqlen.seqlen_k,
|
|
window_size_left,
|
|
window_size_right,
|
|
self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
|
|
)
|
|
mask_fn = partial(
|
|
mask.apply_mask,
|
|
m_block=m_block,
|
|
thr_mma=thr_mma_qk,
|
|
mask_causal=self.is_causal,
|
|
mask_local=self.is_local,
|
|
fastdiv_mods=fastdiv_mods if const_expr(self.mask_mod is not None) else None,
|
|
)
|
|
|
|
# First iteration with seqlen masking
|
|
smem_pipe_read = Int32(0)
|
|
smem_pipe_write = Int32(self.num_stages - 1)
|
|
compute_one_n_block(
|
|
n_block,
|
|
smem_pipe_read,
|
|
smem_pipe_write,
|
|
is_first_n_block=True,
|
|
check_inf=True,
|
|
mask_fn=partial(mask_fn, mask_seqlen=True),
|
|
)
|
|
smem_pipe_read = self.advance_pipeline(smem_pipe_read)
|
|
smem_pipe_write = self.advance_pipeline(smem_pipe_write)
|
|
# Next couple of iterations with causal masking
|
|
if const_expr(self.is_causal or self.is_local):
|
|
n_block_min_causal_local_mask = block_info.get_n_block_min_causal_local_mask(
|
|
seqlen, m_block, n_block_min
|
|
)
|
|
for n_tile in cutlass.range(n_block_max - 1 - n_block_min_causal_local_mask, unroll=1):
|
|
n_block = n_block_max - 2 - n_tile
|
|
compute_one_n_block(
|
|
n_block,
|
|
smem_pipe_read,
|
|
smem_pipe_write,
|
|
check_inf=True,
|
|
mask_fn=partial(mask_fn, mask_seqlen=False),
|
|
)
|
|
smem_pipe_read = self.advance_pipeline(smem_pipe_read)
|
|
smem_pipe_write = self.advance_pipeline(smem_pipe_write)
|
|
# The remaining iterations have no masking
|
|
for n_tile in cutlass.range(n_block, unroll=1):
|
|
compute_one_n_block(
|
|
n_block - n_tile - 1, smem_pipe_read, smem_pipe_write, check_inf=True
|
|
)
|
|
smem_pipe_read = self.advance_pipeline(smem_pipe_read)
|
|
smem_pipe_write = self.advance_pipeline(smem_pipe_write)
|
|
# TODO: local
|
|
|
|
# normalize acc_O by row_sum and calculate the lse
|
|
row_scale = softmax.finalize()
|
|
softmax.rescale_O(acc_O, row_scale)
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Epilogue
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# reuse sQ's data iterator
|
|
sO = cute.make_tensor(sQ.iterator, sO_layout)
|
|
self.epilogue(
|
|
acc_O,
|
|
softmax.row_sum,
|
|
mO,
|
|
mLSE,
|
|
sO,
|
|
seqlen,
|
|
gmem_tiled_copy_O,
|
|
None,
|
|
tiled_mma_pv,
|
|
tidx,
|
|
m_block,
|
|
num_head,
|
|
batch_size,
|
|
)
|
|
|
|
@cute.jit
|
|
def compute_one_n_block(
|
|
self,
|
|
n_block: Int32,
|
|
smem_pipe_read: Int32,
|
|
smem_pipe_write: Int32,
|
|
mma_params: SimpleNamespace,
|
|
smem_copy_params: SimpleNamespace,
|
|
softmax: Softmax,
|
|
load_K: Callable,
|
|
load_V: Callable,
|
|
score_mod: Callable | None,
|
|
batch_idx: cutlass.Int32,
|
|
head_idx: cutlass.Int32,
|
|
m_block: cutlass.Int32,
|
|
seqlen: SeqlenInfoQK,
|
|
aux_tensors=None,
|
|
fastdiv_mods=None,
|
|
mask_fn: Optional[Callable] = None,
|
|
is_first_n_block: cutlass.Constexpr = False,
|
|
check_inf: cutlass.Constexpr = True,
|
|
):
|
|
"""Compute one n_block of S/O.
|
|
|
|
This function provides different variants for processing the first n block versus
|
|
subsequent blocks.
|
|
"""
|
|
|
|
def sync():
|
|
cute.arch.cp_async_wait_group(self.num_stages * 2 - 2)
|
|
cute.arch.barrier()
|
|
|
|
acc_shape_S = mma_params.thr_mma_qk.partition_shape_C((self.tile_m, self.tile_n))
|
|
acc_S = cute.make_fragment(acc_shape_S, Float32)
|
|
acc_S.fill(0.0)
|
|
# wait for smem tile QK before mma calculation for S
|
|
sync()
|
|
|
|
# need predicates for the first tile
|
|
def load_V_next():
|
|
if self.num_stages == 1 or n_block - self.num_stages + 1 >= 0:
|
|
load_V(
|
|
n_block - self.num_stages + 1,
|
|
smem_pipe_write,
|
|
need_predicates=is_first_n_block and self.num_stages == 1,
|
|
)
|
|
cute.arch.cp_async_commit_group()
|
|
|
|
load_V_next()
|
|
sm80_utils.gemm(
|
|
mma_params.thr_mma_qk,
|
|
acc_S,
|
|
mma_params.tSrQ,
|
|
mma_params.tSrK,
|
|
smem_copy_params.tSsQ,
|
|
smem_copy_params.tSsK[
|
|
None, None, None, smem_pipe_read if const_expr(self.num_stages > 1) else 0
|
|
],
|
|
smem_copy_params.smem_thr_copy_Q,
|
|
smem_copy_params.smem_thr_copy_K,
|
|
# hook_fn=load_V_next,
|
|
A_in_regs=self.Q_in_regs,
|
|
)
|
|
if const_expr(score_mod is not None):
|
|
self.apply_score_mod(
|
|
mma_params.thr_mma_qk,
|
|
batch_idx,
|
|
head_idx,
|
|
m_block,
|
|
acc_S,
|
|
n_block,
|
|
seqlen,
|
|
softmax_scale=softmax.softmax_scale,
|
|
aux_tensors=aux_tensors,
|
|
fastdiv_mods=fastdiv_mods,
|
|
)
|
|
|
|
smem_pipe_write = self.advance_pipeline(smem_pipe_write)
|
|
|
|
def load_K_next():
|
|
if n_block - self.num_stages >= 0:
|
|
load_K(n_block - self.num_stages, smem_pipe_write, need_predicates=False)
|
|
cute.arch.cp_async_commit_group()
|
|
|
|
# wait for smem tile V for O
|
|
if const_expr(self.num_stages == 1):
|
|
sync()
|
|
load_K_next()
|
|
if const_expr(mask_fn is not None):
|
|
mask_fn(acc_S, n_block=n_block)
|
|
row_scale = softmax.online_softmax(acc_S, is_first=is_first_n_block, check_inf=check_inf)
|
|
softmax.rescale_O(mma_params.acc_O, row_scale)
|
|
rP = cute.make_fragment_like(acc_S, self.dtype)
|
|
rP.store(acc_S.load().to(self.dtype))
|
|
tOrP = cute.make_tensor(rP.iterator, utils.convert_layout_acc_frgA(rP.layout))
|
|
if const_expr(self.num_stages > 1):
|
|
sync()
|
|
load_K_next()
|
|
sm80_utils.gemm_rs(
|
|
mma_params.thr_mma_pv,
|
|
mma_params.acc_O,
|
|
tOrP,
|
|
mma_params.tOrVt,
|
|
smem_copy_params.tOsVt[
|
|
None, None, None, smem_pipe_read if const_expr(self.num_stages > 1) else 0
|
|
],
|
|
smem_copy_params.smem_thr_copy_V,
|
|
# hook_fn=load_K_next,
|
|
)
|
|
# if const_expr(self.num_stages > 1):
|
|
# load_K_next()
|
|
|
|
|
|
class FlashAttentionForwardSm90(FlashAttentionForwardBase):
|
|
arch = 90
|
|
|
|
def __init__(
|
|
self,
|
|
*args,
|
|
intra_wg_overlap: bool = True,
|
|
mma_pv_is_rs: bool = True,
|
|
**kwargs,
|
|
):
|
|
super().__init__(*args, **kwargs)
|
|
self.intra_wg_overlap = intra_wg_overlap
|
|
self.mma_pv_is_rs = mma_pv_is_rs
|
|
self.buffer_align_bytes = 1024
|
|
|
|
def _get_smem_layout_atom(self):
|
|
sQ_layout_atom = warpgroup.make_smem_layout_atom(
|
|
sm90_utils_basic.get_smem_layout_atom(LayoutEnum.ROW_MAJOR, self.dtype, self.tile_hdim),
|
|
self.dtype,
|
|
)
|
|
sK_layout_atom = sQ_layout_atom
|
|
sV_layout_atom = warpgroup.make_smem_layout_atom(
|
|
sm90_utils_basic.get_smem_layout_atom(
|
|
LayoutEnum.ROW_MAJOR, self.dtype, self.tile_hdimv
|
|
),
|
|
self.dtype,
|
|
)
|
|
sO_layout_atom = sV_layout_atom
|
|
if not self.mma_pv_is_rs:
|
|
sP_layout_atom = warpgroup.make_smem_layout_atom(
|
|
sm90_utils_basic.get_smem_layout_atom(
|
|
LayoutEnum.ROW_MAJOR, self.dtype, self.tile_n
|
|
),
|
|
self.dtype,
|
|
)
|
|
else:
|
|
sP_layout_atom = None
|
|
return sQ_layout_atom, sK_layout_atom, sV_layout_atom, sO_layout_atom, sP_layout_atom
|
|
|
|
def _get_tiled_mma(self):
|
|
tiled_mma_qk = sm90_utils_basic.make_trivial_tiled_mma(
|
|
self.dtype,
|
|
self.dtype,
|
|
warpgroup.OperandMajorMode.K,
|
|
warpgroup.OperandMajorMode.K,
|
|
Float32,
|
|
atom_layout_mnk=(self.tile_m // 64, 1, 1), # Might need (1, 2, 1) for hdim 512
|
|
tiler_mn=(64, self.tile_n),
|
|
)
|
|
tiled_mma_pv = sm90_utils_basic.make_trivial_tiled_mma(
|
|
self.dtype,
|
|
self.dtype,
|
|
warpgroup.OperandMajorMode.K,
|
|
warpgroup.OperandMajorMode.MN,
|
|
Float32,
|
|
atom_layout_mnk=(self.tile_m // 64, 1, 1), # Might need (1, 2, 1) for hdim 512
|
|
tiler_mn=(64, self.tile_hdimv),
|
|
a_source=warpgroup.OperandSource.RMEM
|
|
if self.mma_pv_is_rs
|
|
else warpgroup.OperandSource.SMEM,
|
|
)
|
|
tiled_mma_pv_rs = sm90_utils_basic.make_trivial_tiled_mma(
|
|
self.dtype,
|
|
self.dtype,
|
|
warpgroup.OperandMajorMode.K,
|
|
warpgroup.OperandMajorMode.MN,
|
|
Float32,
|
|
atom_layout_mnk=(self.tile_m // 64, 1, 1), # Might need (1, 2, 1) for hdim 512
|
|
tiler_mn=(64, self.tile_hdimv),
|
|
a_source=warpgroup.OperandSource.RMEM,
|
|
)
|
|
return tiled_mma_qk, tiled_mma_pv, tiled_mma_pv_rs
|
|
|
|
def _get_shared_storage_cls(self):
|
|
# If we use cp.async to load Q, we want sQ to align to 1024 bytes
|
|
sQ_struct, sK_struct, sV_struct = [
|
|
cute.struct.Align[cute.struct.MemRange[self.dtype, cute.cosize(layout)], self.buffer_align_bytes]
|
|
for layout in (self.sQ_layout, self.sK_layout, self.sV_layout)
|
|
|
|
]
|
|
cosize_sQV = max(cute.cosize(self.sQ_layout), cute.cosize(self.sV_layout))
|
|
sQV_struct = cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sQV], 1024]
|
|
cosize_sP = cute.cosize(self.sP_layout) if const_expr(self.sP_layout is not None) else 0
|
|
sP_struct = cute.struct.Align[cute.struct.MemRange[self.dtype, cosize_sP], 1024]
|
|
# 1 for Q, 1 for O, self.num_stages*2 for K, self.num_stages*2 for V,
|
|
mbar_ptr_QO_struct = cute.struct.MemRange[cutlass.Int64, 2]
|
|
mbar_ptr_K_struct = cute.struct.MemRange[cutlass.Int64, self.num_stages * 2]
|
|
mbar_ptr_V_struct = cute.struct.MemRange[cutlass.Int64, self.num_stages * 2]
|
|
|
|
@cute.struct
|
|
class SharedStorageQKV:
|
|
mbar_ptr: mbar_ptr_QO_struct
|
|
mbar_ptr_K: mbar_ptr_K_struct
|
|
mbar_ptr_V: mbar_ptr_V_struct
|
|
sV: sV_struct
|
|
sQ: sQ_struct
|
|
sK: sK_struct
|
|
sP: sP_struct
|
|
|
|
@cute.struct
|
|
class SharedStorageSharedQV:
|
|
mbar_ptr: mbar_ptr_QO_struct
|
|
mbar_ptr_K: mbar_ptr_K_struct
|
|
mbar_ptr_V: mbar_ptr_V_struct
|
|
sQ: sQV_struct
|
|
sK: sK_struct
|
|
sP: sP_struct
|
|
|
|
return SharedStorageQKV if const_expr(not self.Q_in_regs) else SharedStorageSharedQV
|
|
|
|
@cute.jit
|
|
def __call__(
|
|
self,
|
|
mQ: cute.Tensor, # (b, s_q, h, d) or (total_q, h, d) if there is cu_seqlens_q
|
|
mK: cute.Tensor, # (b_k, s_k, h_k, d) or (total_k, h_k, d) if there is cu_seqlens_k or (num_pages, page_size, h_k, d) if there is page_table
|
|
mV: cute.Tensor, # (b_k, s_k, h_k, dv) or (total_k, h_k, dv) if there is cu_seqlens_k or (num_pages, page_size, h_k, dv) if there is page_table
|
|
mO: cute.Tensor, # (b, s_q, h, dv) or (total_q, h, dv) if there is cu_seqlens_q
|
|
mLSE: Optional[cute.Tensor],
|
|
softmax_scale: Float32,
|
|
stream: cuda.CUstream,
|
|
mCuSeqlensQ: Optional[cute.Tensor] = None,
|
|
mCuSeqlensK: Optional[cute.Tensor] = None,
|
|
mSeqUsedQ: Optional[cute.Tensor] = None,
|
|
mSeqUsedK: Optional[cute.Tensor] = None,
|
|
mPageTable: Optional[cute.Tensor] = None, # (b_k, max_num_pages_per_seq)
|
|
window_size_left: Int32 | int | None = None,
|
|
window_size_right: Int32 | int | None = None,
|
|
learnable_sink: Optional[cute.Tensor] = None,
|
|
blocksparse_tensors: Optional[BlockSparseTensors] = None,
|
|
aux_tensors: Optional[list] = None,
|
|
):
|
|
"""Configures and launches the flash attention kernel.
|
|
|
|
mQ/mK/mV/mO has same data types(supports fp16 and bf16) and same layout:
|
|
(batch_size, seqlen_q, num_head, head_dim):(_, _, _, 1)
|
|
"""
|
|
|
|
self._check_type(
|
|
*(
|
|
t.element_type if t is not None else None
|
|
for t in (mQ, mK, mV, mO, mLSE, mCuSeqlensQ, mCuSeqlensK, mSeqUsedQ, mSeqUsedK)
|
|
)
|
|
)
|
|
|
|
# Assume all strides are divisible by 128 bits except the last stride
|
|
# Skip cute.assume() for stride=0 (broadcast dims from expand() are Python ints)
|
|
new_stride = lambda t: (
|
|
*(
|
|
cute.assume(s, divby=128 // t.element_type.width)
|
|
if s != 0
|
|
else s
|
|
for s in t.stride[:-1]
|
|
),
|
|
t.stride[-1],
|
|
)
|
|
|
|
mQ, mK, mV, mO = [
|
|
cute.make_tensor(t.iterator, cute.make_layout(t.shape, stride=new_stride(t)))
|
|
for t in (mQ, mK, mV, mO)
|
|
]
|
|
QO_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 2, 1]
|
|
mQ, mO = [utils.select(t, QO_layout_transpose) for t in (mQ, mO)]
|
|
KV_layout_transpose = [1, 3, 2, 0] if const_expr(mCuSeqlensK is None) else [0, 2, 1]
|
|
mK, mV = [utils.select(t, KV_layout_transpose) for t in (mK, mV)]
|
|
LSE_layout_transpose = [2, 1, 0] if const_expr(mCuSeqlensQ is None) else [1, 0]
|
|
mLSE = utils.select(mLSE, LSE_layout_transpose) if const_expr(mLSE is not None) else None
|
|
|
|
tiled_mma_qk, tiled_mma_pv, tiled_mma_pv_rs = self._get_tiled_mma()
|
|
self.num_mma_threads = tiled_mma_qk.size
|
|
self.num_threads_per_warp_group = 128
|
|
self.num_mma_warp_groups = self.num_mma_threads // self.num_threads_per_warp_group
|
|
self.num_threads = self.num_threads_per_warp_group * (self.num_mma_warp_groups + 1)
|
|
self.num_producer_threads = 32
|
|
self.num_Q_load_threads = self.num_mma_threads # If not TMA_Q, MMA threads load Q
|
|
self.num_epilogue_threads = self.num_mma_threads
|
|
self.num_mma_regs = (
|
|
256
|
|
if self.num_mma_warp_groups == 1
|
|
else (240 if self.num_mma_warp_groups == 2 else 160)
|
|
)
|
|
self.num_producer_regs = (
|
|
56 if self.num_mma_warp_groups == 1 else (24 if self.num_mma_warp_groups == 2 else 32)
|
|
)
|
|
# self.num_mma_regs = 232
|
|
# self.num_producer_regs = 40
|
|
self.use_block_sparsity = cutlass.const_expr(blocksparse_tensors is not None)
|
|
|
|
self.use_scheduler_barrier = (
|
|
(self.num_mma_warp_groups >= 2 and self.tile_hdim <= 128)
|
|
if const_expr(self.intra_wg_overlap)
|
|
else (self.num_mma_warp_groups == 2)
|
|
)
|
|
self.use_tma_Q = self.arch >= 90 and not (
|
|
self.pack_gqa and self.tile_m % self.qhead_per_kvhead != 0
|
|
)
|
|
self.use_tma_O = (
|
|
self.arch >= 90 and mCuSeqlensQ is None and mSeqUsedQ is None and not self.pack_gqa
|
|
)
|
|
# TODO: rescale_O_before_gemm
|
|
self._setup_attributes()
|
|
# TODO: we prob don't need most of what's in _setup_attributes
|
|
self.sQ_layout, self.sK_layout, self.sV_layout, self.sO_layout = [
|
|
sm90_utils.make_smem_layout(mX.element_type, LayoutEnum.ROW_MAJOR, shape, stage)
|
|
for mX, shape, stage in [
|
|
(mQ, (self.tile_m, self.tile_hdim), None),
|
|
(mK, (self.tile_n, self.tile_hdim), self.num_stages),
|
|
(mV, (self.tile_n, self.tile_hdimv), self.num_stages),
|
|
(mO, (self.tile_m, self.tile_hdimv), None),
|
|
]
|
|
]
|
|
self.sP_layout = None
|
|
if const_expr(not self.mma_pv_is_rs):
|
|
self.sP_layout = sm90_utils.make_smem_layout(
|
|
mV.dtype, LayoutEnum.ROW_MAJOR, (self.tile_m, self.tile_n)
|
|
)
|
|
|
|
SharedStorage = self._get_shared_storage_cls()
|
|
|
|
if const_expr(self.pack_gqa):
|
|
shape_Q_packed = (
|
|
(self.qhead_per_kvhead, mQ.shape[0]),
|
|
mQ.shape[1],
|
|
mK.shape[2],
|
|
*mQ.shape[3:],
|
|
)
|
|
stride_Q_packed = (
|
|
(mQ.stride[2], mQ.stride[0]),
|
|
mQ.stride[1],
|
|
mQ.stride[2] * self.qhead_per_kvhead,
|
|
*mQ.stride[3:],
|
|
)
|
|
mQ = cute.make_tensor(
|
|
mQ.iterator, cute.make_layout(shape_Q_packed, stride=stride_Q_packed)
|
|
)
|
|
shape_O_packed = (
|
|
(self.qhead_per_kvhead, mO.shape[0]),
|
|
mK.shape[1],
|
|
mK.shape[2],
|
|
*mO.shape[3:],
|
|
)
|
|
stride_O_packed = (
|
|
(mO.stride[2], mO.stride[0]),
|
|
mO.stride[1],
|
|
mO.stride[2] * self.qhead_per_kvhead,
|
|
*mO.stride[3:],
|
|
)
|
|
mO = cute.make_tensor(
|
|
mO.iterator, cute.make_layout(shape_O_packed, stride=stride_O_packed)
|
|
)
|
|
if const_expr(mLSE is not None):
|
|
shape_LSE_packed = (
|
|
(self.qhead_per_kvhead, mLSE.shape[0]),
|
|
mK.shape[2],
|
|
*mLSE.shape[2:],
|
|
)
|
|
stride_LSE_packed = (
|
|
(mLSE.stride[1], mLSE.stride[0]),
|
|
mLSE.stride[1] * self.qhead_per_kvhead,
|
|
*mLSE.stride[2:],
|
|
)
|
|
mLSE = cute.make_tensor(
|
|
mLSE.iterator, cute.make_layout(shape_LSE_packed, stride=stride_LSE_packed)
|
|
)
|
|
|
|
# TMA
|
|
gmem_tiled_copy_Q = cpasync.CopyBulkTensorTileG2SOp()
|
|
gmem_tiled_copy_KV = cpasync.CopyBulkTensorTileG2SOp() # Might multicast
|
|
gmem_tiled_copy_O = cpasync.CopyBulkTensorTileS2GOp()
|
|
self.tma_copy_bytes = {
|
|
name: cute.size_in_bytes(mX.element_type, cute.select(layout, mode=[0, 1]))
|
|
for name, mX, layout in [
|
|
("Q", mQ, self.sQ_layout),
|
|
("K", mK, self.sK_layout),
|
|
("V", mV, self.sV_layout),
|
|
]
|
|
}
|
|
tma_atom_Q, tma_tensor_Q = None, None
|
|
if const_expr(self.use_tma_Q):
|
|
tma_atom_Q, tma_tensor_Q = cpasync.make_tiled_tma_atom(
|
|
gmem_tiled_copy_Q,
|
|
mQ,
|
|
self.sQ_layout,
|
|
(self.tile_m, self.tile_hdim), # No mcast
|
|
)
|
|
tma_atom_K, tma_tensor_K = cpasync.make_tiled_tma_atom(
|
|
gmem_tiled_copy_KV,
|
|
mK,
|
|
cute.select(self.sK_layout, mode=[0, 1]),
|
|
(self.tile_n, self.tile_hdim),
|
|
1, # No mcast for now
|
|
)
|
|
tma_atom_V, tma_tensor_V = cpasync.make_tiled_tma_atom(
|
|
gmem_tiled_copy_KV,
|
|
mV,
|
|
cute.select(self.sV_layout, mode=[0, 1]),
|
|
(self.tile_n, self.tile_hdimv),
|
|
1, # No mcast for now
|
|
)
|
|
tma_atom_O, tma_tensor_O = None, None
|
|
if const_expr(self.use_tma_O):
|
|
tma_atom_O, tma_tensor_O = cpasync.make_tiled_tma_atom(
|
|
gmem_tiled_copy_O,
|
|
mO,
|
|
self.sO_layout,
|
|
(self.tile_m, self.tile_hdimv), # No mcast
|
|
)
|
|
if const_expr(mCuSeqlensQ is not None or mSeqUsedQ is not None):
|
|
TileScheduler = SingleTileVarlenScheduler
|
|
else:
|
|
TileScheduler = (
|
|
SingleTileScheduler
|
|
if const_expr(not self.is_causal or self.is_local)
|
|
else SingleTileLPTScheduler
|
|
)
|
|
tile_sched_args = TileSchedulerArguments(
|
|
cute.ceil_div(cute.size(mQ.shape[0]), self.tile_m),
|
|
cute.size(mQ.shape[2]),
|
|
cute.size(mQ.shape[3])
|
|
if const_expr(mCuSeqlensQ is None)
|
|
else cute.size(mCuSeqlensQ.shape[0] - 1),
|
|
1, # num_splits
|
|
cute.size(mK.shape[0]),
|
|
mQ.shape[1],
|
|
mV.shape[1],
|
|
total_q=cute.size(mQ.shape[0])
|
|
if const_expr(mCuSeqlensQ is not None)
|
|
else cute.size(mQ.shape[0]) * cute.size(mQ.shape[3]),
|
|
tile_shape_mn=(self.tile_m, self.tile_n),
|
|
mCuSeqlensQ=mCuSeqlensQ,
|
|
mSeqUsedQ=mSeqUsedQ,
|
|
qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
|
|
element_size=self.dtype.width // 8,
|
|
is_persistent=False,
|
|
lpt=self.is_causal or self.is_local,
|
|
)
|
|
tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
|
|
grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
|
|
LOG2_E = math.log2(math.e)
|
|
if const_expr(self.score_mod is None):
|
|
softmax_scale_log2 = softmax_scale * LOG2_E
|
|
softmax_scale = None
|
|
else:
|
|
# NB: If a user passes in a score mod, we want to apply the score-mod in the sm_scaled qk
|
|
# But in the original base 10. We hijack softmax_scale_log2 to just be the change of base
|
|
# and correctly apply the softmax_scale prior to score_mod in the softmax step
|
|
softmax_scale_log2 = LOG2_E
|
|
softmax_scale = softmax_scale
|
|
if const_expr(window_size_left is not None):
|
|
window_size_left = Int32(window_size_left)
|
|
if const_expr(window_size_right is not None):
|
|
window_size_right = Int32(window_size_right)
|
|
|
|
fastdiv_mods = None
|
|
if const_expr(aux_tensors is not None):
|
|
seqlen_q = cute.size(mQ.shape[0]) // (
|
|
self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1
|
|
)
|
|
seqlen_k = (
|
|
cute.size(mK.shape[0])
|
|
if const_expr(mPageTable is None)
|
|
else mK.shape[0] * mPageTable.shape[1]
|
|
)
|
|
seqlen_q_divmod = FastDivmodDivisor(seqlen_q)
|
|
seqlen_k_divmod = FastDivmodDivisor(seqlen_k)
|
|
fastdiv_mods = (seqlen_q_divmod, seqlen_k_divmod)
|
|
|
|
self.kernel(
|
|
tma_tensor_Q if const_expr(self.use_tma_Q) else mQ,
|
|
tma_tensor_K,
|
|
tma_tensor_V,
|
|
tma_tensor_O if const_expr(self.use_tma_O) else mO,
|
|
mLSE,
|
|
mCuSeqlensQ,
|
|
mCuSeqlensK,
|
|
mSeqUsedQ,
|
|
mSeqUsedK,
|
|
tma_atom_Q,
|
|
tma_atom_K,
|
|
tma_atom_V,
|
|
tma_atom_O,
|
|
softmax_scale_log2,
|
|
softmax_scale,
|
|
window_size_left,
|
|
window_size_right,
|
|
learnable_sink,
|
|
blocksparse_tensors,
|
|
self.sQ_layout,
|
|
self.sK_layout,
|
|
self.sV_layout,
|
|
self.sO_layout,
|
|
self.sP_layout,
|
|
self.gmem_tiled_copy_Q,
|
|
self.gmem_tiled_copy_K,
|
|
self.gmem_tiled_copy_V,
|
|
self.gmem_tiled_copy_O,
|
|
tiled_mma_qk,
|
|
tiled_mma_pv,
|
|
tiled_mma_pv_rs,
|
|
tile_sched_params,
|
|
TileScheduler,
|
|
SharedStorage,
|
|
aux_tensors,
|
|
fastdiv_mods,
|
|
).launch(
|
|
grid=grid_dim,
|
|
block=[self.num_threads, 1, 1],
|
|
stream=stream,
|
|
min_blocks_per_mp=1,
|
|
)
|
|
|
|
@cute.kernel
|
|
def kernel(
|
|
self,
|
|
mQ: cute.Tensor,
|
|
mK: cute.Tensor,
|
|
mV: cute.Tensor,
|
|
mO: cute.Tensor,
|
|
mLSE: Optional[cute.Tensor],
|
|
mCuSeqlensQ: Optional[cute.Tensor],
|
|
mCuSeqlensK: Optional[cute.Tensor],
|
|
mSeqUsedQ: Optional[cute.Tensor],
|
|
mSeqUsedK: Optional[cute.Tensor],
|
|
tma_atom_Q: Optional[cute.CopyAtom],
|
|
tma_atom_K: Optional[cute.CopyAtom],
|
|
tma_atom_V: Optional[cute.CopyAtom],
|
|
tma_atom_O: Optional[cute.CopyAtom],
|
|
softmax_scale_log2: Float32,
|
|
softmax_scale: Optional[Float32],
|
|
window_size_left: Optional[Int32],
|
|
window_size_right: Optional[Int32],
|
|
learnable_sink: Optional[cute.Tensor],
|
|
blocksparse_tensors: Optional[BlockSparseTensors],
|
|
sQ_layout: cute.ComposedLayout,
|
|
sK_layout: cute.ComposedLayout,
|
|
sV_layout: cute.ComposedLayout,
|
|
sO_layout: cute.ComposedLayout,
|
|
sP_layout: cute.ComposedLayout | None,
|
|
gmem_tiled_copy_Q: cute.TiledCopy,
|
|
gmem_tiled_copy_K: cute.TiledCopy,
|
|
gmem_tiled_copy_V: cute.TiledCopy,
|
|
gmem_tiled_copy_O: cute.TiledCopy,
|
|
tiled_mma_qk: cute.TiledMma,
|
|
tiled_mma_pv: cute.TiledMma,
|
|
tiled_mma_pv_rs: cute.TiledMma,
|
|
tile_sched_params: ParamsBase,
|
|
TileScheduler: cutlass.Constexpr[Callable],
|
|
SharedStorage: cutlass.Constexpr[Callable],
|
|
aux_tensors=Optional[list[cute.Tensor]],
|
|
fastdiv_mods=None,
|
|
):
|
|
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
|
|
# Prefetch tma descriptor
|
|
if warp_idx == 0:
|
|
for tma_atom in (tma_atom_Q, tma_atom_K, tma_atom_V, tma_atom_O):
|
|
if const_expr(tma_atom is not None):
|
|
cpasync.prefetch_descriptor(tma_atom)
|
|
|
|
smem = cutlass.utils.SmemAllocator()
|
|
storage = smem.allocate(SharedStorage)
|
|
|
|
# Mbarrier init
|
|
mbar_ptr_Q = storage.mbar_ptr.data_ptr()
|
|
if warp_idx == 1:
|
|
# if tidx < 2:
|
|
# # barrierO num threads should be self.num_mma_threads
|
|
# cute.arch.mbarrier_init(mbar_ptr_Q + tidx, 1 if tidx == 0 else self.num_mma_threads)
|
|
if const_expr(not self.use_tma_Q):
|
|
cute.arch.mbarrier_init(mbar_ptr_Q, self.num_Q_load_threads)
|
|
# cute.arch.mbarrier_init(mbar_ptr_Q + 1, self.num_mma_threads)
|
|
# We rely on pipeline_k and pipeline_v to initialize the mbarrier fence and sync
|
|
pipeline_kv_producer_group = cutlass.pipeline.CooperativeGroup(
|
|
cutlass.pipeline.Agent.Thread
|
|
)
|
|
pipeline_kv_consumer_group = cutlass.pipeline.CooperativeGroup(
|
|
cutlass.pipeline.Agent.Thread, self.num_mma_threads // cute.arch.WARP_SIZE
|
|
)
|
|
pipeline_k = pipeline.PipelineTmaAsync.create(
|
|
barrier_storage=storage.mbar_ptr_K.data_ptr(),
|
|
num_stages=self.num_stages,
|
|
producer_group=pipeline_kv_producer_group,
|
|
consumer_group=pipeline_kv_consumer_group,
|
|
tx_count=self.tma_copy_bytes["K"],
|
|
defer_sync=True,
|
|
)
|
|
pipeline_v = pipeline.PipelineTmaAsync.create(
|
|
barrier_storage=storage.mbar_ptr_V.data_ptr(),
|
|
num_stages=self.num_stages,
|
|
producer_group=pipeline_kv_producer_group,
|
|
consumer_group=pipeline_kv_consumer_group,
|
|
tx_count=self.tma_copy_bytes["V"],
|
|
defer_sync=False
|
|
)
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Get shared memory buffer
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
sQ = storage.sQ.get_tensor(sQ_layout.outer, swizzle=sQ_layout.inner)
|
|
sK = storage.sK.get_tensor(sK_layout.outer, swizzle=sK_layout.inner)
|
|
if const_expr(not self.Q_in_regs):
|
|
sV = storage.sV.get_tensor(sV_layout.outer, swizzle=sV_layout.inner)
|
|
else:
|
|
sV = storage.sQ.get_tensor(
|
|
sV_layout.outer, swizzle=sV_layout.inner, dtype=mV.element_type
|
|
)
|
|
# Transpose view of V to tensor with layout (head_dim_v, tile_n) for tiled mma
|
|
sVt = utils.transpose_view(sV)
|
|
sP = None
|
|
if const_expr(sP_layout is not None):
|
|
sP = storage.sP.get_tensor(sP_layout.outer, swizzle=sP_layout.inner)
|
|
# reuse sQ's data iterator
|
|
sO = storage.sQ.get_tensor(sO_layout.outer, swizzle=sO_layout.inner, dtype=self.dtype)
|
|
|
|
block_info = BlockInfo(
|
|
self.tile_m,
|
|
self.tile_n,
|
|
self.is_causal,
|
|
self.is_local,
|
|
False, # is_split_kv
|
|
window_size_left,
|
|
window_size_right,
|
|
qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
|
|
)
|
|
SeqlenInfoCls = partial(
|
|
SeqlenInfoQK.create,
|
|
seqlen_q_static=mQ.shape[0] if const_expr(not self.pack_gqa) else mQ.shape[0][1],
|
|
seqlen_k_static=mK.shape[0],
|
|
mCuSeqlensQ=mCuSeqlensQ,
|
|
mCuSeqlensK=mCuSeqlensK,
|
|
mSeqUsedQ=mSeqUsedQ,
|
|
mSeqUsedK=mSeqUsedK,
|
|
)
|
|
AttentionMaskCls = partial(
|
|
AttentionMask,
|
|
self.tile_m,
|
|
self.tile_n,
|
|
window_size_left=window_size_left,
|
|
window_size_right=window_size_right,
|
|
qhead_per_kvhead_packgqa=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
|
|
)
|
|
TileSchedulerCls = partial(TileScheduler.create, tile_sched_params)
|
|
|
|
if warp_idx < 4: # Producer
|
|
cute.arch.warpgroup_reg_dealloc(self.num_producer_regs)
|
|
self.load(
|
|
mQ,
|
|
mK,
|
|
mV,
|
|
sQ,
|
|
sK,
|
|
sV,
|
|
tma_atom_Q,
|
|
tma_atom_K,
|
|
tma_atom_V,
|
|
pipeline_k,
|
|
pipeline_v,
|
|
mbar_ptr_Q,
|
|
blocksparse_tensors,
|
|
block_info,
|
|
SeqlenInfoCls,
|
|
TileSchedulerCls,
|
|
)
|
|
|
|
else: # Consumer
|
|
cute.arch.warpgroup_reg_alloc(self.num_mma_regs)
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Tile MMA compute thread partitions and allocate accumulators
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
tidx, _, _ = cute.arch.thread_idx()
|
|
tidx = tidx - 128
|
|
self.mma(
|
|
tiled_mma_qk,
|
|
tiled_mma_pv,
|
|
tiled_mma_pv_rs,
|
|
mQ,
|
|
mO,
|
|
mLSE,
|
|
sQ,
|
|
sK,
|
|
sVt,
|
|
sP,
|
|
sO,
|
|
learnable_sink,
|
|
pipeline_k,
|
|
pipeline_v,
|
|
mbar_ptr_Q,
|
|
gmem_tiled_copy_Q,
|
|
gmem_tiled_copy_O,
|
|
tma_atom_O,
|
|
tidx,
|
|
softmax_scale_log2,
|
|
softmax_scale,
|
|
block_info,
|
|
SeqlenInfoCls,
|
|
AttentionMaskCls,
|
|
TileSchedulerCls,
|
|
blocksparse_tensors,
|
|
aux_tensors,
|
|
fastdiv_mods,
|
|
)
|
|
|
|
@cute.jit
|
|
def load(
|
|
self,
|
|
mQ: cute.Tensor,
|
|
mK: cute.Tensor,
|
|
mV: cute.Tensor,
|
|
sQ: cute.Tensor,
|
|
sK: cute.Tensor,
|
|
sV: cute.Tensor,
|
|
tma_atom_Q: cute.CopyAtom,
|
|
tma_atom_K: cute.CopyAtom,
|
|
tma_atom_V: cute.CopyAtom,
|
|
pipeline_k: cutlass.pipeline.PipelineAsync,
|
|
pipeline_v: cutlass.pipeline.PipelineAsync,
|
|
mbar_ptr_Q: cutlass.Pointer,
|
|
blocksparse_tensors: Optional[BlockSparseTensors],
|
|
block_info: BlockInfo,
|
|
SeqlenInfoCls: Callable,
|
|
TileSchedulerCls: Callable,
|
|
):
|
|
warp_idx_in_wg = cute.arch.make_warp_uniform(cute.arch.warp_idx()) % 4
|
|
if warp_idx_in_wg == 0:
|
|
q_producer_phase = Int32(1)
|
|
kv_producer_state = pipeline.make_pipeline_state(
|
|
cutlass.pipeline.PipelineUserType.Producer, self.num_stages
|
|
)
|
|
tile_scheduler = TileSchedulerCls()
|
|
work_tile = tile_scheduler.initial_work_tile_info()
|
|
while work_tile.is_valid_tile:
|
|
# if work_tile.is_valid_tile:
|
|
m_block, head_idx, batch_idx, _ = work_tile.tile_idx
|
|
seqlen = SeqlenInfoCls(batch_idx)
|
|
mQ_cur = seqlen.offset_batch_Q(mQ, batch_idx, dim=3)[None, None, head_idx]
|
|
head_idx_kv = (
|
|
head_idx // self.qhead_per_kvhead if const_expr(not self.pack_gqa) else head_idx
|
|
)
|
|
mK_cur = seqlen.offset_batch_K(mK, batch_idx, dim=3)[None, None, head_idx_kv]
|
|
mV_cur = seqlen.offset_batch_K(mV, batch_idx, dim=3)[None, None, head_idx_kv]
|
|
gK = cute.local_tile(mK_cur, (self.tile_n, self.tile_hdim), (None, 0))
|
|
gV = cute.local_tile(mV_cur, (self.tile_n, self.tile_hdimv), (None, 0))
|
|
if const_expr(self.use_tma_Q):
|
|
gQ = cute.local_tile(mQ_cur, (self.tile_m, self.tile_hdim), (m_block, 0))
|
|
load_Q, _, _ = copy_utils.tma_get_copy_fn(
|
|
tma_atom_Q, 0, cute.make_layout(1), gQ, sQ, single_stage=True
|
|
)
|
|
# TODO: mcast
|
|
# TODO check warp_idx if we have 128 producer threads
|
|
load_K, _, _ = copy_utils.tma_get_copy_fn(
|
|
tma_atom_K, 0, cute.make_layout(1), gK, sK
|
|
)
|
|
load_K = copy_utils.tma_producer_copy_fn(load_K, pipeline_k)
|
|
load_V, _, _ = copy_utils.tma_get_copy_fn(
|
|
tma_atom_V, 0, cute.make_layout(1), gV, sV
|
|
)
|
|
load_V = copy_utils.tma_producer_copy_fn(load_V, pipeline_v)
|
|
|
|
if const_expr(not self.use_block_sparsity):
|
|
n_block_min, n_block_max = block_info.get_n_block_min_max(seqlen, m_block)
|
|
# if cute.arch.thread_idx()[0] == 0:
|
|
# cute.printf("m_block = %d, n_block_min: %d, n_block_max: %d", m_block, n_block_min, n_block_max)
|
|
# First iteration: load both Q & K with the same mbarrier
|
|
n_block = n_block_max - 1
|
|
pipeline_k.producer_acquire(
|
|
kv_producer_state,
|
|
extra_tx_count=self.tma_copy_bytes["Q"]
|
|
if const_expr(self.use_tma_Q)
|
|
else 0,
|
|
)
|
|
if const_expr(self.use_tma_Q):
|
|
load_Q(tma_bar_ptr=pipeline_k.producer_get_barrier(kv_producer_state))
|
|
load_K(src_idx=n_block, producer_state=kv_producer_state)
|
|
|
|
if const_expr(not self.intra_wg_overlap):
|
|
pipeline_v.producer_acquire(kv_producer_state)
|
|
load_V(src_idx=n_block, producer_state=kv_producer_state)
|
|
kv_producer_state.advance()
|
|
for i in cutlass.range(n_block_max - 1 - n_block_min, unroll=1):
|
|
n_block = n_block_max - 1 - i - 1
|
|
pipeline_k.producer_acquire(kv_producer_state)
|
|
load_K(src_idx=n_block, producer_state=kv_producer_state)
|
|
pipeline_v.producer_acquire(kv_producer_state)
|
|
load_V(src_idx=n_block, producer_state=kv_producer_state)
|
|
kv_producer_state.advance()
|
|
else:
|
|
for i in cutlass.range(n_block_max - 1 - n_block_min, unroll=1):
|
|
n_block_prev = n_block_max - i - 1
|
|
n_block = n_block_prev - 1
|
|
kv_producer_state_prev = kv_producer_state.clone()
|
|
kv_producer_state.advance()
|
|
pipeline_k.producer_acquire(kv_producer_state)
|
|
load_K(src_idx=n_block, producer_state=kv_producer_state)
|
|
pipeline_v.producer_acquire(kv_producer_state_prev)
|
|
load_V(src_idx=n_block_prev, producer_state=kv_producer_state_prev)
|
|
n_block = n_block_min
|
|
pipeline_v.producer_acquire(kv_producer_state)
|
|
load_V(src_idx=n_block, producer_state=kv_producer_state)
|
|
kv_producer_state.advance()
|
|
else:
|
|
kv_producer_state = produce_block_sparse_loads(
|
|
blocksparse_tensors,
|
|
batch_idx,
|
|
head_idx,
|
|
m_block,
|
|
kv_producer_state,
|
|
load_Q,
|
|
load_K,
|
|
load_V,
|
|
pipeline_k,
|
|
pipeline_v,
|
|
self.use_tma_Q,
|
|
self.tma_copy_bytes["Q"],
|
|
self.intra_wg_overlap,
|
|
self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
|
|
)
|
|
|
|
tile_scheduler.prefetch_next_work()
|
|
tile_scheduler.advance_to_next_work()
|
|
work_tile = tile_scheduler.get_current_work()
|
|
# End of persistent scheduler loop
|
|
|
|
@cute.jit
|
|
def mma(
|
|
self,
|
|
tiled_mma_qk: cute.TiledMma,
|
|
tiled_mma_pv: cute.TiledMma,
|
|
tiled_mma_pv_rs: cute.TiledMma,
|
|
# softmax: Softmax,
|
|
# acc_O: cute.Tensor,
|
|
mQ: cute.Tensor,
|
|
mO: cute.Tensor,
|
|
mLSE: Optional[cute.Tensor],
|
|
sQ: cute.Tensor,
|
|
sK: cute.Tensor,
|
|
sVt: cute.Tensor,
|
|
sP: Optional[cute.Tensor],
|
|
sO: cute.Tensor,
|
|
learnable_sink: Optional[cute.Tensor],
|
|
pipeline_k: cutlass.pipeline.PipelineAsync,
|
|
pipeline_v: cutlass.pipeline.PipelineAsync,
|
|
mbar_ptr_Q: cutlass.Pointer,
|
|
gmem_tiled_copy_Q: cute.TiledCopy,
|
|
gmem_tiled_copy_O: cute.TiledCopy,
|
|
tma_atom_O: Optional[cute.CopyAtom],
|
|
tidx: Int32,
|
|
softmax_scale_log2: Float32,
|
|
softmax_scale: Optional[Float32],
|
|
block_info: BlockInfo,
|
|
SeqlenInfoCls: Callable,
|
|
AttentionMaskCls: Callable,
|
|
TileSchedulerCls: Callable,
|
|
blocksparse_tensors: Optional[BlockSparseTensors],
|
|
aux_tensors: Optional[list],
|
|
fastdiv_mods=None,
|
|
):
|
|
warp_group_idx = cute.arch.make_warp_uniform(tidx // self.num_threads_per_warp_group)
|
|
warp_group_thread_layout = cute.make_layout(
|
|
self.num_mma_warp_groups, stride=self.num_threads_per_warp_group
|
|
)
|
|
thr_mma_qk = tiled_mma_qk.get_slice(tidx)
|
|
wg_mma_qk = tiled_mma_qk.get_slice(warp_group_thread_layout(warp_group_idx))
|
|
wg_mma_pv = tiled_mma_pv.get_slice(warp_group_thread_layout(warp_group_idx))
|
|
tSrQ = tiled_mma_qk.make_fragment_A(wg_mma_qk.partition_A(sQ))
|
|
tSrK = tiled_mma_qk.make_fragment_B(wg_mma_qk.partition_B(sK))
|
|
if const_expr(self.mma_pv_is_rs):
|
|
acc_S_shape = tiled_mma_qk.partition_shape_C((self.tile_m, self.tile_n))
|
|
tOrP = cute.make_fragment(
|
|
utils.convert_layout_acc_frgA(cute.make_layout(acc_S_shape)), self.dtype
|
|
)
|
|
else:
|
|
tOrP = tiled_mma_pv.make_fragment_A(wg_mma_pv.partition_A(sP))
|
|
tOrVt = tiled_mma_pv.make_fragment_B(wg_mma_pv.partition_B(sVt))
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Smem copy atom tiling
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
smem_copy_atom_P = utils.get_smem_store_atom(self.arch, self.dtype)
|
|
smem_thr_copy_P = cute.make_tiled_copy_C(smem_copy_atom_P, tiled_mma_qk).get_slice(tidx)
|
|
# tPsP = smem_thr_copy_P.partition_D(sP_pi) if const_expr(sP_pi is not None) else None
|
|
tPsP = smem_thr_copy_P.partition_D(sP) if const_expr(sP is not None) else None
|
|
# if cute.arch.thread_idx()[0] == 0:
|
|
# cute.printf(sP_pi.layout, sP_pi.iterator)
|
|
# cute.printf(sP.layout, sP.iterator)
|
|
# cute.printf(tPsP.layout, tPsP.iterator)
|
|
|
|
self.mma_init()
|
|
|
|
acc_shape_O = tiled_mma_pv.partition_shape_C((self.tile_m, self.tile_hdimv))
|
|
acc_O = cute.make_fragment(acc_shape_O, Float32)
|
|
smem_copy_params = SimpleNamespace(smem_thr_copy_P=smem_thr_copy_P, tPsP=tPsP)
|
|
|
|
mma_qk_fn = partial(
|
|
sm90_utils.gemm_zero_init, tiled_mma_qk, (self.tile_m, self.tile_n), tSrQ, tSrK
|
|
)
|
|
mma_pv_fn = partial(sm90_utils.gemm_w_idx, tiled_mma_pv, acc_O, tOrP, tOrVt)
|
|
|
|
mma_one_n_block_all = partial(
|
|
self.mma_one_n_block_intrawg_overlap
|
|
if const_expr(self.intra_wg_overlap)
|
|
else self.mma_one_n_block,
|
|
mma_qk_fn=mma_qk_fn,
|
|
tiled_mma_pv_rs=tiled_mma_pv_rs,
|
|
pipeline_k=pipeline_k,
|
|
pipeline_v=pipeline_v,
|
|
acc_O=acc_O,
|
|
tOrP=tOrP,
|
|
smem_copy_params=smem_copy_params,
|
|
check_inf=True,
|
|
)
|
|
|
|
q_consumer_phase = Int32(0)
|
|
kv_consumer_state = pipeline.make_pipeline_state(
|
|
cutlass.pipeline.PipelineUserType.Consumer, self.num_stages
|
|
)
|
|
|
|
tile_scheduler = TileSchedulerCls()
|
|
work_tile = tile_scheduler.initial_work_tile_info()
|
|
softmax = Softmax.create(
|
|
softmax_scale_log2,
|
|
num_rows=acc_O.shape[0][0] * acc_O.shape[1],
|
|
softmax_scale=softmax_scale,
|
|
)
|
|
|
|
process_first_half_block = partial(
|
|
self.first_half_block_overlap,
|
|
mma_qk_fn=mma_qk_fn,
|
|
pipeline_k=pipeline_k,
|
|
tOrP=tOrP,
|
|
smem_copy_params=smem_copy_params,
|
|
softmax=softmax,
|
|
)
|
|
process_last_half_block = partial(
|
|
self.last_half_block_overlap,
|
|
pipeline_v=pipeline_v,
|
|
mma_pv_fn=mma_pv_fn,
|
|
)
|
|
while work_tile.is_valid_tile:
|
|
# if work_tile.is_valid_tile:
|
|
|
|
# shape: (atom_v_m * rest_m)
|
|
m_block, head_idx, batch_idx, _ = work_tile.tile_idx
|
|
seqlen = SeqlenInfoCls(batch_idx)
|
|
|
|
# Recompute fastdiv_mods if necessary for varlen with aux_tensors
|
|
recompute_fastdiv_mods_q = cutlass.const_expr(
|
|
aux_tensors is not None and (seqlen.has_cu_seqlens_q or seqlen.has_seqused_q)
|
|
)
|
|
recompute_fastdiv_mods_k = cutlass.const_expr(
|
|
aux_tensors is not None and (seqlen.has_cu_seqlens_k or seqlen.has_seqused_k)
|
|
)
|
|
if cutlass.const_expr(fastdiv_mods is not None):
|
|
seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
|
|
fastdiv_mods = (
|
|
seqlen_q_divmod
|
|
if not recompute_fastdiv_mods_q
|
|
else FastDivmodDivisor(seqlen.seqlen_q),
|
|
seqlen_k_divmod
|
|
if not recompute_fastdiv_mods_k
|
|
else FastDivmodDivisor(seqlen.seqlen_k),
|
|
)
|
|
|
|
mask = AttentionMaskCls(seqlen)
|
|
mask_fn = partial(
|
|
mask.apply_mask,
|
|
batch_idx=batch_idx,
|
|
head_idx=head_idx,
|
|
m_block=m_block,
|
|
thr_mma=thr_mma_qk,
|
|
mask_causal=self.is_causal,
|
|
mask_local=self.is_local,
|
|
aux_tensors=aux_tensors,
|
|
fastdiv_mods=fastdiv_mods,
|
|
)
|
|
score_mod_fn = None
|
|
if const_expr(self.score_mod is not None):
|
|
score_mod_fn = partial(
|
|
self.apply_score_mod,
|
|
thr_mma_qk,
|
|
batch_idx,
|
|
head_idx,
|
|
m_block,
|
|
softmax_scale=softmax_scale,
|
|
aux_tensors=aux_tensors,
|
|
fastdiv_mods=fastdiv_mods,
|
|
)
|
|
mma_one_n_block = partial(
|
|
mma_one_n_block_all,
|
|
seqlen=seqlen,
|
|
softmax=softmax,
|
|
score_mod_fn=score_mod_fn,
|
|
)
|
|
# Load Q if not TMA_Q
|
|
if const_expr(not self.use_tma_Q):
|
|
pack_gqa = PackGQA(
|
|
self.tile_m, self.tile_hdim, self.check_hdim_oob, self.qhead_per_kvhead
|
|
)
|
|
mQ_cur = seqlen.offset_batch_Q(mQ, batch_idx, dim=3)[None, None, head_idx]
|
|
# gmem_thr_copy_Q = gmem_tiled_copy_Q.get_slice(tidx)
|
|
# gQ = cute.local_tile(mQ_cur, (self.tile_m, self.tile_hdim), (m_block, 0))
|
|
# self.load_Q(gmem_thr_copy_Q, gQ, sQ, m_block, seqlen=seqlen.seqlen_q,
|
|
# headdim=mQ.shape[1])
|
|
pack_gqa.load_Q(mQ_cur, sQ, gmem_tiled_copy_Q, tidx, m_block, seqlen.seqlen_q)
|
|
cute.arch.cp_async_mbarrier_arrive_noinc(mbar_ptr_Q)
|
|
|
|
n_block_min, n_block_max = block_info.get_n_block_min_max(seqlen, m_block)
|
|
if const_expr(not self.use_tma_Q):
|
|
cute.arch.mbarrier_wait(mbar_ptr_Q, phase=q_consumer_phase)
|
|
q_consumer_phase ^= 1
|
|
# For performance reason, we separate out two kinds of iterations:
|
|
# those that need masking on S, and those that don't.
|
|
# We need masking on S for the very last block when K and V has length not multiple of tile_n.
|
|
# We also need masking on S if it's causal, for the last several blocks.
|
|
# softmax.reset() # Don't need reset as we explicitly call softmax w is_first=True
|
|
O_should_accumulate = False
|
|
|
|
# ==========================================
|
|
# MAINLOOP
|
|
# ==========================================
|
|
if const_expr(not self.use_block_sparsity):
|
|
# ==========================================
|
|
# No block-sparsity (original path)
|
|
# ==========================================
|
|
# First iteration with seqlen masking
|
|
if const_expr(self.intra_wg_overlap):
|
|
kv_consumer_state = process_first_half_block(
|
|
n_block=n_block_max - 1,
|
|
seqlen=seqlen,
|
|
kv_consumer_state=kv_consumer_state,
|
|
mask_fn=partial(mask_fn, mask_mod=self.mask_mod),
|
|
score_mod_fn=score_mod_fn,
|
|
is_first_block=True,
|
|
)
|
|
# Need to initialize tOrO in the case of RescaleOBeforeGemm where we will scale tOrO even in the 1st iter
|
|
# acc_O.fill(0.0)
|
|
else:
|
|
self.warp_scheduler_barrier_sync()
|
|
kv_consumer_state = mma_one_n_block(
|
|
kv_consumer_state,
|
|
n_block=n_block_max - 1,
|
|
seqlen=seqlen,
|
|
mma_pv_fn=partial(mma_pv_fn, zero_init=True),
|
|
is_first_n_block=True,
|
|
mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=True),
|
|
)
|
|
O_should_accumulate = True
|
|
# if cute.arch.thread_idx()[0] == 128: cute.printf("m_block = {}, n_block_max = {}, n_block_min = {}", m_block, n_block_max, n_block_min)
|
|
n_block_max -= 1
|
|
# Next couple of iterations with causal masking
|
|
if const_expr(self.is_causal or self.is_local):
|
|
n_block_min_causal_local_mask = block_info.get_n_block_min_causal_local_mask(
|
|
seqlen, m_block, n_block_min
|
|
)
|
|
# if cute.arch.thread_idx()[0] == 128: cute.printf("n_block_min_causal_local_mask = {}", n_block_min_causal_local_mask)
|
|
for n_tile in cutlass.range(
|
|
n_block_max - n_block_min_causal_local_mask, unroll=1
|
|
):
|
|
kv_consumer_state = mma_one_n_block(
|
|
kv_consumer_state,
|
|
n_block=n_block_max - 1 - n_tile,
|
|
seqlen=seqlen,
|
|
mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
|
|
mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=False),
|
|
)
|
|
O_should_accumulate = True
|
|
n_block_max = cutlass.min(n_block_max, n_block_min_causal_local_mask)
|
|
# The remaining iterations have no masking
|
|
n_block_min_before_local_mask = block_info.get_n_block_min_before_local_mask(
|
|
seqlen, m_block, n_block_min
|
|
)
|
|
# if cute.arch.thread_idx()[0] == 128: cute.printf("n_block_min_before_local_mask = {}, n_block_min = {}", n_block_min_before_local_mask, n_block_min)
|
|
for n_tile in cutlass.range(n_block_max - n_block_min_before_local_mask, unroll=1):
|
|
kv_consumer_state = mma_one_n_block(
|
|
kv_consumer_state,
|
|
n_block=n_block_max - 1 - n_tile,
|
|
seqlen=seqlen,
|
|
mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
|
|
mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=False),
|
|
)
|
|
O_should_accumulate = True
|
|
# Separate iterations with local masking on the left
|
|
if const_expr(self.is_local and block_info.window_size_left is not None):
|
|
n_block_max = cutlass.min(n_block_max, n_block_min_before_local_mask)
|
|
for n_tile in cutlass.range(n_block_max - n_block_min, unroll=1):
|
|
kv_consumer_state = mma_one_n_block(
|
|
kv_consumer_state,
|
|
n_block=n_block_max - 1 - n_tile,
|
|
seqlen=seqlen,
|
|
mma_pv_fn=partial(mma_pv_fn, zero_init=not O_should_accumulate),
|
|
mask_fn=partial(mask_fn, mask_mod=self.mask_mod, mask_seqlen=False),
|
|
)
|
|
O_should_accumulate = True
|
|
# Last "half" iteration
|
|
if const_expr(self.intra_wg_overlap):
|
|
kv_consumer_state = process_last_half_block(
|
|
kv_consumer_state=kv_consumer_state,
|
|
zero_init=not O_should_accumulate,
|
|
)
|
|
O_should_accumulate = True
|
|
else:
|
|
self.warp_scheduler_barrier_arrive()
|
|
|
|
else:
|
|
# ==========================================
|
|
# Block sparsity
|
|
# ==========================================
|
|
kv_consumer_state, O_should_accumulate, processed_any = consume_block_sparse_loads(
|
|
blocksparse_tensors,
|
|
batch_idx,
|
|
head_idx,
|
|
m_block,
|
|
seqlen,
|
|
kv_consumer_state,
|
|
mma_pv_fn,
|
|
mma_one_n_block,
|
|
process_first_half_block,
|
|
process_last_half_block,
|
|
mask_fn,
|
|
score_mod_fn,
|
|
O_should_accumulate,
|
|
self.mask_mod,
|
|
fastdiv_mods,
|
|
self.intra_wg_overlap,
|
|
self.warp_scheduler_barrier_sync,
|
|
self.warp_scheduler_barrier_arrive,
|
|
self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
|
|
)
|
|
|
|
# Handle empty case (when no blocks to process)
|
|
if not processed_any:
|
|
softmax.reset()
|
|
acc_O.fill(0.0)
|
|
|
|
sink_val = None
|
|
if const_expr(learnable_sink is not None):
|
|
if const_expr(not self.pack_gqa):
|
|
sink_val = Float32(learnable_sink[head_idx])
|
|
else: # Each thread might have a different sink value due to different q_head
|
|
sink_val = cute.make_fragment_like(softmax.row_max, Float32)
|
|
cS = cute.make_identity_tensor((self.tile_m, self.tile_n))
|
|
tScS_mn = utils.make_acc_tensor_mn_view(thr_mma_qk.partition_C(cS))
|
|
for r in cutlass.range(cute.size(sink_val), unroll_full=True):
|
|
row = m_block * self.tile_m + tScS_mn[r][0]
|
|
q_head_idx = row % self.qhead_per_kvhead + head_idx * self.qhead_per_kvhead
|
|
sink_val[r] = Float32(learnable_sink[q_head_idx])
|
|
|
|
# normalize acc_O by row_sum and calculate the lse
|
|
row_scale = softmax.finalize(sink_val=sink_val)
|
|
softmax.rescale_O(acc_O, row_scale)
|
|
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
# Epilogue
|
|
# ///////////////////////////////////////////////////////////////////////////////
|
|
self.epilogue(
|
|
acc_O,
|
|
softmax.row_sum,
|
|
mO,
|
|
mLSE,
|
|
sO,
|
|
seqlen,
|
|
gmem_tiled_copy_O,
|
|
tma_atom_O,
|
|
tiled_mma_pv,
|
|
tidx,
|
|
m_block,
|
|
head_idx,
|
|
batch_idx,
|
|
)
|
|
|
|
tile_scheduler.advance_to_next_work()
|
|
work_tile = tile_scheduler.get_current_work()
|
|
|
|
|
|
@cute.jit
|
|
def first_half_block_overlap(
|
|
self,
|
|
n_block: Int32,
|
|
mma_qk_fn: Callable,
|
|
kv_consumer_state,
|
|
pipeline_k,
|
|
tOrP: cute.Tensor,
|
|
smem_copy_params: SimpleNamespace,
|
|
softmax: Softmax,
|
|
seqlen: SeqlenInfoQK,
|
|
mask_fn: Callable = None,
|
|
score_mod_fn: Optional[Callable] = None,
|
|
is_first_block: bool = False,
|
|
):
|
|
"""Processes the first half block when using intra-warpgroup-overlap"""
|
|
|
|
pipeline_k.consumer_wait(kv_consumer_state, pipeline_k.consumer_try_wait(kv_consumer_state))
|
|
acc_S = mma_qk_fn(B_idx=kv_consumer_state.index, wg_wait=0)
|
|
pipeline_k.consumer_release(kv_consumer_state)
|
|
|
|
# Apply score modification if present
|
|
if const_expr(score_mod_fn is not None):
|
|
score_mod_fn(acc_S, n_block=n_block, seqlen=seqlen)
|
|
|
|
# Apply mask; mask_seqlen always True for first block
|
|
# Caveat: if full block further right than mask block, seqlen masking is redundant;
|
|
# however, masking is being applied anyway, so essentially no perf hit
|
|
mask_fn(acc_S, n_block=n_block, mask_seqlen=True)
|
|
|
|
softmax.online_softmax(acc_S, is_first=is_first_block)
|
|
|
|
tOrP_acc = cute.make_tensor(acc_S.iterator, utils.convert_layout_acc_frgA(acc_S.layout))
|
|
tOrP_cur = (
|
|
tOrP if const_expr(self.mma_pv_is_rs) else cute.make_fragment_like(tOrP_acc, self.dtype)
|
|
)
|
|
tOrP_cur.store(tOrP_acc.load().to(self.dtype))
|
|
|
|
# if pv gemm not rs
|
|
if const_expr(not self.mma_pv_is_rs):
|
|
tPrP = smem_copy_params.smem_thr_copy_P.retile(tOrP_cur)
|
|
cute.copy(smem_copy_params.smem_thr_copy_P, tPrP, smem_copy_params.tPsP)
|
|
# Fence and barrier to make smem store visible to WGMMA
|
|
cute.arch.fence_proxy(
|
|
cute.arch.ProxyKind.async_shared, space=cute.arch.SharedSpace.shared_cta
|
|
)
|
|
cute.arch.sync_warp()
|
|
|
|
return kv_consumer_state
|
|
|
|
@cute.jit
|
|
def last_half_block_overlap(
|
|
self,
|
|
kv_consumer_state,
|
|
pipeline_v,
|
|
mma_pv_fn: Callable,
|
|
zero_init: bool,
|
|
):
|
|
"""Processes the final PV GEMM when using intra-warpgroup-overlap"""
|
|
|
|
pipeline_v.consumer_wait(kv_consumer_state, pipeline_v.consumer_try_wait(kv_consumer_state))
|
|
mma_pv_fn(B_idx=kv_consumer_state.index, zero_init=zero_init, wg_wait=0)
|
|
pipeline_v.consumer_release(kv_consumer_state)
|
|
kv_consumer_state.advance()
|
|
return kv_consumer_state
|
|
|
|
@cute.jit
|
|
def mma_one_n_block(
|
|
self,
|
|
smem_pipe_read: cutlass.pipeline.PipelineState | pipeline.PipelineStateSimple,
|
|
n_block: Int32,
|
|
mma_qk_fn: Callable,
|
|
mma_pv_fn: Callable,
|
|
tiled_mma_pv_rs: cute.TiledMma,
|
|
pipeline_k: cutlass.pipeline.PipelineAsync,
|
|
pipeline_v: cutlass.pipeline.PipelineAsync,
|
|
acc_O: cute.Tensor,
|
|
tOrP: cute.Tensor,
|
|
smem_copy_params: SimpleNamespace,
|
|
softmax: Softmax,
|
|
seqlen: SeqlenInfoQK,
|
|
score_mod_fn: Optional[Callable] = None,
|
|
mask_fn: Optional[Callable] = None,
|
|
is_first_n_block: cutlass.Constexpr = False,
|
|
check_inf: cutlass.Constexpr = True,
|
|
):
|
|
pipeline_k.consumer_wait(smem_pipe_read, pipeline_k.consumer_try_wait(smem_pipe_read))
|
|
# S = Q @ K.T
|
|
acc_S = mma_qk_fn(B_idx=smem_pipe_read.index, wg_wait=-1)
|
|
self.warp_scheduler_barrier_arrive()
|
|
warpgroup.wait_group(0)
|
|
pipeline_k.consumer_release(smem_pipe_read)
|
|
|
|
# handle score mods and masking
|
|
if const_expr(score_mod_fn is not None):
|
|
score_mod_fn(acc_S, n_block=n_block, seqlen=seqlen)
|
|
if const_expr(mask_fn is not None):
|
|
mask_fn(acc_S=acc_S, n_block=n_block)
|
|
|
|
row_scale = softmax.online_softmax(acc_S, is_first=is_first_n_block, check_inf=check_inf)
|
|
# if cute.arch.thread_idx()[0] == 0: cute.print_tensor(utils.make_acc_tensor_mn_view(acc_S))
|
|
tOrP_acc = cute.make_tensor(acc_S.iterator, utils.convert_layout_acc_frgA(acc_S.layout))
|
|
tOrP_cur = (
|
|
tOrP if const_expr(self.mma_pv_is_rs) else cute.make_fragment_like(tOrP_acc, self.dtype)
|
|
)
|
|
# tOrP.store(tOrP_acc.load().to(self.dtype))
|
|
# the "to(self.dtype)" conversion fails to vectorize for block sizes other
|
|
# than 128 x 128, i.e. it calls convert on 1 fp32 element at a time instead of
|
|
# 2 elements. So we just call ptx directly.
|
|
utils.cvt_f16(tOrP_acc, tOrP_cur)
|
|
if const_expr(not self.mma_pv_is_rs):
|
|
tPrP = smem_copy_params.smem_thr_copy_P.retile(tOrP_cur)
|
|
cute.copy(smem_copy_params.smem_thr_copy_P, tPrP, smem_copy_params.tPsP)
|
|
softmax.rescale_O(acc_O, row_scale)
|
|
if const_expr(not self.mma_pv_is_rs):
|
|
# Fence and barrier to make sure smem store is visible to WGMMA
|
|
cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
|
|
cute.arch.sync_warp() # Only need syncwarp since each warp is using its own P values for MmaPV
|
|
pipeline_v.consumer_wait(smem_pipe_read, pipeline_v.consumer_try_wait(smem_pipe_read))
|
|
self.warp_scheduler_barrier_sync()
|
|
# O += P @ V
|
|
mma_pv_fn(B_idx=smem_pipe_read.index, wg_wait=0)
|
|
pipeline_v.consumer_release(smem_pipe_read)
|
|
smem_pipe_read.advance()
|
|
return smem_pipe_read
|
|
|
|
@cute.jit
|
|
def mma_one_n_block_intrawg_overlap(
|
|
self,
|
|
smem_pipe_read: cutlass.pipeline.PipelineState | pipeline.PipelineStateSimple,
|
|
n_block: Int32,
|
|
mma_qk_fn: Callable,
|
|
mma_pv_fn: Callable,
|
|
tiled_mma_pv_rs: cute.TiledMma,
|
|
pipeline_k: cutlass.pipeline.PipelineAsync,
|
|
pipeline_v: cutlass.pipeline.PipelineAsync,
|
|
acc_O: cute.Tensor,
|
|
tOrP: cute.Tensor,
|
|
smem_copy_params: SimpleNamespace,
|
|
softmax: Softmax,
|
|
seqlen: SeqlenInfoQK,
|
|
score_mod_fn: Optional[Callable] = None,
|
|
mask_fn: Optional[Callable] = None,
|
|
check_inf: cutlass.Constexpr = True,
|
|
):
|
|
smem_pipe_read_v = smem_pipe_read.clone()
|
|
smem_pipe_read.advance()
|
|
pipeline_k.consumer_wait(smem_pipe_read, pipeline_k.consumer_try_wait(smem_pipe_read))
|
|
self.warp_scheduler_barrier_sync()
|
|
# S = Q @ K.T
|
|
acc_S = mma_qk_fn(B_idx=smem_pipe_read.index, wg_wait=-1)
|
|
pipeline_v.consumer_wait(smem_pipe_read_v, pipeline_v.consumer_try_wait(smem_pipe_read_v))
|
|
# O += P @ V
|
|
mma_pv_fn(B_idx=smem_pipe_read_v.index, wg_wait=-1)
|
|
self.warp_scheduler_barrier_arrive()
|
|
warpgroup.wait_group(1)
|
|
pipeline_k.consumer_release(smem_pipe_read)
|
|
|
|
# handle score mods and masking
|
|
if const_expr(score_mod_fn is not None):
|
|
score_mod_fn(acc_S, n_block=n_block, seqlen=seqlen)
|
|
if const_expr(mask_fn is not None):
|
|
mask_fn(acc_S=acc_S, n_block=n_block)
|
|
# if cute.arch.thread_idx()[0] == 128: cute.print_tensor(utils.make_acc_tensor_mn_view(acc_S))
|
|
|
|
row_scale = softmax.online_softmax(acc_S, check_inf=check_inf)
|
|
warpgroup.wait_group(0)
|
|
pipeline_v.consumer_release(smem_pipe_read_v)
|
|
tOrP_acc = cute.make_tensor(acc_S.iterator, utils.convert_layout_acc_frgA(acc_S.layout))
|
|
tOrP_cur = (
|
|
tOrP if const_expr(self.mma_pv_is_rs) else cute.make_fragment_like(tOrP_acc, self.dtype)
|
|
)
|
|
# tOrP_cur.store(tOrP_acc.load().to(self.dtype))
|
|
# the "to(self.dtype)" conversion fails to vectorize for block sizes other
|
|
# than 128 x 128, i.e. it calls convert on 1 fp32 element at a time instead of
|
|
# 2 elements. So we just call ptx directly.
|
|
utils.cvt_f16(tOrP_acc, tOrP_cur)
|
|
if const_expr(not self.mma_pv_is_rs):
|
|
tPrP = smem_copy_params.smem_thr_copy_P.retile(tOrP_cur)
|
|
cute.copy(smem_copy_params.smem_thr_copy_P, tPrP, smem_copy_params.tPsP)
|
|
softmax.rescale_O(acc_O, row_scale)
|
|
if const_expr(not self.mma_pv_is_rs):
|
|
# Fence and barrier to make sure smem store is visible to WGMMA
|
|
cute.arch.fence_proxy(ProxyKind.async_shared, space=SharedSpace.shared_cta)
|
|
cute.arch.sync_warp() # Only need syncwarp since each warp is using its own P values for MmaPV
|
|
return smem_pipe_read
|
|
|
|
@cute.jit
|
|
def mma_init(self):
|
|
warp_group_idx = utils.canonical_warp_group_idx(sync=False)
|
|
if const_expr(self.use_scheduler_barrier):
|
|
if warp_group_idx == 1:
|
|
cute.arch.barrier_arrive(
|
|
barrier_id=int(NamedBarrierFwd.WarpSchedulerWG1),
|
|
number_of_threads=2 * self.num_threads_per_warp_group,
|
|
)
|
|
|
|
@cute.jit
|
|
def apply_score_mod(
|
|
self,
|
|
thr_mma_qk,
|
|
batch_idx,
|
|
head_idx,
|
|
m_block,
|
|
acc_S,
|
|
n_block,
|
|
softmax_scale,
|
|
seqlen,
|
|
aux_tensors: Optional[list] = None,
|
|
fastdiv_mods=None,
|
|
):
|
|
# Prepare index tensor
|
|
cS = cute.make_identity_tensor((self.tile_m, self.tile_n))
|
|
cS = cute.domain_offset((m_block * self.tile_m, n_block * self.tile_n), cS)
|
|
tScS = thr_mma_qk.partition_C(cS)
|
|
|
|
apply_score_mod_inner(
|
|
acc_S,
|
|
tScS,
|
|
self.score_mod,
|
|
batch_idx,
|
|
head_idx,
|
|
softmax_scale,
|
|
self.vec_size,
|
|
self.qk_acc_dtype,
|
|
aux_tensors,
|
|
fastdiv_mods,
|
|
seqlen_info=seqlen,
|
|
constant_q_idx=None,
|
|
qhead_per_kvhead=self.qhead_per_kvhead if const_expr(self.pack_gqa) else 1,
|
|
)
|
|
|
|
def warp_scheduler_barrier_sync(self):
|
|
if const_expr(self.use_scheduler_barrier):
|
|
cute.arch.barrier(
|
|
barrier_id=int(NamedBarrierFwd.WarpSchedulerWG1)
|
|
- 1
|
|
+ utils.canonical_warp_group_idx(sync=False),
|
|
number_of_threads=2 * self.num_threads_per_warp_group,
|
|
)
|
|
|
|
def warp_scheduler_barrier_arrive(self):
|
|
if const_expr(self.use_scheduler_barrier):
|
|
assert self.num_mma_warp_groups in [2, 3]
|
|
cur_wg = utils.canonical_warp_group_idx(sync=False) - 1
|
|
if const_expr(self.num_mma_warp_groups == 2):
|
|
next_wg = 1 - cur_wg
|
|
else:
|
|
t = cur_wg + 1
|
|
next_wg = t % self.num_mma_warp_groups
|
|
cute.arch.barrier_arrive(
|
|
barrier_id=int(NamedBarrierFwd.WarpSchedulerWG1) + next_wg,
|
|
number_of_threads=2 * self.num_threads_per_warp_group,
|
|
)
|
|
|