fix(engines): stop SubprocessBackend.generate() self-deadlock on 1-worker (MPS) pools - #1296
Conversation
…rker pools (debpalash#1295) generate() acquires a GPU-pool slot for accounting, but /v1/audio/speech and /generate (and audiobook/dub/batch) dispatch backend.generate() via run_on_gpu_pool_guarded, already ON a pool worker. On a 1-worker pool (MPS) the inner pool.submit queues behind the very job running it and result(timeout=10) raises before the sidecar spawns, so every subprocess engine surfaces the in-process 300s-abandon instead of synthesizing. Skip the slot acquisition when current_thread() is already a gpu-pool worker; the outer guard already accounts for the slot (holds _running for the whole sidecar exchange). Direct off-pool callers (engine self-test, diagnostic probe) still acquire one as a queue wait. Regression test: generate dispatched on a pool worker. Closes debpalash#1295.
| 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 |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough
ChangesSubprocess GPU-pool execution
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: 1
🤖 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/tests/test_subprocess_slot_deadlock.py`:
- Around line 102-106: Update the test around _get_gpu_pool and the nested
generate() submission to create or monkeypatch a GPU pool configured with
exactly one worker before invoking pool.submit. Ensure the test uses that
one-worker pool rather than an ambient multi-worker singleton, while preserving
the existing timeout and result assertions.
🪄 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: 1de89e1a-69d1-4542-aa54-7b9f6f72961f
📒 Files selected for processing (2)
backend/services/subprocess_backend.pybackend/tests/test_subprocess_slot_deadlock.py
|
| Filename | Overview |
|---|---|
| backend/services/subprocess_backend.py | Detects canonical GPU-pool workers and avoids the nested submission that deadlocked single-worker pools. |
| backend/tests/test_subprocess_slot_deadlock.py | Reproduces production dispatch on a single-worker pool and verifies successful sidecar synthesis. |
Reviews (2): Last reviewed commit: "test: force a 1-worker gpu-pool in the s..." | Re-trigger Greptile
…alash#1296) CodeRabbit: the test used the ambient pool singleton, which may have >1 worker on some hosts, so the pre-fix nested submit could succeed and the regression would pass vacuously. Monkeypatch _get_gpu_pool to a fresh 1-worker "gpu-pool"-prefixed pool so the self-deadlock reproduces deterministically. Verified fail-before (TimeoutError at ~10s with the fix reverted) / pass-after.
|
CodeRabbit: addressed in 20a2379. The test now monkeypatches _get_gpu_pool to a fresh 1-worker |
| 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 |
… 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.
|
Superseded by #1298, which folds the on-pool skip (this PR) together with the off-pool hold in one path-aware slot block. Closing in favor of the complete fix. |
…) (on-pool skip + off-pool hold) (#1298) * fix(engines): make SubprocessBackend.generate() path-aware on the GPU 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 #1296 (on-pool-skip-only). Closes #1295, #1297. * Address review: couple on-pool skip to the pool prefix; fix comment /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. * fix(engines): bind slot_future before the off-pool branch 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> * test(engines): make the slot-hold regression deterministic and leak-free 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> --------- Co-authored-by: debpalash <4178343+debpalash@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #1295.
SubprocessBackend.generate()acquires a GPU-pool slot for accounting, but/v1/audio/speechand/generate(and audiobook / dub / batch) dispatchbackend.generate()viarun_on_gpu_pool_guarded, already ON a pool worker. On a 1-worker pool (MPS) the innerpool.submitqueues behind the very job running it andresult(timeout=10)raises before the sidecar spawns, so every subprocess engine (IndexTTS-2, Supertonic-3, dots.tts, MOSS-TTS-v1.5, Confucius4) surfaces the in-process 300s-abandon instead of synthesizing.Changed
generate()skips the slot acquisition whencurrent_thread()is already a gpu-pool worker; the outerrun_on_gpu_pool_guardedalready accounts for the slot (it holds_runningfor the whole sidecar exchange via_tracked). Direct off-pool callers (the engine self-test inengines.py, the diagnostic probe indiagnose.py) still take a slot as a queue wait.Tests / verification
backend/tests/test_subprocess_slot_deadlock.py(generate dispatched on a pool worker; pre-fix it raised the ~10s slot timeout before the sidecar spawned)./v1/audio/speechreturns HTTP 200 with real audio, sidecar spawns.Notes
e9a15d53). This PR is the standalone general bug fix; if it merges first, feat(engines): add omnivoice-subprocess, a crash-isolated (killable) TTS engine #1292 rebases onto it.Updated
SubprocessBackend.generate()to skip the inner GPU-pool “slot” acquisition when the call already runs on agpu-poolworker thread, preventing self-deadlock on one-worker pools (including MPS) while keeping slot accounting for direct off-pool callers. Added a regression test that monkeypatches_get_gpu_poolto a deterministic one-workergpu-poolthread pool and uses a stub sidecar to assertgenerate()completes and returns the expected audio length without timing out. Risk is mainly the thread-name-basedgpu-pooldetection, which should be reviewed for brittleness across pool implementations.