资讯动态

Transformer推理性能优化:如何用算子融合解决kernel调度开销

发布时间:2026/10/22 13:57:27 来源:尧图企业网站定制
最近我在做Transformer推理服务优化时被一个反直觉的现象卡了将近两周模型该做的剪枝和量化都做了单个算子看起来省了不少浮点计算线上时延却几乎没变化。后来一层层往上扒才发现瓶颈根本不在计算量而在算子的启动和调度上。一个Transformer encoder块LayerNorm、Softmax、Residual、QKV乘法这些常见操作会被拆成几十个小kernel每个kernel从下发到执行都有固定开销算子一多这部分空转就被放得很大。这也正是某国产AI计算架构配套的ops-transformer工具想解决的问题。它不是帮你改模型结构也不是一个独立的加速插件而是运行在编译器和运行时之间的一层“算子改造器”把框架里暴露出来的算子组合按规则改写成更贴合芯片执行的融合算子或特殊算子。这篇博文是我这段时间反复调试、翻日志、对比profile数据之后的一次完整记录既讲清楚ops-transformer是什么、加速逻辑在哪也把我实测时遇到的各种意外输出和注意事项一并放出来。1. 为什么Transformer快不起来问题出在“算子调度”这条暗线1.1 Transformer的模型结构好懂算子栈却不是网上讲解Transformer的帖子大多从Attention公式入手Q乘以K的转置做softmax再乘以V后面接FFN。结构上看着很清晰但这只是数学视角。真正把模型跑在加速芯片上时这套公式会被拆成一条很长的算子链中间还要穿插transpose、reshape、scale、add、cast之类的辅助操作。每一个操作节点对执行引擎来说都是一个独立的调度单位。问题就出在这里。加速芯片不像CPU那样天然擅长随便切换任务一个kernel从启动到接收数据再到计算完毕把结果写回都有固定的开销。很多Transformer里的小算子计算强度很低耗时可能只有几微秒但调度开销也是几微秒一加一减真正花在计算上的时间连一半都不到。我这边有一个模拟项目X做的是基于BERT类编码器的文本推荐服务动态输入长度batch不太大。最初用默认框架执行profile数据里大量时间消耗在Data搬运和kernel排队上真正GEMM矩阵计算占比并没有想象中高。这说明什么说明模型结构理解得再透彻落到算子栈上还是会被执行细节拖慢。1.2 追求“算子级”加速的三个理由很多人会问直接把上层模型做大改比如换Attention变体、压缩序列长度不是更直观吗是的但上层优化和算子级优化并不冲突而且算子级优化有一个难以替代的位置。第一适配成本低。模型代码不需要大动ops-transformer在计算图层面偷偷做替换上层看到的还是原来的算子业务方基本无感。第二收益覆盖整个模型。只要Transformer里反复出现的那几个组合被优化那么encoder层数再多收益也是线性叠加的。第三它能把“通用算子”变成“芯片友好算子”。不同加速芯片擅长的计算模式不一样某些组合在通用框架里是分开执行的但芯片底层其实支持更高效的整体执行。ops-transformer就是负责发现这种机会。所以我看ops-transformer的定位不是“性能银弹”而是“算子调度和算子形态的修正层”。它解决的是模型结构和芯片执行之间那层脏活累活。2. ops-transformer到底改了什么从图解析到算子重写2.1 一次典型转写流程要理解ops-transformer怎么工作最好把它看成一条流水线。它输入的是已经解析好的计算图输出的是一份改写后的新计算图这个新图再交给后续编译和运行时去执行。整个过程大致分五步图解析先拿到整个模型的算子流确认每个节点的输入输出、数据类型、shape。这一步和框架本身关系很大不同框架导出的图细节差别不小。规则匹配把当前子图和内置规则库做匹配看有没有满足条件的组合。模式替换命中规则后把一组旧算子替换成新算子。新算子往往是一个融合后的自定义算子。校验修正替换完不是直接结束还要对新图的数值正确性做一次校验至少保证拓扑上没有断裂、数据依赖没有错乱。生成执行配置给后续编译器提供足够的属性信息比如要不要开低精度、是否启用某种数据布局。这五步并不是每次都全量执行。很多工具为了省时间会做缓存第一次跑通后后续相同结构的子图直接走缓存结果。代码层面用起来通常就是配置一个策略对象再把它挂到优化流程里。假设API类似下面这样import ops_transformer as otx policy otx.Policy() policy.enable_fusion(True) policy.enable_low_precision(fp16) policy.set_compile_cache(~/.cache/ops_tx) model otx.transform(model, policypolicy)不同版本里面API名称可能会有差异但整体思路一致先声明要开哪些能力再交给工具改写。什么都不配置直接默认跑也能拿到一些基础优化但收益会打折。2.2 算子重写背后的“三张表”ops-transformer能做的改写在我的理解里主要分成三类可以对应三张规则表。第一张表是“融合规则”。典型场景是多个小算子合并成一个大算子减少kernel启动次数和中间内存读写。比如Residual Add配合LayerNorm很多芯片上可以做成一个整体算子。第二张表是“形态转换规则”。有些算子在当前数据结构下效率不高需要做layout转换、轴重排或者维度补齐。比如某些芯片对一维向量操作不友好需要补成三维再算。第三张表是“精度转换规则”。在合适的位置插入cast算子把fp32计算转换成fp16、bf16甚至int8同时确保精度损失可控。这个不是全图无脑转而是针对匹配到的子图做局部替换。三张表可以叠加使用。我见过一个Norm激活线性层的组合既做了融合又顺带切到低精度最终性能比原始实现快了一倍以上。但收益大不代表没风险下面第3节会详细说。3. 三个能直接拿走的算子融合收益以及我实测的代价3.1 注意力模块的“Q乘K、Softmax、乘V”一条链注意力模块是整个Transformer里最值得动手的地方因为它的计算模式很特殊涉及多个矩阵乘法中间又有softmax这种带宽密集操作。传统执行流程是先算QK^T得到分数矩阵写到内存然后读出来做scale和softmax再写回内存最后再和V做矩阵乘。中间结果矩阵和输入序列长度是平方关系序列越长中间记录越庞大读写开销越明显。ops-transformer在这种位置能做的是“注意力融合”把scale、mask、softmax、乘V这几步整合成一个kernel让中间结果始终留在片上存储里不落回主存。我实测的一个模拟项目里序列长度从128涨到512这部分的耗时增长比原来平缓很多原因就是中间矩阵没有被反复搬运。代价是什么融合后的算子通常由工具动态生成第一次执行时编译时间明显变长。如果你的服务频繁重启、缓存没保存好编译耗时反而会拖累启动时长。所以做模型服务时必须提前预热否则上线瞬间会看到一堆慢请求。3.2 Norm加激活加线性层一条链全部拆开没必要Transformer FFN部分经常是“线性层、激活、再线性层”的结构中间还可能插LayerNorm或RMSNorm。一个常见的优化方向是把Norm和后面的激活算子融合进前一个线性层里。举个例子RMSNorm的输出接着SwishGLU后面又是一个MatMul。如果分开跑意味着前一个MatMul的结果要完整写回内存再做Norm再写再激活再写最后才进入下一个MatMul。这几个算子的数据量都不小内存带宽就这么被白白吃掉了。融合之后相当于把“计算完立刻接着算”改成“一次性算完”中间结果只留在寄存器或者片上缓存里效果非常直接。但这类融合对精度也更敏感。Norm类算子本身有一种数学上的归一化语义一旦融合后scale和偏移量被重新推导某些边界条件下误差会被放大。尤其当原模型用了RMSNorm且内部参数不是标准初始值的时候融合改写后的结果可能和原实现差出一个很小的epsilon。工程上通常认为几个千分点的误差可以接受但如果你在做严格对拍测试务必算一下最大误差而不是只看平均误差。3.3 低精度转换不是白捡的便宜很多AI加速芯片对fp16和bf16的吞吐远高于fp32所以低精度转换看起来是稳赚的。但实际使用中要留意两个坑。第一个坑是转换位置。不是全图统一cast就完事有些算子对动态范围很敏感比如softmax里的exp计算。ops-transformer的做法通常是只在匹配到的安全子图里降精度其他位置保留原类型这样既拿到大部分收益又不会因为极端数据导致NaN或Inf。第二个坑是累积误差。Transformer的残差结构会让每一层的微小误差不断叠加。层数越多最后一层输出的误差就越明显。我做过一次实验某些层开启fp16后单层输出误差不到1e-3但20层叠完最终输出的误差被放大了近十倍。所以低精度转换一定要结合层数做评估30层和12层模型承受能力完全不同。4. 动态Shape和内存搬运在十万次小请求中拉开差距4.1 动态Shape为什么总被优化器忽视Transformer服务最常见的动态变量是batch size和序列长度。静态shape是编译期就确定的动态shape意味着同一个算子在不同请求里可能对应完全不同的计算形状。很多优化工具对动态shape很头疼因为算子融合规则往往需要形状信息才能判断能不能融合。一个形状不确定的算子编译器要么做最保守的选择要么在请求进入时才临时生成kernel这两种都会造成性能损失。ops-transformer处理动态shape的常见思路是“分桶”。把连续变化的序列长度归入几个固定的bucket比如128、256、512每个bucket用静态图编译请求进来时按最近的bucket执行。这个方案不是它独有的但它的融合规则可以和bucket机制配合每个桶单独缓存一份融合后的执行计划这样既保住融合收益又不至于每次请求都现场编译。我在模拟项目X里实践过这个方案把动态长度按8的倍数分桶P99时延下降了20%左右。代价是显存占用变高了因为每个桶都要维护一份中间缓存。内存足够时值得做内存紧张就得缩小桶的数量让一部分请求走通用路径。4.2 内存布局与搬运调度模型优化做得越深入越会意识到一个问题带宽比算力更值钱。计算芯片的FLOPs提升很快但内存带宽的提升速度没那么夸张。所以很多算子的优化本质都是在减少内存搬运。ops-transformer会在图层面做两种改动一是修改数据布局把算子输入输出调整成芯片更喜欢的格式二是插入异步拷贝指令让数据搬运和计算重叠起来。数据布局这件事听起来很底层但影响很大。不同的layout导致同一份数据在芯片上可能连续读取也可能跳着读。连续读取可以充分利用带宽跳着读则会浪费大量时钟周期。很多性能问题翻到最后不是算子算法不够快而是数据摆放不合适。我自己的习惯是在开启融合之前先看一遍内存搬运次数。如果发现map、transpose这类重排算子特别多优先让ops-transformer先把布局整理顺再看融合收益。顺序反了的话融合出来的算子可能被外层的重排浪费掉一半性能。5. 排障实录一次算子回退引发的精度和性能双重陷阱5.1 问题出现的完整链路有一阵我把某个推荐模型切换到新版本的ops-transformer之后出现了两个诡异现象一是时延不仅没降反而涨了二是离线测评指标掉了0.3%。单独看每一个现象都还能忍两个一起出现基本说明优化策略出了问题。一开始怀疑是低精度转换影响精度把fp16开关关掉之后精度恢复了但时延更差了。再往上检查日志发现大量算子被标记为“fallback”也就是回退到默认实现。这个发现让我意识到问题不是某个算子算错而是整个融合规则没生效。顺着日志一层层追原因逐渐浮现这个模型里RMSNorm之后接了一个自定义的GELU变体ops-transformer规则库默认匹配的是标准GELU遇到自定义变体时融合规则不敢乱动只能全部回退。回退之后tineline出现了奇怪的反复拷贝性能自然就崩了。5.2 我是怎么一步步定位的排查过程比较长核心思路是二分法。先关闭所有融合确认性能基线和精度基线都是正常的。然后只开融合不开低精度只开低精度不开融合排掉两个变量的干扰。最后定位到RMSNorm和GELU变体这条链上。做法是写一个几十行的最小化复现脚本构造一个只有Norm加激活加线性层的子图单独跑ops-transformer的转换流程对比转换前后的输出。import torch x torch.randn(128, 768, dtypetorch.float16) norm torch.nn.RMSNorm(768) act CustomGELU() linear torch.nn.Linear(768, 768) def original(x): return linear(act(norm(x))) def transformed(x): return linear(act(norm(x), customTrue))把custom参数打开关闭反复测最终确认是规则匹配时没有识别自定义激活。解决方案也简单告诉工具在融合时忽略自定义GELU的特殊分支或者干脆回退到原始实现。改完之后精度指标恢复正常融合才真正生效。5.3 预防比排障更重要经过这次事故我给自己定了一套规则升级工具版本后先做小范围回归不要直接全量上线保留一份“不开启任何融合”的原始模型作为对照数值测试至少跑三个不同shape避免某一个shape碰巧掩盖问题。另外值得强调的是日志一定要看。很多人看到算子回退就panic其实回退本身是安全机制说明工具检测到当前场景不适合改写。真正要警惕的是“一半融合、一半回退”的中间状态这种状态通常会带来额外的layout转换性能反而比完全不融合更差。6. 落地阶段的可执行建议6.1 三个“先剪”再开每到一个新项目我不会急着把所有优化开关全打开。先做三件事第一砍掉无效的算子重排。如果模型里本来就很少有动态分支动态shape优化可以先不开启减少编译缓存占用。第二确认精度冗余度。有些业务场景对单条输出误差很敏感低精度融合就要收敛着开如果是推荐排序这类只看相对顺序的场景就可以放得更开。第三准备一条快速回滚通道。ops-transformer生成的融合算子会缓存在本地万一升级后出现问题把缓存目录清掉重跑一次就能快速恢复这一点很实用。6.2 值得跑的第一版性能基线我的经验是优化前先花半天时间把性能基线和算子分布情况记录清楚不要上来就开融合。我自己常用的做法是准备一组本地测试样本覆盖典型长度和batch范围然后在原始框架、只开图优化、图优化加融合、融合加低精度这四档配置下分别测试记录时延、吞吐、内存占用。跑完得到的对比表比猜测哪里快有用得多。配置平均时延P99时延内存占用备注原始框架12.6ms18.4ms2.3GB基线只开图优化11.2ms16.1ms2.3GB提升约11%图优化加融合9.1ms13.0ms2.5GB提升约28%融合加低精度7.4ms10.8ms2.1GB提升约41%需做精度对拍这个表只代表单个项目情况不同模型、不同芯片上的绝对数值差异会很大。但趋势基本一致融合带来的提升通常比单纯图优化更明显低精度则是在此基础上再压一截。6.3 我的一点体会用了这么久的算子转换工具最大的体会不是“某个开关帮了我多少”而是优化Transformer时一定要同时看三个维度算子数量、内存搬运次数、kernel启动次数。只看FLOPs一定会误判只看时延又会找不到根因。ops-transformer这类工具真正的价值是把很多本该工程师手工完成的编译优化动作自动化了。但它毕竟有规则边界模型越特殊越需要人为干预。不要在它没命中规则的时候强行硬开也不要在它回退时盲目关掉全部优化。理解它做了什么、没做什么优化路就能走得更稳。

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

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

免费获取报价 →
↑