refactor : use factory for attention backends
- register built-in backends through BaseFactory - derive benchmark choices from registered backends - cover string selection and invalid backend names
This commit is contained in:
@@ -18,6 +18,7 @@ SDPA is handled by the attention backend, not the wrapper functions.
|
|||||||
from astrai.extension.attention_backend import (
|
from astrai.extension.attention_backend import (
|
||||||
ATTN_BACKEND,
|
ATTN_BACKEND,
|
||||||
AttentionBackend,
|
AttentionBackend,
|
||||||
|
AttentionBackendFactory,
|
||||||
CudaBackend,
|
CudaBackend,
|
||||||
FlashAttnBackend,
|
FlashAttnBackend,
|
||||||
TorchNativeBackend,
|
TorchNativeBackend,
|
||||||
@@ -37,6 +38,7 @@ from astrai.extension.rotary_backend import apply_rotary_emb
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"ATTN_BACKEND",
|
"ATTN_BACKEND",
|
||||||
"AttentionBackend",
|
"AttentionBackend",
|
||||||
|
"AttentionBackendFactory",
|
||||||
"CudaBackend",
|
"CudaBackend",
|
||||||
"TorchNativeBackend",
|
"TorchNativeBackend",
|
||||||
"FlashAttnBackend",
|
"FlashAttnBackend",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ from astrai.extension.attention_ops import (
|
|||||||
attn_paged_decode,
|
attn_paged_decode,
|
||||||
attn_paged_prefill,
|
attn_paged_prefill,
|
||||||
)
|
)
|
||||||
|
from astrai.factory import BaseFactory
|
||||||
from astrai.inference.core.cache import KVCache
|
from astrai.inference.core.cache import KVCache
|
||||||
|
|
||||||
_current_backend: contextvars.ContextVar["AttentionBackend"] = contextvars.ContextVar(
|
_current_backend: contextvars.ContextVar["AttentionBackend"] = contextvars.ContextVar(
|
||||||
@@ -146,11 +147,11 @@ def get_backend() -> "AttentionBackend":
|
|||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def attn_backend(backend: Union[ATTN_BACKEND, "AttentionBackend", type]):
|
def attn_backend(backend: Union[str, ATTN_BACKEND, "AttentionBackend", type]):
|
||||||
"""Context manager to select an attention backend.
|
"""Context manager to select an attention backend.
|
||||||
|
|
||||||
Mirrors ``torch.nn.attention.sdpa_kernel``. Accepts an
|
Mirrors ``torch.nn.attention.sdpa_kernel``. Accepts an
|
||||||
``ATTN_BACKEND`` enum value, a backend class, or a backend instance.
|
registered name, ``ATTN_BACKEND`` enum value, backend class, or instance.
|
||||||
|
|
||||||
Examples::
|
Examples::
|
||||||
|
|
||||||
@@ -162,14 +163,17 @@ def attn_backend(backend: Union[ATTN_BACKEND, "AttentionBackend", type]):
|
|||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
if isinstance(backend, ATTN_BACKEND):
|
if isinstance(backend, ATTN_BACKEND):
|
||||||
instance = _BACKEND_REGISTRY[backend]()
|
instance = AttentionBackendFactory.create(backend.value)
|
||||||
|
elif isinstance(backend, str):
|
||||||
|
instance = AttentionBackendFactory.create(backend)
|
||||||
elif isinstance(backend, type) and issubclass(backend, AttentionBackend):
|
elif isinstance(backend, type) and issubclass(backend, AttentionBackend):
|
||||||
instance = backend()
|
instance = backend()
|
||||||
elif isinstance(backend, AttentionBackend):
|
elif isinstance(backend, AttentionBackend):
|
||||||
instance = backend
|
instance = backend
|
||||||
else:
|
else:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f"expected ATTN_BACKEND, AttentionBackend type, or instance, "
|
f"expected a registered name, ATTN_BACKEND, AttentionBackend type, "
|
||||||
|
f"or instance, "
|
||||||
f"got {type(backend).__name__}"
|
f"got {type(backend).__name__}"
|
||||||
)
|
)
|
||||||
token = _current_backend.set(instance)
|
token = _current_backend.set(instance)
|
||||||
@@ -301,6 +305,11 @@ class AttentionBackend(ABC):
|
|||||||
"""Multi-token prefill or training forward."""
|
"""Multi-token prefill or training forward."""
|
||||||
|
|
||||||
|
|
||||||
|
class AttentionBackendFactory(BaseFactory[AttentionBackend]):
|
||||||
|
"""Factory for registered attention backends."""
|
||||||
|
|
||||||
|
|
||||||
|
@AttentionBackendFactory.register(ATTN_BACKEND.TORCH_NATIVE.value)
|
||||||
class TorchNativeBackend(AttentionBackend):
|
class TorchNativeBackend(AttentionBackend):
|
||||||
"""Reference backend using torch SDPA with indirect KV cache indexing.
|
"""Reference backend using torch SDPA with indirect KV cache indexing.
|
||||||
|
|
||||||
@@ -385,6 +394,7 @@ class TorchNativeBackend(AttentionBackend):
|
|||||||
_default_backend = TorchNativeBackend()
|
_default_backend = TorchNativeBackend()
|
||||||
|
|
||||||
|
|
||||||
|
@AttentionBackendFactory.register(ATTN_BACKEND.CUDA.value)
|
||||||
class CudaBackend(AttentionBackend):
|
class CudaBackend(AttentionBackend):
|
||||||
"""CUDA kernel backend with direct KV cache access.
|
"""CUDA kernel backend with direct KV cache access.
|
||||||
|
|
||||||
@@ -474,6 +484,7 @@ class CudaBackend(AttentionBackend):
|
|||||||
return out.reshape(b, q_len, q.size(2), q.size(3)).flatten(2)
|
return out.reshape(b, q_len, q.size(2), q.size(3)).flatten(2)
|
||||||
|
|
||||||
|
|
||||||
|
@AttentionBackendFactory.register(ATTN_BACKEND.FLASH.value)
|
||||||
class FlashAttnBackend(AttentionBackend):
|
class FlashAttnBackend(AttentionBackend):
|
||||||
"""FlashAttention (FA2/FA3) backend via the optional ``flash-attn`` package.
|
"""FlashAttention (FA2/FA3) backend via the optional ``flash-attn`` package.
|
||||||
|
|
||||||
@@ -557,10 +568,3 @@ class FlashAttnBackend(AttentionBackend):
|
|||||||
q.contiguous(), k.contiguous(), v.contiguous(), causal=is_causal
|
q.contiguous(), k.contiguous(), v.contiguous(), causal=is_causal
|
||||||
)
|
)
|
||||||
return out.contiguous().flatten(2)
|
return out.contiguous().flatten(2)
|
||||||
|
|
||||||
|
|
||||||
_BACKEND_REGISTRY: dict[ATTN_BACKEND, type[AttentionBackend]] = {
|
|
||||||
ATTN_BACKEND.TORCH_NATIVE: TorchNativeBackend,
|
|
||||||
ATTN_BACKEND.CUDA: CudaBackend,
|
|
||||||
ATTN_BACKEND.FLASH: FlashAttnBackend,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional, Union
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
from astrai import setup_logging
|
from astrai import setup_logging
|
||||||
from astrai.config import AutoRegressiveLMConfig
|
from astrai.config import AutoRegressiveLMConfig
|
||||||
from astrai.extension import ATTN_BACKEND, attn_backend
|
from astrai.extension import ATTN_BACKEND, AttentionBackendFactory, attn_backend
|
||||||
from astrai.inference.core.cache import PagePool
|
from astrai.inference.core.cache import PagePool
|
||||||
from astrai.model import AutoModel
|
from astrai.model import AutoModel
|
||||||
|
|
||||||
_DTYPES = ["bfloat16", "float16", "float32"]
|
_DTYPES = ["bfloat16", "float16", "float32"]
|
||||||
_CACHES = ["contiguous", "paged"]
|
_CACHES = ["contiguous", "paged"]
|
||||||
_BACKENDS = ["cuda", "torch_native"]
|
_BACKENDS = AttentionBackendFactory.list_registered()
|
||||||
|
|
||||||
_BACKEND_MAP = {
|
|
||||||
"cuda": ATTN_BACKEND.CUDA,
|
|
||||||
"torch_native": ATTN_BACKEND.TORCH_NATIVE,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class BenchmarkResult:
|
class BenchmarkResult:
|
||||||
@@ -46,7 +41,7 @@ class GenerationBenchmark:
|
|||||||
device: str = "cuda",
|
device: str = "cuda",
|
||||||
dtype: torch.dtype = torch.bfloat16,
|
dtype: torch.dtype = torch.bfloat16,
|
||||||
cache_type: str = "contiguous",
|
cache_type: str = "contiguous",
|
||||||
backend: ATTN_BACKEND = ATTN_BACKEND.CUDA,
|
backend: Union[str, ATTN_BACKEND] = ATTN_BACKEND.CUDA,
|
||||||
):
|
):
|
||||||
self.device = device
|
self.device = device
|
||||||
self.dtype = dtype
|
self.dtype = dtype
|
||||||
@@ -297,7 +292,7 @@ def benchmark_command(
|
|||||||
device=device,
|
device=device,
|
||||||
dtype=dtype_map[dtype],
|
dtype=dtype_map[dtype],
|
||||||
cache_type=cache,
|
cache_type=cache,
|
||||||
backend=_BACKEND_MAP[name],
|
backend=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
click.secho(
|
click.secho(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import pytest
|
|||||||
|
|
||||||
from astrai.extension import (
|
from astrai.extension import (
|
||||||
ATTN_BACKEND,
|
ATTN_BACKEND,
|
||||||
|
AttentionBackendFactory,
|
||||||
CudaBackend,
|
CudaBackend,
|
||||||
TorchNativeBackend,
|
TorchNativeBackend,
|
||||||
attn_backend,
|
attn_backend,
|
||||||
@@ -26,6 +27,26 @@ def test_attn_backend_context_with_enum():
|
|||||||
assert isinstance(get_backend(), TorchNativeBackend)
|
assert isinstance(get_backend(), TorchNativeBackend)
|
||||||
|
|
||||||
|
|
||||||
|
def test_attn_backend_context_with_registered_name():
|
||||||
|
with attn_backend("cuda"):
|
||||||
|
assert isinstance(get_backend(), CudaBackend)
|
||||||
|
assert isinstance(get_backend(), TorchNativeBackend)
|
||||||
|
|
||||||
|
|
||||||
|
def test_attention_backend_factory_lists_builtin_backends():
|
||||||
|
assert AttentionBackendFactory.list_registered() == [
|
||||||
|
"cuda",
|
||||||
|
"flash",
|
||||||
|
"torch_native",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_attn_backend_rejects_unknown_registered_name():
|
||||||
|
with pytest.raises(ValueError, match="Unknown component: 'unknown'"):
|
||||||
|
with attn_backend("unknown"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_attn_backend_context_with_class():
|
def test_attn_backend_context_with_class():
|
||||||
with attn_backend(CudaBackend):
|
with attn_backend(CudaBackend):
|
||||||
assert isinstance(get_backend(), CudaBackend)
|
assert isinstance(get_backend(), CudaBackend)
|
||||||
|
|||||||
Reference in New Issue
Block a user