char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
decaf508b
char/aliceai-t5-moe
1import pytest 2from utils import * 3 4server = ServerPreset .tinyllama2 () 5 6 7@ pytest . fixture ( autouse = True ) 8def create_server (): 9global server 10server = ServerPreset .tinyllama2 () 11server .server_metrics = True 12 13 14def fetch_metrics (server :ServerProcess )-> str : 15"""get /metrics as raw prometheus text""" 16res = server .make_request ("GET" ,"/metrics" ) 17assert res .status_code == 200 18assert "Process-Start-Time-Unix" in res .headers 19assert isinstance (res .body ,str ) 20return res .body 21 22 23def parse_metrics (text :str )-> dict : 24"""parse the prometheus text format into {name: (type, value)}""" 25out = {} 26types = {} 27for line in text .splitlines (): 28if line .startswith ("# TYPE " ): 29_ ,_ ,name ,kind = line .split (" " ,3 ) 30types [name ]= kind 31elif line .startswith ("llamacpp:" )and "{" not in line : 32name ,value = line .split (" " ,1 ) 33assert name in types ,f" { name } has no # TYPE line" 34out [name ]= (types [name ],float (value )) 35return out 36 37 38def test_metrics_disabled (): 39global server 40server .server_metrics = False 41server .start () 42res = server .make_request ("GET" ,"/metrics" ) 43assert res .status_code == 501 # ERROR_TYPE_NOT_SUPPORTED 44 45 46def test_metrics_prometheus_format (): 47global server 48server .start () 49server .make_request ("POST" ,"/completion" ,data = {"prompt" :"I believe" ,"n_predict" :8 }) 50 51text = fetch_metrics (server ) 52metrics = parse_metrics (text ) 53 54expected_counters = [ 55"llamacpp:prompt_tokens_total" , 56"llamacpp:prompt_tokens_cached_total" , 57"llamacpp:prompt_seconds_total" , 58"llamacpp:tokens_predicted_total" , 59"llamacpp:tokens_predicted_seconds_total" , 60"llamacpp:n_decode_total" , 61"llamacpp:n_tokens_max" , 62"llamacpp:spec_decode_num_draft_tokens_total" , 63"llamacpp:spec_decode_num_accepted_tokens_total" , 64"llamacpp:spec_decode_num_drafts_total" , 65 ] 66expected_gauges = [ 67"llamacpp:prompt_tokens_seconds" , 68"llamacpp:predicted_tokens_seconds" , 69"llamacpp:requests_processing" , 70"llamacpp:requests_deferred" , 71"llamacpp:n_busy_slots_per_decode" , 72 ] 73 74for name in expected_counters : 75assert metrics [name ][0 ]== "counter" 76for name in expected_gauges : 77assert metrics [name ][0 ]== "gauge" 78 79# every metric must carry a help line 80for name in expected_counters + expected_gauges : 81assert f"# HELP { name } " in text 82 83assert metrics ["llamacpp:n_decode_total" ][1 ]> 0 84assert metrics ["llamacpp:requests_processing" ][1 ]== 0 85 86 87def test_metrics_prompt_processed_and_cached (): 88global server 89server .n_slots = 1 # keep the prompt cache on a single slot 90server .start () 91 92prompt = "the quick brown fox jumps over the lazy dog" 93 94n_processed = 0 95n_cached = 0 96for _ in range (2 ): 97res = server .make_request ("POST" ,"/completion" ,data = {"prompt" :prompt ,"n_predict" :4 }) 98assert res .status_code == 200 99n_processed += res .body ["timings" ]["prompt_n" ] 100n_cached += res .body ["timings" ]["cache_n" ] 101 102# the second request must reuse the prompt of the first one 103assert n_cached > 0 104 105metrics = parse_metrics (fetch_metrics (server )) 106 107# cached tokens are counted apart, they cost no decode 108assert metrics ["llamacpp:prompt_tokens_total" ][1 ]== n_processed 109assert metrics ["llamacpp:prompt_tokens_cached_total" ][1 ]== n_cached 110 111 112def test_metrics_predicted_total_matches_requests (): 113global server 114server .start () 115 116n_predicted = 0 117for n_predict in [1 ,4 ,16 ]: 118res = server .make_request ("POST" ,"/completion" ,data = {"prompt" :"I believe" ,"n_predict" :n_predict }) 119assert res .status_code == 200 120n_predicted += res .body ["timings" ]["predicted_n" ] 121 122metrics = parse_metrics (fetch_metrics (server )) 123assert metrics ["llamacpp:tokens_predicted_total" ][1 ]== n_predicted 124 125 126def test_metrics_generation_rate_excludes_first_token (): 127global server 128server .start () 129 130# the first token comes from the logits of the last prompt batch, so it costs no decode step 131res = server .make_request ("POST" ,"/completion" ,data = {"prompt" :"I believe" ,"n_predict" :1 }) 132timings = res .body ["timings" ] 133assert timings ["predicted_n" ]== 1 134assert timings ["predicted_per_second" ]== 0.0 135assert timings ["predicted_per_token_ms" ]== 0.0 136 137res = server .make_request ("POST" ,"/completion" ,data = {"prompt" :"I believe" ,"n_predict" :16 }) 138timings = res .body ["timings" ] 139assert timings ["predicted_n" ]== 16 140# the rate is over 15 decode steps, not 16 tokens 141expected = 1e3 / timings ["predicted_ms" ]* 15 142assert abs (timings ["predicted_per_second" ]- expected )< 1e-6 143 144 145@ pytest . mark . parametrize ( "n_predict" , [ 1 , 8 ]) 146def test_metrics_timings_are_finite (n_predict :int ): 147global server 148server .start () 149res = server .make_request ("POST" ,"/completion" ,data = {"prompt" :"I believe" ,"n_predict" :n_predict }) 150timings = res .body ["timings" ] 151 152# a null here means the server produced inf or nan 153for key ,value in timings .items (): 154assert value is not None ,f" { key } is null" 155assert value >= 0 ,f" { key } is negative" 156 157assert timings ["prompt_ms" ]> 0 158assert timings ["prompt_per_token_ms" ]> 0 159 160 161def test_metrics_timings_on_prompt_progress (): 162global server 163server .start () 164 165# a long prompt so that it is split over several batches (n_batch = 32) 166prompt = "the quick brown fox jumps over the lazy dog " * 8 167chunks = list (server .make_stream_request ("POST" ,"/completion" ,data = { 168"prompt" :prompt , 169"n_predict" :4 , 170"stream" :True , 171"timings_per_token" :True , 172"return_progress" :True , 173 })) 174 175progress = [c for c in chunks if "prompt_progress" in c ] 176assert len (progress )> 1 # the prompt did not fit in a single batch 177 178# the very first update is sent before any prompt token is decoded 179first = progress [0 ]["timings" ] 180assert first ["prompt_n" ]== 0 181assert first ["prompt_ms" ]== 0.0 182assert first ["predicted_n" ]== 0 183assert first ["predicted_ms" ]== 0.0 184 185# timings must never go backwards, nor report bogus values 186prompt_ms = 0.0 187for chunk in progress : 188timings = chunk ["timings" ] 189for key ,value in timings .items (): 190assert value is not None ,f" { key } is null" 191assert value >= 0 ,f" { key } is negative" 192assert timings ["prompt_ms" ]>= prompt_ms 193prompt_ms = timings ["prompt_ms" ] 194 195assert prompt_ms > 0 196 197 198def test_metrics_slots_idle_after_completion (): 199global server 200server .server_slots = True 201server .start () 202server .make_request ("POST" ,"/completion" ,data = {"prompt" :"I believe" ,"n_predict" :8 }) 203 204res = server .make_request ("GET" ,"/slots" ) 205assert res .status_code == 200 206for slot in res .body : 207assert slot ["is_processing" ]is False 208if "next_token" in slot : 209# the budget of the finished task must not leak into the idle slot 210assert slot ["next_token" ][0 ]["n_remain" ]== - 1 211assert slot ["next_token" ][0 ]["n_decoded" ]== 0 212 213 214def test_metrics_embedding_prompt_is_counted (): 215global server 216server = ServerPreset .bert_bge_small () 217server .server_metrics = True 218server .start () 219 220res = server .make_request ("POST" ,"/v1/embeddings" ,data = {"input" : ["hello world" ,"goodbye world" ]}) 221assert res .status_code == 200 222 223# embedding tasks never sample a token, but their prompt still costs a decode 224metrics = parse_metrics (fetch_metrics (server )) 225assert metrics ["llamacpp:prompt_tokens_total" ][1 ]> 0 226assert metrics ["llamacpp:n_decode_total" ][1 ]> 0 227assert metrics ["llamacpp:tokens_predicted_total" ][1 ]== 0