let a self-healing rebuild run without the repo-root file it names

The GPU probe shipped a moment ago did its job on the first live run: it
caught the cudnn failure and asked for a rebuild. The rebuild then stopped
with

    Parler requirements file missing: /opt/coderai/app/requirements-parler.txt

The container mounts `codai/`, `tools/` and `third_party/` over the image, so
a repo-ROOT file added since the running image was built is simply not there —
and a worker that heals itself must not stop for a reason that has nothing to
do with the venv. The same mistake was already on record as ACE-Step
reporting a missing `requirements-acestep.txt` for a venv that was fine.

The file stays the source of truth and wins whenever it is present; the same
pins also live in the worker for when it is not, with a test that parses the
file and compares, because two copies of a pin drift.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
parent f9c05c91
......@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here.
__version__ = "0.5.19"
__version__ = "0.5.20"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
......@@ -30,6 +30,15 @@ _REPO_ROOT = Path(__file__).resolve().parents[2]
_SERVICE_SCRIPT = _REPO_ROOT / "tools" / "parler_tts_service.py"
_REQUIREMENTS = _REPO_ROOT / "requirements-parler.txt"
#: The same pins as requirements-parler.txt, for an install where that file is
#: not present (the container mounts only codai/, tools/ and third_party/ over
#: the image, so a repo-root file added since the image was built is absent
#: until the container is recreated). A test keeps the two in step.
_PINS = ("--extra-index-url", "https://download.pytorch.org/whl/cu124",
"torch==2.5.1+cu124",
"git+https://github.com/huggingface/parler-tts.git",
"soundfile>=0.12.0")
# Dedicated venv for the (incompatible) parler-tts stack. Created with access to
# the base interpreter's packages so torch/numpy aren't re-downloaded; parler's
# pinned transformers installs into the venv and shadows the system one.
......@@ -121,13 +130,12 @@ def _bootstrap_venv() -> Path:
shutil.rmtree(_VENV_DIR, ignore_errors=True)
py = venv_env.create_venv(_VENV_DIR)
if not _ok:
if not _REQUIREMENTS.exists():
raise RuntimeError(f"Parler requirements file missing: {_REQUIREMENTS}")
print("[parler] installing parler-tts + a pinned torch into the isolated "
"venv (first run, downloads several GB, this can take a while) …",
flush=True)
venv_env.run([str(py), "-m", "pip", "install", "-U", "pip"], check=True)
venv_env.run([str(py), "-m", "pip", "install", "-r", str(_REQUIREMENTS)],
venv_env.run([str(py), "-m", "pip", "install"]
+ venv_env.pip_install_args(_REQUIREMENTS, _PINS),
check=True)
if not _pip_ok(py):
raise RuntimeError("parler-tts install did not yield an importable package")
......
......@@ -376,3 +376,28 @@ def create_venv(venv_dir, system_site: bool = False,
cmd.append("--system-site-packages")
run(cmd + [str(d)], check=True, timeout=timeout)
return venv_python(d)
def pip_install_args(requirements, fallback_pins) -> list:
"""Pip arguments for a worker's install: the requirements file, or its pins.
The file is the source of truth and is preferred whenever it is there. It
is not always there: the container bind-mounts ``codai/``, ``tools/`` and
``third_party/`` over the image, so a repo-ROOT file added after the
running image was built is simply absent until the container is recreated
— and a self-healing rebuild that stops with "requirements file missing"
is a worker that stays broken for a reason that has nothing to do with it.
Measured twice on zeiss: once as ACE-Step reporting a missing
``requirements-acestep.txt`` for a venv that was fine, and once as a
Parler rebuild refusing to run.
``fallback_pins`` must mirror the file. Each worker that uses this keeps a
test that parses its file and compares, because two copies of a pin drift.
"""
from pathlib import Path as _P
req = _P(requirements)
if req.is_file():
return ["-r", str(req)]
print(f"[worker-env] {req} is not in this install (the container mounts "
f"only codai/, tools/ and third_party/ over the image); installing "
f"the same pins inline", flush=True)
return list(fallback_pins)
......@@ -110,7 +110,12 @@ Two changes:
* **the stack is pinned.** `requirements-parler.txt` names the torch build,
the way every other isolated worker's file does. Pip resolving a CUDA stack
on its own is how a venv ends up newer than the fleet it runs on.
on its own is how a venv ends up newer than the fleet it runs on. The same
pins also live in the worker, because the container mounts only `codai/`,
`tools/` and `third_party/` over the image: a repo-ROOT file added since the
running image was built is simply absent, and the first live rebuild stopped
with `Parler requirements file missing` for a venv that was the only thing
wrong. The file wins whenever it is there, and a test keeps the two in step.
* **the health check runs the op that fails.** The decoder is convolutional,
so the probe is one `conv1d` on the GPU. A venv that imports but cannot run
it is rebuilt from the pinned file; a CPU-only host passes, because there is
......
......@@ -257,3 +257,52 @@ def test_the_service_survives_a_cudnn_that_cannot_load():
assert "cudnn.enabled = False" in body
assert "not torch.backends.cudnn.enabled" in body, (
"a second failure would retry for ever instead of raising")
def test_the_inline_pins_match_the_requirements_file():
"""Two copies of a pin drift. The file is the source of truth; the inline
list exists only for an install where that file is not present, and it has
to say the same thing."""
import os
import re
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
from codai.api import parler_worker as P
wanted = []
for line in open(os.path.join(root, "requirements-parler.txt"),
encoding="utf-8"):
line = line.strip()
if not line or line.startswith("#"):
continue
wanted.extend(line.split(" ", 1) if line.startswith("--") else [line])
assert list(P._PINS) == wanted, (
f"the inline pins and requirements-parler.txt disagree:\n"
f" file: {wanted}\n inline: {list(P._PINS)}")
def test_a_missing_requirements_file_does_not_block_a_rebuild():
"""The container mounts only codai/, tools/ and third_party/ over the
image, so a repo-root file added since the image was built is absent until
the container is recreated. A self-healing rebuild must not stop for a
reason that has nothing to do with the venv -- which is exactly what
happened on the first live run, and what ACE-Step's "requirements missing"
report turned out to be."""
from pathlib import Path
from codai.api import parler_worker as P
args = venv_env.pip_install_args(Path("/nope/requirements-parler.txt"), P._PINS)
assert "-r" not in args
assert any(a.startswith("torch==") for a in args)
here = Path(__file__).resolve().parents[1] / "requirements-parler.txt"
assert venv_env.pip_install_args(here, P._PINS) == ["-r", str(here)], (
"the file must be preferred whenever it is there")
def test_the_worker_no_longer_refuses_without_the_file():
import os
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
src = open(os.path.join(root, "codai/api/parler_worker.py"),
encoding="utf-8").read()
assert "requirements file missing" not in src
assert "pip_install_args(" in src
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment