有段时间我在端侧模型交流群里看到有人晒自己手机跑7B模型的生成速度,每秒二十多个token,评论区都在说“这手机性价比绝了”。再看看我手里这台同芯片平台的设备,跑同一个模型,每秒连一个token都勉强,换算成完整流程的时间差,差不多就是标题里那个数字——慢80倍。这差别大到已经不是“优化”能解释的了,几乎可以断定是有某个环节在拖后腿。
这篇文章就是那次排查过程的完整复盘。我不打算讲太多理论,重点放在“每一步怎么查、看到了什么、改了什么、无效还是有效”,顺便把同类问题最容易踩的坑都给你捋一遍。如果你也在手机、平板或者开发板上跑大语言模型,发现速度比别人晒出来的慢一个数量级,那这篇文章大概率能帮你省掉几天瞎折腾的时间。
1. 现象描述与初步定位:先搞清楚“慢”到底慢在哪
1.1 确认测试条件是否对等:一个参数就能让结果失真
排查之前第一件事不是看代码,不是重刷配置文件,而是先确认一个前提:你跟别人比的是不是同一个东西。
这里说的不只是“同一个模型”这么简单。同一款7B模型,文件来源可能不一样。有人下载的是别人转换好的量化版,有人从模型仓库直接拉了原始权重,还有人用的是自己转换但精度设置不对的版本。这三个版本放在同一台手机上,速度差距能达到数倍甚至数十倍。
另外生成参数也会严重影响速度。我在测试时踩过一个很隐蔽的坑:为了测试方便,把上下文长度(n_ctx)设得特别大,比如2048甚至4096,然后跑了20轮对话的旧消息。结果KV Cache占用的内存把所有可用内存都吃光了,系统开始疯狂换页,每生成一个token都要等好几秒。而别人晒速度的时候,大概率用的是很短的上下文,内存压力完全不一样。
所以第一步,务必先对齐这几个变量:
- 模型文件是否完全一致(最好直接下载同一个文件并校验哈希)
- 量化格式是否一致(
FP16、INT8、INT4是不同的东西) - 测试时的上下文长度、生成参数是否一致
- 统计口径是纯生成速度(token/s)还是包括了模型加载时间
- 是否开了系统级省电模式或者性能模式
我这次遇到的80倍差距,就是因为测试条件从一开始就没对齐。我手里拿的是一个未量化的FP16权重,别人跑的是4bit量化版。光这一项,理论差距就有4到8倍,再叠加其他配置问题,拉到80倍完全不奇怪。
1.2 先量化“80倍”:是端到端还是单Token延迟
很多人说“慢多少倍”的时候,统计口径是模糊的。这里我强烈建议先区分两个指标:端到端延迟和单Token生成延迟。
端到端延迟指的是从输入一句话到屏幕上完整输出全部文字的总时间,里面包含了模型加载、权重读取、预处理、前向计算、后处理等所有开销。单Token延迟则是模型稳定生成后,每生成一个token平均耗时多少秒。前者适合衡量“用起来卡不卡”,后者才是衡量推理引擎性能的标准。
80倍这个数字,如果按照端到端来算,很可能是因为模型加载阶段就出了问题。比如我那次测试,第一次加载模型花了好几分钟,因为推理框架默认把整个模型文件读取进内存并做了完整拷贝,而不是用内存映射(mmap)的方式按需加载。手机存储读取速度本来就不算快,遇到冷缓存的时候更慢。别人跑得快,可能仅仅是因为他们的推理框架配置开启了mmap,模型加载几秒就完成了。
这里有一个实操建议:分阶段计时。先在启动阶段打印时间戳,记录模型加载完成之前和之后的时间;再记录首token延迟(第一个token生成前的时间)和后续平均token速度。分清楚之后,你就会知道要优化的到底是加载、预填充还是解码阶段,不会眉毛胡子一把抓。
提示:不要只盯一个“xx倍慢”的数字就急着调参。先把它拆成“加载慢了多少倍”和“生成慢了多少倍”两块,排查范围立刻缩小一半。
2. 逐层拆解:从模型文件到推理框架的五层排查
2.1 第一层:模型文件与量化等级——带宽决定的理论上限
手机跑大模型,最大的瓶颈不是算力,是内存带宽。这个认知我当时也是花了不少时间才彻底想通。
大模型生成每个token都要往内存里跑一遍完整的模型权重(不考虑投机采样的情况)。假设一个7B模型,如果权重是FP16存储,大小大约是14GB。用一条LPDDR5手机内存带宽约50GB/s来算,理论上每秒最多只能“扫”完3.5遍权重,也就是说上限大概3.5个token每秒。这还是在带宽完全跑满的理想状态下,实际能到一半就不错了。
如果换成INT8量化,模型大小变成约7GB,同样带宽下理论上限是7个token每秒左右。换成INT4量化,模型大小约3.5GB,理论token速度能到14个token每秒以上。看到没有?同款芯片、同一个模型,仅靠量化格式不同,速度和体验就是天壤之别。
所以排查这个问题,第一步就是检查你自己加载的那个模型文件到底是什么格式。命令很简单,用文件管理器看详细信息,或者用任何支持读取模型元信息的工具,看一下模型头部里写入的“量化类型”字段。如果显示的是F16或者FP16,而别人跑的是Q4_K_M之类,那慢个好几倍就是数学上必然的事情,跟推理框架没有关系。
我还遇到过一种情况:文件名写的是“4bit量化版”,但实际加载后推理引擎日志显示模型精度是fp16。这种错位通常是因为模型转换脚本的配置出了问题,转换的时候走了“反量化”逻辑,把量化权重还原成了半精度再保存。这个坑比较隐蔽,光看文件名完全发现不了,一定要看引擎加载时打印的元信息。
2.2 第二层:推理后端与硬件加速是否真正生效
模型没问题之后,下一个要排查的是推理框架有没有真的用上正确的硬件单元。
端侧推理框架通常支持多种后端,比如纯CPU、GPU或者NPU,部分还支持异构调度。不同后端的性能差距非常大。GPU跑LLM比CPU快数倍是很正常的,NPU如果支持对应的算子,速度又比GPU快一截。我自己在实际测试中遇到过一种情况:手机明明有独立GPU和NPU,但推理框架运行时走了兼容模式,所有算子都在CPU上执行,还不是全部大核,而是被系统调度到了低功耗核心上。最后生成速度惨不忍睹,问题就出在“后端没有初始化成功”上。
怎么确认后端是否生效?简单靠谱的方法是看推理框架启动时的日志。通常它会在初始化阶段打印一行类似“backend: cpu”或者“backend: gpu”的信息,并附上设备名称。如果日志显示的是CPU,而你期望的是GPU,那问题就清楚了。
另外还要检查一个细节:推理框架内部是否对“算子”做了优化。LLM推理里面最吃耗时的是注意力机制,尤其是长上下文下的注意力计算。优化过的实现会用分页注意力或者FlashAttention式的算子,大幅减少中间张量读写。如果框架没有启用这些优化路径,而是退回到逐token计算的朴素实现,长上下文下慢个五到十倍都不夸张。排查方法是看日志里有没有出现类似“inefficient attention fallback”之类的警告。见到这类警告,优先级最高的事情是换个推理后端或者升级框架版本,让算子走优化路径。
注意:手机系统权限也会影响后端初始化。某些推理框架想访问GPU/NPU需要相关权限,如果没授予,框架会静默回退到CPU。排查时要留意应用权限设置,尤其是“特殊访问权限”相关的选项。
2.3 第三层:运行时内存与缓存策略——被忽略的隐形杀手
模型格式和后端都没问题,速度仍然慢,就要看运行时的内存策略了。
前面提过的mmap机制是第一个关键点。启用mmap后,模型权重在首次加载时不全部读入内存,而是让操作系统按需从磁盘映射到内存,能显著降低加载时间和启动阶段的内存压力。未启用mmap时,会额外产生一次完整拷贝,意味着加载14GB模型就要真的读14GB数据再写14GB内存,慢很多不说,内存占用还高到触发系统回收。
第二个关键点是内存占用是否触发了交换(swap)。手机上通常没有传统的swap分区,但系统有类似的内存回收机制。当应用占用的内存超过系统阈值,系统会把一部分页面放到zram压缩内存或者直接杀掉后台进程来腾空间。推理过程中如果频繁发生这类情况,每生成一个token都可能伴随换页等待,速度会跌落一个数量级。
实操中怎么判断?最直接的方法是打开手机开发者选项里的“正在运行的服务”,观察推理进程的内存占用趋势。如果内存持续增长且系统开始压缩或回收,设备整体会变得卡顿,同时推理速度断崖式下降。这个问题常见于未量化的模型在8GB内存手机上运行。所以这里再次回到那个建议:端侧跑LLM,老老实实使用4bit或更低精度量化,这是从源头上控制内存压力的最有效手段。
第三个小细节是“KV Cache预留长度”。推理框架通常允许设置最大上下文长度,对应地会在初始化时预留KV Cache内存。把上下文长度设置成4096而实际只用了512,看似无害,实则预留了大量内存,增加了内存压力。经验做法是:按实际需求的最小值来设置,比如测试场景直接设成512或者1024,训练中再逐步调大。
3. 实测复现与修复:三个典型配置的全过程对比
3.1 测试环境与基准基线
为了把问题讲清楚,我重新构造了一次完整的测试环境。设备是一台8GB内存的安卓手机,芯片是某款中高端移动平台,系统是当前厂商最新稳定版。测试模型是同一来源的7B模型,准备了三个版本:原始FP16版、INT8版、INT4量化版。推理框架选用某开源的端侧推理引擎,支持CPU和GPU后端,也支持mmap开关。
先跑一遍“标准理想配置”作为基线预期:INT4量化文件,开启mmap,启用GPU后端,上下文长度设为512。实测下来,首token延迟大约0.9秒,后续生成速度稳定在每秒20个token左右。这个结果和外界晒出来的数据基本一致,说明设备本身没问题,推理框架配置正确情况下能达到主流水平。
然后我故意制造了几种“翻车配置”,用它们来模拟标题里那个80倍差距是怎么产生的,也顺便验证排查结论。
3.2 翻车配置A:未量化模型加未开mmap加小核调度
第一组翻车配置完全复刻了我在真实现场遇到的状况:加载FP16原始权重文件、关闭mmap、后端强制走CPU、让系统自己去调度核心。这个组合的结果非常“灾难现场”。
启动阶段,模型加载花了接近90秒。这个阶段慢是因为FP16权重文件14GB,没有mmap的情况下框架先做了一次完整的磁盘读取和内存写入,同时因为没有量化,8GB内存直接被打满,系统开始强制回收后台进程,整个手机界面都变得卡顿。
生成阶段,速度稳定在每秒0.3个token左右。也就是说,生成一句20个字的回复,用户要等一分多钟。对比基线的20 token/s,这个差距确实是“约80倍”。这里有一个非常直观的计算:20除以0.3约等于66倍,如果把首token延迟也折算进去,在某些场景下超过80倍完全合理。
我还专门用系统自带的性能监测工具确认过一个细节:这组配置下CPU的负载分布在所有核心上,但高负载线程主要落在小核上,大核利用率极低。这就说明不仅是配置选错了,推理框架的线程调度策略也没有主动绑定大核。因此真实速度比“FP16只能跑3.5 token/s”的理论上限又低了一个数量级——理论前提是带宽跑满,实际却连带宽的20%都没用到。
3.3 翻车配置B:单独修正一项之后的效果
为了验证每个环节的独立影响,我做了单因素对照。
第二组配置只做一处修改:把模型从FP16换成INT4量化版,其他保持原样(仍然关闭mmap、CPU后端、默认核心调度)。结果模型加载时间直接从90秒降到20秒左右,因为权重文件变小了,内存压力也骤降,不再触发系统回收。生成速度提升到每秒4个token上下,比第一组快了十倍多,但离基线的20 token/s还有明显差距。
这说明一个道理:量化是这次优化里贡献最大的单一因素,但它不是全部。剩下的差距来自CPU和GPU后端的差别、mmap开关、核心调度策略等因素。很多人的优化只停留在“换了一个量化模型”,速度提升了就觉得到位了,实际上剩下的那部分差距才是真正决定体验好坏的关键。
第三组配置继续修正:打开mmap、启用GPU后端、将线程亲和性设置为优先大核。修正之后,速度回到每秒20 token的水平,首token延迟从几十秒降到1秒内。之前所有现象都消失了。
这里有一个非常重要的体会:端侧推理性能是“木桶效应”,而这只木桶通常有多个漏洞。只堵住最大那个洞是不够的,最好把所有洞都检查一遍。这也是为什么我建议按固定顺序排查:模型格式、后端类型、mmap、上下文配置、核心调度,一个一个验证,不要跳着来。
提示:如果你在实测中只想快速拿到一个“不弱于平均水平”的结果,记住这个组合——4bit量化模型 + 开启mmap + GPU/NPU后端 + 合理上下文长度。这套组合在绝大多数现代移动设备上都能跑出接近理论带宽上限的水平。
4. 常见问题排查速查表与个人体会
4.1 端侧推理“慢十倍以上”排查速查表
把这次排查过程沉淀成一张速查表,下次再遇到类似问题,可以直接对照定位。
| 症状 | 可能原因 | 验证方式 | 解决方案 |
|---|---|---|---|
| 模型加载特别慢,启动要几十秒甚至几分钟 | 未开启mmap或以全精度加载大文件 | 观察加载阶段耗时和日志中的文件操作 | 开启mmap,改用量化模型文件 |
| 生成速度远低于同芯片平均水平 | 模型实际精度是FP16而非量化格式 | 查看引擎加载时打印的精度信息 | 重新转换模型,确认量化生效 |
| GPU后端启用后反而更慢或报错 | 后端初始化失败,实际静默回退CPU | 检查初始化日志中的backend字段 | 重新安装对应后端组件并授予权限 |
| 长时间运行后速度越来越慢 | 系统内存压力大,触发回收 | 观察内存占用和系统卡顿情况 | 减小上下文预留,改用更低位宽量化 |
| 日志里有attention相关警告 | 注意力算子走了低效回退实现 | 搜索日志中的fallback关键词 | 升级推理框架或切换到优化过的算子库 |
| CPU负载高但大核利用率低 | 线程被调度到小核 | 查看核心频率与负载分布 | 手动设置线程亲和性或开启性能模式 |
| 首token延迟长但后续速度快 | 预填充阶段算子优化不足 | 分别统计首token和后续耗时 | 检查prompt处理路径,启用连续批处理优化 |
4.2 被系统调度坑过之后的心得
这次排查中花时间最多的不是模型格式,也不是后端选择,反而是“核心调度”这个看起来最不起眼的点。手机系统和普通电脑不一样,功耗管理策略非常激进。推理框架线程如果没有主动请求高性能核心,系统会倾向于把负载放在能效核上,因为这样省电。问题是LLM推理是“延迟敏感型”负载,小核跑大模型完全跑不动,而且因为负载太重,系统一会儿把线程切到大核、一会儿又切回小核,导致速度忽快忽慢,体验比稳定地慢还要难受。
解决这类问题有几个实用经验,级别从低到高:
- 先在系统层面把推理应用加入“性能模式”白名单,有些厂商会提供“游戏模式”或者“高性能模式”,效果等同于给进程提权。
- 检查推理框架是否有“核心绑定”或者“线程优先级”参数,有些框架允许你通过配置把计算线程固定在大核上,优先级的调整对防止被系统调度器踢走很有效。
- 如果框架没有提供接口,可以借助系统级工具给进程设置
cpu_affinity,把允许使用的核心范围限制在大核子集上,避免系统把小核也算进去。
这里要提醒一点:强制绑核的代价是发热。手机没有主动散热,长时间全速跑大模型,温度会迅速上升,然后系统为了保护硬件开始强制降频,效果可能比不绑核更慢。所以我的个人建议是:测试场景可以绑核,日常使用还是让系统调度,配合适度量化保证负载不要太重,反而是更稳定的长期方案。
4.3 排障顺序与思维习惯:不要被“80倍”带偏
再回头看“为什么我的手机跑LLM比别人慢80倍”这个问题,真正有价值的不是那个80倍的数字,而是数字背后的组装方式。80倍不是单一原因造成的,它通常是三个以上小问题叠加的结果。FP16比INT4慢五六倍,不开启mmap让加载慢十几倍,CPU后端比GPU后端慢三四倍,小核调度又让速度折半——这几个数字乘起来,轻松超过百倍。
这种“乘数效应”也给了我一个重要的思维习惯:遇到数量级夸张的性能差距,不要试图找到一个“一锤定音”的根因,而是先把所有可能的因素列出来,然后用“快速对照实验”逐个排除。每修正一个因素,重新测一次速度。如果某一项修正带来十倍改善,继续找下一项;如果某一项改了完全没变化,说明当前瓶颈不在那里,跳过它。
有几个问题很容易让人白费力气,如果你也想做类似的端侧推理优化,建议提前避开:
- 不要一上来就怀疑处理器型号不行。中高端移动平台跑7B模型的差距没有你想的那么大,瓶颈几乎都在配置和使用姿势上。
- 不要忽略推理框架本身的升级。这类框架迭代非常快,隔一个月的新版本可能针对新硬件做了适配,性能差距就能有一倍。
- 不要把“跑起来能用”误认为“配置正确”。有一个基准测试习惯很重要——用同一个模型、同一批Prompt、同一组参数,在不同配置下跑一遍并记录数字。没有记录就没有对比,没有对比就无法感知慢在哪里。
这次实操下来,我最大的体会是:端侧推理没有你想的那么玄学,它的性能上限基本由内存带宽决定,而实际能拿到多少性能,取决于模型精度选择、推理后端和运行参数的组合是否合理。只要你先把“量化 + mmap + 正确后端”这三板斧用好,就能在绝大多数手机上跑出没有明显短板的体验。至于更进一步的大核调度、算子级优化,那些是锦上添花的东西——先把地基打牢,再谈屋顶。