Ouzhang's picture
Add files using upload-large-folder tool
13c5606 verified
Raw
History Blame Contribute Delete
13.7 kB
#!/usr/bin/env python3
"""One-file evaluator for Nemotron-Labs-Diffusion.
No server. No SLURM. No eval-framework dependency. One Python process:
1. Load the HF model + tokenizer once.
2. Iterate over benchmark datasets via `datasets.load_dataset`.
3. Call the right `model.X_generate` for the chosen --mode.
4. Score with an inline task-specific extractor.
5. Print a per-task pass@1 + TPF table.
pip install torch transformers datasets peft # peft only for --lora
# Smoke (50 problems, ~5 min on 1Γ— H100)
python evaluate.py --mode dlm --tasks gsm8k --limit 50
# Full gsm8k (1319 problems), each mode
python evaluate.py --mode ar --tasks gsm8k
python evaluate.py --mode dlm --tasks gsm8k
python evaluate.py --mode linear_spec --tasks gsm8k
python evaluate.py --mode linear_spec --tasks gsm8k --lora # + bundled LoRA draft
# Multiple tasks in one run
python evaluate.py --mode dlm --tasks gsm8k,math-500
Supported tasks (extend with TASKS dict below):
gsm8k β€” GSM8K test split, 1319 problems. Score: \\boxed{N} or last
number in model output equals the gold answer.
math-500 β€” Hendrycks MATH-500 test split. Score: \\boxed{N} equality
with the gold answer.
For the full 10-benchmark suite (HumanEval / MBPP / MMLU / IFEval /
LiveCodeBench / AIME / GPQA β€” each needs its own scorer) use eval.sh.
"""
import argparse
import json
import os
import re
import sys
import time
from dataclasses import dataclass
from typing import Callable, List, Optional
# Heavy imports (torch, transformers, datasets, peft) are deferred into
# load() / run_one_task() so `python evaluate.py --help` works in a fresh
# clone before users have run pip install.
# ─── Per-mode decoding defaults (mirror eval.sh) ────────────────────────────
MODE_DEFAULTS = {
"ar": dict(block_length=1, threshold=None),
"dlm": dict(block_length=8, threshold=0.9),
"linear_spec": dict(block_length=32, threshold=0.0),
}
# ─── Inline scorers ─────────────────────────────────────────────────────────
_NUMBER_RE = re.compile(r"-?\d+(?:\.\d+)?")
_BOXED_RE = re.compile(r"\\boxed\{([^{}]+)\}")
def _last_number(text: str) -> Optional[str]:
"""Pull the last number-like token from `text`. Strips commas."""
cleaned = text.replace(",", "")
matches = _NUMBER_RE.findall(cleaned)
return matches[-1] if matches else None
def _boxed_answer(text: str) -> Optional[str]:
"""Return the last `\\boxed{...}` payload, or None."""
matches = _BOXED_RE.findall(text)
return matches[-1].strip() if matches else None
def _numbers_equal(a: str, b: str) -> bool:
"""Float-aware equality (so '18', '18.0', and '18.00' all match)."""
try:
return abs(float(a) - float(b)) < 1e-6
except (TypeError, ValueError):
return a.strip() == b.strip()
def score_gsm8k(model_out: str, gold: str) -> bool:
pred = _boxed_answer(model_out) or _last_number(model_out)
return pred is not None and _numbers_equal(pred, gold)
def score_math500(model_out: str, gold: str) -> bool:
# MATH gold is the literal contents of \boxed{...} in the solution.
pred = _boxed_answer(model_out)
if pred is None:
return False
# Normalize whitespace + strip surrounding $$.
norm = lambda s: re.sub(r"\s+", "", s.strip().strip("$"))
return norm(pred) == norm(gold) or _numbers_equal(pred, gold)
# ─── Task registry ──────────────────────────────────────────────────────────
@dataclass
class Task:
name: str
hf_dataset: str # `datasets.load_dataset` repo id
hf_split: str # which split to score
question_field: str # column with the problem statement
gold_extractor: Callable[[dict], str] # row -> gold answer string
scorer: Callable[[str, str], bool]
instruction: str # appended in front of the question
TASKS = {
"gsm8k": Task(
name="gsm8k",
hf_dataset="gsm8k",
hf_split="test",
question_field="question",
gold_extractor=lambda row: row["answer"].split("####")[-1].strip().replace(",", ""),
scorer=score_gsm8k,
instruction=(
"Solve the following math problem. Put the final numerical answer "
"inside \\boxed{} at the very end.\n\n"
),
),
"math-500": Task(
name="math-500",
hf_dataset="HuggingFaceH4/MATH-500",
hf_split="test",
question_field="problem",
gold_extractor=lambda row: row["answer"],
scorer=score_math500,
instruction=(
"Solve the following math problem. Put the final answer inside "
"\\boxed{} at the very end.\n\n"
),
),
}
# ─── Generation dispatch ────────────────────────────────────────────────────
def _round_to_block(n: int, block: int) -> int:
return max(block, (n // block) * block)
def generate(model, tokenizer, prompt_ids, mode: str, max_new_tokens: int,
block_length: int, threshold: Optional[float],
max_thinking_tokens: int) -> tuple:
"""Dispatch to the right `model.X_generate` for the chosen mode.
Returns (output_ids, nfe)."""
eos = tokenizer.eos_token_id
if mode == "ar":
return model.ar_generate(
prompt_ids=prompt_ids, max_new_tokens=max_new_tokens, eos_token_id=eos,
)
if mode == "dlm":
n = _round_to_block(max_new_tokens, block_length)
return model.generate(
prompt_ids, max_new_tokens=n, block_length=block_length,
threshold=threshold, eos_token_id=eos,
max_thinking_tokens=max_thinking_tokens,
)
if mode == "linear_spec":
n = _round_to_block(max_new_tokens, block_length)
return model.linear_spec_generate(
prompt_ids, max_new_tokens=n, block_length=block_length,
eos_token_id=eos, max_thinking_tokens=max_thinking_tokens,
)
raise ValueError(f"unknown mode {mode!r}")
def run_one_task(model, tokenizer, task: Task, args) -> dict:
from datasets import load_dataset
print(f"\n── {task.name} ── loading {task.hf_dataset} [{task.hf_split}]", flush=True)
if task.hf_dataset == "gsm8k":
ds = load_dataset(task.hf_dataset, "main", split=task.hf_split)
else:
ds = load_dataset(task.hf_dataset, split=task.hf_split)
if args.limit:
ds = ds.select(range(min(args.limit, len(ds))))
correct = 0
total = 0
total_new_tokens = 0
total_nfe = 0
t0 = time.time()
for i, row in enumerate(ds):
question = row[task.question_field]
gold = task.gold_extractor(row)
messages = [{"role": "user", "content": task.instruction + question}]
prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
prompt_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(model.device)
out_ids, nfe = generate(
model, tokenizer, prompt_ids,
mode=args.mode, max_new_tokens=args.max_new_tokens,
block_length=args.block_length, threshold=args.threshold,
max_thinking_tokens=args.max_thinking_tokens,
)
new_ids = out_ids[0, prompt_ids.shape[1]:]
new_text = tokenizer.decode(new_ids, skip_special_tokens=True)
ok = task.scorer(new_text, str(gold))
correct += int(ok)
total += 1
total_new_tokens += int(new_ids.numel())
total_nfe += int(nfe) if isinstance(nfe, (int, float)) else 0
if (i + 1) % args.print_every == 0:
acc = 100.0 * correct / total
tpf = total_new_tokens / max(total_nfe, 1)
elapsed = time.time() - t0
print(f" [{i+1:5d}/{len(ds)}] acc={acc:5.2f}% "
f"avg_tok={total_new_tokens/total:6.1f} "
f"avg_nfe={total_nfe/total:6.1f} "
f"TPF={tpf:5.2f} ({elapsed:.0f}s)", flush=True)
acc = 100.0 * correct / max(total, 1)
avg_tok = total_new_tokens / max(total, 1)
avg_nfe = total_nfe / max(total, 1)
tpf = total_new_tokens / max(total_nfe, 1)
print(f" βœ“ {task.name:<12} acc={acc:5.2f}% avg_tok={avg_tok:6.1f} "
f"avg_nfe={avg_nfe:6.1f} TPF={tpf:5.2f} ({total} problems)", flush=True)
return dict(task=task.name, num_entries=total, accuracy=acc,
avg_tokens=avg_tok, avg_nfe=avg_nfe, tpf=tpf,
elapsed_seconds=time.time() - t0)
# ─── Model load ─────────────────────────────────────────────────────────────
def load(args) -> tuple:
import torch
from transformers import AutoModel, AutoTokenizer
print(f"Loading {args.model} ...", file=sys.stderr, flush=True)
tok = AutoTokenizer.from_pretrained(args.model, trust_remote_code=True)
dtype = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}[args.dtype]
m = AutoModel.from_pretrained(args.model, trust_remote_code=True).to(args.device).to(dtype)
if args.lora or args.lora_path:
from peft import PeftModel
lora_dir = args.lora_path or os.path.join(
os.path.dirname(os.path.abspath(__file__)), "miscs", "linear_spec_lora")
if not os.path.isfile(os.path.join(lora_dir, "adapter_config.json")):
sys.exit(f"ERROR: LoRA adapter_config.json not found at {lora_dir}. "
f"Run `bash scripts/fetch_bundled_lora.sh` first.")
print(f"Attaching LoRA from {lora_dir}", file=sys.stderr, flush=True)
wrapped = PeftModel.from_pretrained(m, lora_dir).eval()
m = wrapped.model # unwrap so .linear_spec_generate is reachable
if args.mode != "linear_spec" and (args.lora or args.lora_path):
print(f"WARNING: --lora ignored β€” only meaningful for --mode linear_spec",
file=sys.stderr)
m.eval()
return m, tok
# ─── CLI ────────────────────────────────────────────────────────────────────
def main() -> None:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument("--model", default="nvidia/Nemotron-Labs-Diffusion-8B",
help="HuggingFace model id (default: %(default)s)")
p.add_argument("--mode", default="dlm", choices=list(MODE_DEFAULTS.keys()),
help="Decoding path: ar | dlm | linear_spec")
p.add_argument("--tasks", default="gsm8k",
help=f"Comma-separated task names. Available: {','.join(TASKS.keys())}")
p.add_argument("--lora", action="store_true",
help="(linear_spec only) attach the bundled miscs/linear_spec_lora/ as the draft")
p.add_argument("--lora-path", default=None,
help="Local directory containing adapter_config.json + adapter_model.safetensors")
p.add_argument("--limit", type=int, default=None, help="Cap problems per task (smoke testing)")
p.add_argument("--max-new-tokens", type=int, default=512)
p.add_argument("--block-length", type=int, default=None,
help="Override per-mode default block_length")
p.add_argument("--threshold", type=float, default=None,
help="Override per-mode default confidence threshold (dlm/linear_spec)")
p.add_argument("--max-thinking-tokens", type=int, default=6000)
p.add_argument("--device", default="cuda")
p.add_argument("--dtype", default="bf16", choices=["bf16", "fp16", "fp32"])
p.add_argument("--print-every", type=int, default=50, help="Progress every N problems")
p.add_argument("--output", default=None,
help="If set, write per-task results to this JSON file")
args = p.parse_args()
defaults = MODE_DEFAULTS[args.mode]
if args.block_length is None:
args.block_length = defaults["block_length"]
if args.threshold is None:
args.threshold = defaults["threshold"]
task_names = [t.strip() for t in args.tasks.split(",") if t.strip()]
unknown = set(task_names) - TASKS.keys()
if unknown:
sys.exit(f"ERROR: unknown task(s) {sorted(unknown)}. "
f"Available: {sorted(TASKS.keys())}. For the full 10-benchmark "
f"suite, use eval.sh.")
m, tok = load(args)
results = [run_one_task(m, tok, TASKS[name], args) for name in task_names]
# Summary
print("\n── summary ──")
print(f" mode={args.mode} lora={'on' if (args.lora or args.lora_path) else 'off'} "
f"model={args.model}")
print(f" {'task':<14} {'acc%':>7} {'avg_tok':>8} {'avg_nfe':>8} {'TPF':>6}")
for r in results:
print(f" {r['task']:<14} {r['accuracy']:>7.2f} {r['avg_tokens']:>8.1f} "
f"{r['avg_nfe']:>8.1f} {r['tpf']:>6.2f}")
if args.output:
with open(args.output, "w") as f:
json.dump({"args": vars(args), "results": results}, f, indent=2)
print(f"\nWrote {args.output}")
if __name__ == "__main__":
main()