fix(jobs): address claude review findings on remote training

Resolve the claude[bot] review on #3856:

- Reject reward-model training under --job.target with a clear error instead
  of crashing on a None policy inside build_remote_config_file.
- Support --policy.path remote runs: validate() no longer requires repo_id for
  remote runs (it is auto-generated in submit_to_hf), and repo_id/push_to_hub
  are now set after validate() resolves the policy.
- Narrow the bare `except Exception` in _tail_logs/_poll_until_done to
  (OSError, httpx.HTTPError) so programming errors surface instead of being
  silently retried or counted as job failures.
- Install the SIGINT detach handler only on the main thread.
- Generate model repo timestamps in UTC.
This commit is contained in:
Nicolas Rabault
2026-06-25 16:11:06 +02:00
parent 6b64642bdb
commit ab69bc5f06
3 changed files with 92 additions and 19 deletions
+50 -3
View File
@@ -18,6 +18,7 @@ import threading
from types import SimpleNamespace
import draccus
import httpx
import pytest
from lerobot.configs.train import TrainPipelineConfig
@@ -58,9 +59,9 @@ def test_poll_until_done_exits_when_done_already_set(monkeypatch):
assert _poll_until_done("j", done, poll_interval=0.01) is None
def test_poll_until_done_gives_up_after_repeated_failures(monkeypatch):
def test_poll_until_done_gives_up_after_repeated_network_failures(monkeypatch):
monkeypatch.setattr(
"lerobot.jobs.hf.inspect_job", lambda job_id: (_ for _ in ()).throw(RuntimeError("boom"))
"lerobot.jobs.hf.inspect_job", lambda job_id: (_ for _ in ()).throw(httpx.ConnectError("boom"))
)
done = threading.Event()
result = _poll_until_done("j", done, poll_interval=0.001, max_failures=3)
@@ -68,6 +69,14 @@ def test_poll_until_done_gives_up_after_repeated_failures(monkeypatch):
assert done.is_set()
def test_poll_until_done_propagates_programming_errors(monkeypatch):
"""A bug (e.g. TypeError) must surface, not be silently retried as a transient failure."""
monkeypatch.setattr("lerobot.jobs.hf.inspect_job", lambda job_id: (_ for _ in ()).throw(TypeError("bug")))
done = threading.Event()
with pytest.raises(TypeError):
_poll_until_done("j", done, poll_interval=0.001, max_failures=3)
def test_resolve_wandb_key_from_env(monkeypatch):
monkeypatch.setenv("WANDB_API_KEY", "abc123")
assert resolve_wandb_api_key() == "abc123"
@@ -121,6 +130,23 @@ def _minimal_cfg():
)
def test_validate_skips_repo_id_check_for_remote():
"""Remote runs auto-assign repo_id in submit_to_hf, so validate() must not demand it up front."""
cfg = _minimal_cfg() # remote target, push_to_hub default True, no explicit repo_id
assert cfg.policy.repo_id is None
cfg.validate() # must not raise
def test_validate_requires_repo_id_for_local_push():
"""Local runs that push to the Hub still need an explicit repo_id."""
cfg = draccus.parse(
TrainPipelineConfig,
args=["--dataset.repo_id", "u/d", "--policy.type", "act"],
)
with pytest.raises(ValueError, match="repo_id"):
cfg.validate()
def test_build_remote_config_applies_overrides(tmp_path):
cfg = _minimal_cfg()
dest = tmp_path / "train_config.json"
@@ -192,7 +218,7 @@ def test_submit_requires_login(monkeypatch):
def test_submit_passes_validation_and_submits(monkeypatch):
"""Regression: repo_id must be set BEFORE cfg.validate() or validation raises."""
"""A type-based policy with no explicit repo_id is auto-assigned one and submitted."""
from unittest.mock import MagicMock
# Patch get_token
@@ -261,6 +287,27 @@ def test_submit_passes_validation_and_submits(monkeypatch):
assert call["labels"].get("lerobot") == "true"
def test_submit_rejects_reward_model_training(monkeypatch):
"""Remote training only supports policies; reward-model runs fail fast with a clear error."""
monkeypatch.setattr("lerobot.jobs.hf.get_token", lambda: "tok")
class FakeHfApi:
def __init__(self, token=None):
pass
def whoami(self, token=None):
return {"name": "alice"}
monkeypatch.setattr("lerobot.jobs.hf.HfApi", FakeHfApi)
cfg = _minimal_cfg()
cfg.reward_model = SimpleNamespace(type="reward") # marks this as reward-model training
monkeypatch.setattr(cfg, "validate", lambda: None) # skip pretrained-path resolution
with pytest.raises(ValueError, match="reward model"):
submit_to_hf(cfg)
@pytest.mark.timeout(15)
def test_submit_returns_when_job_completes(monkeypatch):
"""Non-detach path must RETURN (not hang) once the job reaches a terminal stage."""