1818from threading import Condition , Lock , Thread
1919from typing import (
2020 Any ,
21+ Callable ,
2122 Dict ,
2223 List ,
2324 Literal ,
@@ -357,6 +358,11 @@ class GenerationContext:
357358 stop_token_sequences : List [List [int ]]
358359 prompt : List [int ]
359360
361+ _should_stop : bool = False
362+
363+ def stop (self ):
364+ self ._should_stop = True
365+
360366
361367@dataclass
362368class Response :
@@ -512,9 +518,14 @@ def get_next_request():
512518 except QueueEmpty :
513519 return None
514520
521+ def progress_callback (info ):
522+ for uid , processed , total in info :
523+ if uid in batch_results :
524+ batch_results [uid ]["rqueue" ].put ((min (processed , total ), total ))
525+
515526 while True :
516527 request = None
517- if not drain_batch and len ( batch_results ) < 100 :
528+ if not drain_batch :
518529 request = get_next_request ()
519530
520531 # We got a request
@@ -554,6 +565,7 @@ def get_next_request():
554565 [rest ], args .max_tokens , caches = [cache ]
555566 )
556567 batch_results [uid ] = {
568+ "ctx" : ctx ,
557569 "cache_key" : prompt [:],
558570 "rqueue" : rqueue ,
559571 "detokenizer" : tokenizer .detokenizer ,
@@ -596,6 +608,7 @@ def get_next_request():
596608 tokenizer .encode ("\n " ),
597609 ],
598610 ),
611+ prompt_progress_callback = progress_callback ,
599612 )
600613 unprocessed_requests .append ((rqueue , request , args ))
601614 continue
@@ -619,43 +632,63 @@ def get_next_request():
619632 drain_batch = False
620633 continue
621634
622- responses = batch_generator .next ()
623- for r in responses :
624- result = batch_results [r .uid ]
625- result ["cache_key" ].append (r .token )
626- result ["detokenizer" ].add_token (r .token )
627-
628- top_tokens = None
629- if args .logprobs > 0 :
630- sorted_indices = mx .argpartition (
631- - gen .logprobs , kth = args .logprobs - 1
632- )
633- top_indices = sorted_indices [: args .logprobs ]
634- top_logprobs = gen .logprobs [top_indices ]
635- top_token_info = zip (
636- top_indices .tolist (), top_logprobs .tolist ()
637- )
638- top_tokens = tuple (top_token_info )
639- result ["rqueue" ].put (
640- Response (
641- result ["detokenizer" ].last_segment ,
642- r .token ,
643- r .logprobs [r .token ].item (),
644- r .finish_reason ,
645- top_tokens ,
635+ uids_to_remove = []
636+ time_budget = 0.5
637+ start = time .time ()
638+ while True :
639+ if time .time () - start > time_budget :
640+ break
641+
642+ responses = batch_generator .next ()
643+ if not responses :
644+ break
645+
646+ for r in responses :
647+ result = batch_results [r .uid ]
648+ result ["cache_key" ].append (r .token )
649+ result ["detokenizer" ].add_token (r .token )
650+
651+ top_tokens = None
652+ if args .logprobs > 0 :
653+ sorted_indices = mx .argpartition (
654+ - gen .logprobs , kth = args .logprobs - 1
655+ )
656+ top_indices = sorted_indices [: args .logprobs ]
657+ top_logprobs = gen .logprobs [top_indices ]
658+ top_token_info = zip (
659+ top_indices .tolist (), top_logprobs .tolist ()
660+ )
661+ top_tokens = tuple (top_token_info )
662+ result ["rqueue" ].put (
663+ Response (
664+ result ["detokenizer" ].last_segment ,
665+ r .token ,
666+ r .logprobs [r .token ].item (),
667+ r .finish_reason ,
668+ top_tokens ,
669+ )
646670 )
647- )
648671
649- if r .finish_reason is not None :
650- result ["rqueue" ].put (None )
651- self .prompt_cache .insert_cache (
652- current_model_key , result ["cache_key" ], r .prompt_cache
653- )
654- del batch_results [r .uid ]
672+ if r .finish_reason is not None :
673+ result ["rqueue" ].put (None )
674+ self .prompt_cache .insert_cache (
675+ current_model_key , result ["cache_key" ], r .prompt_cache
676+ )
677+ del batch_results [r .uid ]
678+
679+ if result ["ctx" ]._should_stop :
680+ uids_to_remove .append (r .uid )
681+
682+ if uids_to_remove :
683+ batch_generator .remove (uids_to_remove )
655684
656685 def _serve_single (self , request ):
657686 rqueue , request , args = request
658687
688+ # Define the progress callback
689+ def progress (tokens_processed , tokens_total ):
690+ rqueue .put ((tokens_processed , tokens_total ))
691+
659692 try :
660693 # Load the model and tokenizer
661694 model , tokenizer = self .model_provider .load (
@@ -724,7 +757,7 @@ def _serve_single(self, request):
724757 prompt_cache = cache ,
725758 draft_model = draft_model ,
726759 num_draft_tokens = args .num_draft_tokens ,
727- # TODO: prompt progress callback
760+ prompt_progress_callback = progress ,
728761 ):
729762 top_tokens = None
730763 if args .logprobs > 0 :
@@ -746,6 +779,10 @@ def _serve_single(self, request):
746779 )
747780 )
748781 cache_key .append (gen .token )
782+
783+ if ctx ._should_stop :
784+ break
785+
749786 rqueue .put (None )
750787
751788 # Save the KV cache again
@@ -760,6 +797,7 @@ def generate(
760797 self ,
761798 request : CompletionRequest ,
762799 generation_args : GenerationArguments ,
800+ progress_callback : Optional [Callable [[int , int ], None ]] = None ,
763801 ):
764802 response_queue = Queue ()
765803 self .requests .put ((response_queue , request , generation_args ))
@@ -771,6 +809,10 @@ def _inner():
771809 break
772810 if isinstance (response , Exception ):
773811 raise response
812+ if isinstance (response , tuple ):
813+ if progress_callback is not None :
814+ progress_callback (* response )
815+ continue
774816 yield response
775817
776818 ctx = response_queue .get ()
@@ -1097,9 +1139,29 @@ def handle_completion(self, request: CompletionRequest, stop_words: List[str]):
10971139 seed = self .seed ,
10981140 )
10991141
1142+ # Create keepalive callback to send SSE comments during long prompt processing
1143+ def keepalive_callback (processed_tokens , total_tokens ):
1144+ logging .info (
1145+ f"Prompt processing progress: { processed_tokens } /{ total_tokens } "
1146+ )
1147+ if self .stream :
1148+ try :
1149+ # Send SSE comment for keepalive - invisible to clients but keeps connection alive
1150+ self .wfile .write (
1151+ f": keepalive { processed_tokens } /{ total_tokens } \n \n " .encode ()
1152+ )
1153+ self .wfile .flush ()
1154+ except (BrokenPipeError , ConnectionResetError , OSError ):
1155+ # Client disconnected, ignore
1156+ pass
1157+
11001158 # Create the token generator
11011159 try :
1102- ctx , response = self .response_generator .generate (request , args )
1160+ ctx , response = self .response_generator .generate (
1161+ request ,
1162+ args ,
1163+ progress_callback = keepalive_callback ,
1164+ )
11031165 except Exception as e :
11041166 self ._set_completion_headers (404 )
11051167 self .end_headers ()
@@ -1160,14 +1222,12 @@ def handle_completion(self, request: CompletionRequest, stop_words: List[str]):
11601222 top_tokens .append (gen .top_tokens )
11611223
11621224 # Check if we should stop early
1163- # TODO: This doesn't actually stop generation in the generation
1164- # thread we should probably have a way to do that via the ctx
1165- # object
11661225 stop_condition = stopping_criteria (
11671226 tokens , ctx .stop_token_sequences , stop_words , ctx .eos_token_id
11681227 )
11691228 if stop_condition .stop_met :
11701229 finish_reason = "stop"
1230+ ctx .stop ()
11711231 tokens = tokens [: len (tokens ) - stop_condition .trim_length ]
11721232 text = text [: len (text ) - stop_condition .trim_text_length ]
11731233 segment = ""
0 commit comments