---
title: gpu_pool
---

<div class="python-reference">
<a id="fastvideo.entrypoints.streaming.gpu_pool-classes"></a>
<a id="fastvideo.entrypoints.streaming.gpu_pool-functions"></a>
<a id="fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool-functions"></a>
<a id="fastvideo.entrypoints.streaming.gpu_pool"></a>
<p>GPU pool manager for the streaming server.</p>
<p>Replaces the single-generator path in PR 7.5 with a typed pool
abstraction. Three implementations ship here:</p>
<ul>
<li>:class:<code>InProcessGpuPool</code> — one in-process <code>VideoGenerator</code>; used
  by tests and single-GPU dev deployments.</li>
<li>:class:<code>SubprocessGpuPool</code> — one <code>multiprocessing.Process</code> per
  GPU, each running :func:<code>worker_main</code> against a <code>GeneratorConfig</code>.
  Jobs are dispatched via <code>multiprocessing.Queue</code>.</li>
<li>:class:<code>GpuPool</code> (abstract) — the interface both use.</li>
</ul>
<p>Session-to-GPU binding lives in the pool so continuation state stays
on the GPU that generated the previous segment (matching the internal
<code>gpu_pool.py</code>'s per-GPU cache behavior). Cross-GPU handoff is
supported via :class:<code>SessionStore</code> snapshot + hydrate, which
serializes the state before the migration and rehydrates it on the
new worker.</p>
<p>Typed config: workers start from a :class:<code>GeneratorConfig</code> (no flat
LTX-2 kwargs), satisfying the PR 6 + PR 7 contracts that the public
surface doesn't reintroduce the legacy kwarg bag.</p>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.GeneratorConfig"><code>fastvideo.entrypoints.streaming.gpu_pool.GeneratorConfig</code></h2>
<pre class="doc-signature"><code>GeneratorConfig(model_path: str, revision: str | None = None, trust_remote_code: bool = False, engine: EngineConfig = EngineConfig(), pipeline: PipelineSelection = PipelineSelection())</code></pre>


<h2 id="fastvideo.entrypoints.streaming.gpu_pool.GenerationRequest"><code>fastvideo.entrypoints.streaming.gpu_pool.GenerationRequest</code></h2>
<pre class="doc-signature"><code>GenerationRequest(prompt: str | list&#91;str&#93; | None = None, negative_prompt: str | None = None, inputs: InputConfig = InputConfig(), sampling: SamplingConfig = SamplingConfig(), runtime: RequestRuntimeConfig = RequestRuntimeConfig(), output: OutputConfig = OutputConfig(), stage_overrides: dict&#91;str, Any&#93; = dict(), state: ContinuationState | None = None, plan: GenerationPlan | None = None, extensions: dict&#91;str, Any&#93; = dict())</code></pre>


<h2 id="fastvideo.entrypoints.streaming.gpu_pool.GpuPoolConfig"><code>fastvideo.entrypoints.streaming.gpu_pool.GpuPoolConfig</code></h2>
<pre class="doc-signature"><code>GpuPoolConfig(num_workers: int | None = None, enable_audio_reencode: bool = True, conditioning_num_frames: int = 9, conditioning_end_offset: int = 0)</code></pre>


<h2 id="fastvideo.entrypoints.streaming.gpu_pool.WarmupConfig"><code>fastvideo.entrypoints.streaming.gpu_pool.WarmupConfig</code></h2>
<pre class="doc-signature"><code>WarmupConfig(enabled: bool = True, prompt: str = &#x27;A cinematic drone shot over coastal cliffs at sunrise, golden light, gentle ocean waves, ultra detailed&#x27;, timeout_seconds: int = 2400)</code></pre>


<h2 id="fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore"><code>fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore</code></h2>
<pre class="doc-signature"><code>InMemorySessionStore()</code></pre>
<p>Thread-safe in-memory :class:<code>SessionStore</code>.</p>
<p>Default implementation used by single-process deployments; a future
Redis-backed store can be dropped in without changes to the server.</p>
<p>No eviction / TTL / bounded capacity — sessions only leave via
:meth:<code>drop</code>. The live streaming server (PR 7.5+) is responsible
for bounding growth and for dropping any :class:<code>BlobStore</code> blobs
referenced by a state when that state is replaced or a session
ends; this class does not know about blobs.</p>

<h3 id="fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore.store"><code>fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore.store</code></h3>
<pre class="doc-signature"><code>store(session_id: str, state: ContinuationState) -&gt; None</code></pre>

<h3 id="fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore.snapshot"><code>fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore.snapshot</code></h3>
<pre class="doc-signature"><code>snapshot(session_id: str) -&gt; ContinuationState | None</code></pre>

<h3 id="fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore.hydrate"><code>fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore.hydrate</code></h3>
<pre class="doc-signature"><code>hydrate(state: ContinuationState, *, session_id: str | None = None) -&gt; str</code></pre>

<h3 id="fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore.drop"><code>fastvideo.entrypoints.streaming.gpu_pool.InMemorySessionStore.drop</code></h3>
<pre class="doc-signature"><code>drop(session_id: str) -&gt; None</code></pre>

<h2 id="fastvideo.entrypoints.streaming.gpu_pool.SessionStore"><code>fastvideo.entrypoints.streaming.gpu_pool.SessionStore</code></h2>
<pre class="doc-signature"><code>SessionStore()</code></pre>
<p>Keyed store for per-session continuation state.</p>
<p>Implementations own the session-id → state mapping. The streaming
server calls :meth:<code>store</code> after each segment and :meth:<code>snapshot</code>
when a client explicitly asks for an exportable state handle.</p>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SessionStore.store"><code>fastvideo.entrypoints.streaming.gpu_pool.SessionStore.store</code></h3>
<pre class="doc-signature"><code>store(session_id: str, state: ContinuationState) -&gt; None</code></pre>
<p>Persist <code>state</code> for <code>session_id</code>, replacing any prior value.</p>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SessionStore.snapshot"><code>fastvideo.entrypoints.streaming.gpu_pool.SessionStore.snapshot</code></h3>
<pre class="doc-signature"><code>snapshot(session_id: str) -&gt; ContinuationState | None</code></pre>
<p>Return the current state for <code>session_id</code> (or <code>None</code>).</p>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SessionStore.hydrate"><code>fastvideo.entrypoints.streaming.gpu_pool.SessionStore.hydrate</code></h3>
<pre class="doc-signature"><code>hydrate(state: ContinuationState, *, session_id: str | None = None) -&gt; str</code></pre>
<p>Install <code>state</code> as the starting point for a session.</p>
<p>When <code>session_id</code> is <code>None</code> the store allocates a fresh id
(UUID4); when provided the store uses it verbatim, overwriting
any prior state at that id.</p>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SessionStore.drop"><code>fastvideo.entrypoints.streaming.gpu_pool.SessionStore.drop</code></h3>
<pre class="doc-signature"><code>drop(session_id: str) -&gt; None</code></pre>
<p>Forget a session. Missing ids are a no-op.</p>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.worker_main"><code>fastvideo.entrypoints.streaming.gpu_pool.worker_main</code></h2>
<pre class="doc-signature"><code>worker_main(*, gpu_id: int, worker_id: str, generator_config: GeneratorConfig, warmup_config: WarmupConfig, job_queue: mp.Queue, result_queue: mp.Queue, shutdown_event: Any) -&gt; None</code></pre>
<p>Per-worker subprocess entry.</p>
<p>Runs inside the child spawned by <code>SubprocessGpuPool</code>. Blocking
<code>VideoGenerator</code> construction + generation happens here, not in
the parent's event loop.</p>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.init_logger"><code>fastvideo.entrypoints.streaming.gpu_pool.init_logger</code></h2>
<pre class="doc-signature"><code>init_logger(name: str) -&gt; _FastvideoLogger</code></pre>
<p>The main purpose of this function is to ensure that loggers are
retrieved in such a way that we can be sure the root fastvideo logger has
already been configured.</p>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.PoolAssignment"><code>fastvideo.entrypoints.streaming.gpu_pool.PoolAssignment</code></h2>
<pre class="doc-signature"><code>PoolAssignment(gpu_id: int, worker_id: str, pinned_at: float = time.monotonic())</code></pre>
<p>The worker a session is currently bound to.</p>

<details><summary>Source</summary><pre><code>@dataclass
class PoolAssignment:
    &quot;&quot;&quot;The worker a session is currently bound to.&quot;&quot;&quot;

    gpu_id: int
    worker_id: str
    pinned_at: float = field(default_factory=time.monotonic)</code></pre></details>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.GpuPool"><code>fastvideo.entrypoints.streaming.gpu_pool.GpuPool</code></h2>
<pre class="doc-signature"><code>GpuPool()</code></pre>
<p>Abstract GPU pool.</p>
<p><code>acquire</code> binds a session to a worker and holds that binding
across segments so continuation state can stay hot. <code>run</code> submits
a single <code>GenerationRequest</code> for a bound session.</p>
<p>Acquire / release are independent of run — a session can run many
segments on one acquired worker, and must release on disconnect.</p>
<details><summary>Source</summary><pre><code>class GpuPool(ABC):
    &quot;&quot;&quot;Abstract GPU pool.

    ``acquire`` binds a session to a worker and holds that binding
    across segments so continuation state can stay hot. ``run`` submits
    a single ``GenerationRequest`` for a bound session.

    Acquire / release are independent of run — a session can run many
    segments on one acquired worker, and must release on disconnect.
    &quot;&quot;&quot;

    @abstractmethod
    async def acquire(
        self,
        session_id: str,
        *,
        timeout: float | None = None,
    ) -&gt; PoolAssignment:
        ...

    @abstractmethod
    async def run(
        self,
        session_id: str,
        request: GenerationRequest,
    ) -&gt; Any:
        ...

    @abstractmethod
    async def release(self, session_id: str) -&gt; None:
        ...

    @abstractmethod
    async def shutdown(self) -&gt; None:
        ...

    @abstractmethod
    def health(self) -&gt; PoolHealth:
        ...</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.GpuPool.acquire"><code>fastvideo.entrypoints.streaming.gpu_pool.GpuPool.acquire</code></h3>
<pre class="doc-signature"><code>acquire(session_id: str, *, timeout: float | None = None) -&gt; PoolAssignment</code></pre>

<details><summary>Source</summary><pre><code>@abstractmethod
async def acquire(
    self,
    session_id: str,
    *,
    timeout: float | None = None,
) -&gt; PoolAssignment:
    ...</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.GpuPool.run"><code>fastvideo.entrypoints.streaming.gpu_pool.GpuPool.run</code></h3>
<pre class="doc-signature"><code>run(session_id: str, request: GenerationRequest) -&gt; Any</code></pre>

<details><summary>Source</summary><pre><code>@abstractmethod
async def run(
    self,
    session_id: str,
    request: GenerationRequest,
) -&gt; Any:
    ...</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.GpuPool.release"><code>fastvideo.entrypoints.streaming.gpu_pool.GpuPool.release</code></h3>
<pre class="doc-signature"><code>release(session_id: str) -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>@abstractmethod
async def release(self, session_id: str) -&gt; None:
    ...</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.GpuPool.shutdown"><code>fastvideo.entrypoints.streaming.gpu_pool.GpuPool.shutdown</code></h3>
<pre class="doc-signature"><code>shutdown() -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>@abstractmethod
async def shutdown(self) -&gt; None:
    ...</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.GpuPool.health"><code>fastvideo.entrypoints.streaming.gpu_pool.GpuPool.health</code></h3>
<pre class="doc-signature"><code>health() -&gt; PoolHealth</code></pre>

<details><summary>Source</summary><pre><code>@abstractmethod
def health(self) -&gt; PoolHealth:
    ...</code></pre></details>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.PoolHealth"><code>fastvideo.entrypoints.streaming.gpu_pool.PoolHealth</code></h2>
<pre class="doc-signature"><code>PoolHealth(total_workers: int, available_workers: int, active_sessions: int, queued_sessions: int = 0)</code></pre>


<details><summary>Source</summary><pre><code>@dataclass
class PoolHealth:
    total_workers: int
    available_workers: int
    active_sessions: int
    queued_sessions: int = 0</code></pre></details>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.PoolAcquireTimeout"><code>fastvideo.entrypoints.streaming.gpu_pool.PoolAcquireTimeout</code></h2>
<pre class="doc-signature"><code>PoolAcquireTimeout()</code></pre>
<p>Raised when <code>acquire</code> times out waiting for a free worker.</p>
<details><summary>Source</summary><pre><code>class PoolAcquireTimeout(RuntimeError):
    &quot;&quot;&quot;Raised when ``acquire`` times out waiting for a free worker.&quot;&quot;&quot;</code></pre></details>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool"><code>fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool</code></h2>
<pre class="doc-signature"><code>InProcessGpuPool(generator: _GeneratorLike, *, gpu_id: int = 0, session_store: SessionStore | None = None)</code></pre>
<p>Single-process pool backed by one :class:<code>_GeneratorLike</code>.</p>
<p>This is what PR 7.5's server uses by default; PR 7.6 adds the real
<code>SubprocessGpuPool</code> alternative but keeps this one for tests and
small deployments.</p>

<details><summary>Source</summary><pre><code>class InProcessGpuPool(GpuPool):
    &quot;&quot;&quot;Single-process pool backed by one :class:`_GeneratorLike`.

    This is what PR 7.5&#x27;s server uses by default; PR 7.6 adds the real
    ``SubprocessGpuPool`` alternative but keeps this one for tests and
    small deployments.
    &quot;&quot;&quot;

    def __init__(
        self,
        generator: _GeneratorLike,
        *,
        gpu_id: int = 0,
        session_store: SessionStore | None = None,
    ) -&gt; None:
        self._generator = generator
        self._gpu_id = gpu_id
        self._worker_id = f&quot;inproc-&#123;uuid.uuid4().hex&#91;:6&#93;&#125;&quot;
        self._session_store = session_store or InMemorySessionStore()
        self._active: dict&#91;str, PoolAssignment&#93; = &#123;&#125;
        self._lock = asyncio.Lock()
        self._gen_lock = asyncio.Lock()

    async def acquire(
        self,
        session_id: str,
        *,
        timeout: float | None = None,
    ) -&gt; PoolAssignment:
        async with self._lock:
            existing = self._active.get(session_id)
            if existing is not None:
                return existing
            assignment = PoolAssignment(gpu_id=self._gpu_id, worker_id=self._worker_id)
            self._active&#91;session_id&#93; = assignment
            return assignment

    async def run(
        self,
        session_id: str,
        request: GenerationRequest,
    ) -&gt; Any:
        if session_id not in self._active:
            raise RuntimeError(f&quot;session &#123;session_id!r&#125; is not acquired on this pool&quot;)
        # Serialize generator access so one GPU runs one request at a
        # time, matching the internal gpu_pool&#x27;s per-GPU lock.
        async with self._gen_lock:
            loop = asyncio.get_running_loop()
            return await loop.run_in_executor(None, self._generator.generate, request)

    async def release(self, session_id: str) -&gt; None:
        async with self._lock:
            self._active.pop(session_id, None)

    async def shutdown(self) -&gt; None:
        self._active.clear()

    def health(self) -&gt; PoolHealth:
        return PoolHealth(
            total_workers=1,
            available_workers=1 if not self._active else 0,
            active_sessions=len(self._active),
        )</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.acquire"><code>fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.acquire</code></h3>
<pre class="doc-signature"><code>acquire(session_id: str, *, timeout: float | None = None) -&gt; PoolAssignment</code></pre>

<details><summary>Source</summary><pre><code>async def acquire(
    self,
    session_id: str,
    *,
    timeout: float | None = None,
) -&gt; PoolAssignment:
    async with self._lock:
        existing = self._active.get(session_id)
        if existing is not None:
            return existing
        assignment = PoolAssignment(gpu_id=self._gpu_id, worker_id=self._worker_id)
        self._active&#91;session_id&#93; = assignment
        return assignment</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.run"><code>fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.run</code></h3>
<pre class="doc-signature"><code>run(session_id: str, request: GenerationRequest) -&gt; Any</code></pre>

<details><summary>Source</summary><pre><code>async def run(
    self,
    session_id: str,
    request: GenerationRequest,
) -&gt; Any:
    if session_id not in self._active:
        raise RuntimeError(f&quot;session &#123;session_id!r&#125; is not acquired on this pool&quot;)
    # Serialize generator access so one GPU runs one request at a
    # time, matching the internal gpu_pool&#x27;s per-GPU lock.
    async with self._gen_lock:
        loop = asyncio.get_running_loop()
        return await loop.run_in_executor(None, self._generator.generate, request)</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.release"><code>fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.release</code></h3>
<pre class="doc-signature"><code>release(session_id: str) -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>async def release(self, session_id: str) -&gt; None:
    async with self._lock:
        self._active.pop(session_id, None)</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.shutdown"><code>fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.shutdown</code></h3>
<pre class="doc-signature"><code>shutdown() -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>async def shutdown(self) -&gt; None:
    self._active.clear()</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.health"><code>fastvideo.entrypoints.streaming.gpu_pool.InProcessGpuPool.health</code></h3>
<pre class="doc-signature"><code>health() -&gt; PoolHealth</code></pre>

<details><summary>Source</summary><pre><code>def health(self) -&gt; PoolHealth:
    return PoolHealth(
        total_workers=1,
        available_workers=1 if not self._active else 0,
        active_sessions=len(self._active),
    )</code></pre></details>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool"><code>fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool</code></h2>
<pre class="doc-signature"><code>SubprocessGpuPool(generator_config: GeneratorConfig, *, pool_config: GpuPoolConfig, warmup_config: WarmupConfig | None = None, session_store: SessionStore | None = None, worker_factory: WorkerFactory | None = None)</code></pre>
<p>One <code>multiprocessing.Process</code> per GPU.</p>
<p>Each worker boots :class:<code>fastvideo.VideoGenerator</code> from a typed
:class:<code>GeneratorConfig</code> inside the child process (post-
<code>CUDA_VISIBLE_DEVICES</code> setup) and consumes jobs from an mp Queue.</p>
<p>This is the production shape: the parent process stays CPU-only, and
GPU state never crosses process boundaries. Continuation state is
serialized through :class:<code>SessionStore</code> for cross-GPU handoff.</p>
<p>PR 7.6 ships this as an opt-in; PR 7.5's in-process pool remains the
default until nightly runs validate the subprocess path.</p>

<details><summary>Source</summary><pre><code>class SubprocessGpuPool(GpuPool):
    &quot;&quot;&quot;One ``multiprocessing.Process`` per GPU.

    Each worker boots :class:`fastvideo.VideoGenerator` from a typed
    :class:`GeneratorConfig` inside the child process (post-
    ``CUDA_VISIBLE_DEVICES`` setup) and consumes jobs from an mp Queue.

    This is the production shape: the parent process stays CPU-only, and
    GPU state never crosses process boundaries. Continuation state is
    serialized through :class:`SessionStore` for cross-GPU handoff.

    PR 7.6 ships this as an opt-in; PR 7.5&#x27;s in-process pool remains the
    default until nightly runs validate the subprocess path.
    &quot;&quot;&quot;

    def __init__(
        self,
        generator_config: GeneratorConfig,
        *,
        pool_config: GpuPoolConfig,
        warmup_config: WarmupConfig | None = None,
        session_store: SessionStore | None = None,
        worker_factory: WorkerFactory | None = None,
    ) -&gt; None:
        self._generator_config = generator_config
        self._pool_config = pool_config
        self._warmup_config = warmup_config or WarmupConfig()
        self._session_store = session_store or InMemorySessionStore()
        self._worker_factory = worker_factory or _default_worker_factory
        self._workers: list&#91;_WorkerHandle&#93; = &#91;&#93;
        self._available: asyncio.Queue&#91;int&#93; = asyncio.Queue()
        self._assignments: dict&#91;str, PoolAssignment&#93; = &#123;&#125;
        self._worker_by_id: dict&#91;str, _WorkerHandle&#93; = &#123;&#125;
        self._pending: dict&#91;str, _PendingJob&#93; = &#123;&#125;
        self._lock = asyncio.Lock()
        self._result_reader_tasks: list&#91;asyncio.Task&#93; = &#91;&#93;

    async def start(self) -&gt; None:
        &quot;&quot;&quot;Spawn worker processes and wait for each to report ready.&quot;&quot;&quot;
        num_workers = self._pool_config.num_workers or 1
        for gpu_id in range(num_workers):
            handle = self._worker_factory(
                gpu_id=gpu_id,
                generator_config=self._generator_config,
                warmup_config=self._warmup_config,
            )
            self._workers.append(handle)
            self._worker_by_id&#91;handle.worker_id&#93; = handle

        # Wait for each worker&#x27;s ready event in a thread to avoid
        # blocking the event loop.
        loop = asyncio.get_running_loop()
        await asyncio.gather(*&#91;
            loop.run_in_executor(None, handle.ready.wait, self._warmup_config.timeout_seconds)
            for handle in self._workers
        &#93;)

        # Start background result readers — one task per worker
        # drains its result queue and resolves futures in _pending.
        for handle in self._workers:
            task = asyncio.create_task(self._drain_results(handle))
            self._result_reader_tasks.append(task)

        # Only admit workers that successfully booted. Anything that
        # failed boot (timeout, crash, error sentinel) stays out of the
        # available queue so we never assign a session to it.
        for idx, handle in enumerate(self._workers):
            if handle.boot_ok.is_set():
                await self._available.put(idx)
            else:
                logger.error(
                    &quot;pool: worker %s failed to boot; skipping&quot;,
                    handle.worker_id,
                )

    async def acquire(
        self,
        session_id: str,
        *,
        timeout: float | None = None,
    ) -&gt; PoolAssignment:
        async with self._lock:
            existing = self._assignments.get(session_id)
            if existing is not None:
                return existing
        try:
            idx = await asyncio.wait_for(self._available.get(), timeout=timeout)
        except asyncio.TimeoutError as exc:
            raise PoolAcquireTimeout(f&quot;no worker available after &#123;timeout&#125;s&quot;) from exc
        handle = self._workers&#91;idx&#93;
        assignment = PoolAssignment(gpu_id=handle.gpu_id, worker_id=handle.worker_id)
        async with self._lock:
            self._assignments&#91;session_id&#93; = assignment
        return assignment

    async def run(
        self,
        session_id: str,
        request: GenerationRequest,
    ) -&gt; Any:
        assignment = self._assignments.get(session_id)
        if assignment is None:
            raise RuntimeError(f&quot;session &#123;session_id!r&#125; not acquired on this pool&quot;)
        handle = self._worker_by_id&#91;assignment.worker_id&#93;
        job_id = uuid.uuid4().hex
        future: Future = Future()
        self._pending&#91;job_id&#93; = _PendingJob(
            job_id=job_id,
            future=future,
            session_id=session_id,
            worker_id=handle.worker_id,
        )
        # mp.Queue.put can block if the underlying pipe buffer is full;
        # offload to a thread so the event loop keeps serving other
        # sessions. A cancelled caller cannot stop the executor thread or
        # generation, so detach its pending entry and discard late replies.
        loop = asyncio.get_running_loop()
        try:
            await loop.run_in_executor(
                None,
                handle.job_queue.put,
                &#123;
                    &quot;job_id&quot;: job_id,
                    &quot;request&quot;: request
                &#125;,
            )
            return await asyncio.wrap_future(future)
        finally:
            self._pending.pop(job_id, None)
            future.cancel()

    async def release(self, session_id: str) -&gt; None:
        async with self._lock:
            assignment = self._assignments.pop(session_id, None)
        if assignment is None:
            return
        idx = next((i for i, h in enumerate(self._workers) if h.worker_id == assignment.worker_id), None)
        if idx is None:
            return
        # Don&#x27;t return a dead worker to the pool; otherwise the next
        # acquire will hand a session to a process that can&#x27;t run jobs.
        if not self._workers&#91;idx&#93;.process.is_alive():
            logger.warning(
                &quot;pool: worker %s died; not returning to available queue&quot;,
                self._workers&#91;idx&#93;.worker_id,
            )
            return
        await self._available.put(idx)

    async def shutdown(self) -&gt; None:
        loop = asyncio.get_running_loop()

        # Signal all workers in parallel; .put may block on a full pipe,
        # so off-load it the same way run() does.
        async def _signal(handle: _WorkerHandle) -&gt; None:
            try:
                handle.shutdown_event.set()
                await loop.run_in_executor(None, handle.job_queue.put, None)
            except Exception:  # pragma: no cover - best-effort cleanup
                pass

        await asyncio.gather(*(_signal(h) for h in self._workers))
        # Join in parallel so total shutdown is bounded by the slowest
        # worker, not the sum of all timeouts.
        await asyncio.gather(*(loop.run_in_executor(None, handle.process.join, 5.0) for handle in self._workers))
        for handle in self._workers:
            if handle.process.is_alive():
                handle.process.kill()
        for task in self._result_reader_tasks:
            task.cancel()
        self._result_reader_tasks.clear()
        self._workers.clear()
        self._worker_by_id.clear()

    def health(self) -&gt; PoolHealth:
        return PoolHealth(
            total_workers=len(self._workers),
            available_workers=self._available.qsize(),
            active_sessions=len(self._assignments),
        )

    async def _drain_results(self, handle: _WorkerHandle) -&gt; None:
        loop = asyncio.get_running_loop()
        try:
            while not handle.shutdown_event.is_set():
                try:
                    msg = await loop.run_in_executor(None, _safe_queue_get, handle.result_queue, 0.5)
                except Exception:
                    logger.exception(&quot;pool: worker %s result reader failed&quot;, handle.worker_id)
                    return
                if msg is None:
                    continue
                job_id = msg.get(&quot;job_id&quot;)
                if job_id is None:
                    continue
                pending = self._pending.pop(job_id, None)
                # wrap_future can cancel the source future before run()&#x27;s
                # finally block gets its turn on the event loop, and an
                # abandoned job&#x27;s late reply can arrive after run() already
                # popped its entry. Either way there is no live waiter.
                if pending is None or pending.future.done():
                    logger.debug(&quot;pool: worker %s dropping reply for job %s&quot;, handle.worker_id, job_id)
                    continue
                if msg.get(&quot;kind&quot;) == &quot;error&quot;:
                    pending.future.set_exception(RuntimeError(msg&#91;&quot;error&quot;&#93;))
                else:
                    pending.future.set_result(msg.get(&quot;result&quot;))
        finally:
            # If we exit for any reason — shutdown, exception, cancel —
            # surface that to any in-flight jobs on this worker so their
            # await never hangs on a future no one will resolve.
            for jid in &#91;jid for jid, job in self._pending.items() if job.worker_id == handle.worker_id&#93;:
                pending = self._pending.pop(jid, None)
                if pending is not None and not pending.future.done():
                    pending.future.set_exception(
                        RuntimeError(f&quot;worker &#123;handle.worker_id&#125; result reader exited &quot;
                                     &quot;with pending jobs&quot;))</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.start"><code>fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.start</code></h3>
<pre class="doc-signature"><code>start() -&gt; None</code></pre>
<p>Spawn worker processes and wait for each to report ready.</p>
<details><summary>Source</summary><pre><code>async def start(self) -&gt; None:
    &quot;&quot;&quot;Spawn worker processes and wait for each to report ready.&quot;&quot;&quot;
    num_workers = self._pool_config.num_workers or 1
    for gpu_id in range(num_workers):
        handle = self._worker_factory(
            gpu_id=gpu_id,
            generator_config=self._generator_config,
            warmup_config=self._warmup_config,
        )
        self._workers.append(handle)
        self._worker_by_id&#91;handle.worker_id&#93; = handle

    # Wait for each worker&#x27;s ready event in a thread to avoid
    # blocking the event loop.
    loop = asyncio.get_running_loop()
    await asyncio.gather(*&#91;
        loop.run_in_executor(None, handle.ready.wait, self._warmup_config.timeout_seconds)
        for handle in self._workers
    &#93;)

    # Start background result readers — one task per worker
    # drains its result queue and resolves futures in _pending.
    for handle in self._workers:
        task = asyncio.create_task(self._drain_results(handle))
        self._result_reader_tasks.append(task)

    # Only admit workers that successfully booted. Anything that
    # failed boot (timeout, crash, error sentinel) stays out of the
    # available queue so we never assign a session to it.
    for idx, handle in enumerate(self._workers):
        if handle.boot_ok.is_set():
            await self._available.put(idx)
        else:
            logger.error(
                &quot;pool: worker %s failed to boot; skipping&quot;,
                handle.worker_id,
            )</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.acquire"><code>fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.acquire</code></h3>
<pre class="doc-signature"><code>acquire(session_id: str, *, timeout: float | None = None) -&gt; PoolAssignment</code></pre>

<details><summary>Source</summary><pre><code>async def acquire(
    self,
    session_id: str,
    *,
    timeout: float | None = None,
) -&gt; PoolAssignment:
    async with self._lock:
        existing = self._assignments.get(session_id)
        if existing is not None:
            return existing
    try:
        idx = await asyncio.wait_for(self._available.get(), timeout=timeout)
    except asyncio.TimeoutError as exc:
        raise PoolAcquireTimeout(f&quot;no worker available after &#123;timeout&#125;s&quot;) from exc
    handle = self._workers&#91;idx&#93;
    assignment = PoolAssignment(gpu_id=handle.gpu_id, worker_id=handle.worker_id)
    async with self._lock:
        self._assignments&#91;session_id&#93; = assignment
    return assignment</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.run"><code>fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.run</code></h3>
<pre class="doc-signature"><code>run(session_id: str, request: GenerationRequest) -&gt; Any</code></pre>

<details><summary>Source</summary><pre><code>async def run(
    self,
    session_id: str,
    request: GenerationRequest,
) -&gt; Any:
    assignment = self._assignments.get(session_id)
    if assignment is None:
        raise RuntimeError(f&quot;session &#123;session_id!r&#125; not acquired on this pool&quot;)
    handle = self._worker_by_id&#91;assignment.worker_id&#93;
    job_id = uuid.uuid4().hex
    future: Future = Future()
    self._pending&#91;job_id&#93; = _PendingJob(
        job_id=job_id,
        future=future,
        session_id=session_id,
        worker_id=handle.worker_id,
    )
    # mp.Queue.put can block if the underlying pipe buffer is full;
    # offload to a thread so the event loop keeps serving other
    # sessions. A cancelled caller cannot stop the executor thread or
    # generation, so detach its pending entry and discard late replies.
    loop = asyncio.get_running_loop()
    try:
        await loop.run_in_executor(
            None,
            handle.job_queue.put,
            &#123;
                &quot;job_id&quot;: job_id,
                &quot;request&quot;: request
            &#125;,
        )
        return await asyncio.wrap_future(future)
    finally:
        self._pending.pop(job_id, None)
        future.cancel()</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.release"><code>fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.release</code></h3>
<pre class="doc-signature"><code>release(session_id: str) -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>async def release(self, session_id: str) -&gt; None:
    async with self._lock:
        assignment = self._assignments.pop(session_id, None)
    if assignment is None:
        return
    idx = next((i for i, h in enumerate(self._workers) if h.worker_id == assignment.worker_id), None)
    if idx is None:
        return
    # Don&#x27;t return a dead worker to the pool; otherwise the next
    # acquire will hand a session to a process that can&#x27;t run jobs.
    if not self._workers&#91;idx&#93;.process.is_alive():
        logger.warning(
            &quot;pool: worker %s died; not returning to available queue&quot;,
            self._workers&#91;idx&#93;.worker_id,
        )
        return
    await self._available.put(idx)</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.shutdown"><code>fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.shutdown</code></h3>
<pre class="doc-signature"><code>shutdown() -&gt; None</code></pre>

<details><summary>Source</summary><pre><code>async def shutdown(self) -&gt; None:
    loop = asyncio.get_running_loop()

    # Signal all workers in parallel; .put may block on a full pipe,
    # so off-load it the same way run() does.
    async def _signal(handle: _WorkerHandle) -&gt; None:
        try:
            handle.shutdown_event.set()
            await loop.run_in_executor(None, handle.job_queue.put, None)
        except Exception:  # pragma: no cover - best-effort cleanup
            pass

    await asyncio.gather(*(_signal(h) for h in self._workers))
    # Join in parallel so total shutdown is bounded by the slowest
    # worker, not the sum of all timeouts.
    await asyncio.gather(*(loop.run_in_executor(None, handle.process.join, 5.0) for handle in self._workers))
    for handle in self._workers:
        if handle.process.is_alive():
            handle.process.kill()
    for task in self._result_reader_tasks:
        task.cancel()
    self._result_reader_tasks.clear()
    self._workers.clear()
    self._worker_by_id.clear()</code></pre></details>
<h3 id="fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.health"><code>fastvideo.entrypoints.streaming.gpu_pool.SubprocessGpuPool.health</code></h3>
<pre class="doc-signature"><code>health() -&gt; PoolHealth</code></pre>

<details><summary>Source</summary><pre><code>def health(self) -&gt; PoolHealth:
    return PoolHealth(
        total_workers=len(self._workers),
        available_workers=self._available.qsize(),
        active_sessions=len(self._assignments),
    )</code></pre></details>
<h2 id="fastvideo.entrypoints.streaming.gpu_pool.WorkerFactory"><code>fastvideo.entrypoints.streaming.gpu_pool.WorkerFactory</code></h2>
<pre class="doc-signature"><code>WorkerFactory()</code></pre>

<details><summary>Source</summary><pre><code>class WorkerFactory(Protocol):

    def __call__(
        self,
        *,
        gpu_id: int,
        generator_config: GeneratorConfig,
        warmup_config: WarmupConfig,
    ) -&gt; _WorkerHandle:
        ...</code></pre></details>
</div>
