fix(engines): path-aware GPU-pool slot in SubprocessBackend.generate() (on-pool skip + off-pool hold) - #1298
Conversation
| job and over-subscribe a 1-worker GPU. This test reproduces that: while an | ||
| off-pool generate is mid-synthesis, a second pool job must stay blocked. | ||
| """ | ||
| import base64 |
| off-pool generate is mid-synthesis, a second pool job must stay blocked. | ||
| """ | ||
| import base64 | ||
| import json |
| """ | ||
| import base64 | ||
| import json | ||
| import math |
| import base64 | ||
| import json | ||
| import math | ||
| import array |
|
| Filename | Overview |
|---|---|
| backend/services/model_manager.py | Centralizes the GPU-pool thread prefix and exposes pool-worker detection used by scheduling-sensitive paths. |
| backend/services/subprocess_backend.py | Implements path-aware slot ownership, but the previously reported ten-second off-pool acquisition timeout remains. |
| backend/tests/test_off_pool_slot_hold.py | Verifies that off-pool generation retains the sole GPU worker throughout synthesis. |
| backend/tests/test_subprocess_slot_deadlock.py | Verifies that generation already running on a single-worker GPU pool does not reserve a nested slot. |
Reviews (5): Last reviewed commit: "Merge branch 'main' into fix/subprocess-..." | Re-trigger Greptile
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughChangesGPU slot hold
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 8 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (8 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/services/subprocess_backend.py`:
- Around line 525-543: Update the reservation logic in generate() around _occupy
and slot_future to detect when execution is already running on the GPU pool
worker and bypass the additional pool.submit reservation, preserving the
existing in-pool path. Keep the reservation for off-pool callers, and add a
regression test covering reentrant generate() on a one-worker pool without the
ten-second timeout.
In `@backend/tests/test_off_pool_slot_hold.py`:
- Around line 113-129: Make the off-pool slot-hold test synchronize through an
instrumented reservation-start event instead of fixed sleeps, ensuring the
generator has reserved its slot before submitting and checking marker. Wrap the
test body in a finally block that always calls b.shutdown() and pool.shutdown(),
including when assertions or generation fail; preserve the existing assertions
and cleanup behavior otherwise.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: deb4de28-fd7a-41cd-af9d-d6daae9225b9
📒 Files selected for processing (2)
backend/services/subprocess_backend.pybackend/tests/test_off_pool_slot_hold.py
… pool generate()'s slot handling had two bugs: 1. On-pool self-deadlock: /v1/audio/speech and /generate (and audiobook, dub, batch) dispatch generate() via run_on_gpu_pool_guarded, already on a pool worker, so the inner slot submit queued behind the very job running it on a 1-worker (MPS) pool and timed out before the sidecar spawned. Every subprocess engine surfaced the in-process 300s-abandon instead of synthesizing. 2. Off-pool no hold: the off-pool slot was a bare no-op that released the worker before _spawn(), so off-pool callers (engine self-test, diagnostics) could synthesize concurrently with a pool job and over-subscribe the GPU. Make the slot block path-aware: on-pool callers skip (the outer run_on_gpu_pool_guarded already holds _running for the whole sidecar exchange); off-pool callers hold a real slot for the whole synthesis via an _occupy task that blocks the worker until _held is set in the finally. Single release point in the finally. Regression tests: generate dispatched on a pool worker (on-pool skip) and a concurrent pool job blocked during an off-pool generate (off-pool hold). Both verified fail-before / pass-after. Supersedes debpalash#1296 (on-pool-skip-only). Closes debpalash#1295, debpalash#1297.
970e2b1 to
9fdfc17
Compare
| result(timeout=10) raised before the sidecar spawned. This test reproduces that | ||
| dispatch shape (generate on a pool worker) against a stub sidecar. | ||
| """ | ||
| import base64 |
| dispatch shape (generate on a pool worker) against a stub sidecar. | ||
| """ | ||
| import base64 | ||
| import json |
| """ | ||
| import base64 | ||
| import json | ||
| import math |
| import base64 | ||
| import json | ||
| import math | ||
| import array |
| import sys | ||
| from pathlib import Path | ||
|
|
||
| import pytest |
/simplify + /code-review flagged that the on-pool skip keyed on the literal "gpu-pool" string, decoupled from _build_gpu_pool's thread_name_prefix. A rename would silently re-introduce the exact self-deadlock this PR fixes (and the tests can't catch it, since they hardcode the prefix). Centralise the prefix in _GPU_POOL_THREAD_PREFIX + a running_on_gpu_pool() helper, used by _build_gpu_pool, the skip in generate(), and _heal_tts_placement. Also fix the comment: the Settings engine self-test rejects subprocess-isolated engines with a 400, so the only real off-pool caller is the diagnose.py deep-synth probe.
CodeQL py/uninitialized-local-variable (error, blocking CI). `_held is not None` does imply slot_future was assigned, so the current code is correct — but the two are only coupled by convention, which the analyser cannot see and a third exit path would quietly break. Binds it to None up front and guards the cancel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeRabbit, valid on both counts. The test used sleep(0.8)/sleep(0.5) as synchronization — the tests/** contract forbids it, and on a slow runner the marker could be enqueued before the generator had reserved anything, so the assertion passed for the wrong reason. It now waits on an event signalled when the slot task actually starts, and asserts "did not run" via a result() timeout rather than a bare sleep. Cleanup moved into finally: an assertion failure used to leak the sidecar process and the pool thread into the rest of the session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| import array | ||
| import sys | ||
| import threading | ||
| import time |
# Conflicts: # backend/services/subprocess_backend.py
…ribe() (ASR sibling of #1298) (#1304) * fix(engines): path-aware GPU-pool slot in SubprocessASRBackend.transcribe() transcribe() had the same on-pool self-deadlock that generate() had (fixed in #1298): a bare no-op submitted to the GPU pool, but run_transcribe_guarded dispatches it via run_in_executor(_gpu_pool), already on a pool worker, so on a 1-worker (MPS) pool the no-op queued behind the job running it and timed out before the sidecar spawned. IsolatedFasterWhisperBackend on MPS hit this on every transcription. Mirror generate()'s path-aware slot block (on-pool skip via running_on_gpu_pool; off-pool _occupy hold) in transcribe(). The pattern is duplicated rather than extracted into a shared helper to avoid reworking generate(), which just shipped (#1298) with a CodeQL fix; extracting a shared contextmanager is a clean follow-up. Regression test added (transcribe dispatched on a pool worker). * fix(engines): import threading in subprocess_asr transcribe()'s off-pool slot hold uses threading.Event(), but the module never imported threading — every subprocess-ASR transcribe raised NameError, and the three round-trip tests failed in CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(engines): cover the off-pool transcribe branch Only the on-pool path had a test, so `threading.Event()` in the off-pool branch shipped with `threading` never imported — every direct caller hit NameError before the sidecar started. Both bots caught it on review; nothing in the suite did. A branch with no test is how a one-word bug reaches CI. Also asserts the slot is genuinely released afterwards. Fails without the import fix; the pre-existing on-pool test still passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: debpalash <4178343+debpalash@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #1295, #1297. Supersedes #1296.
SubprocessBackend.generate()'s GPU-pool slot handling had two bugs, both fixed here in one path-aware slot block:/v1/audio/speechand/generate(and audiobook/dub/batch) dispatchgenerate()viarun_on_gpu_pool_guarded, already on a pool worker, so the inner slot submit queued behind the very job running it on a 1-worker (MPS) pool and timed out before the sidecar spawned. Every subprocess engine surfaced the in-process 300s-abandon instead of synthesizing._spawn(), so off-pool callers (engine self-test, diagnostics) could synthesize concurrently with a pool job and over-subscribe the GPU.Changed
generate()is now path-aware: on-pool callers skip the slot (the outerrun_on_gpu_pool_guardedalready holds_runningfor the whole sidecar exchange); off-pool callers hold a real slot for the whole synthesis via an_occupytask that blocks the worker until_heldis set in thefinally. Single release point in thefinally.Tests / verification
Notes
services/subprocess_asr.pyhas the same no-op-reserve pattern; extracting areserve_worker()helper on_ResilientGpuPool(used by both) is a clean follow-up, out of scope here.Updated
SubprocessBackend.generate()to avoid GPU-pool self-deadlocks by skipping nested slot acquisition whenrunning_on_gpu_pool()is true, while off-pool callers now submit a blocking_occupytask that holds the single pool slot for the entire sidecar synthesis and releases it in a unifiedfinally(with a 10s wait/TimeoutErrorif the slot can’t be acquired). Centralized detection of GPU pool worker threads inmodel_managerviarunning_on_gpu_pool(). Added regression coverage ensuring (1) on-pool invocation on a 1-worker pool completes without hanging and (2) off-pool generation keeps a second pool job blocked until synthesis finishes; main risk to review is the_occupywait/timeout and future cancellation/cleanup correctness on all exit paths.