feat: propagate attention backend across scheduler threads
- InferenceEngine/Scheduler accept an explicit backend - capture request-level attn_backend context onto Task - split prefill/decode batches by backend instance - ASTR_BACKEND env overrides ContextVar as process-wide policy - report resolved backend and CUDA-graph state in benchmark
This commit is contained in:
@@ -56,8 +56,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
_default_backend: Optional["AttentionBackend"] = None
|
_default_backend: Optional["AttentionBackend"] = None
|
||||||
_default_backend_lock = threading.Lock()
|
_default_backend_lock = threading.Lock()
|
||||||
_current_backend: contextvars.ContextVar["AttentionBackend"] = contextvars.ContextVar(
|
_env_backend_name: Optional[str] = None
|
||||||
"attn_backend"
|
_env_backend: Optional["AttentionBackend"] = None
|
||||||
|
_current_backend: contextvars.ContextVar[Optional["AttentionBackend"]] = (
|
||||||
|
contextvars.ContextVar("attn_backend", default=None)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -149,33 +151,48 @@ def _backend_supports(
|
|||||||
def _resolve_default_backend() -> "AttentionBackend":
|
def _resolve_default_backend() -> "AttentionBackend":
|
||||||
"""Pick the highest-priority available backend (cuda -> flash -> torch).
|
"""Pick the highest-priority available backend (cuda -> flash -> torch).
|
||||||
|
|
||||||
Set ``ASTR_BACKEND`` to override: ``ASTR_BACKEND=cuda``, ``torch_native``,
|
|
||||||
or ``flash``. The value is the registered name (same as the
|
|
||||||
``ATTN_BACKEND`` enum value).
|
|
||||||
|
|
||||||
Resolved lazily on first ``get_backend()`` and cached. Per-call
|
Resolved lazily on first ``get_backend()`` and cached. Per-call
|
||||||
capability fallback happens in ``attention()``, so the default is
|
capability fallback happens in ``attention()``, so the default is
|
||||||
safe for training and fp32 models.
|
safe for training and fp32 models.
|
||||||
"""
|
"""
|
||||||
forced = os.environ.get("ASTR_BACKEND", "").strip().lower()
|
|
||||||
if forced:
|
|
||||||
try:
|
|
||||||
return AttentionBackendFactory.create(forced)
|
|
||||||
except (ValueError, RuntimeError):
|
|
||||||
pass
|
|
||||||
return _priority_backends()[0]
|
return _priority_backends()[0]
|
||||||
|
|
||||||
|
|
||||||
def get_backend() -> "AttentionBackend":
|
def _environment_backend() -> Optional["AttentionBackend"]:
|
||||||
"""Return the active backend for the current thread/context.
|
"""Resolve the process-wide ``ASTR_BACKEND`` override, if configured."""
|
||||||
|
global _env_backend, _env_backend_name
|
||||||
Falls back to the highest-priority available backend (cuda -> flash ->
|
name = os.environ.get("ASTR_BACKEND", "").strip().lower()
|
||||||
torch_native) when no backend has been activated via ``with``. Set
|
if not name:
|
||||||
``ASTR_BACKEND`` to override the default.
|
return None
|
||||||
"""
|
if name != _env_backend_name:
|
||||||
|
with _default_backend_lock:
|
||||||
|
if name != _env_backend_name:
|
||||||
try:
|
try:
|
||||||
return _current_backend.get()
|
_env_backend = AttentionBackendFactory.create(name)
|
||||||
except LookupError:
|
except (ValueError, RuntimeError):
|
||||||
|
_env_backend = None
|
||||||
|
_env_backend_name = name
|
||||||
|
return _env_backend
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_backend(
|
||||||
|
backend: Optional[Union[str, ATTN_BACKEND, "AttentionBackend", type]] = None,
|
||||||
|
) -> "AttentionBackend":
|
||||||
|
"""Resolve a backend configuration, defaulting to the process policy."""
|
||||||
|
if backend is not None:
|
||||||
|
if isinstance(backend, ATTN_BACKEND):
|
||||||
|
return AttentionBackendFactory.create(backend.value)
|
||||||
|
if isinstance(backend, str):
|
||||||
|
return AttentionBackendFactory.create(backend)
|
||||||
|
if isinstance(backend, type) and issubclass(backend, AttentionBackend):
|
||||||
|
return 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__}"
|
||||||
|
)
|
||||||
|
|
||||||
global _default_backend
|
global _default_backend
|
||||||
if _default_backend is None:
|
if _default_backend is None:
|
||||||
with _default_backend_lock:
|
with _default_backend_lock:
|
||||||
@@ -184,6 +201,22 @@ def get_backend() -> "AttentionBackend":
|
|||||||
return _default_backend
|
return _default_backend
|
||||||
|
|
||||||
|
|
||||||
|
def get_backend(
|
||||||
|
use_default: bool = True,
|
||||||
|
) -> Optional["AttentionBackend"]:
|
||||||
|
"""Return the context override, optionally falling back to the process default.
|
||||||
|
|
||||||
|
``ASTR_BACKEND`` is a process-wide override and takes precedence over the
|
||||||
|
context value. Pass ``use_default=False`` at request submission to retain
|
||||||
|
only an environment override or the caller's :func:`attn_backend` value.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
_environment_backend()
|
||||||
|
or _current_backend.get()
|
||||||
|
or (_resolve_backend() if use_default else None)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def attn_backend(backend: Union[str, 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.
|
||||||
@@ -200,20 +233,7 @@ def attn_backend(backend: Union[str, ATTN_BACKEND, "AttentionBackend", type]):
|
|||||||
with attn_backend(TorchNativeBackend()):
|
with attn_backend(TorchNativeBackend()):
|
||||||
...
|
...
|
||||||
"""
|
"""
|
||||||
if isinstance(backend, ATTN_BACKEND):
|
instance = _resolve_backend(backend)
|
||||||
instance = AttentionBackendFactory.create(backend.value)
|
|
||||||
elif isinstance(backend, str):
|
|
||||||
instance = AttentionBackendFactory.create(backend)
|
|
||||||
elif isinstance(backend, type) and issubclass(backend, AttentionBackend):
|
|
||||||
instance = backend()
|
|
||||||
elif isinstance(backend, AttentionBackend):
|
|
||||||
instance = backend
|
|
||||||
else:
|
|
||||||
raise TypeError(
|
|
||||||
f"expected a registered name, ATTN_BACKEND, AttentionBackend type, "
|
|
||||||
f"or instance, "
|
|
||||||
f"got {type(backend).__name__}"
|
|
||||||
)
|
|
||||||
token = _current_backend.set(instance)
|
token = _current_backend.set(instance)
|
||||||
try:
|
try:
|
||||||
yield instance
|
yield instance
|
||||||
@@ -284,10 +304,7 @@ def attention(
|
|||||||
"""
|
"""
|
||||||
backend = get_backend()
|
backend = get_backend()
|
||||||
if not _backend_supports(backend, q, kv_cache, attn_mask, is_causal):
|
if not _backend_supports(backend, q, kv_cache, attn_mask, is_causal):
|
||||||
try:
|
explicit = get_backend(use_default=False)
|
||||||
explicit = _current_backend.get()
|
|
||||||
except LookupError:
|
|
||||||
explicit = None
|
|
||||||
if explicit is not None:
|
if explicit is not None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Explicitly-set backend {type(backend).__name__} cannot "
|
f"Explicitly-set backend {type(backend).__name__} cannot "
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import Any, AsyncGenerator, Dict, Generator, List, Optional, Tuple,
|
|||||||
import torch
|
import torch
|
||||||
import torch.nn as nn
|
import torch.nn as nn
|
||||||
|
|
||||||
|
from astrai.extension import ATTN_BACKEND, AttentionBackend, get_backend
|
||||||
from astrai.inference.cache import PagePool
|
from astrai.inference.cache import PagePool
|
||||||
from astrai.inference.scheduler import InferenceScheduler
|
from astrai.inference.scheduler import InferenceScheduler
|
||||||
from astrai.inference.task import STOP
|
from astrai.inference.task import STOP
|
||||||
@@ -75,6 +76,7 @@ class InferenceEngine:
|
|||||||
max_seq_len: Optional[int] = None,
|
max_seq_len: Optional[int] = None,
|
||||||
cache: Optional[PagePool] = None,
|
cache: Optional[PagePool] = None,
|
||||||
enable_cuda_graph: bool = True,
|
enable_cuda_graph: bool = True,
|
||||||
|
backend: Optional[Union[str, ATTN_BACKEND, AttentionBackend, type]] = None,
|
||||||
):
|
):
|
||||||
self.model = model
|
self.model = model
|
||||||
self.tokenizer = tokenizer
|
self.tokenizer = tokenizer
|
||||||
@@ -85,6 +87,7 @@ class InferenceEngine:
|
|||||||
max_seq_len=max_seq_len,
|
max_seq_len=max_seq_len,
|
||||||
cache=cache,
|
cache=cache,
|
||||||
enable_cuda_graph=enable_cuda_graph,
|
enable_cuda_graph=enable_cuda_graph,
|
||||||
|
backend=backend,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.scheduler.start()
|
self.scheduler.start()
|
||||||
@@ -174,6 +177,7 @@ class InferenceEngine:
|
|||||||
rep_window: int,
|
rep_window: int,
|
||||||
) -> Union[Generator, str, List[str]]:
|
) -> Union[Generator, str, List[str]]:
|
||||||
n = len(prompts)
|
n = len(prompts)
|
||||||
|
request_backend = get_backend(use_default=False)
|
||||||
result = GenerateResult(count=n)
|
result = GenerateResult(count=n)
|
||||||
task_ids = [
|
task_ids = [
|
||||||
self.scheduler.add_task(
|
self.scheduler.add_task(
|
||||||
@@ -184,6 +188,7 @@ class InferenceEngine:
|
|||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
frequency_penalty=frequency_penalty,
|
frequency_penalty=frequency_penalty,
|
||||||
rep_window=rep_window,
|
rep_window=rep_window,
|
||||||
|
backend=request_backend,
|
||||||
stream_callback=lambda token, idx=i: result.append(token, idx),
|
stream_callback=lambda token, idx=i: result.append(token, idx),
|
||||||
)
|
)
|
||||||
for i, p in enumerate(prompts)
|
for i, p in enumerate(prompts)
|
||||||
@@ -223,6 +228,14 @@ class InferenceEngine:
|
|||||||
def get_stats(self) -> Dict[str, Any]:
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
return self.scheduler.get_stats()
|
return self.scheduler.get_stats()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backend_name(self) -> str:
|
||||||
|
return self.scheduler.backend_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cuda_graph_enabled(self) -> bool:
|
||||||
|
return self.scheduler.cuda_graph_enabled
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.scheduler.stop()
|
self.scheduler.stop()
|
||||||
if torch.cuda.is_available():
|
if torch.cuda.is_available():
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import torch
|
|||||||
from torch import Tensor
|
from torch import Tensor
|
||||||
|
|
||||||
from astrai.extension.attention_backend import (
|
from astrai.extension.attention_backend import (
|
||||||
ATTN_BACKEND,
|
|
||||||
CudaBackend,
|
CudaBackend,
|
||||||
attn_backend,
|
|
||||||
get_backend,
|
get_backend,
|
||||||
)
|
)
|
||||||
from astrai.inference.cache import PagePool, TaskCacheManager
|
from astrai.inference.cache import PagePool, TaskCacheManager
|
||||||
@@ -153,7 +151,6 @@ def _warmup_cuda_graphs(
|
|||||||
|
|
||||||
with (
|
with (
|
||||||
torch.inference_mode(),
|
torch.inference_mode(),
|
||||||
attn_backend(ATTN_BACKEND.CUDA),
|
|
||||||
timed(f"warmup decode b={b}", logger),
|
timed(f"warmup decode b={b}", logger),
|
||||||
):
|
):
|
||||||
for step in range(2):
|
for step in range(2):
|
||||||
@@ -208,7 +205,10 @@ class Executor:
|
|||||||
config = model.config
|
config = model.config
|
||||||
max_q_heads = config.num_attention_heads
|
max_q_heads = config.num_attention_heads
|
||||||
head_dim = config.hidden_size // config.num_attention_heads
|
head_dim = config.hidden_size // config.num_attention_heads
|
||||||
self._graph_supported = CudaBackend.supports(head_dim=head_dim)
|
backend = get_backend()
|
||||||
|
self._graph_supported = backend.supports_graph() and CudaBackend.supports(
|
||||||
|
head_dim=head_dim
|
||||||
|
)
|
||||||
self._workspace = InferenceWorkspace(
|
self._workspace = InferenceWorkspace(
|
||||||
max_batch_size=kv_cache.max_batch_size,
|
max_batch_size=kv_cache.max_batch_size,
|
||||||
max_seq_len=kv_cache.max_seq_len,
|
max_seq_len=kv_cache.max_seq_len,
|
||||||
@@ -240,6 +240,10 @@ class Executor:
|
|||||||
device=self.device,
|
device=self.device,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cuda_graph_enabled(self) -> bool:
|
||||||
|
return self._graph_ctx.enabled and self._graph_supported
|
||||||
|
|
||||||
def _sample_logits(
|
def _sample_logits(
|
||||||
self,
|
self,
|
||||||
logits: Tensor,
|
logits: Tensor,
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from contextlib import nullcontext
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
from astrai.extension import (
|
||||||
|
ATTN_BACKEND,
|
||||||
|
AttentionBackend,
|
||||||
|
attn_backend,
|
||||||
|
get_backend,
|
||||||
|
)
|
||||||
from astrai.inference.cache import PagePool, TaskCacheManager
|
from astrai.inference.cache import PagePool, TaskCacheManager
|
||||||
from astrai.inference.metrics import MetricsCollector
|
from astrai.inference.metrics import MetricsCollector
|
||||||
from astrai.inference.runtime.executor import Executor
|
from astrai.inference.runtime.executor import Executor
|
||||||
@@ -28,6 +35,7 @@ class InferenceScheduler:
|
|||||||
dtype: Optional[torch.dtype] = None,
|
dtype: Optional[torch.dtype] = None,
|
||||||
cache: Optional[PagePool] = None,
|
cache: Optional[PagePool] = None,
|
||||||
enable_cuda_graph: bool = True,
|
enable_cuda_graph: bool = True,
|
||||||
|
backend: Optional[Union[str, ATTN_BACKEND, AttentionBackend, type]] = None,
|
||||||
):
|
):
|
||||||
config = model.config
|
config = model.config
|
||||||
|
|
||||||
@@ -69,6 +77,23 @@ class InferenceScheduler:
|
|||||||
metrics=self._metrics,
|
metrics=self._metrics,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if backend is None:
|
||||||
|
self._backend = None
|
||||||
|
default_backend = get_backend()
|
||||||
|
self._backend_name = type(default_backend).__name__
|
||||||
|
with attn_backend(default_backend):
|
||||||
|
self._executor = Executor(
|
||||||
|
model=model,
|
||||||
|
kv_cache=self._cache,
|
||||||
|
task_cache=self._task_cache,
|
||||||
|
device=self.device,
|
||||||
|
dtype=self.dtype,
|
||||||
|
enable_cuda_graph=enable_cuda_graph,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
with attn_backend(backend):
|
||||||
|
self._backend = get_backend()
|
||||||
|
self._backend_name = type(self._backend).__name__
|
||||||
self._executor = Executor(
|
self._executor = Executor(
|
||||||
model=model,
|
model=model,
|
||||||
kv_cache=self._cache,
|
kv_cache=self._cache,
|
||||||
@@ -91,6 +116,26 @@ class InferenceScheduler:
|
|||||||
def get_stats(self) -> Dict[str, Any]:
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
return self._task_mgr.get_stats()
|
return self._task_mgr.get_stats()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backend_name(self) -> str:
|
||||||
|
return self._backend_name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cuda_graph_enabled(self) -> bool:
|
||||||
|
return self._executor.cuda_graph_enabled
|
||||||
|
|
||||||
|
def _backend_context(self):
|
||||||
|
if self._backend is None:
|
||||||
|
return nullcontext()
|
||||||
|
return attn_backend(self._backend)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _task_backend_groups(tasks: List[Task]):
|
||||||
|
groups = {}
|
||||||
|
for task in tasks:
|
||||||
|
groups.setdefault(task.backend, (task.backend, []))[1].append(task)
|
||||||
|
return groups.values()
|
||||||
|
|
||||||
def _step(
|
def _step(
|
||||||
self, tasks: List[Task], return_logprobs: bool = False
|
self, tasks: List[Task], return_logprobs: bool = False
|
||||||
) -> Tuple[List[Task], List[Task]]:
|
) -> Tuple[List[Task], List[Task]]:
|
||||||
@@ -121,15 +166,24 @@ class InferenceScheduler:
|
|||||||
for t in to_prefill:
|
for t in to_prefill:
|
||||||
t.input_tokens = len(t.prompt_ids)
|
t.input_tokens = len(t.prompt_ids)
|
||||||
|
|
||||||
groups: Dict[Tuple[int, int], List[Task]] = {}
|
groups: Dict[Tuple[int, int, Optional[AttentionBackend]], List[Task]] = {}
|
||||||
for t in to_prefill:
|
for t in to_prefill:
|
||||||
start_pos = min(
|
start_pos = min(
|
||||||
self._task_cache.task_cached(t.task_id), len(t.prompt_ids) - 1
|
self._task_cache.task_cached(t.task_id), len(t.prompt_ids) - 1
|
||||||
)
|
)
|
||||||
groups.setdefault((len(t.prompt_ids), start_pos), []).append(t)
|
groups.setdefault((len(t.prompt_ids), start_pos, t.backend), []).append(
|
||||||
|
t
|
||||||
|
)
|
||||||
|
|
||||||
for (prompt_len, start_pos), group in groups.items():
|
for (prompt_len, start_pos, _), group in groups.items():
|
||||||
with self._metrics.record([t.task_id for t in group], "prefill"):
|
backend = group[0].backend
|
||||||
|
backend_context = (
|
||||||
|
attn_backend(backend) if backend is not None else nullcontext()
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
backend_context,
|
||||||
|
self._metrics.record([t.task_id for t in group], "prefill"),
|
||||||
|
):
|
||||||
prefilled, step_out = self._executor.execute_prefill(
|
prefilled, step_out = self._executor.execute_prefill(
|
||||||
group, prompt_len, start_pos, return_logprobs=return_logprobs
|
group, prompt_len, start_pos, return_logprobs=return_logprobs
|
||||||
)
|
)
|
||||||
@@ -158,12 +212,18 @@ class InferenceScheduler:
|
|||||||
t.status = TaskStatus.ABORTED
|
t.status = TaskStatus.ABORTED
|
||||||
aborted.append(t)
|
aborted.append(t)
|
||||||
|
|
||||||
if decoded:
|
for backend, group in self._task_backend_groups(decoded):
|
||||||
with self._metrics.record([t.task_id for t in decoded], "decode"):
|
backend_context = (
|
||||||
step_out = self._executor.execute_decode(
|
attn_backend(backend) if backend is not None else nullcontext()
|
||||||
decoded, return_logprobs=return_logprobs
|
|
||||||
)
|
)
|
||||||
for t, out in zip(decoded, step_out):
|
with (
|
||||||
|
backend_context,
|
||||||
|
self._metrics.record([t.task_id for t in group], "decode"),
|
||||||
|
):
|
||||||
|
step_out = self._executor.execute_decode(
|
||||||
|
group, return_logprobs=return_logprobs
|
||||||
|
)
|
||||||
|
for t, out in zip(group, step_out):
|
||||||
t.output_ids.append(out[0] if return_logprobs else out)
|
t.output_ids.append(out[0] if return_logprobs else out)
|
||||||
t.output_tokens += 1
|
t.output_tokens += 1
|
||||||
t.advance_kv()
|
t.advance_kv()
|
||||||
@@ -174,6 +234,7 @@ class InferenceScheduler:
|
|||||||
def _run_generation_loop(self):
|
def _run_generation_loop(self):
|
||||||
stop_ids = self._task_mgr.tokenizer.stop_ids
|
stop_ids = self._task_mgr.tokenizer.stop_ids
|
||||||
try:
|
try:
|
||||||
|
with self._backend_context():
|
||||||
while not self._stop_event.is_set():
|
while not self._stop_event.is_set():
|
||||||
finished = self._task_mgr.remove_finished_tasks(stop_ids)
|
finished = self._task_mgr.remove_finished_tasks(stop_ids)
|
||||||
for task in finished:
|
for task in finished:
|
||||||
@@ -192,7 +253,9 @@ class InferenceScheduler:
|
|||||||
candidates = self._task_mgr.pull_candidates(available)
|
candidates = self._task_mgr.pull_candidates(available)
|
||||||
failed = []
|
failed = []
|
||||||
for task in candidates:
|
for task in candidates:
|
||||||
if self._task_cache.task_alloc(task.task_id, task.prompt_ids):
|
if self._task_cache.task_alloc(
|
||||||
|
task.task_id, task.prompt_ids
|
||||||
|
):
|
||||||
self._task_mgr.activate(task)
|
self._task_mgr.activate(task)
|
||||||
else:
|
else:
|
||||||
failed.append(task)
|
failed.append(task)
|
||||||
@@ -286,6 +349,7 @@ class InferenceScheduler:
|
|||||||
"""
|
"""
|
||||||
stop_ids = self._task_mgr.tokenizer.stop_ids
|
stop_ids = self._task_mgr.tokenizer.stop_ids
|
||||||
seq_cap = self.max_seq_len
|
seq_cap = self.max_seq_len
|
||||||
|
request_backend = get_backend(use_default=False)
|
||||||
|
|
||||||
tasks: List[Task] = []
|
tasks: List[Task] = []
|
||||||
for ids in prompt_ids_list:
|
for ids in prompt_ids_list:
|
||||||
@@ -309,6 +373,7 @@ class InferenceScheduler:
|
|||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
frequency_penalty=frequency_penalty,
|
frequency_penalty=frequency_penalty,
|
||||||
rep_window=rep_window,
|
rep_window=rep_window,
|
||||||
|
backend=request_backend,
|
||||||
)
|
)
|
||||||
if not self._task_cache.task_alloc(task.task_id, task.prompt_ids):
|
if not self._task_cache.task_alloc(task.task_id, task.prompt_ids):
|
||||||
tasks.append(None)
|
tasks.append(None)
|
||||||
@@ -320,6 +385,7 @@ class InferenceScheduler:
|
|||||||
try:
|
try:
|
||||||
live = [t for t in tasks if t is not None]
|
live = [t for t in tasks if t is not None]
|
||||||
|
|
||||||
|
with self._backend_context():
|
||||||
while live:
|
while live:
|
||||||
decoded, _ = self._step(live, return_logprobs=return_logprobs)
|
decoded, _ = self._step(live, return_logprobs=return_logprobs)
|
||||||
live = [t for t in decoded if not t.is_finished(stop_ids)]
|
live = [t for t in decoded if not t.is_finished(stop_ids)]
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Callable, Deque, Dict, List, Optional
|
from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, List, Optional
|
||||||
|
|
||||||
from tokenizers.decoders import DecodeStream
|
from tokenizers.decoders import DecodeStream
|
||||||
|
|
||||||
from astrai.inference.metrics import MetricsCollector
|
from astrai.inference.metrics import MetricsCollector
|
||||||
from astrai.tokenize.tokenizer import AutoTokenizer
|
from astrai.tokenize.tokenizer import AutoTokenizer
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from astrai.extension import AttentionBackend
|
||||||
|
|
||||||
STOP = object()
|
STOP = object()
|
||||||
|
|
||||||
|
|
||||||
@@ -62,6 +65,7 @@ class Task:
|
|||||||
top_k: int = 50,
|
top_k: int = 50,
|
||||||
frequency_penalty: float = 0.0,
|
frequency_penalty: float = 0.0,
|
||||||
rep_window: int = 64,
|
rep_window: int = 64,
|
||||||
|
backend: Optional["AttentionBackend"] = None,
|
||||||
):
|
):
|
||||||
self.task_id = task_id
|
self.task_id = task_id
|
||||||
self.prompt_ids = prompt_ids
|
self.prompt_ids = prompt_ids
|
||||||
@@ -71,6 +75,7 @@ class Task:
|
|||||||
self.top_k = top_k
|
self.top_k = top_k
|
||||||
self.frequency_penalty = frequency_penalty
|
self.frequency_penalty = frequency_penalty
|
||||||
self.rep_window = rep_window
|
self.rep_window = rep_window
|
||||||
|
self.backend = backend
|
||||||
|
|
||||||
self.status = TaskStatus.PENDING
|
self.status = TaskStatus.PENDING
|
||||||
self.output_ids: List[int] = []
|
self.output_ids: List[int] = []
|
||||||
@@ -152,6 +157,7 @@ class TaskManager:
|
|||||||
top_k: int = 50,
|
top_k: int = 50,
|
||||||
frequency_penalty: float = 0.0,
|
frequency_penalty: float = 0.0,
|
||||||
rep_window: int = 64,
|
rep_window: int = 64,
|
||||||
|
backend: Optional["AttentionBackend"] = None,
|
||||||
stream_callback: Optional[Callable[[str], None]] = None,
|
stream_callback: Optional[Callable[[str], None]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
task_id = f"task_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
task_id = f"task_{int(time.time())}_{uuid.uuid4().hex[:8]}"
|
||||||
@@ -173,6 +179,7 @@ class TaskManager:
|
|||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
frequency_penalty=frequency_penalty,
|
frequency_penalty=frequency_penalty,
|
||||||
rep_window=rep_window,
|
rep_window=rep_window,
|
||||||
|
backend=backend,
|
||||||
)
|
)
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
|||||||
@@ -259,20 +259,19 @@ class GenerationBenchmark:
|
|||||||
max_seq_len=max_seq_len,
|
max_seq_len=max_seq_len,
|
||||||
cache=pool,
|
cache=pool,
|
||||||
enable_cuda_graph=self.cuda_graph,
|
enable_cuda_graph=self.cuda_graph,
|
||||||
|
backend=self.backend,
|
||||||
)
|
)
|
||||||
prompts = [prompt] * batch_size
|
prompts = [prompt] * batch_size
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Capture graphs and populate the allocator before timing. The
|
# Capture graphs and populate the allocator before timing. The
|
||||||
# first request also includes model/scheduler startup effects.
|
# first request also includes model/scheduler startup effects.
|
||||||
with attn_backend(self.backend):
|
|
||||||
engine.generate(prompts, max_tokens=gen_length, temperature=0.0)
|
engine.generate(prompts, max_tokens=gen_length, temperature=0.0)
|
||||||
if self.device.startswith("cuda"):
|
if self.device.startswith("cuda"):
|
||||||
torch.cuda.synchronize()
|
torch.cuda.synchronize()
|
||||||
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
for _ in range(num_trials):
|
for _ in range(num_trials):
|
||||||
with attn_backend(self.backend):
|
|
||||||
engine.generate(prompts, max_tokens=gen_length, temperature=0.0)
|
engine.generate(prompts, max_tokens=gen_length, temperature=0.0)
|
||||||
if self.device.startswith("cuda"):
|
if self.device.startswith("cuda"):
|
||||||
torch.cuda.synchronize()
|
torch.cuda.synchronize()
|
||||||
@@ -291,7 +290,8 @@ class GenerationBenchmark:
|
|||||||
"benchmark_type": "engine_decode",
|
"benchmark_type": "engine_decode",
|
||||||
"num_trials": num_trials,
|
"num_trials": num_trials,
|
||||||
"prompt_length": prompt_tokens,
|
"prompt_length": prompt_tokens,
|
||||||
"engine": True,
|
"backend": engine.backend_name,
|
||||||
|
"cuda_graph": engine.cuda_graph_enabled,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ def test_attn_backend_context_with_registered_name():
|
|||||||
assert get_backend() is default
|
assert get_backend() is default
|
||||||
|
|
||||||
|
|
||||||
|
def test_backend_can_read_only_context_selection():
|
||||||
|
assert get_backend(use_default=False) is None
|
||||||
|
with attn_backend("cuda") as backend:
|
||||||
|
assert get_backend(use_default=False) is backend
|
||||||
|
assert get_backend(use_default=False) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_environment_backend_overrides_context(monkeypatch):
|
||||||
|
monkeypatch.setenv("ASTR_BACKEND", "torch_native")
|
||||||
|
with attn_backend("cuda"):
|
||||||
|
assert type(get_backend()).__name__ == "TorchNativeBackend"
|
||||||
|
assert type(get_backend(use_default=False)).__name__ == "TorchNativeBackend"
|
||||||
|
|
||||||
|
|
||||||
def test_attention_backend_factory_lists_builtin_backends():
|
def test_attention_backend_factory_lists_builtin_backends():
|
||||||
assert AttentionBackendFactory.list_registered() == [
|
assert AttentionBackendFactory.list_registered() == [
|
||||||
"cuda",
|
"cuda",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import threading
|
import threading
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from astrai.extension import TorchNativeBackend, attn_backend
|
||||||
from astrai.inference import STOP
|
from astrai.inference import STOP
|
||||||
from astrai.inference.engine import GenerateResult, InferenceEngine
|
from astrai.inference.engine import GenerateResult, InferenceEngine
|
||||||
|
|
||||||
@@ -199,3 +200,40 @@ def test_engine_generate_zero_max_tokens_stream_is_empty():
|
|||||||
eng = InferenceEngine(mock_model, mock_tokenizer, max_batch_size=1)
|
eng = InferenceEngine(mock_model, mock_tokenizer, max_batch_size=1)
|
||||||
assert list(eng.generate("hello", stream=True, max_tokens=0)) == []
|
assert list(eng.generate("hello", stream=True, max_tokens=0)) == []
|
||||||
instance.add_task.assert_not_called()
|
instance.add_task.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_engine_passes_backend_to_scheduler():
|
||||||
|
mock_model = MagicMock()
|
||||||
|
mock_tokenizer = MagicMock()
|
||||||
|
|
||||||
|
with patch("astrai.inference.engine.InferenceScheduler") as MockSched:
|
||||||
|
InferenceEngine(
|
||||||
|
mock_model,
|
||||||
|
mock_tokenizer,
|
||||||
|
max_batch_size=1,
|
||||||
|
backend="torch_native",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert MockSched.call_args.kwargs["backend"] == "torch_native"
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_captures_calling_backend_context():
|
||||||
|
mock_model = MagicMock()
|
||||||
|
mock_tokenizer = MagicMock()
|
||||||
|
captured = []
|
||||||
|
|
||||||
|
with patch("astrai.inference.engine.InferenceScheduler") as MockSched:
|
||||||
|
instance = MockSched.return_value
|
||||||
|
|
||||||
|
def fake_add(prompt, **kwargs):
|
||||||
|
captured.append(kwargs["backend"])
|
||||||
|
kwargs["stream_callback"](STOP)
|
||||||
|
return "task"
|
||||||
|
|
||||||
|
instance.add_task.side_effect = fake_add
|
||||||
|
engine = InferenceEngine(mock_model, mock_tokenizer)
|
||||||
|
with attn_backend("torch_native"):
|
||||||
|
assert engine.generate("hello") == ""
|
||||||
|
|
||||||
|
assert len(captured) == 1
|
||||||
|
assert isinstance(captured[0], TorchNativeBackend)
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ from unittest.mock import MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
from astrai.extension import CudaBackend, TorchNativeBackend, get_backend
|
||||||
from astrai.inference import InferenceScheduler
|
from astrai.inference import InferenceScheduler
|
||||||
|
from astrai.inference.metrics import MetricsCollector
|
||||||
from astrai.inference.runtime.executor import DecodeSteadyState, Executor
|
from astrai.inference.runtime.executor import DecodeSteadyState, Executor
|
||||||
from astrai.inference.task import Task
|
from astrai.inference.task import Task
|
||||||
from astrai.model.transformer import AutoRegressiveLM
|
from astrai.model.transformer import AutoRegressiveLM
|
||||||
@@ -76,6 +78,69 @@ def test_scheduler_concurrent_add_task(mock_model_and_tokenizer):
|
|||||||
assert len(results["task_ids"]) == 50
|
assert len(results["task_ids"]) == 50
|
||||||
|
|
||||||
|
|
||||||
|
def test_generation_loop_activates_backend_in_worker_thread():
|
||||||
|
scheduler = object.__new__(InferenceScheduler)
|
||||||
|
scheduler._backend = TorchNativeBackend()
|
||||||
|
scheduler._stop_event = threading.Event()
|
||||||
|
scheduler._task_cache = MagicMock()
|
||||||
|
|
||||||
|
observed = []
|
||||||
|
task_mgr = MagicMock()
|
||||||
|
task_mgr.tokenizer.stop_ids = [0]
|
||||||
|
task_mgr.remove_finished_tasks.return_value = []
|
||||||
|
task_mgr.get_active_tasks.return_value = []
|
||||||
|
task_mgr.max_batch_size = 1
|
||||||
|
task_mgr.pull_candidates.return_value = []
|
||||||
|
task_mgr.has_work.return_value = False
|
||||||
|
|
||||||
|
def observe_backend(*args, **kwargs):
|
||||||
|
observed.append(type(get_backend()))
|
||||||
|
scheduler._stop_event.set()
|
||||||
|
|
||||||
|
task_mgr.wait_for_tasks.side_effect = observe_backend
|
||||||
|
scheduler._task_mgr = task_mgr
|
||||||
|
|
||||||
|
thread = threading.Thread(target=scheduler._run_generation_loop)
|
||||||
|
thread.start()
|
||||||
|
thread.join(timeout=5)
|
||||||
|
|
||||||
|
assert not thread.is_alive()
|
||||||
|
assert observed == [TorchNativeBackend]
|
||||||
|
|
||||||
|
|
||||||
|
def test_step_splits_decode_batch_by_request_backend():
|
||||||
|
scheduler = object.__new__(InferenceScheduler)
|
||||||
|
scheduler._task_cache = MagicMock()
|
||||||
|
scheduler._task_cache.task_extend.return_value = True
|
||||||
|
scheduler._metrics = MetricsCollector()
|
||||||
|
scheduler._executor = MagicMock()
|
||||||
|
|
||||||
|
observed = []
|
||||||
|
|
||||||
|
def execute(tasks, **kwargs):
|
||||||
|
observed.append((type(get_backend()), [task.task_id for task in tasks]))
|
||||||
|
return [1] * len(tasks)
|
||||||
|
|
||||||
|
scheduler._executor.execute_decode.side_effect = execute
|
||||||
|
|
||||||
|
torch_task = Task("torch", [1], backend=TorchNativeBackend())
|
||||||
|
cuda_task = Task("cuda", [1], backend=CudaBackend())
|
||||||
|
for task in (torch_task, cuda_task):
|
||||||
|
task.input_tokens = 1
|
||||||
|
task.output_ids = [1]
|
||||||
|
task.mark_prefill_done()
|
||||||
|
scheduler._metrics.register(task.task_id)
|
||||||
|
|
||||||
|
produced, aborted = scheduler._step([torch_task, cuda_task])
|
||||||
|
|
||||||
|
assert aborted == []
|
||||||
|
assert produced == [torch_task, cuda_task]
|
||||||
|
assert observed == [
|
||||||
|
(TorchNativeBackend, ["torch"]),
|
||||||
|
(CudaBackend, ["cuda"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_scheduler_concurrent_add_remove_task(mock_model_and_tokenizer):
|
def test_scheduler_concurrent_add_remove_task(mock_model_and_tokenizer):
|
||||||
"""Test concurrent add and remove task operations."""
|
"""Test concurrent add and remove task operations."""
|
||||||
mock_model, mock_tokenizer = mock_model_and_tokenizer
|
mock_model, mock_tokenizer = mock_model_and_tokenizer
|
||||||
|
|||||||
Reference in New Issue
Block a user