77import sys
88import time
99from dataclasses import dataclass
10+ from functools import partial
1011from typing import (
1112 Any ,
1213 Callable ,
@@ -786,6 +787,12 @@ def _left_pad_prompts(prompts, max_length=None):
786787 return mx .array ([[0 ] * (max_length - len (p )) + p for p in prompts ])
787788
788789
790+ def _right_pad_prompts (prompts , max_length = None ):
791+ if max_length is None :
792+ max_length = max (len (p ) for p in prompts )
793+ return mx .array ([p + [0 ] * (max_length - len (p )) for p in prompts ])
794+
795+
789796@dataclass
790797class BatchStats :
791798 """
@@ -822,6 +829,7 @@ class BatchResponse:
822829
823830 texts : List [str ]
824831 stats : BatchStats
832+ caches : Optional [List [List [Any ]]]
825833
826834
827835@dataclass
@@ -855,6 +863,9 @@ def extend(self, other):
855863 for c , o in zip (self .cache , other .cache ):
856864 c .extend (o )
857865
866+ def extract_cache (self , idx ):
867+ return [c .extract (idx ) for c in self .cache ]
868+
858869
859870def _make_cache (model , left_padding ):
860871 """
@@ -884,6 +895,22 @@ def to_batch_cache(c):
884895 return [BatchKVCache (left_padding ) for _ in model .layers ]
885896
886897
898+ def _merge_caches (caches ):
899+ batch_cache = []
900+ for i in range (len (caches [0 ])):
901+ cache = None
902+ if isinstance (caches [0 ][i ], KVCache ):
903+ cache = BatchKVCache .merge ([c [i ] for c in caches ])
904+ elif isinstance (caches [0 ][i ], RotatingKVCache ):
905+ cache = BatchRotatingKVCache .merge ([c [i ] for c in caches ])
906+ else :
907+ raise ValueError (
908+ f"{ type (caches [0 ][i ])} does not yet support batching with history"
909+ )
910+ batch_cache .append (cache )
911+ return batch_cache
912+
913+
887914class BatchGenerator :
888915
889916 @dataclass
@@ -892,6 +919,7 @@ class Response:
892919 token : int
893920 logprobs : mx .array
894921 finish_reason : Optional [str ]
922+ prompt_cache : Callable [[], List [Any ]]
895923
896924 def __init__ (
897925 self ,
@@ -911,44 +939,85 @@ def __init__(
911939 self .uid_count = 0
912940 self .prefill_step_size = prefill_step_size
913941 self .prefill_batch_size = prefill_batch_size
914- self .completion_batch_size = completion_batch_size
942+ self .completion_batch_size = max ( completion_batch_size , prefill_batch_size )
915943 self ._stats = BatchStats ()
916944
917945 self .active_batch = None
918946
919- def insert (self , prompts , max_tokens : Union [List [int ], int , None ] = None ):
947+ def insert (
948+ self , prompts , max_tokens : Union [List [int ], int , None ] = None , caches = None
949+ ):
920950 uids = []
921951
922952 if max_tokens is None or isinstance (max_tokens , int ):
923953 max_tokens = [max_tokens or self .max_tokens ] * len (prompts )
924954
925- for p , m in zip (prompts , max_tokens ):
926- self .unprocessed_prompts .append ((self .uid_count , p , m ))
955+ if caches is None :
956+ caches = [None ] * len (prompts )
957+ for i in range (len (prompts )):
958+ if caches [i ] is None :
959+ caches [i ] = cache .make_prompt_cache (self .model )
960+
961+ for p , m , c in zip (prompts , max_tokens , caches ):
962+ self .unprocessed_prompts .append ((self .uid_count , p , m , c ))
927963 uids .append (self .uid_count )
928964 self .uid_count += 1
929965 # Sort in ascending order of length
930966 self .unprocessed_prompts = sorted (
931- self .unprocessed_prompts , key = lambda x : len (x [1 ])
967+ self .unprocessed_prompts , key = lambda x : len (x [1 ]) + cache . cache_length ( x [ 3 ])
932968 )
933969 return uids
934970
935971 def _process_prompts (self , prompts ):
936- uids , inputs , max_tokens = zip (* prompts )
972+ uids , inputs , max_tokens , caches = zip (* prompts )
973+
974+ cache_lengths = [cache .cache_length (c ) for c in caches ]
975+ max_cache_length = max (cache_lengths )
937976 lengths = [len (p ) for p in inputs ]
938977 max_length = max (lengths )
939- batch_size = self .prefill_batch_size
940- self ._stats .prompt_tokens += sum (lengths )
941- left_padding = [max_length - l for l in lengths ]
942- inputs = _left_pad_prompts (inputs , max_length = max_length )
978+ padding = [max_length - l for l in lengths ]
943979
944- prompt_cache = _make_cache ( self . model , left_padding )
980+ self . _stats . prompt_tokens += sum ( lengths )
945981
946- while inputs .shape [1 ] > 1 :
947- n_to_process = min (self .prefill_step_size , inputs .shape [1 ] - 1 )
948- self .model (inputs [:, :n_to_process ], cache = prompt_cache )
982+ # New prompts so
983+ # 1. Left-pad the inputs
984+ # 2. Process
985+ if max_cache_length == 0 :
986+ inputs = _left_pad_prompts (inputs , max_length = max_length )
987+ prompt_cache = _make_cache (self .model , padding )
988+
989+ while inputs .shape [1 ] > 1 :
990+ n_to_process = min (self .prefill_step_size , inputs .shape [1 ] - 1 )
991+ self .model (inputs [:, :n_to_process ], cache = prompt_cache )
992+ mx .eval ([c .state for c in prompt_cache ])
993+ inputs = inputs [:, n_to_process :]
994+ mx .clear_cache ()
995+
996+ # Further prompt processing so we need to
997+ # 1. Merge the KV caches and prepare for right padded prompts
998+ # 2. Right pad the inputs
999+ # 2. Process
1000+ # 3. Finalize the KV caches so they are left padded again
1001+ else :
1002+ last_inputs = mx .array ([p [- 1 :] for p in inputs ])
1003+ inputs = _right_pad_prompts (inputs , max_length = max_length )
1004+ prompt_cache = _merge_caches (caches )
1005+
1006+ for c in prompt_cache :
1007+ c .prepare (lengths = lengths , right_padding = padding )
1008+
1009+ while inputs .shape [1 ] > 1 :
1010+ n_to_process = min (self .prefill_step_size , inputs .shape [1 ] - 1 )
1011+ self .model (inputs [:, :n_to_process ], cache = prompt_cache )
1012+ mx .eval ([c .state for c in prompt_cache ])
1013+ inputs = inputs [:, n_to_process :]
1014+ mx .clear_cache ()
1015+
1016+ for c in prompt_cache :
1017+ c .finalize ()
9491018 mx .eval ([c .state for c in prompt_cache ])
950- inputs = inputs [:, n_to_process :]
9511019 mx .clear_cache ()
1020+ inputs = last_inputs
9521021
9531022 y , logprobs = self ._step (inputs , prompt_cache )
9541023 mx .async_eval (y , logprobs )
@@ -1026,6 +1095,7 @@ def _next(self):
10261095 for e , (t , uid , num_tok , max_tok ) in enumerate (
10271096 zip (y , batch .uids , batch .num_tokens , batch .max_tokens )
10281097 ):
1098+ cache = None
10291099 num_tok += 1
10301100 batch .num_tokens [e ] = num_tok
10311101 if t in self .stop_tokens :
@@ -1037,7 +1107,9 @@ def _next(self):
10371107 else :
10381108 finish_reason = None
10391109 keep_idx .append (e )
1040- responses .append (self .Response (uid , t , logprobs [e ], finish_reason ))
1110+ if finish_reason is not None :
1111+ cache = batch .extract_cache (e )
1112+ responses .append (self .Response (uid , t , logprobs [e ], finish_reason , cache ))
10411113
10421114 # Remove any finished completions
10431115 if len (end_idx ):
@@ -1058,8 +1130,10 @@ def batch_generate(
10581130 model ,
10591131 tokenizer ,
10601132 prompts : List [int ],
1133+ prompt_caches : Optional [List [List [Any ]]] = None ,
10611134 max_tokens : Union [int , List [int ]] = 128 ,
10621135 verbose : bool = False ,
1136+ return_prompt_caches : bool = False ,
10631137 ** kwargs ,
10641138) -> BatchResponse :
10651139 """
@@ -1069,10 +1143,15 @@ def batch_generate(
10691143 model (nn.Module): The language model.
10701144 tokenizer (PreTrainedTokenizer): The tokenizer.
10711145 prompt (List[List[int]]): The input prompts.
1146+ prompt_caches (List[List[Any]], optional): Pre-computed prompt-caches
1147+ for each input prompt. Note, unlike ``generate_step``, the caches
1148+ won't be updated in-place.
10721149 verbose (bool): If ``True``, print tokens and timing information.
10731150 Default: ``False``.
10741151 max_tokens (Union[int, List[int]): Maximum number of output tokens. This
10751152 can be per prompt if a list is provided.
1153+ return_prompt_caches (bool): Return the prompt caches in the batch
1154+ responses. Default: ``False``.
10761155 kwargs: The remaining options get passed to :obj:`BatchGenerator`.
10771156 See :obj:`BatchGenerator` for more details.
10781157 """
@@ -1084,16 +1163,20 @@ def batch_generate(
10841163 print (f"[batch_generate] Finished processing 0/{ num_samples } ..." , end = "\r " )
10851164
10861165 with wired_limit (model , [generation_stream ]):
1087- uids = gen .insert (prompts , max_tokens )
1166+ uids = gen .insert (prompts , max_tokens , caches = prompt_caches )
10881167 results = {uid : [] for uid in uids }
1168+ prompt_caches = {}
10891169 while responses := gen .next ():
10901170 for r in responses :
1091- if verbose and r .finish_reason != None :
1092- fin += 1
1093- print (
1094- f"[batch_generate] Finished processing { fin } /{ num_samples } ..." ,
1095- end = "\r " ,
1096- )
1171+ if r .finish_reason is not None :
1172+ if return_prompt_caches :
1173+ prompt_caches [r .uid ] = r .prompt_cache
1174+ if verbose :
1175+ fin += 1
1176+ print (
1177+ f"[batch_generate] Finished processing { fin } /{ num_samples } ..." ,
1178+ end = "\r " ,
1179+ )
10971180 if r .finish_reason != "stop" :
10981181 results [r .uid ].append (r .token )
10991182 if verbose :
@@ -1102,6 +1185,7 @@ def batch_generate(
11021185 # Return results in correct order
11031186 texts = [tokenizer .decode (results [uid ]) for uid in uids ]
11041187 stats = gen .stats ()
1188+ caches = [prompt_caches [uid ] for uid in uids ] if return_prompt_caches else None
11051189 if verbose :
11061190 print (
11071191 f"[batch_generate] Prompt: { stats .prompt_tokens } tokens, { stats .prompt_tps :.3f} tokens-per-sec"
@@ -1111,7 +1195,7 @@ def batch_generate(
11111195 f"{ stats .generation_tps :.3f} tokens-per-sec"
11121196 )
11131197 print (f"[batch_generate] Peak memory: { stats .peak_memory :.3f} GB" )
1114- return BatchResponse (texts , stats )
1198+ return BatchResponse (texts , stats , caches )
11151199
11161200
11171201def main ():
0 commit comments