LM Studio 速度慢怎么办:Mac 实测首字等 39 秒的真正原因是 prompt 预填充,不是 GPU 没开满
先回答你搜的问题
- 先分清是「等很久才出第一个字」还是「出字本身慢」。LM Studio 的 API 返回里有
time_to_first_token和tokens_per_second,界面上也会显示,看一眼就知道。- 等很久才出第一个字:多半是 prompt 太长(长对话、塞了大文件、或者把 LM Studio 接给了 Claude Code / Cline 这类系统提示上万 token 的工具)。实测 1.3 万 token 的 prompt 要等 39 秒才出第一个字。好消息是前缀相同的下一次请求只要 0.1 秒。
- 出字本身慢:先看 GPU offload 是不是被调低了。不过在 Mac 上影响没想象中大:全开 29.5 tok/s,全关 26.4 tok/s。
- 多个工具同时用一个 LM Studio:如果加载时设了
--parallel 4,同时来 4 个请求,每个只剩 9.7 tok/s;设成 1 则排队,每个都是满速。- 还是嫌慢:模型太大就换小一号或更低的量化;Mac 上也可以试同一模型的 MLX 版本(站内 Qwen3.8-27B 实测 MLX 6.4
6.5 tok/s,GGUF 5.96.0 tok/s)。
问题背景
「LM Studio 很慢」是个很模糊的抱怨,背后至少有两种完全不同的慢:
- 首字慢:发出去之后半天没反应,一旦开始出字速度还行;
- 出字慢:一个字一个字往外蹦。
网上的建议大多是「把 GPU offload 拉满」,但在 Apple Silicon 上这往往不是主因。这篇在一台 24GB 的 M4 Mac mini 上把几个常见因素逐个量出来。
问题分析
一次请求的耗时由两段组成:
- 预填充(prefill):模型先把整段 prompt 读一遍,算完才能出第一个字。耗时和 prompt 长度成正比,决定「首字等多久」(
time_to_first_token)。 - 解码(decode):之后一个 token 一个 token 地生成,决定「出字速度」(
tokens_per_second)。
能影响这两段的设置主要有:GPU offload 比例(--gpu)、上下文长度(-c)、并发预测数(--parallel),以及 LM Studio 会不会复用上一次请求的前缀。LM Studio 的 lms 命令行工具把这些都暴露了出来,REST API(/api/v0/chat/completions)的返回里直接带 stats.tokens_per_second 和 stats.time_to_first_token,正好拿来测。
技术方案与选型
| 方案 | 结论 | 理由 |
|---|---|---|
lms 加载 + REST API 读 stats |
✅ 采用 | 加载参数可以逐项控制;速度数据是 LM Studio 自己统计的 |
| 在聊天界面里手动试 | ❌ 排除 | 参数改动难以记录,没法复现 |
| 下载 MLX 版本做对比 | ❌ 本次不做 | 要再下载约 6GB 模型;MLX 与 llama.cpp 的对比站内已有实测(见相关阅读) |
实验约束:
- 开始前记录 LM Studio 的状态(没有加载模型、本地服务未开),结束后恢复成同样的状态;本地服务开在单独的端口 1239。
- 内存守护脚本:系统可用内存低于 35% 时执行
lms unload --all(后面触发了一次)。 - 模型
google/gemma-4-e4b(7.5B 参数,GGUF Q4_K_M,6.33GB,llama.cpp 引擎 2.13.0);每次生成 200 个 token,temperature 0;每组跑 2 次。
实测过程
1. 默认设置:4k 上下文,29.5 tok/s
不加任何参数加载:上下文 4096,GPU offload 自动(结果与 --gpu max 相同),生成速度 29.62 / 29.46 tok/s,短 prompt 首字 0.3 秒。
--estimate-only 可以在不加载的情况下估算内存(这一点比 Ollama 友好,后者不会提前拦你,见 Ollama 显存不足实测):
| 上下文 | LM Studio 估算内存 |
|---|---|
| 4096 | 6.29 GiB |
| 32768 | 7.85 GiB |
| 131072 | 13.18 GiB |
每次估算都附带一句 This model may be loaded based on your resource guardrails settings,说明它在加载前会按资源护栏检查。
2. GPU offload:在 Mac 上影响不大
上下文 4096,短 prompt:
--gpu |
生成速度 | 首字时间 |
|---|---|---|
| max | 29.56 / 29.52 tok/s | 0.32 / 0.31 s |
| 0.75 | 27.27 / 26.99 | 0.41 / 0.39 |
| 0.5 | 26.98 / 26.84 | 0.47 / 0.46 |
| 0.25 | 26.38 / 26.12 | 0.58 / 0.69 |
| off(全 CPU) | 26.16 / 26.66 | 0.65 / 0.65 |
**全部放 CPU,生成速度只慢约 11%。**这和 Ollama 那篇 的结论一致:在 Apple Silicon 上,CPU 和 GPU 共用同一块内存,把层挪到 CPU 的代价比独立显卡小得多。首字时间翻了一倍,但短 prompt 下也就 0.65 秒,所以要看长 prompt。
3. prompt 长度:首字慢的真正原因
上下文设为 32768,分别发约 600、3300、13000 token 的 prompt:
| prompt token | 全 GPU 首字 | 全 GPU 出字 | 全 CPU 首字 | 全 CPU 出字 |
|---|---|---|---|---|
| 576 | 1.65 s | 29.42 tok/s | 5.53 s | 24.37 tok/s |
| 3268 | 8.99 s | 28.66 tok/s | 33.51 s | 20.62 tok/s |
| 12961 | 39.44 s | 25.95 tok/s | 未测完(见下) | — |
- 全 GPU 的预填充速度约 330~360 token/s(576/1.65、3268/8.99、12961/39.44)。一段 1.3 万 token 的 prompt,要等 39 秒才开始出字。
- 全 CPU 时预填充慢 3.4~3.7 倍(5.53 vs 1.65、33.51 vs 8.99)。GPU offload 对出字速度影响不大,但对首字影响很大。
- 上下文越长,出字也会变慢一些:同样全 GPU,从 29.4 降到 26.0 tok/s。
全 CPU + 1.3 万 token 那一组,预填充过程中系统可用内存降到 31%,内存守护触发并卸载了模型,所以这组没有数据。
这正是把 LM Studio 接给 Claude Code、Cline 之类编程工具时「卡半天」的原因:这些工具每次请求都带上万 token 的系统提示和工具定义。
4. 前缀缓存:第二次就快了
上表每组都连续发了两次完全相同的请求,第二次的首字时间:
| prompt token | 第一次首字 | 第二次首字 |
|---|---|---|
| 576 | 1.65 s | 0.08 s |
| 3268 | 8.99 s | 0.08 s |
| 12961 | 39.44 s | 0.12 s |
LM Studio 复用了上一次请求的前缀计算结果。所以一段对话里,只要前面的内容不变,后续轮次只需要算新增的部分。反过来说,如果你的工具每次都改动 prompt 开头(比如在系统提示里插入时间戳),缓存就会失效,每轮都要从头预填充。全 CPU 那几组的第二次请求同样只要 0.09~0.10 秒。
5. 并发预测:吞吐高了,单个请求慢了
--parallel 控制模型同时处理几个请求。同时发出 4 个请求(每个 200 token):
| 设置 | 单个请求速度 | 4 个全部完成 | 总吞吐 |
|---|---|---|---|
--parallel 1,只发 1 个 |
29.71 tok/s | — | — |
--parallel 4,只发 1 个 |
29.71 tok/s | — | — |
--parallel 1,同时发 4 个 |
每个 29.6~29.8 tok/s,但排队:6.99 / 13.94 / 20.87 / 27.79 s 依次完成 | 27.79 s | 28.8 tok/s |
--parallel 4,同时发 4 个 |
每个 9.66~9.69 tok/s | 21.35 s | 37.5 tok/s |
开了并发,4 个请求一起推进,总吞吐高 30%,最后一个也更早完成。但对每个用户来说,出字速度只剩三分之一。如果你同时开着聊天窗口、编辑器补全和一个 Agent,又设了并发,看到的就是「每个都很慢」。只有一个请求时,并发设置不影响速度。
实践效果
**1. 先判断是哪种慢。**看 time_to_first_token(首字)和 tokens_per_second(出字)。用 API 时它们就在返回的 stats 里:
curl -s http://127.0.0.1:1234/api/v0/chat/completions \
-H 'content-type: application/json' \
-d '{"model":"<你的模型 ID>","messages":[{"role":"user","content":"hi"}],"max_tokens":50}' \
| python3 -c "import json,sys; print(json.load(sys.stdin)['stats'])"
(端口换成你的 LM Studio 服务端口,默认 1234。)
2. 首字慢:
- 控制 prompt 长度:长对话适时新开会话;大文件别整篇塞进去。
- 接编程工具时,首轮几十秒的等待是预填充,无法避免;后续轮次靠前缀缓存会快很多。别让工具在 prompt 开头放每次都变的内容。
- 确认 GPU offload 是满的:它对预填充的影响是 3~4 倍。
3. 出字慢:
- GPU offload 调回 max(在 Mac 上收益约 10%);
- 上下文别开得远超需要,越长出字越慢一些;
- 换更小的模型或更低的量化;Mac 上可以试同一模型的 MLX 版本(站内只有一组对比:Qwen3.8-27B 上 MLX 比 GGUF 快约 8%)。
**4. 多个工具共用:**加载时用 --parallel 1(或在界面里把并发预测数设为 1),让请求排队、每个都满速;只有在你确实更在乎总吞吐时才开并发。
5. 加载前先估算:lms load <模型> -c 32768 --estimate-only 不会真的加载,能看到要多少内存。
踩坑
- **zsh 不会拆分未加引号的变量。**并发测试用
for pc in "1 1" "4 1"; do python3 par.py $pc; done传参,结果脚本只收到一个参数;在 zsh 里要么写成${=pc},要么直接分开写。 - **内存守护在全 CPU + 长 prompt 时触发了。**CPU 预填充 1.3 万 token 时内存占用明显更高,守护按阈值卸载了模型。这组数据缺失,但机器上的其他服务没受影响。
未验证清单:MLX 引擎下的同项对比(需要另外下载模型);更大的模型(本次只有一个 7.5B 的 GGUF);全 CPU 时 1.3 万 token prompt 的预填充时间;前缀缓存在多轮对话、prompt 部分变化时的命中情况;LM Studio 界面中其他设置(如 Flash Attention、KV 缓存量化)对速度的影响。每组只跑了 2 次。