同一台机器、同一个 7B 模型、同一条
llama-bench命令,只把-ngl 0换成-ngl 99(把权重从 CPU 挪到 GPU):prefill 从 40.42 tok/s 涨到 1541.65 tok/s,快了 38 倍;decode 从 21.37 tok/s 涨到 53.33 tok/s,只快了 2.5 倍。
这两个数字是我今天在自己这台 24GB 的 M5 Pro 上量出来的。它们同时成立,而且它们就是「本地部署该看哪些本机参数」这个问题的全部答案的入口。
因为它说明:没有哪个本机参数是笼统地「决定性能」的——每个参数只对推理的某一段有杠杆。 于是看规格表的正确顺序不是「哪个数字大」,而是先确定你卡在哪一段,再去看管那一段的那个数字。GPU 核心数对着 decode 使劲是白使劲,内存带宽对着 prefill 使劲也是白使劲。
本文的交付物是一张表(参数 → 它管哪一段 → 一行换算公式)和一个 15 分钟的自测流程。所有带小数点的数字都是我这次亲手跑出来的,测法在文中写明;标称参数和论文结论单独标注来源。
名词速查
| 术语 | 一句话解释 |
|---|---|
| prefill / decode | 推理的两个阶段:把你输入的整段提示一次性算完 vs 之后逐个 token 往外蹦 |
| tok/s | 每秒处理多少个 token;prefill 和 decode 的 tok/s 不是一回事,不能混着比 |
-ngl N | llama.cpp 参数:把模型的前 N 层放到 GPU 上算,-ngl 0 是纯 CPU |
| 量化 / Q4_K_M | 把每个权重从 16 位压到约 4 位存,模型文件变小、搬运变快,代价是精度损失 |
| KV cache | 注意力对已生成 token 的中间结果缓存,省掉重复计算,代价是占内存(本站旧文算过这笔账) |
| 可达带宽 | 实际能跑出来的内存读写速度,通常明显低于规格表上的标称带宽 |
| Roofline / ridge point | 一张「算力天花板 + 带宽斜坡」的图;两条线的交点叫 ridge point,交点左边是带宽卡住、右边是算力卡住 |
| 统一内存 working set | Apple Silicon 上 CPU/GPU 共用一池内存,但系统只允许 GPU 用其中一部分,这个上限就是真正的显存 |
结论先行:四个预算,各管一段
| 本机参数 | 它管哪一段 | 一行换算 | 我这台实测 |
|---|---|---|---|
| GPU 可用内存(不是内存条容量) | 能不能跑起来 | 权重 + KV + 激活 ≤ 预算 | 18186 MiB / 24576 MiB(74%) |
| 可达内存带宽 | decode 有多快 | tok/s ≈ 可达带宽 ÷ 模型字节数 | 反推 261 GB/s(标称 307 的 85%) |
| 算力(GPU 后端是否启用) | prefill 与长上下文有多快 | 与模型大小几乎无关,与上下文长度强相关 | prefill 38× / decode 2.5× |
| 核数 / 线程数 | 只在纯 CPU 路径上是杠杆,且会过冲 | 有最优点,不是越多越好 | 14 线程最优,18 线程掉 49% |
三个常被误读的「不是闸门」的数字:GPU 核心数(对 decode 几乎不兑现)、CPU 核数(GPU 路径上只需要 6 个线程喂数据)、标称带宽(我这台只能吃到 72%~85%)。
下面逐个参数拆,每节先给测法再给数。
参数一:可用内存 ≠ 内存条容量,差了 26%
这是唯一一个「不满足就直接跑不起来」的硬闸,也是最容易看错的一个。
24GB 的机器,llama.cpp 加载时报出来的 Metal 设备预算是:
- MTL0 : Apple M5 Pro (18186 MiB, 18185 MiB free)
- CPU : Apple M5 Pro (24576 MiB, 24576 MiB free)
ggml_metal_device_init: recommendedMaxWorkingSetSize = 19069.67 MB
18186 MiB / 24576 MiB = 74.0%。 剩下的 26% 归操作系统和其他进程,GPU 拿不到。所以「我有 24G 内存」这句话在选模型时的正确读法是「我有 17.8 GiB 显存」。
容量账要算三项,只有第一项写在模型文件名上:
权重 = GGUF 文件大小 Q4_K_M 7B → 4.36 GiB
KV cache = n_layer × (k_gqa + v_gqa) × 每元素字节 × 上下文长度
激活/图 = 几百 MiB 量级,随 batch 和 ubatch 变
这个模型的元数据是 n_layer = 28、n_embd_k_gqa = n_embd_v_gqa = 512(都是 llama-bench -v 打印的)。代进去:
28 × (512 + 512) × 2 bytes = 57344 B = 56.0 KiB / token (f16)
→ 32K 上下文 = 1.75 GiB
→ 128K 上下文 = 7.00 GiB
于是 4.36 + 7.00 = 11.36 GiB,小于 17.76 GiB 预算,满 128K 上下文放得下。这不是我推的——llama-fit-params(这个 build 里自带的自动配参工具)对这个模型直接吐出 -c 0 -ngl -1,意思是「全上下文、全层上 GPU,都装得下」。我的手算和它的结论一致。
值得注意的是 GQA 的杠杆有多大:如果这个模型不是 4 个 KV head 而是 28 个(标准 MHA),56 KiB/token 会变成 392 KiB/token,128K 上下文要 49 GiB——同一台机器直接判死。「能跑多长上下文」这个问题,模型的注意力结构比你的内存条更有话语权。
参数二:带宽只看可达值,我这台是标称的 72%~85%
M5 Pro 的标称内存带宽是 307 GB/s(Apple 官方 MacBook Pro 规格页,二档证据:检索核实,非我实测)。这个数字不能直接拿去估 decode 速度。我用两种独立方法量它的可达值。
方法一:写个多线程读带宽探针。 一个 4GB 缓冲区分给 N 个线程,每线程用 NEON 向量指令连续累加(保证走 DRAM 而不是缓存),跑 6 轮:
threads= 1 read= 47.7 GB/s
threads= 2 read=121.0 GB/s
threads= 4 read=186.2 GB/s
threads= 6 read=208.7 GB/s
threads= 8 read=216.7 GB/s
threads=18 read=222.4 GB/s
CPU 侧饱和在约 222 GB/s,是标称的 72.4%。
方法二:从 decode 速度反推。 decode 每生成一个 token 至少要把全部权重读一遍,所以 模型字节数 × tok/s 是「实际达到的搬运速率」的下界。这条公式本站之前专门推过一遍,这里不重复推导,直接用它做实验——把同一个模型量化成三种大小,看反推值怎么走:
| 模型 | 大小 | decode tok/s | 反推带宽 | 占标称 |
|---|---|---|---|---|
| Q2_K | 2.80 GiB | 71.51 | 215.0 GB/s | 70.0% |
| Q4_K_M | 4.36 GiB | 52.06 | 243.7 GB/s | 79.4% |
| Q8_0 | 7.54 GiB | 32.25 | 261.1 GB/s | 85.0% |
(测法:llama-quantize --allow-requantize 从同一个 Q4_K_M 文件转出 Q2_K 和 Q8_0,llama-bench -p 512 -n 128 -ngl 99 -r 3。必须说清楚:这三个文件只是「同架构、不同字节数」的尺寸探针,不是质量可比的模型——从 Q4 再压到 Q2、或再撑回 Q8,精度是坏的,我这里只借它们的体积做带宽实验。)
两个可以带走的结论:
- GPU 路径的可达带宽(261 GB/s)高于我的 CPU 探针(222 GB/s)。 所以「用 CPU 测出来的带宽」不能当作 GPU 推理的上限——它是另一条更矮的天花板。想知道你的机器能吃到多少带宽,最靠谱的探针就是拿一个尽量大的模型去跑 decode。
- 模型越大,带宽利用率越高(70% → 85%)。因为每 token 的固定开销(kernel 启动、采样、同步)被摊薄了。这解释了一个反直觉现象:量化带来的加速永远低于文件缩小的比例——Q8 到 Q2 文件小了 2.69 倍,速度只快了 2.22 倍。
参数三:算力只在 prefill 和长上下文上兑现
回到开头那两个数字。同一模型、同一命令,只切换 -ngl:
| 后端 | prefill (pp512) | decode (tg128) |
|---|---|---|
-ngl 0(纯 CPU,6 线程) | 40.42 tok/s | 21.37 tok/s |
-ngl 99(全上 Metal) | 1541.65 tok/s | 53.33 tok/s |
| 提升 | 38.1× | 2.5× |
但真正判决性的证据是上一节那张量化表里我没提的一列——prefill 速度:
| 模型 | 大小 | prefill tok/s |
|---|---|---|
| Q2_K | 2.80 GiB | 1613.38 |
| Q4_K_M | 4.36 GiB | 1430.28 |
| Q8_0 | 7.54 GiB | 1464.82 |
模型字节数变了 2.69 倍,prefill 速度只在 12.8% 的范围里晃(而且不单调)。同一组数据里,decode 速度严格反比于字节数。
一个参数变了 2.69 倍,一段速度动了 12.8%,另一段动了 2.22 倍——这就是「两段推理被两个不同的物理量卡住」的直接证据,不需要引用任何论文。prefill 一次算 512 个 token,权重读一遍被 512 个 token 分摊,于是它是算力题;decode 一次算 1 个 token,权重读一遍只服务 1 个 token,于是它是搬运题。(本站讲投机解码那篇的标题就是这个意思:解码慢不是算力问题,是搬运问题。)
所以:如果你的场景是「长提示、短回答」(代码库问答、文档摘要、批量分类),算力是你的闸,GPU 后端和 GPU 核心数值得花钱;如果是「短提示、长回答」(对话、写作、Agent 逐步输出),带宽和模型体积是你的闸,加 GPU 核心几乎白花。
参数四:核数是有最优点的,不是越多越好
sysctl 报出的这台机器的 CPU 拓扑很有意思——两个性能层级叫 Super(6 核)和 Performance(12 核),共 18 核。纯 CPU 路径上扫线程数(-ngl 0 -n 64):
| 线程 | 2 | 4 | 6 | 10 | 14 | 18 |
|---|---|---|---|---|---|---|
| decode tok/s | 9.91 | 17.14 | 21.53 | 29.94 | 36.14 | 18.32 |
14 线程是最优点(36.14 tok/s,反推 169.2 GB/s)。打满 18 线程反而掉 49.3%——没给运行时和系统留核,调度开销把收益全吃掉了。
这个反直觉结果有两个实用含义。一是「我的机器有 N 核,就设 N 个线程」是错的,留 2~4 个核给系统通常更快,具体最优点必须扫一次。二是注意 GPU 路径下 llama-bench 默认只用 6 线程就跑出了 53.33 tok/s——比 CPU 拿 14 线程拼出来的 36.14 还高。GPU 路径上 CPU 核数几乎不是杠杆,它只负责喂数据和采样。
隐形参数:上下文长度会同时啃掉两段速度
上下文长度不写在规格表上,但它是唯一一个「你自己每天在调、且同时影响所有闸」的参数。用 llama-bench -d N(先塞 N 个 token 再测):
| 已有上下文深度 | 0 | 2048 | 8192 | 16384 | 32768 |
|---|---|---|---|---|---|
| decode tok/s | 53.39 | 50.38 | 46.46 | 41.72 | 33.53 |
| prefill tok/s (pp512) | 1422.78 | — | 739.29 | — | 263.90 |
从空上下文到 32K:decode 掉 37.2%,prefill 掉 81.5%。 注意这不是「变慢了一点」——32K 上下文下再追加 512 token 的提示,处理速度只有空上下文时的 1/5.4。
这解释了本地 Agent 最常见的体验断崖:会话前几轮飞快,跑到几万 token 后每一步都在等。你的「机器够不够快」这个判断,如果是在空上下文下做的,那它对真实工作负载没有参考价值。
顺手测了一个常被推荐的省内存手段——KV cache 量化。同深度(32K)、同开 flash attention 的受控对比:
| KV 数据类型 | decode tok/s | KV 内存 |
|---|---|---|
| f16 | 34.38 | 56.0 KiB/token |
| q8_0 | 27.45 | 29.8 KiB/token |
省了 46.8% 的 KV 内存,付出 20.2% 的 decode 速度。 我的判断:KV 量化是容量杠杆,不是速度杠杆——只在你被容量闸卡住(想跑更长上下文或更多并发)时才划算,为了「更快」去开它是反向操作。这一点和「权重量化既省容量又提速」正好相反,别把两件事混成一条经验。
并发:把带宽闸换成算力闸的开关
上面所有 decode 数字都是单流。多流一起跑,权重读一遍能服务多个请求,带宽就被摊薄了。用 llama-batched-bench(256 token 提示 + 128 token 生成,并发数 B):
| 并发 B | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| decode 聚合 tok/s | 51.86 | 70.55 | 114.91 | 131.74 | 170.59 |
| prefill 聚合 tok/s | 1379.04 | 1269.30 | 1339.04 | 1466.84 | 1449.11 |
聚合吞吐 B=1 → B=16 涨 3.29 倍,但单流速度从 51.86 掉到 10.66 tok/s(慢 4.86 倍)。prefill 聚合吞吐全程基本不动(已经是算力饱和的)。
这是本地部署最容易被浪费的一个参数。 单人对话时你的带宽利用率就是上面那 85% 的上限,加机器只能加带宽;但如果你的负载是「批量处理 200 个文件」,把它们并发喂进去,同一台机器的总吞吐能翻 3 倍以上——代价是每一条的首字延迟变长。灰度地说:交互场景优化单流延迟(选小模型、低量化),批处理场景优化聚合吞吐(提并发、别管单流),同一台机器上这两个目标是对立的。
论文怎么说:这套框架不是我编的,但有分歧
上面全是单机实测,学术侧的对应框架是 Roofline。
- LLM Inference Unveiled: Survey and Roofline Model Insights(arXiv:2402.16363,ID 已回查核实)把 decode 明确刻画为 memory-bound,并用 Roofline 分析量化的三种效果分支。我这次的三点量化实验,就是它那套分析在一台笔记本上的最小复现。
- RooflineBench(arXiv:2602.11506)是端侧专门做的 Roofline 框架,摘要里两个结论和我的实测方向一致:算术强度受序列长度显著影响;以及模型深度增加时算术强度出现回退。它还提出「硬件异质性导致的效率陷阱」——对应我这里 CPU 探针 222 GB/s 与 GPU 反推 261 GB/s 的分裂。
- 分歧要说清楚:Mind the Memory Gap(arXiv:2503.08311)通过 GPU profiling 反驳了「大 batch 就能进入算力饱和」的常见假设,测到大批量下计算/访存比仍在 0.5~1 ops/byte,依然是 memory-bound。这和我上面「并发把带宽闸换成算力闸」的说法不完全一致——我的数据只能说明并发让聚合吞吐涨了 3.29 倍、prefill 吞吐已经饱和,不能证明 decode 在 B=16 时已经变成算力受限。诚实的表述是:并发把 decode 从「严重带宽受限」推向「不那么受限」,至于有没有跨过 ridge point,我这次没测。
15 分钟自测流程:换任何一台机器都跑这五步
不用记规格表,按顺序跑一遍,你会拿到自己那台机器的四个预算数字。
# 1. 容量闸:加载日志里找 GPU 实际预算(不是内存条容量)
llama-bench -m <model.gguf> -p 1 -n 1 -ngl 99 2>&1 | grep -i "WorkingSetSize\|MTL0"
# 2. 基线:prefill / decode 各自的 tok/s
llama-bench -m <model.gguf> -p 512 -n 128 -ngl 99 -r 3
# 3. 算力闸:GPU 对两段各值多少倍(把 2 的结果和这条比)
llama-bench -m <model.gguf> -p 512 -n 128 -ngl 0 -r 2
# 4. 带宽可达值:模型字节数 × decode tok/s(用你手上最大的模型测最准)
# 再和厂商标称带宽比,得出你的利用率
# 5. 真实负载:在你实际会用到的上下文深度上重测一遍
llama-bench -m <model.gguf> -p 0 -n 64 -ngl 99 -d 0,8192,32768 -r 2
第 5 步是最容易被跳过、也最值钱的一步。空上下文下的跑分和你真实的使用体验之间,我这台差了 37%。
一张总表收尾
| 你的症状 | 卡住的闸 | 该调的参数 | 别调的参数 |
|---|---|---|---|
| 加载就 OOM / 掉到 CPU | 容量 | 换更小量化、降 -c、开 KV 量化 | 线程数、并发 |
| 首字慢(长提示) | 算力 | 开 GPU 后端、降上下文深度、复用前缀缓存 | 换更小量化(几乎无效) |
| 逐字输出慢 | 带宽 | 换更小的模型/量化、投机解码 | 加 GPU 核心、加线程 |
| 批量任务总时长长 | 并发未用满 | 提并发数、提 -b/-ub | 单流延迟优化 |
| 会话越久越慢 | 上下文深度 | 截断/摘要历史、前缀缓存 | 换机器 |
再压成一句话:规格表上的数字都是「上限」,本地部署的全部功夫是找出你当前撞在哪个上限上——而这件事只能测,不能读。
诚实的提醒
- 标称 307 GB/s 是检索核实的官方规格,我没有独立验证过厂商数字本身;文中所有百分比是「我的实测值 ÷ 这个标称值」。如果标称值的定义与我的测法口径不同(比如含读写合计),这些百分比会整体偏移。
- 三档量化文件是 requantize 出来的尺寸探针,不是质量可比的模型。 我没有测它们的 perplexity,因此本文对「量化对质量的影响」不做任何断言。
- KV cache 128K = 7.00 GiB 是手算 +
llama-fit-params的结论一致,不是我从加载日志里直接读到的分配数。 我尝试用llama-server -c 131072抓分配日志,两次都在超时前没等到那行输出,没拿到就不写。 - 并发那组数据只有单机单模型一个尺寸,B>16 我没测,也没测并发下的显存占用曲线。
- 成本最低的验证实验:上面自测流程的第 2 步和第 3 步,两条命令、五分钟,就能在你自己机器上复现「38 倍 vs 2.5 倍」这个核心分裂。如果你那台机器上这两个倍数接近,说明你的 CPU/GPU 带宽差距很小(常见于统一内存的低配机型),本文关于「算力只在 prefill 兑现」的结论在你那里会更弱。
参考来源
测试环境(本文全部实测数据的来源)
- 机器:Apple M5 Pro,18 核 CPU(
sysctl报Super6 核 +Performance12 核)/ 20 核 GPU,24 GB 统一内存,macOS 26.6.2 - 软件:
llama.cppbuild8ed274ef4(9630),Metal 后端,ggml0.15.1 - 模型:Qwen2.5-7B-Instruct GGUF,基线 Q4_K_M(4.36 GiB / 7.62 B 参数,
n_layer=28,n_embd_k_gqa=512) - 工具:
llama-bench、llama-batched-bench、llama-quantize、llama-fit-params,以及一个自写的多线程 NEON 读带宽探针
厂商规格
- MacBook Pro Tech Specs — Apple:M5 Pro 标称 307 GB/s 内存带宽
arXiv 论文(ID 均已回查核实)
- arXiv:2402.16363 — LLM Inference Unveiled: Survey and Roofline Model Insights
- arXiv:2602.11506 — RooflineBench: A Benchmarking Framework for On-Device LLMs via Roofline Analysis
- arXiv:2503.08311 — Mind the Memory Gap: Unveiling GPU Bottlenecks in Large-Batch LLM Inference
- arXiv:2607.13068 — The Economics of AI Decoding Chips: Rebalancing Compute, Capacity, and Bandwidth for Efficient LLM Inference
本站相关
- Decode 速度上限公式:为什么 4.677GB 模型、16.99 tok/s 反推约 79.5GB/s — 本文第二节用到的反推公式的完整推导
- KV cache 那笔账:为什么 Qwen2.5-7B 在 2048 长度下大约是 117MB — 容量账里 KV 那一项的逐项拆解
- Medusa 深读:解码慢不是算力问题,是搬运问题 — 撞上带宽闸之后的算法解法
- Magnitude 源码深读:它不是又一个 Ollama,而是一台本地 Agent 推理控制器 — 把本文这些参数决策自动化的一次工程尝试