IR 与融合
这一部分讲算子主线在 GPU 设备上多出来的那一段。Flex 这样的即时后端在调用当场算完;cubecl 后端默认套着 Fusion<CubeBackend>(burn-cubecl 的默认 feature 打开 fusion,见怎么读这一卷),每个 op 只登记一条描述就返回,等到必须出结果时再决定怎么执行。一次 matmul 已经看到 matmul 那一行只花几微秒、内核在 into_data 时才在 DSU 线程上提交;这里把中间那段拆开:登记的是什么数据,攒在哪里,什么时候必须执行,哪些 op 能合成一个内核,合出来的内核长什么样。
涉及三个 crate:burn-ir(8 311 行,数据格式)、burn-fusion(2.5 万行,含测试;装饰器、流、搜索)、burn-cubecl-fusion(16 660 行,cubecl 上的融合内核)。所有实验在 M2 Max 的 Metal 上跑,同一份代码分别用默认 feature 和 default-features = false 编译,对比开不开融合;CUDA、ROCm、Vulkan 走同一套代码,只读了源码。
- burn-ir:
OperationIr是两层枚举,外层按张量种类和类别分 14 种、带运行期 dtype,内层是具体 op;TensorIr的NotInit/ReadOnly/ReadWrite告诉执行器能不能就地写、是不是最后一次读;HandleContainer按 id 管句柄。Fusion、Router、Remote、Capture 都用这份格式,同一个 relu 在 Fusion 和 Capture 眼里不一样。 - Fusion 装饰器:
register当场建好输出张量、把登记任务交给设备的DSU线程;任务先攒在 32 槽的通道缓冲区里,服务端按StreamId(默认每线程一条)分流排队;读数据、sync、跨线程共享、融合器关闭时执行,没人读的结果不提交内核;FusionTensor的引用计数决定 IR 里的状态;错误挂在张量上,读时才交出。同一串 8 个逐元素 op,8 个内核变 1 个。 - 融合搜索:每条流的
Processor先查执行计划缓存,没有就探索;op 换成相对形式(id 重编号、维度值换编号、标量换占位符),按共享张量分块,每块里五个融合器竞争、按省下的读写打分;方案连同触发条件缓存起来,换标量、换同结构的形状都能命中;BURN_FUSION_LOG能看到每一步。 - 生成融合内核:逐元素块记成
FuseTrace,所有逐元素融合共用一个#[cube]函数,op 列表作为 comptime 配置展开成一串寄存器运算;实测的 MSL 里中间结果不进显存、结果就地写回;向量宽度按末维选;matmul 和按维度的归约把前后的逐元素 op 收进 cubek 内核;(a * b + c).exp()稳定后快约 1.8 倍。
flowchart TB tensorop["Tensor op 经 Dispatch"] --> fop["Fusion 的 op 实现"] fop --> ir["OperationIr 和 TensorIr"] ir --> chan["设备通道 32 槽"] chan --> srv["DSU 线程 FusionServer"] srv --> stream["按 StreamId 的队列"] stream --> search["融合搜索 查缓存或探索"] search --> fused["融合内核 burn-cubecl-fusion"] search --> unfused["逐个执行 CubeBackend"] fused --> cubecl["cubecl 编译 提交"] unfused --> cubecl
前两页在「登记」这一侧:数据长什么样、怎么到达、什么时候被迫执行。后两页在「执行」这一侧:怎么决定、决定之后生成什么。不融合的 op 怎么由 CubeBackend 执行、cubecl 怎么把内核编成 MSL 并提交、autotune 怎么挑 matmul 的变体,见 06 CubeCL 与 GPU。
💬 关联讨论
发起讨论加载中...