char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
e3a74b299
char/aliceai-t5-moe
1from __future__import annotations 2 3import argparse 4import json 5import os 6import re 7import signal 8import socket 9import subprocess 10import sys 11import threading 12import time 13import traceback 14from contextlib import closing 15from datetime import datetime 16 17import matplotlib 18import matplotlib .dates 19import matplotlib .pyplot as plt 20import requests 21from statistics import mean 22 23 24def main (args_in :list [str ]| None = None )-> None : 25parser = argparse .ArgumentParser (description = "Start server benchmark scenario" ) 26parser .add_argument ("--name" ,type = str ,help = "Bench name" ,required = True ) 27parser .add_argument ("--runner-label" ,type = str ,help = "Runner label" ,required = True ) 28parser .add_argument ("--branch" ,type = str ,help = "Branch name" ,default = "detached" ) 29parser .add_argument ("--commit" ,type = str ,help = "Commit name" ,default = "dirty" ) 30parser .add_argument ("--host" ,type = str ,help = "Server listen host" ,default = "0.0.0.0" ) 31parser .add_argument ("--port" ,type = int ,help = "Server listen host" ,default = "8080" ) 32parser .add_argument ("--model-path-prefix" ,type = str ,help = "Prefix where to store the model files" ,default = "models" ) 33parser .add_argument ("--n-prompts" ,type = int , 34help = "SERVER_BENCH_N_PROMPTS: total prompts to randomly select in the benchmark" ,required = True ) 35parser .add_argument ("--max-prompt-tokens" ,type = int , 36help = "SERVER_BENCH_MAX_PROMPT_TOKENS: maximum prompt tokens to filter out in the dataset" , 37required = True ) 38parser .add_argument ("--max-tokens" ,type = int , 39help = "SERVER_BENCH_MAX_CONTEXT: maximum context size of the completions request to filter out in the dataset: prompt + predicted tokens" , 40required = True ) 41parser .add_argument ("--hf-repo" ,type = str ,help = "Hugging Face model repository" ,required = True ) 42parser .add_argument ("--hf-file" ,type = str ,help = "Hugging Face model file" ,required = True ) 43parser .add_argument ("--offline" ,action = "store_true" ,default = False ,help = "Offline mode: forces use of cache, prevents network access" ) 44parser .add_argument ("-ngl" ,"--n-gpu-layers" ,type = int ,help = "layers to the GPU for computation" ,required = True ) 45parser .add_argument ("--ctx-size" ,type = int ,help = "Set the size of the prompt context" ,required = True ) 46parser .add_argument ("--parallel" ,type = int ,help = "Set the number of slots for process requests" ,required = True ) 47parser .add_argument ("--batch-size" ,type = int ,help = "Set the batch size for prompt processing" ,required = True ) 48parser .add_argument ("--ubatch-size" ,type = int ,help = "physical maximum batch size" ,required = True ) 49parser .add_argument ("--scenario" ,type = str ,help = "Scenario to run" ,required = True ) 50parser .add_argument ("--duration" ,type = str ,help = "Bench scenario" ,required = True ) 51 52args = parser .parse_args (args_in ) 53 54start_time = time .time () 55 56# Start the server and performance scenario 57try : 58server_process = start_server (args ) 59except Exception : 60"bench: server start error :" ) 61traceback .print_exc (file = sys .stdout ) 62sys .exit (1 ) 63 64# start the benchmark 65iterations = 0 66data = {} 67try : 68start_benchmark (args ) 69 70with open ("results.github.env" ,'w' )as github_env : 71# parse output 72with open ('k6-results.json' ,'r' )as bench_results : 73# Load JSON data from file 74data = json .load (bench_results ) 75for metric_name in data ['metrics' ]: 76for metric_metric in data ['metrics' ][metric_name ]: 77value = data ['metrics' ][metric_name ][metric_metric ] 78if isinstance (value ,float )or isinstance (value ,int ): 79value = round (value ,2 ) 80data ['metrics' ][metric_name ][metric_metric ]= value 81github_env .write ( 82f" { escape_metric_name ( metric_name ) } _ { escape_metric_name ( metric_metric ) } = { value } \n" ) 83iterations = data ['root_group' ]['checks' ]['success completion' ]['passes' ] 84 85except Exception : 86"bench: error :" ) 87traceback .print_exc (file = sys .stdout ) 88 89# Stop the server 90if server_process : 91try : 92f"bench: shutting down server pid= { server_process . pid } ..." ) 93if os .name == 'nt' : 94interrupt = signal .CTRL_C_EVENT 95else : 96interrupt = signal .SIGINT 97server_process .send_signal (interrupt ) 98server_process .wait (0.5 ) 99 100except subprocess .TimeoutExpired : 101f"server still alive after 500ms, force-killing pid= { server_process . pid } ..." ) 102server_process .kill ()# SIGKILL 103server_process .wait () 104 105while is_server_listening (args .host ,args .port ): 106time .sleep (0.1 ) 107 108title = (f"llama.cpp { args . name } on { args . runner_label } \n " 109f"duration= { args . duration } { iterations } iterations" ) 110xlabel = (f" { args . hf_repo } / { args . hf_file } \n" 111f"parallel= { args . parallel } ctx-size= { args . ctx_size } ngl= { args . n_gpu_layers } batch-size= { args . batch_size } ubatch-size= { args . ubatch_size } pp= { args . max_prompt_tokens } pp+tg= { args . max_tokens } \n" 112f"branch= { args . branch } commit= { args . commit } " ) 113 114# Prometheus 115end_time = time .time () 116prometheus_metrics = {} 117if is_server_listening ("0.0.0.0" ,9090 ): 118metrics = ['prompt_tokens_seconds' ,'predicted_tokens_seconds' , 119'kv_cache_usage_ratio' ,'requests_processing' ,'requests_deferred' ] 120 121for metric in metrics : 122resp = requests .get (f"http://localhost:9090/api/v1/query_range" , 123params = {'query' :'llamacpp:' + metric ,'start' :start_time ,'end' :end_time ,'step' :2 }) 124 125with open (f" { metric } .json" ,'w' )as metric_json : 126metric_json .write (resp .text ) 127 128if resp .status_code != 200 : 129f"bench: unable to extract prometheus metric { metric } : { resp . text } " ) 130else : 131metric_data = resp .json () 132values = metric_data ['data' ]['result' ][0 ]['values' ] 133timestamps ,metric_values = zip (* values ) 134metric_values = [float (value )for value in metric_values ] 135prometheus_metrics [metric ]= metric_values 136timestamps_dt = [str (datetime .fromtimestamp (int (ts )))for ts in timestamps ] 137plt .figure (figsize = (16 ,10 ),dpi = 80 ) 138plt .plot (timestamps_dt ,metric_values ,label = metric ) 139plt .xticks (rotation = 0 ,fontsize = 14 ,horizontalalignment = 'center' ,alpha = .7 ) 140plt .yticks (fontsize = 12 ,alpha = .7 ) 141 142ylabel = f"llamacpp: { metric } " 143plt .title (title , 144fontsize = 14 ,wrap = True ) 145plt .grid (axis = 'both' ,alpha = .3 ) 146plt .ylabel (ylabel ,fontsize = 22 ) 147plt .xlabel (xlabel ,fontsize = 14 ,wrap = True ) 148plt .gca ().xaxis .set_major_locator (matplotlib .dates .MinuteLocator ()) 149plt .gca ().xaxis .set_major_formatter (matplotlib .dates .DateFormatter ("%Y-%m-%d %H:%M:%S" )) 150plt .gcf ().autofmt_xdate () 151 152# Remove borders 153plt .gca ().spines ["top" ].set_alpha (0.0 ) 154plt .gca ().spines ["bottom" ].set_alpha (0.3 ) 155plt .gca ().spines ["right" ].set_alpha (0.0 ) 156plt .gca ().spines ["left" ].set_alpha (0.3 ) 157 158# Save the plot as a jpg image 159plt .savefig (f' { metric } .jpg' ,dpi = 60 ) 160plt .close () 161 162# Mermaid format in case images upload failed 163with open (f" { metric } .mermaid" ,'w' )as mermaid_f : 164mermaid = ( 165f"""--- 166config: 167xyChart: 168titleFontSize: 12 169width: 900 170height: 600 171themeVariables: 172xyChart: 173titleColor: "#000000" 174--- 175xychart-beta 176title " { title } " 177y-axis "llamacpp: { metric } " 178x-axis "llamacpp: { metric } " { int ( min ( timestamps )) } --> { int ( max ( timestamps )) } 179line [ { ', ' . join ([ str ( round ( float ( value ), 2 )) for value in metric_values ]) } ] 180""" ) 181mermaid_f .write (mermaid ) 182 183# 140 chars max for commit status description 184bench_results = { 185"i" :iterations , 186"req" : { 187"p95" :round (data ['metrics' ]["http_req_duration" ]["p(95)" ],2 ), 188"avg" :round (data ['metrics' ]["http_req_duration" ]["avg" ],2 ), 189 }, 190"pp" : { 191"p95" :round (data ['metrics' ]["llamacpp_prompt_processing_second" ]["p(95)" ],2 ), 192"avg" :round (data ['metrics' ]["llamacpp_prompt_processing_second" ]["avg" ],2 ), 193"0" :round (mean (prometheus_metrics ['prompt_tokens_seconds' ]),2 )if 'prompt_tokens_seconds' in prometheus_metrics else 0 , 194 }, 195"tg" : { 196"p95" :round (data ['metrics' ]["llamacpp_tokens_second" ]["p(95)" ],2 ), 197"avg" :round (data ['metrics' ]["llamacpp_tokens_second" ]["avg" ],2 ), 198"0" :round (mean (prometheus_metrics ['predicted_tokens_seconds' ]),2 )if 'predicted_tokens_seconds' in prometheus_metrics else 0 , 199 }, 200 } 201with open ("results.github.env" ,'a' )as github_env : 202github_env .write (f"BENCH_RESULTS= { json . dumps ( bench_results , indent = None , separators = ( ',' , ':' ) ) } \n" ) 203github_env .write (f"BENCH_ITERATIONS= { iterations } \n" ) 204 205title = title .replace ('\n' ,' ' ) 206xlabel = xlabel .replace ('\n' ,' ' ) 207github_env .write (f"BENCH_GRAPH_TITLE= { title } \n" ) 208github_env .write (f"BENCH_GRAPH_XLABEL= { xlabel } \n" ) 209 210 211def start_benchmark (args ): 212k6_path = './k6' 213if 'BENCH_K6_BIN_PATH' in os .environ : 214k6_path = os .environ ['BENCH_K6_BIN_PATH' ] 215k6_args = [ 216'run' ,args .scenario , 217'--no-color' , 218'--no-connection-reuse' , 219'--no-vu-connection-reuse' , 220 ] 221k6_args .extend (['--duration' ,args .duration ]) 222k6_args .extend (['--iterations' ,args .n_prompts ]) 223k6_args .extend (['--vus' ,args .parallel ]) 224k6_args .extend (['--summary-export' ,'k6-results.json' ]) 225k6_args .extend (['--out' ,'csv=k6-results.csv' ]) 226args = f"SERVER_BENCH_N_PROMPTS= { args . n_prompts } SERVER_BENCH_MAX_PROMPT_TOKENS= { args . max_prompt_tokens } SERVER_BENCH_MAX_CONTEXT= { args . max_tokens } " 227args = args + ' ' .join ([str (arg )for arg in [k6_path ,* k6_args ]]) 228f"bench: starting k6 with: { args } " ) 229k6_completed = subprocess .run (args ,shell = True ,stdout = sys .stdout ,stderr = sys .stderr ) 230if k6_completed .returncode != 0 : 231raise Exception ("bench: unable to run k6" ) 232 233 234def start_server (args ): 235server_process = start_server_background (args ) 236 237attempts = 0 238max_attempts = 600 239if 'GITHUB_ACTIONS' in os .environ : 240max_attempts *= 2 241 242while not is_server_listening (args .host ,args .port ): 243attempts += 1 244if attempts > max_attempts : 245assert False ,"server not started" 246f"bench: waiting for server to start ..." ) 247time .sleep (0.5 ) 248 249attempts = 0 250while not is_server_ready (args .host ,args .port ): 251attempts += 1 252if attempts > max_attempts : 253assert False ,"server not ready" 254f"bench: waiting for server to be ready ..." ) 255time .sleep (0.5 ) 256 257"bench: server started and ready." ) 258return server_process 259 260 261def start_server_background (args ): 262# Start the server 263server_path = '../../../build/bin/llama-server' 264if 'LLAMA_SERVER_BIN_PATH' in os .environ : 265server_path = os .environ ['LLAMA_SERVER_BIN_PATH' ] 266server_args = [ 267'--host' ,args .host , 268'--port' ,args .port , 269 ] 270server_args .extend (['--hf-repo' ,args .hf_repo ]) 271server_args .extend (['--hf-file' ,args .hf_file ]) 272if args .offline : 273server_args .append ('--offline' ) 274server_args .extend (['--n-gpu-layers' ,args .n_gpu_layers ]) 275server_args .extend (['--ctx-size' ,args .ctx_size ]) 276server_args .extend (['--parallel' ,args .parallel ]) 277server_args .extend (['--batch-size' ,args .batch_size ]) 278server_args .extend (['--ubatch-size' ,args .ubatch_size ]) 279server_args .extend (['--n-predict' ,args .max_tokens * 2 ]) 280server_args .append ('--cont-batching' ) 281server_args .append ('--metrics' ) 282server_args .append ('--flash-attn' ) 283args = [str (arg )for arg in [server_path ,* server_args ]] 284f"bench: starting server with: { ' ' . join ( args ) } " ) 285pkwargs = { 286'stdout' :subprocess .PIPE , 287'stderr' :subprocess .PIPE 288 } 289server_process = subprocess .Popen ( 290args , 291** pkwargs )# pyright: ignore[reportArgumentType, reportCallIssue] # ty: ignore[no-matching-overload] 292 293def server_log (in_stream ,out_stream ): 294for line in iter (in_stream .readline ,b'' ): 295line .decode ('utf-8' ),end = '' ,file = out_stream ) 296 297thread_stdout = threading .Thread (target = server_log ,args = (server_process .stdout ,sys .stdout )) 298thread_stdout .start () 299thread_stderr = threading .Thread (target = server_log ,args = (server_process .stderr ,sys .stderr )) 300thread_stderr .start () 301 302return server_process 303 304 305def is_server_listening (server_fqdn ,server_port ): 306with closing (socket .socket (socket .AF_INET ,socket .SOCK_STREAM ))as sock : 307result = sock .connect_ex ((server_fqdn ,server_port )) 308_is_server_listening = result == 0 309if _is_server_listening : 310f"server is listening on { server_fqdn } : { server_port } ..." ) 311return _is_server_listening 312 313 314def is_server_ready (server_fqdn ,server_port ): 315url = f"http:// { server_fqdn } : { server_port } /health" 316response = requests .get (url ) 317return response .status_code == 200 318 319 320def escape_metric_name (metric_name ): 321return re .sub ('[^A-Z0-9]' ,'_' ,metric_name .upper ()) 322 323 324if __name__ == '__main__' : 325main ()