| """Baseline fairness eval on real Qwen2.5-7B, run on Modal GPU. |
| |
| Modal does ONLY the GPU generation; parsing + scoring happen locally with your |
| existing engine, so this also doubles as a clean test of whether the prompted 7B |
| emits parseable JSON (it reports the parse-failure rate explicitly). |
| |
| pip install modal && modal setup # one-time |
| modal run modal_eval.py # baseline on prompted Qwen2.5-7B |
| |
| Run this BEFORE fine-tuning to get your "before" number. After you train the LoRA, |
| point MODEL_ID at the merged checkpoint and run it again for the "after". The delta |
| is your Field Notes post + the evidence the fine-tune made Gorm *fairer*. |
| """ |
|
|
| import json |
| from pathlib import Path |
|
|
| import modal |
|
|
| APP = modal.App("bridge-troll-eval") |
| MODEL_ID = "Qwen/Qwen2.5-7B-Instruct" |
|
|
| image = modal.Image.debian_slim().pip_install( |
| "torch", "transformers>=4.45,<5", "accelerate", "sentencepiece" |
| ) |
|
|
| SEEDS = Path(__file__).parent / "data" / "eval_seeds.jsonl" |
| BANDS = {"weak": (0, 2), "mid": (2, 4), "strong": (4, 5), "none": (0, 0)} |
|
|
|
|
| @APP.function(image=image, gpu="A10G", timeout=60 * 30) |
| def generate_all(system_prompt: str, texts: list[str]) -> list[str]: |
| import torch |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
| tok = AutoTokenizer.from_pretrained(MODEL_ID) |
| model = AutoModelForCausalLM.from_pretrained( |
| MODEL_ID, torch_dtype=torch.bfloat16, device_map="auto" |
| ) |
| out = [] |
| for text in texts: |
| msgs = [{"role": "system", "content": system_prompt}, |
| {"role": "user", "content": text}] |
| inputs = tok.apply_chat_template( |
| msgs, add_generation_prompt=True, return_tensors="pt" |
| ).to(model.device) |
| with torch.no_grad(): |
| gen = model.generate( |
| inputs, max_new_tokens=220, do_sample=True, |
| temperature=0.7, top_p=0.9, pad_token_id=tok.eos_token_id, |
| ) |
| out.append(tok.decode(gen[0][inputs.shape[1]:], skip_special_tokens=True)) |
| return out |
|
|
|
|
| @APP.local_entrypoint() |
| def main(): |
| |
| |
| from troll_engine import SYSTEM_PROMPT, parse_judgment |
|
|
| seeds = [json.loads(l) for l in SEEDS.read_text().splitlines() if l.strip()] |
| raws = generate_all.remote(SYSTEM_PROMPT, [s["text"] for s in seeds]) |
|
|
| tactic_hits = band_hits = band_total = parse_fails = 0 |
| unfair = [] |
| for s, raw in zip(seeds, raws): |
| j = parse_judgment(raw) |
| if j.reason == "unparseable judgment": |
| parse_fails += 1 |
| ok = j.tactic.value == s["tactic"] |
| tactic_hits += ok |
| if s["tactic"] == "genuine": |
| band_total += 1 |
| lo, hi = BANDS[s["band"]] |
| band_hits += ok and lo <= j.persuasiveness <= hi |
| d = j.resolve_delta() |
| if s["tactic"] != "genuine" and d < 0: |
| unfair.append(f"FOLDED to {s['tactic']}: {s['text'][:55]}") |
| if s["band"] == "strong" and d > -12: |
| unfair.append(f"IGNORED strong (d={d:+d}): {s['text'][:55]}") |
| print(f"[{s['tactic']:12}->{j.tactic.value:12}] p={j.persuasiveness} d={d:+3d} {'ok' if ok else 'MISS'}") |
|
|
| n = len(seeds) |
| print("\n--- BASELINE (prompted Qwen2.5-7B) ---") |
| print(f"tactic accuracy: {tactic_hits}/{n} = {tactic_hits/n:.0%}") |
| print(f"persuasiveness in-band: {band_hits}/{band_total} = {band_hits/max(1,band_total):.0%}") |
| print(f"JSON parse failures: {parse_fails}/{n} = {parse_fails/n:.0%}") |
| if unfair: |
| print("\nUNFAIR CASES:") |
| for u in unfair: |
| print(" -", u) |
|
|