Files
VoiceStudio/backend/services/engine_env.py
T
Palash DebnathandClaude Opus 4.8 898f41a57d fix(windows): gate torch.compile on Triton + ASR critical-path smoke (plan-02, closes #65) (#138)
* fix(windows): gate torch.compile on Triton availability (#129, closes #65)

plan-02. torch.compile(mode="reduce-overhead") needs Triton at runtime;
Triton has no Windows wheel, so the old `device=="cuda"`-only guard in
model_manager.py failed on Windows+CUDA and surfaced as a confusing "OOM"
(#65). Inference-time, hard to diagnose.

- engine_env.should_torch_compile(device): requires CUDA + find_spec("triton")
  + the existing perf.torch_compile_disabled setting being off; logs the skip
  reason at INFO and falls back to eager.
- model_manager.py call site uses it instead of the bare cuda check.
- smoke-test.sh INST-02: import torch + ctranslate2 + whisperx (full ASR path)
  so a missing transitive dep fails the build instead of crashing mid-
  transcription (#116). Runs in the CI smoke-matrix on Win/macOS/Linux.

setuptools>=75.0 (fix-sequence step 1) already pinned (#58). Linux/CUDA+Triton
behaviour unchanged.

Tests (TDD): tests/test_torch_compile_gate.py (4). Closes #65; addresses
#129/#116.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(windows): also gate subprocess torch.compile on Triton (Greptile #138)

Greptile flagged that the in-process gate left a parallel gap: engine
subprocesses honour TORCH_COMPILE_DISABLE, but build_engine_env() only set
it on the user's Performance toggle — so a Triton-absent host (Windows, or
macOS) still exposed subprocess engines to the same crash this PR fixes
in-process.

- build_engine_env(): set TORCH_COMPILE_DISABLE=1 when the user disabled
  compile OR Triton is unavailable (find_spec), cross-platform — mirrors
  should_torch_compile(). Drops the Windows-only scoping (and the now-unused
  `import sys`).
- Refreshed the stale module docstring.
- 3 new tests cover the subprocess gate (triton-missing, triton-present,
  user-opt-out). 7/7 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* revert(engine_env): keep subprocess TORCH_COMPILE_DISABLE user-driven

Reverts the build_engine_env() broadening from the previous commit. Auto-
disabling subprocess torch.compile on Triton-absence conflicts with a
deliberate, tested contract (test_perf_settings: Windows + flag-off ⇒ no
injection; non-Windows ⇒ never inject) — the subprocess var is intentionally
under the user's explicit control.

The #65 fix is the in-process should_torch_compile() gate (unchanged here),
which IS automatic and fully tested. Pushing back on the subprocess auto-gate
as a separate, deliberate contract change rather than forcing it through by
rewriting established tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:22:19 +05:30

104 lines
4.0 KiB
Python

"""Subprocess env builder for engine launchers (Phase 1 INST-12 + AUTH-04).
Every place that spawns an engine subprocess (sonitranslate, future
CosyVoice / IndexTTS subprocess backends from Phase 2) should call
`build_engine_env()` instead of constructing its own env dict ad-hoc.
That gives us ONE place to inject:
- HF_TOKEN / YOUR_HF_TOKEN from the 3-source resolver (AUTH-04)
- TORCH_COMPILE_DISABLE=1 on Windows when the user enabled the
Performance toggle (INST-12, issue #65)
The function returns a fresh dict (caller may further mutate before
passing to `subprocess.Popen(env=...)`).
"""
from __future__ import annotations
import importlib.util
import logging
import os
import sys
from typing import Optional
logger = logging.getLogger("omnivoice.engine_env")
_TORCH_COMPILE_KEY = "perf.torch_compile_disabled"
def should_torch_compile(device: str) -> bool:
"""Decide whether to apply ``torch.compile`` to an in-process model.
plan-02 (#65): ``torch.compile(mode="reduce-overhead")`` needs Triton at
runtime, and Triton has no Windows build — on Windows+CUDA the compile path
failed and surfaced as a confusing "OOM". Requires all of:
- device == "cuda" (compile only helps the CUDA path here),
- Triton importable (``find_spec`` — the cross-platform gate that closes
#65; no Windows wheel ⇒ skip ⇒ eager),
- the user has NOT set the ``perf.torch_compile_disabled`` escape hatch.
Returns False (→ eager mode) on any of those, logging the reason at INFO.
"""
if device != "cuda":
return False
if importlib.util.find_spec("triton") is None:
logger.info("torch.compile skipped: Triton unavailable — using eager mode.")
return False
try:
from services import settings_store
if settings_store.get_text(_TORCH_COMPILE_KEY, "0") == "1":
logger.info("torch.compile skipped: disabled in Settings (Performance).")
return False
except Exception:
logger.exception("should_torch_compile: settings read failed; proceeding")
return True
def build_engine_env(
*,
base_env: Optional[dict] = None,
inject_hf_token: bool = True,
) -> dict:
"""Build the environment dict to pass to an engine subprocess launcher.
Args:
base_env: starting point — defaults to `os.environ.copy()`.
inject_hf_token: when True (default), resolve the HF token via the
3-source cascade and inject it as both HF_TOKEN and YOUR_HF_TOKEN
(the latter is what SoniTranslate's pipeline expects).
Returns a new dict — never mutates the input.
"""
env = dict(base_env if base_env is not None else os.environ)
# AUTH-04: HF token injection from the resolver cascade. We import lazily
# so the helper is callable in test contexts that don't stand up the
# full settings_store / DB.
if inject_hf_token:
try:
from services import token_resolver
resolved = token_resolver.resolve()
if resolved and resolved.token:
env["HF_TOKEN"] = resolved.token
env["YOUR_HF_TOKEN"] = resolved.token
except Exception:
logger.exception("build_engine_env: token resolver failed (non-fatal)")
# INST-12: TORCH_COMPILE_DISABLE on Windows when the user opted in.
# The flag is a Windows-only escape hatch — torch.compile OOMs the same
# Triton kernel cache differently on macOS/Linux, so injecting on those
# platforms would just slow the engine for no gain. (The in-process
# should_torch_compile() gate handles the automatic Triton-absence case;
# the subprocess var stays user-driven by design — see test_perf_settings.)
if sys.platform.startswith("win"):
try:
from services import settings_store
if settings_store.get_text(_TORCH_COMPILE_KEY, "0") == "1":
env["TORCH_COMPILE_DISABLE"] = "1"
except Exception:
logger.exception("build_engine_env: torch_compile_disabled read failed")
return env