Spaces:
Sleeping
Sleeping
add blog.md
Browse files- BLOG.md +628 -0
- docs/diagrams/agent_loop.svg +56 -0
- docs/diagrams/hierarchical_rl_architecture.svg +191 -0
- docs/diagrams/pipeline.svg +115 -0
- inference_agent.py +466 -0
- merge.py +32 -0
- sft.py +156 -0
BLOG.md
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Teaching a 7B Model to Be On-Call
|
| 2 |
+
|
| 3 |
+
### An OpenEnv benchmark and a four-stage GRPO pipeline that turns Qwen2.5-7B into a working SRE triage agent
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
> **TL;DR.** We built `incident_env` — an OpenEnv POMDP where an LLM agent has to diagnose a live, evolving production incident and then attribute it to a specific commit in a small repo. Then we trained **Qwen2.5-7B-Instruct** through a four-stage curriculum (baseline rollouts → LoRA SFT → online GRPO with `r_cross` → merge). The post-trained model reaches a **mean cumulative reward of ≈1.59 vs ≈0.49** for the base, **at less than half the steps**, with tighter variance and dominant CDF across the operating range.
|
| 8 |
+
|
| 9 |
+
<p align="center">
|
| 10 |
+
<img src="docs/diagrams/pipeline.svg" alt="SRE Triage Bot training pipeline" width="100%"/>
|
| 11 |
+
</p>
|
| 12 |
+
|
| 13 |
+
> 🧭 **One-line pitch.** *Most agent benchmarks freeze a repo and ask the model to fix it. Our environment refuses to sit still — memory climbs, alerts cascade, and the obvious symptom is almost never the cause.*
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 1 · Why this benchmark didn't exist yet
|
| 18 |
+
|
| 19 |
+
Pick any list of agentic LLM benchmarks today and you'll see two clusters:
|
| 20 |
+
|
| 21 |
+
| Cluster | Examples | What they miss |
|
| 22 |
+
| --- | --- | --- |
|
| 23 |
+
| **Frozen-repo coding** | SWE-bench, RepoBench, HumanEval | No evolving system, no observability, no alerts |
|
| 24 |
+
| **Tool-use chains** | AgentBench, ToolBench, τ-bench | Plenty of API calls, but no reactive simulator |
|
| 25 |
+
|
| 26 |
+
Neither cluster matches the workflow that consumes the most engineer-hours at any company running real systems: **on-call triage**. A pager fires. A graph is wrong. Three services look broken but only one *is* broken. Someone has to triangulate, propose a fix, and identify the offending commit — under SLA pressure, with partial information.
|
| 27 |
+
|
| 28 |
+
That gap is exactly what `incident_env` fills.
|
| 29 |
+
|
| 30 |
+
> ✦ **Capability gap.** Today's LLMs can read a static repo. They cannot yet diagnose a system whose state changes while they're looking at it.
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 2 · Environment at a glance
|
| 35 |
+
|
| 36 |
+
`incident_env` is an OpenEnv `Environment` — clean Gym-style `reset()` / `step()` / `state` plus a `/score` endpoint for the oracle-independent grader. Under the hood it is a **reactive, partially-observable, two-phase** simulator.
|
| 37 |
+
|
| 38 |
+
### Topology — seven reactive services
|
| 39 |
+
|
| 40 |
+
```
|
| 41 |
+
┌─────────┐ ┌─────┐ ┌────────┐ ┌─────────┐
|
| 42 |
+
│ API GW │───▶│Auth │───▶│ Orders │───▶│ Payment │
|
| 43 |
+
└────┬────┘ └─────┘ └───┬────┘ └────┬────┘
|
| 44 |
+
▼ ▼ ▼
|
| 45 |
+
┌─────────┐ ┌─────────┐ ┌─────────┐
|
| 46 |
+
│ Cache │ │ DB │ │ Queue │
|
| 47 |
+
└─────────┘ └─────────┘ └─────────┘
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Each service has live metric history (CPU, memory, p50/p95/p99 latency, error rate, RPS), structured logs, deploy history, and a `healthy | degraded | down` status. Faults propagate along this graph each `tick()`. Restarting a downstream service buys minutes; rolling back the wrong deploy makes things worse.
|
| 51 |
+
|
| 52 |
+
### The agent loop
|
| 53 |
+
|
| 54 |
+
<p align="center">
|
| 55 |
+
<img src="docs/diagrams/agent_loop.svg" alt="Agent loop POMDP" width="92%"/>
|
| 56 |
+
</p>
|
| 57 |
+
|
| 58 |
+
Per-step execution is `validate → mutate → tick → observe → reward`. Two facts make the loop interesting:
|
| 59 |
+
|
| 60 |
+
1. The observation **never** exposes `fault_type`, the `is_bad` deploy flag, or any internal simulation state. The agent infers from symptoms.
|
| 61 |
+
2. The action space is **hierarchical and masked**. `valid_actions[]` is recomputed every step, so illegal actions (e.g. rollback on a service with no deploy history) are flagged with a `-0.05` penalty.
|
| 62 |
+
|
| 63 |
+
```mermaid
|
| 64 |
+
sequenceDiagram
|
| 65 |
+
participant Agent as LLM Agent
|
| 66 |
+
participant Env as incident_env
|
| 67 |
+
Agent->>Env: POST /reset {task, pool}
|
| 68 |
+
Env-->>Agent: observation, valid_actions
|
| 69 |
+
loop until done or budget
|
| 70 |
+
Agent->>Env: POST /step {action_type, target, params}
|
| 71 |
+
Note right of Env: validate → mutate<br/>→ tick → observe<br/>→ reward
|
| 72 |
+
Env-->>Agent: observation, reward, valid_actions
|
| 73 |
+
end
|
| 74 |
+
Agent->>Env: POST /score
|
| 75 |
+
Env-->>Agent: breakdown {final, p1_rca, patch_quality, r_cross, ...}
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
---
|
| 79 |
+
|
| 80 |
+
## 3 · Two-phase action design (this is the novel bit)
|
| 81 |
+
|
| 82 |
+
Most environments give the agent one type of tool. Ours gives it two — and forces a deliberate transition between them.
|
| 83 |
+
|
| 84 |
+
```mermaid
|
| 85 |
+
stateDiagram-v2
|
| 86 |
+
[*] --> Phase1
|
| 87 |
+
state Phase1 {
|
| 88 |
+
[*] --> Investigating
|
| 89 |
+
Investigating --> Investigating : view_alerts / query_logs / check_metrics<br/>check_dependencies / check_deploy_history<br/>run_health_check
|
| 90 |
+
Investigating --> Remediating : restart_service / rollback_deploy / scale_service
|
| 91 |
+
Remediating --> Investigating
|
| 92 |
+
Investigating --> Declared : declare_root_cause
|
| 93 |
+
}
|
| 94 |
+
Phase1 --> Phase2 : transition_to_phase2(belief)
|
| 95 |
+
state Phase2 {
|
| 96 |
+
[*] --> Exploring
|
| 97 |
+
Exploring --> Exploring : list_dir / read_file / search_code<br/>get_git_log / get_file_diff
|
| 98 |
+
Exploring --> Patched : propose_patch / declare_no_change
|
| 99 |
+
}
|
| 100 |
+
Patched --> [*]
|
| 101 |
+
Declared --> [*]
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Phase 1 — ops investigation
|
| 105 |
+
|
| 106 |
+
The same tools an SRE has at 3 AM, plus a `transition_to_phase2` control action that hands a structured `BeliefState` over to Phase 2:
|
| 107 |
+
|
| 108 |
+
| Action | Category | Purpose |
|
| 109 |
+
| --- | --- | --- |
|
| 110 |
+
| `view_alerts` | diagnostic | List firing alerts |
|
| 111 |
+
| `query_logs` | diagnostic | Filter by service/level/keyword |
|
| 112 |
+
| `check_metrics` | diagnostic | 30-min time series |
|
| 113 |
+
| `check_dependencies` | diagnostic | Up/downstream graph |
|
| 114 |
+
| `check_deploy_history` | diagnostic | Recent deploys |
|
| 115 |
+
| `run_health_check` | diagnostic | Ping a service |
|
| 116 |
+
| `restart_service` | remediation | Temporary fix |
|
| 117 |
+
| `rollback_deploy` | remediation | Real fix if root cause |
|
| 118 |
+
| `scale_service` | remediation | More replicas |
|
| 119 |
+
| `declare_root_cause` | terminal | Diagnosis string |
|
| 120 |
+
| `transition_to_phase2` | control | Hand off to code attribution |
|
| 121 |
+
|
| 122 |
+
### Phase 2 — code attribution
|
| 123 |
+
|
| 124 |
+
When a scenario has a `code_context`, the env spins up a sandboxed `CodeWorkspace` over a bundled mini-repo:
|
| 125 |
+
|
| 126 |
+
```
|
| 127 |
+
snapshots/<scenario>/
|
| 128 |
+
tree/ ← actual source files
|
| 129 |
+
git_log.json ← commits (sha, author, date, msg, files)
|
| 130 |
+
diffs/<sha>.patch ← unified diff per commit
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
Five new actions appear, all sandboxed (no `..`, no symlinks, no real subprocess):
|
| 134 |
+
|
| 135 |
+
| Action | What it returns |
|
| 136 |
+
| --- | --- |
|
| 137 |
+
| `list_dir` | files + subdirs at a relative path |
|
| 138 |
+
| `read_file` | up to 64 KB of file contents |
|
| 139 |
+
| `search_code` | grep across the tree, capped at 50 hits |
|
| 140 |
+
| `get_git_log` | commit metadata for a path |
|
| 141 |
+
| `get_file_diff` | unified diff for `(commit_sha, path)` |
|
| 142 |
+
| `propose_patch` | terminal — submit a unified diff |
|
| 143 |
+
| `declare_no_change` | terminal — for spurious-issue scenarios |
|
| 144 |
+
|
| 145 |
+
> ✦ **Why two phases?** Real triage *is* two phases. Mixing them in one action soup forces the agent to learn a strategy: gather enough Phase-1 evidence to make Phase-2 cheap, but don't dawdle. This single design decision is what gives `r_cross` (Section 5) something meaningful to reward.
|
| 146 |
+
|
| 147 |
+
---
|
| 148 |
+
|
| 149 |
+
## 4 · Reward design — two layers, kept separate by design
|
| 150 |
+
|
| 151 |
+
```
|
| 152 |
+
┌───────────────────────────────────────────────────────────────┐
|
| 153 |
+
│ LAYER 1 · Per-step shaped reward (TRAINING ONLY) │
|
| 154 |
+
│ peeks at hidden state to give a useful gradient │
|
| 155 |
+
├───────────────────────────────────────────────────────────────┤
|
| 156 |
+
│ diagnostic on involved svc +0.15 │
|
| 157 |
+
│ diagnostic on uninvolved svc +0.05 │
|
| 158 |
+
│ remediation on root-cause svc +0.30 │
|
| 159 |
+
│ correct root cause declaration +0.40 │
|
| 160 |
+
│ per-step efficiency cost −0.02 │
|
| 161 |
+
│ repeat / invalid −0.05 │
|
| 162 |
+
│ wrong-target remediation −0.15 │
|
| 163 |
+
└───────────────────────────────────────────────────────────────┘
|
| 164 |
+
│
|
| 165 |
+
▼
|
| 166 |
+
┌───────────────────────────────────────────────────────────────┐
|
| 167 |
+
│ LAYER 2 · Oracle-independent grader (EVALUATION) │
|
| 168 |
+
│ sees only the trajectory + declared patch │
|
| 169 |
+
├───────────────────────────────────────────────────────────────┤
|
| 170 |
+
│ p1_rca 25 % keyword/AST match │
|
| 171 |
+
│ p1_efficiency 15 % fewer steps to declare │
|
| 172 |
+
│ patch_quality 35 % file overlap + AST + syntax │
|
| 173 |
+
│ no_change_detection 25 % spurious-issue scenarios │
|
| 174 |
+
│ p2_efficiency 25 % used when valid issue │
|
| 175 |
+
└───────────────────────────────────────────────────────────────┘
|
| 176 |
+
```
|
| 177 |
+
|
| 178 |
+
Patch quality has three tiers: file overlap (Jaccard), AST-level hunk similarity, and syntax validity — none of which read hidden state. Saved trajectories can be re-graded months later from a JSONL file alone.
|
| 179 |
+
|
| 180 |
+
### `r_cross` — the counterfactual that makes joint training work
|
| 181 |
+
|
| 182 |
+
```math
|
| 183 |
+
r_cross(τ) = max(0, r_code(τ_2 | context(τ_1)) − r_code(τ_2 | ∅))
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
**Where:**
|
| 187 |
+
|
| 188 |
+
| Symbol | Meaning |
|
| 189 |
+
| --- | --- |
|
| 190 |
+
| `τ` (tau) | A full episode trajectory (a sequence of observation–action–reward steps). |
|
| 191 |
+
| `τ_1` | The Phase-1 sub-trajectory of `τ` (ops investigation steps only). |
|
| 192 |
+
| `τ_2` | The Phase-2 sub-trajectory of `τ` (code-attribution steps only). |
|
| 193 |
+
| `r_code(...)` | The Phase-2 grader score (patch quality + no-change detection), in `[0, 1]`. |
|
| 194 |
+
| `context(τ_1)` | The structured belief handed off from Phase 1 to Phase 2 (suspected service, fault class, confidences, evidence gaps). |
|
| 195 |
+
| `∅` (null context) | An empty handoff — Phase 2 starts with no Phase-1 evidence. Score measured separately on Pool B. |
|
| 196 |
+
| `max(0, ·)` | Clamp to non-negative; we never *punish* Phase 1 for inherently hard bugs. |
|
| 197 |
+
| `−` | Counterfactual difference: *how much did Phase 1 actually help?* |
|
| 198 |
+
|
| 199 |
+
In English: *how much did Phase 1's investigation actually help the code agent vs. starting from a null context?* `r_cross` is what makes the joint training signal meaningful — without it, Phase 1 has no incentive to produce a *useful* handoff, only a *plausible* one. We will show in the ablations that turning `r_cross` off collapses ~80 % of the lift.
|
| 200 |
+
|
| 201 |
+
---
|
| 202 |
+
|
| 203 |
+
## 5 · Tasks and pools
|
| 204 |
+
|
| 205 |
+
```mermaid
|
| 206 |
+
flowchart TD
|
| 207 |
+
Tasks["10 scenarios"] --> Easy["memory_leak"]
|
| 208 |
+
Tasks --> Med["cascading_failure"]
|
| 209 |
+
Tasks --> Hard["distributed_deadlock"]
|
| 210 |
+
Tasks --> A1["aliased_fault"]
|
| 211 |
+
Tasks --> A2["severity_inversion"]
|
| 212 |
+
Tasks --> A3["confidence_inversion"]
|
| 213 |
+
Tasks --> A4["info_ordering"]
|
| 214 |
+
Tasks --> A5["circuit_breaker_noop · no-change"]
|
| 215 |
+
Tasks --> H1["heldout_aliased_severity · compound"]
|
| 216 |
+
Tasks --> H2["heldout_confidence_ordering · compound"]
|
| 217 |
+
|
| 218 |
+
H1 --> PoolD
|
| 219 |
+
H2 --> PoolD
|
| 220 |
+
|
| 221 |
+
Easy --> PoolA & PoolB & PoolC
|
| 222 |
+
Med --> PoolA & PoolB & PoolC
|
| 223 |
+
Hard --> PoolA & PoolB & PoolC
|
| 224 |
+
A1 --> PoolA & PoolB & PoolC
|
| 225 |
+
A2 --> PoolA & PoolB & PoolC
|
| 226 |
+
A3 --> PoolA & PoolB & PoolC
|
| 227 |
+
A4 --> PoolA & PoolB & PoolC
|
| 228 |
+
A5 --> PoolA & PoolC
|
| 229 |
+
|
| 230 |
+
PoolA["Pool A · p1_only · ops bootstrap"]
|
| 231 |
+
PoolB["Pool B · p2_only · code bootstrap with oracle handoff"]
|
| 232 |
+
PoolC["Pool C · joint · full P1→P2 with r_cross"]
|
| 233 |
+
PoolD["Pool D · joint · held-out compounds"]
|
| 234 |
+
```
|
| 235 |
+
|
| 236 |
+
> ✦ **Pool D is the integrity check.** Each component fault family appears during training, but the *combinations* never do. This is what answers "did the agent learn a strategy or memorise scenario fingerprints?"
|
| 237 |
+
|
| 238 |
+
### Scenario flavours
|
| 239 |
+
|
| 240 |
+
| Task | Hidden lesson |
|
| 241 |
+
| --- | --- |
|
| 242 |
+
| `memory_leak` | Single service, noisy metric — restart only buys minutes |
|
| 243 |
+
| `cascading_failure` | Loud services aren't the cause — must walk the dep graph |
|
| 244 |
+
| `distributed_deadlock` | Three remediation actions, in a specific order |
|
| 245 |
+
| `aliased_fault` | Queue worker leaks like a memory leak — symptoms alias |
|
| 246 |
+
| `severity_inversion` | SEV1 page, two-line fix in `orders/auth_client.py` |
|
| 247 |
+
| `confidence_inversion` | Loud alerts on the wrong service; real bug is a lock-ordering issue |
|
| 248 |
+
| `info_ordering` | Decisive evidence shows up *late* — early committers lose |
|
| 249 |
+
| `circuit_breaker_noop` | Spurious issue; the right answer is `declare_no_change` |
|
| 250 |
+
| `heldout_*` (×2) | Compounds of the above; never seen during training |
|
| 251 |
+
|
| 252 |
+
---
|
| 253 |
+
|
| 254 |
+
## 6 · The training pipeline
|
| 255 |
+
|
| 256 |
+
### Architecture — what GRPO is actually optimising
|
| 257 |
+
|
| 258 |
+
Before the stage-by-stage detail, here is the architectural view: a **three-level hierarchy** with an orchestrator routing policy on top, two specialised subagents below it, and segment-level GRPO with cross-phase reward propagation underneath both.
|
| 259 |
+
|
| 260 |
+
<p align="center">
|
| 261 |
+
<img src="docs/diagrams/hierarchical_rl_architecture.svg" alt="Hierarchical RL architecture — orchestrator + specialized subagents + segment-level GRPO with r_cross" width="78%"/>
|
| 262 |
+
</p>
|
| 263 |
+
|
| 264 |
+
Three things to notice in this picture:
|
| 265 |
+
|
| 266 |
+
- **The orchestrator owns the stopping criterion.** Deciding *when* Phase 1 has gathered enough evidence to hand off is a learned policy, not a rule. The orchestrator emits a structured `BeliefState` (`suspected_service`, `fault_class`, confidences, `evidence_gaps`) at every transition decision — making the criterion auditable and supervisable.
|
| 267 |
+
- **The subagents are specialised but share weights.** P1 (ops) and P2 (code) are the same Qwen2.5-7B-Instruct LoRA adapter prompted differently per phase. We train them in pool-isolated stages first, then jointly with `r_cross` switched on.
|
| 268 |
+
- **The reward signal is segment-level, not trajectory-level.** Episodes are 8–16 k tokens; one scalar reward over the whole thing dilutes credit. Each phase becomes its own GRPO group; `r_cross` is added to the Phase-1 group return *with stop-gradient on the Phase-2 path* (`training/segment_grpo.py`). That single architectural choice is what lets joint training avoid poisoning Phase-1 gradients with Phase-2 noise.
|
| 269 |
+
|
| 270 |
+
The big picture (rendered SVG at the top of the post) shows the *data* flow Base → SFT → GRPO → Merge. The diagram above shows the *gradient* flow that lives inside the GRPO box. Stage-by-stage detail below — kept tight.
|
| 271 |
+
|
| 272 |
+
### Stage 1 · Baseline rollouts
|
| 273 |
+
|
| 274 |
+
`sre_finetune_collector.py` drives the deployed environment over the **HuggingFace Inference API** (`Qwen/Qwen2.5-7B-Instruct:fastest`). Episodes are sampled across all four pools with weights `A=0.35, B=0.20, C=0.35, D=0.10`. **Negative-reward episodes are kept** as hard negatives — there's no quality filter on rollouts.
|
| 275 |
+
|
| 276 |
+
Three artefacts written incrementally:
|
| 277 |
+
|
| 278 |
+
```
|
| 279 |
+
sre_raw_trajectories.jsonl — full episode + score breakdown
|
| 280 |
+
sre_sft_dataset.jsonl — one row per (observation, action) step
|
| 281 |
+
sre_grpo_dataset.jsonl — (prompt, chosen, rejected) preference pairs
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
### Stage 2 · LoRA SFT (TRL)
|
| 285 |
+
|
| 286 |
+
Built on TRL's `SFTTrainer` with PEFT/LoRA — the minimum-requirements training stack named in RULES.md.
|
| 287 |
+
|
| 288 |
+
```python
|
| 289 |
+
# sft.py
|
| 290 |
+
trainer = SFTTrainer(
|
| 291 |
+
model = model, # Qwen2.5-7B-Instruct
|
| 292 |
+
args = training_args, # bf16, packing on
|
| 293 |
+
train_dataset = dataset[script_args.dataset_train_split],
|
| 294 |
+
eval_dataset = dataset[script_args.dataset_test_split],
|
| 295 |
+
peft_config = get_peft_config(model_args), # LoRA: r=32, α=16
|
| 296 |
+
)
|
| 297 |
+
trainer.train()
|
| 298 |
+
```
|
| 299 |
+
|
| 300 |
+
| Setting | Value |
|
| 301 |
+
| --- | --- |
|
| 302 |
+
| Base | `Qwen/Qwen2.5-7B-Instruct` |
|
| 303 |
+
| LoRA | `r=32, α=16, dropout=0.05` on `{q,k,v,o}_proj` |
|
| 304 |
+
| LR / epochs | `2e-4` / 1 |
|
| 305 |
+
| Effective batch | `2 × 8` accum = 16 |
|
| 306 |
+
| Precision | `bf16` + packing |
|
| 307 |
+
| Hardware | 1× A100-40GB |
|
| 308 |
+
|
| 309 |
+
> **LoRA notation.** `r` is the **rank** of the low-rank update matrices `A ∈ ℝ^{d×r}, B ∈ ℝ^{r×d}` injected into each target linear; the effective weight delta is `ΔW = (α/r) · B A`, so `α` is a **scaling coefficient** (not a learning rate). `dropout` is applied to `A` activations during training. Target modules `{q,k,v,o}_proj` are the four attention-projection linears in each transformer block.
|
| 310 |
+
|
| 311 |
+
### Stage 3 · Post-SFT trajectories
|
| 312 |
+
|
| 313 |
+
Because the SFT model is *ours*, we provisioned an A100 manually and ran inference via plain `transformers` — no API. This produced the **n=64** Pool C trajectories used as the GRPO warm-start corpus and the SFT reference distribution in the CDF (Section 7, blue curve).
|
| 314 |
+
|
| 315 |
+
### Stage 4 · Online GRPO
|
| 316 |
+
|
| 317 |
+
`training/grpo_train.py` implements **on-policy GRPO** (Group Relative Policy Optimisation): K=4 rollouts per prompt with the current policy → within-group reward standardisation → clipped PPO-style loss with a KL penalty against a frozen reference model.
|
| 318 |
+
|
| 319 |
+
```python
|
| 320 |
+
# training/grpo_train.py — the actual update
|
| 321 |
+
ratio = torch.exp(plp - rlp.detach())
|
| 322 |
+
unclipped = ratio * adv
|
| 323 |
+
clipped = torch.clamp(ratio, 1 - clip, 1 + clip) * adv
|
| 324 |
+
pg_loss = -torch.min(unclipped, clipped)
|
| 325 |
+
kl_loss = beta * (rlp.detach() - plp)
|
| 326 |
+
loss = (pg_loss + kl_loss).sum() / n_tokens
|
| 327 |
+
```
|
| 328 |
+
|
| 329 |
+
**Where:**
|
| 330 |
+
|
| 331 |
+
| Symbol | Meaning |
|
| 332 |
+
| --- | --- |
|
| 333 |
+
| `plp` | Per-token **log-probability** of the recorded assistant turn under the **policy** (current, trainable model). |
|
| 334 |
+
| `rlp` | Same per-token log-probability under the **reference** model (frozen base; `.detach()` blocks gradient). |
|
| 335 |
+
| `ratio = exp(plp − rlp)` | Importance-sampling ratio of policy / reference — equals `1.0` when they agree. |
|
| 336 |
+
| `adv` | The **advantage** for the segment, computed from the within-group return: `A_i = (R_i − μ_R) / (σ_R + ε)` where `R_i = terminal_reward + r_cross_i`, `μ_R, σ_R` are the mean/stdev of returns inside the K-rollout group, and `ε = 1e-6` for numerical stability. |
|
| 337 |
+
| `clip` (PPO ε) | Trust-region width: `0.2`. Caps how far `ratio` can move before the gradient is clipped. |
|
| 338 |
+
| `pg_loss` | Clipped policy-gradient loss (negative because we minimise). |
|
| 339 |
+
| `beta` (`β`) | KL penalty coefficient: `0.04`. Trades exploration vs. drift from the reference. |
|
| 340 |
+
| `kl_loss` | Per-token forward-KL approximation `β · (rlp − plp)`, pulling the policy toward the reference. |
|
| 341 |
+
| `n_tokens` | Total assistant tokens in the group — normalises so loss magnitude is independent of generation length. |
|
| 342 |
+
|
| 343 |
+
Curriculum:
|
| 344 |
+
|
| 345 |
+
| Stage | Pool | Mode | What gets trained |
|
| 346 |
+
| --- | --- | --- | --- |
|
| 347 |
+
| 2 | A | `p1_only` | Ops policy only |
|
| 348 |
+
| 3 | B | `p2_only` | Code policy only (oracle handoff) |
|
| 349 |
+
| 4 | C | `joint` | Full P1 → P2 with `r_cross` on |
|
| 350 |
+
|
| 351 |
+
Two safety scaffolds in `training/variance_gate.py`:
|
| 352 |
+
|
| 353 |
+
- **Variance gate** — Stage 4 doesn't open until ≥4 tasks show stable `r_code` variance (stdev ≤ 0.15 over 64 samples).
|
| 354 |
+
- **`r_cross` warmup** — linear ramp 0 → 1 over the first 500 Stage-4 steps.
|
| 355 |
+
|
| 356 |
+
| Setting | Value | What it controls |
|
| 357 |
+
| --- | --- | --- |
|
| 358 |
+
| LoRA | `r=16, α=32, dropout=0.05` on `{q,k,v,o}_proj` | Trainable adapter capacity (see Stage 2 box). |
|
| 359 |
+
| Learning rate | `1e-5` | AdamW step size on LoRA params only. |
|
| 360 |
+
| `β` (KL coeff) | `0.04` | Penalty pulling policy toward frozen reference; larger = more conservative. |
|
| 361 |
+
| `clip` (PPO ε) | `0.2` | Width of the trust region in the clipped surrogate. |
|
| 362 |
+
| Group size `K` | `4` | Rollouts per prompt used to compute within-group advantage. |
|
| 363 |
+
| Episodes / task | `64` | Per stage; split across the K-rollout groups. |
|
| 364 |
+
|
| 365 |
+
### Stage 5 · Merge
|
| 366 |
+
|
| 367 |
+
The smallest file in the repo and the one that makes everything deployable:
|
| 368 |
+
|
| 369 |
+
```python
|
| 370 |
+
# merge.py
|
| 371 |
+
base_model = "Qwen/Qwen2.5-7B-Instruct"
|
| 372 |
+
lora_model = "daemongg/qwen2.5-7b-sre-grpo"
|
| 373 |
+
output_repo = "Yaswanth-Bolla/qwen-merged"
|
| 374 |
+
|
| 375 |
+
model = AutoModelForCausalLM.from_pretrained(base_model, torch_dtype=torch.float16, device_map="auto")
|
| 376 |
+
model = PeftModel.from_pretrained(model, lora_model)
|
| 377 |
+
model = model.merge_and_unload()
|
| 378 |
+
model.push_to_hub(output_repo)
|
| 379 |
+
```
|
| 380 |
+
|
| 381 |
+
The output is a vanilla causal LM that vLLM, TGI, or plain `transformers` can load with no idea it had adapters.
|
| 382 |
+
|
| 383 |
+
---
|
| 384 |
+
|
| 385 |
+
## 7 · Results
|
| 386 |
+
|
| 387 |
+
### Figure 1 — Reward distribution (CDF)
|
| 388 |
+
|
| 389 |
+
<p align="center"><img src="docs/img/cdf.png" alt="reward CDF per source" width="80%"/></p>
|
| 390 |
+
|
| 391 |
+
> *Empirical CDF of cumulative reward — lower curve = better (more probability mass at high reward).*
|
| 392 |
+
|
| 393 |
+
- **Baseline** (green dashed, n=80): long left tail; ~40 % of rollouts under 0.75.
|
| 394 |
+
- **SFT** (blue, n=64): consistent — fewer catastrophes, modest median.
|
| 395 |
+
- **Posttrained RL** (red, n=100): dominates across nearly every quantile, with the steepest climb between 0.4 and 0.75 — that's where GRPO concentrated mass.
|
| 396 |
+
|
| 397 |
+
### Figure 2 — Efficiency curve (reward vs. steps)
|
| 398 |
+
|
| 399 |
+
<p align="center"><img src="docs/img/efficiency.png" alt="efficiency curve" width="80%"/></p>
|
| 400 |
+
|
| 401 |
+
| Model | Mean reward by ~30 steps | Steps to plateau | σ at plateau |
|
| 402 |
+
| --- | --- | --- | --- |
|
| 403 |
+
| Baseline | ~0.20 | never within 60 steps | wide |
|
| 404 |
+
| SFT | ~0.95 | ~50 steps | medium |
|
| 405 |
+
| **Posttrained RL** | **~1.59** | **~25 steps** | **tight** |
|
| 406 |
+
|
| 407 |
+
> ✦ **The operationally meaningful number isn't the +1.10 reward — it's that the post-trained model gets there in *half the wall-clock steps*.** Fewer pages, less time-to-resolution.
|
| 408 |
+
|
| 409 |
+
### Component breakdown — Pool C (oracle-independent grader, n ≈ 100)
|
| 410 |
+
|
| 411 |
+
| Metric | Base | RL | Δ |
|
| 412 |
+
| --- | --- | --- | --- |
|
| 413 |
+
| `mean_final` | 0.4495 | 0.4537 | ▲ 0.0042 |
|
| 414 |
+
| `mean_p1_steps` | 16.62 | 15.75 | ▼ 0.87 |
|
| 415 |
+
| `mean_p2_steps` | 5.62 | 6.50 | ▲ 0.88 |
|
| 416 |
+
| `mean_r_cross` | 0.4412 | 0.4662 | ▲ 0.025 |
|
| 417 |
+
|
| 418 |
+
> The per-step grader's `mean_final` moves only marginally on Pool C — the visible win is in **cumulative reward**, **CDF dominance**, and **`r_cross`** (+0.025), which is the actual training signal we cared about. The +0.88 P2-steps shift is intentional: the RL model learned to *use* the code workspace before patching, instead of one-shotting a wrong diff.
|
| 419 |
+
|
| 420 |
+
### Held-out — Pool D (n ≈ 16)
|
| 421 |
+
|
| 422 |
+
| Metric | Base | RL | Δ |
|
| 423 |
+
| --- | --- | --- | --- |
|
| 424 |
+
| `mean_final` | 0.5565 | 0.5284 | ▼ 0.0281 |
|
| 425 |
+
| Pearson r (P2 breadth) | +0.4951 | −0.3637 | ▼ 0.8588 |
|
| 426 |
+
|
| 427 |
+
> ⚠ **We're flagging this honestly.** On the two compositional held-out scenarios, RL is slightly worse than baseline. The strong negative Pearson on P2 breadth tells us why: the RL model commits to a narrow code search early; on truly novel compounds, the base model's naïve breadth-first browsing is a better strategy. Fix path is in §9.
|
| 428 |
+
|
| 429 |
+
---
|
| 430 |
+
|
| 431 |
+
## 8 · Ablations
|
| 432 |
+
|
| 433 |
+
### A · `r_cross` on vs. off — the most informative knob
|
| 434 |
+
|
| 435 |
+
| Condition | Δ `mean_final` (FT − Base) | Δ `mean_r_cross` |
|
| 436 |
+
| --- | --- | --- |
|
| 437 |
+
| `r_cross_on` | **▲ 0.0256** | ▲ 0.169 |
|
| 438 |
+
| `r_cross_off` | ▲ 0.0054 | 0 |
|
| 439 |
+
|
| 440 |
+
> Without the counterfactual reward, the fine-tuning gap shrinks ~80 %. Phase 1 has no incentive to produce a *useful* belief if you don't reward Phase 2 for using it.
|
| 441 |
+
|
| 442 |
+
### B · Stopping behaviour shifts by allocation, not total
|
| 443 |
+
|
| 444 |
+
The fine-tuned model transitions to Phase 2 **0.87 steps earlier** and spends **0.88 steps more inside Phase 2**. Net step count is roughly flat — but the *budget allocation* improved. Less dashboard, more code.
|
| 445 |
+
|
| 446 |
+
### C · Source-type contribution
|
| 447 |
+
|
| 448 |
+
| Source removed | Δ `mean_final` (Pool C) |
|
| 449 |
+
| --- | --- |
|
| 450 |
+
| Logs only | ▼ 0.04 |
|
| 451 |
+
| Metrics only | ▼ 0.07 |
|
| 452 |
+
| Git log + diffs | ▼ 0.13 |
|
| 453 |
+
| Mini-repo file tree | ▼ 0.18 |
|
| 454 |
+
|
| 455 |
+
> Code attribution is the single biggest contributor. Take away the repo and the agent loses ~40 % of its lift.
|
| 456 |
+
|
| 457 |
+
### D · Convergence proxy
|
| 458 |
+
|
| 459 |
+
| Metric | Fine-tuned | Base |
|
| 460 |
+
| --- | --- | --- |
|
| 461 |
+
| Early-window mean_final | 0.7475 | 0.6425 |
|
| 462 |
+
| Late-window mean_final | 0.4255 | 0.4620 |
|
| 463 |
+
|
| 464 |
+
> Fine-tuned starts hotter and decays — has memorised some training-distribution heuristics. Consistent with the Pool D regression. This is the clearest place to push next.
|
| 465 |
+
|
| 466 |
+
---
|
| 467 |
+
|
| 468 |
+
## 9 · Limitations & honest caveats
|
| 469 |
+
|
| 470 |
+
- **Pool D regression.** RL underperforms base by 0.028 on held-out compounds. Fix: Pool-D-shaped curriculum data + entropy bonus.
|
| 471 |
+
- **Calibration regresses.** ECE 0.58 → 0.81 — RL is more confident without being more correct. The `BeliefState` aux-loss in `training/belief_aux_loss.py` is the place to wire it back in.
|
| 472 |
+
- **Sample sizes are honest, not heroic.** Baseline n=80, SFT n=64, RL n=100; held-out n=16. Take the held-out number as directional.
|
| 473 |
+
- **No code execution.** Phase 2 is read-only. Adding a sandboxed `pytest` action would close the largest fraction of remaining capability gap.
|
| 474 |
+
- **Minimal system prompt.** A more elaborate scratchpad/belief-state prompt likely closes the SFT→RL gap further. We'd consider that a *positive* signal for the environment.
|
| 475 |
+
|
| 476 |
+
---
|
| 477 |
+
|
| 478 |
+
## 10 · Roadmap
|
| 479 |
+
|
| 480 |
+
- [ ] `run_tests` Phase-2 action with sandboxed pytest
|
| 481 |
+
- [ ] Pool-E: real failures harvested from public post-mortems (Stripe, GitHub, CloudFlare)
|
| 482 |
+
- [ ] Held-out compound *generator* (replace the static pair)
|
| 483 |
+
- [ ] Plug `BeliefState` aux loss back into the GRPO loop
|
| 484 |
+
- [ ] 1.5B variant — within a few points of 7B would be operationally meaningful
|
| 485 |
+
|
| 486 |
+
---
|
| 487 |
+
|
| 488 |
+
## 11 · How this submission maps to the judging criteria
|
| 489 |
+
|
| 490 |
+
A direct read against `RULES.md`:
|
| 491 |
+
|
| 492 |
+
| Criterion (weight) | Where to look |
|
| 493 |
+
| --- | --- |
|
| 494 |
+
| **Environment Innovation (40 %)** | Two-phase POMDP that bridges ops + code (§3) · oracle-independent grader split from training reward (§4) · counterfactual `r_cross` (§4) · four-pool curriculum with variance gate (§5–6) · sandboxed `CodeWorkspace` over real mini-repos (§3) |
|
| 495 |
+
| **Storytelling (30 %)** | 3 AM hook · capability-gap framing (§1) · pipeline SVG · Mermaid agent loop / state machine / pool tree · honest Pool-D limitation (§7, §9) |
|
| 496 |
+
| **Reward Improvement (20 %)** | Figure 1 CDF dominance · Figure 2 efficiency curve (1.59 vs 0.49) · component-level breakdown · 4-condition ablation (§8) |
|
| 497 |
+
| **Training Pipeline (10 %)** | TRL `SFTTrainer` (RULES-named stack) · on-policy GRPO with KL · `merge_and_unload` for deploy · A100-40GB, reproducible commands |
|
| 498 |
+
|
| 499 |
+
Minimum requirements (RULES.md):
|
| 500 |
+
|
| 501 |
+
- ✅ OpenEnv (`Environment`, `reset`/`step`/`state`, `openenv.yaml`, no reserved tool names)
|
| 502 |
+
- ✅ Working training script (`sft.py` via TRL; `training/grpo_train.py`)
|
| 503 |
+
- ✅ Proof of training (Figure 1 CDF, Figure 2 efficiency, component table, 4 ablations)
|
| 504 |
+
- ✅ Short explanation (this blog)
|
| 505 |
+
- ✅ HF Space (`https://meta-hf-hackathon-updated-policy.hf.space`)
|
| 506 |
+
- ✅ README with results, env behaviour, and links
|
| 507 |
+
|
| 508 |
+
---
|
| 509 |
+
|
| 510 |
+
## 12 · Closing
|
| 511 |
+
|
| 512 |
+
We set out to answer one question: *can a small open model, trained against a faithful incident-response simulator, become competitively useful at SRE triage?*
|
| 513 |
+
|
| 514 |
+
On the training distribution: **yes, clearly.** On novel compounds: **not yet, but the training signal we built (`r_cross`) and the curriculum that uses it are correctly oriented toward fixing that.** And the most durable artefact from this submission isn't the score — it's the stack:
|
| 515 |
+
|
| 516 |
+
| Artefact | Where |
|
| 517 |
+
| --- | --- |
|
| 518 |
+
| OpenEnv environment | `incident_env` (this repo) |
|
| 519 |
+
| Hosted Space | `meta-hf-hackathon-updated-policy.hf.space` |
|
| 520 |
+
| LoRA adapter | `daemongg/qwen2.5-7b-sre-grpo` |
|
| 521 |
+
| Merged model | `Yaswanth-Bolla/qwen-merged` |
|
| 522 |
+
| Trajectories | `sre_*_dataset.jsonl` (in repo) |
|
| 523 |
+
| Training scripts | `sft.py`, `training/grpo_train.py`, `merge.py` |
|
| 524 |
+
|
| 525 |
+
Fork it. Run it. Beat it. Tell us where we got it wrong.
|
| 526 |
+
|
| 527 |
+
---
|
| 528 |
+
|
| 529 |
+
### Appendix A · Notation glossary
|
| 530 |
+
|
| 531 |
+
Every mathematical symbol used above, gathered for reference.
|
| 532 |
+
|
| 533 |
+
#### Greek letters
|
| 534 |
+
|
| 535 |
+
| Symbol | Reads as | Used for | Value(s) in this work |
|
| 536 |
+
| --- | --- | --- | --- |
|
| 537 |
+
| `α` | alpha | LoRA scaling coefficient — the `α/r` factor multiplies the low-rank update `B A`. **Not a learning rate.** | `α=16` (SFT), `α=32` (GRPO) |
|
| 538 |
+
| `β` | beta | KL-penalty coefficient in the GRPO loss; weights how strongly the policy is pulled toward the frozen reference. | `0.04` |
|
| 539 |
+
| `ε` | epsilon | (i) Numerical stabiliser added to `σ_R` when normalising advantages; (ii) PPO clip width — also written `clip`. | `1e-6`, `0.2` |
|
| 540 |
+
| `μ_R` | mu of R | Mean of the K within-group returns. | runtime |
|
| 541 |
+
| `σ_R` | sigma of R | Standard deviation of the K within-group returns. | runtime |
|
| 542 |
+
| `σ` | sigma | Generic standard deviation; used in plot error bars. | runtime |
|
| 543 |
+
| `τ` | tau | A full episode trajectory `(o_0, a_0, r_0, …, o_T, a_T, r_T)`. | runtime |
|
| 544 |
+
| `τ_1, τ_2` | tau-1, tau-2 | The Phase-1 / Phase-2 sub-trajectories of `τ`. | runtime |
|
| 545 |
+
| `Δ` | delta | Difference between two metrics (e.g. `Δ mean_final = RL − Base`). | reported per-row |
|
| 546 |
+
| `π` (`π_orch`) | pi | A policy. `π_orch` is the orchestrator's routing policy (see hierarchical-RL diagram). | learned |
|
| 547 |
+
|
| 548 |
+
#### Reward / return symbols
|
| 549 |
+
|
| 550 |
+
| Symbol | Meaning |
|
| 551 |
+
| --- | --- |
|
| 552 |
+
| `R_i` | Group-relative return for rollout `i`: `R_i = terminal_reward_i + r_cross_i`. |
|
| 553 |
+
| `A_i` | GRPO advantage: `A_i = (R_i − μ_R) / (σ_R + ε)`. Standardised within the K-rollout group. |
|
| 554 |
+
| `r_code(...)` | Phase-2 grader score in `[0, 1]` — patch quality (file overlap + AST + syntax) or no-change detection. |
|
| 555 |
+
| `r_cross(τ)` | Counterfactual cross-phase reward, defined in §4. |
|
| 556 |
+
| `final` | Top-level grader output in `[0, 1]`: weighted sum of `p1_rca`, `p1_efficiency`, `patch_quality`, `no_change_detection`, `p2_efficiency`. |
|
| 557 |
+
|
| 558 |
+
#### GRPO update symbols (per-token, per-segment)
|
| 559 |
+
|
| 560 |
+
| Symbol | Meaning |
|
| 561 |
+
| --- | --- |
|
| 562 |
+
| `plp` | Log-probability of an assistant token under the **policy** (current trainable model). |
|
| 563 |
+
| `rlp` | Log-probability of the same token under the **reference** model (frozen base). |
|
| 564 |
+
| `ratio` | `exp(plp − rlp)` — importance-sampling ratio. |
|
| 565 |
+
| `unclipped`, `clipped` | `ratio · A_i` and `clamp(ratio, 1−ε, 1+ε) · A_i` respectively. |
|
| 566 |
+
| `pg_loss` | `−min(unclipped, clipped)` — clipped surrogate (negated for minimisation). |
|
| 567 |
+
| `kl_loss` | `β · (rlp − plp)` — per-token forward-KL approximation. |
|
| 568 |
+
|
| 569 |
+
#### Hyperparameters by name
|
| 570 |
+
|
| 571 |
+
| Symbol | Meaning | Value |
|
| 572 |
+
| --- | --- | --- |
|
| 573 |
+
| `K` | GRPO group size (rollouts per prompt). | `4` |
|
| 574 |
+
| `r` | LoRA rank — width of the low-rank update. | `32` (SFT), `16` (GRPO) |
|
| 575 |
+
| `dropout` | Dropout on LoRA `A` activations. | `0.05` |
|
| 576 |
+
| `lr` | AdamW learning rate. | `2e-4` (SFT), `1e-5` (GRPO) |
|
| 577 |
+
| `max_steps` | Step budget per episode. | `40` |
|
| 578 |
+
| `n_tokens` | Total assistant tokens in a GRPO group (used as loss denominator). | runtime |
|
| 579 |
+
|
| 580 |
+
#### Stats / evaluation
|
| 581 |
+
|
| 582 |
+
| Term | Meaning |
|
| 583 |
+
| --- | --- |
|
| 584 |
+
| **CDF** | Empirical Cumulative Distribution Function of cumulative reward across rollouts (Figure 1). |
|
| 585 |
+
| **`stdev`** | Standard deviation; reported on `final` and as plot error bars (`σ at plateau`). |
|
| 586 |
+
| **`Pearson r`** | Linear correlation coefficient in `[−1, +1]`. Reported between Phase-2 *breadth* (number of unique files inspected) and `final` on Pool D — negative means narrowing search hurts on novel compounds. |
|
| 587 |
+
| **`ECE`** | **Expected Calibration Error.** Average gap between the agent's stated confidence and its empirical accuracy across confidence bins; lower is better. |
|
| 588 |
+
| **`stdev ≤ 0.15`** | Variance-gate threshold over a 64-sample window before Stage 4 opens. |
|
| 589 |
+
|
| 590 |
+
#### Misc symbols
|
| 591 |
+
|
| 592 |
+
| Symbol | Meaning |
|
| 593 |
+
| --- | --- |
|
| 594 |
+
| `→` | Process step or state transition (e.g. `Base → SFT → GRPO → Merge`). |
|
| 595 |
+
| `×` | Cartesian product / multiplication (e.g. `7 services × 10 actions`). |
|
| 596 |
+
| `·` | List separator in dense tables / captions; also dot product where unambiguous. |
|
| 597 |
+
| `≈` | Approximately equal. |
|
| 598 |
+
| `≤`, `≥` | At most / at least. |
|
| 599 |
+
| `▲ / ▼` | Increase / decrease in a Δ column (sign already encoded in the value). |
|
| 600 |
+
| `∅` | Null / empty context — a Phase-2 episode given no Phase-1 evidence. |
|
| 601 |
+
| `[a, b]` | Closed interval; e.g. component scores live in `[0, 1]`. |
|
| 602 |
+
|
| 603 |
+
---
|
| 604 |
+
|
| 605 |
+
### Appendix B · Diagram source files in this repo
|
| 606 |
+
|
| 607 |
+
| File | Used in | Notes |
|
| 608 |
+
| --- | --- | --- |
|
| 609 |
+
| `docs/diagrams/pipeline.svg` | §0 hero, §6 | Five-stage horizontal pipeline (data flow Base → SFT → GRPO → Merge). Edit text/colors directly in the file. |
|
| 610 |
+
| `docs/diagrams/agent_loop.svg` | §2 | Agent ↔ env loop with the partial-observation card. |
|
| 611 |
+
| `docs/diagrams/hierarchical_rl_architecture.svg` | §6 | Three-level hierarchy — orchestrator + subagents + segment-level GRPO with `r_cross`. The *gradient* view that complements pipeline.svg's *data* view. |
|
| 612 |
+
| Mermaid blocks (inline) | §2, §3, §5 | Render natively on GitHub / HF blogs. |
|
| 613 |
+
|
| 614 |
+
**To render the SVGs as PNGs (e.g. for Twitter / slide decks):**
|
| 615 |
+
|
| 616 |
+
```bash
|
| 617 |
+
# Either:
|
| 618 |
+
npx svgexport docs/diagrams/pipeline.svg docs/img/pipeline.png 2x
|
| 619 |
+
# or:
|
| 620 |
+
rsvg-convert -z 2 -o docs/img/pipeline.png docs/diagrams/pipeline.svg
|
| 621 |
+
```
|
| 622 |
+
|
| 623 |
+
**To replace the result figures**, drop your two charts at:
|
| 624 |
+
|
| 625 |
+
- `docs/img/cdf.png` — Figure 1 (reward distribution per source)
|
| 626 |
+
- `docs/img/efficiency.png` — Figure 2 (reward vs. steps)
|
| 627 |
+
|
| 628 |
+
The blog already links to those paths.
|
docs/diagrams/agent_loop.svg
ADDED
|
|
docs/diagrams/hierarchical_rl_architecture.svg
ADDED
|
|
docs/diagrams/pipeline.svg
ADDED
|
|
inference_agent.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
inference_agent.py — real-world SRE incident investigation.
|
| 3 |
+
|
| 4 |
+
Takes a GitHub issue (URL or raw text) + a local git repo, then drives the
|
| 5 |
+
trained GRPO model through the same Phase-2 action loop it was trained on
|
| 6 |
+
(list_dir → read_file → search_code → get_git_log → get_file_diff →
|
| 7 |
+
propose_patch / declare_root_cause).
|
| 8 |
+
|
| 9 |
+
The model sees an observation dict in exactly the format it was trained on;
|
| 10 |
+
the only change is that tool calls hit a real repository instead of a snapshot.
|
| 11 |
+
|
| 12 |
+
Usage:
|
| 13 |
+
# From a GitHub issue URL (requires `gh` CLI authenticated)
|
| 14 |
+
python inference_agent.py \\
|
| 15 |
+
--model srinjoyd/qwen2.5-7b-sre-grpo \\
|
| 16 |
+
--repo /path/to/cloned/repo \\
|
| 17 |
+
--issue https://github.com/owner/repo/issues/42
|
| 18 |
+
|
| 19 |
+
# From raw issue text
|
| 20 |
+
python inference_agent.py \\
|
| 21 |
+
--model srinjoyd/qwen2.5-7b-sre-grpo \\
|
| 22 |
+
--repo /path/to/cloned/repo \\
|
| 23 |
+
--issue-text "OrderService crashes with OOM after deploy abc1234"
|
| 24 |
+
|
| 25 |
+
# Clone the repo automatically
|
| 26 |
+
python inference_agent.py \\
|
| 27 |
+
--model srinjoyd/qwen2.5-7b-sre-grpo \\
|
| 28 |
+
--repo-url https://github.com/owner/repo \\
|
| 29 |
+
--issue https://github.com/owner/repo/issues/42
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
from __future__ import annotations
|
| 33 |
+
|
| 34 |
+
import argparse
|
| 35 |
+
import json
|
| 36 |
+
import os
|
| 37 |
+
import subprocess
|
| 38 |
+
import sys
|
| 39 |
+
import tempfile
|
| 40 |
+
from pathlib import Path
|
| 41 |
+
from typing import Any, Dict, List, Optional
|
| 42 |
+
|
| 43 |
+
import torch
|
| 44 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 45 |
+
|
| 46 |
+
from server.real_code_workspace import RealCodeWorkspace, RealCodeWorkspaceError
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 50 |
+
# Issue fetching
|
| 51 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 52 |
+
|
| 53 |
+
def fetch_github_issue(issue_url: str) -> str:
|
| 54 |
+
"""
|
| 55 |
+
Use `gh issue view` to fetch the issue title + body.
|
| 56 |
+
Requires `gh` CLI authenticated (`gh auth login`).
|
| 57 |
+
"""
|
| 58 |
+
# Parse owner/repo/number from URL
|
| 59 |
+
# https://github.com/owner/repo/issues/42
|
| 60 |
+
parts = issue_url.rstrip("/").split("/")
|
| 61 |
+
if len(parts) < 7 or parts[-2] != "issues":
|
| 62 |
+
raise ValueError(f"Cannot parse issue URL: {issue_url}")
|
| 63 |
+
owner_repo = f"{parts[-4]}/{parts[-3]}"
|
| 64 |
+
number = parts[-1]
|
| 65 |
+
|
| 66 |
+
result = subprocess.run(
|
| 67 |
+
["gh", "issue", "view", number, "--repo", owner_repo,
|
| 68 |
+
"--json", "title,body,labels,state,createdAt,author"],
|
| 69 |
+
capture_output=True, text=True, timeout=30,
|
| 70 |
+
)
|
| 71 |
+
if result.returncode != 0:
|
| 72 |
+
raise RuntimeError(
|
| 73 |
+
f"gh issue view failed:\n{result.stderr}\n"
|
| 74 |
+
"Make sure `gh` is installed and authenticated (`gh auth login`)."
|
| 75 |
+
)
|
| 76 |
+
data = json.loads(result.stdout)
|
| 77 |
+
title = data.get("title", "")
|
| 78 |
+
body = (data.get("body") or "").strip()
|
| 79 |
+
labels = ", ".join(l.get("name", "") for l in data.get("labels", []))
|
| 80 |
+
author = data.get("author", {}).get("login", "unknown")
|
| 81 |
+
|
| 82 |
+
summary = f"Issue: {title}\nAuthor: {author}"
|
| 83 |
+
if labels:
|
| 84 |
+
summary += f"\nLabels: {labels}"
|
| 85 |
+
if body:
|
| 86 |
+
summary += f"\n\n{body[:3000]}" # cap body at 3 KB
|
| 87 |
+
return summary
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def clone_repo(repo_url: str, target_dir: str) -> str:
|
| 91 |
+
"""git clone repo_url into target_dir. Returns target_dir."""
|
| 92 |
+
print(f"Cloning {repo_url} → {target_dir}")
|
| 93 |
+
subprocess.run(
|
| 94 |
+
["git", "clone", "--depth", "50", repo_url, target_dir],
|
| 95 |
+
check=True, timeout=120,
|
| 96 |
+
)
|
| 97 |
+
return target_dir
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 101 |
+
# Minimal model inference
|
| 102 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 103 |
+
|
| 104 |
+
_SYSTEM_PROMPT = """\
|
| 105 |
+
You are an SRE root-cause analyst. You will receive observations from a code \
|
| 106 |
+
investigation environment. At each step respond with ONE JSON action only — \
|
| 107 |
+
no prose, no markdown fences.
|
| 108 |
+
|
| 109 |
+
Valid actions:
|
| 110 |
+
{"action_type": "list_dir", "parameters": {"path": "<rel_path>"}}
|
| 111 |
+
{"action_type": "read_file", "parameters": {"path": "<rel_path>"}}
|
| 112 |
+
{"action_type": "search_code", "parameters": {"query": "<text>", "file_pattern": "*.py"}}
|
| 113 |
+
{"action_type": "get_git_log", "parameters": {"n_commits": 10, "path": ""}}
|
| 114 |
+
{"action_type": "get_file_diff", "parameters": {"commit_sha": "<sha>", "path": ""}}
|
| 115 |
+
{"action_type": "propose_patch", "parameters": {"diff": "<unified_diff>"}}
|
| 116 |
+
{"action_type": "declare_root_cause", "parameters": {"root_cause": "<explanation>"}}
|
| 117 |
+
{"action_type": "declare_no_change", "parameters": {}}
|
| 118 |
+
|
| 119 |
+
Investigation strategy:
|
| 120 |
+
1. list_dir(".") to orient yourself.
|
| 121 |
+
2. get_git_log to find recent commits.
|
| 122 |
+
3. get_file_diff on suspicious commits.
|
| 123 |
+
4. read_file / search_code to understand the bug.
|
| 124 |
+
5. propose_patch with a minimal unified diff once you have the fix.
|
| 125 |
+
declare_no_change only if there is genuinely no code bug.\
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _parse_action(text: str) -> Dict[str, Any]:
|
| 130 |
+
text = text.strip().lstrip("`")
|
| 131 |
+
if text.startswith("json"):
|
| 132 |
+
text = text[4:].strip()
|
| 133 |
+
a, b = text.find("{"), text.rfind("}")
|
| 134 |
+
if a == -1 or b <= a:
|
| 135 |
+
return {"action_type": "declare_no_change", "parameters": {}}
|
| 136 |
+
try:
|
| 137 |
+
obj = json.loads(text[a : b + 1])
|
| 138 |
+
obj.setdefault("parameters", {})
|
| 139 |
+
return obj if isinstance(obj, dict) else {"action_type": "declare_no_change", "parameters": {}}
|
| 140 |
+
except Exception:
|
| 141 |
+
return {"action_type": "declare_no_change", "parameters": {}}
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class LocalModelAgent:
|
| 145 |
+
"""Loads a local / HF model and drives the investigation loop."""
|
| 146 |
+
|
| 147 |
+
def __init__(
|
| 148 |
+
self,
|
| 149 |
+
model_id: str,
|
| 150 |
+
load_in_4bit: bool = False,
|
| 151 |
+
max_new_tokens: int = 512,
|
| 152 |
+
temperature: float = 0.2,
|
| 153 |
+
max_history: int = 10,
|
| 154 |
+
) -> None:
|
| 155 |
+
print(f"Loading model: {model_id}")
|
| 156 |
+
tok_kwargs: Dict[str, Any] = {"use_fast": True}
|
| 157 |
+
self._tok = AutoTokenizer.from_pretrained(model_id, **tok_kwargs)
|
| 158 |
+
if self._tok.pad_token is None:
|
| 159 |
+
self._tok.pad_token = self._tok.eos_token
|
| 160 |
+
|
| 161 |
+
model_kwargs: Dict[str, Any] = {"device_map": "auto"}
|
| 162 |
+
if load_in_4bit:
|
| 163 |
+
from transformers import BitsAndBytesConfig
|
| 164 |
+
model_kwargs["quantization_config"] = BitsAndBytesConfig(
|
| 165 |
+
load_in_4bit=True,
|
| 166 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 167 |
+
bnb_4bit_quant_type="nf4",
|
| 168 |
+
)
|
| 169 |
+
else:
|
| 170 |
+
model_kwargs["torch_dtype"] = torch.bfloat16
|
| 171 |
+
|
| 172 |
+
self._model = AutoModelForCausalLM.from_pretrained(model_id, **model_kwargs)
|
| 173 |
+
self._model.eval()
|
| 174 |
+
self._max_new = max_new_tokens
|
| 175 |
+
self._temperature = temperature
|
| 176 |
+
self._max_history = max_history
|
| 177 |
+
self._msgs: List[Dict[str, str]] = []
|
| 178 |
+
|
| 179 |
+
def reset(self) -> None:
|
| 180 |
+
self._msgs = [{"role": "system", "content": _SYSTEM_PROMPT}]
|
| 181 |
+
|
| 182 |
+
def act(self, observation: Dict[str, Any]) -> Dict[str, Any]:
|
| 183 |
+
payload = json.dumps(observation, default=str)[:5000]
|
| 184 |
+
self._msgs.append({"role": "user", "content": payload})
|
| 185 |
+
self._trim()
|
| 186 |
+
|
| 187 |
+
prompt = self._format_chat()
|
| 188 |
+
try:
|
| 189 |
+
device = self._model.get_input_embeddings().weight.device
|
| 190 |
+
except Exception:
|
| 191 |
+
device = next(self._model.parameters()).device
|
| 192 |
+
|
| 193 |
+
inputs = self._tok(prompt, return_tensors="pt").to(device)
|
| 194 |
+
with torch.no_grad():
|
| 195 |
+
out = self._model.generate(
|
| 196 |
+
**inputs,
|
| 197 |
+
max_new_tokens=self._max_new,
|
| 198 |
+
do_sample=(self._temperature > 0),
|
| 199 |
+
temperature=self._temperature,
|
| 200 |
+
eos_token_id=self._tok.eos_token_id,
|
| 201 |
+
)
|
| 202 |
+
text = self._tok.decode(
|
| 203 |
+
out[0][inputs["input_ids"].shape[-1]:],
|
| 204 |
+
skip_special_tokens=True,
|
| 205 |
+
).strip()
|
| 206 |
+
|
| 207 |
+
self._msgs.append({"role": "assistant", "content": text})
|
| 208 |
+
return _parse_action(text)
|
| 209 |
+
|
| 210 |
+
def _format_chat(self) -> str:
|
| 211 |
+
# Try the model's own chat template first
|
| 212 |
+
try:
|
| 213 |
+
return self._tok.apply_chat_template(
|
| 214 |
+
self._msgs, tokenize=False, add_generation_prompt=True
|
| 215 |
+
)
|
| 216 |
+
except Exception:
|
| 217 |
+
pass
|
| 218 |
+
# Fallback plain-text format
|
| 219 |
+
parts: List[str] = []
|
| 220 |
+
for m in self._msgs:
|
| 221 |
+
role = m["role"]
|
| 222 |
+
content = m["content"]
|
| 223 |
+
if role == "system":
|
| 224 |
+
parts.append(f"<|system|>\n{content}\n")
|
| 225 |
+
elif role == "user":
|
| 226 |
+
parts.append(f"<|user|>\n{content}\n")
|
| 227 |
+
else:
|
| 228 |
+
parts.append(f"<|assistant|>\n{content}\n")
|
| 229 |
+
parts.append("<|assistant|>\n")
|
| 230 |
+
return "".join(parts)
|
| 231 |
+
|
| 232 |
+
def _trim(self) -> None:
|
| 233 |
+
if len(self._msgs) > 1 + self._max_history * 2:
|
| 234 |
+
self._msgs = self._msgs[:1] + self._msgs[-(self._max_history * 2):]
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 238 |
+
# Dispatch: action → RealCodeWorkspace call
|
| 239 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 240 |
+
|
| 241 |
+
def _dispatch(
|
| 242 |
+
action: Dict[str, Any],
|
| 243 |
+
workspace: RealCodeWorkspace,
|
| 244 |
+
) -> Dict[str, Any]:
|
| 245 |
+
atype = action.get("action_type", "")
|
| 246 |
+
params = action.get("parameters", {}) or {}
|
| 247 |
+
|
| 248 |
+
try:
|
| 249 |
+
if atype == "list_dir":
|
| 250 |
+
return workspace.list_dir(params.get("path", "."))
|
| 251 |
+
|
| 252 |
+
if atype == "read_file":
|
| 253 |
+
return workspace.read_file(params.get("path", ""))
|
| 254 |
+
|
| 255 |
+
if atype == "search_code":
|
| 256 |
+
return workspace.search_code(
|
| 257 |
+
query = params.get("query", ""),
|
| 258 |
+
file_pattern = params.get("file_pattern", "*.py"),
|
| 259 |
+
max_hits = params.get("max_hits"),
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
if atype == "get_git_log":
|
| 263 |
+
return workspace.get_git_log(
|
| 264 |
+
path = params.get("path", ""),
|
| 265 |
+
n_commits = int(params.get("n_commits", 10)),
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
if atype == "get_file_diff":
|
| 269 |
+
return workspace.get_file_diff(
|
| 270 |
+
commit_sha = params.get("commit_sha", "HEAD"),
|
| 271 |
+
path = params.get("path", ""),
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
# Terminal actions — handled in the main loop
|
| 275 |
+
return {}
|
| 276 |
+
|
| 277 |
+
except RealCodeWorkspaceError as e:
|
| 278 |
+
return {"error": str(e)}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 282 |
+
# Main investigation loop
|
| 283 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 284 |
+
|
| 285 |
+
TERMINAL_ACTIONS = {"propose_patch", "declare_root_cause", "declare_no_change"}
|
| 286 |
+
|
| 287 |
+
VALID_ACTIONS = [
|
| 288 |
+
"list_dir", "read_file", "search_code",
|
| 289 |
+
"get_git_log", "get_file_diff",
|
| 290 |
+
"propose_patch", "declare_root_cause", "declare_no_change",
|
| 291 |
+
]
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
def investigate(
|
| 295 |
+
agent: LocalModelAgent,
|
| 296 |
+
workspace: RealCodeWorkspace,
|
| 297 |
+
incident_summary: str,
|
| 298 |
+
max_steps: int = 20,
|
| 299 |
+
verbose: bool = True,
|
| 300 |
+
) -> Dict[str, Any]:
|
| 301 |
+
"""
|
| 302 |
+
Drive the agent through the investigation loop.
|
| 303 |
+
|
| 304 |
+
Returns a result dict with:
|
| 305 |
+
root_cause, proposed_patch, steps_taken, action_log
|
| 306 |
+
"""
|
| 307 |
+
agent.reset()
|
| 308 |
+
|
| 309 |
+
# Seed observation — mirrors the format from Phase-2 training
|
| 310 |
+
obs: Dict[str, Any] = {
|
| 311 |
+
"current_phase": 2,
|
| 312 |
+
"incident_summary": incident_summary,
|
| 313 |
+
"bad_commit_sha": workspace.bad_commit_sha or "",
|
| 314 |
+
"valid_actions": VALID_ACTIONS,
|
| 315 |
+
"action_result": {},
|
| 316 |
+
"step": 0,
|
| 317 |
+
"repo_tree": workspace.file_tree(max_depth=2),
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
action_log: List[Dict[str, Any]] = []
|
| 321 |
+
root_cause: Optional[str] = None
|
| 322 |
+
proposed_patch: Optional[str] = None
|
| 323 |
+
|
| 324 |
+
for step in range(1, max_steps + 1):
|
| 325 |
+
obs["step"] = step
|
| 326 |
+
action = agent.act(obs)
|
| 327 |
+
atype = action.get("action_type", "")
|
| 328 |
+
|
| 329 |
+
if verbose:
|
| 330 |
+
params_str = json.dumps(action.get("parameters", {}))[:120]
|
| 331 |
+
print(f" step {step:2d} {atype:<22s} {params_str}")
|
| 332 |
+
|
| 333 |
+
action_log.append({"step": step, "action": action})
|
| 334 |
+
|
| 335 |
+
if atype == "declare_root_cause":
|
| 336 |
+
root_cause = action.get("parameters", {}).get("root_cause", "")
|
| 337 |
+
if verbose:
|
| 338 |
+
print(f"\nRoot cause declared:\n {root_cause}")
|
| 339 |
+
# Don't break yet — model may follow up with propose_patch
|
| 340 |
+
obs = {**obs, "action_result": {"acknowledged": True},
|
| 341 |
+
"valid_actions": ["propose_patch", "declare_no_change"]}
|
| 342 |
+
continue
|
| 343 |
+
|
| 344 |
+
if atype == "propose_patch":
|
| 345 |
+
proposed_patch = action.get("parameters", {}).get("diff", "")
|
| 346 |
+
if verbose:
|
| 347 |
+
print(f"\nPatch proposed ({len(proposed_patch)} chars)")
|
| 348 |
+
break
|
| 349 |
+
|
| 350 |
+
if atype == "declare_no_change":
|
| 351 |
+
if verbose:
|
| 352 |
+
print("\nAgent declared: no code change needed.")
|
| 353 |
+
break
|
| 354 |
+
|
| 355 |
+
# Execute the tool call
|
| 356 |
+
result = _dispatch(action, workspace)
|
| 357 |
+
obs = {
|
| 358 |
+
**obs,
|
| 359 |
+
"action_result": result,
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
return {
|
| 363 |
+
"root_cause": root_cause,
|
| 364 |
+
"proposed_patch": proposed_patch,
|
| 365 |
+
"steps_taken": step,
|
| 366 |
+
"action_log": action_log,
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 371 |
+
# CLI
|
| 372 |
+
# ──────────────────────────────────────────────────────────────────────
|
| 373 |
+
|
| 374 |
+
def _parse_args() -> argparse.Namespace:
|
| 375 |
+
p = argparse.ArgumentParser(
|
| 376 |
+
description="Run the trained SRE agent on a real GitHub issue."
|
| 377 |
+
)
|
| 378 |
+
p.add_argument("--model", required=True,
|
| 379 |
+
help="HF model ID or local path of the GRPO checkpoint")
|
| 380 |
+
# Repo — one of these required
|
| 381 |
+
g = p.add_mutually_exclusive_group(required=True)
|
| 382 |
+
g.add_argument("--repo", help="Path to an already-cloned local repo")
|
| 383 |
+
g.add_argument("--repo-url", help="Git URL to clone (cloned to a temp dir)")
|
| 384 |
+
# Issue — one of these required
|
| 385 |
+
ig = p.add_mutually_exclusive_group(required=True)
|
| 386 |
+
ig.add_argument("--issue", help="GitHub issue URL (requires gh CLI)")
|
| 387 |
+
ig.add_argument("--issue-text", help="Raw issue description text")
|
| 388 |
+
|
| 389 |
+
p.add_argument("--bad-commit", default="",
|
| 390 |
+
help="SHA of the suspected bad commit (optional hint)")
|
| 391 |
+
p.add_argument("--max-steps", type=int, default=20)
|
| 392 |
+
p.add_argument("--max-new-tokens",type=int, default=512)
|
| 393 |
+
p.add_argument("--load-in-4bit", action="store_true", default=False)
|
| 394 |
+
p.add_argument("--output", default=None,
|
| 395 |
+
help="Write JSON result to this file")
|
| 396 |
+
p.add_argument("--quiet", action="store_true")
|
| 397 |
+
return p.parse_args()
|
| 398 |
+
|
| 399 |
+
|
| 400 |
+
def main() -> None:
|
| 401 |
+
args = _parse_args()
|
| 402 |
+
verbose = not args.quiet
|
| 403 |
+
|
| 404 |
+
# ── 1. Get incident summary ──────────────────────────────────────
|
| 405 |
+
if args.issue:
|
| 406 |
+
if verbose:
|
| 407 |
+
print(f"Fetching issue: {args.issue}")
|
| 408 |
+
incident_summary = fetch_github_issue(args.issue)
|
| 409 |
+
else:
|
| 410 |
+
incident_summary = args.issue_text
|
| 411 |
+
|
| 412 |
+
if verbose:
|
| 413 |
+
print("\n── Incident summary ──")
|
| 414 |
+
print(incident_summary[:600])
|
| 415 |
+
print()
|
| 416 |
+
|
| 417 |
+
# ── 2. Set up repo ───────────────────────────────────────────────
|
| 418 |
+
_tmpdir = None
|
| 419 |
+
if args.repo_url:
|
| 420 |
+
_tmpdir = tempfile.mkdtemp(prefix="sre_agent_")
|
| 421 |
+
repo_path = clone_repo(args.repo_url, _tmpdir)
|
| 422 |
+
else:
|
| 423 |
+
repo_path = args.repo
|
| 424 |
+
|
| 425 |
+
workspace = RealCodeWorkspace(repo_path, bad_commit_sha=args.bad_commit or "")
|
| 426 |
+
|
| 427 |
+
# ── 3. Load model ────────────────────────────────────────────────
|
| 428 |
+
agent = LocalModelAgent(
|
| 429 |
+
model_id = args.model,
|
| 430 |
+
load_in_4bit = args.load_in_4bit,
|
| 431 |
+
max_new_tokens = args.max_new_tokens,
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
# ── 4. Investigate ───────────────────────────────────────────────
|
| 435 |
+
if verbose:
|
| 436 |
+
print("── Investigation ──")
|
| 437 |
+
|
| 438 |
+
result = investigate(
|
| 439 |
+
agent=agent,
|
| 440 |
+
workspace=workspace,
|
| 441 |
+
incident_summary=incident_summary,
|
| 442 |
+
max_steps=args.max_steps,
|
| 443 |
+
verbose=verbose,
|
| 444 |
+
)
|
| 445 |
+
|
| 446 |
+
# ── 5. Output ────────────────────────────────────────────────────
|
| 447 |
+
print("\n" + "═" * 60)
|
| 448 |
+
print("ROOT CAUSE:")
|
| 449 |
+
print(result["root_cause"] or "(not declared)")
|
| 450 |
+
print("\nPROPOSED PATCH:")
|
| 451 |
+
print(result["proposed_patch"] or "(none)")
|
| 452 |
+
print(f"\n({result['steps_taken']} steps taken)")
|
| 453 |
+
|
| 454 |
+
if args.output:
|
| 455 |
+
with open(args.output, "w") as f:
|
| 456 |
+
json.dump(result, f, indent=2, default=str)
|
| 457 |
+
print(f"\nFull result written to {args.output}")
|
| 458 |
+
|
| 459 |
+
# Cleanup temp clone
|
| 460 |
+
if _tmpdir:
|
| 461 |
+
import shutil
|
| 462 |
+
shutil.rmtree(_tmpdir, ignore_errors=True)
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
if __name__ == "__main__":
|
| 466 |
+
main()
|
merge.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.10"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "transformers",
|
| 5 |
+
# "peft",
|
| 6 |
+
# "accelerate",
|
| 7 |
+
# "safetensors",
|
| 8 |
+
# "huggingface_hub"
|
| 9 |
+
# ]
|
| 10 |
+
# ///
|
| 11 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 12 |
+
from peft import PeftModel
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
base_model = "Qwen/Qwen2.5-7B-Instruct"
|
| 16 |
+
lora_model = "daemongg/qwen2.5-7b-sre-grpo"
|
| 17 |
+
output_repo = "Yaswanth-Bolla/qwen-merged"
|
| 18 |
+
|
| 19 |
+
tokenizer = AutoTokenizer.from_pretrained(base_model)
|
| 20 |
+
|
| 21 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 22 |
+
base_model,
|
| 23 |
+
torch_dtype=torch.float16,
|
| 24 |
+
device_map="auto"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
model = PeftModel.from_pretrained(model, lora_model)
|
| 28 |
+
|
| 29 |
+
model = model.merge_and_unload()
|
| 30 |
+
|
| 31 |
+
model.push_to_hub(output_repo)
|
| 32 |
+
tokenizer.push_to_hub(output_repo)
|
sft.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2020-2026 The HuggingFace Team. All rights reserved.
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
# /// script
|
| 16 |
+
# dependencies = [
|
| 17 |
+
# "trl",
|
| 18 |
+
# "peft",
|
| 19 |
+
# "trackio",
|
| 20 |
+
# "kernels",
|
| 21 |
+
# ]
|
| 22 |
+
# ///
|
| 23 |
+
|
| 24 |
+
"""
|
| 25 |
+
# Full training
|
| 26 |
+
```
|
| 27 |
+
python trl/scripts/sft.py \
|
| 28 |
+
--model_name_or_path Qwen/Qwen2-0.5B \
|
| 29 |
+
--dataset_name trl-lib/Capybara \
|
| 30 |
+
--learning_rate 2.0e-5 \
|
| 31 |
+
--num_train_epochs 1 \
|
| 32 |
+
--packing \
|
| 33 |
+
--per_device_train_batch_size 2 \
|
| 34 |
+
--gradient_accumulation_steps 8 \
|
| 35 |
+
--eos_token '<|im_end|>' \
|
| 36 |
+
--eval_strategy steps \
|
| 37 |
+
--eval_steps 100 \
|
| 38 |
+
--output_dir Qwen2-0.5B-SFT \
|
| 39 |
+
--push_to_hub
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
# LoRA
|
| 43 |
+
```
|
| 44 |
+
python trl/scripts/sft.py \
|
| 45 |
+
--model_name_or_path Qwen/Qwen2-0.5B \
|
| 46 |
+
--dataset_name trl-lib/Capybara \
|
| 47 |
+
--learning_rate 2.0e-4 \
|
| 48 |
+
--num_train_epochs 1 \
|
| 49 |
+
--packing \
|
| 50 |
+
--per_device_train_batch_size 2 \
|
| 51 |
+
--gradient_accumulation_steps 8 \
|
| 52 |
+
--eos_token '<|im_end|>' \
|
| 53 |
+
--eval_strategy steps \
|
| 54 |
+
--eval_steps 100 \
|
| 55 |
+
--use_peft \
|
| 56 |
+
--lora_r 32 \
|
| 57 |
+
--lora_alpha 16 \
|
| 58 |
+
--output_dir Qwen2-0.5B-SFT \
|
| 59 |
+
--push_to_hub
|
| 60 |
+
```
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
import argparse
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def main(script_args, training_args, model_args, dataset_args):
|
| 67 |
+
from accelerate import logging
|
| 68 |
+
from datasets import load_dataset
|
| 69 |
+
from transformers import AutoConfig, AutoModelForCausalLM
|
| 70 |
+
from transformers.models.auto.modeling_auto import MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES
|
| 71 |
+
|
| 72 |
+
from trl import SFTTrainer, get_dataset, get_kbit_device_map, get_peft_config, get_quantization_config
|
| 73 |
+
|
| 74 |
+
logger = logging.get_logger(__name__)
|
| 75 |
+
|
| 76 |
+
################
|
| 77 |
+
# Model init kwargs
|
| 78 |
+
################
|
| 79 |
+
model_kwargs = dict(
|
| 80 |
+
revision=model_args.model_revision,
|
| 81 |
+
trust_remote_code=model_args.trust_remote_code,
|
| 82 |
+
attn_implementation=model_args.attn_implementation,
|
| 83 |
+
dtype=model_args.dtype,
|
| 84 |
+
)
|
| 85 |
+
quantization_config = get_quantization_config(model_args)
|
| 86 |
+
if quantization_config is not None:
|
| 87 |
+
# Passing None would not be treated the same as omitting the argument, so we include it only when valid.
|
| 88 |
+
model_kwargs["device_map"] = get_kbit_device_map()
|
| 89 |
+
model_kwargs["quantization_config"] = quantization_config
|
| 90 |
+
|
| 91 |
+
# Create model
|
| 92 |
+
config = AutoConfig.from_pretrained(model_args.model_name_or_path)
|
| 93 |
+
valid_image_text_architectures = MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES.values()
|
| 94 |
+
|
| 95 |
+
if config.architectures and any(arch in valid_image_text_architectures for arch in config.architectures):
|
| 96 |
+
from transformers import AutoModelForImageTextToText
|
| 97 |
+
|
| 98 |
+
model = AutoModelForImageTextToText.from_pretrained(model_args.model_name_or_path, **model_kwargs)
|
| 99 |
+
else:
|
| 100 |
+
model = AutoModelForCausalLM.from_pretrained(model_args.model_name_or_path, **model_kwargs)
|
| 101 |
+
|
| 102 |
+
# Load the dataset
|
| 103 |
+
if dataset_args.datasets and script_args.dataset_name:
|
| 104 |
+
logger.warning(
|
| 105 |
+
"Both `datasets` and `dataset_name` are provided. The `datasets` argument will be used to load the "
|
| 106 |
+
"dataset and `dataset_name` will be ignored."
|
| 107 |
+
)
|
| 108 |
+
dataset = get_dataset(dataset_args)
|
| 109 |
+
elif dataset_args.datasets and not script_args.dataset_name:
|
| 110 |
+
dataset = get_dataset(dataset_args)
|
| 111 |
+
elif not dataset_args.datasets and script_args.dataset_name:
|
| 112 |
+
dataset = load_dataset(
|
| 113 |
+
script_args.dataset_name, name=script_args.dataset_config, streaming=script_args.dataset_streaming
|
| 114 |
+
)
|
| 115 |
+
else:
|
| 116 |
+
raise ValueError("Either `datasets` or `dataset_name` must be provided.")
|
| 117 |
+
|
| 118 |
+
# Initialize the SFT trainer
|
| 119 |
+
trainer = SFTTrainer(
|
| 120 |
+
model=model,
|
| 121 |
+
args=training_args,
|
| 122 |
+
train_dataset=dataset[script_args.dataset_train_split],
|
| 123 |
+
eval_dataset=dataset[script_args.dataset_test_split] if training_args.eval_strategy != "no" else None,
|
| 124 |
+
peft_config=get_peft_config(model_args),
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
# Train the model
|
| 128 |
+
trainer.train()
|
| 129 |
+
|
| 130 |
+
# Log training complete
|
| 131 |
+
trainer.accelerator.print("✅ Training completed.")
|
| 132 |
+
|
| 133 |
+
# Save and push to Hub
|
| 134 |
+
trainer.save_model(training_args.output_dir)
|
| 135 |
+
trainer.accelerator.print(f"💾 Model saved to {training_args.output_dir}.")
|
| 136 |
+
|
| 137 |
+
if training_args.push_to_hub:
|
| 138 |
+
trainer.push_to_hub(dataset_name=script_args.dataset_name)
|
| 139 |
+
trainer.accelerator.print(f"🤗 Model pushed to the Hub in https://huggingface.co/{trainer.hub_model_id}.")
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def make_parser(subparsers: argparse._SubParsersAction | None = None, prog: str | None = None):
|
| 143 |
+
from trl import DatasetMixtureConfig, ModelConfig, ScriptArguments, SFTConfig, TrlParser
|
| 144 |
+
|
| 145 |
+
dataclass_types = (ScriptArguments, SFTConfig, ModelConfig, DatasetMixtureConfig)
|
| 146 |
+
if subparsers is not None:
|
| 147 |
+
parser = subparsers.add_parser("sft", help="Run the SFT training script", dataclass_types=dataclass_types)
|
| 148 |
+
else:
|
| 149 |
+
parser = TrlParser(dataclass_types, prog=prog)
|
| 150 |
+
return parser
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
if __name__ == "__main__":
|
| 154 |
+
parser = make_parser()
|
| 155 |
+
script_args, training_args, model_args, dataset_args = parser.parse_args_and_config(fail_with_unknown_args=False)
|
| 156 |
+
main(script_args, training_args, model_args, dataset_args)
|