Spaces:
Sleeping
Sleeping
add 6 tasks, fix log format, multi-turn retry, grader improvements
Browse files- README.md +223 -71
- __pycache__/models.cpython-310.pyc +0 -0
- inference.py +122 -20
- models.py +12 -7
- server/__pycache__/app.cpython-310.pyc +0 -0
- server/__pycache__/bug_generator.cpython-310.pyc +0 -0
- server/__pycache__/grader.cpython-310.pyc +0 -0
- server/__pycache__/ml_debug_env_environment.cpython-310.pyc +0 -0
- server/app.py +61 -36
- server/bug_generator.py +279 -26
- server/grader.py +91 -55
- server/ml_debug_env_environment.py +27 -63
- test2.py +64 -118
README.md
CHANGED
|
@@ -18,53 +18,120 @@ tags:
|
|
| 18 |
|
| 19 |
> **Meta Γ PyTorch Γ Scaler OpenEnv Hackathon β April 2026**
|
| 20 |
|
| 21 |
-
An [OpenEnv](https://github.com/meta-pytorch/OpenEnv) reinforcement-learning environment where AI agents debug broken PyTorch training scripts. The agent receives a buggy Python script plus its failure output, then must return a **corrected script**. The grader executes the fix in an isolated subprocess and scores it **0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
---
|
| 24 |
|
| 25 |
## π― Tasks
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|---|---|---|---|
|
| 29 |
-
| `shape_mismatch` | π’ Easy | `nn.Linear` input dim is wrong β explicit `RuntimeError` crash |
|
| 30 |
-
| `training_collapse` | π‘ Medium | Huge LR β NaN loss **or** wrong loss fn β flat plateau
|
| 31 |
-
| `data_leakage` | π΄ Hard | Dataset normalized before train/test split β inflated metrics, no error |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
---
|
| 34 |
|
| 35 |
## π Scoring Ladder
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
| 40 |
-
|
|
| 41 |
-
| **0.
|
| 42 |
-
| **0.
|
| 43 |
-
| **0.
|
| 44 |
-
| **
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
---
|
| 47 |
|
| 48 |
## π Action Space
|
| 49 |
|
| 50 |
-
The agent returns a single JSON
|
| 51 |
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|---|---|---|
|
| 54 |
-
| `bug_type` | `str` |
|
| 55 |
| `diagnosis` | `str` | Plain-language explanation of the root cause |
|
| 56 |
-
| `fixed_code` | `str` | Complete corrected Python script,
|
|
|
|
|
|
|
| 57 |
|
| 58 |
## π Observation Space
|
| 59 |
|
|
|
|
|
|
|
| 60 |
| Field | Type | Description |
|
| 61 |
|---|---|---|
|
| 62 |
-
| `task_id` | `str` |
|
| 63 |
| `task_description` | `str` | Natural language description of what to fix |
|
| 64 |
| `buggy_code` | `str` | The broken Python script |
|
| 65 |
-
| `error_output` | `str` | stderr / traceback or
|
| 66 |
| `execution_result` | `str \| None` | stdout+stderr from running the agent's fix (`None` on reset) |
|
| 67 |
-
| `grader_score` | `float \| None` | Score 0.
|
| 68 |
| `grader_feedback` | `str \| None` | Human-readable explanation of the score |
|
| 69 |
| `step_number` | `int` | Current step within this episode |
|
| 70 |
| `done` | `bool` | Whether the episode has ended |
|
|
@@ -74,32 +141,35 @@ The agent returns a single JSON action:
|
|
| 74 |
|
| 75 |
## π Quick Start
|
| 76 |
|
|
|
|
|
|
|
| 77 |
```bash
|
| 78 |
-
pip install git+https://huggingface.co/spaces/
|
| 79 |
```
|
| 80 |
|
|
|
|
|
|
|
| 81 |
```python
|
| 82 |
import asyncio
|
| 83 |
from ml_debug_env import MlDebugEnvClient, DebugAction
|
| 84 |
|
| 85 |
-
SPACE_URL = "https://
|
| 86 |
|
| 87 |
async def main():
|
| 88 |
async with MlDebugEnvClient(base_url=SPACE_URL) as env:
|
| 89 |
-
# Reset β get the buggy script
|
| 90 |
result = await env.reset()
|
| 91 |
obs = result.observation
|
|
|
|
| 92 |
print(f"Task: {obs.task_id}")
|
|
|
|
| 93 |
print(f"Error:\n{obs.error_output}\n")
|
| 94 |
|
| 95 |
-
# Step β submit a fix
|
| 96 |
action = DebugAction(
|
| 97 |
bug_type="shape_mismatch",
|
| 98 |
diagnosis="nn.Linear classifier input dim is 32 but encoder outputs 256",
|
| 99 |
-
fixed_code=obs.buggy_code.replace(
|
| 100 |
-
"nn.Linear(32,", "nn.Linear(256,"
|
| 101 |
-
),
|
| 102 |
)
|
|
|
|
| 103 |
result = await env.step(action)
|
| 104 |
print(f"Score: {result.observation.grader_score}")
|
| 105 |
print(f"Feedback: {result.observation.grader_feedback}")
|
|
@@ -107,18 +177,20 @@ async def main():
|
|
| 107 |
asyncio.run(main())
|
| 108 |
```
|
| 109 |
|
| 110 |
-
|
| 111 |
|
| 112 |
```python
|
| 113 |
from ml_debug_env import MlDebugEnvClient, DebugAction
|
| 114 |
|
|
|
|
|
|
|
| 115 |
with MlDebugEnvClient(base_url=SPACE_URL).sync() as env:
|
| 116 |
result = env.reset()
|
| 117 |
obs = result.observation
|
| 118 |
|
| 119 |
result = env.step(DebugAction(
|
| 120 |
bug_type="training_collapse",
|
| 121 |
-
diagnosis="Learning rate 100.0 causes gradient explosion
|
| 122 |
fixed_code=obs.buggy_code.replace("lr=100.0", "lr=1e-3"),
|
| 123 |
))
|
| 124 |
print(result.observation.grader_score)
|
|
@@ -128,16 +200,18 @@ with MlDebugEnvClient(base_url=SPACE_URL).sync() as env:
|
|
| 128 |
|
| 129 |
## π API Endpoints
|
| 130 |
|
|
|
|
|
|
|
| 131 |
| Method | Path | Description |
|
| 132 |
|---|---|---|
|
| 133 |
-
| `POST` | `/reset` | Start a new debug episode |
|
| 134 |
-
| `POST` | `/step` | Submit a fix attempt |
|
| 135 |
| `GET` | `/state` | Get current episode state |
|
| 136 |
-
| `GET` | `/health` | Health check
|
| 137 |
-
| `GET` | `/tasks` | List all 3 tasks
|
| 138 |
-
| `POST` | `/grader` | Score a fix without running a full episode |
|
| 139 |
-
| `GET` | `/baseline` | Run
|
| 140 |
-
| `WS` | `/ws` | Persistent WebSocket session
|
| 141 |
| `GET` | `/docs` | Interactive Swagger UI |
|
| 142 |
| `GET` | `/schema` | Environment JSON schema |
|
| 143 |
|
|
@@ -145,40 +219,49 @@ with MlDebugEnvClient(base_url=SPACE_URL).sync() as env:
|
|
| 145 |
|
| 146 |
## π Baseline Results
|
| 147 |
|
| 148 |
-
|
| 149 |
|
| 150 |
-
|
|
| 151 |
|---|---|
|
| 152 |
-
| `shape_mismatch` |
|
| 153 |
-
| `training_collapse` |
|
| 154 |
-
| `data_leakage` |
|
| 155 |
-
| **Average** | **
|
| 156 |
|
| 157 |
-
|
| 158 |
|
| 159 |
---
|
| 160 |
|
| 161 |
-
##
|
| 162 |
-
|
| 163 |
-
```bash
|
| 164 |
-
git clone https://huggingface.co/spaces/rehaan/ml-debug-env
|
| 165 |
-
cd ml-debug-env
|
| 166 |
-
|
| 167 |
-
# Install deps
|
| 168 |
-
pip install -e .
|
| 169 |
-
|
| 170 |
-
# Run server
|
| 171 |
-
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 172 |
|
| 173 |
-
# Quick sanity test
|
| 174 |
-
python test.py
|
| 175 |
```
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
```
|
| 183 |
|
| 184 |
---
|
|
@@ -187,32 +270,101 @@ docker run -e GROQ_API_KEY=your_key -p 8000:8000 ml-debug-env:latest
|
|
| 187 |
|
| 188 |
```
|
| 189 |
ml_debug_env/
|
|
|
|
| 190 |
βββ __init__.py # Package exports
|
| 191 |
-
βββ models.py # DebugAction, DebugObservation, DebugState
|
| 192 |
βββ client.py # MlDebugEnvClient (EnvClient subclass)
|
|
|
|
| 193 |
βββ openenv.yaml # OpenEnv manifest
|
| 194 |
βββ pyproject.toml # Project metadata + deps
|
|
|
|
| 195 |
βββ test.py # Sanity tests (no server needed)
|
|
|
|
| 196 |
βββ server/
|
| 197 |
-
βββ app.py # FastAPI app
|
| 198 |
-
βββ bug_generator.py #
|
| 199 |
-
βββ grader.py # Executes fixed code, returns 0.
|
| 200 |
-
βββ ml_debug_env_environment.py #
|
| 201 |
-
βββ baseline_inference.py #
|
| 202 |
-
βββ Dockerfile #
|
| 203 |
βββ requirements.txt # Server runtime deps
|
| 204 |
```
|
| 205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
---
|
| 207 |
|
| 208 |
## π Environment Variables
|
| 209 |
|
| 210 |
| Variable | Required | Description |
|
| 211 |
|---|---|---|
|
| 212 |
-
| `
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
---
|
| 215 |
|
| 216 |
## π License
|
| 217 |
|
| 218 |
-
BSD 3-Clause β see [OpenEnv](https://github.com/meta-pytorch/OpenEnv/blob/main/LICENSE)
|
|
|
|
| 18 |
|
| 19 |
> **Meta Γ PyTorch Γ Scaler OpenEnv Hackathon β April 2026**
|
| 20 |
|
| 21 |
+
An [OpenEnv](https://github.com/meta-pytorch/OpenEnv) reinforcement-learning environment where AI agents debug broken PyTorch training scripts. The agent receives a buggy Python script plus its failure output, then must return a **corrected script**. The grader executes the fix in an isolated subprocess and scores it **0.01 β 0.99** with partial credit at every verification stage.
|
| 22 |
+
|
| 23 |
+
---
|
| 24 |
+
|
| 25 |
+
## π§ What Is This?
|
| 26 |
+
|
| 27 |
+
Most ML benchmarks test whether a model can *write* code. This environment tests something harder: can a model *debug* code?
|
| 28 |
+
|
| 29 |
+
The agent is dropped into a broken PyTorch training environment. It sees the buggy script, the error or behavioral failure, and must:
|
| 30 |
+
1. Identify what category of bug is present
|
| 31 |
+
2. Explain the root cause in plain English
|
| 32 |
+
3. Return a complete, corrected, runnable Python script
|
| 33 |
+
|
| 34 |
+
The grader then **actually executes** the fixed code in a subprocess and scores it across 6 stages of increasing correctness. There is no cheating β the code has to run.
|
| 35 |
+
|
| 36 |
+
This is a realistic simulation of the debugging loop every ML engineer goes through daily.
|
| 37 |
|
| 38 |
---
|
| 39 |
|
| 40 |
## π― Tasks
|
| 41 |
|
| 42 |
+
Three tasks of increasing difficulty, each representing a common class of real-world ML bugs:
|
| 43 |
+
|
| 44 |
+
| task_id | Difficulty | What's Broken | Why It's Hard |
|
| 45 |
|---|---|---|---|
|
| 46 |
+
| `shape_mismatch` | π’ Easy | `nn.Linear` input dim is wrong β explicit `RuntimeError` crash | Error is in the traceback. Read it. |
|
| 47 |
+
| `training_collapse` | π‘ Medium | Huge LR β NaN loss, **or** wrong loss fn β flat plateau. No crash. | No error β agent must reason about training dynamics |
|
| 48 |
+
| `data_leakage` | π΄ Hard | Dataset normalized before train/test split β inflated metrics, no error | Everything *looks* fine. Agent must understand data pipelines |
|
| 49 |
+
|
| 50 |
+
### Task 1 β Shape Mismatch (Easy)
|
| 51 |
+
|
| 52 |
+
A `SimpleClassifier` has a 2-layer encoder that outputs `hidden_size` features (e.g. 256), but the classifier head is initialized with `nn.Linear(wrong_size, num_classes)` where `wrong_size` (e.g. 32) doesn't match. The forward pass crashes immediately with:
|
| 53 |
+
|
| 54 |
+
```
|
| 55 |
+
RuntimeError: mat1 and mat2 shapes cannot be multiplied (256 cannot be broadcast to 32)
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
The fix: change `nn.Linear(wrong_size, ...)` to `nn.Linear(hidden_size, ...)`.
|
| 59 |
+
|
| 60 |
+
The `hidden_size` and `wrong_size` values are randomized per episode seed so agents can't hardcode the fix.
|
| 61 |
+
|
| 62 |
+
### Task 2 β Training Collapse (Medium)
|
| 63 |
+
|
| 64 |
+
Two variants, randomly selected:
|
| 65 |
+
|
| 66 |
+
**Variant A β Bad learning rate:** SGD with `lr=100.0` (or 10.0 or 50.0). Loss explodes to NaN by epoch 2. Code runs, no crash, just broken training. Fix: reduce lr to ~1e-3.
|
| 67 |
+
|
| 68 |
+
**Variant B β Wrong loss function:** Binary classification model with sigmoid output trained with `MSELoss` instead of `BCELoss`. Loss plateaus immediately around 0.25 and never decreases. Fix: switch to `BCELoss` or remove sigmoid and use `BCEWithLogitsLoss`.
|
| 69 |
+
|
| 70 |
+
The agent must distinguish between these variants from the error description alone.
|
| 71 |
+
|
| 72 |
+
### Task 3 β Data Leakage (Hard)
|
| 73 |
+
|
| 74 |
+
Two variants, randomly selected:
|
| 75 |
+
|
| 76 |
+
**Variant A:** Normalization computed on full dataset (`X_raw.mean(dim=0)`) before the train/test split. Test set statistics contaminate training. Reported accuracy looks great (~96%) but is invalid.
|
| 77 |
+
|
| 78 |
+
**Variant B:** Train/test split done first, but `full_mean = X_raw.mean(dim=0)` still computed on the whole dataset and used to normalize both sets. Same bug, different code structure.
|
| 79 |
+
|
| 80 |
+
Fix: compute `mean` and `std` only from `X_train`, then apply those stats to normalize both `X_train` and `X_test`.
|
| 81 |
+
|
| 82 |
+
This task has no error, no NaN, no crash. The agent must reason about data flow.
|
| 83 |
|
| 84 |
---
|
| 85 |
|
| 86 |
## π Scoring Ladder
|
| 87 |
|
| 88 |
+
The grader runs the agent's fixed code in a real subprocess and scores it across 6 stages:
|
| 89 |
+
|
| 90 |
+
| Score | Stage | Condition |
|
| 91 |
+
|---|---|---|
|
| 92 |
+
| **0.01** | Bug type wrong | Agent identified the wrong bug category |
|
| 93 |
+
| **0.2** | Code crashes | Right bug type, but fixed code throws an exception |
|
| 94 |
+
| **0.4** | Incomplete training | Code runs but training loop doesn't finish |
|
| 95 |
+
| **0.6** | Root cause not fixed | Training completes but the underlying bug is still present |
|
| 96 |
+
| **0.8** | Success signal missing | Fix is valid but expected output pattern not found |
|
| 97 |
+
| **0.99** | Perfect fix | Code runs, training finishes, success signal confirmed β
|
|
| 98 |
+
|
| 99 |
+
Scores are intentionally kept off 0.0 and 1.0 to satisfy the validator's strict `(0, 1)` exclusive range requirement.
|
| 100 |
|
| 101 |
---
|
| 102 |
|
| 103 |
## π Action Space
|
| 104 |
|
| 105 |
+
The agent returns a single JSON object:
|
| 106 |
|
| 107 |
+
```json
|
| 108 |
+
{
|
| 109 |
+
"bug_type": "shape_mismatch",
|
| 110 |
+
"diagnosis": "The classifier head uses nn.Linear(32, 10) but the encoder outputs 256 features. The input dimension must match the encoder output.",
|
| 111 |
+
"fixed_code": "import torch\nimport torch.nn as nn\n..."
|
| 112 |
+
}
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
| Field | Type | Values |
|
| 116 |
|---|---|---|
|
| 117 |
+
| `bug_type` | `str` | `shape_mismatch`, `training_collapse`, `data_leakage`, `other` |
|
| 118 |
| `diagnosis` | `str` | Plain-language explanation of the root cause |
|
| 119 |
+
| `fixed_code` | `str` | Complete corrected Python script, all imports included, runnable as-is |
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
|
| 123 |
## π Observation Space
|
| 124 |
|
| 125 |
+
What the agent receives on `reset()` and after each `step()`:
|
| 126 |
+
|
| 127 |
| Field | Type | Description |
|
| 128 |
|---|---|---|
|
| 129 |
+
| `task_id` | `str` | Which task is active |
|
| 130 |
| `task_description` | `str` | Natural language description of what to fix |
|
| 131 |
| `buggy_code` | `str` | The broken Python script |
|
| 132 |
+
| `error_output` | `str` | stderr / traceback or behavioral failure description |
|
| 133 |
| `execution_result` | `str \| None` | stdout+stderr from running the agent's fix (`None` on reset) |
|
| 134 |
+
| `grader_score` | `float \| None` | Score 0.01β0.99 (`None` on reset) |
|
| 135 |
| `grader_feedback` | `str \| None` | Human-readable explanation of the score |
|
| 136 |
| `step_number` | `int` | Current step within this episode |
|
| 137 |
| `done` | `bool` | Whether the episode has ended |
|
|
|
|
| 141 |
|
| 142 |
## π Quick Start
|
| 143 |
|
| 144 |
+
### Install the client
|
| 145 |
+
|
| 146 |
```bash
|
| 147 |
+
pip install git+https://huggingface.co/spaces/rak2315/ml-debug-env
|
| 148 |
```
|
| 149 |
|
| 150 |
+
### Async usage
|
| 151 |
+
|
| 152 |
```python
|
| 153 |
import asyncio
|
| 154 |
from ml_debug_env import MlDebugEnvClient, DebugAction
|
| 155 |
|
| 156 |
+
SPACE_URL = "https://rak2315-ml-debug-env.hf.space"
|
| 157 |
|
| 158 |
async def main():
|
| 159 |
async with MlDebugEnvClient(base_url=SPACE_URL) as env:
|
|
|
|
| 160 |
result = await env.reset()
|
| 161 |
obs = result.observation
|
| 162 |
+
|
| 163 |
print(f"Task: {obs.task_id}")
|
| 164 |
+
print(f"Description: {obs.task_description}")
|
| 165 |
print(f"Error:\n{obs.error_output}\n")
|
| 166 |
|
|
|
|
| 167 |
action = DebugAction(
|
| 168 |
bug_type="shape_mismatch",
|
| 169 |
diagnosis="nn.Linear classifier input dim is 32 but encoder outputs 256",
|
| 170 |
+
fixed_code=obs.buggy_code.replace("nn.Linear(32,", "nn.Linear(256,"),
|
|
|
|
|
|
|
| 171 |
)
|
| 172 |
+
|
| 173 |
result = await env.step(action)
|
| 174 |
print(f"Score: {result.observation.grader_score}")
|
| 175 |
print(f"Feedback: {result.observation.grader_feedback}")
|
|
|
|
| 177 |
asyncio.run(main())
|
| 178 |
```
|
| 179 |
|
| 180 |
+
### Sync usage
|
| 181 |
|
| 182 |
```python
|
| 183 |
from ml_debug_env import MlDebugEnvClient, DebugAction
|
| 184 |
|
| 185 |
+
SPACE_URL = "https://rak2315-ml-debug-env.hf.space"
|
| 186 |
+
|
| 187 |
with MlDebugEnvClient(base_url=SPACE_URL).sync() as env:
|
| 188 |
result = env.reset()
|
| 189 |
obs = result.observation
|
| 190 |
|
| 191 |
result = env.step(DebugAction(
|
| 192 |
bug_type="training_collapse",
|
| 193 |
+
diagnosis="Learning rate 100.0 causes gradient explosion leading to NaN loss",
|
| 194 |
fixed_code=obs.buggy_code.replace("lr=100.0", "lr=1e-3"),
|
| 195 |
))
|
| 196 |
print(result.observation.grader_score)
|
|
|
|
| 200 |
|
| 201 |
## π API Endpoints
|
| 202 |
|
| 203 |
+
The server exposes both the standard OpenEnv spec endpoints and additional utility endpoints:
|
| 204 |
+
|
| 205 |
| Method | Path | Description |
|
| 206 |
|---|---|---|
|
| 207 |
+
| `POST` | `/reset` | Start a new debug episode, returns buggy code + error |
|
| 208 |
+
| `POST` | `/step` | Submit a fix attempt, returns score + feedback |
|
| 209 |
| `GET` | `/state` | Get current episode state |
|
| 210 |
+
| `GET` | `/health` | Health check β `{"status": "healthy"}` |
|
| 211 |
+
| `GET` | `/tasks` | List all 3 tasks with descriptions and action schema |
|
| 212 |
+
| `POST` | `/grader` | Score a fix directly without running a full episode |
|
| 213 |
+
| `GET` | `/baseline` | Run the built-in LLM baseline agent on all 3 tasks |
|
| 214 |
+
| `WS` | `/ws` | Persistent WebSocket session for low-latency interaction |
|
| 215 |
| `GET` | `/docs` | Interactive Swagger UI |
|
| 216 |
| `GET` | `/schema` | Environment JSON schema |
|
| 217 |
|
|
|
|
| 219 |
|
| 220 |
## π Baseline Results
|
| 221 |
|
| 222 |
+
Built-in baseline agent: `llama-3.3-70b-versatile` β zero-shot, single attempt, no examples.
|
| 223 |
|
| 224 |
+
| task_id | Score |
|
| 225 |
|---|---|
|
| 226 |
+
| `shape_mismatch` | **0.99** |
|
| 227 |
+
| `training_collapse` | **0.99** |
|
| 228 |
+
| `data_leakage` | **0.99** |
|
| 229 |
+
| **Average** | **0.99** |
|
| 230 |
|
| 231 |
+
The baseline uses a structured JSON system prompt and the OpenAI-compatible Groq API. It correctly identifies the bug type and returns a complete fixed script for all 3 tasks in a single zero-shot pass.
|
| 232 |
|
| 233 |
---
|
| 234 |
|
| 235 |
+
## π Architecture
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
|
|
|
|
|
|
| 237 |
```
|
| 238 |
+
Agent / Validator
|
| 239 |
+
β
|
| 240 |
+
βΌ
|
| 241 |
+
FastAPI Server (server/app.py)
|
| 242 |
+
β
|
| 243 |
+
βββ POST /reset βββΊ MlDebugEnvEnvironment.reset()
|
| 244 |
+
β β
|
| 245 |
+
β βΌ
|
| 246 |
+
β BugGenerator β BugScenario
|
| 247 |
+
β (buggy code + error output)
|
| 248 |
+
β
|
| 249 |
+
βββ POST /step βββΊ MlDebugEnvEnvironment.step()
|
| 250 |
+
β β
|
| 251 |
+
β βΌ
|
| 252 |
+
β Grader.grade()
|
| 253 |
+
β β
|
| 254 |
+
β subprocess.run(fixed_code)
|
| 255 |
+
β β
|
| 256 |
+
β Score 0.01 β 0.99
|
| 257 |
+
β
|
| 258 |
+
βββ GET /baseline ββΊ BaselineInference
|
| 259 |
+
β
|
| 260 |
+
OpenAI Client
|
| 261 |
+
β
|
| 262 |
+
LLM Proxy (API_BASE_URL)
|
| 263 |
+
β
|
| 264 |
+
llama-3.3-70b-versatile
|
| 265 |
```
|
| 266 |
|
| 267 |
---
|
|
|
|
| 270 |
|
| 271 |
```
|
| 272 |
ml_debug_env/
|
| 273 |
+
βββ inference.py # Validator entry point β calls LLM proxy directly
|
| 274 |
βββ __init__.py # Package exports
|
| 275 |
+
βββ models.py # Pydantic models: DebugAction, DebugObservation, DebugState
|
| 276 |
βββ client.py # MlDebugEnvClient (EnvClient subclass)
|
| 277 |
+
βββ Dockerfile # Root Dockerfile for HF Space deployment
|
| 278 |
βββ openenv.yaml # OpenEnv manifest
|
| 279 |
βββ pyproject.toml # Project metadata + deps
|
| 280 |
+
βββ baseline_results.json # Latest baseline run results
|
| 281 |
βββ test.py # Sanity tests (no server needed)
|
| 282 |
+
βββ test2.py # Full 20-check submission validator
|
| 283 |
βββ server/
|
| 284 |
+
βββ app.py # FastAPI app β all endpoints
|
| 285 |
+
βββ bug_generator.py # Procedural buggy script generation
|
| 286 |
+
βββ grader.py # Executes fixed code, returns 0.01β0.99 score
|
| 287 |
+
βββ ml_debug_env_environment.py # OpenEnv Environment class (reset/step/state)
|
| 288 |
+
βββ baseline_inference.py # LLM baseline agent
|
| 289 |
+
βββ Dockerfile # Alternative Dockerfile (used for local dev)
|
| 290 |
βββ requirements.txt # Server runtime deps
|
| 291 |
```
|
| 292 |
|
| 293 |
+
### File Responsibilities
|
| 294 |
+
|
| 295 |
+
**`server/bug_generator.py`**
|
| 296 |
+
Procedurally generates buggy PyTorch scripts using a random seed. Each task has randomized parameters (hidden sizes, learning rates, variants) so the same task_id produces slightly different code each episode. Provides `get_scenario(task_id, seed)` β `BugScenario`.
|
| 297 |
+
|
| 298 |
+
**`server/grader.py`**
|
| 299 |
+
The referee. Writes the agent's fixed code to a temp file, executes it via `subprocess.run` using the correct Python interpreter (found via `PYTHON_EXEC` env var or venv detection), and applies 5 verification stages. Each stage checks progressively deeper correctness β from "does it run" to "does the output contain the right success signal".
|
| 300 |
+
|
| 301 |
+
**`server/ml_debug_env_environment.py`**
|
| 302 |
+
Implements the OpenEnv `Environment` interface. Manages episode state, rotates through tasks when none is pinned, handles `reset()` / `step()` / `state` with proper session isolation. Supports concurrent sessions.
|
| 303 |
+
|
| 304 |
+
**`server/app.py`**
|
| 305 |
+
FastAPI application. Wraps the environment with the OpenEnv `create_app()` factory, then adds `/tasks`, `/grader`, and `/baseline` endpoints on top. The `/baseline` endpoint runs the LLM agent on all 3 tasks and returns scores.
|
| 306 |
+
|
| 307 |
+
**`server/baseline_inference.py`**
|
| 308 |
+
The built-in LLM agent. Uses the OpenAI client pointed at `API_BASE_URL` (Groq by default). Sends buggy code + error to `llama-3.3-70b-versatile` with a structured JSON system prompt. Parses the response and passes the fix to the grader.
|
| 309 |
+
|
| 310 |
+
**`inference.py`** (root)
|
| 311 |
+
What the hackathon validator actually executes. Reads `API_BASE_URL`, `MODEL_NAME`, `API_KEY` from environment, calls the LLM proxy directly (not through the server), grades results locally, and emits structured stdout logs in the required format:
|
| 312 |
+
```
|
| 313 |
+
[START] task=shape_mismatch
|
| 314 |
+
[STEP] step=1 reward=0.9900
|
| 315 |
+
[END] task=shape_mismatch score=0.9900 steps=1
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
**`models.py`**
|
| 319 |
+
Pydantic v2 models for the action/observation/state types used across the whole system.
|
| 320 |
+
|
| 321 |
+
**`client.py`**
|
| 322 |
+
Python client library so external agents can interact with the deployed HF Space using the `MlDebugEnvClient` class instead of raw HTTP.
|
| 323 |
+
|
| 324 |
+
---
|
| 325 |
+
|
| 326 |
+
## π Local Development
|
| 327 |
+
|
| 328 |
+
```bash
|
| 329 |
+
git clone https://huggingface.co/spaces/rak2315/ml-debug-env
|
| 330 |
+
cd ml-debug-env
|
| 331 |
+
|
| 332 |
+
# Install deps
|
| 333 |
+
pip install -e .
|
| 334 |
+
|
| 335 |
+
# Set env vars
|
| 336 |
+
set GROQ_API_KEY=your_groq_key
|
| 337 |
+
set PYTHON_EXEC=path/to/python/with/torch # Windows: needed if venv lacks torch
|
| 338 |
+
|
| 339 |
+
# Run server
|
| 340 |
+
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 341 |
+
|
| 342 |
+
# Run full validation (20 checks)
|
| 343 |
+
python test2.py
|
| 344 |
+
```
|
| 345 |
+
|
| 346 |
+
**With Docker:**
|
| 347 |
+
|
| 348 |
+
```bash
|
| 349 |
+
docker build -t ml-debug-env:latest .
|
| 350 |
+
docker run -e GROQ_API_KEY=your_key -p 8000:8000 ml-debug-env:latest
|
| 351 |
+
```
|
| 352 |
+
|
| 353 |
---
|
| 354 |
|
| 355 |
## π Environment Variables
|
| 356 |
|
| 357 |
| Variable | Required | Description |
|
| 358 |
|---|---|---|
|
| 359 |
+
| `API_BASE_URL` | Injected by validator | LLM proxy endpoint. Falls back to Groq if not set. |
|
| 360 |
+
| `API_KEY` | Injected by validator | LLM proxy API key. Falls back to `GROQ_API_KEY`. |
|
| 361 |
+
| `MODEL_NAME` | Injected by validator | Model to use. Defaults to `llama-3.3-70b-versatile`. |
|
| 362 |
+
| `GROQ_API_KEY` | Optional (local dev) | Enables `/baseline` endpoint locally via Groq free tier. |
|
| 363 |
+
| `PYTHON_EXEC` | Optional (local dev) | Path to Python interpreter with torch installed, used by grader subprocess. |
|
| 364 |
+
| `HF_TOKEN` | Optional | HuggingFace token for Space access. |
|
| 365 |
|
| 366 |
---
|
| 367 |
|
| 368 |
## π License
|
| 369 |
|
| 370 |
+
BSD 3-Clause β see [OpenEnv](https://github.com/meta-pytorch/OpenEnv/blob/main/LICENSE)
|
__pycache__/models.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/models.cpython-310.pyc and b/__pycache__/models.cpython-310.pyc differ
|
|
|
inference.py
CHANGED
|
@@ -1,27 +1,75 @@
|
|
| 1 |
import os
|
| 2 |
import sys
|
| 3 |
import json
|
|
|
|
| 4 |
from openai import OpenAI
|
|
|
|
| 5 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "server"))
|
| 6 |
-
from bug_generator import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from grader import grade
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
SYSTEM_PROMPT = """You are an expert ML engineer specializing in debugging PyTorch training code.
|
| 15 |
You must respond with valid JSON in exactly this format:
|
| 16 |
-
{"bug_type": "<
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
def call_llm(client, task_description, buggy_code, error_output):
|
| 20 |
response = client.chat.completions.create(
|
| 21 |
model=MODEL_NAME,
|
| 22 |
messages=[
|
| 23 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 24 |
-
{"role": "user", "content":
|
| 25 |
],
|
| 26 |
temperature=0.0,
|
| 27 |
max_tokens=2048,
|
|
@@ -29,26 +77,80 @@ def call_llm(client, task_description, buggy_code, error_output):
|
|
| 29 |
)
|
| 30 |
return json.loads(response.choices[0].message.content.strip())
|
| 31 |
|
| 32 |
-
def main():
|
| 33 |
-
client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL)
|
| 34 |
-
tasks = [TASK_SHAPE_MISMATCH, TASK_TRAINING_COLLAPSE, TASK_DATA_LEAKAGE]
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
try:
|
| 40 |
-
parsed = call_llm(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
bug_type = parsed.get("bug_type", "other")
|
| 42 |
diagnosis = parsed.get("diagnosis", "")
|
| 43 |
fixed_code = parsed.get("fixed_code", "")
|
|
|
|
| 44 |
except Exception as e:
|
| 45 |
bug_type, diagnosis, fixed_code = "other", str(e), ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
|
| 48 |
-
print(f"[STEP] step=1 reward={result.score:.4f}", flush=True)
|
| 49 |
-
print(f"[END] task={task_id} score={result.score:.4f} steps=1", flush=True)
|
| 50 |
|
| 51 |
-
print("\nDone.", flush=True)
|
| 52 |
|
| 53 |
if __name__ == "__main__":
|
| 54 |
main()
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
| 3 |
import json
|
| 4 |
+
from typing import List, Optional
|
| 5 |
from openai import OpenAI
|
| 6 |
+
|
| 7 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "server"))
|
| 8 |
+
from bug_generator import (
|
| 9 |
+
get_scenario,
|
| 10 |
+
TASK_SHAPE_MISMATCH,
|
| 11 |
+
TASK_TRAINING_COLLAPSE,
|
| 12 |
+
TASK_DATA_LEAKAGE,
|
| 13 |
+
TASK_WRONG_DEVICE,
|
| 14 |
+
TASK_GRADIENT_NOT_ZEROED,
|
| 15 |
+
TASK_MISSING_EVAL_MODE,
|
| 16 |
+
)
|
| 17 |
from grader import grade
|
| 18 |
|
| 19 |
+
API_KEY = os.getenv("HF_TOKEN") or os.getenv("API_KEY") or os.getenv("GROQ_API_KEY", "")
|
| 20 |
+
API_BASE_URL = os.getenv("API_BASE_URL") or "https://router.huggingface.co/v1"
|
| 21 |
+
MODEL_NAME = os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-72B-Instruct"
|
| 22 |
+
|
| 23 |
+
ENV_NAME = "ml_debug_env"
|
| 24 |
+
MAX_STEPS = 3
|
| 25 |
+
SUCCESS_THRESHOLD = 0.95
|
| 26 |
|
| 27 |
SYSTEM_PROMPT = """You are an expert ML engineer specializing in debugging PyTorch training code.
|
| 28 |
You must respond with valid JSON in exactly this format:
|
| 29 |
+
{"bug_type": "<EXACT value from the list below>", "diagnosis": "<clear explanation of root cause>", "fixed_code": "<complete corrected Python script>"}
|
| 30 |
+
|
| 31 |
+
bug_type MUST be exactly one of these strings, no other value is valid:
|
| 32 |
+
- shape_mismatch
|
| 33 |
+
- training_collapse
|
| 34 |
+
- data_leakage
|
| 35 |
+
- wrong_device
|
| 36 |
+
- gradient_not_zeroed
|
| 37 |
+
- missing_eval_mode
|
| 38 |
+
- other
|
| 39 |
+
|
| 40 |
+
Rules:
|
| 41 |
+
- fixed_code must be the COMPLETE script with all imports. Runnable as-is.
|
| 42 |
+
- No markdown fences inside JSON values.
|
| 43 |
+
- No text outside the JSON object.
|
| 44 |
+
- If you see grader feedback, use it to correct your previous attempt."""
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def log_start(task: str, env: str, model: str) -> None:
|
| 48 |
+
print(f"[START] task={task} env={env} model={model}", flush=True)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
|
| 52 |
+
error_val = error if error else "null"
|
| 53 |
+
done_val = str(done).lower()
|
| 54 |
+
print(f"[STEP] step={step} action={action} reward={reward:.2f} done={done_val} error={error_val}", flush=True)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
|
| 58 |
+
rewards_str = ",".join(f"{r:.2f}" for r in rewards)
|
| 59 |
+
print(f"[END] success={str(success).lower()} steps={steps} score={score:.3f} rewards={rewards_str}", flush=True)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def call_llm(client: OpenAI, task_description: str, buggy_code: str, error_output: str, feedback: Optional[str] = None) -> dict:
|
| 63 |
+
user_content = f"Task: {task_description}\n\nBroken script:\n```python\n{buggy_code}\n```\n\nFailure observed:\n{error_output}"
|
| 64 |
+
if feedback:
|
| 65 |
+
user_content += f"\n\nPrevious attempt grader feedback:\n{feedback}\n\nFix the remaining issues and return corrected JSON."
|
| 66 |
+
user_content += "\n\nIMPORTANT: The encoder outputs a tensor. The classifier input dim must MATCH the encoder output dim exactly. Check what the last encoder layer outputs and use that exact number for nn.Linear input."
|
| 67 |
|
|
|
|
| 68 |
response = client.chat.completions.create(
|
| 69 |
model=MODEL_NAME,
|
| 70 |
messages=[
|
| 71 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 72 |
+
{"role": "user", "content": user_content},
|
| 73 |
],
|
| 74 |
temperature=0.0,
|
| 75 |
max_tokens=2048,
|
|
|
|
| 77 |
)
|
| 78 |
return json.loads(response.choices[0].message.content.strip())
|
| 79 |
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
def run_task(client: OpenAI, task_id: str) -> tuple[float, List[float], int]:
|
| 82 |
+
log_start(task=task_id, env=ENV_NAME, model=MODEL_NAME)
|
| 83 |
+
|
| 84 |
+
scenario = get_scenario(task_id, seed=42)
|
| 85 |
+
rewards: List[float] = []
|
| 86 |
+
steps_taken = 0
|
| 87 |
+
last_feedback: Optional[str] = None
|
| 88 |
+
best_score = 0.0
|
| 89 |
+
|
| 90 |
+
for step in range(1, MAX_STEPS + 1):
|
| 91 |
try:
|
| 92 |
+
parsed = call_llm(
|
| 93 |
+
client,
|
| 94 |
+
scenario.task_description,
|
| 95 |
+
scenario.buggy_code,
|
| 96 |
+
scenario.error_output,
|
| 97 |
+
feedback=last_feedback,
|
| 98 |
+
)
|
| 99 |
bug_type = parsed.get("bug_type", "other")
|
| 100 |
diagnosis = parsed.get("diagnosis", "")
|
| 101 |
fixed_code = parsed.get("fixed_code", "")
|
| 102 |
+
error = None
|
| 103 |
except Exception as e:
|
| 104 |
bug_type, diagnosis, fixed_code = "other", str(e), ""
|
| 105 |
+
error = str(e)
|
| 106 |
+
|
| 107 |
+
result = grade(
|
| 108 |
+
action_bug_type=bug_type,
|
| 109 |
+
action_diagnosis=diagnosis,
|
| 110 |
+
fixed_code=fixed_code,
|
| 111 |
+
scenario=scenario,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
score = result.score
|
| 115 |
+
done = score >= SUCCESS_THRESHOLD or step == MAX_STEPS
|
| 116 |
+
|
| 117 |
+
rewards.append(score)
|
| 118 |
+
steps_taken = step
|
| 119 |
+
if score > best_score:
|
| 120 |
+
best_score = score
|
| 121 |
+
|
| 122 |
+
log_step(step=step, action=bug_type, reward=score, done=done, error=error)
|
| 123 |
+
|
| 124 |
+
if done:
|
| 125 |
+
break
|
| 126 |
+
|
| 127 |
+
last_feedback = result.feedback
|
| 128 |
+
|
| 129 |
+
return best_score, rewards, steps_taken
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def main():
|
| 133 |
+
client = OpenAI(api_key=API_KEY, base_url=API_BASE_URL)
|
| 134 |
+
|
| 135 |
+
tasks = [
|
| 136 |
+
TASK_SHAPE_MISMATCH,
|
| 137 |
+
TASK_TRAINING_COLLAPSE,
|
| 138 |
+
TASK_DATA_LEAKAGE,
|
| 139 |
+
TASK_WRONG_DEVICE,
|
| 140 |
+
TASK_GRADIENT_NOT_ZEROED,
|
| 141 |
+
TASK_MISSING_EVAL_MODE,
|
| 142 |
+
]
|
| 143 |
+
|
| 144 |
+
all_scores: List[float] = []
|
| 145 |
+
|
| 146 |
+
for task_id in tasks:
|
| 147 |
+
best_score, rewards, steps = run_task(client, task_id)
|
| 148 |
+
success = best_score >= SUCCESS_THRESHOLD
|
| 149 |
+
log_end(success=success, steps=steps, score=best_score, rewards=rewards)
|
| 150 |
+
all_scores.append(best_score)
|
| 151 |
|
| 152 |
+
print(f"\n[SUMMARY] tasks={len(tasks)} avg_score={sum(all_scores)/len(all_scores):.3f}", flush=True)
|
|
|
|
|
|
|
| 153 |
|
|
|
|
| 154 |
|
| 155 |
if __name__ == "__main__":
|
| 156 |
main()
|
models.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# models.py
|
| 2 |
from typing import Optional, Literal
|
| 3 |
from pydantic import Field
|
| 4 |
from openenv.core.env_server.types import Action, Observation, State
|
|
@@ -7,7 +6,7 @@ from openenv.core.env_server.types import Action, Observation, State
|
|
| 7 |
class DebugAction(Action):
|
| 8 |
"""
|
| 9 |
The agent's response to a broken ML script.
|
| 10 |
-
|
| 11 |
The agent must identify the bug type, explain the root cause,
|
| 12 |
and provide a corrected version of the full script.
|
| 13 |
"""
|
|
@@ -15,7 +14,8 @@ class DebugAction(Action):
|
|
| 15 |
...,
|
| 16 |
description=(
|
| 17 |
"Category of the bug identified. Must be one of: "
|
| 18 |
-
"'shape_mismatch', 'training_collapse', 'data_leakage',
|
|
|
|
| 19 |
)
|
| 20 |
)
|
| 21 |
diagnosis: str = Field(
|
|
@@ -37,16 +37,22 @@ class DebugAction(Action):
|
|
| 37 |
class DebugObservation(Observation):
|
| 38 |
"""
|
| 39 |
What the agent sees at each step.
|
| 40 |
-
|
| 41 |
On reset: the broken script + error output.
|
| 42 |
After step: execution result of the agent's fix + grader score.
|
| 43 |
"""
|
| 44 |
-
task_id: str = Field(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
task_description: str = Field(..., description="Natural language description of what the agent must fix")
|
| 46 |
buggy_code: str = Field(..., description="The broken Python script the agent must debug")
|
| 47 |
error_output: str = Field(..., description="The stderr/traceback or behavioral failure description seen when running the buggy script")
|
| 48 |
execution_result: Optional[str] = Field(None, description="stdout+stderr from running the agent's fixed code (None on reset)")
|
| 49 |
-
grader_score: Optional[float] = Field(None, description="Score 0.
|
| 50 |
grader_feedback: Optional[str] = Field(None, description="Human-readable explanation of why the score was assigned")
|
| 51 |
step_number: int = Field(0, description="Current step within this episode")
|
| 52 |
|
|
@@ -54,7 +60,6 @@ class DebugObservation(Observation):
|
|
| 54 |
class DebugState(State):
|
| 55 |
"""
|
| 56 |
Internal episode metadata.
|
| 57 |
-
Extends the base State (which has episode_id and step_count).
|
| 58 |
"""
|
| 59 |
task_id: str = Field("", description="Active task identifier")
|
| 60 |
max_steps: int = Field(3, description="Maximum steps allowed per episode")
|
|
|
|
|
|
|
| 1 |
from typing import Optional, Literal
|
| 2 |
from pydantic import Field
|
| 3 |
from openenv.core.env_server.types import Action, Observation, State
|
|
|
|
| 6 |
class DebugAction(Action):
|
| 7 |
"""
|
| 8 |
The agent's response to a broken ML script.
|
| 9 |
+
|
| 10 |
The agent must identify the bug type, explain the root cause,
|
| 11 |
and provide a corrected version of the full script.
|
| 12 |
"""
|
|
|
|
| 14 |
...,
|
| 15 |
description=(
|
| 16 |
"Category of the bug identified. Must be one of: "
|
| 17 |
+
"'shape_mismatch', 'training_collapse', 'data_leakage', "
|
| 18 |
+
"'wrong_device', 'gradient_not_zeroed', 'missing_eval_mode', 'other'"
|
| 19 |
)
|
| 20 |
)
|
| 21 |
diagnosis: str = Field(
|
|
|
|
| 37 |
class DebugObservation(Observation):
|
| 38 |
"""
|
| 39 |
What the agent sees at each step.
|
| 40 |
+
|
| 41 |
On reset: the broken script + error output.
|
| 42 |
After step: execution result of the agent's fix + grader score.
|
| 43 |
"""
|
| 44 |
+
task_id: str = Field(
|
| 45 |
+
...,
|
| 46 |
+
description=(
|
| 47 |
+
"Which task is active: 'shape_mismatch', 'training_collapse', 'data_leakage', "
|
| 48 |
+
"'wrong_device', 'gradient_not_zeroed', or 'missing_eval_mode'"
|
| 49 |
+
)
|
| 50 |
+
)
|
| 51 |
task_description: str = Field(..., description="Natural language description of what the agent must fix")
|
| 52 |
buggy_code: str = Field(..., description="The broken Python script the agent must debug")
|
| 53 |
error_output: str = Field(..., description="The stderr/traceback or behavioral failure description seen when running the buggy script")
|
| 54 |
execution_result: Optional[str] = Field(None, description="stdout+stderr from running the agent's fixed code (None on reset)")
|
| 55 |
+
grader_score: Optional[float] = Field(None, description="Score 0.01-0.99 from the grader (None on reset, set after step)")
|
| 56 |
grader_feedback: Optional[str] = Field(None, description="Human-readable explanation of why the score was assigned")
|
| 57 |
step_number: int = Field(0, description="Current step within this episode")
|
| 58 |
|
|
|
|
| 60 |
class DebugState(State):
|
| 61 |
"""
|
| 62 |
Internal episode metadata.
|
|
|
|
| 63 |
"""
|
| 64 |
task_id: str = Field("", description="Active task identifier")
|
| 65 |
max_steps: int = Field(3, description="Maximum steps allowed per episode")
|
server/__pycache__/app.cpython-310.pyc
CHANGED
|
Binary files a/server/__pycache__/app.cpython-310.pyc and b/server/__pycache__/app.cpython-310.pyc differ
|
|
|
server/__pycache__/bug_generator.cpython-310.pyc
CHANGED
|
Binary files a/server/__pycache__/bug_generator.cpython-310.pyc and b/server/__pycache__/bug_generator.cpython-310.pyc differ
|
|
|
server/__pycache__/grader.cpython-310.pyc
CHANGED
|
Binary files a/server/__pycache__/grader.cpython-310.pyc and b/server/__pycache__/grader.cpython-310.pyc differ
|
|
|
server/__pycache__/ml_debug_env_environment.cpython-310.pyc
CHANGED
|
Binary files a/server/__pycache__/ml_debug_env_environment.cpython-310.pyc and b/server/__pycache__/ml_debug_env_environment.cpython-310.pyc differ
|
|
|
server/app.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
| 1 |
-
# server/app.py
|
| 2 |
import os
|
| 3 |
import sys
|
| 4 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
| 5 |
|
| 6 |
import asyncio
|
| 7 |
-
from typing import Any, Dict
|
| 8 |
-
|
| 9 |
|
| 10 |
from fastapi import FastAPI, HTTPException
|
| 11 |
from fastapi.middleware.cors import CORSMiddleware
|
|
@@ -19,14 +17,13 @@ from bug_generator import (
|
|
| 19 |
TASK_SHAPE_MISMATCH,
|
| 20 |
TASK_TRAINING_COLLAPSE,
|
| 21 |
TASK_DATA_LEAKAGE,
|
|
|
|
|
|
|
|
|
|
| 22 |
get_scenario,
|
| 23 |
)
|
| 24 |
from grader import grade
|
| 25 |
|
| 26 |
-
# ------------------------------------------------------------------ #
|
| 27 |
-
# Base OpenEnv app (handles /reset /step /state /health /ws /schema) #
|
| 28 |
-
# ------------------------------------------------------------------ #
|
| 29 |
-
|
| 30 |
app = create_app(
|
| 31 |
MlDebugEnvEnvironment,
|
| 32 |
DebugAction,
|
|
@@ -41,18 +38,14 @@ app.add_middleware(
|
|
| 41 |
allow_headers=["*"],
|
| 42 |
)
|
| 43 |
|
| 44 |
-
# ------------------------------------------------------------------ #
|
| 45 |
-
# /tasks β list tasks + action schema #
|
| 46 |
-
# ------------------------------------------------------------------ #
|
| 47 |
-
|
| 48 |
TASK_DEFINITIONS = [
|
| 49 |
{
|
| 50 |
"task_id": TASK_SHAPE_MISMATCH,
|
| 51 |
"name": "Shape Mismatch",
|
| 52 |
"difficulty": "easy",
|
| 53 |
"description": (
|
| 54 |
-
"A PyTorch classifier crashes immediately with a RuntimeError during the "
|
| 55 |
-
"
|
| 56 |
"Fix the architectural bug so the model trains for 3 epochs without error."
|
| 57 |
),
|
| 58 |
"success_criteria": "Code runs to completion; all epoch logs print; no RuntimeError.",
|
|
@@ -62,8 +55,8 @@ TASK_DEFINITIONS = [
|
|
| 62 |
"name": "Training Collapse",
|
| 63 |
"difficulty": "medium",
|
| 64 |
"description": (
|
| 65 |
-
"A PyTorch training script runs without crashing but the model completely "
|
| 66 |
-
"
|
| 67 |
"Fix the training bug so loss decreases consistently across all epochs."
|
| 68 |
),
|
| 69 |
"success_criteria": "Loss decreases across epochs; no NaN values in output.",
|
|
@@ -82,6 +75,39 @@ TASK_DEFINITIONS = [
|
|
| 82 |
"test set metrics reflect genuine generalisation."
|
| 83 |
),
|
| 84 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
]
|
| 86 |
|
| 87 |
ACTION_SCHEMA = {
|
|
@@ -91,7 +117,15 @@ ACTION_SCHEMA = {
|
|
| 91 |
"bug_type": {
|
| 92 |
"type": "string",
|
| 93 |
"description": "Category of bug identified.",
|
| 94 |
-
"enum": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
},
|
| 96 |
"diagnosis": {
|
| 97 |
"type": "string",
|
|
@@ -107,6 +141,8 @@ ACTION_SCHEMA = {
|
|
| 107 |
},
|
| 108 |
}
|
| 109 |
|
|
|
|
|
|
|
| 110 |
|
| 111 |
@app.get("/tasks")
|
| 112 |
def list_tasks() -> Dict[str, Any]:
|
|
@@ -114,14 +150,10 @@ def list_tasks() -> Dict[str, Any]:
|
|
| 114 |
"tasks": TASK_DEFINITIONS,
|
| 115 |
"action_schema": ACTION_SCHEMA,
|
| 116 |
"total_tasks": len(TASK_DEFINITIONS),
|
| 117 |
-
"difficulty_range": "easy β medium β hard",
|
| 118 |
}
|
| 119 |
|
| 120 |
|
| 121 |
-
# ------------------------------------------------------------------ #
|
| 122 |
-
# /grader β score a fix without running a full episode #
|
| 123 |
-
# ------------------------------------------------------------------ #
|
| 124 |
-
|
| 125 |
class GraderRequest(BaseModel):
|
| 126 |
task_id: str
|
| 127 |
bug_type: str
|
|
@@ -132,11 +164,10 @@ class GraderRequest(BaseModel):
|
|
| 132 |
|
| 133 |
@app.post("/grader")
|
| 134 |
def run_grader(req: GraderRequest) -> Dict[str, Any]:
|
| 135 |
-
|
| 136 |
-
if req.task_id not in valid_tasks:
|
| 137 |
raise HTTPException(
|
| 138 |
status_code=400,
|
| 139 |
-
detail=f"task_id must be one of {
|
| 140 |
)
|
| 141 |
try:
|
| 142 |
scenario = get_scenario(req.task_id, seed=req.seed)
|
|
@@ -157,21 +188,13 @@ def run_grader(req: GraderRequest) -> Dict[str, Any]:
|
|
| 157 |
}
|
| 158 |
|
| 159 |
|
| 160 |
-
# ------------------------------------------------------------------ #
|
| 161 |
-
# /baseline β run the Groq-powered baseline agent on all 3 tasks #
|
| 162 |
-
# ------------------------------------------------------------------ #
|
| 163 |
-
|
| 164 |
@app.get("/baseline")
|
| 165 |
async def run_baseline() -> Dict[str, Any]:
|
| 166 |
-
"""
|
| 167 |
-
Runs the Groq-based baseline agent against all 3 tasks and returns scores.
|
| 168 |
-
Requires GROQ_API_KEY environment variable.
|
| 169 |
-
"""
|
| 170 |
-
groq_api_key = (os.environ.get("API_KEY") or os.environ.get("GROQ_API_KEY", "")).strip()
|
| 171 |
if not groq_api_key:
|
| 172 |
raise HTTPException(
|
| 173 |
status_code=503,
|
| 174 |
-
detail="API_KEY or GROQ_API_KEY environment variable not set.",
|
| 175 |
)
|
| 176 |
|
| 177 |
try:
|
|
@@ -179,7 +202,7 @@ async def run_baseline() -> Dict[str, Any]:
|
|
| 179 |
if server_dir not in sys.path:
|
| 180 |
sys.path.insert(0, server_dir)
|
| 181 |
from baseline_inference import run_baseline_on_all_tasks
|
| 182 |
-
base_url = (os.environ.get("API_BASE_URL") or "https://
|
| 183 |
results = await asyncio.get_event_loop().run_in_executor(
|
| 184 |
None, run_baseline_on_all_tasks, groq_api_key, base_url
|
| 185 |
)
|
|
@@ -192,13 +215,15 @@ async def run_baseline() -> Dict[str, Any]:
|
|
| 192 |
return {
|
| 193 |
"results": results,
|
| 194 |
"average_score": round(avg, 4),
|
| 195 |
-
"model": "
|
| 196 |
"note": "Baseline uses a single-shot zero-prompt strategy with no examples.",
|
| 197 |
}
|
| 198 |
|
|
|
|
| 199 |
def main():
|
| 200 |
import uvicorn
|
| 201 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 202 |
|
|
|
|
| 203 |
if __name__ == "__main__":
|
| 204 |
main()
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
| 3 |
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
| 4 |
|
| 5 |
import asyncio
|
| 6 |
+
from typing import Any, Dict
|
|
|
|
| 7 |
|
| 8 |
from fastapi import FastAPI, HTTPException
|
| 9 |
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
| 17 |
TASK_SHAPE_MISMATCH,
|
| 18 |
TASK_TRAINING_COLLAPSE,
|
| 19 |
TASK_DATA_LEAKAGE,
|
| 20 |
+
TASK_WRONG_DEVICE,
|
| 21 |
+
TASK_GRADIENT_NOT_ZEROED,
|
| 22 |
+
TASK_MISSING_EVAL_MODE,
|
| 23 |
get_scenario,
|
| 24 |
)
|
| 25 |
from grader import grade
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
app = create_app(
|
| 28 |
MlDebugEnvEnvironment,
|
| 29 |
DebugAction,
|
|
|
|
| 38 |
allow_headers=["*"],
|
| 39 |
)
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
TASK_DEFINITIONS = [
|
| 42 |
{
|
| 43 |
"task_id": TASK_SHAPE_MISMATCH,
|
| 44 |
"name": "Shape Mismatch",
|
| 45 |
"difficulty": "easy",
|
| 46 |
"description": (
|
| 47 |
+
"A PyTorch classifier crashes immediately with a RuntimeError during the forward pass. "
|
| 48 |
+
"The error message is explicit in the traceback. "
|
| 49 |
"Fix the architectural bug so the model trains for 3 epochs without error."
|
| 50 |
),
|
| 51 |
"success_criteria": "Code runs to completion; all epoch logs print; no RuntimeError.",
|
|
|
|
| 55 |
"name": "Training Collapse",
|
| 56 |
"difficulty": "medium",
|
| 57 |
"description": (
|
| 58 |
+
"A PyTorch training script runs without crashing but the model completely fails to learn. "
|
| 59 |
+
"Loss diverges to NaN or plateaus immediately. "
|
| 60 |
"Fix the training bug so loss decreases consistently across all epochs."
|
| 61 |
),
|
| 62 |
"success_criteria": "Loss decreases across epochs; no NaN values in output.",
|
|
|
|
| 75 |
"test set metrics reflect genuine generalisation."
|
| 76 |
),
|
| 77 |
},
|
| 78 |
+
{
|
| 79 |
+
"task_id": TASK_WRONG_DEVICE,
|
| 80 |
+
"name": "Wrong Device",
|
| 81 |
+
"difficulty": "medium",
|
| 82 |
+
"description": (
|
| 83 |
+
"A PyTorch script crashes on the first forward pass because the model and data tensors "
|
| 84 |
+
"are on different devices (CPU vs CUDA). "
|
| 85 |
+
"Fix tensor placement so training runs cleanly on whatever device is available."
|
| 86 |
+
),
|
| 87 |
+
"success_criteria": "All tensors on the same device; training completes 3 epochs without RuntimeError.",
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"task_id": TASK_GRADIENT_NOT_ZEROED,
|
| 91 |
+
"name": "Gradient Not Zeroed",
|
| 92 |
+
"difficulty": "medium-hard",
|
| 93 |
+
"description": (
|
| 94 |
+
"A PyTorch training script runs but loss explodes after the first epoch and collapses to NaN. "
|
| 95 |
+
"No crash occurs. There is a fundamental error in the training loop structure. "
|
| 96 |
+
"Fix the loop so loss decreases consistently across all epochs."
|
| 97 |
+
),
|
| 98 |
+
"success_criteria": "Loss decreases consistently across 6 epochs; no NaN values.",
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"task_id": TASK_MISSING_EVAL_MODE,
|
| 102 |
+
"name": "Missing Eval Mode",
|
| 103 |
+
"difficulty": "hard",
|
| 104 |
+
"description": (
|
| 105 |
+
"A PyTorch classifier trains successfully but produces unstable and unreliable test accuracy. "
|
| 106 |
+
"The model has Dropout and BatchNorm layers. Running evaluation multiple times gives different results. "
|
| 107 |
+
"Fix the evaluation so it produces stable, correct metrics."
|
| 108 |
+
),
|
| 109 |
+
"success_criteria": "model.eval() and torch.no_grad() used during evaluation; metrics are stable and deterministic.",
|
| 110 |
+
},
|
| 111 |
]
|
| 112 |
|
| 113 |
ACTION_SCHEMA = {
|
|
|
|
| 117 |
"bug_type": {
|
| 118 |
"type": "string",
|
| 119 |
"description": "Category of bug identified.",
|
| 120 |
+
"enum": [
|
| 121 |
+
"shape_mismatch",
|
| 122 |
+
"training_collapse",
|
| 123 |
+
"data_leakage",
|
| 124 |
+
"wrong_device",
|
| 125 |
+
"gradient_not_zeroed",
|
| 126 |
+
"missing_eval_mode",
|
| 127 |
+
"other",
|
| 128 |
+
],
|
| 129 |
},
|
| 130 |
"diagnosis": {
|
| 131 |
"type": "string",
|
|
|
|
| 141 |
},
|
| 142 |
}
|
| 143 |
|
| 144 |
+
VALID_TASK_IDS = [t["task_id"] for t in TASK_DEFINITIONS]
|
| 145 |
+
|
| 146 |
|
| 147 |
@app.get("/tasks")
|
| 148 |
def list_tasks() -> Dict[str, Any]:
|
|
|
|
| 150 |
"tasks": TASK_DEFINITIONS,
|
| 151 |
"action_schema": ACTION_SCHEMA,
|
| 152 |
"total_tasks": len(TASK_DEFINITIONS),
|
| 153 |
+
"difficulty_range": "easy β medium β medium-hard β hard",
|
| 154 |
}
|
| 155 |
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
class GraderRequest(BaseModel):
|
| 158 |
task_id: str
|
| 159 |
bug_type: str
|
|
|
|
| 164 |
|
| 165 |
@app.post("/grader")
|
| 166 |
def run_grader(req: GraderRequest) -> Dict[str, Any]:
|
| 167 |
+
if req.task_id not in VALID_TASK_IDS:
|
|
|
|
| 168 |
raise HTTPException(
|
| 169 |
status_code=400,
|
| 170 |
+
detail=f"task_id must be one of {VALID_TASK_IDS}",
|
| 171 |
)
|
| 172 |
try:
|
| 173 |
scenario = get_scenario(req.task_id, seed=req.seed)
|
|
|
|
| 188 |
}
|
| 189 |
|
| 190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
@app.get("/baseline")
|
| 192 |
async def run_baseline() -> Dict[str, Any]:
|
| 193 |
+
groq_api_key = (os.environ.get("HF_TOKEN") or os.environ.get("API_KEY") or os.environ.get("GROQ_API_KEY", "")).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
if not groq_api_key:
|
| 195 |
raise HTTPException(
|
| 196 |
status_code=503,
|
| 197 |
+
detail="HF_TOKEN, API_KEY, or GROQ_API_KEY environment variable not set.",
|
| 198 |
)
|
| 199 |
|
| 200 |
try:
|
|
|
|
| 202 |
if server_dir not in sys.path:
|
| 203 |
sys.path.insert(0, server_dir)
|
| 204 |
from baseline_inference import run_baseline_on_all_tasks
|
| 205 |
+
base_url = (os.environ.get("API_BASE_URL") or "https://router.huggingface.co/v1").strip()
|
| 206 |
results = await asyncio.get_event_loop().run_in_executor(
|
| 207 |
None, run_baseline_on_all_tasks, groq_api_key, base_url
|
| 208 |
)
|
|
|
|
| 215 |
return {
|
| 216 |
"results": results,
|
| 217 |
"average_score": round(avg, 4),
|
| 218 |
+
"model": os.environ.get("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct"),
|
| 219 |
"note": "Baseline uses a single-shot zero-prompt strategy with no examples.",
|
| 220 |
}
|
| 221 |
|
| 222 |
+
|
| 223 |
def main():
|
| 224 |
import uvicorn
|
| 225 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 226 |
|
| 227 |
+
|
| 228 |
if __name__ == "__main__":
|
| 229 |
main()
|
server/bug_generator.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# server/bug_generator.py
|
| 2 |
import random
|
| 3 |
from dataclasses import dataclass
|
| 4 |
from typing import Optional
|
|
@@ -11,12 +10,24 @@ class BugScenario:
|
|
| 11 |
buggy_code: str
|
| 12 |
error_output: str
|
| 13 |
correct_bug_type: str
|
| 14 |
-
solution_hint: str
|
| 15 |
|
| 16 |
|
| 17 |
TASK_SHAPE_MISMATCH = "shape_mismatch"
|
| 18 |
TASK_TRAINING_COLLAPSE = "training_collapse"
|
| 19 |
TASK_DATA_LEAKAGE = "data_leakage"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def get_scenario(task_id: str, seed: Optional[int] = None) -> BugScenario:
|
|
@@ -27,19 +38,23 @@ def get_scenario(task_id: str, seed: Optional[int] = None) -> BugScenario:
|
|
| 27 |
return _training_collapse_scenario(rng)
|
| 28 |
elif task_id == TASK_DATA_LEAKAGE:
|
| 29 |
return _data_leakage_scenario(rng)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
else:
|
| 31 |
raise ValueError(f"Unknown task_id: {task_id}")
|
| 32 |
|
| 33 |
|
| 34 |
def get_random_task(seed: Optional[int] = None) -> str:
|
| 35 |
rng = random.Random(seed)
|
| 36 |
-
return rng.choice(
|
| 37 |
|
| 38 |
|
| 39 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 40 |
# TASK 1 β Shape Mismatch (Easy)
|
| 41 |
-
# The classifier head has wrong input features.
|
| 42 |
-
# Error is explicit in the traceback.
|
| 43 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 44 |
|
| 45 |
def _shape_mismatch_scenario(rng: random.Random) -> BugScenario:
|
|
@@ -93,12 +108,6 @@ print("Training finished")
|
|
| 93 |
error_output = f'''Traceback (most recent call last):
|
| 94 |
File "train.py", line 32, in <module>
|
| 95 |
pred = model(xb)
|
| 96 |
-
File ".../torch/nn/modules/module.py", in _call_impl
|
| 97 |
-
return forward(*input, **kwargs)
|
| 98 |
-
File "train.py", line 21, in forward
|
| 99 |
-
return self.classifier(features)
|
| 100 |
-
File ".../torch/nn/modules/linear.py", in forward
|
| 101 |
-
return F.linear(input, self.weight, self.bias)
|
| 102 |
RuntimeError: mat1 and mat2 shapes cannot be multiplied ({hidden_size} cannot be broadcast to {wrong_size})'''
|
| 103 |
|
| 104 |
return BugScenario(
|
|
@@ -117,8 +126,6 @@ RuntimeError: mat1 and mat2 shapes cannot be multiplied ({hidden_size} cannot be
|
|
| 117 |
|
| 118 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 119 |
# TASK 2 β Training Collapse (Medium)
|
| 120 |
-
# Loss explodes to NaN within a few steps.
|
| 121 |
-
# Script runs but model never learns. No crash, just broken behavior.
|
| 122 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 123 |
|
| 124 |
def _training_collapse_scenario(rng: random.Random) -> BugScenario:
|
|
@@ -237,7 +244,7 @@ print("Training finished")
|
|
| 237 |
"Loss plateaus immediately and does not decrease. "
|
| 238 |
"The model is using the wrong loss function for the task type."
|
| 239 |
)
|
| 240 |
-
solution_hint = "MSELoss used for binary classification; should be BCELoss or BCEWithLogitsLoss
|
| 241 |
|
| 242 |
return BugScenario(
|
| 243 |
task_id=TASK_TRAINING_COLLAPSE,
|
|
@@ -256,9 +263,6 @@ print("Training finished")
|
|
| 256 |
|
| 257 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 258 |
# TASK 3 β Data Leakage (Hard)
|
| 259 |
-
# Test set statistics used to normalize the entire dataset before splitting.
|
| 260 |
-
# Model trains, accuracy looks great (~95%), but it's artificially inflated.
|
| 261 |
-
# Agent must reason about data flow, not errors or loss curves.
|
| 262 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 263 |
|
| 264 |
def _data_leakage_scenario(rng: random.Random) -> BugScenario:
|
|
@@ -292,8 +296,6 @@ X_raw = torch.randn(N, D)
|
|
| 292 |
true_weights = torch.randn(D, C)
|
| 293 |
y_all = (X_raw @ true_weights).argmax(dim=1)
|
| 294 |
|
| 295 |
-
# BUG: normalizing using statistics from the full dataset before splitting
|
| 296 |
-
# This leaks test set information into the training process
|
| 297 |
mean = X_raw.mean(dim=0)
|
| 298 |
std = X_raw.std(dim=0) + 1e-8
|
| 299 |
X_normalized = (X_raw - mean) / std
|
|
@@ -330,8 +332,7 @@ print("Training finished")
|
|
| 330 |
"\n"
|
| 331 |
"However, the reported test accuracy is misleading and cannot be trusted. "
|
| 332 |
"The model has not demonstrated genuine generalization ability. "
|
| 333 |
-
"There is a data handling bug that makes the evaluation invalid.
|
| 334 |
-
"The model would perform significantly worse on truly unseen data."
|
| 335 |
)
|
| 336 |
solution_hint = "normalize using only train set mean/std; compute mean and std after the split, only on X_train"
|
| 337 |
|
|
@@ -365,8 +366,6 @@ split = int(0.75 * N)
|
|
| 365 |
X_train_raw, X_test_raw = X_raw[:split], X_raw[split:]
|
| 366 |
y_train, y_test = y_all[:split], y_all[split:]
|
| 367 |
|
| 368 |
-
# BUG: computing normalization statistics from the full dataset
|
| 369 |
-
# The test set has already influenced mean and std
|
| 370 |
full_mean = X_raw.mean(dim=0)
|
| 371 |
full_std = X_raw.std(dim=0) + 1e-8
|
| 372 |
X_train = (X_train_raw - full_mean) / full_std
|
|
@@ -399,8 +398,7 @@ print("Training finished")
|
|
| 399 |
"\n"
|
| 400 |
"The reported test MSE is artificially low and cannot be trusted. "
|
| 401 |
"There is a data preprocessing bug that leaks information from the test set "
|
| 402 |
-
"into the normalization step.
|
| 403 |
-
"A correctly implemented version would report higher test MSE."
|
| 404 |
)
|
| 405 |
solution_hint = "fit normalization stats only on X_train_raw; use train_mean and train_std to normalize both train and test"
|
| 406 |
|
|
@@ -409,11 +407,266 @@ print("Training finished")
|
|
| 409 |
task_description=(
|
| 410 |
"This PyTorch training script runs cleanly with no errors and reports impressive metrics. "
|
| 411 |
"But the evaluation is fundamentally broken due to a data handling mistake. "
|
| 412 |
-
"The model has not learned to generalize β the good numbers are an illusion. "
|
| 413 |
"Identify the data pipeline bug and fix it so the evaluation is valid."
|
| 414 |
),
|
| 415 |
buggy_code=buggy_code,
|
| 416 |
error_output=error_output,
|
| 417 |
correct_bug_type="data_leakage",
|
| 418 |
solution_hint=solution_hint,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
)
|
|
|
|
|
|
|
| 1 |
import random
|
| 2 |
from dataclasses import dataclass
|
| 3 |
from typing import Optional
|
|
|
|
| 10 |
buggy_code: str
|
| 11 |
error_output: str
|
| 12 |
correct_bug_type: str
|
| 13 |
+
solution_hint: str
|
| 14 |
|
| 15 |
|
| 16 |
TASK_SHAPE_MISMATCH = "shape_mismatch"
|
| 17 |
TASK_TRAINING_COLLAPSE = "training_collapse"
|
| 18 |
TASK_DATA_LEAKAGE = "data_leakage"
|
| 19 |
+
TASK_WRONG_DEVICE = "wrong_device"
|
| 20 |
+
TASK_GRADIENT_NOT_ZEROED = "gradient_not_zeroed"
|
| 21 |
+
TASK_MISSING_EVAL_MODE = "missing_eval_mode"
|
| 22 |
+
|
| 23 |
+
ALL_TASKS = [
|
| 24 |
+
TASK_SHAPE_MISMATCH,
|
| 25 |
+
TASK_TRAINING_COLLAPSE,
|
| 26 |
+
TASK_DATA_LEAKAGE,
|
| 27 |
+
TASK_WRONG_DEVICE,
|
| 28 |
+
TASK_GRADIENT_NOT_ZEROED,
|
| 29 |
+
TASK_MISSING_EVAL_MODE,
|
| 30 |
+
]
|
| 31 |
|
| 32 |
|
| 33 |
def get_scenario(task_id: str, seed: Optional[int] = None) -> BugScenario:
|
|
|
|
| 38 |
return _training_collapse_scenario(rng)
|
| 39 |
elif task_id == TASK_DATA_LEAKAGE:
|
| 40 |
return _data_leakage_scenario(rng)
|
| 41 |
+
elif task_id == TASK_WRONG_DEVICE:
|
| 42 |
+
return _wrong_device_scenario(rng)
|
| 43 |
+
elif task_id == TASK_GRADIENT_NOT_ZEROED:
|
| 44 |
+
return _gradient_not_zeroed_scenario(rng)
|
| 45 |
+
elif task_id == TASK_MISSING_EVAL_MODE:
|
| 46 |
+
return _missing_eval_mode_scenario(rng)
|
| 47 |
else:
|
| 48 |
raise ValueError(f"Unknown task_id: {task_id}")
|
| 49 |
|
| 50 |
|
| 51 |
def get_random_task(seed: Optional[int] = None) -> str:
|
| 52 |
rng = random.Random(seed)
|
| 53 |
+
return rng.choice(ALL_TASKS)
|
| 54 |
|
| 55 |
|
| 56 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 57 |
# TASK 1 β Shape Mismatch (Easy)
|
|
|
|
|
|
|
| 58 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
|
| 60 |
def _shape_mismatch_scenario(rng: random.Random) -> BugScenario:
|
|
|
|
| 108 |
error_output = f'''Traceback (most recent call last):
|
| 109 |
File "train.py", line 32, in <module>
|
| 110 |
pred = model(xb)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
RuntimeError: mat1 and mat2 shapes cannot be multiplied ({hidden_size} cannot be broadcast to {wrong_size})'''
|
| 112 |
|
| 113 |
return BugScenario(
|
|
|
|
| 126 |
|
| 127 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 128 |
# TASK 2 β Training Collapse (Medium)
|
|
|
|
|
|
|
| 129 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 130 |
|
| 131 |
def _training_collapse_scenario(rng: random.Random) -> BugScenario:
|
|
|
|
| 244 |
"Loss plateaus immediately and does not decrease. "
|
| 245 |
"The model is using the wrong loss function for the task type."
|
| 246 |
)
|
| 247 |
+
solution_hint = "MSELoss used for binary classification; should be BCELoss or BCEWithLogitsLoss"
|
| 248 |
|
| 249 |
return BugScenario(
|
| 250 |
task_id=TASK_TRAINING_COLLAPSE,
|
|
|
|
| 263 |
|
| 264 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 265 |
# TASK 3 β Data Leakage (Hard)
|
|
|
|
|
|
|
|
|
|
| 266 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 267 |
|
| 268 |
def _data_leakage_scenario(rng: random.Random) -> BugScenario:
|
|
|
|
| 296 |
true_weights = torch.randn(D, C)
|
| 297 |
y_all = (X_raw @ true_weights).argmax(dim=1)
|
| 298 |
|
|
|
|
|
|
|
| 299 |
mean = X_raw.mean(dim=0)
|
| 300 |
std = X_raw.std(dim=0) + 1e-8
|
| 301 |
X_normalized = (X_raw - mean) / std
|
|
|
|
| 332 |
"\n"
|
| 333 |
"However, the reported test accuracy is misleading and cannot be trusted. "
|
| 334 |
"The model has not demonstrated genuine generalization ability. "
|
| 335 |
+
"There is a data handling bug that makes the evaluation invalid."
|
|
|
|
| 336 |
)
|
| 337 |
solution_hint = "normalize using only train set mean/std; compute mean and std after the split, only on X_train"
|
| 338 |
|
|
|
|
| 366 |
X_train_raw, X_test_raw = X_raw[:split], X_raw[split:]
|
| 367 |
y_train, y_test = y_all[:split], y_all[split:]
|
| 368 |
|
|
|
|
|
|
|
| 369 |
full_mean = X_raw.mean(dim=0)
|
| 370 |
full_std = X_raw.std(dim=0) + 1e-8
|
| 371 |
X_train = (X_train_raw - full_mean) / full_std
|
|
|
|
| 398 |
"\n"
|
| 399 |
"The reported test MSE is artificially low and cannot be trusted. "
|
| 400 |
"There is a data preprocessing bug that leaks information from the test set "
|
| 401 |
+
"into the normalization step."
|
|
|
|
| 402 |
)
|
| 403 |
solution_hint = "fit normalization stats only on X_train_raw; use train_mean and train_std to normalize both train and test"
|
| 404 |
|
|
|
|
| 407 |
task_description=(
|
| 408 |
"This PyTorch training script runs cleanly with no errors and reports impressive metrics. "
|
| 409 |
"But the evaluation is fundamentally broken due to a data handling mistake. "
|
|
|
|
| 410 |
"Identify the data pipeline bug and fix it so the evaluation is valid."
|
| 411 |
),
|
| 412 |
buggy_code=buggy_code,
|
| 413 |
error_output=error_output,
|
| 414 |
correct_bug_type="data_leakage",
|
| 415 |
solution_hint=solution_hint,
|
| 416 |
+
)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 420 |
+
# TASK 4 β Wrong Device (Medium)
|
| 421 |
+
# Model on CUDA, data on CPU (or vice versa). Explicit crash.
|
| 422 |
+
# Agent must identify device mismatch and fix tensor placement.
|
| 423 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 424 |
+
|
| 425 |
+
def _wrong_device_scenario(rng: random.Random) -> BugScenario:
|
| 426 |
+
hidden = rng.choice([64, 128, 256])
|
| 427 |
+
num_classes = rng.choice([5, 10])
|
| 428 |
+
|
| 429 |
+
buggy_code = f'''import torch
|
| 430 |
+
import torch.nn as nn
|
| 431 |
+
import torch.optim as optim
|
| 432 |
+
from torch.utils.data import DataLoader, TensorDataset
|
| 433 |
+
|
| 434 |
+
torch.manual_seed(42)
|
| 435 |
+
|
| 436 |
+
class Classifier(nn.Module):
|
| 437 |
+
def __init__(self):
|
| 438 |
+
super().__init__()
|
| 439 |
+
self.net = nn.Sequential(
|
| 440 |
+
nn.Linear(784, {hidden}),
|
| 441 |
+
nn.ReLU(),
|
| 442 |
+
nn.Linear({hidden}, {num_classes}),
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
def forward(self, x):
|
| 446 |
+
return self.net(x)
|
| 447 |
+
|
| 448 |
+
X = torch.randn(200, 784)
|
| 449 |
+
y = torch.randint(0, {num_classes}, (200,))
|
| 450 |
+
dataset = TensorDataset(X, y)
|
| 451 |
+
loader = DataLoader(dataset, batch_size=32, shuffle=True)
|
| 452 |
+
|
| 453 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 454 |
+
model = Classifier().to(device)
|
| 455 |
+
optimizer = optim.Adam(model.parameters(), lr=1e-3)
|
| 456 |
+
criterion = nn.CrossEntropyLoss()
|
| 457 |
+
|
| 458 |
+
for epoch in range(3):
|
| 459 |
+
for xb, yb in loader:
|
| 460 |
+
optimizer.zero_grad()
|
| 461 |
+
pred = model(xb)
|
| 462 |
+
loss = criterion(pred, yb)
|
| 463 |
+
loss.backward()
|
| 464 |
+
optimizer.step()
|
| 465 |
+
print(f"Epoch {{epoch+1}} complete")
|
| 466 |
+
|
| 467 |
+
print("Training finished")
|
| 468 |
+
'''
|
| 469 |
+
|
| 470 |
+
error_output = (
|
| 471 |
+
"Traceback (most recent call last):\n"
|
| 472 |
+
" File \"train.py\", line 30, in <module>\n"
|
| 473 |
+
" pred = model(xb)\n"
|
| 474 |
+
" File \".../torch/nn/modules/linear.py\", in forward\n"
|
| 475 |
+
" return F.linear(input, self.weight, self.bias)\n"
|
| 476 |
+
"RuntimeError: Expected all tensors to be on the same device, "
|
| 477 |
+
"but found at least two devices, cuda:0 and cpu!\n\n"
|
| 478 |
+
"The model was moved to GPU but the data batches remain on CPU. "
|
| 479 |
+
"Every forward pass crashes with a device mismatch error."
|
| 480 |
+
)
|
| 481 |
+
|
| 482 |
+
return BugScenario(
|
| 483 |
+
task_id=TASK_WRONG_DEVICE,
|
| 484 |
+
task_description=(
|
| 485 |
+
"This PyTorch training script crashes on the first forward pass with a device mismatch error. "
|
| 486 |
+
"The model and data tensors are on different devices. "
|
| 487 |
+
"Fix the script so training runs for 3 epochs without error on whatever device is available."
|
| 488 |
+
),
|
| 489 |
+
buggy_code=buggy_code,
|
| 490 |
+
error_output=error_output,
|
| 491 |
+
correct_bug_type="wrong_device",
|
| 492 |
+
solution_hint="move xb and yb to device inside the training loop: xb, yb = xb.to(device), yb.to(device)",
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 497 |
+
# TASK 5 β Gradient Not Zeroed (Medium-Hard)
|
| 498 |
+
# optimizer.zero_grad() is missing. Gradients accumulate across
|
| 499 |
+
# batches, loss behaves erratically, model fails to converge.
|
| 500 |
+
# No crash. Agent must reason about the training loop structure.
|
| 501 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 502 |
+
|
| 503 |
+
def _gradient_not_zeroed_scenario(rng: random.Random) -> BugScenario:
|
| 504 |
+
hidden = rng.choice([32, 64, 128])
|
| 505 |
+
lr = rng.choice([1e-3, 5e-4])
|
| 506 |
+
|
| 507 |
+
buggy_code = f'''import torch
|
| 508 |
+
import torch.nn as nn
|
| 509 |
+
import torch.optim as optim
|
| 510 |
+
from torch.utils.data import DataLoader, TensorDataset
|
| 511 |
+
|
| 512 |
+
torch.manual_seed(42)
|
| 513 |
+
|
| 514 |
+
class MLP(nn.Module):
|
| 515 |
+
def __init__(self):
|
| 516 |
+
super().__init__()
|
| 517 |
+
self.net = nn.Sequential(
|
| 518 |
+
nn.Linear(10, {hidden}),
|
| 519 |
+
nn.ReLU(),
|
| 520 |
+
nn.Linear({hidden}, {hidden}),
|
| 521 |
+
nn.ReLU(),
|
| 522 |
+
nn.Linear({hidden}, 1),
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
def forward(self, x):
|
| 526 |
+
return self.net(x).squeeze(-1)
|
| 527 |
+
|
| 528 |
+
X = torch.randn(500, 10)
|
| 529 |
+
y = X[:, 0] * 1.5 - X[:, 2] * 0.8 + torch.randn(500) * 0.2
|
| 530 |
+
dataset = TensorDataset(X, y)
|
| 531 |
+
loader = DataLoader(dataset, batch_size=32, shuffle=True)
|
| 532 |
+
|
| 533 |
+
model = MLP()
|
| 534 |
+
optimizer = optim.Adam(model.parameters(), lr={lr})
|
| 535 |
+
criterion = nn.MSELoss()
|
| 536 |
+
|
| 537 |
+
for epoch in range(6):
|
| 538 |
+
epoch_loss = 0.0
|
| 539 |
+
for xb, yb in loader:
|
| 540 |
+
pred = model(xb)
|
| 541 |
+
loss = criterion(pred, yb)
|
| 542 |
+
loss.backward()
|
| 543 |
+
optimizer.step()
|
| 544 |
+
epoch_loss += loss.item()
|
| 545 |
+
avg = epoch_loss / len(loader)
|
| 546 |
+
print(f"Epoch {{epoch+1}}, loss: {{avg:.4f}}")
|
| 547 |
+
|
| 548 |
+
print("Training finished")
|
| 549 |
+
'''
|
| 550 |
+
|
| 551 |
+
error_output = (
|
| 552 |
+
"Script runs without crashing but training is highly unstable.\n"
|
| 553 |
+
"Output observed:\n"
|
| 554 |
+
" Epoch 1, loss: 12.4821\n"
|
| 555 |
+
" Epoch 2, loss: 847.2341\n"
|
| 556 |
+
" Epoch 3, loss: 23451.8821\n"
|
| 557 |
+
" Epoch 4, loss: nan\n"
|
| 558 |
+
" Epoch 5, loss: nan\n"
|
| 559 |
+
" Epoch 6, loss: nan\n"
|
| 560 |
+
"Loss explodes dramatically after the first epoch and collapses to NaN. "
|
| 561 |
+
"No crash occurs. The model never converges. "
|
| 562 |
+
"There is a fundamental error in the training loop structure."
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
return BugScenario(
|
| 566 |
+
task_id=TASK_GRADIENT_NOT_ZEROED,
|
| 567 |
+
task_description=(
|
| 568 |
+
"This PyTorch training script runs without crashing but loss explodes "
|
| 569 |
+
"after the first epoch and collapses to NaN. The model never learns anything. "
|
| 570 |
+
"Find the training loop bug and fix it so loss decreases consistently across 6 epochs."
|
| 571 |
+
),
|
| 572 |
+
buggy_code=buggy_code,
|
| 573 |
+
error_output=error_output,
|
| 574 |
+
correct_bug_type="gradient_not_zeroed",
|
| 575 |
+
solution_hint="optimizer.zero_grad() is missing before loss.backward(); gradients accumulate across batches causing explosion",
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
|
| 579 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 580 |
+
# TASK 6 β Missing Eval Mode (Hard)
|
| 581 |
+
# model.eval() and torch.no_grad() missing during evaluation.
|
| 582 |
+
# Dropout active, BatchNorm uses batch stats not running stats.
|
| 583 |
+
# Everything runs. Metrics are noisy and unreliable. No crash.
|
| 584 |
+
# Agent must understand train vs eval mode semantics.
|
| 585 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 586 |
+
|
| 587 |
+
def _missing_eval_mode_scenario(rng: random.Random) -> BugScenario:
|
| 588 |
+
dropout_p = rng.choice([0.3, 0.4, 0.5])
|
| 589 |
+
hidden = rng.choice([64, 128])
|
| 590 |
+
num_classes = rng.choice([3, 5])
|
| 591 |
+
|
| 592 |
+
buggy_code = f'''import torch
|
| 593 |
+
import torch.nn as nn
|
| 594 |
+
import torch.optim as optim
|
| 595 |
+
from torch.utils.data import DataLoader, TensorDataset
|
| 596 |
+
|
| 597 |
+
torch.manual_seed(42)
|
| 598 |
+
|
| 599 |
+
class DropoutClassifier(nn.Module):
|
| 600 |
+
def __init__(self, input_dim, num_classes):
|
| 601 |
+
super().__init__()
|
| 602 |
+
self.net = nn.Sequential(
|
| 603 |
+
nn.Linear(input_dim, {hidden}),
|
| 604 |
+
nn.BatchNorm1d({hidden}),
|
| 605 |
+
nn.ReLU(),
|
| 606 |
+
nn.Dropout(p={dropout_p}),
|
| 607 |
+
nn.Linear({hidden}, {hidden}),
|
| 608 |
+
nn.BatchNorm1d({hidden}),
|
| 609 |
+
nn.ReLU(),
|
| 610 |
+
nn.Dropout(p={dropout_p}),
|
| 611 |
+
nn.Linear({hidden}, num_classes),
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
def forward(self, x):
|
| 615 |
+
return self.net(x)
|
| 616 |
+
|
| 617 |
+
torch.manual_seed(42)
|
| 618 |
+
N, D, C = 800, 20, {num_classes}
|
| 619 |
+
X = torch.randn(N, D)
|
| 620 |
+
true_w = torch.randn(D, C)
|
| 621 |
+
y = (X @ true_w).argmax(dim=1)
|
| 622 |
+
|
| 623 |
+
split = int(0.8 * N)
|
| 624 |
+
X_train, X_test = X[:split], X[split:]
|
| 625 |
+
y_train, y_test = y[:split], y[split:]
|
| 626 |
+
|
| 627 |
+
train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=32, shuffle=True)
|
| 628 |
+
|
| 629 |
+
model = DropoutClassifier(D, C)
|
| 630 |
+
optimizer = optim.Adam(model.parameters(), lr=1e-3)
|
| 631 |
+
criterion = nn.CrossEntropyLoss()
|
| 632 |
+
|
| 633 |
+
for epoch in range(10):
|
| 634 |
+
model.train()
|
| 635 |
+
for xb, yb in train_loader:
|
| 636 |
+
optimizer.zero_grad()
|
| 637 |
+
loss = criterion(model(xb), yb)
|
| 638 |
+
loss.backward()
|
| 639 |
+
optimizer.step()
|
| 640 |
+
print(f"Epoch {{epoch+1}} complete")
|
| 641 |
+
|
| 642 |
+
preds = model(X_test).argmax(dim=1)
|
| 643 |
+
accuracy = (preds == y_test).float().mean().item()
|
| 644 |
+
print(f"Test accuracy: {{accuracy:.4f}}")
|
| 645 |
+
print("Evaluation complete")
|
| 646 |
+
print("Training finished")
|
| 647 |
+
'''
|
| 648 |
+
|
| 649 |
+
error_output = (
|
| 650 |
+
"Script runs to completion with no errors.\n"
|
| 651 |
+
f"Reported test accuracy varies between runs: 0.51, 0.58, 0.49, 0.61\n"
|
| 652 |
+
"\n"
|
| 653 |
+
f"The model has Dropout(p={dropout_p}) and BatchNorm layers. "
|
| 654 |
+
"Test accuracy is inconsistent and significantly lower than expected. "
|
| 655 |
+
"Running the same script multiple times gives different accuracy values each time. "
|
| 656 |
+
"The evaluation is unreliable. No exception is raised. "
|
| 657 |
+
"The model appears to be in the wrong mode during evaluation."
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
return BugScenario(
|
| 661 |
+
task_id=TASK_MISSING_EVAL_MODE,
|
| 662 |
+
task_description=(
|
| 663 |
+
"This PyTorch classifier trains successfully but produces unreliable and "
|
| 664 |
+
"inconsistent test accuracy. Running evaluation multiple times gives different results. "
|
| 665 |
+
"The model has Dropout and BatchNorm layers. "
|
| 666 |
+
"Fix the evaluation so it produces stable, correct metrics."
|
| 667 |
+
),
|
| 668 |
+
buggy_code=buggy_code,
|
| 669 |
+
error_output=error_output,
|
| 670 |
+
correct_bug_type="missing_eval_mode",
|
| 671 |
+
solution_hint=f"model.eval() and torch.no_grad() must be called before evaluation; dropout p={dropout_p} stays active in train mode causing stochastic predictions",
|
| 672 |
)
|
server/grader.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# server/grader.py
|
| 2 |
import subprocess
|
| 3 |
import sys
|
| 4 |
import tempfile
|
|
@@ -12,23 +11,12 @@ from bug_generator import BugScenario
|
|
| 12 |
|
| 13 |
@dataclass
|
| 14 |
class GradeResult:
|
| 15 |
-
score: float
|
| 16 |
feedback: str
|
| 17 |
-
execution_output: str
|
| 18 |
|
| 19 |
|
| 20 |
def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario: BugScenario) -> GradeResult:
|
| 21 |
-
"""
|
| 22 |
-
Score the agent's fix attempt. Partial credit at every stage:
|
| 23 |
-
|
| 24 |
-
0.0 β wrong bug type identified (agent is confused about what's wrong)
|
| 25 |
-
0.2 β correct bug type, but code fails to run (syntax error, import error, crash)
|
| 26 |
-
0.4 β correct bug type + code runs, but training does not complete
|
| 27 |
-
0.6 β correct bug type + code runs + training completes + no NaN loss
|
| 28 |
-
0.8 β all above + fix addresses the actual root cause (verified by heuristic)
|
| 29 |
-
1.0 β all above + output shows the expected successful signal
|
| 30 |
-
"""
|
| 31 |
-
# Stage 1: bug type identification
|
| 32 |
type_correct = _check_bug_type(action_bug_type, scenario.correct_bug_type)
|
| 33 |
if not type_correct:
|
| 34 |
return GradeResult(
|
|
@@ -41,7 +29,6 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
|
|
| 41 |
execution_output="(code not executed β bug type was wrong)",
|
| 42 |
)
|
| 43 |
|
| 44 |
-
# Stage 2: run the fixed code
|
| 45 |
exec_output, ran_ok = _run_code(fixed_code, timeout=30)
|
| 46 |
|
| 47 |
if not ran_ok:
|
|
@@ -54,8 +41,7 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
|
|
| 54 |
execution_output=exec_output,
|
| 55 |
)
|
| 56 |
|
| 57 |
-
|
| 58 |
-
completed = _check_training_completed(exec_output)
|
| 59 |
if not completed:
|
| 60 |
return GradeResult(
|
| 61 |
score=0.4,
|
|
@@ -66,7 +52,6 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
|
|
| 66 |
execution_output=exec_output,
|
| 67 |
)
|
| 68 |
|
| 69 |
-
# Stage 4: check fix actually addresses root cause
|
| 70 |
fix_valid, fix_feedback = _verify_fix(fixed_code, scenario, exec_output)
|
| 71 |
if not fix_valid:
|
| 72 |
return GradeResult(
|
|
@@ -79,7 +64,6 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
|
|
| 79 |
execution_output=exec_output,
|
| 80 |
)
|
| 81 |
|
| 82 |
-
# Stage 5: check for success signal in output
|
| 83 |
success, success_feedback = _check_success_signal(scenario.task_id, exec_output)
|
| 84 |
if not success:
|
| 85 |
return GradeResult(
|
|
@@ -107,12 +91,20 @@ def _check_bug_type(submitted: str, correct: str) -> bool:
|
|
| 107 |
submitted_clean = submitted.strip().lower().replace(" ", "_").replace("-", "_")
|
| 108 |
correct_clean = correct.strip().lower()
|
| 109 |
aliases = {
|
| 110 |
-
"shape_mismatch": {"shape_mismatch", "shape", "dimension", "size_mismatch", "linear", "matmul"},
|
| 111 |
"training_collapse": {"training_collapse", "collapse", "nan", "diverge", "learning_rate", "loss_fn", "loss_function", "wrong_loss"},
|
| 112 |
"data_leakage": {"data_leakage", "leakage", "leak", "train_test_leak", "normalization", "preprocessing"},
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
valid = aliases.get(correct_clean, {correct_clean})
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
|
| 118 |
def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
|
|
@@ -120,15 +112,13 @@ def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
|
|
| 120 |
f.write(code)
|
| 121 |
tmp_path = f.name
|
| 122 |
|
| 123 |
-
# Use PYTHON_EXEC env var if set (Docker), else find venv python, else fall back
|
| 124 |
python_exe = os.environ.get("PYTHON_EXEC")
|
| 125 |
if not python_exe:
|
| 126 |
-
# Walk up from server/ to find .venv
|
| 127 |
server_dir = os.path.dirname(os.path.abspath(__file__))
|
| 128 |
project_dir = os.path.dirname(server_dir)
|
| 129 |
-
candidate = os.path.join(project_dir, ".venv", "Scripts", "python.exe")
|
| 130 |
if not os.path.exists(candidate):
|
| 131 |
-
candidate = os.path.join(project_dir, ".venv", "bin", "python")
|
| 132 |
python_exe = candidate if os.path.exists(candidate) else sys.executable
|
| 133 |
|
| 134 |
try:
|
|
@@ -153,18 +143,20 @@ def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
|
|
| 153 |
pass
|
| 154 |
|
| 155 |
|
| 156 |
-
def _check_training_completed(output: str) -> bool:
|
|
|
|
|
|
|
|
|
|
| 157 |
markers = [
|
| 158 |
"training finished",
|
| 159 |
"training complete",
|
| 160 |
-
"
|
| 161 |
"epoch 3",
|
|
|
|
|
|
|
| 162 |
"epoch 8",
|
| 163 |
"epoch 10",
|
| 164 |
]
|
| 165 |
-
lower = output.lower()
|
| 166 |
-
if "nan" in lower and "loss" in lower:
|
| 167 |
-
return False
|
| 168 |
return any(m in lower for m in markers)
|
| 169 |
|
| 170 |
|
|
@@ -173,15 +165,18 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
|
|
| 173 |
|
| 174 |
if task == "shape_mismatch":
|
| 175 |
hint = scenario.solution_hint
|
| 176 |
-
# hint format: "classifier input must be {hidden_size} not {wrong_size}"
|
| 177 |
match = re.search(r"must be (\d+) not (\d+)", hint)
|
| 178 |
if match:
|
| 179 |
-
correct_size = match.group(1)
|
| 180 |
wrong_size = match.group(2)
|
| 181 |
-
#
|
| 182 |
-
|
| 183 |
-
if
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
return True, ""
|
| 186 |
|
| 187 |
elif task == "training_collapse":
|
|
@@ -190,7 +185,6 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
|
|
| 190 |
return False, "Loss is still NaN in the fixed code output."
|
| 191 |
hint = scenario.solution_hint
|
| 192 |
if "learning rate" in hint or "lr" in hint.lower():
|
| 193 |
-
# check no absurdly large lr remains
|
| 194 |
lr_matches = re.findall(r"lr\s*=\s*([\d.e\-+]+)", fixed_code)
|
| 195 |
for lr_str in lr_matches:
|
| 196 |
try:
|
|
@@ -202,19 +196,6 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
|
|
| 202 |
return True, ""
|
| 203 |
|
| 204 |
elif task == "data_leakage":
|
| 205 |
-
# The fix must compute mean/std only from training data, not full X_raw
|
| 206 |
-
# Look for the key pattern: mean computed before split (bad) vs after (good)
|
| 207 |
-
lines = fixed_code.split("\n")
|
| 208 |
-
split_line_idx = None
|
| 209 |
-
mean_line_idx = None
|
| 210 |
-
for i, line in enumerate(lines):
|
| 211 |
-
stripped = line.strip()
|
| 212 |
-
if "split" in stripped and ("=" in stripped or "int(" in stripped):
|
| 213 |
-
split_line_idx = i
|
| 214 |
-
if re.search(r"(mean|\.mean\()", stripped) and "train" in stripped.lower():
|
| 215 |
-
mean_line_idx = i
|
| 216 |
-
|
| 217 |
-
# If mean is computed on something called x_raw before split, that's still wrong
|
| 218 |
bad_patterns = [
|
| 219 |
r"mean\s*=\s*[Xx]_raw\.mean",
|
| 220 |
r"full_mean\s*=\s*[Xx]_raw\.mean",
|
|
@@ -223,8 +204,6 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
|
|
| 223 |
for pat in bad_patterns:
|
| 224 |
if re.search(pat, fixed_code):
|
| 225 |
return False, "Normalization statistics still computed from full dataset before split."
|
| 226 |
-
|
| 227 |
-
# Good: should see train mean/std
|
| 228 |
good_patterns = [
|
| 229 |
r"train.*mean",
|
| 230 |
r"mean.*train",
|
|
@@ -236,6 +215,39 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
|
|
| 236 |
return False, "Could not confirm that normalization uses only training data statistics."
|
| 237 |
return True, ""
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
return True, ""
|
| 240 |
|
| 241 |
|
|
@@ -252,7 +264,6 @@ def _check_success_signal(task_id: str, output: str) -> tuple[bool, str]:
|
|
| 252 |
elif task_id == "training_collapse":
|
| 253 |
if "nan" in lower:
|
| 254 |
return False, "Output still contains NaN loss values."
|
| 255 |
-
# Check loss is decreasing: find all loss values in output
|
| 256 |
loss_values = re.findall(r"loss[:\s]+([\d.]+)", lower)
|
| 257 |
if len(loss_values) >= 2:
|
| 258 |
first, last = float(loss_values[0]), float(loss_values[-1])
|
|
@@ -263,8 +274,6 @@ def _check_success_signal(task_id: str, output: str) -> tuple[bool, str]:
|
|
| 263 |
return has_finished, "Could not confirm loss decreased across epochs."
|
| 264 |
|
| 265 |
elif task_id == "data_leakage":
|
| 266 |
-
# The fixed version should NOT show suspiciously high accuracy or very low loss
|
| 267 |
-
# After fixing leakage, accuracy drops or MSE rises β that's the correct behaviour
|
| 268 |
acc_match = re.search(r"accuracy[:\s]+([\d.]+)", lower)
|
| 269 |
if acc_match:
|
| 270 |
acc = float(acc_match.group(1))
|
|
@@ -278,4 +287,31 @@ def _check_success_signal(task_id: str, output: str) -> tuple[bool, str]:
|
|
| 278 |
has_finished = "training finished" in lower
|
| 279 |
return has_finished, "Training did not complete."
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
return True, ""
|
|
|
|
|
|
|
| 1 |
import subprocess
|
| 2 |
import sys
|
| 3 |
import tempfile
|
|
|
|
| 11 |
|
| 12 |
@dataclass
|
| 13 |
class GradeResult:
|
| 14 |
+
score: float
|
| 15 |
feedback: str
|
| 16 |
+
execution_output: str
|
| 17 |
|
| 18 |
|
| 19 |
def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario: BugScenario) -> GradeResult:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
type_correct = _check_bug_type(action_bug_type, scenario.correct_bug_type)
|
| 21 |
if not type_correct:
|
| 22 |
return GradeResult(
|
|
|
|
| 29 |
execution_output="(code not executed β bug type was wrong)",
|
| 30 |
)
|
| 31 |
|
|
|
|
| 32 |
exec_output, ran_ok = _run_code(fixed_code, timeout=30)
|
| 33 |
|
| 34 |
if not ran_ok:
|
|
|
|
| 41 |
execution_output=exec_output,
|
| 42 |
)
|
| 43 |
|
| 44 |
+
completed = _check_training_completed(exec_output, scenario.task_id)
|
|
|
|
| 45 |
if not completed:
|
| 46 |
return GradeResult(
|
| 47 |
score=0.4,
|
|
|
|
| 52 |
execution_output=exec_output,
|
| 53 |
)
|
| 54 |
|
|
|
|
| 55 |
fix_valid, fix_feedback = _verify_fix(fixed_code, scenario, exec_output)
|
| 56 |
if not fix_valid:
|
| 57 |
return GradeResult(
|
|
|
|
| 64 |
execution_output=exec_output,
|
| 65 |
)
|
| 66 |
|
|
|
|
| 67 |
success, success_feedback = _check_success_signal(scenario.task_id, exec_output)
|
| 68 |
if not success:
|
| 69 |
return GradeResult(
|
|
|
|
| 91 |
submitted_clean = submitted.strip().lower().replace(" ", "_").replace("-", "_")
|
| 92 |
correct_clean = correct.strip().lower()
|
| 93 |
aliases = {
|
| 94 |
+
"shape_mismatch": {"shape_mismatch", "shape", "dimension", "size_mismatch", "linear", "matmul", "incompatible", "input_shape", "classifier"},
|
| 95 |
"training_collapse": {"training_collapse", "collapse", "nan", "diverge", "learning_rate", "loss_fn", "loss_function", "wrong_loss"},
|
| 96 |
"data_leakage": {"data_leakage", "leakage", "leak", "train_test_leak", "normalization", "preprocessing"},
|
| 97 |
+
"wrong_device": {"wrong_device", "device", "device_mismatch", "cuda", "cpu", "device_error"},
|
| 98 |
+
"gradient_not_zeroed": {"gradient_not_zeroed", "gradient", "zero_grad", "missing_zero_grad", "accumulate", "gradient_accumulation"},
|
| 99 |
+
"missing_eval_mode": {"missing_eval_mode", "eval_mode", "eval", "dropout", "batchnorm", "no_grad", "inference_mode"},
|
| 100 |
}
|
| 101 |
valid = aliases.get(correct_clean, {correct_clean})
|
| 102 |
+
if submitted_clean in valid:
|
| 103 |
+
return True
|
| 104 |
+
for alias in valid:
|
| 105 |
+
if alias in submitted_clean:
|
| 106 |
+
return True
|
| 107 |
+
return False
|
| 108 |
|
| 109 |
|
| 110 |
def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
|
|
|
|
| 112 |
f.write(code)
|
| 113 |
tmp_path = f.name
|
| 114 |
|
|
|
|
| 115 |
python_exe = os.environ.get("PYTHON_EXEC")
|
| 116 |
if not python_exe:
|
|
|
|
| 117 |
server_dir = os.path.dirname(os.path.abspath(__file__))
|
| 118 |
project_dir = os.path.dirname(server_dir)
|
| 119 |
+
candidate = os.path.join(project_dir, ".venv", "Scripts", "python.exe")
|
| 120 |
if not os.path.exists(candidate):
|
| 121 |
+
candidate = os.path.join(project_dir, ".venv", "bin", "python")
|
| 122 |
python_exe = candidate if os.path.exists(candidate) else sys.executable
|
| 123 |
|
| 124 |
try:
|
|
|
|
| 143 |
pass
|
| 144 |
|
| 145 |
|
| 146 |
+
def _check_training_completed(output: str, task_id: str) -> bool:
|
| 147 |
+
lower = output.lower()
|
| 148 |
+
if "nan" in lower and "loss" in lower:
|
| 149 |
+
return False
|
| 150 |
markers = [
|
| 151 |
"training finished",
|
| 152 |
"training complete",
|
| 153 |
+
"evaluation complete",
|
| 154 |
"epoch 3",
|
| 155 |
+
"epoch 5",
|
| 156 |
+
"epoch 6",
|
| 157 |
"epoch 8",
|
| 158 |
"epoch 10",
|
| 159 |
]
|
|
|
|
|
|
|
|
|
|
| 160 |
return any(m in lower for m in markers)
|
| 161 |
|
| 162 |
|
|
|
|
| 165 |
|
| 166 |
if task == "shape_mismatch":
|
| 167 |
hint = scenario.solution_hint
|
|
|
|
| 168 |
match = re.search(r"must be (\d+) not (\d+)", hint)
|
| 169 |
if match:
|
|
|
|
| 170 |
wrong_size = match.group(2)
|
| 171 |
+
# Check encoder final output matches classifier input
|
| 172 |
+
# Just verify code runs without shape error β already confirmed in stage 2/3
|
| 173 |
+
# Only fail if the EXACT original wrong Linear is unchanged
|
| 174 |
+
original_wrong = f"nn.Linear({wrong_size}, "
|
| 175 |
+
lines = fixed_code.split("\n")
|
| 176 |
+
classifier_lines = [l for l in lines if "classifier" in l.lower() and "nn.Linear" in l]
|
| 177 |
+
encoder_lines = [l for l in lines if "encoder" not in l.lower() and "nn.Linear" in l and "classifier" not in l.lower()]
|
| 178 |
+
# If code runs and trains (already verified), the shape is fixed
|
| 179 |
+
return True, ""
|
| 180 |
return True, ""
|
| 181 |
|
| 182 |
elif task == "training_collapse":
|
|
|
|
| 185 |
return False, "Loss is still NaN in the fixed code output."
|
| 186 |
hint = scenario.solution_hint
|
| 187 |
if "learning rate" in hint or "lr" in hint.lower():
|
|
|
|
| 188 |
lr_matches = re.findall(r"lr\s*=\s*([\d.e\-+]+)", fixed_code)
|
| 189 |
for lr_str in lr_matches:
|
| 190 |
try:
|
|
|
|
| 196 |
return True, ""
|
| 197 |
|
| 198 |
elif task == "data_leakage":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
bad_patterns = [
|
| 200 |
r"mean\s*=\s*[Xx]_raw\.mean",
|
| 201 |
r"full_mean\s*=\s*[Xx]_raw\.mean",
|
|
|
|
| 204 |
for pat in bad_patterns:
|
| 205 |
if re.search(pat, fixed_code):
|
| 206 |
return False, "Normalization statistics still computed from full dataset before split."
|
|
|
|
|
|
|
| 207 |
good_patterns = [
|
| 208 |
r"train.*mean",
|
| 209 |
r"mean.*train",
|
|
|
|
| 215 |
return False, "Could not confirm that normalization uses only training data statistics."
|
| 216 |
return True, ""
|
| 217 |
|
| 218 |
+
elif task == "wrong_device":
|
| 219 |
+
# Fixed code must move data to device inside the loop
|
| 220 |
+
good_patterns = [
|
| 221 |
+
r"xb\s*=\s*xb\.to\(device\)",
|
| 222 |
+
r"xb\.to\(device\)",
|
| 223 |
+
r"\.to\(device\)",
|
| 224 |
+
]
|
| 225 |
+
has_device_move = any(re.search(p, fixed_code) for p in good_patterns)
|
| 226 |
+
if not has_device_move:
|
| 227 |
+
return False, "Data tensors are not being moved to device inside the training loop."
|
| 228 |
+
# Must not crash with device error
|
| 229 |
+
if "expected all tensors" in exec_output.lower():
|
| 230 |
+
return False, "Device mismatch error still present in output."
|
| 231 |
+
return True, ""
|
| 232 |
+
|
| 233 |
+
elif task == "gradient_not_zeroed":
|
| 234 |
+
# Fixed code must have zero_grad before backward
|
| 235 |
+
if "optimizer.zero_grad()" not in fixed_code and "optim.zero_grad()" not in fixed_code:
|
| 236 |
+
return False, "optimizer.zero_grad() is still missing from the training loop."
|
| 237 |
+
lower_output = exec_output.lower()
|
| 238 |
+
if "nan" in lower_output and "loss" in lower_output:
|
| 239 |
+
return False, "Loss is still NaN β gradients may still be accumulating."
|
| 240 |
+
return True, ""
|
| 241 |
+
|
| 242 |
+
elif task == "missing_eval_mode":
|
| 243 |
+
# Fixed code must have model.eval() before evaluation
|
| 244 |
+
if "model.eval()" not in fixed_code:
|
| 245 |
+
return False, "model.eval() is missing before the evaluation block."
|
| 246 |
+
# Should also have no_grad
|
| 247 |
+
if "torch.no_grad()" not in fixed_code and "no_grad" not in fixed_code:
|
| 248 |
+
return False, "torch.no_grad() context manager is missing during evaluation."
|
| 249 |
+
return True, ""
|
| 250 |
+
|
| 251 |
return True, ""
|
| 252 |
|
| 253 |
|
|
|
|
| 264 |
elif task_id == "training_collapse":
|
| 265 |
if "nan" in lower:
|
| 266 |
return False, "Output still contains NaN loss values."
|
|
|
|
| 267 |
loss_values = re.findall(r"loss[:\s]+([\d.]+)", lower)
|
| 268 |
if len(loss_values) >= 2:
|
| 269 |
first, last = float(loss_values[0]), float(loss_values[-1])
|
|
|
|
| 274 |
return has_finished, "Could not confirm loss decreased across epochs."
|
| 275 |
|
| 276 |
elif task_id == "data_leakage":
|
|
|
|
|
|
|
| 277 |
acc_match = re.search(r"accuracy[:\s]+([\d.]+)", lower)
|
| 278 |
if acc_match:
|
| 279 |
acc = float(acc_match.group(1))
|
|
|
|
| 287 |
has_finished = "training finished" in lower
|
| 288 |
return has_finished, "Training did not complete."
|
| 289 |
|
| 290 |
+
elif task_id == "wrong_device":
|
| 291 |
+
has_epoch = any(f"epoch {i}" in lower for i in range(1, 4))
|
| 292 |
+
has_finished = "training finished" in lower
|
| 293 |
+
no_device_error = "expected all tensors" not in lower
|
| 294 |
+
if has_epoch and has_finished and no_device_error:
|
| 295 |
+
return True, ""
|
| 296 |
+
return False, "Training did not complete cleanly or device error still present."
|
| 297 |
+
|
| 298 |
+
elif task_id == "gradient_not_zeroed":
|
| 299 |
+
if "nan" in lower:
|
| 300 |
+
return False, "Loss still diverges to NaN."
|
| 301 |
+
loss_values = re.findall(r"loss[:\s]+([\d.]+)", lower)
|
| 302 |
+
if len(loss_values) >= 2:
|
| 303 |
+
first, last = float(loss_values[0]), float(loss_values[-1])
|
| 304 |
+
if last < first * 0.9:
|
| 305 |
+
return True, ""
|
| 306 |
+
return False, f"Loss did not decrease sufficiently: {first:.4f} -> {last:.4f}."
|
| 307 |
+
has_finished = "training finished" in lower
|
| 308 |
+
return has_finished, "Could not confirm stable training."
|
| 309 |
+
|
| 310 |
+
elif task_id == "missing_eval_mode":
|
| 311 |
+
has_accuracy = "accuracy" in lower or "complete" in lower
|
| 312 |
+
has_finished = "training finished" in lower or "evaluation complete" in lower
|
| 313 |
+
if has_accuracy and has_finished:
|
| 314 |
+
return True, ""
|
| 315 |
+
return False, "Evaluation did not complete or accuracy not reported."
|
| 316 |
+
|
| 317 |
return True, ""
|
server/ml_debug_env_environment.py
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
-
# server/ml_debug_env_environment.py
|
| 2 |
import sys
|
| 3 |
import os
|
| 4 |
from uuid import uuid4
|
| 5 |
from typing import Optional
|
| 6 |
|
| 7 |
-
# server/ is the working dir inside Docker; add parent for models import
|
| 8 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 9 |
|
| 10 |
from openenv.core.env_server.interfaces import Environment
|
|
@@ -18,36 +16,37 @@ from bug_generator import (
|
|
| 18 |
TASK_SHAPE_MISMATCH,
|
| 19 |
TASK_TRAINING_COLLAPSE,
|
| 20 |
TASK_DATA_LEAKAGE,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
)
|
| 22 |
from grader import grade, GradeResult
|
| 23 |
|
| 24 |
-
TASK_ORDER = [TASK_SHAPE_MISMATCH, TASK_TRAINING_COLLAPSE, TASK_DATA_LEAKAGE]
|
| 25 |
MAX_STEPS = 3
|
| 26 |
|
| 27 |
|
| 28 |
class MlDebugEnvEnvironment(Environment):
|
| 29 |
"""
|
| 30 |
-
ML Debugging Environment.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
Episodes are single-step by default (one fix attempt = one episode).
|
| 43 |
-
MAX_STEPS=3 allows retry attempts with partial credit carried forward.
|
| 44 |
"""
|
| 45 |
|
| 46 |
SUPPORTS_CONCURRENT_SESSIONS = True
|
| 47 |
|
| 48 |
def __init__(self, task_id: Optional[str] = None):
|
| 49 |
super().__init__()
|
| 50 |
-
self._task_id: Optional[str] = task_id
|
| 51 |
self._current_scenario: Optional[BugScenario] = None
|
| 52 |
self._state = DebugState(
|
| 53 |
episode_id=None,
|
|
@@ -57,11 +56,7 @@ class MlDebugEnvEnvironment(Environment):
|
|
| 57 |
current_score=0.0,
|
| 58 |
attempts=0,
|
| 59 |
)
|
| 60 |
-
self._episode_count = 0
|
| 61 |
-
|
| 62 |
-
# ------------------------------------------------------------------ #
|
| 63 |
-
# reset() #
|
| 64 |
-
# ------------------------------------------------------------------ #
|
| 65 |
|
| 66 |
def reset(
|
| 67 |
self,
|
|
@@ -70,16 +65,6 @@ class MlDebugEnvEnvironment(Environment):
|
|
| 70 |
task_id: Optional[str] = None,
|
| 71 |
**kwargs,
|
| 72 |
) -> DebugObservation:
|
| 73 |
-
"""
|
| 74 |
-
Start a new episode. Returns the buggy script + error output.
|
| 75 |
-
|
| 76 |
-
Args:
|
| 77 |
-
seed: Random seed for reproducible scenario generation.
|
| 78 |
-
episode_id: Optional custom episode identifier.
|
| 79 |
-
task_id: Pin to a specific task ('shape_mismatch',
|
| 80 |
-
'training_collapse', 'data_leakage').
|
| 81 |
-
If None, rotates through tasks in order.
|
| 82 |
-
"""
|
| 83 |
active_task = task_id or self._task_id or self._next_task()
|
| 84 |
scenario = get_scenario(active_task, seed=seed)
|
| 85 |
|
|
@@ -106,24 +91,12 @@ class MlDebugEnvEnvironment(Environment):
|
|
| 106 |
reward=None,
|
| 107 |
)
|
| 108 |
|
| 109 |
-
# ------------------------------------------------------------------ #
|
| 110 |
-
# step() #
|
| 111 |
-
# ------------------------------------------------------------------ #
|
| 112 |
-
|
| 113 |
def step(
|
| 114 |
self,
|
| 115 |
action: DebugAction,
|
| 116 |
timeout_s: Optional[float] = None,
|
| 117 |
**kwargs,
|
| 118 |
) -> DebugObservation:
|
| 119 |
-
"""
|
| 120 |
-
Evaluate the agent's fix attempt and return a scored observation.
|
| 121 |
-
|
| 122 |
-
The agent must supply:
|
| 123 |
-
- bug_type : category of the bug
|
| 124 |
-
- diagnosis : plain-language explanation
|
| 125 |
-
- fixed_code : complete corrected Python script
|
| 126 |
-
"""
|
| 127 |
if self._current_scenario is None:
|
| 128 |
raise RuntimeError("Call reset() before step().")
|
| 129 |
|
|
@@ -137,12 +110,11 @@ class MlDebugEnvEnvironment(Environment):
|
|
| 137 |
scenario=self._current_scenario,
|
| 138 |
)
|
| 139 |
|
| 140 |
-
# Keep best score across retry attempts
|
| 141 |
if result.score > self._state.current_score:
|
| 142 |
self._state.current_score = result.score
|
| 143 |
|
| 144 |
done = (
|
| 145 |
-
result.score =
|
| 146 |
or self._state.step_count >= MAX_STEPS
|
| 147 |
)
|
| 148 |
|
|
@@ -159,21 +131,12 @@ class MlDebugEnvEnvironment(Environment):
|
|
| 159 |
reward=result.score,
|
| 160 |
)
|
| 161 |
|
| 162 |
-
# ------------------------------------------------------------------ #
|
| 163 |
-
# state property #
|
| 164 |
-
# ------------------------------------------------------------------ #
|
| 165 |
-
|
| 166 |
@property
|
| 167 |
def state(self) -> DebugState:
|
| 168 |
return self._state
|
| 169 |
|
| 170 |
-
# ------------------------------------------------------------------ #
|
| 171 |
-
# helpers #
|
| 172 |
-
# ------------------------------------------------------------------ #
|
| 173 |
-
|
| 174 |
def _next_task(self) -> str:
|
| 175 |
-
|
| 176 |
-
task = TASK_ORDER[self._episode_count % len(TASK_ORDER)]
|
| 177 |
self._episode_count += 1
|
| 178 |
return task
|
| 179 |
|
|
@@ -183,12 +146,13 @@ class MlDebugEnvEnvironment(Environment):
|
|
| 183 |
name="ML Debugging Environment",
|
| 184 |
description=(
|
| 185 |
"An RL environment where agents debug broken PyTorch training scripts. "
|
| 186 |
-
"
|
| 187 |
-
"training collapse (medium),
|
| 188 |
-
"
|
| 189 |
-
"
|
| 190 |
-
"
|
|
|
|
| 191 |
),
|
| 192 |
-
version="
|
| 193 |
author="ml-debug-env",
|
| 194 |
)
|
|
|
|
|
|
|
| 1 |
import sys
|
| 2 |
import os
|
| 3 |
from uuid import uuid4
|
| 4 |
from typing import Optional
|
| 5 |
|
|
|
|
| 6 |
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 7 |
|
| 8 |
from openenv.core.env_server.interfaces import Environment
|
|
|
|
| 16 |
TASK_SHAPE_MISMATCH,
|
| 17 |
TASK_TRAINING_COLLAPSE,
|
| 18 |
TASK_DATA_LEAKAGE,
|
| 19 |
+
TASK_WRONG_DEVICE,
|
| 20 |
+
TASK_GRADIENT_NOT_ZEROED,
|
| 21 |
+
TASK_MISSING_EVAL_MODE,
|
| 22 |
+
ALL_TASKS,
|
| 23 |
)
|
| 24 |
from grader import grade, GradeResult
|
| 25 |
|
|
|
|
| 26 |
MAX_STEPS = 3
|
| 27 |
|
| 28 |
|
| 29 |
class MlDebugEnvEnvironment(Environment):
|
| 30 |
"""
|
| 31 |
+
ML Debugging Environment β 6 tasks, easy β hard.
|
| 32 |
+
|
| 33 |
+
Tasks:
|
| 34 |
+
shape_mismatch (easy) β explicit crash, wrong linear layer size
|
| 35 |
+
training_collapse (medium) β NaN loss or wrong loss function
|
| 36 |
+
data_leakage (hard) β silent, evaluation is invalid
|
| 37 |
+
wrong_device (medium) β CPU/CUDA tensor mismatch, explicit crash
|
| 38 |
+
gradient_not_zeroed (medium-hard) β missing zero_grad, loss explodes
|
| 39 |
+
missing_eval_mode (hard) β no model.eval(), unreliable metrics
|
| 40 |
+
|
| 41 |
+
Episodes are single-step by default. MAX_STEPS=3 allows retry with
|
| 42 |
+
grader feedback fed back to the agent.
|
|
|
|
|
|
|
| 43 |
"""
|
| 44 |
|
| 45 |
SUPPORTS_CONCURRENT_SESSIONS = True
|
| 46 |
|
| 47 |
def __init__(self, task_id: Optional[str] = None):
|
| 48 |
super().__init__()
|
| 49 |
+
self._task_id: Optional[str] = task_id
|
| 50 |
self._current_scenario: Optional[BugScenario] = None
|
| 51 |
self._state = DebugState(
|
| 52 |
episode_id=None,
|
|
|
|
| 56 |
current_score=0.0,
|
| 57 |
attempts=0,
|
| 58 |
)
|
| 59 |
+
self._episode_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
def reset(
|
| 62 |
self,
|
|
|
|
| 65 |
task_id: Optional[str] = None,
|
| 66 |
**kwargs,
|
| 67 |
) -> DebugObservation:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
active_task = task_id or self._task_id or self._next_task()
|
| 69 |
scenario = get_scenario(active_task, seed=seed)
|
| 70 |
|
|
|
|
| 91 |
reward=None,
|
| 92 |
)
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
def step(
|
| 95 |
self,
|
| 96 |
action: DebugAction,
|
| 97 |
timeout_s: Optional[float] = None,
|
| 98 |
**kwargs,
|
| 99 |
) -> DebugObservation:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
if self._current_scenario is None:
|
| 101 |
raise RuntimeError("Call reset() before step().")
|
| 102 |
|
|
|
|
| 110 |
scenario=self._current_scenario,
|
| 111 |
)
|
| 112 |
|
|
|
|
| 113 |
if result.score > self._state.current_score:
|
| 114 |
self._state.current_score = result.score
|
| 115 |
|
| 116 |
done = (
|
| 117 |
+
result.score >= 0.95
|
| 118 |
or self._state.step_count >= MAX_STEPS
|
| 119 |
)
|
| 120 |
|
|
|
|
| 131 |
reward=result.score,
|
| 132 |
)
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
@property
|
| 135 |
def state(self) -> DebugState:
|
| 136 |
return self._state
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
def _next_task(self) -> str:
|
| 139 |
+
task = ALL_TASKS[self._episode_count % len(ALL_TASKS)]
|
|
|
|
| 140 |
self._episode_count += 1
|
| 141 |
return task
|
| 142 |
|
|
|
|
| 146 |
name="ML Debugging Environment",
|
| 147 |
description=(
|
| 148 |
"An RL environment where agents debug broken PyTorch training scripts. "
|
| 149 |
+
"Six tasks of increasing difficulty: shape mismatch (easy), "
|
| 150 |
+
"training collapse (medium), wrong device (medium), "
|
| 151 |
+
"gradient not zeroed (medium-hard), data leakage (hard), "
|
| 152 |
+
"and missing eval mode (hard). "
|
| 153 |
+
"Agents receive a buggy script and must return a corrected version. "
|
| 154 |
+
"The grader executes the fix and scores 0.01β0.99 with partial credit."
|
| 155 |
),
|
| 156 |
+
version="2.0.0",
|
| 157 |
author="ml-debug-env",
|
| 158 |
)
|
test2.py
CHANGED
|
@@ -1,134 +1,80 @@
|
|
| 1 |
-
# test_submission.py
|
| 2 |
-
# Run this before every submission to catch validator failures early.
|
| 3 |
-
# Usage: python test_submission.py
|
| 4 |
-
|
| 5 |
import os
|
| 6 |
import sys
|
| 7 |
-
import json
|
| 8 |
-
import subprocess
|
| 9 |
-
import urllib.request
|
| 10 |
-
import urllib.error
|
| 11 |
-
import time
|
| 12 |
-
|
| 13 |
-
LOCAL_URL = "http://localhost:8000"
|
| 14 |
-
HF_URL = "https://rak2315-ml-debug-env.hf.space"
|
| 15 |
-
|
| 16 |
-
PASS = "\033[92m[PASS]\033[0m"
|
| 17 |
-
FAIL = "\033[91m[FAIL]\033[0m"
|
| 18 |
-
WARN = "\033[93m[WARN]\033[0m"
|
| 19 |
-
|
| 20 |
-
results = []
|
| 21 |
-
|
| 22 |
-
def check(name, passed, detail=""):
|
| 23 |
-
icon = PASS if passed else FAIL
|
| 24 |
-
print(f"{icon} {name}" + (f" β {detail}" if detail else ""))
|
| 25 |
-
results.append((name, passed))
|
| 26 |
-
|
| 27 |
-
# ββ 1. inference.py exists ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 28 |
-
check("inference.py exists at repo root", os.path.exists("inference.py"))
|
| 29 |
-
|
| 30 |
-
# ββ 2. baseline_inference.py reads API_BASE_URL / API_KEY ββββββββββββββββββββ
|
| 31 |
-
bi_path = os.path.join("server", "baseline_inference.py")
|
| 32 |
-
if os.path.exists(bi_path):
|
| 33 |
-
content = open(bi_path).read()
|
| 34 |
-
uses_api_base = "API_BASE_URL" in content
|
| 35 |
-
uses_api_key = "API_KEY" in content
|
| 36 |
-
check("baseline_inference.py uses API_BASE_URL", uses_api_base)
|
| 37 |
-
check("baseline_inference.py uses API_KEY", uses_api_key)
|
| 38 |
-
check("baseline_inference.py does NOT hardcode Groq URL only",
|
| 39 |
-
"API_BASE_URL" in content,
|
| 40 |
-
"must prefer injected base_url over hardcoded Groq")
|
| 41 |
-
else:
|
| 42 |
-
check("server/baseline_inference.py exists", False)
|
| 43 |
-
|
| 44 |
-
# ββ 3. inference.py tries localhost first βββββββββββββββββββββββββββββββββββββ
|
| 45 |
-
infer_content = open("inference.py").read() if os.path.exists("inference.py") else ""
|
| 46 |
-
localhost_pos = infer_content.find("localhost")
|
| 47 |
-
hf_pos = infer_content.find("hf.space")
|
| 48 |
-
if localhost_pos != -1 and hf_pos != -1:
|
| 49 |
-
check("inference.py tries localhost before HF Space", localhost_pos < hf_pos)
|
| 50 |
-
elif localhost_pos != -1:
|
| 51 |
-
check("inference.py tries localhost", True)
|
| 52 |
-
else:
|
| 53 |
-
check("inference.py tries localhost", False, "only HF Space URL found β validator can't reach it")
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
env["API_KEY"] = os.environ.get("API_KEY", "test-key")
|
| 60 |
|
| 61 |
-
|
| 62 |
-
[sys.executable, "inference.py"],
|
| 63 |
-
capture_output=True, text=True, timeout=60, env=env
|
| 64 |
-
)
|
| 65 |
-
stdout = proc.stdout
|
| 66 |
-
stderr = proc.stderr
|
| 67 |
|
| 68 |
-
print("
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
check("[END] found in stdout", "[END]" in stdout)
|
| 77 |
-
|
| 78 |
-
# Parse and validate blocks
|
| 79 |
-
tasks_found = []
|
| 80 |
-
for line in stdout.splitlines():
|
| 81 |
-
if line.startswith("[END]"):
|
| 82 |
-
parts = dict(p.split("=") for p in line[5:].strip().split() if "=" in p)
|
| 83 |
-
tasks_found.append(parts)
|
| 84 |
-
|
| 85 |
-
check("At least 3 [END] blocks found", len(tasks_found) >= 3,
|
| 86 |
-
f"found {len(tasks_found)}")
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
-
|
| 94 |
-
print("\nββ Checking local server ββ")
|
| 95 |
try:
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
except Exception as e:
|
| 100 |
-
|
| 101 |
-
|
|
|
|
| 102 |
|
|
|
|
| 103 |
try:
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
check("GET /tasks returns task list", isinstance(tasks, (list, dict)), str(tasks)[:80])
|
| 107 |
except Exception as e:
|
| 108 |
-
|
|
|
|
|
|
|
| 109 |
|
| 110 |
-
|
| 111 |
-
print("\nββ Checking /baseline endpoint ββ")
|
| 112 |
try:
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
| 122 |
except Exception as e:
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
-
|
| 127 |
-
print("\nββ SUMMARY ββββββββββββββββββββββββββββββ")
|
| 128 |
-
passed = sum(1 for _, p in results if p)
|
| 129 |
-
total = len(results)
|
| 130 |
-
print(f"{passed}/{total} checks passed")
|
| 131 |
-
if passed == total:
|
| 132 |
-
print(f"{PASS} Ready to submit!")
|
| 133 |
-
else:
|
| 134 |
-
print(f"{FAIL} Fix the above before submitting.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
os.environ['API_BASE_URL'] = 'https://api.groq.com/openai/v1'
|
| 5 |
+
os.environ['API_KEY'] = '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'
|
| 6 |
+
os.environ['MODEL_NAME'] = 'llama-3.3-70b-versatile'
|
| 7 |
+
os.environ['PYTHON_EXEC'] = r'C:\Users\rehtr\AppData\Local\Programs\Python\Python310\python.exe'
|
|
|
|
| 8 |
|
| 9 |
+
sys.path.insert(0, 'server')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
print("[1] Importing bug_generator...")
|
| 12 |
+
try:
|
| 13 |
+
from bug_generator import get_scenario, TASK_SHAPE_MISMATCH
|
| 14 |
+
print(" OK")
|
| 15 |
+
except Exception as e:
|
| 16 |
+
import traceback
|
| 17 |
+
traceback.print_exc()
|
| 18 |
+
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
print("[2] Getting scenario...")
|
| 21 |
+
try:
|
| 22 |
+
scenario = get_scenario(TASK_SHAPE_MISMATCH, seed=42)
|
| 23 |
+
print(" OK")
|
| 24 |
+
except Exception as e:
|
| 25 |
+
import traceback
|
| 26 |
+
traceback.print_exc()
|
| 27 |
+
sys.exit(1)
|
| 28 |
|
| 29 |
+
print("[3] Calling Groq via OpenAI client...")
|
|
|
|
| 30 |
try:
|
| 31 |
+
from openai import OpenAI
|
| 32 |
+
client = OpenAI(
|
| 33 |
+
api_key=os.environ['API_KEY'],
|
| 34 |
+
base_url=os.environ['API_BASE_URL']
|
| 35 |
+
)
|
| 36 |
+
r = client.chat.completions.create(
|
| 37 |
+
model='llama-3.3-70b-versatile',
|
| 38 |
+
messages=[
|
| 39 |
+
{"role": "system", "content": "You are an ML debugger. Respond only in JSON with keys: bug_type, diagnosis, fixed_code"},
|
| 40 |
+
{"role": "user", "content": f"Task: {scenario.task_description}\n\nCode:\n{scenario.buggy_code}\n\nError:\n{scenario.error_output}\n\nReturn JSON only."}
|
| 41 |
+
],
|
| 42 |
+
temperature=0.0,
|
| 43 |
+
max_tokens=2048,
|
| 44 |
+
response_format={"type": "json_object"},
|
| 45 |
+
)
|
| 46 |
+
print(" OK:", r.choices[0].message.content[:200])
|
| 47 |
except Exception as e:
|
| 48 |
+
import traceback
|
| 49 |
+
traceback.print_exc()
|
| 50 |
+
sys.exit(1)
|
| 51 |
|
| 52 |
+
print("[4] Importing grader...")
|
| 53 |
try:
|
| 54 |
+
from grader import grade
|
| 55 |
+
print(" OK")
|
|
|
|
| 56 |
except Exception as e:
|
| 57 |
+
import traceback
|
| 58 |
+
traceback.print_exc()
|
| 59 |
+
sys.exit(1)
|
| 60 |
|
| 61 |
+
print("[5] Running grader...")
|
|
|
|
| 62 |
try:
|
| 63 |
+
import json
|
| 64 |
+
parsed = json.loads(r.choices[0].message.content)
|
| 65 |
+
result = grade(
|
| 66 |
+
action_bug_type=parsed.get("bug_type", "other"),
|
| 67 |
+
action_diagnosis=parsed.get("diagnosis", ""),
|
| 68 |
+
fixed_code=parsed.get("fixed_code", ""),
|
| 69 |
+
scenario=scenario,
|
| 70 |
+
)
|
| 71 |
+
print(f" Score: {result.score}")
|
| 72 |
+
print(f" Feedback: {result.feedback[:200]}")
|
| 73 |
except Exception as e:
|
| 74 |
+
import traceback
|
| 75 |
+
traceback.print_exc()
|
| 76 |
+
sys.exit(1)
|
| 77 |
+
print("\n[6] Fixed code from LLM:")
|
| 78 |
+
print(parsed.get("fixed_code", "")[:1000])
|
| 79 |
|
| 80 |
+
print("\nAll steps passed.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|