liangsu9988's picture
Promote latest kernel artifacts to main
8c8128e verified
Raw
History Blame Contribute Delete
24.5 kB
"""
Block-sparsity utilities for FlexAttention
"""
from typing import Callable, NamedTuple, Tuple
import cutlass.cute as cute
import torch
from fa4_cute_runtime.flashrt_fa4.cute.cute_dsl_utils import get_broadcast_dims, to_cute_tensor
def ceildiv(a: int, b: int) -> int:
return (a + b - 1) // b
class BlockSparseTensors(NamedTuple):
mask_block_cnt: cute.Tensor
mask_block_idx: cute.Tensor
full_block_cnt: cute.Tensor | None
full_block_idx: cute.Tensor | None
dq_write_order: cute.Tensor | None = None
dq_write_order_full: cute.Tensor | None = None
def __new_from_mlir_values__(self, values):
if len(values) == 2:
values = (*values, None, None, None, None)
elif len(values) == 4:
values = (*values, None, None)
return BlockSparseTensors(*values)
class BlockSparseTensorsTorch(NamedTuple):
mask_block_cnt: torch.Tensor
mask_block_idx: torch.Tensor
full_block_cnt: torch.Tensor | None = None
full_block_idx: torch.Tensor | None = None
block_size: tuple[int, int] | None = None
dq_write_order: torch.Tensor | None = None
dq_write_order_full: torch.Tensor | None = None
spt: bool | None = None
def _ordered_to_dense_simple(
num_blocks: torch.Tensor,
indices: torch.Tensor,
num_cols: int,
) -> torch.Tensor:
"""Convert ordered sparse representation to dense binary matrix.
Args:
num_blocks: [B, H, num_rows] count of valid entries per row
indices: [B, H, num_rows, max_entries] column indices (valid entries packed left)
num_cols: total number of columns
Returns:
dense: [B, H, num_rows, num_cols] binary int32 matrix
"""
B, H, num_rows, max_entries = indices.shape
device = indices.device
dense = torch.zeros(B, H, num_rows, num_cols + 1, dtype=torch.int32, device=device)
col_range = torch.arange(max_entries, device=device)
valid = col_range[None, None, None, :] < num_blocks[:, :, :, None]
safe_indices = torch.where(valid, indices.long(), num_cols)
row_idx = torch.arange(num_rows, device=device)[None, None, :, None].expand_as(indices)
b_idx = torch.arange(B, device=device)[:, None, None, None].expand_as(indices)
h_idx = torch.arange(H, device=device)[None, :, None, None].expand_as(indices)
dense[b_idx, h_idx, row_idx, safe_indices] = 1
return dense[:, :, :, :num_cols]
def compute_dq_write_order(
fwd_mask_cnt: torch.Tensor,
fwd_mask_idx: torch.Tensor,
fwd_full_cnt: torch.Tensor | None,
fwd_full_idx: torch.Tensor | None,
bwd_mask_cnt: torch.Tensor,
bwd_mask_idx: torch.Tensor,
bwd_full_cnt: torch.Tensor | None,
bwd_full_idx: torch.Tensor | None,
spt: bool = False,
) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Compute dQ write-order metadata for deterministic block-sparse backward.
For each (n_block, i) in the backward iteration, computes the semaphore
lock value: the rank of n_block in the combined (partial + full) sorted
contributor list for the target m_block.
Lock values are assigned in ascending n_block order (or descending if spt=True)
to guarantee deadlock-freedom with the CTA scheduling order.
Args:
fwd_mask_cnt: [B, H, num_m_blocks] partial contributor counts per m_block
fwd_mask_idx: [B, H, num_m_blocks, max_kv] partial contributor n_block indices (ascending)
fwd_full_cnt: [B, H, num_m_blocks] full contributor counts per m_block (optional)
fwd_full_idx: [B, H, num_m_blocks, max_kv] full contributor n_block indices (optional)
bwd_mask_cnt: [B, H, num_n_blocks] partial iteration counts per n_block
bwd_mask_idx: [B, H, num_n_blocks, max_q] partial iteration m_block indices
bwd_full_cnt: [B, H, num_n_blocks] full iteration counts per n_block (optional)
bwd_full_idx: [B, H, num_n_blocks, max_q] full iteration m_block indices (optional)
spt: if True, reverse ordering (highest n_block gets lock_value=0)
Returns:
(dq_write_order, dq_write_order_full): tensors parallel to bwd_mask_idx
and bwd_full_idx respectively, containing lock values.
"""
device = fwd_mask_idx.device
B, H, num_m, max_kv_partial = fwd_mask_idx.shape
_, _, num_n, max_q_partial = bwd_mask_idx.shape
has_full = fwd_full_cnt is not None and fwd_full_idx is not None
dense_partial = _ordered_to_dense_simple(fwd_mask_cnt, fwd_mask_idx, num_n)
if has_full:
dense_full = _ordered_to_dense_simple(fwd_full_cnt, fwd_full_idx, num_n)
dense = (dense_partial + dense_full).clamp(max=1)
else:
dense = dense_partial
cumsum = dense.cumsum(dim=-1)
rank_table = (cumsum - dense).to(torch.int32)
if spt:
total_per_m = cumsum[:, :, :, -1:]
rank_table = (total_per_m - 1 - rank_table).to(torch.int32)
def _gather_write_order(bwd_idx, bwd_cnt):
b_i = torch.arange(B, device=device)[:, None, None, None].expand_as(bwd_idx)
h_i = torch.arange(H, device=device)[None, :, None, None].expand_as(bwd_idx)
n_i = torch.arange(bwd_idx.shape[2], device=device)[None, None, :, None].expand_as(bwd_idx)
m_vals = bwd_idx.long().clamp(0, num_m - 1)
return rank_table[b_i, h_i, m_vals, n_i].to(torch.int32)
dq_write_order = _gather_write_order(bwd_mask_idx, bwd_mask_cnt)
dq_write_order_full = None
if has_full and bwd_full_cnt is not None and bwd_full_idx is not None:
dq_write_order_full = _gather_write_order(bwd_full_idx, bwd_full_cnt)
return dq_write_order, dq_write_order_full
def compute_dq_write_order_from_block_mask(
block_mask,
spt: bool = False,
) -> tuple[torch.Tensor, torch.Tensor | None]:
(
_seq_q,
_seq_k,
kv_mask_cnt,
kv_mask_idx,
full_kv_cnt,
full_kv_idx,
q_mask_cnt,
q_mask_idx,
full_q_cnt,
full_q_idx,
*_,
) = block_mask.as_tuple()
return compute_dq_write_order(
kv_mask_cnt,
kv_mask_idx,
full_kv_cnt,
full_kv_idx,
q_mask_cnt,
q_mask_idx,
full_q_cnt,
full_q_idx,
spt=spt,
)
def get_sparse_q_block_size(
tensors: BlockSparseTensorsTorch | None,
seqlen_q: int,
) -> int | None:
"""Return the Q sparse block size, or None when sparsity is unset or ambiguous."""
if tensors is None:
return None
if tensors.block_size is not None:
return tensors.block_size[0]
num_m_blocks = tensors.mask_block_idx.shape[2]
min_block_size = ceildiv(seqlen_q, num_m_blocks)
max_block_size = seqlen_q if num_m_blocks == 1 else (seqlen_q - 1) // (num_m_blocks - 1)
if min_block_size != max_block_size:
return None
return min_block_size
def _expand_sparsity_tensor(
tensor: torch.Tensor,
expected_shape: Tuple[int, ...],
tensor_name: str,
context: str | None,
hint: str | Callable[[], str] | None,
) -> torch.Tensor:
"""Check if we need to expand the tensor to expected shape, and do so if possible."""
needs_expand = tensor.shape != expected_shape
if not needs_expand:
return tensor
can_expand = all(map(lambda cur, tgt: cur == tgt or cur == 1, tensor.shape, expected_shape))
if not can_expand:
context_clause = f" ({context})" if context else ""
resolved_hint = hint() if callable(hint) else hint
hint_clause = f" Hint: {resolved_hint}" if resolved_hint else ""
raise ValueError(
f"{tensor_name}{context_clause} with shape {tensor.shape} cannot be expanded to expected shape {expected_shape}."
f"{hint_clause}"
)
return tensor.expand(*expected_shape)
def _check_and_expand_block(
name: str,
cnt: torch.Tensor | None,
idx: torch.Tensor | None,
expected_count_shape: Tuple[int, int, int],
expected_index_shape: Tuple[int, int, int, int],
context: str | None,
hint: str | Callable[[], str] | None,
) -> Tuple[torch.Tensor | None, torch.Tensor | None]:
if (cnt is None) != (idx is None):
raise ValueError(
f"{name}_block_cnt and {name}_block_idx must both be provided or both be None"
)
if cnt is None or idx is None:
return None, None
if cnt.dtype != torch.int32 or idx.dtype != torch.int32:
raise ValueError(f"{name}_block tensors must have dtype torch.int32")
if cnt.device != idx.device:
raise ValueError(f"{name}_block_cnt and {name}_block_idx must be on the same device")
if not cnt.is_cuda or not idx.is_cuda:
raise ValueError(f"{name}_block tensors must live on CUDA")
expanded_cnt = _expand_sparsity_tensor(
cnt, expected_count_shape, f"{name}_block_cnt", context, hint
)
# [Note] Allow Compact block sparse indices
# Allow the last dimension (n_blocks) of idx to be <= expected, since
# FA4 only accesses indices 0..cnt-1 per query tile. This enables compact
# index tensors that avoid O(N^2) memory at long sequence lengths.
if idx.ndim == 4 and idx.shape[3] <= expected_index_shape[3]:
expected_index_shape = (*expected_index_shape[:3], idx.shape[3])
expanded_idx = _expand_sparsity_tensor(
idx, expected_index_shape, f"{name}_block_idx", context, hint
)
return expanded_cnt, expanded_idx
def _check_and_expand_metadata_tensor(
name: str,
tensor: torch.Tensor | None,
expected_shape: Tuple[int, ...],
context: str | None,
hint: str | Callable[[], str] | None,
device: torch.device,
) -> torch.Tensor | None:
if tensor is None:
return None
if tensor.dtype != torch.int32:
raise ValueError(f"{name} must have dtype torch.int32")
if tensor.device != device:
raise ValueError(f"{name} must be on the same device as block sparse tensors")
if not tensor.is_cuda:
raise ValueError(f"{name} must live on CUDA")
return _expand_sparsity_tensor(tensor, expected_shape, name, context, hint)
def get_block_sparse_expected_shapes(
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
m_block_size: int,
n_block_size: int,
q_stage: int,
) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
"""Return (expected_count_shape, expected_index_shape) for block sparse normalization."""
m_block_size_effective = q_stage * m_block_size
expected_m_blocks = ceildiv(seqlen_q, m_block_size_effective)
expected_n_blocks = ceildiv(seqlen_k, n_block_size)
expected_count_shape = (batch_size, num_head, expected_m_blocks)
expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
return expected_count_shape, expected_index_shape
def infer_block_sparse_expected_shapes(
tensors: BlockSparseTensorsTorch,
*,
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
m_block_size: int,
n_block_size: int,
q_stage: int,
context: str,
sparse_block_size_q: int | None = None,
sparse_block_size_kv: int | None = None,
) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int], int]:
"""Infer shapes and scaling for block-sparse tensors.
Expectations:
- mask_block_cnt is (B, H, M) and mask_block_idx is (B, H, M, N).
- Batch/head dims may be 1 for broadcast, or match the requested sizes.
- sparse_block_size_kv must match tile_n.
- sparse_block_size_q must be a multiple of q_stage * tile_m.
- If sparse_block_size_q is omitted and seqlen_q/num_m_blocks is ambiguous,
the caller must provide block_size to disambiguate. TODO will make this required in a future PR.
"""
base_m_block = q_stage * m_block_size
base_n_block = n_block_size
if sparse_block_size_kv is None:
sparse_block_size_kv = base_n_block
if sparse_block_size_kv != base_n_block:
raise ValueError(f"Block sparse tensors{context} require BLOCK_SIZE_KV={base_n_block}.")
if tensors.mask_block_idx is None:
raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
num_m_blocks = tensors.mask_block_idx.shape[2]
if sparse_block_size_q is None:
sparse_block_size_q = get_sparse_q_block_size(tensors, seqlen_q)
if sparse_block_size_q is None and base_m_block != 1:
raise ValueError(
f"Block sparse tensors{context} require explicit sparse_block_size[0] "
f"to disambiguate block size for seqlen_q={seqlen_q} and num_m_blocks={num_m_blocks}."
)
if sparse_block_size_q is None:
sparse_block_size_q = ceildiv(seqlen_q, num_m_blocks)
if sparse_block_size_q % base_m_block != 0:
raise ValueError(
f"Block sparse tensors{context} have block size {sparse_block_size_q}, "
f"which must be a multiple of {base_m_block}."
)
expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
expected_n_blocks = ceildiv(seqlen_k, sparse_block_size_kv)
q_subtile_factor = sparse_block_size_q // base_m_block
expected_count_shape = (batch_size, num_head, expected_m_blocks)
expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
mask_block_cnt = tensors.mask_block_cnt
mask_block_idx = tensors.mask_block_idx
if mask_block_cnt is None or mask_block_idx is None:
raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
if mask_block_cnt.ndim != 3 or mask_block_idx.ndim != 4:
raise ValueError(
f"Block sparse tensors{context} must have shapes (B, H, M) and (B, H, M, N)."
)
for dim_name, cur, tgt in (
("batch", mask_block_cnt.shape[0], expected_count_shape[0]),
("head", mask_block_cnt.shape[1], expected_count_shape[1]),
):
if cur != tgt and cur != 1:
raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
for dim_name, cur, tgt in (
("batch", mask_block_idx.shape[0], expected_index_shape[0]),
("head", mask_block_idx.shape[1], expected_index_shape[1]),
):
if cur != tgt and cur != 1:
raise ValueError(f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1.")
if mask_block_cnt.shape[2] != mask_block_idx.shape[2]:
raise ValueError(f"Block sparse tensors{context} must share the same m-block dimension.")
# [Note] Allow Compact block sparse indices: FA4 only accesses indices 0..cnt-1
# per query tile, so idx.shape[3] can be <= expected_n_blocks.
if mask_block_idx.shape[3] > expected_n_blocks:
raise ValueError(
f"Block sparse tensors{context} n-block dimension must be <= {expected_n_blocks}."
)
if expected_m_blocks != num_m_blocks:
raise ValueError(
f"Block sparse tensors{context} m-block dimension {num_m_blocks} does not match "
f"sparse_block_size_q={sparse_block_size_q}. "
f"Set BlockSparseTensorsTorch.block_size to match the BlockMask BLOCK_SIZE."
)
return expected_count_shape, expected_index_shape, q_subtile_factor
def get_block_sparse_expected_shapes_bwd(
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
m_block_size: int,
n_block_size: int,
subtile_factor: int,
) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
"""Return (expected_count_shape, expected_index_shape) for backward block sparse normalization.
Backward uses Q-direction indexing (transposed from forward), where shapes are
indexed by N-blocks first, then M-blocks. The sparse_block_size_q is determined
by subtile_factor * m_block_size.
"""
sparse_block_size_q = subtile_factor * m_block_size
expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
expected_n_blocks = ceildiv(seqlen_k, n_block_size)
expected_count_shape = (batch_size, num_head, expected_n_blocks)
expected_index_shape = (batch_size, num_head, expected_n_blocks, expected_m_blocks)
return expected_count_shape, expected_index_shape
def normalize_block_sparse_tensors(
tensors: BlockSparseTensorsTorch,
*,
expected_count_shape: Tuple[int, int, int],
expected_index_shape: Tuple[int, int, int, int],
context: str | None = None,
hint: str | Callable[[], str] | None = None,
) -> BlockSparseTensorsTorch:
if tensors.mask_block_cnt is None or tensors.mask_block_idx is None:
raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
mask_cnt, mask_idx = _check_and_expand_block(
"mask",
tensors.mask_block_cnt,
tensors.mask_block_idx,
expected_count_shape,
expected_index_shape,
context,
hint,
)
if mask_cnt is None or mask_idx is None:
raise ValueError("mask_block_cnt and mask_block_idx must be provided for block sparsity.")
full_cnt, full_idx = _check_and_expand_block(
"full",
tensors.full_block_cnt,
tensors.full_block_idx,
expected_count_shape,
expected_index_shape,
context,
hint,
)
if full_cnt is not None and mask_cnt.device != full_cnt.device:
raise ValueError("All block sparse tensors must be on the same device")
dq_write_order = _check_and_expand_metadata_tensor(
"dq_write_order",
tensors.dq_write_order,
tuple(mask_idx.shape),
context,
hint,
mask_cnt.device,
)
dq_write_order_full = _check_and_expand_metadata_tensor(
"dq_write_order_full",
tensors.dq_write_order_full,
tuple(full_idx.shape) if full_idx is not None else expected_index_shape,
context,
hint,
mask_cnt.device,
)
spt = tensors.spt
if spt is not None and not isinstance(spt, bool):
raise ValueError("spt must be a bool when provided")
if spt is not None and dq_write_order is None:
raise ValueError("spt requires dq_write_order to be provided")
return BlockSparseTensorsTorch(
mask_block_cnt=mask_cnt,
mask_block_idx=mask_idx,
full_block_cnt=full_cnt,
full_block_idx=full_idx,
block_size=tensors.block_size,
dq_write_order=dq_write_order,
dq_write_order_full=dq_write_order_full,
spt=spt,
)
def is_block_sparsity_enabled(tensors: BlockSparseTensorsTorch) -> bool:
return any(t is not None for t in (tensors.full_block_cnt, tensors.mask_block_cnt))
def get_block_sparse_broadcast_pattern(
tensors: BlockSparseTensorsTorch,
) -> Tuple[Tuple[bool, ...], ...] | None:
"""Return broadcast pattern for block sparse tensors by checking actual strides.
Returns a tuple of broadcast patterns (one per tensor) where each pattern
is a tuple of bools indicating which dims have stride=0.
This is used in compile keys to ensure kernels are recompiled when
broadcast patterns change, since CuTe's mark_layout_dynamic() keeps
stride=0 as static.
The tensors should already be expanded/normalized before calling this function.
Returns None if block sparsity is not enabled.
"""
if not is_block_sparsity_enabled(tensors):
return None
patterns = []
for tensor in (
tensors.mask_block_cnt,
tensors.mask_block_idx,
tensors.full_block_cnt,
tensors.full_block_idx,
tensors.dq_write_order,
tensors.dq_write_order_full,
):
if tensor is not None:
patterns.append(get_broadcast_dims(tensor))
else:
patterns.append(None)
return tuple(patterns)
def normalize_block_sparse_config(
tensors: BlockSparseTensorsTorch,
*,
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
block_size: tuple[int, int],
q_stage: int,
) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None, int]:
m_block_size, n_block_size = block_size
if tensors.block_size is None:
sparse_block_size_q, sparse_block_size_kv = None, n_block_size
else:
sparse_block_size_q, sparse_block_size_kv = tensors.block_size
if sparse_block_size_kv != n_block_size:
raise ValueError(
f"Block sparsity requires sparse_block_size[1]={n_block_size} to match tile_n."
)
expected_count_shape, expected_index_shape, q_subtile_factor = (
infer_block_sparse_expected_shapes(
tensors,
batch_size=batch_size,
num_head=num_head,
seqlen_q=seqlen_q,
seqlen_k=seqlen_k,
m_block_size=m_block_size,
n_block_size=n_block_size,
q_stage=q_stage,
context="forward",
sparse_block_size_q=sparse_block_size_q,
sparse_block_size_kv=sparse_block_size_kv,
)
)
normalized_tensors = normalize_block_sparse_tensors(
tensors,
expected_count_shape=expected_count_shape,
expected_index_shape=expected_index_shape,
)
return (
normalized_tensors,
get_block_sparse_broadcast_pattern(normalized_tensors),
q_subtile_factor,
)
def normalize_block_sparse_config_bwd(
tensors: BlockSparseTensorsTorch,
*,
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
block_size: tuple[int, int],
subtile_factor: int,
) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None]:
m_block_size, n_block_size = block_size
if tensors.block_size is None:
sparse_block_size_q, sparse_block_size_kv = subtile_factor * m_block_size, n_block_size
else:
sparse_block_size_q, sparse_block_size_kv = tensors.block_size
if sparse_block_size_q != subtile_factor * m_block_size:
raise ValueError(
f"Block sparsity expects sparse_block_size_q={subtile_factor * m_block_size} "
f"for subtile_factor={subtile_factor}."
)
if sparse_block_size_kv != n_block_size:
raise ValueError(
f"Block sparsity expects sparse_block_size[1]={n_block_size} to match tile_n."
)
expected_count_shape, expected_index_shape = get_block_sparse_expected_shapes_bwd(
batch_size,
num_head,
seqlen_q,
seqlen_k,
m_block_size,
n_block_size,
subtile_factor,
)
normalized_tensors = normalize_block_sparse_tensors(
tensors,
expected_count_shape=expected_count_shape,
expected_index_shape=expected_index_shape,
context="_flash_attn_bwd",
hint=lambda: (
f"Backward expects Q-direction block-sparse tensors (q_mask_cnt/q_mask_idx, "
f"and optionally full_q_cnt/full_q_idx). Regenerate the backward BlockMask with "
f"BLOCK_SIZE=({subtile_factor * m_block_size}, {n_block_size})."
),
)
return normalized_tensors, get_block_sparse_broadcast_pattern(normalized_tensors)
def to_cute_block_sparse_tensors(
tensors: BlockSparseTensorsTorch, enable_tvm_ffi: bool = True
) -> BlockSparseTensors | None:
"""Convert torch block sparsity tensors to CuTe tensors, optionally for tvm ffi"""
if not is_block_sparsity_enabled(tensors):
return None
mask_block_cnt_tensor, mask_block_idx_tensor = [
to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
for t in (tensors.mask_block_cnt, tensors.mask_block_idx)
]
full_block_cnt_tensor, full_block_idx_tensor = [
to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
if t is not None
else None
for t in (tensors.full_block_cnt, tensors.full_block_idx)
]
dq_write_order_tensor, dq_write_order_full_tensor = [
to_cute_tensor(t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi)
if t is not None
else None
for t in (tensors.dq_write_order, tensors.dq_write_order_full)
]
return BlockSparseTensors(
mask_block_cnt_tensor,
mask_block_idx_tensor,
full_block_cnt_tensor,
full_block_idx_tensor,
dq_write_order_tensor,
dq_write_order_full_tensor,
)
def fast_sampling(mask_mod):
"""Convenience decorator to mark mask_mod as safe for 5-point fast sampling"""
mask_mod.use_fast_sampling = True
return mask_mod