Spaces:
Running
Running
r1cksync commited on
Commit Β·
ca4df62
1
Parent(s): 8d39d55
feat(train): live tqdm+ETA, per-update HF Hub checkpoint upload, HF Jobs entrypoint + launcher
Browse files- colab/train_lib.py +119 -12
- scripts/HF_JOBS.md +73 -0
- scripts/download_latest_ckpt.py +70 -0
- scripts/hf_job_entrypoint.sh +26 -0
- scripts/launch_hf_job.ps1 +47 -0
- scripts/run_training.py +114 -0
colab/train_lib.py
CHANGED
|
@@ -514,11 +514,65 @@ class PPOTrainer:
|
|
| 514 |
return {k: float(sum(v) / max(len(v), 1)) for k, v in stats.items()}
|
| 515 |
|
| 516 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
# ---------------------------------------------------------------------------
|
| 518 |
# Public entry point.
|
| 519 |
# ---------------------------------------------------------------------------
|
| 520 |
def train_loop(cfg: dict | None = None) -> Path:
|
| 521 |
-
"""Run the full training loop. Returns the path to the JSON log.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
cfg = {**CFG, **(cfg or {})}
|
| 523 |
run_name = cfg["run_name"] or f"run_{int(time.time())}"
|
| 524 |
log_path = LOGS_DIR / f"training_{run_name}.json"
|
|
@@ -526,6 +580,8 @@ def train_loop(cfg: dict | None = None) -> Path:
|
|
| 526 |
print(f"[train] device CUDA?: {torch.cuda.is_available()} "
|
| 527 |
f"name: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else '-'}")
|
| 528 |
|
|
|
|
|
|
|
| 529 |
actor = QwenActor(model_name=cfg["actor_model"],
|
| 530 |
max_seq_len=cfg["max_seq_len"],
|
| 531 |
lora_r=cfg["lora_r"], lora_alpha=cfg["lora_alpha"],
|
|
@@ -545,7 +601,23 @@ def train_loop(cfg: dict | None = None) -> Path:
|
|
| 545 |
log: dict[str, Any] = {"config": cfg, "updates": []}
|
| 546 |
log_path.write_text(json.dumps(log, indent=2))
|
| 547 |
|
| 548 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
t0 = time.time()
|
| 550 |
trans = collector.collect(cfg["rollouts_per_update"])
|
| 551 |
adv_ret = compute_gae(trans, cfg["gamma"], cfg["gae_lambda"])
|
|
@@ -556,10 +628,20 @@ def train_loop(cfg: dict | None = None) -> Path:
|
|
| 556 |
for tr in trans:
|
| 557 |
ep_rewards.setdefault(tr.task_id, []).append(tr.reward)
|
| 558 |
per_ep = {tid: round(sum(rs), 3) for tid, rs in ep_rewards.items()}
|
| 559 |
-
elapsed =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
entry = {
|
| 561 |
"update": upd,
|
| 562 |
-
"elapsed_s": elapsed,
|
|
|
|
|
|
|
| 563 |
"n_transitions": len(trans),
|
| 564 |
"mean_reward": round(sum(t.reward for t in trans) / max(len(trans), 1), 4),
|
| 565 |
"mean_value": round(sum(t.value for t in trans) / max(len(trans), 1), 4),
|
|
@@ -568,19 +650,44 @@ def train_loop(cfg: dict | None = None) -> Path:
|
|
| 568 |
}
|
| 569 |
log["updates"].append(entry)
|
| 570 |
log_path.write_text(json.dumps(log, indent=2))
|
| 571 |
-
print(f"[upd {upd:03d}] reward={entry['mean_reward']:+.3f} "
|
| 572 |
-
f"VΜ={entry['mean_value']:+.3f} loss={stats['loss']:+.3f} "
|
| 573 |
-
f"kl={stats['kl']:+.4f} ({elapsed}s)")
|
| 574 |
|
| 575 |
-
|
| 576 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
actor.model.save_pretrained(str(ckpt))
|
| 578 |
actor.tokenizer.save_pretrained(str(ckpt))
|
| 579 |
-
print(f"[ckpt] saved {ckpt}")
|
|
|
|
| 580 |
|
|
|
|
| 581 |
final_ckpt = LOGS_DIR / f"adapter_{run_name}_final"
|
| 582 |
-
|
| 583 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 584 |
print(f"[done] final adapter -> {final_ckpt}")
|
| 585 |
print(f"[done] JSON log -> {log_path}")
|
| 586 |
return log_path
|
|
|
|
| 514 |
return {k: float(sum(v) / max(len(v), 1)) for k, v in stats.items()}
|
| 515 |
|
| 516 |
|
| 517 |
+
def _fmt_dur(seconds: float) -> str:
|
| 518 |
+
seconds = int(max(0, seconds))
|
| 519 |
+
h, rem = divmod(seconds, 3600)
|
| 520 |
+
m, s = divmod(rem, 60)
|
| 521 |
+
if h: return f"{h}h{m:02d}m{s:02d}s"
|
| 522 |
+
if m: return f"{m}m{s:02d}s"
|
| 523 |
+
return f"{s}s"
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
def _make_hf_pusher(cfg: dict, run_name: str):
|
| 527 |
+
"""Return a callable(local_dir, path_in_repo) that uploads a folder to a
|
| 528 |
+
private HF model repo. No-op if HF push isn't configured."""
|
| 529 |
+
push_user = os.environ.get("IC_PUSH_USER", "").strip()
|
| 530 |
+
if not push_user or not os.environ.get("HF_TOKEN"):
|
| 531 |
+
print("[ckpt-push] disabled (set IC_PUSH_USER + HF_TOKEN to enable).")
|
| 532 |
+
return lambda *_a, **_kw: None
|
| 533 |
+
|
| 534 |
+
try:
|
| 535 |
+
from huggingface_hub import HfApi, create_repo
|
| 536 |
+
except ImportError: # pragma: no cover
|
| 537 |
+
print("[ckpt-push] huggingface_hub missing β skipping push.")
|
| 538 |
+
return lambda *_a, **_kw: None
|
| 539 |
+
|
| 540 |
+
repo = f"{push_user}/incident-commander-actor"
|
| 541 |
+
token = os.environ["HF_TOKEN"]
|
| 542 |
+
create_repo(repo, exist_ok=True, repo_type="model", token=token,
|
| 543 |
+
private=False)
|
| 544 |
+
api = HfApi(token=token)
|
| 545 |
+
print(f"[ckpt-push] enabled β https://huggingface.co/{repo}")
|
| 546 |
+
|
| 547 |
+
def _push(local_dir: Path, path_in_repo: str) -> None:
|
| 548 |
+
try:
|
| 549 |
+
api.upload_folder(
|
| 550 |
+
folder_path=str(local_dir),
|
| 551 |
+
repo_id=repo,
|
| 552 |
+
repo_type="model",
|
| 553 |
+
path_in_repo=path_in_repo,
|
| 554 |
+
commit_message=f"{run_name}: {path_in_repo}",
|
| 555 |
+
run_as_future=False,
|
| 556 |
+
)
|
| 557 |
+
print(f"[ckpt-push] uploaded {path_in_repo}")
|
| 558 |
+
except Exception as exc: # noqa: BLE001
|
| 559 |
+
print(f"[ckpt-push] upload failed for {path_in_repo}: {exc}",
|
| 560 |
+
file=sys.stderr)
|
| 561 |
+
|
| 562 |
+
return _push
|
| 563 |
+
|
| 564 |
+
|
| 565 |
# ---------------------------------------------------------------------------
|
| 566 |
# Public entry point.
|
| 567 |
# ---------------------------------------------------------------------------
|
| 568 |
def train_loop(cfg: dict | None = None) -> Path:
|
| 569 |
+
"""Run the full training loop. Returns the path to the JSON log.
|
| 570 |
+
|
| 571 |
+
Progress + ETA are printed every update. If `IC_PUSH_USER` and `HF_TOKEN`
|
| 572 |
+
are set, every checkpoint **and** the live JSON log are streamed to
|
| 573 |
+
`<user>/incident-commander-actor` on HF Hub β so the moment your compute
|
| 574 |
+
credits run out, the latest checkpoint is already safe in the cloud.
|
| 575 |
+
"""
|
| 576 |
cfg = {**CFG, **(cfg or {})}
|
| 577 |
run_name = cfg["run_name"] or f"run_{int(time.time())}"
|
| 578 |
log_path = LOGS_DIR / f"training_{run_name}.json"
|
|
|
|
| 580 |
print(f"[train] device CUDA?: {torch.cuda.is_available()} "
|
| 581 |
f"name: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else '-'}")
|
| 582 |
|
| 583 |
+
push_ckpt = _make_hf_pusher(cfg, run_name)
|
| 584 |
+
|
| 585 |
actor = QwenActor(model_name=cfg["actor_model"],
|
| 586 |
max_seq_len=cfg["max_seq_len"],
|
| 587 |
lora_r=cfg["lora_r"], lora_alpha=cfg["lora_alpha"],
|
|
|
|
| 601 |
log: dict[str, Any] = {"config": cfg, "updates": []}
|
| 602 |
log_path.write_text(json.dumps(log, indent=2))
|
| 603 |
|
| 604 |
+
# tqdm if available, otherwise a no-op shim.
|
| 605 |
+
try:
|
| 606 |
+
from tqdm.auto import tqdm
|
| 607 |
+
bar = tqdm(total=cfg["total_updates"], desc=f"PPO[{run_name}]",
|
| 608 |
+
unit="upd", dynamic_ncols=True)
|
| 609 |
+
except Exception: # noqa: BLE001
|
| 610 |
+
class _Shim:
|
| 611 |
+
def update(self, *_a, **_kw): pass
|
| 612 |
+
def set_postfix_str(self, *_a, **_kw): pass
|
| 613 |
+
def close(self): pass
|
| 614 |
+
bar = _Shim()
|
| 615 |
+
|
| 616 |
+
total = cfg["total_updates"]
|
| 617 |
+
train_t0 = time.time()
|
| 618 |
+
rolling: list[float] = [] # last-N step times
|
| 619 |
+
|
| 620 |
+
for upd in range(1, total + 1):
|
| 621 |
t0 = time.time()
|
| 622 |
trans = collector.collect(cfg["rollouts_per_update"])
|
| 623 |
adv_ret = compute_gae(trans, cfg["gamma"], cfg["gae_lambda"])
|
|
|
|
| 628 |
for tr in trans:
|
| 629 |
ep_rewards.setdefault(tr.task_id, []).append(tr.reward)
|
| 630 |
per_ep = {tid: round(sum(rs), 3) for tid, rs in ep_rewards.items()}
|
| 631 |
+
elapsed = time.time() - t0
|
| 632 |
+
|
| 633 |
+
rolling.append(elapsed)
|
| 634 |
+
rolling = rolling[-10:] # last 10 updates
|
| 635 |
+
avg_per_upd = sum(rolling) / len(rolling)
|
| 636 |
+
remaining = total - upd
|
| 637 |
+
eta_s = remaining * avg_per_upd
|
| 638 |
+
wall = time.time() - train_t0
|
| 639 |
+
|
| 640 |
entry = {
|
| 641 |
"update": upd,
|
| 642 |
+
"elapsed_s": round(elapsed, 2),
|
| 643 |
+
"wall_s": round(wall, 1),
|
| 644 |
+
"eta_s": round(eta_s, 1),
|
| 645 |
"n_transitions": len(trans),
|
| 646 |
"mean_reward": round(sum(t.reward for t in trans) / max(len(trans), 1), 4),
|
| 647 |
"mean_value": round(sum(t.value for t in trans) / max(len(trans), 1), 4),
|
|
|
|
| 650 |
}
|
| 651 |
log["updates"].append(entry)
|
| 652 |
log_path.write_text(json.dumps(log, indent=2))
|
|
|
|
|
|
|
|
|
|
| 653 |
|
| 654 |
+
bar.set_postfix_str(
|
| 655 |
+
f"r={entry['mean_reward']:+.3f} "
|
| 656 |
+
f"V={entry['mean_value']:+.3f} "
|
| 657 |
+
f"kl={stats['kl']:+.4f} "
|
| 658 |
+
f"upd={_fmt_dur(elapsed)} "
|
| 659 |
+
f"ETA={_fmt_dur(eta_s)}")
|
| 660 |
+
bar.update(1)
|
| 661 |
+
# Always print a line too so non-tty environments (HF Jobs logs,
|
| 662 |
+
# nohup) still show progress.
|
| 663 |
+
print(f"[upd {upd:03d}/{total:03d}] reward={entry['mean_reward']:+.3f} "
|
| 664 |
+
f"VΜ={entry['mean_value']:+.3f} loss={stats['loss']:+.3f} "
|
| 665 |
+
f"kl={stats['kl']:+.4f} upd={_fmt_dur(elapsed)} "
|
| 666 |
+
f"wall={_fmt_dur(wall)} ETA={_fmt_dur(eta_s)}",
|
| 667 |
+
flush=True)
|
| 668 |
+
|
| 669 |
+
# Stream the JSON log to HF every update so even if the pod dies
|
| 670 |
+
# mid-step you can recover the metrics.
|
| 671 |
+
push_ckpt(log_path.parent, "logs")
|
| 672 |
+
|
| 673 |
+
if upd % cfg["checkpoint_every"] == 0 or upd == total:
|
| 674 |
+
ckpt_name = (f"adapter_{run_name}_u{upd:04d}"
|
| 675 |
+
if upd != total else f"adapter_{run_name}_final")
|
| 676 |
+
ckpt = LOGS_DIR / ckpt_name
|
| 677 |
actor.model.save_pretrained(str(ckpt))
|
| 678 |
actor.tokenizer.save_pretrained(str(ckpt))
|
| 679 |
+
print(f"[ckpt] saved {ckpt}", flush=True)
|
| 680 |
+
push_ckpt(ckpt, ckpt_name)
|
| 681 |
|
| 682 |
+
bar.close()
|
| 683 |
final_ckpt = LOGS_DIR / f"adapter_{run_name}_final"
|
| 684 |
+
if not final_ckpt.exists():
|
| 685 |
+
actor.model.save_pretrained(str(final_ckpt))
|
| 686 |
+
actor.tokenizer.save_pretrained(str(final_ckpt))
|
| 687 |
+
push_ckpt(final_ckpt, final_ckpt.name)
|
| 688 |
+
|
| 689 |
+
total_wall = time.time() - train_t0
|
| 690 |
+
print(f"[done] total wall: {_fmt_dur(total_wall)}")
|
| 691 |
print(f"[done] final adapter -> {final_ckpt}")
|
| 692 |
print(f"[done] JSON log -> {log_path}")
|
| 693 |
return log_path
|
scripts/HF_JOBS.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Train on Hugging Face Jobs (real GPU, billed by the second)
|
| 2 |
+
|
| 3 |
+
Goal: launch a real GPU run from your laptop, watch live progress, and never lose a checkpoint even if the credits run out.
|
| 4 |
+
|
| 5 |
+
## What you get
|
| 6 |
+
- **Live progress bar + ETA** every PPO update (`[upd 027/120] reward=+0.31 V=+0.18 kl=+0.0042 upd=18s wall=8m12s ETA=27m11s`).
|
| 7 |
+
- **Checkpoints uploaded to HF Hub** every 20 updates (and at the end). If your $30 credit dies mid-run, the latest adapter is already at `https://huggingface.co/<you>/incident-commander-actor`.
|
| 8 |
+
- **Rolling JSON log** also uploaded after every update β you can refresh the HF page and see metrics live.
|
| 9 |
+
|
| 10 |
+
## One-time setup (on your laptop)
|
| 11 |
+
```powershell
|
| 12 |
+
pip install -U "huggingface_hub[cli]"
|
| 13 |
+
hf auth login # paste your hf_β¦ token
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
## Launch the run
|
| 17 |
+
Pick **one** of these GPU flavors (cheapest β fastest):
|
| 18 |
+
|
| 19 |
+
| Flavor | Price | 120-update run | Notes |
|
| 20 |
+
|-----------------|--------|----------------|-------|
|
| 21 |
+
| `l4x1` | ~$0.80/hr | ~25β35 min | **Recommended** β best $/perf. ~$0.50 total. |
|
| 22 |
+
| `a10g-large` | ~$1.05/hr | ~20β30 min | Slightly faster, slightly more expensive. |
|
| 23 |
+
| `a100-large` | ~$3.40/hr | ~8β12 min | Fastest, biggest dent in credits. |
|
| 24 |
+
|
| 25 |
+
```powershell
|
| 26 |
+
$env:HF_TOKEN = "<paste-your-hf_-token-here>"
|
| 27 |
+
$env:IC_PUSH_USER = "sagnik-mukherjee" # checkpoints land here
|
| 28 |
+
$env:IC_REPO_URL = "https://github.com/r1cksync/meta-rl-hack.git"
|
| 29 |
+
|
| 30 |
+
hf jobs run `
|
| 31 |
+
--flavor l4x1 `
|
| 32 |
+
--secret HF_TOKEN=$env:HF_TOKEN `
|
| 33 |
+
--env IC_PUSH_USER=$env:IC_PUSH_USER `
|
| 34 |
+
--env IC_REPO_URL=$env:IC_REPO_URL `
|
| 35 |
+
--env IC_TOTAL_UPDATES=120 `
|
| 36 |
+
--env IC_ROLLOUTS=6 `
|
| 37 |
+
--env IC_RUN_NAME=hfjob01 `
|
| 38 |
+
--env HF_HUB_ENABLE_HF_TRANSFER=1 `
|
| 39 |
+
--image "huggingface/transformers-pytorch-gpu:latest" `
|
| 40 |
+
-- bash -c "git clone --depth 1 `$IC_REPO_URL /workspace/ic && cd /workspace/ic && bash scripts/hf_job_entrypoint.sh"
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
> The `--secret` flag injects HF_TOKEN at runtime so it never lands in HF Hub history. The `--env` flags are visible in the job UI but contain no secrets.
|
| 44 |
+
|
| 45 |
+
## Watch progress
|
| 46 |
+
The `hf jobs run` command streams logs to your terminal. To detach + reattach:
|
| 47 |
+
```powershell
|
| 48 |
+
hf jobs run --detach ... # prints a job ID
|
| 49 |
+
hf jobs logs <job-id> --follow
|
| 50 |
+
hf jobs ps # list running jobs + spend so far
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
You'll see a `tqdm` bar plus a per-update line so even non-TTY logs are readable. Look for `ETA=` to know how much wall-time is left.
|
| 54 |
+
|
| 55 |
+
## Recover if credits die mid-run
|
| 56 |
+
Every 20 updates the adapter is uploaded to:
|
| 57 |
+
```
|
| 58 |
+
https://huggingface.co/sagnik-mukherjee/incident-commander-actor
|
| 59 |
+
```
|
| 60 |
+
under paths like `adapter_hfjob01_u0040/`, `adapter_hfjob01_u0060/`, etc. The training log streams to `logs/training_hfjob01.json` every update. So even if the job is killed at update 67 you still have:
|
| 61 |
+
- the u0060 adapter (LoRA + tokenizer)
|
| 62 |
+
- the partial log up through update 67
|
| 63 |
+
|
| 64 |
+
To resume locally:
|
| 65 |
+
```powershell
|
| 66 |
+
hf download sagnik-mukherjee/incident-commander-actor adapter_hfjob01_u0060 --local-dir .\resume
|
| 67 |
+
```
|
| 68 |
+
Then point the trainer at the `--resume-from` adapter (planned, not wired yet β for now you'd start fresh from the saved adapter as the actor).
|
| 69 |
+
|
| 70 |
+
## Cost guardrails
|
| 71 |
+
- The `Qwen2.5-72B-Instruct` critic runs on **HF Inference Providers**, billed against the same $30. Calls are cached, so a 120-update run is typically ~$1β2 in critic spend.
|
| 72 |
+
- Training compute on `l4x1` for ~30 min β **$0.40**.
|
| 73 |
+
- **Total budget**: ~$2β3 for the full real run, leaving ~$27 for re-runs or longer training.
|
scripts/download_latest_ckpt.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Download the latest checkpoint from the HF Hub model repo.
|
| 2 |
+
|
| 3 |
+
Use this if your HF Jobs run died mid-way and you want the most recent
|
| 4 |
+
adapter on your laptop.
|
| 5 |
+
|
| 6 |
+
python scripts/download_latest_ckpt.py --user sagnik-mukherjee
|
| 7 |
+
--run hfjob01
|
| 8 |
+
--out ./resume
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def main() -> None:
|
| 19 |
+
ap = argparse.ArgumentParser()
|
| 20 |
+
ap.add_argument("--user", required=True,
|
| 21 |
+
help="HF username (e.g. sagnik-mukherjee)")
|
| 22 |
+
ap.add_argument("--run", default=None,
|
| 23 |
+
help="Run name (e.g. hfjob01). If omitted, picks latest.")
|
| 24 |
+
ap.add_argument("--out", default="./resume",
|
| 25 |
+
help="Local directory to write the adapter into.")
|
| 26 |
+
ap.add_argument("--prefer-final", action="store_true",
|
| 27 |
+
help="Pick *_final if present, else newest u#### dir.")
|
| 28 |
+
args = ap.parse_args()
|
| 29 |
+
|
| 30 |
+
if not os.environ.get("HF_TOKEN"):
|
| 31 |
+
sys.exit("HF_TOKEN env var required.")
|
| 32 |
+
|
| 33 |
+
from huggingface_hub import HfApi, snapshot_download
|
| 34 |
+
|
| 35 |
+
repo = f"{args.user}/incident-commander-actor"
|
| 36 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 37 |
+
print(f"Listing files in {repo} β¦")
|
| 38 |
+
files = api.list_repo_files(repo, repo_type="model")
|
| 39 |
+
|
| 40 |
+
# Top-level checkpoint dirs look like adapter_<run>_u0040/... or
|
| 41 |
+
# adapter_<run>_final/...
|
| 42 |
+
prefix = "adapter_"
|
| 43 |
+
if args.run:
|
| 44 |
+
prefix = f"adapter_{args.run}_"
|
| 45 |
+
candidates = sorted({f.split("/")[0] for f in files
|
| 46 |
+
if f.startswith(prefix)})
|
| 47 |
+
if not candidates:
|
| 48 |
+
sys.exit(f"No checkpoint dirs found under prefix '{prefix}*'.")
|
| 49 |
+
|
| 50 |
+
if args.prefer_final and any(c.endswith("_final") for c in candidates):
|
| 51 |
+
chosen = [c for c in candidates if c.endswith("_final")][-1]
|
| 52 |
+
else:
|
| 53 |
+
# Sort so u0120 > u0100 > _final lexically β pick the lexicographically
|
| 54 |
+
# largest, which works because u#### are zero-padded.
|
| 55 |
+
chosen = candidates[-1]
|
| 56 |
+
|
| 57 |
+
out = Path(args.out) / chosen
|
| 58 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 59 |
+
print(f"Downloading {chosen} β {out}")
|
| 60 |
+
snapshot_download(
|
| 61 |
+
repo_id=repo, repo_type="model",
|
| 62 |
+
allow_patterns=[f"{chosen}/*"],
|
| 63 |
+
local_dir=str(Path(args.out)),
|
| 64 |
+
token=os.environ["HF_TOKEN"],
|
| 65 |
+
)
|
| 66 |
+
print(f"Done. Adapter at: {out}")
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
if __name__ == "__main__":
|
| 70 |
+
main()
|
scripts/hf_job_entrypoint.sh
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# HF Jobs entrypoint. Clones the repo, installs deps, runs the real training.
|
| 3 |
+
# Invoked by `hf jobs run` β see scripts/HF_JOBS.md for the exact command.
|
| 4 |
+
set -euo pipefail
|
| 5 |
+
|
| 6 |
+
REPO_URL="${IC_REPO_URL:-https://github.com/r1cksync/meta-rl-hack.git}"
|
| 7 |
+
WORK="/workspace/incident-commander"
|
| 8 |
+
|
| 9 |
+
echo "[hfjob] === stage 1: clone ==="
|
| 10 |
+
git clone --depth 1 "$REPO_URL" "$WORK"
|
| 11 |
+
cd "$WORK"
|
| 12 |
+
|
| 13 |
+
echo "[hfjob] === stage 2: install ==="
|
| 14 |
+
python -m pip install -q --upgrade pip
|
| 15 |
+
python -m pip install -q --no-deps \
|
| 16 |
+
"unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
|
| 17 |
+
python -m pip install -q --no-deps \
|
| 18 |
+
"xformers<0.0.27" trl peft accelerate bitsandbytes
|
| 19 |
+
python -m pip install -q "huggingface_hub>=0.25" "pydantic>=2,<3" httpx
|
| 20 |
+
|
| 21 |
+
echo "[hfjob] === stage 3: train ==="
|
| 22 |
+
python scripts/run_training.py
|
| 23 |
+
|
| 24 |
+
echo "[hfjob] === stage 4: artifacts ==="
|
| 25 |
+
ls -la colab/logs/ || true
|
| 26 |
+
ls -la rl-agent/replays/ || true
|
scripts/launch_hf_job.ps1
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# One-line launcher for the HF Jobs training run.
|
| 2 |
+
#
|
| 3 |
+
# Usage:
|
| 4 |
+
# $env:HF_TOKEN = "hf_..." # required
|
| 5 |
+
# $env:IC_PUSH_USER = "sagnik-mukherjee" # required if you want checkpoints pushed
|
| 6 |
+
# ./scripts/launch_hf_job.ps1 # uses defaults (l4x1, 120 updates, 6 rollouts)
|
| 7 |
+
#
|
| 8 |
+
# Optional overrides:
|
| 9 |
+
# $env:IC_GPU_FLAVOR = "a10g-large" # default l4x1
|
| 10 |
+
# $env:IC_TOTAL_UPDATES = 200 # default 120
|
| 11 |
+
# $env:IC_REPO_URL = "https://github.com/<you>/<repo>.git"
|
| 12 |
+
|
| 13 |
+
$ErrorActionPreference = "Stop"
|
| 14 |
+
|
| 15 |
+
if (-not $env:HF_TOKEN) { throw "HF_TOKEN env var is required." }
|
| 16 |
+
if (-not $env:IC_PUSH_USER) { Write-Warning "IC_PUSH_USER not set β checkpoints WILL NOT be pushed." }
|
| 17 |
+
|
| 18 |
+
$flavor = if ($env:IC_GPU_FLAVOR) { $env:IC_GPU_FLAVOR } else { "l4x1" }
|
| 19 |
+
$updates = if ($env:IC_TOTAL_UPDATES) { $env:IC_TOTAL_UPDATES } else { "120" }
|
| 20 |
+
$rollouts = if ($env:IC_ROLLOUTS) { $env:IC_ROLLOUTS } else { "6" }
|
| 21 |
+
$repo = if ($env:IC_REPO_URL) { $env:IC_REPO_URL } else { "https://github.com/r1cksync/meta-rl-hack.git" }
|
| 22 |
+
$run = if ($env:IC_RUN_NAME) { $env:IC_RUN_NAME } else { "hfjob_$(Get-Date -Format yyyyMMdd_HHmm)" }
|
| 23 |
+
|
| 24 |
+
Write-Host "Launching HF Job:"
|
| 25 |
+
Write-Host " GPU flavor : $flavor"
|
| 26 |
+
Write-Host " Run name : $run"
|
| 27 |
+
Write-Host " Updates : $updates Γ $rollouts rollouts"
|
| 28 |
+
Write-Host " Repo : $repo"
|
| 29 |
+
Write-Host " Push user : $($env:IC_PUSH_USER)"
|
| 30 |
+
|
| 31 |
+
$cmd = @(
|
| 32 |
+
"git clone --depth 1 `$IC_REPO_URL /workspace/ic",
|
| 33 |
+
"cd /workspace/ic",
|
| 34 |
+
"bash scripts/hf_job_entrypoint.sh"
|
| 35 |
+
) -join " && "
|
| 36 |
+
|
| 37 |
+
hf jobs run `
|
| 38 |
+
--flavor $flavor `
|
| 39 |
+
--secret "HF_TOKEN=$env:HF_TOKEN" `
|
| 40 |
+
--env "IC_PUSH_USER=$env:IC_PUSH_USER" `
|
| 41 |
+
--env "IC_REPO_URL=$repo" `
|
| 42 |
+
--env "IC_TOTAL_UPDATES=$updates" `
|
| 43 |
+
--env "IC_ROLLOUTS=$rollouts" `
|
| 44 |
+
--env "IC_RUN_NAME=$run" `
|
| 45 |
+
--env "HF_HUB_ENABLE_HF_TRANSFER=1" `
|
| 46 |
+
--image "huggingface/transformers-pytorch-gpu:latest" `
|
| 47 |
+
-- bash -c "$cmd"
|
scripts/run_training.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Non-notebook entrypoint for the IncidentCommander RL training run.
|
| 2 |
+
|
| 3 |
+
Designed to be invoked by `hf jobs run` (or any plain `python` shell). Reads
|
| 4 |
+
configuration from env vars so the same script works in Colab, HF Jobs, and
|
| 5 |
+
local docker.
|
| 6 |
+
|
| 7 |
+
Required env vars:
|
| 8 |
+
HF_TOKEN - HF token with Read scope (Write if pushing adapters)
|
| 9 |
+
|
| 10 |
+
Optional env vars:
|
| 11 |
+
IC_TOTAL_UPDATES - default 120
|
| 12 |
+
IC_ROLLOUTS - default 6
|
| 13 |
+
IC_MAX_STEPS - default 16
|
| 14 |
+
IC_RUN_NAME - default "hfjob01"
|
| 15 |
+
IC_PUSH_USER - if set, pushes adapter+logs to <user>/incident-commander-actor
|
| 16 |
+
IC_CRITIC_MODEL - default "Qwen/Qwen2.5-72B-Instruct"
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import logging
|
| 21 |
+
import os
|
| 22 |
+
import sys
|
| 23 |
+
import warnings
|
| 24 |
+
from pathlib import Path
|
| 25 |
+
|
| 26 |
+
# Silence the noisy Qwen FutureWarning before importing transformers.
|
| 27 |
+
warnings.filterwarnings("ignore", category=FutureWarning, module="transformers")
|
| 28 |
+
warnings.filterwarnings("ignore", message=".*max_new_tokens.*max_length.*")
|
| 29 |
+
warnings.filterwarnings("ignore", message=".*attention mask API.*")
|
| 30 |
+
logging.getLogger("transformers").setLevel(logging.ERROR)
|
| 31 |
+
|
| 32 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 33 |
+
sys.path.insert(0, str(ROOT))
|
| 34 |
+
sys.path.insert(0, str(ROOT / "rl-agent"))
|
| 35 |
+
|
| 36 |
+
# Mock AWS so the simulator runs without boto3 creds.
|
| 37 |
+
os.environ.setdefault("INCIDENT_COMMANDER_MOCK", "true")
|
| 38 |
+
|
| 39 |
+
if not os.environ.get("HF_TOKEN"):
|
| 40 |
+
sys.exit("HF_TOKEN env var is required.")
|
| 41 |
+
os.environ.setdefault("HUGGING_FACE_HUB_TOKEN", os.environ["HF_TOKEN"])
|
| 42 |
+
|
| 43 |
+
from huggingface_hub import login, whoami # noqa: E402
|
| 44 |
+
login(os.environ["HF_TOKEN"], add_to_git_credential=False)
|
| 45 |
+
print(f"[hfjob] logged in as {whoami(token=os.environ['HF_TOKEN']).get('name')}")
|
| 46 |
+
|
| 47 |
+
from colab.train_lib import CFG, train_loop # noqa: E402
|
| 48 |
+
|
| 49 |
+
CFG.update({
|
| 50 |
+
"total_updates": int(os.environ.get("IC_TOTAL_UPDATES", 120)),
|
| 51 |
+
"rollouts_per_update": int(os.environ.get("IC_ROLLOUTS", 6)),
|
| 52 |
+
"max_steps_per_ep": int(os.environ.get("IC_MAX_STEPS", 16)),
|
| 53 |
+
"checkpoint_every": int(os.environ.get("IC_CKPT_EVERY", 20)),
|
| 54 |
+
"critic_provider": "hf",
|
| 55 |
+
"critic_model": os.environ.get("IC_CRITIC_MODEL",
|
| 56 |
+
"Qwen/Qwen2.5-72B-Instruct"),
|
| 57 |
+
"lr": 1e-5,
|
| 58 |
+
"kl_coef": 0.02,
|
| 59 |
+
"clip_eps": 0.20,
|
| 60 |
+
"gae_lambda": 0.92,
|
| 61 |
+
"run_name": os.environ.get("IC_RUN_NAME", "hfjob01"),
|
| 62 |
+
"tasks": [
|
| 63 |
+
"sim_easy_lambda_throttle_001", "sim_easy_lambda_throttle_010",
|
| 64 |
+
"sim_med_eb_lambda_016", "sim_med_eb_lambda_021",
|
| 65 |
+
"sim_hard_apigw_chain_001", "sim_hard_ddb_chain_021",
|
| 66 |
+
"sim_hard_iam_chain_011",
|
| 67 |
+
"sim_advanced_cascade_users_db_001",
|
| 68 |
+
"sim_advanced_runbook_trap_postgres_001",
|
| 69 |
+
"sim_advanced_trolley_orders_db_001",
|
| 70 |
+
"sim_advanced_saboteur_duel_001",
|
| 71 |
+
"sim_advanced_slack_redherring_001",
|
| 72 |
+
"sim_gen_app_leak_checkout_007", "sim_gen_app_leak_payments_019",
|
| 73 |
+
"sim_gen_db_duel_users_db_003", "sim_gen_db_duel_orders_db_015",
|
| 74 |
+
"sim_gen_redherring_payments_013", "sim_gen_redherring_auth_001",
|
| 75 |
+
"sim_gen_cascade_payments_db_004", "sim_gen_cascade_users_db_023",
|
| 76 |
+
"sim_gen_cache_warm_session_cache_004",
|
| 77 |
+
"sim_gen_peak_frontend_001",
|
| 78 |
+
"sim_gen_restore_payments_db_001",
|
| 79 |
+
],
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
print(f"[hfjob] starting run '{CFG['run_name']}': "
|
| 83 |
+
f"{CFG['total_updates']}Γ{CFG['rollouts_per_update']}Γ{CFG['max_steps_per_ep']} "
|
| 84 |
+
f"(~{CFG['total_updates'] * CFG['rollouts_per_update'] * CFG['max_steps_per_ep']:,} transitions)")
|
| 85 |
+
|
| 86 |
+
log_path = train_loop()
|
| 87 |
+
print(f"[hfjob] training log β {log_path}")
|
| 88 |
+
|
| 89 |
+
# ββ Optional: push artifacts to a HF model repo βββββββββββββββββββββββββ
|
| 90 |
+
push_user = os.environ.get("IC_PUSH_USER", "").strip()
|
| 91 |
+
if push_user:
|
| 92 |
+
import glob
|
| 93 |
+
from huggingface_hub import HfApi, create_repo
|
| 94 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 95 |
+
repo = f"{push_user}/incident-commander-actor"
|
| 96 |
+
create_repo(repo, exist_ok=True, repo_type="model",
|
| 97 |
+
token=os.environ["HF_TOKEN"])
|
| 98 |
+
finals = sorted(glob.glob(str(ROOT / "colab" / "logs" / "adapter_*_final")))
|
| 99 |
+
if finals:
|
| 100 |
+
api.upload_folder(folder_path=finals[-1], repo_id=repo,
|
| 101 |
+
repo_type="model", path_in_repo="adapter")
|
| 102 |
+
api.upload_folder(folder_path=str(ROOT / "colab" / "logs"),
|
| 103 |
+
repo_id=repo, repo_type="model", path_in_repo="logs",
|
| 104 |
+
allow_patterns=["*.json"])
|
| 105 |
+
replay_dir = ROOT / "rl-agent" / "replays"
|
| 106 |
+
if replay_dir.exists():
|
| 107 |
+
api.upload_folder(folder_path=str(replay_dir), repo_id=repo,
|
| 108 |
+
repo_type="model", path_in_repo="replays",
|
| 109 |
+
allow_patterns=["*.html"])
|
| 110 |
+
print(f"[hfjob] pushed β https://huggingface.co/{repo}")
|
| 111 |
+
else:
|
| 112 |
+
print("[hfjob] IC_PUSH_USER not set β skipping HF push.")
|
| 113 |
+
|
| 114 |
+
print("[hfjob] done.")
|