Agent Memory Below the Prompt: Persistent Q4 KV Cache for Multi-Agent LLM Inference on Edge Devices
Abstract
Multi-agent LLM systems on edge devices face a memory management problem: device RAM is too small to hold every agent's KV cache simultaneously. On Apple M4 Pro with 10.2 GB of cache budget, only 3 agents fit at 8K context in FP16. A 10-agent workflow must constantly evict and reload caches. Without persistence, every eviction forces a full re-prefill through the model -- 15.7 seconds per agent at 4K context. We address this by persisting each agent's KV cache to disk in 4-bit quantized format and reloading it directly into the attention layer, eliminating redundant O(n) prefill computation via direct cache restoration. The system comprises three components: a block pool providing per-agent isolated Q4 KV caches in safetensors format, a BatchQuantizedKVCache for concurrent inference over multiple agents' quantized caches, and cross-phase context injection that accumulates attention state across conversation phases without re-computation. Evaluated on three architectures (Gemma 3 12B, dense GQA, 48 layers; DeepSeek-Coder-V2-Lite 16B, MoE MLA, 27 layers; Llama 3.1 8B, dense GQA, 32 layers), cache restoration reduces time-to-first-token by up to 136x (Gemma: 22--136x at 4K--32K; DeepSeek: 11--76x at 4K--32K; Llama: 24--111x at 4K--16K; 3--10x at 1K). Q4 quantization fits 4x more agent contexts into fixed device memory than FP16. Perplexity measured with actual Q4 KV caches shows -0.7% for Gemma, +2.8% for Llama, and +3.0% for DeepSeek. Open-source at https://github.com/yshk-mxim/agent-memory
Figures & tables
| Device | Mem | BW | SSD | Type |
| (GB) | (GB/s) | (GB/s) | ||
| M4 Pro (MacBook Pro) | 24 | 273 | 7 | Unified |
| M4 Max (MacBook) | 128 | 546 | 7 | Unified |
| DGX Spark | 128 | 273 | 11 | Unified |
| RTX 5090 (VRAM) | 32 | 1792 | 64 ∗ | Discrete |
| RTX 4090 (VRAM) | 24 | 1008 | 32 ∗ | Discrete |
| Context | FP16/agent | Q4/agent | FP16 fits | Q4 fits |
|---|---|---|---|---|
| 4K | 1.5 GB | 0.42 GB | 6 | 24 |
| 8K | 3.0 GB | 0.84 GB | 3 | 12 |
| 16K | 6.0 GB | 1.7 GB | 1 | 6 |
| 32K | 12.0 GB | 3.4 GB | 0 | 3 |
| Model | Cache | 1K | 2K | 4K | 8K | 16K | 32K |
|---|---|---|---|---|---|---|---|
| Gemma 3 | Cold | 3964 | 7119 | 15736 | 35009 | 74219 | 172096 |
| Warm | 475 | 495 | 577 | 626 | 795 | 1819 | |
| Hot | 683 | 709 | 719 | 807 | 934 | 1264 | |
| DeepSeek | Cold | 1043 | 1737 | 3970 | 8296 | 19396 | 47315 |
| Warm | 234 | 246 | 271 | 338 | 434 | 633 | |
| Hot | 345 | 368 | 366 | 386 | 490 | 624 |
| Gemma 3 | DeepSeek | Llama 3.1 | |||||
|---|---|---|---|---|---|---|---|
| Context | Cache | SysTPS | Per | SysTPS | Per | SysTPS | Per |
| 1K | Cold | 10.4 | 5.2 | 35.5 | 17.8 | 16.6 | 8.3 |
| 1K | Warm | 22.6 | 11.2 | 63.0 | 31.5 | 37.8 | 18.9 |
| 1K | Hot | 22.1 | 11.1 | 73.1 | 36.6 | 38.3 | 19.1 |
| 4K | Cold | 3.5 | 1.8 | 13.5 | 6.8 | 5.9 | 2.9 |
| 4K | Warm | 19.6 | 9.9 | 52.6 | 26.3 | 34.5 | 17.2 |
| agent-memory (Q4 KV) | vllm-mlx (FP16 KV) | |||||
| Context | Cold | Warm | Restart | Cold | Prefix | Restart |
| 1K | 2,500 | 260 | 260 | 2,289 | 273 | 2,262 |
| 2K | 4,530 | 274 | 274 | 2,204 | 204 | 4,134 |
| 4K | 10,235 | 290 | 290 | 4,394 | 290 | 8,079 |
| 8K | 21,536 | 331 | 331 | 9,472 † | 244 † | — |
| 16K | 47,629 | 431 | 431 | 37,935 † | 371 ‡ | — |
| Component | Metric | With | Without | Effect |
| Persistence | TTFT (ms), Gemma 4K | 577 | 15736 | 27 |
| Q4 vs FP16 | Agents at 8K | 12 | 3 | 4.0 |
| Batching | SysTPS, Gemma 1K warm | 22.6 | 11.2 ∗ | 2.0 |
| Cross-phase | TTFT (ms), Phase 5 | 1705 | 3292 | 1.9 |
| ∗ Per-agent TPS = SysTPS/2. The 2.0 is definitional (system vs per-agent), | ||||
| not a measured throughput gain over batch=1. | ||||
| Gemma 3 | DeepSeek | |||||
| Phase | Cold | Pers | Cold | Pers | ||
| 1: Interrogation A | 1136 | 1079 | 1.1 | 477 | 460 | 1.0 |
| 2: Interrogation B | 1119 | 976 | 1.1 | 465 | 430 | 1.1 |
| 3: The Yard | 1648 | 1019 | 1.6 | 532 | 474 | 1.1 |
| 4: Final Reckoning | 2195 | 1250 | 1.8 | 664 | 542 | 1.2 |
| 5: Verdict | 3292 | 1705 | 1.9 | 874 | 649 | 1.3 |
| Gemma 3 | DeepSeek | |||
| Phase | TTFT | Quality | TTFT | Quality |
| 1: Priming (cold) | 20514 | 8/10 | 5140 | 3/10 |
| 2: Queries (warm) | 847 | 8/10 | 396 | 4/10 |
| 3: Repeated (hot) | 860 | 3/3 | 424 | 2/3 |
| Warm/cold speedup | 24.2 | 13.0 | ||
| Hot/cold speedup | 23.9 | 12.1 | ||
| Model | FP16 PPL | Q4 PPL | PPL | % |
|---|---|---|---|---|
| Gemma 3 12B | 14.40 | 14.30 | 0.10 | 0.7 |
| DeepSeek-V2-Lite 16B | 6.26 | 6.45 | 0.19 | 3.0 |
| Llama 3.1 8B | 4.28 | 4.40 | 0.12 | 2.8 |
| RAG | KV Persist | Msg Pass | |
|---|---|---|---|
| Restore cost | O( ) prefill | O( ) I/O | O( ) rebuild |
| Stores | Text chunks | Attn state | Structured msgs |
| Scope | External KB | Conv. history | Inter-agent |
| Model-specific | No | Yes | No |
| Hardware | Vector DB | SSD/RAM | Network |
| System | Pool | BQ4 | WM | Edge | Disk |
|---|---|---|---|---|---|
| vLLM [ 12 ] | Paged | FP8 † | No | No | No |
| SGLang [ 36 ] | Radix | No | No | No | No |
| vllm-mlx [ 3 ] | Prefix | No | No | Yes | No |
| llama.cpp [ 7 ] | Slot | Q4 ‡ | No | Yes | Slot § |
| mlx-engine [ 18 ] | No | Q8 ∥ | No | Yes | No |
| KVSwap [ 35 ] | No | No | No | Yes | No |
Appendix figures & tables9 assets
Supplementary material from the paper’s appendix.
Appendix
| Symptom | Root Cause | Fix |
|---|---|---|
| Cache appears empty after prefill | Missing mx.eval() after cache update | Evaluate after update |
| OOM during batch | Graph accumulates without clearing | Evaluate per iteration |
| Zeros after disk reload | loaded tensors not evaluated | Evaluate after load |
| Quantization corruption | Scales/biases lazy | Evaluate quantize output |
| Attention NaNs | Q4 tensors invalid post-load | Validate dtype/shape |
| Batch hangs | Merge built graph but not executed | Evaluate before attention |
| Model | Cache | 1K | 4K | 16K | 32K |
|---|---|---|---|---|---|
| Gemma | Cold | 3815–3964–4055 | 15478–15736–16076 | 74120–74219–74260 | 171718–172096–172436 |
| Warm | 455–475–539 | 504–577–608 | 782–795–817 | 1748–1819–1921 | |
| Hot | 668–683–701 | 697–719–735 | 920–934–964 | 1240–1264–1345 | |
| DeepSeek | Cold | 1019–1043–1077 | 3861–3970–4050 | 19257–19396–19466 | 47066–47315–47359 † |
| Warm | 227–234–271 | 263–271–287 | 407–434–505 | 631–633–650 † | |
| Hot | 323–345–457 | 352–366–406 | 483–490–497 | 614–624–656 † |
| 4K | 8K | 16K | 32K | |||||
|---|---|---|---|---|---|---|---|---|
| Model | FP16 | Q4 | FP16 | Q4 | FP16 | Q4 | FP16 | Q4 |
| Gemma 3 | 6 | 24 | 3 | 12 | 1 | 6 | 0 | 3 |
| DeepSeek | 9 | 33 | 4 | 16 | 2 | 8 | 1 | 4 |
| Llama 3.1 | 19 | 70 | 9 | 35 | 4 | 17 | 2 | 8 |
| Category | Count | Representative Example |
|---|---|---|
| 1: External dependency | 2 | Transformers v5.0.0rc1 SentencePiece regression: space markers stripped during decode, all words run together. Fix: pin transformers 5.0 . |
| 2: Integration | 5 | DeepSeek MLA caches K at dim=192 (128+64 rope) but V at dim=128; spec extractor fell back to 128 for both, causing 20% memory undercount. DeepSeek chat template closes every assistant turn with EOS, killing identity injection. |
| 3: Concurrency | 6 | Batch=2 warm cache produces EOS on first token. Root cause: scheduler thread runs submit() mx.eval() while event loop runs _save_to_disk() safetensors save with different locks SIGSEGV. Initial hypothesis (lazy tensor chain) was wrong; verification script masked the race with sequential timing. |
| 4: Silent correctness | 4 | Gemma 3 sliding window mask ignored during chunked prefill: QuantizedKVCache.make_mask() returns causal mask regardless of window_size . 40/48 layers get full attention instead of 1024-token window. Output looks plausible; perplexity scaling with context length was the tell. |
| Model | Input | Output | Cache W | Cache R | Total |
|---|---|---|---|---|---|
| Opus 4.5 | $7 | $120 | $908 | $1,225 | $2,260 |
| Sonnet 4.5 | $2 | $50 | $306 | $544 | $902 |
| Opus 4.6 | $2 | $23 | $238 | $522 | $783 |
| Haiku 4.5 | $0 | $0 | $1 | $1 | $2 |
| Total | $11 | $193 | $1,453 | $2,291 | $3,947 |