| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import math |
| import time |
| from pathlib import Path |
|
|
| import torch |
| import torch.nn.functional as F |
| import yaml |
| from datasets import get_dataset_config_names, load_dataset |
|
|
| from litgpt import Tokenizer |
| from litgpt.config import Config |
| from litgpt.model import GPT |
|
|
|
|
| def load_model(checkpoint_dir: Path, device: torch.device) -> GPT: |
| config = Config.from_file(checkpoint_dir / "model_config.yaml") |
| model = GPT(config) |
| hyperparameters_path = checkpoint_dir / "hyperparameters.yaml" |
| if hyperparameters_path.is_file(): |
| hyperparameters = yaml.safe_load(hyperparameters_path.read_text(encoding="utf-8")) or {} |
| if hyperparameters.get("train", {}).get("tie_embeddings"): |
| model.transformer.wte.weight = model.lm_head.weight |
| checkpoint = torch.load(checkpoint_dir / "lit_model.pth", map_location="cpu") |
| state_dict = checkpoint["model"] if "model" in checkpoint else checkpoint |
| model.load_state_dict(state_dict) |
| model.to(device) |
| model.eval() |
| return model |
|
|
|
|
| def encode_pair(tokenizer: Tokenizer, prompt: str, continuation: str, block_size: int) -> tuple[torch.Tensor, int]: |
| prompt_ids = tokenizer.encode(prompt, bos=True, eos=False).long() |
| continuation_ids = tokenizer.encode(continuation, bos=False, eos=False).long() |
| ids = torch.cat([prompt_ids, continuation_ids]) |
| if ids.numel() > block_size: |
| keep = min(block_size, continuation_ids.numel() + min(32, prompt_ids.numel())) |
| ids = ids[-keep:] |
| continuation_len = min(continuation_ids.numel(), ids.numel() - 1) |
| else: |
| continuation_len = continuation_ids.numel() |
| return ids, continuation_len |
|
|
|
|
| @torch.no_grad() |
| def continuation_nll( |
| model: GPT, |
| tokenizer: Tokenizer, |
| prompt: str, |
| continuation: str, |
| device: torch.device, |
| ) -> float: |
| ids, continuation_len = encode_pair(tokenizer, prompt, continuation, model.max_seq_length) |
| if continuation_len <= 0 or ids.numel() <= 1: |
| return float("inf") |
| inputs = ids[:-1].unsqueeze(0).to(device) |
| targets = ids[1:].to(device) |
| use_autocast = device.type == "cuda" |
| with torch.autocast(device_type="cuda", dtype=torch.bfloat16, enabled=use_autocast): |
| logits = model(inputs)[0].float() |
| start = max(0, targets.numel() - continuation_len) |
| loss = F.cross_entropy(logits[start:], targets[start:], reduction="sum") |
| return float(loss.cpu()) / continuation_len |
|
|
|
|
| def normalize_answer_key(answer_key: str, labels: list[str], texts: list[str]) -> int | None: |
| if answer_key in labels: |
| return labels.index(answer_key) |
| for index, text in enumerate(texts): |
| if answer_key.strip().lower() == text.strip().lower(): |
| return index |
| return None |
|
|
|
|
| def benchmark_arc_easy(model: GPT, tokenizer: Tokenizer, device: torch.device, limit: int) -> dict[str, float | int]: |
| dataset = load_dataset("ai2_arc", "ARC-Easy", split=f"validation[:{limit}]") |
| correct = 0 |
| total = 0 |
| start = time.perf_counter() |
| for row in dataset: |
| question = row["question"].strip() |
| labels = [str(label) for label in row["choices"]["label"]] |
| texts = [str(text) for text in row["choices"]["text"]] |
| target = normalize_answer_key(str(row["answerKey"]), labels, texts) |
| if target is None: |
| continue |
| prompt = f"Question: {question}\nAnswer:" |
| scores = [continuation_nll(model, tokenizer, prompt, " " + choice, device) for choice in texts] |
| prediction = min(range(len(scores)), key=scores.__getitem__) |
| correct += int(prediction == target) |
| total += 1 |
| elapsed = time.perf_counter() - start |
| return { |
| "arc_easy_validation_examples": total, |
| "arc_easy_accuracy": correct / total if total else math.nan, |
| "arc_easy_seconds": elapsed, |
| } |
|
|
|
|
| def benchmark_blimp( |
| model: GPT, |
| tokenizer: Tokenizer, |
| device: torch.device, |
| configs: list[str], |
| examples_per_config: int, |
| ) -> dict[str, float | int | dict[str, float]]: |
| per_config: dict[str, float] = {} |
| correct = 0 |
| total = 0 |
| start = time.perf_counter() |
| for config in configs: |
| dataset = load_dataset("nyu-mll/blimp", config, split=f"train[:{examples_per_config}]") |
| config_correct = 0 |
| config_total = 0 |
| for row in dataset: |
| good = str(row["sentence_good"]).strip() |
| bad = str(row["sentence_bad"]).strip() |
| good_score = continuation_nll(model, tokenizer, "", good, device) |
| bad_score = continuation_nll(model, tokenizer, "", bad, device) |
| config_correct += int(good_score < bad_score) |
| config_total += 1 |
| per_config[config] = config_correct / config_total if config_total else math.nan |
| correct += config_correct |
| total += config_total |
| elapsed = time.perf_counter() - start |
| return { |
| "blimp_examples": total, |
| "blimp_configs": len(configs), |
| "blimp_accuracy": correct / total if total else math.nan, |
| "blimp_per_config": per_config, |
| "blimp_seconds": elapsed, |
| } |
|
|
|
|
| def default_blimp_configs(limit: int) -> list[str]: |
| preferred = [ |
| "adjunct_island", |
| "anaphor_number_agreement", |
| "determiner_noun_agreement_1", |
| "irregular_past_participle_adjectives", |
| "subject_verb_agreement_simple", |
| ] |
| available = set(get_dataset_config_names("nyu-mll/blimp")) |
| configs = [name for name in preferred if name in available] |
| if len(configs) < limit: |
| configs.extend(name for name in sorted(available) if name not in configs) |
| return configs[:limit] |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--checkpoint-dir", type=Path, required=True) |
| parser.add_argument("--tokenizer-dir", type=Path, required=True) |
| parser.add_argument("--arc-limit", type=int, default=100) |
| parser.add_argument("--blimp-configs", type=int, default=5) |
| parser.add_argument("--blimp-examples", type=int, default=50) |
| parser.add_argument("--out", type=Path) |
| args = parser.parse_args() |
|
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| model = load_model(args.checkpoint_dir, device) |
| tokenizer = Tokenizer(args.tokenizer_dir) |
| metrics: dict[str, object] = { |
| "checkpoint_dir": str(args.checkpoint_dir), |
| "tokenizer_dir": str(args.tokenizer_dir), |
| "device": str(device), |
| "model_name": model.config.name, |
| "parameters": sum(parameter.numel() for parameter in model.parameters()), |
| } |
| metrics.update(benchmark_arc_easy(model, tokenizer, device, args.arc_limit)) |
| metrics.update( |
| benchmark_blimp( |
| model, |
| tokenizer, |
| device, |
| default_blimp_configs(args.blimp_configs), |
| args.blimp_examples, |
| ) |
| ) |
|
|
| text = json.dumps(metrics, indent=2, sort_keys=True) |
| print(text) |
| if args.out: |
| args.out.parent.mkdir(parents=True, exist_ok=True) |
| args.out.write_text(text + "\n", encoding="utf-8") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|