You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
MLX_KV_BITS=4 degrades agentic tool calling far more than "occasional garbled tool calls" — and because the prompt cache is reused, the damage persists for the life of the server process.
I hit this while tuning for memory headroom on a 32 GB machine, where 4-bit KV looks like an easy ~5 GB win. It isn't. Sharing the numbers because the failure is quiet and easy to attribute to "local models are just flaky."
Caveat up front: I tested divinetribe/Hermes-4-14B-abliterated-4bit-mlx, not the Qwen 2.5 Coder 14B that Claude Agentico.command ships with. I have not verified Qwen shows the same behaviour. Flagging it because Agentico defaults to MLX_KV_BITS=4 with MLX_KV_QUANT_START=0 — quantizing from token 0, i.e. more aggressive than the config I measured.
Numbers
scripts/test_mlx_server.py, same machine, same model, same build — only MLX_KV_BITS changed. Fresh server per sweep, multiple runs because the sampler makes single runs noisy:
MLX_KV_BITS
Passed / 14, across runs
0
13, 13, 13, 12
4
10, 5, 7, 3
4-bit isn't just lower, it's unstable — 3 to 10 out of 14.
The failure mode matters more than the pass rate
Dominant error is Tool 'Bash' has empty arguments — 12, 20, and 14 occurrences in the three 4-bit runs. The model emits a syntactically valid tool call with the right name and an empty argument object.
Critically, once these start they largely don't stop. Run 1, in execution order:
✗ Simple Bash command — Expected tool 'Bash' but got: ['Glob']
✓ Create directory with mkdir
✗ Read a file — Expected tool 'Read' but got: ['Bash']
✓ Complex Bash with pipes
✓ Edit a file
✓ Multi-tool: find then read
✓ Rapid-fire #1: Run: echo 'test 1'
✗ Rapid-fire #2: Run: echo 'test 2' — Tool 'Bash' has empty arguments
✗ Rapid-fire #3: Run: pwd — Tool 'Bash' has empty arguments
✗ Rapid-fire #4: Run: date — Tool 'Bash' has empty arguments
✗ Rapid-fire #5: Run: whoami — Tool 'Bash' has empty arguments
✗ Calendar Step 1: Create month folders — Tool 'Bash' has empty arguments
✗ Calendar Step 2: Delete all months except September — Tool 'Bash' has empty arguments
✗ Calendar Step 3: Verify remaining folders — Tool 'Bash' has empty arguments
Seven healthy requests, then every subsequent one fails the same way. Run 3 degrades from the second request and never recovers (3/14). The same suite at MLX_KV_BITS=0 on the same server:
✗ Simple Bash command — Expected tool 'Bash' but got: ['Glob'] <- assertion artifact, Glob is a fine answer here
✓ Create directory with mkdir
✓ Read a file
✓ Complex Bash with pipes
✓ Edit a file
✓ Multi-tool: find then read
✓ Rapid-fire #1..#5
✓ Calendar Step 1: Create month folders
✓ Calendar Step 2: Delete all months except September
Why it persists
server.py keeps a single module-level _prompt_cache and reuses KV state across requests — that's the point of the caching design, and the logs confirm it (Cache hit: 727 reused, 390 new tokens to prefill). With quantization on, error introduced into that cache is carried into every subsequent request rather than discarded. So this is not per-request numeric noise you can retry your way out of; it's state that goes bad and stays bad until the process restarts.
Worth noting recover_garbled_tool_json / MAX_TOOL_RETRIES doesn't help here: the JSON isn't garbled. It parses cleanly into {}. The retry path keys off tool-intent phrases in unparsed text, so a well-formed call with empty arguments sails straight through.
Evidence this is cache-state rather than sampling: 16 identical single-shot requests against a warm 4-bit server (same 6-tool schema, same prompt shape) returned Bash with correct arguments 16/16. The corruption only shows up once a session has accumulated enough reused cache.
It also doesn't buy what you'd expect
Not faster. 44.4s vs 38.5s total across the suite (mean 3.2s vs 2.8s per turn). Quantize/dequantize overhead exceeds the bandwidth saving at these context lengths.
No memory saved at realistic prompt sizes. 8222 MB vs 8229 MB resident (footprint -p). With MLX_KV_QUANT_START=256 and ~700–1100 token prompts the cache is small either way. For this model (40 layers, 8 KV heads, head_dim 128) the KV cache is only 0.67 GB at 4K context; the ~5 GB saving needs something near the full 40K.
So on a 32 GB machine the trade is strictly bad: no speed, no meaningful memory saved, materially worse tool calling.
On Claude Agentico.command
The launcher's stated rationale is sound — Claude Code's ~5860-token system prompt does make the KV cache the dominant consumer, and kIOGPUCommandBufferCallbackErrorOutOfMemory is a real failure on 16 GB. I'm not arguing the default is wrong for 16 GB, where the alternative may be not running at all.
But the header currently sets expectations at "occasional garbled tool calls," and at least on Hermes the actual behaviour is closer to "tool arguments stop being populated partway through a session and don't come back." If Qwen 2.5 Coder behaves similarly, that's worth saying more loudly in the comment, since the symptom in a real session looks like the model getting confused rather than a tuning knob misfiring.
Possible directions, in rough order of effort:
Document the trade-off honestly in the Agentico header, and note that MLX_KV_BITS=0 is strongly preferred wherever RAM allows.
Drop MLX_KV_BITS to 0 by default on machines with headroom — the launcher could pick from hw.memsize the way setup.sh already picks a model.
Invalidate or rebuild the prompt cache periodically when KV_BITS is on, trading some prefill cost for bounded error accumulation.
Add an empty-arguments check to the retry path: a tool_use block whose input is {} for a tool with required properties is almost certainly a miss and could trigger the existing retry.
Happy to test any of these on Hermes, and to re-run the sweep against Qwen 2.5 Coder 14B to confirm whether Agentico's shipped default is affected — just say which would be useful.
MLX_KV_BITS=4degrades agentic tool calling far more than "occasional garbled tool calls" — and because the prompt cache is reused, the damage persists for the life of the server process.I hit this while tuning for memory headroom on a 32 GB machine, where 4-bit KV looks like an easy ~5 GB win. It isn't. Sharing the numbers because the failure is quiet and easy to attribute to "local models are just flaky."
Caveat up front: I tested
divinetribe/Hermes-4-14B-abliterated-4bit-mlx, not the Qwen 2.5 Coder 14B thatClaude Agentico.commandships with. I have not verified Qwen shows the same behaviour. Flagging it because Agentico defaults toMLX_KV_BITS=4withMLX_KV_QUANT_START=0— quantizing from token 0, i.e. more aggressive than the config I measured.Numbers
scripts/test_mlx_server.py, same machine, same model, same build — onlyMLX_KV_BITSchanged. Fresh server per sweep, multiple runs because the sampler makes single runs noisy:MLX_KV_BITS4-bit isn't just lower, it's unstable — 3 to 10 out of 14.
The failure mode matters more than the pass rate
Dominant error is
Tool 'Bash' has empty arguments— 12, 20, and 14 occurrences in the three 4-bit runs. The model emits a syntactically valid tool call with the right name and an empty argument object.Critically, once these start they largely don't stop. Run 1, in execution order:
Seven healthy requests, then every subsequent one fails the same way. Run 3 degrades from the second request and never recovers (3/14). The same suite at
MLX_KV_BITS=0on the same server:Why it persists
server.pykeeps a single module-level_prompt_cacheand reuses KV state across requests — that's the point of the caching design, and the logs confirm it (Cache hit: 727 reused, 390 new tokens to prefill). With quantization on, error introduced into that cache is carried into every subsequent request rather than discarded. So this is not per-request numeric noise you can retry your way out of; it's state that goes bad and stays bad until the process restarts.Worth noting
recover_garbled_tool_json/MAX_TOOL_RETRIESdoesn't help here: the JSON isn't garbled. It parses cleanly into{}. The retry path keys off tool-intent phrases in unparsed text, so a well-formed call with empty arguments sails straight through.Evidence this is cache-state rather than sampling: 16 identical single-shot requests against a warm 4-bit server (same 6-tool schema, same prompt shape) returned
Bashwith correct arguments 16/16. The corruption only shows up once a session has accumulated enough reused cache.It also doesn't buy what you'd expect
footprint -p). WithMLX_KV_QUANT_START=256and ~700–1100 token prompts the cache is small either way. For this model (40 layers, 8 KV heads, head_dim 128) the KV cache is only 0.67 GB at 4K context; the ~5 GB saving needs something near the full 40K.So on a 32 GB machine the trade is strictly bad: no speed, no meaningful memory saved, materially worse tool calling.
On
Claude Agentico.commandThe launcher's stated rationale is sound — Claude Code's ~5860-token system prompt does make the KV cache the dominant consumer, and
kIOGPUCommandBufferCallbackErrorOutOfMemoryis a real failure on 16 GB. I'm not arguing the default is wrong for 16 GB, where the alternative may be not running at all.But the header currently sets expectations at "occasional garbled tool calls," and at least on Hermes the actual behaviour is closer to "tool arguments stop being populated partway through a session and don't come back." If Qwen 2.5 Coder behaves similarly, that's worth saying more loudly in the comment, since the symptom in a real session looks like the model getting confused rather than a tuning knob misfiring.
Possible directions, in rough order of effort:
MLX_KV_BITS=0is strongly preferred wherever RAM allows.MLX_KV_BITSto 0 by default on machines with headroom — the launcher could pick fromhw.memsizethe waysetup.shalready picks a model.KV_BITSis on, trading some prefill cost for bounded error accumulation.tool_useblock whoseinputis{}for a tool with required properties is almost certainly a miss and could trigger the existing retry.Happy to test any of these on Hermes, and to re-run the sweep against Qwen 2.5 Coder 14B to confirm whether Agentico's shipped default is affected — just say which would be useful.
Environment
mlx0.32.0,mlx-lm0.31.3divinetribe/Hermes-4-14B-abliterated-4bit-mlx(Qwen3 arch, 40 layers, 40K context)MLX_KV_QUANT_START=256for the 4-bit runsproxy/server.pyat a88bc96, plus the two fixes in fix: stop generation on the tokenizer's real EOS, and tolerate empty env ints #41 (without the EOS fix in that PR, this model can't complete the suite at all, so the comparison isn't runnable on a clean checkout)