char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
d2e179a47
char/aliceai-t5-moe
1#!/usr/bin/env python3 2 3import argparse 4import json 5import random 6import re 7import time 8import sys 9import os 10import threading 11from http .server import HTTPServer ,BaseHTTPRequestHandler 12from typing import Dict ,List ,Optional 13from dataclasses import dataclass 14from pathlib import Path 15 16import datasets 17 18# Set cache directory for HuggingFace datasets 19cache_dir = Path .home ()/ ".cache" / "huggingface" / "datasets" 20cache_dir .mkdir (parents = True ,exist_ok = True ) 21os .environ ["HF_DATASETS_CACHE" ]= str (cache_dir ) 22 23def dice (s1 :str ,s2 :str )-> float : 24"""Calculate Dice coefficient between two strings based on bigram overlap.""" 25if not s1 and not s2 : 26return 1.0 27 28def _bigrams (s :str ): 29return [s [i :i + 2 ]for i in range (len (s )- 1 )] 30 31bigrams1 = _bigrams (s1 ) 32bigrams2 = _bigrams (s2 ) 33 34if not bigrams1 and not bigrams2 : 35return 1.0 36 37from collections import Counter 38 39freq1 = Counter (bigrams1 ) 40freq2 = Counter (bigrams2 ) 41 42intersection = sum (min (freq1 [bg ],freq2 [bg ])for bg in freq1 ) 43dice_coeff = 2 * intersection / (len (bigrams1 )+ len (bigrams2 )) 44return dice_coeff 45 46def debug_log (message :str ): 47"""Log debug messages to both stdout and a file""" 48message ,file = sys .stderr ) 49with open ("/tmp/simulator-debug.log" ,"a" )as f : 50f .write (message + "\n" ) 51 52simulator :Optional ["Simulator" ]= None 53 54@dataclass 55class EvalState : 56id :str 57tasks :List [str ] 58task_states :Dict [str ,Dict ] 59sampling_config :Dict 60 61def normalize_number (s :str )-> Optional [int ]: 62match = re .match (r"\d+" ,s )# match digits from the start 63if not match : 64return None 65return int (match .group (0 )) 66 67class AimeDataset : 68def __init__ (self ,split :str = "train" ,dataset_type :str = "aime" ): 69self .split = split 70self .dataset_type = dataset_type 71self .questions :List [Dict ]= [] 72self ._load_dataset () 73 74def _get_question_text (self ,question :Dict )-> str : 75"""Get question text, handling different dataset field names.""" 76return question .get ("problem" ,question .get ("question" ,"" )) 77 78def _load_dataset (self ): 79if self .dataset_type == "aime" : 80f"Loading AIME dataset (split: { self . split } )..." ) 81cache_path = Path .home ()/ ".cache" / "huggingface" / "datasets" / "AI-MO___aimo-validation-aime" / "default" / "0.0.0" 82if cache_path .exists (): 83f"Using cached dataset from { cache_path } " ) 84ds = datasets .load_dataset ("AI-MO/aimo-validation-aime" ,split = self .split ,cache_dir = str (cache_path )) 85else : 86ds = datasets .load_dataset ("AI-MO/aimo-validation-aime" ,split = self .split ) 87elif self .dataset_type == "aime2025" : 88f"Loading AIME2025 dataset..." ) 89ds_list = [] 90for config_name in ["AIME2025-I" ,"AIME2025-II" ]: 91cache_path = Path .home ()/ ".cache" / "huggingface" / "datasets" / "opencompass___AIME2025" / "default" / "0.0.0" 92if cache_path .exists (): 93f"Using cached dataset from { cache_path } " ) 94ds = datasets .load_dataset ("opencompass/AIME2025" ,config_name ,split = "test" ,cache_dir = str (cache_path )) 95else : 96ds = datasets .load_dataset ("opencompass/AIME2025" ,config_name ,split = "test" ) 97ds_list .extend (ds ) 98ds = ds_list 99else : 100raise ValueError (f"Unknown dataset type: { self . dataset_type } " ) 101 102self .questions = list (ds ) 103f" { self . dataset_type } dataset loaded: { len ( self . questions ) } questions" ) 104 105def find_question (self ,request_text :str )-> Optional [Dict ]: 106# Strip common template prefixes to get the actual question text 107# Templates include things like "Solve the following math problem step by step..." 108# The actual question usually follows a blank line or after the template instruction 109cleaned = request_text 110# Split on double newline and take the part that looks like the problem 111parts = cleaned .split ('\n\n' ) 112if len (parts )> 1 : 113# Find the part that's longest (likely the actual problem text) 114problem_parts = [p for p in parts if len (p .strip ())> 100 ] 115if problem_parts : 116cleaned = max (problem_parts ,key = lambda x :len (x )) 117 118best_match = None 119best_distance = - 1 120best_index = - 1 121 122for i ,question in enumerate (self .questions ): 123question_text = self ._get_question_text (question ) 124request_lower = cleaned .lower () 125question_lower = question_text .lower () 126 127# Check if question text is contained in the cleaned request 128if question_lower in request_lower or request_lower in question_lower : 129debug_log (f"DEBUG: Found substring match at index { i } " ) 130return question 131 132# Exact match 133if question_lower == request_lower : 134debug_log (f"DEBUG: Found exact match at index { i } " ) 135return question 136 137# Remove LaTeX formatting for more flexible matching 138question_no_latex = re .sub (r'\$[^$]+\$' ,'' ,question_text ) 139if question_no_latex .lower ()== request_lower : 140debug_log (f"DEBUG: Found match (no LaTeX) at index { i } " ) 141return question 142 143# Calculate Dice coefficient for partial matches 144# Only consider if request is at least 50% of question length 145if len (request_lower )>= len (question_lower )* 0.5 : 146distance = dice (question_lower ,request_lower ) 147 148if distance > best_distance : 149best_distance = distance 150best_match = question 151best_index = i 152 153if best_match and best_distance > 0.3 :# Threshold for partial match 154debug_log (f"DEBUG: Found best partial match at index { best_index } with distance { best_distance :.3f } " ) 155return best_match 156 157debug_log (f"DEBUG: No matching question found for cleaned: { cleaned [: 100 ] } ..." ) 158return None 159 160def get_answer (self ,question :Dict )-> str : 161answer = question ["answer" ] 162if isinstance (answer ,str ): 163normalized = normalize_number (answer ) 164return str (normalized )if normalized is not None else answer 165return str (answer ) 166 167class Simulator : 168def __init__ ( 169self , 170port :int = 8033 , 171host :str = "localhost" , 172success_rate :float = 0.8 , 173dataset_split :str = "train" , 174dataset_type :str = "aime" 175 ): 176self .port = port 177self .host = host 178self .success_rate = success_rate 179self .dataset = AimeDataset (dataset_split ,dataset_type ) 180self .eval_state = EvalState ( 181id = dataset_type , 182tasks = [dataset_type ], 183task_states = {}, 184sampling_config = {"temperature" :0 ,"max_tokens" :2048 } 185 ) 186 187def _generate_response ( 188self , 189question :Dict , 190should_be_correct :bool 191 )-> Dict : 192expected_answer = self .dataset .get_answer (question ) 193 194if should_be_correct : 195response_text = expected_answer 196else : 197response_text = self ._generate_wrong_answer (question ) 198 199comp_tokens = random .randint (10000 ,60000 ) 200tps_gen = random .uniform (90.0 ,110.0 ) 201t_gen_ms = comp_tokens / tps_gen * 1000 202 203return { 204"id" :f"chatcmpl- { int ( time . time ()) } " , 205"object" :"chat.completion" , 206"created" :int (time .time ()), 207"model" :"llama" , 208"choices" : [ 209 { 210"index" :0 , 211"message" : { 212"role" :"assistant" , 213"content" :response_text 214 }, 215"finish_reason" :"stop" 216 } 217 ], 218"usage" : { 219"prompt_tokens" :100 , 220"completion_tokens" :comp_tokens , 221"total_tokens" :100 + comp_tokens 222 }, 223"timings" : { 224"predicted_ms" :t_gen_ms , 225"predicted_per_second" :tps_gen 226 } 227 } 228 229def _generate_wrong_answer (self ,question :Dict )-> str : 230expected_answer = self .dataset .get_answer (question ) 231 232if expected_answer .isdigit (): 233wrong_answer = str (int (expected_answer )+ 1 ) 234else : 235wrong_answer = expected_answer + " (wrong)" 236 237return wrong_answer 238 239def _process_request (self ,request_data :Dict )-> Dict : 240messages = request_data .get ("messages" , []) 241if not messages : 242return {"error" :"No messages in request" } 243 244request_text = messages [0 ].get ("content" ,"" ) 245debug_log (f"DEBUG: Received request with content: { request_text [: 150 ] } ..." ) 246 247question = self .dataset .find_question (request_text ) 248if not question : 249debug_log (f"DEBUG: find_question returned None" ) 250return {"error" :"No matching question found" } 251 252should_be_correct = random .random ()< self .success_rate 253 254response = self ._generate_response (question ,should_be_correct ) 255 256task_id = "aime" 257self .eval_state .task_states [task_id ]= { 258"correct" :should_be_correct , 259"expected" :self .dataset .get_answer (question ), 260"predicted" :response ["choices" ][0 ]["message" ]["content" ] 261 } 262 263return response 264 265class RequestHandler (BaseHTTPRequestHandler ): 266def do_GET (self ): 267if self .path == "/v1/models" : 268self ._send_json ({"data" : [{"id" :"llama" ,"object" :"model" }]},200 ) 269return 270self ._send_json ({"error" :"Not found" },404 ) 271 272def do_POST (self ): 273if self .path != "/v1/chat/completions" : 274self ._send_json ({"error" :"Not found" },404 ) 275return 276 277try : 278content_length = int (self .headers .get ("Content-Length" ,0 )) 279body = self .rfile .read (content_length ) 280request_data = json .loads (body )if body else None 281 282if not request_data : 283self ._send_json ({"error" :"Invalid JSON" },400 ) 284return 285 286if simulator is None : 287self ._send_json ({"error" :"Simulator not initialized" },500 ) 288return 289 290response = simulator ._process_request (request_data ) 291self ._send_json (response ,200 ) 292 293except json .JSONDecodeError : 294self ._send_json ({"error" :"Invalid JSON" },400 ) 295except Exception as e : 296f"Error processing request: { e } " ) 297self ._send_json ({"error" :str (e )},500 ) 298 299def _send_json (self ,data :dict ,status :int = 200 ): 300body = json .dumps (data ).encode ("utf-8" ) 301self .send_response (status ) 302self .send_header ("Content-Type" ,"application/json" ) 303self .send_header ("Content-Length" ,str (len (body ))) 304self .end_headers () 305self .wfile .write (body ) 306 307def log_message (self ,format ,* args ): 308# Suppress default request logging 309pass 310 311 312def main (): 313parser = argparse .ArgumentParser ( 314description = "llama-server simulator for testing eval scripts" 315 ) 316parser .add_argument ( 317"--port" , 318type = int , 319default = 8033 , 320help = "Server port (default: 8033)" 321 ) 322parser .add_argument ( 323"--host" , 324type = str , 325default = "localhost" , 326help = "Server host (default: localhost)" 327 ) 328parser .add_argument ( 329"--success-rate" , 330type = float , 331default = 0.8 , 332help = "Success rate 0-1 (default: 0.8)" 333 ) 334parser .add_argument ( 335"--dataset" , 336type = str , 337default = "aime" , 338choices = ["aime" ,"aime2025" ], 339help = "Dataset type (default: aime)" 340 ) 341parser .add_argument ( 342"--dataset-split" , 343type = str , 344default = "train" , 345help = "AIME dataset split to use (default: train)" 346 ) 347 348args = parser .parse_args () 349 350global simulator 351simulator = Simulator ( 352port = args .port , 353host = args .host , 354success_rate = args .success_rate , 355dataset_split = args .dataset_split , 356dataset_type = args .dataset 357 ) 358 359server = HTTPServer ((args .host ,args .port ),RequestHandler ) 360server_thread = threading .Thread (target = server .serve_forever ,daemon = True ) 361server_thread .start () 362 363"\n=== llama-server-simulator ===" ) 364f"Server running on http:// { args . host } : { args . port } " ) 365f"Success rate: { args . success_rate } " ) 366f" { args . dataset } dataset loaded: { len ( simulator . dataset . questions ) } questions" ) 367"\nPress Ctrl+C to stop\n" ) 368 369try : 370server_thread .join () 371except KeyboardInterrupt : 372"\nShutting down..." ) 373server .shutdown () 374 375if __name__ == "__main__" : 376main ()