Summary
generate_step() and speculative_generate_step() default quantized_kv_start
to 0, while the CLI (mlx_lm.generate, mlx_lm.cache_prompt) ships
DEFAULT_QUANTIZED_KV_START = 5000. So the same arguments behave differently
depending on the entry point: a library caller passing kv_bits=8 without also
passing quantized_kv_start quantizes the KV cache from the very first token,
where the same request through the CLI would not quantize until step 5000.
Measurement
mlx-community/Qwen3-0.6B-4bit, kv_bits=8, 8-token prompt, median of 3,
Apple M4 Pro (14-core), macOS 26.6:
| new tokens |
start=0 (library) |
start=5000 (CLI) |
speed delta |
peak mem start=0 |
peak mem start=5000 |
| 64 |
325.4 tok/s |
375.4 tok/s |
+15.4% |
367.2 MB |
367.7 MB |
| 128 |
314.2 tok/s |
382.3 tok/s |
+21.7% |
367.2 MB |
367.7 MB |
| 256 |
336.3 tok/s |
381.9 tok/s |
+13.6% |
367.4 MB |
421.7 MB |
| 512 |
335.9 tok/s |
368.1 tok/s |
+9.6% |
371.2 MB |
471.7 MB |
| 1024 |
322.8 tok/s |
341.9 tok/s |
+5.9% |
408.2 MB |
576.8 MB |
This is a genuine trade-off, not a free win, and I want to be explicit about
that. Quantizing from step 0 costs 6-22% decode throughput, but below ~256
generated tokens it saves essentially no memory (367 MB either way), and above
that it does save real memory — 169 MB at 1024 tokens.
So 0 is a defensible memory-first default in isolation. What seems wrong is
that the library and the CLI disagree while sharing a constant that documents
the intended value.
Question for maintainers
Two ways to close the gap, and the choice is a policy call that is yours:
- Align the library defaults to
DEFAULT_QUANTIZED_KV_START — consistent
with the CLI, faster by default, at the cost of more memory for long
generations. This changes behaviour for existing library callers who pass
kv_bits= and rely on the current 0.
- Leave the defaults and document the difference — no behaviour change,
but the inconsistency stays and remains easy to trip over.
I have (1) implemented with tests if that is the direction you want; happy to
switch to (2) instead.
Summary
generate_step()andspeculative_generate_step()defaultquantized_kv_startto
0, while the CLI (mlx_lm.generate,mlx_lm.cache_prompt) shipsDEFAULT_QUANTIZED_KV_START = 5000. So the same arguments behave differentlydepending on the entry point: a library caller passing
kv_bits=8without alsopassing
quantized_kv_startquantizes the KV cache from the very first token,where the same request through the CLI would not quantize until step 5000.
Measurement
mlx-community/Qwen3-0.6B-4bit,kv_bits=8, 8-token prompt, median of 3,Apple M4 Pro (14-core), macOS 26.6:
start=0(library)start=5000(CLI)start=0start=5000This is a genuine trade-off, not a free win, and I want to be explicit about
that. Quantizing from step 0 costs 6-22% decode throughput, but below ~256
generated tokens it saves essentially no memory (367 MB either way), and above
that it does save real memory — 169 MB at 1024 tokens.
So
0is a defensible memory-first default in isolation. What seems wrong isthat the library and the CLI disagree while sharing a constant that documents
the intended value.
Question for maintainers
Two ways to close the gap, and the choice is a policy call that is yours:
DEFAULT_QUANTIZED_KV_START— consistentwith the CLI, faster by default, at the cost of more memory for long
generations. This changes behaviour for existing library callers who pass
kv_bits=and rely on the current0.but the inconsistency stays and remains easy to trip over.
I have (1) implemented with tests if that is the direction you want; happy to
switch to (2) instead.