QwenPaw 在 Mac Studio 上 KV Cache 命中率归零的排查与根因
2026-05-08 tech qwenpaw lm-studio apple-silicon mlx kv-cache 5 mins 2075 字

最近给 QwenPaw 配置 LM Studio 时,发现一个现象:LLM 请求的 KV Cache 命中率始终为 0。
排查了一圈配置和代理链路,最终发现这不是 bug,而是 Apple Silicon (MLX) 与 NVIDIA CUDA 在 KV Cache 实现上的根本性差异。记录一下排查过程和实验数据。
架构与现象
我们的请求链路如下:
QwenPaw (Linux) → Docker(cli-proxyapi:8317) → LM Studio (Mac M2 Max, 100.100.100.11:1234)
在 LM Studio 的 Web UI 中,我已经开启了 KV缓存量化 (Experimental),上下文长度设为 262144。但 QwenPaw 的 token 用量日志显示,每次请求都是全新的 prompt,缓存从未命中。
排查过程
Step 1: 排除代理层干扰
首先怀疑是 cli-proxyapi 在转发时注入了动态 header(如 X-Request-Id、时间戳等),导致每次请求的 HTTP 头不一致。
curl -sv ... http://cli-proxyapi.sh4.local:8317/v1/chat/completions 2>&1 | grep -E "^[<>]"
结果:两次请求的 header 完全一致,代理层干净。
Step 2: 排除多实例负载均衡
如果 LM Studio 背后有多台机器轮询,缓存自然无法复用。我是单实例运行,无负载均衡。
Step 3: KV Cache 压力测试
我构造了两组对比实验:
实验 A:完全相同的 prompt 重复发送
{"model":"qwen3.6-35b-a3b@8bit","messages":[{"role":"system","content":"You are a test bot."},{"role":"user","content":"What is 1+1?"},{"role":"assistant","content":"2"},{"role":"user","content":"And 2+2?"}],"max_tokens":5,"temperature":0}
- Req1: 1011ms
- Req2: 444ms (↓ 56% 加速)
实验 B:相同前缀(100K chars) + 不同后缀(模拟 Agent 多轮对话)
PREFIX=$(python3 -c "print('A'*100000)") # 约 25K tokens
# Req1: system="$PREFIX", user="Say ONE word: test"
# Req2: system="$PREFIX", user="Say ONE word: test" (完全相同)
- Req1: 632ms
- Req2: 753ms (反而更慢,缓存未生效)
核心结论:MLX vs CUDA 的 KV Cache 差异
实验数据说明了原因:
| 场景 | CUDA (vLLM/TGI) | Apple Silicon (MLX/LM Studio) |
|---|---|---|
| 完全相同的 prompt | ✅ 命中,复用 KV Cache | ✅ 命中,复用 KV Cache |
| 相同前缀 + 不同后缀 | ✅ 自动前缀缓存 (Prefix Caching) | ❌ 不支持,必须逐字节完全匹配 |
QwenPaw 的 Agent 场景每轮发送的 prompt 结构如下:
[固定 System Prompt (~15K tokens)] + [变化的对话历史] + [新的 User Message]
↑ 前缀固定 ↑ 每轮变化
在 CUDA 推理框架中,vLLM/TGI 会自动提取相同的前缀进行 KV Cache 复用,Agent 场景的缓存命中率通常能达到 30%~50%。
但在 Apple Silicon 上,LM Studio 底层使用 MLX 框架。MLX 的 KV Cache 实现较为朴素,仅支持精确匹配(Exact Match),不支持前缀缓存 (Prefix Caching)。Apple Silicon 的 KV Cache 优化目前仍处于实验阶段,前缀复用功能尚未稳定。
踩坑记录
- Apple Silicon 的 MLX 框架限制:MLX 在推理速度上已经非常优秀,但在企业级特性(如 Prefix Caching、Continuous Batching)上仍落后于 CUDA 生态。选型时需明确场景需求。
- 实验设计:用
date +%s%N记录毫秒级耗时,配合完全相同的 prompt vs 前缀相同后缀不同的对比,能最快定位缓存生效边界。