longcat: the external training hand-off had never run (v0.2.51)

  ✗ training failed: _lora_dir() missing 1 required positional argument: 'name'

Progress, in that everything before it worked: the Train button built the dataset,
the request carried dataset_config, the server accepted it and dispatched to the
LongCat path. It then died on the first line of _train_externally that does real
work — proof that function had never once executed.

Two bugs in one line. _lora_dir(name) already resolves the per-LoRA directory AND
routes a non-main DDP rank to scratch; the call treated it as the parent and
joined the name on again, so even with the argument supplied it would have written
<loras>/<name>/<name> and bypassed the DDP handling.

The test calls the real _train_externally with only the worker stubbed — no GPU,
no checkpoint, no venv — and checks it reaches the worker, that output_dir equals
_lora_dir(name) with the name not doubled, that the job carries steps/rank/seed/
num_frames/dataset_config, that both refusal paths still refuse, and that the
worker's **kwargs progress callback matches the receiver's signature (a mismatch
there would only show up mid-training).

Verified to FAIL on the real defect: restoring the old line turns 4 of the 6 red
with exactly the TypeError above.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182DhR1eFNPQyGmDrpHbedY
parent 1802b428
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.2.50" __version__ = "0.2.51"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -1912,7 +1912,10 @@ def _train_externally(arch: str, req, base_path, images, instance_prompt, ...@@ -1912,7 +1912,10 @@ def _train_externally(arch: str, req, base_path, images, instance_prompt,
if not os.path.isfile(os.path.expanduser(dataset)): if not os.path.isfile(os.path.expanduser(dataset)):
raise RuntimeError(f"dataset_config does not exist: {dataset}") raise RuntimeError(f"dataset_config does not exist: {dataset}")
out_dir = os.path.join(_lora_dir(), getattr(req, "name", "longcat-lora")) # _lora_dir(name) already resolves the per-LoRA directory — and routes a
# non-main DDP rank to scratch. Joining the name onto it again would have
# written <loras>/<name>/<name> even once the missing argument was supplied.
out_dir = _lora_dir(getattr(req, "name", "longcat-lora"))
work = tempfile.mkdtemp(prefix="longcat-train-") work = tempfile.mkdtemp(prefix="longcat-train-")
job = { job = {
"arch": arch, "arch": arch,
......
"""The hand-off from coderai to the external LongCat trainer.
This function had never executed. Its first real run died on
_lora_dir() missing 1 required positional argument: 'name'
one line in — after the dataset had been built and sent, so every earlier fix was
working and this was simply the next thing in the way. A unit test that calls it
with the worker stubbed would have caught that without a GPU, a checkpoint or a
venv, which is why it exists now.
"""
import json
import pathlib
import sys
import types
import pytest
ROOT = pathlib.Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
loras = pytest.importorskip("codai.api.loras")
# The function imports the worker locally (`from codai.api import
# longcat_worker`), so the module object is what has to be patched.
longcat_worker = pytest.importorskip("codai.api.longcat_worker")
@pytest.fixture
def dataset(tmp_path):
cfg = tmp_path / "data_backend.json"
cfg.write_text(json.dumps([{"id": "x", "dataset_type": "video"}]))
return cfg
def _req(**kw):
kw.setdefault("name", "vfighter_khumalo__longcat")
kw.setdefault("dataset_config", "")
return types.SimpleNamespace(**kw)
def _run(monkeypatch, req, **extra):
"""Call the real function with only the worker replaced."""
seen = {}
def _fake_train(job, workdir, on_progress=None):
seen["job"] = job
seen["workdir"] = workdir
if on_progress:
on_progress(step=1, total=10, message="warming up")
return {"path": job["output_dir"] + "/pytorch_lora_weights.safetensors"}
monkeypatch.setattr(longcat_worker, "train_lora", _fake_train)
monkeypatch.setattr(loras, "_set_progress", lambda **kw: None)
out = loras._train_externally(
extra.pop("arch", "longcat"), req, extra.pop("base_path", "/models/longcat"),
extra.pop("images", []), extra.pop("instance_prompt", "a fighter"),
extra.pop("steps", 800), extra.pop("rank", 8),
extra.pop("resolution", "480x832"), extra.pop("lr", 1e-4),
extra.pop("seed", 42))
return out, seen
def test_it_reaches_the_worker_at_all(monkeypatch, dataset):
"""The regression: it used to raise before ever getting here."""
out, seen = _run(monkeypatch, _req(dataset_config=str(dataset)))
assert seen.get("job"), "the worker was never called"
assert out["trainer"] == "simpletuner"
assert out["arch"] == "longcat"
def test_the_output_directory_is_the_loras_directory_for_that_name(monkeypatch, dataset):
"""_lora_dir(name) IS the per-LoRA directory. Joining the name onto it again
would have written <loras>/<name>/<name>."""
name = "vfighter_khumalo__longcat"
_, seen = _run(monkeypatch, _req(name=name, dataset_config=str(dataset)))
out_dir = pathlib.Path(seen["job"]["output_dir"])
assert out_dir == pathlib.Path(loras._lora_dir(name))
assert out_dir.name == name
assert out_dir.parent.name != name, "the name is doubled in the path"
def test_the_job_carries_what_the_trainer_needs(monkeypatch, dataset):
_, seen = _run(monkeypatch, _req(dataset_config=str(dataset)),
steps=1200, rank=16, seed=7)
job = seen["job"]
assert job["dataset_config"] == str(dataset)
assert job["steps"] == 1200 and job["rank"] == 16 and job["seed"] == 7
assert job["num_frames"] == 93 # 4n+1, one LongCat segment
assert job["arch"] == "longcat"
assert job["base_path"] == "/models/longcat"
def test_a_missing_dataset_is_refused_before_any_work(monkeypatch):
with pytest.raises(RuntimeError, match="dataset_config"):
_run(monkeypatch, _req(dataset_config=""))
def test_a_dataset_path_that_does_not_exist_is_refused(monkeypatch, tmp_path):
with pytest.raises(RuntimeError, match="does not exist"):
_run(monkeypatch, _req(dataset_config=str(tmp_path / "nope.json")))
def test_progress_from_the_worker_is_accepted(monkeypatch, dataset):
"""The worker emits JSON lines as **kwargs; a signature mismatch here would
only surface mid-training."""
got = []
monkeypatch.setattr(longcat_worker, "train_lora",
lambda job, workdir, on_progress=None: (
on_progress(step=3, total=9, message="step"),
{"path": job["output_dir"]})[1])
monkeypatch.setattr(loras, "_set_progress", lambda **kw: got.append(kw))
loras._train_externally("longcat", _req(dataset_config=str(dataset)),
"/models/longcat", [], "p", 10, 4, "480x832", 1e-4, 1)
assert got and got[0]["step"] == 3 and got[0]["total"] == 9
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