infosec-v1 / code /training /scripts /intraining_eval.py
adhikjoshi's picture
Super-squash branch 'main' using huggingface_hub
994182c
Raw
History Blame Contribute Delete
5.51 kB
#!/usr/bin/env python3
"""In-memory held-out evaluation used by run_sft.py during training.
On a single GPU you cannot serve vLLM while training holds VRAM, so the per-epoch
benchmark runs on the *training* model directly (model.generate) on a small sample
of the same held-out sets eval_endpoint.py uses. Scoring/parse logic is imported
from eval_endpoint so in-training numbers are comparable to the base baseline.
Robustness: callers should wrap this in try/except — a benchmark hiccup must never
kill a training run.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parent))
from eval_endpoint import MCQ_INSTRUCTION, parse_letter, parse_verdict # noqa: E402
def _read_jsonl(path: Path, limit: int | None) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
with path.open("r", encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if line:
rows.append(json.loads(line))
if limit and len(rows) >= limit:
break
return rows
def _generate(model, tokenizer, messages: list[dict[str, str]], max_new_tokens: int,
enable_thinking: bool = False) -> str:
import torch
try:
prompt = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True, enable_thinking=enable_thinking
)
except TypeError:
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
out = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id,
)
return tokenizer.decode(out[0][inputs["input_ids"].shape[-1]:], skip_special_tokens=False)
def _score_vuln(model, tokenizer, rows, max_new_tokens, enable_thinking=False) -> dict[str, Any]:
tp = fp = tn = fn = unparsed = 0
for r in rows:
pred = parse_verdict(_generate(model, tokenizer, r["messages"], max_new_tokens, enable_thinking))
gold = r["gold_label"]
if pred is None:
unparsed += 1
continue
if gold == "vulnerable":
tp += pred == "vulnerable"; fn += pred != "vulnerable"
else:
tn += pred == "not_vulnerable"; fp += pred != "not_vulnerable"
n = len(rows)
prec = tp / (tp + fp) if (tp + fp) else 0.0
rec = tp / (tp + fn) if (tp + fn) else 0.0
return {
"kind": "vuln_detection", "n": n,
"accuracy": (tp + tn) / n if n else 0.0,
"precision_vuln": prec, "recall_vuln": rec,
"f1_vuln": 2 * prec * rec / (prec + rec) if (prec + rec) else 0.0,
"unparsed": unparsed,
}
def _score_mcq(model, tokenizer, rows, max_new_tokens, enable_thinking=False) -> dict[str, Any]:
correct = unparsed = 0
for r in rows:
choices = r["choices"]
user = r["question"] + "\n\n" + "\n".join(
f"{chr(ord('A') + j)}. {c}" for j, c in enumerate(choices)
) + "\n\n" + MCQ_INSTRUCTION
messages = [
{"role": "system", "content": "You are a cybersecurity expert. Authorized security research context."},
{"role": "user", "content": user},
]
pred = parse_letter(_generate(model, tokenizer, messages, max_new_tokens, enable_thinking), len(choices))
if pred is None:
unparsed += 1
continue
correct += int(pred == r["gold_index"])
n = len(rows)
return {"kind": "mcq", "n": n, "accuracy": correct / n if n else 0.0, "unparsed": unparsed}
def run_eval_sets(model, tokenizer, eval_files: list[str], sample_per_set: int = 80,
max_new_tokens: int = 256, enable_thinking: bool = False) -> dict[str, Any]:
"""Return {set_name: metrics} for each eval file (sampled for speed).
enable_thinking defaults to False: this tracking eval measures direct-answer
accuracy so generations stay short, fast and parseable. The base baseline and
every per-epoch run use the same setting, so the numbers stay comparable. (The
primary CyberGym metric exercises the full agentic thinking flow separately.)
"""
was_training = model.training
model.eval()
# generate() needs the kv-cache, which gradient checkpointing disables
prev_use_cache = getattr(model.config, "use_cache", None)
try:
model.config.use_cache = True
except Exception:
pass
results: dict[str, Any] = {}
try:
for f in eval_files:
path = Path(f)
if not path.is_file():
results[path.stem] = {"error": "missing"}
continue
rows = _read_jsonl(path, sample_per_set)
if not rows:
results[path.stem] = {"error": "empty"}
continue
kind = rows[0].get("kind", "vuln_detection")
scorer = _score_mcq if kind == "mcq" else _score_vuln
results[path.stem] = scorer(model, tokenizer, rows, max_new_tokens, enable_thinking)
finally:
if prev_use_cache is not None:
try:
model.config.use_cache = prev_use_cache
except Exception:
pass
if was_training:
model.train()
return results