56m's picture
Update evaluate.py
6a04def verified
Raw History Blame Contribute Delete
17.1 kB
# ==============================================================================
# Multiplication SLM Benchmark Suite (Interactive TUI Wizard)
# Dynamic Dataset Generator corresponding to 'cot_partial', 'cot_direct', 'cot_cot'
# ==============================================================================
import json
import math
import os
import random
import subprocess
import sys
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader, Dataset
# ------------------------------------------------------------------------------
# 1. Install & Import Rich
# ------------------------------------------------------------------------------
try:
from rich.align import Align
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.progress import (
BarColumn,
Progress,
SpinnerColumn,
TextColumn,
TimeRemainingColumn,
)
from rich.prompt import Prompt
from rich.table import Table
except ImportError:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "rich", "-q"]
)
from rich.align import Align
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.progress import (
BarColumn,
Progress,
SpinnerColumn,
TextColumn,
TimeRemainingColumn,
)
from rich.prompt import Prompt
from rich.table import Table
from transformers import GPT2LMHeadModel
console = Console()
# ------------------------------------------------------------------------------
# 2. Tokenizer Definition
# ------------------------------------------------------------------------------
DEFAULT_CHARS = sorted(
list(
set(
"0123456789 *+=:\nQStepsAnsLet'sthinkstepby"
+ "<|endoftext|>"
+ "<|pad|>"
)
)
)
class CharTokenizer:
def __init__(self, chars=None):
if chars is None:
chars = DEFAULT_CHARS
self.chars = sorted(list(set(chars)))
if "<|pad|>" not in self.chars:
self.chars.append("<|pad|>")
self.c2i = {ch: i for i, ch in enumerate(self.chars)}
self.i2c = {i: ch for i, ch in enumerate(self.chars)}
self.pad_id = self.c2i["<|pad|>"]
self.vocab_size = len(self.chars)
@classmethod
def load(cls, filepath):
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
return cls(data["chars"])
def encode(self, text):
return [self.c2i[c] for c in text if c in self.c2i]
def decode(self, ids):
return "".join(
[self.i2c[i] for i in ids if i in self.i2c and i != self.pad_id]
)
# ------------------------------------------------------------------------------
# 3. Interactive TUI Wizard
# ------------------------------------------------------------------------------
console.clear()
console.print()
console.print(
Panel(
Align.center(
"[bold yellow]🤗 Hugging Face Micro-SLM Benchmark Harness[/bold yellow]\n"
"[dim]10,000 Qs Log-Likelihood & Exact Match | 500 Qs Perplexity (PPL)\n"
"Rule: Strictly for Small Models (≤ 250M Parameters)[/dim]"
),
box=box.ROUNDED,
border_style="yellow",
padding=(1, 2),
)
)
# Step 1: Model Directory
candidate_paths = [
d
for d in ["./slm_mult_model", "./slm_mult_checkpoints", "."]
if os.path.exists(os.path.join(d, "config.json"))
or os.path.exists(os.path.join(d, "tokenizer.json"))
]
detected_hint = (
f"[green]Found model at:[/green] [bold]{candidate_paths[0]}[/bold]"
if candidate_paths
else "[dim]No model detected in current folder.[/dim]"
)
input_dialog = Table.grid(padding=1)
input_dialog.add_column(style="bold cyan")
input_dialog.add_column()
input_dialog.add_row("Auto Detected:", detected_hint)
input_dialog.add_row("Default Path:", "[bold yellow]./slm_mult_model[/bold yellow]")
console.print(
Panel(
input_dialog,
title="[bold white] 📁 Step 1/4: Model Directory [/bold white]",
title_align="left",
border_style="cyan",
box=box.ROUNDED,
)
)
while True:
model_dir = Prompt.ask(
" [bold cyan]➤ Model Directory Path[/bold cyan]",
default=candidate_paths[0] if candidate_paths else "./slm_mult_model",
)
if os.path.exists(model_dir):
break
console.print("[bold red]Directory not found! Please try again.[/bold red]")
# Step 2: Model Display Name
default_name = f"YourOrg/{os.path.basename(os.path.abspath(model_dir))}"
console.print()
console.print(
Panel(
"[dim]Enter the model name for the leaderboard (e.g. OrgName/ModelName).[/dim]",
title="[bold white] 🏷️ Step 2/4: Model ID / Display Name [/bold white]",
title_align="left",
border_style="cyan",
box=box.ROUNDED,
)
)
model_name = Prompt.ask(
" [bold cyan]➤ Model Name[/bold cyan]",
default=default_name
)
# Step 3: CoT Mode Selection
cot_table = Table(show_header=False, box=None, padding=(0, 1))
cot_table.add_row("[bold yellow]1. cot_partial[/bold yellow]", ": Step-by-step arithmetic decomposition (筆算分解)")
cot_table.add_row("[bold yellow]2. cot_direct [/bold yellow]", ": Direct answer output without intermediate steps (直接出力)")
cot_table.add_row("[bold yellow]3. cot_cot [/bold yellow]", ": Chain-of-Thought prompt -> Final Answer only (プロンプトCoT/最終回答のみ)")
console.print()
console.print(
Panel(
cot_table,
title="[bold white] 🧠 Step 3/4: CoT (Reasoning) Mode [/bold white]",
title_align="left",
border_style="cyan",
box=box.ROUNDED,
)
)
cot_choice = Prompt.ask(
" [bold cyan]➤ Select CoT Mode[/bold cyan]",
choices=["cot_partial", "cot_direct", "cot_cot"],
default="cot_partial"
)
# Step 4: Highlight Tag
console.print()
console.print(
Panel(
"[dim]Highlight this entry as a BASE model on the leaderboard?[/dim]",
title="[bold white] ⭐ Step 4/4: Base Model Highlight [/bold white]",
title_align="left",
border_style="cyan",
box=box.ROUNDED,
)
)
is_highlight_str = Prompt.ask(
" [bold cyan]➤ Highlight as Base Model?[/bold cyan]",
choices=["y", "n"],
default="n"
)
is_highlight = (is_highlight_str.lower() == "y")
# ------------------------------------------------------------------------------
# 4. Model Loading & Verification
# ------------------------------------------------------------------------------
tokenizer_path = os.path.join(model_dir, "tokenizer.json")
if os.path.exists(tokenizer_path):
tokenizer = CharTokenizer.load(tokenizer_path)
tokenizer_status = f"[green]Loaded from ({tokenizer_path})[/green]"
else:
tokenizer = CharTokenizer(DEFAULT_CHARS)
tokenizer_status = "[yellow]Standard Vocab (Fallback)[/yellow]"
device = "cuda" if torch.cuda.is_available() else "cpu"
with console.status(f"[bold cyan]Loading weights onto [bold green]{device.upper()}[/bold green]...[/bold cyan]"):
model = GPT2LMHeadModel.from_pretrained(model_dir).to(device)
model.eval()
total_params = sum(p.numel() for p in model.parameters())
params_str = (
f"{total_params / 1e6:.1f}M"
if total_params >= 1e6
else f"{total_params / 1e3:.1f}K"
)
is_qualified = total_params <= 250_000_000
info_table = Table(show_header=False, box=None, padding=(0, 2))
info_table.add_row("Model Name:", f"[bold white]{model_name}[/bold white]")
info_table.add_row("Directory:", f"[dim]{os.path.abspath(model_dir)}[/dim]")
info_table.add_row("CoT Mode:", f"[bold yellow]{cot_choice}[/bold yellow]")
info_table.add_row("Highlighted:", f"[bold magenta]{is_highlight}[/bold magenta]")
info_table.add_row("Total Parameters:", f"[bold cyan]{params_str}[/bold cyan] ({total_params:,})")
info_table.add_row(
"Eligibility (≤ 250M):",
"[bold green]✔ QUALIFIED[/bold green]" if is_qualified else "[bold red]✖ DISQUALIFIED (>250M)[/bold red]"
)
console.print()
console.print(
Panel(
info_table,
title="[bold green] 🤖 Model Specification Confirmed [/bold green]",
title_align="left",
border_style="green",
box=box.ROUNDED,
)
)
# ------------------------------------------------------------------------------
# 5. Dynamic Dataset Generator (Based on cot_choice)
# ------------------------------------------------------------------------------
def generate_benchmark_sample(cot_mode="cot_partial"):
mode = random.choices(["2terms", "3terms"], weights=[0.7, 0.3])[0]
if mode == "2terms":
a = random.randint(2, 999)
b = random.randint(2, 99)
expr = f"{a} * {b}"
ans = a * b
if cot_mode == "cot_direct":
prompt = f"Q: {expr}\nAns:"
completion = f" {ans}<|endoftext|>"
elif cot_mode == "cot_cot":
prompt = f"Q: {expr}\nLet's think step by step.\nAns:"
completion = f" {ans}<|endoftext|>"
else: # cot_partial (standard step-by-step)
if b >= 10:
steps = []
sub_products = []
for i, digit in enumerate(reversed(str(b))):
p_val = int(digit) * (10**i)
if p_val > 0:
part_ans = a * p_val
sub_products.append(part_ans)
steps.append(f"Step {len(steps)+1}: {a} * {p_val} = {part_ans}")
if len(sub_products) > 1:
add_expr = " + ".join(map(str, sub_products))
steps.append(f"Step {len(steps)+1}: {add_expr} = {ans}")
steps_text = "\n".join(steps)
else:
steps_text = f"Step 1: {expr} = {ans}"
prompt = f"Q: {expr}\nSteps:\n"
completion = f"{steps_text}\nAns: {ans}<|endoftext|>"
else: # 3terms
a = random.randint(2, 50)
b = random.randint(2, 20)
c = random.randint(2, 20)
expr = f"{a} * {b} * {c}"
r1, r2 = a * b, a * b * c
if cot_mode == "cot_direct":
prompt = f"Q: {expr}\nAns:"
completion = f" {r2}<|endoftext|>"
elif cot_mode == "cot_cot":
prompt = f"Q: {expr}\nLet's think step by step.\nAns:"
completion = f" {r2}<|endoftext|>"
else: # cot_partial
prompt = f"Q: {expr}\nSteps:\n"
completion = f"Step 1: {a} * {b} = {r1}\nStep 2: {r1} * {c} = {r2}\nAns: {r2}<|endoftext|>"
return prompt, completion
class EvalDataset(Dataset):
def __init__(self, samples, max_len=160):
self.items = []
for prompt, comp in samples:
full_text = prompt + comp
p_tokens = tokenizer.encode(prompt)
full_tokens = tokenizer.encode(full_text)
prompt_len = len(p_tokens)
seq_len = len(full_tokens)
if seq_len > max_len:
full_tokens = full_tokens[:max_len]
labels = list(full_tokens)
for i in range(min(prompt_len, len(labels))):
labels[i] = -100
pad_len = max_len - len(full_tokens)
input_ids = full_tokens + [tokenizer.pad_id] * pad_len
labels = labels + [-100] * pad_len
self.items.append(
{
"input_ids": torch.tensor(input_ids, dtype=torch.long),
"labels": torch.tensor(labels, dtype=torch.long),
}
)
def __len__(self):
return len(self.items)
def __getitem__(self, idx):
return self.items[idx]
with console.status(f"[bold blue]Generating 10,500 questions in '[yellow]{cot_choice}[/yellow]' mode...[/bold blue]"):
random.seed(2025)
eval_10k_data = [generate_benchmark_sample(cot_choice) for _ in range(10000)]
eval_ppl_data = [generate_benchmark_sample(cot_choice) for _ in range(500)]
dataset_10k = EvalDataset(eval_10k_data)
dataset_ppl = EvalDataset(eval_ppl_data)
# Auto Batch Size Discovery
def find_optimal_batch_size(model, min_b=1, max_b=1024, seq_len=160, device="cuda"):
if device == "cpu": return 64
candidate_sizes = [2**i for i in range(int(math.log2(min_b)), int(math.log2(max_b)) + 1)]
optimal_b = min_b
with console.status("[bold magenta]Probing maximum safe batch size...[/bold magenta]"):
for b in candidate_sizes:
try:
torch.cuda.empty_cache()
dummy_input = torch.randint(0, tokenizer.vocab_size, (b, seq_len), device=device)
with torch.no_grad():
_ = model(dummy_input)
optimal_b = b
except Exception:
break
return optimal_b
auto_batch_size = find_optimal_batch_size(model, device=device)
console.print(f"\n[bold green]✔ Hardware Optimized:[/bold green] Batch Size set to [bold cyan]{auto_batch_size}[/bold cyan]\n")
# ------------------------------------------------------------------------------
# 6. Benchmark Execution Loop
# ------------------------------------------------------------------------------
loader_10k = DataLoader(dataset_10k, batch_size=auto_batch_size, shuffle=False)
loader_ppl = DataLoader(dataset_ppl, batch_size=auto_batch_size, shuffle=False)
total_log_likelihood = 0.0
total_tokens = 0
correct_tokens = 0
perfect_sequences = 0
total_sequences = 0
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(complete_style="cyan", finished_style="green"),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeRemainingColumn(),
console=console,
) as progress:
task1 = progress.add_task(f"[cyan]Evaluating 10,000 Qs ({cot_choice})...", total=len(loader_10k))
with torch.no_grad():
for batch in loader_10k:
input_ids = batch["input_ids"].to(device)
labels = batch["labels"].to(device)
outputs = model(input_ids)
logits = outputs.logits
shift_logits = logits[:, :-1, :].contiguous()
shift_labels = labels[:, 1:].contiguous()
log_probs = F.log_softmax(shift_logits, dim=-1)
mask = shift_labels != -100
gathered = torch.gather(log_probs, 2, shift_labels.unsqueeze(-1).clamp(min=0)).squeeze(-1)
gathered = gathered * mask
total_log_likelihood += gathered.sum().item()
total_tokens += mask.sum().item()
preds = torch.argmax(shift_logits, dim=-1)
matches = (preds == shift_labels) & mask
correct_tokens += matches.sum().item()
seq_matches = (matches.sum(dim=1) == mask.sum(dim=1)).sum().item()
perfect_sequences += seq_matches
total_sequences += input_ids.size(0)
progress.update(task1, advance=1)
task2 = progress.add_task("[magenta]Calculating Perplexity on 500 Qs...", total=len(loader_ppl))
total_loss = 0.0
ppl_batches = 0
loss_fn = torch.nn.CrossEntropyLoss(ignore_index=-100)
with torch.no_grad():
for batch in loader_ppl:
input_ids = batch["input_ids"].to(device)
labels = batch["labels"].to(device)
outputs = model(input_ids)
shift_logits = outputs.logits[:, :-1, :].contiguous()
shift_labels = labels[:, 1:].contiguous()
loss = loss_fn(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
total_loss += loss.item()
ppl_batches += 1
progress.update(task2, advance=1)
# Metrics Calculation
token_accuracy = (correct_tokens / total_tokens) * 100
sequence_accuracy = (perfect_sequences / total_sequences) * 100
avg_loss = total_loss / ppl_batches
ppl_score = math.exp(avg_loss)
p = sequence_accuracy / 100.0
p_smooth = min(max(p, 1e-4), 1.0 - 1e-5)
logit_val = math.log(p_smooth / (1.0 - p_smooth))
rating_score = max(0.0, 1050.0 + (252.47 * logit_val))
# ------------------------------------------------------------------------------
# 7. HTML Leaderboard Ready JSON Output
# ------------------------------------------------------------------------------
hf_leaderboard_entry = {
"name": model_name,
"params_str": params_str,
"params_num": total_params,
"cot": cot_choice,
"score": round(rating_score, 1),
"exact": round(sequence_accuracy, 1),
"token_acc": round(token_accuracy, 1),
"ppl": round(ppl_score, 3),
"highlight": is_highlight
}
json_str = json.dumps(hf_leaderboard_entry, indent=4)
console.print()
console.print(
Panel(
f"[bold green]{json_str}[/bold green]",
title="[bold yellow] 📋 HTML Ready Leaderboard Entry (Copy & Paste to modelData) [/bold yellow]",
title_align="left",
border_style="yellow",
box=box.ROUNDED,
)
)
console.print("[dim]Copy the JSON above and paste it directly into the 'modelData' array inside index.html![/dim]\n")