Spaces:
Running on Zero
Running on Zero
| """A bounded, single-worker asynchronous inference queue.""" | |
| from __future__ import annotations | |
| import asyncio | |
| from contextvars import Context, copy_context | |
| from dataclasses import dataclass | |
| from typing import Any, Callable, Generic, TypeVar | |
| from core.errors import GatewayError, QueueCapacityError | |
| T = TypeVar("T") | |
| class QueueJob(Generic[T]): | |
| request_id: str | |
| label: str | |
| operation: Callable[[], T] | |
| context: Context | |
| future: asyncio.Future[T] | |
| started: asyncio.Future[None] | |
| cancelled: bool = False | |
| class InferenceQueue: | |
| """Serialize all accelerator work through one worker.""" | |
| def __init__(self, capacity: int, timeout_seconds: int) -> None: | |
| self.capacity = capacity | |
| self.timeout_seconds = timeout_seconds | |
| self._queue: asyncio.Queue[QueueJob[Any] | None] = asyncio.Queue(maxsize=capacity) | |
| self._worker: asyncio.Task[None] | None = None | |
| self._active = False | |
| def depth(self) -> int: | |
| return self._queue.qsize() | |
| def active(self) -> bool: | |
| return self._active | |
| async def start(self) -> None: | |
| if self._worker is None or self._worker.done(): | |
| self._worker = asyncio.create_task(self._work(), name="inference-worker") | |
| async def stop(self) -> None: | |
| if self._worker is None: | |
| return | |
| await self._queue.put(None) | |
| await self._worker | |
| self._worker = None | |
| async def submit(self, request_id: str, label: str, operation: Callable[[], T]) -> T: | |
| """Add a synchronous operation without blocking the event loop.""" | |
| await self.start() | |
| loop = asyncio.get_running_loop() | |
| future: asyncio.Future[T] = loop.create_future() | |
| started: asyncio.Future[None] = loop.create_future() | |
| job = QueueJob( | |
| request_id=request_id, | |
| label=label, | |
| operation=operation, | |
| context=copy_context(), | |
| future=future, | |
| started=started, | |
| ) | |
| try: | |
| self._queue.put_nowait(job) | |
| except asyncio.QueueFull as exc: | |
| raise QueueCapacityError() from exc | |
| try: | |
| await asyncio.wait_for(asyncio.shield(started), timeout=self.timeout_seconds) | |
| except asyncio.CancelledError: | |
| if started.done(): | |
| future.add_done_callback(self._consume_late_result) | |
| else: | |
| job.cancelled = True | |
| future.cancel() | |
| raise | |
| except TimeoutError as exc: | |
| if started.done(): | |
| return await asyncio.shield(future) | |
| job.cancelled = True | |
| future.cancel() | |
| raise GatewayError( | |
| "Inference timed out waiting in the queue", | |
| status_code=504, | |
| code="queue_timeout", | |
| ) from exc | |
| return await asyncio.shield(future) | |
| def _consume_late_result(future: asyncio.Future[Any]) -> None: | |
| if not future.cancelled(): | |
| try: | |
| future.exception() | |
| except asyncio.CancelledError: | |
| pass | |
| async def _work(self) -> None: | |
| while True: | |
| job = await self._queue.get() | |
| if job is None: | |
| self._queue.task_done() | |
| break | |
| if job.cancelled: | |
| self._queue.task_done() | |
| continue | |
| self._active = True | |
| if not job.started.done(): | |
| job.started.set_result(None) | |
| try: | |
| result = await asyncio.to_thread(job.context.run, job.operation) | |
| except Exception as exc: | |
| if not job.future.done(): | |
| job.future.set_exception(exc) | |
| else: | |
| if not job.future.done(): | |
| job.future.set_result(result) | |
| finally: | |
| self._active = False | |
| self._queue.task_done() | |