2026/10/16 13:17:42

端侧LLM推理优化实录:从0.4到18 token/s的80倍提升路径

端侧LLM推理优化实录:从0.4到18 token/s的80倍提升路径 上周我把一个开源对话模型部署到手机上打算做一个完全离线的个人助手。模型选了一款7B级别、量化后不到4GB的那种按说以现在主流的旗舰级移动平台跑这个规模的端侧推理应该不至于太难堪。结果一测生成速度傻眼了稳定输出时每秒钟只能吐出不到半个token短一点的回复都要等一两分钟体验完全是上世纪拨号上网的感觉。而同一时间我在开发者社区里看到有位老哥用同款手机跑类似模型每秒能出三十多个token。我掐指一算正好差了80倍。这个差距大到让人没法接受。我把这次排查的全过程记录下来包括一开始的误判、拆解出来的各类瓶颈以及每步优化对应的实测数据。如果你也在做端侧LLM部署或者只是好奇为什么别人的手机能跑飞快、自己的手机却在原地转圈这篇实录应该能帮你省不少时间。排查到最后我发现所谓80倍差距里真正属于硬件差异的部分其实很小大部分是被配置细节和测量口径放大的误会。1. 现象复盘同一个模型、同一部手机慢80倍的落差从哪来1.1 先把“慢”拆成两个指标LLM推理的耗时大多数人只知道一个笼统的“生成速度”实际上必须分成两个阶段来看。prefill阶段是处理你输入的prompt它要一次性对所有词元做并行自注意力计算这个阶段更吃算力decode阶段是逐词生成回复每生成一个token都要把整个权重过一遍这个阶段极度依赖内存带宽。如果你只记录“首字延迟”或者“平均速度”很容易被某一个阶段拖垮从而误判原因。我一开始只记录了“从按下发送到收到完整回复”的总时间这其实是个很粗糙的指标。后来我改成分别计时输入固定长度的prompt后到第一个token到达的时间首token延迟以及后续稳态生成阶段的每秒token数。这才发现慢的不只是decodeprefill更是慢得离谱300字左右的prompt光处理就要将近半分钟这在别人那里通常只需要1到2秒。1.2 实测基线我的设备、我的配置先把基线交代清楚。设备是一台8GB内存的某主流旗舰手机芯片是当前时间点的高端移动平台。模型本身没问题量化后体积符合预期推理引擎用的是某个开源项目配置方面基本都是默认值全部算子走CPU模型以FP16格式在内存里跑没有额外的上下文优化。测量工具是一个简单的计时脚本在流式输出的回调里打时间戳取三次运行的中位数。import time start_time None first_token_time None token_count 0 timestamps [] def on_token(text): global start_time, first_token_time, token_count now time.time() if start_time is None: start_time now if first_token_time is None: first_token_time now - start_time token_count 1 timestamps.append(now - start_time) # 推理结束后稳态速度取中间段 # token_count / (timestamps[-1] - timestamps[中间点])基线数据是这样的首token延迟约75秒稳态解码约0.4 token/s参考那位开发者的数据首token延迟约1秒出头稳态解码约32 token/s。稳态速度差距正好80倍。注意我这里对齐参考配置后发现他跑的是一个更小的3B级别混合量化模型还有主动散热和外接供电的加持。但即便如此我自己这台设备在正常情况下也远不该这么慢。1.3 三个容易混淆的对比陷阱复盘过程中我发现很多“慢80倍”的案例根本不是配置问题而是测量口径不同。第一用首token延迟对比别人的稳态生成速度这是最离谱的因为首token包含了模型加载、预热、prompt prefill整个过程第二prompt长度差异喂500字和喂50字首token时间可能差出几十倍第三流式输出和整体等待的差异别人可能只是边生成边刷新而你非要等全部输出完成才看到结果。我当时为了快速对齐参考数据特意统一了测量点都用一段固定200字的中文prompt开头从引擎收到请求开始计时首token延迟取流式输出的第一个字符时间戳稳态速度取中间100个token的平均耗时。这样才敢说“慢80倍”不是测量误差。2. 第一次误判我一开始以为模型装错了其实是加速单元根本没干活2.1 为什么手机上的默认配置会这么坑先说结论移动平台上的开源推理引擎装完之后默认很可能根本没启用NPU或GPU。很多引擎为了兼容性优先在CPU上执行全部算子。桌面平台问题不大因为CPU核多、主频高散热也好但手机的CPU是“大核少、小核多”的混合架构推理这种持续高负载任务很难长时间维持峰值频率。更要命的是如果模型内部某些算子不被加速库支持调度器可能直接放弃整条执行链路的加速静默回退到CPU而不是只把那几个算子挪到CPU跑。我当时完全没有怀疑这一层。因为模型能正常加载、能出字我的第一反应是“模型文件的量化是不是坏了”或者“是不是prompt设置有问题”白白浪费了半天。2.2 确认过程监控、日志、算子级观测要确认“加速单元有没有干活”我用了几种手段按从粗到细排列先看设备资源监控。跑推理的时候切到后台观察CPU和NPU占用曲线。如果是纯CPU回退能看到CPU各核心几乎全部满载或者交替满载而NPU部分始终是0%如果启用了NPUNPU占用会拉满甚至顶到100%CPU占用反而会低一些。这一步是最快的。再翻推理引擎日志。把引擎日志级别调到verbose重点搜索几个特征词fallback、unsupported、not optimized、partition。如果有大量“算子回退到CPU”的记录基本可以实锤。很多情况下原因就是引擎版本和芯片加速库不匹配或者模型导出的格式带了不支持的算子组合。ENGINE_LOG_LEVELverbose your_app 21 | grep -Ei fallback|unsupported|not supported|partition最后做算子级观测。让引擎把每个算子的执行设备打印出来或者干脆把模型拆开看算子列表。我在日志里看到模型的主要计算图里约七成算子走了CPU路径尤其是Attention里的Softmax、LayerNorm和一些非标准形状的矩阵乘都没有被加速库接受。2.3 修复动作和第一次提速找到原因之后修复其实不复杂。我把推理引擎升级到带对应芯片加速插件的新版本重新导出了模型文件打开层融合选项让QKV线性层合并、LayerNorm融合成单个算子最后在初始化代码里显式声明设备绑定到NPU。另外还有个意外收获升级后发现引擎默认会做一次权重布局转换恰好把之前“按需解包”的问题也顺带解决了模型被整体预加载到内存里。修复后同一段prompt重测首token延迟从75秒降到约10秒稳态解码从0.4 token/s提升到5 token/s提升了12倍左右。这一步让我意识到10倍级的差距往往不是模型问题而是硬件加速根本没有参与。不过距离32 token/s还差得远接下来才是真正要啃的硬骨头。3. 真正拉开差距的是“内存搬运”不是算力标称值3.1 手动算一遍每生成一个token要搬多少GB数据很多人看到芯片厂商宣传的TOPS算力就觉得只要算力够大跑LLM就快。这是一个非常普遍的误解。拿数据来算一遍就清楚了假设一个7B参数的模型FP16存储的话权重大小约14GB。decode阶段每次生成一个token理论上都需要把这14GB权重从内存搬进计算单元因为Transformer的权重是逐层复用、按顺序参与计算的没法提前缓存全部。现代手机的内存带宽大约在50GB/s上下那么14GB除以50GB/s约等于0.28秒也就是说推理的理论极限也就3.5 token/s左右。如果换成INT4量化权重大约3.5GB理论极限可以到14 token/s左右。这才是为什么端侧跑LLM几乎必须要量化的原因——不是精度不够才量化而是不量化根本喂不饱计算单元。这就解释了为什么“CPU看起来好像没多忙但速度就是上不去”decode阶段本来就是内存负载型任务CPU只是在那等着数据送过来。如果发现推理时CPU占用率不高但耗时很长大概率不是CPU不行而是访存等待占据了绝大部分时间。3.2 同一个手机为什么还能差出好几倍理论计算解释的是“上限”但现实里还会出现很多离谱的慢。我那个0.4 token/s就是典型。正常情况下即使纯CPU加FP16也不该这么慢。后来我发现问题出在三个叠加项上。第一模型文件一开始放在一个需要按需解包的路径里。推理引擎默认惰性加载每遇到一个权重块就现场解包一次一个token要触发成百上千次小块读取时间全耗在I/O上。这听起来很蠢但很多直接从压缩包解压出来就用的部署方式都会踩这个坑。第二8GB内存被大量占用系统把模型的部分页换到了swap区。decode阶段每个token都会随机访问权重只要有几页在swap里就可能出现灾难性的随机磁盘读取速度直接掉两个数量级。第三Attention的计算里存在大量内存重排。某些算子会对KV cache做按token维度的拷贝和拼接连续读变成了散列读。同一份数据连续读和随机读的性能差距可以达到5到10倍。3.3 怎么判断瓶颈到底是不是内存带宽这里有个实用经验在推理过程中持续观察CPU频率和内存压力。如果是算力瓶颈CPU会长期保持高频率满载如果是内存带宽瓶颈CPU占用率反而不太高可能只有20%到40%但耗时并没有变短。另一个信号是关闭后台应用、清空内存之后速度立刻提升而增加CPU核心数反而没有明显变化。我修掉了按需解包路径、把模型预加载到内存并让KV cache连续分配之后稳态速度从5 token/s提升到了约9 token/s。这中间的提升没有改任何计算逻辑纯粹是减少内存和I/O层面的浪费。# 粗略观察CPU频率是否维持在高位 while true; do grep cpu MHz /proc/cpuinfo | head -n 8; sleep 1; done4. 从“能跑”到“快跑”把每个环节的隐藏开销逐个挖掉4.1 量化不是无脑INT4混合精度才是端侧常态在内存带宽受限的前提下量化位宽直接决定decode速度。我用INT4把全模型重新量化一遍速度从5提升到9 token/s。但性能提升的同时我也得盯住精度指标。实测下来直接全模型INT4在某些长文本任务上回答质量有明显下降尤其是最后的输出层量化误差会被放大。后来我改成混合精度量化Attention里的QKV投影这类结构规整、冗余度高的层用INT4最后的lm_head输出层和部分embedding层保留INT8其余按敏感度选择。校准样本不需要太多几百条就行关键在于量化工具能不能按层给出误差贡献。这个调整让速度几乎不变但回答质量基本回到可用水平。顺带提醒一句端侧推理的速度优化一定要和精度评估绑定不能只盯着tokens/s。我在后面加了一组固定的问答集做回归测试每次量化策略改动都跑一遍防止“快得很但满嘴胡话”。4.2 KV Cache复用长对话变慢的隐形杀手如果只是单轮短对话KV cache优化感觉不明显。但做多轮对话时很多实现会犯一个错误每一轮都把整个历史prompt重新prefill一遍。历史越长每次prefill的处理量越大速度越来越慢到后面几轮几乎等于卡死。排查方式是在日志里找cache命中和未命中的计数如果未命中率极高说明缓存没有正确复用。对我这个场景我在引擎里显式开启了上下文缓存并且把最大上下文长度从默认的8K收紧到4K。这么做的一个额外好处是预分配KV cache缓冲变小内存占用降低系统更不容易触发swap。调整后长对话场景的首token延迟从十几秒降到了三秒左右。4.3 线程绑核、性能模式和散热手机不是服务器桌面服务器上“线程数越大越好”在手机上完全不成立。手机的核分大小核如果让推理引擎把线程撒给所有核心一部分线程会被调度到小核上反而拖慢整体。我试过把线程数量全部打开然后观察结果速度反而下降了接近20%。正确做法是启用手工绑核把推理线程只绑定到大核集群或者至少设置线程亲和性。不同平台API差异很大这个需要查具体引擎的文档。功耗策略的影响也很大。我曾经在省电模式下测过一次速度只有性能模式的一半左右插电状态下又比电池供电更稳。手机端做长时间推理时温控降频几乎不可避免典型表现是刚开始速度正常跑几十秒后越来越慢。解决办法有两个方向一个是从配置上限制batch大小和上下文长度降低瞬时功耗另一个是物理手段我用一个普通散热背夹把温度压住之后连续三分钟推理的速度稳定度明显改善。4.4 优化过程中每一阶段对应的实测数据把整个优化过程的数字汇总一下方便大家对照参考阶段首token延迟稳态速度累计倍数初始基线纯CPUFP16按需加载约75秒约0.4 token/s1x启用NPU加速并预加载模型约10秒约5 token/s12.5xINT4量化约6秒约9 token/s22.5xKV cache复用短上下文约3秒约12 token/s30x混合量化线程绑核性能模式约2.5秒约18 token/s45x从0.4提到18累计45倍。剩下那点差距主要是我的模型是7B级别而参考配置用的是3B级别的小模型再加上参考环境里有外接供电和主动散热。如果真用同一模型、同一环境对齐差距大概只有1.5倍左右这属于硬件平台内存带宽的正常差异不是配置问题。5. 复盘清单以后再遇到端侧推理慢按这个顺序查5.1 排查顺序和每步的关键信号第一看加速单元是否真的参与。设备监控里NPU占用是否为0日志里有没有大量fallback。这一步没做好后面所有优化都可能在错误的地基上瞎使劲。第二看模型位宽和存储路径。模型是不是FP16硬跑有没有做量化模型权重是不是每次都在从压缩包或慢速存储按需读取。第三看内存和swap。8GB内存的手机跑7B模型本来内存就紧一旦触发换页速度会急剧恶化。可以用设备自带的内存监控先确认可用内存。第四看KV cache复用和上下文长度。多轮对话慢主要在这一层。第五看线程调度和功耗模式。确认推理线程没被调度到小核、性能模式是否打开、温度是否导致降频。5.2 哪些“慢”其实不是模型的锅一些常见的“看起来慢80倍”的情况查到最后和模型本身都没关系。例如只测首token延迟但prompt长度是别人的二十倍后台有一堆应用占着内存系统频繁swap把模型跑在没插电且省电模式的手机上和别人插电加性能模式比还有的是因为流式输出设置不对引擎全部生成完才一次性返回体验上极其卡顿。这些情况只要对齐测量条件所谓的80倍差距往往会瞬间缩水到几倍以内。5.3 几条建议总结让我少走弯路的东西如果你刚接触端侧LLM部署我的建议是先用一两天时间把完整的性能测量脚本做好把首token延迟、稳态速度、内存占用、芯片温度几个指标固定下来每次改动配置都跑一遍所有优化项目都建立在这个基准之上。没有基准就做优化等于闭眼开车。另外心态上要有个预期端侧推理的性能优化不是一个开关就能解决的事通常是内存、量化、调度三个层面各挤一点合起来才有效果。单项优化可能只带来10%到20%的提升但叠起来很容易追回几十倍的差距。我自己这次排查最大的收获是真正理解了端侧LLM的瓶颈不是“算力不够”而是“数据运不过来”。以后看到芯片厂商宣传某个平台算力有多强我都会先问一句它的内存带宽支持每秒搬几GB这个问题想明白了端侧推理的很多怪现象就都有了答案。