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:
2026-08-09 13:32:40 +08:00
parent c1d05ae11d
commit 47b3ed4e44
9 changed files with 334 additions and 110 deletions
+65
View File
@@ -7,7 +7,9 @@ from unittest.mock import MagicMock, patch
import pytest
import torch
from astrai.extension import CudaBackend, TorchNativeBackend, get_backend
from astrai.inference import InferenceScheduler
from astrai.inference.metrics import MetricsCollector
from astrai.inference.runtime.executor import DecodeSteadyState, Executor
from astrai.inference.task import Task
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
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):
"""Test concurrent add and remove task operations."""
mock_model, mock_tokenizer = mock_model_and_tokenizer