char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
67672dc5b
char/aliceai-t5-moe
1#!/usr/bin/env uv run 2''' 3Simplistic tool call benchmarks for llama-server and ollama. 4 5Essentially runs the tests at server/tools/server/tests/unit/test_tool_call.py N times, at different temperatures and on different backends (current llama-server, baseline llama-server and ollama), 6and plots the results of multiple runs (from same .jsonl file or multiple ones) as a success rate heatmap. 7 8Simple usage example: 9 10cmake -B build && cmake --build build --config Release -j -t llama-server 11 12export LLAMA_SERVER_BIN_PATH=$PWD/build/bin/llama-server 13export LLAMA_CACHE=${LLAMA_CACHE:-$HOME/Library/Caches/llama.cpp} 14 15./scripts/tool_bench.py run --n 10 --temp -1 --temp 0 --temp 1 --temp 2 --temp 5 --llama-baseline $PWD/buildMaster/bin/llama-server --output qwen14b.jsonl --hf bartowski/Qwen2.5-14B-Instruct-GGUF:Q4_K_L 16./scripts/tool_bench.py run --n 30 --temp -1 --temp 0 --temp 1 --model "Qwen 2.5 1.5B Q4_K_M" --output qwen1.5b.jsonl --hf bartowski/Qwen2.5-1.5B-Instruct-GGUF --ollama qwen2.5:1.5b-instruct-q4_K_M 17./scripts/tool_bench.py run --n 30 --temp -1 --temp 0 --temp 1 --model "Qwen 2.5 Coder 7B Q4_K_M" --output qwenc7b.jsonl --hf bartowski/Qwen2.5-Coder-7B-Instruct-GGUF --ollama qwen2.5-coder:7b 18 19./scripts/tool_bench.py plot *.jsonl # Opens window w/ heatmap 20./scripts/tool_bench.py plot qwen*.jsonl --output qwen.png # Saves heatmap to qwen.png 21 22(please see ./scripts/tool_bench.sh for a more complete example) 23''' 24# /// script 25# requires-python = ">=3.10" 26# dependencies = [ 27# "pytest", 28# "pandas", 29# "matplotlib", 30# "seaborn", 31# "requests", 32# "wget", 33# "typer", 34# ] 35# /// 36from contextlib import contextmanager 37from pathlib import Path 38import re 39from statistics import mean ,median 40from typing import Annotated ,Dict ,List ,Optional ,Tuple 41import atexit 42import json 43import logging 44import matplotlib .pyplot as plt 45import numpy as np 46import pandas as pd 47import seaborn as sns 48import subprocess 49import sys 50import time 51import typer 52 53sys .path .insert (0 ,Path (__file__ ).parent .parent .as_posix ()) 54if True : 55from tools .server .tests .utils import ServerProcess # ty: ignore[unresolved-import] 56from tools .server .tests .unit .test_tool_call import do_test_calc_result ,do_test_hello_world ,do_test_weather # ty: ignore[unresolved-import] 57 58 59@contextmanager 60def scoped_server (sp :ServerProcess ): 61def stop (): 62nonlocal sp 63if sp is not None : 64sp .stop () 65sp = None # type: ignore 66atexit .register (stop ) 67yield sp 68stop () 69 70 71logging .basicConfig ( 72level = logging .INFO , 73format = '%(asctime)s - %(levelname)s - %(message)s' 74) 75logger = logging .getLogger (__name__ ) 76 77app = typer .Typer () 78 79 80@ app . command () 81def plot (files :List [Path ],output :Optional [Path ]= None ,test_regex :Optional [str ]= None ,server_regex :Optional [str ]= None ): 82 83lines :List [Dict ]= [] 84for file in files : 85if not file .exists (): 86logger .error (f"File not found: { file } " ) 87continue 88 89try : 90with file .open ()as f : 91raw_data = f .read () 92logger .info (f"Reading { file } ( { len ( raw_data ) } bytes)" ) 93 94for line_num ,line in enumerate (raw_data .split ('\n' ),1 ): 95line = line .strip () 96if not line : 97continue 98try : 99record = json .loads (line ) 100lines .append (record ) 101except json .JSONDecodeError as e : 102logger .warning (f"Invalid JSON at { file } : { line_num } - { e } " ) 103except Exception as e : 104logger .error (f"Error processing { file } : { e } " ) 105 106if not lines : 107raise Exception ("No valid data was loaded" ) 108 109data_dict :Dict [Tuple ,float ]= {} 110models :List [str ]= [] 111temps = set () 112tests = set () 113server_names = set () 114total_counts = set () 115for rec in lines : 116try : 117model = rec ["model" ] 118temp = rec ["temp" ] 119server_name = rec ["server_name" ] 120test = rec ["test" ] 121success = rec ["success_ratio" ] 122success_count = rec ["success_count" ] 123failure_count = rec ["failure_count" ] 124total_count = success_count + failure_count 125total_counts .add (total_count ) 126 127if test_regex and not re .search (test_regex ,test ): 128continue 129 130if server_regex and not re .search (server_regex ,server_name ): 131continue 132 133data_dict [(model ,temp ,server_name ,test )]= success 134 135if model not in models : 136models .append (model ) 137temps .add (temp ) 138tests .add (test ) 139server_names .add (server_name ) 140 141except KeyError as e : 142logger .warning (f"Missing required field in record: { e } " ) 143 144if len (total_counts )> 1 : 145logger .warning (f"Total counts are not consistent: { total_counts } " ) 146 147# Sort the collected values 148temps = list (sorted (temps ,key = lambda x :x if x is not None else - 1 )) 149tests = list (sorted (tests )) 150server_names = list (sorted (server_names )) 151 152logger .info (f"Processed { len ( lines ) } lines" ) 153logger .info (f"Found { len ( data_dict ) } valid data points" ) 154logger .info (f"Models: { models } " ) 155logger .info (f"Temperatures: { temps } " ) 156logger .info (f"Tests: { tests } " ) 157logger .info (f"Servers: { server_names } " ) 158 159matrix :list [list [float ]]= [] 160index :list [str ]= [] 161 162all_cols = [ 163 (server_name ,test ) 164for server_name in server_names 165for test in tests 166 ] 167for model in models : 168for temp in temps : 169index .append (f" { model } @ { temp } " ) 170row_vals = [ 171data_dict .get ((model ,temp ,server_name ,test ),np .nan ) 172for server_name ,test in all_cols 173 ] 174matrix .append (row_vals ) 175 176columns :list [str ]= [f" { server_name } \n { test } " for server_name ,test in all_cols ] 177 178df = pd .DataFrame (matrix ,index = np .array (index ),columns = np .array (columns )) 179 180plt .figure (figsize = (12 ,6 )) 181 182sns .heatmap ( 183df ,annot = True ,cmap = "RdYlGn" ,vmin = 0.0 ,vmax = 1.0 ,cbar = True ,fmt = ".2f" ,center = 0.5 ,square = True ,linewidths = 0.5 , 184cbar_kws = {"label" :"Success Ratio" }, 185 ) 186 187plt .title (f"Tool Call Bench (n = { str ( min ( total_counts )) if len ( total_counts ) == 1 else f' { min ( total_counts ) } - { max ( total_counts ) } ' } )\nSuccess Ratios by Server & Test" ,pad = 20 ) 188plt .xlabel ("Server & Test" ,labelpad = 10 ) 189plt .ylabel ("Model @ Temperature" ,labelpad = 10 ) 190 191plt .xticks (rotation = 45 ,ha = 'right' ) 192plt .yticks (rotation = 0 ) 193 194plt .tight_layout () 195 196if output : 197plt .savefig (output ,dpi = 300 ,bbox_inches = 'tight' ) 198logger .info (f"Plot saved to { output } " ) 199else : 200plt .show () 201 202 203@ app . command () 204def run ( 205output :Annotated [Path ,typer .Option (help = "Output JSON file" )], 206model :Annotated [Optional [str ],typer .Option (help = "Name of the model to test (server agnostic)" )]= None , 207hf :Annotated [Optional [str ],typer .Option (help = "GGUF huggingface model repo id (+ optional quant) to test w/ llama-server" )]= None , 208chat_template :Annotated [Optional [str ],typer .Option (help = "Chat template override for llama-server" )]= None , 209chat_template_file :Annotated [Optional [str ],typer .Option (help = "Chat template file override for llama-server" )]= None , 210ollama :Annotated [Optional [str ],typer .Option (help = "Ollama model tag to test" )]= None , 211llama_baseline :Annotated [Optional [str ],typer .Option (help = "llama-server baseline binary path to use as baseline" )]= None , 212n :Annotated [int ,typer .Option (help = "Number of times to run each test" )]= 10 , 213temp :Annotated [Optional [List [float ]],typer .Option (help = "Set of temperatures to test" )]= None , 214top_p :Annotated [Optional [float ],typer .Option (help = "top_p" )]= None , 215top_k :Annotated [Optional [int ],typer .Option (help = "top_k" )]= None , 216ctk :Annotated [Optional [str ],typer .Option (help = "ctk" )]= None , 217ctv :Annotated [Optional [str ],typer .Option (help = "ctv" )]= None , 218fa :Annotated [Optional [bool ],typer .Option (help = "fa" )]= None , 219seed :Annotated [Optional [int ],typer .Option (help = "Random seed" )]= None , 220port :Annotated [int ,typer .Option (help = "llama-server port" )]= 8084 , 221force :Annotated [bool ,typer .Option (help = "Force overwrite of output file" )]= False , 222append :Annotated [bool ,typer .Option (help = "Append to output file" )]= False , 223 224test_hello_world :Annotated [bool ,typer .Option (help = "Whether to run the hello world test" )]= True , 225test_weather :Annotated [bool ,typer .Option (help = "Whether to run the weather test" )]= True , 226test_calc_result :Annotated [bool ,typer .Option (help = "Whether to run the calc result test" )]= False , 227): 228# Check only one of output and append 229 230n_predict = 512 # High because of DeepSeek R1 231# n_ctx = 8192 232n_ctx = 2048 233 234if model is None : 235if hf is not None : 236model = hf .split ("/" )[- 1 ] 237elif ollama is not None : 238model = ollama 239 240assert force or append or not output .exists (),f"Output file already exists: { output } ; use --force to overwrite" 241 242with output .open ('a' if append else 'w' )as output_file : 243 244def run (server :ServerProcess ,* ,server_name :str ,model_id :str ,temp :Optional [float ]= None ,output_kwargs = {},request_kwargs = {}): 245request_kwargs = {** request_kwargs } 246if temp is not None : 247request_kwargs ['temperature' ]= temp 248if top_p is not None : 249request_kwargs ['top_p' ]= top_p 250if top_k is not None : 251request_kwargs ['top_k' ]= top_k 252if seed is not None : 253request_kwargs ['seed' ]= seed 254 255request_kwargs ['cache_prompt' ]= False 256 257tests = {} 258if test_hello_world : 259tests ["hello world" ]= lambda server :do_test_hello_world (server ,** request_kwargs ) 260if test_weather : 261tests ["weather" ]= lambda server :do_test_weather (server ,** request_kwargs ) 262if test_calc_result : 263tests ["calc result" ]= lambda server :do_test_calc_result (server ,None ,512 ,** request_kwargs ) 264 265for test_name ,test in tests .items (): 266success_count = 0 267failure_count = 0 268failures = [] 269success_times = [] 270failure_times = [] 271logger .info (f"Running { test_name } ( { server_name } , { model } ): " ) 272for i in range (n ): 273start_time = time .time () 274 275def elapsed (): 276return time .time ()- start_time 277 278try : 279test (server ) 280success_times .append (elapsed ()) 281success_count += 1 282logger .info ('success' ) 283except Exception as e : 284logger .error (f'failure: { e } ' ) 285failure_count += 1 286failure_times .append (elapsed ()) 287failures .append (str (e )) 288# import traceback 289# traceback.print_exc() 290output_file .write (json .dumps ({** output_kwargs ,** dict ( 291model = model , 292server_name = server_name , 293model_id = model_id , 294test = test_name , 295temp = t , 296top_p = top_p , 297top_k = top_k , 298ctk = ctk , 299ctv = ctv , 300seed = seed , 301success_ratio = float (success_count )/ n , 302avg_time = mean (success_times + failure_times ), 303median_time = median (success_times + failure_times ), 304success_count = success_count , 305success_times = success_times , 306failure_count = failure_count , 307failure_times = failure_times , 308failures = list (set (failures )), 309 )})+ '\n' ) 310output_file .flush () 311 312for t in [None ]if temp is None else [t if t >= 0 else None for t in temp ]: 313if hf is not None : 314 315servers :list [Tuple [str ,Optional [str ]]]= [('llama-server' ,None )] 316if llama_baseline is not None : 317servers .append (('llama-server (baseline)' ,llama_baseline )) 318 319for server_name ,server_path in servers : 320server = ServerProcess () 321server .n_ctx = n_ctx 322server .n_slots = 1 323server .jinja = True 324server .ctk = ctk 325server .ctv = ctv 326server .fa = "on" if fa else "off" 327server .n_predict = n_predict 328server .model_hf_repo = hf 329server .model_hf_file = None 330server .chat_template = chat_template 331server .chat_template_file = chat_template_file 332server .server_path = server_path 333if port is not None : 334server .server_port = port 335# server.debug = True 336 337with scoped_server (server ): 338server .start (timeout_seconds = 15 * 60 ) 339for ignore_chat_grammar in [False ]: 340run ( 341server , 342server_name = server_name , 343model_id = hf , 344temp = t , 345output_kwargs = dict ( 346chat_template = chat_template , 347chat_template_file = chat_template_file , 348 ), 349request_kwargs = dict ( 350ignore_chat_grammar = ignore_chat_grammar , 351 ), 352 ) 353 354if ollama is not None : 355server = ServerProcess () 356server .server_port = 11434 357server .server_host = "localhost" 358subprocess .check_call (["ollama" ,"pull" ,ollama ]) 359 360with scoped_server (server ): 361run ( 362server , 363server_name = "ollama" , 364model_id = ollama , 365temp = t , 366output_kwargs = dict ( 367chat_template = None , 368chat_template_file = None , 369 ), 370request_kwargs = dict ( 371model = ollama , 372max_tokens = n_predict , 373num_ctx = n_ctx , 374 ), 375 ) 376 377 378if __name__ == "__main__" : 379app ()