Skip to content

Commit d4eb136

Browse files
authored
Bring back max-kv-size to the batch generator (ml-explore#1106)
1 parent 4469ad4 commit d4eb136

3 files changed

Lines changed: 75 additions & 8 deletions

File tree

mlx_lm/generate.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1498,6 +1498,7 @@ def __init__(
14981498
completion_batch_size: int = 32,
14991499
prefill_batch_size: int = 8,
15001500
prefill_step_size: int = 2048,
1501+
max_kv_size: Optional[int] = None,
15011502
):
15021503
self.model = model
15031504
self.max_tokens = max_tokens
@@ -1507,6 +1508,7 @@ def __init__(
15071508
self.prefill_step_size = prefill_step_size
15081509
self.prefill_batch_size = prefill_batch_size
15091510
self.completion_batch_size = max(completion_batch_size, prefill_batch_size)
1511+
self.max_kv_size = max_kv_size
15101512

15111513
self._default_state_machine = SequenceStateMachine(
15121514
{"normal": [(seq, None) for seq in stop_tokens]} if stop_tokens else {},
@@ -1613,7 +1615,7 @@ def insert_segments(
16131615
caches = caches or [None] * len(segments)
16141616
for i in range(len(segments)):
16151617
if caches[i] is None:
1616-
caches[i] = cache.make_prompt_cache(self.model)
1618+
caches[i] = self._make_new_cache()
16171619

16181620
for seq, m, c, at, s, lp, sm in zip(
16191621
segments,
@@ -1636,6 +1638,19 @@ def insert_segments(
16361638

16371639
return uids
16381640

1641+
def _make_new_cache(self):
1642+
if self.max_kv_size is None:
1643+
return cache.make_prompt_cache(self.model)
1644+
1645+
return [
1646+
(
1647+
RotatingKVCache(max_size=self.max_kv_size)
1648+
if isinstance(ci, KVCache)
1649+
else ci
1650+
)
1651+
for ci in cache.make_prompt_cache(self.model)
1652+
]
1653+
16391654
def _find_uids(self, uids):
16401655
uids = set(uids)
16411656
results = {}

mlx_lm/server.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,12 +1345,13 @@ def generate_response(
13451345
# Add dynamic response
13461346
if self.object_type.startswith("chat.completion"):
13471347
key_name = "delta" if self.stream else "message"
1348-
choice[key_name] = {
1349-
"role": "assistant",
1350-
"content": text,
1351-
"reasoning": reasoning_text,
1352-
"tool_calls": tool_calls,
1353-
}
1348+
choice[key_name] = {"role": "assistant"}
1349+
if text:
1350+
choice[key_name]["content"] = text
1351+
if reasoning_text:
1352+
choice[key_name]["reasoning"] = reasoning_text
1353+
if tool_calls:
1354+
choice[key_name]["tool_calls"] = tool_calls
13541355
elif self.object_type == "text_completion":
13551356
choice.update(text=text)
13561357
else:

tests/test_generate.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
generate_step,
1616
stream_generate,
1717
)
18-
from mlx_lm.models.cache import RotatingKVCache
18+
from mlx_lm.models.cache import KVCache, RotatingKVCache
1919
from mlx_lm.sample_utils import make_logits_processors, make_sampler
2020
from mlx_lm.utils import load
2121

@@ -719,6 +719,57 @@ def test_remove_prompt_batch_updates_currently_processing(self):
719719
if all(r.finish_reason is not None for r in responses):
720720
break
721721

722+
def test_batch_max_kv_size_creates_rotating_cache(self):
723+
max_kv_size = 256
724+
gen = BatchGenerator(
725+
self.model,
726+
max_tokens=1,
727+
max_kv_size=max_kv_size,
728+
)
729+
730+
prompt = self.tokenizer.encode("Write a long story about a cat")
731+
gen.insert([prompt])
732+
733+
for r in gen.next_generated():
734+
if r.finish_reason is not None:
735+
for cache in r.prompt_cache:
736+
self.assertIsInstance(cache, RotatingKVCache)
737+
self.assertEqual(cache.max_size, max_kv_size)
738+
739+
def test_batch_max_kv_size_limits_cache_growth(self):
740+
max_kv_size = 5
741+
gen = BatchGenerator(
742+
self.model,
743+
max_tokens=10,
744+
max_kv_size=max_kv_size,
745+
prefill_batch_size=1,
746+
prefill_step_size=128,
747+
completion_batch_size=1,
748+
)
749+
750+
prompt = self.tokenizer.encode("Write a long story about a cat")
751+
gen.insert([prompt])
752+
753+
for r in gen.next_generated():
754+
if r.finish_reason is not None:
755+
for cache in r.prompt_cache:
756+
self.assertLessEqual(cache.keys.shape[2], max_kv_size)
757+
758+
def test_batch_max_kv_size_none_creates_regular_cache(self):
759+
gen = BatchGenerator(
760+
self.model,
761+
max_tokens=1,
762+
max_kv_size=None,
763+
)
764+
765+
prompt = self.tokenizer.encode("Write a long story about a cat")
766+
gen.insert([prompt])
767+
768+
for r in gen.next_generated():
769+
if r.finish_reason is not None:
770+
for cache in r.prompt_cache:
771+
self.assertIsInstance(cache, KVCache)
772+
722773

723774
if __name__ == "__main__":
724775
unittest.main()

0 commit comments

Comments
 (0)