Files

105 lines
4.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""sm_100a (Blackwell) CUDA block-sparse VSA forward.
A third backend behind the same VSA op as the Triton and CuTe-DSL paths. Forward only: it
returns ``(out, lse)`` with ``lse`` in exactly the form ``triton_block_sparse_attn_forward``
writes -- ``max(qk * qk_scale) + log2(l)``, ``[B, H, S]`` fp32 -- so
``block_sparse_attn_backward_triton`` runs against it unchanged.
The extension carries TWO instantiations of the kernel, for 64- and 128-token sparse blocks
(tile volumes 64 and 128 in ``build_vsa_metadata``); the block size is inferred from the
tensors and picks the op. Anything else falls back to Triton via ``is_supported``.
"""
from typing import Tuple
import torch
try:
# The pybind symbols live on fastvideo_kernel_ops, NOT on the _C package that contains it.
# `import fastvideo_kernel._C as _C` resolves to the namespace package, whose __init__ is
# empty, so hasattr() fails on a wheel install and the caller silently falls back with the
# kernel built and present.
from fastvideo_kernel._C import fastvideo_kernel_ops as _C
_FWD_BY_BLOCK = {
64: getattr(_C, "block_sparse_sm100a_fwd", None),
128: getattr(_C, "block_sparse_sm100a_blk128_fwd", None),
}
_HAS_VSA_SM100A = any(_FWD_BY_BLOCK.values())
except ImportError: # pragma: no cover - extension not built
_C = None
_FWD_BY_BLOCK = {}
_HAS_VSA_SM100A = False
_SM100 = (10, 0)
HEAD_DIM = 128
# Must match the -DVSA_BHSD the extension was compiled with (see CMakeLists).
BHSD = True
def _block_size(q: torch.Tensor, variable_block_sizes: torch.Tensor) -> int:
num_blocks = variable_block_sizes.numel()
seqlen = q.shape[2] if BHSD else q.shape[1]
return 0 if num_blocks == 0 or seqlen % num_blocks else seqlen // num_blocks
def is_supported(q: torch.Tensor, variable_block_sizes: torch.Tensor) -> bool:
"""True iff this build can run these tensors; otherwise the caller uses Triton.
Static facts only -- shapes, dtypes, arch, layout. Deliberately NO reads of tensor
contents: the previous ``int(variable_block_sizes.min())`` was a GPU->CPU sync on every
call, and the kernel no longer needs it (see below). This predicate must stay cheap
enough to sit on a per-layer dispatch path.
What the kernel accepts (and is tested to handle):
* q/k/v: contiguous 4-D bf16 CUDA tensors on an sm_100 device, head_dim 128, laid out
as compiled (BHSD here); seqlen == num_blocks * block with an EVEN num_blocks (a CTA
owns an adjacent pair of query blocks) and a 64- or 128-token build present.
* q2k_num: any per-row counts in [0, max_kv], NON-uniform across rows included. Rows
with count 0 produce exactly-zero output rows (and a finite LSE sentinel) rather
than attending anywhere -- so no ``.min()`` floor is required of the caller.
* q2k_idx: rows only need valid entries (in [0, num_blocks)) BELOW that row's count;
padding past the count (e.g. map_to_index's -1 fill) is never dereferenced. max_kv
(= q2k_idx.shape[-1]) must be >= 1, which the host launcher re-checks.
* variable_block_sizes: per-KV-block valid-token counts in [0, block]; keys at or past
a block's count are masked. Integer metadata is converted to int32/contiguous by
``block_sparse_attn_sm100a`` itself, so int64 inputs merely cost a cast.
"""
if not _HAS_VSA_SM100A or not q.is_cuda:
return False
if torch.cuda.get_device_capability(q.device) != _SM100:
return False
if q.dtype != torch.bfloat16 or q.dim() != 4 or q.shape[-1] != HEAD_DIM:
return False
if not q.is_contiguous():
return False
if _FWD_BY_BLOCK.get(_block_size(q, variable_block_sizes)) is None:
return False
# A CTA owns an adjacent pair of query blocks.
if variable_block_sizes.numel() % 2 != 0:
return False
# Metadata must be integer-typed so the wrapper's int32 conversion is value-preserving.
if not variable_block_sizes.is_cuda or variable_block_sizes.dtype not in (torch.int32, torch.int64):
return False
return True
def block_sparse_attn_sm100a(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
q2k_idx: torch.Tensor,
q2k_num: torch.Tensor,
variable_block_sizes: torch.Tensor,
need_lse: bool = True,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Forward pass. Returns ``(out, lse)``; ``out`` has q's layout."""
fwd = _FWD_BY_BLOCK[_block_size(q, variable_block_sizes)]
idx = q2k_idx.to(torch.int32).contiguous()
num = q2k_num.to(torch.int32).contiguous()
vbs = variable_block_sizes.to(torch.int32).contiguous()
sm_scale = 1.0 / (q.shape[-1]**0.5)
res = fwd(q.contiguous(), k.contiguous(), v.contiguous(), None,
idx, num, vbs, sm_scale, need_lse)
return (res[0], res[1]) if need_lse else (res[0], None)