Spaces:
Running
Running
File size: 7,619 Bytes
e313085 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | """Mac-runnable checks. Rollout test needs a mock server:
cd ../kernel_env && KERNEL_ENV_EVALUATOR=mock KERNELBENCH_DIR=<dir with level1/*.py> \
.venv/bin/uvicorn server.app:app --port 8765
KERNEL_ENV_URL=http://127.0.0.1:8765 KERNEL_ENV_TASK=level1/19_ReLU .venv/bin/python -m pytest test_train.py
"""
import os
import sys
from collections import defaultdict
from pathlib import Path
from types import SimpleNamespace
import pytest
from baseline_eval import extract_code, run_episodes, run_episodes_pipelined, summarize
from grpo_kernel import KernelArgs, episode_return, feedback_ids, make_rollout_func
sys.path.insert(0, str(Path(__file__).parent.parent / "kernel_env"))
from tests.kernels import GOOD # noqa: E402 (kernel_env's fixture kernel)
URL = os.environ.get("KERNEL_ENV_URL")
TASK = os.environ.get("KERNEL_ENV_TASK", "level1/19_ReLU")
TOKENIZER = "Qwen/Qwen2.5-0.5B-Instruct"
BAD = "class ModelNew:\n pass" # no kernel -> rejected by static checks, reward 0
def test_extract_code():
assert extract_code("hi\n```python\na = 1\n```\nbye") == "a = 1"
assert extract_code("```py\nx\n```\n```python\nlast\n```") == "last"
assert extract_code("```\nplain\n```") == "plain"
assert extract_code("```python\nb = 2\n```\n```text\nlog\n```") == "b = 2" # python beats later non-python
assert extract_code("```python\ncut = 1\nmore") == "cut = 1\nmore" # truncated fence
assert extract_code(" import torch\n") == "import torch" # bare source, as the prompt asks
def test_episode_return():
assert episode_return([0.0, 0.3, 1.3], "discounted", 0.5) == pytest.approx(0.5 * 0.3 + 0.25 * 1.3)
assert episode_return([0.0, 1.3, 0.3], "best", 0.5) == 1.3
assert episode_return([], "best", 1.0) == 0.0
def test_summarize():
eps = [{"turns": [{"stage": "format", "correct": False, "speedup": 0.0},
{"stage": "timed", "correct": True, "speedup": 2.0}]},
{"turns": [{"stage": "compile", "correct": False, "speedup": 0.0}]}]
s = summarize(eps)
assert (s["compile_pct"], s["correct_pct"], s["correct_turn1_pct"], s["median_speedup"]) == (50, 50, 0, 2.0)
assert s["stage_hist_furthest"] == {"timed": 1, "compile": 1}
def _tok():
from transformers import AutoTokenizer
return AutoTokenizer.from_pretrained(TOKENIZER)
def test_feedback_ids_stitch_like_chat_template():
tok = _tok()
conv = [{"role": "user", "content": "P"}, {"role": "assistant", "content": "A1"}, {"role": "user", "content": "F"}]
want = tok.apply_chat_template(conv, add_generation_prompt=True, tokenize=True, return_dict=False)
p = tok.apply_chat_template(conv[:1], add_generation_prompt=True, tokenize=True, return_dict=False)
got = p + tok.encode("A1") + [tok.eos_token_id] + feedback_ids(tok, "F")
assert got == want
class FakeTrainer:
"""Stands in for GRPOTrainer: replies BAD on turn 1 and GOOD (fenced) afterwards."""
def __init__(self, tok):
self.processing_class, self.tok, self.calls = tok, tok, 0
self.model = SimpleNamespace(training=True)
self.state = SimpleNamespace(global_step=0)
self._metrics = {"train": defaultdict(list), "eval": defaultdict(list)}
def _generate_single_turn(self, prompt_ids, images, mm, n):
self.calls += 1
text = BAD if self.calls == 1 else f"Here:\n```python\n{GOOD}\n```"
ids = self.tok.encode(text) + [self.tok.eos_token_id]
return [ids for _ in prompt_ids], None
@pytest.mark.skipif(not URL, reason="set KERNEL_ENV_URL to a mock kernel_env server")
def test_rollout_func_against_mock_server(tmp_path):
tok, n = _tok(), 3
trainer = FakeTrainer(tok)
fn = make_rollout_func(KernelArgs(env_url=URL, gamma=0.5), str(tmp_path / "s.jsonl"))
out = fn([TASK] * n, trainer)
assert out["logprobs"] is None and len(out["prompt_ids"]) == n
max_turns = trainer.calls # one generate call per turn, all episodes in lockstep
for p, c, m in zip(out["prompt_ids"], out["completion_ids"], out["env_mask"]):
assert len(c) == len(m)
# stitched token stream == chat template of the same conversation
text = tok.decode(p + c)
assert text.count("<|im_start|>assistant") == max_turns and "stage=format" in text
model_text = tok.decode([t for t, k in zip(c, m) if k])
assert BAD in model_text and "stage=" not in model_text # feedback tokens are masked out
# mock evaluator: turn 1 rejected (0), later turns speedup 1.0 -> 0.3 each
want = sum(0.5**t * 0.3 for t in range(1, max_turns))
assert out["episode_return"] == pytest.approx([want] * n)
assert trainer._metrics["train"]["kernel/correct_pct"] == [100.0]
# every episode logged for report.py
import json
from report import render
logged = [json.loads(line) for line in (tmp_path / "rollouts-rank0.jsonl").read_text().splitlines()]
assert len(logged) == n and all(len(ep["completions"]) == len(ep["turns"]) == max_turns for ep in logged)
assert "Turn by turn" in render(logged, "rollouts")
@pytest.mark.skipif(not URL, reason="set KERNEL_ENV_URL to a mock kernel_env server")
def test_run_episodes_against_mock_server():
eps = run_episodes(lambda convs: [f"```python\n{GOOD}\n```"] * len(convs), URL, [TASK, TASK])
assert all(t["correct"] for ep in eps for t in ep["turns"])
assert summarize(eps)["correct_turn1_pct"] == 100
@pytest.mark.skipif(not URL, reason="set KERNEL_ENV_URL to a mock kernel_env server")
def test_pipelined_episodes_against_mock_server():
import threading
seen_feedback, lock = [], threading.Lock()
def chat(conv):
with lock:
seen_feedback.append(conv[-1]["content"])
if len(conv) == 1: # first turn: cut off mid-module
return "```python\nimport torch\n# reasoning...", "length", None, 8192
return f"```python\n{GOOD}\n```", "stop", None, 500
done = []
eps = run_episodes_pipelined(chat, URL, [TASK, TASK, "level1/does_not_exist"], on_done=done.append)
assert len(done) == 3 and "error" in eps[2] and not eps[2]["turns"]
for ep in eps[:2]:
assert ep["turns"][0]["truncated"] and ep["turns"][0]["stage"] == "format"
assert all(t["correct"] and not t["truncated"] for t in ep["turns"][1:])
assert ep["episode_id"] and len(ep["history"]) == len(ep["turns"])
assert sum("cut off before the module was complete" in f for f in seen_feedback) == 2
def test_feedback_ids_qwen35_template():
# Qwen3.5 is not prefix-stable (earlier assistant turns lose their think block); suffix must still be exact
transformers = pytest.importorskip("transformers")
try:
tok = transformers.AutoTokenizer.from_pretrained("Qwen/Qwen3.5-9B")
except OSError:
pytest.skip("Qwen3.5 tokenizer not available offline")
from grpo_kernel import feedback_ids
assert tok.decode(feedback_ids(tok, "fb")) == "\n<|im_start|>user\nfb<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n"
@pytest.mark.skipif(not URL, reason="set KERNEL_ENV_URL to a mock kernel_env server")
def test_rollout_unwraps_processor(tmp_path):
# VLM checkpoints (Qwen3.5) hand the rollout a Processor whose chat template returns nested lists
tok = _tok()
trainer = FakeTrainer(tok)
trainer.processing_class = SimpleNamespace(tokenizer=tok)
out = make_rollout_func(KernelArgs(env_url=URL), str(tmp_path / "s.jsonl"))([TASK] * 2, trainer)
assert all(isinstance(t, int) for p in out["prompt_ids"] for t in p)
assert all(isinstance(t, int) for c in out["completion_ids"] for t in c)
|