资讯动态

模型推理性能优化:五大加速角度深度解析

发布时间:2026/10/13 6:32:32 来源:尧图企业网站定制
很多人在部署模型推理时都有过这种体验同一个模型换一套推理后端或者改一下精度延迟能差出一大截同一块硬件调整批处理方式后吞吐直接翻倍。这些“变快”并不是玄学背后是实打实的性能优化手段。我平时在这个方向踩过不少坑也做过不少对比实验慢慢发现模型推理的性能优化可以拆成五个角度来理解模型结构的源头瘦身、硬件层面的适配利用、量化的精度交换、系统层面的批处理与内存调度、缓存复用带来的重复计算消除。这五个角度各有各的加速原理实际工程里经常是组合拳但先把每一拳的逻辑搞明白你遇到具体卡点时才不会抓瞎。1. 角度一从模型结构上做源头瘦身1.1 剪枝把影响最小的参数直接删掉剪枝的思路很朴素一个神经网络里并不是每个权重都一样重要。很多参数的绝对值很小训练结束后它们对最终输出的贡献趋近于零。把这些“无用”的权重清掉网络依然能保持大部分精度但计算量和访存量都会降下来。剪枝按粒度分两种。非结构化剪枝针对单个权重细到矩阵里任意一个元素都可以被置零。这种方式理论压缩率最高但稀疏矩阵计算依赖专门的算子库和硬件支持处理不好反而比稠密矩阵更慢。结构化剪枝则是按行、按通道甚至按整层去裁它直接改变的是算子的输入形状比如把卷积层的输出通道从256减到128不需要特殊指令集的配合就能直接提速缺点是精度损失往往更大需要重新微调恢复。实操时我的习惯是先用一个小型代理数据集做敏感性分析看每一层取舍对精度的伤害有多大再从影响最小的层开始剪。剪枝率一般从50%开始尝试逐步往上加每次剪完做几步微调。剪枝不只是省FLOPs它还省显存带宽很多场景里访存开销比计算开销更大这点比想象的赚。注意剪枝风险最大的地方是剪完不评估就直接上线。模型在标准测试集上精度没掉不代表在真实数据分布上没事。剪枝前一定保留好原始权重作为回归基线。1.2 知识蒸馏与结构重设计小模型学大模型蒸馏的思路是用一个轻量小模型去模仿大模型的输出分布。大模型在Softmax层输出的不只是正确标签还携带了“这个类别跟另一个类别有点相似”的暗知识。小模型学习这种软标签而不是硬标签往往能学到更多泛化信息最终体量远小于大模型但精度接近。蒸馏里温度参数是核心。温度越高Softmax分布越平滑小模型能从大模型中学到更多类别之间的相对关系温度太低则退化成硬标签训练。一般做法是先在高温下蒸馏再逐步降温做微调这样小模型既能学到结构性的相似性也能拟合正确分类。除此之外结构重设计也属于源头优化的范畴。比如把标准注意力替换成线性注意力或稀疏注意力让复杂度从序列长度的平方降为线性又比如把激活函数换成更便宜的实现或者调整算子融合友好的网络结构。这类改动通常需要训练侧配合部署团队拿到现成模型后没法直接改所以很多时候是在蒸馏阶段就一并设计好。我见过一个实际案例某对话模型从原有规模蒸馏到一半参数量推理吞吐翻了将近一倍任务精度只掉了不到一个点。这里的关键不是模型小所以快而是小模型的结构和部署硬件匹配度更高访存压力小缓存命中率高。2. 角度二硬件层面的适配与利用2.1 算子执行过程算力到底消耗在哪里加速卡上的一次算子执行可以拆成四步把数据从显存搬到计算单元、指令调度、执行算术运算、把结果写回显存。很多人以为耗时全花在第三步实测下来的结果往往相反——数据搬运才是大头。模型权重几百兆甚至上GB每次计算都要反复搬进搬出内存带宽成了最稀缺的资源。这就是为什么算子融合能带来立竿见影的效果。举个例子卷积后面通常跟着批归一化和激活函数。常规做法是先算卷积把中间结果写回显存再读出来做批归一化再写回去再做激活。融合后的做法是在一个核函数里把三步全部算完中间结果只留在寄存器或者高速缓存里不经过显存跳动。一来省掉两次搬运二来减少核函数启动开销整体耗时可以降一半以上。算子融合的好处不只是省时间。核函数减少之后CPU端的调度开销也变小了。在CPU上跑的时候指令调度是流水线式的融合能增加单次指令的有效计算量在GPU上则是减少kernel launch的间隙。很多推理引擎自动做这些事但它能识别的模式有限遇上自定义算子时还得手写融合核。我踩过一个坑某次优化图里看到了几十个小算子连续执行每个单独看耗时都很低但合起来占了总延迟的30%。这就是典型的融合没做干净。后来手动把相邻的归一化层和激活层合并写进前一个卷积核延迟直接降了三分之一。2.2 访存优化让数据流动成为提速的关键现代芯片上算力提升的速度远快于内存带宽提升的速度所以推理经常不是“算不动”而是“喂不饱”。解决这个问题的核心思想就是分块计算把大矩阵切成小块让块在高速缓存里充分复用而不是每次都跑到显存取数。以注意力机制为例标准实现里要计算一个完整的注意力分数矩阵并把它存到显存里。模型规模一大这个矩阵的尺寸就爆炸了。优化的做法是不计算完整矩阵而是分块计算Softmax把每个分块的中间结果在缓存里聚合最终避免把平方级的中间矩阵写回显存。这个思路说白了就是“把大文件拆开在内存里就地处理”归功于对访存模式的重新设计这种优化在自回归模型的长序列推理中收益尤其明显。还有两个容易忽略的访存优化点。一个是内存布局数据是按通道连续还是按空间连续对向量化指令影响很大另一个是对齐内存地址没对齐会导致多读好几个字节。很多推理引擎在预处理阶段自动做布局转换但如果是自定义算子得自己去确认数据排布的合理性。实操建议跑一个Profiler看算子的时间构成。如果计算单元利用率已经很高瓶颈在内存的话优先做融合和访存重构如果计算单元利用率不到30%瓶颈在算力调度重点看算子是否选对了硬件实现。3. 角度三量化用精度换速度但要换得聪明3.1 量化的基本逻辑与表示方式量化是把模型里的浮点参数从高精度转成低精度。最常见的是FP32转FP16或INT8。低精度的数据占用更少空间带宽需求下降计算单元可以塞更多并行通道速度自然上去。比如同一块推理芯片上INT8的峰值算力往往有FP16的两倍以上而在带宽受限的场景里INT8开销量减半带来的收益更加直接。INT8表示方式分两种对称量化和非对称量化。对称量化用最大值把浮点范围映射到[-127,127]实现简单但可能浪费一部分表示范围非对称量化为正负区间分别设偏移量信息保留更好但推理时多一步偏移计算。实际工程里激活值常常是正数用非对称量化更合适权重值正负分布对称直接对称量化就行。量化的难点不在定标方式而在怎么确定缩放系数。训练后量化PTQ流程里要用一小批校准数据统计激活值范围。最简单的策略是取最小值最大值但激活值往往有长尾分布一个极端值就能把整个范围拉大导致大多数值精度被压缩。更稳妥的做法是用KL散度或者熵来搜索最优阈值让量化前后的分布损失最小。这块计算量不大却直接影响最终精度。3.2 量化的粒度、混合精度和实操取舍量化的粒度是指缩放系数作用的范围。Per-tensor是整张张量一个系数实现最简单但网络不同通道的数值范围差异大直接用会损失精度。Per-channel是每个输出通道单独一个系数精度保真度明显提升也是很多推理引擎默认的权重量化粒度。激活量化因为布局问题通常还是per-tensor必要时再叠加Bias校正。实际操作中更常用的策略是混合精度不是每层都量化到同一精度敏感层保留FP16或FP32其他层压到INT8。判断敏感层的一个办法是逐层做一个敏感性扫描——把某一层单独量化后看精度掉多少把这个信息做成一张排序表精度损失大的层优先放宽。这活儿用脚本几十分钟就能跑完比盲目全量化靠谱得多。我实测过几个常见结构的视觉模型INT8量化后精度损失通常在一个百分点以内有时甚至出现量化后精度略微提升的情况——因为低精度带来隐性的正则化效果。但在小模型或者本身分布就很敏感的模型上量化掉点可能非常严重这种情况必须上量化感知训练让模型在训练时就适应低精度数值范围。注意量化后一定要在实际推理硬件上测试速度而不是只看理论加速比。有些硬件对INT8支持得很好有些则通过模拟方式执行INT8结果反而更慢。另外警惕数值溢出特别是激活值累计到很大时INT8的表示范围不够会导致输出异常这种情况加一个动态裁剪就能解决。4. 角度四系统层面的批处理与内存调度4.1 动态批处理把零散请求合并成高效批次大多数推理服务面对的是零散到达的请求如果每个请求单独占一块计算资源跑一遍模型硬件利用率上不去吞吐量也被拖垮。批处理的思路是把多个请求合并成一个batch同时执行摊薄固定开销。但静态批处理会锁死输入形状批次里有短序列时只能填充到最大长度大量算力花在无效的填充符上。动态批处理则是在服务端按时间窗口攒请求比如每20毫秒把到达的请求收集起来按输入长度尽量分到相近的组里再执行。短的跟短的在一起长的跟长的在一起这样执行效率更高。代价是单请求的等待时间会拉长所以服务配置里要设一个最大等待时间不能无限等下去。实际部署时延迟的P50和P99是两个极端。动态批处理能大幅压低P50的吞吐成本但控制不好会让P99涨得很难看。我的经验是把最大等待时间设为模型单次前向时间的0.3倍左右这样吞吐和延迟的平衡点比较舒服。同时要监控批次的token填充率如果这个指标低于60%说明动态批的合并逻辑还有优化空间。4.2 显存管理与连续批处理自回归模型有一个特殊问题每个请求在推理时逐token生成生成过程有长有短。如果用固定批次执行批次里最先结束的请求还会一直占着显存直到整个批次完成显存浪费很严重。连续批处理解决了这个问题调度器在迭代级别管理请求生成完成的请求立刻释放资源等待队列里的新请求马上补进空位。这样显存始终被有效利用整体吞吐提升很大。这个机制要求推理引擎支持迭代级调度很多高性能推理框架都实现了开箱即用。显存碎片也需要关注。推理服务运行久了频繁分配释放会留下碎片加剧显存压力。工程上常见的做法是显存池化预分配若干固定大小的块请求来了直接复用空闲块不反复走操作系统的内存申请。这个优化在CPU侧也有效HugePage对大页内存的提升就是类似道理。实操建议先监控硬件利用率和请求队列长度。如果利用率低、队列又积压优先调批处理策略如果利用率不低但延迟高可能瓶颈在算力本身这时才考虑模型结构或量化。5. 角度五缓存复用让重复计算直接消失5.1 自回归推理中的KV缓存自回归模型生成每个token时注意力层都要根据当前已有的token去计算Key和Value。这些K和V其实就是前文内容的投影结果每次生成新token时重新算一遍纯属浪费。KV缓存把已经算过的K、V存在显存里新步骤只需要计算新token对应的部分再拼接上旧的缓存整体计算量大幅下降。KV缓存的大小和组织方式直接影响显存占用和调度效率。组织上缓存被切成固定大小的块类似操作系统的内存页每个请求按需挂载块生成完了释放。大语言模型里KV缓存动辄占几GB显存几千token的上下文就会吃掉很大一部分容量这也是长上下文推理成本高的根源。实际工程里还有一层优化——提前分配。推理引擎根据用户输入显式预测最长上下文预先分配一批KV缓存块避免生成过程中动态扩容带来的开销和碎片。如果分配得太保守请求还没生成完缓存就用完了分配得太激进显存又浪费。这个尺寸预测直接影响服务稳定性。注意KV缓存复用只适用于前缀完全相同的场景换来的是显存压力。开启时要根据模型规模和并发数试算最坏情况不然高并发下很容易出现显存溢出。5.2 前缀缓存与请求结果复用除了KV缓存还有一层更粗粒度的缓存如果多个请求拥有相同的前缀比如相同的系统提示词或相同的几轮对话历史它们的前缀KV计算结果是可共享的。调度器可以识别这些前缀在内存里维护一棵前缀树后续请求命中前缀时直接复用之前的缓存不用重新计算。这个优化在批量服务同一类任务时收益极明显。再往外扩展完全相同的请求可以直接缓存完整输出结果。比如测试中反复调用同一个固定输入或者服务里存在大量只包含参数变化的模板请求只要请求内容一致直接返回历史输出就行。实现上可以用请求内容的哈希值做缓存键配一个LRU淘汰策略。还要小心缓存一致性提示词版本更新之后旧缓存必须及时作废否则输出张冠李戴。这五个角度串联起来看就清晰了模型结构决定速度上限硬件适配决定速度比例量化决定精度和速度的交换率系统调度决定整体吞吐缓存消除重复计算。真正做优化的人不是拿这五个工具各试一遍而是先用Profiler定位当前最大的瓶颈在哪个维度再决定优先动哪一块。我个人经验是访存密集的场景优先做融合和布局优化算力饱和的场景优先做量化和轻量化改造服务吞吐不够时先看批处理和显存调度至于KV缓存和前缀复用是所有长文本模型推理服务的必选项。最后分享一个我踩过好几次坑后形成的习惯每次性能优化只改一个变量跑一组完整的基准测试记录再把改动都版本化管理。性能优化最怕的就是好几个变量同时调最后出了问题根本不知道是哪个环节引起的。留好基线数据每次量化、剪枝、改批处理参数时都能随时回滚这套流程比任何单个优化技巧都值钱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑