Dishaaa25 commited on
Commit
7f2d9e7
·
1 Parent(s): e0dadb6

Smoke-train deployment update

Browse files
README.md CHANGED
@@ -176,7 +176,7 @@ Recommended artifacts to include here:
176
 
177
  ```powershell
178
  cd C:\Users\kaust\PycharmProjects\meta-rl-dsa-solver
179
- python -m venv .venv
180
  .\.venv\Scripts\pip install -e .
181
  ```
182
 
@@ -186,6 +186,12 @@ For training and plotting, also install your training extras:
186
  .\.venv\Scripts\pip install trl unsloth matplotlib wandb
187
  ```
188
 
 
 
 
 
 
 
189
  ### 2. Start the OpenEnv server
190
 
191
  ```powershell
@@ -222,13 +228,13 @@ curl "http://localhost:7860/state?session_id=<SESSION_ID>"
222
  python training\train_grpo.py ^
223
  --generator-mode reward_aware ^
224
  --baseline-eval ^
225
- --output-dir outputs_v3
226
  ```
227
 
228
  ### 7. Plot the training curves
229
 
230
  ```powershell
231
- python training\plot_results.py outputs_v3\reward_curve.csv
232
  ```
233
 
234
  ## Hugging Face Space
 
176
 
177
  ```powershell
178
  cd C:\Users\kaust\PycharmProjects\meta-rl-dsa-solver
179
+ py -3.11 -m venv .venv
180
  .\.venv\Scripts\pip install -e .
181
  ```
182
 
 
186
  .\.venv\Scripts\pip install trl unsloth matplotlib wandb
187
  ```
188
 
189
+ Recommended training target:
190
+
191
+ - Python `3.11`
192
+ - Base model `Qwen/Qwen2.5-3B-Instruct`
193
+ - Single NVIDIA L4 with 4-bit LoRA + Unsloth GRPO
194
+
195
  ### 2. Start the OpenEnv server
196
 
197
  ```powershell
 
228
  python training\train_grpo.py ^
229
  --generator-mode reward_aware ^
230
  --baseline-eval ^
231
+ --output-dir outputs_l4
232
  ```
233
 
234
  ### 7. Plot the training curves
235
 
236
  ```powershell
237
+ python training\plot_results.py outputs_l4\reward_curve.csv
238
  ```
239
 
240
  ## Hugging Face Space
env/__init__.py CHANGED
@@ -1,4 +1,3 @@
1
- from env.adapt_env import AdaptEnvironment
2
- from env.generator import GeneratorAgent, generator_reward, validate_problem
3
 
4
- __all__ = ["AdaptEnvironment", "GeneratorAgent", "generator_reward", "validate_problem"]
 
1
+ from env.generator import GeneratorAgent, generator_reward, normalize_problem, validate_problem
 
2
 
3
+ __all__ = ["GeneratorAgent", "generator_reward", "normalize_problem", "validate_problem"]
env/adapt_env.py CHANGED
@@ -1,12 +1,12 @@
1
  from __future__ import annotations
2
 
3
- import ast
4
  from typing import Any, Generic, TypeVar
5
  from uuid import uuid4
6
 
7
  from env.generator import DIFFICULTY_LABELS, GeneratorAgent, generator_reward, validate_problem
8
  from models import AdaptAction, AdaptObservation, AdaptState
9
- from verifier.metrics import compute_reward
 
10
 
11
  try:
12
  from openenv.core.env_server.interfaces import Environment
@@ -22,7 +22,6 @@ except ImportError:
22
  pass
23
 
24
 
25
- FORBIDDEN_IMPORTS = {"os", "pathlib", "shutil", "socket", "subprocess"}
26
  MAX_STEPS_PER_EPISODE = 3
27
  TARGET_EFFICIENCY_SCORE = 0.95
28
 
@@ -158,76 +157,27 @@ class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
158
  previous_status = self.previous_execution_status
159
  previous_pass_rate = float(self._state.last_pass_rate or 0.0)
160
 
161
- syntax_ok, syntax_error = self._check_syntax(action.code)
162
- if not syntax_ok:
163
- done = attempt_number >= MAX_STEPS_PER_EPISODE
164
- observation = self._build_observation(
165
- reward=0.0,
166
- done=done,
167
- feedback=self._format_static_feedback(
168
- attempt_number=attempt_number,
169
- previous_status=previous_status,
170
- execution_status="syntax_error",
171
- details=f"Syntax error: {syntax_error}",
172
- ),
173
- syntax_valid=False,
174
- execution_status="syntax_error",
175
- reward_components={
176
- "correctness": 0.0,
177
- "step_discount": 1.0 if attempt_number == 1 else (0.85 if attempt_number == 2 else 0.70),
178
- "progress_delta": 0.0,
179
- },
180
- )
181
- self.last_results = []
182
- self.previous_execution_status = observation.execution_status
183
- self._record_metrics(observation)
184
- if done:
185
- self._finalize_episode(observation)
186
- return observation
187
-
188
- safety_ok, safety_error = self._check_safety(action.code)
189
- if not safety_ok:
190
- done = attempt_number >= MAX_STEPS_PER_EPISODE
191
- observation = self._build_observation(
192
- reward=0.0,
193
- done=done,
194
- feedback=self._format_static_feedback(
195
- attempt_number=attempt_number,
196
- previous_status=previous_status,
197
- execution_status="safety_violation",
198
- details=safety_error,
199
- ),
200
- syntax_valid=True,
201
- execution_status="safety_violation",
202
- reward_components={
203
- "correctness": 0.0,
204
- "step_discount": 1.0 if attempt_number == 1 else (0.85 if attempt_number == 2 else 0.70),
205
- "progress_delta": 0.0,
206
- },
207
- )
208
- self.last_results = []
209
- self.previous_execution_status = observation.execution_status
210
- self._record_metrics(observation)
211
- if done:
212
- self._finalize_episode(observation)
213
- return observation
214
-
215
- _, metadata = self._verify_submission(action.code)
216
  self.last_results = list(metadata.get("results", []))
 
217
  hidden_pass_rate = float(metadata.get("hidden_pass_rate", metadata.get("pass_rate", 0.0)))
218
  visible_pass_rate = float(metadata.get("visible_pass_rate", 0.0))
219
  execution_status = str(metadata.get("execution_status", "completed"))
 
 
 
220
  efficiency_score = float(metadata.get("efficiency_score", 0.0))
221
- efficiency_target_met = hidden_pass_rate == 1.0 and efficiency_score >= TARGET_EFFICIENCY_SCORE
222
- done = efficiency_target_met or attempt_number >= MAX_STEPS_PER_EPISODE
223
- reward, reward_components = self._shape_reward(
 
224
  pass_rate=hidden_pass_rate,
225
  step_number=attempt_number,
226
  execution_status=execution_status,
227
  previous_pass_rate=previous_pass_rate,
228
  done=done,
229
  efficiency_score=efficiency_score,
230
- optimization_target_met=efficiency_target_met,
231
  )
232
  feedback = self._format_feedback(
233
  results=self.last_results,
@@ -238,8 +188,19 @@ class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
238
  visible_pass_rate=visible_pass_rate,
239
  efficiency_score=efficiency_score,
240
  optimization_hints=list(metadata.get("optimization_hints", [])),
241
- optimization_target_met=efficiency_target_met,
 
242
  )
 
 
 
 
 
 
 
 
 
 
243
  observation = self._build_observation(
244
  reward=reward,
245
  done=done,
@@ -247,7 +208,7 @@ class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
247
  pass_rate=hidden_pass_rate,
248
  visible_pass_rate=visible_pass_rate,
249
  hidden_pass_rate=hidden_pass_rate,
250
- syntax_valid=True,
251
  execution_status=execution_status,
252
  timeout_count=int(metadata.get("timeout_count", 0)),
253
  runtime_error_count=int(metadata.get("runtime_error_count", 0)),
@@ -337,23 +298,15 @@ class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
337
  raise ValueError("Generator produced an invalid problem twice in a row.")
338
  return fallback
339
 
340
- def _verify_submission(self, code: str) -> tuple[float, dict[str, Any]]:
341
- try:
342
- from verifier.verifier import verify
343
- except ImportError as exc:
344
- return 0.0, {
345
- "feedback": f"Verifier unavailable: {exc}",
346
- "execution_status": "verifier_error",
347
- "results": [],
348
- }
349
-
350
  try:
351
- reward, metadata = verify(code, self.test_cases)
352
  except Exception as exc:
353
  return 0.0, {
354
  "feedback": f"Verifier crashed: {exc}",
355
  "execution_status": "verifier_error",
356
  "results": [],
 
357
  }
358
 
359
  metadata = dict(metadata or {})
@@ -367,48 +320,6 @@ class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
367
  )
368
  return float(reward), metadata
369
 
370
- def _shape_reward(
371
- self,
372
- pass_rate: float,
373
- step_number: int,
374
- execution_status: str,
375
- previous_pass_rate: float,
376
- done: bool,
377
- efficiency_score: float,
378
- optimization_target_met: bool,
379
- ) -> tuple[float, dict[str, float]]:
380
- step_discount = 1.0 if step_number == 1 else (0.85 if step_number == 2 else 0.70)
381
- progress_delta = max(0.0, float(pass_rate) - float(previous_pass_rate))
382
- efficiency_score = max(0.0, min(float(efficiency_score), 1.0))
383
-
384
- if execution_status in {"timeout", "syntax_error", "safety_violation"}:
385
- reward = 0.0
386
- elif pass_rate == 1.0:
387
- reward = round(
388
- compute_reward(
389
- pass_rate=pass_rate,
390
- step_number=step_number,
391
- execution_status=execution_status,
392
- format_compliance=0.0,
393
- )
394
- * (0.6 + 0.4 * efficiency_score),
395
- 4,
396
- )
397
- if not optimization_target_met and not done:
398
- reward = min(reward, 0.94)
399
- elif done:
400
- reward = 0.0
401
- else:
402
- reward = round(0.1 * progress_delta, 4)
403
-
404
- return reward, {
405
- "correctness": round(float(pass_rate), 4),
406
- "efficiency_score": round(efficiency_score, 4),
407
- "step_discount": round(step_discount, 4),
408
- "progress_delta": round(progress_delta, 4),
409
- "reward": round(float(reward), 4),
410
- }
411
-
412
  def _format_feedback(
413
  self,
414
  results: list[dict[str, Any]],
@@ -420,7 +331,16 @@ class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
420
  efficiency_score: float,
421
  optimization_hints: list[str],
422
  optimization_target_met: bool,
 
423
  ) -> str:
 
 
 
 
 
 
 
 
424
  lines = [
425
  f"Attempt {attempt_number}/{MAX_STEPS_PER_EPISODE}.",
426
  f"Previous attempt status: {previous_status}.",
@@ -556,9 +476,7 @@ class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
556
  return ""
557
  chunks = []
558
  for test_case in visible_cases:
559
- chunks.append(
560
- f"Input:\n{test_case['input']}Expected Output:\n{test_case['output']}\n"
561
- )
562
  return "\n".join(chunks).rstrip()
563
 
564
  def _diversity_bonus(self, problem_type: str) -> float:
@@ -569,29 +487,6 @@ class AdaptEnvironment(Environment[AdaptAction, AdaptObservation, AdaptState]):
569
  return 0.0
570
  return 0.1
571
 
572
- def _check_syntax(self, code: str) -> tuple[bool, str]:
573
- try:
574
- ast.parse(code)
575
- except SyntaxError as exc:
576
- return False, str(exc)
577
- return True, ""
578
-
579
- def _check_safety(self, code: str) -> tuple[bool, str]:
580
- tree = ast.parse(code)
581
- for node in ast.walk(tree):
582
- if isinstance(node, ast.Import):
583
- for alias in node.names:
584
- root_name = alias.name.split(".", 1)[0]
585
- if root_name in FORBIDDEN_IMPORTS:
586
- return False, f"Forbidden import: {root_name}"
587
-
588
- if isinstance(node, ast.ImportFrom):
589
- root_name = (node.module or "").split(".", 1)[0]
590
- if root_name in FORBIDDEN_IMPORTS:
591
- return False, f"Forbidden import: {root_name}"
592
-
593
- return True, ""
594
-
595
  def _tier_to_difficulty(self, tier: int) -> str:
596
  return DIFFICULTY_LABELS.get(tier, "easy")
597
 
 
1
  from __future__ import annotations
2
 
 
3
  from typing import Any, Generic, TypeVar
4
  from uuid import uuid4
5
 
6
  from env.generator import DIFFICULTY_LABELS, GeneratorAgent, generator_reward, validate_problem
7
  from models import AdaptAction, AdaptObservation, AdaptState
8
+ from verifier.metrics import compute_episode_reward
9
+ from verifier.verifier import verify
10
 
11
  try:
12
  from openenv.core.env_server.interfaces import Environment
 
22
  pass
23
 
24
 
 
25
  MAX_STEPS_PER_EPISODE = 3
26
  TARGET_EFFICIENCY_SCORE = 0.95
27
 
 
157
  previous_status = self.previous_execution_status
158
  previous_pass_rate = float(self._state.last_pass_rate or 0.0)
159
 
160
+ _, metadata = self._verify_submission(action.code, attempt_number=attempt_number)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  self.last_results = list(metadata.get("results", []))
162
+
163
  hidden_pass_rate = float(metadata.get("hidden_pass_rate", metadata.get("pass_rate", 0.0)))
164
  visible_pass_rate = float(metadata.get("visible_pass_rate", 0.0))
165
  execution_status = str(metadata.get("execution_status", "completed"))
166
+ syntax_valid = bool(metadata.get("syntax_valid", execution_status != "syntax_error"))
167
+ safety_valid = bool(metadata.get("safety_valid", execution_status != "safety_violation"))
168
+ error_detail = str(metadata.get("error", "")).strip()
169
  efficiency_score = float(metadata.get("efficiency_score", 0.0))
170
+ optimization_target_met = hidden_pass_rate == 1.0 and efficiency_score >= TARGET_EFFICIENCY_SCORE
171
+ done = optimization_target_met or attempt_number >= MAX_STEPS_PER_EPISODE
172
+
173
+ reward, reward_components = compute_episode_reward(
174
  pass_rate=hidden_pass_rate,
175
  step_number=attempt_number,
176
  execution_status=execution_status,
177
  previous_pass_rate=previous_pass_rate,
178
  done=done,
179
  efficiency_score=efficiency_score,
180
+ optimization_target_met=optimization_target_met,
181
  )
182
  feedback = self._format_feedback(
183
  results=self.last_results,
 
188
  visible_pass_rate=visible_pass_rate,
189
  efficiency_score=efficiency_score,
190
  optimization_hints=list(metadata.get("optimization_hints", [])),
191
+ optimization_target_met=optimization_target_met,
192
+ error_detail=error_detail,
193
  )
194
+
195
+ reward_components.update(
196
+ {
197
+ "format_compliance": round(float(metadata.get("format_compliance", 0.0)), 4),
198
+ "anti_cheat_compliance": round(1.0 if safety_valid and syntax_valid else 0.0, 4),
199
+ "hidden_correctness": round(hidden_pass_rate, 4),
200
+ "visible_correctness": round(visible_pass_rate, 4),
201
+ }
202
+ )
203
+
204
  observation = self._build_observation(
205
  reward=reward,
206
  done=done,
 
208
  pass_rate=hidden_pass_rate,
209
  visible_pass_rate=visible_pass_rate,
210
  hidden_pass_rate=hidden_pass_rate,
211
+ syntax_valid=syntax_valid,
212
  execution_status=execution_status,
213
  timeout_count=int(metadata.get("timeout_count", 0)),
214
  runtime_error_count=int(metadata.get("runtime_error_count", 0)),
 
298
  raise ValueError("Generator produced an invalid problem twice in a row.")
299
  return fallback
300
 
301
+ def _verify_submission(self, code: str, *, attempt_number: int) -> tuple[float, dict[str, Any]]:
 
 
 
 
 
 
 
 
 
302
  try:
303
+ reward, metadata = verify(code, self.test_cases, step_number=attempt_number)
304
  except Exception as exc:
305
  return 0.0, {
306
  "feedback": f"Verifier crashed: {exc}",
307
  "execution_status": "verifier_error",
308
  "results": [],
309
+ "error": str(exc),
310
  }
311
 
312
  metadata = dict(metadata or {})
 
320
  )
321
  return float(reward), metadata
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  def _format_feedback(
324
  self,
325
  results: list[dict[str, Any]],
 
331
  efficiency_score: float,
332
  optimization_hints: list[str],
333
  optimization_target_met: bool,
334
+ error_detail: str,
335
  ) -> str:
336
+ if execution_status in {"syntax_error", "safety_violation"}:
337
+ return self._format_static_feedback(
338
+ attempt_number=attempt_number,
339
+ previous_status=previous_status,
340
+ execution_status=execution_status,
341
+ details=error_detail or execution_status.replace("_", " ").title(),
342
+ )
343
+
344
  lines = [
345
  f"Attempt {attempt_number}/{MAX_STEPS_PER_EPISODE}.",
346
  f"Previous attempt status: {previous_status}.",
 
476
  return ""
477
  chunks = []
478
  for test_case in visible_cases:
479
+ chunks.append(f"Input:\n{test_case['input']}Expected Output:\n{test_case['output']}\n")
 
 
480
  return "\n".join(chunks).rstrip()
481
 
482
  def _diversity_bonus(self, problem_type: str) -> float:
 
487
  return 0.0
488
  return 0.1
489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  def _tier_to_difficulty(self, tier: int) -> str:
491
  return DIFFICULTY_LABELS.get(tier, "easy")
492
 
env/app.py CHANGED
@@ -1,13 +1,176 @@
 
 
 
 
1
  import gradio as gr
2
 
3
- def solve(problem):
4
- return "Model output will come here"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- demo = gr.Interface(
7
- fn=solve,
8
- inputs="text",
9
- outputs="text",
10
- title="DSA Solver"
11
- )
12
 
13
- demo.launch()
 
 
1
+ from __future__ import annotations
2
+
3
+ from uuid import uuid4
4
+
5
  import gradio as gr
6
 
7
+ from env.adapt_env import AdaptEnvironment
8
+ from models import AdaptAction
9
+ from server.runtime import SpaceTrainingManager
10
+
11
+ TRAINING_MANAGER = SpaceTrainingManager()
12
+ SESSIONS: dict[str, AdaptEnvironment] = {}
13
+
14
+
15
+ def _get_env(session_id: str | None) -> AdaptEnvironment:
16
+ if not session_id or session_id not in SESSIONS:
17
+ session_id = str(uuid4())
18
+ SESSIONS[session_id] = AdaptEnvironment(session_id=session_id)
19
+ return SESSIONS[session_id]
20
+
21
+
22
+ def _problem_markdown(observation: dict) -> str:
23
+ return (
24
+ f"### {observation['problem_type']} ({observation['difficulty']})\n\n"
25
+ f"{observation['problem']}\n\n"
26
+ f"**Input Format**\n{observation['input_format']}\n\n"
27
+ f"**Constraints**\n{observation['constraints']}"
28
+ )
29
+
30
+
31
+ def sample_problem(problem_id: str, difficulty: str) -> tuple[str, str, str, str, dict]:
32
+ env = _get_env(None)
33
+ observation = env.reset(
34
+ problem_id=problem_id or None,
35
+ difficulty=difficulty or None,
36
+ session_id=env.session_id,
37
+ )
38
+ payload = observation.model_dump()
39
+ return (
40
+ env.session_id,
41
+ _problem_markdown(payload),
42
+ payload["feedback"],
43
+ "",
44
+ payload,
45
+ )
46
+ def evaluate_submission(session_id: str, code: str) -> tuple[str, str, str, dict]:
47
+ env = _get_env(session_id)
48
+ observation = env.step(AdaptAction(session_id=env.session_id, code=code))
49
+ payload = observation.model_dump()
50
+ status = (
51
+ f"Reward: {payload['reward']:.2f} | Hidden pass rate: {payload['hidden_pass_rate']:.2f} | "
52
+ f"Visible pass rate: {payload['visible_pass_rate']:.2f} | Status: {payload['execution_status']}"
53
+ )
54
+ return payload["feedback"], status, code, payload
55
+
56
+
57
+ def model_attempt(session_id: str) -> tuple[str, str, str, dict]:
58
+ env = _get_env(session_id)
59
+ if not env.problem:
60
+ observation = env.reset(session_id=env.session_id)
61
+ else:
62
+ observation = env._build_observation(
63
+ reward=float(env.state.last_reward or 0.0),
64
+ done=bool(env.episode_done),
65
+ feedback=env.state.last_feedback or "No attempt yet.",
66
+ pass_rate=float(env.state.last_pass_rate or 0.0),
67
+ visible_pass_rate=float(env.state.recent_metrics.get("visible_pass_rate", 0.0)),
68
+ hidden_pass_rate=float(env.state.last_pass_rate or 0.0),
69
+ syntax_valid=env.state.last_execution_status != "syntax_error",
70
+ execution_status=env.state.last_execution_status or "ready",
71
+ timeout_count=int(env.state.recent_metrics.get("timeout_count", 0)),
72
+ runtime_error_count=int(env.state.recent_metrics.get("runtime_error_count", 0)),
73
+ invalid_output_count=int(env.state.recent_metrics.get("invalid_output_count", 0)),
74
+ wrong_answer_count=int(env.state.recent_metrics.get("wrong_answer_count", 0)),
75
+ format_compliance=float(env.state.recent_metrics.get("format_compliance", 0.0)),
76
+ reward_components=dict(env.state.recent_metrics.get("reward_components", {})),
77
+ generator_reward_signal=float(env.state.generator_reward_signal or 0.0),
78
+ )
79
+
80
+ try:
81
+ generation = TRAINING_MANAGER.generate_code(
82
+ problem=observation.problem,
83
+ input_format=observation.input_format,
84
+ constraints=observation.constraints,
85
+ feedback=observation.feedback,
86
+ problem_id=observation.problem_id,
87
+ problem_type=observation.problem_type,
88
+ difficulty=observation.difficulty,
89
+ attempt_number=observation.attempt_number + 1,
90
+ max_steps=observation.max_steps,
91
+ )
92
+ except Exception as exc:
93
+ return str(exc), "Model generation unavailable", "", {"error": str(exc)}
94
+
95
+ return evaluate_submission(session_id, generation["code"])
96
+
97
+
98
+ with gr.Blocks(
99
+ title="ADAPT DSA Tutor Demo",
100
+ css="""
101
+ .panel {border: 1px solid #d7d3c9; border-radius: 18px; background: #fffaf2;}
102
+ .hero {background: linear-gradient(135deg, #f7eedb, #f3f8ef); border-radius: 22px; padding: 18px;}
103
+ """,
104
+ ) as demo:
105
+ session_id = gr.Textbox(label="Session ID", interactive=False)
106
+ state_payload = gr.JSON(label="Observation Payload")
107
+
108
+ gr.Markdown(
109
+ """
110
+ # ADAPT DSA Tutor
111
+ Sample a problem, inspect the verifier feedback, and compare your repair attempt with the currently loaded model path.
112
+ """,
113
+ elem_classes=["hero"],
114
+ )
115
+
116
+ with gr.Row():
117
+ problem_id = gr.Dropdown(
118
+ choices=[
119
+ "",
120
+ "sum_even_numbers",
121
+ "range_span",
122
+ "count_vowels",
123
+ "max_consecutive_ones",
124
+ "fizzbuzz_variant",
125
+ "running_total",
126
+ "count_local_peaks",
127
+ "longest_non_decreasing_run",
128
+ "two_sum_count",
129
+ "max_subarray_sum",
130
+ "group_anagrams_count",
131
+ "balanced_brackets",
132
+ "matrix_diagonal_sum",
133
+ "smallest_most_frequent",
134
+ "reverse_words",
135
+ "longest_common_subsequence",
136
+ "word_ladder_steps",
137
+ "merge_intervals",
138
+ "min_coins",
139
+ "rotate_matrix_90",
140
+ ],
141
+ value="",
142
+ label="Problem Family",
143
+ info="Leave blank to sample automatically.",
144
+ )
145
+ difficulty = gr.Radio(choices=["easy", "medium", "hard"], value="easy", label="Difficulty")
146
+ sample_btn = gr.Button("Sample Problem", variant="primary")
147
+
148
+ problem_view = gr.Markdown(elem_classes=["panel"])
149
+ with gr.Row():
150
+ code = gr.Textbox(label="Python Submission", lines=18, max_lines=24, placeholder="Write code that reads stdin and prints stdout.")
151
+ with gr.Column():
152
+ feedback = gr.Textbox(label="Verifier Feedback", lines=14)
153
+ status = gr.Textbox(label="Scorecard", lines=4)
154
+ with gr.Row():
155
+ verify_btn = gr.Button("Verify Submission", variant="primary")
156
+ model_btn = gr.Button("Run Current Model", variant="secondary")
157
+
158
+ sample_btn.click(
159
+ fn=sample_problem,
160
+ inputs=[problem_id, difficulty],
161
+ outputs=[session_id, problem_view, feedback, code, state_payload],
162
+ )
163
+ verify_btn.click(
164
+ fn=evaluate_submission,
165
+ inputs=[session_id, code],
166
+ outputs=[feedback, status, code, state_payload],
167
+ )
168
+ model_btn.click(
169
+ fn=model_attempt,
170
+ inputs=[session_id],
171
+ outputs=[feedback, status, code, state_payload],
172
+ )
173
 
 
 
 
 
 
 
174
 
175
+ if __name__ == "__main__":
176
+ demo.launch()
env/executor.py CHANGED
@@ -1,48 +1,97 @@
1
  from __future__ import annotations
2
 
3
  import os
 
4
  import shutil
5
  import subprocess
6
  import sys
 
 
7
  from pathlib import Path
8
- from uuid import uuid4
9
 
 
 
 
10
 
11
- TIMEOUT_SECONDS = 1
12
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- def run_code(code: str, input_data: str, timeout_seconds: int | float | None = None) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  temp_parent = Path(os.getenv("ADAPT_TMP_DIR", ".adapt_tmp")).resolve()
16
  temp_parent.mkdir(parents=True, exist_ok=True)
17
- tmpdir = temp_parent / f"run_{uuid4().hex}"
18
- tmpdir.mkdir()
19
- timeout_value = TIMEOUT_SECONDS if timeout_seconds is None else timeout_seconds
20
 
21
- try:
22
- file_path = Path(tmpdir) / "submission.py"
23
- file_path.write_text(code, encoding="utf-8")
24
 
 
 
25
  try:
26
  result = subprocess.run(
27
- [sys.executable, str(file_path)],
28
  input=input_data,
29
  text=True,
30
  capture_output=True,
31
  timeout=timeout_value,
 
 
 
32
  )
33
  except subprocess.TimeoutExpired as exc:
 
34
  return {
35
- "stdout": exc.stdout or "",
36
  "stderr": "Execution timed out",
37
  "exit_code": -1,
38
  "timed_out": True,
 
 
 
39
  }
40
 
 
 
 
41
  return {
42
- "stdout": result.stdout,
43
- "stderr": result.stderr,
44
- "exit_code": result.returncode,
45
  "timed_out": False,
 
 
 
46
  }
47
  finally:
48
- shutil.rmtree(tmpdir, ignore_errors=True)
 
1
  from __future__ import annotations
2
 
3
  import os
4
+ import platform
5
  import shutil
6
  import subprocess
7
  import sys
8
+ import tempfile
9
+ import time
10
  from pathlib import Path
11
+ from typing import Any
12
 
13
+ TIMEOUT_SECONDS = 1.0
14
+ MEMORY_LIMIT_MB = 512
15
+ OUTPUT_LIMIT_BYTES = 256 * 1024
16
 
 
17
 
18
+ def _sandbox_env(tmpdir: Path) -> dict[str, str]:
19
+ # Run user code with a tightly scoped environment to reduce hidden state.
20
+ return {
21
+ "PYTHONIOENCODING": "utf-8",
22
+ "PYTHONUNBUFFERED": "1",
23
+ "PYTHONNOUSERSITE": "1",
24
+ "HOME": str(tmpdir),
25
+ "TMPDIR": str(tmpdir),
26
+ "TEMP": str(tmpdir),
27
+ "TMP": str(tmpdir),
28
+ }
29
 
30
+
31
+ def _linux_preexec_fn(timeout_seconds: float) -> Any:
32
+ if platform.system().lower() != "linux":
33
+ return None
34
+
35
+ def _apply_limits() -> None:
36
+ import resource
37
+
38
+ memory_limit = MEMORY_LIMIT_MB * 1024 * 1024
39
+ cpu_limit = max(1, int(timeout_seconds) + 1)
40
+ file_limit = OUTPUT_LIMIT_BYTES
41
+
42
+ resource.setrlimit(resource.RLIMIT_AS, (memory_limit, memory_limit))
43
+ resource.setrlimit(resource.RLIMIT_CPU, (cpu_limit, cpu_limit))
44
+ resource.setrlimit(resource.RLIMIT_FSIZE, (file_limit, file_limit))
45
+ resource.setrlimit(resource.RLIMIT_NOFILE, (32, 32))
46
+
47
+ return _apply_limits
48
+
49
+
50
+ def run_code(code: str, input_data: str, timeout_seconds: int | float | None = None) -> dict[str, Any]:
51
+ timeout_value = float(TIMEOUT_SECONDS if timeout_seconds is None else timeout_seconds)
52
  temp_parent = Path(os.getenv("ADAPT_TMP_DIR", ".adapt_tmp")).resolve()
53
  temp_parent.mkdir(parents=True, exist_ok=True)
 
 
 
54
 
55
+ tmpdir_path = Path(tempfile.mkdtemp(prefix="run_", dir=str(temp_parent)))
56
+ submission_path = tmpdir_path / "submission.py"
57
+ submission_path.write_text(code, encoding="utf-8")
58
 
59
+ started = time.perf_counter()
60
+ try:
61
  try:
62
  result = subprocess.run(
63
+ [sys.executable, "-I", "-S", str(submission_path)],
64
  input=input_data,
65
  text=True,
66
  capture_output=True,
67
  timeout=timeout_value,
68
+ cwd=str(tmpdir_path),
69
+ env=_sandbox_env(tmpdir_path),
70
+ preexec_fn=_linux_preexec_fn(timeout_value),
71
  )
72
  except subprocess.TimeoutExpired as exc:
73
+ duration_ms = round((time.perf_counter() - started) * 1000, 2)
74
  return {
75
+ "stdout": str(exc.stdout or ""),
76
  "stderr": "Execution timed out",
77
  "exit_code": -1,
78
  "timed_out": True,
79
+ "duration_ms": duration_ms,
80
+ "sandboxed": True,
81
+ "sandbox_mode": "linux_limited" if platform.system().lower() == "linux" else "portable",
82
  }
83
 
84
+ duration_ms = round((time.perf_counter() - started) * 1000, 2)
85
+ stdout = result.stdout[:OUTPUT_LIMIT_BYTES]
86
+ stderr = result.stderr[:OUTPUT_LIMIT_BYTES]
87
  return {
88
+ "stdout": stdout,
89
+ "stderr": stderr,
90
+ "exit_code": int(result.returncode),
91
  "timed_out": False,
92
+ "duration_ms": duration_ms,
93
+ "sandboxed": True,
94
+ "sandbox_mode": "linux_limited" if platform.system().lower() == "linux" else "portable",
95
  }
96
  finally:
97
+ shutil.rmtree(tmpdir_path, ignore_errors=True)
env/generator.py CHANGED
@@ -104,6 +104,16 @@ def validate_problem(problem_dict: dict[str, Any]) -> bool:
104
  return True
105
 
106
 
 
 
 
 
 
 
 
 
 
 
107
  class GeneratorAgent:
108
  """Deterministic, dependency-free generator for DSA-style problems."""
109
 
@@ -157,8 +167,9 @@ class GeneratorAgent:
157
  "generation_mode": "deterministic_fallback" if self.deterministic else "local_rule_based",
158
  "validity_bonus": 0.15,
159
  }
160
- if validate_problem(problem):
161
- return problem
 
162
 
163
  raise ValueError(f"Unable to generate a valid problem for template {template.problem_type}")
164
 
 
104
  return True
105
 
106
 
107
+ def normalize_problem(problem_dict: dict[str, Any]) -> dict[str, Any]:
108
+ normalized = dict(problem_dict)
109
+ normalized["problem"] = str(problem_dict.get("problem", "")).strip()
110
+ normalized["input_format"] = str(problem_dict.get("input_format", "")).strip()
111
+ normalized["constraints"] = str(problem_dict.get("constraints", "")).strip()
112
+ normalized["test_cases"] = [dict(test_case) for test_case in problem_dict.get("test_cases", [])]
113
+ normalized["visible_problem"] = dict(problem_dict.get("visible_problem", {}))
114
+ return normalized
115
+
116
+
117
  class GeneratorAgent:
118
  """Deterministic, dependency-free generator for DSA-style problems."""
119
 
 
167
  "generation_mode": "deterministic_fallback" if self.deterministic else "local_rule_based",
168
  "validity_bonus": 0.15,
169
  }
170
+ normalized_problem = normalize_problem(problem)
171
+ if validate_problem(normalized_problem):
172
+ return normalized_problem
173
 
174
  raise ValueError(f"Unable to generate a valid problem for template {template.problem_type}")
175
 
pyproject.toml CHANGED
@@ -7,8 +7,9 @@ name = "adapt-dsa-tutor"
7
  version = "0.2.0"
8
  description = "OpenEnv-compliant adversarial DSA tutor environment for RLVR code generation."
9
  readme = "README.md"
10
- requires-python = ">=3.10"
11
  dependencies = [
 
12
  "openenv-core>=0.2.3",
13
  "fastapi>=0.104.0",
14
  "pydantic>=2.0.0",
 
7
  version = "0.2.0"
8
  description = "OpenEnv-compliant adversarial DSA tutor environment for RLVR code generation."
9
  readme = "README.md"
10
+ requires-python = ">=3.11,<3.12"
11
  dependencies = [
12
+ "gradio>=6.0.0",
13
  "openenv-core>=0.2.3",
14
  "fastapi>=0.104.0",
15
  "pydantic>=2.0.0",
requirements.txt CHANGED
@@ -1,7 +1,8 @@
1
- openenv
2
- fastapi
3
- uvicorn
4
- pydantic
5
- huggingface_hub
6
- transformers
7
- peft
 
 
1
+ gradio>=6.0.0
2
+ openenv-core>=0.2.3
3
+ fastapi>=0.104.0
4
+ uvicorn>=0.24.0
5
+ pydantic>=2.0.0
6
+ huggingface_hub>=0.30.0
7
+ transformers>=4.48.0
8
+ peft>=0.14.0
scripts/deploy_and_smoke_train.py CHANGED
@@ -8,16 +8,79 @@ import sys
8
  import time
9
  import urllib.error
10
  import urllib.request
 
11
  from pathlib import Path
12
  from typing import Any
13
 
14
-
15
  DEFAULT_REMOTE = "space"
16
  DEFAULT_REMOTE_BRANCH = "main"
17
  DEFAULT_POLL_INTERVAL_SECONDS = 10
18
- DEFAULT_TIMEOUT_SECONDS = 60 * 30
 
19
  DEFAULT_REQUIRED_HEALTHY_CHECKS = 3
20
  DEFAULT_MIN_DEPLOY_WAIT_SECONDS = 30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
 
23
  def run_command(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
@@ -34,7 +97,7 @@ def ensure_success(result: subprocess.CompletedProcess[str], action: str) -> Non
34
  if result.returncode == 0:
35
  return
36
  message = result.stderr.strip() or result.stdout.strip() or f"{action} failed with exit code {result.returncode}"
37
- raise RuntimeError(f"{action} failed: {message}")
38
 
39
 
40
  def has_uncommitted_changes(repo_root: Path) -> bool:
@@ -43,29 +106,6 @@ def has_uncommitted_changes(repo_root: Path) -> bool:
43
  return bool(result.stdout.strip())
44
 
45
 
46
- def commit_changes(repo_root: Path, message: str) -> str | None:
47
- if not has_uncommitted_changes(repo_root):
48
- print("No uncommitted changes found. Reusing the current HEAD commit.", flush=True)
49
- return None
50
-
51
- add_result = run_command(["git", "add", "-A"], repo_root)
52
- ensure_success(add_result, "git add")
53
-
54
- commit_result = run_command(["git", "commit", "-m", message], repo_root)
55
- ensure_success(commit_result, "git commit")
56
- print(commit_result.stdout.strip(), flush=True)
57
-
58
- rev_result = run_command(["git", "rev-parse", "HEAD"], repo_root)
59
- ensure_success(rev_result, "git rev-parse")
60
- return rev_result.stdout.strip()
61
-
62
-
63
- def push_to_space(repo_root: Path, remote: str, remote_branch: str) -> None:
64
- push_result = run_command(["git", "push", remote, f"HEAD:{remote_branch}"], repo_root)
65
- ensure_success(push_result, f"git push {remote} HEAD:{remote_branch}")
66
- print(push_result.stdout.strip() or push_result.stderr.strip(), flush=True)
67
-
68
-
69
  def current_head_sha(repo_root: Path) -> str:
70
  result = run_command(["git", "rev-parse", "HEAD"], repo_root)
71
  ensure_success(result, "git rev-parse HEAD")
@@ -81,247 +121,382 @@ def remote_branch_sha(repo_root: Path, remote: str, remote_branch: str) -> str |
81
  return line.split()[0]
82
 
83
 
84
- def deployment_needed(repo_root: Path, remote: str, remote_branch: str) -> bool:
85
- local_sha = current_head_sha(repo_root)
86
- remote_sha = remote_branch_sha(repo_root, remote, remote_branch)
87
- if remote_sha is None:
88
- print(
89
- f"Remote branch {remote}/{remote_branch} does not exist yet. A deployment push is required.",
90
- flush=True,
91
- )
92
- return True
93
 
94
- if local_sha == remote_sha:
95
- print(
96
- f"Skipping Space deploy because {remote}/{remote_branch} is already at local HEAD {local_sha}.",
97
- flush=True,
98
  )
99
- return False
100
 
101
- print(
102
- f"Space deploy required: local HEAD {local_sha} differs from {remote}/{remote_branch} {remote_sha}.",
103
- flush=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  )
105
  return True
106
 
107
 
108
- def http_json(method: str, url: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
109
- data = None
110
- headers = {}
111
- if payload is not None:
112
- data = json.dumps(payload).encode("utf-8")
113
- headers["Content-Type"] = "application/json"
 
114
 
115
- request = urllib.request.Request(url=url, data=data, headers=headers, method=method)
116
- try:
117
- with urllib.request.urlopen(request, timeout=60) as response:
118
- raw = response.read().decode("utf-8")
119
- return json.loads(raw)
120
- except urllib.error.HTTPError as exc:
121
- body = exc.read().decode("utf-8", errors="replace")
122
- raise RuntimeError(f"HTTP {exc.code} for {url}: {body}") from exc
123
- except urllib.error.URLError as exc:
124
- raise RuntimeError(f"Request to {url} failed: {exc}") from exc
125
 
 
 
126
 
127
- def try_get_space_health(health_url: str) -> dict[str, Any] | None:
128
- try:
129
- return http_json("GET", health_url)
130
- except RuntimeError:
131
- return None
132
 
133
-
134
- def wait_for_space_health(
135
  base_url: str,
 
136
  timeout_seconds: int,
137
  poll_interval_seconds: int,
138
  required_healthy_checks: int,
139
  min_deploy_wait_seconds: int,
140
- ) -> None:
141
- health_url = f"{base_url.rstrip('/')}/health"
142
  deadline = time.time() + timeout_seconds
143
  push_started_at = time.time()
144
- previous_payload = try_get_space_health(health_url)
145
- deployment_transition_seen = previous_payload is None
146
- fallback_notice_emitted = False
147
- consecutive_healthy_checks = 0
148
 
149
  while time.time() < deadline:
150
  try:
151
- payload = http_json("GET", health_url)
152
- except RuntimeError as exc:
153
- deployment_transition_seen = True
154
- consecutive_healthy_checks = 0
155
- print(f"Waiting for Space deployment: {exc}", flush=True)
156
  time.sleep(poll_interval_seconds)
157
  continue
158
 
159
- if previous_payload != payload:
160
- deployment_transition_seen = True
161
-
162
- waited_long_enough = (time.time() - push_started_at) >= min_deploy_wait_seconds
163
 
164
- if not deployment_transition_seen and not waited_long_enough:
165
- print(
166
- "Space is still serving the pre-push health payload; waiting for the new deployment to take over.",
167
- flush=True,
168
- )
169
- previous_payload = payload
170
- time.sleep(poll_interval_seconds)
171
- continue
172
-
173
- if not deployment_transition_seen and waited_long_enough and not fallback_notice_emitted:
174
- print(
175
- "No observable health payload transition detected after the minimum deploy wait. "
176
- "Falling back to healthy-check stabilization.",
177
- flush=True,
178
- )
179
- fallback_notice_emitted = True
180
-
181
- if not waited_long_enough:
182
- remaining = max(0, min_deploy_wait_seconds - int(time.time() - push_started_at))
183
- print(
184
- f"Deployment transition detected. Waiting {remaining}s more for the Space to stabilize.",
185
- flush=True,
186
- )
187
- previous_payload = payload
188
  time.sleep(poll_interval_seconds)
189
  continue
190
 
191
  if payload.get("status") == "healthy":
192
- consecutive_healthy_checks += 1
193
- print(
194
- f"Space healthy check {consecutive_healthy_checks}/{required_healthy_checks}: {payload}",
195
- flush=True,
196
  )
197
- if consecutive_healthy_checks >= required_healthy_checks:
198
- print("Space deployment looks stable. Starting smoke training.", flush=True)
199
- return
 
200
  else:
201
- consecutive_healthy_checks = 0
202
- print(f"Space health not ready yet: {payload}", flush=True)
203
 
204
- previous_payload = payload
205
  time.sleep(poll_interval_seconds)
206
 
207
- raise TimeoutError(f"Space did not finish deploying within {timeout_seconds} seconds.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
 
210
- def start_smoke_training(base_url: str) -> dict[str, Any]:
211
- train_url = f"{base_url.rstrip('/')}/train"
212
- payload = http_json("POST", train_url, {"preset": "smoke"})
213
- print(f"Started smoke training: {json.dumps(payload, indent=2)}", flush=True)
214
  return payload
215
 
216
 
217
- def poll_training_status(base_url: str, poll_interval_seconds: int, timeout_seconds: int) -> dict[str, Any]:
218
- status_url = f"{base_url.rstrip('/')}/train/status"
 
 
 
 
219
  deadline = time.time() + timeout_seconds
 
220
 
221
  while time.time() < deadline:
222
- payload = http_json("GET", status_url)
223
- status = payload.get("status")
224
- phase = payload.get("phase")
225
- completed_steps = payload.get("completed_steps")
226
- total_steps = payload.get("total_steps")
227
- print(
228
- f"Training status: status={status} phase={phase} completed_steps={completed_steps}/{total_steps}",
229
- flush=True,
 
230
  )
 
 
 
231
 
 
232
  if status == "failed":
233
- print("Training failed. Full error payload:", flush=True)
234
- print(json.dumps(payload, indent=2), flush=True)
235
  return payload
236
-
237
  if status == "succeeded":
238
- print("Training succeeded. Final payload:", flush=True)
239
- print(json.dumps(payload, indent=2), flush=True)
240
  return payload
241
 
242
  time.sleep(poll_interval_seconds)
243
 
244
- raise TimeoutError(f"Training did not finish within {timeout_seconds} seconds.")
 
 
 
 
 
 
 
 
 
 
245
 
246
 
247
  def build_parser() -> argparse.ArgumentParser:
248
- parser = argparse.ArgumentParser(description="Commit, push, trigger smoke training, and poll HF Space status.")
 
 
249
  parser.add_argument(
250
  "--base-url",
251
  required=True,
252
- help="Base URL of the deployed Hugging Face Space, for example https://<space>.hf.space",
253
  )
254
  parser.add_argument(
255
- "--commit-message",
256
- default="Automated smoke training update",
257
- help="Commit message to use when local changes are present.",
258
  )
259
- parser.add_argument("--remote", default=DEFAULT_REMOTE, help="Git remote for the Hugging Face Space.")
260
  parser.add_argument(
261
- "--remote-branch",
262
- default=DEFAULT_REMOTE_BRANCH,
263
- help="Remote branch that deploys the Hugging Face Space.",
264
  )
265
  parser.add_argument(
266
  "--poll-interval-seconds",
267
  type=int,
268
  default=DEFAULT_POLL_INTERVAL_SECONDS,
269
- help="How often to poll /train/status.",
270
  )
271
  parser.add_argument(
272
- "--timeout-seconds",
273
  type=int,
274
- default=DEFAULT_TIMEOUT_SECONDS,
275
- help="Overall timeout for Space health and smoke training completion.",
 
 
 
 
 
 
276
  )
277
  parser.add_argument(
278
  "--required-healthy-checks",
279
  type=int,
280
  default=DEFAULT_REQUIRED_HEALTHY_CHECKS,
281
- help="How many consecutive healthy checks to require before starting training.",
282
  )
283
  parser.add_argument(
284
  "--min-deploy-wait-seconds",
285
  type=int,
286
  default=DEFAULT_MIN_DEPLOY_WAIT_SECONDS,
287
- help="Minimum number of seconds to wait after the push before treating the Space as fully deployed.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  )
289
  return parser
290
 
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  def main(argv: list[str] | None = None) -> int:
293
  args = build_parser().parse_args(argv)
294
  repo_root = Path(__file__).resolve().parents[1]
295
 
296
  try:
297
- commit_hash = commit_changes(repo_root, args.commit_message)
298
- if commit_hash:
299
- print(f"Committed changes at {commit_hash}", flush=True)
300
-
301
- should_deploy = deployment_needed(repo_root, args.remote, args.remote_branch)
302
- if should_deploy:
303
- push_to_space(repo_root, args.remote, args.remote_branch)
304
- wait_for_space_health(
305
- base_url=args.base_url,
306
- timeout_seconds=args.timeout_seconds,
 
 
 
 
 
 
 
 
307
  poll_interval_seconds=args.poll_interval_seconds,
308
  required_healthy_checks=args.required_healthy_checks,
309
  min_deploy_wait_seconds=args.min_deploy_wait_seconds,
310
  )
311
  else:
312
- print("Reusing the currently deployed Space revision. Skipping deploy wait.", flush=True)
313
- start_smoke_training(args.base_url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  final_status = poll_training_status(
315
- base_url=args.base_url,
316
  poll_interval_seconds=args.poll_interval_seconds,
317
- timeout_seconds=args.timeout_seconds,
318
  )
 
319
  except Exception as exc:
320
- print(f"Deployment/training loop failed: {exc}", file=sys.stderr, flush=True)
321
  return 1
322
 
323
- return 0 if final_status.get("status") == "succeeded" else 1
324
-
325
 
326
  if __name__ == "__main__":
327
  raise SystemExit(main())
 
8
  import time
9
  import urllib.error
10
  import urllib.request
11
+ from dataclasses import dataclass
12
  from pathlib import Path
13
  from typing import Any
14
 
 
15
  DEFAULT_REMOTE = "space"
16
  DEFAULT_REMOTE_BRANCH = "main"
17
  DEFAULT_POLL_INTERVAL_SECONDS = 10
18
+ DEFAULT_DEPLOY_TIMEOUT_SECONDS = 60 * 20
19
+ DEFAULT_TRAIN_TIMEOUT_SECONDS = 60 * 30
20
  DEFAULT_REQUIRED_HEALTHY_CHECKS = 3
21
  DEFAULT_MIN_DEPLOY_WAIT_SECONDS = 30
22
+ DEFAULT_COMMIT_MESSAGE = "Smoke-train deployment update"
23
+
24
+
25
+ class ScriptError(RuntimeError):
26
+ pass
27
+
28
+
29
+ def print_info(message: str) -> None:
30
+ print(f"[info] {message}", flush=True)
31
+
32
+
33
+ def print_warn(message: str) -> None:
34
+ print(f"[warn] {message}", flush=True)
35
+
36
+
37
+ def print_error(message: str) -> None:
38
+ print(f"[error] {message}", file=sys.stderr, flush=True)
39
+
40
+
41
+ def pretty_json(payload: dict[str, Any]) -> str:
42
+ return json.dumps(payload, indent=2, sort_keys=True)
43
+
44
+
45
+ def api_url(base_url: str, path: str) -> str:
46
+ return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
47
+
48
+
49
+ def http_json(
50
+ method: str,
51
+ url: str,
52
+ *,
53
+ payload: dict[str, Any] | None = None,
54
+ timeout_seconds: int = 60,
55
+ ) -> dict[str, Any]:
56
+ data = None
57
+ headers = {"Accept": "application/json"}
58
+ if payload is not None:
59
+ data = json.dumps(payload).encode("utf-8")
60
+ headers["Content-Type"] = "application/json"
61
+
62
+ request = urllib.request.Request(url=url, data=data, headers=headers, method=method.upper())
63
+ try:
64
+ with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
65
+ raw = response.read().decode("utf-8")
66
+ if not raw.strip():
67
+ return {}
68
+ return json.loads(raw)
69
+ except urllib.error.HTTPError as exc:
70
+ body = exc.read().decode("utf-8", errors="replace")
71
+ raise ScriptError(f"HTTP {exc.code} for {url}: {body}") from exc
72
+ except urllib.error.URLError as exc:
73
+ raise ScriptError(f"Request to {url} failed: {exc}") from exc
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class GitOptions:
78
+ repo_root: Path
79
+ remote: str
80
+ remote_branch: str
81
+ commit_message: str
82
+ skip_commit: bool
83
+ skip_push: bool
84
 
85
 
86
  def run_command(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
 
97
  if result.returncode == 0:
98
  return
99
  message = result.stderr.strip() or result.stdout.strip() or f"{action} failed with exit code {result.returncode}"
100
+ raise ScriptError(f"{action} failed: {message}")
101
 
102
 
103
  def has_uncommitted_changes(repo_root: Path) -> bool:
 
106
  return bool(result.stdout.strip())
107
 
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  def current_head_sha(repo_root: Path) -> str:
110
  result = run_command(["git", "rev-parse", "HEAD"], repo_root)
111
  ensure_success(result, "git rev-parse HEAD")
 
121
  return line.split()[0]
122
 
123
 
124
+ def commit_changes_if_needed(options: GitOptions) -> str:
125
+ repo_root = options.repo_root
126
+ head_before = current_head_sha(repo_root)
127
+ if not has_uncommitted_changes(repo_root):
128
+ print_info(f"Working tree is clean at {head_before}.")
129
+ return head_before
 
 
 
130
 
131
+ if options.skip_commit:
132
+ raise ScriptError(
133
+ "Working tree has uncommitted changes, but --skip-commit was set. "
134
+ "Commit the changes manually or remove --skip-commit."
135
  )
 
136
 
137
+ print_info("Staging and committing local changes before deploy.")
138
+ ensure_success(run_command(["git", "add", "-A"], repo_root), "git add -A")
139
+ commit_result = run_command(["git", "commit", "-m", options.commit_message], repo_root)
140
+ ensure_success(commit_result, "git commit")
141
+ if commit_result.stdout.strip():
142
+ print(commit_result.stdout.strip(), flush=True)
143
+ head_after = current_head_sha(repo_root)
144
+ print_info(f"Created commit {head_after}.")
145
+ return head_after
146
+
147
+
148
+ def deployment_needed(options: GitOptions, local_head_sha: str) -> bool:
149
+ remote_sha = remote_branch_sha(options.repo_root, options.remote, options.remote_branch)
150
+ if remote_sha is None:
151
+ print_info(f"Remote branch {options.remote}/{options.remote_branch} does not exist yet.")
152
+ return True
153
+ if remote_sha == local_head_sha:
154
+ print_info(f"Remote {options.remote}/{options.remote_branch} already points to {local_head_sha}.")
155
+ return False
156
+ print_info(
157
+ f"Remote {options.remote}/{options.remote_branch} is at {remote_sha}; "
158
+ f"local HEAD is {local_head_sha}."
159
  )
160
  return True
161
 
162
 
163
+ def push_current_head(options: GitOptions) -> None:
164
+ print_info(f"Pushing HEAD to {options.remote}/{options.remote_branch}.")
165
+ result = run_command(["git", "push", options.remote, f"HEAD:{options.remote_branch}"], options.repo_root)
166
+ ensure_success(result, f"git push {options.remote} HEAD:{options.remote_branch}")
167
+ summary = result.stdout.strip() or result.stderr.strip()
168
+ if summary:
169
+ print(summary, flush=True)
170
 
 
 
 
 
 
 
 
 
 
 
171
 
172
+ def fetch_health(base_url: str) -> dict[str, Any]:
173
+ return http_json("GET", api_url(base_url, "/health"))
174
 
 
 
 
 
 
175
 
176
+ def wait_for_health(
 
177
  base_url: str,
178
+ *,
179
  timeout_seconds: int,
180
  poll_interval_seconds: int,
181
  required_healthy_checks: int,
182
  min_deploy_wait_seconds: int,
183
+ ) -> dict[str, Any]:
 
184
  deadline = time.time() + timeout_seconds
185
  push_started_at = time.time()
186
+ prior_payload: dict[str, Any] | None = None
187
+ transition_observed = False
188
+ healthy_streak = 0
 
189
 
190
  while time.time() < deadline:
191
  try:
192
+ payload = fetch_health(base_url)
193
+ except ScriptError as exc:
194
+ healthy_streak = 0
195
+ transition_observed = True
196
+ print_info(f"Waiting for service health: {exc}")
197
  time.sleep(poll_interval_seconds)
198
  continue
199
 
200
+ if prior_payload is not None and payload != prior_payload:
201
+ transition_observed = True
 
 
202
 
203
+ elapsed = time.time() - push_started_at
204
+ if elapsed < min_deploy_wait_seconds:
205
+ remaining = max(0, int(min_deploy_wait_seconds - elapsed))
206
+ print_info(f"Health endpoint reachable. Waiting {remaining}s for deployment stabilization.")
207
+ prior_payload = payload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  time.sleep(poll_interval_seconds)
209
  continue
210
 
211
  if payload.get("status") == "healthy":
212
+ healthy_streak += 1
213
+ print_info(
214
+ f"Health check {healthy_streak}/{required_healthy_checks}: "
215
+ f"training={payload.get('training')} model_loaded={payload.get('model_loaded')}"
216
  )
217
+ if healthy_streak >= required_healthy_checks:
218
+ if not transition_observed and prior_payload is not None:
219
+ print_warn("No payload transition was observed after deploy; continuing because health stabilized.")
220
+ return payload
221
  else:
222
+ healthy_streak = 0
223
+ print_info(f"Health payload not ready yet: {pretty_json(payload)}")
224
 
225
+ prior_payload = payload
226
  time.sleep(poll_interval_seconds)
227
 
228
+ raise ScriptError(f"Service did not report healthy within {timeout_seconds} seconds.")
229
+
230
+
231
+ def fetch_training_status(base_url: str) -> dict[str, Any]:
232
+ return http_json("GET", api_url(base_url, "/train/status"))
233
+
234
+
235
+ def summarize_training_status(payload: dict[str, Any]) -> str:
236
+ status = payload.get("status")
237
+ phase = payload.get("phase")
238
+ completed = payload.get("completed_steps")
239
+ total = payload.get("total_steps")
240
+ difficulty = payload.get("current_difficulty")
241
+ problem_family = payload.get("last_problem_family")
242
+ reward = payload.get("last_reward")
243
+ pieces = [
244
+ f"status={status}",
245
+ f"phase={phase}",
246
+ f"steps={completed}/{total}",
247
+ ]
248
+ if difficulty:
249
+ pieces.append(f"difficulty={difficulty}")
250
+ if problem_family:
251
+ pieces.append(f"family={problem_family}")
252
+ if reward is not None:
253
+ pieces.append(f"reward={reward}")
254
+ return " ".join(pieces)
255
+
256
+
257
+ def ensure_no_active_training(base_url: str) -> None:
258
+ payload = fetch_training_status(base_url)
259
+ if payload.get("status") == "running":
260
+ raise ScriptError(
261
+ "The remote training manager already reports an active run. "
262
+ f"Current status: {summarize_training_status(payload)}"
263
+ )
264
 
265
 
266
+ def start_training(base_url: str, train_payload: dict[str, Any]) -> dict[str, Any]:
267
+ print_info(f"Starting training with payload: {pretty_json(train_payload)}")
268
+ payload = http_json("POST", api_url(base_url, "/train"), payload=train_payload)
269
+ print_info(f"Training accepted: {pretty_json(payload)}")
270
  return payload
271
 
272
 
273
+ def poll_training_status(
274
+ base_url: str,
275
+ *,
276
+ poll_interval_seconds: int,
277
+ timeout_seconds: int,
278
+ ) -> dict[str, Any]:
279
  deadline = time.time() + timeout_seconds
280
+ last_signature: tuple[Any, ...] | None = None
281
 
282
  while time.time() < deadline:
283
+ payload = fetch_training_status(base_url)
284
+ signature = (
285
+ payload.get("status"),
286
+ payload.get("phase"),
287
+ payload.get("completed_steps"),
288
+ payload.get("total_steps"),
289
+ payload.get("last_problem_family"),
290
+ payload.get("last_reward"),
291
+ payload.get("error"),
292
  )
293
+ if signature != last_signature:
294
+ print_info(summarize_training_status(payload))
295
+ last_signature = signature
296
 
297
+ status = payload.get("status")
298
  if status == "failed":
299
+ print_error("Training failed. Final payload follows.")
300
+ print(pretty_json(payload), flush=True)
301
  return payload
 
302
  if status == "succeeded":
303
+ print_info("Training succeeded. Final payload follows.")
304
+ print(pretty_json(payload), flush=True)
305
  return payload
306
 
307
  time.sleep(poll_interval_seconds)
308
 
309
+ raise ScriptError(f"Training did not finish within {timeout_seconds} seconds.")
310
+
311
+
312
+ def build_train_payload(args: argparse.Namespace) -> dict[str, Any]:
313
+ payload: dict[str, Any] = {"preset": args.preset}
314
+ if args.train_payload_json:
315
+ extra_payload = json.loads(args.train_payload_json)
316
+ if not isinstance(extra_payload, dict):
317
+ raise ScriptError("--train-payload-json must decode to a JSON object.")
318
+ payload.update(extra_payload)
319
+ return payload
320
 
321
 
322
  def build_parser() -> argparse.ArgumentParser:
323
+ parser = argparse.ArgumentParser(
324
+ description="Deploy the current repo to a Hugging Face Space and run a smoke training job.",
325
+ )
326
  parser.add_argument(
327
  "--base-url",
328
  required=True,
329
+ help="Base URL of the running server, for example https://<space>.hf.space or http://localhost:7860",
330
  )
331
  parser.add_argument(
332
+ "--preset",
333
+ default="smoke",
334
+ help="Training preset to send to /train. Defaults to smoke.",
335
  )
 
336
  parser.add_argument(
337
+ "--train-payload-json",
338
+ default=None,
339
+ help="Optional JSON object merged into the /train request body.",
340
  )
341
  parser.add_argument(
342
  "--poll-interval-seconds",
343
  type=int,
344
  default=DEFAULT_POLL_INTERVAL_SECONDS,
345
+ help="How often to poll /health and /train/status.",
346
  )
347
  parser.add_argument(
348
+ "--deploy-timeout-seconds",
349
  type=int,
350
+ default=DEFAULT_DEPLOY_TIMEOUT_SECONDS,
351
+ help="Maximum time to wait for the service to become healthy after push.",
352
+ )
353
+ parser.add_argument(
354
+ "--train-timeout-seconds",
355
+ type=int,
356
+ default=DEFAULT_TRAIN_TIMEOUT_SECONDS,
357
+ help="Maximum time to wait for the smoke train run to finish.",
358
  )
359
  parser.add_argument(
360
  "--required-healthy-checks",
361
  type=int,
362
  default=DEFAULT_REQUIRED_HEALTHY_CHECKS,
363
+ help="Number of consecutive healthy /health checks required before training starts.",
364
  )
365
  parser.add_argument(
366
  "--min-deploy-wait-seconds",
367
  type=int,
368
  default=DEFAULT_MIN_DEPLOY_WAIT_SECONDS,
369
+ help="Minimum time to wait after push before treating health as stable.",
370
+ )
371
+ parser.add_argument(
372
+ "--remote",
373
+ default=DEFAULT_REMOTE,
374
+ help="Git remote to push to when deploying.",
375
+ )
376
+ parser.add_argument(
377
+ "--remote-branch",
378
+ default=DEFAULT_REMOTE_BRANCH,
379
+ help="Remote branch to push HEAD to when deploying.",
380
+ )
381
+ parser.add_argument(
382
+ "--commit-message",
383
+ default=DEFAULT_COMMIT_MESSAGE,
384
+ help="Commit message to use if local changes need to be committed before push.",
385
+ )
386
+ parser.add_argument(
387
+ "--skip-commit",
388
+ action="store_true",
389
+ help="Do not auto-commit local changes before deploy.",
390
+ )
391
+ parser.add_argument(
392
+ "--skip-push",
393
+ action="store_true",
394
+ help="Skip git deploy entirely and just hit the running server.",
395
+ )
396
+ parser.add_argument(
397
+ "--skip-health-check",
398
+ action="store_true",
399
+ help="Skip waiting on /health before training.",
400
+ )
401
+ parser.add_argument(
402
+ "--trigger-only",
403
+ action="store_true",
404
+ help="Start the smoke run and exit without polling to completion.",
405
+ )
406
+ parser.add_argument(
407
+ "--status-only",
408
+ action="store_true",
409
+ help="Do not start a new run; just print /train/status and optionally poll it.",
410
+ )
411
+ parser.add_argument(
412
+ "--follow-running",
413
+ action="store_true",
414
+ help="If /train/status already reports a running job, follow it instead of failing.",
415
  )
416
  return parser
417
 
418
 
419
+ def maybe_deploy(args: argparse.Namespace, repo_root: Path) -> None:
420
+ if args.skip_push:
421
+ print_info("Skipping git deploy because --skip-push was set.")
422
+ return
423
+
424
+ git_options = GitOptions(
425
+ repo_root=repo_root,
426
+ remote=args.remote,
427
+ remote_branch=args.remote_branch,
428
+ commit_message=args.commit_message,
429
+ skip_commit=args.skip_commit,
430
+ skip_push=args.skip_push,
431
+ )
432
+ local_head_sha = commit_changes_if_needed(git_options)
433
+ if not deployment_needed(git_options, local_head_sha):
434
+ print_info("Skipping push because the remote is already on the current local HEAD.")
435
+ return
436
+
437
+ push_current_head(git_options)
438
+
439
+
440
  def main(argv: list[str] | None = None) -> int:
441
  args = build_parser().parse_args(argv)
442
  repo_root = Path(__file__).resolve().parents[1]
443
 
444
  try:
445
+ if args.status_only:
446
+ payload = fetch_training_status(args.base_url)
447
+ print(pretty_json(payload), flush=True)
448
+ if args.follow_running and payload.get("status") == "running":
449
+ final_status = poll_training_status(
450
+ args.base_url,
451
+ poll_interval_seconds=args.poll_interval_seconds,
452
+ timeout_seconds=args.train_timeout_seconds,
453
+ )
454
+ return 0 if final_status.get("status") == "succeeded" else 1
455
+ return 0
456
+
457
+ maybe_deploy(args, repo_root)
458
+
459
+ if not args.skip_health_check:
460
+ wait_for_health(
461
+ args.base_url,
462
+ timeout_seconds=args.deploy_timeout_seconds,
463
  poll_interval_seconds=args.poll_interval_seconds,
464
  required_healthy_checks=args.required_healthy_checks,
465
  min_deploy_wait_seconds=args.min_deploy_wait_seconds,
466
  )
467
  else:
468
+ print_info("Skipping health wait because --skip-health-check was set.")
469
+
470
+ current_status = fetch_training_status(args.base_url)
471
+ if current_status.get("status") == "running":
472
+ if args.follow_running:
473
+ print_warn(
474
+ "A training job is already running; following the existing run instead of starting a new one."
475
+ )
476
+ final_status = poll_training_status(
477
+ args.base_url,
478
+ poll_interval_seconds=args.poll_interval_seconds,
479
+ timeout_seconds=args.train_timeout_seconds,
480
+ )
481
+ return 0 if final_status.get("status") == "succeeded" else 1
482
+ ensure_no_active_training(args.base_url)
483
+
484
+ train_payload = build_train_payload(args)
485
+ start_training(args.base_url, train_payload)
486
+ if args.trigger_only:
487
+ print_info("Training was triggered successfully; exiting because --trigger-only was set.")
488
+ return 0
489
+
490
  final_status = poll_training_status(
491
+ args.base_url,
492
  poll_interval_seconds=args.poll_interval_seconds,
493
+ timeout_seconds=args.train_timeout_seconds,
494
  )
495
+ return 0 if final_status.get("status") == "succeeded" else 1
496
  except Exception as exc:
497
+ print_error(str(exc))
498
  return 1
499
 
 
 
500
 
501
  if __name__ == "__main__":
502
  raise SystemExit(main())
scripts/test_env.py CHANGED
@@ -46,6 +46,7 @@ def main() -> None:
46
  assert correct.execution_status == "completed"
47
  assert correct.done is True
48
  assert correct.reward_components["efficiency_score"] >= 0.95
 
49
 
50
  observation = env.reset(problem_id="running_total", difficulty="easy")
51
  repair_1 = env.step(
@@ -98,6 +99,7 @@ def main() -> None:
98
  assert less_optimized.done is False
99
  assert less_optimized.reward < 1.0
100
  assert "can still be optimized further" in less_optimized.feedback
 
101
 
102
  observation = env.reset(problem_id="sum_even_numbers", difficulty="easy")
103
  syntax = env.step(AdaptAction(code="def broken(:\n pass"))
@@ -130,6 +132,7 @@ def main() -> None:
130
  assert unsafe.reward == 0.0
131
  assert unsafe.execution_status == "safety_violation"
132
  assert unsafe.done is False
 
133
 
134
  assert env.state.history["attempts"]
135
  assert_hidden_tests_are_not_exposed(timeout.model_dump())
 
46
  assert correct.execution_status == "completed"
47
  assert correct.done is True
48
  assert correct.reward_components["efficiency_score"] >= 0.95
49
+ assert correct.reward_components["hidden_correctness"] == 1.0
50
 
51
  observation = env.reset(problem_id="running_total", difficulty="easy")
52
  repair_1 = env.step(
 
99
  assert less_optimized.done is False
100
  assert less_optimized.reward < 1.0
101
  assert "can still be optimized further" in less_optimized.feedback
102
+ assert less_optimized.reward_components["format_compliance"] == 1.0
103
 
104
  observation = env.reset(problem_id="sum_even_numbers", difficulty="easy")
105
  syntax = env.step(AdaptAction(code="def broken(:\n pass"))
 
132
  assert unsafe.reward == 0.0
133
  assert unsafe.execution_status == "safety_violation"
134
  assert unsafe.done is False
135
+ assert unsafe.reward_components["anti_cheat_compliance"] == 0.0
136
 
137
  assert env.state.history["attempts"]
138
  assert_hidden_tests_are_not_exposed(timeout.model_dump())
scripts/test_training_config.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ ROOT = Path(__file__).resolve().parents[1]
7
+ if str(ROOT) not in sys.path:
8
+ sys.path.insert(0, str(ROOT))
9
+
10
+ from training.train_grpo import build_training_config, resolve_precision_policy
11
+ from verifier.metrics import compute_episode_reward
12
+
13
+
14
+ class FakeCuda:
15
+ def __init__(self, available: bool, bf16_supported: bool) -> None:
16
+ self._available = available
17
+ self._bf16_supported = bf16_supported
18
+
19
+ def is_available(self) -> bool:
20
+ return self._available
21
+
22
+ def is_bf16_supported(self) -> bool:
23
+ return self._bf16_supported
24
+
25
+
26
+ class FakeTorch:
27
+ bfloat16 = "bfloat16"
28
+ float16 = "float16"
29
+ float32 = "float32"
30
+
31
+ def __init__(self, available: bool, bf16_supported: bool) -> None:
32
+ self.cuda = FakeCuda(available=available, bf16_supported=bf16_supported)
33
+
34
+
35
+ def main() -> None:
36
+ l4_config = build_training_config("l4")
37
+ smoke_config = build_training_config("smoke")
38
+
39
+ assert l4_config.model_name == "Qwen/Qwen2.5-3B-Instruct"
40
+ assert l4_config.load_in_4bit is True
41
+ assert l4_config.gradient_checkpointing is True
42
+ assert l4_config.num_generations == 4
43
+
44
+ assert smoke_config.load_in_4bit is False
45
+ assert smoke_config.gradient_checkpointing is False
46
+
47
+ bf16_policy = resolve_precision_policy(l4_config, FakeTorch(available=True, bf16_supported=True))
48
+ assert bf16_policy["precision_mode"] == "bf16"
49
+ assert bf16_policy["load_in_4bit"] is True
50
+
51
+ fp16_policy = resolve_precision_policy(l4_config, FakeTorch(available=True, bf16_supported=False))
52
+ assert fp16_policy["precision_mode"] == "fp16"
53
+ assert fp16_policy["load_in_4bit"] is True
54
+
55
+ cpu_policy = resolve_precision_policy(smoke_config, FakeTorch(available=False, bf16_supported=False))
56
+ assert cpu_policy["precision_mode"] == "fp32"
57
+ assert cpu_policy["load_in_4bit"] is False
58
+
59
+ reward, components = compute_episode_reward(
60
+ pass_rate=1.0,
61
+ step_number=1,
62
+ execution_status="completed",
63
+ previous_pass_rate=0.0,
64
+ done=False,
65
+ efficiency_score=0.94,
66
+ optimization_target_met=False,
67
+ )
68
+ assert reward == 0.94
69
+ assert components["progress_delta"] == 1.0
70
+ print("Training config smoke tests passed")
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()
scripts/test_verifier.py CHANGED
@@ -54,6 +54,11 @@ nums = list(map(int, input().split()))
54
  print(nums[n])
55
  """
56
 
 
 
 
 
 
57
  for name, code in [
58
  ("correct", correct_code),
59
  ("wrong", wrong_code),
@@ -61,6 +66,7 @@ for name, code in [
61
  ("invalid_output", invalid_output_code),
62
  ("timeout", timeout_code),
63
  ("runtime_error", runtime_error_code),
 
64
  ]:
65
  reward, info = verify(code, test_cases)
66
 
@@ -77,5 +83,10 @@ for name, code in [
77
 
78
  reward_optimal, info_optimal = verify(correct_code, test_cases)
79
  reward_less_optimal, info_less_optimal = verify(less_optimized_code, test_cases)
 
80
  assert info_optimal["efficiency_score"] > info_less_optimal["efficiency_score"]
81
  assert info_less_optimal["complexity_signals"]["list_comprehensions"] > 0
 
 
 
 
 
54
  print(nums[n])
55
  """
56
 
57
+ safety_violation_code = """
58
+ import os
59
+ print(os.listdir("."))
60
+ """
61
+
62
  for name, code in [
63
  ("correct", correct_code),
64
  ("wrong", wrong_code),
 
66
  ("invalid_output", invalid_output_code),
67
  ("timeout", timeout_code),
68
  ("runtime_error", runtime_error_code),
69
+ ("safety_violation", safety_violation_code),
70
  ]:
71
  reward, info = verify(code, test_cases)
72
 
 
83
 
84
  reward_optimal, info_optimal = verify(correct_code, test_cases)
85
  reward_less_optimal, info_less_optimal = verify(less_optimized_code, test_cases)
86
+ reward_safety, info_safety = verify(safety_violation_code, test_cases)
87
  assert info_optimal["efficiency_score"] > info_less_optimal["efficiency_score"]
88
  assert info_less_optimal["complexity_signals"]["list_comprehensions"] > 0
89
+ assert info_optimal["verifier_components"]["hidden_correctness"] == 1.0
90
+ assert info_optimal["verifier_components"]["anti_cheat_compliance"] == 1.0
91
+ assert reward_safety == 0.0
92
+ assert info_safety["execution_status"] == "safety_violation"
server/app.py CHANGED
@@ -46,7 +46,7 @@ class ResetRequest(BaseModel):
46
 
47
 
48
  class TrainRequest(BaseModel):
49
- preset: str = "smoke"
50
  model_name: Optional[str] = None
51
  output_dir: Optional[str] = None
52
  dataset_size: Optional[int] = None
@@ -54,10 +54,13 @@ class TrainRequest(BaseModel):
54
  batch_size: Optional[int] = None
55
  gradient_accumulation_steps: Optional[int] = None
56
  num_generations: Optional[int] = None
 
 
57
  evaluation_episodes: Optional[int] = None
58
  baseline_eval: Optional[bool] = None
59
  generator_mode: Optional[str] = None
60
  disable_wandb: Optional[bool] = None
 
61
 
62
 
63
  class RunTrainedPolicyRequest(BaseModel):
 
46
 
47
 
48
  class TrainRequest(BaseModel):
49
+ preset: str = "l4"
50
  model_name: Optional[str] = None
51
  output_dir: Optional[str] = None
52
  dataset_size: Optional[int] = None
 
54
  batch_size: Optional[int] = None
55
  gradient_accumulation_steps: Optional[int] = None
56
  num_generations: Optional[int] = None
57
+ load_in_4bit: Optional[bool] = None
58
+ gradient_checkpointing: Optional[bool] = None
59
  evaluation_episodes: Optional[int] = None
60
  baseline_eval: Optional[bool] = None
61
  generator_mode: Optional[str] = None
62
  disable_wandb: Optional[bool] = None
63
+ save_merged_model: Optional[bool] = None
64
 
65
 
66
  class RunTrainedPolicyRequest(BaseModel):
server/runtime.py CHANGED
@@ -161,7 +161,7 @@ class SpaceModelRegistry:
161
  return torch, AutoPeftModelForCausalLM, (AutoModelForCausalLM, AutoTokenizer)
162
 
163
  def _base_model_name(self) -> str:
164
- return os.getenv("BASE_MODEL_NAME") or os.getenv("MODEL_NAME") or "unsloth/Llama-3.2-3B-Instruct"
165
 
166
  def _active_generation_stack(
167
  self,
@@ -194,7 +194,12 @@ class SpaceModelRegistry:
194
  )
195
  return self.status_payload()
196
 
197
- dtype = torch.float16 if torch.cuda.is_available() else torch.float32
 
 
 
 
 
198
  tokenizer = AutoTokenizer.from_pretrained(base_model_name)
199
  if tokenizer.pad_token is None and tokenizer.eos_token is not None:
200
  tokenizer.pad_token = tokenizer.eos_token
@@ -232,7 +237,12 @@ class SpaceModelRegistry:
232
  raise RuntimeError(f"Trained artifact directory does not exist: {artifact_dir}")
233
 
234
  with self._lock:
235
- dtype = torch.float16 if torch.cuda.is_available() else torch.float32
 
 
 
 
 
236
  tokenizer = AutoTokenizer.from_pretrained(str(artifact_dir))
237
  if tokenizer.pad_token is None and tokenizer.eos_token is not None:
238
  tokenizer.pad_token = tokenizer.eos_token
 
161
  return torch, AutoPeftModelForCausalLM, (AutoModelForCausalLM, AutoTokenizer)
162
 
163
  def _base_model_name(self) -> str:
164
+ return os.getenv("BASE_MODEL_NAME") or os.getenv("MODEL_NAME") or "Qwen/Qwen2.5-3B-Instruct"
165
 
166
  def _active_generation_stack(
167
  self,
 
194
  )
195
  return self.status_payload()
196
 
197
+ if torch.cuda.is_available() and torch.cuda.is_bf16_supported():
198
+ dtype = torch.bfloat16
199
+ elif torch.cuda.is_available():
200
+ dtype = torch.float16
201
+ else:
202
+ dtype = torch.float32
203
  tokenizer = AutoTokenizer.from_pretrained(base_model_name)
204
  if tokenizer.pad_token is None and tokenizer.eos_token is not None:
205
  tokenizer.pad_token = tokenizer.eos_token
 
237
  raise RuntimeError(f"Trained artifact directory does not exist: {artifact_dir}")
238
 
239
  with self._lock:
240
+ if torch.cuda.is_available() and torch.cuda.is_bf16_supported():
241
+ dtype = torch.bfloat16
242
+ elif torch.cuda.is_available():
243
+ dtype = torch.float16
244
+ else:
245
+ dtype = torch.float32
246
  tokenizer = AutoTokenizer.from_pretrained(str(artifact_dir))
247
  if tokenizer.pad_token is None and tokenizer.eos_token is not None:
248
  tokenizer.pad_token = tokenizer.eos_token
test.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
 
3
  from scripts.test_env import main as run_env_smoke
4
  from scripts.test_space_api import main as run_space_api_smoke
 
5
  from scripts.test_trace_logging import main as run_trace_logging_smoke
6
  from scripts.test_verifier import test_cases
7
  from verifier.verifier import verify
@@ -10,6 +11,7 @@ from verifier.verifier import verify
10
  def main() -> None:
11
  run_env_smoke()
12
  run_space_api_smoke()
 
13
  run_trace_logging_smoke()
14
 
15
  reward, info = verify(
 
2
 
3
  from scripts.test_env import main as run_env_smoke
4
  from scripts.test_space_api import main as run_space_api_smoke
5
+ from scripts.test_training_config import main as run_training_config_smoke
6
  from scripts.test_trace_logging import main as run_trace_logging_smoke
7
  from scripts.test_verifier import test_cases
8
  from verifier.verifier import verify
 
11
  def main() -> None:
12
  run_env_smoke()
13
  run_space_api_smoke()
14
+ run_training_config_smoke()
15
  run_trace_logging_smoke()
16
 
17
  reward, info = verify(
training/train_grpo.py CHANGED
@@ -35,20 +35,21 @@ SMOKE_PREFERRED_PRECISION = "fp16"
35
 
36
  @dataclass
37
  class TrainingConfig:
38
- model_name: str = "unsloth/Llama-3.2-3B-Instruct"
39
- output_dir: str = "outputs_v3"
40
  dataset_size: int = 200
41
  max_steps: int = 250
42
  batch_size: int = 1
43
  gradient_accumulation_steps: int = 8
44
- num_generations: int = 8
45
  max_seq_length: int = 2048
46
  max_prompt_length: int = 1024
47
  max_completion_length: int = 512
48
  learning_rate: float = 5e-6
49
  lora_rank: int = 16
50
  lora_alpha: int = 16
51
- disable_4bit: bool = False
 
52
  bf16: bool = False
53
  baseline_eval: bool = False
54
  evaluation_episodes: int = 20
@@ -60,6 +61,7 @@ class TrainingConfig:
60
  non_deterministic_generator: bool = False
61
  trace_logging_enabled: bool = True
62
  checkpoint_log_interval_steps: int = 10
 
63
 
64
  def to_dict(self) -> dict[str, Any]:
65
  return asdict(self)
@@ -67,6 +69,7 @@ class TrainingConfig:
67
 
68
  TRAINING_PRESETS: dict[str, dict[str, Any]] = {
69
  "smoke": {
 
70
  "dataset_size": 12,
71
  "max_steps": 6,
72
  "batch_size": 1,
@@ -75,12 +78,40 @@ TRAINING_PRESETS: dict[str, dict[str, Any]] = {
75
  "evaluation_episodes": 3,
76
  "baseline_eval": False,
77
  "disable_wandb": True,
78
- "disable_4bit": True,
 
79
  "bf16": False,
80
  "output_dir": "outputs_smoke",
81
  "checkpoint_log_interval_steps": 2,
82
  },
83
- "default": {},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  }
85
 
86
 
@@ -165,7 +196,8 @@ def namespace_to_config(args: argparse.Namespace) -> TrainingConfig:
165
  learning_rate=args.learning_rate,
166
  lora_rank=args.lora_rank,
167
  lora_alpha=args.lora_alpha,
168
- disable_4bit=args.disable_4bit,
 
169
  bf16=args.bf16,
170
  baseline_eval=args.baseline_eval,
171
  evaluation_episodes=args.evaluation_episodes,
@@ -177,6 +209,7 @@ def namespace_to_config(args: argparse.Namespace) -> TrainingConfig:
177
  non_deterministic_generator=args.non_deterministic_generator,
178
  trace_logging_enabled=args.trace_logging_enabled,
179
  checkpoint_log_interval_steps=args.checkpoint_log_interval_steps,
 
180
  )
181
 
182
 
@@ -786,14 +819,11 @@ def resolve_precision_policy(config: TrainingConfig, torch: Any) -> dict[str, An
786
  if bf16_requested and not gpu_supports_bf16:
787
  raise RuntimeError("bf16 was requested, but the active GPU/runtime does not report BF16 support.")
788
 
789
- output_dir_str = str(config.output_dir)
790
- is_smoke_run = "outputs_smoke" in output_dir_str
791
-
792
- if bf16_requested:
793
  precision_mode = "bf16"
794
  model_dtype = torch.bfloat16
795
  elif use_cuda:
796
- precision_mode = SMOKE_PREFERRED_PRECISION if is_smoke_run else "fp16"
797
  model_dtype = torch.float16
798
  else:
799
  precision_mode = "fp32"
@@ -801,13 +831,7 @@ def resolve_precision_policy(config: TrainingConfig, torch: Any) -> dict[str, An
801
 
802
  use_bf16 = precision_mode == "bf16"
803
  use_fp16 = precision_mode == "fp16"
804
- load_in_4bit = not config.disable_4bit
805
-
806
- if use_cuda and load_in_4bit and (use_bf16 or use_fp16):
807
- raise RuntimeError(
808
- "4-bit loading with mixed-precision GRPO is disabled for this training path. "
809
- "Set disable_4bit=true or use a full-precision CPU run."
810
- )
811
 
812
  return {
813
  "use_cuda": use_cuda,
@@ -953,6 +977,7 @@ def run_training(
953
  target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
954
  lora_alpha=config.lora_alpha,
955
  lora_dropout=0.0,
 
956
  )
957
  if not load_in_4bit:
958
  model = model.to(model_dtype)
@@ -967,9 +992,16 @@ def run_training(
967
  "reason": "load_in_4bit=True",
968
  }
969
 
970
- critical_precision_audit = audit_critical_module_precision(model, model_dtype)
 
 
 
 
 
 
 
971
  print(f"[training] critical precision audit {json.dumps(critical_precision_audit, sort_keys=True)}")
972
- if critical_precision_audit["has_mismatch"]:
973
  raise RuntimeError(
974
  "Critical projection modules remain in the wrong dtype before GRPOTrainer initialization. "
975
  f"Audit: {json.dumps(critical_precision_audit, sort_keys=True)}"
@@ -1114,7 +1146,10 @@ def run_training(
1114
  )
1115
  trainer.train()
1116
 
1117
- model.save_pretrained(str(output_dir))
 
 
 
1118
  tokenizer.save_pretrained(str(output_dir))
1119
 
1120
  if config.baseline_eval:
@@ -1180,13 +1215,13 @@ def run_training(
1180
 
1181
  def build_parser() -> argparse.ArgumentParser:
1182
  parser = argparse.ArgumentParser(description="GRPO training entrypoint for the ADAPT DSA environment.")
1183
- parser.add_argument("--model-name", default="unsloth/Llama-3.2-3B-Instruct")
1184
- parser.add_argument("--output-dir", default="outputs_v3")
1185
  parser.add_argument("--dataset-size", type=int, default=200)
1186
  parser.add_argument("--max-steps", type=int, default=250)
1187
  parser.add_argument("--batch-size", type=int, default=1)
1188
  parser.add_argument("--gradient-accumulation-steps", type=int, default=8)
1189
- parser.add_argument("--num-generations", type=int, default=8)
1190
  parser.add_argument("--max-seq-length", type=int, default=2048)
1191
  parser.add_argument("--max-prompt-length", type=int, default=1024)
1192
  parser.add_argument("--max-completion-length", type=int, default=512)
@@ -1194,6 +1229,7 @@ def build_parser() -> argparse.ArgumentParser:
1194
  parser.add_argument("--lora-rank", type=int, default=16)
1195
  parser.add_argument("--lora-alpha", type=int, default=16)
1196
  parser.add_argument("--disable-4bit", action="store_true")
 
1197
  parser.add_argument("--bf16", action="store_true")
1198
  parser.add_argument("--baseline-eval", action="store_true")
1199
  parser.add_argument("--evaluation-episodes", type=int, default=20)
@@ -1201,6 +1237,7 @@ def build_parser() -> argparse.ArgumentParser:
1201
  parser.add_argument("--disable-wandb", action="store_true")
1202
  parser.add_argument("--wandb-project", default="adapt-dsa-tutor")
1203
  parser.add_argument("--wandb-run-name", default=None)
 
1204
  parser.add_argument("--trace-logging-enabled", action=argparse.BooleanOptionalAction, default=True)
1205
  parser.add_argument("--checkpoint-log-interval-steps", type=int, default=10)
1206
  parser.add_argument(
 
35
 
36
  @dataclass
37
  class TrainingConfig:
38
+ model_name: str = "Qwen/Qwen2.5-3B-Instruct"
39
+ output_dir: str = "outputs_l4"
40
  dataset_size: int = 200
41
  max_steps: int = 250
42
  batch_size: int = 1
43
  gradient_accumulation_steps: int = 8
44
+ num_generations: int = 4
45
  max_seq_length: int = 2048
46
  max_prompt_length: int = 1024
47
  max_completion_length: int = 512
48
  learning_rate: float = 5e-6
49
  lora_rank: int = 16
50
  lora_alpha: int = 16
51
+ load_in_4bit: bool = True
52
+ gradient_checkpointing: bool = True
53
  bf16: bool = False
54
  baseline_eval: bool = False
55
  evaluation_episodes: int = 20
 
61
  non_deterministic_generator: bool = False
62
  trace_logging_enabled: bool = True
63
  checkpoint_log_interval_steps: int = 10
64
+ save_merged_model: bool = False
65
 
66
  def to_dict(self) -> dict[str, Any]:
67
  return asdict(self)
 
69
 
70
  TRAINING_PRESETS: dict[str, dict[str, Any]] = {
71
  "smoke": {
72
+ "model_name": "Qwen/Qwen2.5-3B-Instruct",
73
  "dataset_size": 12,
74
  "max_steps": 6,
75
  "batch_size": 1,
 
78
  "evaluation_episodes": 3,
79
  "baseline_eval": False,
80
  "disable_wandb": True,
81
+ "load_in_4bit": False,
82
+ "gradient_checkpointing": False,
83
  "bf16": False,
84
  "output_dir": "outputs_smoke",
85
  "checkpoint_log_interval_steps": 2,
86
  },
87
+ "l4": {
88
+ "model_name": "Qwen/Qwen2.5-3B-Instruct",
89
+ "output_dir": "outputs_l4",
90
+ "batch_size": 1,
91
+ "gradient_accumulation_steps": 8,
92
+ "num_generations": 4,
93
+ "max_seq_length": 2048,
94
+ "max_prompt_length": 1024,
95
+ "max_completion_length": 512,
96
+ "lora_rank": 16,
97
+ "lora_alpha": 16,
98
+ "load_in_4bit": True,
99
+ "gradient_checkpointing": True,
100
+ },
101
+ "default": {
102
+ "model_name": "Qwen/Qwen2.5-3B-Instruct",
103
+ "output_dir": "outputs_l4",
104
+ "batch_size": 1,
105
+ "gradient_accumulation_steps": 8,
106
+ "num_generations": 4,
107
+ "max_seq_length": 2048,
108
+ "max_prompt_length": 1024,
109
+ "max_completion_length": 512,
110
+ "lora_rank": 16,
111
+ "lora_alpha": 16,
112
+ "load_in_4bit": True,
113
+ "gradient_checkpointing": True,
114
+ },
115
  }
116
 
117
 
 
196
  learning_rate=args.learning_rate,
197
  lora_rank=args.lora_rank,
198
  lora_alpha=args.lora_alpha,
199
+ load_in_4bit=not args.disable_4bit,
200
+ gradient_checkpointing=not getattr(args, "disable_gradient_checkpointing", False),
201
  bf16=args.bf16,
202
  baseline_eval=args.baseline_eval,
203
  evaluation_episodes=args.evaluation_episodes,
 
209
  non_deterministic_generator=args.non_deterministic_generator,
210
  trace_logging_enabled=args.trace_logging_enabled,
211
  checkpoint_log_interval_steps=args.checkpoint_log_interval_steps,
212
+ save_merged_model=getattr(args, "save_merged_model", False),
213
  )
214
 
215
 
 
819
  if bf16_requested and not gpu_supports_bf16:
820
  raise RuntimeError("bf16 was requested, but the active GPU/runtime does not report BF16 support.")
821
 
822
+ if bf16_requested or (use_cuda and gpu_supports_bf16):
 
 
 
823
  precision_mode = "bf16"
824
  model_dtype = torch.bfloat16
825
  elif use_cuda:
826
+ precision_mode = SMOKE_PREFERRED_PRECISION
827
  model_dtype = torch.float16
828
  else:
829
  precision_mode = "fp32"
 
831
 
832
  use_bf16 = precision_mode == "bf16"
833
  use_fp16 = precision_mode == "fp16"
834
+ load_in_4bit = bool(config.load_in_4bit)
 
 
 
 
 
 
835
 
836
  return {
837
  "use_cuda": use_cuda,
 
977
  target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
978
  lora_alpha=config.lora_alpha,
979
  lora_dropout=0.0,
980
+ use_gradient_checkpointing="unsloth" if config.gradient_checkpointing else False,
981
  )
982
  if not load_in_4bit:
983
  model = model.to(model_dtype)
 
992
  "reason": "load_in_4bit=True",
993
  }
994
 
995
+ if load_in_4bit:
996
+ critical_precision_audit = {
997
+ "target_dtype": str(model_dtype),
998
+ "skipped": True,
999
+ "reason": "load_in_4bit=True",
1000
+ }
1001
+ else:
1002
+ critical_precision_audit = audit_critical_module_precision(model, model_dtype)
1003
  print(f"[training] critical precision audit {json.dumps(critical_precision_audit, sort_keys=True)}")
1004
+ if not load_in_4bit and critical_precision_audit["has_mismatch"]:
1005
  raise RuntimeError(
1006
  "Critical projection modules remain in the wrong dtype before GRPOTrainer initialization. "
1007
  f"Audit: {json.dumps(critical_precision_audit, sort_keys=True)}"
 
1146
  )
1147
  trainer.train()
1148
 
1149
+ if config.save_merged_model and hasattr(model, "save_pretrained_merged"):
1150
+ model.save_pretrained_merged(str(output_dir), tokenizer, save_method="merged_16bit")
1151
+ else:
1152
+ model.save_pretrained(str(output_dir))
1153
  tokenizer.save_pretrained(str(output_dir))
1154
 
1155
  if config.baseline_eval:
 
1215
 
1216
  def build_parser() -> argparse.ArgumentParser:
1217
  parser = argparse.ArgumentParser(description="GRPO training entrypoint for the ADAPT DSA environment.")
1218
+ parser.add_argument("--model-name", default="Qwen/Qwen2.5-3B-Instruct")
1219
+ parser.add_argument("--output-dir", default="outputs_l4")
1220
  parser.add_argument("--dataset-size", type=int, default=200)
1221
  parser.add_argument("--max-steps", type=int, default=250)
1222
  parser.add_argument("--batch-size", type=int, default=1)
1223
  parser.add_argument("--gradient-accumulation-steps", type=int, default=8)
1224
+ parser.add_argument("--num-generations", type=int, default=4)
1225
  parser.add_argument("--max-seq-length", type=int, default=2048)
1226
  parser.add_argument("--max-prompt-length", type=int, default=1024)
1227
  parser.add_argument("--max-completion-length", type=int, default=512)
 
1229
  parser.add_argument("--lora-rank", type=int, default=16)
1230
  parser.add_argument("--lora-alpha", type=int, default=16)
1231
  parser.add_argument("--disable-4bit", action="store_true")
1232
+ parser.add_argument("--disable-gradient-checkpointing", action="store_true")
1233
  parser.add_argument("--bf16", action="store_true")
1234
  parser.add_argument("--baseline-eval", action="store_true")
1235
  parser.add_argument("--evaluation-episodes", type=int, default=20)
 
1237
  parser.add_argument("--disable-wandb", action="store_true")
1238
  parser.add_argument("--wandb-project", default="adapt-dsa-tutor")
1239
  parser.add_argument("--wandb-run-name", default=None)
1240
+ parser.add_argument("--save-merged-model", action="store_true")
1241
  parser.add_argument("--trace-logging-enabled", action=argparse.BooleanOptionalAction, default=True)
1242
  parser.add_argument("--checkpoint-log-interval-steps", type=int, default=10)
1243
  parser.add_argument(
verifier/metrics.py CHANGED
@@ -2,6 +2,13 @@ from __future__ import annotations
2
 
3
  from typing import Any
4
 
 
 
 
 
 
 
 
5
 
6
  def compute_reward(
7
  pass_rate: float,
@@ -9,26 +16,21 @@ def compute_reward(
9
  execution_status: str,
10
  format_compliance: float,
11
  ) -> float:
12
- """
13
- Clean, interpretable reward signal for GRPO training.
14
- """
15
  del format_compliance
16
-
17
- step_discount = 1.0 if step_number == 1 else (0.85 if step_number == 2 else 0.70)
18
- correctness = pass_rate
19
-
20
- if execution_status == "timeout":
21
- return 0.0
22
- if execution_status == "syntax_error":
23
  return 0.0
24
-
25
- reward = correctness * step_discount
26
  return round(min(max(reward, 0.0), 1.0), 4)
27
 
28
 
29
  def compute_pass_rate(
30
  results: list[dict[str, Any]],
31
  step_number: int = 1,
 
 
 
 
32
  ) -> tuple[float, dict[str, Any]]:
33
  total = len(results)
34
  hidden_results = [result for result in results if result.get("visibility") == "hidden"]
@@ -36,15 +38,14 @@ def compute_pass_rate(
36
 
37
  hidden_total = len(hidden_results)
38
  visible_total = len(visible_results)
 
 
 
39
 
40
- hidden_passed = sum(1 for result in hidden_results if result["passed"])
41
- visible_passed = sum(1 for result in visible_results if result["passed"])
42
- passed = sum(1 for result in results if result["passed"])
43
-
44
- timeout_count = sum(1 for result in results if result["status"] == "timeout")
45
- runtime_error_count = sum(1 for result in results if result["status"] == "runtime_error")
46
- invalid_output_count = sum(1 for result in results if result["status"] == "invalid_output_format")
47
- wrong_answer_count = sum(1 for result in results if result["status"] == "wrong_answer")
48
  format_ok_count = sum(1 for result in results if result.get("format_ok", False))
49
 
50
  hidden_pass_rate = hidden_passed / hidden_total if hidden_total else 0.0
@@ -52,7 +53,13 @@ def compute_pass_rate(
52
  pass_rate = hidden_pass_rate if hidden_total else (passed / total if total else 0.0)
53
  format_compliance = format_ok_count / total if total else 0.0
54
 
55
- if timeout_count:
 
 
 
 
 
 
56
  execution_status = "timeout"
57
  elif runtime_error_count:
58
  execution_status = "runtime_error"
@@ -70,6 +77,15 @@ def compute_pass_rate(
70
  format_compliance=format_compliance,
71
  )
72
 
 
 
 
 
 
 
 
 
 
73
  return reward, {
74
  "passed": passed,
75
  "total": total,
@@ -85,10 +101,48 @@ def compute_pass_rate(
85
  "invalid_output_count": invalid_output_count,
86
  "wrong_answer_count": wrong_answer_count,
87
  "format_compliance": round(format_compliance, 4),
 
 
88
  "execution_status": execution_status,
89
  "reward_components": {
90
  "correctness": round(float(pass_rate), 4),
91
- "step_discount": 1.0 if step_number == 1 else (0.85 if step_number == 2 else 0.70),
92
  "reward": reward,
93
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
 
2
 
3
  from typing import Any
4
 
5
+ STEP_DISCOUNTS = {1: 1.0, 2: 0.85, 3: 0.70}
6
+ TERMINAL_ZERO_STATUSES = {"syntax_error", "safety_violation", "timeout"}
7
+
8
+
9
+ def step_discount(step_number: int) -> float:
10
+ return STEP_DISCOUNTS.get(int(step_number), 0.70)
11
+
12
 
13
  def compute_reward(
14
  pass_rate: float,
 
16
  execution_status: str,
17
  format_compliance: float,
18
  ) -> float:
 
 
 
19
  del format_compliance
20
+ correctness = max(0.0, min(float(pass_rate), 1.0))
21
+ if execution_status in TERMINAL_ZERO_STATUSES:
 
 
 
 
 
22
  return 0.0
23
+ reward = correctness * step_discount(step_number)
 
24
  return round(min(max(reward, 0.0), 1.0), 4)
25
 
26
 
27
  def compute_pass_rate(
28
  results: list[dict[str, Any]],
29
  step_number: int = 1,
30
+ *,
31
+ syntax_ok: bool = True,
32
+ safety_ok: bool = True,
33
+ precheck_status: str | None = None,
34
  ) -> tuple[float, dict[str, Any]]:
35
  total = len(results)
36
  hidden_results = [result for result in results if result.get("visibility") == "hidden"]
 
38
 
39
  hidden_total = len(hidden_results)
40
  visible_total = len(visible_results)
41
+ hidden_passed = sum(1 for result in hidden_results if result.get("passed"))
42
+ visible_passed = sum(1 for result in visible_results if result.get("passed"))
43
+ passed = sum(1 for result in results if result.get("passed"))
44
 
45
+ timeout_count = sum(1 for result in results if result.get("status") == "timeout")
46
+ runtime_error_count = sum(1 for result in results if result.get("status") == "runtime_error")
47
+ invalid_output_count = sum(1 for result in results if result.get("status") == "invalid_output_format")
48
+ wrong_answer_count = sum(1 for result in results if result.get("status") == "wrong_answer")
 
 
 
 
49
  format_ok_count = sum(1 for result in results if result.get("format_ok", False))
50
 
51
  hidden_pass_rate = hidden_passed / hidden_total if hidden_total else 0.0
 
53
  pass_rate = hidden_pass_rate if hidden_total else (passed / total if total else 0.0)
54
  format_compliance = format_ok_count / total if total else 0.0
55
 
56
+ if not syntax_ok:
57
+ execution_status = "syntax_error"
58
+ elif not safety_ok:
59
+ execution_status = "safety_violation"
60
+ elif precheck_status and precheck_status not in {"ready", "completed"}:
61
+ execution_status = precheck_status
62
+ elif timeout_count:
63
  execution_status = "timeout"
64
  elif runtime_error_count:
65
  execution_status = "runtime_error"
 
77
  format_compliance=format_compliance,
78
  )
79
 
80
+ verifier_components = {
81
+ "hidden_correctness": round(hidden_pass_rate, 4),
82
+ "visible_correctness": round(visible_pass_rate, 4),
83
+ "format_compliance": round(format_compliance, 4),
84
+ "runtime_reliability": round(0.0 if timeout_count or runtime_error_count else 1.0, 4),
85
+ "anti_cheat_compliance": round(1.0 if syntax_ok and safety_ok else 0.0, 4),
86
+ "step_discount": round(step_discount(step_number), 4),
87
+ }
88
+
89
  return reward, {
90
  "passed": passed,
91
  "total": total,
 
101
  "invalid_output_count": invalid_output_count,
102
  "wrong_answer_count": wrong_answer_count,
103
  "format_compliance": round(format_compliance, 4),
104
+ "syntax_valid": bool(syntax_ok),
105
+ "safety_valid": bool(safety_ok),
106
  "execution_status": execution_status,
107
  "reward_components": {
108
  "correctness": round(float(pass_rate), 4),
109
+ "step_discount": round(step_discount(step_number), 4),
110
  "reward": reward,
111
  },
112
+ "verifier_components": verifier_components,
113
+ }
114
+
115
+
116
+ def compute_episode_reward(
117
+ *,
118
+ pass_rate: float,
119
+ step_number: int,
120
+ execution_status: str,
121
+ previous_pass_rate: float,
122
+ done: bool,
123
+ efficiency_score: float,
124
+ optimization_target_met: bool,
125
+ ) -> tuple[float, dict[str, float]]:
126
+ discount = step_discount(step_number)
127
+ clipped_pass_rate = max(0.0, min(float(pass_rate), 1.0))
128
+ clipped_efficiency = max(0.0, min(float(efficiency_score), 1.0))
129
+ progress_delta = max(0.0, clipped_pass_rate - max(0.0, min(float(previous_pass_rate), 1.0)))
130
+
131
+ if execution_status in TERMINAL_ZERO_STATUSES:
132
+ reward = 0.0
133
+ elif clipped_pass_rate == 1.0:
134
+ reward = round(discount * (0.6 + 0.4 * clipped_efficiency), 4)
135
+ if not optimization_target_met and not done:
136
+ reward = min(reward, 0.94)
137
+ elif done:
138
+ reward = 0.0
139
+ else:
140
+ reward = round(0.1 * progress_delta, 4)
141
+
142
+ return reward, {
143
+ "correctness": round(clipped_pass_rate, 4),
144
+ "efficiency_score": round(clipped_efficiency, 4),
145
+ "step_discount": round(discount, 4),
146
+ "progress_delta": round(progress_delta, 4),
147
+ "reward": round(float(reward), 4),
148
  }
verifier/sandbox.py CHANGED
@@ -1,7 +1,89 @@
1
  from __future__ import annotations
2
 
 
 
 
3
  from env.executor import run_code as execute_submission
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- def run_code(code: str, stdin: str, timeout: int = 1) -> dict[str, object]:
7
  return execute_submission(code, stdin, timeout_seconds=timeout)
 
1
  from __future__ import annotations
2
 
3
+ import ast
4
+ from typing import Any
5
+
6
  from env.executor import run_code as execute_submission
7
 
8
+ FORBIDDEN_IMPORTS = {
9
+ "ctypes",
10
+ "os",
11
+ "pathlib",
12
+ "resource",
13
+ "shutil",
14
+ "signal",
15
+ "socket",
16
+ "subprocess",
17
+ }
18
+ FORBIDDEN_CALLS = {
19
+ "__import__",
20
+ "breakpoint",
21
+ "compile",
22
+ "eval",
23
+ "exec",
24
+ "open",
25
+ }
26
+
27
+
28
+ def _call_name(node: ast.AST) -> str:
29
+ if isinstance(node, ast.Name):
30
+ return node.id
31
+ if isinstance(node, ast.Attribute):
32
+ parent = _call_name(node.value)
33
+ return f"{parent}.{node.attr}" if parent else node.attr
34
+ return ""
35
+
36
+
37
+ def validate_code(code: str) -> dict[str, Any]:
38
+ try:
39
+ tree = ast.parse(code)
40
+ except SyntaxError as exc:
41
+ return {
42
+ "syntax_ok": False,
43
+ "safety_ok": False,
44
+ "execution_status": "syntax_error",
45
+ "error": str(exc),
46
+ }
47
+
48
+ for node in ast.walk(tree):
49
+ if isinstance(node, ast.Import):
50
+ for alias in node.names:
51
+ root_name = alias.name.split(".", 1)[0]
52
+ if root_name in FORBIDDEN_IMPORTS:
53
+ return {
54
+ "syntax_ok": True,
55
+ "safety_ok": False,
56
+ "execution_status": "safety_violation",
57
+ "error": f"Forbidden import: {root_name}",
58
+ }
59
+
60
+ if isinstance(node, ast.ImportFrom):
61
+ root_name = (node.module or "").split(".", 1)[0]
62
+ if root_name in FORBIDDEN_IMPORTS:
63
+ return {
64
+ "syntax_ok": True,
65
+ "safety_ok": False,
66
+ "execution_status": "safety_violation",
67
+ "error": f"Forbidden import: {root_name}",
68
+ }
69
+
70
+ if isinstance(node, ast.Call):
71
+ fn_name = _call_name(node.func)
72
+ if fn_name in FORBIDDEN_CALLS:
73
+ return {
74
+ "syntax_ok": True,
75
+ "safety_ok": False,
76
+ "execution_status": "safety_violation",
77
+ "error": f"Forbidden call: {fn_name}",
78
+ }
79
+
80
+ return {
81
+ "syntax_ok": True,
82
+ "safety_ok": True,
83
+ "execution_status": "ready",
84
+ "error": "",
85
+ }
86
+
87
 
88
+ def run_code(code: str, stdin: str, timeout: int | float = 1) -> dict[str, object]:
89
  return execute_submission(code, stdin, timeout_seconds=timeout)
verifier/verifier.py CHANGED
@@ -4,13 +4,36 @@ from typing import Any
4
 
5
  from verifier.complexity import analyze_code_complexity
6
  from verifier.metrics import compute_pass_rate
7
- from verifier.sandbox import run_code
8
 
9
 
10
- def verify(code: str, test_cases: list[dict[str, Any]] | list[tuple[str, str]]) -> tuple[float, dict[str, Any]]:
11
- results: list[dict[str, Any]] = []
 
 
 
 
 
12
  complexity = analyze_code_complexity(code)
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  for index, test_case in enumerate(test_cases):
15
  if isinstance(test_case, dict):
16
  stdin = str(test_case.get("input", ""))
@@ -51,13 +74,15 @@ def verify(code: str, test_cases: list[dict[str, Any]] | list[tuple[str, str]])
51
  "input": stdin if is_visible else "",
52
  "timed_out": timed_out,
53
  "exit_code": exit_code,
 
 
 
54
  "visibility": "visible" if is_visible else "hidden",
55
  }
56
  )
57
 
58
- reward, metrics = compute_pass_rate(results)
59
  feedback = _build_feedback(metrics)
60
-
61
  return reward, {
62
  **metrics,
63
  **complexity,
@@ -66,13 +91,18 @@ def verify(code: str, test_cases: list[dict[str, Any]] | list[tuple[str, str]])
66
  }
67
 
68
 
69
- def _build_feedback(metrics: dict[str, Any]) -> str:
70
- if metrics["execution_status"] == "timeout":
 
 
 
 
 
71
  return "Submission timed out on one or more hidden evaluation tests."
72
- if metrics["execution_status"] == "runtime_error":
73
  return "Submission raised a runtime error on one or more hidden evaluation tests."
74
- if metrics["execution_status"] == "invalid_output_format":
75
  return "Submission completed but produced invalid output format on one or more tests."
76
- if metrics["execution_status"] == "wrong_answer":
77
  return "Submission ran successfully but returned an incorrect answer on one or more hidden tests."
78
  return f"All hidden tests passed. Pass rate: {metrics['pass_rate']:.2f}"
 
4
 
5
  from verifier.complexity import analyze_code_complexity
6
  from verifier.metrics import compute_pass_rate
7
+ from verifier.sandbox import run_code, validate_code
8
 
9
 
10
+ def verify(
11
+ code: str,
12
+ test_cases: list[dict[str, Any]] | list[tuple[str, str]],
13
+ *,
14
+ step_number: int = 1,
15
+ ) -> tuple[float, dict[str, Any]]:
16
+ precheck = validate_code(code)
17
  complexity = analyze_code_complexity(code)
18
 
19
+ if not precheck["syntax_ok"] or not precheck["safety_ok"]:
20
+ reward, metrics = compute_pass_rate(
21
+ [],
22
+ step_number=step_number,
23
+ syntax_ok=bool(precheck["syntax_ok"]),
24
+ safety_ok=bool(precheck["safety_ok"]),
25
+ precheck_status=str(precheck["execution_status"]),
26
+ )
27
+ feedback = _build_feedback(metrics, error=str(precheck["error"]))
28
+ return reward, {
29
+ **metrics,
30
+ **complexity,
31
+ "feedback": feedback,
32
+ "results": [],
33
+ "error": str(precheck["error"]),
34
+ }
35
+
36
+ results: list[dict[str, Any]] = []
37
  for index, test_case in enumerate(test_cases):
38
  if isinstance(test_case, dict):
39
  stdin = str(test_case.get("input", ""))
 
74
  "input": stdin if is_visible else "",
75
  "timed_out": timed_out,
76
  "exit_code": exit_code,
77
+ "duration_ms": execution.get("duration_ms", 0.0),
78
+ "sandboxed": bool(execution.get("sandboxed", False)),
79
+ "sandbox_mode": execution.get("sandbox_mode", "portable"),
80
  "visibility": "visible" if is_visible else "hidden",
81
  }
82
  )
83
 
84
+ reward, metrics = compute_pass_rate(results, step_number=step_number)
85
  feedback = _build_feedback(metrics)
 
86
  return reward, {
87
  **metrics,
88
  **complexity,
 
91
  }
92
 
93
 
94
+ def _build_feedback(metrics: dict[str, Any], *, error: str = "") -> str:
95
+ execution_status = str(metrics.get("execution_status", "unknown"))
96
+ if execution_status == "syntax_error":
97
+ return f"Submission has a syntax error. {error}".strip()
98
+ if execution_status == "safety_violation":
99
+ return f"Submission violated the sandbox policy. {error}".strip()
100
+ if execution_status == "timeout":
101
  return "Submission timed out on one or more hidden evaluation tests."
102
+ if execution_status == "runtime_error":
103
  return "Submission raised a runtime error on one or more hidden evaluation tests."
104
+ if execution_status == "invalid_output_format":
105
  return "Submission completed but produced invalid output format on one or more tests."
106
+ if execution_status == "wrong_answer":
107
  return "Submission ran successfully but returned an incorrect answer on one or more hidden tests."
108
  return f"All hidden tests passed. Pass rate: {metrics['pass_rate']:.2f}"