char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
da46e59cb
char/aliceai-t5-moe
1#!/usr/bin/env python3 2 3import argparse 4import json 5import os 6import re 7import subprocess 8import sys 9import threading 10import time 11from abc import ABC ,abstractmethod 12from concurrent .futures import ThreadPoolExecutor ,as_completed 13from dataclasses import dataclass ,asdict ,field 14from pathlib import Path 15from queue import Queue 16from typing import Dict ,List ,Optional ,Any ,Tuple 17import requests 18from tqdm import tqdm 19import random 20from math import sqrt 21 22 23@dataclass 24class ServerConfig : 25url :str 26threads :int 27name :str = "" 28 29def wilson_interval (correct :int ,total :int ,z :float = 1.96 )-> Tuple [float ,float ]: 30"""Wilson score confidence interval for a proportion.""" 31if total == 0 : 32return (0.0 ,1.0 ) 33p = correct / total 34z2 = z * z / total 35center = (p + z2 / 2 )/ (1 + z2 ) 36margin = z * sqrt ((p * (1 - p )+ z2 / 4 )/ total )/ (1 + z2 ) 37return (center - margin ,center + margin ) 38 39cache_dir = Path .home ()/ ".cache" / "huggingface" / "datasets" 40cache_dir .mkdir (parents = True ,exist_ok = True ) 41os .environ ["HF_DATASETS_CACHE" ]= str (cache_dir ) 42os .environ ["HF_HUB_DISABLE_TELEMETRY" ]= "1" 43 44GRADER_PATTERNS = { 45"aime" :r'\boxed{(\d+)}|\b(\d+)\b' , 46"aime2025" :r'\boxed{(\d+)}|\b(\d+)\b' , 47"aime2026" :r'\boxed{(\d+)}|\b(\d+)\b' , 48"gsm8k" :r'\b(\d+)\b' , 49} 50 51SAMPLE_ANSWERS = { 52"aime" : [ 53"42" , 54"-123" , 55"999" 56 ], 57"aime2025" : [ 58"42" , 59"-123" , 60"999" 61 ], 62"aime2026" : [ 63"42" , 64"-123" , 65"999" 66 ], 67"gsm8k" : [ 68"42" , 69"-123" , 70"999" 71 ], 72"gpqa" : [ 73"A" , 74"D" , 75"C" 76 ], 77} 78 79TEMPLATE_REGISTRY = { 80"aime" :"""Solve the following math problem step by step. Put your answer inside \\boxed{{}}. 81 82{question} 83 84Remember to put your answer inside \\boxed{{}}. 85""" , 86"aime2025" :"""Solve the following math problem step by step. Put your answer inside \\boxed{{}}. 87 88{question} 89 90Remember to put your answer inside \\boxed{{}}. 91""" , 92"aime2026" :"""Solve the following math problem step by step. Put your answer inside \\boxed{{}}. 93 94{question} 95 96Remember to put your answer inside \\boxed{{}}. 97""" , 98"gsm8k" :"""{question} 99Please reason step by step, and put your final numeric answer within \\boxed{{}} without any extra characters. 100""" , 101"gpqa" :"""Answer the following multiple choice question. The last line of your response should be in the following format: 'Answer: A/B/C/D' (e.g. 'Answer: A'). 102 103{Question} 104 105A) {A} 106B) {B} 107C) {C} 108D) {D} 109""" , 110} 111 112 113class BaseDataset (ABC ): 114questions :List [Dict ] 115 116@abstractmethod 117def get_question (self ,index :int )-> Dict : 118pass 119 120@abstractmethod 121def get_question_text (self ,question :Dict )-> str : 122pass 123 124@abstractmethod 125def get_answer (self ,question :Dict )-> str : 126pass 127 128@abstractmethod 129def get_prompt (self ,question :Dict )-> str : 130pass 131 132def __len__ (self )-> int : 133return len (self .questions ) 134 135 136@dataclass 137class TaskState : 138task_id :str 139prompt :str 140expected :str 141question_text :str = "" 142response :Optional [str ]= None 143answer :Optional [str ]= None 144grader_log :Dict [str ,Any ]= field (default_factory = dict ) 145correct :bool = False 146status :str = "pending" 147tokens :Optional [int ]= None 148tps_gen :Optional [float ]= None 149t_gen_ms :Optional [float ]= None 150reasoning_content :Optional [str ]= None 151server_name :Optional [str ]= None 152chunk_idx :int = 0 153problem_idx :int = 0 154 155 156class EvalState : 157def __init__ ( 158self , 159dataset_type :str , 160sampling_config :Dict [str ,Any ], 161output_file :Path = Path ("llama-eval-state.json" ), 162model_name :Optional [str ]= None 163 ): 164self .dataset_type = dataset_type 165self .sampling_config = sampling_config 166self .output_file = output_file 167self .model_name = model_name 168self .dataset :Optional [BaseDataset ]= None 169self .tasks :List [Tuple [int ,str ]]= [] 170self .all_tasks :List [Tuple [int ,str ]]= [] 171self .task_states :Dict [str ,Any ]= {} 172self .total = 0 173self .correct = 0 174self .processed = 0 175self .total_time :float = 0.0 176self ._lock = threading .Lock () 177 178def load_dataset (self ,seed :int = 1234 ): 179if self .dataset_type == "aime" : 180self .dataset = AimeDataset () 181elif self .dataset_type == "aime2025" : 182self .dataset = Aime2025Dataset () 183elif self .dataset_type == "aime2026" : 184self .dataset = Aime2026Dataset () 185elif self .dataset_type == "gsm8k" : 186self .dataset = Gsm8kDataset () 187elif self .dataset_type == "gpqa" : 188self .dataset = GpqaDataset (variant = "diamond" ,seed = seed ) 189else : 190raise ValueError (f"Unknown dataset type: { self . dataset_type } " ) 191 192def setup_tasks (self ,n_cases :Optional [int ]= None ,seed :int = 1234 ): 193if self .dataset is None : 194raise ValueError ("Dataset not loaded. Call load_dataset() first." ) 195 196if n_cases is None : 197n_cases = len (self .dataset ) 198 199dataset_size = len (self .dataset ) 200rng = random .Random (seed ) 201 202self .tasks = [] 203for chunk_idx in range ((n_cases + dataset_size - 1 )// dataset_size ): 204chunk_size = min (dataset_size ,n_cases - chunk_idx * dataset_size ) 205indices = list (range (dataset_size )) 206rng .shuffle (indices ) 207chunk_indices = indices [:chunk_size ] 208 209for i in chunk_indices : 210task_id = f" { self . dataset_type } _ { chunk_idx :03d } _ { i :03d } " 211self .tasks .append ((i ,task_id )) 212 213self .all_tasks = list (self .tasks ) 214 215def get_case (self ,index :int )-> Tuple [str ,str ,str ]: 216if self .dataset is None : 217raise ValueError ("Dataset not loaded." ) 218question = self .dataset .get_question (index ) 219question_text = self .dataset .get_question_text (question ) 220prompt = self .dataset .get_prompt (question ) 221expected = self .dataset .get_answer (question ) 222return question_text ,prompt ,expected 223 224def add_result ( 225self , 226task_id :str , 227prompt :str , 228expected :str , 229response :Optional [str ], 230answer :Optional [str ], 231grader_log :Dict [str ,Any ], 232correct :bool , 233status :str , 234tokens :Optional [int ]= None , 235tps_gen :Optional [float ]= None , 236t_gen_ms :Optional [float ]= None , 237reasoning_content :Optional [str ]= None , 238server_name :Optional [str ]= None , 239chunk_idx :int = 0 , 240problem_idx :int = 0 , 241 ): 242with self ._lock : 243if "cases" not in self .task_states : 244self .task_states ["cases" ]= {} 245 246self .task_states ["cases" ][task_id ]= { 247"task_id" :task_id , 248"prompt" :prompt , 249"expected" :expected , 250"response" :response , 251"answer" :answer , 252"grader_log" :grader_log , 253"correct" :correct , 254"status" :status , 255"tokens" :tokens , 256"tps_gen" :tps_gen , 257"t_gen_ms" :t_gen_ms , 258"reasoning_content" :reasoning_content , 259"server_name" :server_name , 260"chunk_idx" :chunk_idx , 261"problem_idx" :problem_idx , 262 } 263 264self .correct = sum (1 for c in self .task_states .get ("cases" , {}).values ()if c .get ("correct" ,False )) 265 266def print_progress (self ,task_state :TaskState ,total_tasks :int ,n_correct :int = 0 ): 267display_answer = task_state .answer if task_state .answer else "N/A" 268display_tokens = str (task_state .tokens )if task_state .tokens is not None else "N/A" 269display_tps = f" { task_state . tps_gen :.1f } " if task_state .tps_gen is not None else "N/A" 270display_t_gen = f" { task_state . t_gen_ms / 1000 :.1f } " if task_state .t_gen_ms is not None else "N/A" 271display_server = task_state .server_name if task_state .server_name else "N/A" 272success_ratio = n_correct / self .processed if self .processed > 0 else 0.0 273first_line = task_state .question_text .split ('\n' )[0 ] 274truncated_question = first_line [:43 ] 275if len (first_line )> 43 : 276truncated_question += "..." 277else : 278truncated_question = truncated_question .ljust (43 )+ "..." 279f" { self . processed :3 } / { total_tasks :3 } { task_state . task_id :<20 } { self . dataset_type . upper () } { truncated_question :<40 } { task_state . expected :<10 } { display_answer :<10 } { display_tokens :<6 } { display_tps :<6 } { display_t_gen :<8 } { '✓' if task_state . correct else '✗' } [ { n_correct :3 } / { self . processed :3 } , { success_ratio :.3f } ] { display_server } " ) 280 281def print_summary (self ): 282if self .total == 0 : 283f"\n { '=' * 60 } " ) 284f"Results: 0/0 correct (0.0%)" ) 285f" { '=' * 60 } " ) 286else : 287ci_lower ,ci_upper = self .accuracy_ci () 288f"\n { '=' * 60 } " ) 289f"Results: { self . correct } / { self . total } correct ( { self . correct / self . total * 100 :.1f } %) [ { ci_lower * 100 :.1f } %, { ci_upper * 100 :.1f } %]" ) 290f" { '=' * 60 } " ) 291 292def dump (self ): 293with self ._lock : 294tasks_to_save = self .all_tasks if self .all_tasks else self .tasks 295all_cases = {} 296for i ,task_id in tasks_to_save : 297question_text ,prompt ,expected = self .get_case (i ) 298# Extract chunk_idx from task_id for pending cases 299_parts = task_id .rsplit ("_" ,2 ) 300_chunk_idx = int (_parts [- 2 ])if len (_parts )>= 3 else 0 301if task_id in self .task_states .get ("cases" , {}): 302all_cases [task_id ]= self .task_states ["cases" ][task_id ] 303else : 304all_cases [task_id ]= { 305"task_id" :task_id , 306"prompt" :prompt , 307"expected" :expected , 308"question_text" :question_text , 309"response" :None , 310"answer" :None , 311"grader_log" : {}, 312"correct" :False , 313"status" :"pending" , 314"tokens" :None , 315"tps_gen" :None , 316"t_gen_ms" :None , 317"reasoning_content" :None , 318"server_name" :None , 319"chunk_idx" :_chunk_idx , 320"problem_idx" :i , 321 } 322 323ci_lower ,ci_upper = self .accuracy_ci () 324data = { 325"id" :self .dataset_type , 326"model_name" :self .model_name , 327"tasks" : [tid for _ ,tid in tasks_to_save ], 328"task_states" : { 329"total" :self .total , 330"correct" :self .correct , 331"total_time" :self .total_time , 332"ci_lower" :ci_lower , 333"ci_upper" :ci_upper , 334"cases" :all_cases , 335 }, 336"sampling_config" :self .sampling_config 337 } 338with open (self .output_file ,"w" )as f : 339json .dump (data ,f ,indent = 2 ) 340 341self .dump_html (tasks_to_save ,all_cases ) 342 343def dump_html (self ,tasks_to_save :List [Tuple [int ,str ]],all_cases :Dict [str ,Any ]): 344html_file = Path (str (self .output_file )+ ".html" ) 345 346cases = all_cases 347completed = {tid :c for tid ,c in cases .items ()if c .get ("status" )== "ok" } 348n_correct = sum (1 for c in completed .values ()if c .get ("correct" ,False )) 349n_incorrect = len (completed )- n_correct 350n_pending = len (tasks_to_save )- len (completed ) 351accuracy = n_correct / len (completed )* 100 if completed else 0.0 352ci_lower ,ci_upper = wilson_interval (n_correct ,len (completed ))if completed else (0.0 ,1.0 ) 353 354sampling_parts = [] 355for k ,v in self .sampling_config .items (): 356if v is not None : 357sampling_parts .append (f" { k } = { v } " ) 358sampling_str = ", " .join (sampling_parts )if sampling_parts else "default" 359 360rows = [] 361for i ,task_id in tasks_to_save : 362case = cases .get (task_id , {}) 363status = case .get ("status" ,"pending" ) 364expected = case .get ("expected" ,"" ) 365answer = case .get ("answer" )or "" if status == "ok" else "" 366is_correct = case .get ("correct" ,False )if status == "ok" else False 367response = case .get ("response" ,"" )or "" 368prompt = case .get ("prompt" ,"" )or "" 369grader_log = case .get ("grader_log" , {}) 370 371if status == "ok" : 372status_class = "correct" if is_correct else "incorrect" 373status_text = "✓" if is_correct else "✗" 374elif status == "pending" : 375status_class = "pending" 376status_text = "–" 377else : 378status_class = "error" 379status_text = "!" 380 381tokens = case .get ("tokens" ) 382tokens_str = str (tokens )if tokens is not None else "" 383tps_gen = case .get ("tps_gen" ) 384tps_str = f" { tps_gen :.1f } " if tps_gen is not None else "" 385t_gen_ms = case .get ("t_gen_ms" ) 386t_gen_str = f" { t_gen_ms / 1000 :.1f } " if t_gen_ms is not None else "" 387reasoning_content = case .get ("reasoning_content" ,"" )or "" 388server_name = case .get ("server_name" ,"" )or "" 389 390escaped_response = self ._escape_html (response ) 391escaped_prompt = self ._escape_html (prompt ) 392escaped_reasoning = self ._escape_html (reasoning_content ) 393grader_log_str = self ._escape_html (json .dumps (grader_log ,indent = 2 )) 394escaped_server = self ._escape_html (server_name ) 395 396answer_class = status_class if status == "ok" else "" 397rows .append (f"""<tr class="task-row" onclick="toggleDetails(' { task_id } ')"> 398<td> { task_id } </td> 399<td class=" { status_class } "> { status_text } </td> 400<td> { self . _escape_html ( expected ) } </td> 401<td class=" { answer_class } "> { self . _escape_html ( answer ) } </td> 402<td> { tokens_str } </td> 403<td> { tps_str } </td> 404<td> { t_gen_str } </td> 405<td> { escaped_server } </td> 406</tr> 407<tr id="details- { task_id } " class="details-row"> 408<td colspan="8"> 409<div class="details-content"> 410<b>Prompt</b><pre> { escaped_prompt } </pre> 411<b>Response</b><pre> { escaped_response } </pre> 412{ f'<b>Reasoning</b><pre> { escaped_reasoning } </pre>' if escaped_reasoning else '' } 413<b>Grader</b><pre> { grader_log_str } </pre> 414</div> 415</td> 416</tr>""" ) 417 418rows_html = "\n" .join (rows ) 419 420# ---- per-problem summary table ---- 421problem_groups :Dict [int ,List [Dict [str ,Any ]]]= {} 422for _tid ,_case in cases .items (): 423if _case .get ("status" )!= "ok" : 424continue 425_pidx = _case .get ("problem_idx" ) 426if _pidx is None : 427_p_parts = _tid .rsplit ("_" ,2 ) 428_pidx = int (_p_parts [- 1 ])if len (_p_parts )>= 3 else 0 429problem_groups .setdefault (_pidx , []).append (_case ) 430 431summary_rows_html = "" 432if problem_groups : 433def _stat (v ,fmt = ".1f" ,avg_fmt = None ): 434if not v : 435return ("–" ,"–" ,"–" ) 436af = fmt if avg_fmt is None else avg_fmt 437return (f" { min ( v ):{ fmt } } " ,f" { sum ( v ) / len ( v ):{ af } } " ,f" { max ( v ):{ fmt } } " ) 438 439summary_data = [] 440for pidx ,g in problem_groups .items (): 441runs = len (g ) 442n_ok = sum (1 for c in g if c .get ("correct" ,False )) 443toks = [c ["tokens" ]for c in g if c .get ("tokens" )is not None ] 444tps = [c ["tps_gen" ]for c in g if c .get ("tps_gen" )is not None ] 445tg = [c ["t_gen_ms" ]/ 1000 for c in g if c .get ("t_gen_ms" )is not None ] 446summary_data .append (( 447pidx ,runs ,n_ok , 448_stat (toks ,"d" ,".0f" ), 449_stat (tps ), 450_stat (tg ), 451 )) 452 453summary_data .sort (key = lambda r :r [0 ])# sort by problem index ascending 454 455summary_rows_html = "\n" .join ( 456f"""<tr class="summary-row"> 457<td> { p :03d } </td> 458<td> { r } </td> 459<td> { n } / { r } </td> 460<td> { tk [ 0 ] } </td><td> { tk [ 1 ] } </td><td> { tk [ 2 ] } </td> 461<td> { tp [ 0 ] } </td><td> { tp [ 1 ] } </td><td> { tp [ 2 ] } </td> 462<td> { tg [ 0 ] } </td><td> { tg [ 1 ] } </td><td> { tg [ 2 ] } </td> 463</tr>""" 464for p ,r ,n ,tk ,tp ,tg in summary_data 465 ) 466 467html_content = f"""<!DOCTYPE html> 468<html> 469<head> 470<meta charset="UTF-8"> 471<title> { self . dataset_type . upper () } Eval</title> 472<style> 473body {{ font-family: system-ui, sans-serif; margin: 0; padding: 16px; background: #fff; color: #222; }} 474.bar {{ padding: 8px 0; font-size: 13px; color: #555; font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; display: grid; grid-template-columns: auto 1fr auto 1fr; gap: 2px 12px; align-items: baseline; }} 475.bar .label {{ color: #888; }} 476.bar .value {{ color: #222; }} 477table {{ width: 100%; border-collapse: collapse; font-size: 13px; font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; }} 478th {{ text-align: left; padding: 6px 8px; border-bottom: 2px solid #ccc; font-weight: 600; }} 479td {{ padding: 4px 8px; border-bottom: 1px solid #eee; vertical-align: top; }} 480.task-row {{ cursor: pointer; }} 481.task-row:hover {{ background: #f5f5f5; }} 482.correct {{ color: #1a7f37; }} 483.incorrect {{ color: #cf222e; }} 484.pending {{ color: #888; }} 485.error {{ color: #9a6700; }} 486.details-row {{ display: none; }} 487.details-row.open {{ display: table-row; }} 488.details-content {{ padding: 8px 16px; background: #f6f8fa; font-size: 12px; }} 489.details-content b {{ color: #555; }} 490.details-content pre {{ background: #fff; border: 1px solid #e1e4e8; padding: 8px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; margin: 4px 0 8px; }} 491.summary-table {{ margin-bottom: 16px; font-size: 13px; width: 100%; }} 492.summary-row {{ background: #fafbfc; }} 493.summary-row:hover {{ background: #f5f5f5; }} 494.summary-table th {{ text-align: right; font-weight: 600; }} 495.summary-table th:first-child {{ text-align: left; }} 496.summary-table th[colspan] {{ text-align: center; }} 497.summary-table td {{ text-align: right; }} 498.summary-table td:first-child {{ text-align: left; }} 499.tabs {{ display: flex; border-bottom: 2px solid #ddd; margin: 12px 0 0; }} 500.tab-btn {{ padding: 6px 16px; border: none; background: none; font-size: 13px; cursor: pointer; color: #555; border-bottom: 2px solid transparent; margin-bottom: -2px; font-weight: 500; }} 501.tab-btn:hover {{ color: #222; }} 502.tab-btn.active {{ color: #222; border-bottom-color: #222; font-weight: 600; }} 503.tab-content {{ display: none; }} 504.tab-content.active {{ display: block; }} 505</style> 506</head> 507<body> 508<div class="bar"> 509<div class="label">Dataset</div><div class="value"><b> { self . dataset_type . upper () } </b></div> 510<div class="label">Model</div><div class="value"><b> { self . model_name or 'N/A' } </b></div> 511<div class="label">Accuracy</div><div class="value"><b> { accuracy :.1f } %</b> [ { ci_lower * 100 :.1f } %, { ci_upper * 100 :.1f } %]</div> 512<div class="label">Correct</div><div class="value"><span class="correct"> { n_correct } </span> / { len ( completed ) } </div> 513<div class="label">Pending</div><div class="value"> { n_pending } </div> 514<div class="label">Time</div><div class="value"> { self . total_time :.1f } s</div> 515<div class="label">Sampling</div><div class="value"> { sampling_str } </div> 516</div> 517<div class="tabs"> 518<button class="tab-btn active" data-tab="detailed" onclick="switchTab(this)">Detailed</button> 519<button class="tab-btn" data-tab="summary" onclick="switchTab(this)">Summary</button> 520</div> 521<div id="tab-detailed" class="tab-content active"> 522<table> 523<thead> 524<tr> 525<th>ID</th> 526<th></th> 527<th>Gold</th> 528<th>Answer</th> 529<th>Tokens</th> 530<th>T/s</th> 531<th>Gen s</th> 532<th>Server</th> 533</tr> 534</thead> 535<tbody> 536{ rows_html } 537</tbody> 538</table> 539</div> 540<div id="tab-summary" class="tab-content"> 541<table class="summary-table"> 542<thead> 543<tr> 544<th>Problem</th> 545<th>Runs</th> 546<th>Correct</th> 547<th colspan="3">Tokens</th> 548<th colspan="3">T/s</th> 549<th colspan="3">Gen s</th> 550</tr> 551<tr> 552<th></th> 553<th></th> 554<th></th> 555<th>min</th><th>avg</th><th>max</th> 556<th>min</th><th>avg</th><th>max</th> 557<th>min</th><th>avg</th><th>max</th> 558</tr> 559</thead> 560<tbody> 561{ summary_rows_html } 562</tbody> 563</table> 564</div> 565<script> 566function toggleDetails(id) {{ document.getElementById('details-'+id).classList.toggle('open'); }} 567function switchTab(btn) {{ 568document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); 569document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); 570btn.classList.add('active'); 571document.getElementById('tab-'+btn.dataset.tab).classList.add('active'); 572}} 573</script> 574</body> 575</html>""" 576 577with open (html_file ,"w" )as f : 578f .write (html_content ) 579 580def _escape_html (self ,s :str )-> str : 581return (s .replace ("&" ,"&" ) 582 .replace ("<" ,"<" ) 583 .replace (">" ,">" ) 584 .replace ('"' ,""" ) 585 .replace ("'" ,"'" )) 586 587@classmethod 588def load (cls ,path :Path )-> "EvalState" : 589with open (path ,"r" )as f : 590data = json .load (f ) 591 592eval_state = cls ( 593dataset_type = data ["id" ], 594sampling_config = data ["sampling_config" ], 595output_file = path , 596model_name = data .get ("model_name" ) 597 ) 598eval_state .load_dataset () 599 600eval_state .tasks = [] 601eval_state .all_tasks = [] 602for task_id in data .get ("tasks" , []): 603parts = task_id .rsplit ("_" ,2 ) 604if len (parts )>= 3 : 605idx = int (parts [- 1 ]) 606else : 607idx = 0 608eval_state .tasks .append ((idx ,task_id )) 609eval_state .all_tasks .append ((idx ,task_id )) 610 611eval_state .task_states = data .get ("task_states" , {}) 612 613cases = eval_state .task_states .get ("cases" , {}) 614eval_state .total = eval_state .task_states .get ("total" ,0 ) 615eval_state .correct = eval_state .task_states .get ("correct" ,0 ) 616eval_state .total_time = eval_state .task_states .get ("total_time" ,0.0 ) 617 618if eval_state .total == 0 : 619eval_state .total = len (cases ) 620eval_state .correct = sum (1 for c in cases .values ()if c .get ("correct" ,False )) 621 622return eval_state 623 624def is_complete (self )-> bool : 625if not self .all_tasks : 626return False 627cases = self .task_states .get ("cases" , {}) 628completed = {tid for tid in self .task_states .get ("cases" , {}).keys ()if cases .get (tid , {}).get ("status" )== "ok" } 629return len (completed )== len (self .all_tasks ) 630 631def get_pending_tasks (self )-> List [Tuple [int ,str ]]: 632cases = self .task_states .get ("cases" , {}) 633pending = [] 634for i ,task_id in self .all_tasks : 635status = cases .get (task_id , {}).get ("status" ,"pending" ) 636if status != "ok" : 637pending .append ((i ,task_id )) 638return pending 639 640def print_all_tasks (self ): 641cases = self .task_states .get ("cases" , {}) 642tasks_to_show = self .all_tasks if self .all_tasks else self .tasks 643"Tasks:" ) 645" Task ID Dataset Prompt (first 40 chars) Expected Answer Tokens T/s Gen s Status" ) 646for i ,task_id in tasks_to_show : 647question ,prompt ,expected = self .get_case (i ) 648case = cases .get (task_id , {}) 649status = case .get ("status" ,"pending" ) 650answer = case .get ("answer" )or "N/A" if status == "ok" else "N/A" 651tokens = case .get ("tokens" ) 652tokens_str = str (tokens )if tokens is not None else "N/A" 653tps_gen = case .get ("tps_gen" ) 654tps_str = f" { tps_gen :.1f } " if tps_gen is not None else "N/A" 655t_gen_ms = case .get ("t_gen_ms" ) 656t_gen_str = f" { t_gen_ms / 1000 :.1f } " if t_gen_ms is not None else "N/A" 657server_name = case .get ("server_name" ,"" )or "" 658is_correct = case .get ("correct" ,False )if status == "ok" else False 659symbol = "✓ " if is_correct else ("✗ " if status == "ok" else "" ) 660first_line = question .split ('\n' )[0 ] 661question_trunc = first_line [:43 ] 662if len (first_line )> 43 : 663question_trunc += "..." 664else : 665question_trunc = question_trunc .ljust (43 )+ "..." 666f" { task_id :<20 } { self . dataset_type . upper () } { question_trunc :<40 } { expected :<10 } { answer :<10 } { tokens_str :<6 } { tps_str :<6 } { t_gen_str :<8 } { symbol } { status } { server_name } " ) 667def print_existing_summary (self ): 670cases = self .task_states .get ("cases" , {}) 671completed_cases = {tid :c for tid ,c in cases .items ()if c .get ("status" )== "ok" } 672correct = sum (1 for c in completed_cases .values ()if c .get ("correct" ,False )) 673total = len (completed_cases ) 674if total == 0 : 675f" { '=' * 60 } " ) 676f"Results: 0/0 correct (0.0%)" ) 677f" { '=' * 60 } " ) 678else : 679ci_lower ,ci_upper = self .accuracy_ci () 680f" { '=' * 60 } " ) 681f"Results: { correct } / { total } correct ( { correct / total * 100 :.1f } %) [ { ci_lower * 100 :.1f } %, { ci_upper * 100 :.1f } %]" ) 682f" { '=' * 60 } " ) 683 684def accuracy_ci (self )-> Tuple [float ,float ]: 685"""Compute Wilson score confidence interval from completed cases.""" 686cases = self .task_states .get ("cases" , {}) 687completed = {tid :c for tid ,c in cases .items ()if c .get ("status" )== "ok" } 688correct = sum (1 for c in completed .values ()if c .get ("correct" ,False )) 689total = len (completed ) 690return wilson_interval (correct ,total ) 691 692def normalize_number (s :str )-> Optional [int ]: 693match = re .match (r"\d+" ,s )# match digits from the start 694if not match : 695return None 696return int (match .group (0 )) 697 698class AimeDataset (BaseDataset ): 699def __init__ (self ,split :str = "train" ): 700self .split = split 701self .questions = [] 702self ._load_dataset () 703 704def _load_dataset (self ): 705f"Loading AIME dataset (split: { self . split } )..." ) 706from datasets import load_dataset 707 708cache_path = cache_dir / "AI-MO___aimo-validation-aime" / "default" / "0.0.0" 709if cache_path .exists (): 710f"Using cached dataset from { cache_path } " ) 711ds = load_dataset ("AI-MO/aimo-validation-aime" ,split = self .split ,cache_dir = str (cache_path )) 712else : 713ds = load_dataset ("AI-MO/aimo-validation-aime" ,split = self .split ) 714 715self .questions = [] 716for row in ds : 717question = dict (row ) 718question ["dataset_type" ]= "aime" 719self .questions .append (question ) 720 721f"AIME dataset loaded: { len ( self . questions ) } questions" ) 722 723def get_question (self ,index :int )-> Dict : 724"""Get question by index""" 725return self .questions [index ] 726 727def get_question_text (self ,question :Dict )-> str : 728"""Get question string""" 729return question ["problem" ]if "problem" in question else question ["question" ] 730 731def get_answer (self ,question :Dict )-> str : 732answer = question ["answer" ] 733if isinstance (answer ,str ): 734normalized = normalize_number (answer ) 735return str (normalized )if normalized is not None else answer 736return str (answer ) 737 738def get_prompt (self ,question :Dict )-> str : 739"""Get formatted prompt for the question""" 740return TEMPLATE_REGISTRY [question ["dataset_type" ]].format ( 741question = self .get_question_text (question ), 742 ) 743 744class Aime2025Dataset (BaseDataset ): 745def __init__ (self ): 746self .questions = [] 747self ._load_dataset () 748 749def _load_dataset (self ): 750f"Loading AIME2025 dataset..." ) 751from datasets import load_dataset 752 753config_name = "AIME2025-I" 754cache_path = cache_dir / "opencompass___AIME2025" / "default" / "0.0.0" 755if cache_path .exists (): 756f"Using cached dataset from { cache_path } " ) 757ds = load_dataset ("opencompass/AIME2025" ,config_name ,split = "test" ,cache_dir = str (cache_path )) 758else : 759ds = load_dataset ("opencompass/AIME2025" ,config_name ,split = "test" ) 760 761self .questions = [] 762for row in ds : 763question = dict (row ) 764question ["dataset_type" ]= "aime2025" 765self .questions .append (question ) 766 767f"AIME2025 dataset loaded: { len ( self . questions ) } questions" ) 768 769f"Loading AIME2025 dataset (part 2)..." ) 770config_name_2 = "AIME2025-II" 771cache_path_2 = cache_dir / "opencompass___AIME2025" / "default" / "0.0.0" 772if cache_path_2 .exists (): 773f"Using cached dataset from { cache_path_2 } " ) 774ds_2 = load_dataset ("opencompass/AIME2025" ,config_name_2 ,split = "test" ,cache_dir = str (cache_path_2 )) 775else : 776ds_2 = load_dataset ("opencompass/AIME2025" ,config_name_2 ,split = "test" ) 777 778for row in ds_2 : 779question = dict (row ) 780question ["dataset_type" ]= "aime2025" 781self .questions .append (question ) 782 783f"AIME2025 dataset loaded: { len ( self . questions ) } questions (total)" ) 784 785def get_question (self ,index :int )-> Dict : 786"""Get question by index""" 787return self .questions [index ] 788 789def get_question_text (self ,question :Dict )-> str : 790"""Get question string""" 791return question ["question" ] 792 793def get_answer (self ,question :Dict )-> str : 794answer = question ["answer" ] 795if isinstance (answer ,str ): 796normalized = normalize_number (answer ) 797return str (normalized )if normalized is not None else answer 798return str (answer ) 799 800def get_prompt (self ,question :Dict )-> str : 801"""Get formatted prompt for the question""" 802return TEMPLATE_REGISTRY ["aime2025" ].format ( 803question = self .get_question_text (question ), 804 ) 805 806class Aime2026Dataset (BaseDataset ): 807def __init__ (self ): 808self .questions = [] 809self ._load_dataset () 810 811def _load_dataset (self ): 812f"Loading AIME2026 dataset..." ) 813from datasets import load_dataset 814 815cache_path = cache_dir / "MathArena___aime_2026" / "default" / "0.0.0" 816if cache_path .exists (): 817f"Using cached dataset from { cache_path } " ) 818ds = load_dataset ("MathArena/aime_2026" ,"default" ,split = "train" ,cache_dir = str (cache_path )) 819else : 820ds = load_dataset ("MathArena/aime_2026" ,"default" ,split = "train" ) 821 822self .questions = [] 823for row in ds : 824question = dict (row ) 825question ["dataset_type" ]= "aime2026" 826self .questions .append (question ) 827 828f"AIME2026 dataset loaded: { len ( self . questions ) } questions" ) 829 830def get_question (self ,index :int )-> Dict : 831"""Get question by index""" 832return self .questions [index ] 833 834def get_question_text (self ,question :Dict )-> str : 835"""Get question string""" 836return question ["problem" ] 837 838def get_answer (self ,question :Dict )-> str : 839return str (question ["answer" ]) 840 841def get_prompt (self ,question :Dict )-> str : 842"""Get formatted prompt for the question""" 843return TEMPLATE_REGISTRY ["aime2026" ].format ( 844question = self .get_question_text (question ), 845 ) 846 847class Gsm8kDataset (BaseDataset ): 848def __init__ (self ,split :str = "test" ): 849self .split = split 850self .questions = [] 851self ._load_dataset () 852 853def _load_dataset (self ): 854f"Loading GSM8K dataset (split: { self . split } )..." ) 855from datasets import load_dataset 856 857cache_path = cache_dir / "openai___gsm8k" / "default" / "0.0.0" 858if cache_path .exists (): 859f"Using cached dataset from { cache_path } " ) 860ds = load_dataset ("openai/gsm8k" ,"main" ,split = self .split ,cache_dir = str (cache_path )) 861else : 862ds = load_dataset ("openai/gsm8k" ,"main" ,split = self .split ) 863 864self .questions = [] 865for row in ds : 866question = dict (row ) 867question ["dataset_type" ]= "gsm8k" 868 869# Extract numeric answer from the answer field (already has #### prefix) 870gold = question ["answer" ] 871# Split by #### and take the last part 872parts = gold .split ("####" ) 873if len (parts )> 1 : 874gold = parts [- 1 ].strip () 875# Extract the first number from the remaining text 876normalized = normalize_number (gold ) 877question ["gold" ]= str (normalized )if normalized is not None else gold 878 879self .questions .append (question ) 880 881f"GSM8K dataset loaded: { len ( self . questions ) } questions" ) 882 883def get_question (self ,index :int )-> Dict : 884"""Get question by index""" 885return self .questions [index ] 886 887def get_question_text (self ,question :Dict )-> str : 888"""Get question string""" 889return question ["problem" ]if "problem" in question else question ["question" ] 890 891def get_answer (self ,question :Dict )-> str : 892# GSM8K has pre-extracted gold field, AIME uses answer field 893if "gold" in question : 894return question ["gold" ] 895answer = question ["answer" ] 896if isinstance (answer ,str ): 897normalized = normalize_number (answer ) 898return str (normalized )if normalized is not None else answer 899return str (answer ) 900 901def get_prompt (self ,question :Dict )-> str : 902"""Get formatted prompt for the question""" 903return TEMPLATE_REGISTRY [question ["dataset_type" ]].format ( 904question = self .get_question_text (question ), 905 ) 906 907class GpqaDataset (BaseDataset ): 908def __init__ (self ,variant :str = "diamond" ,seed :int = 1234 ): 909self .variant = variant 910self .seed = seed 911self .questions = [] 912self ._load_dataset () 913 914def _load_dataset (self ): 915f"Loading GPQA dataset (variant: { self . variant } )..." ) 916import pandas as pd 917 918url = f"https://openaipublic.blob.core.windows.net/simple-evals/gpqa_ { self . variant } .csv" 919df = pd .read_csv (url ) 920 921rng = random .Random (self .seed ) 922 923self .questions = [] 924for _ ,row in df .iterrows (): 925question = row .to_dict () 926question ["dataset_type" ]= "gpqa" 927 928# Shuffle the answer options 929correct_answer = question ["Correct Answer" ] 930incorrect_answers = [ 931question ["Incorrect Answer 1" ], 932question ["Incorrect Answer 2" ], 933question ["Incorrect Answer 3" ] 934 ] 935 936# Create list of (answer, is_correct) tuples 937options = [(ans ,ans == correct_answer )for ans in incorrect_answers ] 938options .append ((correct_answer ,True )) 939 940# Shuffle the options 941rng .shuffle (options ) 942 943# Extract shuffled answers and determine correct letter 944shuffled_answers = [ans for ans ,_ in options ] 945correct_letter = chr (ord ('A' )+ options .index ((correct_answer ,True ))) 946 947# Store shuffled answers and correct letter 948question ["shuffled_answers" ]= shuffled_answers 949question ["correct_letter" ]= correct_letter 950 951self .questions .append (question ) 952 953f"GPQA dataset loaded: { len ( self . questions ) } questions" ) 954 955def get_question (self ,index :int )-> Dict : 956"""Get question by index""" 957return self .questions [index ] 958 959def get_question_text (self ,question :Dict )-> str : 960"""Get question string""" 961return question ["Question" ] 962 963def get_answer (self ,question :Dict )-> str : 964# GPQA returns the correct letter (A, B, C, or D) 965return question ["correct_letter" ] 966 967def get_prompt (self ,question :Dict )-> str : 968"""Get formatted prompt for the question""" 969return TEMPLATE_REGISTRY ["gpqa" ].format ( 970Question = self .get_question_text (question ), 971A = question ["shuffled_answers" ][0 ], 972B = question ["shuffled_answers" ][1 ], 973C = question ["shuffled_answers" ][2 ], 974D = question ["shuffled_answers" ][3 ] 975 ) 976 977class Grader : 978def __init__ ( 979self , 980grader_type :str = "llm" , 981grader_script :Optional [str ]= None , 982grader_model_name :Optional [str ]= None , 983grader_server_url :str = "" , 984dataset_type :str = "aime" 985 ): 986self .grader_type = grader_type 987self .grader_script = grader_script 988self .grader_model_name = grader_model_name 989self .grader_server_url = grader_server_url 990self .dataset_type = dataset_type 991self .pattern = self ._get_pattern () 992 993def _get_pattern (self )-> Optional [str ]: 994if self .grader_type == "regex" : 995return GRADER_PATTERNS .get (self .dataset_type )# Use dataset_type as key 996return None 997 998def _extract_answer_regex (self ,pred :str )-> Optional [str ]: 999"""Extract answer using regex pattern""" 1000if not self .pattern : 1001return None 1002 1003# For AIME datasets, prioritize boxed answers 1004if self .dataset_type in ["aime" ,"aime2025" ]: 1005boxed_pattern = r'\\boxed{([^}]+)}' 1006boxed_matches = re .findall (boxed_pattern ,pred ,re .IGNORECASE ) 1007if boxed_matches : 1008# Return the last boxed answer found (most likely the final answer) 1009return boxed_matches [- 1 ].strip () 1010 1011# For other datasets, search for numbers from the end of the text 1012# This prioritizes numbers that appear later in the response 1013matches = re .findall (self .pattern ,pred ,re .IGNORECASE ) 1014if not matches : 1015return None 1016 1017# Process matches from end to start 1018for match in reversed (matches ): 1019if isinstance (match ,tuple ): 1020match = match [0 ]if match [0 ]else match [1 ] 1021answer = match .strip () 1022if answer : 1023return answer 1024return None 1025 1026def _grade_regex (self ,gold :str ,pred :str )-> Tuple [bool ,Optional [str ]]: 1027"""Grade using regex pattern matching""" 1028answer = self ._extract_answer_regex (pred ) 1029if answer is None : 1030return False ,None 1031is_correct = answer .strip ()== gold .strip () 1032return is_correct ,answer 1033 1034def _grade_cli (self ,gold :str ,pred :str )-> Tuple [bool ,Optional [str ]]: 1035"""Grade using external CLI script""" 1036if not self .grader_script : 1037raise ValueError ("CLI grader requires --grader-script" ) 1038 1039script_path = Path (self .grader_script ) 1040if not script_path .exists (): 1041raise FileNotFoundError (f"Grader script not found: { self . grader_script } " ) 1042 1043try : 1044result = subprocess .run ( 1045 [str (script_path ),"--answer" ,pred ,"--expected" ,gold ], 1046capture_output = True , 1047text = True , 1048timeout = 30 1049 ) 1050is_correct = result .returncode == 0 1051answer = pred if is_correct else None 1052return is_correct ,answer 1053except subprocess .TimeoutExpired : 1054return False ,None 1055except Exception as e : 1056return False ,None 1057 1058def _grade_llm (self ,gold :str ,pred :str ,problem :str )-> Tuple [bool ,Optional [str ]]: 1059"""Grade using LLM-based extraction with few-shot examples""" 1060sample_answers = SAMPLE_ANSWERS .get (self .dataset_type , []) 1061sample_examples = "\n" .join ([ 1062f"Example { i + 1 } : { ans } " for i ,ans in enumerate (sample_answers ) 1063 ]) 1064 1065system_prompt = f"""You are an answer extraction system. Your task is to extract the answer from the model's response. 1066 1067Here are some examples of extracted answers to demonstrate what you are supposed to output: 1068 1069{ sample_examples } 1070 1071When extracting the answer, provide only the extracted answer itself, nothing else. If there is no clear answer that can be extracted from the response, reply with 'no answer'.""" 1072 1073user_prompt = f"""Extract the answer from the following response: 1074 1075" { pred } " 1076 1077Please provide only the extracted answer, nothing else. If there is no clear answer that can be extracted from the response, reply with 'no answer'.""" 1078 1079url = f" { self . grader_server_url } /v1/chat/completions" 1080headers = {"Content-Type" :"application/json" } 1081data = { 1082"model" :self .grader_model_name , 1083"messages" : [ 1084 {"role" :"system" ,"content" :system_prompt }, 1085 {"role" :"user" ,"content" :user_prompt } 1086 ], 1087"temperature" :0 , 1088 } 1089#print(json.dumps(data, indent=2)) 1090 1091try : 1092response = requests .post (url ,headers = headers ,json = data ) 1093response .raise_for_status () 1094answer = response .json ()["choices" ][0 ]["message" ]["content" ].strip () 1095is_correct = answer .strip ().lower ()== gold .strip ().lower () 1096return is_correct ,answer 1097except Exception as e : 1098return False ,None 1099 1100def _truncate_response (self ,response :str ,max_lines :int = 6 )-> str : 1101"""Keep only last N lines of response""" 1102lines = response .split ('\n' ) 1103return '\n' .join (lines [- max_lines :])if len (lines )> max_lines else response 1104 1105def grade (self ,gold :str ,pred :str ,problem :str = "" )-> Tuple [bool ,Optional [str ]]: 1106"""Grade the response""" 1107if self .grader_type == "regex" : 1108return self ._grade_regex (gold ,pred ) 1109elif self .grader_type == "cli" : 1110return self ._grade_cli (gold ,pred ) 1111elif self .grader_type == "llm" : 1112return self ._grade_llm (gold ,pred ,problem ) 1113else : 1114raise ValueError (f"Unknown grader type: { self . grader_type } " ) 1115 1116class Processor : 1117def __init__ ( 1118self , 1119server_configs :List [ServerConfig ], 1120grader :Grader , 1121model_name :Optional [str ]= None , 1122n_predict :int = - 1 1123 ): 1124self .server_configs = server_configs 1125self .grader = grader 1126self .model_name = model_name 1127self .n_predict = n_predict 1128 1129@staticmethod 1130def _check_server (server_config :ServerConfig )-> List [str ]: 1131url = f" { server_config . url } /v1/models" 1132try : 1133response = requests .get (url ) 1134response .raise_for_status () 1135models = [m ["id" ]for m in response .json ().get ("data" , [])] 1136return models 1137except Exception as e : 1138f"Error: Cannot reach server { server_config . name } ( { server_config . url } ): { e } " ,file = sys .stderr ) 1139sys .exit (1 ) 1140 1141def _make_request ( 1142self ,server_config :ServerConfig ,eval_state :EvalState ,prompt :str 1143 )-> Tuple [Dict [str ,Any ],int ,Optional [float ],Optional [float ],str ]: 1144url = f" { server_config . url } /v1/chat/completions" 1145headers = {"Content-Type" :"application/json" } 1146data = { 1147"model" :self .model_name if self .model_name else "llama" , 1148"messages" : [{"role" :"user" ,"content" :prompt }], 1149"n_predict" :self .n_predict 1150 } 1151if eval_state .sampling_config .get ("temperature" )is not None : 1152data ["temperature" ]= eval_state .sampling_config ["temperature" ] 1153if eval_state .sampling_config .get ("top_k" )is not None : 1154data ["top_k" ]= eval_state .sampling_config ["top_k" ] 1155if eval_state .sampling_config .get ("top_p" )is not None : 1156data ["top_p" ]= eval_state .sampling_config ["top_p" ] 1157if eval_state .sampling_config .get ("min_p" )is not None : 1158data ["min_p" ]= eval_state .sampling_config ["min_p" ] 1159 1160response = requests .post (url ,headers = headers ,json = data ) 1161response .raise_for_status () 1162result = response .json () 1163tokens = result .get ("usage" , {}).get ("completion_tokens" ,0 ) 1164timings = result .get ("timings" , {}) 1165tps_gen = timings .get ("predicted_per_second" )if timings else None 1166t_gen_ms = timings .get ("predicted_ms" )if timings else None 1167finish_reason = result .get ("choices" , [{}])[0 ].get ("finish_reason" ,"stop" ) 1168return result ,tokens ,tps_gen ,t_gen_ms ,finish_reason 1169 1170def _process_single_case ( 1171self ,server_config :ServerConfig ,eval_state :EvalState ,i :int ,task_id :str 1172 )-> TaskState : 1173question_text ,prompt ,expected = eval_state .get_case (i ) 1174 1175# Extract chunk_idx from task_id: "{dataset_type}_{chunk_idx:03d}_{index:03d}" 1176_parts = task_id .rsplit ("_" ,2 ) 1177chunk_idx = int (_parts [- 2 ])if len (_parts )>= 3 else 0 1178problem_idx = i 1179 1180task_state = TaskState ( 1181task_id = task_id , 1182prompt = prompt , 1183expected = expected , 1184question_text = question_text , 1185server_name = server_config .name , 1186chunk_idx = chunk_idx , 1187problem_idx = problem_idx , 1188 ) 1189 1190try : 1191response ,tokens ,tps_gen ,t_gen_ms ,finish_reason = self ._make_request (server_config ,eval_state ,prompt ) 1192result = response ["choices" ][0 ]["message" ]["content" ] 1193reasoning_content = response ["choices" ][0 ].get ("message" , {}).get ("reasoning_content" ) 1194task_state .response = result 1195task_state .tokens = tokens 1196task_state .tps_gen = tps_gen 1197task_state .t_gen_ms = t_gen_ms 1198task_state .reasoning_content = reasoning_content 1199 1200if finish_reason != "stop" : 1201task_state .status = f"error: finish_reason= { finish_reason } " 1202eval_state .add_result ( 1203task_id ,prompt ,expected ,result ,None , 1204 {"finish_reason" :finish_reason },False ,task_state .status , 1205tokens ,tps_gen ,t_gen_ms ,reasoning_content ,server_config .name , 1206chunk_idx ,problem_idx , 1207 ) 1208eval_state .dump () 1209return task_state 1210 1211result_truncated = self .grader ._truncate_response (result ,max_lines = 10 ) 1212is_correct ,answer = self .grader .grade (expected ,result_truncated ,prompt ) 1213 1214grader_log = { 1215"pred" :result_truncated , 1216"grader_type" :self .grader .grader_type 1217 } 1218if self .grader .grader_type == "regex" and self .grader .pattern : 1219grader_log ["pattern" ]= self .grader .pattern 1220 1221task_state .correct = is_correct 1222task_state .answer = answer 1223task_state .grader_log = grader_log 1224task_state .status = "ok" 1225 1226eval_state .add_result ( 1227task_id ,prompt ,expected ,result ,answer , 1228grader_log ,is_correct ,"ok" , 1229tokens ,tps_gen ,t_gen_ms ,reasoning_content ,server_config .name , 1230chunk_idx ,problem_idx , 1231 ) 1232 1233eval_state .dump () 1234 1235except Exception as e : 1236task_state .status = f"error: { str ( e ) } " 1237 1238return task_state 1239 1240@staticmethod 1241def _worker ( 1242server_config :ServerConfig , 1243processor :"Processor" , 1244eval_state :EvalState , 1245task_queue :Queue , 1246results_queue :Queue , 1247 ): 1248"""Worker that pulls tasks from a shared queue and sends them to its server.""" 1249while True : 1250task = task_queue .get () 1251if task is None :# sentinel 1252task_queue .task_done () 1253break 1254try : 1255i ,task_id = task 1256result = processor ._process_single_case (server_config ,eval_state ,i ,task_id ) 1257results_queue .put (result ) 1258finally : 1259task_queue .task_done () 1260 1261def evaluate (self ,eval_state :EvalState ,verbose :bool = False ,resume :bool = False ): 1262total_tasks = len (eval_state .tasks ) 1263eval_state .total = len (eval_state .all_tasks )if eval_state .all_tasks else total_tasks 1264eval_state .processed = 0 1265start_time = time .time () 1266 1267# Check servers and list models 1268server_models = [self ._check_server (sc )for sc in self .server_configs ] 1269 1270# Print server info 1271f"\nProcessing { len ( eval_state . tasks ) } { eval_state . dataset_type . upper () } tasks ..." ) 1272f"Servers ( { len ( self . server_configs ) } ):" ) 1273for i ,sc in enumerate (self .server_configs ): 1274models_str = ", " .join (server_models [i ])if server_models [i ]else "(none)" 1275f" { i + 1 } . { sc . name } — { sc . url } ( { sc . threads } threads) [ { models_str } ]" ) 1276f"Model: { self . model_name } " ) 1277f"Grader: { self . grader . grader_type } " ) 1278f"Sampling: temp= { eval_state . sampling_config . get ( 'temperature' , 'skip' ) } , top-k= { eval_state . sampling_config . get ( 'top_k' , 'skip' ) } , top-p= { eval_state . sampling_config . get ( 'top_p' , 'skip' ) } , min-p= { eval_state . sampling_config . get ( 'min_p' , 'skip' ) } " ) 1279# Shared task queue: all workers compete for tasks 1282task_queue :Queue = Queue () 1283for i ,task_id in eval_state .tasks : 1284task_queue .put ((i ,task_id )) 1285 1286# Results queue: workers push completed TaskStates here 1287results_queue :Queue = Queue () 1288 1289# Total worker threads across all servers 1290total_threads = sum (sc .threads for sc in self .server_configs ) 1291 1292# Add one sentinel per worker so every worker exits cleanly 1293for _ in range (total_threads ): 1294task_queue .put (None ) 1295 1296# Launch workers: one ThreadPoolExecutor per server 1297executors :List [ThreadPoolExecutor ]= [] 1298worker_futures :List [Any ]= [] 1299for server_config in self .server_configs : 1300executor = ThreadPoolExecutor (max_workers = server_config .threads ) 1301executors .append (executor ) 1302for _ in range (server_config .threads ): 1303future = executor .submit ( 1304self ._worker ,server_config ,self ,eval_state , 1305task_queue ,results_queue 1306 ) 1307worker_futures .append (future ) 1308 1309# Drain results as they complete 1310n_correct = 0 1311session_time = 0.0 1312completed_count = 0 1313 1314while completed_count < total_tasks : 1315task_state = results_queue .get () 1316eval_state .processed += 1 1317completed_count += 1 1318if task_state .correct : 1319n_correct += 1 1320elapsed = time .time ()- start_time 1321eval_state .total_time += elapsed 1322session_time += elapsed 1323start_time = time .time () 1324eval_state .print_progress (task_state ,total_tasks ,n_correct ) 1325 1326if verbose : 1327f"\nCase { eval_state . processed } : { task_state . correct } " ) 1328f" Expected: { task_state . expected } " ) 1329if task_state .response : 1330f" Response: { task_state . response } " ) 1331if task_state .answer : 1332f" Answer: { task_state . answer } " ) 1333f" Status: { task_state . status } " ) 1334 1335# Wait for all workers to finish and shut down executors 1336for future in worker_futures : 1337future .result () 1338for executor in executors : 1339executor .shutdown (wait = True ) 1340 1341f"\nSession time: { session_time :.1f } s | Total accumulated time: { eval_state . total_time :.1f } s" ) 1342eval_state .print_summary () 1343eval_state .dump () 1344 1345def main (): 1346parser = argparse .ArgumentParser ( 1347description = "Simplified evaluation tool for llama.cpp" 1348 ) 1349parser .add_argument ( 1350"--server" , 1351type = str , 1352default = "http://localhost:8033" , 1353help = "Comma-separated llama-server URLs (default: http://localhost:8033)" 1354 ) 1355parser .add_argument ( 1356"--server-name" , 1357type = str , 1358default = "" , 1359help = "Comma-separated display names for servers (default: use URLs)" 1360 ) 1361parser .add_argument ( 1362"--dataset" , 1363type = str , 1364default = "aime" , 1365choices = ["aime" ,"aime2025" ,"aime2026" ,"gsm8k" ,"gpqa" ], 1366help = "Dataset type (default: aime)" 1367 ) 1368parser .add_argument ( 1369"--n_cases" , 1370type = int , 1371default = None , 1372help = "Number of cases to evaluate (default: all)" 1373 ) 1374parser .add_argument ( 1375"--seed" , 1376type = int , 1377default = 1234 , 1378help = "Random seed for shuffling (default: 1234)" 1379 ) 1380parser .add_argument ( 1381"--n_predict" , 1382type = int , 1383default =- 1 , 1384help = "Max tokens to predict per prompt (default: -1, infinite)" 1385 ) 1386parser .add_argument ( 1387"--temperature" , 1388type = float , 1389default = None , 1390help = "Sampling temperature (default: not passed)" 1391 ) 1392parser .add_argument ( 1393"--top-k" , 1394type = int , 1395default = None , 1396help = "Top K sampling (default: not passed)" 1397 ) 1398parser .add_argument ( 1399"--top-p" , 1400type = float , 1401default = None , 1402help = "Top P sampling (default: not passed)" 1403 ) 1404parser .add_argument ( 1405"--min-p" , 1406type = float , 1407default = None , 1408help = "Min P sampling (default: not passed)" 1409 ) 1410parser .add_argument ( 1411"--threads" , 1412type = str , 1413default = "32" , 1414help = "Comma-separated thread counts per server (default: 32)" 1415 ) 1416parser .add_argument ( 1417"--model" , 1418type = str , 1419default = None , 1420help = "Model name to append as query parameter (e.g., gpt-oss-20b-hf)" 1421 ) 1422parser .add_argument ( 1423"--verbose" , 1424action = "store_true" , 1425help = "Show detailed output for each case" 1426 ) 1427parser .add_argument ( 1428"--output" , 1429type = Path , 1430default = Path ("llama-eval-state.json" ), 1431help = "Output file for eval state (default: llama-eval-state.json)" 1432 ) 1433parser .add_argument ( 1434"--grader-type" , 1435type = str , 1436default = "llm" , 1437choices = ["regex" ,"cli" ,"llm" ], 1438help = "Grader type: regex, cli, or llm (default: llm)" 1439 ) 1440parser .add_argument ( 1441"--grader-script" , 1442type = str , 1443default = None , 1444help = "CLI grader script path (required for --grader-type cli)" 1445 ) 1446parser .add_argument ( 1447"--grader-server" , 1448type = str , 1449default = "" , 1450help = "Server URL for LLM grader (default: same as main server)" 1451 ) 1452parser .add_argument ( 1453"--grader-model" , 1454type = str , 1455default = "" , 1456help = "Model name for LLM grader (default: same as main model)" 1457 ) 1458parser .add_argument ( 1459"--resume" , 1460action = "store_true" , 1461help = "Resume from existing eval state" 1462 ) 1463 1464args = parser .parse_args () 1465 1466# Parse server URLs and thread counts 1467server_urls = [u .strip ()for u in args .server .split ("," )if u .strip ()] 1468thread_counts = [int (t .strip ())for t in args .threads .split ("," )if t .strip ()] 1469 1470if len (server_urls )!= len (thread_counts ): 1471f"Error: --server ( { len ( server_urls ) } URLs) and --threads ( { len ( thread_counts ) } values) must have the same count" ) 1472sys .exit (1 ) 1473 1474# Parse server names (optional, defaults to URLs) 1475if args .server_name : 1476server_names = [n .strip ()for n in args .server_name .split ("," )if n .strip ()] 1477if len (server_names )!= len (server_urls ): 1478f"Error: --server-name ( { len ( server_names ) } names) and --server ( { len ( server_urls ) } URLs) must have the same count" ) 1479sys .exit (1 ) 1480else : 1481server_names = server_urls # fallback to URLs 1482 1483server_configs = [ 1484ServerConfig (url = url ,threads = threads ,name = name ) 1485for url ,threads ,name in zip (server_urls ,thread_counts ,server_names ) 1486 ] 1487 1488if args .dataset == "gpqa" and args .grader_type != "llm" : 1489"Error: GPQA dataset requires --grader-type llm" ) 1490parser .print_help () 1491sys .exit (1 ) 1492 1493if args .output .exists (): 1494f"Loading existing eval state from { args . output } " ) 1495eval_state = EvalState .load (args .output ) 1496 1497# Verify model matches 1498if eval_state .model_name is not None and args .model != eval_state .model_name : 1499f"Error: Model mismatch. State has ' { eval_state . model_name } ', but --model is ' { args . model } '" ) 1500sys .exit (1 ) 1501 1502eval_state .print_all_tasks () 1503eval_state .print_existing_summary () 1504 1505if eval_state .is_complete (): 1506return 1507 1508if not args .resume : 1511f"Evaluation incomplete. Run with --resume to continue." ) 1512return 1513 1514pending_tasks = eval_state .get_pending_tasks () 1515f"Resuming from { len ( pending_tasks ) } pending tasks" ) 1516 1517existing_cases = eval_state .task_states .get ("cases" , {}) 1518 1519eval_state .tasks = pending_tasks 1520eval_state .task_states ["cases" ]= existing_cases 1521 1522grader_server_url = args .grader_server if args .grader_server else server_configs [0 ].url 1523grader_model_name = args .grader_model if args .grader_model else args .model 1524if args .grader_type == "llm" and not grader_model_name : 1525"Error: --grader-type llm requires --grader-model or --model" ) 1526sys .exit (1 ) 1527grader = Grader ( 1528grader_type = args .grader_type , 1529grader_script = args .grader_script , 1530grader_model_name = grader_model_name , 1531grader_server_url = grader_server_url , 1532dataset_type = eval_state .dataset_type 1533 ) 1534resume = True 1535else : 1536if args .resume : 1537"Error: No existing eval state found to resume" ) 1538sys .exit (1 ) 1539 1540grader_server_url = args .grader_server if args .grader_server else server_configs [0 ].url 1541grader_model_name = args .grader_model if args .grader_model else args .model 1542if args .grader_type == "llm" and not grader_model_name : 1543"Error: --grader-type llm requires --grader-model or --model" ) 1544sys .exit (1 ) 1545 1546grader = Grader ( 1547grader_type = args .grader_type , 1548grader_script = args .grader_script , 1549grader_model_name = grader_model_name , 1550grader_server_url = grader_server_url , 1551dataset_type = args .dataset 1552 ) 1553 1554if args .grader_type == "llm" and not args .grader_server : 1555"Warning: Using same server for LLM grader (no --grader-server specified)" ) 1556 1557sampling_config = {} 1558if args .temperature is not None : 1559sampling_config ["temperature" ]= args .temperature 1560if args .top_k is not None : 1561sampling_config ["top_k" ]= args .top_k 1562if args .top_p is not None : 1563sampling_config ["top_p" ]= args .top_p 1564if args .min_p is not None : 1565sampling_config ["min_p" ]= args .min_p 1566 1567eval_state = EvalState ( 1568dataset_type = args .dataset , 1569sampling_config = sampling_config , 1570output_file = args .output , 1571model_name = args .model 1572 ) 1573eval_state .load_dataset (seed = args .seed ) 1574eval_state .setup_tasks (n_cases = args .n_cases ,seed = args .seed ) 1575eval_state .dump () 1576resume = False 1577 1578eval_state .print_all_tasks () 1579 1580processor = Processor ( 1581server_configs = server_configs , 1582grader = grader , 1583model_name = args .model , 1584n_predict = args .n_predict 1585 ) 1586 1587processor .evaluate (eval_state ,verbose = args .verbose ,resume = resume ) 1588f"\nEval state dumped to { args . output } " ) 1589 1590if __name__ == "__main__" : 1591main ()