char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
c747294b2
char/aliceai-t5-moe
1import argparse 2import requests 3import json 4from pathlib import Path 5import logging 6 7logger = logging .getLogger ("compare-logprobs" ) 8logging .basicConfig (level = logging .INFO ) 9 10 11DESCRIPTION = """ 12Compare logits between llama.cpp and another inference engine using OpenAI-compatible server endpoints. 13 14Unlike compare-logits.py, it allows dumping logits from a hosted API endpoint. Useful when it's not possible to run both models locally. 15 16Example usage: 17Step 1: Dump logits from two different servers 18python scripts/compare-logprobs.py dump logits_llama.log http://localhost:8080/v1/completions 19python scripts/compare-logprobs.py dump logits_other.log http://other-engine:8000/v1/completions 20 21(optionally, you can add --api-key <key> if the endpoint requires authentication) 22 23Step 2: Compare the dumped logits 24python scripts/compare-logprobs.py compare logits_llama.log logits_other.log report.md 25""" 26 27 28def get_remote_corpus (url :str ,length :int )-> list [str ]: 29response = requests .get (url ) 30response .raise_for_status () 31corpus = response .text 32words = [w .strip ()for w in corpus .strip ().split (" " )] 33words = [w for w in words if "<" not in w ]# make sure nothing looks like special tokens 34words = [w for w in words if len (w )> 0 ]# filter out empty strings 35while len (words )< length : 36words += words 37return words [:length ] 38 39 40def dump_logits ( 41endpoint :str , 42output_path :Path , 43input_words :list [str ], 44pattern :list [tuple [bool ,int ]], 45api_key = None , 46): 47logger .info (f"Dumping logits to { output_path } from endpoint { endpoint } ..." ) 48words = input_words 49curr_text = "" 50n_total = sum (n for get ,n in pattern if get ) 51n_done = 0 52i_cur = 0 53i_total = len (words ) 54with output_path .open ("w" )as f : 55for get ,n in pattern : 56if not get : 57# skip n words 58for i in range (n ): 59curr_text += words .pop (0 )+ " " 60i_cur += 1 61continue 62# get n words 63for i in range (n ): 64curr_text += words .pop (0 )+ " " 65payload = { 66"prompt" :curr_text .strip (), 67"temperature" :0.0 , 68"top_k" :1 , 69"max_tokens" :1 , 70"logprobs" :1 , 71"stream" :False , 72 } 73response = requests .post ( 74endpoint , 75json = payload , 76headers = {"Authorization" :f"Bearer { api_key } " }if api_key else {}, 77 ) 78response .raise_for_status () 79data = response .json () 80data ["__index" ]= i_cur # add index for easier debugging later 81data = json .dumps (data ) 82f .write (f" { data } \n" ) 83n_done += 1 84i_cur += 1 85logger .info ( 86f"\n\n { data } \n\n[Step: { n_done } / { n_total } | Word: { i_cur } / { i_total } ]" 87 ) 88logger .info (f"Logits dumped to { output_path } " ) 89 90 91def get_token_logprobs (data :dict ): 92logprobs = data ["choices" ][0 ]["logprobs" ] 93if "content" in logprobs : 94# llama.cpp case 95top = logprobs ["content" ][0 ]["top_logprobs" ][0 ] 96return top ["token" ],top ["logprob" ] 97else : 98# vllm case 99tokens = logprobs ["tokens" ] 100token_logprobs = logprobs ["token_logprobs" ] 101return tokens [0 ],token_logprobs [0 ] 102 103 104def clean_text (text :str )-> str : 105return ( 106"'" 107+ text .replace ("\n" ,"\\n" ) 108 .replace ("\t" ,"\\t" ) 109 .replace ("\r" ,"\\r" ) 110 .replace ("|" ,"\\|" ) 111+ "'" 112 ) 113 114 115def compare_logits (input1 :Path ,input2 :Path ,output_path :Path ): 116with input1 .open ("r" )as f1 ,input2 .open ("r" )as f2 ,output_path .open ("w" )as fout : 117lines1 = f1 .readlines () 118lines2 = f2 .readlines () 119 120tab_header = [ 121"idx" , 122input1 .name , 123"logprob_1" , 124input2 .name , 125"logprob_2" , 126"diff (abs)" , 127 ] 128tab_entries = [] 129tab_max_widths = [len (h )for h in tab_header ] 130 131assert len (lines1 )== len ( 132lines2 133 ),"Input files must have the same number of lines." 134 135fout .write ("# Logits Comparison Report\n\n" ) 136for i , (line1 ,line2 )in enumerate (zip (lines1 ,lines2 )): 137if not line1 .strip ()or not line2 .strip (): 138continue # skip empty lines 139 140data1 = json .loads (line1 ) 141data2 = json .loads (line2 ) 142 143idx1 = data1 .get ("__index" ,- 1 ) 144idx2 = data2 .get ("__index" ,- 1 ) 145if idx1 != idx2 : 146logger .warning ( 147f"Warning: Mismatched indices at line { i } : { idx1 } vs { idx2 } " 148 ) 149 150token1 ,logprob1 = get_token_logprobs (data1 ) 151token2 ,logprob2 = get_token_logprobs (data2 ) 152 153token1 = clean_text (token1 ) 154token2 = clean_text (token2 ) 155abs_diff = abs (logprob1 - logprob2 ) 156 157tab_entries .append ( 158 ( 159str (idx1 + 1 ), 160token1 , 161f" { logprob1 :.4f } " , 162token2 , 163f" { logprob2 :.4f } " , 164f" { ( abs_diff ):.4f } " , 165 ) 166 ) 167 168for i in range (len (tab_entries )): 169for j in range (len (tab_header )): 170tab_max_widths [j ]= max (tab_max_widths [j ],len (tab_entries [i ][j ])) 171 172output = "" 173for j in range (len (tab_header )): 174output += f"| { tab_header [ j ]:<{ tab_max_widths [ j ]} } " 175output += "|\n" 176for j in range (len (tab_header )): 177output += f"| { '-' * ( tab_max_widths [ j ] + 2 ) } " 178output += "|\n" 179for entry in tab_entries : 180for j in range (len (tab_header )): 181output += f"| { entry [ j ]:<{ tab_max_widths [ j ]} } " 182output += "|\n" 183 184logger .info ("\n" + output ) 185fout .write (output ) 186logger .info (f"Report written to { output_path } " ) 187 188 189def parse_pattern (pattern :str )-> list [tuple [bool ,int ]]: 190parts = pattern .split ("," ) 191result = [] 192for i ,part in enumerate (parts ): 193n = int (part ) 194if i % 2 == 0 : 195result .append ((True ,n ))# get n words 196else : 197result .append ((False ,n ))# skip n words 198return result 199 200 201def parse_args ()-> argparse .Namespace : 202parser = argparse .ArgumentParser ( 203description = DESCRIPTION ,formatter_class = argparse .RawTextHelpFormatter 204 ) 205subparsers = parser .add_subparsers ( 206dest = "verb" ,required = True ,help = "action to perform" 207 ) 208 209# dump subcommand 210parser_dump = subparsers .add_parser ("dump" ,help = "dump logits from an endpoint" ) 211parser_dump .add_argument ( 212"output" ,type = Path ,help = "output path for dumped logits (.log)" 213 ) 214parser_dump .add_argument ( 215"endpoint" ,type = str ,help = "OAI-compat /completions endpoint" 216 ) 217parser_dump .add_argument ( 218"--api-key" , 219type = str , 220default = None , 221help = "API key for authentication (if required)" , 222 ) 223parser_dump .add_argument ( 224"--file" , 225type = str , 226default = "https://raw.githubusercontent.com/ggml-org/llama.cpp/eaba92c3dcc980ebe753348855d4a5d75c069997/tools/server/README.md" , 227help = "File containing prompt to use instead of the default (can also be an URL)" , 228 ) 229parser_dump .add_argument ( 230"--pattern" , 231type = str , 232default = "10,1000,10,4000,10" , 233help = "Pattern n_get,n_skip,... where n_get is number of words to get and n_skip is number of words to skip (num of words, NOT num of tokens)" , 234 ) 235 236# compare subcommand 237parser_compare = subparsers .add_parser ( 238"compare" ,help = "compare two dumped logits files" 239 ) 240parser_compare .add_argument ("input1" ,type = Path ,help = "first input file (.log)" ) 241parser_compare .add_argument ("input2" ,type = Path ,help = "second input file (.log)" ) 242parser_compare .add_argument ( 243"output" ,type = Path ,help = "output path for comparison report (.md)" 244 ) 245 246try : 247return parser .parse_args () 248except Exception as e : 249parser .print_help () 250raise e 251 252 253def main (): 254args = parse_args () 255 256if args .verb == "dump" : 257pattern = parse_pattern (args .pattern ) 258required_words = sum (n for _ ,n in pattern ) 259if args .file .startswith ("http" ): 260input_words = get_remote_corpus (args .file ,required_words ) 261logger .info (f"Fetched { len ( input_words ) } words from remote { args . file } " ) 262else : 263with open (args .file ,"r" )as f : 264input_words = f .read ().strip ().split (" " ) 265input_words = [w for w in input_words if len (w )> 0 ]# filter out empty strings 266if len (input_words )< required_words : 267raise ValueError ( 268f"Input file has only { len ( input_words ) } words, but pattern requires at least { required_words } words." 269 ) 270logger .info (f"Using { len ( input_words ) } words" ) 271dump_logits (args .endpoint ,args .output ,input_words ,pattern ,args .api_key ) 272elif args .verb == "compare" : 273compare_logits (args .input1 ,args .input2 ,args .output ) 274else : 275raise ValueError (f"Unknown verb: { args . verb } " ) 276 277 278if __name__ == "__main__" : 279main ()