File size: 8,022 Bytes
ca4df62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a116aa4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca4df62
 
 
2824f8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
01853a1
 
 
 
 
 
 
 
 
2824f8c
c1e7f70
 
2824f8c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca4df62
 
 
 
 
01853a1
 
 
ca4df62
 
 
 
 
 
 
2824f8c
 
ca4df62
 
 
 
 
 
 
 
 
 
 
a116aa4
 
 
ca4df62
 
 
a116aa4
ca4df62
a116aa4
ca4df62
 
 
 
 
 
 
 
 
 
 
 
 
 
a116aa4
ca4df62
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
"""Non-notebook entrypoint for the IncidentCommander RL training run.

Designed to be invoked by `hf jobs run` (or any plain `python` shell). Reads
configuration from env vars so the same script works in Colab, HF Jobs, and
local docker.

Required env vars:
    HF_TOKEN              - HF token with Read scope (Write if pushing adapters)

Optional env vars:
    IC_TOTAL_UPDATES      - default 120
    IC_ROLLOUTS           - default 6
    IC_MAX_STEPS          - default 16
    IC_RUN_NAME           - default "hfjob01"
    IC_PUSH_USER          - if set, pushes adapter+logs to <user>/incident-commander-actor
    IC_CRITIC_MODEL       - default "Qwen/Qwen2.5-72B-Instruct"
"""
from __future__ import annotations

import logging
import os
import sys
import warnings
from pathlib import Path

# Silence the noisy Qwen FutureWarning before importing transformers.
warnings.filterwarnings("ignore", category=FutureWarning, module="transformers")
warnings.filterwarnings("ignore", message=".*max_new_tokens.*max_length.*")
warnings.filterwarnings("ignore", message=".*attention mask API.*")
logging.getLogger("transformers").setLevel(logging.ERROR)

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "rl-agent"))

# Mock AWS so the simulator runs without boto3 creds.
os.environ.setdefault("INCIDENT_COMMANDER_MOCK", "true")

# HF_TOKEN is OPTIONAL β€” only needed if pushing checkpoints/adapters to the
# Hub. Training itself works fully offline (e.g. on Kaggle with attached
# Models). When absent we just skip login and disable Hub upload below.
_hf_token = os.environ.get("HF_TOKEN", "").strip()
if _hf_token:
    os.environ.setdefault("HUGGING_FACE_HUB_TOKEN", _hf_token)
    from huggingface_hub import login, whoami                   # noqa: E402
    try:
        login(_hf_token, add_to_git_credential=False)
        print(f"[hfjob] logged in as {whoami(token=_hf_token).get('name')}")
    except Exception as _exc:                                   # noqa: BLE001
        print(f"[hfjob] HF login failed ({_exc}); continuing offline")
        _hf_token = ""
else:
    print("[hfjob] no HF_TOKEN set β€” running offline (no Hub upload)")

from colab.train_lib import CFG, train_loop                     # noqa: E402

# ── Task list selection ────────────────────────────────────────────────
import glob as _glob
import json as _json

TASK_MODE = os.environ.get("IC_TASK_MODE", "curated").lower()
_SCEN_ROOT = ROOT / "rl-agent" / "scenarios" / "sim"

_CURATED = [
    "sim_easy_lambda_throttle_001", "sim_easy_lambda_throttle_010",
    "sim_med_eb_lambda_016",       "sim_med_eb_lambda_021",
    "sim_hard_apigw_chain_001",    "sim_hard_ddb_chain_021",
    "sim_hard_iam_chain_011",
    "sim_advanced_cascade_users_db_001",
    "sim_advanced_runbook_trap_postgres_001",
    "sim_advanced_trolley_orders_db_001",
    "sim_advanced_saboteur_duel_001",
    "sim_advanced_slack_redherring_001",
    "sim_gen_app_leak_checkout_007",   "sim_gen_app_leak_payments_019",
    "sim_gen_db_duel_users_db_003",    "sim_gen_db_duel_orders_db_015",
    "sim_gen_redherring_payments_013", "sim_gen_redherring_auth_001",
    "sim_gen_cascade_payments_db_004", "sim_gen_cascade_users_db_023",
    "sim_gen_cache_warm_session_cache_004",
    "sim_gen_peak_frontend_001",
    "sim_gen_restore_payments_db_001",
]

def _discover_all_task_ids():
    ids = []
    for p in _glob.glob(str(_SCEN_ROOT / "**" / "*.json"), recursive=True):
        try:
            data = _json.loads(open(p, encoding="utf-8").read())
            tid = data.get("task_id") or data.get("id")
            if tid:
                ids.append(tid)
        except Exception:                                       # noqa: BLE001
            pass
    return sorted(set(ids))

if TASK_MODE == "all":
    tasks = _discover_all_task_ids()
elif TASK_MODE == "hard":
    tasks = [t for t in _discover_all_task_ids()
             if t.startswith("sim_hard_") or t.startswith("sim_advanced_")]
else:
    tasks = _CURATED

# Optional sharding: take every Nth task starting at SHARD index. Used for
# splitting the corpus across parallel Kaggle notebooks (no warm-start needed
# between them; outputs can be averaged at the end).
_n_shards = int(os.environ.get("IC_TASK_SHARDS", 1))
_shard    = int(os.environ.get("IC_TASK_SHARD", 0))
if _n_shards > 1:
    tasks = [t for i, t in enumerate(tasks) if i % _n_shards == _shard]
    print(f"[hfjob] sharded {_shard}/{_n_shards} β†’ {len(tasks)} tasks")

print(f"[hfjob] task_mode={TASK_MODE}  count={len(tasks)}")
print(f"[hfjob] first 5 task ids: {tasks[:5]}")
print(f"[hfjob] last  5 task ids: {tasks[-5:]}")

# ── Optional warm-start from a prior phase's HF model repo ─────────────
init_repo = os.environ.get("IC_INIT_ADAPTER_REPO", "").strip()
init_subfolder = os.environ.get("IC_INIT_ADAPTER_SUBFOLDER", "adapter").strip()
init_path = None
if init_repo:
    print(f"[hfjob] warm-start: downloading {init_repo}/{init_subfolder} ...")
    from huggingface_hub import snapshot_download
    local_dir = snapshot_download(
        repo_id=init_repo, repo_type="model",
        allow_patterns=[f"{init_subfolder}/*"],
    )
    init_path = str(Path(local_dir) / init_subfolder)
    print(f"[hfjob] warm-start adapter at {init_path}")

CFG.update({
    "total_updates":       int(os.environ.get("IC_TOTAL_UPDATES", 120)),
    "rollouts_per_update": int(os.environ.get("IC_ROLLOUTS", 6)),
    "max_steps_per_ep":    int(os.environ.get("IC_MAX_STEPS", 16)),
    "checkpoint_every":    int(os.environ.get("IC_CKPT_EVERY", 20)),
    "actor_model":         os.environ.get("IC_ACTOR_MODEL",
                                          CFG.get("actor_model")),
    "critic_provider":     os.environ.get("IC_CRITIC_PROVIDER", "hf"),
    "critic_model":        os.environ.get("IC_CRITIC_MODEL",
                                          "Qwen/Qwen2.5-72B-Instruct"),
    "lr":                  1e-5,
    "kl_coef":             0.02,
    "clip_eps":            0.20,
    "gae_lambda":          0.92,
    "run_name":            os.environ.get("IC_RUN_NAME", "hfjob01"),
    "tasks":               tasks,
    "init_adapter_path":   init_path,
})

print(f"[hfjob] starting run '{CFG['run_name']}': "
      f"{CFG['total_updates']}Γ—{CFG['rollouts_per_update']}Γ—{CFG['max_steps_per_ep']} "
      f"(~{CFG['total_updates'] * CFG['rollouts_per_update'] * CFG['max_steps_per_ep']:,} transitions)")

log_path = train_loop()
print(f"[hfjob] training log β†’ {log_path}")

# ── Optional: push artifacts to a HF model repo ─────────────────────────
push_user = os.environ.get("IC_PUSH_USER", "").strip()
if push_user and not _hf_token:
    print("[hfjob] IC_PUSH_USER set but no HF_TOKEN β€” skipping push.")
    push_user = ""
if push_user:
    import glob
    from huggingface_hub import HfApi, create_repo
    api  = HfApi(token=_hf_token)
    repo = f"{push_user}/incident-commander-actor"
    create_repo(repo, exist_ok=True, repo_type="model", token=_hf_token)
    finals = sorted(glob.glob(str(ROOT / "colab" / "logs" / "adapter_*_final")))
    if finals:
        api.upload_folder(folder_path=finals[-1], repo_id=repo,
                          repo_type="model", path_in_repo="adapter")
    api.upload_folder(folder_path=str(ROOT / "colab" / "logs"),
                      repo_id=repo, repo_type="model", path_in_repo="logs",
                      allow_patterns=["*.json"])
    replay_dir = ROOT / "rl-agent" / "replays"
    if replay_dir.exists():
        api.upload_folder(folder_path=str(replay_dir), repo_id=repo,
                          repo_type="model", path_in_repo="replays",
                          allow_patterns=["*.html"])
    print(f"[hfjob] pushed β†’ https://huggingface.co/{repo}")
else:
    print("[hfjob] skipping HF push (no IC_PUSH_USER or no HF_TOKEN).")

print("[hfjob] done.")