Spaces:
Sleeping
Sleeping
Smoke-train deployment update
Browse files- README.md +9 -3
- env/__init__.py +2 -3
- env/adapt_env.py +38 -143
- env/app.py +172 -9
- env/executor.py +64 -15
- env/generator.py +13 -2
- pyproject.toml +2 -1
- requirements.txt +8 -7
- scripts/deploy_and_smoke_train.py +343 -168
- scripts/test_env.py +3 -0
- scripts/test_training_config.py +74 -0
- scripts/test_verifier.py +11 -0
- server/app.py +4 -1
- server/runtime.py +13 -3
- test.py +2 -0
- training/train_grpo.py +62 -25
- verifier/metrics.py +76 -22
- verifier/sandbox.py +83 -1
- verifier/verifier.py +40 -10
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 |
-
|
| 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
|
| 226 |
```
|
| 227 |
|
| 228 |
### 7. Plot the training curves
|
| 229 |
|
| 230 |
```powershell
|
| 231 |
-
python training\plot_results.py
|
| 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.
|
| 2 |
-
from env.generator import GeneratorAgent, generator_reward, validate_problem
|
| 3 |
|
| 4 |
-
__all__ = ["
|
|
|
|
| 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
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 222 |
-
done =
|
| 223 |
-
|
|
|
|
| 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=
|
| 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=
|
|
|
|
| 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=
|
| 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 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
demo = gr.Interface(
|
| 7 |
-
fn=solve,
|
| 8 |
-
inputs="text",
|
| 9 |
-
outputs="text",
|
| 10 |
-
title="DSA Solver"
|
| 11 |
-
)
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
| 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
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
TIMEOUT_SECONDS = 1
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
|
|
|
|
|
|
|
| 25 |
try:
|
| 26 |
result = subprocess.run(
|
| 27 |
-
[sys.executable, str(
|
| 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":
|
| 43 |
-
"stderr":
|
| 44 |
-
"exit_code": result.returncode,
|
| 45 |
"timed_out": False,
|
|
|
|
|
|
|
|
|
|
| 46 |
}
|
| 47 |
finally:
|
| 48 |
-
shutil.rmtree(
|
|
|
|
| 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 |
-
|
| 161 |
-
|
|
|
|
| 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.
|
| 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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
| 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
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
if
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
flush=True,
|
| 91 |
-
)
|
| 92 |
-
return True
|
| 93 |
|
| 94 |
-
if
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
)
|
| 99 |
-
return False
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
)
|
| 105 |
return True
|
| 106 |
|
| 107 |
|
| 108 |
-
def
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
| 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 |
-
) ->
|
| 141 |
-
health_url = f"{base_url.rstrip('/')}/health"
|
| 142 |
deadline = time.time() + timeout_seconds
|
| 143 |
push_started_at = time.time()
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
consecutive_healthy_checks = 0
|
| 148 |
|
| 149 |
while time.time() < deadline:
|
| 150 |
try:
|
| 151 |
-
payload =
|
| 152 |
-
except
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
time.sleep(poll_interval_seconds)
|
| 157 |
continue
|
| 158 |
|
| 159 |
-
if
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
waited_long_enough = (time.time() - push_started_at) >= min_deploy_wait_seconds
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 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 |
-
|
| 193 |
-
|
| 194 |
-
f"
|
| 195 |
-
|
| 196 |
)
|
| 197 |
-
if
|
| 198 |
-
|
| 199 |
-
|
|
|
|
| 200 |
else:
|
| 201 |
-
|
| 202 |
-
|
| 203 |
|
| 204 |
-
|
| 205 |
time.sleep(poll_interval_seconds)
|
| 206 |
|
| 207 |
-
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
|
| 210 |
-
def
|
| 211 |
-
|
| 212 |
-
payload = http_json("POST",
|
| 213 |
-
|
| 214 |
return payload
|
| 215 |
|
| 216 |
|
| 217 |
-
def poll_training_status(
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
deadline = time.time() + timeout_seconds
|
|
|
|
| 220 |
|
| 221 |
while time.time() < deadline:
|
| 222 |
-
payload =
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
|
|
|
| 230 |
)
|
|
|
|
|
|
|
|
|
|
| 231 |
|
|
|
|
| 232 |
if status == "failed":
|
| 233 |
-
|
| 234 |
-
print(
|
| 235 |
return payload
|
| 236 |
-
|
| 237 |
if status == "succeeded":
|
| 238 |
-
|
| 239 |
-
print(
|
| 240 |
return payload
|
| 241 |
|
| 242 |
time.sleep(poll_interval_seconds)
|
| 243 |
|
| 244 |
-
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
|
| 247 |
def build_parser() -> argparse.ArgumentParser:
|
| 248 |
-
parser = argparse.ArgumentParser(
|
|
|
|
|
|
|
| 249 |
parser.add_argument(
|
| 250 |
"--base-url",
|
| 251 |
required=True,
|
| 252 |
-
help="Base URL of the
|
| 253 |
)
|
| 254 |
parser.add_argument(
|
| 255 |
-
"--
|
| 256 |
-
default="
|
| 257 |
-
help="
|
| 258 |
)
|
| 259 |
-
parser.add_argument("--remote", default=DEFAULT_REMOTE, help="Git remote for the Hugging Face Space.")
|
| 260 |
parser.add_argument(
|
| 261 |
-
"--
|
| 262 |
-
default=
|
| 263 |
-
help="
|
| 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=
|
| 275 |
-
help="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
)
|
| 277 |
parser.add_argument(
|
| 278 |
"--required-healthy-checks",
|
| 279 |
type=int,
|
| 280 |
default=DEFAULT_REQUIRED_HEALTHY_CHECKS,
|
| 281 |
-
help="
|
| 282 |
)
|
| 283 |
parser.add_argument(
|
| 284 |
"--min-deploy-wait-seconds",
|
| 285 |
type=int,
|
| 286 |
default=DEFAULT_MIN_DEPLOY_WAIT_SECONDS,
|
| 287 |
-
help="Minimum
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 298 |
-
|
| 299 |
-
print(
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
final_status = poll_training_status(
|
| 315 |
-
|
| 316 |
poll_interval_seconds=args.poll_interval_seconds,
|
| 317 |
-
timeout_seconds=args.
|
| 318 |
)
|
|
|
|
| 319 |
except Exception as exc:
|
| 320 |
-
|
| 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 = "
|
| 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 "
|
| 165 |
|
| 166 |
def _active_generation_stack(
|
| 167 |
self,
|
|
@@ -194,7 +194,12 @@ class SpaceModelRegistry:
|
|
| 194 |
)
|
| 195 |
return self.status_payload()
|
| 196 |
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = "
|
| 39 |
-
output_dir: str = "
|
| 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 =
|
| 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 |
-
|
|
|
|
| 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 |
-
"
|
|
|
|
| 79 |
"bf16": False,
|
| 80 |
"output_dir": "outputs_smoke",
|
| 81 |
"checkpoint_log_interval_steps": 2,
|
| 82 |
},
|
| 83 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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
|
| 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 =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 1184 |
-
parser.add_argument("--output-dir", default="
|
| 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=
|
| 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 |
-
|
| 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 |
-
|
| 41 |
-
|
| 42 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 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(
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
return "Submission timed out on one or more hidden evaluation tests."
|
| 72 |
-
if
|
| 73 |
return "Submission raised a runtime error on one or more hidden evaluation tests."
|
| 74 |
-
if
|
| 75 |
return "Submission completed but produced invalid output format on one or more tests."
|
| 76 |
-
if
|
| 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}"
|