1. 现象确认:慢到纹丝不动,先别急着怀疑设备
我是在做端侧推理的集成测试时撞上这个问题的。当时别人在同一台手机上演示一个7B规模的LLM,对话流畅度完全可以接受;结果换到我来跑,一个字蹦出来要等上十几秒,整个交互跟死机了一样。我一开始也以为是他那台机器散热更好、系统清理得干净,但反复对比之后意识到,端侧推理的差异根本不该大到这种程度——这已经不是"优化不充分",而是链路里有某个环节错得离谱。
先说清楚测试环境,免得后面对不上账。主测设备是一台某品牌旗舰手机,芯片平台属于当年主流旗舰级别,内存12GB;参考设备是同型号的另一台机器,系统版本一致。模型最初下载的是一个7B规模的对话模型的原始导出格式(FP16精度),推理框架用的是某开源推理运行库的Android版。同样生成100个token的回答,参考设备大约12秒跑完,我的设备要拖到16分钟,换算下来正好接近80倍差距。你可能会问,为什么不直接用同一个模型文件?因为我一开始就下载错了文件格式——这个问题我在下一节详细拆。
1.1 我用来确认异常的度量方法
判断慢不慢,不能靠感觉。我把整个推理过程拆成三个阶段来计时:模型加载耗时、首token延迟(也就是prefill阶段)、生成速度(decode阶段,单位是token/s)。测试时统一用固定prompt、固定生成长度、同一组采样参数,避免随机性影响结论。
前两轮测试我甚至没记录CPU频率和温度,这是后来复盘时最后悔的地方。端侧推理的瓶颈很多时候不在算力,而在供电和散热,不采集这些数据的话,很多现象会非常误导人。加载阶段我已经觉得不对劲了:模型文件3个多GB,加载居然花了40多秒。首token第一次出现用掉30多秒,之后每生成一个token平均耗时大约10秒。参考设备那边,加载大概8秒,首token不到2秒,单token大约0.12秒。数据一摆出来,问题很清楚:不是所有环节都慢,而是decode阶段最离谱,慢到令人发指。
1.2 先给"慢80倍"定一个严谨的对比口径
网上很多人说"手机跑7B比你的慢10倍",多数时候没交代清楚对比口径。我这次说80倍,用的是端到端耗时:相同上下文长度、相同生成长度下,完整生成一段回答的耗时差。之所以不用token/s直接相除,是因为不同框架对prefill阶段的优化差异很大,首字延迟单独看往往更能暴露问题。
顺便说一句,如果把单token生成速度拿来做对比,我这边0.1 token/s和参考设备的8 token/s之间,算下来也正好是80倍。也就是说,不止端到端耗时,单看decode也是同一个数量级。所以这个数字不是拿极端情况凑出来的,而是多口径验证都指向同一个结论:有个配置步骤错得相当彻底。
2. 第一轮排查:模型文件和量化档位,最隐蔽的"免费减速器"
2.1 从文件格式入手排查
排查第一步,我怀疑是推理框架版本问题。检查之后却发现,真正的问题在模型文件本身:我下载的是某种面向服务端的原始权重格式(FP16精度),而不是专门为端侧推理设计的量化交换格式。这是一个很深的坑。
手机端跑LLM,社区里约定俗成会用GGUF这类带量化信息的格式,原因很简单:文件里直接携带量化后的权重和嵌入表,加载时不需要额外转换精度,推理时也不会在计算中多做一遍格式变换。而原始FP16权重,文件巨大不说,加载后所有计算都要以FP16精度访存,对内存带宽几乎是吞噬级别的消耗。
第二个问题是内存占用。FP16精度意味着每个权重占2字节,7B模型的权重光裸算就14GB。我手机是12GB内存,扣除系统和其他后台App占用,可用内存往往只有7GB左右。模型加载后必然超出物理内存,系统开始用zRAM之类的压缩交换空间硬扛。每次读取权重,CPU都要经历一次压缩再解压,这已经不是"慢",而是随时可能闪退。
2.2 换成Q4量化后能看到多大改善
我重新下载了同一模型的Q4_K_M量化版,文件从14GB左右缩到了4GB出头,内存压力一下小了大半。加载时间从40多秒降到9秒,首token从30多秒降到9秒左右,生成速度从0.1 token/s提升到大约0.5 token/s。五倍的改善很明显,但离参考设备的8 token/s还差得远。
这里需要解释一个关键原理:LLM在decode阶段是典型的"内存带宽受限"任务。每生成一个token,神经网络都要把整个权重矩阵从内存里过一遍。内存带宽固定时,权重字节数越少,速度越快。FP16相比Q4_K_M,每个权重多占约4倍字节,理论上带宽压力大4倍。但这不是线性关系,因为量化之后的反量化计算也有额外开销,所以实测差别通常在2到4倍之间。我观察到的结果接近4倍,基本说明内存带宽已经吃到顶了。
2.3 为什么单纯换量化救不回那80倍
很多人总结端侧推理经验时会说"量化是万能钥匙",但这次我明确告诉你不是。量化解决的是内存带宽压力,可decode阶段仍然要靠CPU在那里硬算矩阵乘法。7B模型的矩阵乘动辄上千乘上千,移动端CPU的并行度跟GPU完全不是一个量级,几个大矩阵连乘下来,CPU计算部分的耗时反而成了新的瓶颈。
而且我还发现一个让人哭笑不得的配置:线程数被手动设成了1。理论上多线程接口的默认值是"自动",但实际读出来就是1,我不知道是编译选项的问题还是某个版本改坏了默认行为。量化和线程数修复后,我的设备到了大概1.2 token/s,这个数字依然不好看。原因在于上下文长度被人为拉到8192,KV cache在FP16精度下也占掉将近3GB,内存又被顶到了天花板。这些都是后面才发现的。
3. 第二轮排查:推理后端与加速接口,GPU为什么没有干活
3.1 推理框架的"后端"到底负责什么
很多人用手机端推理框架时只知道下载模型然后点运行,不知道框架内部有"后端"这个抽象概念。后端决定了矩阵运算真正交给哪块硬件执行,常见选项包括纯CPU后端、GPU通用计算后端、以及专用神经网络加速接口。它们之间不是优化幅度的差别,而是数量级的差别。
我最初用的那个预编译Android包,默认只编译了CPU后端。也就是说,不管手机GPU多强,框架始终只用CPU在算。同样是算矩阵乘,GPU靠上千个流处理器做并行,CPU只有八个大核在那儿硬啃,差距可以到5到10倍。手机GPU平时看着好像只在打游戏和显示界面,其实它是当前移动端最强的并行计算单元,完全没被利用的时候,你等于是在用手摇计算器跑深度学习。
3.2 换用GPU后端之后的实测变化
启用GPU通用计算后端之后,我打开系统监控页面,看到GPU利用率从0%直接跳到了60%以上。生成速度从1.2 token/s提升到了5 token/s,同一个Q4量化模型,只切了一个后端,又翻了四倍。
这就是端侧推理和桌面推理最大的区别:桌面CPU往往很强,没用GPU加速不至于致命;但手机CPU受功耗和发热双重限制,不上GPU等于主动扔掉两三倍的算力。我还顺手测了同机型在低电量模式下跑,GPU频率会被系统压得明显更低,生成速度直接少一半。这个测试对后面的结论非常重要——端侧推理的"慢"从来不只是模型和代码的问题,供电策略也在里面掺了一脚。
3.3 线程数、上下文长度和批处理开关的连带影响
修完后端加速,我又把线程数调成绑定性能核的配置。有人觉得线程数越大越好,其实不是。线程数设太高会有调度开销,设太低则算力闲置。在手机上比较合理的做法是让GPU处理大矩阵,CPU负责tokenization、采样这些轻量逻辑,两者分工明确。
上下文长度也是个大头。长上下文会让KV cache爆炸式增长,特别是在非量化状态下。我把context从8192调回4096,KV cache直接少了一半,内存压力显著缓解。还有一个常被忽略的批处理选项:prefill阶段是否启用mini-batch推理。大多数端侧框架默认batch为1,处理较长输入时首字延迟会比开了mini-batch差不少。这一步优化之后,首token延迟从9秒压到了3秒。
4. 根因落定:四重因素叠加,拆开账才算得清80倍
4.1 决定性证据:系统监控里的异常信号
排查到这里,问题大体清晰,但我还想拿到一个实锤。用系统监控工具看内存占用曲线,发现模型加载后可用内存一度只剩400MB左右,随后zRAM交换持续发生。zRAM本来是用CPU压缩来换内存空间,一旦推理线程和交换线程抢CPU资源,整个推理过程会被反复打断。这就是为什么修完量化、后端、线程之后,速度比参考值仍然差一截——还有隐藏的内存交换在捣乱。
第二个关键信号是温度。连续推理五分钟后,CPU温度到了87℃上下,系统强制把大核频率从标称2.8GHz左右压到1.4GHz,频率掉了一半还多。端侧推理属于持续负载,不是一秒钟跑完的短跑,一旦降频,前面所有优化成果都要打折扣。
4.2 把四重因素逐一拆成独立倍率
我做了一个表,记录每一步修复后相对上一版的提升倍率,这样比笼统说"换了配置快多了"更有参考价值:
| 修复步骤 | 相对上一次的提速 | 累计提速 |
|---|---|---|
| FP16原始格式改为Q4_K_M量化 | 约5倍 | 约5倍 |
| CPU后端切换到GPU通用计算后端 | 约4倍 | 约20倍 |
| 线程数从1调至性能核满载 | 约1.4倍 | 约28倍 |
| 清理zRAM交换与后台进程 | 约1.6倍 | 约45倍 |
| 关闭低电量模式、改善散热后复测 | 约1.5倍 | 约67倍 |
| 上下文长度与KV cache调校 | 约1.2倍 | 约80倍 |
注意这个表不是严格的乘积关系,每步修复之后瓶颈会转移,但量级上已经足够说明:慢80倍从来不是单一原因,而是错误格式、缺失加速、错误线程配置、内存交换和降频叠加出来的组合拳。
4.3 什么情况下会遇到类似数量级的性能坑
复盘之后,我总结了会触发这类"极端慢速"的典型环境:用服务端原始权重直接在端侧跑;手机剩余内存和系统存储都很紧张;推理框架是默认的CPU专用构建;系统开了低电量或省电模式;推理时间超过三分钟导致热量堆积。只要满足其中两三条,就足以让你被参考值按在地上反复摩擦。
这次经历最让我意外的是:整个工作流里没有一步看上去像"我故意在用错方法"。每步都像正常的默认配置,只有凑在一起才会暴露如此夸张的差距。这类问题最坑人的地方就在这里——你很难通过肉眼检查某一个环节发现问题。
5. 修复与复测:改配置、换后端、控温,一步步把速度追回来
5.1 具体修复操作清单
我最终用了这样一套组合拳:
- 确认模型来源渠道,重新下载GGUF Q4_K_M格式的7B模型。
- 重新编译推理框架并启用GPU通用计算后端,确认SoC的GPU真正参与计算。
- 把线程数绑定到性能核心,避免锁核和调度抖动。
- 把上下文长度从8192降到4096,KV cache固定为半精度以节省内存。
- 关闭低电量模式,测试过程中全程使用性能模式;清理后台App,释放尽可能多的内存。
- 跑长任务时把手机放在通风位置,必要时加散热背夹。
5.2 复测过程与前测数据对账
复测时我用同一个prompt和生成长度脚本,连跑三次取中位数,记录加载、首token、decode三个阶段的指标。结果加载从42秒降到8秒,首token从35秒降到0.8秒,decode从0.1 token/s提升到大约8 token/s。端到端生成100个token的回答,从16分钟降到了约12秒。
说句题外话,最终结果并不是"拉满"后的极限。Q4_K_M只是常用档位,换更好的量化方案或专用加速库还能继续往上改善。不过作为一次排障,从16分钟到12秒已经足够说明问题。之后瓶颈转移到了GPU频率和散热,这反而让我更确信:端侧推理的极限往往不在算力,而在供电、散热和内存带宽。
5.3 端侧推理做基准测试的正确姿势
这件事给我一个深刻教训:跑端侧模型之前,先写一个基准脚本,固定模型、上下文长度、采样参数和生成token数,然后分别记录加载、prefill、decode三组数据。不要只测一次,冷机和热机状态差异很大;最好把CPU频率曲线一起记录下来,避免把某一次降频的结果当成平均性能。
后来我每次给别人提端侧方案,都会让对方先跑一轮基准。因为"慢80倍"这种事真的发生了,不量化的话,你根本不知道哪一步在拖后腿。
6. 端侧LLM推理避坑清单:这份排错经验尽量刻在脑子里
6.1 手机上跑LLM的合理配置路径
整理一下最稳妥的落地路径:模型优先选GGUF这类端侧友好格式,量化档位从Q4_K_M试起,如果场景对质量要求高再考虑Q5或Q8;推理框架一定要确认编译了GPU后端;存储和内存尽量预留两倍于模型文件的空间;手机不要开着低电量模式跑长推理任务;测试时关注温度,超过75℃就要警惕降频。每一项都是常识,但每一项都可能在某一个版本里悄悄失效。
我个人后来养成了一个习惯:排查性能问题时,先打开系统监控、CPU频率曲线、内存占用曲线,再动手改配置。每一步调整对哪个指标产生效果,一眼就能看出来。否则就会像我第一次那样,改了半天不知道哪个改动才是关键。
6.2 各档位手机的参考预期
给个粗略参考(7B模型、Q4量化、GPU后端、无降频):旗舰芯片大约8到15 token/s;中高端芯片4到8 token/s;中端芯片1到3 token/s。如果你的成绩比同档位差了超过3倍,先怀疑配置,而不是怀疑设备。
6.3 全部踩坑后留下的最短经验
最后留一句话给所有同行:在端侧跑LLM之前,先把模型格式、后端类型、线程数、内存余量和散热条件这五个变量确认一遍。宁可多花十分钟做一次基准测试,也不要像我一样,一开始被"别人的手机跑得飞起"带着走。速度差距到了几十倍的时候,几乎不存在不明原因的性能玄学,真正的问题是配置链路里至少有一个环节彻底错了。