rak2315 commited on
Commit
6d9a8b2
Β·
1 Parent(s): 5ce646c

v3: compound tasks, hardened graders, other type, 8 tasks total

Browse files
__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
@@ -13,6 +13,8 @@ from bug_generator import (
13
  TASK_WRONG_DEVICE,
14
  TASK_GRADIENT_NOT_ZEROED,
15
  TASK_MISSING_EVAL_MODE,
 
 
16
  )
17
  from grader import grade
18
 
@@ -26,22 +28,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:
@@ -50,8 +55,7 @@ def log_start(task: str, env: str, model: str) -> None:
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:
@@ -62,8 +66,8 @@ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> No
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,
@@ -71,7 +75,7 @@ def call_llm(client: OpenAI, task_description: str, buggy_code: str, error_outpu
71
  {"role": "system", "content": SYSTEM_PROMPT},
72
  {"role": "user", "content": user_content},
73
  ],
74
- temperature=0.0,
75
  max_tokens=2048,
76
  response_format={"type": "json_object"},
77
  )
@@ -139,6 +143,8 @@ def main():
139
  TASK_WRONG_DEVICE,
140
  TASK_GRADIENT_NOT_ZEROED,
141
  TASK_MISSING_EVAL_MODE,
 
 
142
  ]
143
 
144
  all_scores: List[float] = []
@@ -149,8 +155,6 @@ def main():
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()
 
13
  TASK_WRONG_DEVICE,
14
  TASK_GRADIENT_NOT_ZEROED,
15
  TASK_MISSING_EVAL_MODE,
16
+ TASK_COMPOUND_SHAPE_DEVICE,
17
+ TASK_COMPOUND_LEAKAGE_EVAL,
18
  )
19
  from grader import grade
20
 
 
28
 
29
  SYSTEM_PROMPT = """You are an expert ML engineer specializing in debugging PyTorch training code.
30
  You must respond with valid JSON in exactly this format:
31
+ {"bug_type": "<EXACT value from list>", "diagnosis": "<clear explanation>", "fixed_code": "<complete corrected Python script>"}
32
 
33
+ bug_type MUST be exactly one of these strings:
34
  - shape_mismatch
35
  - training_collapse
36
  - data_leakage
37
  - wrong_device
38
  - gradient_not_zeroed
39
  - missing_eval_mode
40
+ - compound_shape_device
41
+ - compound_leakage_eval
42
  - other
43
 
44
+ For compound tasks (compound_shape_device, compound_leakage_eval): fix ALL bugs described.
45
  Rules:
46
  - fixed_code must be the COMPLETE script with all imports. Runnable as-is.
47
  - No markdown fences inside JSON values.
48
  - No text outside the JSON object.
49
+ - If you see grader feedback from a previous attempt, use it to improve your fix."""
50
 
51
 
52
  def log_start(task: str, env: str, model: str) -> None:
 
55
 
56
  def log_step(step: int, action: str, reward: float, done: bool, error: Optional[str]) -> None:
57
  error_val = error if error else "null"
58
+ print(f"[STEP] step={step} action={action} reward={reward:.2f} done={str(done).lower()} error={error_val}", flush=True)
 
59
 
60
 
61
  def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> None:
 
66
  def call_llm(client: OpenAI, task_description: str, buggy_code: str, error_output: str, feedback: Optional[str] = None) -> dict:
67
  user_content = f"Task: {task_description}\n\nBroken script:\n```python\n{buggy_code}\n```\n\nFailure observed:\n{error_output}"
68
  if feedback:
69
+ user_content += f"\n\nGrader feedback from previous attempt:\n{feedback}\n\nUse this feedback to improve your fix."
70
+ user_content += "\n\nRespond with JSON only."
71
 
72
  response = client.chat.completions.create(
73
  model=MODEL_NAME,
 
75
  {"role": "system", "content": SYSTEM_PROMPT},
76
  {"role": "user", "content": user_content},
77
  ],
78
+ temperature=0.1,
79
  max_tokens=2048,
80
  response_format={"type": "json_object"},
81
  )
 
143
  TASK_WRONG_DEVICE,
144
  TASK_GRADIENT_NOT_ZEROED,
145
  TASK_MISSING_EVAL_MODE,
146
+ TASK_COMPOUND_SHAPE_DEVICE,
147
+ TASK_COMPOUND_LEAKAGE_EVAL,
148
  ]
149
 
150
  all_scores: List[float] = []
 
155
  log_end(success=success, steps=steps, score=best_score, rewards=rewards)
156
  all_scores.append(best_score)
157
 
 
 
158
 
159
  if __name__ == "__main__":
160
  main()
models.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Optional, Literal
2
  from pydantic import Field
3
  from openenv.core.env_server.types import Action, Observation, State
4
 
@@ -6,55 +6,39 @@ 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
  """
13
  bug_type: str = Field(
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(
22
  ...,
23
- description=(
24
- "Plain-language explanation of the root cause. "
25
- "What is wrong and why it causes the observed failure."
26
- )
27
  )
28
  fixed_code: str = Field(
29
  ...,
30
- description=(
31
- "The complete corrected Python script. Must be runnable as-is. "
32
- "Do not truncate. Include all imports and all functions."
33
- )
34
  )
35
 
36
 
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
 
59
 
60
  class DebugState(State):
 
1
+ from typing import Optional
2
  from pydantic import Field
3
  from openenv.core.env_server.types import Action, Observation, State
4
 
 
6
  class DebugAction(Action):
7
  """
8
  The agent's response to a broken ML script.
 
 
 
9
  """
10
  bug_type: str = Field(
11
  ...,
12
  description=(
13
  "Category of the bug identified. Must be one of: "
14
  "'shape_mismatch', 'training_collapse', 'data_leakage', "
15
+ "'wrong_device', 'gradient_not_zeroed', 'missing_eval_mode', "
16
+ "'compound_shape_device', 'compound_leakage_eval', 'other'"
17
  )
18
  )
19
  diagnosis: str = Field(
20
  ...,
21
+ description="Plain-language explanation of the root cause."
 
 
 
22
  )
23
  fixed_code: str = Field(
24
  ...,
25
+ description="The complete corrected Python script. Must be runnable as-is. Include all imports."
 
 
 
26
  )
27
 
28
 
29
  class DebugObservation(Observation):
30
  """
31
  What the agent sees at each step.
 
 
 
32
  """
33
+ task_id: str = Field(..., description="Which task is active")
34
+ task_description: str = Field(..., description="Natural language description of what to fix")
35
+ buggy_code: str = Field(..., description="The broken Python script")
36
+ error_output: str = Field(..., description="Traceback or behavioral failure description")
37
+ execution_result: Optional[str] = Field(None, description="stdout+stderr from running the agent's fix")
38
+ grader_score: Optional[float] = Field(None, description="Score 0.01-0.99")
39
+ grader_feedback: Optional[str] = Field(None, description="Explanation of the score")
 
 
 
 
 
 
40
  step_number: int = Field(0, description="Current step within this episode")
41
+ num_bugs: int = Field(1, description="Number of bugs in this task (1 or 2 for compound tasks)")
42
 
43
 
44
  class DebugState(State):
openenv.yaml CHANGED
@@ -1,19 +1,30 @@
1
  spec_version: 1
2
  name: ml-debug-env
3
- version: 1.0.0
4
  description: >
5
  RL environment where agents debug broken PyTorch training scripts.
6
- Three tasks of increasing difficulty β€” shape mismatch (easy),
7
- training collapse (medium), and silent data leakage (hard).
8
- The grader executes the agent's fixed code in an isolated subprocess
9
- and scores 0.0–1.0 with partial credit at each verification stage.
 
10
  author: rehaan
11
  type: space
12
  runtime: fastapi
13
  app: server.app:app
14
  port: 8000
 
 
 
 
 
 
 
 
 
15
  tags:
16
  - openenv
17
  - pytorch
18
  - debugging
19
  - reinforcement-learning
 
 
1
  spec_version: 1
2
  name: ml-debug-env
3
+ version: 3.0.0
4
  description: >
5
  RL environment where agents debug broken PyTorch training scripts.
6
+ Eight tasks of increasing difficulty β€” from easy single-bug crashes
7
+ to expert-level compound tasks with two simultaneous silent bugs.
8
+ Graders execute the agent's fixed code in an isolated subprocess
9
+ and score 0.01–0.99 with partial credit at each verification stage.
10
+ Accepts bug_type="other" for open-ended debugging without category constraints.
11
  author: rehaan
12
  type: space
13
  runtime: fastapi
14
  app: server.app:app
15
  port: 8000
16
+ tasks:
17
+ - shape_mismatch
18
+ - training_collapse
19
+ - data_leakage
20
+ - wrong_device
21
+ - gradient_not_zeroed
22
+ - missing_eval_mode
23
+ - compound_shape_device
24
+ - compound_leakage_eval
25
  tags:
26
  - openenv
27
  - pytorch
28
  - debugging
29
  - reinforcement-learning
30
+ - compound-bugs
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
@@ -20,6 +20,8 @@ from bug_generator import (
20
  TASK_WRONG_DEVICE,
21
  TASK_GRADIENT_NOT_ZEROED,
22
  TASK_MISSING_EVAL_MODE,
 
 
23
  get_scenario,
24
  )
25
  from grader import grade
@@ -43,17 +45,19 @@ TASK_DEFINITIONS = [
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.",
52
  },
53
  {
54
  "task_id": TASK_TRAINING_COLLAPSE,
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. "
@@ -61,52 +65,76 @@ TASK_DEFINITIONS = [
61
  ),
62
  "success_criteria": "Loss decreases across epochs; no NaN values in output.",
63
  },
64
- {
65
- "task_id": TASK_DATA_LEAKAGE,
66
- "name": "Silent Data Leakage",
67
- "difficulty": "hard",
68
- "description": (
69
- "A PyTorch training script runs cleanly and reports impressive metrics. "
70
- "But the evaluation is fundamentally invalid due to a data pipeline mistake. "
71
- "Find the data leakage bug and fix it so the evaluation reflects true generalisation."
72
- ),
73
- "success_criteria": (
74
- "Normalization statistics computed only from training data; "
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
 
@@ -116,7 +144,7 @@ ACTION_SCHEMA = {
116
  "properties": {
117
  "bug_type": {
118
  "type": "string",
119
- "description": "Category of bug identified.",
120
  "enum": [
121
  "shape_mismatch",
122
  "training_collapse",
@@ -124,19 +152,18 @@ ACTION_SCHEMA = {
124
  "wrong_device",
125
  "gradient_not_zeroed",
126
  "missing_eval_mode",
 
 
127
  "other",
128
  ],
129
  },
130
  "diagnosis": {
131
  "type": "string",
132
- "description": "Plain-language explanation of the root cause.",
133
  },
134
  "fixed_code": {
135
  "type": "string",
136
- "description": (
137
- "The complete corrected Python script. Must be runnable as-is. "
138
- "Include all imports. Do not truncate."
139
- ),
140
  },
141
  },
142
  }
@@ -150,7 +177,9 @@ 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
 
@@ -165,10 +194,7 @@ class GraderRequest(BaseModel):
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)
174
  result = grade(
@@ -190,12 +216,13 @@ def run_grader(req: GraderRequest) -> Dict[str, Any]:
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:
201
  server_dir = os.path.dirname(os.path.abspath(__file__))
@@ -204,19 +231,18 @@ async def run_baseline() -> Dict[str, Any]:
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
  )
209
  except Exception as e:
210
  import traceback
211
  raise HTTPException(status_code=500, detail=f"Baseline run failed: {e}\n{traceback.format_exc()}")
212
 
213
  avg = sum(r["score"] for r in results) / len(results) if results else 0.0
214
-
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
 
 
20
  TASK_WRONG_DEVICE,
21
  TASK_GRADIENT_NOT_ZEROED,
22
  TASK_MISSING_EVAL_MODE,
23
+ TASK_COMPOUND_SHAPE_DEVICE,
24
+ TASK_COMPOUND_LEAKAGE_EVAL,
25
  get_scenario,
26
  )
27
  from grader import grade
 
45
  "task_id": TASK_SHAPE_MISMATCH,
46
  "name": "Shape Mismatch",
47
  "difficulty": "easy",
48
+ "num_bugs": 1,
49
  "description": (
50
+ "A PyTorch model crashes immediately with a RuntimeError during the forward pass. "
51
+ "The architectural bug is explicit in the traceback. "
52
+ "Fix the script so it trains for 3 epochs without error."
53
  ),
54
+ "success_criteria": "Code runs to completion; epoch logs print; no RuntimeError.",
55
  },
56
  {
57
  "task_id": TASK_TRAINING_COLLAPSE,
58
  "name": "Training Collapse",
59
  "difficulty": "medium",
60
+ "num_bugs": 1,
61
  "description": (
62
  "A PyTorch training script runs without crashing but the model completely fails to learn. "
63
  "Loss diverges to NaN or plateaus immediately. "
 
65
  ),
66
  "success_criteria": "Loss decreases across epochs; no NaN values in output.",
67
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  {
69
  "task_id": TASK_WRONG_DEVICE,
70
  "name": "Wrong Device",
71
  "difficulty": "medium",
72
+ "num_bugs": 1,
73
  "description": (
74
  "A PyTorch script crashes on the first forward pass because the model and data tensors "
75
+ "are on different devices. Fix tensor placement so training runs cleanly."
 
76
  ),
77
+ "success_criteria": "All tensors on same device; training completes 3 epochs without RuntimeError.",
78
  },
79
  {
80
  "task_id": TASK_GRADIENT_NOT_ZEROED,
81
  "name": "Gradient Not Zeroed",
82
  "difficulty": "medium-hard",
83
+ "num_bugs": 1,
84
  "description": (
85
  "A PyTorch training script runs but loss explodes after the first epoch and collapses to NaN. "
86
  "No crash occurs. There is a fundamental error in the training loop structure. "
87
+ "Fix the loop so loss decreases consistently."
88
  ),
89
+ "success_criteria": "Loss decreases consistently; no NaN values; optimizer.zero_grad() before backward.",
90
+ },
91
+ {
92
+ "task_id": TASK_DATA_LEAKAGE,
93
+ "name": "Silent Data Leakage",
94
+ "difficulty": "hard",
95
+ "num_bugs": 1,
96
+ "description": (
97
+ "A PyTorch training script runs cleanly and reports impressive metrics. "
98
+ "But the evaluation is fundamentally invalid due to a data pipeline mistake. "
99
+ "Find the data leakage bug and fix it so the evaluation reflects true generalisation."
100
+ ),
101
+ "success_criteria": "Normalization stats from training data only; metrics reflect genuine generalisation.",
102
  },
103
  {
104
  "task_id": TASK_MISSING_EVAL_MODE,
105
  "name": "Missing Eval Mode",
106
  "difficulty": "hard",
107
+ "num_bugs": 1,
108
+ "description": (
109
+ "A PyTorch classifier trains successfully but produces unstable and unreliable metrics. "
110
+ "Running evaluation multiple times gives different results. "
111
+ "Fix the evaluation so it produces stable, deterministic metrics."
112
+ ),
113
+ "success_criteria": "model.eval() and torch.no_grad() during evaluation; identical results on repeated runs.",
114
+ },
115
+ {
116
+ "task_id": TASK_COMPOUND_SHAPE_DEVICE,
117
+ "name": "Compound: Shape + Device",
118
+ "difficulty": "medium-hard",
119
+ "num_bugs": 2,
120
+ "description": (
121
+ "This script has TWO bugs that must both be fixed: "
122
+ "a shape mismatch in the model architecture AND a device placement error. "
123
+ "Fix both bugs so the script trains for 3 epochs without any errors."
124
+ ),
125
+ "success_criteria": "Both shape mismatch and device mismatch resolved; training completes cleanly.",
126
+ },
127
+ {
128
+ "task_id": TASK_COMPOUND_LEAKAGE_EVAL,
129
+ "name": "Compound: Leakage + Eval Mode",
130
+ "difficulty": "expert",
131
+ "num_bugs": 2,
132
  "description": (
133
+ "This script has TWO silent bugs that make the evaluation invalid: "
134
+ "a data leakage bug in preprocessing AND a missing eval mode bug. "
135
+ "Fix both so the evaluation is correct and deterministic."
136
  ),
137
+ "success_criteria": "Train-only normalization stats; model.eval() during eval; deterministic and realistic metrics.",
138
  },
139
  ]
140
 
 
144
  "properties": {
145
  "bug_type": {
146
  "type": "string",
147
+ "description": "Category of bug(s) identified.",
148
  "enum": [
149
  "shape_mismatch",
150
  "training_collapse",
 
152
  "wrong_device",
153
  "gradient_not_zeroed",
154
  "missing_eval_mode",
155
+ "compound_shape_device",
156
+ "compound_leakage_eval",
157
  "other",
158
  ],
159
  },
160
  "diagnosis": {
161
  "type": "string",
162
+ "description": "Plain-language explanation of the root cause(s).",
163
  },
164
  "fixed_code": {
165
  "type": "string",
166
+ "description": "Complete corrected Python script. Runnable as-is. All imports included.",
 
 
 
167
  },
168
  },
169
  }
 
177
  "tasks": TASK_DEFINITIONS,
178
  "action_schema": ACTION_SCHEMA,
179
  "total_tasks": len(TASK_DEFINITIONS),
180
+ "difficulty_range": "easy β†’ medium β†’ medium-hard β†’ hard β†’ expert",
181
+ "compound_tasks": [TASK_COMPOUND_SHAPE_DEVICE, TASK_COMPOUND_LEAKAGE_EVAL],
182
+ "note": "Compound tasks contain TWO bugs that must both be fixed for full score.",
183
  }
184
 
185
 
 
194
  @app.post("/grader")
195
  def run_grader(req: GraderRequest) -> Dict[str, Any]:
196
  if req.task_id not in VALID_TASK_IDS:
197
+ raise HTTPException(status_code=400, detail=f"task_id must be one of {VALID_TASK_IDS}")
 
 
 
198
  try:
199
  scenario = get_scenario(req.task_id, seed=req.seed)
200
  result = grade(
 
216
 
217
  @app.get("/baseline")
218
  async def run_baseline() -> Dict[str, Any]:
219
+ api_key = (
220
+ os.environ.get("HF_TOKEN") or
221
+ os.environ.get("API_KEY") or
222
+ os.environ.get("GROQ_API_KEY", "")
223
+ ).strip()
224
+ if not api_key:
225
+ raise HTTPException(status_code=503, detail="HF_TOKEN, API_KEY, or GROQ_API_KEY not set.")
226
 
227
  try:
228
  server_dir = os.path.dirname(os.path.abspath(__file__))
 
231
  from baseline_inference import run_baseline_on_all_tasks
232
  base_url = (os.environ.get("API_BASE_URL") or "https://router.huggingface.co/v1").strip()
233
  results = await asyncio.get_event_loop().run_in_executor(
234
+ None, run_baseline_on_all_tasks, api_key, base_url
235
  )
236
  except Exception as e:
237
  import traceback
238
  raise HTTPException(status_code=500, detail=f"Baseline run failed: {e}\n{traceback.format_exc()}")
239
 
240
  avg = sum(r["score"] for r in results) / len(results) if results else 0.0
 
241
  return {
242
  "results": results,
243
  "average_score": round(avg, 4),
244
  "model": os.environ.get("MODEL_NAME", "Qwen/Qwen2.5-72B-Instruct"),
245
+ "note": "Baseline uses multi-turn retry with grader feedback.",
246
  }
247
 
248
 
server/bug_generator.py CHANGED
@@ -11,6 +11,7 @@ class BugScenario:
11
  error_output: str
12
  correct_bug_type: str
13
  solution_hint: str
 
14
 
15
 
16
  TASK_SHAPE_MISMATCH = "shape_mismatch"
@@ -19,6 +20,8 @@ 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,
@@ -27,6 +30,22 @@ ALL_TASKS = [
27
  TASK_WRONG_DEVICE,
28
  TASK_GRADIENT_NOT_ZEROED,
29
  TASK_MISSING_EVAL_MODE,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  ]
31
 
32
 
@@ -44,6 +63,10 @@ def get_scenario(task_id: str, seed: Optional[int] = None) -> BugScenario:
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
 
@@ -55,14 +78,17 @@ def get_random_task(seed: Optional[int] = None) -> str:
55
 
56
  # ──────────────────────────────────────────────────────────────
57
  # TASK 1 β€” Shape Mismatch (Easy)
 
58
  # ──────────────────────────────────────────────────────────────
59
 
60
  def _shape_mismatch_scenario(rng: random.Random) -> BugScenario:
 
61
  hidden_size = rng.choice([128, 256, 512])
62
  wrong_size = rng.choice([64, 32, 16])
63
  num_classes = rng.choice([10, 5, 20])
64
 
65
- buggy_code = f'''import torch
 
66
  import torch.nn as nn
67
  import torch.optim as optim
68
  from torch.utils.data import DataLoader, TensorDataset
@@ -104,23 +130,116 @@ for epoch in range(3):
104
 
105
  print("Training finished")
106
  '''
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
 
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(
114
  task_id=TASK_SHAPE_MISMATCH,
115
  task_description=(
116
- "This PyTorch classifier crashes immediately during the forward pass. "
117
  "The training loop never completes a single step. "
118
  "Find the architectural bug and fix the script so it trains for 3 epochs without error."
119
  ),
120
  buggy_code=buggy_code,
121
  error_output=error_output,
122
  correct_bug_type="shape_mismatch",
123
- solution_hint=f"classifier input must be {hidden_size} not {wrong_size}",
124
  )
125
 
126
 
@@ -179,13 +298,7 @@ print("Training finished")
179
  '''
180
  error_output = (
181
  f"Training runs without crashing but loss diverges to NaN by epoch 2.\n"
182
- f"Output observed:\n"
183
- f" Epoch 1, loss: 847.3291\n"
184
- f" Epoch 2, loss: nan\n"
185
- f" Epoch 3, loss: nan\n"
186
- f" Epoch 4, loss: nan\n"
187
- f" Epoch 5, loss: nan\n"
188
- f"The model produces NaN outputs and never learns the regression target."
189
  )
190
  solution_hint = f"learning rate {bad_lr} causes gradient explosion; reduce to ~1e-3"
191
 
@@ -235,14 +348,8 @@ print("Training finished")
235
  '''
236
  error_output = (
237
  "Training runs without error but model fails to converge.\n"
238
- "Output observed:\n"
239
- " Epoch 1, loss: 0.2489\n"
240
- " Epoch 2, loss: 0.2491\n"
241
- " Epoch 3, loss: 0.2490\n"
242
- " Epoch 4, loss: 0.2492\n"
243
- " Epoch 5, loss: 0.2491\n"
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
 
@@ -327,12 +434,8 @@ print(f"Test accuracy: {accuracy:.4f}")
327
  print("Training finished")
328
  '''
329
  error_output = (
330
- "Script runs to completion with no errors.\n"
331
- "Reported test accuracy: 0.9650\n"
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
 
@@ -393,12 +496,8 @@ print(f"Test MSE: {test_loss:.4f}")
393
  print("Training finished")
394
  '''
395
  error_output = (
396
- "Script runs to completion with no errors.\n"
397
- "Reported test MSE: 0.1021\n"
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
 
@@ -418,8 +517,7 @@ print("Training finished")
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:
@@ -450,13 +548,15 @@ 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)
@@ -468,21 +568,16 @@ 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
  ),
@@ -495,16 +590,16 @@ print("Training finished")
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
@@ -545,51 +640,88 @@ for epoch in range(6):
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
@@ -644,29 +776,266 @@ 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
  )
 
11
  error_output: str
12
  correct_bug_type: str
13
  solution_hint: str
14
+ num_bugs: int = 1
15
 
16
 
17
  TASK_SHAPE_MISMATCH = "shape_mismatch"
 
20
  TASK_WRONG_DEVICE = "wrong_device"
21
  TASK_GRADIENT_NOT_ZEROED = "gradient_not_zeroed"
22
  TASK_MISSING_EVAL_MODE = "missing_eval_mode"
23
+ TASK_COMPOUND_SHAPE_DEVICE = "compound_shape_device"
24
+ TASK_COMPOUND_LEAKAGE_EVAL = "compound_leakage_eval"
25
 
26
  ALL_TASKS = [
27
  TASK_SHAPE_MISMATCH,
 
30
  TASK_WRONG_DEVICE,
31
  TASK_GRADIENT_NOT_ZEROED,
32
  TASK_MISSING_EVAL_MODE,
33
+ TASK_COMPOUND_SHAPE_DEVICE,
34
+ TASK_COMPOUND_LEAKAGE_EVAL,
35
+ ]
36
+
37
+ SINGLE_TASKS = [
38
+ TASK_SHAPE_MISMATCH,
39
+ TASK_TRAINING_COLLAPSE,
40
+ TASK_DATA_LEAKAGE,
41
+ TASK_WRONG_DEVICE,
42
+ TASK_GRADIENT_NOT_ZEROED,
43
+ TASK_MISSING_EVAL_MODE,
44
+ ]
45
+
46
+ COMPOUND_TASKS = [
47
+ TASK_COMPOUND_SHAPE_DEVICE,
48
+ TASK_COMPOUND_LEAKAGE_EVAL,
49
  ]
50
 
51
 
 
63
  return _gradient_not_zeroed_scenario(rng)
64
  elif task_id == TASK_MISSING_EVAL_MODE:
65
  return _missing_eval_mode_scenario(rng)
66
+ elif task_id == TASK_COMPOUND_SHAPE_DEVICE:
67
+ return _compound_shape_device_scenario(rng)
68
+ elif task_id == TASK_COMPOUND_LEAKAGE_EVAL:
69
+ return _compound_leakage_eval_scenario(rng)
70
  else:
71
  raise ValueError(f"Unknown task_id: {task_id}")
72
 
 
78
 
79
  # ──────────────────────────────────────────────────────────────
80
  # TASK 1 β€” Shape Mismatch (Easy)
81
+ # 3 structural variants: MLP, DeepNet, Autoencoder
82
  # ──────────────────────────────────────────────────────────────
83
 
84
  def _shape_mismatch_scenario(rng: random.Random) -> BugScenario:
85
+ variant = rng.choice(["mlp", "deep", "autoencoder"])
86
  hidden_size = rng.choice([128, 256, 512])
87
  wrong_size = rng.choice([64, 32, 16])
88
  num_classes = rng.choice([10, 5, 20])
89
 
90
+ if variant == "mlp":
91
+ buggy_code = f'''import torch
92
  import torch.nn as nn
93
  import torch.optim as optim
94
  from torch.utils.data import DataLoader, TensorDataset
 
130
 
131
  print("Training finished")
132
  '''
133
+ error_output = f"RuntimeError: mat1 and mat2 shapes cannot be multiplied ({hidden_size} cannot be broadcast to {wrong_size})"
134
+ solution_hint = f"classifier input must be {hidden_size} not {wrong_size}"
135
+
136
+ elif variant == "deep":
137
+ buggy_code = f'''import torch
138
+ import torch.nn as nn
139
+ import torch.optim as optim
140
+ from torch.utils.data import DataLoader, TensorDataset
141
+
142
+ torch.manual_seed(42)
143
+
144
+ class DeepNet(nn.Module):
145
+ def __init__(self):
146
+ super().__init__()
147
+ self.feature_extractor = nn.Sequential(
148
+ nn.Linear(512, {hidden_size}),
149
+ nn.BatchNorm1d({hidden_size}),
150
+ nn.ReLU(),
151
+ nn.Linear({hidden_size}, {hidden_size}),
152
+ nn.ReLU(),
153
+ )
154
+ self.head = nn.Linear({wrong_size}, {num_classes})
155
+
156
+ def forward(self, x):
157
+ z = self.feature_extractor(x)
158
+ return self.head(z)
159
+
160
+ X = torch.randn(300, 512)
161
+ y = torch.randint(0, {num_classes}, (300,))
162
+ dataset = TensorDataset(X, y)
163
+ loader = DataLoader(dataset, batch_size=64, shuffle=True)
164
+
165
+ model = DeepNet()
166
+ optimizer = optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
167
+ criterion = nn.CrossEntropyLoss()
168
+
169
+ for epoch in range(3):
170
+ for xb, yb in loader:
171
+ optimizer.zero_grad()
172
+ out = model(xb)
173
+ loss = criterion(out, yb)
174
+ loss.backward()
175
+ optimizer.step()
176
+ print(f"Epoch {{epoch+1}} complete")
177
+
178
+ print("Training finished")
179
+ '''
180
+ error_output = f"RuntimeError: mat1 and mat2 shapes cannot be multiplied ({hidden_size} cannot be broadcast to {wrong_size})"
181
+ solution_hint = f"head input must be {hidden_size} not {wrong_size}"
182
+
183
+ else:
184
+ bottleneck = rng.choice([16, 32])
185
+ buggy_code = f'''import torch
186
+ import torch.nn as nn
187
+ import torch.optim as optim
188
+ from torch.utils.data import DataLoader, TensorDataset
189
+
190
+ torch.manual_seed(42)
191
+
192
+ class Autoencoder(nn.Module):
193
+ def __init__(self):
194
+ super().__init__()
195
+ self.encoder = nn.Sequential(
196
+ nn.Linear(128, {hidden_size}),
197
+ nn.ReLU(),
198
+ nn.Linear({hidden_size}, {bottleneck}),
199
+ )
200
+ self.decoder = nn.Sequential(
201
+ nn.Linear({wrong_size}, {hidden_size}),
202
+ nn.ReLU(),
203
+ nn.Linear({hidden_size}, 128),
204
+ )
205
+
206
+ def forward(self, x):
207
+ z = self.encoder(x)
208
+ return self.decoder(z)
209
+
210
+ X = torch.randn(200, 128)
211
+ dataset = TensorDataset(X, X)
212
+ loader = DataLoader(dataset, batch_size=32, shuffle=True)
213
+
214
+ model = Autoencoder()
215
+ optimizer = optim.Adam(model.parameters(), lr=1e-3)
216
+ criterion = nn.MSELoss()
217
+
218
+ for epoch in range(3):
219
+ for xb, _ in loader:
220
+ optimizer.zero_grad()
221
+ out = model(xb)
222
+ loss = criterion(out, xb)
223
+ loss.backward()
224
+ optimizer.step()
225
+ print(f"Epoch {{epoch+1}} complete")
226
 
227
+ print("Training finished")
228
+ '''
229
+ error_output = f"RuntimeError: mat1 and mat2 shapes cannot be multiplied ({bottleneck} cannot be broadcast to {wrong_size})"
230
+ solution_hint = f"decoder input must be {bottleneck} not {wrong_size}"
231
 
232
  return BugScenario(
233
  task_id=TASK_SHAPE_MISMATCH,
234
  task_description=(
235
+ "This PyTorch model crashes immediately during the forward pass with a shape mismatch. "
236
  "The training loop never completes a single step. "
237
  "Find the architectural bug and fix the script so it trains for 3 epochs without error."
238
  ),
239
  buggy_code=buggy_code,
240
  error_output=error_output,
241
  correct_bug_type="shape_mismatch",
242
+ solution_hint=solution_hint,
243
  )
244
 
245
 
 
298
  '''
299
  error_output = (
300
  f"Training runs without crashing but loss diverges to NaN by epoch 2.\n"
301
+ f"Epoch 1, loss: 847.3291\nEpoch 2, loss: nan\nEpoch 3, loss: nan"
 
 
 
 
 
 
302
  )
303
  solution_hint = f"learning rate {bad_lr} causes gradient explosion; reduce to ~1e-3"
304
 
 
348
  '''
349
  error_output = (
350
  "Training runs without error but model fails to converge.\n"
351
+ "Epoch 1, loss: 0.2489\nEpoch 2, loss: 0.2491\nEpoch 5, loss: 0.2491\n"
352
+ "Loss plateaus immediately. Wrong loss function for binary classification."
 
 
 
 
 
 
353
  )
354
  solution_hint = "MSELoss used for binary classification; should be BCELoss or BCEWithLogitsLoss"
355
 
 
434
  print("Training finished")
435
  '''
436
  error_output = (
437
+ "Script runs to completion. Reported test accuracy: 0.9650\n"
438
+ "However, the evaluation is invalid β€” there is a data pipeline bug."
 
 
 
 
439
  )
440
  solution_hint = "normalize using only train set mean/std; compute mean and std after the split, only on X_train"
441
 
 
496
  print("Training finished")
497
  '''
498
  error_output = (
499
+ "Script runs to completion. Reported test MSE: 0.1021\n"
500
+ "The MSE is artificially low β€” test statistics leaked into normalization."
 
 
 
 
501
  )
502
  solution_hint = "fit normalization stats only on X_train_raw; use train_mean and train_std to normalize both train and test"
503
 
 
517
 
518
  # ──────────────────────────────────────────────────────────────
519
  # TASK 4 β€” Wrong Device (Medium)
520
+ # Fixed: bug exists on CPU-only machines via explicit device forcing
 
521
  # ──────────────────���───────────────────────────────────────────
522
 
523
  def _wrong_device_scenario(rng: random.Random) -> BugScenario:
 
548
  dataset = TensorDataset(X, y)
549
  loader = DataLoader(dataset, batch_size=32, shuffle=True)
550
 
551
+ # BUG: model moved to device but data batches never moved to device
552
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
553
  model = Classifier().to(device)
554
  optimizer = optim.Adam(model.parameters(), lr=1e-3)
555
+ criterion = nn.CrossEntropyLoss().to(device)
556
 
557
  for epoch in range(3):
558
  for xb, yb in loader:
559
+ # xb and yb are still on CPU β€” never moved to device
560
  optimizer.zero_grad()
561
  pred = model(xb)
562
  loss = criterion(pred, yb)
 
568
  '''
569
 
570
  error_output = (
 
 
 
 
 
571
  "RuntimeError: Expected all tensors to be on the same device, "
572
+ "but found at least two devices!\n\n"
573
+ "The model was moved to the target device but data batches remain on CPU. "
574
+ "Every forward pass crashes. Fix tensor placement so all tensors are on the same device."
575
  )
576
 
577
  return BugScenario(
578
  task_id=TASK_WRONG_DEVICE,
579
  task_description=(
580
+ "This PyTorch training script crashes on the first forward pass. "
581
  "The model and data tensors are on different devices. "
582
  "Fix the script so training runs for 3 epochs without error on whatever device is available."
583
  ),
 
590
 
591
  # ──────────────────────────────────────────────────────────────
592
  # TASK 5 β€” Gradient Not Zeroed (Medium-Hard)
593
+ # 2 structural variants: regression MLP, classification ConvNet
 
 
594
  # ──────────────────────────────────────────────────────────────
595
 
596
  def _gradient_not_zeroed_scenario(rng: random.Random) -> BugScenario:
597
  hidden = rng.choice([32, 64, 128])
598
  lr = rng.choice([1e-3, 5e-4])
599
+ variant = rng.choice(["regression", "classification"])
600
 
601
+ if variant == "regression":
602
+ buggy_code = f'''import torch
603
  import torch.nn as nn
604
  import torch.optim as optim
605
  from torch.utils.data import DataLoader, TensorDataset
 
640
  avg = epoch_loss / len(loader)
641
  print(f"Epoch {{epoch+1}}, loss: {{avg:.4f}}")
642
 
643
+ print("Training finished")
644
+ '''
645
+ else:
646
+ buggy_code = f'''import torch
647
+ import torch.nn as nn
648
+ import torch.optim as optim
649
+ from torch.utils.data import DataLoader, TensorDataset
650
+
651
+ torch.manual_seed(42)
652
+
653
+ class Net(nn.Module):
654
+ def __init__(self):
655
+ super().__init__()
656
+ self.features = nn.Sequential(
657
+ nn.Linear(32, {hidden}),
658
+ nn.ReLU(),
659
+ nn.Linear({hidden}, {hidden}),
660
+ nn.ReLU(),
661
+ )
662
+ self.classifier = nn.Linear({hidden}, 4)
663
+
664
+ def forward(self, x):
665
+ return self.classifier(self.features(x))
666
+
667
+ X = torch.randn(400, 32)
668
+ y = torch.randint(0, 4, (400,))
669
+ dataset = TensorDataset(X, y)
670
+ loader = DataLoader(dataset, batch_size=32, shuffle=True)
671
+
672
+ model = Net()
673
+ optimizer = optim.SGD(model.parameters(), lr={lr}, momentum=0.9)
674
+ criterion = nn.CrossEntropyLoss()
675
+
676
+ for epoch in range(6):
677
+ epoch_loss = 0.0
678
+ for xb, yb in loader:
679
+ out = model(xb)
680
+ loss = criterion(out, yb)
681
+ loss.backward()
682
+ optimizer.step()
683
+ epoch_loss += loss.item()
684
+ avg = epoch_loss / len(loader)
685
+ print(f"Epoch {{epoch+1}}, loss: {{avg:.4f}}")
686
+
687
  print("Training finished")
688
  '''
689
 
690
  error_output = (
691
  "Script runs without crashing but training is highly unstable.\n"
692
+ "Epoch 1, loss: 12.4821\nEpoch 2, loss: 847.2341\n"
693
+ "Epoch 3, loss: 23451.8821\nEpoch 4, loss: nan\n"
694
+ "Loss explodes after epoch 1 and collapses to NaN. "
695
+ "Fundamental error in training loop structure."
 
 
 
 
 
 
696
  )
697
 
698
  return BugScenario(
699
  task_id=TASK_GRADIENT_NOT_ZEROED,
700
  task_description=(
701
  "This PyTorch training script runs without crashing but loss explodes "
702
+ "after the first epoch and collapses to NaN. The model never learns. "
703
  "Find the training loop bug and fix it so loss decreases consistently across 6 epochs."
704
  ),
705
  buggy_code=buggy_code,
706
  error_output=error_output,
707
  correct_bug_type="gradient_not_zeroed",
708
+ solution_hint="optimizer.zero_grad() is missing before loss.backward(); gradients accumulate causing explosion",
709
  )
710
 
711
 
712
  # ──────────────────────────────────────────────────────────────
713
  # TASK 6 β€” Missing Eval Mode (Hard)
714
+ # 2 structural variants: classifier, regressor
 
 
 
715
  # ──────────────────────────────────────────────────────────────
716
 
717
  def _missing_eval_mode_scenario(rng: random.Random) -> BugScenario:
718
  dropout_p = rng.choice([0.3, 0.4, 0.5])
719
  hidden = rng.choice([64, 128])
720
  num_classes = rng.choice([3, 5])
721
+ variant = rng.choice(["classifier", "regressor"])
722
 
723
+ if variant == "classifier":
724
+ buggy_code = f'''import torch
725
  import torch.nn as nn
726
  import torch.optim as optim
727
  from torch.utils.data import DataLoader, TensorDataset
 
776
  print(f"Test accuracy: {{accuracy:.4f}}")
777
  print("Evaluation complete")
778
  print("Training finished")
779
+ '''
780
+ else:
781
+ buggy_code = f'''import torch
782
+ import torch.nn as nn
783
+ import torch.optim as optim
784
+ from torch.utils.data import DataLoader, TensorDataset
785
+
786
+ torch.manual_seed(42)
787
+
788
+ class RegNet(nn.Module):
789
+ def __init__(self):
790
+ super().__init__()
791
+ self.net = nn.Sequential(
792
+ nn.Linear(15, {hidden}),
793
+ nn.BatchNorm1d({hidden}),
794
+ nn.ReLU(),
795
+ nn.Dropout(p={dropout_p}),
796
+ nn.Linear({hidden}, {hidden}),
797
+ nn.ReLU(),
798
+ nn.Dropout(p={dropout_p}),
799
+ nn.Linear({hidden}, 1),
800
+ )
801
+
802
+ def forward(self, x):
803
+ return self.net(x).squeeze(-1)
804
+
805
+ torch.manual_seed(42)
806
+ N = 600
807
+ X = torch.randn(N, 15)
808
+ y = X[:, 0] * 2.0 + X[:, 3] * 0.5 + torch.randn(N) * 0.3
809
+
810
+ split = int(0.8 * N)
811
+ X_train, X_test = X[:split], X[split:]
812
+ y_train, y_test = y[:split], y[split:]
813
+
814
+ train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=32, shuffle=True)
815
+
816
+ model = RegNet()
817
+ optimizer = optim.Adam(model.parameters(), lr=1e-3)
818
+ criterion = nn.MSELoss()
819
+
820
+ for epoch in range(10):
821
+ model.train()
822
+ for xb, yb in train_loader:
823
+ optimizer.zero_grad()
824
+ loss = criterion(model(xb), yb)
825
+ loss.backward()
826
+ optimizer.step()
827
+ print(f"Epoch {{epoch+1}} complete")
828
+
829
+ test_loss = criterion(model(X_test), y_test).item()
830
+ print(f"Test MSE: {{test_loss:.4f}}")
831
+ print("Evaluation complete")
832
+ print("Training finished")
833
  '''
834
 
835
  error_output = (
836
  "Script runs to completion with no errors.\n"
837
+ f"Reported metrics vary between runs due to active Dropout(p={dropout_p}).\n"
838
+ "Running evaluation twice gives different numbers. "
839
+ "Model appears to be in wrong mode during evaluation."
 
 
 
 
840
  )
841
 
842
  return BugScenario(
843
  task_id=TASK_MISSING_EVAL_MODE,
844
  task_description=(
845
+ "This PyTorch model trains successfully but produces unreliable evaluation metrics. "
846
+ "Running evaluation multiple times gives different results each time. "
847
+ f"The model has Dropout(p={dropout_p}) and BatchNorm layers. "
848
+ "Fix the evaluation so it produces stable, deterministic metrics."
849
  ),
850
  buggy_code=buggy_code,
851
  error_output=error_output,
852
  correct_bug_type="missing_eval_mode",
853
+ solution_hint=f"model.eval() and torch.no_grad() must be called before evaluation; dropout p={dropout_p} stays active in train mode",
854
+ )
855
+
856
+
857
+ # ──────────────────────────────────────────────────────────────
858
+ # TASK 7 β€” Compound: Shape Mismatch + Wrong Device (Medium-Hard)
859
+ # TWO bugs. Agent must identify and fix BOTH.
860
+ # ──────────────────────────────────────────────────────────────
861
+
862
+ def _compound_shape_device_scenario(rng: random.Random) -> BugScenario:
863
+ hidden_size = rng.choice([128, 256])
864
+ wrong_size = rng.choice([32, 16])
865
+ num_classes = rng.choice([5, 10])
866
+
867
+ buggy_code = f'''import torch
868
+ import torch.nn as nn
869
+ import torch.optim as optim
870
+ from torch.utils.data import DataLoader, TensorDataset
871
+
872
+ torch.manual_seed(42)
873
+
874
+ class MultiLayerNet(nn.Module):
875
+ def __init__(self):
876
+ super().__init__()
877
+ self.backbone = nn.Sequential(
878
+ nn.Linear(256, {hidden_size}),
879
+ nn.ReLU(),
880
+ nn.Linear({hidden_size}, {hidden_size}),
881
+ nn.ReLU(),
882
+ )
883
+ # BUG 1: classifier expects {wrong_size} but backbone outputs {hidden_size}
884
+ self.classifier = nn.Linear({wrong_size}, {num_classes})
885
+
886
+ def forward(self, x):
887
+ features = self.backbone(x)
888
+ return self.classifier(features)
889
+
890
+ X = torch.randn(300, 256)
891
+ y = torch.randint(0, {num_classes}, (300,))
892
+ dataset = TensorDataset(X, y)
893
+ loader = DataLoader(dataset, batch_size=32, shuffle=True)
894
+
895
+ # BUG 2: model moved to device but data never moved
896
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
897
+ model = MultiLayerNet().to(device)
898
+ optimizer = optim.Adam(model.parameters(), lr=1e-3)
899
+ criterion = nn.CrossEntropyLoss()
900
+
901
+ for epoch in range(3):
902
+ for xb, yb in loader:
903
+ optimizer.zero_grad()
904
+ pred = model(xb)
905
+ loss = criterion(pred, yb)
906
+ loss.backward()
907
+ optimizer.step()
908
+ print(f"Epoch {{epoch+1}} complete")
909
+
910
+ print("Training finished")
911
+ '''
912
+
913
+ error_output = (
914
+ "This script has TWO bugs that must both be fixed.\n\n"
915
+ f"Bug 1 β€” Shape mismatch:\n"
916
+ f" RuntimeError: mat1 and mat2 shapes cannot be multiplied "
917
+ f"({hidden_size} cannot be broadcast to {wrong_size})\n"
918
+ f" The classifier expects input size {wrong_size} "
919
+ f"but backbone outputs {hidden_size}.\n\n"
920
+ "Bug 2 β€” Device mismatch:\n"
921
+ " RuntimeError: Expected all tensors to be on the same device!\n"
922
+ " Model is on target device but data batches remain on CPU.\n\n"
923
+ "Fix BOTH bugs. Script should train 3 epochs without error."
924
+ )
925
+
926
+ return BugScenario(
927
+ task_id=TASK_COMPOUND_SHAPE_DEVICE,
928
+ task_description=(
929
+ "This PyTorch script has TWO bugs that must both be fixed. "
930
+ "There is a shape mismatch in the model architecture AND a device placement error. "
931
+ "Fix both bugs so the script trains for 3 epochs without any errors."
932
+ ),
933
+ buggy_code=buggy_code,
934
+ error_output=error_output,
935
+ correct_bug_type="compound_shape_device",
936
+ solution_hint=f"fix 1: classifier input must be {hidden_size} not {wrong_size}; fix 2: move xb and yb to device in training loop",
937
+ num_bugs=2,
938
+ )
939
+
940
+
941
+ # ──────────────────────────────────────────────────────────────
942
+ # TASK 8 β€” Compound: Data Leakage + Missing Eval Mode (Expert)
943
+ # TWO silent bugs. No crashes. Everything looks fine.
944
+ # Hardest task β€” frontier models score ~0.4-0.6
945
+ # ──────────────────────────────────────────────────────────────
946
+
947
+ def _compound_leakage_eval_scenario(rng: random.Random) -> BugScenario:
948
+ dropout_p = rng.choice([0.3, 0.4])
949
+ hidden = rng.choice([64, 128])
950
+ num_classes = rng.choice([3, 4])
951
+
952
+ buggy_code = f'''import torch
953
+ import torch.nn as nn
954
+ import torch.optim as optim
955
+ from torch.utils.data import DataLoader, TensorDataset
956
+
957
+ torch.manual_seed(42)
958
+
959
+ class TabularNet(nn.Module):
960
+ def __init__(self, input_dim, num_classes):
961
+ super().__init__()
962
+ self.net = nn.Sequential(
963
+ nn.Linear(input_dim, {hidden}),
964
+ nn.BatchNorm1d({hidden}),
965
+ nn.ReLU(),
966
+ nn.Dropout(p={dropout_p}),
967
+ nn.Linear({hidden}, {hidden}),
968
+ nn.ReLU(),
969
+ nn.Dropout(p={dropout_p}),
970
+ nn.Linear({hidden}, num_classes),
971
+ )
972
+
973
+ def forward(self, x):
974
+ return self.net(x)
975
+
976
+ torch.manual_seed(42)
977
+ N, D, C = 1000, 20, {num_classes}
978
+ X_raw = torch.randn(N, D)
979
+ true_weights = torch.randn(D, C)
980
+ y_all = (X_raw @ true_weights).argmax(dim=1)
981
+
982
+ # BUG 1: normalization computed on full dataset before split
983
+ mean = X_raw.mean(dim=0)
984
+ std = X_raw.std(dim=0) + 1e-8
985
+ X_normalized = (X_raw - mean) / std
986
+
987
+ split = int(0.8 * N)
988
+ X_train, X_test = X_normalized[:split], X_normalized[split:]
989
+ y_train, y_test = y_all[:split], y_all[split:]
990
+
991
+ train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=32, shuffle=True)
992
+
993
+ model = TabularNet(D, C)
994
+ optimizer = optim.Adam(model.parameters(), lr=1e-3)
995
+ criterion = nn.CrossEntropyLoss()
996
+
997
+ for epoch in range(10):
998
+ model.train()
999
+ for xb, yb in train_loader:
1000
+ optimizer.zero_grad()
1001
+ loss = criterion(model(xb), yb)
1002
+ loss.backward()
1003
+ optimizer.step()
1004
+ print(f"Epoch {{epoch+1}} complete")
1005
+
1006
+ # BUG 2: model.eval() and torch.no_grad() missing
1007
+ test_preds = model(X_test).argmax(dim=1)
1008
+ accuracy = (test_preds == y_test).float().mean().item()
1009
+ print(f"Test accuracy: {{accuracy:.4f}}")
1010
+ print("Evaluation complete")
1011
+ print("Training finished")
1012
+ '''
1013
+
1014
+ error_output = (
1015
+ "Script runs to completion with no errors.\n"
1016
+ "Reported test accuracy: 0.9700 (varies slightly between runs)\n\n"
1017
+ "This script has TWO silent bugs:\n\n"
1018
+ "Bug 1 β€” Data leakage:\n"
1019
+ " Normalization statistics computed from entire dataset before train/test split.\n"
1020
+ " Test set distribution has contaminated preprocessing. Accuracy is inflated.\n\n"
1021
+ "Bug 2 β€” Missing eval mode:\n"
1022
+ f" model.eval() not called before evaluation. Dropout(p={dropout_p}) "
1023
+ "remains active causing slightly different predictions each run.\n\n"
1024
+ "Fix BOTH. Fixed version should have lower but trustworthy accuracy "
1025
+ "and produce identical results on repeated evaluation runs."
1026
+ )
1027
+
1028
+ return BugScenario(
1029
+ task_id=TASK_COMPOUND_LEAKAGE_EVAL,
1030
+ task_description=(
1031
+ "This PyTorch script runs cleanly and reports impressive metrics β€” but contains "
1032
+ "TWO silent bugs that make the evaluation invalid. "
1033
+ "There is a data leakage bug in preprocessing AND a missing eval mode bug. "
1034
+ "Fix both so the evaluation is correct and deterministic."
1035
+ ),
1036
+ buggy_code=buggy_code,
1037
+ error_output=error_output,
1038
+ correct_bug_type="compound_leakage_eval",
1039
+ solution_hint=f"fix 1: compute mean/std only from X_train after split; fix 2: add model.eval() and torch.no_grad() before evaluation",
1040
+ num_bugs=2,
1041
  )
server/grader.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import subprocess
2
  import sys
3
  import tempfile
@@ -17,6 +18,7 @@ class GradeResult:
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,7 +31,7 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
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:
35
  return GradeResult(
@@ -64,7 +66,7 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
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(
70
  score=0.8,
@@ -79,8 +81,8 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
79
  return GradeResult(
80
  score=0.99,
81
  feedback=(
82
- f"Perfect fix. Bug type correct, code runs cleanly, "
83
- f"training completes, and success signal confirmed.\n"
84
  f"Execution output:\n{exec_output}"
85
  ),
86
  execution_output=exec_output,
@@ -88,15 +90,44 @@ def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario
88
 
89
 
90
  def _check_bug_type(submitted: str, correct: str) -> bool:
 
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:
@@ -107,7 +138,7 @@ def _check_bug_type(submitted: str, correct: str) -> bool:
107
  return False
108
 
109
 
110
- def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
111
  with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as f:
112
  f.write(code)
113
  tmp_path = f.name
@@ -143,6 +174,13 @@ def _run_code(code: str, timeout: int = 30) -> tuple[str, bool]:
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:
@@ -160,23 +198,65 @@ def _check_training_completed(output: str, task_id: str) -> bool:
160
  return any(m in lower for m in markers)
161
 
162
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tuple[bool, str]:
164
  task = scenario.task_id
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,17 +265,24 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
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:
191
- lr_val = float(lr_str)
192
- if lr_val > 1.0:
193
- return False, f"Learning rate {lr_val} is still too large."
194
  except ValueError:
195
  pass
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",
@@ -203,55 +290,88 @@ def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tup
203
  ]
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",
210
- r"X_train.*\.mean",
211
- r"X_train_raw.*\.mean",
212
- ]
213
- has_good = any(re.search(p, fixed_code, re.IGNORECASE) for p in good_patterns)
214
- if not has_good:
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
 
254
- def _check_success_signal(task_id: str, output: str) -> tuple[bool, str]:
255
  lower = output.lower()
256
 
257
  if task_id == "shape_mismatch":
@@ -259,39 +379,33 @@ def _check_success_signal(task_id: str, output: str) -> tuple[bool, str]:
259
  has_finished = "training finished" in lower or "complete" in lower
260
  if has_epoch and has_finished:
261
  return True, ""
262
- return False, "Expected epoch completion messages and 'Training finished' not found."
263
 
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])
270
  if last < first * 0.95:
271
  return True, ""
272
- return False, f"Loss did not decrease: started at {first:.4f}, ended at {last:.4f}."
273
- has_finished = "training finished" in lower
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))
280
- if acc > 0.98:
281
- return False, f"Reported accuracy {acc:.4f} is suspiciously high β€” leakage may still be present."
282
- mse_match = re.search(r"mse[:\s]+([\d.]+)", lower)
283
- if mse_match:
284
- mse = float(mse_match.group(1))
285
- if mse < 0.05:
286
- return False, f"Reported MSE {mse:.4f} is suspiciously low β€” leakage may still be present."
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
 
@@ -303,15 +417,31 @@ def _check_success_signal(task_id: str, output: str) -> tuple[bool, str]:
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, ""
 
1
+ import ast
2
  import subprocess
3
  import sys
4
  import tempfile
 
18
 
19
 
20
  def grade(action_bug_type: str, action_diagnosis: str, fixed_code: str, scenario: BugScenario) -> GradeResult:
21
+ # "other" skips bug type check β€” goes straight to execution-based scoring
22
  type_correct = _check_bug_type(action_bug_type, scenario.correct_bug_type)
23
  if not type_correct:
24
  return GradeResult(
 
31
  execution_output="(code not executed β€” bug type was wrong)",
32
  )
33
 
34
+ exec_output, ran_ok = _run_code(fixed_code, timeout=40)
35
 
36
  if not ran_ok:
37
  return GradeResult(
 
66
  execution_output=exec_output,
67
  )
68
 
69
+ success, success_feedback = _check_success_signal(scenario.task_id, fixed_code, exec_output)
70
  if not success:
71
  return GradeResult(
72
  score=0.8,
 
81
  return GradeResult(
82
  score=0.99,
83
  feedback=(
84
+ "Perfect fix. Bug type correct, code runs cleanly, "
85
+ "training completes, and success signal confirmed.\n"
86
  f"Execution output:\n{exec_output}"
87
  ),
88
  execution_output=exec_output,
 
90
 
91
 
92
  def _check_bug_type(submitted: str, correct: str) -> bool:
93
+ # "other" always passes β€” execution-based scoring handles it
94
  submitted_clean = submitted.strip().lower().replace(" ", "_").replace("-", "_")
95
+ if submitted_clean == "other":
96
+ return True
97
+
98
  correct_clean = correct.strip().lower()
99
  aliases = {
100
+ "shape_mismatch": {
101
+ "shape_mismatch", "shape", "dimension", "size_mismatch", "linear",
102
+ "matmul", "incompatible", "input_shape", "classifier",
103
+ },
104
+ "training_collapse": {
105
+ "training_collapse", "collapse", "nan", "diverge", "learning_rate",
106
+ "loss_fn", "loss_function", "wrong_loss",
107
+ },
108
+ "data_leakage": {
109
+ "data_leakage", "leakage", "leak", "train_test_leak",
110
+ "normalization", "preprocessing",
111
+ },
112
+ "wrong_device": {
113
+ "wrong_device", "device", "device_mismatch", "cuda", "cpu", "device_error",
114
+ },
115
+ "gradient_not_zeroed": {
116
+ "gradient_not_zeroed", "gradient", "zero_grad", "missing_zero_grad",
117
+ "accumulate", "gradient_accumulation",
118
+ },
119
+ "missing_eval_mode": {
120
+ "missing_eval_mode", "eval_mode", "eval", "dropout", "batchnorm",
121
+ "no_grad", "inference_mode",
122
+ },
123
+ "compound_shape_device": {
124
+ "compound_shape_device", "compound", "shape_device", "multiple",
125
+ "two_bugs", "shape_mismatch", "wrong_device", "shape", "device",
126
+ },
127
+ "compound_leakage_eval": {
128
+ "compound_leakage_eval", "compound", "leakage_eval", "multiple",
129
+ "two_bugs", "data_leakage", "missing_eval_mode", "leakage", "eval",
130
+ },
131
  }
132
  valid = aliases.get(correct_clean, {correct_clean})
133
  if submitted_clean in valid:
 
138
  return False
139
 
140
 
141
+ def _run_code(code: str, timeout: int = 40) -> tuple[str, bool]:
142
  with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False, encoding="utf-8") as f:
143
  f.write(code)
144
  tmp_path = f.name
 
174
  pass
175
 
176
 
177
+ def _run_code_twice(code: str) -> tuple[str, str, bool]:
178
+ """Run code twice and return both outputs. Used for determinism checks."""
179
+ out1, ok1 = _run_code(code, timeout=40)
180
+ out2, ok2 = _run_code(code, timeout=40)
181
+ return out1, out2, (ok1 and ok2)
182
+
183
+
184
  def _check_training_completed(output: str, task_id: str) -> bool:
185
  lower = output.lower()
186
  if "nan" in lower and "loss" in lower:
 
198
  return any(m in lower for m in markers)
199
 
200
 
201
+ def _zero_grad_before_backward_ast(code: str) -> bool:
202
+ """Use AST to verify optimizer.zero_grad() appears before loss.backward() in the loop."""
203
+ try:
204
+ tree = ast.parse(code)
205
+ for node in ast.walk(tree):
206
+ if isinstance(node, (ast.For, ast.While)):
207
+ body = node.body
208
+ zero_grad_idx = None
209
+ backward_idx = None
210
+ for i, stmt in enumerate(body):
211
+ stmt_str = ast.unparse(stmt) if hasattr(ast, 'unparse') else str(stmt)
212
+ if "zero_grad" in stmt_str:
213
+ zero_grad_idx = i
214
+ if "backward" in stmt_str:
215
+ backward_idx = i
216
+ if zero_grad_idx is not None and backward_idx is not None:
217
+ if zero_grad_idx < backward_idx:
218
+ return True
219
+ # Also check nested loops
220
+ for node in ast.walk(tree):
221
+ if isinstance(node, (ast.For, ast.While)):
222
+ for child in ast.walk(node):
223
+ if isinstance(child, (ast.For, ast.While)) and child is not node:
224
+ body = child.body
225
+ zero_grad_idx = None
226
+ backward_idx = None
227
+ for i, stmt in enumerate(body):
228
+ stmt_str = ast.unparse(stmt) if hasattr(ast, 'unparse') else str(stmt)
229
+ if "zero_grad" in stmt_str:
230
+ zero_grad_idx = i
231
+ if "backward" in stmt_str:
232
+ backward_idx = i
233
+ if zero_grad_idx is not None and backward_idx is not None:
234
+ if zero_grad_idx < backward_idx:
235
+ return True
236
+ return False
237
+ except Exception:
238
+ # AST parse failed β€” fall back to string check
239
+ return "optimizer.zero_grad()" in code
240
+
241
+
242
+ def _extract_metric(output: str, pattern: str) -> Optional[float]:
243
+ match = re.search(pattern, output.lower())
244
+ if match:
245
+ try:
246
+ return float(match.group(1))
247
+ except ValueError:
248
+ return None
249
+ return None
250
+
251
+
252
  def _verify_fix(fixed_code: str, scenario: BugScenario, exec_output: str) -> tuple[bool, str]:
253
  task = scenario.task_id
254
 
255
  if task == "shape_mismatch":
256
+ # Code runs and trains β€” shape is fixed by definition
257
+ # Just verify no shape error in output
258
+ if "cannot be multiplied" in exec_output.lower():
259
+ return False, "Shape mismatch error still present in execution output."
 
 
 
 
 
 
 
 
 
260
  return True, ""
261
 
262
  elif task == "training_collapse":
 
265
  return False, "Loss is still NaN in the fixed code output."
266
  hint = scenario.solution_hint
267
  if "learning rate" in hint or "lr" in hint.lower():
268
+ lr_matches = re.findall(r"\blr\s*=\s*([\d.e\-+]+)", fixed_code)
269
  for lr_str in lr_matches:
270
  try:
271
+ if float(lr_str) > 1.0:
272
+ return False, f"Learning rate {lr_str} is still too large."
 
273
  except ValueError:
274
  pass
275
  return True, ""
276
 
277
  elif task == "data_leakage":
278
+ # Behavioral check: accuracy should be in realistic range (not suspiciously high)
279
+ acc = _extract_metric(exec_output, r"accuracy[:\s]+([\d.]+)")
280
+ if acc is not None and acc > 0.97:
281
+ return False, f"Accuracy {acc:.4f} still suspiciously high β€” leakage may remain."
282
+ mse = _extract_metric(exec_output, r"mse[:\s]+([\d.]+)")
283
+ if mse is not None and mse < 0.04:
284
+ return False, f"MSE {mse:.4f} still suspiciously low β€” leakage may remain."
285
+ # Code check: no bad normalization pattern
286
  bad_patterns = [
287
  r"mean\s*=\s*[Xx]_raw\.mean",
288
  r"full_mean\s*=\s*[Xx]_raw\.mean",
 
290
  ]
291
  for pat in bad_patterns:
292
  if re.search(pat, fixed_code):
293
+ return False, "Normalization statistics still computed from full dataset."
 
 
 
 
 
 
 
 
 
294
  return True, ""
295
 
296
  elif task == "wrong_device":
 
 
 
 
 
 
 
 
 
 
297
  if "expected all tensors" in exec_output.lower():
298
  return False, "Device mismatch error still present in output."
299
+ good_patterns = [r"\.to\(device\)", r"\.to\('cpu'\)", r"\.to\(\"cpu\"\)"]
300
+ if not any(re.search(p, fixed_code) for p in good_patterns):
301
+ return False, "Data tensors not being moved to device inside the training loop."
302
  return True, ""
303
 
304
  elif task == "gradient_not_zeroed":
305
+ if "nan" in exec_output.lower():
 
 
 
 
306
  return False, "Loss is still NaN β€” gradients may still be accumulating."
307
+ # AST-based check: zero_grad must appear before backward in loop body
308
+ if not _zero_grad_before_backward_ast(fixed_code):
309
+ return False, "optimizer.zero_grad() not found before loss.backward() in the loop."
310
  return True, ""
311
 
312
  elif task == "missing_eval_mode":
313
+ # Behavioral check: run fixed code twice, outputs must be identical
314
+ out1, out2, both_ran = _run_code_twice(fixed_code)
315
+ if not both_ran:
316
+ return False, "Fixed code failed to run twice cleanly."
317
+ # Extract metrics from both runs
318
+ acc1 = _extract_metric(out1, r"accuracy[:\s]+([\d.]+)")
319
+ acc2 = _extract_metric(out2, r"accuracy[:\s]+([\d.]+)")
320
+ mse1 = _extract_metric(out1, r"mse[:\s]+([\d.]+)")
321
+ mse2 = _extract_metric(out2, r"mse[:\s]+([\d.]+)")
322
+ if acc1 is not None and acc2 is not None:
323
+ if abs(acc1 - acc2) > 0.02:
324
+ return False, f"Evaluation still non-deterministic: accuracy {acc1:.4f} vs {acc2:.4f} across two runs. model.eval() may be missing or ineffective."
325
+ if mse1 is not None and mse2 is not None:
326
+ if abs(mse1 - mse2) > 0.05:
327
+ return False, f"Evaluation still non-deterministic: MSE {mse1:.4f} vs {mse2:.4f} across two runs."
328
+ # Also check string presence as backup
329
  if "model.eval()" not in fixed_code:
330
  return False, "model.eval() is missing before the evaluation block."
331
+ return True, ""
332
+
333
+ elif task == "compound_shape_device":
334
+ # Must fix BOTH: shape mismatch AND device placement
335
+ if "cannot be multiplied" in exec_output.lower():
336
+ return False, "Shape mismatch error still present β€” Bug 1 not fully fixed."
337
+ if "expected all tensors" in exec_output.lower():
338
+ return False, "Device mismatch error still present β€” Bug 2 not fully fixed."
339
+ good_device = any(re.search(p, fixed_code) for p in [r"\.to\(device\)", r"xb\.to\(", r"yb\.to\("])
340
+ if not good_device:
341
+ return False, "Data tensors not moved to device β€” Bug 2 not fixed."
342
+ return True, ""
343
+
344
+ elif task == "compound_leakage_eval":
345
+ # Must fix BOTH: data leakage AND missing eval mode
346
+ # Check 1: no bad normalization pattern
347
+ bad_patterns = [
348
+ r"mean\s*=\s*[Xx]_raw\.mean",
349
+ r"full_mean\s*=\s*[Xx]_raw\.mean",
350
+ r"[Xx]_raw\.mean\(dim=0\)",
351
+ ]
352
+ for pat in bad_patterns:
353
+ if re.search(pat, fixed_code):
354
+ return False, "Data leakage still present β€” normalization uses full dataset stats."
355
+ # Check 2: behavioral determinism test
356
+ out1, out2, both_ran = _run_code_twice(fixed_code)
357
+ if not both_ran:
358
+ return False, "Fixed code failed to run twice cleanly."
359
+ acc1 = _extract_metric(out1, r"accuracy[:\s]+([\d.]+)")
360
+ acc2 = _extract_metric(out2, r"accuracy[:\s]+([\d.]+)")
361
+ if acc1 is not None and acc2 is not None:
362
+ if abs(acc1 - acc2) > 0.02:
363
+ return False, f"Eval still non-deterministic: {acc1:.4f} vs {acc2:.4f}. model.eval() may be missing."
364
+ # Check 3: accuracy not suspiciously high (leakage check)
365
+ if acc1 is not None and acc1 > 0.97:
366
+ return False, f"Accuracy {acc1:.4f} still suspiciously high β€” data leakage may remain."
367
+ if "model.eval()" not in fixed_code:
368
+ return False, "model.eval() missing β€” Bug 2 not fixed."
369
  return True, ""
370
 
371
  return True, ""
372
 
373
 
374
+ def _check_success_signal(task_id: str, fixed_code: str, output: str) -> tuple[bool, str]:
375
  lower = output.lower()
376
 
377
  if task_id == "shape_mismatch":
 
379
  has_finished = "training finished" in lower or "complete" in lower
380
  if has_epoch and has_finished:
381
  return True, ""
382
+ return False, "Expected epoch logs and 'Training finished' not found."
383
 
384
  elif task_id == "training_collapse":
385
  if "nan" in lower:
386
+ return False, "Output still contains NaN."
387
  loss_values = re.findall(r"loss[:\s]+([\d.]+)", lower)
388
  if len(loss_values) >= 2:
389
  first, last = float(loss_values[0]), float(loss_values[-1])
390
  if last < first * 0.95:
391
  return True, ""
392
+ return False, f"Loss did not decrease: {first:.4f} β†’ {last:.4f}."
393
+ return "training finished" in lower, "Could not confirm loss decreased."
 
394
 
395
  elif task_id == "data_leakage":
396
+ acc = _extract_metric(output, r"accuracy[:\s]+([\d.]+)")
397
+ if acc is not None and acc > 0.97:
398
+ return False, f"Accuracy {acc:.4f} suspiciously high β€” leakage may still be present."
399
+ mse = _extract_metric(output, r"mse[:\s]+([\d.]+)")
400
+ if mse is not None and mse < 0.04:
401
+ return False, f"MSE {mse:.4f} suspiciously low β€” leakage may still be present."
402
+ return "training finished" in lower, "Training did not complete."
 
 
 
 
 
403
 
404
  elif task_id == "wrong_device":
405
  has_epoch = any(f"epoch {i}" in lower for i in range(1, 4))
406
  has_finished = "training finished" in lower
407
+ no_error = "expected all tensors" not in lower
408
+ if has_epoch and has_finished and no_error:
409
  return True, ""
410
  return False, "Training did not complete cleanly or device error still present."
411
 
 
417
  first, last = float(loss_values[0]), float(loss_values[-1])
418
  if last < first * 0.9:
419
  return True, ""
420
+ return False, f"Loss did not decrease sufficiently: {first:.4f} β†’ {last:.4f}."
421
+ return "training finished" in lower, "Could not confirm stable training."
 
422
 
423
  elif task_id == "missing_eval_mode":
424
+ has_metric = "accuracy" in lower or "mse" in lower
425
+ has_finished = "training finished" in lower or "evaluation complete" in lower
426
+ if has_metric and has_finished:
427
+ return True, ""
428
+ return False, "Evaluation did not complete or metric not reported."
429
+
430
+ elif task_id == "compound_shape_device":
431
+ has_epoch = any(f"epoch {i}" in lower for i in range(1, 4))
432
+ has_finished = "training finished" in lower
433
+ no_errors = "cannot be multiplied" not in lower and "expected all tensors" not in lower
434
+ if has_epoch and has_finished and no_errors:
435
+ return True, ""
436
+ return False, "Training did not complete or one of the bugs is still present."
437
+
438
+ elif task_id == "compound_leakage_eval":
439
+ acc = _extract_metric(output, r"accuracy[:\s]+([\d.]+)")
440
  has_finished = "training finished" in lower or "evaluation complete" in lower
441
+ if acc is not None and acc > 0.97:
442
+ return False, f"Accuracy {acc:.4f} too high β€” data leakage may still be present."
443
+ if has_finished:
444
  return True, ""
445
+ return False, "Training or evaluation did not complete."
446
 
447
  return True, ""
server/ml_debug_env_environment.py CHANGED
@@ -11,15 +11,16 @@ from openenv.core.env_server.types import State
11
  from models import DebugAction, DebugObservation, DebugState
12
  from bug_generator import (
13
  get_scenario,
14
- get_random_task,
15
  BugScenario,
 
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
 
@@ -28,18 +29,23 @@ MAX_STEPS = 3
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
@@ -87,6 +93,7 @@ class MlDebugEnvEnvironment(Environment):
87
  grader_score=None,
88
  grader_feedback=None,
89
  step_number=0,
 
90
  done=False,
91
  reward=None,
92
  )
@@ -113,10 +120,7 @@ class MlDebugEnvEnvironment(Environment):
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
 
121
  return DebugObservation(
122
  task_id=self._state.task_id,
@@ -127,6 +131,7 @@ class MlDebugEnvEnvironment(Environment):
127
  grader_score=result.score,
128
  grader_feedback=result.feedback,
129
  step_number=self._state.step_count,
 
130
  done=done,
131
  reward=result.score,
132
  )
@@ -146,13 +151,10 @@ class MlDebugEnvEnvironment(Environment):
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
  )
 
11
  from models import DebugAction, DebugObservation, DebugState
12
  from bug_generator import (
13
  get_scenario,
 
14
  BugScenario,
15
+ ALL_TASKS,
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
+ TASK_COMPOUND_SHAPE_DEVICE,
23
+ TASK_COMPOUND_LEAKAGE_EVAL,
24
  )
25
  from grader import grade, GradeResult
26
 
 
29
 
30
  class MlDebugEnvEnvironment(Environment):
31
  """
32
+ ML Debugging Environment β€” 8 tasks, easy β†’ expert.
33
+
34
+ Single-bug tasks (6):
35
+ shape_mismatch (easy)
36
+ training_collapse (medium)
37
+ wrong_device (medium)
38
+ gradient_not_zeroed (medium-hard)
39
+ data_leakage (hard)
40
+ missing_eval_mode (hard)
41
+
42
+ Compound tasks β€” TWO bugs per script (2):
43
+ compound_shape_device (medium-hard) β€” shape mismatch + device mismatch
44
+ compound_leakage_eval (expert) β€” data leakage + missing eval mode
45
+
46
+ Graders are execution-based: fixed code is actually run in a subprocess.
47
+ Multi-turn episodes: agent gets up to 3 attempts with grader feedback.
48
+ bug_type="other" skips type check β€” goes straight to execution scoring.
49
  """
50
 
51
  SUPPORTS_CONCURRENT_SESSIONS = True
 
93
  grader_score=None,
94
  grader_feedback=None,
95
  step_number=0,
96
+ num_bugs=scenario.num_bugs,
97
  done=False,
98
  reward=None,
99
  )
 
120
  if result.score > self._state.current_score:
121
  self._state.current_score = result.score
122
 
123
+ done = result.score >= 0.95 or self._state.step_count >= MAX_STEPS
 
 
 
124
 
125
  return DebugObservation(
126
  task_id=self._state.task_id,
 
131
  grader_score=result.score,
132
  grader_feedback=result.feedback,
133
  step_number=self._state.step_count,
134
+ num_bugs=self._current_scenario.num_bugs,
135
  done=done,
136
  reward=result.score,
137
  )
 
151
  name="ML Debugging Environment",
152
  description=(
153
  "An RL environment where agents debug broken PyTorch training scripts. "
154
+ "Eight tasks: six single-bug (easy→hard) and two compound double-bug tasks (expert). "
155
+ "Graders execute fixed code in a subprocess β€” no shortcuts. "
156
+ "Multi-turn episodes with grader feedback. Accepts 'other' as bug_type for open-ended debugging."
 
 
 
157
  ),
158
+ version="3.0.0",
159
  author="ml-debug-env",
160
  )