☰
TIRx Harness:Agentic GPU编程的开放编译器脚手架
2026/10/14 7:04:34 网站建设 项目流程

1. 为什么要有TIRx Harness——Agentic GPU编程的痛点

先把话说在前头:Agent写GPU代码这件事,单看demo很美好,真正跑起来通常是一地鸡毛。过去半年我反复在CUDA、Triton、TVM这几套体系里折腾AI Agent自动写kernel的流程,最深的体会是——模型不缺生成代码的能力,缺的是一个能把生成结果约束住、验证掉、反馈回去的工程框架。TIRx Harness就是冲着这个缺口去的。

先说清楚Agentic GPU Programming是什么。它不是拿ChatGPT问一句“给我写个GEMM kernel”然后复制粘贴,而是让Agent在一个闭环里工作:分析目标算子 → 生成候选实现 → 自动编译 → 自动跑正确性校验 → 自动做性能剖析 → 把失败信息喂回Agent让它修改 → 再编译再验证。这个循环听起来顺理成章,但每一环都有坑。

最大的坑在于“编译器中间表示”这个环节。Agent直接生成CUDA代码,语法错、类型错、越界错全都要靠编译器报错来发现,而nvcc的报错信息对于LLM来说非常不友好,Agent经常被一段模板实例化的报错绕晕。如果让Agent生成类似Triton这种高层语言,表达力又受限,很多融合算子和自定义数据布局写不出来。TIRx走的是另一条路:让Agent直接在一层开放的编译器IR上工作——TIRx。把这层IR作为Agent与硬件之间的“共同语言”,Agent产出TIRx代码,Harness负责把TIRx编译成目标代码、链接运行时、执行验证和剖析。

这就是TIRx Harness的核心定位:一个开放的、可插拔的编译器脚手架,专门为Agent循环设计。它解决的不是“某个kernel怎么写”的问题,而是“Agent写kernel这件事怎么工程化”的问题。适合谁用?三类人。第一类是搞MLSys的研究生,想在自己算子库上接入Agent辅助开发;第二类是Infra团队,需要一个可控的、能在CI里跑的GPU代码自动生成管道;第三类是像我这样喜欢折腾工具链的独立开发者,想给开源社区贡献一套Agent友好的编译框架。如果你只是想快速得到一个高性能kernel,那直接用现成算子库反而更快,TIRx Harness的价值在于长期迭代和覆盖长尾算子。

为什么强调“Open”?因为Agent编程最怕黑盒。如果中间的编译、验证、评分环节完全不可见,Agent一旦失败你根本不知道是代码问题、编译问题还是评分策略问题,整个循环就没法调试。开放意味着每一层都可以打印、可以替换、可以注入自定义pass,这是我能把它真正用起来的前提。

2. TIRx Harness的整体架构与核心设计

2.1 “Harness”在这里到底是什么意思

Harness这个词,搞过测试的人都很熟,test harness就是测试夹具。但TIRx里的Harness比测试夹具含义更重。它是一整套套在Agent外围的工程装置,包含几个角色:

  • 任务解析器:把自然语言或结构化描述的目标算子转成TIRx的spec,包括输入输出shape、dtype、layout、对齐要求、允许的融合策略。
  • 工具提供层:暴露给Agent的工具集合,每个工具是一段可执行动作,比如“编译这个TIRx模块”、“跑一波tvm.testing的数值对比”、“执行profiler并返回kernel耗时”。
  • 状态管理器:维护一轮迭代中的完整上下文——当前代码、编译日志、正确性结果、性能数据、历史修改记录。
  • 评分器:把正确性、性能、代码质量转成Agent可以优化的反馈信号。
  • 回归保护器:跑完一个算子之后,把最优版本固化进测试集,防止Agent后续改动把之前的好结果破坏掉。

这五个角色合在一起,才称得上Harness。只有工具没有状态管理,Agent就是一个无记忆的调用器;只有编译没有自动反馈,Agent就是在盲写。所以Harness的精髓在于把Agent当作一个外部优化器来对待,给它清晰的目标函数、可靠的评估信号、丰富的修改动作,然后让它在这个闭环里滚。

2.2 Agent与编译器之间的“对话协议”

Agent不能直接操控TVM内部的各种Pass和数据结构,那样太危险,一次错误的IR改写就可能让整个编译流程崩溃。TIRx Harness在Agent和编译器之间设置了一层薄薄的协议层,协议的核心是一组工具函数。

我设计这层协议的思路参照了OpenAI Function Calling的语义:Agent不是自由执行Python代码,而是只能请求工具。每个工具都有明确的输入输出schema,有副作用边界。比如“编译模块”工具接收一个dict,包含TIRx源码字符串和编译参数,返回编译是否成功、错误消息、生成的目标代码大小;而“跑性能剖析”工具则返回各阶段耗时。这样做的原因很朴素:一旦让Agent自由写脚本,它可能写出一个死循环,可能调用系统命令删掉你的临时目录,也可能一气之下把显存占满。限制动作空间,不是限制Agent能力,而是给循环加上可恢复的安全网。

TIRx代码本身也有严格的语法边界。它是一层兼容TIR语法风格、但做了大规模简化的IR。每个kernel必须显式声明grid、block、内存作用域,每个函数必须标明host还是device。这样设计是为了让Agent生成的代码具有可预测的结构。LLM生成代码最怕自由度过高,给它一个带有强制字段的模板,生成质量会稳定非常多。

我在项目里验证过一个数据:同样是让同一个模型生成矩阵乘kernel,自由编写CUDA的成功率大概五成左右,而通过TIRx模板生成再走Harness自动修复,三轮以内的成功率能到八成以上。这不是模型变聪明了,而是IR和工具的约束把它的出错空间压缩了。

2.3 核心模块:从源码到评测的可插拔链路

TIRx Harness的流水线是模块化的,每个阶段都可以单独替换。

Task Spec -> TIRx Codegen -> Compiler Passes -> Module Build -> Host Runner -> Correctness Check -> Profiler -> Report

TIRx Codegen模块负责把Agent的JSON结构或自然语言描述转成TIRx源码。Compiler Passes阶段内置了存储规划、线程绑定、向量化判断等基础pass,但默认保持保守,不做过激优化,因为Agent生成的是动态代码,编译器优化激进反而容易引入错误。Host Runner是一个轻量运行时,负责分配显存、拷贝输入、启动kernel、同步。Correctness Check用参考实现(比如PyTorch的CPU/GPU算子)做数值对比,支持rtol/atol自定义。Profiler则用CUDA event做计时,统计平均耗时和p95耗时。

整条链路最容易被忽视的是Report这一步。Agent在下一轮迭代中能看到的全部信息就是这份报告。报告必须结构化、精炼、可操作,不能把整个编译日志都倒给模型,否则上下文长度爆炸,模型也会被无效信息带偏。我会在后续章节详细讲报告怎么写。

3. 从零搭建TIRx Harness的实操路径

3.1 环境初始化与最小项目骨架

如果你想复现这套东西,我建议从最小可用版本开始,别一上来就追求完整功能。环境要求不复杂:一台带NVIDIA GPU的Linux机器,Python 3.10以上,装好PyTorch(作为参考实现和tensor的宿主),以及TIRx这个包本身。你还需要一个可用的LLM API——本地用vLLM跑开源模型也行,调闭源API也行,关键是接口要能让你传入自定义工具定义。

我习惯的项目骨架是这样:

tirx_harness/ agents/ llm_client.py # 模型调用封装 prompts.py # 系统提示词与工具说明书 harness/ spec.py # 任务描述数据结构 tools.py # 暴露给Agent的工具集 loop.py # 主循环控制 reporter.py # 结果报告格式化 runner/ # host runner与CUDA封装 tirx_runtime/ codegen.py # 从spec生成TIRx源码 passes.py # 编译器基础pass build.py # 编译与链接 tests/ regress/ # 回归测试集 cases/ # 算子任务用例

loop.py是主控,控制着Agent最多迭代几轮、每轮超时多久、评分低于多少停止。我一般设置最多10轮迭代,单轮工具调用总时长不超过5分钟,这样即使Agent跑偏,整个任务也不会拖太久。

启动一个任务只需要一个spec。

spec = { "op": "matmul", "inputs": { "A": {"shape": [1024, 1024], "dtype": "float32", "layout": "row_major"}, "B": {"shape": [1024, 1024], "dtype": "float32", "layout": "row_major"}, }, "outputs": { "C": {"shape": [1024, 1024], "dtype": "float32", "layout": "row_major"}, }, "constraints": { "grid_limit": 65535, "block_limit": 1024, "shared_mem_limit": 49152, "target_arch": "sm_80", }, }

这个spec本身也是一种接口约定。Agent看到它,就知道自己面对的是什么形状的算子、有哪些硬件约束不能碰。相比给一大段自然语言描述,结构化的spec对模型更友好,也不容易产生理解偏差。

3.2 工具定义与Agent可执行动作

工具集是整个Harness的“手动挡”。一开始不要给太多工具,五个以内最好。我的最小集是:

  1. generate_tirx:根据spec生成初始TIRx代码。
  2. build_and_check:编译TIRx模块,跑正确性验证,返回通过/失败及错误码。
  3. profile:对已通过验证的模块做性能剖析,返回耗时指标。
  4. edit_code:修改当前TIRx源码,需要传入diff或完整替换文本。
  5. get_history:查看此前所有版本的代码与评测结果。

每个工具的定义要让模型能看懂用途。工具说明写得好不好,直接决定Agent会不会乱调用。我的经验是:在工具描述里写清“什么时候用这个工具”和“什么时候不要用”,效果比只写功能要好。比如profile工具的描述我加了句“请确保代码已通过build_and_check后再调用本工具,否则剖析结果无意义”。

工具实现上要注意原子性。每次edit_code之后,当前代码版本号加一,所有历史版本都保留。这样即使Agent连续改出十版垃圾代码,你也能随时回退。很多Agent循环失败是因为没有版本管理,改坏了只能重来。TIRx Harness把版本管理内置了,这个设计在长迭代中非常救命。

3.3 主循环:如何让Agent在反馈中收敛

主循环的逻辑不算复杂,但细节决定成败。

async def run_tirx_agent(spec, max_rounds=10): state = TaskState(spec) for rnd in range(max_rounds): messages = build_messages(state) action = await llm_client.request_tool_call(messages, tools) if action is None: break result = dispatch_tool(action) state.apply(result) report = reporter.format(result) state.add_message(report) if state.is_solved_and_good_enough(): break return state.best_result()

这个循环里有个容易被忽视的细节:is_solved_and_good_enough不能只看正确性,还要看性能是否达到了基线。我的做法是,先跑一个粗糙的参考kernel(比如PyTorch原生算子)算一个baseline,Agent的结果至少要跑到baseline的60%才算合格,否则即使正确也继续迭代。这样Agent不会在一版正确但极慢的代码上躺平。

另一个细节是上下文管理。每轮报告如果太长,会把前几轮的历史压缩成摘要。我通常在系统提示词里要求模型:“之前的反馈仅作参考,重点分析最近一轮的profile数据与错误信息。”这能有效防止Agent被陈旧信息带偏。

3.4 最小可用的Prompts与函数调用配置

这里给一份我实际在用的Prompt骨架。系统提示词的核心是界定Agent的角色边界。

你是GPU Kernel优化专家。你的任务是分析算子规格,编写TIRx代码,并通过编译、数值验证和性能剖析逐步优化实现。 你需要遵守以下规则: 1. 每次只生成一个版本的代码,通过工具提交,不要猜测结果。 2. 严格使用结构化字段,不要修改spec中的shape和dtype。 3. 如果build_and_check失败,优先阅读编译错误信息,缩小问题范围。 4. 如果性能不达标,优先分析profile数据中的耗时分布,而不是盲目改写代码。 5. 所有代码必须适配sm_80架构。不要使用未在tools中提供的功能。

Agent的API调用配置也需要留意。如果你用的是OpenAI兼容接口,tools参数就是函数定义的列表,tool_choice设为"auto"。但我建议把第一个工具调用限定为generate_tirx,办法是在任务初始消息里明确要求:“首先调用generate_tirx生成初始版本。”这能避免Agent一上来就调用edit_code改一个不存在的文件。

4. Agent循环里的性能优化与收敛保障

4.1 正确性验证:不只看rtol与atol

很多人以为正确性验证就是np.allclose,但实际做kernel自动生成时,验证策略必须更细致。我总结了一套分层验证法。

第一层是shape与strided校验。输出tensor的shape、dtype、内存连续性必须和spec完全一致,否则后续验证全是空中楼阁。第二层是数值验证,rtol和atol要分开设置。对于float32的矩阵乘,我通常设rtol=1e-3, atol=1e-4,但如果是float16或者有累加误差大的算子,阈值要放宽到rtol=1e-2。第三层是边界样例验证,专门构造一些容易触发的数值边界,比如全零矩阵、全一矩阵、极大极小值混合。

第四层是随机多播验证。固定一个随机种子,跑多次输入,验证结果稳定。这一层能发现隐藏的未初始化内存和race condition。我遇到过不止一次:单次验证通过,换一批随机数就翻车,原因就是shared memory没有完全初始化或同步屏障缺失。

提示:Agent生成的kernel里,最常见的正确性问题是漏了__syncthreads。TIRx层会做保守的同步插入,但这只能兜底,不保证没有语义问题。验证时务必跑多种shape。

4.2 Profile报告的构造:让Agent“看见”瓶颈

性能剖析不是跑个计时就完了。原始耗时数据对Agent的指导价值很低,它需要知道慢在哪。我的做法是把一个kernel的时间拆成几段:kernel总耗时、内存传输时间、内核启动开销、计算段耗时、同步等待耗时。

TIRx Harness的profile工具返回的是这样一份精简报告:

kernel_total: 0.832 ms memcpy_h2d: 0.105 ms kernel_launch: 0.012 ms compute_region: 0.712 ms memcpy_d2h: 0.198 ms occupancy: 62.5% shared_mem_used: 24 KB register_used: 32

Agent看到这份报告之后,至少能判断瓶颈在计算还是访存。如果计算区域占大头,它应该去改算法或增加并行度;如果memcpy占大头,它应该想算子的融合或减少传输次数。没有这种结构化报告,Agent只能瞎猜,优化效率会很低。

我还会在报告里加一个“bottleneck_hint”字段,由代码分析器自动生成。比如检测到高shared memory占用和低occupancy时,hint会写“shared memory可能是瓶颈,考虑削减每个block的smem分配或调整block大小”。hint不一定完全准确,但它能给Agent一个优化方向,显著减少无效探索。

4.3 回归保护与最优版本固化

Agent循环最气人的一点:好不容易第7轮优化出一个好版本,第8轮一改又崩了,然后崩掉的版本可能因为某种原因被选为最终结果。所以TIRx Harness必须有“最优版本固化”机制。

我的实现是:每当出现一个通过正确性验证且性能是目前最好的版本,就立刻把它快照到best_results/目录。主循环结束的时候,直接取快照中的最优版本,而不是Agent的最后一版。这等于给循环装了一个“贪心选择器”,保证最终交付的代码不会比历史最优差。

回归保护也不复杂。每完成一个新算子,就把该算子的最优TIRx代码加入回归测试集。后续再跑别的算子或者同一算子换shape时,先跑回归集,确保没有意外破坏之前的结果。Agent编程的长期风险是“优化A算子导致B算子退化”,回归集是唯一的防线。

5. 常见问题与排查技巧实录

5.1 “Agent反复修改但性能不涨”怎么办

这是最常遇到的状况,模型在一版代码上反复微调,但profile数据就是没有明显变化。用前面提过的profile报告一查,通常会发现两个问题。

第一个问题是Agent在修改无关参数。比如它调整了grid的维度顺序,但实际访存模式没变,自然没有性能收益。应对办法是在prompt里明确要求“每次修改必须说明你预期的瓶颈变化,如果无法说明,不要提交”。这个约束能逼着Agent做有依据的修改。

第二个问题是Agent陷入了局部最优,它在一版已经不错的实现上做小改动,收益自然有限。我会在特定轮次强制做一次“跳变”:清空Agent当前代码,只保留spec,让它重新生成一个全新实现。这听起来很粗暴,但实际效果惊人,很多当前最优版本就是这么跳出来的。

5.2 编译错误信息太复杂,Agent看不懂怎么办

TVM底层的错误信息包含大量内部IR细节和C++模板堆栈,直接把原始日志给LLM,它会被淹没。我在Harness里做了一个错误信息压缩器,专门把编译错误映射到TIRx层的语义错误。

比如底层报错是“Check failed: (index >= 0) ...”,压缩器会识别这是越界索引,转成“错误类型:越界访问;建议检查循环边界和数组索引计算”。Agent看到后者,修改效率高得多。错误分类表是我手工维护的,目前覆盖了几十种常见错误类型,足够支撑大多数算子开发场景。

如果你不想维护完整的错误映射表,也有一个过渡方案:把报错信息交给一个单独的“错误翻译模型”先做摘要,再把摘要送入主Agent循环。实测这个中转方案能减少大约30%的无谓迭代。

5.3 显存不足与OOM的预防策略

GPU显存只有那么多,Agent自动生成的代码可能会在一次任务里反复分配大量中间tensor,甚至把显存挤爆。TIRx Harness的做法是在runner层加一个显存预算器,每次编译前先估算新kernel的显存占用,超过预算直接拒绝编译,并把预算信息写入报告。

显存预算的计算依赖shape和dtype,很简单,但很有效。我默认设置单个任务的显存上限为总显存的70%,留出30%给参考实现和传输缓冲。预防比恢复便宜得多——一旦OOM,Python进程可能直接崩掉,整个循环就白跑了。

5.4 把Harness扩展成自定义Domain的注意点

TIRx Harness的定位是开放框架,所以替换算子类型、调整编译Pass、切换模型后端都应该可行。但扩展时最需要小心的不是代码接口,而是“评测基准”。换了一个硬件架构(比如从NVIDIA换到AMD),评测基线全变了,之前固化的回归数据全部失效。我自己踩过一次坑:在A100上调好的kernel,换到消费级卡上优化,由于共享内存容量差异,之前的很多决策完全不适用。

因此,扩展的第一步永远是重新定义评测基线和约束条件,第二步才是改代码。

一些想说的经验

关于TIRx Harness,我最想提醒后来者的一句话是:Agent能不能写出好kernel,有一半取决于你给它的工具和反馈质量,而不是模型本身。把时间花在打磨反馈循环上,回报率远高于换一个更大的模型。网格搜索那种活交给Agent做,但方向判断、约束定义、阈值设定,这些还是要人工把关。

最后再分享一个实操小技巧:跑Agent循环之前,先用一个极小的算子(比如两个256x256矩阵相乘)把整套链路通一遍,确认工具调用、编译、验证、报告每个环节都正常,再上大规模算子。别问我为什么知道要这么做——我头三次大规模实验全是栽在链路本身没通这个问题上的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询