Skip to content

Commit 088e7ad

Browse files
authored
Allow providing prompt caches in batched generation (ml-explore#602)
1 parent 1d01257 commit 088e7ad

4 files changed

Lines changed: 401 additions & 27 deletions

File tree

mlx_lm/examples/batch_generate_response.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,26 @@
2626
]
2727

2828
# Set `verbose=True` to see generation statistics
29-
result = batch_generate(model, tokenizer, prompts, verbose=False, max_tokens=128)
29+
result = batch_generate(
30+
model, tokenizer, prompts, verbose=False, return_prompt_caches=True
31+
)
32+
print(result.texts[-1])
3033

31-
# The returned result contains texts completions in the same order as prompts
32-
print(result.texts[0])
34+
prompts = [
35+
"Could you summarize that?",
36+
"And what about the sea?",
37+
"Try again?",
38+
"And Mt Olympus?",
39+
]
40+
prompts = [
41+
tokenizer.apply_chat_template(
42+
[{"role": "user", "content": p}],
43+
add_generation_prompt=True,
44+
)
45+
for p in prompts
46+
]
47+
48+
result = batch_generate(
49+
model, tokenizer, prompts, verbose=False, prompt_caches=result.caches
50+
)
51+
print(result.texts[-1])

mlx_lm/generate.py

Lines changed: 108 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
import time
99
from dataclasses import dataclass
10+
from functools import partial
1011
from 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
790797
class 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

859870
def _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+
887914
class 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

11171201
def main():

0 commit comments

Comments
 (0)