2026年下半年,我聊得最多的技术话题,不是某张新显卡的峰值算力,而是GPU算子的kernel优化方式变了天。在过去很长一段时间里,算子优化就是“程序员拿着profile跑一遍,盯着热点函数,手写汇编级别的调整”。但今年我们的一个跨平台推理项目把LLM代理正式接进了优化闭环,让它承担从瓶颈定位、候选生成到自动评测迭代的完整工作。这篇文章想复盘这次实践的思路、架构和真实踩坑记录,给同样在优化GPU算子的团队一个参考。
这个方案解决的痛点是:算子优化的专家经验高度依赖个人,且无法规模化复制。GPU架构更新一轮,旧经验就要打一次折。而LLM代理能以极低的边际成本同时探索几十种优化路径,把性能数据反馈给模型,形成快速迭代。它不是来替代内核工程师的聊天机器人,而是作为优化流水线中的“一等公民”——代理可以自动读代码、读profile、生成多种候选实现、发起编译运行测试、再基于结果改写方案。我们验证下来,一批中等复杂度的融合算子,优化周期从一周压缩到一两天,而且很多方案是团队里没人想过的。
这篇复盘适合三类人:正在优化自研AI框架算子性能的工程师,想引入AI辅助工具但不知道怎么设计闭环的架构师,以及对GPU底层优化感兴趣但找不到合适切入点的学习者。下面直接进入正题。
1. 为什么算子优化到了必须引入LLM代理的节点
1.1 传统算子优化流程的瓶颈
传统的kernel优化流程,绝大多数团队都差不多:先跑一遍nsight或厂商的profile工具,找到热点核函数;然后由有经验的工程师读一下生成的汇编代码,尝试调整线程块大小、循环展开、向量化宽度、shared memory布局等参数;改完之后再跑一次benchmark,用数据验证效果。这个过程看起来清晰,实际执行起来处处是坑。
第一个问题是热点识别不等于瓶颈归因。profile告诉你核函数占用了多少时间,但它不会告诉你是因为访存带宽饱和、计算吞吐不够、还是线程同步开销过大。一个kernel慢,往往不是单一原因,比如某个矩阵乘法算子,既可能是global memory访问未对齐,也可能是L2 cache命中率太低,还可能是warp调度已经出现严重阻塞。人工去逐项排除,成本极高。
第二个问题在于优化空间是组合爆炸。一个普通的矩阵乘法算子,tile大小有几十种选择,向量化宽度和维度可以互换,double buffering要不要用,shared memory的padding加多少,这些都是不同维度的决策。组合数量轻松超过几千种。一个人一天能手工尝试十几个版本已经很不错,而且即使试出来一个不错的结果,也不敢保证不是局部最优。
第三个问题是经验不可迁移。换一个GPU架构,原本最优的block size和访存策略可能变成最差方案。我们在优化某个序列模型的融合算子时,把在上一代硬件上验证过的最优参数原样搬到新平台,结果性能直接掉了40%。这种事靠记忆和文档根本管不住。
所以2026年我们面临的现状是:模型越来越大,算子越来越长,底层硬件迭代越来越快,而人工优化能力是线性增长的。这个剪刀差已经大到让人无法忽视。
1.2 LLM代理的定位:不是替代编译器,而是嵌入优化闭环
很多人第一次听到“LLM代理优化kernel”,会以为就是让GPT写一段CUDA代码然后直接替换。这是最常见的误解。我们的实践表明,单纯让LLM生成代码对生产级算子是远远不够的,因为它不了解你的实际数据规模、访问模式、架构特性和性能约束。必须把LLM放进“感知-生成-评测-反馈”的闭环里,让它在每一步都能看到真实运行结果。
\
所以“一等优化手段”这个词,我的理解是:LLM代理不再是一个辅助提词工具,而是优化系统里的正式成员。它拥有调用profiler的权限,能读取性能计数器结果,能向编译沙箱提交代码并等待结果,能根据失败日志自动修正。它和人类工程师一样,在同样的信息输入下进行决策。
我们当时设计了一个最朴素的闭环:给代理一段kernel源码和profile摘要,让它提出一个优化假设,生成一个新版本;系统自动在目标硬件上编译、跑正确性测试、收集耗时;再把“假设-代码-实测结果”作为历史上下文重新喂给代理,让它提出下一版。这个循环跑了几轮之后,agent产的版本往往就能进入候选池。它有推理能力,有工具调用能力,也有记忆能力,能记上一版的性能缺陷,这跟AutoTVM那种靠预设模板做黑盒搜索的思路完全不同。
1.3 什么样的团队适合这套方案
不是所有团队都应该立刻上LLM代理。我们内部讨论过适用范围,总结下来有三个关键条件。
第一,算子体量很大,但单个算子的性能还没有被顶到硬件极限。比如大模型推理场景里有大量融合算子,可能只需要达到硬件峰值的60%就能满足在线latency目标。传统方案需要写大量模板和调参脚本,而LLM代理可以比“半自动模板搜索”覆盖更广的方案空间。
第二,团队已经有完整的profiling和benchmark基础设施。如果连跑数都不可靠,那代理反馈的一堆性能数据全是噪音,反而会把模型带偏。我们是在已经有稳定基准测试框架之后才接入代理的。
第三,组织能接受“agent建议、人类审查”的协作模式。LLM会犯错,生成一些看起来合理但实际上会崩或性能很差的代码。如果团队没有能力做代码审查和硬件回归,那不建议直接自动化跑生产环境。
2. LLM代理优化算子的整体架构设计
2.1 从“静态代码审查”到“假设空间搜索”
传统编译器优化本质上是静态分析加规则变换,LLM代理的加入让优化过程变成了一个有先验知识的“假设空间搜索”。比如某算子访存密集,代理会提“这个kernel应该用tiling降低对global memory的重复访问”,然后顺着这个假设生成多版本代码。这比传统编译器更灵活,也比AutoTVM更聪明。
为了让代理的空间搜索不至于失控,我们给每个优化任务限定了候选数量,比如每轮最多生成8个变体。代理在生成变体时,同时会附带一段优化理由。然后评测系统会把8个变体全部跑一遍,将性能结果和正确性结果一起返回。下一轮,代理能基于哪些变体有效、哪些无效做进一步修改。
这样循环的收敛速度比想象中快。我们在一个融合算子上面跑了三轮,实际只花了两个多小时,代理已经把tile size从8x8试到64x64,向量化宽度也从float试到了float4,还尝试了几种不同的shared memory padding方案。如果没有代理,让工程师手动枚举这些组合可能要一整天。
但这里有一个设计关键:不能把“生成候选”全交给LLM的想象力,而是要给它硬件约束条件。比如我们会在提示里显式写出每个kernel的shared memory上限、寄存器数量上限、线程块最大维度等。这样能显著减少生成非法代码的概率。
2.2 核心模块:代码理解、候选生成、自动评测、反馈迭代
一个能落地的LLM代理优化系统,我们拆成了五个模块:上下文收集器、瓶颈分析器、候选生成器、沙箱评测器、反馈聚合器。
上下文收集器负责拉取kernel源码、相关注释、profile结果、调用方信息和数据shape示例。这些信息不是全堆给代理的,而是按优先级截断,避免上下文过长导致模型忽略关键约束。我们发现在profile摘要里,最重要的指标是访存吞吐、计算吞吐、occupancy占比、以及是否存在warp divergence。
瓶颈分析器可以是一个规则引擎,也可以让代理自己分析。但我们的经验是混合最可靠:先用规则引擎把“是不是memory bound”这种问题用计数器判断掉,再把规则引擎的结论连同原始数据给代理,作为附加先验。这样能大幅降低代理把计算密集型算子误判为访存瓶颈的概率。
候选生成器并不总是从头生成kernel。它有一组“动作原语”,比如“将global内存访问改为向量化加载”、“引入shared memory tiling”、“拆分block的x和y维度”、“改变数据布局”等。代理在生成新代码时先选择动作原语,再结合具体代码做修改。这个设计让生成结果更可控,也更容易在后续按变量做回放。
沙箱评测器是系统中最重要的安全边界。因为LLM生成的kernel可能有越界访问、死循环、空指针等严重问题。我们在独立进程里运行每个候选,限制内存使用量和执行时间。一旦超过阈值直接kill,并记录失败类型。正确性验证则通过把输出和参考实现做逐元素对比,允许一定浮点误差。
反馈聚合器会把每个候选的编译状态、正确性结果、耗时、占用率等信息整理成结构化文本,附上性能排名。下一次代理请求时携带这些信息,而不是让代理自己翻日志。我们发现这个环节能明显提升迭代质量。
2.3 工程化落地:我们需要屏蔽什么,暴露什么
工程化落地最大的难点不是模型能力,而是怎么把信息暴露给代理、怎么把约束嵌入流程。
我们选择屏蔽底层编译细节,代理只需要生成符合特定接口的kernel函数,剩下的编译参数和runtime调度由框架统一处理。这样代理不会因为忘记某个编译头文件而反复失败。同时,我们把硬件性能counters映射成一组语义标签:memory throughput、compute throughput、latency bottleneck、occupancy limit。代理看到的不是原始寄存器值,而是这些语义标签。
另外,我们在评测环节强制把“失败日志”也作为反馈的一部分。很多代理在第一次尝试失败后,不知道错误原因,就会陷入同一个死循环。失败类型包括编译错误、默认值越界、bank conflict导致的性能下降,这些信息可以让代理下一步修改更有针对性。
下面是一个实验中的简化流程,供参考:
- 收集源kernel + profile摘要,组成初始任务上下文。
- 代理产出N个候选kernel,并附带每个候选的优化思路。
- 沙箱编译运行,得到正确性和性能数据。
- 系统汇总前三名和后三名的实现特征。
- 将排名和特征反馈给代理,进入下一轮迭代。
我们在实际中常跑四轮左右,之后性能提升就趋于饱和。继续多轮的成本上升很快,收益却微乎其微,所以一般四轮后就交给工程师做最终代码审查和合并。
3. 一个具体案例:融合注意力算子的优化过程
3.1 优化目标与基线
拿我们去年底接手的一个融合算子来举例,这个算子在推理链路中出现频率极高:输入一个序列特征张量,先和一个权重矩阵做矩阵乘法,然后加偏置,再过ReLU,最后输出给下一层。我们姑且叫它“融合线性层”。当时在不做任何优化的朴素kernel上,单个算子耗时约2.1毫秒,占用整层推理时间超过30%,很明显是头部瓶颈。
第一步是让profiler跑出基线性能指标:该kernel的occupancy只有不到25%,访存吞吐只用了理论带宽的三分之一,计算吞吐更低。这通常意味着kernel既没把访存利用好,也没有足够多并行线程来隐藏延迟。代理第一轮看到这些数据后,给出的假设很直接:“这是一个memory-bound算子,核心问题是global memory重复读取和线程数量不足。”
基于这个假设,代理生成了三个候选:第一个把输出矩阵按照32x32分块,使用shared memory缓存输入分块;第二个在tiling基础上增加循环展开;第三个则尝试把线程块分布改为以列方向为主。我们在沙箱跑了全部三个版本,结果第一个提升明显,第三个反而比基线还慢。原因是当前数据的列数较少,列方向划分导致很多线程空转。
3.2 LLM代理的提示与决策轨迹
第二轮我们给代理的反馈里比较了第一轮三个候选的耗时和瓶颈标签。代理很快意识到当前最大问题是bank conflict。因为tiling版本里,所有线程的shared memory访问模式出现了严重的冲突。于是它提出了一个非常简单但有效的修改:在shared memory数组的每行末尾多分配一个float作为padding。这个改动让有效带宽提升了近一倍。
给大家看一下我们当时设计的部分提示模板,大致长这样:
你正在优化一个GPU kernel。以下是当前kernel源码、profile摘要和上一轮候选结果的排名。 约束条件: - 最大线程数:1024 - 共享内存上限:48KB - 必须保持输出精度与参考实现误差小于1e-3 - 不要改变函数签名 上一轮性能排名: 1. tiling_v1: 0.93ms 2. vector_v2: 1.18ms 3. naive_v3: 2.05ms 请分析为什么tiling_v1表现最好,并提出下一个优化假设。注意检查shared memory访问是否存在bank conflict。代理回复的决策轨迹大概可以概括为三步:第一步读取上一轮排名,第二步对比代码差异,第三步提出新的修改说明。这个“先分析,再生成”的顺序很重要。如果直接让代理写代码,它容易堆砌看起来很高级但没经过分析的方案。
第三轮代理提出把tiling尺寸从32x32改成64x16,并用vectorized load 一次性读入4个连续float。理由也很清晰:当前数据矩阵的宽度比较小,64x16的分块能更好地匹配访存连续性和线程数量。这个版本最终跑到了0.64毫秒。相比基线2.1毫秒,已经提升了3.3倍。
3.3 评测数据与收益分析
我们整理了一下三轮优化的数据对比。基线耗时约2.1ms,整体吞吐大约9.8 TFLOPS;第一轮tiling版本降到0.93ms,吞吐提升到约22.4 TFLOPS;第二轮加padding解决bank conflict之后,耗时进一步降到0.71ms,吞吐约29.3 TFLOPS;第三轮调整tile尺寸和向量化后,最终耗时0.64ms,吞吐约32.5 TFLOPS。
| 版本 | 关键改动 | 耗时(ms) | 估算吞吐(TFLOPS) | 加速比 |
|---|---|---|---|---|
| 基线 | 朴素实现 | 2.10 | 9.8 | 1.0x |
| v1 | 32x32全缓存tiling | 0.93 | 22.4 | 2.26x |
| v2 | shared memory padding | 0.71 | 29.3 | 2.96x |
| v3 | 64x16 tile + 向量化 | 0.64 | 32.5 | 3.28x |
这个案例说明LLM代理并非“随便跑跑碰运气”。它每一轮的修改都对应一个可解释的假设,而评测数据能快速验证或推翻假设。这种闭环模式比纯粹让工程师凭经验猜要稳定得多。
当然,我们也不会盲信代理。最终合入代码前,工程师仍然会读一遍v3的实现,确认没有隐藏的边缘case,比如处理末尾block时不会越界、向量化读入不会访问超出矩阵边界的地址等。人是最终守门员,这句话任何时候都不过时。
4. 常见问题与避坑心得
4.1 幻觉代码与安全约束
LLM代理最大的坑是它可能一本正经地生成不存在的函数,或者设定一个错误的同步逻辑。我们在早期迭代里遇到过模型写了一个“syncthreads”放在if分支里,导致死锁。这种问题在代码审查时一眼能看到,但如果不做沙箱隔离,直接在生产环境跑,后果非常严重。
我的建议是:用现有框架里的kernel模板来限制生成范围。不要把整个kernel完全交给模型从零编写,而是让它基于已有的kernel做局部替换。比如限定只能修改计算核心内部循环,不能碰spacial边界判断、不能移除同步原语。从工程安全角度,这能拦住80%以上的严重错误。
另外,编译错误日志一定要原样回传给代理。我们见过代理在编译失败后,没有看到报错信息,结果连续三次生成同一个错误代码。把错误信息截断到最近几行的技巧,比给完整日志更有效。因为完整日志可能几千行,模型反而容易被无关警告带偏。
4.2 硬件平台差异与可移植性
LLM代理在一个硬件平台上训出来的最优方案,换一个架构可能完全无效。比如在架构A上,将矩阵分块为64x32和向量化宽度4是合理的;但架构B的shared memory更小、向量宽度支持更大,这套方案反而退化。
我们在跨平台验证中踩过一次坑,代理在本地平台生成的版本跑到另一家人工智能加速卡上,性能直接降低了50%。后来我们给代理的任务描述里加了明确的硬件标识,并要求它兼顾两个平台的共性优化。更重要的是,评测沙箱同时挂了两类硬件的环境,代理迭代时会被同时要求在这两类硬件上运行,性能得分取两者综合。
如果你不想维护复杂的跨硬件评测,至少要在提示词中要求代理避免强绑定的汇编级优化。
4.3 “代理”会取代内核工程师吗
不少朋友问我,LLM代理这么强,是不是以后不需要专门做kernel优化的人了。我的看法是,短期完全不会,但角色的确会变化。代理解决的是“从低效版本到较优版本”的搜索问题,它很难独立完成“从零设计一个新算子的最佳分解方式”这种需要系统思考的任务。
例如,某些新出现的模型结构里包含了动态shape的张量操作,代理生成kernel时往往无法预判运行时的shape变化范围,因此会假设固定shape来实现最佳性能。真到了生产环境,一个batch大小变化就让tiling失效。这种场景,代理很难设计出具备自适应策略的kernel。
未来内核工程师更像是优化代理的“教练”和“守门员”:定义搜索空间,设计评测指标,审查最终方案。而不是把时间花在重复枚举不同版本上。我们团队现在代码合并的效率提升了不止一倍,因为很多候选版本已经经过评测,工程师只需要做最后一道质量关。
4.4 实操中总结的关键清单
最后给一份我们自己在实操中沉淀下来的清单,欢迎直接抄作业。
- 先建好稳定基准,再启动LLM代理优化。没有基准,所有性能数据都是噪音。
- 代理每轮最多生成8个候选,不要贪多。候选太多会导致评测时间太长,反馈信息过载。
- 给代理的profile摘要里,一定要包含“访存吞吐/计算吞吐/occupancy”这三个字段。
- 生成代码前,强制检查共享内存和缓存边界,减少非法访存概率。
- 设置沙箱超时时间,比如每个候选最多执行2秒,超过即视为失败。
- 把上一轮结果排名作为下一轮prompt的前缀,而不是让它自己读历史日志。
- 任何候选在合并前都要做一个“最坏shape”回归测试,防止只对单一shape有效。
- 不要完全依赖数值误差自动对比,有时候误差在阈值内但输出分布不对,让工程师肉眼看一下。
我个人在实际操作中最大的体会是:LLM代理带来的加速效果,不取决于模型多聪明,而取决于你把优化问题封装的有多好。信息给的越精确,反馈循环建立的越清晰,代理的产出质量就越稳定。它就像一个学习极快的组员,你得告诉它目标、边界以及上一轮哪里失败了,而不是只说一句“帮我优化一下”。
2026下半年这个节点,各个团队的差距可能不在于谁的GPU更多,而在于谁能更高效地把LLM代理嵌入到算子优化的工程流程里。这也算是我们这次实践最想留给同行的一句话。