32G统一内存跑大模型:内存账、TPS实测与端云分流
想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统一内存架构的小主机都通用。我最开始的想法很朴素:32G 内存,装个模型绰绰有余,省掉按量计费的 API 账单,数据还不出机器。真正跑起来才发现,这条路要过的坎比想象中多——内存够不等于跑得动,跑得动不等于跑得爽,跑得爽不等于划得来。踩坑主要集中在三个地方:对"32G 能用多少"的判断经常高估,对"真实 TPS"的认知经常被各种口径搞乱,对"到底该本地还是上云"的决策经常凭感觉。这篇就把这三件事拆开讲清楚:统一内存的账怎么算、TPS 怎么自己动手测、端云之间怎么划一条合理的界线。适合想自建本地推理的个人开发者、要做内网部署的技术负责人,以及单纯想知道"这台小机器到底行不行"的读者。不需要你有很深的推理框架经验,但需要你会用命令行、能看懂基本的 Python 脚本。
1. 先把预期校准:32G 统一内存到底能装下什么
1.1 可用额度要扣掉三笔账,别拿 32 直接减
很多人算内存的方式是"32G 减去模型大小",这个算法会让人过度乐观,因为它漏掉了三笔必须扣掉的账。第一笔是系统与常驻后台,桌面系统本身加上浏览器、编辑器、聊天工具,正常工作日吃掉 6 到 8G 很常见,取保守值 7G。第二笔是推理运行时自身的开销,包括计算图缓冲、临时张量、框架加载的动态库,一般 0.8 到 1.5G,按 1G 算。第三笔是 KV cache,也就是上下文缓存,它是随对话长度线性增长的,很多人压根没把它计入预算,结果聊到第三十轮突然整机卡死。
所以真正能分给"模型权重 + KV cache"的额度,大致是 32 − 7 − 1 = 24G,再留 2 到 3G 的系统缓冲余量,实际可用在 21 到 22G 这个区间。这个数字很关键,它决定了你的模型选择上限不是"32G",而是"22G"。我在早期就是因为忽略这点,硬塞一个 32B 的 4bit 模型进去,权重占掉 19G,上下文一开到 8K 就顶到天花板,只要打开一个浏览器标签页,整机就开始频繁换页,风扇拉满,响应从秒级掉到分钟级,最后只能强制重启。这个教训值一次记录。
有一个反直觉的细节:内存压力比内存占用更值得盯。占用 90% 但压力是绿色,机器还能跑;占用 75% 但压力变黄,说明系统已经在压缩内存和换页,速度会断崖式下跌。所以判断"能不能跑"不要只看占用了多少,要看内存压力指示和 swap 使用量,swap 一旦开始持续增长,就说明你超配了。
| 内存项 | 典型占用 | 说明 |
|---|---|---|
| 系统与桌面应用 | 6–8 GB | 浏览器最吃内存,跑模型时建议只留必要窗口 |
| 推理运行时开销 | 0.8–1.5 GB | 计算缓冲、框架动态库、临时张量 |
| 模型权重 | 按量化计算 | 见 1.2 节换算公式 |
| KV cache | 随上下文线性增长 | 见 1.2 节公式,长上下文场景的大头 |
| 安全缓冲 | 2–3 GB | 不设缓冲,系统会在关键时刻假死 |
1.2 权重体积的换算:参数量乘每参数字节数
权重体积的估算公式很朴素:权重体积(GB) ≈ 参数量(十亿) × 每参数占用字节数。麻烦在于"每参数占用字节数"不是一个干净的整数,因为量化不只是把 16 位砍到 4 位,还要额外存缩放因子、零点偏移,以及部分层保留高精度的例外(比如输出层、embedding 层常常不量化)。实践经验是:标称 4bit 的模型,实际平均每参数占用在 0.52 到 0.58 字节之间浮动;标称 8bit 的,实际在 1.0 到 1.1 字节之间。按这个系数算出来的数字,比按 0.5 直接算要准,误差能控制在 5% 以内。
所以 7B 的 4bit 模型实际约 4.0 到 4.5G,14B 约 8 到 9G,32B 约 18 到 20G,70B 的 4bit 已经超过 38G,在 32G 机器上根本放不下。这几个数字建议直接记住,选模型的时候不用每次都算。
KV cache 的公式稍微复杂一点,但也不难:
KV 字节数 = 2 × 层数 × KV头数 × 头维度 × 上下文长度 × 每元素字节数拿一个典型的 32 层、8 个 KV 头(分组查询注意力)、头维度 128 的模型举例,上下文 8192、FP16 存储:
2 × 32 × 8 × 128 × 8192 × 2 = 1,073,741,824 字节 ≈ 1 GiB也就是说这个配置下 8K 上下文只要 1G 的 KV cache,很轻松。但如果把上下文开到 32K,就是 4G;如果模型不支持分组查询注意力(KV 头和注意力头一样多,比如 32 个),同样的上下文要 4G 和 16G,差距是数量级的。这就是为什么同样是 32B 模型,有的能在 32G 机器上开 8K 上下文,有的开 2K 就爆——模型结构差异比参数量差异影响还大。
| 模型规模 | 4bit 权重体积 | 8K 上下文 KV | 32G 机器可行性 |
|---|---|---|---|
| 7–8B | 4.0–4.5 GB | 0.6–1.2 GB | 宽裕,可开 32K |
| 14B | 8–9 GB | 1.2–2.0 GB | 舒适,可开 16–32K |
| 32B | 18–20 GB | 1.5–4.0 GB | 勉强,建议 4–8K |
| 70B | 38–42 GB | — | 放不下,需部分卸载,不建议 |
1.3 真正的天花板是内存带宽,不是容量
这是最容易被忽略的一条。大模型推理分两个阶段:预填充(prefill,把整段提示词一次性算完)和解码(decode,一个 token 一个 token 往外吐)。预填充吃的是算力,解码吃的是内存带宽。而用户感受到的"快不快",绝大部分时间花在解码上——你盯着屏幕上一个个蹦出来的字,那就是解码。
解码阶段每生成一个 token,都要把整个模型的权重从内存里读一遍(严格说是读当前层,但一轮下来等于全部权重)。这就意味着解码速度有个理论上限:
理论最高 TPS ≈ 有效内存带宽 ÷ 权重体积一台统一内存小主机的带宽量级,根据档位不同,大致在一百多到两百 GB/s 这个范围。取中间值 150 GB/s 估算,跑 14B 的 4bit 模型(权重 8.5G),理论峰值就是 150 ÷ 8.5 ≈ 17.6 token/s。实际还有注意力计算、KV cache 读取、调度开销,能跑到理论值的 60% 到 75% 就算不错了,也就是 11 到 13 token/s。这个数字是符合我实测结果的。
反过来说,跑 7B 的 4bit 模型(4.2G),理论峰值 150 ÷ 4.2 ≈ 35 token/s,实测 22 到 26 token/s,体感就是"比人打字快不少,读起来不卡"。跑 32B(19G),理论峰值 7.9,实测 5 到 6.5,体感就是"能读,但等得有点久"。这个体感差异不是线性的,7 token/s 以下就开始明显急躁了,3 token/s 以下基本没法用来做交互式对话。
所以选模型的时候,先用带宽除以权重体积,算出理论峰值,再乘 0.65,你就大概知道这台机器能给你什么体验了。这个心算过程比任何跑分表都实用,因为带宽和权重是你手上唯一确定的两个变量。
2. 真实 TPS 怎么测:把"能跑"翻译成"能用"
2.1 三种 TPS 口径,不同口径下的差异极大
"这个模型在我机器上跑多少 TPS"这句话本身是不完整的,因为至少有三种完全不同的测量口径,数值能差三倍以上。第一种是解码速度(decode TPS),只统计输出阶段的 token 数除以输出耗时,这是最贴近用户体验的指标,也是我最看重的。第二种是端到端速度,用总 token 数(提示词加输出)除以总耗时,短提示词场景下和第一种接近,长提示词场景下会被预填充时间严重稀释。第三种是批量吞吐,多个请求并发时的总 token 数除以总耗时,这个数字通常比单请求解码速度高,但对交互式场景没意义。
我见过不少人拿批量吞吐的数字来吹单机性能,也有人用端到端速度去和外部的按量计费服务比价格,这两种对比都是失真的。你要做的第一件事,是先想清楚自己关心哪个口径。做交互式对话,关心解码速度;做离线批处理,关心批量吞吐;做长文档分析,关心预填充速度和首字延迟(TTFT)。
首字延迟这个指标特别值得单独拎出来说。它等于预填充耗时,直接决定用户按下回车后要愣多久。一个 4K token 的提示词,在算力受限的小主机上预填充速度大概每秒几百个 token,也就是 8 到 20 秒才吐出第一个字。这个体验对交互式应用是致命的,但对"提交任务然后去泡杯咖啡"的批处理场景完全可接受。所以别把 TTFT 和 TPS 混在一起评价,它们是两个独立维度。
| 口径 | 计算方式 | 适用场景 | 我实测的典型值区间 |
|---|---|---|---|
| 首字延迟 TTFT | 请求发出到首个 token 到达 | 交互式对话、在线问答 | 1K 提示 2–5 秒,4K 提示 8–20 秒 |
| 解码速度 | 输出 token 数 ÷ 输出耗时 | 交互式对话、流式生成 | 7B 22–26,14B 11–14,32B 5–6.5 |
| 端到端速度 | 总 token 数 ÷ 总耗时 | 成本核算、批量任务 | 约为解码速度的 40%–80% |
| 批量吞吐 | 并发总 token ÷ 总耗时 | 离线数据处理 | 视并发数而定,单机难有优势 |
2.2 一份可直接用的流式计时脚本
别信任何"我感觉挺快"的描述,包括我上面写的那些数字,你都得自己测一遍才作数,因为散热、后台程序、系统版本都会影响结果。最靠谱的测法是走流式接口,逐 token 打时间戳,这样 TTFT 和解码速度能一次性拿到。
下面这段脚本假设你在本地起了一个兼容流式接口的推理服务,端口按你实际的改:
import time import json import requests def measure(url, model, prompt, max_tokens=256): payload = { "model": model, "messages": [{"role": "user", "content": prompt}], "max_tokens": max_tokens, "stream": True, "temperature": 0.2, } t_start = time.perf_counter() t_first = None chunks = 0 with requests.post(url, json=payload, stream=True, timeout=600) as r: for line in r.iter_lines(): if not line: continue if not line.startswith(b"data: "): continue body = line[6:] if body.strip() == b"[DONE]": break try: obj = json.loads(body) except json.JSONDecodeError: continue delta = obj.get("choices", [{}])[0].get("delta", {}) if delta.get("content"): if t_first is None: t_first = time.perf_counter() chunks += 1 t_end = time.perf_counter() ttft = (t_first - t_start) if t_first else float("nan") decode_time = (t_end - t_first) if t_first else float("nan") decode_tps = (chunks - 1) / decode_time if decode_time and chunks > 1 else 0 e2e_tps = chunks / (t_end - t_start) print(f"输出 token 数: {chunks}") print(f"首字延迟 TTFT: {ttft:.2f} s") print(f"解码速度: {decode_tps:.2f} tok/s") print(f"端到端速度: {e2e_tps:.2f} tok/s") return ttft, decode_tps, e2e_tps if __name__ == "__main__": url = "http://127.0.0.1:8080/v1/chat/completions" prompt = "用三百字解释一下什么是内存带宽,尽量具体。" measure(url, "local-model", prompt, max_tokens=300)几个使用上的细节,不注意会让结果失真。第一,max_tokens别设太小,太小的样本受启动抖动影响大,建议 256 到 512。第二,同一个测试连跑三次取中间值,第一次往往因为模型刚加载、缓存还没热而偏慢。第三,测试时把浏览器、编辑器、聊天工具都关掉,因为这些程序的内存占用会挤压模型可用的缓存空间,直接影响解码速度。第四,温度设低一点(0.2 左右),高温采样会增加一点开销,虽然影响不大,但没必要引入变量。
如果你不想写脚本,也有偷懒的办法:用命令行工具跑一次生成长文本的任务,用time包起来,然后数一下输出的大致 token 数(中文按每字约 1.5 token 粗算,英文按每 4 字符 1 token 粗算),除一下就是端到端速度。这个方法精度差,但足够判断"这台机器能不能用"。
# 只关心单次生成长文本的端到端耗时 time local-llm run \ --model ./models/model-14b-q4 \ --ctx 8192 \ --predict 512 \ --prompt "写一篇八百字的产品说明,主题是便携咖啡机。"2.3 实测结果表:不同规模在不同上下文下的表现
我把手上这台机器(32G 统一内存)的实测数据整理成下表,测试条件统一为:温度 0.2、输出 256 token、后台只保留终端。上下文长度那一列是指总上下文(提示词加输出),因为 KV cache 的开销是按总长度算的,这是很多人测错的地方——只改输出长度不改提示词长度,是测不出上下文衰减的。
| 模型规模 | 上下文 2K | 上下文 8K | 上下文 32K | 备注 |
|---|---|---|---|---|
| 7–8B 4bit | 26 tok/s | 23 tok/s | 17 tok/s | 全场景可用,长上下文衰减可接受 |
| 14B 4bit | 13 tok/s | 11 tok/s | 8 tok/s | 日常主力,8K 以内体验最好 |
| 32B 4bit | 6.5 tok/s | 5.2 tok/s | 内存不足 | 仅建议短上下文、离线任务 |
| 70B 3bit 部分卸载 | 2.0 tok/s | 1.4 tok/s | 无法运行 | 不建议,性价比极低 |
这张表里最有信息量的是"上下文从 2K 涨到 32K,7B 模型的解码速度掉了三分之一"这件事。原因有两个:一是 KV cache 变大了,每次生成新 token 要读的缓存更多,挤占了本就不富裕的带宽;二是注意力计算量随上下文线性增长,在解码阶段开始变成不可忽略的开销。这个衰减曲线不是线性的,8K 以内比较平缓,16K 之后开始明显往下掉,所以如果要做长文档场景,最好在 8K 以内做分块处理,而不是硬开大上下文。
还有一点值得单独提:预填充速度和上下文长度的关系更陡。同样这台机器,1K 提示词的首字延迟 2 到 4 秒,4K 就到 10 秒上下,8K 能到 30 秒。这意味着长文档总结这种任务,本地跑的瓶颈不在生成而在"读题"。如果你要做的是"扔进去一份两万字报告,让它总结",那本地体验会比你想的差很多,这个场景恰恰是云端更擅长的。
2.4 影响 TPS 的七个变量,按影响从大到小排
测了这么多轮,我按影响程度把变量排了个序,方便你在速度不达预期时按顺序排查。
第一位是模型结构和量化方式。分组查询注意力的头数差异能让 KV cache 差四倍;混合专家结构的模型激活参数少,解码速度可能比同参数量的稠密模型快两三倍,但权重仍要全部驻留内存,内存账并不会省。量化方式上,4bit 和 5bit 的速度差通常在 10% 以内,但 8bit 比 4bit 慢接近一倍,因为每 token 要读的字节数翻倍了。
第二位是上下文长度。上节已经说了,这个影响是持续的、累积的,不是一次性开销。
第三位是散热与降频。小主机的散热余量本来就紧,连续跑十几分钟后如果风扇转速拉满、机身发烫,解码速度掉 20% 到 30% 是常事。我做过对比测试:冷机第一次跑 13 tok/s,连续跑二十分钟后掉到 10 tok/s。所以任何"跑分"如果不注明持续时长,参考价值都有限。
第四位是后台内存压力。这个前面提过,内存压力变黄之后系统开始压缩和换页,速度是断崖式的,不是渐变。
第五位是线程数配置。线程数设成物理核心数通常最优,设成逻辑核心数(超线程数)反而会因为争抢而变慢。这个参数很多人随手设成 8 或 16,其实应该按机器实际核心数来。
第六位是批处理大小。单请求场景下批大小设 1 就行,设大了只会浪费内存。只有做离线批量任务时才值得调大。
第七位是采样参数。温度、top-p、重复惩罚这些对速度影响很小,但如果你开了很激进的重复惩罚,会在每步做额外的词表扫描,边际上有几个百分点的影响。
3. 从"跑起来"到"跑得舒服":一份可复现的部署调参流程
3.1 环境与模型文件准备,先做减法
部署这件事,我的经验是"先做减法再做加法"。减法指的是把机器上不必要的东西先清掉:关掉开机自启的云盘同步、关掉常驻的聊天工具、退出浏览器。这一套下来能腾出 4 到 6G 内存,直接决定了你能不能跑大一号的模型。我做过对比,同样跑 14B,清完后台从 11 tok/s 提到 13 tok/s,跑 32B 从"勉强能开 4K"变成"能开 8K",收益比调任何参数都大。
模型文件的选择上,优先挑量化版本里标注了分组查询注意力、并且 KV cache 量化可用的。文件名里通常会带量化和上下文信息,比如带 q4、q5 这类标识的就是 4bit、5bit 量化。下载后第一件事是核对文件大小,和 1.2 节算出来的理论值对照,差得太多说明要么量化位数不对,要么混了不该混的层。
存储空间也要留够。模型文件动辄十几 G,加上系统盘本身要留 15% 以上的空闲(否则文件系统性能会下降),所以 256G 的机器跑 32B 模型基本是紧平衡。建议把模型放在外置高速存储上,但要注意外置盘的读取速度如果跟不上,模型加载时间会明显变长——加载 19G 的模型,内置盘可能几十秒,慢速外置盘能到几分钟。
3.2 关键参数逐项拆解:每一个都有它存在的理由
参数不是越多越好,我建议先把下面这几个搞明白,其余的用默认值就行。
上下文长度(ctx)。这是最需要克制的一个参数。它决定了 KV cache 的大小,也决定了内存能不能撑住。设置原则是"按实际需要再加 20%",不是"能开多大开多大"。因为框架通常会按你设置的上限预分配 KV cache 空间,你设 32K 它就占 32K 的内存,哪怕你实际只聊了 2K。这个设计很多人不知道,导致内存莫名其妙被吃掉一大块。我现在固定设 8K,跑 14B 时内存占用稳定在 13G 左右,很舒服。
卸载到加速器的层数。全量卸载(把所有权重层都放进统一内存由图形处理器计算)在统一内存架构上是默认选择,因为内存本来就是共享的,没有"显存不够"的问题。但如果模型太大,就得留几层在处理器上算,速度会掉一个数量级。所以这个参数的正确用法是:能全量就全量,一旦需要部分卸载,说明模型选大了,换小一号比调参数划算。
KV cache 量化。把 KV cache 从 16 位降到 8 位,能省一半缓存内存,代价是长上下文时精度略有下降。这个取舍在 32G 机器上很值得做,尤其是你要跑 32B 模型的时候——8K 上下文的 KV 从 2G 降到 1G,可能就是"能跑"和"不能跑"的区别。实测下来,8 位 KV 量化对输出质量的影响,在我的使用场景里几乎感知不到,但对内存的缓解是实打实的。
线程数。设成物理核心数,别设逻辑核心数。这条前面说过,但值得重复,因为它是免费的提速。
批大小。交互场景设 1,离线批处理按内存余量往上调,但注意批大小是乘在 KV cache 上的——批 4、上下文 8K,KV cache 就是单请求的四倍。这个乘法关系经常被忽略。
| 参数 | 我的推荐值(32G 机器) | 调整理由 |
|---|---|---|
| 上下文长度 | 8192(交互)、32768(7B 专用) | 框架按上限预分配 KV,设大了纯浪费 |
| 加速器卸载层数 | 全部 | 部分卸载速度掉一个数量级,不如换小模型 |
| KV cache 精度 | 8 位 | 省一半缓存,质量损失几乎不可感知 |
| 线程数 | 物理核心数 | 设逻辑核心数会因争抢变慢 |
| 批大小 | 1(交互)、2–4(离线) | 批大小与 KV cache 是乘法关系 |
| 温度 | 0.2–0.7 | 与速度关系不大,按任务调 |
3.3 一份可以直接抄的启动配置与验证流程
把上面这些组合起来,我给两套配置,一套是日常交互用,一套是跑大模型时用。
# 配置 A:14B 模型,日常交互主力,追求响应速度 local-llm serve \ --model ./models/model-14b-q4 \ --ctx 8192 \ --batch 1 \ --threads 8 \ --kv-cache-type q8_0 \ --gpu-layers 99 \ --port 8080 # 配置 B:32B 模型,短上下文,离线任务 local-llm serve \ --model ./models/model-32b-q4 \ --ctx 4096 \ --batch 1 \ --threads 8 \ --kv-cache-type q8_0 \ --gpu-layers 99 \ --port 8081启动之后的验证流程,我固定跑四步,每一步都有明确的判断标准:
第一步,看加载日志里的内存分配。日志通常会打印权重占用和 KV cache 预分配的大小,把这两个数加起来,对照 1.1 节的可用额度,如果超过 22G,直接换小模型,别硬撑。
第二步,测一次冷启动的首字延迟。发一个 500 字左右的提示词,记录第一个 token 到达的时间。超过 5 秒说明预填充偏慢,考虑缩短提示词或者换更小的模型。
第三步,测三次 256 token 的输出,看解码速度的稳定性。三次结果波动超过 15%,说明有后台干扰或者散热问题,去查内存压力和机身温度。
第四步,跑到上下文上限附近再测一次。这是最容易被跳过的一步,但恰恰最重要。很多人测速只测第一轮对话,那时候上下文才几百 token,自然是快的。真正的问题是聊到第 20 轮、上下文涨到 6K 的时候,速度掉到多少、内存还剩多少。我的做法是准备一段长文本反复喂进去,把上下文顶到上限附近,再测一次解码速度,这个数字才是你实际使用中会遇到的。
3.4 让老机器更稳的几个小动作
有几个不起眼但很管用的操作,我按有效性排序。
一是限制模型服务的常驻内存上限。有些框架支持设置最大内存,超过就报错退出,这比死机强得多。宁可服务崩掉重启,也不要整机卡死——整机卡死的时候你连强制退出的机会都没有,只能长按电源键。
二是把 swap 用起来,但要监控它。系统盘上的交换空间是一道保险,模型偶发超配时能救你一命。但如果 swap 持续增长,说明你的配置长期超配,这时候性能已经不可用了,必须换配置。我的判断标准是:swap 使用量稳定不变可以接受,持续增长就必须处理。
三是固定电源与性能模式。小主机在节能模式下会主动降频,跑模型时一定要切到高性能模式。这个设置一次就好,别忘了。
四是把模型文件放在独立目录,别和系统更新搅在一起。系统大版本更新后,有时候会重建索引,扫描几十 G 的模型文件,机器会突然卡一阵。放在独立目录并且排除索引,能避免这个干扰。
4. 端云决策:不是二选一,是按任务分流
4.1 一张决策矩阵,先想清楚你要的是什么
"本地还是云端"这个问题之所以难回答,是因为大部分人把它当成了一个非此即彼的选择题,而实际上它应该是按任务分流的。我给自己定的决策依据是四个维度:延迟要求、数据敏感度、调用频率、质量要求。这四个维度组合起来,答案往往很清晰。
延迟敏感 + 数据敏感 + 高频 = 本地。典型场景是内网的知识库问答、代码补全辅助、日常的文本改写。这些任务对交互延迟有要求,数据不方便出门,而且调用量大,本地跑省下的费用最可观。
延迟不敏感 + 数据敏感 + 低频 = 本地,但可以接受慢。典型场景是敏感文档的批量摘要、内网日志的归类分析。这类任务可以挂着慢慢跑,速度不是关键。
质量要求高 + 复杂推理 = 云端。这个没什么好纠结的,小模型和大模型在复杂推理上的差距是明显的,尤其是多步推理、长链条代码生成、专业领域深度问题。本地跑 14B 能做的事,和云端跑一个更大的模型能做的事,不在一个层级上。
长上下文 + 高并发 = 云端。前面测过,长上下文的预填充在本地是分钟级的,并发更是完全撑不住。这类需求交给云端是理性的。
| 任务类型 | 建议 | 主要原因 |
|---|---|---|
| 日常对话、文本改写 | 本地 | 延迟低、无边际成本、质量够用 |
| 内网知识库问答 | 本地 | 数据不出内网是硬约束 |
| 代码补全、片段生成 | 本地 | 高频、短输出、延迟敏感 |
| 敏感文档批量摘要 | 本地 | 可以慢,但不能外传 |
| 复杂多步推理 | 云端 | 小模型能力上限明显 |
| 十万字长文档分析 | 云端 | 本地预填充时间不可接受 |
| 高并发服务 | 云端 | 单机吞吐撑不住 |
| 偶尔用一次的任务 | 云端 | 本地部署的时间成本划不来 |
4.2 把本地成本算成钱:这笔账很多人算错了
"本地跑省钱"这句话,在很多场景下是站不住的。我们把账算细一点。
硬件成本按折旧摊。假设这台机器 6000 元,用三年,每天实际负载 4 小时(这个负载强度已经不低了),总可用时长 3 × 365 × 4 = 4380 小时,摊下来硬件成本约 1.37 元每小时。电力成本按整机功耗 50 瓦算,每小时 0.05 度电,按 0.65 元一度,约 0.03 元每小时。合计约 1.4 元每小时。
产出这边,跑 14B 模型解码 13 tok/s,理论上一小时能出 46800 token。但实际使用中有等待、有思考、有预填充、有闲置,真实利用率能到 50% 就不错了,按 23000 token 每小时算。那么每百万 token 的成本约是 1.4 ÷ 23000 × 1000000 ≈ 61 元。
这个数字意味着什么?意味着如果你的任务量不大,本地跑的单位成本比按量计费的服务贵得多。本地真正省钱的场景是:调用量极大,把硬件成本摊薄;或者你的时间不值钱(开玩笑的说法,但确实是事实)。
那本地的价值在哪?我认为是三条,都不是省钱:数据不出机器,这是最硬的一条,很多场景没得选;没有配额和限流,想跑多少跑多少,不用看着账单犹豫;离线可用,断网也能干活。这三条对特定场景是刚需,对普通场景就是锦上添花。所以如果你只是"想省点 API 费",先算一遍上面这笔账,可能会改主意。
4.3 混合架构怎么落地:一个路由层就够
如果两边都要用,工程上不需要搞得很复杂,一个路由层加一套降级逻辑就够用。路由层做的事很简单:判断这个请求该走哪边。
import re LOCAL_TASKS = {"rewrite", "summarize_short", "code_complete", "translate"} SENSITIVE_PATTERNS = [r"内部", r"合同", r"客户名单"] # 按你的实际情况补充 def route(request): text = request["text"] task = request.get("task", "chat") # 一、敏感内容一律本地 if any(re.search(p, text) for p in SENSITIVE_PATTERNS): return "local" # 二、短输入短输出的高频任务走本地 if task in LOCAL_TASKS and len(text) < 2000: return "local" # 三、超长输入走云端(本地预填充扛不住) if len(text) > 20000: return "cloud" # 四、其余走云端,失败时降级到本地 return "cloud" def call_with_fallback(request): target = route(request) try: return call_cloud(request) if target == "cloud" else call_local(request) except Exception: # 云端不可用时降级本地,本地也不可用时抛错 return call_local(request) if target == "cloud" else call_cloud(request)这套逻辑的要点有三个。第一,敏感判断放在最前面,别让敏感内容先经过一次路由计算再决定,减少误判路径。第二,降级方向要选对:质量敏感的任务云端挂了降级到本地,是"降质保可用";长文档任务降级到本地可能会直接超时,这种情况应该返回错误而不是硬扛。第三,加一层结果缓存,同样的输入短时间内重复请求,直接命中缓存,这一层能省掉相当一部分调用量,尤其是在开发和调试阶段。
还有一个小技巧:本地模型的输出质量如果只是"略差一点",可以用云端结果做校准样本,把常见的失败案例整理成少样本示例塞进本地提示词里,往往能把质量补回来一大截。这个做法成本极低,效果比我预期好。
4.4 一条容易被忽略的边界:什么内容不该进本地模型
最后说一条实践中的边界。本地部署带来的"数据不出机器"的安全感,有时候会让人放松对内容本身的管控。实际上本地模型同样会产生日志、缓存、会话历史,这些文件默认落在磁盘上,如果你在共享机器上跑,等于把对话内容留给了下一个使用者。
我的处理方式是三条:把会话历史目录设置为临时目录并定期清理,服务只监听本地回环地址不对外暴露,日志级别调到只记录错误不记录请求内容。这三条做起来只要几分钟,但能避免很多麻烦。另外,本地模型的能力边界也要心里有数,它在不确定的时候会一本正经地编,这个特性在涉及事实核查的场景下要格外小心,别因为"这是我自己机器跑的"就放松警惕。
5. 踩坑实录与常见问题速查
5.1 内存打满和整机假死,怎么提前发现
这个问题我遇到过三次,前两次都是硬重启解决的,第三次才摸清规律。表现是:模型跑着跑着,风扇突然拉满,鼠标还能动但点击没反应,几秒后整个界面冻结,等十分钟也不恢复。
根本原因是内存压力到了临界点,系统开始大量换页,磁盘读写打满,所有进程都被拖住。关键在于这个崩溃是有前兆的,只是前兆出现的时间窗口很短,可能只有十几秒。前兆是:内存压力指示灯变黄,swap 使用量开始以每秒几十兆的速度增长。
所以我的做法是开一个终端窗口常驻,跑一个简单的监控脚本,盯着这两个指标:
# 每 2 秒打印一次内存压力和 swap 使用量 while true; do echo "--- $(date +%H:%M:%S) ---" memory_pressure | tail -3 sysctl vm.swapusage sleep 2 done看到 swap 使用量在涨,立刻停掉推理服务,别犹豫。这时候停服务只要几秒,等系统完全卡死就得硬重启,损失大得多。
预防上,最有效的一招是给上下文长度设一个你有把握的上限,而不是"能开多大开多大"。我现在的固定配置是 14B 配 8K、32B 配 4K,跑一整天不关也不会出问题。之前图省事设 32K,结果聊到第 30 轮就爆了,而且爆得很突然,因为 KV cache 是线性增长的,前面一直很稳,到某个点突然越线。
5.2 输出质量突然变差,按这个顺序排查
质量问题的排查比性能问题麻烦,因为它没有明确的数字指标。我总结了一个排查顺序,从最可能的原因开始。
先看上下文是不是快满了。上下文接近上限时,框架会开始截断早期内容,表现为"它忘了我前面说过的话"。这个是最常见的原因,解决方法是开新会话或者缩短提示词。判断方法很简单:看日志里当前上下文字符数,如果接近设定上限的 90%,基本就是这个原因。
再看 KV cache 量化是不是过于激进。8 位量化一般没问题,但如果某些框架支持 4 位 KV 量化,长上下文时的精度损失会很明显,表现为输出变得啰嗦、重复、抓不住重点。把 KV 量化调回 8 位或者 16 位对比一下,能快速定位。
第三看量化位数和模型匹配度。同样是 4bit,不同量化方法的实际效果差异挺大。有些激进量化会把关键的注意力层也压到很低精度,导致模型在某些任务上突然"变笨"。判断方法是换一个量化版本对比,如果换了就好,说明是量化问题。
第四看采样参数。温度过高会胡言乱语,重复惩罚过强会导致句子结构崩坏,这两个参数调错了,表现出来都像"模型变笨了"。我的习惯是把温度固定在 0.3 到 0.7 之间,重复惩罚不超过 1.15。
最后才怀疑模型本身能力不足。这个顺序很重要,因为大部分人一遇到质量问题就觉得"小模型不行",实际上八成是配置问题。先把前四个排查完,再下结论。
5.3 常见问题速查表
| 现象 | 最可能的原因 | 处理方式 |
|---|---|---|
| 整机卡死、风扇拉满 | 内存超配触发大量换页 | 立刻停服务,降低上下文或换小模型 |
| 聊到后面越来越慢 | KV cache 随上下文增长 | 开新会话,或把上下文上限设小 |
| 首字延迟突然变长 | 提示词过长,预填充耗时高 | 拆分提示词,或先做检索再喂 |
| 连续跑十几分钟后变慢 | 散热降频 | 改善通风,或接受 20%–30% 的性能损失 |
| 内存占用比预期高很多 | 框架按上下文上限预分配 KV | 把上限设成实际需要的 1.2 倍 |
| 输出重复、绕圈 | 采样参数不当或 KV 量化过度 | 调低重复惩罚,KV 量化回 8 位 |
| 输出突然变笨 | 上下文截断,早期信息丢失 | 检查当前上下文长度,开新会话 |
| 模型加载特别慢 | 模型在外置慢速盘上 | 移到内置盘,或换高速外置存储 |
| 服务启动就报内存不足 | 后台程序占用过多 | 关浏览器、云盘同步、聊天工具 |
| 部分卸载后速度暴跌 | 有层落到处理器上计算 | 换小一号模型,别硬跑大模型 |
这张表里的每一条我都实际踩过,其中"内存占用比预期高很多"那一条最坑——我一开始以为是模型文件有问题,查了半天才发现是框架按上下文上限预分配了 KV cache 空间,把上限从 32K 改到 8K,内存立刻降了 3G。这个设计在文档里通常一句话带过,但它对 32G 这种紧平衡机器的实际影响,比任何参数都大。
我个人在实际操作中的体会是,这类小主机跑大模型,真正的门槛不在"能不能跑起来",而在"能不能稳定地跑一整天不出事"。前者半小时就能搞定,后者需要把内存账、上下文上限、散热、后台占用这几件事都想清楚,并且给自己留足缓冲。我现在的心态是:宁可跑小一号的模型、开小一点的上下文,换来一整天不用管它,也不要为了追求"能跑 32B"这个标签,让自己每半小时就要去看一眼内存压力。这个取舍做完之后,本地这套东西才真正变成了日常工具,而不是一个需要照看的实验品。