Spaces:
Running
Running
Download train/baseline_eval.py from Shreyansh327/kernel-env: direct link, hf CLI and curl.
- Browser
- Download file 19.5 kB
-
https://huggingface.co/spaces/Shreyansh327/kernel-env/resolve/main/train/baseline_eval.py
- Command line
-
hf download hf://spaces/Shreyansh327/kernel-env/train/baseline_eval.py
-
curl -L -o baseline_eval.py https://huggingface.co/spaces/Shreyansh327/kernel-env/resolve/main/train/baseline_eval.py
19.5 kB
| """Baseline: frozen model on the held-out KernelBench split, multi-turn, via the kernel_env server. | |
| python baseline_eval.py --env-url http://localhost:8000 --model Qwen/Qwen2.5-Coder-7B-Instruct --backend vllm | |
| Each eval task is one episode of up to `max_turns` submissions; after each submission the model | |
| sees the env feedback (stage, errors, timings) and may refine. Reports compile %, correct %, | |
| mean/median speedup (best correct submission per episode) and a stage histogram. | |
| Also holds the helpers grpo_kernel.py shares (code extraction, feedback message, metrics). | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import shlex | |
| import statistics | |
| import subprocess | |
| import threading | |
| import time | |
| import urllib.request | |
| from collections import Counter | |
| from concurrent.futures import ThreadPoolExecutor | |
| from kernel_env import KernelAction, KernelEnv | |
| from kernel_env.server.tasks import load_tasks | |
| STAGE_ORDER = ["format", "compile", "runtime", "timeout", "correctness", "timed"] | |
| _FENCE = re.compile(r"```(\w*)[^\n]*\n(.*?)```", re.DOTALL) | |
| def extract_code(text: str) -> str: | |
| """Last ```python block, else last fenced block, else the raw text (prompt asks for bare source).""" | |
| blocks = _FENCE.findall(text) | |
| py = [body for lang, body in blocks if lang.lower() in ("python", "py")] | |
| if py or blocks: | |
| return (py or [body for _, body in blocks])[-1].strip() | |
| # unterminated fence (generation cut off): drop the opening line | |
| if "```" in text: | |
| return text.split("```", 1)[1].split("\n", 1)[-1].strip() | |
| return text.strip() | |
| def feedback_message(obs, truncated: bool = False) -> str: | |
| """User turn shown to the model after a submission.""" | |
| msg = f"Submission {obs.turn}/{obs.max_turns}: stage={obs.stage}." | |
| if truncated: | |
| msg = ("Your previous reply hit the token limit and was cut off before the module was complete. " | |
| "Reply with the module source only, no commentary.\n" + msg) | |
| if obs.correct and obs.speedup is not None: | |
| msg += f" Correct, speedup {obs.speedup:.2f}x (ref {obs.t_ref_ms:.3f} ms, yours {obs.t_new_ms:.3f} ms)." | |
| if obs.feedback: | |
| msg += f"\n{obs.feedback}" | |
| if obs.turn < obs.max_turns: | |
| msg += "\nImprove it (fix errors first, then make it faster). Reply with the complete module source only." | |
| return msg | |
| def summarize(episodes: list) -> dict: | |
| """episodes: dicts with `turns` = [{"stage", "correct", "speedup", "reward"}, ...].""" | |
| n = max(len(episodes), 1) | |
| furthest = [max((STAGE_ORDER.index(t["stage"]) for t in ep["turns"]), default=0) for ep in episodes] | |
| best = [max((t["speedup"] for t in ep["turns"] if t["correct"]), default=None) for ep in episodes] | |
| sp = [s for s in best if s is not None] | |
| roof = [max((t["roofline_frac"] for t in ep["turns"] if t["correct"] and t.get("roofline_frac")), default=None) | |
| for ep in episodes] | |
| roof = [r for r in roof if r is not None] | |
| return { | |
| "episodes": len(episodes), | |
| "compile_pct": 100 * sum(f >= STAGE_ORDER.index("runtime") for f in furthest) / n, | |
| "correct_pct": 100 * len(sp) / n, | |
| "correct_turn1_pct": 100 * sum(bool(ep["turns"] and ep["turns"][0]["correct"]) for ep in episodes) / n, | |
| "faster_than_ref_pct": 100 * sum(s > 1.0 for s in sp) / n, | |
| "mean_speedup": statistics.fmean(sp) if sp else None, | |
| "median_speedup": statistics.median(sp) if sp else None, | |
| "median_roofline_frac": statistics.median(roof) if roof else None, | |
| "stage_hist_all_turns": dict(Counter(t["stage"] for ep in episodes for t in ep["turns"])), | |
| "stage_hist_furthest": dict(Counter(STAGE_ORDER[f] for f in furthest)), | |
| "suspicious": sum(bool(h.get("suspicious")) for ep in episodes for h in ep.get("history", [])), | |
| **_pass_at_k(episodes, best), | |
| } | |
| def _pass_at_k(episodes, best) -> dict: | |
| """With several samples per task (--samples k): pass@k = share of tasks with any correct episode.""" | |
| by_task = {} | |
| for ep, b in zip(episodes, best): | |
| by_task.setdefault(ep.get("task_id"), []).append(b is not None) | |
| k = max((len(v) for v in by_task.values()), default=1) | |
| if k < 2: | |
| return {} | |
| return {"tasks": len(by_task), "samples_per_task": k, | |
| "pass@1_pct": 100 * sum(sum(v) / len(v) for v in by_task.values()) / len(by_task), | |
| f"pass@{k}_pct": 100 * sum(any(v) for v in by_task.values()) / len(by_task)} | |
| def split_task_ids(split: str, levels, kernelbench_dir: str = "") -> list: | |
| """Same deterministic split as the server (hash of task id), enumerated from a local KernelBench copy.""" | |
| tasks = load_tasks(kernelbench_dir, tuple(levels)) | |
| return sorted(t for t, task in tasks.items() if task.split == split) | |
| def run_episodes(generate, env_url: str, task_ids: list, split: str = "eval", env_timeout_s: float = 900.0, | |
| on_done=None, on_turn=None) -> list: | |
| """Turn-synchronous batched episodes: one generate() call per turn for all live episodes. | |
| on_done(ep) fires as each episode finishes (for incremental writes); on_turn(turn, stats) after each turn. | |
| """ | |
| envs = [KernelEnv(base_url=env_url, message_timeout_s=env_timeout_s).sync() for _ in task_ids] | |
| pool = ThreadPoolExecutor(max_workers=max(len(envs), 1)) | |
| try: | |
| obs = list(pool.map(lambda e, t: e.reset(task_id=t, split=split).observation, envs, task_ids)) | |
| convs = [[{"role": "user", "content": o.problem}] for o in obs] | |
| eps = [{"task_id": t, "turns": [], "completions": []} for t in task_ids] | |
| live = list(range(len(envs))) | |
| turn = 0 | |
| while live: | |
| turn += 1 | |
| t0 = time.monotonic() | |
| texts = generate([convs[i] for i in live]) | |
| gen_s = time.monotonic() - t0 | |
| def step(i, code): | |
| t = time.monotonic() | |
| return envs[i].step(KernelAction(code=code)), time.monotonic() - t | |
| results = list(pool.map(step, live, [extract_code(t) for t in texts])) | |
| for i, text, (r, step_s) in zip(live, texts, results): | |
| o = r.observation | |
| eps[i]["completions"].append(text) | |
| eps[i]["turns"].append({"stage": o.stage, "correct": bool(o.correct), "speedup": o.speedup or 0.0, | |
| "reward": r.reward or 0.0, "feedback": o.feedback, | |
| "t_ref_ms": o.t_ref_ms, "t_new_ms": o.t_new_ms, "t_min_ms": o.t_min_ms, | |
| "bound": o.bound, "roofline_frac": o.roofline_frac, | |
| "ref_roofline_frac": o.ref_roofline_frac, | |
| "gen_s": gen_s, "step_s": step_s}) | |
| convs[i] += [{"role": "assistant", "content": text}, {"role": "user", "content": feedback_message(o)}] | |
| if r.done: | |
| st = envs[i].state() | |
| eps[i].update(episode_id=st.episode_id, history=st.history) | |
| if on_done: | |
| on_done(eps[i]) | |
| if on_turn: | |
| on_turn(turn, {"live": len(live), "gen_s": gen_s, | |
| "stages": dict(Counter(eps[i]["turns"][-1]["stage"] for i in live)), | |
| "max_step_s": max(s for _, s in results)}) | |
| live = [i for i, (r, _) in zip(live, results) if not r.done] | |
| return eps | |
| finally: | |
| for e in envs: | |
| e.close() | |
| pool.shutdown() | |
| def run_episodes_pipelined(chat, env_url: str, task_ids: list, split: str = "eval", env_timeout_s: float = 3600.0, | |
| on_done=None, on_step=None) -> list: | |
| """One independent loop per episode (generate -> step -> ...), all concurrent. | |
| Against a vLLM server, continuous batching overlaps one episode's generation with another's | |
| kernel eval, instead of every episode waiting for the slowest one each turn. An episode that | |
| raises is kept with its error so one failure never loses the run. | |
| """ | |
| def one(task_id): | |
| ep = {"task_id": task_id, "turns": [], "completions": [], "reasoning": []} | |
| env = KernelEnv(base_url=env_url, message_timeout_s=env_timeout_s).sync() | |
| try: | |
| o = env.reset(task_id=task_id, split=split).observation | |
| conv = [{"role": "user", "content": o.problem}] | |
| while True: | |
| t0 = time.monotonic() | |
| text, finish, reasoning, ntok = chat(conv) | |
| gen_s = time.monotonic() - t0 | |
| t1 = time.monotonic() | |
| r = env.step(KernelAction(code=extract_code(text))) | |
| step_s = time.monotonic() - t1 | |
| o, truncated = r.observation, finish == "length" | |
| ep["completions"].append(text) | |
| ep["reasoning"].append(reasoning) | |
| ep["turns"].append({"stage": o.stage, "correct": bool(o.correct), "speedup": o.speedup or 0.0, | |
| "reward": r.reward or 0.0, "feedback": o.feedback, | |
| "t_ref_ms": o.t_ref_ms, "t_new_ms": o.t_new_ms, "t_min_ms": o.t_min_ms, | |
| "bound": o.bound, "roofline_frac": o.roofline_frac, | |
| "ref_roofline_frac": o.ref_roofline_frac, "truncated": truncated, | |
| "completion_tokens": ntok, "gen_s": gen_s, "step_s": step_s}) | |
| if on_step: | |
| on_step(ep) | |
| if r.done: | |
| break | |
| conv += [{"role": "assistant", "content": text}, {"role": "user", "content": feedback_message(o, truncated)}] | |
| st = env.state() | |
| ep.update(episode_id=st.episode_id, history=st.history) | |
| except Exception as e: # keep partial episode | |
| ep["error"] = repr(e) | |
| finally: | |
| env.close() | |
| if on_done: | |
| on_done(ep) | |
| return ep | |
| with ThreadPoolExecutor(max_workers=max(len(task_ids), 1)) as pool: | |
| return list(pool.map(one, task_ids)) | |
| def start_vllm_server(model: str, port: int, max_model_len: int, tp: int, spec_mtp: int, extra: str): | |
| """`vllm serve` subprocess; returns (proc, chat_factory).""" | |
| cmd = ["vllm", "serve", model, "--port", str(port), "--max-model-len", str(max_model_len), | |
| "--tensor-parallel-size", str(tp), *shlex.split(extra)] | |
| if spec_mtp: # model's own multi-token-prediction layer drafts tokens; greedy output is identical to plain decoding | |
| cmd += ["--speculative-config", json.dumps({"method": "mtp", "num_speculative_tokens": spec_mtp})] | |
| print("starting:", shlex.join(cmd), flush=True) | |
| proc = subprocess.Popen(cmd) | |
| deadline = time.monotonic() + 1800 | |
| while True: | |
| if proc.poll() is not None: | |
| raise RuntimeError(f"vllm serve exited with {proc.returncode}") | |
| try: | |
| urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=2) | |
| return proc | |
| except OSError: | |
| if time.monotonic() > deadline: | |
| proc.kill() | |
| raise RuntimeError("vllm serve not healthy after 30 min") | |
| time.sleep(3) | |
| def make_chat(base_url: str, model: str, temperature: float, max_new_tokens: int, enable_thinking: bool): | |
| from openai import OpenAI | |
| client = OpenAI(base_url=base_url, api_key="EMPTY", timeout=7200) | |
| def chat(conv): | |
| r = client.chat.completions.create( | |
| model=model, messages=conv, temperature=temperature, max_tokens=max_new_tokens, | |
| extra_body={"chat_template_kwargs": {"enable_thinking": enable_thinking}}) | |
| c = r.choices[0] | |
| return (c.message.content or "", c.finish_reason, getattr(c.message, "reasoning_content", None), | |
| r.usage.completion_tokens if r.usage else None) | |
| return chat | |
| def make_generate(backend: str, model: str, temperature: float, max_new_tokens: int, tp: int, batch_size: int, | |
| enable_thinking: bool = False, max_model_len: int = 32768): | |
| # templates without a thinking switch (Qwen2.5) ignore the kwarg | |
| tmpl = {"enable_thinking": enable_thinking} | |
| if backend == "vllm": | |
| from vllm import LLM, SamplingParams | |
| llm = LLM(model=model, tensor_parallel_size=tp, max_model_len=max_model_len) | |
| params = SamplingParams(temperature=temperature, max_tokens=max_new_tokens) | |
| return lambda convs: [o.outputs[0].text for o in llm.chat(convs, params, use_tqdm=False, chat_template_kwargs=tmpl)] | |
| import torch | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| tok = AutoTokenizer.from_pretrained(model, padding_side="left") | |
| lm = AutoModelForCausalLM.from_pretrained(model, dtype="auto", device_map="auto") | |
| def generate(convs): | |
| out = [] | |
| for k in range(0, len(convs), batch_size): | |
| enc = tok.apply_chat_template(convs[k:k + batch_size], add_generation_prompt=True, padding=True, | |
| return_tensors="pt", return_dict=True, **tmpl).to(lm.device) | |
| with torch.no_grad(): | |
| ids = lm.generate(**enc, max_new_tokens=max_new_tokens, do_sample=temperature > 0, | |
| temperature=temperature or None, pad_token_id=tok.pad_token_id or tok.eos_token_id) | |
| out += tok.batch_decode(ids[:, enc["input_ids"].shape[1]:], skip_special_tokens=True) | |
| return out | |
| return generate | |
| def main(): | |
| p = argparse.ArgumentParser() | |
| p.add_argument("--env-url", default="http://localhost:8000") | |
| p.add_argument("--model", default="Qwen/Qwen2.5-Coder-7B-Instruct") | |
| p.add_argument("--backend", choices=["vllm-server", "openai", "vllm", "hf"], default="vllm-server", | |
| help="vllm-server: start `vllm serve` and pipeline episodes (fastest); openai: use --base-url; " | |
| "vllm/hf: in-process, turn-synchronous batches") | |
| p.add_argument("--base-url", default="http://127.0.0.1:8001/v1", help="OpenAI-compatible endpoint (openai backend)") | |
| p.add_argument("--port", type=int, default=8001, help="vllm-server port") | |
| p.add_argument("--max-model-len", type=int, default=65536) | |
| p.add_argument("--spec-mtp", type=int, default=0, help="speculative decoding with N MTP draft tokens (Qwen3.5: 2)") | |
| p.add_argument("--vllm-args", default="", help="extra `vllm serve` flags, e.g. '--language-model-only'") | |
| p.add_argument("--tp", type=int, default=1, help="vLLM tensor parallel size") | |
| p.add_argument("--split", default="eval") | |
| p.add_argument("--levels", type=int, nargs="+", default=[1]) | |
| p.add_argument("--kernelbench-dir", default=os.environ.get("KERNELBENCH_DIR", "")) | |
| p.add_argument("--task-ids", nargs="*", help="override task list") | |
| p.add_argument("--limit", type=int, default=0) | |
| p.add_argument("--samples", type=int, default=1, help="episodes per task (use temperature > 0) for pass@k") | |
| p.add_argument("--temperature", type=float, default=0.0) | |
| p.add_argument("--max-new-tokens", type=int, default=4096) | |
| p.add_argument("--enable-thinking", action="store_true", help="Qwen3-style <think>; raise --max-new-tokens with it") | |
| p.add_argument("--batch-size", type=int, default=8, help="HF backend generate batch") | |
| p.add_argument("--out", default="baseline_results.jsonl") | |
| p.add_argument("--wandb", metavar="PROJECT", help="log progress, summary and an episode table to W&B") | |
| args = p.parse_args() | |
| task_ids = args.task_ids or split_task_ids(args.split, args.levels, args.kernelbench_dir) | |
| task_ids = task_ids[: args.limit] if args.limit else task_ids | |
| task_ids = [t for t in task_ids for _ in range(args.samples)] | |
| print(f"{len(task_ids)} {args.split} tasks, model {args.model} ({args.backend})") | |
| server = None | |
| if args.backend == "vllm-server": | |
| server = start_vllm_server(args.model, args.port, args.max_model_len, args.tp, args.spec_mtp, args.vllm_args) | |
| args.base_url = f"http://127.0.0.1:{args.port}/v1" | |
| if args.backend in ("vllm-server", "openai"): | |
| chat = make_chat(args.base_url, args.model, args.temperature, args.max_new_tokens, args.enable_thinking) | |
| else: | |
| generate = make_generate(args.backend, args.model, args.temperature, args.max_new_tokens, args.tp, | |
| args.batch_size, args.enable_thinking, args.max_model_len) | |
| run = None | |
| if args.wandb: | |
| import wandb | |
| run = wandb.init(project=args.wandb, config=vars(args), job_type="baseline") | |
| done, lock = [], threading.Lock() | |
| out = open(args.out, "w") | |
| t_start = time.monotonic() | |
| def on_done(ep): | |
| with lock: | |
| done.append(ep) | |
| out.write(json.dumps(ep) + "\n") | |
| out.flush() | |
| best = max((t["speedup"] for t in ep["turns"] if t["correct"]), default=None) | |
| print(f"[{len(done)}/{len(task_ids)} done, {time.monotonic() - t_start:.0f}s] {ep['task_id']}: " | |
| f"{' > '.join(t['stage'] for t in ep['turns'])}" + (f", best {best:.2f}x" if best else "") | |
| + (f", ERROR {ep['error']}" if ep.get("error") else ""), flush=True) | |
| def on_step(ep): | |
| t = ep["turns"][-1] | |
| tok = t.get("completion_tokens") | |
| rate = f", {tok / t['gen_s']:.0f} tok/s" if tok and t["gen_s"] else "" | |
| print(f" {ep['task_id']} turn {len(ep['turns'])}: {t['stage']}" | |
| + (f" {t['speedup']:.2f}x" if t["correct"] else "") + (" [TRUNCATED]" if t.get("truncated") else "") | |
| + f" | gen {t['gen_s']:.0f}s ({tok} tok{rate}) | env step {t['step_s']:.0f}s", flush=True) | |
| if run: | |
| run.log({"gen_s": t["gen_s"], "step_s": t["step_s"], "completion_tokens": tok or 0, | |
| "truncated": int(bool(t.get("truncated"))), "correct": int(t["correct"])}) | |
| def on_turn(turn, stats): | |
| s = summarize(done) if done else {} | |
| print(f"[turn {turn}] {stats['live']} live, gen {stats['gen_s']:.0f}s, slowest step {stats['max_step_s']:.0f}s, " | |
| f"stages {stats['stages']}; done {len(done)}, correct {s.get('correct_pct', 0):.0f}%", flush=True) | |
| if run: | |
| run.log({"turn": turn, "live": stats["live"], "gen_s": stats["gen_s"], "max_step_s": stats["max_step_s"], | |
| **{f"stage/{k}": v for k, v in stats["stages"].items()}}) | |
| try: | |
| if args.backend in ("vllm-server", "openai"): | |
| eps = run_episodes_pipelined(chat, args.env_url, task_ids, args.split, on_done=on_done, on_step=on_step) | |
| else: | |
| eps = run_episodes(generate, args.env_url, task_ids, args.split, on_done=on_done, on_turn=on_turn) | |
| finally: | |
| out.close() | |
| if server: | |
| server.terminate() | |
| print(f"wall time {time.monotonic() - t_start:.0f}s") | |
| summary = summarize(eps) | |
| print(json.dumps(summary, indent=2)) | |
| if run: | |
| run.summary.update({k: v for k, v in summary.items() if not isinstance(v, dict)}) | |
| cols = ["task_id", "episode_id", "stages", "best_speedup", "turns"] | |
| rows = [[ep["task_id"], ep.get("episode_id"), " > ".join(t["stage"] for t in ep["turns"]), | |
| max((t["speedup"] for t in ep["turns"] if t["correct"]), default=None), len(ep["turns"])] for ep in eps] | |
| run.log({"episodes": wandb.Table(columns=cols, data=rows)}) | |
| run.finish() | |
| if __name__ == "__main__": | |
| main() | |