okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
923a9a9
main
1//// Run explicitly with: gleam run -m manual/stream_benchmark 2//// Offline: frames and reduces synthetic Responses and Chat Completions 3//// streams, and encodes a large request, printing time per run. Profile a 4//// run with eprof for where the time goes. 5 6import albedo/clock 7import albedo/openai_api/request 8import albedo/openai_api/sse 9import albedo/openai_api/stream 10import albedo/openai_api/types 11import gleam/bit_array 12import gleam/float 13import gleam/int 14import gleam/io 15import gleam/json .{ type Json } 16import gleam/list 17import gleam/option .{ None , Some } 18import gleam/result 19import gleam/string 20import gleam/string_tree .{ type StringTree } 21 22const deltas = 20_000 23 24const chunk_bytes = 1400 25 26const runs = 10 27 28pub fn main () -> Nil { 29let responses = chunks ( responses_stream ( deltas )) 30let chat = chunks ( chat_stream ( deltas )) 31time ( "responses stream" , stream_bytes ( responses ), fn () { 32drive ( types . Responses , responses ) 33}) 34time ( "chat stream" , stream_bytes ( chat ), fn () { 35drive ( types . ChatCompletions , chat ) 36}) 37let responses_request = large_request ( types . Responses , 400 , 40 ) 38let chat_request = large_request ( types . ChatCompletions , 400 , 40 ) 39time ( "encode responses request" , 0 , fn () { 40encode ( types . Responses , responses_request ) 41}) 42time ( "encode chat request" , 0 , fn () { 43encode ( types . ChatCompletions , chat_request ) 44}) 45} 46 47fn encode ( protocol : types . Protocol , request : types . Request ) -> String { 48request . encode ( protocol , request ) 49|> result . map ( string_tree . byte_size ) 50|> string . inspect 51} 52 53fn time ( label : String , bytes : Int , run : fn () -> String ) -> Nil { 54let summary = run () 55let started = clock . monotonic_ms () 56list . each ( list . repeat ( Nil , runs ), fn ( _ ) { run () }) 57let per_run = 58int . to_float ( clock . monotonic_ms () - started ) /. int . to_float ( runs ) 59let rate = case bytes { 600 -> "" 61_ -> 62" " 63<> float . to_string ( float . to_precision ( 64int . to_float ( bytes ) /. per_run *. 1000.0 /. 1_048_576.0 , 651 , 66)) 67<> " MB/s" 68} 69io . println ( 70label 71<> ": " 72<> float . to_string ( float . to_precision ( per_run , 2 )) 73<> " ms/run" 74<> rate 75<> " " 76<> summary , 77) 78} 79 80fn stream_bytes ( chunks : List ( BitArray )) -> Int { 81list . fold ( chunks , 0 , fn ( total , chunk ) { total + bit_array . byte_size ( chunk ) }) 82} 83 84fn drive ( protocol : types . Protocol , chunks : List ( BitArray )) -> String { 85case run ( sse . new ( 8 * 1024 * 1024 ), stream . new ( protocol ), chunks ) { 86Ok ( turn ) -> 87"turn: " 88<> int . to_string ( list . length ( turn . output )) 89<> " items, " 90<> int . to_string ( list . length ( turn . tool_calls )) 91<> " calls, " 92<> string . inspect ( turn . finish ) 93Error ( error ) -> "error: " <> error 94} 95} 96 97type Step { 98Done ( types . Turn ) 99More ( stream . State ) 100} 101 102fn run ( 103parser : sse . Parser , 104state : stream . State , 105chunks : List ( BitArray ), 106) -> Result ( types . Turn , String ) { 107case chunks { 108[] -> Error ( "no terminal event" ) 109[ chunk , .. rest ] -> { 110use #( parser , events ) <- result . try ( 111sse . feed ( parser , chunk ) |> result . map_error ( string . inspect ), 112) 113case reduce ( state , events ) { 114Ok ( Done ( turn )) -> Ok ( turn ) 115Ok ( More ( state )) -> run ( parser , state , rest ) 116Error ( error ) -> Error ( error ) 117} 118} 119} 120} 121 122fn reduce ( 123state : stream . State , 124events : List ( sse . Event ), 125) -> Result ( Step , String ) { 126case events { 127[] -> Ok ( More ( state )) 128[ event , .. rest ] -> 129case stream . feed ( state , event ) { 130Ok (#( state , _ , None )) -> reduce ( state , rest ) 131Ok (#( _ , _ , Some ( turn ))) -> Ok ( Done ( turn )) 132Error ( error ) -> Error ( string . inspect ( error )) 133} 134} 135} 136 137fn chunks ( stream : StringTree ) -> List ( BitArray ) { 138split ( bit_array . from_string ( string_tree . to_string ( stream )), []) 139} 140 141fn split ( bytes : BitArray , acc : List ( BitArray )) -> List ( BitArray ) { 142case bit_array . byte_size ( bytes ) <= chunk_bytes { 143True -> list . reverse ([ bytes , .. acc ]) 144False -> { 145let assert << head : bytes - size ( chunk_bytes ), rest : bytes >> = bytes 146split ( rest , [ head , .. acc ]) 147} 148} 149} 150 151fn event ( name : String , data : Json ) -> StringTree { 152string_tree . from_strings ([ 153"event: " , 154name , 155"\ndata: " , 156json . to_string ( data ), 157"\n\n" , 158]) 159} 160 161fn responses_stream ( count : Int ) -> StringTree { 162let message = fn ( text : String ) { 163json . object ([ 164#( "id" , json . string ( "msg_001" )), 165#( "type" , json . string ( "message" )), 166#( "status" , json . string ( "completed" )), 167#( "role" , json . string ( "assistant" )), 168#( 169"content" , 170json . array ( 171[ 172json . object ([ 173#( "type" , json . string ( "output_text" )), 174#( "text" , json . string ( text )), 175#( "annotations" , json . array ([], json . string )), 176]), 177], 178fn ( part ) { part }, 179), 180), 181]) 182} 183let calls = 184range ( 1 , 5 ) 185|> list . map ( fn ( i ) { 186json . object ([ 187#( "id" , json . string ( "fc_" <> int . to_string ( i ))), 188#( "type" , json . string ( "function_call" )), 189#( "status" , json . string ( "completed" )), 190#( "call_id" , json . string ( "call_" <> int . to_string ( i ))), 191#( "name" , json . string ( "python" )), 192#( "arguments" , json . string ( "{\"code\":\"print(1)\"}" )), 193]) 194}) 195let tokens = 196range ( 1 , count ) 197|> list . map ( fn ( i ) { "token " <> int . to_string ( i ) <> " " }) 198let deltas = 199list . index_map ( tokens , fn ( token , i ) { 200event ( 201"response.output_text.delta" , 202json . object ([ 203#( "type" , json . string ( "response.output_text.delta" )), 204#( "sequence_number" , json . int ( i + 2 )), 205#( "item_id" , json . string ( "msg_001" )), 206#( "output_index" , json . int ( 0 )), 207#( "content_index" , json . int ( 0 )), 208#( "delta" , json . string ( token )), 209#( "logprobs" , json . array ([], json . string )), 210]), 211) 212}) 213let full = message ( string . concat ( tokens )) 214string_tree . concat ([ 215event ( 216"response.created" , 217json . object ([ 218#( "type" , json . string ( "response.created" )), 219#( "sequence_number" , json . int ( 0 )), 220#( 221"response" , 222json . object ([ 223#( "id" , json . string ( "resp_abc123" )), 224#( "status" , json . string ( "in_progress" )), 225#( "output" , json . array ([], json . string )), 226]), 227), 228]), 229), 230event ( 231"response.output_item.added" , 232json . object ([ 233#( "type" , json . string ( "response.output_item.added" )), 234#( "sequence_number" , json . int ( 1 )), 235#( "output_index" , json . int ( 0 )), 236#( "item" , message ( "" )), 237]), 238), 239string_tree . concat ( deltas ), 240event ( 241"response.output_item.done" , 242json . object ([ 243#( "type" , json . string ( "response.output_item.done" )), 244#( "output_index" , json . int ( 0 )), 245#( "item" , full ), 246]), 247), 248event ( 249"response.completed" , 250json . object ([ 251#( "type" , json . string ( "response.completed" )), 252#( 253"response" , 254json . object ([ 255#( "id" , json . string ( "resp_abc123" )), 256#( "status" , json . string ( "completed" )), 257#( "output" , json . preprocessed_array ([ full , .. calls ])), 258#( 259"usage" , 260json . object ([ 261#( "input_tokens" , json . int ( 1200 )), 262#( 263"input_tokens_details" , 264json . object ([#( "cached_tokens" , json . int ( 1000 ))]), 265), 266#( "output_tokens" , json . int ( count )), 267#( 268"output_tokens_details" , 269json . object ([#( "reasoning_tokens" , json . int ( 0 ))]), 270), 271]), 272), 273]), 274), 275]), 276), 277]) 278} 279 280fn chat_stream ( count : Int ) -> StringTree { 281let chunk = fn ( delta : Json , finish : Json ) { 282string_tree . from_strings ([ 283"data: " , 284json . to_string ( 285json . object ([ 286#( "id" , json . string ( "chatcmpl-abc" )), 287#( "object" , json . string ( "chat.completion.chunk" )), 288#( "created" , json . int ( 1_700_000_000 )), 289#( "model" , json . string ( "gpt-x" )), 290#( 291"choices" , 292json . preprocessed_array ([ 293json . object ([ 294#( "index" , json . int ( 0 )), 295#( "delta" , delta ), 296#( "logprobs" , json . null ()), 297#( "finish_reason" , finish ), 298]), 299]), 300), 301#( "usage" , json . null ()), 302]), 303), 304"\n\n" , 305]) 306} 307let deltas = 308range ( 1 , count ) 309|> list . map ( fn ( i ) { 310chunk ( 311json . object ([ 312#( "content" , json . string ( "token " <> int . to_string ( i ) <> " " )), 313]), 314json . null (), 315) 316}) 317let tool = fn ( i : Int , fields : List (#( String , Json ))) { 318chunk ( 319json . object ([ 320#( 321"tool_calls" , 322json . preprocessed_array ([ 323json . object ([#( "index" , json . int ( i )), .. fields ]), 324]), 325), 326]), 327json . null (), 328) 329} 330let opened = 331range ( 0 , 4 ) 332|> list . map ( fn ( i ) { 333tool ( i , [ 334#( "id" , json . string ( "call_" <> int . to_string ( i ))), 335#( "type" , json . string ( "function" )), 336#( 337"function" , 338json . object ([ 339#( "name" , json . string ( "python" )), 340#( "arguments" , json . string ( "" )), 341]), 342), 343]) 344}) 345let arguments = 346range ( 0 , 4 ) 347|> list . map ( fn ( i ) { 348tool ( i , [ 349#( 350"function" , 351json . object ([#( "arguments" , json . string ( "{\"code\":\"print(1)\"}" ))]), 352), 353]) 354}) 355string_tree . concat ([ 356chunk ( 357json . object ([ 358#( "role" , json . string ( "assistant" )), 359#( "content" , json . string ( "" )), 360#( "refusal" , json . null ()), 361]), 362json . null (), 363), 364string_tree . concat ( deltas ), 365string_tree . concat ( opened ), 366string_tree . concat ( arguments ), 367chunk ( json . object ([]), json . string ( "tool_calls" )), 368string_tree . from_strings ([ 369"data: " , 370json . to_string ( 371json . object ([ 372#( "id" , json . string ( "chatcmpl-abc" )), 373#( "choices" , json . array ([], json . string )), 374#( 375"usage" , 376json . object ([ 377#( "prompt_tokens" , json . int ( 1200 )), 378#( "completion_tokens" , json . int ( count )), 379#( 380"prompt_tokens_details" , 381json . object ([#( "cached_tokens" , json . int ( 1000 ))]), 382), 383]), 384), 385]), 386), 387"\n\ndata: [DONE]\n\n" , 388]), 389]) 390} 391 392/// `from` through `to`, ascending. 393fn range ( from : Int , to : Int ) -> List ( Int ) { 394int . range ( to , from - 1 , [], list . prepend ) 395} 396 397fn lorem ( i : Int ) -> String { 398string . repeat ( 399"the quick brown fox " 400<> int . to_string ( i ) 401<> " jumps over the lazy dog with \"quotes\" and \n newlines. " , 40220 , 403) 404} 405 406fn tools ( count : Int ) -> List ( types . Tool ) { 407range ( 1 , count ) 408|> list . map ( fn ( i ) { 409types . Tool ( 410"tool_" <> int . to_string ( i ), 411"Execute Python in your persistent session. Provide code (string); timeout_ms is optional." , 412json . object ([ 413#( "type" , json . string ( "object" )), 414#( 415"properties" , 416json . object ([ 417#( "code" , json . object ([#( "type" , json . string ( "string" ))])), 418#( "timeout_ms" , json . object ([#( "type" , json . string ( "integer" ))])), 419]), 420), 421#( "required" , json . array ([ "code" ], json . string )), 422#( "additionalProperties" , json . bool ( False )), 423]), 424False , 425) 426}) 427} 428 429/// A replayed `tool_1` call as `protocol` would have produced it. 430fn replayed_call ( protocol : types . Protocol , i : Int ) -> types . Input { 431let arguments = 432json . string ( json . to_string ( json . object ([#( "code" , json . string ( lorem ( i )))]))) 433let item = case protocol { 434types . Responses -> 435json . object ([ 436#( "id" , json . string ( "fc_" <> int . to_string ( i ))), 437#( "type" , json . string ( "function_call" )), 438#( "status" , json . string ( "completed" )), 439#( "call_id" , json . string ( "call_" <> int . to_string ( i ))), 440#( "name" , json . string ( "tool_1" )), 441#( "arguments" , arguments ), 442]) 443types . ChatCompletions -> 444json . object ([ 445#( "role" , json . string ( "assistant" )), 446#( "content" , json . null ()), 447#( 448"tool_calls" , 449json . preprocessed_array ([ 450json . object ([ 451#( "id" , json . string ( "call_" <> int . to_string ( i ))), 452#( "type" , json . string ( "function" )), 453#( 454"function" , 455json . object ([ 456#( "name" , json . string ( "tool_1" )), 457#( "arguments" , arguments ), 458]), 459), 460]), 461]), 462), 463]) 464} 465let assert Ok ( item ) = 466json . parse ( json . to_string ( item ), types . replay_decoder ( protocol )) 467types . Replay ( item ) 468} 469 470/// `inputs` inputs in user, assistant, replayed call, tool output order, 471/// with `tool_count` tools and a long system prompt. 472fn large_request ( 473protocol : types . Protocol , 474inputs : Int , 475tool_count : Int , 476) -> types . Request { 477let input = 478range ( 1 , inputs / 4 ) 479|> list . flat_map ( fn ( i ) { 480[ 481types . User ( lorem ( i )), 482types . Assistant ( lorem ( i )), 483replayed_call ( protocol , i ), 484types . ToolOutput ( "call_" <> int . to_string ( i ), lorem ( i ), []), 485] 486}) 487types . Request ( 488"gpt-x" , 489Some ( string . repeat ( lorem ( 0 ), 200 )), 490input , 491tools ( tool_count ), 492None , 493types . defaults , 494) 495}