# Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao. from typing import Tuple, Optional from dataclasses import dataclass import cutlass import cutlass.cute as cute from cutlass import Int32, const_expr from fa4_cute_runtime.flashrt_fa4.cute.seqlen_info import SeqlenInfoQK, SeqlenInfoQKNewK @dataclass(frozen=True) class BlockInfo: tile_m: cutlass.Constexpr[int] tile_n: cutlass.Constexpr[int] is_causal: cutlass.Constexpr[bool] is_local: cutlass.Constexpr[bool] = False is_split_kv: cutlass.Constexpr[bool] = False window_size_left: Optional[Int32] = None window_size_right: Optional[Int32] = None qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1 @cute.jit def get_n_block_min_max( self, seqlen_info: SeqlenInfoQK, m_block: Int32, split_idx: Int32 = 0, num_splits: Int32 = 1, ) -> Tuple[Int32, Int32]: n_block_max = cute.ceil_div(seqlen_info.seqlen_k, self.tile_n) if const_expr(self.is_causal or (self.is_local and self.window_size_right is not None)): m_idx_max = (m_block + 1) * self.tile_m if const_expr(self.qhead_per_kvhead_packgqa > 1): m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa) n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q n_idx_right = n_idx if const_expr(self.is_causal) else n_idx + self.window_size_right n_block_max = min(n_block_max, cute.ceil_div(n_idx_right, self.tile_n)) n_block_min = 0 if const_expr(self.is_local and self.window_size_left is not None): m_idx_min = m_block * self.tile_m if const_expr(self.qhead_per_kvhead_packgqa > 1): m_idx_min = m_idx_min // self.qhead_per_kvhead_packgqa n_idx = m_idx_min + seqlen_info.seqlen_k - seqlen_info.seqlen_q n_idx_left = n_idx - self.window_size_left n_block_min = cutlass.max(n_idx_left // self.tile_n, 0) if cutlass.const_expr(self.is_split_kv): num_n_blocks_per_split = ( Int32(0) if n_block_max <= n_block_min else (n_block_max - n_block_min + num_splits - 1) // num_splits ) n_block_min = n_block_min + split_idx * num_n_blocks_per_split n_block_max = cutlass.min(n_block_min + num_n_blocks_per_split, n_block_max) return n_block_min, n_block_max @cute.jit def get_m_block_min_max(self, seqlen_info: SeqlenInfoQK, n_block: Int32) -> Tuple[Int32, Int32]: m_block_max = cute.ceil_div(seqlen_info.seqlen_q, self.tile_m) m_block_min = 0 if const_expr(self.is_causal or (self.is_local and self.window_size_right is not None)): n_idx_min = n_block * self.tile_n m_idx = n_idx_min + seqlen_info.seqlen_q - seqlen_info.seqlen_k m_idx_right = m_idx if const_expr(self.is_causal) else m_idx - self.window_size_right m_block_min = max(m_block_min, m_idx_right // self.tile_m) if const_expr(self.is_local and self.window_size_left is not None): n_idx_max = (n_block + 1) * self.tile_n m_idx = n_idx_max + seqlen_info.seqlen_q - seqlen_info.seqlen_k m_idx_left = m_idx + self.window_size_left m_block_max = min(m_block_max, cute.ceil_div(m_idx_left, self.tile_m)) return m_block_min, m_block_max @cute.jit def get_n_block_k_new_min_max( self, seqlen_info: SeqlenInfoQKNewK, m_block: Int32, split_idx: Int32 = 0, num_splits: Int32 = 1, ) -> Tuple[Int32, Int32]: """Get the block range for new K tokens (append KV). First computes the full n_block range via get_n_block_min_max, then maps those blocks into the new-K index space by subtracting seqlen_k_og. """ n_block_min, n_block_max = self.get_n_block_min_max( seqlen_info, m_block, split_idx, num_splits, ) idx_k_new_min = cutlass.max(n_block_min * self.tile_n - seqlen_info.seqlen_k_og, 0) idx_k_new_max = cutlass.min( n_block_max * self.tile_n - seqlen_info.seqlen_k_og, seqlen_info.seqlen_k_new ) n_block_new_min = idx_k_new_min // self.tile_n n_block_new_max = ( cute.ceil_div(idx_k_new_max, self.tile_n) if idx_k_new_max > idx_k_new_min else n_block_new_min ) return n_block_new_min, n_block_new_max @cute.jit def get_n_block_min_causal_local_mask( self, seqlen_info: SeqlenInfoQK, m_block: Int32, n_block_min: Int32, ) -> Int32: """If we have separate iterations with causal or local masking at the start, where do we stop""" m_idx_min = m_block * self.tile_m if const_expr(self.qhead_per_kvhead_packgqa > 1): m_idx_min = m_idx_min // self.qhead_per_kvhead_packgqa n_idx = m_idx_min + seqlen_info.seqlen_k - seqlen_info.seqlen_q n_idx_right = ( n_idx if const_expr(not self.is_local or self.window_size_right is None) else n_idx + self.window_size_right ) return cutlass.max(n_block_min, n_idx_right // self.tile_n) @cute.jit def get_n_block_min_before_local_mask( self, seqlen_info: SeqlenInfoQK, m_block: Int32, n_block_min: Int32, ) -> Int32: """If we have separate iterations with local masking at the end, where do we stop the non-masked iterations""" if const_expr(not self.is_local or self.window_size_left is None): return n_block_min else: m_idx_max = (m_block + 1) * self.tile_m if const_expr(self.qhead_per_kvhead_packgqa > 1): m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa) n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q n_idx_left = n_idx - self.window_size_left return cutlass.max(n_block_min, cute.ceil_div(n_idx_left, self.tile_n)) @cute.jit def get_n_block_max_for_m_block( self, seqlen_info: SeqlenInfoQK, m_block: Int32, n_block_global_max: Int32, ) -> Int32: if const_expr(self.is_causal or self.window_size_right is not None): m_idx_max = (m_block + 1) * self.tile_m if const_expr(self.qhead_per_kvhead_packgqa > 1): m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa) n_idx_right = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q if const_expr(self.window_size_right is not None): n_idx_right += self.window_size_right return min(n_block_global_max, cute.ceil_div(n_idx_right, self.tile_n)) return n_block_global_max