Skip to content

feat(pipeline): retry separation on CPU when a GPU attempt fails - #299

Merged
thcp merged 1 commit into
mainfrom
feat/gpu-cpu-fallback
Jul 17, 2026
Merged

feat(pipeline): retry separation on CPU when a GPU attempt fails#299
thcp merged 1 commit into
mainfrom
feat/gpu-cpu-fallback

Conversation

@thcp

@thcp thcp commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Closes #276. Phase 2 of #273 — plan: #273 (comment) (PR-2A, the flagship).

What

A single MPS/CUDA failure (OOM, unsupported op, driver hiccup) killed the whole job with the generic "Audio processing failed" — the Mac Mini field report verbatim, where the user resorted to a LaunchAgent env-var hack to force CPU. Jobs now retry once on CPU and complete: slower, but alive and self-explanatory.

Design decisions (from the plan)

  • Loud, never silent (the [Bug]: NVIDIA build still runs on CPU on machines with an NVIDIA GPU #247 lesson applied to the runtime path): stage line shows "GPU failed — retrying on CPU (slower)…" during the retry; the WARNING log carries the classified cause (via errors.classify_failure from feat(pipeline): failed-job quarantine + error classification + stage timings #296) + full stderr tail; gpu_fallback and compute_device ("cpu (fallback from cuda)") persist to job state, metadata.json, and the registry.
  • Fires for every failure class and even user-forced devices — the unknown GPU failure is exactly what field reports hit; a dead job with no diagnostics is strictly worse for a user who forced cuda than a slow job that explains itself.
  • Partial GPU output is cleared before the CPU run (collect() must never see half-written stems); progress resets to 0 (CPU restarts from scratch); the stall watchdog applies per attempt.
  • Cancel during the GPU attempt → JobCancelled, no pointless CPU retry.
  • CPU-also-fails → SeparationError carrying both attempts' stderr tails, so the Error path destroys all evidence #277 quarantine's error.txt records the full story.

Mechanics

separate() is now the policy layer over the extracted _run_demucs() (one attempt: spawn, progress streaming, watchdog, cancel translation — moved verbatim), with a module-level _demucs_cmd() seam for tests.

Testing

New tests/test_separate_fallback.py — stubs _demucs_cmd with Python one-liners so the real process machinery runs end-to-end without demucs/GPU:

  • GPU fail → CPU success: stems produced, gpu_fallback=True, warning names device+cause+stderr
  • GPU success: single attempt, no fallback
  • CPU-first failure: exactly one attempt, no self-retry
  • Both fail: one SeparationError with both tails and device="mps, then cpu"
  • Cancel mid-GPU-attempt: JobCancelled, no CPU retry
  • Partial GPU output cleared before retry

Full suite: 186 passed; ruff check + ruff format --check clean.

Manual verification path

On the NVIDIA machine: force demucs_device=cuda with CPU-only torch (the .venv-win dev env is exactly this) → job must complete with the fallback stage visible and "gpu_fallback": true in /api/jobs/{id}.

🤖 Generated with Claude Code

One MPS/CUDA failure (OOM, unsupported op, driver hiccup) killed the
whole job with "Audio processing failed" -- the Mac Mini report
verbatim, where the user needed a LaunchAgent env-var hack to force
CPU. The job now retries once on CPU and completes, slower but alive.

The fallback is loud, never silent (the #247 lesson applied to the
runtime path): the stage line reads "GPU failed -- retrying on CPU
(slower)..." while it runs, the WARNING log carries the classified
cause and full stderr tail, and gpu_fallback/compute_device persist to
job state and metadata. It fires even when the user forced cuda/mps in
Settings -- a dead job with no diagnostics is strictly worse than a
slow one that explains itself.

Mechanics: separate() is now the retry-policy layer over _run_demucs()
(one attempt: spawn, stream progress, stall watchdog, cancel
translation) with a _demucs_cmd() seam for tests. Partial output from
the failed GPU attempt is cleared before the CPU run so collect() can
never pick up half-written stems; progress resets to 0 since CPU
restarts from scratch. A cancel during the GPU attempt raises
JobCancelled without a pointless CPU retry. If CPU also fails, the
SeparationError carries both attempts' stderr tails for the quarantine.

Closes #276
@thcp
thcp merged commit 5355a93 into main Jul 17, 2026
8 checks passed
@thcp
thcp deleted the feat/gpu-cpu-fallback branch July 17, 2026 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No GPU→CPU fallback when separation fails

1 participant