Files
hao-ai-lab-FastVideo/v2/runtime/async_engine.py
T
SolitaryThinker 4e331eb7ff [refactor] v2: use absolute imports (v2.*) everywhere instead of relative
Mechanical conversion of every relative import under v2/ to an absolute v2.* path
(from .x / ..x / ...x -> from v2.<pkg>.x) so imports are unambiguous, grep-able, and
stable when code is copied/moved between entrypoints (VideoGenerator, CLI, server).

Surgical prefix-only rewrite: only the 'from <dots><module>' prefix changed — import
names, parentheses, multi-line formatting, comments, and ordering are byte-for-byte
preserved (no collapsing, no reorder, no unrelated reformatting).

- 471 imports across 125 files; v2/tests/ was already absolute (untouched).
- Validated: all 125 files compile, every 'from v2.* import' target resolves to a real
  module/package, zero relative imports remain (full sweep), CPU mini suite green
  (202 passed, 2 skipped).
2026-07-04 17:15:34 +00:00

218 lines
9.5 KiB
Python

"""AsyncEngine — the serving runtime (design_v3 §6; plan M3).
A real request queue, lifecycle state machine, live streaming, and step-level concurrency over the
same step scheduler. Each request runs as an asyncio task that ticks its runner one step at a time
and yields the event loop (`await asyncio.sleep(0)`), so concurrent requests interleave at step
granularity — and a per-request stall backs off instead of busy-spinning. Drives BOTH inline
``ProgramRunner`` and ``DisaggregatedRunner`` (capacity-aware pool dispatch) through one driver.
Cancellation is common-path (design_v3 §6.4): ``cancel(request_id)`` trips the runner's CancelScope,
which raises at the next step boundary and delivers partial artifacts + a structured ``cancelled``.
"""
from __future__ import annotations
import asyncio
from typing import Any, AsyncIterator
from v2.request.artifacts import Output
from v2.request.cancel import Cancelled
from v2.request.streams import OmniEvent
from v2.runtime.disaggregated import DisaggregatedRunner
from v2.runtime.engine import Engine
_SENTINEL = object()
def _safe_put(q: "asyncio.Queue", item: Any) -> None:
"""Non-blocking put on an unbounded queue — safe to call from finally/cancellation paths."""
try:
q.put_nowait(item)
except Exception:
pass
class RequestState:
WAITING = "waiting"
RUNNING = "running"
COMPLETED = "completed"
CANCELLED = "cancelled"
FAILED = "failed"
class AsyncEngine:
def __init__(self, engine: Engine | None = None, *, max_stall_ticks: int = 200_000,
max_history: int = 512):
self.engine = engine if engine is not None else Engine()
self._disagg: dict[str, tuple] = {} # model_id -> (PoolSet, Program)
self._events: dict[str, asyncio.Queue] = {}
self._states: dict[str, str] = {}
self._results: dict[str, Output] = {}
self._runners: dict[str, Any] = {}
self._order: list[str] = [] # submission order, for bounded eviction
self.max_stall_ticks = max_stall_ticks
self.max_history = max_history
def _evict(self) -> None:
"""Bound retained per-request state to the most recent ``max_history`` terminal requests."""
terminal = (RequestState.COMPLETED, RequestState.CANCELLED, RequestState.FAILED)
i = 0
while len(self._order) > self.max_history and i < len(self._order):
rid = self._order[i]
if self._states.get(rid) in terminal:
self._order.pop(i)
self._events.pop(rid, None)
self._states.pop(rid, None)
self._results.pop(rid, None)
self._runners.pop(rid, None)
else:
i += 1
# --- registration ------------------------------------------------------- #
def register(self, model_id: str, instance: Any, program: Any) -> None:
self.engine.register(model_id, instance, program)
def register_disaggregated(self, model_id: str, pools: Any, program: Any) -> None:
self._disagg[model_id] = (pools, program)
def register_workflow(self, workflow: Any) -> Any:
return self.engine.register_workflow(workflow)
def is_disaggregated(self, model_id: str) -> bool:
return model_id in self._disagg
def _make_runner(self, request: Any) -> Any:
if request.model_id in self._disagg:
pools, program = self._disagg[request.model_id]
return DisaggregatedRunner(pools, program, request,
observers=self.engine.observers, interceptors=self.engine.interceptors)
return self.engine._make_runner(request)
# --- lifecycle / control ------------------------------------------------ #
def state(self, request_id: str) -> str | None:
return self._states.get(request_id)
def cancel(self, request_id: str) -> bool:
runner = self._runners.get(request_id)
if runner is not None and getattr(runner, "cancel_scope", None) is not None:
runner.cancel_scope.cancel()
return True
return False
def result(self, request_id: str) -> Output | None:
return self._results.get(request_id)
@property
def in_flight(self) -> int:
return sum(1 for s in self._states.values() if s == RequestState.RUNNING)
@property
def queue_depth(self) -> int:
return sum(1 for s in self._states.values() if s == RequestState.WAITING)
def serves(self, model_id: str) -> bool:
return (model_id in self.engine._registry or model_id in self._disagg
or model_id in self.engine._workflows)
# --- the driver: one asyncio task per request --------------------------- #
async def _run(self, request: Any) -> None:
rid = request.request_id
evq = self._events[rid]
runner = None
stream_cursor = 0
stall = 0
try:
runner = self._make_runner(request)
self._runners[rid] = runner
self._states[rid] = RequestState.RUNNING
await evq.put(OmniEvent("request.start", rid, payload={"task": request.task.value}))
while not runner.done:
runner.cancel_scope.check() # common-path cancellation (step boundary)
p0 = runner._progress
runner.tick()
# surface any newly emitted media chunks as live events
events = runner.stream.events
while stream_cursor < len(events):
await evq.put(events[stream_cursor])
stream_cursor += 1
if not runner.done and runner._progress == p0:
stall += 1
if stall > self.max_stall_ticks:
raise RuntimeError(f"request {rid} stalled (state={getattr(runner,'state','?')})")
await asyncio.sleep(0.0005) # backpressure backoff (capacity wait)
else:
stall = 0
await asyncio.sleep(0) # cooperative yield → step interleaving
out = runner.output()
self._results[rid] = out
for name in out.artifacts:
await evq.put(OmniEvent("artifact.ready", rid, payload={"name": name}))
self._states[rid] = RequestState.COMPLETED
await evq.put(OmniEvent("request.complete", rid, payload={"metrics": dict(out.metrics)}))
except Cancelled:
self._states[rid] = RequestState.CANCELLED
if runner is not None and hasattr(runner, "output"):
try:
partial = runner.output()
partial.error = "cancelled"
self._results[rid] = partial
except Exception:
pass
_safe_put(evq, OmniEvent("request.cancelled", rid))
except asyncio.CancelledError: # driver task cancelled (e.g. client gone)
self._states[rid] = RequestState.CANCELLED
raise
except Exception as e: # noqa: BLE001 — request-fatal isolation: one request's error
self._states[rid] = RequestState.FAILED
self._results[rid] = Output(request_id=rid, error=str(e))
_safe_put(evq, OmniEvent("request.failed", rid, payload={"error": str(e)}))
finally:
# release any pool this request occupied — a cancel/error mid-loop must NOT leak capacity
if runner is not None and hasattr(runner, "close"):
try:
runner.close()
except Exception:
pass
_safe_put(evq, _SENTINEL)
self._evict()
async def submit(self, request: Any) -> AsyncIterator[OmniEvent]:
"""Submit a request and stream its events live (request.start → media.chunk* → artifact.ready*
→ request.complete | request.cancelled | request.failed). Cancels the driver if the consumer
abandons the stream (e.g. client disconnect), so no orphaned compute keeps running."""
rid = request.request_id
if rid in self._events and self._states.get(rid) in (RequestState.WAITING, RequestState.RUNNING):
raise ValueError(f"request_id {rid!r} is already in flight")
self._events[rid] = asyncio.Queue()
self._states[rid] = RequestState.WAITING
self._order.append(rid)
task = asyncio.create_task(self._run(request))
evq = self._events[rid]
try:
while True:
ev = await evq.get()
if ev is _SENTINEL:
break
yield ev
finally:
if not task.done():
task.cancel()
try:
await task
except (asyncio.CancelledError, Exception): # noqa: BLE001
pass
async def generate(self, request: Any) -> Output:
"""Non-streaming: drive to completion, return the Output (the offline path over the queue)."""
async for _ in self.submit(request):
pass
return self._results[request.request_id]
async def generate_many(self, requests: list[Any]) -> dict[str, Output]:
"""Run requests concurrently (they interleave at step granularity through the event loop)."""
await asyncio.gather(*(self._drain(r) for r in requests))
return {r.request_id: self._results[r.request_id] for r in requests}
async def _drain(self, request: Any) -> None:
async for _ in self.submit(request):
pass