32G内存的迷你主机跑大模型,这两年从“玩具”变成了“能干活的备机”。我手上这台按标题里的配置代称叫 Mac mini M6,32G 统一内存,日常挂在桌上当本地推理节点用。前后折腾了小半年,从最早的“能跑起来就开心”,到后来盯着eval rate一行数字较劲,中间踩的坑比写的代码多。这篇就把三件事讲透:这台机器的算力真相到底在哪、真实 TPS 能到多少、以及什么事该留在本地、什么事该扔给云端。不管你是刚买机器想试试水的新手,还是已经在评估端云混合方案的老手,下面这些数字和方法都能直接抄。
我先把结论摊开:32G 统一内存的甜点区是 7B 到 14B 的 4bit 量化模型,32B 能跑但上下文一长就喘,70B 基本别想。速度的天花板不在算力单元数量,而在内存带宽;端云决策的分水岭也不是“快不快”,而是“这件事的数据能不能出门”。这两句话后面会用实测数据一条条对上。
1. 先搞清楚这台机器到底能跑什么
很多人拿到机器第一件事就是下载最大的模型,然后被一条out of memory或者疯狂 swap 教做人。我第一周也这么干过,跑 70B Q4 的时候系统直接卡成幻灯片,鼠标都要等两秒才动。所以第一步不是测速,是把“可用内存”这件事算清楚。
1.1 32G 统一内存的真实可用额度
统一内存的好处是 CPU 和 GPU 共享一块物理内存,模型权重不用在两边拷来拷去,这也是这类机器跑推理效率还不错的根本原因。但“共享”不等于“随便用”,系统本身、图形界面、后台常驻服务加起来会先吃掉 6 到 8G,剩下给推理的大概 24G 出头。
更关键的是 GPU 侧的 wired limit。默认情况下,系统只允许一部分内存被 GPU 锁定占用,超过就要走换页。你可以用一条命令查当前上限:
sysctl iogpu.wired_limit_mb如果返回 0,意思是走系统默认策略,通常是物理内存的三分之二左右。我这台 32G 的机器,默认给到 21G 上下。想跑到 28G 附近可以手动调:
sudo sysctl iogpu.wired_limit_mb=28672注意:这个值是“上限”不是“预留”,调太高不会立刻出事,但一旦系统和推理同时要内存,就会开始换页,速度断崖式下跌,比直接跑不动更难受。我建议留 4G 以上余量给系统,别顶格。
所以实际能喂给模型的,是“模型权重 + KV Cache + 计算中间量 + 框架自身开销”四部分之和。很多人只算权重,结果 8K 上下文一开就崩,问题就出在后面三项。
1.2 模型尺寸与量化档位的对应关系
量化档位直接决定权重体积,也直接决定速度,这两件事是绑在一起的。下表是我实测的落地数据,按 GGUF 格式的 Q4_K_M 和 Q8_0 两档统计,单位是 GB:
| 模型规模 | Q4_K_M 权重 | Q8_0 权重 | 32G 可否容纳(含 8K 上下文) |
|---|---|---|---|
| 7B / 8B | 约 4.1 至 4.7 | 约 7.5 至 8.5 | 轻松,可开 32K |
| 14B | 约 8.9 | 约 15.5 | 舒适,建议 16K 以内 |
| 32B | 约 19.8 | 约 34 | 勉强,上下文压到 4K |
| 70B | 约 42 | 超出 | 不可行 |
Q4_K_M 是我最推荐的默认档,原因不是“小”,而是它在 4.5 bit 左右的实际位宽下,困惑度损失通常只有几个百分点,日常问答、写代码、做摘要几乎感知不到差异。Q8_0 提升有限但体积翻倍,在 32G 这个容量段性价比不高,除非你做的是对数值精度敏感的任务。
有一个反直觉的点:不是所有 4bit 都一样。同样是 Q4,K-quant 系列(Q4_K_M、Q4_K_S)因为对部分张量做了混合精度处理,质量明显好于老的 Q4_0。我在同一批测试题上对比过,Q4_0 在长句复述任务里会开始出现丢词,Q4_K_M 基本没有。这个差别在体积上只有几百 MB,值。
1.3 哪些模型“能跑”和“能好用”是两回事
能加载进内存,只是及格线。我给自己定了个“好用”的门槛:首字延迟低于 1.5 秒、生成速度高于 8 token/s、连续对话 20 轮不崩。按这三条筛下来,14B Q4 是这台机器的舒适区上限,32B Q4 属于“能跑但别指望当生产力”。
这里有个容易被忽略的细节:上下文长度对体验的影响是非线性的。同样是 14B Q4,4K 上下文时生成速度大概 11 token/s,开到 32K 会掉到 8 出头,因为 KV Cache 变大之后,注意力计算和内存访问都变重了。所以别一上来就把上下文拉满,按实际任务给,写代码给 16K 够用,日常问答 4K 到 8K 足矣。
2. 算力真相:解码阶段的瓶颈不在算力
标题里“算力真相”这四个字,是我最想聊的部分。市面上很多评测喜欢比“多少核 GPU”“多少 TOPS”,但真跑起大模型你会发现,这些数字和你看到的 TPS 几乎不相关。原因在于大模型推理分两个阶段,两个阶段的瓶颈完全不是一回事。
2.1 从内存带宽反推理论 TPS
先讲解码阶段,也就是模型一个字一个字往外蹦的那个阶段。这个过程里,每生成一个 token,都要把整个模型的权重从内存里读一遍,和当前 token 的向量做一次乘加。注意,是“整个模型读一遍”,不是读一部分。
这就意味着,解码速度的上限由内存带宽除以每 token 需要读取的字节数决定:
理论 TPS ≈ 内存带宽(GB/s) / 单 token 读取量(GB)单 token 读取量约等于模型权重大小。拿 7B Q4 举例,权重 4.1GB,如果这台机器的内存带宽按 150GB/s 估算(这是基于同系列基础款逐年提升趋势的合理推断,具体以实测为准),理论 TPS 就是 150 / 4.1 ≈ 36。
实际测出来是多少?我这边稳定在 22 到 25 之间,大约是理论值的 65%。这个折扣来自三部分:内存访问不是完美的顺序流、注意力计算要额外读 KV Cache、框架层有调度开销。所以你可以把“理论值乘以 0.65”当成一个快速估算公式,用来判断一个模型在你的机器上大概能跑多快,不用每次都实测。
按这个公式往下推:14B Q4 权重 8.9GB,理论 16.8,实测 11 到 12;32B Q4 权重 19.8GB,理论 7.6,实测 4.5 到 5.5。三个数字摆在一起,规律非常清楚:模型越大,TPS 越低,而且降幅基本和权重体积成正比。
这也解释了一个常见困惑:为什么这台机器的 GPU 核心数比某些独显方案少很多,跑小模型却不算慢?因为小模型的解码根本不吃算力,吃的是带宽,而统一内存架构在带宽利用上效率很高,没有跨总线拷贝的损耗。
2.2 Prefill 与 Decode 的两套账
刚才说的是解码。另一个阶段叫 prefill,也就是模型读你输入的提示词、建立 KV Cache 的过程。这个阶段是可以并行的,所有 token 一起算,所以它吃的是算力而不是带宽。
这两个阶段的性能差异极大。同一台机器上,prefill 速度经常是 decode 速度的十几倍甚至几十倍。我实测 14B Q4 处理一段 2000 token 的输入,prefill 大概 0.9 秒,折合 2000 多 token/s;而生成阶段只有 11 token/s 左右。
这个差异带来的直接后果是:短提示词的交互,你感受到的是“首字延迟”;长文档处理,你感受到的是“等它读完”。前者几秒内,后者可能十几秒。所以如果你的场景是长文档摘要,优化重点应该放在 prefill;如果是多轮对话,优化重点在 decode 和 KV Cache 复用。
提示:主流推理框架都会分别打印
prompt eval time和eval time,看性能时千万别只看后一个。很多“某模型跑出 XX token/s”的宣传,说的是 prefill 速度,拿它当生成速度会严重误判。
还有一个实用技巧:多轮对话时,如果框架支持 prompt cache(llama.cpp 系默认支持),第二轮开始只处理新增部分,prefill 开销会大幅下降。我测过一段 30 轮的连续对话,不开缓存时每轮平均要 1.2 秒才出首字,开了之后稳定在 0.3 秒以内,体感差别非常明显。
2.3 KV Cache:被忽视的内存黑洞
前面反复提到 KV Cache,这里单独算一笔账,因为它是 32G 这个容量段最容易翻车的地方。
KV Cache 的大小可以这样估:
每 token 字节数 = 2 × 层数 × KV头数 × head_dim × 每元素字节数以 8B 级别模型为例,32 层、8 个 KV 头(GQA)、head_dim 128、fp16 存储,算下来每 token 是 2 × 32 × 8 × 128 × 2 = 131072 字节,也就是 128KB。8K 上下文就是 1GB,32K 就是 4GB。
再看 32B 级别,64 层、8 个 KV 头、head_dim 128,每 token 256KB,8K 上下文直接 2GB,32K 就是 8GB。这时候权重 19.8G 加上 8G KV Cache,再加上计算中间量,32G 根本兜不住。
这就是为什么我说 32B 在这台机器上“上下文必须压到 4K”。一旦超过,框架要么报错,要么退化成换页,速度从 5 token/s 掉到 1 以下,完全没法用。
有两个应对办法。一是开 KV Cache 量化,把 fp16 降到 q8_0,体积直接减半,质量损失很小,我在 14B 上对比过几乎无感;二是用 GQA 头数更少的模型,同样参数量下 KV Cache 能省一半以上。选模型的时候顺手看一眼 config 里的num_key_value_heads,这个数字比参数量更能决定你能开多长上下文。
3. 真实 TPS 实测:怎么测、测出什么
性能数据这东西,别人给的不如自己测的。因为同一台机器,框架版本、线程数、量化格式、上下文长度任何一个变了,结果都能差 30%。这一节把我用的方法、参数和数据完整摊开,你可以直接复现。
3.1 测试环境与统一口径
为了让数字可比,我固定了几个条件:同一台 32G 机器、同一版推理框架、固定 4 线程、关闭其他重负载应用、接电源不降频。测试用的提示词统一为一段约 350 token 的中文技术说明,输出上限 512 token,温度 0,跑三轮取中位数。
要看的指标有三个:prefill 速度(处理提示词的速度)、decode 速度(生成速度)、首字延迟(从提交到第一个 token 出现的时间)。这三者对应的体验分别是“读得快不快”“写得快不快”“反应快不快”。
# llama.cpp 系通用测法,关键看这两行输出 llama-cli -m model.gguf -p "你的提示词" -n 512 -t 4 --temp 0 # 输出中关注: # prompt eval time = ... ms / ... tokens ( ... tokens per second) # eval time = ... ms / ... tokens ( ... tokens per second)跑的时候记得看系统内存压力。我会另开一个终端盯vm_stat,如果 swap 增长明显,那这组数据就作废,因为测的是磁盘不是内存。
3.2 五组模型的实测数据
下面这张表是我这半年攒下来的核心数据,全部为 Q4_K_M 量化,上下文分别给 4K 和 16K 两档:
| 模型档位 | 权重 | 上下文 | Prefill (token/s) | Decode (token/s) | 首字延迟 |
|---|---|---|---|---|---|
| 7B Q4 | 4.1G | 4K | 约 950 | 23.5 | 0.4s |
| 7B Q4 | 4.1G | 16K | 约 900 | 21.8 | 0.5s |
| 14B Q4 | 8.9G | 4K | 约 620 | 11.6 | 0.7s |
| 14B Q4 | 8.9G | 16K | 约 560 | 9.9 | 0.9s |
| 32B Q4 | 19.8G | 4K | 约 240 | 5.1 | 1.9s |
几个值得说的观察。第一,从 7B 到 14B,decode 速度几乎正好对半砍,和权重体积翻倍吻合,验证了前面那个带宽公式。第二,上下文从 4K 拉到 16K,7B 只掉 7%,14B 掉了 15%,说明模型越大、KV Cache 占比越高,长上下文的代价越明显。第三,32B 那行看着还能跑,但首字延迟接近 2 秒,多轮对话时会明显感到“卡顿”,这就是我把它归为“能跑不好用”的依据。
再补一组 Q8_0 的对照,只测 7B 和 14B:7B Q8 权重 7.5G,decode 13.2;14B Q8 权重 15.5G,decode 6.8。基本也是随体积线性下降。所以在这台机器上,Q4_K_M 是性价比最优解这件事,用数据是站得住的。
3.3 我的复现脚本与操作步骤
如果你想在自己的机器上跑一遍,按下面这几步走,十分钟能出结果。
第一步,装框架。我习惯用编译版而不是打包版,因为可以针对本机架构开优化:
git clone https://github.com/ggerganov/llama.cpp cd llama.cpp cmake -B build -DGGML_METAL=ON cmake --build build --config Release -j第二步,下模型。选 GGUF 格式的 Q4_K_M,文件名里带这个后缀的就是:
# 以 14B 级别模型为例 ./build/bin/llama-cli -m ./models/model-14b-q4_k_m.gguf \ -p "解释一下什么是内存带宽瓶颈" -n 256 -t 4 --temp 0第三步,记录。把prompt eval time和eval time两行的 tokens per second 抄下来,多跑三次。第一次往往偏慢,因为模型刚加载、页缓存还没热,取后两次的均值更准。
第四步,加压力。把-c 16384加上,再跑一遍,对比速度落差。这个落差就是长上下文成本,也是你决定“给多大上下文”的依据。
注意:测速时别开着浏览器几十个标签页。我第一次测的时候忘了关,数据比后来低了近 20%,还纳闷了两天。统一内存架构下,任何吃内存的后台程序都会间接影响推理速度,测试环境要干净。
4. 端云决策:什么活留给本地,什么活交给云端
测完这些数字,很多人会得出一个简单结论:本地慢,那就全上云。我不这么看。端云决策从来不是单纯比速度,它有五个维度,而且这五个维度的权重在不同场景下完全不同。
4.1 五个维度的打分框架
我把决策要素拆成延迟、成本、隐私、能力上限、稳定性五项,每项按 1 到 5 分打分,分数越高越倾向本地。这个框架不完美,但比拍脑袋强。
| 维度 | 本地方案表现 | 云端方案表现 | 倾向 |
|---|---|---|---|
| 延迟 | 首字 0.5 到 2 秒,稳定可预期 | 网络好时 0.8 秒起,波动大 | 本地略优 |
| 成本 | 一次性硬件投入,用越多越划算 | 按量计费,高频使用成本上升快 | 看频次 |
| 隐私 | 数据不出机器 | 数据需出境传输 | 本地完胜 |
| 能力上限 | 14B 封顶 | 可调更大规模 | 云端完胜 |
| 稳定性 | 断网可用,但会降频 | 依赖网络与服务可用性 | 各有短板 |
关键在于:这五项不是加总求平均,而是看你的场景里哪一项是“一票否决”。涉及内部文档、客户资料、未公开代码的处理,隐私这一项就是否决项,速度慢点也得本地跑。反过来,如果是公开资料的翻译、通用知识问答,隐私不敏感,那能力上限就成了主导因素,该上云就上云。
4.2 三种典型场景的决策表
我把日常遇到的活分成三类,每类给一个明确结论,免得每次都要重新纠结。
第一类是“数据敏感 + 中等复杂度”,比如整理内部会议记录、给私有代码写注释、检索自己的笔记库。这类任务我全部放本地,用 7B 或 14B 的 Q4 模型。速度虽然只有 10 token/s 上下,但一段 500 字的总结也就等 50 秒,可以接受。关键是数据一步都没离开机器,这个安心感值这个等待。
第二类是“数据不敏感 + 高复杂度”,比如啃一篇长论文、做多步推理的数学题、生成复杂代码结构。这类我用云端,因为本地 14B 在这类任务上会明显吃力,容易答一半跑偏。这里的判断标准很简单:如果本地模型连续两次答非所问,就别硬扛了。
第三类是“高频轻量”,比如每天几十次的短文本改写、翻译、格式转换。这类我倾向本地,原因是延迟稳定且没有边际成本。云端方案每次几百毫秒的网络往返,几十次累积起来很烦;而且高频调用下,按量计费的成本会比想象中高。
4.3 混合架构的落地方式
真正实用的不是二选一,而是一个能自动分流的入口。我自己的做法是在本地起一个兼容 OpenAI 接口的服务,然后在调用层加一层路由规则。
# 本地起服务,暴露标准接口 ./build/bin/llama-server -m ./models/model-14b-q4_k_m.gguf \ -c 8192 --host 127.0.0.1 --port 8080 -t 4路由规则按优先级排:命中敏感词库或指定目录的请求走本地;上下文超过 8K 的走云端;本地队列积压超过 2 个请求时溢出到云端;其余默认本地。这套规则写下来不到 50 行代码,但能把本地的隐私优势和云端的能力优势都吃到。
提示:路由层一定要做失败兜底。本地服务偶尔会因为内存压力退出,这时候如果没兜底,整个流程就断了。我的做法是本地调用超时 30 秒自动转云端,并且把这次失败记进日志,方便后面调参。
还有一点经验:不要为了“全本地”而牺牲任务质量。我早期有过一段时间坚持什么都本地跑,结果一份需要跨文档对比的分析报告,14B 模型给出的结论有明显遗漏,返工花的时间远超省下的那点调用费。后来想明白了,本地节点的定位是“高频、敏感、轻量”,重活该外包就外包。
5. 踩坑与排查速查表
这部分是我这半年攒下来的问题清单,几乎每个都真实卡过我半天以上。按内存、速度、稳定性三类整理,遇到问题可以直接对表。
5.1 内存相关的坑
最常见的报错是加载到一半退出,提示分配失败。这时候先别怀疑模型坏了,按顺序查三件事:当前 wired limit 是多少、有没有别的进程占着内存、上下文是不是给太大了。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 加载到 80% 退出 | 权重加 KV Cache 超限 | 降量化档或减上下文 |
| 能加载但一对话就崩 | 上下文开太大 | 从 4K 开始往上试 |
| 系统整体变卡 | 触发换页 | 调低 wired limit 或关后台程序 |
| 速度突然掉到 1 以下 | 正在 swap | 立刻停掉,减模型 |
有一条经验值得单独说:内存不够时的表现不是“报错”,而是“变慢”。系统不会立刻告诉你撑不住了,它会悄悄开始换页,你以为模型在思考,其实它在等磁盘。所以测速时一定要盯 swap,这个指标比任何日志都诚实。
5.2 速度相关的坑
同样是 14B Q4,有人测出 12 token/s,有人只有 6,差距往往不在硬件而在配置。
第一个坑是线程数。这类机器的核心是大小核混合架构,线程数开太大会把任务调度到小核上,反而拖慢。我实测 4 线程最优,开 8 线程反而慢 15%。这个参数没有通用答案,得自己二分试。
第二个坑是没开 Metal 加速。编译时如果漏了-DGGML_METAL=ON,跑的就是纯 CPU,速度差三到五倍。判断方法很简单,看启动日志里有没有 Metal 相关的初始化信息。
第三个坑是电源管理。用电池供电时会降频,decode 速度能掉三成。插电测和电池测,数据要分开记,别混在一起比。
5.3 稳定性相关的坑
长时间跑任务时,最容易遇到的是“跑着跑着变慢”。这通常是热降频,机器温度上来后频率被压下去。我的处理是给任务分批,每批之间留十几秒间隙,让温度回落,比一直满载跑要稳。
另一个是服务进程莫名退出。查日志十有八九是内存峰值超了,尤其是处理超长输入的时候,prefill 阶段的中间激活值会比平时大不少。解决办法是给输入做长度上限,或者把长文档先切块再喂。
最后一条,也是我觉得最值的一条经验:给本地节点定一个“能力边界”,写进配置文件,而不是靠记忆。比如我的是“上下文不超过 8K、单次输出不超过 1K、并发不超过 2”。有了这条线,路由层可以自动判断该不该转走,也不会出现某天心血来潮丢个超长文档进去把服务搞挂的情况。边界清晰了,这套本地加云端的组合才真的能长期跑下去,而不是折腾两天就吃灰。