cv-multitask-pipeline / src /api /batch_queue.py
OmUniyal
feat: phase 4 — FastAPI inference server (schemas, model manager, router, main)
54d3696
Raw
History Blame Contribute Delete
3.8 kB
import asyncio
import time
from typing import List, Tuple, Optional
from dataclasses import dataclass, field
@dataclass
class InferenceRequest:
"""
A single inference request waiting in the queue.
future: asyncio.Future the HTTP handler is awaiting.
image_tensor: preprocessed image tensor ready for model.
request_id: unique ID for logging.
enqueue_time: when the request entered the queue.
"""
future: asyncio.Future
image_tensor: object # torch.Tensor
request_id: str
enqueue_time: float = field(default_factory=time.time)
class BatchQueue:
"""
Async batch accumulator for inference requests.
How it works:
1. HTTP handler preprocesses image, creates a Future, adds to queue
2. Handler awaits the Future (suspends, doesn't block the server)
3. Background worker wakes every max_wait_ms OR when batch is full
4. Worker collects pending requests, runs one batched forward pass
5. Worker sets results on each Future — all handlers wake up simultaneously
This means one model forward pass can serve multiple users at once.
"""
def __init__(
self,
max_batch_size: int = 8,
max_wait_ms: float = 50.0,
):
self.max_batch_size = max_batch_size
self.max_wait_ms = max_wait_ms
self._queue: List[InferenceRequest] = []
self._lock = asyncio.Lock()
self._batch_event = asyncio.Event()
async def enqueue(
self,
image_tensor,
request_id: str,
):
"""
Add an image to the queue and wait for its result.
The HTTP handler calls this and awaits — it suspends here
until the batch worker processes its request.
"""
loop = asyncio.get_event_loop()
future = loop.create_future()
request = InferenceRequest(
future=future,
image_tensor=image_tensor,
request_id=request_id,
)
async with self._lock:
self._queue.append(request)
# signal the worker that there's work to do
self._batch_event.set()
# suspend here until worker sets the result
return await future
async def get_batch(self) -> List[InferenceRequest]:
"""
Wait until we have requests, then return a batch.
Wakes up when:
- batch is full (max_batch_size reached), OR
- max_wait_ms timeout expires
Whichever comes first.
"""
# wait for at least one request
await self._batch_event.wait()
# wait up to max_wait_ms for more requests to accumulate
await asyncio.sleep(self.max_wait_ms / 1000.0)
async with self._lock:
# take up to max_batch_size requests
batch = self._queue[:self.max_batch_size]
self._queue = self._queue[self.max_batch_size:]
# reset event if queue is now empty
if not self._queue:
self._batch_event.clear()
return batch
def set_results(
self,
batch: List[InferenceRequest],
results: List,
):
"""
Set results on each request's Future.
This wakes up all the HTTP handlers that were awaiting.
"""
for request, result in zip(batch, results):
if not request.future.done():
request.future.set_result(result)
def set_error(
self,
batch: List[InferenceRequest],
error: Exception,
):
"""Set an exception on all requests in a failed batch."""
for request in batch:
if not request.future.done():
request.future.set_exception(error)
@property
def queue_size(self) -> int:
return len(self._queue)