Spaces:
Running
Running
v3: compound tasks, hardened graders, other type, 8 tasks total
Browse files- __pycache__/models.cpython-310.pyc +0 -0
- inference.py +14 -10
- models.py +13 -29
- openenv.yaml +16 -5
- 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 +73 -47
- server/bug_generator.py +448 -79
- server/grader.py +213 -83
- server/ml_debug_env_environment.py +27 -25
__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
|
| 30 |
|
| 31 |
-
bug_type MUST be exactly one of these strings
|
| 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
|
| 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 |
-
|
| 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\
|
| 66 |
-
user_content += "\n\
|
| 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.
|
| 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
|
| 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',
|
|
|
|
| 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 |
-
|
| 47 |
-
|
| 48 |
-
|
| 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:
|
| 4 |
description: >
|
| 5 |
RL environment where agents debug broken PyTorch training scripts.
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
and
|
|
|
|
| 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
|
| 48 |
-
"The
|
| 49 |
-
"Fix the
|
| 50 |
),
|
| 51 |
-
"success_criteria": "Code runs to completion;
|
| 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
|
| 85 |
-
"Fix tensor placement so training runs cleanly on whatever device is available."
|
| 86 |
),
|
| 87 |
-
"success_criteria": "All tensors on
|
| 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
|
| 97 |
),
|
| 98 |
-
"success_criteria": "Loss decreases consistently
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
},
|
| 100 |
{
|
| 101 |
"task_id": TASK_MISSING_EVAL_MODE,
|
| 102 |
"name": "Missing Eval Mode",
|
| 103 |
"difficulty": "hard",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
"description": (
|
| 105 |
-
"
|
| 106 |
-
"
|
| 107 |
-
"Fix
|
| 108 |
),
|
| 109 |
-
"success_criteria": "model.eval()
|
| 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 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 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,
|
| 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
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
|
| 113 |
return BugScenario(
|
| 114 |
task_id=TASK_SHAPE_MISMATCH,
|
| 115 |
task_description=(
|
| 116 |
-
"This PyTorch
|
| 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=
|
| 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"
|
| 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 |
-
"
|
| 239 |
-
"
|
| 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
|
| 331 |
-
"
|
| 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
|
| 397 |
-
"
|
| 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 |
-
#
|
| 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
|
| 478 |
-
"The model was moved to
|
| 479 |
-
"Every forward pass crashes
|
| 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
|
| 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 |
-
#
|
| 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 |
-
|
|
|
|
| 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 |
-
"
|
| 554 |
-
"
|
| 555 |
-
"
|
| 556 |
-
"
|
| 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
|
| 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
|
| 576 |
)
|
| 577 |
|
| 578 |
|
| 579 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 580 |
# TASK 6 β Missing Eval Mode (Hard)
|
| 581 |
-
#
|
| 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 |
-
|
|
|
|
| 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
|
| 652 |
-
"
|
| 653 |
-
|
| 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
|
| 664 |
-
"
|
| 665 |
-
"The model has Dropout and BatchNorm layers. "
|
| 666 |
-
"Fix the evaluation so it produces stable,
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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 |
-
|
| 83 |
-
|
| 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": {
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
"
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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 |
-
|
| 168 |
-
|
| 169 |
-
if
|
| 170 |
-
|
| 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"
|
| 189 |
for lr_str in lr_matches:
|
| 190 |
try:
|
| 191 |
-
|
| 192 |
-
|
| 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
|
| 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 |
-
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
if "model.eval()" not in fixed_code:
|
| 245 |
return False, "model.eval() is missing before the evaluation block."
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 263 |
|
| 264 |
elif task_id == "training_collapse":
|
| 265 |
if "nan" in lower:
|
| 266 |
-
return False, "Output still contains NaN
|
| 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:
|
| 273 |
-
|
| 274 |
-
return has_finished, "Could not confirm loss decreased across epochs."
|
| 275 |
|
| 276 |
elif task_id == "data_leakage":
|
| 277 |
-
|
| 278 |
-
if
|
| 279 |
-
acc
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 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 |
-
|
| 294 |
-
if has_epoch and has_finished and
|
| 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}
|
| 307 |
-
|
| 308 |
-
return has_finished, "Could not confirm stable training."
|
| 309 |
|
| 310 |
elif task_id == "missing_eval_mode":
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
has_finished = "training finished" in lower or "evaluation complete" in lower
|
| 313 |
-
if
|
|
|
|
|
|
|
| 314 |
return True, ""
|
| 315 |
-
return False, "
|
| 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 |
-
|
|
|
|
| 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 β
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
shape_mismatch (easy)
|
| 35 |
-
training_collapse (medium)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
missing_eval_mode (hard)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
| 150 |
-
"
|
| 151 |
-
"
|
| 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="
|
| 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 |
)
|