
MLX runner: KV cache not released between requests causes memory accumulation and severe tgTPS collapse (v0.30.8)
快速结论: 在 Ollama v0.30.8 上运行 MLX 模型(如 qwen3.6:35b-mlx)时,连续发送独立请求会导致 KV cache 内存累计增长至物理内存上限(高达 75 GB vs 模型加载基线 ~24 GB),进而引发系统重度 swap,最终在长上下文(131k token)请求上造成 tgTPS 下跌 77%(从 58.7 tok/s 降至 13.35 tok/s)。优先排查是否使用了 v0.30.8 版本,并回退到 v0.30.7 验证。
问题场景
用户在 Apple M4 Max(64 GB 统一内存)macOS 系统上,使用 Ollama v0.30.8 运行 MLX 模型 qwen3.6:35b-mlx(上下文窗口 262144)。通过 bench.py 脚本按递增 prompt 长度(1024, 4096, 8192, 16384, 32768, 65536, 131072)依次发送独立 /v1/completions 请求(每个请求使用唯一 UUID 前缀,无共享前缀)后,即使在所有请求完成后,内存仍未释放,后续单独 131k 上下文请求性能急剧下降。
报错原文
# v0.30.8 复现步骤
# Step 1: restart Ollama (clean state — baseline is ~24 GB)
# Step 2: run a sequence of increasing context sizes
python3 bench.py --model qwen3.6:35b-mlx \
--prompt-lengths 1024 4096 8192 16384 32768 65536 131072 --no-batch
# Step 3: immediately run a standalone 131k request
python3 bench.py --model qwen3.6:35b-mlx \
--prompt-lengths 131072 --no-batch
# Expected: tgTPS collapses to ~13 tok/s (baseline: ~58 tok/s)
# 内存增长数据(Activity Monitor)
# | State | Ollama RSS | Swap used |
# |------------------------------------|-----------|-----------|
# | Fresh load (model only) | ~24 GB | ~0 GB |
# | After pp1024–pp65536 sequence | 58.5 GB | 15.6 GB |
# | During standalone pp131072 | **75.2 GB** | **31.6 GB**|
# | After all requests complete (idle) | **68.6 GB** | **32.7 GB**|
# 性能退化数据
# | Condition | TTFT pp131072 | tgTPS pp131072 |
# |----------------------------------------------|:-------------:|:--------------:|
# | Baseline (fresh Ollama, standalone) | ~182 s | ~58.7 tok/s |
# | pp131072 within sequence (swap active) | 249 s (+37%) | 54.3 tok/s |
# | **Standalone after sequence (heavy swap)** | **317 s (+74%)** | **13.35 tok/s (−77%)**|
# Server log (peak memory per request in sequence)
# peak memory pp1024: 20.78 GiB
# peak memory pp4096: 22.15 GiB
# peak memory pp8192: 23.42 GiB
# peak memory pp16384: 26.09 GiB
# peak memory pp32768: 31.19 GiB
# peak memory pp65536: 41.01 GiB
# peak memory pp131072: 60.58 GiB ← swap already active
原因分析
可能原因:Ollama v0.30.8 引入的“Improved prompt caching (decoupled from context shift)”和“MLX runner snapshots during prompt processing”改动中,MLX runner 在请求完成后未释放 KV cache 内存。具体来说,x/mlxrunner/cache* 文件中的缓存机制存在设计缺陷:
x/mlxrunner/cache.go:32声明了const maxPagedOutBytes int64 = 8 << 30(8 GiB)上限,但该上限仅追踪 snapshot tier 的pagedOutBytes(分页出去的快照足迹),并未覆盖 MLX 侧仍然驻留在内存中的 KV cache 状态。因此当请求无共享前缀时(如使用唯一 UUID 前缀),findBestMatch会始终从根节点分配新分支,这些分支在后续请求切换后才可能进入 snapshot tier,导致累积增长。enforceEvictionPolicy仅在后续请求分配路径中触发,而不是在请求完成时执行,导致已结束请求的“死分支”保留 KV 层引用,内存无法回收。- 在 64 GB 系统加载 35B MLX 模型(占用 ~24 GB)后,实际可用缓存预算约 40 GB,但 7 个请求累计 ~259k token 的上下文超出该预算,触发重度 swap,进而导致 token 生成阶段因 SSD I/O 瓶颈性能崩溃。
权宜方法:在两个请求之间重启 Ollama。
环境排查
- Ollama 版本:确认是否为 v0.30.8。v0.30.7 及更早版本不受影响(已验证 v0.30.7 正常,后序列内存仅 ~33 GB,无 swap 压力,tgTPS 保持 ~58.64 tok/s)。
- 硬件:Apple Silicon(M4 Max 64 GB 统一内存)。
- 模型:
qwen3.6:35b-mlx(上下文窗口 262144)。 - 使用
ollama ps查看 SIZE 是否远大于模型加载基线(~24 GB),如 56–75 GB 表明存在内存泄漏。 - 确认请求间无共享前缀(如使用唯一 UUID 前缀)以触发该问题。
解决步骤
- 回退版本(优先尝试):将 Ollama 回退到 v0.30.7。在 v0.30.7 下运行完全相同的请求序列,验证内存和性能恢复正常。
- 在请求完成后重启 Ollama:作为临时权宜方法,在每次独立请求或请求序列之间重启 Ollama 服务,确保 KV cache 内存被完全释放。
- 检查是否使用了 MLX 模型且为独立无共享前缀请求:如果是 agentic 会话(如 Claude Code 子代理)等场景,即使缓存命中率达 94-99%,该问题也会因结构性积累而出现,应遵循步骤1回退。
- 关注上游修复:推测修复方案包括:降低
maxPagedOutBytes预算并改为在请求完成时执行 eviction(而非仅在后续请求分配路径上);对“无共享前缀”的分支在请求完成后立即剪枝;将 eviction 与 MLX 侧驻留字节数绑定。这些方案正在讨论中,需等待官方发布修复版本。
验证方法
在 v0.30.7 下运行相同基准测试脚本 python3 bench.py --model qwen3.6:35b-mlx --prompt-lengths 1024 4096 8192 16384 32768 65536 131072 --no-batch 后再运行 python3 bench.py --model qwen3.6:35b-mlx --prompt-lengths 131072 --no-batch。确认以下指标恢复到正常范围:
ollama psSIZE 应在序列后保持在 ~33 GB(而非 56-75 GB)。- Swap 使用量应保持很低(~3.73 GB 而非 31.63 GB)。
- pp131072 独立请求的 tgTPS 应保持 ~58.64 tok/s(而非 13.35 tok/s)。
- pp131072 TTFT 应保持 ~177 s(而非 317 s)。
另外,通过 Activity Monitor 确认内存压力曲线为平坦绿色(无压力),而非黄色/橙色(压力状态)。



