char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
d73c1d6b2
char/aliceai-t5-moe
1#include "server-stream.h" 2#include "server-common.h" 3#include "server-http.h" 4#include "server-queue.h" 5 6#include <chrono> 7#include <memory> 8#include <utility> 9#include <shared_mutex> 10 11enum class stream_read_status { 12OK , 13OFFSET_LOST , 14}; 15 16namespace { 17constexpr int64_t STREAM_SESSION_TTL_SECONDS = 300 ; 18constexpr size_t STREAM_SESSION_MAX_BYTES = 4 * 1024 * 1024 ; 19constexpr int64_t STREAM_SESSION_GC_INTERVAL_SECONDS = 60 ; 20constexpr int64_t STREAM_READ_WAKE_INTERVAL_MS = 200 ; 21 22int64_t now_seconds () { 23return std::chrono::duration_cast < std::chrono::seconds > ( 24 std::chrono::system_clock::now ().time_since_epoch () 25 ).count (); 26} 27} 28 29// owns all live sessions keyed by conversation_id, one conv = at most one live session. 30// a periodic GC evicts expired ones 31class stream_session_manager { 32public : 33stream_session_manager (); 34 ~stream_session_manager (); 35 36stream_session_manager (const stream_session_manager & )= delete ; 37stream_session_manager & operator= (const stream_session_manager & )= delete ; 38 39// install a new session, evicting and cancelling any previous one. conversation_id must be non empty 40stream_session_ptr create_or_replace (const std::string & conversation_id ); 41 42stream_session_ptr get (const std::string & conversation_id ); 43 44 std::vector < stream_session_ptr > list_all ()const ; 45 46void evict (const std::string & conversation_id ); 47 48void evict_and_cancel (const std::string & conversation_id ); 49 50void start_gc (); 51void stop_gc (); 52 53private : 54void gc_loop (); 55 56mutable std::shared_mutex map_mu ; 57 std::unordered_map < std::string ,stream_session_ptr > sessions ;// key: conversation_id 58 std::thread gc_thread ; 59bool running ; 60 std::mutex gc_wake_mu ; 61 std::condition_variable gc_wake_cv ; 62}; 63 64// process wide manager, lifecycle controlled by llama-server main() via start_gc/stop_gc 65static stream_session_manager g_stream_sessions ; 66 67void server_stream_session_manager_start () { 68g_stream_sessions .start_gc (); 69} 70 71void server_stream_session_manager_stop () { 72g_stream_sessions .stop_gc (); 73} 74 75struct stream_session { 76 std::string conversation_id ; 77int64_t started_ts ;// unix seconds at construction 78 79stream_session (std::string conversation_id_ ,size_t max_bytes_ ); 80stream_session (const stream_session & )= delete ; 81stream_session & operator= (const stream_session & )= delete ; 82 83bool append (const char * data ,size_t len ); 84 85void finalize (); 86 87// drain from offset into sink, blocking for more bytes or finalize. OFFSET_LOST if offset 88// fell below the dropped prefix 89stream_read_status read_from (size_t offset , 90const std::function < bool (const char * ,size_t )> & sink , 91const std::function < bool ()> & should_stop ); 92 93bool is_done ()const ; 94bool is_cancelled ()const ; 95size_t total_size ()const ;// bytes that ever entered the session 96size_t dropped_prefix ()const ;// bytes evicted from the front due to cap 97int64_t completed_at ()const ;// 0 while alive, unix seconds after finalize 98 99void cancel (); 100 101private : 102mutable std::mutex mu ; 103 std::condition_variable cv ; 104 std::vector < char > buffer ; 105size_t prefix_dropped ; 106size_t cap_bytes ; 107bool done ; 108 std::atomic < bool > cancelled ;// polled lock-free by the should_stop closure, no mu 109int64_t completed_ts ; 110}; 111stream_session::stream_session (std::string conversation_id_ ,size_t max_bytes_ ) 112 :conversation_id (std::move (conversation_id_ )) 113 ,started_ts (now_seconds ()) 114 ,prefix_dropped (0 ) 115 ,cap_bytes (max_bytes_ ) 116 ,done (false) 117 ,cancelled (false) 118 ,completed_ts (0 ) { 119buffer .reserve (64 * 1024 ); 120} 121 122bool stream_session::append (const char * data ,size_t len ) { 123if (len == 0 ) { 124return true; 125 } 126 { 127 std::lock_guard < std::mutex > lock (mu ); 128if (done ) { 129return false; 130 } 131if (len >=cap_bytes ) { 132// single chunk bigger than the cap, keep only the tail that fits 133size_t skip = len - cap_bytes ; 134prefix_dropped += buffer .size ()+ skip ; 135buffer .clear (); 136buffer .insert (buffer .end (),data + skip ,data + len ); 137 }else { 138size_t needed = buffer .size ()+ len ; 139if (needed > cap_bytes ) { 140size_t to_drop = needed - cap_bytes ; 141buffer .erase (buffer .begin (),buffer .begin ()+ to_drop ); 142prefix_dropped += to_drop ; 143 } 144buffer .insert (buffer .end (),data ,data + len ); 145 } 146 } 147cv .notify_all (); 148return true; 149} 150 151void stream_session::finalize () { 152 { 153 std::lock_guard < std::mutex > lock (mu ); 154if (done ) { 155return ; 156 } 157done = true; 158completed_ts = now_seconds (); 159 } 160cv .notify_all (); 161} 162 163stream_read_status stream_session::read_from (size_t offset , 164const std::function < bool (const char * ,size_t )> & sink , 165const std::function < bool ()> & should_stop ) { 166 std::unique_lock < std::mutex > lock (mu ); 167while (true) { 168if (should_stop && should_stop ()) { 169return stream_read_status::OK ; 170 } 171if (offset < prefix_dropped ) { 172return stream_read_status::OFFSET_LOST ; 173 } 174size_t logical_end = prefix_dropped + buffer .size (); 175if (offset < logical_end ) { 176size_t local_off = offset - prefix_dropped ; 177size_t n = buffer .size ()- local_off ; 178// copy the available chunk under the lock, release before calling the sink 179 std::vector < char > chunk (buffer .begin ()+ local_off ,buffer .begin ()+ local_off + n ); 180offset += n ; 181lock .unlock (); 182bool keep_going = sink (chunk .data (),chunk .size ()); 183if (!keep_going ) { 184return stream_read_status::OK ; 185 } 186lock .lock (); 187continue ; 188 } 189if (done ) { 190return stream_read_status::OK ; 191 } 192// wait for new bytes, finalize, or a periodic wake to re check should_stop 193cv .wait_for (lock , std::chrono::milliseconds (STREAM_READ_WAKE_INTERVAL_MS )); 194 } 195} 196 197bool stream_session::is_done ()const { 198 std::lock_guard < std::mutex > lock (mu ); 199return done ; 200} 201 202size_t stream_session::total_size ()const { 203 std::lock_guard < std::mutex > lock (mu ); 204return prefix_dropped + buffer .size (); 205} 206 207size_t stream_session::dropped_prefix ()const { 208 std::lock_guard < std::mutex > lock (mu ); 209return prefix_dropped ; 210} 211 212int64_t stream_session::completed_at ()const { 213 std::lock_guard < std::mutex > lock (mu ); 214return completed_ts ; 215} 216 217void stream_session::cancel () { 218// the should_stop closure on both the producer and any HTTP reader polls is_cancelled() 219// so flipping this is the only signal needed to unwind both sides 220cancelled .store (true, std::memory_order_release ); 221} 222 223bool stream_session::is_cancelled ()const { 224return cancelled .load (std::memory_order_acquire ); 225} 226 227stream_session_manager::stream_session_manager () 228 :running (false) { 229} 230 231stream_session_manager::~stream_session_manager () { 232stop_gc (); 233} 234 235stream_session_ptr stream_session_manager::create_or_replace (const std::string & conversation_id ) { 236// evict any previous session on the same conv, this guarantees the invariant 237// "one conv = at most one live session" and propagates cancel to its producer 238stream_session_ptr previous ; 239auto fresh = std::make_shared < stream_session > (conversation_id ,STREAM_SESSION_MAX_BYTES ); 240 { 241 std::unique_lock < std::shared_mutex > lock (map_mu ); 242auto it = sessions .find (conversation_id ); 243if (it != sessions .end ()) { 244previous = it -> second ; 245it -> second = fresh ; 246 }else { 247sessions .emplace (conversation_id ,fresh ); 248 } 249 } 250if (previous ) { 251previous -> cancel (); 252previous -> finalize (); 253 } 254return fresh ; 255} 256 257stream_session_ptr stream_session_manager::get (const std::string & conversation_id ) { 258 std::shared_lock < std::shared_mutex > lock (map_mu ); 259auto it = sessions .find (conversation_id ); 260if (it == sessions .end ()) { 261return nullptr ; 262 } 263return it -> second ; 264} 265 266std::vector < stream_session_ptr > stream_session_manager::list_all ()const { 267 std::vector < stream_session_ptr > out ; 268 std::shared_lock < std::shared_mutex > lock (map_mu ); 269out .reserve (sessions .size ()); 270for (auto & kv :sessions ) { 271out .push_back (kv .second ); 272 } 273return out ; 274} 275 276void stream_session_manager::evict (const std::string & conversation_id ) { 277stream_session_ptr s ; 278 { 279 std::unique_lock < std::shared_mutex > lock (map_mu ); 280auto it = sessions .find (conversation_id ); 281if (it == sessions .end ()) { 282return ; 283 } 284s = it -> second ; 285sessions .erase (it ); 286 } 287// finalize outside the map lock so any pending readers wake up and exit 288s -> finalize (); 289} 290 291void stream_session_manager::evict_and_cancel (const std::string & conversation_id ) { 292stream_session_ptr s ; 293 { 294 std::unique_lock < std::shared_mutex > lock (map_mu ); 295auto it = sessions .find (conversation_id ); 296if (it == sessions .end ()) { 297 std::string live ; 298for (const auto & kv :sessions ) { 299if (!live .empty ())live += ", " ; 300live += kv .first ; 301 } 302SRV_WRN ("stop on unknown stream session, conv_id=%s matched nothing, %zu live: [%s]\n" , 303conversation_id .c_str (),sessions .size (),live .c_str ()); 304return ; 305 } 306s = it -> second ; 307sessions .erase (it ); 308 } 309// cancel first so the producer's on_complete() drain loop and any pending HTTP reader 310// observe is_cancelled() and stop pulling further output, then finalize to wake readers 311// blocked in read_from(). note: this does not interrupt the underlying generation itself, 312// which keeps running to its own natural stop condition (EOS/max_tokens) 313s -> cancel (); 314s -> finalize (); 315} 316 317void stream_session_manager::start_gc () { 318 { 319 std::lock_guard < std::mutex > lock (gc_wake_mu ); 320if (running ) { 321return ; 322 } 323running = true; 324 } 325gc_thread = std::thread ([this ] {gc_loop (); }); 326} 327 328void stream_session_manager::stop_gc () { 329bool was_running ; 330 { 331 std::lock_guard < std::mutex > lock (gc_wake_mu ); 332was_running = running ; 333running = false; 334 } 335if (was_running ) { 336gc_wake_cv .notify_all (); 337if (gc_thread .joinable ()) { 338gc_thread .join (); 339 } 340 } 341// finalize all live sessions so no reader ever hangs 342 std::vector < stream_session_ptr > snapshot ; 343 { 344 std::unique_lock < std::shared_mutex > lock (map_mu ); 345snapshot .reserve (sessions .size ()); 346for (auto & kv :sessions ) { 347snapshot .push_back (kv .second ); 348 } 349sessions .clear (); 350 } 351for (auto & s :snapshot ) { 352s -> finalize (); 353 } 354} 355 356void stream_session_manager::gc_loop () { 357while (true) { 358 { 359 std::unique_lock < std::mutex > lock (gc_wake_mu ); 360gc_wake_cv .wait_for (lock , 361 std::chrono::seconds (STREAM_SESSION_GC_INTERVAL_SECONDS ), 362 [this ] {return !running ; }); 363if (!running ) { 364return ; 365 } 366 } 367int64_t cutoff = now_seconds ()- STREAM_SESSION_TTL_SECONDS ; 368 std::vector < stream_session_ptr > to_drop ; 369 { 370 std::unique_lock < std::shared_mutex > lock (map_mu ); 371for (auto it = sessions .begin ();it != sessions .end (); ) { 372int64_t completed = it -> second -> completed_at (); 373if (completed != 0 && completed <=cutoff ) { 374to_drop .push_back (it -> second ); 375it = sessions .erase (it ); 376 }else { 377++ it ; 378 } 379 } 380 } 381// finalize outside the map lock, idempotent if the session was already done 382for (auto & s :to_drop ) { 383s -> finalize (); 384 } 385 } 386} 387 388// stream_pipe 389 390// consumer end: read-only replay of the ring buffer, the destructor does not finalize the session 391struct stream_pipe_consumer :stream_pipe { 392stream_read_status read (size_t & offset , 393const std::function < bool (const char * ,size_t )> & sink , 394const std::function < bool ()> & should_stop ); 395 396static std::shared_ptr < stream_pipe_consumer > create (stream_session_ptr session ); 397 398private : 399explicit stream_pipe_consumer (stream_session_ptr session ); 400}; 401 402stream_pipe::stream_pipe (stream_session_ptr session ) 403 :session_ (std::move (session )) { 404} 405 406bool stream_pipe::is_cancelled ()const { 407return session_ -> is_cancelled (); 408} 409 410// stream_pipe_producer 411 412stream_pipe_producer::stream_pipe_producer (stream_session_ptr session ) 413 :stream_pipe (std::move (session )) { 414} 415 416stream_pipe_producer::~stream_pipe_producer () { 417session_ -> finalize (); 418} 419 420bool stream_pipe_producer::write (const char * data ,size_t len ) { 421return session_ -> append (data ,len ); 422} 423 424stream_pipe_producer * stream_pipe_producer::create (stream_session_ptr session ) { 425return new stream_pipe_producer (std::move (session )); 426} 427 428// stream_pipe_consumer 429 430stream_pipe_consumer::stream_pipe_consumer (stream_session_ptr session ) 431 :stream_pipe (std::move (session )) { 432} 433 434stream_read_status stream_pipe_consumer::read (size_t & offset , 435const std::function < bool (const char * ,size_t )> & sink , 436const std::function < bool ()> & should_stop ) { 437return session_ -> read_from (offset ,sink ,should_stop ); 438} 439 440std::shared_ptr < stream_pipe_consumer > stream_pipe_consumer::create (stream_session_ptr session ) { 441return std::shared_ptr < stream_pipe_consumer > (new stream_pipe_consumer (std::move (session ))); 442} 443 444// helper, builds the standard error response and assigns it to a brand new http_res 445static server_http_res_ptr make_error_response (int status ,const std::string & message ,error_type type ) { 446auto res = std::make_unique < server_http_res > (); 447json err = format_error_response (message ,type ); 448res -> status = json_value (err ,"code" ,status ); 449res -> content_type = "application/json; charset=utf-8" ; 450res -> data = safe_json_to_str ({{"error" ,err }}); 451return res ; 452} 453 454server_http_context::handler_t server_stream_make_get_handler () { 455return [](const server_http_req & req )-> server_http_res_ptr { 456// GET /v1/stream?conv_id=<id>&from=N replays buffered SSE bytes then blocks for live 457// bytes until the session finalizes, streamed as text/event-stream for EventSource 458 std::string conv_id = req .get_param ("conv_id" ); 459if (conv_id .empty ()) { 460return make_error_response (400 ,"Missing conversation id in path" ,ERROR_TYPE_INVALID_REQUEST ); 461 } 462auto session = g_stream_sessions .get (conv_id ); 463if (!session ) { 464return make_error_response (404 ,"Stream not found or expired" ,ERROR_TYPE_NOT_FOUND ); 465 } 466size_t from = 0 ; 467 std::string from_str = req .get_param ("from" ); 468if (!from_str .empty ()) { 469try { 470from = static_cast < size_t > (std::stoull (from_str )); 471 }catch (const std::exception & ) { 472return make_error_response (400 ,"Invalid 'from' offset" ,ERROR_TYPE_INVALID_REQUEST ); 473 } 474 } 475if (from < session -> dropped_prefix ()) { 476return make_error_response (400 ,"Stream offset lost, please restart" ,ERROR_TYPE_INVALID_REQUEST ); 477 } 478auto res = std::make_unique < server_http_res > (); 479res -> status = 200 ; 480res -> content_type = "text/event-stream" ; 481// the next closure reads from the ring buffer at the requested offset, blocks until 482// bytes arrive or the session finalizes. exit each call after draining the available 483// chunk so set_chunked_content_provider gets a chance to flush to the socket 484auto offset_ptr = std::make_shared < size_t > (from ); 485// consumer pipe: read-only, does not finalize the session on destruction 486auto pipe = stream_pipe_consumer::create (session ); 487res -> next = [pipe ,offset_ptr ,& req ](std::string & output )-> bool { 488bool got_any = false; 489pipe -> read (* offset_ptr , 490 [& ](const char * d ,size_t n ) { 491output .append (d ,n ); 492* offset_ptr += n ; 493got_any = true; 494return false; 495 }, 496req .should_stop ); 497return got_any ; 498 }; 499return res ; 500 }; 501} 502 503server_http_context::handler_t server_stream_make_lookup_handler () { 504return [](const server_http_req & req )-> server_http_res_ptr { 505// POST /v1/streams/lookup returns the matching sessions, only for ids the caller already 506// knows. each id matches the exact key and any "<id>::<model>" per model variant 507 std::vector < std::string > requested ; 508try { 509json body = json::parse (req .body ); 510if (body .contains ("conversation_ids" )&& body ["conversation_ids" ].is_array ()) { 511for (const auto & v :body ["conversation_ids" ]) { 512if (v .is_string ()) { 513 std::string id = v .get < std::string > (); 514if (!id .empty ()) { 515requested .push_back (std::move (id )); 516 } 517 } 518 } 519 } 520 }catch (const std::exception & e ) { 521auto res = std::make_unique < server_http_res > (); 522res -> status = 400 ; 523res -> content_type = "application/json; charset=utf-8" ; 524res -> data = safe_json_to_str ({{"error" , {{"message" , std::string ("invalid body: " )+ e .what ()}, 525 {"type" ,"invalid_request_error" }}}}); 526return res ; 527 } 528 529 std::vector < stream_session_ptr > sessions ; 530if (!requested .empty ()) { 531auto all = g_stream_sessions .list_all (); 532for (const auto & rid :requested ) { 533const std::string with_sep = rid + "::" ; 534for (auto & s :all ) { 535if (s -> conversation_id == rid || 536s -> conversation_id .compare (0 ,with_sep .size (),with_sep )== 0 ) { 537sessions .push_back (s ); 538 } 539 } 540 } 541 } 542 543json arr = json::array (); 544for (auto & s :sessions ) { 545arr .push_back ({ 546 {"conversation_id" ,s -> conversation_id }, 547 {"is_done" ,s -> is_done ()}, 548 {"total_bytes" ,s -> total_size ()}, 549 {"started_at" ,s -> started_ts }, 550 {"completed_at" ,s -> completed_at ()}, 551 }); 552 } 553auto res = std::make_unique < server_http_res > (); 554res -> status = 200 ; 555res -> content_type = "application/json; charset=utf-8" ; 556res -> data = safe_json_to_str (arr ); 557return res ; 558 }; 559} 560 561server_http_context::handler_t server_stream_make_delete_handler () { 562return [](const server_http_req & req )-> server_http_res_ptr { 563// DELETE /v1/stream?conv_id=<id> is the explicit user Stop, cancels the producer and evicts 564// the buffer. idempotent, returns 204 even if the session was already gone 565 std::string conv_id = req .get_param ("conv_id" ); 566if (conv_id .empty ()) { 567return make_error_response (400 ,"Missing conversation id in path" ,ERROR_TYPE_INVALID_REQUEST ); 568 } 569SRV_TRC ("DELETE /v1/stream conv_id=%s -> evict_and_cancel\n" ,conv_id .c_str ()); 570g_stream_sessions .evict_and_cancel (conv_id ); 571auto res = std::make_unique < server_http_res > (); 572res -> status = 204 ; 573res -> content_type = "application/json" ; 574return res ; 575 }; 576} 577 578std::string server_stream_conv_id_from_headers (const std::map < std::string , std::string > & headers ) { 579// case-insensitive scan for x-conversation-id 580static constexpr char target []= "x-conversation-id" ; 581static constexpr size_t target_len = sizeof (target )- 1 ; 582for (const auto & [hk ,hv ] :headers ) { 583if (hk .size ()!= target_len )continue ; 584bool match = true; 585for (size_t i = 0 ;i < target_len ;++ i ) { 586char c = hk [i ]; 587if (c >='A' && c <='Z' )c = char (c + 32 ); 588if (c != target [i ]) {match = false;break ; } 589 } 590if (match ) { 591return hv ; 592 } 593 } 594return std::string (); 595} 596 597static stream_pipe_producer * server_stream_create_spipe (const std::map < std::string , std::string > & headers ) { 598 std::string conversation_id = server_stream_conv_id_from_headers (headers ); 599SRV_TRC ("conv_id=%s (empty=%d)\n" ,conversation_id .c_str (),conversation_id .empty () ?1 :0 ); 600if (conversation_id .empty ()) { 601return nullptr ; 602 } 603auto session = g_stream_sessions .create_or_replace (conversation_id ); 604return stream_pipe_producer::create (session ); 605} 606 607// 608// server_res_spipe 609// 610 611void server_res_spipe::set_req (const server_http_req * req ) { 612this -> req = req ; 613// optionally attach spipe to the response when X-Conversation-Id is present 614spipe .reset (server_stream_create_spipe (req -> headers )); 615} 616 617bool server_res_spipe::conn_alive () { 618GGML_ASSERT (req != nullptr ); 619return !req -> should_stop (); 620} 621 622bool server_res_spipe::should_stop () { 623if (spipe ) { 624// note: if DELETE /v1/stream is called for this conv, is_cancelled() will be true 625return spipe -> is_cancelled (); 626 }else { 627return !conn_alive (); 628 } 629} 630 631void server_res_spipe::on_complete () { 632if (!spipe || next_finished ) { 633return ; 634 } 635// an empty next_orig means set_next() never ran: the request failed before streaming 636// started, typically a params validation throw. evict the session installed by set_req() 637// so the failed request leaves nothing behind for discovery or replay 638if (!next_orig ) { 639g_stream_sessions .evict (server_stream_conv_id_from_headers (req -> headers )); 640return ; 641 } 642 std::string chunk ; 643while (!spipe -> is_cancelled ()) { 644chunk .clear (); 645bool has_next = next_orig (chunk ); 646if (!chunk .empty ()) { 647spipe -> write (chunk .data (),chunk .size ()); 648 } 649if (!has_next ) { 650break ; 651 } 652 } 653} 654 655void server_res_spipe::set_next (std::function < bool (std::string & )> next_fn ) { 656next_orig = std::move (next_fn ); 657next = [this ](std::string & out ) { 658bool has_next = next_orig (out ); 659if (spipe ) { 660// if spipe is set, tee-style pipe input to both HTTP and spipe 661spipe -> write (out .data (),out .size ()); 662 } 663if (!has_next ) { 664next_finished = true; 665 } 666return has_next ; 667 }; 668}