最近被一个“80 倍”的性能对比搞得相当狼狈。前提是端侧推理——模型不联网,完全在手机本地跑 LLM 的那种部署方式。别人演示视频里每秒稳定输出二十多个 token,我手里这台设备一个字一个字往外蹦,体感差距大到让人怀疑是不是拿错了机器。以前遇到这种问题,第一反应基本是“这手机不行”,但这次我咬着牙把整个链路翻了一遍,结论完全不是那回事:慢 80 倍是六七个独立因素叠乘的结果,每个因素单独看只有 2 倍左右,叠起来就夸张了。
这篇文章就是那次排查的完整记录,包括怎么拉齐对比口径、怎么用带宽公式估算理论极限、怎么逐项确认每层瓶颈,以及最终把速度从 0.4 token/s 拉到 12.8 token/s 的每一步操作。适合正在做端侧 AI 应用落地、或是在手机上折腾本地模型性能的人看;如果你只是遇到过“同样的手机别人为什么比我快”的疑问,也能当一份排查思路参考。
1. 对比跑分前,先对齐这 6 个变量
1.1 我到底是怎么测出 80 倍的
先说现象。我手里的某中端机型,跑一个 7B 参数的通用对话模型,当时选的量化档位是偏高精度的 Q8,回答一句十几字的话要憋二十秒;参照对象是网上某位博主的演示——同一个模型家族,但只有 3B 参数、4bit 量化,输出速度接近每秒 25 个 token。这么一算,完成同样一段回复,我这边要超过半分钟,人家一秒出头,表面差异已经 25 倍了。真正把差距推到 80 倍的,是我自己又叠了 32K 超长上下文设置和系统均衡模式,于是数字一路放大到“连一篇短文都生成不完”的夸张程度。
第一次计时我没有偷懒。用秒表从点击发送开始计时,到第一个字出现记为“首字延迟”,从开始生成到最后结束记为“总时长”。同一条 prompt 在两条设备上各跑三轮,取中间值,最后 A 设备平均 0.4 token/s,B 设备平均 32 token/s,差值正好 80 倍。数据看起来很唬人,但拿着这个结果去做定位是完全无效的——因为两个设备的变量太多了。性能对比的第一步不是找原因,而是把所有会影响的变量列出来,确认哪些对齐了、哪些没有,否则你比出来的根本不是“设备差距”,而是“配置差距”的混合体。
1.2 每个变量都能让速度差出数量级
我在排查前整理了一张变量表,一列下来冷汗都快出来了,几乎每一项都足以让速度产生数倍变化。
| 变量 | 为什么影响大 | 我这边初始状态 | 参照状态 | 是否对齐 |
|---|---|---|---|---|
| 模型参数量 | 每生成一个 token 都要把全部权重搬一遍,3B 和 7B 的搬运量差两倍多 | 7B | 3B | 否 |
| 量化档位 | Q4 与 Q8 相比速度差距约 2 倍,与 F16 相比接近 4 倍 | Q8 | Q4 | 否 |
| 推理后端 | CPU、GPU、NPU 三种路径差异不止 1 倍,同框架内换后端就能看出区别 | CPU | GPU | 否 |
| 上下文长度 | KV cache 大小决定解码时额外读取的内存字节数,2K 和 32K 能拉出接近 2 倍差距 | 32K | 2K 左右 | 否 |
| 系统模式 | 性能、均衡、省电三档对 CPU/GPU 最高频率影响巨大 | 均衡 | 未确认 | 否 |
| 加载方式 | 是否内存映射、是否每次重新读权重,直接影响首轮速度和稳定速度 | 每次加载 | 内存映射 | 否 |
结论很清晰:所谓“我的手机比别人的慢 80 倍”,一大半是拿我的 A 变量对比别人的 B 变量,真正的硬件差距反而被淹没了。所以排查第一步,是把两台手机拉到同样的模型、同样的量化、同样的 context、同样的框架,这时候再测出的差距才是硬件的真实差距。这个道理听起来简单,但实际排查中太容易跳过——看到别人视频里参数就“默认”和他一致,是我踩的第一个坑。
2. 用内存带宽公式,算出手机端侧推理的极限速度
2.1 端侧解码是带宽游戏,不是算力游戏
这是整个排查里最重要的认知升级。大模型生成 token 的时候,每个新 token 都依赖前面所有 token 的状态,所以解码阶段的 batch 基本等于 1,根本没有什么并行度。在这个前提下,每一轮解码的核心动作是:把模型的全部权重从内存搬到计算单元,算一遍前向,得到下一个 token。也就是说,生成速度的上限不是算力,而是内存带宽。
用个朴素公式估算:
极限 token/s ≈ 内存带宽 / 单次解码需要读取的权重字节数举个例子。一个 7B 模型用 4bit 量化后大约 4GB,如果手机内存带宽是 20GB/s,解码天花板就是每秒 5 个 token;带宽到 40GB/s 时,天花板到 10 个 token/s。中端机和旗舰机在内存带宽上经常差一倍甚至更多——这个不是靠优化能弥补的,是硬件规格决定的。所谓“某些自研 SoC 跑 LLM 特别快”的真实原因之一,就是内存带宽做到了 60GB/s 以上,而且统一内存架构让 CPU/GPU 共享同一块高带宽,权重搬运路径更短。
还要区分两个阶段。日常聊天时,速度感知来自解码阶段,就是那个逐字往外蹦的过程;但首次回复的“思考”阶段其实是 prefill,一次并行处理几百上千个 prompt token,虽然总计算量不小,但并行度高,耗时往往在半秒到几秒之间。很多人误以为两个阶段应该一样快,或者只看“下载完模型第一次运行多快”,这也是对比口径经常出错的地方。
2.2 用实测数字逆推,瓶颈到底在哪一层
拿到手机之后,我第一件事不是调参,而是用推理框架自带的计时输出测量两个数字:prompt 处理阶段的速度和生成阶段的速度。典型日志长这样:处理 prompt 用了多少 token 和多少秒,单位是 token/s;随后是生成阶段,显示每 token 平均耗时。如果你的前端不打印这类信息,就自己计时算:记录总生成 token 数和总耗时,相除即可。
我在一台中端机上跑了 3B Q4 模型,实测生成速度大约 4.2 token/s。用带宽公式反推:假设这台设备的内存带宽在 17GB/s 左右,3B Q4 权重约 2GB,理论极限大概 8 token/s。实测 4.2,说明前端优化还有空间,但不属于病态;相比之下,那台慢速场景里的 0.4 token/s 就完全不正常——它比理论极限低了二十倍,根本不能用“带宽不够”解释,而是没跑在正确的路径上。
这里提炼出排查的第二条方法论:先算天花板,再看地板离天花板有多远。如果实测速度接近理论极限,就不要再纠结后端和线程,老老实实换更小的模型、更低的量化、更短的上下文才是出路;如果实测比理论极限低好几倍,就不要在量化档位上钻牛角尖,立刻回头检查后端、加载方式、核心调度。
3. 排查实录:模型、框架、上下文中最容易忽视的瓶颈
3.1 模型格式与量化档位:最容易白捡的 4 倍差距
现在开始逐项排查。第一个大变量是模型格式。以本地推理最常见的 GGUF 容器格式为例,它把权重、词表、超参数打包在一个文件里,支持多种量化档位:F16 半精度、Q8_0、Q4_K_M、Q2_K 等。这几档的速度差异几乎和文件大小成反比。同样的 7B 模型,F16 约 14GB,Q8_0 约 7.5GB,Q4_K_M 约 4.5GB,Q2_K 约 2.7GB——每一档之间都接近两倍差距,从 F16 到 Q4 整体就是三到四倍,完全没有中间商。
排查中第一个重大发现是:我当初下载的“某模型”,虽然命名上写着 4bit 量化,但实际存成的是未量化的 F16 版本。某些前端在加载这种文件时要做动态反量化,速度自然惨不忍睹。前端显示的“已量化”和推理时是否真的走量化内核,是两回事。判断方法很直接:看文件大小。7B 模型如果文件超过 8GB,基本不可能是 Q4;3B 模型超过 4GB,也很可疑。文件大小和量化元数据对不上,直接换文件,别和它较劲。
再补一条实战经验:同是 4bit 量化,像 Q4_K_M 这类较新的量化方法,和老式 Q4_0 相比质量损失更小,速度几乎不变。在端侧绝大多数场景里,Q4_K_M 是性价比最高的甜蜜点。想追求更高输出质量再考虑 Q5/Q8,但代价是速度明显下降,值不值完全取决于你的任务。
3.2 推理框架与后端:是不是真的用上了 GPU
第二个大变量是推理后端。同一款手机、同一个模型,不同的推理方案跑,速度差出 3 到 5 倍很常见。常见方案大致三类:第一类是纯 CPU 优化型,依赖 ARM 指令集优化和线程调度;第二类是编译到系统图形计算框架的 GPU 后端;第三类是直接走 NPU 的专用方案,但这类方案的模型支持和工具链成熟度参差不齐,不是开箱即用。
我在某台中端机上做了个对照实验:同一个 3B 模型,方案 A 测出 4.2 token/s,方案 B 测出 12.6 token/s。差异来源就是方案 B 走了 GPU 路径。怎么确认有没有走 GPU?看运行时日志里的设备标识是一个办法,但更可靠的是系统资源监视器——推理期间如果 CPU 占用率接近 100% 而 GPU 利用率很低,那大概率没走 GPU。更隐蔽的情况是某个后端声称支持 NPU,实际走的却是 NPU 兼容层,兼容层内部又悄悄回退到 CPU。这个问题误导了我一整天,最后是靠硬件占用视图里“NPU 空闲”这个事实才抓出来的。
同时,GPU 路径也不是一定就快。有的设备在用图形计算框架跑大模型时,频繁做显存分配和内存拷贝,反而比优化好的 CPU 路径更慢;或者某厂商驱动对某类矩阵乘法有缺陷,导致个别算子性能异常。所以正确态度是:优先选成熟方案,但每台设备都实测,不要迷信“GPU 一定比 CPU 快”。
3.3 context 长度与 KV Cache:越长越慢,慢多少可以算
第三个大变量是上下文长度,context length。很多前端默认把上下文配到 16K 甚至 32K,用户根本感知不到。KV cache 是自回归解码时的临时状态,用来缓存每个历史 token 的 Key 和 Value。上下文越长,KV cache 越大,解码时不仅要把全部权重从内存搬到计算单元,还要反复读取 KV cache,带宽消耗显著增加。极端情况下,KV cache 的读取开销可以接近甚至超过权重搬运开销,速度会再掉一截。
具体慢多少,还是可以用字节估算。KV cache 的大小和层数、注意力头数、是否使用分组查询注意力(GQA)有关。GQA 把 KV 头数压缩到原来的几分之一,是端侧跑长文本的关键。同一个模型,开不开 GQA,KV cache 可能差四倍;不同模型之间,同样 32K 上下文,KV cache 可能是 200MB,也可能是 2GB。这也是为什么别人 32K 能流畅跑,你 32K 直接卡成幻灯片——很可能他的模型用了 GQA 和 KV 量化,你没有。
我把同一条 prompt 分别在 4K、16K、32K 上下文下跑了一遍,生成速度分别是 5.1、3.9、3.2 token/s。不同模型没有普适性,但趋势非常明确:长上下文不是免费的。你要么接受它慢,要么就主动裁剪。如果应用场景只是普通问答,2K 到 4K 完全够用,没必要为理论上的“支持长文本”买单。
3.4 系统模式、温度与大小核:看不见的手
最后一个容易忽略的层面是系统状态。同一台手机,开性能和开省电,CPU 最高频率可能差 30% 以上;手机发热触发温控后,性能可以直接腰斩;推理线程被调度到小核上,速度掉到三分之一甚至五分之一都是常事。中端机的大小核架构尤其危险:如果框架没有做绑核,系统很容易把负载线程扔到功耗较低的小核上,表面看着“CPU 占用很高”,实际跑在错误的核心上。
我在排查 0.4 token/s 的慢速场景时,翻开系统资源监视器发现:四个大核全部空闲,推理任务全部压在四个小核上,CPU 频率还被限制在中低频段。再加上后台有一个系统更新服务一直在跑,内存带宽被分走一部分,三重因素叠加,解释了那一半以上的慢速。关掉后台任务、切到性能模式、对推理线程做绑核之后,速度直接从 0.4 跳到了 2.8 token/s,翻了整整 7 倍。这比换模型换框架的收益来得更快,也提醒我:调优顺序应该先软件后硬件、先配置后参数。
4. 按收益排序的调优清单:从 0.4 到 12.8 token/s 的完整过程
4.1 五步调优,每一步都带来成倍收益
经过上面的排查,我把所有变量按收益和改造成本排了个序,实际操作时也按这个顺序来。
第一步,对齐模型档位。如果任务不是必须 7B,就换 3B 甚至更小的 1B 级模型。模型参数减半,带宽压力直接减半,收益在 2 到 3 倍。 第二步,把量化降到 Q4_K_M 这类高效档位。Q8 到 Q4 大约 2 倍收益,F16 到 Q4 是 3 到 4 倍。成本几乎为零,唯一要关心的是质量能不能接受。 第三步,检查推理后端。打开日志确认是否走 GPU 路径,如果没有,换支持 GPU 的方案或者打开对应开关,收益 2 到 4 倍。 第四步,裁剪上下文。根据实际需求设置 context,能 2K 就别 32K,收益 1.5 到 2 倍。 第五步,系统模式与线程绑定。切到性能模式、关后台、对推理线程绑大核,收益 2 到 5 倍。
这五步叠起来,理论上的综合收益超过 30 倍。前提是每一步都真的生效——只改设置不看日志,很容易以为自己做了,实际还是老样子。
4.2 我的实测记录与最终配置
我在那台中端机上完整记录了调优过程。原始状态:7B 模型、Q8、CPU 后端、32K context、均衡模式、后台还有一堆应用,实测 0.4 token/s。逐步调整结果如下:
| 调整动作 | 实测速率 | 本次收益 | 累计倍数 |
|---|---|---|---|
| 原始状态 | 0.4 token/s | - | 1 倍 |
| 切性能模式 + 清理后台 | 1.1 token/s | 约 2.8 倍 | 2.8 倍 |
| context 从 32K 降到 4K | 1.8 token/s | 约 1.6 倍 | 4.5 倍 |
| 7B 模型换成 3B 模型 | 3.5 token/s | 约 1.9 倍 | 8.8 倍 |
| Q8 量化降到 Q4_K_M | 5.1 token/s | 约 1.5 倍 | 12.8 倍 |
| 切换到 GPU 路径的新后端 | 12.8 token/s | 约 2.5 倍 | 32 倍 |
这套组合下来合计约 32 倍,虽然没有完整反向追平 80 倍,但体感已经从“完全不可用”拉到了“日常聊天完全可用”。剩下和旗舰机对比的约 2 倍差距,就是中端 LPDDR4X 与旗舰 LPDDR5X 之间的带宽差异,属于硬件天花板,没有再优化的空间。这个结果也验证了最初的判断:80 倍里没有一个“元凶”,全是小因素叠乘出来的。
5. 三个误判现场与真正有用的排查工具
5.1 容易误判的坑,我都替你踩过了
误判一:“这个框架天生就慢”。实际换一个框架快了很多,不是因为框架本身强,而是原来那个框架的默认配置太保守:线程数没给够、默认走 CPU、context 给得极大。框架和配置是两回事,别让框架背配置的锅。看到“某框架慢”的说法,先问一句:你是在默认配置下跑的吗?
误判二:“我已经开了 NPU 加速”。很多端侧方案把“NPU 支持”写得很响亮,但真实链路里 NPU 根本不干活。判断方法很直接:推理过程中看系统级硬件占用,NPU 没有任何负载,CPU 却满负荷;或者模型格式不对,NPU 后端直接拒绝加载,悄悄回退到 CPU。日志里通常有回退提示,只是没多少人翻。我在这方面吃过亏之后,养成的习惯是每次换后端都先跑一轮短任务,同时用监视器看三块硬件各自占用。
误判三:“prompt 处理快就是生成快”。prefill 阶段一次处理几百个 token,每秒上千 token 都很正常,但这不代表生成阶段也能这么快。只看“加载模型后第一次运行多快”完全没用。务必区分 prefill 和 decode,单独记录生成阶段的 token/s。很多对比视频里的“跑得飞快”,截的其实是 prefill 阶段,真正逐字生成时并没有那么夸张。
5.2 排查工具不在多,关键是会用
排查工具未必多高级,关键是方法。我自己主要在用的就四样:系统自带的资源监视器,看 CPU 核心占用、频率、GPU 占用和内存水位;推理框架的计时输出,看 prefill 和 decode 两个阶段的明细;一个三行脚本,记录多次运行的平均值和 p95,避免凭感觉;以及一台可以随时做对照的参照设备。
实操建议是:每改一个变量,就在相同 prompt、相同模型、设备回到常温的状态下重新测一轮。手机如果已经发烫,先放凉五分钟再测,否则结果全被温控扭曲了。我自己用脚本循环测十次再取中位数,稳定性比单次强很多——手机后台任务太不可控,单次数据很容易被带偏。宁可多花十分钟跑脚本,也不要拿一次结果去下结论。
6. 进阶优化:量化档位、权重缓存与 KV cache 压缩
6.1 量化不是越低越好,先看任务再决定
把量化降到 Q4 能白捡 4 倍速度,但代价是模型变“笨”。对代码生成、数学推理这类任务,4bit 经常会给出错误答案;对闲聊、摘要、格式化文本,4bit 几乎无感。更激进的 Q2 档,速度更快但质量骤降,常见任务基本都不适合。我的判断标准很简单:把实际任务往模型里跑一遍,对比 Q4 和 Q8 的输出质量,如果关键指标没有明显下滑,就选 Q4。对端侧绝大多数助手类场景,Q4_K_M 是我现在最常用的默认项,不踩坑。
6.2 权重缓存与 KV cache 量化:被低估的两条提速路
确定模型档位之后,还能再抠一倍的地方是缓存机制。端侧推理里如果每跑一轮对话就重新从磁盘加载权重,速度会被磁盘 I/O 直接锁死。正确做法是利用内存映射让进程直接访问文件权重,同时依赖操作系统的页面缓存——权重文件只要热过一遍,后续基本就不读磁盘。我那个 0.4 token/s 的慢速场景里,有一份“功劳”就是某前端没有用内存映射,每次启动都从闪存里重新读 4GB 权重。改成内存映射后,首轮加载从十几秒降到 1 秒以内,稳定输出也轻松上了一阶。
KV cache 量化则是端侧长文本场景的隐藏开关。把 KV cache 从 F16 压到 8bit,几乎不损失质量的前提下,KV cache 内存减半,长上下文解码时的带宽开销也明显下降。和模型权重量化是一个道理,只是多数人没想起来。我一般在 context 要求超过 8K 时都会开启它,配合 GQA 模型,32K 上下文才不至于变成一场灾难。
这次排查下来,我最大的体会是:端侧推理的性能问题,90% 不是玄学,而是“对比口径没对齐、理论极限没算清、实际路径没走对”三件事的排列组合。拿到一张慢得离谱的性能数据,先别急着怀疑手机和框架,第一步永远是拉变量表逐项核对,再用带宽公式算一次理论天花板,最后通过日志和系统监视工具确认推理请求到底跑在什么硬件上。做到这三步,九成问题都能被框定到一个很小的范围。
最后分享一个小技巧。你可以给用户提供一个一行命令的“端侧健康检查”:先读出设备型号和当前内存带宽档位,再跑一个固定 3B Q4 模型的 20 token 生成基准,把结果存成历史曲线。以后任何版本更新、模型替换、系统升级,是否需要做性能回归,看这条曲线就知道。这个习惯帮我在后续几个项目里省掉了大量排查时间,比临时抱佛脚翻日志高效得多。