import argparse import json import sys import time import urllib.request from pathlib import Path import torch from peft import PeftModel from tokenizers import Tokenizer from transformers import AutoModelForCausalLM, AutoTokenizer ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "src")) from generate import extract_answer # noqa: E402 from model import GPT, GPTConfig # noqa: E402 DEVICE = "cuda" if torch.cuda.is_available() else "cpu" DOMAIN_TAGS = { "godot": "<|godot|>\n", "unity": "Domain: Unity\n", "unreal": "Domain: Unreal Engine\n", "general": "", } SYSTEM_BY_DOMAIN = { "godot": "You are a senior Godot 4 and GDScript engineer. Write concise, runnable game code.", "unity": "You are a senior Unity and C# engineer. Write concise, runnable Unity game code.", "unreal": "You are a senior Unreal Engine 5 and C++ engineer. Write concise, runnable Unreal code.", } def read_jsonl(path): rows = [] with Path(path).open("r", encoding="utf-8") as f: for line in f: line = line.strip() if line: rows.append(json.loads(line)) return rows def build_yuspec_prompt(item): return ( f"<|bos|>{DOMAIN_TAGS.get(item['domain'], '')}" "<|user|>\n" f"{item['prompt']}\n" "<|assistant|>\n" ) def load_yuspec(checkpoint): ckpt = torch.load(checkpoint, map_location=DEVICE) cfg = ckpt["config"] model = GPT(GPTConfig(**cfg["model"])).to(DEVICE) model.load_state_dict(ckpt["model"]) model.eval() tokenizer = Tokenizer.from_file(cfg["data"]["tokenizer_path"]) return model, tokenizer @torch.no_grad() def call_yuspec(model, tokenizer, item, max_new_tokens): ids = tokenizer.encode(build_yuspec_prompt(item)).ids[-900:] x = torch.tensor([ids], dtype=torch.long, device=DEVICE) eos_id = tokenizer.token_to_id("<|eos|>") started = time.time() out = model.generate( x, max_new_tokens=max_new_tokens, temperature=0.18, top_k=12, eos_id=eos_id, vocab_limit=tokenizer.get_vocab_size(), ) decoded = tokenizer.decode(out[0].tolist()) return extract_answer(decoded, item["prompt"]).replace("\ufffd", "").strip(), time.time() - started def load_lora(base_model, adapter): dtype = torch.float16 if DEVICE == "cuda" else torch.float32 tokenizer = AutoTokenizer.from_pretrained(adapter, trust_remote_code=True) base = AutoModelForCausalLM.from_pretrained(base_model, torch_dtype=dtype, trust_remote_code=True).to(DEVICE) model = PeftModel.from_pretrained(base, adapter).to(DEVICE) model.eval() return model, tokenizer @torch.no_grad() def call_lora(model, tokenizer, item, max_new_tokens): messages = [ {"role": "system", "content": SYSTEM_BY_DOMAIN.get(item["domain"], "Write game-development code.")}, {"role": "user", "content": item["prompt"]}, ] prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=900).to(DEVICE) started = time.time() out = model.generate( **inputs, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.18, top_k=20, pad_token_id=tokenizer.eos_token_id, ) text = tokenizer.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True) return text.strip(), time.time() - started def post_json(url, payload, timeout): data = json.dumps(payload, ensure_ascii=False).encode("utf-8") req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST") with urllib.request.urlopen(req, timeout=timeout) as response: return json.loads(response.read().decode("utf-8")) def call_ollama(model_name, item, max_new_tokens, timeout): prompt = ( f"{SYSTEM_BY_DOMAIN.get(item['domain'], 'Write game-development code.')}\n" "Return practical code first. Do not switch to another engine.\n\n" f"Task: {item['prompt']}" ) started = time.time() data = post_json( "http://127.0.0.1:11434/api/generate", { "model": model_name, "prompt": prompt, "stream": False, "options": {"temperature": 0.18, "top_k": 20, "num_predict": max_new_tokens}, }, timeout, ) return data.get("response", "").strip(), time.time() - started def has_mojibake(text): return any(token in text for token in ("Ã", "�", "\ufffd")) def expected_hits(answer, expected): lower = answer.lower() return [term for term in expected if term.lower() in lower] def score_answer(item, answer): hits = expected_hits(answer, item.get("expected", [])) wrong = [term for term in item.get("wrong", []) if term.lower() in answer.lower()] checks = { "not_empty": len(answer.strip()) >= 120, "has_code_fence": "```" in answer or item["domain"] == "unreal", "no_mojibake": not has_mojibake(answer), "no_wrong_engine": not wrong, "has_expected_terms": len(hits) >= max(3, min(6, len(item.get("expected", [])) - 1)), "mentions_task_object": any(term.lower() in answer.lower() for term in item["prompt"].replace(",", " ").split() if len(term) >= 6), } score = sum(int(value) for value in checks.values()) score += min(4, len(hits)) return { "score": min(10, score), "max_score": 10, "checks": checks, "expected_hits": hits, "wrong_terms": wrong, } def write_summary(rows, out_md): candidates = [] for row in rows: if row["candidate"] not in candidates: candidates.append(row["candidate"]) lines = ["# Direct Game Command Benchmark", ""] lines.append("| Candidate | Total | Average | Avg latency |") lines.append("|---|---:|---:|---:|") for candidate in candidates: subset = [row for row in rows if row["candidate"] == candidate] total = sum(row["metrics"]["score"] for row in subset) max_total = sum(row["metrics"]["max_score"] for row in subset) latencies = [row["latency_sec"] for row in subset if row["latency_sec"] is not None] avg_latency = sum(latencies) / len(latencies) if latencies else 0.0 lines.append(f"| `{candidate}` | {total}/{max_total} | {total / max_total:.2%} | {avg_latency:.2f}s |") lines.append("") lines.append("## Per Command") lines.append("") lines.append("| Command | Domain | " + " | ".join(f"`{c}`" for c in candidates) + " |") lines.append("|---|---|" + "|".join(["---:"] * len(candidates)) + "|") for item_id in sorted({row["id"] for row in rows}): first = next(row for row in rows if row["id"] == item_id) scores = [] for candidate in candidates: row = next(row for row in rows if row["id"] == item_id and row["candidate"] == candidate) scores.append(str(row["metrics"]["score"])) lines.append(f"| `{first['prompt']}` | {first['domain']} | " + " | ".join(scores) + " |") Path(out_md).write_text("\n".join(lines) + "\n", encoding="utf-8") def main(): if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") parser = argparse.ArgumentParser() parser.add_argument("--benchmark", default="eval/direct_command_benchmark.jsonl") parser.add_argument("--out-jsonl", default="eval/results_direct_command_benchmark.jsonl") parser.add_argument("--out-md", default="eval/results_direct_command_benchmark.md") parser.add_argument("--yuspec-checkpoint", default="checkpoints/compound_game_commands_60m_v5/best.pt") parser.add_argument("--qwen-base", default="Qwen/Qwen2.5-0.5B-Instruct") parser.add_argument("--qwen-adapter", default="checkpoints/qwen2_5_0_5b_gamedev_lora_godot_balanced") parser.add_argument("--qwen05", default="qwen2.5:0.5b") parser.add_argument("--qwen7b", default="qwen2.5:7b-instruct-q4_K_M") parser.add_argument("--max-new-tokens", type=int, default=700) parser.add_argument("--timeout", type=int, default=240) parser.add_argument("--skip-yuspec", action="store_true") parser.add_argument("--skip-lora", action="store_true") parser.add_argument("--skip-qwen05", action="store_true") parser.add_argument("--skip-qwen7b", action="store_true") args = parser.parse_args() items = read_jsonl(args.benchmark) rows = [] candidates = [] if not args.skip_yuspec: yuspec = load_yuspec(args.yuspec_checkpoint) candidates.append(("yuspec_60m_compound_v5", lambda item: call_yuspec(*yuspec, item, args.max_new_tokens), "local_yuspec")) if not args.skip_lora: lora = load_lora(args.qwen_base, args.qwen_adapter) candidates.append(("qwen2.5_0.5b_lora", lambda item: call_lora(*lora, item, args.max_new_tokens), "hf_lora")) if not args.skip_qwen05: candidates.append(("qwen2.5_0.5b", lambda item: call_ollama(args.qwen05, item, args.max_new_tokens, args.timeout), "ollama")) if not args.skip_qwen7b: candidates.append(("qwen2.5_7b", lambda item: call_ollama(args.qwen7b, item, args.max_new_tokens, args.timeout), "ollama")) for name, call_fn, mode in candidates: for item in items: try: answer, latency = call_fn(item) metrics = score_answer(item, answer) print(f"{name} | {item['id']}: {metrics['score']}/10") error = None except Exception as exc: answer = "" latency = None metrics = {"score": 0, "max_score": 10, "checks": {}, "error": str(exc)} error = str(exc) print(f"{name} | {item['id']}: ERROR {exc}") rows.append( { "candidate": name, "id": item["id"], "domain": item["domain"], "prompt": item["prompt"], "answer": answer, "metrics": metrics, "latency_sec": latency, "mode": mode, "error": error, } ) out_jsonl = Path(args.out_jsonl) out_jsonl.parent.mkdir(parents=True, exist_ok=True) with out_jsonl.open("w", encoding="utf-8") as f: for row in rows: f.write(json.dumps(row, ensure_ascii=False) + "\n") write_summary(rows, args.out_md) print(f"wrote {args.out_jsonl}") print(f"wrote {args.out_md}") if __name__ == "__main__": main()