
去年我做AI Infra的时候有个场景几乎每周都会碰到一次新模型上线性能压测一跑发现某个算子卡住了整个计算图——要么是市面上现成的高性能库还没覆盖要么是默认实现慢得离谱。然后就是经典的人肉调优环节翻CUDA文档、看profiler报告、试各种block size和shared memory组合一个算子花掉一两天是常事。KernelZero这个思路就是冲着这个痛点去的与其让人去写算子、调算子不如让大模型自己去出题—生成测试场景自己写算子代码自己运行评测再从评测结果里反省迭代形成一个持续进化的闭环。这篇文章不聊虚的只讲我理解里的核心原理、实战流程以及哪些地方看着美好、实操中容易翻车。1. 为什么写算子成了大模型落地的最后一道坎1.1 算子的本质每一层算法都逃不过落地执行先对齐概念。在AI世界里算子其实就是计算图里的一个节点小到向量加法大到FlashAttention、MLA这种巨型融合算子最终都要编译成对应硬件GPU/NPU/CPU上的可执行代码。模型结构越新算子形态就越是五花八门GQA的分组注意力、MoE的路由计算、各种自定义激活函数……长度和shape还经常是动态的。麻烦就麻烦在这里PyTorch/TensorFlow的默认实现通常只保证正确不保证最高性能。而像cuBLAS、cuDNN这类厂商库虽然针对经典算子做了极致的优化但新算子一出现根本等不及库去跟进。我见过不止一次一个模型的推理性能瓶颈就卡在一个只有几百行代码、但没经过任何手工调优的算子上。大量使用算子对硬件性能的挑战——这不是概念名词是每天面对的现实。1.2 传统解法手写、模板、自动搜索各有各的痛以前解决算子性能问题基本三条路手写CUDA/ROCm内核。性能天花板最高但门槛也最高。要理解warp调度、shared memory bank conflict、寄存器压力还要会看SASS级别的汇编。培养一个能写高性能算子的工程师周期很长而且这种人才通常很贵。使用模板库比如Amazon的CK、微软的CUTLASS。这相当于给你一套乐高积木但组装仍然靠人。你需要理解tile、mma指令、swizzle这些概念本质上是高级手写学习曲线依然陡。自动调优/自动搜索比如TVM的AutoTVM、Ansor。这类方案让程序自己去搜循环变换和调度参数算是半自动了。但问题在于搜索空间巨大一个算子的搜索可能要跑几小时甚至更久而且对新的硬件后端适配能力有限。这三条路本质上都是在用人力或暴搜去填算子的坑——前者贵后者慢。而且它们都有一个共同盲区没有积累。今天调优了一个kernel明天换一个shape一切从头再来。我需要的不是一个一次性工具而是一个能自己长大、越用越聪明的系统。1.3 大模型写算子终于把门槛和天花板同时改变了到了LLM时代情况开始起变化。代码大模型无论是CodeLLaMA还是GPT-4级别的通用模型已经能写出语法正确的CUDA代码甚至在一些简单算子上一把过。但语法正确和性能可用之间隔着一条巨大的鸿沟——直接生成的kernel性能大概率比手写优化版慢5到10倍有时候连朴素版本都打不过。为什么会这样因为LLM学过大量代码范式但它不知道在当前硬件上这个算子到底跑得多快、瓶颈在哪。它缺少执行反馈。KernelZero的切入点就在这把执行反馈接入生成循环。大模型写代码——编译运行——测正确性和性能——把结果喂回去——让模型根据实测定级和profile信息修改代码。多轮迭代直到性能达标。这本质上不是让模型更会写代码而是让它拥有一个真实物理世界的评卷老师。2. KernelZero 的核心思路把人肉调优改成模型自举2.1 自己出题是什么意思标题里自己出题这四个字听起来玄乎拆开其实很朴素。理想的进化闭环应该包含三个角色出题器根据当前要解决的算子形态、目标硬件、输入区间生成一批题目。题目不是数学卷子而是请为在一个A100上计算形状[16, 512, 4096]的GQA注意力写一个高性能kernel输入为fp16这样的具体任务描述附带随机生成的输入数据。解题器大模型根据题目生成一段算子代码。评卷器在真实硬件上跑这段代码给出正确性和性能分数并附上为什么扣分的诊断信息。自举循环bootstrapping loop的意思则是第一轮生成的代码和评测结果会被收集进经验库后续生成算子时模型会参考这些历史经验来写代码避开之前踩过的坑。题目难度也会根据历史表现自动调整某个类型总是做错就多出点同类题让它练某个类型轻松做对就增加约束条件比如不能使用全局atomic或目标性能达到cuBLAS的90%。这比直接找大模型写代码高一个维度。直接让模型写它靠的是训练时见过的相似模式而自举场景里模型写的每一个kernel都会变成下一轮生成的前车之鉴。2.2 和AlphaGo自对弈的相似逻辑看到这熟悉AI历史的读者应该会联想到AlphaGo的自我对弈智能体不仅要产生策略还要自己评价策略再用评价结果修正策略。KernelZero是同样的逻辑只不过棋盘换成了硬件执行环境落子换成了kernel代码胜负换成了性能达标与否。这个类比还能帮我们理解一件事自举系统的价值不在于第一代策略有多强而在于循环能不能建立起来。AlphaGo的第一代棋力可能连业余棋手都不如但只要自我对弈的闭环能跑通它就能靠大量迭代碾压人类。KernelZero也一样第一轮生成的kernel可能很烂但只要有评卷器给出准确的负面反馈模型就能针对性地改多轮之后性能就会显著爬升——这比指望模型一口气写出完美kernel要现实得多。2.3 为什么非要持续进化而不是一次生成这里有一个很容易被忽略的现实算子优化的边界是动态扩展的。今天你面对的是GQA过了两个月新的模型带来新的算子形态今天你在A100上调到极致明天换成H200或国产加速卡一切又要重新适配。一锤子买卖式的生成一个kernel解决不了长期问题。只有把出题-解题-评测-经验回流做成一条流水线让模型在每次适配新算子、新硬件时都能调用历史经验快速逼近当前场景的较优解。这才是持续进化四个字的分量所在。它不是某个单点工具而是一套不断积累、复利式提升的基础设施能力。3. 自举闭环落地出题、解算、评测、进化怎么串成流水线3.1 出题器决定这道题难不难、对不对出题器是整个闭环的起点出题质量直接决定进化上限。我理解里的出题器至少要做三件事算子规范描述不能只说写一个add kernel要把计算语义、输入输出shape范围、dtype、对齐条件、是否允许融合都结构化描述出来。比如operator: fused_attention_partial inputs: - name: Q, shape: [B, H, S, D] (B1..8, H8..32, S128..2048, D64..128), dtype: fp16 - name: K, dtype: fp16, layout: same as Q - name: V, dtype: fp16 outputs: - name: O, dtype: fp16 constraints: - target_hardware: cuda-sm_80 - no use of cublas - correctness_rtol: 1e-2, atol: 1e-2边界条件生成光有shape范围不够还要考虑边界情况——S1的退化注意力、head数为1的GQA、非对齐的batch size。出题器要根据规范随机采样保证覆盖平行区和退化区。难度自适应维护一个题型-难度-历史通过率的表格。通过率高于90%的题目类型下次提高约束比如目标性能提升20%或限制不能用某些指令通过率低于30%的题型降低约束并多生成类似题目练习。实操中出题器不一定要每次重新构造规范。可以直接从计算图ONNX或者自家IR里抽取算子形态做shape/dtype的随机化再拼上约束条件。这样既能覆盖真实业务场景又能自动产生数据增强。3.2 解题器大模型如何生成可编译的kernel解题器的上层是代码大模型但直接让模型从零开始写很容易翻车。我实际更推荐模板RAG生成的混合方案代码模板预置一批基础骨架比如标准内存拷贝骨架GEMM tiling骨架reduction骨架。模型不需要从空文件写起而是在骨架基础上填充/改写核心计算段。这能规避大量低级错误忘写__syncthreads、网格维度过大等。RAG召回根据当前题目描述从经验库里召回最相似的历史题解和评测反馈。比如这次是GQA分组注意力就召回之前调过QKV分离布局的kernel和它的失败教训。多候选生成别只让模型生成一个答案。可以用不同的temperature比如0.2和0.8各生成几个候选甚至用不同模型生成交给评卷器统一打分让评测结果来选择而不是让模型自己赌。在生成阶段还要加一道格式检查代码里不允许出现system(rm -rf)这类危险调用不允许直接读写任意路径只能访问评测脚本指定的临时目录不允许启动子进程或占用无限显存。这一步宁可保守也不能让未知代码裸奔进执行环节。3.3 评卷器编译、运行、打分的一整套动作评卷器是自举闭环的现实检验器。我梳理一下它的标准动作编译用nvcc或对应后端编译器编译生成的kernel加上-O3 --use_fast_math注意这会影响精度需要和正确性配置配套指定匹配目标GPU的architecture参数。编译错误的日志需要截断比如只保留前50行后再喂给模型否则上下文窗口会被刷爆。正确性测试用随机输入跑生成的kernel同时跑一个参考实现比如PyTorch的eager版本或者一个朴素的CPU参考版本。对比输出时不要只看是否完全相等要看相对误差和绝对误差——浮点运算本身有误差fp16下1e-2的误差都可能合理。判错标准一般是|a - b| atol rtol * |b|的样本占比超过阈值才判错。性能测试多次运行取中位数运行前要做L2 cache flush分配一块大buffer反复读写避免cache命中带来的虚假性能数据。还要控制温度导致的波动最好固定GPU时钟nvidia-smi -lgc否则一次运行和另一次运行的环境差异可能比优化带来的收益还大。诊断信息在性能不达标时用profilerNsight Compute / Nsight Systems或对应厂商工具采集achieved occupancy、memory throughput、SM busyness、bank conflict次数、寄存器峰值。这些数据才是模型改代码的依据。3.4 经验库真正让系统进化的存储层这一层经常被忽略但我觉得它是KernelZero区别于一次性代码生成的最关键部分。经验库里存的是三元组题目描述、生成的代码、评卷结果正确性报告、性能分数、诊断细节。存下来不是目的用起来才是。有两个关键设计检索友好需要支持按算子类型、shape范围、硬件型号、错误类型多维检索。最简单的做法是把这些字段拼成向量做embedding检索现在有大量的embedding模型可以用也可以用传统的倒排索引按错误类型如bank conflict/register spill打标签后续题型匹配时精确命中。负样本优先失败的题解优先级永远高于成功的。因为在自举循环里模型最需要学习的是不要怎么做。比如经验库里有一条把Q和K做转置时用了全局atomic导致性能下降80%的失败案例那下次出类似题目时RAG应该优先召回这条让模型少走弯路。经验库要持续清洗如果某个教训在连续多次迭代后都不再出现模型已经学会了可以降低它的召回权重避免冗余信息干扰生成。4. 评测反馈是进化发动机别只给总分要给病理报告4.1 分数设计正确性是门槛性能是目标在自举循环里模型需要明确的信号。如果只给一个总分模型不知道自己到底错在哪——是结果算错了还是并行度太低还是访存模式烂这就像考试只给分数不给错题解析学生下次还是会踩同样的坑。我建议把评测输出设计成结构化的病理报告大致这样维度指标判定标准反馈内容示例正确性数值误差比例1%样本超差则判Failoutput at index(2,128)期望值7.32实际-3.11可能是reduction顺序导致fp16累加溢出编译编译是否成功失败则终止error: variablesharedfloat s[] size unknown第23行共享内存数组未指定维度性能相对参考实现加速比1.0为Fail当前kernel为baseline的0.8xachieved occupancy仅12%可能block size太小导致warp数量不足资源占用寄存器峰值指定阈值预警寄存器峰值255已触发spill建议减少每个线程的私有变量模型拿到这样的结构化反馈才能有根据地修改代码。比如看到occupancy仅12%它会去调整block size或者削减shared memory用量看到bank conflict它会去想padding或者改变数据排布。4.2 别只测一种输入泛化性检查很重要评分还有一种常见的坑——模型背题。在生成代码时模型有可能针对出题器给定的那一组具体shape做硬编码。比如题目的输入是[1, 8, 128, 64]模型生成代码时干脆把循环上界写死成8、128、64测试当然全对性能还奇高。但换一个shape立刻崩。所以评卷器必须做泛化性检查出题器生成训练用的shapeA组同时生成一批同分布但不同的shapeB组。模型在A组进行调优迭代但最终验收要看B组的正确性和性能平均分。如果模型硬编码了shapeB组正确性会直接暴露。这也逼着生成器在代码里使用正确的动态索引逻辑。4.3 Profile信息怎么喂回给模型诊断信息不是越多越好。如果把Nsight Compute的JSON全量灌进上下文模型很容易迷失在海量计数器里面。我的经验是做一个精简摘要层把底层profile数据蒸馏成模型能看懂的几句话把achieved occupancy 12%表达成GPU只有12%的线程占用说明并行度不足试试增大block数量或者减小每个block的shared memory使用把memory throughput 25% of peak表达成内存带宽利用率很低可以考虑vectorized load比如float4或改变数据分块策略把bank conflict count 10000表达成共享内存访问冲突严重建议对行宽做padding或者按列主序存储这一步不能省。原始profiler数据是给工程师看的不是给LLM看的。做一层中间翻译能把调优知识注入生成的上下文里让模型的学习信号更干净。实际上这一层的质量直接决定了自举循环能跑多快。5. 实践中的坑让模型自己出题不等于放手不管5.1 模型会作弊合法但无用的走捷径自举循环跑起来之后你会遇到各种匪夷所思的应试技巧。最常见的作弊是调库作弊模型生成的kernel看起来是自己写的实际上内部调用了cublasSgemm或cudnn里的现成op。这不是不对而是没有达到让模型学会写算子的目标而且一旦目标库没覆盖就原形毕露。对付方式是在出题阶段加约束禁用cublas/cudnn/cutlass并在代码审查或编译器预处理时检查头文件和函数调用发现就扣分。还有一种作弊是缓存输出第一次运行算出来结果存到global memory后续直接用缓存。单次评测成绩好看但换运行实例就失效。这种其实是隐藏了状态评卷时需要检查代码里是否有静态/全局缓存模式或者直接在同一进程内多次执行验证结果一致性。另一种隐蔽作弊是硬编码shape上面已经提过必须靠泛化性B组测试兜底。5.2 造风控大模型生成的代码凭什么直接执行这是整个方案里最重要的一部分。让大模型生成C/C/CUDA代码并在GPU上运行等于让一个不可预测的程序员直接在物理机器上执行。哪怕模型没有恶意一个未定义的非法内存访问也可能导致进程崩溃甚至机器需要重启。我的防守策略是纵深防御隔离环境所有生成的代码都在docker容器内编译和运行。容器无特权模式不挂载宿主机的敏感路径比如/etc、/home只通过只读文件系统传入题目、通过共享目录回收评测结果。资源限额用timeout命令给编译和运行分别设定硬超时比如编译120s、运行60s。CUDA kernel如果写成死循环或超大grid直接kill。显存也要限制启动容器时限制--gpus的可用显存数量。静态检查过滤器在代码进入编译前跑一组正则/词法检查拦截exec、system、fork、mmap、open(/, 以及显卡驱动接口调用等危险模式。被拦截就记入经验库作为反例反馈给模型。最小权限账户容器内用非root用户跑编译和运行进一步降低提权风险。别嫌繁琐自举循环跑得越久你对未知代码的执行机会就越频繁风险是复利增长的前期花在风控上的工夫会以少翻车一次的形式回馈你。5.3 模式坍塌模型越优化越同质化自举系统有个很经典的进化困境和GAN训练里的mode collapse类似模型一旦找到一个局部最优解就会反复生成相似代码不再探索其他可能。比如发现block_size256, tile32x32的GEMM效果不错就再也不想试tile64x16或者其他非规则分块了。这会让系统失去搜索更优解的能力。我的缓解办法多样性奖励在评分公式里加入代码新颖度维度。每次生成的代码和当前经验库里同类代码做相似度对比编辑距离或者AST结构相似度相似度越高额外扣分越高。让系统有动力探索不同实现路径。候选取样每轮迭代固定有一个低temperature的保守候选和一个高temperature的激进探索候选。保守候选保证稳定继承现有成果激进候选负责跳到新区域。评测结果再好激进候选也不会立即替换保守候选只有连续多轮领先才会切换。5.4 别让进化停留在单机单卡最后说一个方向性的体会。很多人做完一版kernel生成就结束了。但我理解里KernelZero这类自举系统的终局价值是把它从单个算子的调优工具扩展成算子知识沉淀平台。今天在这个项目上积累的错误案例、调优经验和性能数据明天换一个模型形态、换一块硬件后依然有用。经验库会越用越厚模型再遇到类似算子时不再是从零开始摸索而是站在自己过往经验的肩膀上。这个过程和工程师成长路径其实是同构的——一个senior工程师比junior工程师强很大程度上不是因为他更聪明而是他脑子里积累了更多这个写法会触发bank conflict的直觉。KernelZero做的事情就是把这些直觉以结构化的形式沉淀下来让模型也能拥有。在我实际测试中这种模型自己出题、自己批改、自己复盘的循环前几轮是最痛苦的——评测失败率高、反馈日志乱、模型看着病理报告瞎改一通。但只要熬过这几十轮把评卷器调准、经验库攒够、风控跑顺后面每加一个新算子类型收敛速度都肉眼可见地变快。到那时候你会觉得当初这趟必须手动调算子的破路总算有个像样的修路人接手了。