别人手机跑 7B 模型轻轻松松 40 token/s,我手机装上同一个推理框架、同一个模型文件,测出来 0.5 token/s 出头。两个数字摆一起,差了 80 倍。
这不是夸张,是我最近调端侧推理时真实踩过的坑。当时第一反应是手机太老、硬件不行,但仔细扒完日志才发现,硬件只背了一小半锅,真正的问题出在量化格式、线程配置和系统调度上。如果你也遇到过"同一个模型,在不同设备上速度天差地别"的情况,这篇排查实录应该能帮你少走不少弯路。
先说结论:端侧 LLM 的性能瓶颈从来不是"手机不够好",而是"你的推理栈到底有没有跑在正确的路径上"。下面我把这次排查的完整过程、对比数据、修复手段和事后整理的速查清单全部写出来,照着做,大部分性能问题都能在半小时内定位。
1. 先搞清楚:端侧跑 LLM 的"正常"到底是多少
在排查之前,得先建立一条基线。没有基线,"80 倍慢"就没有参照物,后面所有改动也说不清有没有效。
1.1 端侧推理的两个阶段:预填充与解码
大模型推理分成两个明显不同的阶段。第一个是**预填充(Prefill)阶段,模型一次性吃进你输入的整段上下文,所有 token 并行计算,这个阶段是典型的计算密集型操作。第二个是解码(Decode)**阶段,模型一个一个往外蹦 token,每蹦一个都要把全部权重从头读一遍,所以这个阶段卡在内存带宽上。
举个直观例子:假设一个 7B 规模的模型,int4 量化后权重大约是 3.5GB,你手机内存带宽如果是 25GB/s,解码阶段每蹦一个 token 就得花 3.5GB ÷ 25GB/s ≈ 0.14 秒,换算过来也就 7 token/s 左右。带宽砍一半,速度跟着砍一半。如果你用的还是 fp16 精度,权重直接奔 14GB 去,速度更是断崖式下跌。
这就是为什么我测速时一定要把两个阶段分开看。很多框架默认打印的数字是"prefill + decode 混在一起的平均值",上下文越长越失真。排查性能问题时,只看 decode 阶段的 token/s 才是公平对比。
1.2 建立对照设备基线
我这次手里有两台设备。参考设备 A 是近两年的旗舰芯片,对比设备 B 是台老款中端芯片,系统、内存、存储都不太一样。同一个 int4 量化模型文件,同一套推理框架,放在两台设备上分别测。
设备 A 用默认参数跑,decode 稳定在 43~45 token/s,设备 B 用同样参数跑,只有 0.5~0.6 token/s。当时差点就把锅甩给硬件差距了,但冷静算一下:即便设备 B 芯片算力、带宽都弱一截,再怎么样老款芯片跑 int4 7B 模型也不会只有 0.5 token/s。这个数字低得太反常,反而说明问题不在硬件,在软件栈。
注意:排查性能问题,第一件事永远是建基线。找一台你心里有底的设备,跑同一份模型文件、同一套框架版本,把两个阶段的耗时分别记下来,再开始动配置。没有基线的调优都是瞎猜。
2. 慢 80 倍的根源:从算子到系统的三层检查
拿到异常数据后,我按"从代码到硬件"的顺序一层层往下查,总共锁定到三个层面的问题。这种自顶向下的排查方式推荐大家都试试,比盲改配置高效得多。
2.1 第一层:框架状态与量化格式核对
我第一个怀疑对象是量化格式。端侧推理框架为了压速度,都会对 4bit 权重做特殊的内存布局和算子优化,比如按组打包、矩阵分块等。如果模型文件虽然是 int4 命名,但实际加载时框架没有走加速路径,权重就可能在运行时被"展开"成 fp16 甚至 fp32 来计算。
怎么验证?框架启动后看到日志里其实写得很明白。正常走量化加速路径时,日志会标注类似"加载 4bit 权重,已启用整型矩阵乘加速算子";异常时通常会有一行警告,提示"当前算子不支持 int4,回退到 fp16 展开",或者干脆静默回退。
我这次就撞上了第二种。设备 A 的芯片有完整的整型加速指令集,框架自动走最优路径;设备 B 的老款芯片缺了关键指令,框架直接静默回退到 fp16 展开。一个 int4 模型实际跑出来是 fp16 的权重和算子,内存开销翻到接近 3.5 倍,速度自然按比例崩掉。
排查方式很简单:
- 打开框架日志,搜"revert""fallback""unsupported"关键字,看有没有算子回退告警
- 加载模型后看内存占用。7B int4 模型理论占用约 3.5~4GB,如果看到 7GB 以上,基本可以断定实际在跑 fp16
- 对比 CPU 占用特征。fp16 展开时多个大核都会接近满载,int4 加速路径下占用率通常没这么夸张
2.2 第二层:线程数与调度策略
第二层我查的是线程配置。推理框架的线程数不是越多越好,尤其手机上的大小核混合架构,线程要是全都轰到小核上,速度能慢得让你怀疑人生。
框架默认的线程数有两种极端:一种是把 CPU 核数全部劈开用,比如检测到 8 核就开 8 线程;另一种是保守起见只开 2 线程。两种在我这台设备 B 上都会出问题——开满了线程会被系统调度到一堆小核上,开少了矩阵乘算力又吃不满。
而且这次还抓到一个隐蔽点:框架把线程固定到了某个 CPU 分组里,而设备 B 的大核和小核分属不同的调度域。明明有 4 个大核,框架偏只认了默认组里的 2 个,剩下几个线程全挤在小核上跑。0.5 token/s 里有相当一部分就是这么来的。
排查办法:
- 用系统命令实时看每个线程跑在哪个核心上,确认线程是不是都挤在小核
- 看单线程和多线程的实际速度差异。如果 8 线程比 4 线程还慢,说明调度出问题了
2.3 第三层:功耗与散热策略带来的隐形降频
最后一个大坑在系统功耗管理。设备 B 当时处于默认的智能省电模式,系统检测到持续高负载后,直接把大核频率往死里压,甚至短时间全核降频。我跑推理的时候,核心频率被锁到标称的一半不到。
这种问题最坑,因为你看软件层完全正常,量化也对、线程也对,但帧率就是上不去。手机厂商为了续航和温控,往往对"持续重负载"非常敏感,而跑大模型恰恰是最典型的持续重负载。
排查思路:先排除软件层问题,再怀疑散热降频。如果代码没有任何变化,把设备放在冰袋上跑一次,速度明显提升,那基本就是温度墙在捣鬼。注意别把冷凝水弄进设备,我就因為这个废过一台测试机。
3. 逐步复现与修复:把 0.5 token/s 拉回正常水平
问题锁定之后,剩下的工作就是一个个修。这个过程我强烈建议一次只改一个变量,改完立刻测速,记录在案,再改下一个。不然多个变量混在一起,你根本不知道是谁起了作用。
3.1 第一步:建立隔离环境
在动手之前,我先把设备 B 恢复成"干净状态":关掉所有后台应用,切到性能模式,关闭自动亮度,把屏幕调暗,开启飞行模式,拔掉充电线。原因很简单——跑模型本身就是重负载,后台刷信息流的 app、无线传输、充电发热都会干扰测试结果。
然后我对模型文件做了一次 SHA 校验,确认两台设备上跑的是同一个文件,二进制级别一致。这个步骤很多人会跳过,但我排查过不止一次"设备 A 正常、设备 B 异常",最后发现是两台设备上的模型文件根本不一样,一个是 int4 一个是 fp16。
3.2 修复一:强制启用量化加速路径
设备 B 的框架默认回退到 fp16,我需要确认框架是否提供"强制走加速路径"的开关。翻了框架文档后发现确实有关联的编译选项,但这个选项是编译期决定的,设备 B 上跑的是别人预编译的包,没法现场改。
于是换了个思路:换成有对应版本指令集优化的构建。同一套框架,设备 A 的构建是完整版,设备 B 我之前图省事装了个兼容性构建,两者内部算子实现完全不同。换掉之后,日志里终于出现了"整型矩阵乘加速已启用"的标记,内存占用也从 7GB 多降回了 3.8GB——量化路径通了。
实操心得:端侧推理框架在不同处理器上的性能差异,很大程度取决于你用的是哪个构建版本。全功能构建体积大,但算子优化完整;兼容性构建什么芯片都能跑,但性能可能差数十倍。追求性能就选针对你芯片优化的版本,别图省事。
3.3 修复二:手动绑定大核与调整线程数
量化路径修好之后,速度已经起来了一截,但离设备 A 还有很大差距。接下来处理线程调度问题。
框架支持通过配置文件或环境变量指定线程数和 CPU 亲和性。我直接指定"优先使用高性能核心"的策略,并把线程数从默认值调到一个更合理的值。这里有个细节值得说一下:线程数不是越大越好,对于 4 个高性能核的手机,推理线程设 4,再留一两个线程给系统做调度、渲染,效果往往比全开要好。
调完之后运行 top,确认推理线程确实都跑在大核上,CPU 频率也稳定在较高档位,速度又往上走了一截。
3.4 修复三:电源策略与模型加载优化
最后处理散热和功耗策略。设备 B 性能模式下,系统对大核频率的限制会放宽,但代价是发热更快,跑长文本时会撞到温度墙。
另外一个容易被忽略的点是模型加载速度。设备 B 用的是慢速存储,模型加载花了 30 多秒,期间框架界面卡死。后来我把模型文件换成加载到内存后缓存权重的方式,第二次加载只要 1~2 秒。
最后看数据:设备 B 从最初的 0.5 token/s,一路修到 7 token/s,再到 30+ token/s。虽然没有完全追平设备 A 的 43 token/s,但已经从"完全不可用"变成了"能正常对话"的水平。后端几个问题叠加起来,还真能凑出 80 倍的差距。
| 配置阶段 | 速度 (token/s) | 主要瓶颈 |
|---|---|---|
| 初始状态 | 0.5~0.6 | fp16 展开、线程挤小核、降频 |
| 更换量化版本 | 约 7 | 线程调度仍不理想 |
| 绑核+调线程 | 约 22 | 功耗/散热限制 |
| 性能模式+缓存 | 30+ | 硬件上限 |
4. 端侧推理性能排查清单与常用工具
这次排查完之后,我把经验整理成了一份快速自查清单。以后遇到类似的问题,直接按顺序走一遍,大多数情况 20 分钟内能定位。
4.1 5 分钟快速自查表
- 模型文件是否一致:比文件大小、SHA、量化位数。两台设备跑不同精度还对比速度,是排查里最常见的乌龙。
- 日志搜回退告警:找"revert""fallback""unsupported""unsupported kernel"字样。
- 看内存占用:int4 模型却占用 fp16 级别的内存,说明走了展开路径。
- 看线程分布:用监控工具实测推理线程是否都在大核,还是挤在小核。
- 关省电模式:切到性能模式再测试,排除频率墙。
- 测温度:设备发热严重时先降温再测,避免把温度墙误判成代码问题。
- 跑三次取中位数:单次测速受后台任务、系统调度影响很大,至少跑三次。
4.2 排查过程中最常用的几个工具
- 系统自带的 CPU 频率和核心状态查看命令,实时看每个核的运行频率
top/ 增强版进程监控,看线程 CPU 占用率和进程状态- 日志抓取命令,观察框架运行时的告警输出
- 基准测试脚本,反复压测,记录 prefill 和 decode 的耗时分布
独家技巧:测速时不要用第一次的结果。模型加载后会有权重预热、缓存命中的过程,直接测第一次通常偏慢。先空跑几轮把推理链路热起来,再正式记录数据,这样得到的数值才有参考价值。
5. 复盘:影响端侧 LLM 推理速度的几个底层因素
如果你把上面所有问题剥开看,真正决定"手机跑 LLM 快不快"的其实是几个底层因素。我顺手整理了一下,方便以后选设备或调框架时心里有底。
- 量化格式和算子支持:同样的模型,int4 在支持的芯片上是高速路径,在缺指令集的芯片上可能回退成 fp16,速度差可以到 5~10 倍。这个往往是最关键的一环。
- 单次解码的内存搬运量:解码阶段每生成一个 token 都要把全部权重读一遍,内存带宽直接决定天花板。带宽翻倍,解码速度就有可能翻倍。
- 大小核调度策略:手机芯片的小核跑 LLM 就是灾难,线程被调度到小核上,再好的量化也发挥不出来。
- 系统功耗控制:在散热有限的空间里塞进大模型推理,温度墙是绕不开的。性能模式和智能省电模式下的速度差距,实测能差到 3 倍以上。
- 模型文件加载成本:大模型文件从闪存加载到内存的时间,在慢速存储上会明显放大。首 token 延迟和冷启动体验,往往坏在这上面,而不是推理本身。
顺手再提一个很多人踩过的坑:比较不同设备的性能时,一定要用同一个框架版本、同一个模型文件、同一个量化位数。我见过太多"设备 A 比设备 B 快几十倍"的结论,最后发现一个是 int4 模型、一个是 fp16 模型,根本没可比性。屏蔽掉这些干扰项,设备间真实的硬件差距往往远小于直觉判断。
这次排查给我最大的一个体会是:端侧推理的"慢",绝大多数不是硬件的硬伤,而是软件栈没有吃满硬件。从回退掉型的量化算子,到被调度错位的线程,再到被系统悄悄压掉的频率,任何一环出问题都会让你的手机"看起来"比别人的慢一个数量级。先查日志、再查线程、最后查功耗,这个顺序能筛掉 95% 的问题。希望这份实录能帮你在类似的问题上省下几个通宵。