| |
| """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 |
|
|
| |
| |
| |
|
|
| |
|
|
| 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), |
| } |
|
|
|
|
| |
|
|
| _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: |
| |
| pred = _boxed_answer(model_out) |
| if pred is None: |
| return False |
| |
| norm = lambda s: re.sub(r"\s+", "", s.strip().strip("$")) |
| return norm(pred) == norm(gold) or _numbers_equal(pred, gold) |
|
|
|
|
| |
|
|
| @dataclass |
| class Task: |
| name: str |
| hf_dataset: str |
| hf_split: str |
| question_field: str |
| gold_extractor: Callable[[dict], str] |
| scorer: Callable[[str, str], bool] |
| instruction: str |
|
|
|
|
| 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" |
| ), |
| ), |
| } |
|
|
|
|
| |
|
|
| 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) |
|
|
|
|
| |
|
|
| 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 |
|
|
| 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 |
|
|
|
|
| |
|
|
| 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] |
|
|
| |
| 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() |
|
|