gorm-lora / modal_eval.py
10Pratibh's picture
Upload folder using huggingface_hub
9f2dbe8 verified
Raw
History Blame Contribute Delete
3.71 kB
"""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():
# Imported HERE (local only) — never inside the Modal container, which has no
# troll_engine.py. Modal ships the entrypoint file, not its local imports.
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)