2026下半年GPU / AI 算子 kernel优化案例:LLM 代理成为一等优化手段
2026年下半年的算子优化圈子里,最明显的变化是:LLM代理已经不是“辅助写代码”那种边缘工具了,而是直接接管了kernel优化的主流程,成为和手写CUDA、手工调tile同等重要的优化手段。我身边不少做AI Infra的团队,把预算从“雇人铲kernel”转向“搭一个代理优化闭环”。本文把这些实际案例和踩坑经验整理出来,聊一聊为什么LLM代理能站到这个位置,以及一个可复现的优化工作流长什么样。
适合看这篇文章的人:正在做GPU算子开发或性能调优的工程师、做推理/训练框架的架构师,还有对自动代码生成感兴趣的开发者。内容不局限于某一个特定芯片,主要思路在主流GPU平台都能落地。
1. 为什么“先手写、再调参”这套老玩法开始不够用了
1.1 kernel优化的核心矛盾:硬件峰值很高,实际可达性能很低
一块主流加速卡的FP16/FP32理论算力,动不动就是几十到上百TFLOPS,但真实跑一个算子,能达到理论峰值60%已经很不错了。差距主要出在访存、shared memory带宽、寄存器分配、指令调度这些细节上。一个GEMM,不同tile大小和循环展开策略,性能可以差3到5倍。这不是玄学,而是几十个参数组合成的超大搜索空间里,只有少数区域是性能最优的。
手工调优最大的问题是:一个人一天能试十几种配置就算快了。尤其是当算子从简单GEMM变成融合Attention、多级流水线GEMM的时候,参数之间的耦合关系非常复杂。比如把tile从64x64改成128x128,表面上只是数字变了,实际上会影响shared memory占用、寄存器压力、乃至L2缓存的行为。靠人肉经验去猜,很容易陷入“调了两天反而变慢了”的困境。
自动搜索工具一直存在,早期的TVM Ansor、Halide autoscheduler都属于这一类。它们用成本模型加启发式搜索,在数学上很漂亮,但落地时经常被现实问题卡住。成本模型不可能精确预测现代芯片上所有硬件的复杂交互,搜出来最好的配置,可能要经过几万次编译实验才能确认。更麻烦的是,每次换一个新的kernel结构,搜索框架往往要从头再来。
LLM代理带来的改变,不是替代了自动搜索,而是把自动搜索的“搜索起点”和“搜索策略”都提升了一个量级。它能直接读懂源码,理解优化意图,基于对常见pattern的记忆生成一组高度可用的候选代码。这意味着搜索空间可以从几万个配置,迅速缩小到几十个高质量候选,再由测量环节做最终裁决。
1.2 LLM代理“转正”的关键:从代码生成走向优化闭环
以前LLM在kernel优化里的角色很尴尬。生成一段能编译的CUDA代码不难,难的是这段代码性能不一定好。一个不了解硬件细节的模型,可能会生成一个完全合法的kernel,但它的shared memory布局恰好触发大量bank conflict,性能甚至不如朴素实现。
真正让LLM代理“转正”的转折点是:它开始被嵌入到“生成→编译→测量→反馈→再生成”的闭环中,而不是停留在“一次生成”的静态模式。在这个闭环里,模型可以通过编译错误信息修正语法问题,通过profiler数据调整tile尺寸,通过性能回退记录避免再次踩坑。它不再是一个代码生成器,而是一个会自我反省的优化agent。
从实际效果来看,一个中等规模的LLM代理系统,在处理常见算子的效率上,已经可以逼近甚至超过有两年经验的kernel工程师。这里面最关键的原因,不是模型“懂”硬件,而是模型掌握了大量“什么样的改动会产生什么样的性能变化”的经验知识,这些知识被海量代码和性能报告中锻炼过。当一个agent拿到一个陌生kernel时,它能快速联想到类似的已知案例,提出几个在历史数据中大概率有效的优化方向。
在2026下半年这个时间节点上,很多团队已经把agent纳入正式开发流程,不再要求“人工审查每一行生成代码”,而是通过自动化测试和性能回归来控制风险。这种做法在半年之前还不被主流接受,如今已是大规模落地的前提条件。
2. 一个能用的代理优化闭环是怎么设计的
2.1 闭环的五个核心阶段
我这边跑通的一个典型闭环,分成五个阶段:分析、提案、编译、测量、回溯。听起来简单,真正落地时每一步都有大量细节。
第一阶段是分析。代理拿到的是原始kernel源码、编译参数、目标硬件型号和性能约束。它要做的第一件事不是改代码,而是先判断瓶颈。这一步非常关键,如果连瓶颈都判断错,后面所有优化都是在浪费时间。常见的判断手段包括:读profiler报告里的关键指标,比如memory throughput、SM busy、指令issue率,或者直接看代码结构做启发式判断。很多成功的优化案例,都是从“我看了下这个kernel的访存比非常高,应该是访存受限”开始的。
第二阶段是提案。基于分析结论,代理给出候选修改方案。这里需要设计一个“动作空间”,也就是它被允许改什么、不能改什么。约束太松,模型可能把整个kernel重写成另一种算法,虽然偶尔有惊喜,但大多数时候是灾难;约束太紧,又失去了agent优化的意义。我个人的习惯是:默认允许修改循环结构、tile参数、向量化宽度、shared memory布局,但禁止改变计算核心的数学语义,除非单独开启实验开关。
第三阶段是编译。每次生成的代码都要经过完整编译链。编译不是简单跑一下nvcc就结束,还需要做正确性校验。代理经常会生成一些逻辑上正确但数值上有细微差异的代码,比如调整了中间计算顺序,导致浮点误差累积方式变化。严格来说,这种情况需要设置一个误差容忍阈值来判断是否通过,而不是直接要求逐位一致。
第四阶段是测量。性能测量不能只跑一次。现代GPU的频率波动、缓存状态、时钟控制策略都会影响测量结果。一个稳定可用的测量模块,至少要跑三轮以上,用中位数或最小值作为最终指标。这个模块还要同时记录非功能指标,比如编译时长、显存占用、是否触发大量寄存器溢出,这些信息会作为下一轮的反馈信号。
第五阶段是回溯。这是LLM代理和传统autotuner最大的不同点。传统搜索器只会记录“哪个配置好”,而LLM代理能把性能数据和代码改动建立因果关联。它可以输出类似“这一轮把double buffer加上之后,shared memory利用率上升,但寄存器压力也变大,下一轮可以尝试把unroll factor从4降到2”这样的推理。这种具有迁移能力的经验,对于同一系列后续算子的优化非常有用。
2.2 为什么说它是一等优化手段,而不是辅助工具
判断一个工具是否是一等优化手段,有一个很简单的标准:当它不参与时,流程是否会发生本质性改变。
以前,自动搜索器也参与优化,但它在流程中是“末位测试器”。工程团队先手写几十个候选,搜索器只负责从中挑一个。LLM代理的参与方式完全不同。它可以自己决定改哪里、怎么改、验证什么假设。人类工程师的角色从“写代码的人”变成了“定义目标和检查结果的人”。
这种转变带来的直接收益是效率。一个曾经要花两周才能完成的算子优化任务,在代理闭环下通常只需要两三天。其中大部分时间还是花在等待编译和测量上,真正的“思考”时间反而很短。这让团队可以把人力投入到更难的问题上,比如设计新的融合策略、重构kernel架构,而不是反复调一个循环展开的系数。
不过,一等手段也意味着工程要求更高。一个纯人工优化的流程里,人本身就是容错机制,代码出了问题工程师会看一眼就知道怎么回事。代理没有这种“直觉”,所以系统必须有强大的错误隔离机制。比如,当一个候选实现性能回退超过阈值时,系统必须自动拒绝并回退到基线,不能因为它看起来“更聪明”就破例。再比如,当model突然生成一段完全不同的算法结构时,系统需要有结构性对比能力,发现它偏离了原有优化方向,及时打断。
3. 实战案例拆解:GEMM和Attention两类典型算子
3.1 GEMM调优案例:从40%峰值到78%峰值的全过程
某个模拟项目X中的GEMM算子,初版性能只有理论峰值的40%。代码是一个相对朴素的共享内存tile版本,tile大小64x64,每个线程计算4x4的微tile,循环展开因子是2。瓶颈很明显:访存比偏低,线程的空闲等待时间过长。
代理分析后的第一轮提案是:把tile改为128x128,每个线程计算8x8的微tile,循环展开因子改为4,同时增加double buffer。这些改动有明确的内在逻辑。tile变大能提高数据复用率,让每个加载到shared memory的float4被更多次计算使用;微tile增大能减少循环开销;double buffer解决的是流水线stall问题,让下一次加载和当前计算重叠。
编译测量之后发现,性能确实提升了,从40%到了63%,但出现了寄存器溢出警告。代理捕捉到这个信号后,在第二轮把unroll factor降到2,并重新调整了register blocking策略,没有选择更大的4x8微tile,而是保持8x4,以降低每个线程的寄存器占用。这一轮实测来到了71%。
第三轮没有改大结构,而是处理了一个很隐蔽的问题:shared memory bank conflict。之前所有线程加载A矩阵时,列地址恰好落在同一个bank中。代理从上一轮的性能计数器数据中发现了较高的bank conflict占比,于是采用了pad one column的经典做法,即在shared memory数组中每行多留一个float的空间。这个看似很小的改动,让性能从71%提升到了78%。
最终版本的配置总结如下:
| 参数 | 初版 | 最终版 |
|---|---|---|
| tile大小 | 64x64 | 128x128 |
| 微tile | 4x4 | 8x4 |
| 循环展开 | 2 | 4 |
| double buffer | 无 | 有 |
| shared memory padding | 无 | 每行+1 |
| 实测峰值占比 | 约40% | 约78% |
这个案例最有价值的地方在于:每一个改动都不是凭空冒出来的,而是从上一个测量结果中推导出来的。寄存器溢出导致性能瓶颈,那就降低寄存器压力;bank conflict比例高,那就padding。这种“根据反馈调整假设”的行为模式,正是agent优化最值得关注的能力。
3.2 Attention类算子:数值稳定性与性能的同步优化
GEMM这类规则计算是agent比较容易上手的,Attention算子更有挑战性。它涉及的不只是性能,还有数值稳定性。当序列长度很大时,直接对整个矩阵做softmax会溢出,必须采用分块计算加在线rescaling的方式。
模拟项目X中的某个多头注意力kernel,初始版本用的是全量softmax,性能尚可,但当序列长度超过4096时,显存占用攀升很快,而且在大数值场景下出现了NaN。代理的分析结论是:需要改成分块softmax,在每块计算时维护running max和running sum,同时把QK^T的计算融合进来,减少中间矩阵的写回。
第一轮提案用的是8x64的分块,块内计算QK^T后立即做局部softmax,再与V相乘后累加。编译通过,性能比原版提升约20%,数值稳定性也正常了。但profiler显示了一个明显问题:矩阵V的shared memory加载存在bank conflict,因为访问模式是线程连续取V的不同行。
第二轮,代理调整了shared memory中的存储布局,把V矩阵做了transpose存储,这样一来线程读取时正好命中连续的bank,conflict消除。这一轮让性能在数值稳定的基础上再提升12%。
这里有一个值得说道的点:代理在数值稳定性上的处理非常谨慎。它没有强行把整个softmax计算改成更高精度的float64,而是通过分块加rescaling来保持float32精度。这说明一个好的agent不仅知道“怎么改代码”,还知道“在什么约束下改代码”。在实际使用时,这类安全行为的判断通常通过结果检测来引导,但真正落地时还是需要有经验的工程师在初期给予正确的反馈信号。
4. 工具链搭建与关键参数配置经验
4.1 我常用的工具链组合和选型理由
搭一个可用的LLM代理优化环境,说难不难,说简单也不简单。难在如何把各种工具平滑地衔接起来。我这里给出一个经过验证的配置组合,供参考。
| 组件 | 选型 | 用途与理由 |
|---|---|---|
| 模型后端 | 本地部署的开源推理服务 | 保证编译和测量环境的稳定性,且源码数据不出内网 |
| 编译工具链 | nvcc + 固定版本CUDA工具包 | 与线上部署环境完全一致,避免编译差异 |
| 性能采样 | nsight compute + 自定义解析脚本 | 提取关键指标,转成可直接喂给模型的文本摘要 |
| 正确性校验 | 自研数值对比模块 | 支持浮点误差容忍,支持多case批量比对 |
| 编排控制 | 自研Python状态机 | 控制多轮迭代,负责中断、回滚、记录日志 |
| 缓存和版本管理 | Git + 编译缓存 | 每个候选版本的源码都可回溯,实验可复现 |
这里面最容易出问题的,是模型后端和编译环境之间的分离。先让模型生成候选代码,然后把代码提交到另一个隔离的解释器或者编译器容器中执行,是更安全也更稳定的做法。模型服务的任何抖动,都不应该直接影响编译和测量任务的核心流程。
另一个值得推荐的实践是为每个候选版本建立一个专属目录,把源码、编译日志、性能数据、模型反馈整合在一起。这样一旦后续出现问题,或者需要分析一个性能异常点时,可以快速回溯到当时的上下文,而不至于对着一个编译缓存干瞪眼。
4.2 上下文窗口怎么管、迭代预算怎么定
LLM代理在优化场景中面临的第一个现实问题是上下文长度。一个复杂的kernel源码,加上编译日志,再加上profiler报告,可能轻松超过常见模型的大上下文窗口。但把所有内容不加选择地塞进去,会稀释模型的注意力,让它不知道该优先关注什么。
我常用的裁剪策略是分级反馈。第一轮只给源码摘要和最高频的前几个性能指标,比如访存吞吐、SM占用率、bank conflict计数。编译失败时,只给错误摘要的前几行加对应代码的上下文片段。测量通过后,再给出完整性能报告和相对上一版本的差异。这个策略的核心是“每次反馈只告诉它当前最该知道的信息”,避免信息过载导致决策漂移。
迭代预算方面,经验值是最多5到8轮。前两三轮通常会快速打开局面,第四轮到第六轮是做细微调优,到第七轮以后,大部分场景的收益已经很低,还常常出现为了1%性能引入极大复杂性的情况。因此我的默认配置是:最多6轮,如果达到预期性能目标则提前终止。如果6轮还没有明显进展,通常说明初始分析方向就是错的,需要人工介入重新审视瓶颈判断。
奖励信号的设计也很重要。直接以“最终性能数值”作为唯一反馈,模型容易陷入过拟合。更好的做法是把回报拆成多个维度:性能提升幅度、编译成功率、是否引入明显复杂度、数值稳定性是否满足约束。这样模型在每一步都能获得更平滑的信号,也更容易学会“保持安全和稳健地优化”,而不是只追求极端性能。
5. 常见问题与排查技巧实录
5.1 LLM生成的kernel编译失败怎么办
这是最常遇到的情况,尤其在项目初期。代理生成的代码出现编译错误,常见类型包括:shared memory数组越界、整数类型不匹配、使用了对当前架构不支持的intrinsic、模板参数推导失败。
处理这类问题,我总结了一套结构化反馈法。不要把完整的编译错误输出直接扔给模型,因为现代编译器的错误信息冗长而且带有大量上下文无关警告。更好的是先用脚本对错误日志做分类提取,只保留错误类型、出错文件名、行号和核心错误消息。然后把这些信息附上对应代码片段,再给模型一轮修正机会。
5.2 性能倒挂:新代码比旧代码更慢
模型生成的新版本,在没改变算法结构的前提下,性能反而不如上一版本。这通常由几个原因导致:寄存器溢出导致local memory traffic急剧上升、循环展开过度导致指令缓存失效、shared memory占用过高导致occupancy下降。
解决的关键是保留完整的基线测量数据。不是简单地保存一个“上次最好的性能数字”,而是把对应的配置、源码、profiler快照都存档。当出现性能倒挂时,通过对比profiler指标,很快就能定位是哪个资源项爆了。代理也能通过反馈看到具体差异,从而在下一次提案中主动避开类似的改动。
5.3 优化效果不稳定:同一份代码每次测结果差异较大
造成测量结果不稳定的原因很多,包括GPU频率自适应、线程块调度随机性、大页内存分配差异、以及系统中的其他并发任务。解决方法是采用稳健的测量协议:每个版本连续测至少5次,取中位数作为最终性能;所有测量过程在同一个容器内顺序执行,避免并发干扰;必要时锁定GPU时钟频率。
另外要提醒的是,浮点非确定性也会让性能测量出现微小差异。比如某些编译器优化会把表达式改写成FMA指令,导致结果多处一位小数差异。对于数值校验系统,需要明确设定误差容忍范围,通常用相对误差阈值来控制。如果一个通过校验的实现偶尔出现不通过,不要着急去怀疑代码正确性,先检查是不是校验逻辑本身对浮点位移过度敏感。
最后分享一个我在实际运行中体会最深的点。LLM代理优化得好不好,很多时候不取决于模型多聪明,而取决于你为它搭建的反馈闭环有多清晰。清晰的定义是:代理做出的每一个改动,系统都能给出一个明确、及时、可归因的响应。响应好,它就知道这个方向可以继续;响应差,它能立刻更换策略。这就像带新人工程师,与其每次都手把手告诉它改哪里,不如先把工程规范和反馈机制搭好,它自己就会越做越顺。