char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
689a9a470
char/aliceai-t5-moe
1#!/usr/bin/env python3 2from __future__import annotations 3 4import argparse 5import concurrent .futures 6import json 7import statistics 8import sys 9import time 10from dataclasses import asdict ,dataclass 11from typing import Any 12from urllib .parse import urlparse 13 14import requests 15from datasets import get_dataset_config_names ,load_dataset 16from tqdm import tqdm 17 18 19DATASET_REPO = "nvidia/SPEED-Bench" 20 21@dataclass 22class Sample : 23id :str 24category :str 25turns :list [str ] 26 27 28@dataclass 29class RequestResult : 30id :str 31category :str 32ok :bool 33turns :int 34latency_s :float 35prompt_tokens :int 36completion_tokens :int 37total_tokens :int 38finish_reason :str | None 39draft_n :int 40draft_n_accepted :int 41prompt_ms :float | None 42predicted_ms :float | None 43prompt_per_second :float | None 44predicted_per_second :float | None 45error :str | None 46 47 48def normalize_base_url (url :str )-> str : 49url = url .strip ().rstrip ("/" ) 50if not url : 51raise ValueError ("--url cannot be empty" ) 52if "://" not in url : 53url = "http://" + url 54parsed = urlparse (url ) 55if not parsed .scheme or not parsed .netloc : 56raise ValueError (f"invalid --url: { url } " ) 57if not parsed .path .rstrip ("/" ).endswith ("/v1" ): 58url = url + "/v1" 59return url .rstrip ("/" ) 60 61 62def parse_extra_inputs (value :str )-> dict [str ,Any ]: 63extra = json .loads (value ) 64if not isinstance (extra ,dict ): 65raise ValueError ("--extra-inputs must be a JSON object" ) 66return extra 67 68 69def extract_turns (row :dict [str ,Any ])-> list [str ]: 70turns = row .get ("turns" ) 71if isinstance (turns ,list )and turns : 72clean_turns = [str (turn ).strip ()for turn in turns if turn and str (turn ).strip ()] 73if clean_turns : 74return clean_turns 75raise ValueError ("missing or empty turns" ) 76 77 78def load_samples (args :argparse .Namespace )-> list [Sample ]: 79bench_names = get_dataset_config_names (DATASET_REPO ) 80if args .bench not in bench_names : 81raise ValueError ( 82f"unknown --bench { args . bench !r } ; available benches: { ', ' . join ( bench_names ) } " 83 ) 84 85dataset = load_dataset (DATASET_REPO ,name = args .bench ,split = "test" ) 86categories = list (dict .fromkeys (str (category )for category in dataset ["category" ])) 87requested_categories = None 88if args .category != "all" : 89requested_list = [category .strip ()for category in args .category .split ("," )if category .strip ()] 90if not requested_list : 91raise ValueError ( 92f"--category must be 'all' or a comma-separated list; available categories: { ', ' . join ( categories ) } " 93 ) 94requested_categories = set (requested_list ) 95unknown_categories = [category for category in requested_list if category not in categories ] 96if unknown_categories : 97unknown = ", " .join (unknown_categories ) 98raise ValueError ( 99f"unknown --category { unknown !r } for bench { args . bench !r } ; " 100f"available categories: all, { ', ' . join ( categories ) } " 101 ) 102 103samples :list [Sample ]= [] 104samples_per_category :dict [str ,int ]= {} 105skipped = 0 106for index ,row_raw in enumerate (dataset ): 107row = dict (row_raw ) 108category_raw = row .get ("category" ) 109if not isinstance (category_raw ,str )or not category_raw .strip (): 110skipped += 1 111continue 112category = category_raw .strip () 113if requested_categories is not None and category not in requested_categories : 114continue 115if args .limit is not None and samples_per_category .get (category ,0 )>= args .limit : 116continue 117 118try : 119turns = extract_turns (row ) 120except ValueError : 121skipped += 1 122continue 123question_id = row .get ("question_id" ) 124if not isinstance (question_id ,str )or not question_id .strip (): 125skipped += 1 126continue 127sample_id = question_id .strip () 128samples .append (Sample (id = sample_id ,category = category ,turns = turns )) 129samples_per_category [category ]= samples_per_category .get (category ,0 )+ 1 130 131if not samples : 132raise RuntimeError (f"no samples selected from bench= { args . bench } category= { args . category } " ) 133 134if skipped : 135f"speed_bench: skipped { skipped } rows without usable turns" ) 136return samples 137 138 139def parse_completion_response (data :dict [str ,Any ])-> tuple [dict [str ,Any ],dict [str ,Any ],str | None ,str ]: 140usage = data .get ("usage" )or {} 141timings = data .get ("timings" )or {} 142finish_reason = None 143content = "" 144choices = data .get ("choices" ) 145if isinstance (choices ,list )and choices and isinstance (choices [0 ],dict ): 146choice = choices [0 ] 147finish_reason = choice .get ("finish_reason" ) 148message = choice .get ("message" ) 149if isinstance (message ,dict )and isinstance (message .get ("content" ),str ): 150content = message ["content" ] 151elif isinstance (choice .get ("text" ),str ): 152content = choice ["text" ] 153return usage ,timings ,finish_reason ,content 154 155 156def run_request ( 157endpoint :str , 158model :str | None , 159messages :list [dict [str ,str ]], 160osl :int , 161extra_inputs :dict [str ,Any ], 162timeout :float , 163)-> tuple [dict [str ,Any ],float ]: 164payload :dict [str ,Any ]= { 165"messages" :messages , 166"max_tokens" :osl , 167"stream" :False , 168 } 169if model : 170payload ["model" ]= model 171payload .update (extra_inputs ) 172payload ["max_tokens" ]= osl 173 174start = time .perf_counter () 175response = requests .post (endpoint ,json = payload ,timeout = timeout ) 176latency_s = time .perf_counter ()- start 177if response .status_code != 200 : 178body = response .text [:500 ].replace ("\n" ,"\\n" ) 179raise RuntimeError (f"HTTP { response . status_code } : { body } " ) 180return response .json (),latency_s 181 182 183def run_one ( 184sample :Sample , 185endpoint :str , 186model :str | None , 187osl :int , 188extra_inputs :dict [str ,Any ], 189timeout :float , 190)-> RequestResult : 191selected_turns = sample .turns 192messages :list [dict [str ,str ]]= [] 193total_latency_s = 0.0 194prompt_tokens = 0 195completion_tokens = 0 196total_tokens = 0 197draft_n = 0 198draft_n_accepted = 0 199prompt_ms = 0.0 200predicted_ms = 0.0 201prompt_per_second = None 202predicted_per_second = None 203finish_reason :str | None = None 204try : 205for turn in selected_turns : 206messages .append ({"role" :"user" ,"content" :turn }) 207data ,latency_s = run_request (endpoint ,model ,messages ,osl ,extra_inputs ,timeout ) 208total_latency_s += latency_s 209usage ,timings ,finish_reason ,assistant_text = parse_completion_response (data ) 210 211turn_prompt_tokens = int (usage .get ("prompt_tokens" )or timings .get ("prompt_n" )or 0 ) 212turn_completion_tokens_count = int (usage .get ("completion_tokens" )or timings .get ("predicted_n" )or 0 ) 213turn_total_tokens_count = int (usage .get ("total_tokens" )or (turn_prompt_tokens + turn_completion_tokens_count )) 214prompt_tokens += turn_prompt_tokens 215completion_tokens += turn_completion_tokens_count 216total_tokens += turn_total_tokens_count 217draft_n += int (timings .get ("draft_n" )or 0 ) 218draft_n_accepted += int (timings .get ("draft_n_accepted" )or 0 ) 219prompt_ms += float (timings .get ("prompt_ms" )or 0 ) 220predicted_ms += float (timings .get ("predicted_ms" )or 0 ) 221if len (selected_turns )== 1 and isinstance (timings .get ("prompt_per_second" ), (int ,float )): 222prompt_per_second = float (timings ["prompt_per_second" ]) 223if len (selected_turns )== 1 and isinstance (timings .get ("predicted_per_second" ), (int ,float )): 224predicted_per_second = float (timings ["predicted_per_second" ]) 225 226messages .append ({"role" :"assistant" ,"content" :assistant_text }) 227 228if total_tokens == 0 : 229total_tokens = prompt_tokens + completion_tokens 230if len (selected_turns )> 1 : 231prompt_per_second = (prompt_tokens / (prompt_ms / 1000 ))if prompt_ms > 0 else None 232predicted_per_second = (completion_tokens / (predicted_ms / 1000 ))if predicted_ms > 0 else None 233 234return RequestResult ( 235id = sample .id , 236category = sample .category , 237ok = True , 238turns = len (selected_turns ), 239latency_s = total_latency_s , 240prompt_tokens = prompt_tokens , 241completion_tokens = completion_tokens , 242total_tokens = total_tokens , 243finish_reason = finish_reason , 244draft_n = draft_n , 245draft_n_accepted = draft_n_accepted , 246prompt_ms = prompt_ms if prompt_ms > 0 else None , 247predicted_ms = predicted_ms if predicted_ms > 0 else None , 248prompt_per_second = prompt_per_second , 249predicted_per_second = predicted_per_second , 250error = None , 251 ) 252except Exception as exc : 253return RequestResult ( 254id = sample .id , 255category = sample .category , 256ok = False , 257turns = len (selected_turns ), 258latency_s = total_latency_s , 259prompt_tokens = 0 , 260completion_tokens = 0 , 261total_tokens = 0 , 262finish_reason = None , 263draft_n = 0 , 264draft_n_accepted = 0 , 265prompt_ms = None , 266predicted_ms = None , 267prompt_per_second = None , 268predicted_per_second = None , 269error = str (exc ), 270 ) 271 272 273def summarize_group (category :str ,results :list [RequestResult ])-> dict [str ,Any ]: 274ok_results = [result for result in results if result .ok ] 275latencies = [result .latency_s for result in ok_results ] 276server_prompt_speeds = [ 277result .prompt_per_second 278for result in ok_results 279if result .prompt_per_second is not None 280 ] 281server_completion_speeds = [ 282result .predicted_per_second 283for result in ok_results 284if result .predicted_per_second is not None 285 ] 286turns = sum (result .turns for result in ok_results ) 287draft_n = sum (result .draft_n for result in ok_results ) 288accepted = sum (result .draft_n_accepted for result in ok_results ) 289 290return { 291"category" :category , 292"requests" :len (ok_results ), 293"turns" :turns , 294"failed" :len (results )- len (ok_results ), 295"avg_prompt_t_s" :statistics .mean (server_prompt_speeds )if server_prompt_speeds else None , 296"avg_pred_t_s" :statistics .mean (server_completion_speeds )if server_completion_speeds else None , 297"avg_latency" :statistics .mean (latencies )if latencies else None , 298"draft_n" :draft_n , 299"accepted" :accepted , 300"accept_rate" : (accepted / draft_n )if draft_n > 0 else None , 301 } 302 303 304def fmt_value (value :Any ,kind :str = "" )-> str : 305if value is None : 306return "n/a" 307if kind == "int" : 308return str (int (value )) 309if kind == "rate" : 310return f" { float ( value ):.4f } " 311if kind == "seconds" : 312return f" { float ( value ):.3f } s" 313if kind == "speed" : 314return f" { float ( value ):.2f } " 315if kind == "speedup" : 316return f" { float ( value ):.2f } x" 317return str (value ) 318 319 320def print_table (rows :list [dict [str ,Any ]])-> None : 321columns = [ 322 ("category" ,"category" ,"" ), 323 ("samples" ,"requests" ,"int" ), 324 ("avg_prompt_t/s" ,"avg_prompt_t_s" ,"speed" ), 325 ("avg_pred_t/s" ,"avg_pred_t_s" ,"speed" ), 326 ("avg_latency" ,"avg_latency" ,"seconds" ), 327 ("accept_rate" ,"accept_rate" ,"rate" ), 328 ] 329print_rows (rows ,columns ) 330 331 332def print_rows (rows :list [dict [str ,Any ]],columns :list [tuple [str ,str ,str ]])-> None : 333rendered_rows = [] 334for row in rows : 335rendered_rows .append ([fmt_value (row .get (key ),kind )for _ ,key ,kind in columns ]) 336 337widths = [len (header )for header ,_ ,_ in columns ] 338for rendered in rendered_rows : 339for i ,cell in enumerate (rendered ): 340widths [i ]= max (widths [i ],len (cell )) 341 342header = " " .join (header .ljust (widths [i ])for i , (header ,_ ,_ )in enumerate (columns )) 343header ) 344" " .join ("-" * width for width in widths )) 345for rendered in rendered_rows : 346" " .join (cell .ljust (widths [i ])for i ,cell in enumerate (rendered ))) 347 348 349def save_output (path :str ,args :argparse .Namespace ,samples :list [Sample ],results :list [RequestResult ],summary :list [dict [str ,Any ]])-> None : 350payload = { 351"config" : { 352"url" :args .url , 353"model" :args .model , 354"bench" :args .bench , 355"category" :args .category , 356"osl" :args .osl , 357"concurrency" :args .concurrency , 358"extra_inputs" :args .extra_inputs , 359 }, 360"selected_samples" :len (samples ), 361"completed_samples" :sum (1 for result in results if result .ok ), 362"failed_samples" :sum (1 for result in results if not result .ok ), 363"summary" :summary , 364"results" : [asdict (result )for result in results ], 365 } 366with open (path ,"w" ,encoding = "utf-8" )as f : 367json .dump (payload ,f ,indent = 2 ,sort_keys = True ) 368 369 370def main (argv :list [str ]| None = None )-> int : 371parser = argparse .ArgumentParser (description = "Run SPEED-Bench against an OpenAI-compatible llama-server." ) 372parser .add_argument ("--url" ,default = "localhost:8080" ,help = "Server URL, for example localhost:8080 or http://localhost:8080/v1" ) 373parser .add_argument ("--model" ,default = None ,help = "Optional model name to send in OpenAI requests" ) 374parser .add_argument ("--bench" ,default = "qualitative" ,help = "SPEED-Bench config to run, for example qualitative or throughput_1k" ) 375parser .add_argument ("--category" ,default = "all" ,help = "Category to run within the selected bench; use all for no category filter" ) 376parser .add_argument ("--osl" ,type = int ,default = 4096 ,help = "Output sequence length, mapped to max_tokens" ) 377parser .add_argument ("--extra-inputs" ,default = '{"temperature":0}' ,help = "Extra request fields as a JSON object" ) 378parser .add_argument ("--concurrency" ,type = int ,default = 1 ,help = "Concurrent client requests; usually match llama-server --np" ) 379parser .add_argument ("--limit" ,type = int ,default = None ,help = "Optional sample limit per category for smoke tests" ) 380parser .add_argument ("--timeout" ,type = float ,default = 600 ,help = "Per-request timeout in seconds" ) 381parser .add_argument ("--output" ,default = None ,help = "Optional path to save raw results JSON" ) 382args = parser .parse_args (argv ) 383try : 384base_url = normalize_base_url (args .url ) 385endpoint = base_url + "/chat/completions" 386extra_inputs = parse_extra_inputs (args .extra_inputs ) 387args .extra_inputs = extra_inputs 388samples = load_samples (args ) 389except Exception as exc : 390f"speed_bench: setup failed: { exc } " ,file = sys .stderr ) 391return 2 392 393f"speed_bench: loaded { len ( samples ) } samples from bench= { args . bench } category= { args . category } " ) 394 395results :list [RequestResult ]= [] 396started = time .perf_counter () 397with concurrent .futures .ThreadPoolExecutor (max_workers = args .concurrency )as executor : 398futures = [ 399executor .submit (run_one ,sample ,endpoint ,args .model ,args .osl ,extra_inputs ,args .timeout ) 400for sample in samples 401 ] 402for future in tqdm (concurrent .futures .as_completed (futures ),total = len (futures ),desc = "speed_bench" ,unit = "sample" ): 403result = future .result () 404results .append (result ) 405 406elapsed = time .perf_counter ()- started 407categories = list (dict .fromkeys (sample .category for sample in samples )) 408summary = [ 409summarize_group (category , [result for result in results if result .category == category ]) 410for category in categories 411 ] 412summary .append (summarize_group ("overall" ,results )) 413f"Summary (elapsed= { elapsed :.2f } s)" ) 415print_table (summary ) 416 417if args .output : 418save_output (args .output ,args ,samples ,results ,summary ) 419f"\nspeed_bench: wrote { args . output } " ) 420 421failed = sum (1 for result in results if not result .ok ) 422if failed : 423f"\nspeed_bench: { failed } samples failed" ,file = sys .stderr ) 424first_error = next ((result .error for result in results if result .error ),None ) 425if first_error : 426f"first error: { first_error } " ,file = sys .stderr ) 427return 1 428return 0 429 430 431if __name__ == "__main__" : 432raise SystemExit (main ())