qwen3-8b-code-pkpo / eval_lib.py
bk1dr's picture
Initial scaffold
b53d9a0 verified
Raw
History Blame Contribute Delete
15 kB
"""Dataset, judging, and sandbox helpers for the PKPO coding run.
All generated code is executed inside Modal containers. This module still applies
per-process time and memory limits and uses a guarded Python runner that blocks
network-oriented imports.
"""
from __future__ import annotations
import base64
import json
import os
import pickle
import random
import re
import resource
import subprocess
import sys
import tempfile
import textwrap
import time
import zlib
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any
from agent_core import coding_task_message, extract_code
@dataclass
class Problem:
source: str
problem_id: str
title: str
statement: str
public_tests: list[dict[str, str]]
hidden_tests: list[dict[str, str]]
starter_code: str = ""
difficulty: str = ""
contest_date: str = ""
def first_user_message(self, max_turns: int = 1) -> str:
return coding_task_message(
f"{self.title}\n\n{self.statement}",
starter_code=self.starter_code,
max_turns=max_turns,
)
def to_public_dict(self) -> dict[str, Any]:
d = asdict(self)
d["hidden_tests"] = [{"input": t.get("input", "")[:80], "output_len": len(t.get("output", ""))}
for t in self.hidden_tests[:3]]
d["statement"] = self.statement[:500]
return d
def _json_or_encoded_tests(value: Any) -> list[dict[str, str]]:
if value is None or value == "":
return []
if isinstance(value, list):
return value
if not isinstance(value, str):
return []
s = value.strip()
if not s:
return []
try:
obj = json.loads(s)
return obj if isinstance(obj, list) else []
except Exception:
pass
raw = zlib.decompress(base64.b64decode(s))
try:
obj = pickle.loads(raw)
except Exception:
obj = raw.decode("utf-8")
if isinstance(obj, str):
obj = json.loads(obj)
return obj if isinstance(obj, list) else []
def _stdin_tests(tests: list[dict[str, Any]], limit: int | None = None) -> list[dict[str, str]]:
out = []
for t in tests:
if t.get("testtype", "stdin") != "stdin":
continue
if "input" not in t or "output" not in t:
continue
out.append({"input": str(t["input"]), "output": str(t["output"]), "testtype": "stdin"})
if limit and len(out) >= limit:
break
return out
def load_lcb_v6_subset(limit: int = 12, seed: int = 7341) -> list[Problem]:
"""Latest code_generation_lite release file is v6/test6.jsonl."""
from datasets import load_dataset
ds = load_dataset("livecodebench/code_generation_lite", "v6", split="test", trust_remote_code=True)
candidates: list[Problem] = []
for row in ds:
if str(row.get("starter_code") or "").strip():
continue
public = _stdin_tests(_json_or_encoded_tests(row.get("public_test_cases")))
hidden = _stdin_tests(_json_or_encoded_tests(row.get("private_test_cases")))
if not public or not hidden:
continue
candidates.append(Problem(
source="livecodebench/code_generation_lite:v6",
problem_id=str(row["question_id"]),
title=str(row.get("question_title") or row["question_id"]),
statement=str(row.get("question_content") or ""),
public_tests=public,
hidden_tests=hidden,
starter_code=str(row.get("starter_code") or ""),
difficulty=str(row.get("difficulty") or ""),
contest_date=str(row.get("contest_date") or ""),
))
candidates.sort(key=lambda p: (p.contest_date, p.problem_id), reverse=True)
rng = random.Random(seed)
latest_pool = candidates[: max(limit * 4, limit)]
rng.shuffle(latest_pool)
return latest_pool[:limit]
def _tests_from_io_dict(value: Any, limit: int | None = None) -> list[dict[str, str]]:
if not isinstance(value, dict):
return []
ins = value.get("input") or []
outs = value.get("output") or []
if len(ins) != len(outs):
return []
out = []
for i, o in zip(ins, outs):
out.append({"input": str(i), "output": str(o), "testtype": "stdin"})
if limit and len(out) >= limit:
break
return out
def load_codecontest_train(
limit: int = 12,
seed: int = 20260709,
exclude_problem_ids: set[str] | None = None,
max_rows: int | None = None,
min_cf_rating: int = 800,
max_cf_rating: int = 1300,
) -> list[Problem]:
"""Sample valid old CodeContests train problems without an early-prefix bias.
``streaming=True`` yields a stable dataset prefix; shuffling only after
prematurely stopping at ``limit`` is not a sample. Reservoir sampling lets a
bounded scan supply a deterministic, broader curriculum while keeping SFT and
RL problem ids disjoint.
A Qwen3-8B base policy earns ~0 reward on unrated/hard Codeforces problems,
which starves PKPO of nonzero groups, so the RL pool is restricted to
problems with a KNOWN cf_rating inside [min_cf_rating, max_cf_rating].
"""
from datasets import load_dataset
stream = load_dataset("deepmind/code_contests", split="train", streaming=True)
excluded = exclude_problem_ids or set()
rng = random.Random(seed)
rows: list[Problem] = []
seen = 0
max_rows = max_rows or max(3000, limit * 45)
for row_idx, row in enumerate(stream, start=1):
if row_idx > max_rows:
break
rating = int(row.get("cf_rating") or 0)
if rating < min_cf_rating or rating > max_cf_rating:
continue
desc = str(row.get("description") or "")
if not desc or len(desc) > 4500:
continue
low = desc.lower()
if "interactive" in low or "output-only" in low:
continue
if str(row.get("input_file") or "").strip() or str(row.get("output_file") or "").strip():
continue
public = _tests_from_io_dict(row.get("public_tests"), limit=3)
generated = _tests_from_io_dict(row.get("generated_tests"), limit=18)
private = _tests_from_io_dict(row.get("private_tests"), limit=18)
hidden = generated or private
problem_id = str(row.get("name") or f"codecontest-{row_idx}")
if problem_id in excluded or not public or len(hidden) < 6:
continue
problem = Problem(
source="deepmind/code_contests:train",
problem_id=problem_id,
title=str(row.get("name") or "CodeContests problem"),
statement=desc,
public_tests=public,
hidden_tests=hidden,
difficulty=str(row.get("difficulty") or ""),
)
seen += 1
if len(rows) < limit:
rows.append(problem)
else:
replace_idx = rng.randrange(seen)
if replace_idx < limit:
rows[replace_idx] = problem
rng.shuffle(rows)
return rows
def python3_verified_solutions(
limit: int = 8,
seed: int = 20260710,
candidate_multiplier: int = 2,
max_rows: int | None = None,
min_cf_rating: int = 800,
max_cf_rating: int = 1500,
) -> list[tuple[Problem, str]]:
"""Return runnable CodeContests Python-3 reference solutions only.
``solutions.language`` is a ClassLabel integer in CodeContests: 1 is Python
(Python 2) while 3 is Python 3. The earlier loader ignored that paired field,
so it could SFT on ``raw_input``/bare-``print`` programs that fail our Python 3
judge. Each candidate here is also compiled and run on held-out generated
tests before it is admitted to the warm-up set.
"""
from datasets import load_dataset
stream = load_dataset("deepmind/code_contests", split="train", streaming=True)
pairs: list[tuple[Problem, str]] = []
wanted = max(limit, limit * max(1, candidate_multiplier))
max_rows = max_rows or min(6000, max(500, wanted * 40))
for row_idx, row in enumerate(stream, start=1):
if row_idx > max_rows:
break
rating = int(row.get("cf_rating") or 0)
if rating < min_cf_rating or rating > max_cf_rating:
continue
desc = str(row.get("description") or "")
if not desc or len(desc) > 4500:
continue
low = desc.lower()
if "interactive" in low or "output-only" in low:
continue
if str(row.get("input_file") or "").strip() or str(row.get("output_file") or "").strip():
continue
sols = row.get("solutions") or {}
languages = sols.get("language") or []
solutions = sols.get("solution") or []
public = _tests_from_io_dict(row.get("public_tests"), limit=2)
generated = _tests_from_io_dict(row.get("generated_tests"), limit=3)
private = _tests_from_io_dict(row.get("private_tests"), limit=3)
held_out = generated or private
if not public or not held_out:
continue
for language, raw_solution in zip(languages, solutions):
# CodeContests' ClassLabel id 3 is PYTHON3. Do not accept PYTHON (id
# 1), even if it happens to compile under the local interpreter.
if language != 3:
continue
solution = textwrap.dedent(str(raw_solution)).strip()
if not 40 <= len(solution) <= 7000:
continue
try:
compile(solution, "<codecontests-python3>", "exec")
except (SyntaxError, ValueError, TypeError):
continue
verdict = judge_code(solution, held_out, timeout_s=3, memory_mb=768, max_tests=3)
if not verdict.get("passed"):
continue
prob = Problem(
source="deepmind/code_contests:train",
problem_id=str(row.get("name") or f"sft-{len(pairs)}"),
title=str(row.get("name") or "CodeContests problem"),
statement=desc,
public_tests=public,
hidden_tests=held_out,
difficulty=str(row.get("difficulty") or ""),
)
pairs.append((prob, solution))
break
if len(pairs) >= wanted:
break
rng = random.Random(seed)
rng.shuffle(pairs)
return pairs[:limit]
GUARD = r'''
import builtins
import sys
blocked = {
"_socket", "socket", "ssl", "urllib", "http", "ftplib", "requests",
"subprocess", "multiprocessing", "ctypes",
}
real_import = builtins.__import__
def guarded_import(name, globals=None, locals=None, fromlist=(), level=0):
root = name.split(".", 1)[0]
if root in blocked:
raise ImportError(f"blocked import: {name}")
return real_import(name, globals, locals, fromlist, level)
builtins.__import__ = guarded_import
path = sys.argv[1]
with open(path, "r", encoding="utf-8", errors="replace") as f:
src = f.read()
ns = {"__name__": "__main__", "__file__": path}
exec(compile(src, path, "exec"), ns, ns)
'''
def _limit_child(timeout_s: int, memory_mb: int, cwd: str):
def inner():
os.chdir(cwd)
cpu = max(1, int(timeout_s) + 1)
for res, lim in [
(resource.RLIMIT_CPU, (cpu, cpu + 1)),
(resource.RLIMIT_AS, (memory_mb * 1024 * 1024, memory_mb * 1024 * 1024)),
(resource.RLIMIT_FSIZE, (32 * 1024 * 1024, 32 * 1024 * 1024)),
(resource.RLIMIT_NOFILE, (64, 64)),
]:
try:
resource.setrlimit(res, lim)
except Exception:
pass
return inner
def normalize_output(text: str) -> str:
text = text.replace("\r\n", "\n").replace("\r", "\n")
return "\n".join(line.rstrip() for line in text.strip().split("\n")).strip()
def judge_code(code: str, tests: list[dict[str, str]], timeout_s: int = 3,
memory_mb: int = 768, max_tests: int | None = None) -> dict[str, Any]:
tests = _stdin_tests(tests, limit=max_tests)
started = time.time()
if not code.strip():
return {
"passed": False,
"passed_tests": 0,
"total_tests": len(tests),
"error": "empty code",
"seconds": time.time() - started,
}
with tempfile.TemporaryDirectory(prefix="pkpo_exec_") as td:
sol = Path(td) / "solution.py"
runner = Path(td) / "guarded_runner.py"
sol.write_text(code, encoding="utf-8")
runner.write_text(GUARD, encoding="utf-8")
for idx, test in enumerate(tests):
try:
cp = subprocess.run(
[sys.executable, "-I", str(runner), str(sol)],
input=test["input"],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout_s + 1,
preexec_fn=_limit_child(timeout_s, memory_mb, td),
)
except subprocess.TimeoutExpired:
return {
"passed": False,
"passed_tests": idx,
"total_tests": len(tests),
"error": "timeout",
"seconds": time.time() - started,
}
if cp.returncode != 0:
return {
"passed": False,
"passed_tests": idx,
"total_tests": len(tests),
"error": "runtime_error",
"exit_code": cp.returncode,
"stderr": cp.stderr[-500:],
"seconds": time.time() - started,
}
got = normalize_output(cp.stdout)
want = normalize_output(test["output"])
if got != want:
return {
"passed": False,
"passed_tests": idx,
"total_tests": len(tests),
"error": "wrong_answer",
"got": got[:300],
"want": want[:300],
"seconds": time.time() - started,
}
return {"passed": True, "passed_tests": len(tests), "total_tests": len(tests), "seconds": time.time() - started}
def judge_final_answer(final_answer: str, tests: list[dict[str, str]], **kwargs) -> dict[str, Any]:
return judge_code(extract_code(final_answer), tests, **kwargs)
def compact_json_dump(path: str | Path, obj: Any):
Path(path).parent.mkdir(parents=True, exist_ok=True)
Path(path).write_text(json.dumps(obj, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def short_completion_for_sft(solution: str) -> str:
solution = textwrap.dedent(solution).strip()
return (
"I will provide a direct Python solution.\n</think>\n"
"<answer>Tool type: final\nTool query: ```python\n"
+ solution
+ "\n```</answer>"
)