- Move task_alloc/task_free/task_extend/task_cached/task_record_hashes and bind from the PagePool card to a new TaskCacheManager card matching pool.py - Drop the nonexistent Executor tokenizer attribute and association, add task_cache instead - Add AllocationStrategy/ContiguousStrategy/PagedStrategy cards and point Allocator/RadixCache composition at PagedStrategy - Add TaskCacheManager and the allocation strategies to the module overview, add _task_cache to InferenceScheduler - Fix the design-pattern count in the table of contents (15 -> 16) - Rewrite the FlashAttnBackend class docstring: packed decode gathers flat K/V via req_to_token and calls flash_attn_varlen_func; dense prefill uses flash_attn_func (no flash_attn_with_kvcache exists) - Apply the same correction to the backend bullets in internals.md and cuda_kernels.md - Rename the stale fp8_mma_test.cu reference to fp8_test.cu in cuda_kernels.md
820 lines
27 KiB
Python
820 lines
27 KiB
Python
"""Attention backend abstraction with context-manager switching.
|
|
|
|
The backend encapsulates KV cache I/O and attention computation. The
|
|
attention module (GQA/MLA) keeps projections, rotary, QK-norm, gating,
|
|
and output projection; the backend handles everything from "write K/V
|
|
to cache" through "SDPA output".
|
|
|
|
Usage — mirroring ``torch.nn.attention.sdpa_kernel``:
|
|
|
|
from astrai.extension import attn_backend, ATTN_BACKEND
|
|
|
|
with attn_backend(ATTN_BACKEND.TORCH_NATIVE):
|
|
engine.generate("hello")
|
|
|
|
# or with an instance:
|
|
with attn_backend(TorchNativeBackend()):
|
|
...
|
|
|
|
# or the shorthand (instance is itself a context manager):
|
|
with TorchNativeBackend():
|
|
...
|
|
|
|
Thread-safe via ``contextvars`` — each scheduler thread gets its own
|
|
active backend. Backend resolution follows a strict precedence:
|
|
|
|
1. explicit ``attn_backend(...)`` context (wins over everything),
|
|
2. the process-wide ``ASTR_BACKEND`` environment override,
|
|
3. an implicit default picked from the available backends
|
|
(cuda > flash > torch).
|
|
|
|
Capability is polymorphic: every backend declares ``available()``
|
|
(machine-level) and ``supports_call(...)`` (per-call), so adding a new
|
|
backend requires no changes to the resolution logic. Training calls
|
|
(``fwd=None``, no KV cache) resolve through the same priority list: the
|
|
CUDA cache kernels cannot run without a cache, so they fall back to
|
|
flash (when it can handle the call — mask-free/causal only) and finally
|
|
to the reference ``TorchNativeBackend``.
|
|
|
|
Layout convention: all q/k/v are ``[batch, seq_len, n_heads, head_dim]``
|
|
(blhd). The backend returns ``[batch, seq_len, n_heads * head_dim]``.
|
|
"""
|
|
|
|
import contextvars
|
|
import enum
|
|
import functools
|
|
import logging
|
|
import os
|
|
import threading
|
|
from abc import ABC, abstractmethod
|
|
from contextlib import contextmanager
|
|
from typing import TYPE_CHECKING, Dict, Optional, Tuple, Union
|
|
|
|
import torch
|
|
import torch.nn.functional as F
|
|
from torch import Tensor
|
|
|
|
from astrai.extension.loader import is_available
|
|
from astrai.extension.ops.attention import (
|
|
attn_paged_decode,
|
|
attn_paged_prefill,
|
|
)
|
|
from astrai.factory import BaseFactory
|
|
|
|
try:
|
|
import flash_attn as _flash_attn
|
|
except Exception:
|
|
_flash_attn = None
|
|
|
|
if TYPE_CHECKING:
|
|
from astrai.inference.cache import KVCache
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
_default_backend_lock = threading.Lock()
|
|
_env_backend_name: Optional[str] = None
|
|
_env_backend: Optional["AttentionBackend"] = None
|
|
_current_backend: contextvars.ContextVar[Optional["AttentionBackend"]] = (
|
|
contextvars.ContextVar("attn_backend", default=None)
|
|
)
|
|
|
|
# Backends are stateless — one canonical instance per class, created lazily
|
|
# and reused everywhere (resolution, fallback, context managers).
|
|
_singletons: Dict[type, "AttentionBackend"] = {}
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def flash_attn_available() -> bool:
|
|
if not torch.cuda.is_available():
|
|
return False
|
|
fa = _flash_attn
|
|
if fa is None:
|
|
return False
|
|
|
|
try:
|
|
major = int(fa.__version__.split(".")[0])
|
|
cc = torch.cuda.get_device_capability()
|
|
cc_num = cc[0] * 10 + cc[1]
|
|
except Exception:
|
|
major, cc_num = 0, 0
|
|
if (major >= 3 and cc_num < 90) or (major < 3 and 0 < cc_num < 70):
|
|
return False
|
|
|
|
try:
|
|
if not hasattr(fa, "flash_attn_func"):
|
|
return False
|
|
x = torch.zeros(1, 1, 1, 64, device="cuda", dtype=torch.bfloat16)
|
|
out = fa.flash_attn_func(x, x, x, causal=True)
|
|
return bool(torch.isfinite(out).all().item())
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
class ATTN_BACKEND(enum.Enum):
|
|
"""Backend selector enum, mirroring ``torch.nn.attention.SDPBackend``."""
|
|
|
|
TORCH_NATIVE = "torch_native"
|
|
CUDA = "cuda"
|
|
FLASH = "flash"
|
|
|
|
|
|
def _instance(backend_cls: type) -> "AttentionBackend":
|
|
"""Return the canonical singleton instance for a backend class.
|
|
|
|
Backends hold no per-instance state, so a single cached instance is
|
|
safe and avoids per-call allocation on the attention hot path.
|
|
"""
|
|
backend = _singletons.get(backend_cls)
|
|
if backend is None:
|
|
backend = backend_cls()
|
|
_singletons[backend_cls] = backend
|
|
return backend
|
|
|
|
|
|
@functools.lru_cache(maxsize=1)
|
|
def _priority_backends() -> Tuple["AttentionBackend", ...]:
|
|
"""Available backends in priority order: cuda -> flash -> torch.
|
|
|
|
Computed once (machine availability cannot change at runtime) and
|
|
cached forever; the tuple always ends with ``TorchNativeBackend``,
|
|
which is unconditionally available.
|
|
"""
|
|
return tuple(
|
|
_instance(cls)
|
|
for cls in (CudaBackend, FlashAttnBackend, TorchNativeBackend)
|
|
if cls.available()
|
|
)
|
|
|
|
|
|
def _resolve_default_backend() -> "AttentionBackend":
|
|
"""Pick the highest-priority available backend (cuda -> flash -> torch).
|
|
|
|
Resolved lazily on first use and cached via ``_priority_backends``.
|
|
Per-call capability fallback happens in ``attention()``, so the
|
|
default is safe for training and fp32 models.
|
|
"""
|
|
return _priority_backends()[0]
|
|
|
|
|
|
def _environment_backend() -> Optional["AttentionBackend"]:
|
|
"""Resolve the process-wide ``ASTR_BACKEND`` override, if configured."""
|
|
global _env_backend, _env_backend_name
|
|
name = os.environ.get("ASTR_BACKEND", "").strip().lower()
|
|
if not name:
|
|
return None
|
|
if name != _env_backend_name:
|
|
with _default_backend_lock:
|
|
if name != _env_backend_name:
|
|
try:
|
|
_env_backend = _resolve_backend(name)
|
|
except (ValueError, RuntimeError):
|
|
_env_backend = None
|
|
logger.warning(
|
|
"ASTR_BACKEND=%r is not a registered attention backend; "
|
|
"falling back to default resolution",
|
|
name,
|
|
)
|
|
_env_backend_name = name
|
|
return _env_backend
|
|
|
|
|
|
def _resolve_backend(
|
|
backend: Optional[Union[str, ATTN_BACKEND, "AttentionBackend", type]] = None,
|
|
) -> "AttentionBackend":
|
|
"""Resolve a backend configuration to its canonical instance.
|
|
|
|
Accepts a registered name, ``ATTN_BACKEND`` enum value, backend class,
|
|
or instance. Names/classes resolve to the shared singleton; a caller
|
|
may still pass its own instance to opt out of sharing.
|
|
"""
|
|
if backend is not None:
|
|
if isinstance(backend, ATTN_BACKEND):
|
|
return _instance(AttentionBackendFactory.get_component_class(backend.value))
|
|
if isinstance(backend, str):
|
|
return _instance(AttentionBackendFactory.get_component_class(backend))
|
|
if isinstance(backend, type) and issubclass(backend, AttentionBackend):
|
|
return _instance(backend)
|
|
if isinstance(backend, AttentionBackend):
|
|
return backend
|
|
raise TypeError(
|
|
f"expected a registered name, ATTN_BACKEND, AttentionBackend type, "
|
|
f"or instance, got {type(backend).__name__}"
|
|
)
|
|
return _resolve_default_backend()
|
|
|
|
|
|
def get_backend(
|
|
use_default: bool = True,
|
|
) -> Optional["AttentionBackend"]:
|
|
"""Resolve the active backend: explicit context > env > default.
|
|
|
|
An ``attn_backend(...)`` context is the caller's explicit choice and
|
|
always wins. ``ASTR_BACKEND`` is a process-wide override consulted
|
|
only when no context is set. Pass ``use_default=False`` at request
|
|
submission to retain only an environment override or the caller's
|
|
:func:`attn_backend` value.
|
|
"""
|
|
context_backend = _current_backend.get()
|
|
if context_backend is not None:
|
|
return context_backend
|
|
env_backend = _environment_backend()
|
|
if env_backend is not None:
|
|
return env_backend
|
|
return _resolve_default_backend() if use_default else None
|
|
|
|
|
|
@contextmanager
|
|
def attn_backend(backend: Union[str, ATTN_BACKEND, "AttentionBackend", type]):
|
|
"""Context manager to select an attention backend.
|
|
|
|
Mirrors ``torch.nn.attention.sdpa_kernel``. Accepts an
|
|
registered name, ``ATTN_BACKEND`` enum value, backend class, or instance.
|
|
|
|
Examples::
|
|
|
|
with attn_backend(ATTN_BACKEND.TORCH_NATIVE):
|
|
...
|
|
with attn_backend(TorchNativeBackend):
|
|
...
|
|
with attn_backend(TorchNativeBackend()):
|
|
...
|
|
"""
|
|
instance = _resolve_backend(backend)
|
|
token = _current_backend.set(instance)
|
|
try:
|
|
yield instance
|
|
finally:
|
|
_current_backend.reset(token)
|
|
|
|
|
|
def repeat_kv(x: Tensor, n_rep: int) -> Tensor:
|
|
"""Expand KV heads to match Q heads for GQA."""
|
|
if n_rep == 1:
|
|
return x
|
|
n_heads, head_dim = x.shape[-2:]
|
|
return (
|
|
x.unsqueeze(-2)
|
|
.expand(*x.shape[:-2], n_heads, n_rep, head_dim)
|
|
.reshape(*x.shape[:-2], n_heads * n_rep, head_dim)
|
|
)
|
|
|
|
|
|
def attention(
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"] = None,
|
|
layer_id: int = 0,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
fwd: Optional[str] = None,
|
|
backend: Optional[Union[str, ATTN_BACKEND, "AttentionBackend", type]] = None,
|
|
) -> Tensor:
|
|
"""Functional attention entry point — mirrors ``F.scaled_dot_product_attention``.
|
|
|
|
Delegates to the active backend. ``backend`` (optional) is an explicit
|
|
escape hatch; when omitted the backend is resolved as
|
|
explicit context > ``ASTR_BACKEND`` env > default (cuda > flash > torch).
|
|
Handles KV cache I/O, GQA head expansion, and causal masking so the
|
|
caller only needs to provide projected q/k/v.
|
|
|
|
Training calls (``fwd=None``, ``kv_cache=None``) resolve through the
|
|
same capability chain — the CUDA cache kernels cannot run without a
|
|
cache, so they fall back to flash (mask-free/causal calls only) and
|
|
finally to torch SDPA. An explicitly-selected backend that cannot
|
|
handle the call raises — an implicit one falls back down the priority
|
|
list to the first capable backend.
|
|
|
|
Args:
|
|
q: [batch, q_len, n_heads, head_dim] (blhd)
|
|
k: [batch, q_len, n_kv_heads, head_dim] (blhd)
|
|
v: [batch, q_len, n_kv_heads, head_dim] (blhd)
|
|
kv_cache: cache dataclass, or None for training (no cache).
|
|
layer_id: transformer layer index for buffer access.
|
|
attn_mask: pre-built attention mask (SDPA-compatible).
|
|
is_causal: whether to apply causal masking.
|
|
fwd: "prefill" / "decode" for inference, None for training.
|
|
backend: optional explicit backend (name, enum, class, or instance).
|
|
|
|
Returns:
|
|
[batch, q_len, n_heads * head_dim]
|
|
"""
|
|
if backend is not None:
|
|
selected = _resolve_backend(backend)
|
|
explicit = True
|
|
else:
|
|
context_backend = _current_backend.get()
|
|
explicit = context_backend is not None
|
|
# Resolve through the same chain as inference: explicit context >
|
|
# ASTR_BACKEND env > default. Training calls (fwd=None, no cache)
|
|
# land on the CUDA backend and fall back by capability below —
|
|
# flash when it can handle the call, else torch SDPA.
|
|
selected = get_backend()
|
|
assert selected is not None
|
|
|
|
if not selected.supports_call(q, kv_cache, attn_mask, is_causal, fwd):
|
|
if explicit:
|
|
raise RuntimeError(
|
|
f"Explicitly-set backend {type(selected).__name__} cannot "
|
|
f"handle this attention call (shape={q.shape}, "
|
|
f"dtype={q.dtype}, kv_cache={'none' if kv_cache is None else 'present'}, "
|
|
f"attn_mask={'none' if attn_mask is None else 'present'}). "
|
|
f"Remove the attn_backend() context or switch to a compatible backend."
|
|
)
|
|
selected = next(
|
|
(
|
|
candidate
|
|
for candidate in _priority_backends()
|
|
if candidate.supports_call(q, kv_cache, attn_mask, is_causal, fwd)
|
|
),
|
|
_instance(TorchNativeBackend),
|
|
)
|
|
return selected.forward(q, k, v, kv_cache, layer_id, attn_mask, is_causal, fwd)
|
|
|
|
|
|
class AttentionBackend(ABC):
|
|
"""Abstract base for attention computation strategies.
|
|
|
|
Subclasses implement ``fwd_decode`` (q_len == 1, with cache) and
|
|
``fwd_prefill`` (q_len > 1, with or without cache). The public
|
|
``forward`` method dispatches based on q_len.
|
|
|
|
Capability contract — every backend declares:
|
|
|
|
* ``available()`` — machine-level: can this backend exist here
|
|
(kernel ``.so`` loaded, flash-attn present, GPU available)?
|
|
Used once to build the default priority list.
|
|
* ``supports_call(q, kv_cache, attn_mask, is_causal, fwd)`` — can this
|
|
backend run this *specific* call (shape/dtype/cache/mask)? Used by
|
|
``attention()`` for the per-call fallback. Resolution logic never
|
|
checks concrete backend types, so adding a backend requires no
|
|
changes outside its own class.
|
|
|
|
Three equivalent ways to activate a backend::
|
|
|
|
with attn_backend(ATTN_BACKEND.TORCH_NATIVE): # enum
|
|
...
|
|
with attn_backend(TorchNativeBackend): # class
|
|
...
|
|
with TorchNativeBackend(): # instance
|
|
...
|
|
"""
|
|
|
|
def __enter__(self) -> "AttentionBackend":
|
|
self._token = _current_backend.set(self)
|
|
return self
|
|
|
|
def __exit__(self, *exc) -> None:
|
|
_current_backend.reset(self._token)
|
|
|
|
@classmethod
|
|
@abstractmethod
|
|
def available(cls) -> bool:
|
|
"""Return True if this backend can run on the current machine.
|
|
|
|
Checks static availability only (compiled kernels, optional
|
|
packages, GPU presence) — not call-specific constraints.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def supports_call(
|
|
self,
|
|
q: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
attn_mask: Optional[Tensor],
|
|
is_causal: bool,
|
|
fwd: Optional[str],
|
|
) -> bool:
|
|
"""Return True if this backend can run this specific attention call.
|
|
|
|
Called on the canonical singleton instance (or a caller-provided
|
|
one); must be side-effect free.
|
|
"""
|
|
|
|
def forward(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
fwd: Optional[str] = None,
|
|
) -> Tensor:
|
|
"""Dispatch to decode or extend based on q_len.
|
|
|
|
Args:
|
|
q: [batch, q_len, n_heads, head_dim]
|
|
k: [batch, q_len, n_kv_heads, head_dim]
|
|
v: [batch, q_len, n_kv_heads, head_dim]
|
|
kv_cache: cache dataclass, or None for training (no cache).
|
|
layer_id: transformer layer index for buffer access.
|
|
attn_mask: pre-built attention mask compatible with SDPA.
|
|
is_causal: whether to apply causal masking.
|
|
|
|
Returns:
|
|
[batch, q_len, n_heads * head_dim]
|
|
"""
|
|
if fwd == "decode":
|
|
return self.fwd_decode(q, k, v, kv_cache, layer_id, attn_mask, is_causal)
|
|
if fwd == "prefill" or fwd is None:
|
|
return self.fwd_prefill(q, k, v, kv_cache, layer_id, attn_mask, is_causal)
|
|
raise ValueError(f"unsupported attention forward mode: {fwd}")
|
|
|
|
@abstractmethod
|
|
def fwd_decode(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
"""Single-token decode with KV cache."""
|
|
|
|
@abstractmethod
|
|
def fwd_prefill(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
"""Multi-token prefill or training forward."""
|
|
|
|
@staticmethod
|
|
def supports_graph() -> bool:
|
|
"""Return True if this backend supports CUDA-graph capture.
|
|
|
|
Override in subclasses that can run under ``torch.cuda.graph``.
|
|
|
|
Called on the *active* backend instance (or its class) — a cheap
|
|
boolean check with no side-effects.
|
|
"""
|
|
return False
|
|
|
|
|
|
class AttentionBackendFactory(BaseFactory[AttentionBackend]):
|
|
"""Factory for registered attention backends."""
|
|
|
|
|
|
@AttentionBackendFactory.register(ATTN_BACKEND.TORCH_NATIVE.value)
|
|
class TorchNativeBackend(AttentionBackend):
|
|
"""Reference backend using torch SDPA with indirect KV cache indexing.
|
|
|
|
Writes new K/V into the cache buffers, gathers the full sequence K/V
|
|
via ``req_to_token`` indirect indexing, then calls
|
|
``F.scaled_dot_product_attention``.
|
|
|
|
For training (``kv_cache is None``), skips cache I/O entirely and
|
|
runs SDPA directly on the projected q/k/v.
|
|
"""
|
|
|
|
@classmethod
|
|
def available(cls) -> bool:
|
|
return True
|
|
|
|
def supports_call(
|
|
self,
|
|
q: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
attn_mask: Optional[Tensor],
|
|
is_causal: bool,
|
|
fwd: Optional[str],
|
|
) -> bool:
|
|
return True
|
|
|
|
def fwd_decode(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
return self._forward(q, k, v, kv_cache, layer_id, attn_mask, is_causal)
|
|
|
|
def fwd_prefill(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
return self._forward(q, k, v, kv_cache, layer_id, attn_mask, is_causal)
|
|
|
|
def _forward(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
if q.ndim == 4:
|
|
n_rep = q.size(2) // k.size(2)
|
|
if n_rep > 1:
|
|
k = repeat_kv(k, n_rep)
|
|
v = repeat_kv(v, n_rep)
|
|
return (
|
|
F.scaled_dot_product_attention(
|
|
q.permute(0, 2, 1, 3),
|
|
k.permute(0, 2, 1, 3),
|
|
v.permute(0, 2, 1, 3),
|
|
attn_mask,
|
|
is_causal=is_causal,
|
|
)
|
|
.permute(0, 2, 1, 3)
|
|
.contiguous()
|
|
)
|
|
|
|
if kv_cache is None or kv_cache.qo_indptr is None:
|
|
raise ValueError("packed attention requires KV cache metadata")
|
|
kv_cache.k_buffer[layer_id, kv_cache.out_cache_loc] = k
|
|
kv_cache.v_buffer[layer_id, kv_cache.out_cache_loc] = v
|
|
outputs = []
|
|
n_rep = q.size(1) // k.size(1)
|
|
for i in range(kv_cache.req_pool_indices.numel()):
|
|
q_start = int(kv_cache.qo_indptr[i])
|
|
q_end = int(kv_cache.qo_indptr[i + 1])
|
|
indices = kv_cache.req_to_token[
|
|
kv_cache.req_pool_indices[i], : kv_cache.seq_lens[i]
|
|
]
|
|
k_i = kv_cache.k_buffer[layer_id, indices]
|
|
v_i = kv_cache.v_buffer[layer_id, indices]
|
|
if n_rep > 1:
|
|
k_i = repeat_kv(k_i, n_rep)
|
|
v_i = repeat_kv(v_i, n_rep)
|
|
q_len = q_end - q_start
|
|
kv_len = k_i.size(0)
|
|
q_pos = torch.arange(kv_len - q_len, kv_len, device=q.device)
|
|
causal_mask = q_pos[:, None] >= torch.arange(kv_len, device=q.device)
|
|
out = F.scaled_dot_product_attention(
|
|
q[q_start:q_end].transpose(0, 1).unsqueeze(0),
|
|
k_i.transpose(0, 1).unsqueeze(0),
|
|
v_i.transpose(0, 1).unsqueeze(0),
|
|
attn_mask=causal_mask,
|
|
)
|
|
outputs.append(out.squeeze(0).transpose(0, 1))
|
|
return torch.cat(outputs)
|
|
|
|
|
|
@AttentionBackendFactory.register(ATTN_BACKEND.CUDA.value)
|
|
class CudaBackend(AttentionBackend):
|
|
"""CUDA kernel backend with direct KV cache access.
|
|
|
|
Decode path: writes K/V to the flat pool, then calls
|
|
``attn_paged_decode`` with req_to_token + kv_indptr.
|
|
|
|
Prefill path: writes K/V to the flat pool, then calls
|
|
``attn_paged_prefill`` with ragged-batch support via qo_indptr +
|
|
kv_indptr.
|
|
|
|
``kv_cache is None`` (training) raises — the per-call fallback to
|
|
torch SDPA for training / fp32 / unsupported head_dim happens in the
|
|
``attention()`` entry point.
|
|
|
|
Raises ``RuntimeError`` if the required kernel is not available.
|
|
"""
|
|
|
|
# Head dims supported by the CUDA kernels (single source of truth).
|
|
HEAD_DIMS = (32, 64, 128, 256)
|
|
|
|
@classmethod
|
|
def available(cls) -> bool:
|
|
return (
|
|
torch.cuda.is_available()
|
|
and is_available("attn_paged_decode")
|
|
and is_available("attn_paged_prefill")
|
|
)
|
|
|
|
def supports_call(
|
|
self,
|
|
q: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
attn_mask: Optional[Tensor],
|
|
is_causal: bool,
|
|
fwd: Optional[str],
|
|
) -> bool:
|
|
# The CUDA kernels are bf16-only, support head_dim in
|
|
# HEAD_DIMS, and need a KV cache (decode/prefill); everything
|
|
# else falls back down the priority list to torch.
|
|
return (
|
|
fwd in ("prefill", "decode")
|
|
and kv_cache is not None
|
|
and q.ndim == 3
|
|
and q.dtype == torch.bfloat16
|
|
and q.size(-1) in self.HEAD_DIMS
|
|
and is_available(f"attn_paged_{fwd}")
|
|
)
|
|
|
|
@staticmethod
|
|
def supports_graph() -> bool:
|
|
return True
|
|
|
|
def fwd_decode(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
if kv_cache is None:
|
|
raise RuntimeError("CudaBackend does not support training (kv_cache=None)")
|
|
|
|
kv_indptr = kv_cache.kv_indptr
|
|
|
|
out = attn_paged_decode(
|
|
q,
|
|
kv_cache.k_buffer[layer_id],
|
|
kv_cache.v_buffer[layer_id],
|
|
kv_cache.req_to_token,
|
|
kv_cache.req_pool_indices,
|
|
kv_indptr,
|
|
new_k=k,
|
|
new_v=v,
|
|
is_causal=True,
|
|
o_part_buf=kv_cache.decode_o_part,
|
|
ml_part_buf=kv_cache.decode_ml_part,
|
|
out_buf=kv_cache.decode_out,
|
|
)
|
|
return out
|
|
|
|
def fwd_prefill(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
if kv_cache is None:
|
|
raise RuntimeError("CudaBackend does not support training (kv_cache=None)")
|
|
|
|
loc = kv_cache.out_cache_loc
|
|
kv_cache.k_buffer[layer_id, loc] = k
|
|
kv_cache.v_buffer[layer_id, loc] = v
|
|
|
|
out = attn_paged_prefill(
|
|
q,
|
|
kv_cache.k_buffer[layer_id],
|
|
kv_cache.v_buffer[layer_id],
|
|
kv_cache.req_to_token,
|
|
kv_cache.req_pool_indices,
|
|
kv_cache.kv_indptr,
|
|
kv_cache.qo_indptr,
|
|
kv_cache.q_tile_to_batch,
|
|
kv_cache.q_tile_to_index,
|
|
attn_mask,
|
|
is_causal=is_causal,
|
|
)
|
|
return out
|
|
|
|
|
|
@AttentionBackendFactory.register(ATTN_BACKEND.FLASH.value)
|
|
class FlashAttnBackend(AttentionBackend):
|
|
"""FlashAttention backend via the optional ``flash-attn`` package.
|
|
|
|
Decode (q_len=1, contiguous cache): writes K/V to the pool, gathers
|
|
flat K/V via the ``req_to_token`` page table, and calls
|
|
``flash_attn_varlen_func`` over the ragged batch
|
|
(``qo_indptr``/``kv_indptr``).
|
|
|
|
Prefill: packed 3-D calls share the ``flash_attn_varlen_func`` path;
|
|
dense 4-D calls go through ``flash_attn_func`` (mask-free only).
|
|
"""
|
|
|
|
@classmethod
|
|
def available(cls) -> bool:
|
|
return flash_attn_available()
|
|
|
|
def supports_call(
|
|
self,
|
|
q: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
attn_mask: Optional[Tensor],
|
|
is_causal: bool,
|
|
fwd: Optional[str],
|
|
) -> bool:
|
|
if not self.available():
|
|
return False
|
|
if q.dtype not in (torch.float16, torch.bfloat16):
|
|
return False
|
|
if fwd is not None:
|
|
return q.ndim == 3 and hasattr(_flash_attn, "flash_attn_varlen_func")
|
|
# Dense (training) path: flash_attn_func cannot apply a custom
|
|
# mask, so only mask-free calls are supported — ``is_causal`` is
|
|
# a flag, not a mask. Masked training (SFT/DPO/GRPO) must fall
|
|
# back to TorchNativeBackend instead of silently ignoring the mask.
|
|
return attn_mask is None
|
|
|
|
def fwd_decode(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
return self._forward_packed(q, k, v, kv_cache, layer_id)
|
|
|
|
def fwd_prefill(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: Optional["KVCache"],
|
|
layer_id: int,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
if q.ndim == 3:
|
|
return self._forward_packed(q, k, v, kv_cache, layer_id)
|
|
return self._forward_dense(q, k, v, attn_mask, is_causal)
|
|
|
|
def _forward_dense(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
attn_mask: Optional[Tensor] = None,
|
|
is_causal: bool = False,
|
|
) -> Tensor:
|
|
n_rep = q.size(2) // k.size(2)
|
|
if n_rep > 1:
|
|
k = repeat_kv(k, n_rep)
|
|
v = repeat_kv(v, n_rep)
|
|
|
|
if attn_mask is not None:
|
|
raise ValueError(
|
|
"FlashAttnBackend cannot handle a custom attention mask; "
|
|
"use a causal mask or select TorchNativeBackend."
|
|
)
|
|
fa = _flash_attn
|
|
if fa is None:
|
|
raise RuntimeError(
|
|
"FlashAttnBackend requires the optional 'flash-attn' package. "
|
|
"Install with `pip install flash-attn`."
|
|
)
|
|
out = fa.flash_attn_func(
|
|
q.contiguous(),
|
|
k.contiguous(),
|
|
v.contiguous(),
|
|
causal=is_causal,
|
|
)
|
|
return out.contiguous()
|
|
|
|
def _forward_packed(
|
|
self,
|
|
q: Tensor,
|
|
k: Tensor,
|
|
v: Tensor,
|
|
kv_cache: "KVCache",
|
|
layer_id: int,
|
|
) -> Tensor:
|
|
fa = _flash_attn
|
|
if fa is None or not hasattr(fa, "flash_attn_varlen_func"):
|
|
raise RuntimeError("packed inference requires flash_attn_varlen_func")
|
|
kv_cache.k_buffer[layer_id, kv_cache.out_cache_loc] = k
|
|
kv_cache.v_buffer[layer_id, kv_cache.out_cache_loc] = v
|
|
page_table = kv_cache.req_to_token[
|
|
kv_cache.req_pool_indices, : kv_cache.max_len
|
|
]
|
|
positions = torch.arange(kv_cache.max_len, device=q.device)
|
|
indices = page_table[positions.unsqueeze(0) < kv_cache.seq_lens.unsqueeze(1)]
|
|
k_flat = kv_cache.k_buffer[layer_id, indices].contiguous()
|
|
v_flat = kv_cache.v_buffer[layer_id, indices].contiguous()
|
|
out = fa.flash_attn_varlen_func(
|
|
q.contiguous(),
|
|
k_flat,
|
|
v_flat,
|
|
kv_cache.qo_indptr,
|
|
kv_cache.kv_indptr,
|
|
int((kv_cache.qo_indptr[1:] - kv_cache.qo_indptr[:-1]).max()),
|
|
int(kv_cache.seq_lens.max()),
|
|
dropout_p=0.0,
|
|
causal=True,
|
|
)
|
|
return out
|