Spaces:
Sleeping
Sleeping
update files
Browse files- README.md +250 -113
- jobs/Dockerfile +70 -0
- jobs/README.md +236 -0
- jobs/eval_in_job.sh +169 -0
- jobs/launch_hf_job.py +226 -0
- jobs/run_in_job.sh +169 -0
- jobs/schedule_eval_job.py +272 -0
- reward_manager/episode.py +189 -0
- reward_manager/long_horizon_reward.py +225 -0
README.md
CHANGED
|
@@ -7,168 +7,293 @@ sdk: docker
|
|
| 7 |
app_port: 8000
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
-
# Important Links
|
| 11 |
-
1) [BLOG.md](https://huggingface.co/spaces/Meta-HF-hackathon/updated-policy/blob/main/BLOG.md)
|
| 12 |
-
2) Logger (Used HF Jobs, attaching logs and code used to run them)
|
| 13 |
-
3) [Environment link](https://huggingface.co/spaces/Meta-HF-hackathon/updated-policy/)
|
| 14 |
-
# 🚨 SRE Incident Response Simulator
|
| 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 |
-
│ │ Cache │ │ DB │ │ Queue │ │
|
| 45 |
-
│ └─────────┘ └─────────┘ └─────────┘ │
|
| 46 |
-
└──────────────────────────────────────────────────────────────────┘
|
| 47 |
```
|
| 48 |
|
| 49 |
-
7 services with reactive metrics, logs, alerts, and dependency-aware cascade propagation.
|
| 50 |
-
|
| 51 |
---
|
| 52 |
|
| 53 |
-
## Action
|
| 54 |
|
| 55 |
-
###
|
| 56 |
|
| 57 |
| Action | Category | Description |
|
| 58 |
-
|---
|
| 59 |
-
| `view_alerts` |
|
| 60 |
-
| `query_logs` |
|
| 61 |
-
| `check_metrics` |
|
| 62 |
-
| `check_dependencies` |
|
| 63 |
-
| `check_deploy_history` |
|
| 64 |
-
| `run_health_check` |
|
| 65 |
-
| `restart_service` |
|
| 66 |
-
| `rollback_deploy` |
|
| 67 |
-
| `scale_service` |
|
| 68 |
-
| `declare_root_cause` |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
---
|
| 77 |
|
| 78 |
-
## Observation
|
| 79 |
|
| 80 |
-
The agent **never** sees: `fault_type`, `is_bad` deploy flag,
|
| 81 |
|
| 82 |
It **does** see:
|
| 83 |
-
|
| 84 |
-
-
|
| 85 |
-
-
|
| 86 |
-
-
|
| 87 |
-
-
|
| 88 |
-
-
|
| 89 |
-
-
|
|
|
|
|
|
|
| 90 |
|
| 91 |
---
|
| 92 |
|
| 93 |
-
## Tasks
|
| 94 |
|
| 95 |
-
| Task |
|
| 96 |
-
|---
|
| 97 |
-
| `memory_leak` |
|
| 98 |
-
| `cascading_failure` |
|
| 99 |
-
| `distributed_deadlock` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
---
|
| 102 |
|
| 103 |
-
## Reward
|
| 104 |
-
|
| 105 |
-
### Layer 1
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
|
| 109 |
-
|
|
| 110 |
-
| Diagnostic |
|
| 111 |
-
|
|
| 112 |
-
|
|
| 113 |
-
| Remediation |
|
| 114 |
-
| Remediation |
|
| 115 |
-
|
|
| 116 |
-
| Declaration |
|
| 117 |
-
|
|
| 118 |
-
|
|
| 119 |
-
| Completion |
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
| 126 |
-
|
|
| 127 |
-
|
|
| 128 |
-
|
|
| 129 |
-
|
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
---
|
| 133 |
|
| 134 |
-
## Quick
|
| 135 |
|
| 136 |
-
###
|
| 137 |
|
| 138 |
```bash
|
| 139 |
-
# Install dependencies
|
| 140 |
-
cd incident_env
|
| 141 |
pip install -e .
|
|
|
|
|
|
|
| 142 |
|
| 143 |
-
|
| 144 |
-
uvicorn incident_env.server.app:app --host 0.0.0.0 --port 8000
|
| 145 |
-
|
| 146 |
-
# Test endpoints
|
| 147 |
curl http://localhost:8000/health
|
| 148 |
-
curl -X POST http://localhost:8000/reset
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
```
|
| 151 |
|
| 152 |
-
### Run
|
| 153 |
|
| 154 |
```bash
|
| 155 |
-
export OPENAI_API_KEY=sk-...
|
| 156 |
-
export MODEL_NAME=gpt-4o-mini
|
| 157 |
export ENV_BASE_URL=http://localhost:8000
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
```
|
| 161 |
|
| 162 |
### Docker
|
| 163 |
|
| 164 |
```bash
|
| 165 |
-
docker build -t incident-env
|
| 166 |
docker run -p 8000:8000 incident-env
|
| 167 |
```
|
| 168 |
|
| 169 |
---
|
| 170 |
|
| 171 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
```
|
| 174 |
Agent: POST /reset {"task_name": "memory_leak"}
|
|
@@ -176,21 +301,33 @@ Agent: POST /reset {"task_name": "memory_leak"}
|
|
| 176 |
→ Services: orders=degraded, rest=healthy
|
| 177 |
|
| 178 |
Agent: POST /step {"action_type": "view_alerts"}
|
| 179 |
-
→ 3 alerts: orders HighMemoryUsage (critical),
|
| 180 |
→ reward = +0.13
|
| 181 |
|
| 182 |
Agent: POST /step {"action_type": "check_metrics", "target_service": "orders"}
|
| 183 |
-
→ 30 data points: memory climbing
|
| 184 |
→ reward = +0.13
|
| 185 |
|
| 186 |
Agent: POST /step {"action_type": "check_deploy_history", "target_service": "orders"}
|
| 187 |
-
→
|
| 188 |
→ reward = +0.13
|
| 189 |
|
| 190 |
Agent: POST /step {"action_type": "rollback_deploy", "target_service": "orders"}
|
| 191 |
-
→ "Rolled back orders
|
| 192 |
→ reward = +0.28
|
| 193 |
|
| 194 |
-
Agent: POST /step {"action_type": "declare_root_cause",
|
|
|
|
| 195 |
→ Episode done. Final grade: 0.97
|
| 196 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
app_port: 8000
|
| 8 |
pinned: false
|
| 9 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
# 🚨 SRE Triage Bot — OpenEnv Incident Response Simulator
|
| 12 |
|
| 13 |
+
> An OpenEnv environment + a four-stage GRPO pipeline that turns **Qwen2.5-7B-Instruct** into a working SRE triage agent. Runs against a reactive, partially-observable microservices simulation with two phases: **ops investigation** (logs, metrics, alerts, deploy history) and **code attribution** (sandboxed mini-repo with git log + diffs).
|
| 14 |
|
| 15 |
+
---
|
| 16 |
|
| 17 |
+
## 🔗 Important Links
|
| 18 |
+
|
| 19 |
+
| Resource | Link |
|
| 20 |
+
| --- | --- |
|
| 21 |
+
| 📝 **Blog post (full write-up)** | [`BLOG.md`](https://huggingface.co/spaces/Meta-HF-hackathon/updated-policy/blob/main/BLOG.md) |
|
| 22 |
+
| 🛰️ **Live environment (HF Space)** | [Meta-HF-hackathon/updated-policy](https://huggingface.co/spaces/Meta-HF-hackathon/updated-policy/) |
|
| 23 |
+
| 🧠 **Merged model (deployable)** | [`Yaswanth-Bolla/qwen-merged`](https://huggingface.co/Yaswanth-Bolla/qwen-merged) |
|
| 24 |
+
| 🧩 **LoRA adapter (post-GRPO)** | [`daemongg/qwen2.5-7b-sre-grpo`](https://huggingface.co/daemongg/qwen2.5-7b-sre-grpo) |
|
| 25 |
+
| 🏗️ **Base model** | [`Qwen/Qwen2.5-7B-Instruct`](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct) |
|
| 26 |
+
| 📒 **Training logs + scripts** | [`./logger/`](./logger/) |
|
| 27 |
+
| 📊 **Ablation results** | [`./ablation.md`](./ablation.md) |
|
| 28 |
+
|
| 29 |
+
> ⚠️ **Note on training infrastructure.** We ran the full pipeline (SFT, GRPO, merge) on **HuggingFace Jobs** (A100-40GB) instead of a Colab notebook — Colab's free + Pro tiers OOM'd on the 7B base + reference model + GRPO group buffers. The **complete training logs and the exact scripts we executed** are committed under [`./logger/`](./logger/) (`sft_finetune.log`, `grpo_finetune.log`, `merge.log`, `trajectory.log`, `ablation.log`, plus the `.py` scripts that produced them) so the run is reproducible end-to-end.
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 🎯 What this submission delivers
|
| 34 |
+
|
| 35 |
+
- A novel **two-phase POMDP** environment with hierarchical, masked actions (10 ops actions + 7 code actions).
|
| 36 |
+
- A **two-layer reward** — dense oracle-shaped per-step signal for training, oracle-independent grader for evaluation.
|
| 37 |
+
- A **counterfactual cross-phase reward** (`r_cross`) that makes joint training meaningful.
|
| 38 |
+
- A four-pool **curriculum** (A → B → C, with held-out D) executed via on-policy **GRPO** with a variance gate and `r_cross` warmup.
|
| 39 |
+
- **Real measured improvement**: mean cumulative reward **≈1.59 (RL) vs ≈0.49 (base)** at less than half the steps. See `BLOG.md` §7 and `ablation.md`.
|
| 40 |
|
| 41 |
---
|
| 42 |
|
| 43 |
+
## 📐 Environment at a glance
|
| 44 |
+
|
| 45 |
+
A **Partially-Observable Markov Decision Process** over a reactive microservices simulator. The agent never sees the root cause — it sees *symptoms*: climbing memory, cascading errors, firing alerts. It must gather evidence, transition to code attribution, and propose a patch — exactly like an on-call SRE at 3 AM.
|
| 46 |
+
|
| 47 |
+
| Dimension | Detail |
|
| 48 |
+
|---|---|
|
| 49 |
+
| **Observation** | Alerts · metric timeseries · structured logs · dependency graphs · deploy history · sandboxed repo tree + git log |
|
| 50 |
+
| **Action space** | Phase 1: 10 ops actions × 7 services. Phase 2: 5 code-exploration + 2 terminal actions. |
|
| 51 |
+
| **Difficulty** | Easy (single-service leak) → Medium (cascade) → Hard (distributed deadlock) → 5 research tasks → 2 held-out compounds |
|
| 52 |
+
| **Reward** | Oracle-shaped per-step signal for training + oracle-independent grader for eval + counterfactual `r_cross` |
|
| 53 |
+
| **Realism** | Reactive simulation — memory climbs, cascades propagate, restarts don't fix root causes |
|
| 54 |
+
|
| 55 |
+
### Topology
|
| 56 |
|
| 57 |
```
|
| 58 |
+
┌─────────┐ ┌─────┐ ┌────────┐ ┌─────────┐
|
| 59 |
+
│ API GW │──►│Auth │──► │ Orders │──►│ Payment │
|
| 60 |
+
└────┬────┘ └─────┘ └───┬────┘ └────┬────┘
|
| 61 |
+
▼ ▼ ▼
|
| 62 |
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
| 63 |
+
│ Cache │ │ DB │ │ Queue │
|
| 64 |
+
└─────────┘ └─────────┘ └─────────┘
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
```
|
| 66 |
|
|
|
|
|
|
|
| 67 |
---
|
| 68 |
|
| 69 |
+
## 🔧 Action space (hierarchical + masked)
|
| 70 |
|
| 71 |
+
### Phase 1 — ops investigation
|
| 72 |
|
| 73 |
| Action | Category | Description |
|
| 74 |
+
|---|---|---|
|
| 75 |
+
| `view_alerts` | diagnostic | List firing alerts |
|
| 76 |
+
| `query_logs` | diagnostic | Service logs (level/keyword filters) |
|
| 77 |
+
| `check_metrics` | diagnostic | 30-min metric time series |
|
| 78 |
+
| `check_dependencies` | diagnostic | Up/downstream dependency map |
|
| 79 |
+
| `check_deploy_history` | diagnostic | Recent deploys per service |
|
| 80 |
+
| `run_health_check` | diagnostic | Ping a service |
|
| 81 |
+
| `restart_service` | remediation | Temporary fix |
|
| 82 |
+
| `rollback_deploy` | remediation | Real fix if root cause |
|
| 83 |
+
| `scale_service` | remediation | More replicas |
|
| 84 |
+
| `declare_root_cause` | terminal | Diagnosis string |
|
| 85 |
+
| `transition_to_phase2` | control | Hand off to code attribution |
|
| 86 |
+
|
| 87 |
+
### Phase 2 — code attribution
|
| 88 |
+
|
| 89 |
+
| Action | What it returns |
|
| 90 |
+
|---|---|
|
| 91 |
+
| `list_dir` | Files + subdirs at relative path |
|
| 92 |
+
| `read_file` | Up to 64 KB of file contents |
|
| 93 |
+
| `search_code` | grep across the tree (≤50 hits) |
|
| 94 |
+
| `get_git_log` | Commit metadata for a path |
|
| 95 |
+
| `get_file_diff` | Unified diff for `(commit_sha, path)` |
|
| 96 |
+
| `propose_patch` | Terminal — submit a unified diff |
|
| 97 |
+
| `declare_no_change` | Terminal — for spurious-issue scenarios |
|
| 98 |
+
|
| 99 |
+
> **Action masking:** every observation includes `valid_actions[]`. Illegal actions (e.g. rollback on a service with no deploy history) cost `-0.05` and are recorded for analysis.
|
| 100 |
|
| 101 |
---
|
| 102 |
|
| 103 |
+
## 👁️ Observation space (POMDP)
|
| 104 |
|
| 105 |
+
The agent **never** sees: `fault_type`, `is_bad` deploy flag, internal simulation state.
|
| 106 |
|
| 107 |
It **does** see:
|
| 108 |
+
|
| 109 |
+
- Incident summary + severity (`SEV1` / `SEV2` / `SEV3`)
|
| 110 |
+
- Service statuses (`healthy` / `degraded` / `down`)
|
| 111 |
+
- Active alert count
|
| 112 |
+
- Action result (data from the most recent action)
|
| 113 |
+
- `valid_actions[]` (action mask)
|
| 114 |
+
- Time elapsed / budget (SLA pressure)
|
| 115 |
+
- Cumulative reward and step count
|
| 116 |
+
- `current_phase` ∈ {1, 2}
|
| 117 |
|
| 118 |
---
|
| 119 |
|
| 120 |
+
## 📋 Tasks (10 scenarios, 4 pools)
|
| 121 |
|
| 122 |
+
| Task | Difficulty | Hidden lesson |
|
| 123 |
+
|---|---|---|
|
| 124 |
+
| `memory_leak` | easy | Single service, noisy metric — restart only buys minutes |
|
| 125 |
+
| `cascading_failure` | medium | Loud services aren't the cause — walk the dep graph |
|
| 126 |
+
| `distributed_deadlock` | hard | Three remediation actions in a specific order |
|
| 127 |
+
| `aliased_fault` | research | Symptoms alias across fault families |
|
| 128 |
+
| `severity_inversion` | research | SEV1 page, two-line code fix |
|
| 129 |
+
| `confidence_inversion` | research | Loud alerts on the wrong service |
|
| 130 |
+
| `info_ordering` | research | Decisive evidence shows up *late* |
|
| 131 |
+
| `circuit_breaker_noop` | research | Spurious issue — `declare_no_change` is correct |
|
| 132 |
+
| `heldout_aliased_severity` | held-out | Compound; never seen during training |
|
| 133 |
+
| `heldout_confidence_ordering` | held-out | Compound; never seen during training |
|
| 134 |
+
|
| 135 |
+
Pools: **A** (`p1_only`), **B** (`p2_only` with oracle handoff), **C** (`joint` with `r_cross`), **D** (held-out generalisation).
|
| 136 |
|
| 137 |
---
|
| 138 |
|
| 139 |
+
## 🎁 Reward design (two layers)
|
| 140 |
+
|
| 141 |
+
### Layer 1 — per-step shaped reward (training only)
|
| 142 |
+
|
| 143 |
+
| Action | Condition | Reward |
|
| 144 |
+
|---|---|---|
|
| 145 |
+
| Diagnostic | involved service | +0.15 |
|
| 146 |
+
| Diagnostic | uninvolved service | +0.05 |
|
| 147 |
+
| Any | repeat | −0.05 |
|
| 148 |
+
| Remediation | correct target (root cause svc) | +0.30 |
|
| 149 |
+
| Remediation | helpful (affected, not root) | +0.10 |
|
| 150 |
+
| Remediation | harmful (healthy svc) | −0.15 |
|
| 151 |
+
| Declaration | correct root cause | +0.40 |
|
| 152 |
+
| Declaration | wrong root cause | −0.20 |
|
| 153 |
+
| Any | per-step efficiency cost | −0.02 |
|
| 154 |
+
| Completion | all services healthy | +0.20 |
|
| 155 |
+
| Completion | budget exceeded | −0.10 |
|
| 156 |
+
|
| 157 |
+
### Layer 2 — oracle-independent grader (evaluation)
|
| 158 |
+
|
| 159 |
+
| Component | Weight | Measures |
|
| 160 |
+
|---|---|---|
|
| 161 |
+
| `p1_rca` | 25 % | Did the agent declare the correct root cause? |
|
| 162 |
+
| `p1_efficiency` | 15 % | Fewer steps to declare = better |
|
| 163 |
+
| `patch_quality` | 35 % | File overlap (Jaccard) + AST hunk similarity + syntax validity |
|
| 164 |
+
| `no_change_detection` | 25 % | Correct `declare_no_change` on spurious-issue scenarios |
|
| 165 |
+
| `p2_efficiency` | 25 % | Phase-2 step efficiency (replaces `no_change` slot when valid issue) |
|
| 166 |
+
|
| 167 |
+
Plus the counterfactual cross-phase reward:
|
| 168 |
+
|
| 169 |
+
```
|
| 170 |
+
r_cross(τ) = max(0, r_code(τ_2 | context(τ_1)) − r_code(τ_2 | ∅))
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
---
|
| 174 |
+
|
| 175 |
+
## 📈 Headline result
|
| 176 |
+
|
| 177 |
+
| Model | Mean cumulative reward (≈30 steps) | Steps to plateau | σ at plateau |
|
| 178 |
+
|---|---|---|---|
|
| 179 |
+
| Base (Qwen2.5-7B-Instruct) | ~0.20 | never within 60 | wide |
|
| 180 |
+
| SFT (LoRA) | ~0.95 | ~50 | medium |
|
| 181 |
+
| **Post-trained (GRPO + merge)** | **~1.59** | **~25** | **tight** |
|
| 182 |
+
|
| 183 |
+
Full plots, ablations, and component breakdown in [`BLOG.md`](./BLOG.md) §7–8.
|
| 184 |
|
| 185 |
---
|
| 186 |
|
| 187 |
+
## 🚀 Quick start
|
| 188 |
|
| 189 |
+
### Run the environment locally
|
| 190 |
|
| 191 |
```bash
|
|
|
|
|
|
|
| 192 |
pip install -e .
|
| 193 |
+
uvicorn server.app:app --host 0.0.0.0 --port 8000
|
| 194 |
+
```
|
| 195 |
|
| 196 |
+
```bash
|
|
|
|
|
|
|
|
|
|
| 197 |
curl http://localhost:8000/health
|
| 198 |
+
curl -X POST http://localhost:8000/reset \
|
| 199 |
+
-H "Content-Type: application/json" \
|
| 200 |
+
-d '{"task_name": "memory_leak"}'
|
| 201 |
+
curl -X POST http://localhost:8000/step \
|
| 202 |
+
-H "Content-Type: application/json" \
|
| 203 |
+
-d '{"action_type": "view_alerts"}'
|
| 204 |
```
|
| 205 |
|
| 206 |
+
### Run the trained agent
|
| 207 |
|
| 208 |
```bash
|
|
|
|
|
|
|
| 209 |
export ENV_BASE_URL=http://localhost:8000
|
| 210 |
+
python inference.py --model Yaswanth-Bolla/qwen-merged
|
| 211 |
+
```
|
| 212 |
+
|
| 213 |
+
### Run the agent against a real GitHub issue + repo
|
| 214 |
|
| 215 |
+
```bash
|
| 216 |
+
python inference_agent.py \
|
| 217 |
+
--model Yaswanth-Bolla/qwen-merged \
|
| 218 |
+
--repo /path/to/cloned/repo \
|
| 219 |
+
--issue https://github.com/owner/repo/issues/42
|
| 220 |
```
|
| 221 |
|
| 222 |
### Docker
|
| 223 |
|
| 224 |
```bash
|
| 225 |
+
docker build -t incident-env .
|
| 226 |
docker run -p 8000:8000 incident-env
|
| 227 |
```
|
| 228 |
|
| 229 |
---
|
| 230 |
|
| 231 |
+
## 🏋️ Reproducing the training run
|
| 232 |
+
|
| 233 |
+
We ran every stage on **HuggingFace Jobs** (A100-40GB) — see [`./logger/`](./logger/) for the exact scripts and their full stdout.
|
| 234 |
+
|
| 235 |
+
```bash
|
| 236 |
+
# Stage 1 — collect baseline trajectories (HF Inference API)
|
| 237 |
+
python sre_finetune_collector.py # → sre_*_dataset.jsonl
|
| 238 |
+
|
| 239 |
+
# Stage 2 — LoRA SFT via TRL
|
| 240 |
+
python sft.py \
|
| 241 |
+
--model_name_or_path Qwen/Qwen2.5-7B-Instruct \
|
| 242 |
+
--dataset_name <your-sft-dataset> \
|
| 243 |
+
--use_peft --lora_r 32 --lora_alpha 16 \
|
| 244 |
+
--learning_rate 2e-4 --num_train_epochs 1 \
|
| 245 |
+
--packing --eos_token '<|im_end|>' \
|
| 246 |
+
--output_dir Qwen2.5-7B-SRE-SFT --push_to_hub
|
| 247 |
+
|
| 248 |
+
# Stage 3+4 — online GRPO (Pool A → B → C)
|
| 249 |
+
python training/grpo_train.py \
|
| 250 |
+
--model <your-sft-checkpoint> \
|
| 251 |
+
--stages 2 3 4 \
|
| 252 |
+
--group_size 4 --episodes_per_task 64 \
|
| 253 |
+
--use_lora --lora_r 16 --lora_alpha 32 \
|
| 254 |
+
--push_to_hub daemongg/qwen2.5-7b-sre-grpo
|
| 255 |
+
|
| 256 |
+
# Stage 5 — merge LoRA into base
|
| 257 |
+
python merge.py
|
| 258 |
+
```
|
| 259 |
+
|
| 260 |
+
Logs from these exact runs:
|
| 261 |
+
|
| 262 |
+
| Stage | Log |
|
| 263 |
+
|---|---|
|
| 264 |
+
| Trajectory collection | [`logger/trajectory.log`](./logger/trajectory.log) |
|
| 265 |
+
| SFT | [`logger/sft_finetune.log`](./logger/sft_finetune.log) |
|
| 266 |
+
| GRPO | [`logger/grpo_finetune.log`](./logger/grpo_finetune.log) |
|
| 267 |
+
| Merge | [`logger/merge.log`](./logger/merge.log) |
|
| 268 |
+
| Ablations | [`logger/ablation.log`](./logger/ablation.log) |
|
| 269 |
+
|
| 270 |
+
---
|
| 271 |
+
|
| 272 |
+
## 🗂️ Repository layout
|
| 273 |
+
|
| 274 |
+
```
|
| 275 |
+
.
|
| 276 |
+
├── BLOG.md # Full write-up (start here)
|
| 277 |
+
├── README.md # This file
|
| 278 |
+
├── ablation.md # Ablation results table
|
| 279 |
+
├── openenv.yaml # OpenEnv spec
|
| 280 |
+
├── server/ # FastAPI server + IncidentEnvironment + CodeWorkspace
|
| 281 |
+
├── scenarios/ # 10 scenarios, code-context registry, P2 grader
|
| 282 |
+
├── simulation/ # Reactive infra: services, metrics, logs, alerts
|
| 283 |
+
├── snapshots/ # 8 mini-repo snapshots for Phase 2 (tree + git log + diffs)
|
| 284 |
+
├── training/ # GRPO trainer, curriculum, variance gate, segment-GRPO loss
|
| 285 |
+
├── sft.py # TRL SFTTrainer entry point
|
| 286 |
+
├── merge.py # peft.merge_and_unload + push_to_hub
|
| 287 |
+
├── inference.py # Run any LLM against the env
|
| 288 |
+
├── inference_agent.py # Run the trained agent against a real repo + GitHub issue
|
| 289 |
+
├── sre_finetune_collector.py # Stage-1 trajectory collector
|
| 290 |
+
├── assets/ # Diagrams + result figures (referenced from BLOG.md)
|
| 291 |
+
└── logger/ # ★ Full HF Jobs logs + the scripts that produced them
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
---
|
| 295 |
+
|
| 296 |
+
## 💬 Example interaction
|
| 297 |
|
| 298 |
```
|
| 299 |
Agent: POST /reset {"task_name": "memory_leak"}
|
|
|
|
| 301 |
→ Services: orders=degraded, rest=healthy
|
| 302 |
|
| 303 |
Agent: POST /step {"action_type": "view_alerts"}
|
| 304 |
+
→ 3 alerts: orders HighMemoryUsage (critical), HighErrorRate, HighLatencyP99
|
| 305 |
→ reward = +0.13
|
| 306 |
|
| 307 |
Agent: POST /step {"action_type": "check_metrics", "target_service": "orders"}
|
| 308 |
+
→ 30 data points: memory climbing 35 % → 78 % over 20 min
|
| 309 |
→ reward = +0.13
|
| 310 |
|
| 311 |
Agent: POST /step {"action_type": "check_deploy_history", "target_service": "orders"}
|
| 312 |
+
→ v2.3.1 (20 min ago, "batch order processing") · v1.2.0
|
| 313 |
→ reward = +0.13
|
| 314 |
|
| 315 |
Agent: POST /step {"action_type": "rollback_deploy", "target_service": "orders"}
|
| 316 |
+
→ "Rolled back orders v2.3.1 → v1.2.0 — service recovering"
|
| 317 |
→ reward = +0.28
|
| 318 |
|
| 319 |
+
Agent: POST /step {"action_type": "declare_root_cause",
|
| 320 |
+
"parameters": {"root_cause": "memory leak in orders caused by bad deploy v2.3.1"}}
|
| 321 |
→ Episode done. Final grade: 0.97
|
| 322 |
```
|
| 323 |
+
|
| 324 |
+
---
|
| 325 |
+
|
| 326 |
+
## 📜 License & credits
|
| 327 |
+
|
| 328 |
+
- Environment, training scripts, scenarios: this repo.
|
| 329 |
+
- Base model: `Qwen/Qwen2.5-7B-Instruct` (Apache-2.0).
|
| 330 |
+
- Trainer: HuggingFace TRL (`SFTTrainer`) and our on-policy GRPO loop in `training/grpo_train.py`.
|
| 331 |
+
- Built for the **OpenEnv hackathon** — see [`RULES.md`](./RULES.md).
|
| 332 |
+
|
| 333 |
+
For the full story, results, and ablations, read [`BLOG.md`](./BLOG.md).
|
jobs/Dockerfile
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Docker image for HF Jobs post-training of incident_env.
|
| 2 |
+
#
|
| 3 |
+
# Runs three processes inside one container:
|
| 4 |
+
# 1. uvicorn incident_env server (:8000)
|
| 5 |
+
# 2. vLLM OpenAI-compatible server (:8080)
|
| 6 |
+
# 3. verl-agent trainer (foreground)
|
| 7 |
+
#
|
| 8 |
+
# Built on the official NVIDIA PyTorch + CUDA base so vLLM works.
|
| 9 |
+
# Push to a registry (e.g. ghcr.io or Docker Hub) or build as an HF Space
|
| 10 |
+
# image; HF Jobs accepts both.
|
| 11 |
+
|
| 12 |
+
FROM pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel
|
| 13 |
+
|
| 14 |
+
ENV DEBIAN_FRONTEND=noninteractive \
|
| 15 |
+
PYTHONUNBUFFERED=1 \
|
| 16 |
+
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
| 17 |
+
PIP_NO_CACHE_DIR=1 \
|
| 18 |
+
HF_HUB_ENABLE_HF_TRANSFER=1
|
| 19 |
+
|
| 20 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 21 |
+
git curl ca-certificates build-essential \
|
| 22 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 23 |
+
|
| 24 |
+
WORKDIR /workspace
|
| 25 |
+
|
| 26 |
+
# ----- incident_env (this repo) -------------------------------------------
|
| 27 |
+
# The job entrypoint script pulls the latest commit at runtime, but we
|
| 28 |
+
# bake a copy in so initial startup is fast.
|
| 29 |
+
ARG INCIDENT_REPO=https://github.com/your-org/scaler-hackathon.git
|
| 30 |
+
ARG INCIDENT_REF=main
|
| 31 |
+
RUN git clone --depth 1 --branch "${INCIDENT_REF}" "${INCIDENT_REPO}" /workspace/incident_env || \
|
| 32 |
+
(mkdir -p /workspace/incident_env && echo "Clone failed; entrypoint will retry")
|
| 33 |
+
|
| 34 |
+
# ----- verl-agent ---------------------------------------------------------
|
| 35 |
+
ARG VERL_REPO=https://github.com/langfengQ/verl-agent.git
|
| 36 |
+
ARG VERL_REF=main
|
| 37 |
+
RUN git clone --depth 1 --branch "${VERL_REF}" "${VERL_REPO}" /workspace/verl-agent || \
|
| 38 |
+
(mkdir -p /workspace/verl-agent && echo "Clone failed; entrypoint will retry")
|
| 39 |
+
|
| 40 |
+
# ----- Python deps --------------------------------------------------------
|
| 41 |
+
# Install inside the PyTorch base image; keep versions pinned to what
|
| 42 |
+
# verl-agent + vLLM 0.6.x expect.
|
| 43 |
+
RUN pip install --upgrade pip && \
|
| 44 |
+
pip install \
|
| 45 |
+
"vllm==0.6.3.post1" \
|
| 46 |
+
"transformers>=4.45" \
|
| 47 |
+
"accelerate>=1.0" \
|
| 48 |
+
"peft>=0.13" \
|
| 49 |
+
"trl>=0.12" \
|
| 50 |
+
"datasets>=3.0" \
|
| 51 |
+
"fastapi>=0.104" "uvicorn[standard]>=0.24" "pydantic>=2" \
|
| 52 |
+
"ray[default]>=2.37" \
|
| 53 |
+
"wandb" "huggingface_hub[hf_transfer]" "httpx" "requests" \
|
| 54 |
+
"openenv-core>=0.1.0" || true
|
| 55 |
+
|
| 56 |
+
# Install both repos in editable mode where possible.
|
| 57 |
+
RUN pip install -e /workspace/incident_env || true && \
|
| 58 |
+
pip install -e /workspace/verl-agent || true
|
| 59 |
+
|
| 60 |
+
COPY run_in_job.sh /usr/local/bin/run_in_job.sh
|
| 61 |
+
COPY eval_in_job.sh /usr/local/bin/eval_in_job.sh
|
| 62 |
+
RUN chmod +x /usr/local/bin/run_in_job.sh /usr/local/bin/eval_in_job.sh
|
| 63 |
+
|
| 64 |
+
ENV INCIDENT_REPO_DIR=/workspace/incident_env \
|
| 65 |
+
VERL_AGENT_REPO_DIR=/workspace/verl-agent \
|
| 66 |
+
INCIDENT_REWARD_MODE=long_horizon_v2 \
|
| 67 |
+
ENV_PORT=8000 \
|
| 68 |
+
VLLM_PORT=8080
|
| 69 |
+
|
| 70 |
+
CMD ["/usr/local/bin/run_in_job.sh"]
|
jobs/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Post-training on HF Jobs (GPU)
|
| 2 |
+
|
| 3 |
+
Run verl-agent GiGPO post-training for `incident_env` on a Hugging Face
|
| 4 |
+
Jobs GPU (single A100-80GB recommended). Docs:
|
| 5 |
+
https://huggingface.co/docs/huggingface_hub/guides/jobs
|
| 6 |
+
|
| 7 |
+
## Hardware & scope
|
| 8 |
+
|
| 9 |
+
HF Jobs gives you **one container** on **one GPU** (no Ray multi-node,
|
| 10 |
+
no multi-GPU tensor parallelism). Practical model sizes on the biggest
|
| 11 |
+
flavor `a100-large` (A100-80GB):
|
| 12 |
+
|
| 13 |
+
| Model | LoRA rank | Fits? | Notes |
|
| 14 |
+
|---|---|---|---|
|
| 15 |
+
| Qwen2.5-7B-Instruct | 16 | yes | **default** — `configs/incident_env_gigpo_7b_a100.yaml` |
|
| 16 |
+
| Qwen2.5-14B-Instruct | 32 | tight | needs `gpu_memory_utilization=0.35`, `offload=true`; may OOM with vLLM + FSDP in one box |
|
| 17 |
+
|
| 18 |
+
For Qwen2.5-14B use the Northflank H100 path (`training/entrypoint.sh`).
|
| 19 |
+
|
| 20 |
+
Flavors (from HF docs): `t4-small`, `t4-medium`, `l4x1`, `l4x4`,
|
| 21 |
+
`a10g-small`, `a10g-large`, `a10g-largex2`, `a10g-largex4`,
|
| 22 |
+
`a100-large`.
|
| 23 |
+
|
| 24 |
+
HF Jobs is gated to **Pro / Team / Enterprise** accounts.
|
| 25 |
+
|
| 26 |
+
## Files
|
| 27 |
+
|
| 28 |
+
| File | Role |
|
| 29 |
+
|---|---|
|
| 30 |
+
| `Dockerfile` | Builds the training/eval image (CUDA + PyTorch + vLLM + this repo + verl-agent) |
|
| 31 |
+
| `run_in_job.sh` | Training entrypoint: boots env-server + vLLM + verl-agent trainer |
|
| 32 |
+
| `eval_in_job.sh` | Eval entrypoint: boots env-server + vLLM (with LoRA) + `scripts/eval_incident_env.py` |
|
| 33 |
+
| `launch_hf_job.py` | CLI → `huggingface_hub.run_job` (training) |
|
| 34 |
+
| `schedule_eval_job.py` | CLI → `create_scheduled_job` (recurring eval) |
|
| 35 |
+
| `../configs/incident_env_gigpo_7b_a100.yaml` | Single-A100-tuned GiGPO config |
|
| 36 |
+
|
| 37 |
+
## Step 1 — Build & push the image
|
| 38 |
+
|
| 39 |
+
HF Jobs accepts any Docker image from a registry it can pull from.
|
| 40 |
+
|
| 41 |
+
### Option A: Docker Hub
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
cd jobs
|
| 45 |
+
docker build \
|
| 46 |
+
--build-arg INCIDENT_REPO=https://github.com/<you>/scaler-hackathon.git \
|
| 47 |
+
--build-arg INCIDENT_REF=main \
|
| 48 |
+
-t docker.io/<you>/incident-env-job:latest .
|
| 49 |
+
|
| 50 |
+
docker push docker.io/<you>/incident-env-job:latest
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### Option B: HF Space as image
|
| 54 |
+
|
| 55 |
+
Create a Space of type `Docker`, push this `Dockerfile` to it, and use
|
| 56 |
+
`--image hf.co/spaces/<you>/incident-env-job` at launch time.
|
| 57 |
+
|
| 58 |
+
## Step 2 — Install the launcher & auth
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
pip install "huggingface_hub>=1.0"
|
| 62 |
+
hf auth login # or export HF_TOKEN=...
|
| 63 |
+
export WANDB_API_KEY=... # optional
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Step 3 — (Optional) Create a checkpoint dataset
|
| 67 |
+
|
| 68 |
+
HF Jobs filesystems are ephemeral. Mount a dataset repo at
|
| 69 |
+
`/training-outputs` so LoRA checkpoints survive:
|
| 70 |
+
|
| 71 |
+
```bash
|
| 72 |
+
hf repo create incident-env-checkpoints --type dataset
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
The launcher's `--hf-repo <name>` flag mounts it read/write.
|
| 76 |
+
|
| 77 |
+
## Step 4 — Launch
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
python jobs/launch_hf_job.py \
|
| 81 |
+
--image docker.io/<you>/incident-env-job:latest \
|
| 82 |
+
--flavor a100-large \
|
| 83 |
+
--timeout 6h \
|
| 84 |
+
--hf-repo <you>/incident-env-checkpoints \
|
| 85 |
+
--base-model Qwen/Qwen2.5-7B-Instruct \
|
| 86 |
+
--config incident_env_gigpo_7b_a100 \
|
| 87 |
+
--epochs 30 \
|
| 88 |
+
--num-envs 4 \
|
| 89 |
+
--rollout-n 4 \
|
| 90 |
+
--follow
|
| 91 |
+
```
|
| 92 |
+
|
| 93 |
+
Drop `--follow` to detach after launch; re-attach later with:
|
| 94 |
+
|
| 95 |
+
```python
|
| 96 |
+
from huggingface_hub import fetch_job_logs, inspect_job
|
| 97 |
+
for line in fetch_job_logs(job_id="<JOB_ID>"):
|
| 98 |
+
print(line)
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Step 5 — Sanity smoke test (cheap)
|
| 102 |
+
|
| 103 |
+
Before committing to a 6h A100 run, do a 30-minute wiring check on a
|
| 104 |
+
smaller flavor:
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
python jobs/launch_hf_job.py \
|
| 108 |
+
--image docker.io/<you>/incident-env-job:latest \
|
| 109 |
+
--flavor a10g-large \
|
| 110 |
+
--timeout 30m \
|
| 111 |
+
--base-model Qwen/Qwen2.5-0.5B-Instruct \
|
| 112 |
+
--epochs 2 \
|
| 113 |
+
--num-envs 2 \
|
| 114 |
+
--rollout-n 2 \
|
| 115 |
+
--follow
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
This only verifies the entrypoint boots env-server + vLLM + verl-agent
|
| 119 |
+
without OOM; it will not produce a good policy.
|
| 120 |
+
|
| 121 |
+
## Step 6 — Managing jobs
|
| 122 |
+
|
| 123 |
+
```python
|
| 124 |
+
from huggingface_hub import list_jobs, inspect_job, cancel_job, fetch_job_metrics
|
| 125 |
+
|
| 126 |
+
jobs = list_jobs()
|
| 127 |
+
running = [j for j in jobs if j.status.stage == "RUNNING"]
|
| 128 |
+
|
| 129 |
+
cancel_job(job_id="<JOB_ID>")
|
| 130 |
+
for m in fetch_job_metrics(job_id="<JOB_ID>"):
|
| 131 |
+
print(m)
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
Or via CLI:
|
| 135 |
+
|
| 136 |
+
```bash
|
| 137 |
+
hf jobs ps
|
| 138 |
+
hf jobs logs <JOB_ID>
|
| 139 |
+
hf jobs cancel <JOB_ID>
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
## Scheduled evals
|
| 143 |
+
|
| 144 |
+
Re-run `scripts/eval_incident_env.py` on a recurring schedule against
|
| 145 |
+
the latest LoRA in your checkpoint dataset. Useful for catching
|
| 146 |
+
regressions during long training runs.
|
| 147 |
+
|
| 148 |
+
### Create a weekly eval
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
python jobs/schedule_eval_job.py create \
|
| 152 |
+
--image docker.io/<you>/incident-env-job:latest \
|
| 153 |
+
--schedule "@weekly" \
|
| 154 |
+
--flavor a10g-large \
|
| 155 |
+
--timeout 90m \
|
| 156 |
+
--hf-repo <you>/incident-env-checkpoints \
|
| 157 |
+
--lora-path /training-outputs/incident_env_gigpo_7b_a100/latest_lora \
|
| 158 |
+
--base-model Qwen/Qwen2.5-7B-Instruct \
|
| 159 |
+
--seeds 30
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
Schedule syntax (from HF docs):
|
| 163 |
+
`@hourly | @daily | @weekly | @monthly | @yearly`, or any CRON string
|
| 164 |
+
(e.g. `"0 9 * * 1"` = Mondays 09:00 UTC).
|
| 165 |
+
|
| 166 |
+
### Report output
|
| 167 |
+
|
| 168 |
+
Each run writes `eval_reports/<JOB_ID>.json` to the mounted
|
| 169 |
+
`--hf-repo`, containing:
|
| 170 |
+
|
| 171 |
+
```json
|
| 172 |
+
{
|
| 173 |
+
"summary": {
|
| 174 |
+
"model": "incident_lora",
|
| 175 |
+
"overall_score_mean": 0.78,
|
| 176 |
+
"per_task": {
|
| 177 |
+
"memory_leak": {"score_mean": 0.88, "n": 30, ...},
|
| 178 |
+
"cascading_failure": {"score_mean": 0.71, "n": 30, ...},
|
| 179 |
+
"distributed_deadlock":{"score_mean": 0.75, "n": 30, ...}
|
| 180 |
+
}
|
| 181 |
+
},
|
| 182 |
+
"results": [ ... per-episode rubric audit ... ]
|
| 183 |
+
}
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
A compact summary is also printed into the Job logs so it's visible in
|
| 187 |
+
the HF UI without downloading the JSON.
|
| 188 |
+
|
| 189 |
+
### Manage scheduled jobs
|
| 190 |
+
|
| 191 |
+
```bash
|
| 192 |
+
python jobs/schedule_eval_job.py list
|
| 193 |
+
python jobs/schedule_eval_job.py inspect <SCHEDULED_ID>
|
| 194 |
+
python jobs/schedule_eval_job.py pause <SCHEDULED_ID>
|
| 195 |
+
python jobs/schedule_eval_job.py resume <SCHEDULED_ID>
|
| 196 |
+
python jobs/schedule_eval_job.py delete <SCHEDULED_ID>
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
Omit `--lora-path` to evaluate the **base model** as a reference
|
| 200 |
+
baseline (useful to confirm that your RL-trained LoRA actually beats
|
| 201 |
+
vanilla).
|
| 202 |
+
|
| 203 |
+
## Built-in job env vars (from HF)
|
| 204 |
+
|
| 205 |
+
Available inside the container:
|
| 206 |
+
|
| 207 |
+
- `JOB_ID` — unique id for this job
|
| 208 |
+
- `ACCELERATOR` — e.g. `a100-large`
|
| 209 |
+
- `CPU_CORES`, `MEMORY`
|
| 210 |
+
|
| 211 |
+
`run_in_job.sh` already echoes these at startup.
|
| 212 |
+
|
| 213 |
+
## Monitoring
|
| 214 |
+
|
| 215 |
+
- **W&B**: set `WANDB_API_KEY` before launching; runs appear under
|
| 216 |
+
`$WANDB_PROJECT` (default `incident_env_rl`) with the job id in the
|
| 217 |
+
run name.
|
| 218 |
+
- **Rubric audit**: every terminal step logs `rubric_version`,
|
| 219 |
+
`rubric_raw_components`, and `score` (see `RUBRICS.md`). Use
|
| 220 |
+
`scripts/eval_incident_env.py` against a saved LoRA adapter for
|
| 221 |
+
held-out evaluation.
|
| 222 |
+
|
| 223 |
+
## Troubleshooting
|
| 224 |
+
|
| 225 |
+
- **OOM during vLLM startup** → lower
|
| 226 |
+
`actor_rollout_ref.rollout.gpu_memory_utilization` (default 0.45) or
|
| 227 |
+
drop `max_response_length`.
|
| 228 |
+
- **Trainer OOM during FSDP** → raise
|
| 229 |
+
`actor_rollout_ref.actor.fsdp_config.param_offload: true` and
|
| 230 |
+
`optimizer_offload: true` (already set in 7B config).
|
| 231 |
+
- **vLLM and trainer both allocate the same GPU**: yes, that's intended
|
| 232 |
+
— `hybrid_engine: true` + low `gpu_memory_utilization` shares the
|
| 233 |
+
GPU. If you see `CUDA out of memory`, the balance needs tuning.
|
| 234 |
+
- **Job hits the 30-minute default timeout**: pass `--timeout 6h`.
|
| 235 |
+
- **Pool cleanup** between episodes is automatic via the
|
| 236 |
+
`IncidentEnvManager.close()` path; no action required.
|
jobs/eval_in_job.sh
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# HF Jobs entrypoint for SCHEDULED EVAL runs of incident_env checkpoints.
|
| 3 |
+
#
|
| 4 |
+
# Boots (in order) inside a single GPU container:
|
| 5 |
+
# 1. incident_env FastAPI server on :$ENV_PORT
|
| 6 |
+
# 2. vLLM OpenAI-compatible server on :$VLLM_PORT serving $BASE_MODEL,
|
| 7 |
+
# with the target LoRA adapter hot-loaded (if LORA_PATH provided).
|
| 8 |
+
# 3. scripts/eval_incident_env.py against the two endpoints.
|
| 9 |
+
#
|
| 10 |
+
# The eval report (JSON with per-task score_mean/std + rubric audit) is
|
| 11 |
+
# written under $OUTPUT_DIR/eval_reports/<timestamp>.json, which is a
|
| 12 |
+
# mounted HF dataset/bucket → survives job termination.
|
| 13 |
+
#
|
| 14 |
+
# Env vars consumed (defaults in brackets):
|
| 15 |
+
# BASE_MODEL [Qwen/Qwen2.5-7B-Instruct]
|
| 16 |
+
# LORA_PATH [<unset>] path/repo for LoRA adapter to eval;
|
| 17 |
+
# if unset, evaluates the base model
|
| 18 |
+
# LORA_NAME [incident_lora] served name for vLLM
|
| 19 |
+
# SEEDS [30] seeds per task
|
| 20 |
+
# TASKS [memory_leak cascading_failure distributed_deadlock]
|
| 21 |
+
# TEMPERATURE [0.0]
|
| 22 |
+
# MAX_TURNS [20]
|
| 23 |
+
# REWARD_MODE [long_horizon_v2]
|
| 24 |
+
# OUTPUT_DIR [/training-outputs]
|
| 25 |
+
# REPORT_TAG [<JOB_ID or timestamp>]
|
| 26 |
+
|
| 27 |
+
set -euo pipefail
|
| 28 |
+
|
| 29 |
+
BASE_MODEL="${BASE_MODEL:-Qwen/Qwen2.5-7B-Instruct}"
|
| 30 |
+
LORA_PATH="${LORA_PATH:-}"
|
| 31 |
+
LORA_NAME="${LORA_NAME:-incident_lora}"
|
| 32 |
+
SEEDS="${SEEDS:-30}"
|
| 33 |
+
TASKS="${TASKS:-memory_leak cascading_failure distributed_deadlock}"
|
| 34 |
+
TEMPERATURE="${TEMPERATURE:-0.0}"
|
| 35 |
+
MAX_TURNS="${MAX_TURNS:-20}"
|
| 36 |
+
REWARD_MODE="${REWARD_MODE:-long_horizon_v2}"
|
| 37 |
+
ENV_PORT="${ENV_PORT:-8000}"
|
| 38 |
+
VLLM_PORT="${VLLM_PORT:-8080}"
|
| 39 |
+
OUTPUT_DIR="${OUTPUT_DIR:-/training-outputs}"
|
| 40 |
+
REPORT_TAG="${REPORT_TAG:-${JOB_ID:-$(date +%Y%m%d_%H%M%S)}}"
|
| 41 |
+
|
| 42 |
+
INCIDENT_REPO_DIR="${INCIDENT_REPO_DIR:-/workspace/incident_env}"
|
| 43 |
+
|
| 44 |
+
echo "=========================================="
|
| 45 |
+
echo " HF Job: incident_env EVAL"
|
| 46 |
+
echo "=========================================="
|
| 47 |
+
echo " JOB_ID: ${JOB_ID:-<local>}"
|
| 48 |
+
echo " ACCELERATOR: ${ACCELERATOR:-unknown}"
|
| 49 |
+
echo " BASE_MODEL: $BASE_MODEL"
|
| 50 |
+
echo " LORA_PATH: ${LORA_PATH:-<base model only>}"
|
| 51 |
+
echo " SEEDS: $SEEDS"
|
| 52 |
+
echo " TASKS: $TASKS"
|
| 53 |
+
echo " REWARD_MODE: $REWARD_MODE"
|
| 54 |
+
echo " REPORT_TAG: $REPORT_TAG"
|
| 55 |
+
echo "=========================================="
|
| 56 |
+
|
| 57 |
+
# ---- Auth ----------------------------------------------------------------
|
| 58 |
+
if [ -n "${HF_TOKEN:-}" ]; then
|
| 59 |
+
huggingface-cli login --token "$HF_TOKEN" --add-to-git-credential 2>/dev/null || true
|
| 60 |
+
fi
|
| 61 |
+
|
| 62 |
+
# ---- Refresh incident_env repo if a ref is pinned ------------------------
|
| 63 |
+
if [ -n "${INCIDENT_REPO_REF:-}" ]; then
|
| 64 |
+
(cd "$INCIDENT_REPO_DIR" && git fetch --all --tags && git checkout "$INCIDENT_REPO_REF") || true
|
| 65 |
+
fi
|
| 66 |
+
pip install -e "$INCIDENT_REPO_DIR" 2>/dev/null || true
|
| 67 |
+
export PYTHONPATH="$INCIDENT_REPO_DIR:${PYTHONPATH:-}"
|
| 68 |
+
|
| 69 |
+
# ---- Background process management ---------------------------------------
|
| 70 |
+
PIDS=()
|
| 71 |
+
cleanup() {
|
| 72 |
+
echo ">> cleanup"
|
| 73 |
+
for pid in "${PIDS[@]}"; do
|
| 74 |
+
kill "$pid" 2>/dev/null || true
|
| 75 |
+
done
|
| 76 |
+
wait 2>/dev/null || true
|
| 77 |
+
}
|
| 78 |
+
trap cleanup EXIT INT TERM
|
| 79 |
+
|
| 80 |
+
# ---- 1. env server -------------------------------------------------------
|
| 81 |
+
echo ">> Starting incident_env server on :$ENV_PORT"
|
| 82 |
+
(
|
| 83 |
+
cd "$INCIDENT_REPO_DIR"
|
| 84 |
+
uvicorn server.app:app --host 0.0.0.0 --port "$ENV_PORT" \
|
| 85 |
+
> /tmp/env_server.log 2>&1
|
| 86 |
+
) &
|
| 87 |
+
PIDS+=($!)
|
| 88 |
+
|
| 89 |
+
for i in $(seq 1 60); do
|
| 90 |
+
if curl -sf "http://localhost:$ENV_PORT/health" > /dev/null 2>&1; then
|
| 91 |
+
echo ">> env server ready"
|
| 92 |
+
break
|
| 93 |
+
fi
|
| 94 |
+
sleep 1
|
| 95 |
+
done
|
| 96 |
+
curl -sf "http://localhost:$ENV_PORT/health" > /dev/null || { echo "!! env server failed"; tail -50 /tmp/env_server.log; exit 1; }
|
| 97 |
+
|
| 98 |
+
# ---- 2. vLLM (with optional LoRA) ----------------------------------------
|
| 99 |
+
VLLM_ARGS=(
|
| 100 |
+
--model "$BASE_MODEL"
|
| 101 |
+
--port "$VLLM_PORT"
|
| 102 |
+
--host 0.0.0.0
|
| 103 |
+
--gpu-memory-utilization 0.85
|
| 104 |
+
--max-model-len 4096
|
| 105 |
+
--enforce-eager
|
| 106 |
+
)
|
| 107 |
+
if [ -n "$LORA_PATH" ]; then
|
| 108 |
+
echo ">> vLLM will serve LoRA '$LORA_NAME' from $LORA_PATH"
|
| 109 |
+
VLLM_ARGS+=(
|
| 110 |
+
--enable-lora
|
| 111 |
+
--max-loras 2
|
| 112 |
+
--max-lora-rank 32
|
| 113 |
+
--lora-modules "${LORA_NAME}=${LORA_PATH}"
|
| 114 |
+
)
|
| 115 |
+
SERVED_MODEL="$LORA_NAME"
|
| 116 |
+
else
|
| 117 |
+
echo ">> vLLM will serve BASE model only (no LoRA)"
|
| 118 |
+
SERVED_MODEL="$BASE_MODEL"
|
| 119 |
+
fi
|
| 120 |
+
|
| 121 |
+
VLLM_ALLOW_RUNTIME_LORA_UPDATING=True python -m vllm.entrypoints.openai.api_server \
|
| 122 |
+
"${VLLM_ARGS[@]}" > /tmp/vllm.log 2>&1 &
|
| 123 |
+
PIDS+=($!)
|
| 124 |
+
|
| 125 |
+
for i in $(seq 1 180); do
|
| 126 |
+
if curl -sf "http://localhost:$VLLM_PORT/health" > /dev/null 2>&1; then
|
| 127 |
+
echo ">> vLLM ready"
|
| 128 |
+
break
|
| 129 |
+
fi
|
| 130 |
+
sleep 2
|
| 131 |
+
done
|
| 132 |
+
curl -sf "http://localhost:$VLLM_PORT/health" > /dev/null || { echo "!! vLLM failed"; tail -80 /tmp/vllm.log; exit 1; }
|
| 133 |
+
|
| 134 |
+
# ---- 3. Run eval ---------------------------------------------------------
|
| 135 |
+
REPORT_DIR="$OUTPUT_DIR/eval_reports"
|
| 136 |
+
mkdir -p "$REPORT_DIR" 2>/dev/null || true
|
| 137 |
+
REPORT_PATH="$REPORT_DIR/${REPORT_TAG}.json"
|
| 138 |
+
|
| 139 |
+
echo ">> Running eval → $REPORT_PATH"
|
| 140 |
+
# shellcheck disable=SC2086
|
| 141 |
+
python "$INCIDENT_REPO_DIR/scripts/eval_incident_env.py" \
|
| 142 |
+
--env-url "http://localhost:$ENV_PORT" \
|
| 143 |
+
--endpoint "http://localhost:$VLLM_PORT/v1" \
|
| 144 |
+
--model "$SERVED_MODEL" \
|
| 145 |
+
--seeds "$SEEDS" \
|
| 146 |
+
--tasks $TASKS \
|
| 147 |
+
--temperature "$TEMPERATURE" \
|
| 148 |
+
--max-turns "$MAX_TURNS" \
|
| 149 |
+
--reward-mode "$REWARD_MODE" \
|
| 150 |
+
--out "$REPORT_PATH"
|
| 151 |
+
|
| 152 |
+
echo ">> Eval complete."
|
| 153 |
+
echo ">> Report: $REPORT_PATH"
|
| 154 |
+
|
| 155 |
+
# Print a compact summary so it surfaces in HF Jobs logs
|
| 156 |
+
python - <<PY
|
| 157 |
+
import json, sys
|
| 158 |
+
with open("$REPORT_PATH") as f:
|
| 159 |
+
out = json.load(f)
|
| 160 |
+
s = out["summary"]
|
| 161 |
+
print("=" * 60)
|
| 162 |
+
print("EVAL SUMMARY")
|
| 163 |
+
print(f" model: {s['model']}")
|
| 164 |
+
print(f" overall_mean: {s['overall_score_mean']}")
|
| 165 |
+
print(f" overall_std: {s['overall_score_std']}")
|
| 166 |
+
for task, v in s["per_task"].items():
|
| 167 |
+
print(f" {task:25s} score={v['score_mean']:.3f} reward={v['reward_mean']:.3f} turns={v['turns_mean']:.1f} n={v['n']}")
|
| 168 |
+
print("=" * 60)
|
| 169 |
+
PY
|
jobs/launch_hf_job.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Launch incident_env post-training on HF Jobs.
|
| 3 |
+
|
| 4 |
+
Thin wrapper around `huggingface_hub.run_job` that:
|
| 5 |
+
- selects a GPU flavor (default `a100-large` = single A100-80GB),
|
| 6 |
+
- sets a generous timeout (default 6 h),
|
| 7 |
+
- forwards HF_TOKEN / WANDB_API_KEY as secrets,
|
| 8 |
+
- optionally mounts an HF dataset repo or bucket at /training-outputs
|
| 9 |
+
so checkpoints persist between jobs.
|
| 10 |
+
|
| 11 |
+
Prerequisites
|
| 12 |
+
-------------
|
| 13 |
+
- HF Pro / Team / Enterprise account (HF Jobs is gated).
|
| 14 |
+
- `pip install "huggingface_hub>=1.0"`
|
| 15 |
+
- `hf auth login` (or set HF_TOKEN).
|
| 16 |
+
- A Docker image already pushed somewhere HF Jobs can pull from — either
|
| 17 |
+
Docker Hub (`user/incident-env-job:tag`) or an HF Space used as an image
|
| 18 |
+
(`hf.co/spaces/user/incident-env-job`). See jobs/README.md for build steps.
|
| 19 |
+
|
| 20 |
+
Usage
|
| 21 |
+
-----
|
| 22 |
+
python jobs/launch_hf_job.py \\
|
| 23 |
+
--image docker.io/youruser/incident-env-job:latest \\
|
| 24 |
+
--flavor a100-large \\
|
| 25 |
+
--timeout 6h \\
|
| 26 |
+
--hf-repo youruser/incident-env-checkpoints \\
|
| 27 |
+
--wandb-project incident_env_rl \\
|
| 28 |
+
--base-model Qwen/Qwen2.5-7B-Instruct \\
|
| 29 |
+
--config incident_env_gigpo_7b_a100 \\
|
| 30 |
+
--epochs 30
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
from __future__ import annotations
|
| 34 |
+
|
| 35 |
+
import argparse
|
| 36 |
+
import os
|
| 37 |
+
import sys
|
| 38 |
+
import time
|
| 39 |
+
from typing import Any, Dict, List, Optional
|
| 40 |
+
|
| 41 |
+
try:
|
| 42 |
+
from huggingface_hub import (
|
| 43 |
+
Volume,
|
| 44 |
+
cancel_job,
|
| 45 |
+
fetch_job_logs,
|
| 46 |
+
inspect_job,
|
| 47 |
+
run_job,
|
| 48 |
+
)
|
| 49 |
+
except ImportError:
|
| 50 |
+
print(
|
| 51 |
+
"ERROR: huggingface_hub>=1.0 required. "
|
| 52 |
+
"Install with: pip install 'huggingface_hub>=1.0'",
|
| 53 |
+
file=sys.stderr,
|
| 54 |
+
)
|
| 55 |
+
sys.exit(1)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
VALID_FLAVORS = {
|
| 59 |
+
# GPU — see https://huggingface.co/docs/huggingface_hub/guides/jobs
|
| 60 |
+
"t4-small",
|
| 61 |
+
"t4-medium",
|
| 62 |
+
"l4x1",
|
| 63 |
+
"l4x4",
|
| 64 |
+
"a10g-small",
|
| 65 |
+
"a10g-large",
|
| 66 |
+
"a10g-largex2",
|
| 67 |
+
"a10g-largex4",
|
| 68 |
+
"a100-large", # single A100-80GB — recommended for Qwen2.5-7B + LoRA
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def build_args() -> argparse.Namespace:
|
| 73 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 74 |
+
ap.add_argument("--image", required=True, help="Docker image (Docker Hub or hf.co/spaces/...).")
|
| 75 |
+
ap.add_argument(
|
| 76 |
+
"--flavor",
|
| 77 |
+
default="a100-large",
|
| 78 |
+
choices=sorted(VALID_FLAVORS),
|
| 79 |
+
help="HF Jobs hardware flavor.",
|
| 80 |
+
)
|
| 81 |
+
ap.add_argument("--timeout", default="6h", help="Job timeout (e.g. 6h, 30m, 1d). Default 6h.")
|
| 82 |
+
ap.add_argument("--namespace", default=None, help="Org namespace (optional).")
|
| 83 |
+
|
| 84 |
+
# Training knobs forwarded as env vars to the container.
|
| 85 |
+
ap.add_argument("--base-model", default="Qwen/Qwen2.5-7B-Instruct")
|
| 86 |
+
ap.add_argument("--config", default="incident_env_gigpo_7b_a100", help="YAML config basename in configs/.")
|
| 87 |
+
ap.add_argument("--epochs", type=int, default=30)
|
| 88 |
+
ap.add_argument("--num-envs", type=int, default=4)
|
| 89 |
+
ap.add_argument("--rollout-n", type=int, default=4)
|
| 90 |
+
ap.add_argument("--reward-mode", default="long_horizon_v2", choices=["legacy", "long_horizon_v2"])
|
| 91 |
+
ap.add_argument("--incident-repo-ref", default=None, help="Optional git ref for this repo (otherwise baked image SHA).")
|
| 92 |
+
ap.add_argument("--verl-agent-repo-ref", default=None)
|
| 93 |
+
ap.add_argument("--wandb-project", default="incident_env_rl")
|
| 94 |
+
|
| 95 |
+
# Persistence — checkpoints survive job termination.
|
| 96 |
+
ap.add_argument(
|
| 97 |
+
"--hf-repo",
|
| 98 |
+
default=None,
|
| 99 |
+
help="HF repo (dataset / bucket) to mount at /training-outputs. Requires write access.",
|
| 100 |
+
)
|
| 101 |
+
ap.add_argument(
|
| 102 |
+
"--hf-repo-type",
|
| 103 |
+
default="dataset",
|
| 104 |
+
choices=["dataset", "model", "bucket"],
|
| 105 |
+
help="Repo type for the output mount.",
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Monitoring.
|
| 109 |
+
ap.add_argument("--follow", action="store_true", help="Tail logs after launch.")
|
| 110 |
+
ap.add_argument("--poll-interval", type=float, default=5.0)
|
| 111 |
+
return ap.parse_args()
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def build_env_vars(args: argparse.Namespace) -> Dict[str, str]:
|
| 115 |
+
env = {
|
| 116 |
+
"BASE_MODEL": args.base_model,
|
| 117 |
+
"CONFIG_NAME": args.config,
|
| 118 |
+
"EPOCHS": str(args.epochs),
|
| 119 |
+
"NUM_ENVS": str(args.num_envs),
|
| 120 |
+
"ROLLOUT_N": str(args.rollout_n),
|
| 121 |
+
"INCIDENT_REWARD_MODE": args.reward_mode,
|
| 122 |
+
"WANDB_PROJECT": args.wandb_project,
|
| 123 |
+
}
|
| 124 |
+
if args.incident_repo_ref:
|
| 125 |
+
env["INCIDENT_REPO_REF"] = args.incident_repo_ref
|
| 126 |
+
if args.verl_agent_repo_ref:
|
| 127 |
+
env["VERL_AGENT_REPO_REF"] = args.verl_agent_repo_ref
|
| 128 |
+
if args.hf_repo:
|
| 129 |
+
# The entrypoint writes checkpoints under OUTPUT_DIR.
|
| 130 |
+
env["OUTPUT_DIR"] = "/training-outputs"
|
| 131 |
+
env["HF_REPO"] = args.hf_repo
|
| 132 |
+
return env
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def build_secrets() -> Dict[str, str]:
|
| 136 |
+
"""Forward HF_TOKEN + WANDB_API_KEY from the caller's env as encrypted secrets."""
|
| 137 |
+
out: Dict[str, str] = {}
|
| 138 |
+
hf = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
| 139 |
+
if hf:
|
| 140 |
+
out["HF_TOKEN"] = hf
|
| 141 |
+
wb = os.environ.get("WANDB_API_KEY")
|
| 142 |
+
if wb:
|
| 143 |
+
out["WANDB_API_KEY"] = wb
|
| 144 |
+
return out
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def build_volumes(args: argparse.Namespace) -> Optional[List[Volume]]:
|
| 148 |
+
if not args.hf_repo:
|
| 149 |
+
return None
|
| 150 |
+
vol = Volume(
|
| 151 |
+
type=args.hf_repo_type,
|
| 152 |
+
source=args.hf_repo,
|
| 153 |
+
mount_path="/training-outputs",
|
| 154 |
+
)
|
| 155 |
+
return [vol]
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def follow_logs(job_id: str, poll_interval: float) -> int:
|
| 159 |
+
"""Stream logs until job terminates. Returns 0 on COMPLETED, non-zero on ERROR."""
|
| 160 |
+
print(f">> Following logs for job {job_id} (Ctrl-C to detach; job keeps running)")
|
| 161 |
+
last_line = 0
|
| 162 |
+
try:
|
| 163 |
+
while True:
|
| 164 |
+
status = inspect_job(job_id=job_id).status
|
| 165 |
+
logs = list(fetch_job_logs(job_id=job_id))
|
| 166 |
+
for line in logs[last_line:]:
|
| 167 |
+
print(line)
|
| 168 |
+
last_line = len(logs)
|
| 169 |
+
if status.stage in ("COMPLETED", "ERROR", "CANCELED"):
|
| 170 |
+
print(f">> Job finished: stage={status.stage} message={status.message}")
|
| 171 |
+
return 0 if status.stage == "COMPLETED" else 1
|
| 172 |
+
time.sleep(poll_interval)
|
| 173 |
+
except KeyboardInterrupt:
|
| 174 |
+
print(">> Detached. Job continues running on HF infrastructure.")
|
| 175 |
+
return 0
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def main() -> int:
|
| 179 |
+
args = build_args()
|
| 180 |
+
|
| 181 |
+
env = build_env_vars(args)
|
| 182 |
+
secrets = build_secrets()
|
| 183 |
+
volumes = build_volumes(args)
|
| 184 |
+
|
| 185 |
+
print("=" * 60)
|
| 186 |
+
print(" HF Jobs — incident_env post-training")
|
| 187 |
+
print("=" * 60)
|
| 188 |
+
print(f" image: {args.image}")
|
| 189 |
+
print(f" flavor: {args.flavor}")
|
| 190 |
+
print(f" timeout: {args.timeout}")
|
| 191 |
+
print(f" namespace: {args.namespace or '<user>'}")
|
| 192 |
+
print(f" base_model: {args.base_model}")
|
| 193 |
+
print(f" config: {args.config}")
|
| 194 |
+
print(f" epochs: {args.epochs}")
|
| 195 |
+
print(f" num_envs: {args.num_envs}")
|
| 196 |
+
print(f" rollout_n: {args.rollout_n}")
|
| 197 |
+
print(f" reward_mode: {args.reward_mode}")
|
| 198 |
+
print(f" volumes: {args.hf_repo or '<none>'}")
|
| 199 |
+
print(f" env: {sorted(env.keys())}")
|
| 200 |
+
print(f" secrets: {sorted(secrets.keys())}")
|
| 201 |
+
print("=" * 60)
|
| 202 |
+
|
| 203 |
+
kwargs: Dict[str, Any] = dict(
|
| 204 |
+
image=args.image,
|
| 205 |
+
command=["/usr/local/bin/run_in_job.sh"],
|
| 206 |
+
flavor=args.flavor,
|
| 207 |
+
timeout=args.timeout,
|
| 208 |
+
env=env,
|
| 209 |
+
secrets=secrets,
|
| 210 |
+
)
|
| 211 |
+
if args.namespace:
|
| 212 |
+
kwargs["namespace"] = args.namespace
|
| 213 |
+
if volumes:
|
| 214 |
+
kwargs["volumes"] = volumes
|
| 215 |
+
|
| 216 |
+
job = run_job(**kwargs)
|
| 217 |
+
print(f">> Job launched: {job.url}")
|
| 218 |
+
print(f">> Job ID: {job.id}")
|
| 219 |
+
|
| 220 |
+
if args.follow:
|
| 221 |
+
return follow_logs(job.id, args.poll_interval)
|
| 222 |
+
return 0
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
if __name__ == "__main__":
|
| 226 |
+
sys.exit(main())
|
jobs/run_in_job.sh
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# HF Jobs entrypoint.
|
| 3 |
+
#
|
| 4 |
+
# Inside a single GPU container, boot (in order):
|
| 5 |
+
# 1. incident_env FastAPI server on :$ENV_PORT
|
| 6 |
+
# 2. vLLM OpenAI-compatible server on :$VLLM_PORT (the actor backend)
|
| 7 |
+
# 3. verl-agent trainer (foreground) — when this exits, the job ends
|
| 8 |
+
#
|
| 9 |
+
# Env vars consumed (all optional, reasonable defaults):
|
| 10 |
+
# BASE_MODEL default: Qwen/Qwen2.5-7B-Instruct
|
| 11 |
+
# CONFIG_NAME default: incident_env_gigpo_7b_a100
|
| 12 |
+
# EPOCHS total training epochs
|
| 13 |
+
# NUM_ENVS parallel incident envs
|
| 14 |
+
# ROLLOUT_N GRPO group size
|
| 15 |
+
# HF_REPO HF dataset or model repo to push checkpoints to
|
| 16 |
+
# HF_TOKEN (secret)
|
| 17 |
+
# WANDB_API_KEY (secret)
|
| 18 |
+
# WANDB_PROJECT default: incident_env_rl
|
| 19 |
+
# INCIDENT_REPO_REF optional git ref to check out (otherwise baked-in)
|
| 20 |
+
# VERL_AGENT_REPO_REF optional git ref
|
| 21 |
+
|
| 22 |
+
set -euo pipefail
|
| 23 |
+
|
| 24 |
+
BASE_MODEL="${BASE_MODEL:-Qwen/Qwen2.5-7B-Instruct}"
|
| 25 |
+
CONFIG_NAME="${CONFIG_NAME:-incident_env_gigpo_7b_a100}"
|
| 26 |
+
EPOCHS="${EPOCHS:-30}"
|
| 27 |
+
NUM_ENVS="${NUM_ENVS:-4}"
|
| 28 |
+
ROLLOUT_N="${ROLLOUT_N:-4}"
|
| 29 |
+
ENV_PORT="${ENV_PORT:-8000}"
|
| 30 |
+
VLLM_PORT="${VLLM_PORT:-8080}"
|
| 31 |
+
WANDB_PROJECT="${WANDB_PROJECT:-incident_env_rl}"
|
| 32 |
+
|
| 33 |
+
INCIDENT_REPO_DIR="${INCIDENT_REPO_DIR:-/workspace/incident_env}"
|
| 34 |
+
VERL_AGENT_REPO_DIR="${VERL_AGENT_REPO_DIR:-/workspace/verl-agent}"
|
| 35 |
+
|
| 36 |
+
echo "=========================================="
|
| 37 |
+
echo " HF Job: incident_env post-training"
|
| 38 |
+
echo "=========================================="
|
| 39 |
+
echo " JOB_ID: ${JOB_ID:-<local>}"
|
| 40 |
+
echo " ACCELERATOR: ${ACCELERATOR:-unknown}"
|
| 41 |
+
echo " BASE_MODEL: $BASE_MODEL"
|
| 42 |
+
echo " CONFIG: $CONFIG_NAME"
|
| 43 |
+
echo " EPOCHS: $EPOCHS"
|
| 44 |
+
echo " NUM_ENVS: $NUM_ENVS"
|
| 45 |
+
echo " ROLLOUT_N: $ROLLOUT_N"
|
| 46 |
+
echo " REWARD_MODE: ${INCIDENT_REWARD_MODE:-long_horizon_v2}"
|
| 47 |
+
echo "=========================================="
|
| 48 |
+
|
| 49 |
+
# ---- Auth ----------------------------------------------------------------
|
| 50 |
+
if [ -n "${HF_TOKEN:-}" ]; then
|
| 51 |
+
echo ">> HF login"
|
| 52 |
+
huggingface-cli login --token "$HF_TOKEN" --add-to-git-credential 2>/dev/null || true
|
| 53 |
+
fi
|
| 54 |
+
if [ -n "${WANDB_API_KEY:-}" ]; then
|
| 55 |
+
export WANDB_PROJECT
|
| 56 |
+
echo ">> W&B authenticated"
|
| 57 |
+
fi
|
| 58 |
+
|
| 59 |
+
# ---- Refresh repos if refs provided --------------------------------------
|
| 60 |
+
if [ -n "${INCIDENT_REPO_REF:-}" ]; then
|
| 61 |
+
echo ">> Checkout incident_env @ $INCIDENT_REPO_REF"
|
| 62 |
+
(cd "$INCIDENT_REPO_DIR" && git fetch --all --tags && git checkout "$INCIDENT_REPO_REF") || true
|
| 63 |
+
fi
|
| 64 |
+
if [ -n "${VERL_AGENT_REPO_REF:-}" ]; then
|
| 65 |
+
echo ">> Checkout verl-agent @ $VERL_AGENT_REPO_REF"
|
| 66 |
+
(cd "$VERL_AGENT_REPO_DIR" && git fetch --all --tags && git checkout "$VERL_AGENT_REPO_REF") || true
|
| 67 |
+
fi
|
| 68 |
+
|
| 69 |
+
pip install -e "$INCIDENT_REPO_DIR" 2>/dev/null || true
|
| 70 |
+
pip install -e "$VERL_AGENT_REPO_DIR" 2>/dev/null || true
|
| 71 |
+
|
| 72 |
+
export PYTHONPATH="$INCIDENT_REPO_DIR:$VERL_AGENT_REPO_DIR:${PYTHONPATH:-}"
|
| 73 |
+
export INCIDENT_ENV_BASE_URL="http://localhost:$ENV_PORT"
|
| 74 |
+
|
| 75 |
+
# ---- Wire adapter into verl-agent ----------------------------------------
|
| 76 |
+
ADAPTER_DST="$VERL_AGENT_REPO_DIR/agent_system/environments/incident_env"
|
| 77 |
+
if [ ! -e "$ADAPTER_DST" ]; then
|
| 78 |
+
echo ">> Linking env adapter -> $ADAPTER_DST"
|
| 79 |
+
mkdir -p "$(dirname "$ADAPTER_DST")"
|
| 80 |
+
ln -s "$INCIDENT_REPO_DIR/verl_agent_adapter" "$ADAPTER_DST"
|
| 81 |
+
fi
|
| 82 |
+
|
| 83 |
+
RM_DST="$VERL_AGENT_REPO_DIR/agent_system/reward_manager/incident_episode.py"
|
| 84 |
+
if [ ! -f "$RM_DST" ]; then
|
| 85 |
+
echo ">> Installing reward-manager shim"
|
| 86 |
+
mkdir -p "$(dirname "$RM_DST")"
|
| 87 |
+
cat > "$RM_DST" <<'PYEOF'
|
| 88 |
+
from agent_system.environments.incident_env.reward_registry import IncidentEpisodeRewardManager # noqa: F401
|
| 89 |
+
__all__ = ["IncidentEpisodeRewardManager"]
|
| 90 |
+
PYEOF
|
| 91 |
+
fi
|
| 92 |
+
|
| 93 |
+
# ---- Background process management ---------------------------------------
|
| 94 |
+
PIDS=()
|
| 95 |
+
cleanup() {
|
| 96 |
+
echo ">> Cleaning up subprocesses"
|
| 97 |
+
for pid in "${PIDS[@]}"; do
|
| 98 |
+
kill "$pid" 2>/dev/null || true
|
| 99 |
+
done
|
| 100 |
+
wait 2>/dev/null || true
|
| 101 |
+
}
|
| 102 |
+
trap cleanup EXIT INT TERM
|
| 103 |
+
|
| 104 |
+
# ---- 1. incident_env server ----------------------------------------------
|
| 105 |
+
echo ">> Starting incident_env server on :$ENV_PORT"
|
| 106 |
+
(
|
| 107 |
+
cd "$INCIDENT_REPO_DIR"
|
| 108 |
+
uvicorn server.app:app --host 0.0.0.0 --port "$ENV_PORT" \
|
| 109 |
+
> /tmp/incident_env_server.log 2>&1
|
| 110 |
+
) &
|
| 111 |
+
PIDS+=($!)
|
| 112 |
+
|
| 113 |
+
for i in $(seq 1 60); do
|
| 114 |
+
if curl -sf "http://localhost:$ENV_PORT/health" > /dev/null 2>&1; then
|
| 115 |
+
echo ">> env server ready"
|
| 116 |
+
break
|
| 117 |
+
fi
|
| 118 |
+
sleep 1
|
| 119 |
+
done
|
| 120 |
+
if ! curl -sf "http://localhost:$ENV_PORT/health" > /dev/null 2>&1; then
|
| 121 |
+
echo "!! env server failed to start; logs:"; tail -50 /tmp/incident_env_server.log; exit 1
|
| 122 |
+
fi
|
| 123 |
+
|
| 124 |
+
# ---- 2. vLLM -------------------------------------------------------------
|
| 125 |
+
# One GPU is shared between vLLM and the actor/ref FSDP. Keep vLLM small:
|
| 126 |
+
# gpu_memory_utilization=0.45 leaves room for the FSDP actor + optim state.
|
| 127 |
+
echo ">> Starting vLLM for $BASE_MODEL on :$VLLM_PORT"
|
| 128 |
+
VLLM_ALLOW_RUNTIME_LORA_UPDATING=True python -m vllm.entrypoints.openai.api_server \
|
| 129 |
+
--model "$BASE_MODEL" \
|
| 130 |
+
--port "$VLLM_PORT" \
|
| 131 |
+
--host 0.0.0.0 \
|
| 132 |
+
--enable-lora \
|
| 133 |
+
--max-loras 4 \
|
| 134 |
+
--max-lora-rank 32 \
|
| 135 |
+
--gpu-memory-utilization 0.45 \
|
| 136 |
+
--max-model-len 4096 \
|
| 137 |
+
--enforce-eager \
|
| 138 |
+
> /tmp/vllm.log 2>&1 &
|
| 139 |
+
PIDS+=($!)
|
| 140 |
+
|
| 141 |
+
for i in $(seq 1 180); do
|
| 142 |
+
if curl -sf "http://localhost:$VLLM_PORT/health" > /dev/null 2>&1; then
|
| 143 |
+
echo ">> vLLM ready"
|
| 144 |
+
break
|
| 145 |
+
fi
|
| 146 |
+
sleep 2
|
| 147 |
+
done
|
| 148 |
+
if ! curl -sf "http://localhost:$VLLM_PORT/health" > /dev/null 2>&1; then
|
| 149 |
+
echo "!! vLLM failed to start; logs:"; tail -80 /tmp/vllm.log; exit 1
|
| 150 |
+
fi
|
| 151 |
+
|
| 152 |
+
# ---- 3. verl-agent trainer (foreground) ----------------------------------
|
| 153 |
+
CONFIG_PATH="$INCIDENT_REPO_DIR/configs"
|
| 154 |
+
OUT_DIR="${OUTPUT_DIR:-/training-outputs}"
|
| 155 |
+
mkdir -p "$OUT_DIR" || true
|
| 156 |
+
|
| 157 |
+
echo ">> Launching verl-agent trainer"
|
| 158 |
+
cd "$VERL_AGENT_REPO_DIR"
|
| 159 |
+
exec python -m verl.trainer.main_ppo \
|
| 160 |
+
--config-path "$CONFIG_PATH" \
|
| 161 |
+
--config-name "$CONFIG_NAME" \
|
| 162 |
+
actor_rollout_ref.model.path="$BASE_MODEL" \
|
| 163 |
+
env.num_envs="$NUM_ENVS" \
|
| 164 |
+
env.rollout.n="$ROLLOUT_N" \
|
| 165 |
+
actor_rollout_ref.rollout.n="$ROLLOUT_N" \
|
| 166 |
+
trainer.total_epochs="$EPOCHS" \
|
| 167 |
+
trainer.default_local_dir="$OUT_DIR/$CONFIG_NAME" \
|
| 168 |
+
trainer.project_name="$WANDB_PROJECT" \
|
| 169 |
+
"$@"
|
jobs/schedule_eval_job.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Create / manage a SCHEDULED eval job on HF Jobs.
|
| 3 |
+
|
| 4 |
+
Runs `jobs/eval_in_job.sh` on a recurring schedule (weekly by default)
|
| 5 |
+
against the latest LoRA adapter in a given HF repo. Writes a JSON report
|
| 6 |
+
to the mounted checkpoint dataset under `eval_reports/<tag>.json`.
|
| 7 |
+
|
| 8 |
+
Reference:
|
| 9 |
+
https://huggingface.co/docs/huggingface_hub/guides/jobs#scheduled-jobs
|
| 10 |
+
|
| 11 |
+
Supported schedules (from HF docs):
|
| 12 |
+
@annually | @yearly | @monthly | @weekly | @daily | @hourly
|
| 13 |
+
or a CRON expression, e.g. "0 9 * * 1" (Mon 09:00 UTC)
|
| 14 |
+
|
| 15 |
+
Prerequisites
|
| 16 |
+
-------------
|
| 17 |
+
- The same Docker image built in `jobs/Dockerfile` (which now also
|
| 18 |
+
contains `/usr/local/bin/eval_in_job.sh`).
|
| 19 |
+
- HF Pro / Team / Enterprise account.
|
| 20 |
+
- `pip install "huggingface_hub>=1.0"`.
|
| 21 |
+
|
| 22 |
+
Usage
|
| 23 |
+
-----
|
| 24 |
+
# Create a weekly eval (Mondays 09:00 UTC) on a10g-large
|
| 25 |
+
python jobs/schedule_eval_job.py create \\
|
| 26 |
+
--image docker.io/<you>/incident-env-job:latest \\
|
| 27 |
+
--schedule "@weekly" \\
|
| 28 |
+
--flavor a10g-large \\
|
| 29 |
+
--timeout 90m \\
|
| 30 |
+
--hf-repo <you>/incident-env-checkpoints \\
|
| 31 |
+
--lora-path "/training-outputs/incident_env_gigpo_7b_a100/latest_lora" \\
|
| 32 |
+
--base-model Qwen/Qwen2.5-7B-Instruct \\
|
| 33 |
+
--seeds 30
|
| 34 |
+
|
| 35 |
+
# List / inspect / pause / delete
|
| 36 |
+
python jobs/schedule_eval_job.py list
|
| 37 |
+
python jobs/schedule_eval_job.py inspect <SCHEDULED_ID>
|
| 38 |
+
python jobs/schedule_eval_job.py pause <SCHEDULED_ID>
|
| 39 |
+
python jobs/schedule_eval_job.py resume <SCHEDULED_ID>
|
| 40 |
+
python jobs/schedule_eval_job.py delete <SCHEDULED_ID>
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
from __future__ import annotations
|
| 44 |
+
|
| 45 |
+
import argparse
|
| 46 |
+
import os
|
| 47 |
+
import sys
|
| 48 |
+
from typing import Any, Dict, List, Optional
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
from huggingface_hub import (
|
| 52 |
+
Volume,
|
| 53 |
+
create_scheduled_job,
|
| 54 |
+
delete_scheduled_job,
|
| 55 |
+
inspect_scheduled_job,
|
| 56 |
+
list_scheduled_jobs,
|
| 57 |
+
resume_scheduled_job,
|
| 58 |
+
suspend_scheduled_job,
|
| 59 |
+
)
|
| 60 |
+
except ImportError:
|
| 61 |
+
print(
|
| 62 |
+
"ERROR: huggingface_hub>=1.0 required. "
|
| 63 |
+
"Install with: pip install 'huggingface_hub>=1.0'",
|
| 64 |
+
file=sys.stderr,
|
| 65 |
+
)
|
| 66 |
+
sys.exit(1)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
VALID_FLAVORS = {
|
| 70 |
+
"t4-small", "t4-medium", "l4x1", "l4x4",
|
| 71 |
+
"a10g-small", "a10g-large", "a10g-largex2", "a10g-largex4",
|
| 72 |
+
"a100-large",
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
# ---------------------------------------------------------------------------
|
| 77 |
+
# Builders
|
| 78 |
+
# ---------------------------------------------------------------------------
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def build_env(args: argparse.Namespace) -> Dict[str, str]:
|
| 82 |
+
env: Dict[str, str] = {
|
| 83 |
+
"BASE_MODEL": args.base_model,
|
| 84 |
+
"SEEDS": str(args.seeds),
|
| 85 |
+
"TASKS": " ".join(args.tasks),
|
| 86 |
+
"TEMPERATURE": str(args.temperature),
|
| 87 |
+
"MAX_TURNS": str(args.max_turns),
|
| 88 |
+
"REWARD_MODE": args.reward_mode,
|
| 89 |
+
}
|
| 90 |
+
if args.lora_path:
|
| 91 |
+
env["LORA_PATH"] = args.lora_path
|
| 92 |
+
if args.lora_name:
|
| 93 |
+
env["LORA_NAME"] = args.lora_name
|
| 94 |
+
if args.incident_repo_ref:
|
| 95 |
+
env["INCIDENT_REPO_REF"] = args.incident_repo_ref
|
| 96 |
+
if args.hf_repo:
|
| 97 |
+
env["OUTPUT_DIR"] = "/training-outputs"
|
| 98 |
+
return env
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def build_secrets() -> Dict[str, str]:
|
| 102 |
+
out: Dict[str, str] = {}
|
| 103 |
+
hf = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
| 104 |
+
if hf:
|
| 105 |
+
out["HF_TOKEN"] = hf
|
| 106 |
+
return out
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def build_volumes(args: argparse.Namespace) -> Optional[List[Volume]]:
|
| 110 |
+
if not args.hf_repo:
|
| 111 |
+
return None
|
| 112 |
+
return [
|
| 113 |
+
Volume(
|
| 114 |
+
type=args.hf_repo_type,
|
| 115 |
+
source=args.hf_repo,
|
| 116 |
+
mount_path="/training-outputs",
|
| 117 |
+
)
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# ---------------------------------------------------------------------------
|
| 122 |
+
# Commands
|
| 123 |
+
# ---------------------------------------------------------------------------
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def cmd_create(args: argparse.Namespace) -> int:
|
| 127 |
+
env = build_env(args)
|
| 128 |
+
secrets = build_secrets()
|
| 129 |
+
volumes = build_volumes(args)
|
| 130 |
+
|
| 131 |
+
print("=" * 60)
|
| 132 |
+
print(" Scheduled eval job — incident_env")
|
| 133 |
+
print("=" * 60)
|
| 134 |
+
print(f" image: {args.image}")
|
| 135 |
+
print(f" schedule: {args.schedule}")
|
| 136 |
+
print(f" flavor: {args.flavor}")
|
| 137 |
+
print(f" timeout: {args.timeout}")
|
| 138 |
+
print(f" hf_repo: {args.hf_repo or '<none>'}")
|
| 139 |
+
print(f" lora: {args.lora_path or '<base model>'}")
|
| 140 |
+
print(f" tasks: {args.tasks}")
|
| 141 |
+
print(f" seeds: {args.seeds}")
|
| 142 |
+
print("=" * 60)
|
| 143 |
+
|
| 144 |
+
kwargs: Dict[str, Any] = dict(
|
| 145 |
+
image=args.image,
|
| 146 |
+
command=["/usr/local/bin/eval_in_job.sh"],
|
| 147 |
+
schedule=args.schedule,
|
| 148 |
+
flavor=args.flavor,
|
| 149 |
+
timeout=args.timeout,
|
| 150 |
+
env=env,
|
| 151 |
+
secrets=secrets,
|
| 152 |
+
)
|
| 153 |
+
if args.namespace:
|
| 154 |
+
kwargs["namespace"] = args.namespace
|
| 155 |
+
if volumes:
|
| 156 |
+
kwargs["volumes"] = volumes
|
| 157 |
+
|
| 158 |
+
sched = create_scheduled_job(**kwargs)
|
| 159 |
+
print(f">> created scheduled_job: {sched}")
|
| 160 |
+
# `sched` has .id; surface it for follow-up commands.
|
| 161 |
+
sid = getattr(sched, "id", None)
|
| 162 |
+
if sid:
|
| 163 |
+
print(f">> id: {sid}")
|
| 164 |
+
return 0
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def cmd_list(args: argparse.Namespace) -> int:
|
| 168 |
+
for s in list_scheduled_jobs():
|
| 169 |
+
print(s)
|
| 170 |
+
return 0
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def cmd_inspect(args: argparse.Namespace) -> int:
|
| 174 |
+
print(inspect_scheduled_job(args.scheduled_id))
|
| 175 |
+
return 0
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def cmd_pause(args: argparse.Namespace) -> int:
|
| 179 |
+
suspend_scheduled_job(args.scheduled_id)
|
| 180 |
+
print(f">> paused {args.scheduled_id}")
|
| 181 |
+
return 0
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def cmd_resume(args: argparse.Namespace) -> int:
|
| 185 |
+
resume_scheduled_job(args.scheduled_id)
|
| 186 |
+
print(f">> resumed {args.scheduled_id}")
|
| 187 |
+
return 0
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def cmd_delete(args: argparse.Namespace) -> int:
|
| 191 |
+
delete_scheduled_job(args.scheduled_id)
|
| 192 |
+
print(f">> deleted {args.scheduled_id}")
|
| 193 |
+
return 0
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# ---------------------------------------------------------------------------
|
| 197 |
+
# CLI wiring
|
| 198 |
+
# ---------------------------------------------------------------------------
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _add_create_args(sp: argparse.ArgumentParser) -> None:
|
| 202 |
+
sp.add_argument("--image", required=True)
|
| 203 |
+
sp.add_argument(
|
| 204 |
+
"--schedule",
|
| 205 |
+
default="@weekly",
|
| 206 |
+
help='"@hourly" | "@daily" | "@weekly" | "@monthly" | CRON expr (e.g. "0 9 * * 1")',
|
| 207 |
+
)
|
| 208 |
+
sp.add_argument("--flavor", default="a10g-large", choices=sorted(VALID_FLAVORS))
|
| 209 |
+
sp.add_argument("--timeout", default="90m")
|
| 210 |
+
sp.add_argument("--namespace", default=None)
|
| 211 |
+
|
| 212 |
+
# eval config forwarded as env
|
| 213 |
+
sp.add_argument("--base-model", default="Qwen/Qwen2.5-7B-Instruct")
|
| 214 |
+
sp.add_argument("--lora-path", default=None,
|
| 215 |
+
help="Path inside the container (usually under /training-outputs/...) "
|
| 216 |
+
"or HF repo of the LoRA adapter to evaluate. "
|
| 217 |
+
"If omitted, evaluates the base model only.")
|
| 218 |
+
sp.add_argument("--lora-name", default="incident_lora",
|
| 219 |
+
help="Served model name used to address the LoRA in vLLM.")
|
| 220 |
+
sp.add_argument("--seeds", type=int, default=30)
|
| 221 |
+
sp.add_argument(
|
| 222 |
+
"--tasks",
|
| 223 |
+
nargs="+",
|
| 224 |
+
default=["memory_leak", "cascading_failure", "distributed_deadlock"],
|
| 225 |
+
)
|
| 226 |
+
sp.add_argument("--temperature", type=float, default=0.0)
|
| 227 |
+
sp.add_argument("--max-turns", type=int, default=20)
|
| 228 |
+
sp.add_argument("--reward-mode", default="long_horizon_v2",
|
| 229 |
+
choices=["legacy", "long_horizon_v2"])
|
| 230 |
+
sp.add_argument("--incident-repo-ref", default=None)
|
| 231 |
+
|
| 232 |
+
# Output persistence
|
| 233 |
+
sp.add_argument(
|
| 234 |
+
"--hf-repo",
|
| 235 |
+
default=None,
|
| 236 |
+
help="HF repo to mount at /training-outputs (writes eval_reports/<tag>.json). "
|
| 237 |
+
"Usually the same repo used for training checkpoints.",
|
| 238 |
+
)
|
| 239 |
+
sp.add_argument(
|
| 240 |
+
"--hf-repo-type",
|
| 241 |
+
default="dataset",
|
| 242 |
+
choices=["dataset", "model", "bucket"],
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def main() -> int:
|
| 247 |
+
ap = argparse.ArgumentParser(description=__doc__,
|
| 248 |
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 249 |
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
| 250 |
+
|
| 251 |
+
sp_create = sub.add_parser("create", help="Create a scheduled eval job")
|
| 252 |
+
_add_create_args(sp_create)
|
| 253 |
+
sp_create.set_defaults(func=cmd_create)
|
| 254 |
+
|
| 255 |
+
sub.add_parser("list", help="List scheduled jobs").set_defaults(func=cmd_list)
|
| 256 |
+
|
| 257 |
+
for name, fn in [
|
| 258 |
+
("inspect", cmd_inspect),
|
| 259 |
+
("pause", cmd_pause),
|
| 260 |
+
("resume", cmd_resume),
|
| 261 |
+
("delete", cmd_delete),
|
| 262 |
+
]:
|
| 263 |
+
p = sub.add_parser(name, help=f"{name} a scheduled job")
|
| 264 |
+
p.add_argument("scheduled_id")
|
| 265 |
+
p.set_defaults(func=fn)
|
| 266 |
+
|
| 267 |
+
args = ap.parse_args()
|
| 268 |
+
return args.func(args)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
if __name__ == "__main__":
|
| 272 |
+
sys.exit(main())
|
reward_manager/episode.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Nanyang Technological University (NTU), Singapore
|
| 2 |
+
# and the verl-agent (GiGPO) team.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
"""
|
| 17 |
+
Episode-level reward assignment for RL training on **incident_env**.
|
| 18 |
+
|
| 19 |
+
Rollout collation should attach episode summaries in ``non_tensor_batch`` that
|
| 20 |
+
mirror the HTTP env (see ``server/incident_environment.py`` / AGENTS.md):
|
| 21 |
+
|
| 22 |
+
- ``episode_rewards`` (float): cumulative **oracle-shaped** training reward
|
| 23 |
+
(sum of per-step rewards from ``_compute_reward()``).
|
| 24 |
+
- ``episode_lengths`` (int): number of agent steps in the episode (same idea
|
| 25 |
+
as ``step_count`` / ``info["steps_taken"]`` on the final step).
|
| 26 |
+
- ``data_source`` **or** ``task_name``: incident task id — ``memory_leak``,
|
| 27 |
+
``cascading_failure``, or ``distributed_deadlock`` (see ``tasks.py``).
|
| 28 |
+
|
| 29 |
+
Evaluation **grader** scores from ``BaseScenario.grade()`` live in rollout
|
| 30 |
+
metadata separately; this manager uses training episode return by default.
|
| 31 |
+
|
| 32 |
+
If ``rm_scores`` is already present in ``batch``, it is returned unchanged
|
| 33 |
+
(optional override from an external reward model).
|
| 34 |
+
|
| 35 |
+
``data`` may be verl's ``DataProto`` when installed; any batched object with
|
| 36 |
+
``__len__``, ``__getitem__``, ``.batch``, and ``.non_tensor_batch`` matching
|
| 37 |
+
the above keys works.
|
| 38 |
+
"""
|
| 39 |
+
|
| 40 |
+
from __future__ import annotations
|
| 41 |
+
|
| 42 |
+
import random
|
| 43 |
+
from typing import Any, Dict, Union
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
import torch
|
| 47 |
+
except ImportError: # pragma: no cover - training stacks normally ship torch
|
| 48 |
+
torch = None # type: ignore[misc, assignment]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _as_float(x: Any) -> float:
|
| 52 |
+
if isinstance(x, (float, int)):
|
| 53 |
+
return float(x)
|
| 54 |
+
if hasattr(x, "item"):
|
| 55 |
+
return float(x.item())
|
| 56 |
+
return float(x)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _as_int(x: Any) -> int:
|
| 60 |
+
if isinstance(x, int):
|
| 61 |
+
return x
|
| 62 |
+
if isinstance(x, float):
|
| 63 |
+
return int(x)
|
| 64 |
+
if hasattr(x, "item"):
|
| 65 |
+
return int(x.item())
|
| 66 |
+
return int(x)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _resolve_task_label(non_tensor_batch: Dict[str, Any]) -> str:
|
| 70 |
+
if "data_source" in non_tensor_batch:
|
| 71 |
+
return str(non_tensor_batch["data_source"])
|
| 72 |
+
if "task_name" in non_tensor_batch:
|
| 73 |
+
return str(non_tensor_batch["task_name"])
|
| 74 |
+
return "unknown"
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _episode_score(
|
| 78 |
+
episode_rewards: Any,
|
| 79 |
+
episode_lengths: Any,
|
| 80 |
+
normalize_by_length: bool,
|
| 81 |
+
) -> float:
|
| 82 |
+
r = _as_float(episode_rewards)
|
| 83 |
+
if normalize_by_length:
|
| 84 |
+
length = max(1, _as_int(episode_lengths))
|
| 85 |
+
return r / length
|
| 86 |
+
return r
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class EpisodeRewardManager:
|
| 90 |
+
"""Maps incident episode return onto the last valid response token for RL."""
|
| 91 |
+
|
| 92 |
+
def __init__(
|
| 93 |
+
self,
|
| 94 |
+
tokenizer: Any,
|
| 95 |
+
num_examine: int,
|
| 96 |
+
normalize_by_length: bool = False,
|
| 97 |
+
) -> None:
|
| 98 |
+
self.tokenizer = tokenizer
|
| 99 |
+
self.num_examine = num_examine
|
| 100 |
+
self.normalize_by_length = normalize_by_length
|
| 101 |
+
|
| 102 |
+
def __call__(
|
| 103 |
+
self,
|
| 104 |
+
data: Any,
|
| 105 |
+
return_dict: bool = False,
|
| 106 |
+
) -> Union["torch.Tensor", Dict[str, Any]]:
|
| 107 |
+
if torch is None:
|
| 108 |
+
raise ImportError(
|
| 109 |
+
"EpisodeRewardManager requires `torch` (e.g. pip install torch)."
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
# If there is rm score, we directly return rm score.
|
| 113 |
+
batch_keys = data.batch.keys()
|
| 114 |
+
if "rm_scores" in batch_keys:
|
| 115 |
+
if return_dict:
|
| 116 |
+
return {"reward_tensor": data.batch["rm_scores"]}
|
| 117 |
+
return data.batch["rm_scores"]
|
| 118 |
+
|
| 119 |
+
reward_tensor = torch.zeros_like(
|
| 120 |
+
data.batch["responses"], dtype=torch.float32
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
already_print_data_sources: Dict[str, int] = {}
|
| 124 |
+
reward_extra_info: Dict[str, list] = {
|
| 125 |
+
"task_name": [],
|
| 126 |
+
"episode_return": [],
|
| 127 |
+
"episode_length": [],
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
for i in range(len(data)):
|
| 131 |
+
data_item = data[i]
|
| 132 |
+
|
| 133 |
+
prompt_ids = data_item.batch["prompts"]
|
| 134 |
+
prompt_length = prompt_ids.shape[-1]
|
| 135 |
+
|
| 136 |
+
valid_prompt_length = int(
|
| 137 |
+
data_item.batch["attention_mask"][:prompt_length].sum().item()
|
| 138 |
+
)
|
| 139 |
+
valid_prompt_ids = prompt_ids[-valid_prompt_length:]
|
| 140 |
+
|
| 141 |
+
response_ids = data_item.batch["responses"]
|
| 142 |
+
valid_response_length = int(
|
| 143 |
+
data_item.batch["attention_mask"][prompt_length:].sum().item()
|
| 144 |
+
)
|
| 145 |
+
valid_response_ids = response_ids[:valid_response_length]
|
| 146 |
+
|
| 147 |
+
prompt_str = self.tokenizer.decode(
|
| 148 |
+
valid_prompt_ids, skip_special_tokens=False
|
| 149 |
+
)
|
| 150 |
+
response_str = self.tokenizer.decode(
|
| 151 |
+
valid_response_ids, skip_special_tokens=False
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
ntb = data_item.non_tensor_batch
|
| 155 |
+
data_source = _resolve_task_label(ntb)
|
| 156 |
+
|
| 157 |
+
episode_rewards = ntb["episode_rewards"]
|
| 158 |
+
episode_lengths = ntb["episode_lengths"]
|
| 159 |
+
score = _episode_score(
|
| 160 |
+
episode_rewards, episode_lengths, self.normalize_by_length
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
last_tok = max(0, valid_response_length - 1)
|
| 164 |
+
reward_tensor[i, last_tok] = torch.tensor(
|
| 165 |
+
score, dtype=torch.float32, device=prompt_ids.device
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
reward_extra_info["task_name"].append(data_source)
|
| 169 |
+
reward_extra_info["episode_return"].append(_as_float(episode_rewards))
|
| 170 |
+
reward_extra_info["episode_length"].append(_as_int(episode_lengths))
|
| 171 |
+
|
| 172 |
+
if data_source not in already_print_data_sources:
|
| 173 |
+
already_print_data_sources[data_source] = 0
|
| 174 |
+
|
| 175 |
+
if (
|
| 176 |
+
already_print_data_sources[data_source] < self.num_examine
|
| 177 |
+
and random.random() < 0.1
|
| 178 |
+
):
|
| 179 |
+
already_print_data_sources[data_source] += 1
|
| 180 |
+
print(f"[{data_source}][prompt]", prompt_str)
|
| 181 |
+
print(f"[{data_source}][response]", response_str)
|
| 182 |
+
print(f"[{data_source}][score]", score)
|
| 183 |
+
|
| 184 |
+
if return_dict:
|
| 185 |
+
return {
|
| 186 |
+
"reward_tensor": reward_tensor,
|
| 187 |
+
"reward_extra_info": reward_extra_info,
|
| 188 |
+
}
|
| 189 |
+
return reward_tensor
|
reward_manager/long_horizon_reward.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Long-horizon reward design for incident_env training.
|
| 3 |
+
|
| 4 |
+
This module is a drop-in, more extensive alternative to the current
|
| 5 |
+
`IncidentEnvironment._compute_reward()` shaping. It keeps the same action space
|
| 6 |
+
and scenario contract while adding:
|
| 7 |
+
|
| 8 |
+
- progressive time pressure
|
| 9 |
+
- novelty / anti-thrashing incentives
|
| 10 |
+
- alert and service-health progress shaping
|
| 11 |
+
- phase-aware milestone bonuses
|
| 12 |
+
- terminal quality bonus based on restoration + time efficiency
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from dataclasses import dataclass, field
|
| 18 |
+
from typing import Any, Dict, Optional, Set, Tuple
|
| 19 |
+
|
| 20 |
+
from models import ActionType, DIAGNOSTIC_ACTIONS, IncidentAction, REMEDIATION_ACTIONS
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
@dataclass
|
| 24 |
+
class LongHorizonRewardConfig:
|
| 25 |
+
# Step pressure
|
| 26 |
+
base_step_penalty: float = 0.01
|
| 27 |
+
late_step_penalty: float = 0.04
|
| 28 |
+
|
| 29 |
+
# Action quality
|
| 30 |
+
invalid_action_penalty: float = 0.08
|
| 31 |
+
repeat_action_penalty: float = 0.06
|
| 32 |
+
novelty_bonus: float = 0.03
|
| 33 |
+
oscillation_penalty: float = 0.03
|
| 34 |
+
|
| 35 |
+
# Investigation
|
| 36 |
+
diagnostic_relevant_bonus: float = 0.10
|
| 37 |
+
diagnostic_irrelevant_bonus: float = 0.02
|
| 38 |
+
first_view_alerts_bonus: float = 0.08
|
| 39 |
+
|
| 40 |
+
# Remediation
|
| 41 |
+
remediation_root_bonus: float = 0.20
|
| 42 |
+
remediation_related_bonus: float = 0.08
|
| 43 |
+
remediation_wrong_penalty: float = 0.14
|
| 44 |
+
first_correct_remediation_bonus: float = 0.10
|
| 45 |
+
|
| 46 |
+
# Root cause declaration
|
| 47 |
+
declare_correct_bonus: float = 0.30
|
| 48 |
+
declare_partial_bonus: float = 0.12
|
| 49 |
+
declare_wrong_penalty: float = 0.20
|
| 50 |
+
|
| 51 |
+
# Progress shaping (state deltas)
|
| 52 |
+
healthy_service_delta_weight: float = 0.35
|
| 53 |
+
alert_delta_weight: float = 0.20
|
| 54 |
+
potential_weight: float = 0.25
|
| 55 |
+
|
| 56 |
+
# Terminal shaping
|
| 57 |
+
terminal_full_restore_bonus: float = 0.35
|
| 58 |
+
terminal_partial_restore_scale: float = 0.20
|
| 59 |
+
terminal_no_declaration_penalty: float = 0.20
|
| 60 |
+
terminal_over_budget_penalty: float = 0.12
|
| 61 |
+
|
| 62 |
+
# Output clipping
|
| 63 |
+
min_step_reward: float = -0.60
|
| 64 |
+
max_step_reward: float = 0.80
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@dataclass
|
| 68 |
+
class EpisodeRewardState:
|
| 69 |
+
seen_action_targets: Set[Tuple[str, Optional[str]]] = field(default_factory=set)
|
| 70 |
+
saw_view_alerts: bool = False
|
| 71 |
+
first_correct_remediation_awarded: bool = False
|
| 72 |
+
previous_action: Optional[Tuple[str, Optional[str]]] = None
|
| 73 |
+
max_alerts_seen: int = 1
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@dataclass
|
| 77 |
+
class RewardBreakdown:
|
| 78 |
+
total: float
|
| 79 |
+
components: Dict[str, float]
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _keyword_match_ratio(declared_text: str, keywords: list[str]) -> float:
|
| 83 |
+
if not keywords:
|
| 84 |
+
return 0.0
|
| 85 |
+
declared = (declared_text or "").lower()
|
| 86 |
+
matched = sum(1 for kw in keywords if kw.lower() in declared)
|
| 87 |
+
return matched / max(1, len(keywords))
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _health_ratio(obs: Dict[str, Any]) -> float:
|
| 91 |
+
statuses = obs.get("service_statuses", {}) or {}
|
| 92 |
+
if not statuses:
|
| 93 |
+
return 0.0
|
| 94 |
+
healthy = sum(1 for s in statuses.values() if s == "healthy")
|
| 95 |
+
return healthy / max(1, len(statuses))
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def _alerts_count(obs: Dict[str, Any]) -> int:
|
| 99 |
+
return int(obs.get("active_alerts_count", 0) or 0)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class LongHorizonRewardEngine:
|
| 103 |
+
"""Stateful per-episode reward engine."""
|
| 104 |
+
|
| 105 |
+
def __init__(self, config: Optional[LongHorizonRewardConfig] = None) -> None:
|
| 106 |
+
self.cfg = config or LongHorizonRewardConfig()
|
| 107 |
+
self.state = EpisodeRewardState()
|
| 108 |
+
|
| 109 |
+
def reset(self) -> None:
|
| 110 |
+
self.state = EpisodeRewardState()
|
| 111 |
+
|
| 112 |
+
def compute(
|
| 113 |
+
self,
|
| 114 |
+
*,
|
| 115 |
+
action: IncidentAction,
|
| 116 |
+
scenario: Any,
|
| 117 |
+
prev_obs: Dict[str, Any],
|
| 118 |
+
curr_obs: Dict[str, Any],
|
| 119 |
+
is_valid_action: bool,
|
| 120 |
+
is_repeat: bool,
|
| 121 |
+
root_cause_declared: bool,
|
| 122 |
+
episode_done: bool,
|
| 123 |
+
step_count: int,
|
| 124 |
+
max_steps: int,
|
| 125 |
+
time_elapsed_minutes: int,
|
| 126 |
+
time_budget_minutes: int,
|
| 127 |
+
) -> RewardBreakdown:
|
| 128 |
+
c: Dict[str, float] = {}
|
| 129 |
+
at = action.parsed_type()
|
| 130 |
+
target = action.target_service
|
| 131 |
+
pair = (action.action_type, target)
|
| 132 |
+
|
| 133 |
+
progress = min(1.0, max(0.0, step_count / max(1, max_steps)))
|
| 134 |
+
c["step_penalty"] = -(
|
| 135 |
+
self.cfg.base_step_penalty
|
| 136 |
+
+ (self.cfg.late_step_penalty - self.cfg.base_step_penalty) * progress
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
if not is_valid_action:
|
| 140 |
+
c["invalid_action"] = -self.cfg.invalid_action_penalty
|
| 141 |
+
total = sum(c.values())
|
| 142 |
+
total = max(self.cfg.min_step_reward, min(self.cfg.max_step_reward, total))
|
| 143 |
+
return RewardBreakdown(total=round(total, 3), components=c)
|
| 144 |
+
|
| 145 |
+
if is_repeat:
|
| 146 |
+
c["repeat_action"] = -self.cfg.repeat_action_penalty
|
| 147 |
+
elif pair not in self.state.seen_action_targets:
|
| 148 |
+
c["novelty"] = self.cfg.novelty_bonus
|
| 149 |
+
self.state.seen_action_targets.add(pair)
|
| 150 |
+
|
| 151 |
+
# Investigation / remediation / declaration
|
| 152 |
+
if at in DIAGNOSTIC_ACTIONS:
|
| 153 |
+
if at == ActionType.VIEW_ALERTS and not self.state.saw_view_alerts:
|
| 154 |
+
c["first_view_alerts"] = self.cfg.first_view_alerts_bonus
|
| 155 |
+
self.state.saw_view_alerts = True
|
| 156 |
+
if target and target in scenario.involved_services:
|
| 157 |
+
c["diagnostic_relevant"] = self.cfg.diagnostic_relevant_bonus
|
| 158 |
+
elif target and target not in scenario.involved_services:
|
| 159 |
+
c["diagnostic_irrelevant"] = self.cfg.diagnostic_irrelevant_bonus
|
| 160 |
+
|
| 161 |
+
elif at in REMEDIATION_ACTIONS:
|
| 162 |
+
if target == scenario.root_cause_service:
|
| 163 |
+
c["remediation_root"] = self.cfg.remediation_root_bonus
|
| 164 |
+
if not self.state.first_correct_remediation_awarded:
|
| 165 |
+
c["first_correct_remediation"] = self.cfg.first_correct_remediation_bonus
|
| 166 |
+
self.state.first_correct_remediation_awarded = True
|
| 167 |
+
elif target and target in scenario.involved_services:
|
| 168 |
+
c["remediation_related"] = self.cfg.remediation_related_bonus
|
| 169 |
+
else:
|
| 170 |
+
c["remediation_wrong"] = -self.cfg.remediation_wrong_penalty
|
| 171 |
+
|
| 172 |
+
elif at == ActionType.DECLARE_ROOT_CAUSE:
|
| 173 |
+
ratio = _keyword_match_ratio(
|
| 174 |
+
action.parameters.get("root_cause", ""),
|
| 175 |
+
list(scenario.root_cause_keywords),
|
| 176 |
+
)
|
| 177 |
+
if ratio >= 0.6:
|
| 178 |
+
c["declare_correct"] = self.cfg.declare_correct_bonus
|
| 179 |
+
elif ratio >= 0.3:
|
| 180 |
+
c["declare_partial"] = self.cfg.declare_partial_bonus
|
| 181 |
+
else:
|
| 182 |
+
c["declare_wrong"] = -self.cfg.declare_wrong_penalty
|
| 183 |
+
|
| 184 |
+
# Anti-thrashing: alternating remediation targets often hurts long-horizon stability.
|
| 185 |
+
if self.state.previous_action is not None:
|
| 186 |
+
prev_at, prev_target = self.state.previous_action
|
| 187 |
+
if (
|
| 188 |
+
prev_at in {a.value for a in REMEDIATION_ACTIONS}
|
| 189 |
+
and action.action_type in {a.value for a in REMEDIATION_ACTIONS}
|
| 190 |
+
and prev_target != target
|
| 191 |
+
):
|
| 192 |
+
c["oscillation"] = -self.cfg.oscillation_penalty
|
| 193 |
+
self.state.previous_action = pair
|
| 194 |
+
|
| 195 |
+
# Progress shaping from observation deltas
|
| 196 |
+
prev_h, curr_h = _health_ratio(prev_obs), _health_ratio(curr_obs)
|
| 197 |
+
c["healthy_delta"] = (curr_h - prev_h) * self.cfg.healthy_service_delta_weight
|
| 198 |
+
|
| 199 |
+
prev_alerts = _alerts_count(prev_obs)
|
| 200 |
+
curr_alerts = _alerts_count(curr_obs)
|
| 201 |
+
self.state.max_alerts_seen = max(self.state.max_alerts_seen, prev_alerts, curr_alerts, 1)
|
| 202 |
+
c["alert_delta"] = (
|
| 203 |
+
(prev_alerts - curr_alerts) / float(self.state.max_alerts_seen)
|
| 204 |
+
) * self.cfg.alert_delta_weight
|
| 205 |
+
|
| 206 |
+
# Potential-based shaping to stabilize long-horizon learning
|
| 207 |
+
prev_phi = prev_h + (1.0 - min(1.0, prev_alerts / float(self.state.max_alerts_seen)))
|
| 208 |
+
curr_phi = curr_h + (1.0 - min(1.0, curr_alerts / float(self.state.max_alerts_seen)))
|
| 209 |
+
c["potential"] = (curr_phi - prev_phi) * self.cfg.potential_weight
|
| 210 |
+
|
| 211 |
+
# Terminal shaping
|
| 212 |
+
if episode_done:
|
| 213 |
+
if curr_h >= 0.999:
|
| 214 |
+
c["terminal_restore"] = self.cfg.terminal_full_restore_bonus
|
| 215 |
+
else:
|
| 216 |
+
c["terminal_partial_restore"] = curr_h * self.cfg.terminal_partial_restore_scale
|
| 217 |
+
if not root_cause_declared:
|
| 218 |
+
c["terminal_no_declaration"] = -self.cfg.terminal_no_declaration_penalty
|
| 219 |
+
if time_elapsed_minutes > time_budget_minutes:
|
| 220 |
+
c["terminal_over_budget"] = -self.cfg.terminal_over_budget_penalty
|
| 221 |
+
|
| 222 |
+
total = sum(c.values())
|
| 223 |
+
total = max(self.cfg.min_step_reward, min(self.cfg.max_step_reward, total))
|
| 224 |
+
return RewardBreakdown(total=round(total, 3), components={k: round(v, 4) for k, v in c.items()})
|
| 225 |
+
|