怎么读这一卷
主线位置:三条主线的总览。本页把算子主线、求导主线、训练主线各列一遍,标出每一步落在哪个函数上、哪一页讲它;再讲 Cargo feature 怎么决定这三条线上有哪些层:
burn的 feature 一路传到burn-dispatch,它的构建脚本发出backend_enabled、cube_backend、executing_backend三个 cfg。「一次 matmul」和「一个训练步」用实验把算子主线和训练主线各走一遍。
flowchart TB
start[从这里开始] --> q{读这一卷为了什么}
q -->|会用| use1[01 全景]
use1 --> use2["02 张量 和 07 模块"]
q -->|读懂实现| all[00 到 11 按顺序]
q -->|只关心 GPU| gpu["05 融合 再到 06 CubeCL"]
q -->|自己写一个| hands["04 自动微分 再到 11 动手"]
锁定版本
| 项 | 本卷 | 说明 |
|---|---|---|
| burn 工作区 | 0.22.1,tag v0.22.1,commit 907ab111a(2026-10-20) | 工作区所有 crate 同一个版本号([workspace.package] version = "0.22.1");burn 的 rust-version 是 1.95,edition 2024 |
| cubecl | 0.11.3,tag v0.11.3,commit b26114988(2026-10-22) | burn 工作区写的是 cubecl = "0.11.1",新项目解析到 0.11.3 |
| cubek | 0.3.1,tag v0.3.1,commit 245851cc(2026-10-20) | matmul、reduce、卷积、attention、随机数这些现成的 cube 内核 |
| 实验依赖里的 wgpu / gemm / serde / tracing | 30.0.1 / 0.19.0 / 1.0.229 / 0.1.44 | 锁文件里实际解析到的版本 |
| 上一版 | v0.22.0(2026-10-06)、v0.21.0(2026-05-07) | 0.22.1 是 0.22.0 之后的补丁版 |
本卷的版本组合不是挑出来的:一个新 crate 写 burn = { version = "=0.22.1", features = ["flex", "metal"] },cargo 解析出来就是这一组,依赖树里一共 396 个包。源码链接一律指向 tag:https://github.com/tracel-ai/burn/blob/v0.22.1/crates/...、https://github.com/tracel-ai/cubecl/blob/v0.11.3/crates/...、https://github.com/tracel-ai/cubek/blob/v0.3.1/crates/...。burn 仓库没有 CHANGELOG,合并都是 squash,提交标题末尾的 (#NNNN) 就是 PR 号,本卷的「冷知识」都从这里查。
实测平台是 M2 Max、macOS 27、rustc 1.99.0。GPU 只实测了 Metal(经 wgpu);CUDA、ROCm、Vulkan、WebGPU 只读源码,凡是只读了源码的地方都会写明。cpu 这个 feature(cubecl 的 CPU 运行时,经 MLIR / LLVM)本卷没有构建:它的构建脚本要联网下载 LLVM。
0.22 改了什么
0.22 是一次大改。#4717(2026-05-13,首次发布于 v0.22.0-pre.1)的标题是「[Breaking] Remove Tensor backend generic and add high-level Device struct」:
| 0.21 | 0.22 | |
|---|---|---|
| 张量类型 | Tensor<B, const D: usize, K = Float> | Tensor<const D: usize, K = Float> |
| 选后端 | 类型:type Backend = Autodiff<Wgpu> | 值:Device::wgpu(..).autodiff() |
| 模型 | struct Mlp<B: Backend>,字段 Linear<B> | struct Mlp,字段 Linear |
| 多后端并存 | Router<(Wgpu, Flex)> 这类组合类型 | 一个全局后端 Dispatch,按设备在运行期 match |
能不能对非 autodiff 后端调 backward | 编译期拒绝 | 运行期检查 |
0.21 的 README 写的是 type Backend = Autodiff<Wgpu>;,并说「it is impossible to make the mistake of calling backward on a model that runs on a backend that does not support autodiff」;0.22.1 的 README 改成「backward() checks graph participation at runtime」。用户代码里不再有后端类型参数,后端由张量所在的 Device 决定,所有张量都走 burn-dispatch 的 Dispatch。同一版里 burn-ndarray(#5344)和 burn-tch(#5525)标成弃用,CPU 主推 burn-flex。
网上的大量例子(以及 burn 自己的一些文档注释,比如 Dispatch 上的示例还写着 Tensor::<Dispatch, 2>)是旧写法。0.21 的写法在 0.22.1 下的报错(intro-old,只开 flex):
use burn::backend::Flex;
use burn::tensor::Tensor;
fn main() {
let device = Default::default();
let a = Tensor::<Flex, 2>::ones([2, 2], &device);
println!("{a}");
}
error[E0747]: type provided when a constant was expected
--> src/main.rs:6:20
|
6 | let a = Tensor::<Flex, 2>::ones([2, 2], &device);
| ^^^^
|
help: if this generic argument was intended as a const parameter, surround it with braces
|
6 | let a = Tensor::<{ Flex }, 2>::ones([2, 2], &device);
| + +
use burn::backend::Flex 本身仍然能编译,Flex 这个类型还在,只是不能再放进 Tensor 的参数里。rustc 的建议(加大括号)是错的方向。
三条主线
全卷沿三条线展开,每页开头都会标出本页在哪条主线上。
算子主线:一次 a.matmul(b)
flowchart TB user["Tensor matmul 泛型方法"] --> num["Float 的 Numeric matmul"] num --> disp["Dispatch float_matmul"] disp -->|设备是 autodiff| ad["Autodiff 记节点"] disp -->|Flex 设备| flex["Flex 的 gemm"] ad --> fus["Fusion 记 OperationIr"] disp -->|GPU 设备| fus fus -->|into_data 时| drain["融合流 drain"] drain --> cube["CubeBackend matmul"] cube --> tune["autotune 选 cubek 内核"] tune --> launch["cubecl 编译并提交"] launch --> read["异步读回"]
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. Tensor::matmul 先 TensorCheck::matmul 查形状,再调 K::matmul(self.primitive, other.primitive);泛型方法只做检查和形状计算 | burn-tensor/src/tensor/api/numeric.rs | Tensor 与 Kind、检查与报错 |
2. Float 的 Numeric::matmul 不泛型,只经手 BridgeTensor:拆出种类,两个都是 Float 就调 Dispatch::float_matmul | burn-tensor/src/bridge/ops/float.rs | BridgeTensor 与 *_impl |
3. Dispatch::float_matmul 的函数体只写 B::float_matmul(lhs, rhs),#[backend_dispatch] 把它展开成按 DispatchTensor 种类和设备的 match | burn-dispatch/src/ops/tensor.rs、macros.rs | Dispatch |
4. 设备是 .autodiff() 的,先进 Autodiff::float_matmul:登记一个 Matmul 反向步骤,再调内层后端 | burn-autodiff/src/ops/tensor.rs | 一个算子的反向 |
5. GPU 设备上内层是 Fusion<CubeBackend>:Fusion::float_matmul 把调用记成 OperationIr::Float(.., FloatOperationIr::Matmul(..)),GlobalFusionClient::register 送进流,立刻返回 | burn-fusion/src/ops/tensor.rs、client.rs | Fusion 装饰器、burn-ir |
6. 读数据时(into_data)FusionServer::drain_stream 把流里攒下的操作切块、能融合的生成融合内核,不能融合的逐个执行 | burn-fusion/src/server.rs、stream/、burn-cubecl-fusion | 融合搜索、生成融合内核 |
7. matmul 落到 CubeBackend::float_matmul → kernel::matmul::matmul → matmul_autotune,autotune 在 cubek 的一组 matmul 内核里挑最快的 | burn-cubecl/src/kernel/matmul/ | burn-cubecl 后端、autotune |
8. cubek 的内核是 #[cube] 函数,cubecl 把它展开成 IR、在 Metal 上编成 MSL,Client::launch 提交 | cubek cubek-matmul、cubecl cubecl-runtime、cubecl-wgpu | #[cube] 内核、运行时与 ComputeClient、wgpu 与 Metal |
9. into_data 内部是一个 async fn float_into_data,同步 API 用 cubecl 的 block_on(futures_lite)等它 | burn-cubecl/src/ops/tensor.rs、cubecl cubecl-environment/src/future/base.rs | Backend trait |
CPU:同一个调用在 Flex 设备上直接进 Flex::float_matmul → matmul::matmul → gemm crate,返回时已经算完 | burn-flex/src/ops/matmul.rs | Flex:CPU 后端 |
第 5 步和第 6 步的分界值得记住:在 GPU 上,a.matmul(b) 返回时什么都没算,内核是在之后的 into_data(或 sync)里、在 cubecl 起的另一个线程上提交的。实验见一次 matmul。
求导主线:一次 loss.backward()
flowchart TB dev["Device autodiff"] --> leaf["require_grad 的叶子"] leaf --> fwd["每个 op 登记 Backward 步骤"] fwd --> bw["Tensor backward"] bw --> server["AutodiffServer backward"] server --> topo["收集步骤 按序执行"] topo --> grads["Gradients 按节点 id"] grads --> gp["GradientsParams 按参数 id"]
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. Device::autodiff 把设备包成 DispatchDevice::Autodiff,在它上面建的浮点张量是 AutodiffTensor:内层原语加一个图节点 | burn-tensor/src/device.rs、burn-autodiff/src/tensor.rs | Autodiff 装饰器 |
2. 每个可求导的 op 定义一个实现 Backward 的步骤,prepare 之后由 OpsKind 分成 Tracked / UnTracked,只在有父节点要梯度时保存状态 | burn-autodiff/src/ops/ | 一个算子的反向 |
3. Tensor::backward → AutodiffServer::backward:从根节点收集步骤、按顺序执行,用完的节点清掉 | burn-autodiff/src/runtime/、graph/traversal.rs | backward 与 Gradients |
4. 梯度累加进 Gradients,按节点 id 存;x.grad(&grads) 按 id 取 | burn-autodiff/src/grads.rs | backward 与 Gradients |
5. 想用重算换内存时,Device::gradient_checkpointing() 换成平衡检查点策略,部分 op 只记怎么重算 | burn-autodiff/src/checkpoint/ | 梯度检查点 |
6. GradientsParams::from_grads 遍历模块,把节点 id 上的梯度换成参数 id 上的梯度 | burn-optim/src/optim/grads.rs | 优化器 |
训练主线:一个 step
flowchart TB derive["derive Module 生成 visit 和 map"] --> fwd["forward 走算子主线"] fwd --> loss["loss backward 走求导主线"] loss --> gp["GradientsParams from_grads"] gp --> step["ModuleOptimizer step"] step --> mapper["按参数 id 取梯度和状态"] mapper --> newp["换上新的 Param"] newp --> learner["Learner 包住循环"]
| 步骤 | 源码 | 讲它的页 |
|---|---|---|
1. #[derive(Module)] 为结构体生成 Module 的 visit / map 等遍历;参数是 Param<T>,带一个 ParamId | burn-derive/src/module/、burn-core/src/module/ | Module 派生、Config |
2. forward 是普通方法,Linear 这些模块调张量算子,走算子主线 | burn-nn/src/modules/ | nn 模块 |
3. loss.backward() 走求导主线,GradientsParams::from_grads 换成参数 id | burn-optim/src/optim/grads.rs | 优化器 |
4. ModuleOptimizer::step 用一个 ModuleMapper 遍历模块,map_float 里按参数 id 取梯度和状态,调 Adam::step 算出新张量 | burn-optim/src/optim/module/、adam.rs | 优化器 |
5. 存取模型:into_record / save_file,burn-store 的 burnpack、safetensors、PyTorch 导入 | burn-core/src/store/、burn-store/src/ | 存储与加载 |
6. Learner 把上面的循环、数据加载、指标和 checkpoint 包起来 | burn-train/src/learner/ | Learner 与数据 |
手写一遍这五步、不用 Learner 的实验见一个训练步。
feature 与 cfg
burn 的 feature
crates/burn/Cargo.toml 的 [features] 把每个 feature 转给 burn-core,burn-core 转给 burn-tensor,burn-tensor 转给 burn-dispatch,后者才真正决定依赖哪些后端 crate。和本卷有关的:
| feature | 默认 | 打开后多了什么 | 讲它的页 |
|---|---|---|---|
std | 是 | 标准库;关掉是 no_std + alloc(只有 Flex 支持) | 源码地图 |
optim | 是 | burn-optim,并且打开 autodiff | 优化器 |
autodiff | 是(经 optim) | burn-dispatch 依赖 burn-autodiff,Device::autodiff() 可用 | Autodiff 装饰器 |
flex | 否 | 纯 Rust 的 CPU 后端 burn-flex,Device::flex() | Flex:CPU 后端 |
wgpu | 否 | cubecl 的 wgpu 运行时,Device::wgpu(..),图形 API 由平台决定 | wgpu 与 Metal |
metal / vulkan / webgpu | 否 | 都隐含 wgpu,再固定一种 API:Device::metal(..) 等。metal 在 cubecl 里是 metal = ["wgpu-msl"],走 wgpu 加 MSL 编译器,不是 cubecl-metal | wgpu 与 Metal |
cuda / rocm | 否 | cubecl 的 CUDA / HIP 运行时(本卷只读源码) | 源码地图 |
cpu | 否 | cubecl 的 CPU 运行时(MLIR / LLVM),构建脚本联网下载 LLVM,本卷不构建 | — |
ndarray / tch | 否 | 弃用的两个后端 | Flex:CPU 后端 |
fusion | 否,但见下文 | 打开 burn-fusion,cubecl 后端套上 Fusion | Fusion 装饰器 |
autotune | 否,但见下文 | cubecl 后端第一次遇到某个形状时跑一遍候选内核挑最快的 | autotune |
persistence | 否 | 显式要求 cubecl 的持久缓存;见下文,cubecl 默认已开 | autotune |
train | 否 | burn-train(Learner、指标),连带 dataset 和 optim | Learner 与数据 |
store / safetensors / pytorch | 否 | burn-store 和两种导入格式 | 存储与加载 |
tracing | 否 | 各后端编进 tracing 的 span(from_data、into_data、内核提交、内存池),见 tracing 卷 | 一次 matmul |
默认 feature 里没有任何执行后端。crates/burn/src/lib.rs 写得很直接:「Default features include std and optim (and therefore autodiff), but no execution backend.」只写 burn = "0.22.1" 能编译,但 Device::default() 会 panic,见 Device。
表里 fusion 和 autotune 标的是「否,但见下文」:burn 自己的默认 feature 列表里没有它们,可只要默认 feature 开着,它们就会被打开。默认 feature 沿 burn-core/default → burn-tensor/default → burn-dispatch/default(含 burn-cubecl?/default)传到 burn-cubecl,而 burn-cubecl 的默认 feature 是 autotune、std、fusion。用 cargo tree -e features 对三种写法看 burn-cubecl 最终打开了哪些 feature:
## features = ["metal"]
burn-cubecl feature "autotune" ... "fusion" "metal" "std" "wgpu" 依赖树里 burn-fusion: 有
## default-features = false, features = ["std", "metal"]
burn-cubecl feature "metal" "std" "wgpu" 依赖树里 burn-fusion: 无
## default-features = false, features = ["std", "metal", "fusion", "autotune"]
burn-cubecl feature "autotune" ... "fusion" "metal" "std" "wgpu" 依赖树里 burn-fusion: 有
(每行把同一 crate 的 feature 合并到一行,... 处省略了 burn-cubecl-fusion、burn-fusion、default 这几个由 fusion 带出来的项。)关掉默认 feature 的同时,融合和 autotune 也悄悄关了:用第二种写法在 Metal 设备上打印 Backend::name,得到的是 dispatch<cubecl<wgpu<msl>>>,比默认写法的 dispatch<fusion<cubecl<wgpu<msl>>>> 少了 fusion 一层(打印方法见一次 matmul)。
问: 我只写了 features = ["metal"],没写 fusion,为什么一次 matmul 里打出来的后端名是 dispatch<fusion<cubecl<wgpu<msl>>>>?
答: 因为 fusion 是 burn-cubecl 的默认 feature,而 burn 的默认 feature 会一路把 burn-cubecl?/default 打开(? 表示「如果这个可选依赖被别的 feature 拉进来了」)。burn 的 fusion feature 的作用是在关掉默认 feature 时把它补回来。cubecl 的持久缓存同理:cubecl-runtime 的默认 feature 里就有 persistence,所以不开 burn/persistence,autotune 的结果照样写到磁盘上(位置见一次 matmul)。
burn-dispatch 的三个 cfg
feature 一路传下来,最终由 burn-dispatch/build.rs 归并成三个 cfg:
let cube = cuda || rocm || cpu || metal || vulkan || webgpu || wgpu;
let local = cube || flex || ndarray || tch;
let executing = local || remote;
// Backend-free builds expose tensor/model APIs without installing an execution backend.
if executing || capture {
println!("cargo::rustc-cfg=backend_enabled");
}
// Every cubecl-backed feature selects the same backend type, since the device says which
// runtime, so they share one variant under one cfg rather than a seven-way list at each use.
if cube {
println!("cargo::rustc-cfg=cube_backend");
}
// Capture records operations but never executes them.
if executing {
println!("cargo::rustc-cfg=executing_backend");
}
| cfg | 什么时候有 | 在源码里管什么 |
|---|---|---|
cube_backend | 七个 cubecl feature 任一 | DispatchDevice::Cube、DispatchTensorKind::Cube 这些变体,backends::Cube 的再导出。七种 GPU / CPU 运行时共用这一个变体,运行时是哪一种由设备值决定 |
backend_enabled | 有任何能执行的后端,或者开了 capture | 没有它时 NoBackend(一个含 Infallible 的结构体)占位,生成的 match 分支变成发散的 never.unreachable(),lib.rs 顶部压掉一串 unused 警告 |
executing_backend | 有能执行的后端(本地或 remote) | capture 只记录不执行;只开 capture 时有 backend_enabled 而没有 executing_backend |
这三个 cfg 只对 burn-dispatch 这一个 crate 生效(构建脚本发的 rustc-cfg 不会传给依赖它的 crate)。过程宏 crate burn-backend-extension 的后端目录 BACKENDS 把 "cube_backend"、"feature = \"flex\"" 这些写成字符串,展开成 [Cube, cube_backend] 这样的列表交给 burn-dispatch 里的 macro_rules!,生成的每个分支带一个 #[cfg(..)];Dispatch 怎么用它们生成 match,见 Dispatch。三种实验配置下构建脚本的输出(target/debug/build/burn-dispatch-*/output,只留 rustc-cfg 行):
# features = ["flex", "metal"]
cargo::rustc-cfg=backend_enabled
cargo::rustc-cfg=cube_backend
cargo::rustc-cfg=executing_backend
# features = ["flex"]
cargo::rustc-cfg=backend_enabled
cargo::rustc-cfg=executing_backend
# 默认 feature(没有后端)
(没有 rustc-cfg 行,只有三行 rustc-check-cfg)
没选后端时编出来的是一个「只有 API、不能执行」的 burn,Device::default() 在运行期 panic;0.22.0 之前它会悄悄编进 Flex,这段来历见 Dispatch 里讲 NoBackend 的一节。
cube_backend 也是 0.22 才有的:#5556(首次发布于 v0.22.0-pre.4)把 cubecl 的运行时从类型里拿掉(「runtime erasure」)之后,CUDA、Metal、Vulkan 这些不再是各自的后端类型,于是归并成一个 cfg。0.21 时 burn-dispatch 里 metal、vulkan、webgpu 还是互斥的三个后端,见源码地图。
阅读路线
- 只想会用:读 01 全景;02 的 Device 和检查与报错;07 的 Module 派生、优化器、Learner 与数据。
- 想读懂实现:按顺序读。02、03 讲清楚一次调用怎么离开泛型、落到哪个后端之后,04 的装饰器和 05 的融合才好读。
- 只关心 GPU:一次 matmul → 05 IR 与融合 → 06 CubeCL 与 GPU。
- 想自己写一个:读完 04 自动微分,去 11 动手,不依赖 burn 写一个反向模式自动微分。
前置知识
- Rust 的 trait、关联类型、const 泛型:
Tensor<const D: usize, K>的秩是 const 参数,后端接口是一组 trait,见 Backend trait。 - 一点深度学习:张量、矩阵乘、反向传播(backpropagation)、优化器。不需要会 PyTorch,但会的话对照起来更快,见 burn 解决什么问题。
- async:GPU 读回是
async fn,同步 API 在里面block_on,future 的基本概念见 tokio 卷(burn 本身不依赖 tokio)。 - serde:
Config和模型记录走 serde,见 serde 卷。 - tracing:
tracingfeature 打开的是 span,要配 subscriber 才看得到,见 tracing 卷。
小结
- 全卷对着 burn 0.22.1、cubecl 0.11.3、cubek 0.3.1 写,这是新项目写
burn = "=0.22.1"时解析出来的组合;GPU 只实测 Metal。 - 0.22 去掉了
Tensor的后端类型参数(#4717):后端由Device的值决定,所有调用经过全局后端Dispatch;backward的合法性从编译期检查变成运行期检查。 - 算子主线:
Tensor::matmul→ 不泛型的Numeric::matmul→Dispatch的match→(Autodiff)→Fusion记 IR → 读数据时 drain →CubeBackend+ autotune + cubek 内核 → cubecl 编译提交;CPU 上落到 Flex 的 gemm。 - 求导主线:
Device::autodiff→ 每个 op 登记Backward步骤 →AutodiffServer::backward按序执行 →Gradients→GradientsParams。 - 训练主线:
derive(Module)→ forward → backward →ModuleOptimizer::step按参数 id 更新 →Learner。 - 默认 feature 不含后端;但只要默认 feature 开着,cubecl 后端就自带
fusion和autotune。feature 最终在burn-dispatch里归并成backend_enabled、cube_backend、executing_backend三个 cfg。