px-explorer-v4 / eval /run_4b_eval.py
BuildBot
push_hf: sparse-branch für HF-Push (nur Code, 0 LFS)
9644d0b
Raw
History Blame Contribute Delete
6.59 kB
"""
eval/run_4b_eval.py — Main driver for 4B η² evaluation
======================================================
For each of 4 categories × N prompts:
1. Build a JSON config
2. Spawn runner.py as a fresh subprocess (VRAM isolated)
3. Collect the result JSON
4. (Optional) Pipe to stats.py at the end
Hardware-aware defaults for the RTX 2060 (12GB):
- max_new_tokens = 30
- bf16 weights
- use_cache = False (in runner)
- one prompt per subprocess (no accumulation)
Usage:
python eval/run_4b_eval.py --scale 4B --prompts-per-cat 3
python eval/run_4b_eval.py --scale 4B --prompts-per-cat 20 --preset ACTIVE_MANIFOLD
python eval/run_4b_eval.py --scale 1B --prompts-per-cat 20 # sanity-check at smaller scale
"""
import argparse
import json
import os
import subprocess
import sys
import time
# Project root on path so we can import eval.runner.PROMPTS
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _ROOT not in sys.path:
sys.path.insert(0, _ROOT)
from eval.runner import PROMPTS # noqa: E402
# Model registry shortcuts (kept in one place so adding a new scale is trivial)
SCALE_TO_MODEL = {
"270M": "gemma3-270m-it",
"1B": "gemma3-1b-it",
"4B": "gemma3-4b-it",
"E2B": "gemma4-e2b-it",
}
def _spawn_runner(prompt_text, model_id, preset, max_new_tokens, result_path,
timeout=600):
"""Spawn a single prompt through runner.py. Returns the parsed JSON or
raises if the subprocess failed. timeout=600s = 10 minutes per prompt,
well above the 4B-generation time of ~30s for 30 tokens.
"""
cfg = {
"prompt": prompt_text,
"model_id": model_id,
"preset": preset,
"max_new_tokens": max_new_tokens,
"result_path": result_path,
}
cfg_path = result_path + ".cfg.json"
with open(cfg_path, "w") as f:
json.dump(cfg, f)
cmd = [
sys.executable,
os.path.join(_ROOT, "eval", "runner.py"),
cfg_path,
]
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout,
)
except subprocess.TimeoutExpired:
return {"error": "timeout", "prompt": prompt_text}
if proc.returncode != 0:
return {
"error": f"exit_code_{proc.returncode}",
"prompt": prompt_text,
"stderr_tail": proc.stderr[-500:] if proc.stderr else "",
}
# SR-61 debug: always print stderr to see calibration status
if proc.stderr:
print(proc.stderr, file=sys.stderr, end="")
try:
with open(result_path) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
return {"error": f"read_failed: {e}", "prompt": prompt_text}
def run_eval(scale, prompts_per_cat, preset, max_new_tokens, output_dir, dry_run):
"""Run prompts × categories. Saves per-prompt JSON, returns aggregated list."""
if scale not in SCALE_TO_MODEL:
raise ValueError(f"Unknown scale {scale!r}. Use one of: {list(SCALE_TO_MODEL)}")
model_id = SCALE_TO_MODEL[scale]
os.makedirs(output_dir, exist_ok=True)
all_results = []
n_total = prompts_per_cat * len(PROMPTS)
t_start = time.time()
for cat_idx, (category, prompts) in enumerate(PROMPTS.items()):
for p_idx, prompt in enumerate(prompts[:prompts_per_cat]):
idx = cat_idx * prompts_per_cat + p_idx
result_path = os.path.join(
output_dir, f"{scale}_{preset}_{category}_{p_idx:02d}.json"
)
print(f"\n[{idx+1:3d}/{n_total}] {scale} | {preset} | {category} | "
f"prompt #{p_idx+1}", flush=True)
print(f" > {prompt[:80]}{'...' if len(prompt) > 80 else ''}",
flush=True)
if dry_run:
all_results.append({
"prompt": prompt, "category": category, "dry_run": True,
})
continue
t_prompt = time.time()
result = _spawn_runner(
prompt, model_id, preset, max_new_tokens, result_path
)
dt = time.time() - t_prompt
result["category"] = category
result["elapsed_sec"] = dt
all_results.append(result)
if "error" in result:
print(f" ✗ {result['error']} ({dt:.1f}s)", flush=True)
else:
print(f" ✓ phi={result.get('phi', 'NA'):.3f} "
f"H={result.get('zone_entropy', 'NA'):.3f} "
f"zone={result.get('zone', 'NA')} "
f"({dt:.1f}s, {result.get('completion_tokens', 0)}tok)",
flush=True)
# Aggregate dump
agg_path = os.path.join(
output_dir, f"{scale}_{preset}_aggregate.json"
)
with open(agg_path, "w") as f:
json.dump({
"scale": scale,
"model_id": model_id,
"preset": preset,
"prompts_per_cat": prompts_per_cat,
"n_total": n_total,
"total_time_sec": time.time() - t_start,
"results": all_results,
}, f, indent=2)
print(f"\n[run_4b_eval] Wrote {agg_path}")
return all_results
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--scale", default="4B",
choices=list(SCALE_TO_MODEL.keys()),
help="Model scale (default: 4B)")
p.add_argument("--prompts-per-cat", type=int, default=3,
help="Prompts per category (default: 3 for smoke test)")
p.add_argument("--preset", default="ACTIVE_MANIFOLD",
help="PX preset (default: ACTIVE_MANIFOLD)")
p.add_argument("--max-new-tokens", type=int, default=30,
help="Max generation length (default: 30 for VRAM safety)")
p.add_argument("--output-dir", default=None,
help="Output directory (default: eval/results/<scale>_<preset>_<ts>)")
p.add_argument("--dry-run", action="store_true",
help="Don't actually run — just verify prompt loading")
args = p.parse_args()
out_dir = args.output_dir or os.path.join(
_ROOT, "eval", "results",
f"{args.scale}_{args.preset}_{time.strftime('%Y%m%d_%H%M%S')}"
)
run_eval(
scale=args.scale,
prompts_per_cat=args.prompts_per_cat,
preset=args.preset,
max_new_tokens=args.max_new_tokens,
output_dir=out_dir,
dry_run=args.dry_run,
)
if __name__ == "__main__":
main()