Download code/training/scripts/intraining_eval.py from adhikjoshi/infosec-v1: direct link, hf CLI and curl.
- Browser
- Download file 5.51 kB
-
https://huggingface.co/adhikjoshi/infosec-v1/resolve/main/code/training/scripts/intraining_eval.py
- Command line
-
hf download hf://adhikjoshi/infosec-v1/code/training/scripts/intraining_eval.py
-
curl -L -o intraining_eval.py https://huggingface.co/adhikjoshi/infosec-v1/resolve/main/code/training/scripts/intraining_eval.py
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 | |