NVIDIA用多智能体流水线把 24 个TileGym算子从Python译到Rust
TileGym全部 24 个公开算子已移植到cuTile Rust,平均性能达到cuTile Python的 99.5%;翻译靠一个多智能体技能完成,每个阶段都以机器可校验的结论收尾。
AI解读:NVIDIA把TileGym里用cuTile Python和Triton-TileIR写的 24 个公开算子全部移植到了cuTile Rust,平均性能是cuTile Python的 99.5%。这批算子总共约 40 个GPU内核,覆盖从逐元素运算到flash-attention decode、MLA、MoE模型。
之所以能翻译,是因为cuTile Python、Triton-TileIR和cuTile Rust三个前端最终都汇到同一个CUDA Tile IR(cuda_tile dialect),再交给同一个tileiras编译器生成GPU二进制。所以移植不是重新优化,而是把同一个tile程序用更安全的宿主语言重新表达一遍。
真正干活的是一个叫tilegym-converting-python-to-rust的智能体技能,它已放进TileGym仓库,你可以拿它翻译自己的内核。它把顶层智能体变成只管路由的调度器,分析、写内核、写宿主和FFI、跑基准分别由不同的子智能体完成,彼此只通过固定schema的产物沟通。
翻译中最实际的一道坎是特化方式不同:cuTile Python在调用时隐式特化,没走到的分支会被丢掉;Rust要求内核签名里显式声明每个特化,未走到的分支也得通过类型检查。再比如Python里一行ct.load(..., padding_mode=NEG_INF),在Rust里要拆成建视图和加载两步。
链路里最关键的校验手段是IR diff:先dump参考内核的Tile IR,再dump翻译后内核的Tile IR做比对。一个看起来对、但TMA加载代价提示写错或丢掉整除属性的翻译,可能蒙混过功能测试,却会在测试没覆盖的场景出错,IR比对能把这类结构性问题直接挑出来。
这套设计还有一个前提:一次完整转换要消耗百万级token,而把内核在没有任何宿主代码时就单独验证通过,后面的失败才能找到明确的责任方。性能校验也规定几何平均必须落在参考实现的 5% 以内。实际上,并非所有已交付内核都已迁到文章展示的完整安全API,只为精确复现参考Tile IR的地方仍在用未检查的接口。
NVIDIA技术博客披露,其团队已把TileGym中全部 24 个公开算子从cuTile Python和Triton-TileIR移植到cuTile Rust,平均性能达到cuTile Python的 99.5%。这 24 个算子共包含约 40 个GPU内核,覆盖逐元素运算到flash-attention decode、Multi-head Latent Attention(MLA)和mixture-of-experts(MoE)模型。部分算子需要多个内核变体。
翻译工作由一个名为tilegym-converting-python-to-rust的智能体技能完成,该技能已随NVIDIA/TileGym GitHub仓库发布,用户可将其用于自己的内核。博客作者为Tanya Lenz。
三个前端共用同一套CUDA Tile IR
cuTile Rust(cutile-rs)是面向Rust语言的tile级GPU内核编写系统,把Rust的所有权模型扩展到tile级GPU内核:把可变输出切成互不重叠的部分,并在内核启动之间保持宿主侧所有权约定;需要更底层控制时可在局部退出这套约束,直接执行Tile IR操作。
cuTile Python、Triton-TileIR和cuTile Rust是同一套IR(CUDA Tile IR的cuda_tile dialect)之上的三个前端,三者都输入同一个tileiras编译器,由它做tile级优化并产出GPU二进制。移植因此不是重新优化,而是把同一个tile程序用更安全的宿主语言重新表达,底下仍是同一个编译器和同一套性能模型。
这种共享也让翻译变得可校验:忠实移植应当复现参考内核的IR结构,即相同的内存操作族、tile形状和归约。可以分别dump参考内核与翻译后内核的Tile IR做diff,在跑任何测试之前就完成检查。
- cuTile Python ─┐
- Triton-TileIR ─┼─► CUDA Tile IR(cuda_tile dialect)─► tileiras ─► cubin
- cuTile Rust ─┘
隐式JIT与显式特化的差距
博客称翻译的主要挑战在于:cuTile Python的JIT编译会在调用时隐式特化每个内核,而Rust要求在核函数签名中显式声明每一处特化。多数翻译工作就是把Python源码中隐式的东西写清楚。
Rust前端还有两个要点。其一,Rust源码是提前编译的:tile形状和元素类型由rustc检查;crate内嵌内核AST,首次启动时运行时用具体const-generic值特化并编译出cubin(之后缓存)。GPU二进制仍是JIT编译,但隐式性消失了——签名里没声明的就不会被特化。其二,在TileGym中cuTile Rust只是另一个后端,tilegym.set_backend("cutile-rs") 会把同一套算子API路由到Rust内核。
- 未走到的if ct.Constant分支:Python在编译前丢弃;Rust两个分支都必须通过类型检查,一个Python内核可能变成多个结构性Rust入口,例如layer_norm因分支改变tile秩而拆成 2-D nchw和 1-D w1两个入口。
- dtype组合:Python按需编译任意组合;Rust FFI在固定的符号/dtype表上分发,支持一个dtype就是一次显式ABI扩展,共享表覆盖f32/f16/bf16/i32/i64/f8e5m2/f8e4m3fn。
- 输入校验:Python的JIT类型系统就是校验;越过C ABI后没有安全网,错误的stride是静默内存损坏而非异常,因此有两层防御——Python包装层的语义检查和FFI之后的ABI检查(空指针/dtype/设备),并返回具名错误码。
翻译如何穿过C ABI
博客以softmax内核为例做逐行对照:Python侧ct.load(..., padding_mode=NEG_INF) 在Rust里变成两步——先用make_partition_view(..., padding::NegInf, ...) 建一个带安全分区视图,再调用Partition::load;ct.bid(0) 对应get_tile_block_id();keepdims=True的归约变成reduce_* 后跟显式reshape和broadcast。中间值都是Tile类型。Rust编译出的算子清单与Python原版一致,由IR diff确认:一次视图加载、按正确轴的reduce_max/reduce_sum、一次视图存储,两端都是TMA。
博客指出,并非TileGym中所有已交付内核都用了这种完全安全的风格:每个移植都必须精确复现参考内核的Tile IR,当只有未检查的API能复现时,移植就使用该API;团队仍在把这些内核迁到安全接口。
Rust应用可直接依赖cutile crate、包含内核模块并启动入口,全程不涉及FFI。C ABI层的作用较窄:把这些内核接入TileGym的Python调度和测试框架(以及任何非Rust宿主)。每个算子从聚合的cdylib(整个库一个libcutile_kernels.so)导出一个C符号,张量以ptr、ndim、shape[]、strides[] 组成的描述符结构体在Rust与Python之间传递。
Python侧用cffi绑定该符号,签名出自唯一的cdef字符串,包装层只做校验。启动器不拷贝、不分配、不取得所有权:borrow_f32把PyTorch设备指针包进ManuallyDrop,Rust能在不释放自己并不拥有的内存的前提下拿到张量,内核在调用方的CUDA stream上异步启动。对PyTorch而言这看起来就像普通扩展算子。
cuTile Rust在TileGym内编译是惰性的:后端跟踪源码新旧,编辑任何kernel.rs(或crate清单)都会让下一次调用在分发前自动重建共享库,开发-测试循环里不需要显式cargo build。
多智能体怎么分工
博客称该技能的一个核心设计是:加载它的智能体本身不做任何工程工作。读取SKILL.md后,顶层智能体变成纯粹的编排者,唯一权限是路由;实际工作由它派生的专门子智能体完成,每个子智能体只加载自己阶段需要的参考文档。
分析者负责解决“JIT把规格藏起来”的问题:参考内核经DSL降到cuda_tile dialect后,常量已被固化、未走到的分支消失、启动参数留在宿主代码里。它还会选基线——一个算子常有cuTile Python和Triton-TileIR两个实现,分析者分别跑基准,按结构变体选择更快的一个作为移植必须对齐的参考,并在任何Rust代码存在之前dump该参考的Tile IR,写出analysis.json,内含变体、常量、dtype、容差、启动网格、autotune空间和所选基线的机器可读规格。
内核编写者只产出kernel.rs,被禁止碰宿主代码,因此失败可归因;它要把翻译差距落到技能里的 49 条编码规则,并做两次证明:先用不带FFI、不带Python的Rust内流水线测试做功能验证,再对分析者的参考dump做IR自检。宿主/FFI构建者让已验证的内核可从TileGym调用(C-ABI启动器加Python包装),负责正确性检查,在全部dtype和形状上跑算子的真实TileGym测试套件,只有拿到ALL_PASS才解锁基准测试。
性能校验者运行CUPTI基准协议(测设备时间、在同一GPU上按配置与参考配对),要求几何平均落在参考的 5% 以内;它的职责不是优化,而是诚实测量。另有两个只在失败时加入的专家:IR diff分析者在正确性测试失败或基准异常时启动,逐个变体比对参考与生成的Tile IR并分类分歧,关键是区分翻译错误(带具体修复退回内核编写者)与内核改动无法修复的上游编译器bug;残余性能调查者针对某些输入形状上正确但偏慢的内核,在设备侧(内存操作族、代码生成)和宿主侧(启动配置、autotune、包装逻辑)定位差距并出具报告。
博客给出这套分工的两个理由:一次完整转换要消耗百万级token;把内核在任何宿主代码存在之前单独证明通过,后续失败才有可处理的责任方。三项做法支撑这一拆分:子智能体只通过固定schema的产物沟通,不通过对话;每个阶段以机器可校验的结论收尾,编排者不读文字描述只按结论路由;共享的cuda_tile dialect让IR diff成为校验骨干,既用于内核编写者测试前的自检,也用于失败时IR diff分析者的深度比对,从而拒绝按错轴归约、丢失掩码这类结构错误但看似合理的翻译。
编排循环与重试上限
转换运行是一台小型状态机,编排者自身指令放在很精简的SKILL.md里。预检由scripts/preflight.sh验证环境变量和工具链路径,非零退出即终止运行:环境不可用,再多智能体努力也补不上缺失的编译器。
每个子智能体都用同一个模板派生,提示里只有两个元素:阶段Step-0文件清单(自己的指令文件加该阶段需要的参考文档)和此前阶段产物的具体路径。编排者从不把指令粘进提示,每个子智能体读自己的文件。每个子智能体返回必须以一个字面代码块和一行VERDICT: 结束;编排者检查块内退出码,然后纯按结论路由,绝不解读书面描述推断修复;格式错误的返回只获得一次同智能体重派,不会升级。
博客称缺失owner tag本身算失败,编排者会停下而不是猜,因为把宿主故障误路由到内核阶段会浪费一整次重试。各阶段有硬性派生上限(一次分析、两次内核编写者尝试、两次宿主构建者尝试、一次诊断、两次基准运行、一次可选性能补跑);一次运行要么在预算内收敛,要么带着磁盘上的诊断停止。只有在路由到达完成之后,validate_kernel.sh才会按 17 文件输出契约复核所有阶段的报告、IR dump、正确性和性能日志。