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 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
- for upd in range(1, cfg["total_updates"] + 1):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = round(time.time() - t0, 2)
 
 
 
 
 
 
 
 
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
- if upd % cfg["checkpoint_every"] == 0:
576
- ckpt = LOGS_DIR / f"adapter_{run_name}_u{upd:04d}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- actor.model.save_pretrained(str(final_ckpt))
583
- actor.tokenizer.save_pretrained(str(final_ckpt))
 
 
 
 
 
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.")