Dispatch
主线位置:算子主线的岔路口。
Tensor的方法经BridgeTensor把调用交给Dispatch::float_matmul之类的方法(见 BridgeTensor 与 *_impl);本页讲这一层怎么看张量带的标签,match到Flex::float_matmul、Cube::float_matmul或Autodiff<B, C>::float_matmul。各后端的方法签名见 Backend trait,Autodiff 那一支内部做什么见 Autodiff 装饰器。
flowchart TB
bridge["BridgeTensor"] --> dt["DispatchTensor"]
dt --> ctx["合并 autodiff 标记"]
ctx --> kind{"lhs 的 kind"}
kind -->|Flex| flex["Flex float_matmul"]
kind -->|Cube| cube["Cube float_matmul"]
kind -->|Autodiff| ad["Autodiff B C"]
ad --> flex
ad --> cube
flex --> wrap["包回 DispatchTensor"]
cube --> wrap
burn-dispatch 是 0.21 开发中加进来的(PR #4508,首次进入 v0.21.0-pre.2),当时 Tensor 还带后端类型参数,Dispatch 是可以填进 Tensor<B, D> 的 B、和 Wgpu、NdArray 并列的一个后端(Dispatch 的文档注释至今还是 Tensor::<Dispatch, 2>::zeros 这种旧写法);0.22 把 Tensor 的后端类型参数删掉以后,它成了唯一的后端。代码分在两个 crate:burn-dispatch(约 6500 行)定义类型、手写少数特殊的分派;burn-backend-extension(约 4200 行)是过程宏,生成绝大多数 op 的分派代码。
本页的实验 crate(都在 exp-burn/backend-*,M2 Max、macOS 27、rustc 1.99.0):backend-explore(burn =0.22.1,features std flex ndarray metal autodiff fusion autotune,debug)、backend-dispatch-expand(burn-dispatch 0.22.1 源码的一份拷贝,用来 cargo expand)、backend-overhead(release,测开销)、backend-nobackend(不开后端 feature)。
一个空结构体和三个枚举
Dispatch 没有字段:
#[derive(Debug, Default, Clone)]
pub struct Dispatch;
它的类型表把四种张量原语都设成同一个 DispatchTensor,设备是 DispatchDevice。「往哪个后端走」这个信息不在 Dispatch 的类型里,而在每个张量值上:
/// A tensor that can dispatch operations to any enabled backend at runtime.
#[derive(Clone, Debug)]
pub struct DispatchTensor {
/// Tensor kind primitive.
pub kind: DispatchTensorKind,
/// Autodiff backend association for this tensor.
pub autodiff: DispatchAutodiffContext,
}
DispatchTensorKind 每个开启的后端一个变体,变体里是 BackendTensor<B>,再按 Float / Int / Bool / Quantized / Autodiff 分一层:
pub enum DispatchTensorKind {
#[cfg(not(backend_enabled))]
#[doc(hidden)]
Unavailable(crate::NoBackend),
/// A tensor on the [cubecl backend](Cube) — its device says which runtime.
#[cfg(cube_backend)]
Cube(BackendTensor<Cube>),
/// The [Flex backend](Flex) tensor.
#[cfg(feature = "flex")]
Flex(BackendTensor<Flex>),
// ...
/// The [autodiff enabled backend](Autodiff) tensor.
#[cfg(feature = "autodiff")]
Autodiff(Box<DispatchTensorKind>),
}
DispatchDevice 是同样形状的设备枚举:Cube(CubeDevice)、Flex(FlexDevice)、NdArray、LibTorch、Remote、Capture,加上 Autodiff(AutodiffDevice)。AutodiffDevice 是 Box<DispatchDevice> 加一个检查点策略,构造函数不公开,所以造不出 Autodiff(Autodiff(..))。
三点值得注意:
- 所有 cubecl 运行时共用一个
Cube变体。 CUDA、ROCm、Metal、Vulkan、WebGPU、wgpu、cubecl 的 CPU 运行时,在这一层是同一个后端Cube;具体是哪个运行时,由CubeDevice这个值说了算。Cube在开了fusionfeature 时是Fusion<CubeBackend>,否则是CubeBackend。 - 只有浮点张量会被
Autodiff包一层。 在 autodiff 设备上建的整数、布尔张量仍是DispatchTensorKind::Flex(BackendTensor::Int(..)),只在autodiff字段上记着Enabled(策略);Autodiff<B>的整数原语本来就是B的整数原语(见 Backend trait)。 - 变体没有装箱。
DispatchTensorKind上有一个#[allow(clippy::large_enum_variant)],注释写着「Boxing backend tensors would cost every tensor an allocation」。代价是这个值很大:在backend-overhead里打印size_of,只开flex时DispatchTensor是 192 字节,开flex metal autodiff后是 576 字节(FlexTensor本身 120 字节)。每个 op 都要把它按值搬进搬出。
张量是包裹,DispatchTensorKind 的变体是包裹上的目的地标签(Flex、Cube),autodiff 字段是另一张「要留签收记录」的贴纸。每个 op 是一条传送带:先把同一单里所有包裹的贴纸对一遍(merge),再看第一件包裹的目的地标签,推进对应的分拣口;有签收贴纸的先过登记台(Autodiff<B, C>)。同一单里的其它包裹只核对标签,标签不一样就当场报错(input tensor rhs is on the wrong backend)。
build.rs 的三个 cfg
变体上的 #[cfg(cube_backend)]、#[cfg(not(backend_enabled))] 不是 Cargo feature,而是 build.rs 按 feature 组合算出来的:
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 任开一个 | Cube 变体及其所有 match 分支 |
executing_backend | 有任何能执行的后端(本地或 remote) | 只记录不执行的 capture 不算;autodiff 相关代码在只开 capture 时被裁掉 |
backend_enabled | 能执行,或开了 capture | 没有它时出现 Unavailable(NoBackend) 变体,并在 lib.rs 顶部关掉一串 unused 警告 |
cube_backend 存在的理由就写在注释里:否则每个 match 分支前都得写七个 feature 的 any(..)。
NoBackend:一个后端都没开
burn 的默认 feature 不含任何后端。这时三个枚举里只剩 Unavailable 一个变体,装的是 NoBackend:
#[cfg(not(backend_enabled))]
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NoBackend {
never: core::convert::Infallible,
}
它包着 Infallible,所以不可能有值;但它是个有私有字段的结构体,而不是空枚举。源码注释解释了这个区别:如果 DispatchTensor 在下游看来是空枚举,编译器会认定所有接收、返回张量的 API 都是不可达代码。这样一个库可以只依赖 burn 的张量和模块 API、不选后端,把选择留给最终的应用。所有分派分支里的 Unavailable(never) => never.unreachable() 都是 match self.never {},不会有运行期代价。
真要执行时,第一个需要设备的地方是 DispatchDevice::default(),它在列表末尾 panic。backend-nobackend 只依赖 burn = "=0.22.1"(默认 feature),main 里调 Device::default() 建一个张量:
thread 'main' (3150467) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-dispatch-0.22.1/src/device.rs:381:9:
No execution backend is enabled. Enable a Burn backend feature such as `flex`, `wgpu`, or `cuda`. To record a graph without executing it, enable `capture` and use Device::capture().
同一个 crate 把 Device::default() 换成 Device::flex(),错误提前到编译期,因为 Device::flex 只在 flex feature 下存在:
error[E0599]: no associated function or constant named `flex` found for struct `burn::tensor::Device` in the current scope
--> src/main.rs:5:24
|
5 | let device = Device::flex();
| ^^^^ associated function or constant not found in `burn::tensor::Device`
...
Device::default() 在有后端时按什么顺序挑、BURN_DEVICE 环境变量怎么覆盖,见 Device。
PR #5722(「fix(dispatch): require explicit backend selection and support backend-free builds」,首次进入 v0.22.0-pre.4)之前,burn-flex 是 burn-dispatch 的非可选依赖,build.rs 在一个后端 feature 都没开时发出 default_backend cfg,Flex 变体写成 #[cfg(any(feature = "flex", default_backend))],--no-default-features 也能跑在 CPU 上;DispatchDevice::default() 里还留着「TODO: which priority?」的注释。这个 PR 把 burn-flex 改成可选依赖,删掉 default_backend,加上 backend_enabled 和 NoBackend,并在 default() 末尾加了上面那句 panic。现在只依赖张量 API 的库不会再被迫编译一个 CPU 后端。
backend_catalog!:后端名单只写一次
每个 match 都要列出所有后端,名单只在一个地方:burn-backend-extension/src/catalog.rs 里的 BACKENDS 常量,每项是名字、cfg 字符串和两个标志:
pub(crate) const BACKENDS: &[BackendSpec] = &[
BackendSpec {
// Every cubecl runtime is this one backend; a device says which of them
// it runs on. `cube_backend` is set when any of their features is.
name: "Cube",
cfg: "cube_backend",
// ...
distributed: true,
unidirectional_transfer: false,
},
BackendSpec {
name: "Flex",
cfg: "feature = \"flex\"",
distributed: false,
unidirectional_transfer: false,
},
// ... NdArray、LibTorch、Remote、Capture
];
cfg 是字符串,过程宏把它原样放进生成代码的 #[cfg(..)] 里,所以它在调用宏的 crate(burn-dispatch)里求值,cube_backend 也就是 burn-dispatch 的 build.rs 设的那个。
过程宏 backend_catalog! 从这份名单导出三种视图,交给一个回调宏:backends(全部)、distributed(distributed: true 的)、matrix(除 Capture 外,每个后端到其余每个后端的列表)。burn-dispatch 的 macros.rs 只包了三层薄壳 backend_list!、distributed_backend_list!、backend_matrix!,文件开头说明了为什么不让过程宏直接生成 macro_rules!:那样 rust-analyzer 解析不稳定。手写的分派(Backend 的设备级方法、to_device、事务、backward)用这些壳生成 match;例如 Dispatch::sync 就是 dispatch_device!(device, |device| B::sync(device)),展开成「每个后端一个分支,分支里 type B = 那个后端」,Autodiff 设备递归进内层设备,B 换成 Autodiff<那个后端>。
#[backend_dispatch]:一个 op 展开成什么
绝大多数 op 不手写分派。burn-dispatch/src/ops/tensor.rs 里的写法是把 B 当成「某个具体后端」来写函数体,再在 impl 上挂 #[backend_dispatch]:
#[backend_dispatch]
impl FloatTensorOps<Self> for Dispatch {
// ...
fn float_matmul(lhs: FloatTensor<Self>, rhs: FloatTensor<Self>) -> FloatTensor<Self> {
B::float_matmul(lhs, rhs)
}
宏(dispatch::expand)逐个方法看参数:类型是 DispatchDevice 的参数说明这是「创建」类 op,按设备分派;否则取第一个张量参数决定后端,所有带张量的参数一起参与 autodiff 标记的合并。两样都没有就报编译错,要求写 #[backend_dispatch(skip)] 手写分派,float_to_device 就是这么做的。
把 burn-dispatch 0.22.1 的源码拷进实验目录,只开 std,flex 跑 cargo expand --lib,float_matmul 展开成(缩进是 cargo expand 的):
fn float_matmul(
lhs: FloatTensor<Self>,
rhs: FloatTensor<Self>,
) -> FloatTensor<Self> {
let __burn_selected_context = lhs.autodiff;
let mut __ad_ctx = crate::DispatchAutodiffContext::Disabled;
__ad_ctx = __ad_ctx.merge(lhs.autodiff);
__ad_ctx = __ad_ctx.merge(rhs.autodiff);
match lhs.kind {
crate::DispatchTensorKind::Flex(__burn_selected) => {
match __ad_ctx {
crate::DispatchAutodiffContext::Disabled => {
type B = crate::backends::Flex;
// ...(检查 lhs 的 context)
let lhs = __burn_selected.float();
let rhs = match rhs.kind {
crate::DispatchTensorKind::Flex(__inner) => __inner.float(),
_ => {
::core::panicking::panic_fmt(
format_args!(
"input tensor `{0}` is on the wrong backend",
"rhs",
),
);
}
};
let __output = { B::float_matmul(lhs, rhs) };
crate::DispatchTensor {
kind: crate::DispatchTensorKind::Flex(
crate::BackendTensor::Float(__output),
),
autodiff: __ad_ctx,
}
}
crate::DispatchAutodiffContext::Enabled(_) => {
// ... panic: "autodiff context requires the `autodiff` feature"
}
}
}
#[allow(unreachable_patterns)]
_ => {
::core::panicking::panic_fmt(
format_args!("unsupported dispatch backend"),
);
}
}
}
原来那一行 B::float_matmul(lhs, rhs) 原样出现在最里层,B 被一个块内的 type B = ... 绑定;外面一层拆包(.float() 取出 FlexTensor),里面一层装包(BackendTensor::Float 再包回 DispatchTensor)。rhs 不参与选择,只检查它是不是同一个后端。
开的 feature 越多,同一行被复制的次数越多。数 float_matmul 展开结果里 B::float_matmul 出现的次数,以及整个 crate 展开后的行数:
cargo expand 的 features | B::float_matmul 出现次数 | crate 展开后行数 |
|---|---|---|
std,flex | 1 | 21052 |
std,flex,autodiff | 5 | 77881 |
std,flex,metal,autodiff | 10 | 143573 |
每个后端贡献 5 份:不带 autodiff 一份;lhs 是普通张量但别的输入带 autodiff 时,两种检查点策略各一份(lhs 先用 from_inner 抬成 Autodiff 的常量);lhs 本身是 Autodiff 张量时,两种策略又各一份。运行期的策略变成类型参数 Autodiff<Flex, BalancedCheckpointing> / Autodiff<Flex, NoCheckpointing> 的这段 match 由过程宏里的 routing::with_autodiff_backend 生成;burn-dispatch 里同名的声明宏 with_autodiff_backend! 只用在 to_device、量化和分布式这几处手写分派里。策略怎么选、merge 的规则,在 Autodiff 装饰器里讲。这些副本都是单态化后的真代码,burn-dispatch 编译慢、而 Tensor 要把它挡在下游 crate 之外,原因就在这里(见 BridgeTensor 与 *_impl)。
「创建」类 op 没有输入张量,按设备分派。float_from_data 展开后是 match device,Autodiff 设备分支里再 match __device.inner.as_ref(),取出 __device.checkpointing 选 Autodiff<.., ..>,结果包成 DispatchTensorKind::Autodiff(..),autodiff 字段设成 Enabled(策略)。
生成代码里的 panic 位置指向宏所在的那一行。直接对 Dispatch 调 float_add,lhs 在 Flex、rhs 在 Metal:
let a = Dispatch::float_from_data(TensorData::from([1.0f32, 2.0]), Device::flex().as_dispatch());
let b = Dispatch::float_from_data(TensorData::from([3.0f32, 4.0]), Device::metal(DeviceKind::DefaultDevice).as_dispatch());
let _ = Dispatch::float_add(a, b);
thread 'main' (3146872) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-dispatch-0.22.1/src/ops/tensor.rs:11:1:
input tensor `rhs` is on the wrong backend
ops/tensor.rs:11:1 是 #[backend_dispatch] 那一行,不是 float_add。
问: 那用户把一个 Flex 张量和一个 Metal 张量相加,看到的就是这句「wrong backend」吗?
答: 不是。Tensor 层先检查设备,同样两个张量用 a + b 相加,panic 在 burn-tensor 的 numeric.rs,信息是 The provided tensors are not on the same device. Lhs tensor device Device<Flex(Cpu)>, Rhs tensor device Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))>.(格式见检查与报错)。Dispatch 里的这层检查是给绕过 Tensor 的调用者(后端扩展、burn-dispatch 自己的测试)兜底的。反过来,一个 flex().autodiff() 上的张量乘一个普通 flex() 张量能通过两层检查:设备相等不看 autodiff 标记,merge 把普通张量当成常量抬进图里,backend-explore 里这样乘出来的结果在 Autodiff { device: Flex(Cpu), .. } 上,左边的梯度是右边的值。
to_device 的矩阵
float_to_device 标了 #[backend_dispatch(skip)],手写:
#[backend_dispatch(skip)]
fn float_to_device(tensor: FloatTensor<Self>, device: &DispatchDevice) -> FloatTensor<Self> {
// `Tensor::to_device` aligns the target's autodiff context with the source tensor before
// reaching this low-level operation. Direct callers must likewise provide compatible
// contexts; this layer only routes the transfer between compute resources.
float_to_device!(
Float,
float,
tensor,
device,
float_to_device,
|inner, device| { super::transfer::float_transfer::<B1, B2>(inner, device) }
)
}
float_to_device! 用 backend_matrix! 拿到「每个源后端 → 其余每个目标后端」的列表,float_to_device_arms! 按 (张量的 kind, 目标设备) 生成 match 分支:
| 源 → 目标 | 怎么搬 |
|---|---|
| 同一后端(Flex → Flex,Cube → Cube) | 调该后端自己的 float_to_device。Metal 到另一块 GPU、CUDA 到 Metal 都是 Cube 内部的事 |
| 不同后端,都不带 autodiff | float_transfer:read_sync(Src::float_into_data(..)) 读到主机,再 Dst::float_from_data |
| 不同后端,两边都是 autodiff | Autodiff<B1, C>::to_backend::<B2, HostTransfer>,前向和反向都经主机,图不断 |
| 一边 autodiff、一边不是 | panic Cannot move between autodiff and non-autodiff instances. |
任意 → Capture | 可以(把实际值变成捕获图的初始值);从 Capture 出去一律 panic |
pub(crate) fn float_transfer<Src: Backend, Dst: Backend>(
tensor: FloatTensor<Src>,
device: &Dst::Device,
) -> FloatTensor<Dst> {
let data = burn_backend::read_sync(Src::float_into_data(tensor))
.expect("Failed to read tensor during cross-backend transfer");
Dst::float_from_data(data, device)
}
跨后端没有零拷贝的捷径,哪怕是 Flex 到 NdArray 这种两边都在 CPU 上的组合,也是一次 into_data 加一次 from_data。「一边 autodiff 一边不是」在用户代码里碰不到:Tensor::to_device 先按源张量的标记调整目标设备(见 Device),注释里的「Direct callers must likewise provide compatible contexts」说的就是这一点。
backend-explore 把一个 Flex 张量搬到 Metal、乘 2 再搬回来;再在 autodiff 设备上做同样的往返并求梯度:
let a = Tensor::<1>::from_floats([1.0, 2.0, 3.0], &flex);
let b = a.clone().to_device(&metal);
let c = (b * 2.0).to_device(&flex);
let x = Tensor::<1>::from_floats([1.0, 2.0, 3.0], &flex.clone().autodiff()).require_grad();
let y = x.clone().to_device(&metal.clone().autodiff());
let loss = (y.clone() * y).sum();
let grads = loss.backward();
let g = x.grad(&grads).unwrap();
a.device=Device<Flex(Cpu)>
b.device=Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))>
c=[2.0, 4.0, 6.0] device=Device<Flex(Cpu)>
loss.device=Device<Autodiff { device: Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal })), checkpointing: Disabled }>
x.grad=[2.0, 4.0, 6.0] device=Device<Flex(Cpu)>
loss 在 Metal 上算出来,梯度 2x 经 HostTransfer 的反向回到 Flex,落在 x 所在的设备上。这条「图跨后端不断」的路径是 PR #5624(「feat(autodiff): support graph-preserving cross-backend transfers」,首次进入 v0.22.0-pre.4)加的,它在 Autodiff 里怎么记节点归 04 部分。
Cube 一个变体以前是七个PR #5556(「Chore/update cubecl runtime erasure」,首次进入 v0.22.0-pre.4)之前,BACKENDS 名单的开头是七项:Cpu、Cuda、Metal、Rocm、Vulkan、Wgpu、WebGpu,每项有自己的 feature cfg,DispatchTensorKind、DispatchDevice 各有七个对应变体,to_device 的矩阵在它们之间两两生成分支。cubecl 0.11 把运行时从类型里擦掉以后,这个 PR 把七项合成一项 Cube,cfg 换成 cube_backend。代价写进了这一项的注释:七项时只有 Cuda 标 distributed: true,合并后整个 Cube 都标 true,在 wgpu 或 CPU 设备上调集合通信会一路走到 cubecl 的 ComputeServer::all_reduce 才 panic,报错里不提是哪个设备。
设备编号
DispatchDevice 也要实现 cubecl 的 Device trait,给出一个 DeviceId { type_id: u16, index_id: u16 }。各后端的 type_id 会撞(FlexDevice 和 NdArrayDevice 都想用 0),encode_type_id 把它拆成两半:
fn encode_type_id(&self, backend_type_id: u16) -> u16 {
// Use the lower 8 bits for the backend's internal type ID
let internal_type_id = backend_type_id & INTERNAL_ID_MASK;
// Use the upper 8 bits for the DispatchDevice/DispatchDeviceId
let backend = u16::from(self.backend_id()) << BACKEND_SHIFT;
backend | internal_type_id
}
高 8 位是 DispatchDeviceId:Cube = 0、Flex = 4、LibTorch = 5、NdArray = 6、Remote = 10、Capture = 11。编号不连续,是删掉七个 cubecl 变体后留下的空位。Backend::device_count 收到的就是这个编码后的 type_id,先 decode_type_id 拆出后端,再交给它。
Device::flex().autodiff() 的 to_id() 是多少?和 Device::flex() 一样吗?在 backend-explore 里对四个设备打印 as_dispatch() 和 to_id():
for (name, device) in [
("flex", Device::flex()),
("flex.autodiff", Device::flex().autodiff()),
("ndarray", Device::ndarray()),
("metal", Device::metal(DeviceKind::DefaultDevice)),
] {
println!("{name:<14} {:?} -> {}", device.as_dispatch(), device.as_dispatch().to_id());
}
先按上面的位运算算一下 Flex 和 NdArray 的 type,再看页尾。
这一层花多少时间
backend-overhead(release)在同一台机器上对一个 16 个元素的 f32 张量做加法,分三档调用,每档 100 万次为一轮、先热身 10 万次、跑 7 轮,报告每次的纳秒数:
// 1. Flex directly, through the burn-backend trait.
black_box(Flex::float_add(black_box(fa.clone()), black_box(fb.clone())));
// 2. The Dispatch backend, same trait, DispatchTensor in and out.
black_box(Dispatch::float_add(black_box(da.clone()), black_box(db.clone())));
// 3. The user-facing Tensor API.
black_box(black_box(ta.clone()) + black_box(tb.clone()));
三档的输入都是被 clone 过的共享张量,所以 Flex 每次都走分配新缓冲的路径(见 Flex),三档做的实际计算完全相同,差值就是每多一层的代价。同一个程序编两次:只开 flex,以及开 flex metal autodiff(match 多出 Cube 和 Autodiff 分支,DispatchTensor 从 192 字节变成 576 字节)。各跑两遍,下面是其中一遍:
float add, shape [16], f32, 1000000 ops x 7 reps, release
features: metal=false autodiff=false
Flex::float_add min 138.8 ns median 140.3 ns max 141.1 ns
Dispatch::float_add min 157.9 ns median 159.7 ns max 160.7 ns
Tensor + (flex) min 203.3 ns median 205.0 ns max 206.1 ns
float add, shape [16], f32, 1000000 ops x 7 reps, release
features: metal=true autodiff=true
Flex::float_add min 138.1 ns median 138.8 ns max 139.5 ns
Dispatch::float_add min 173.6 ns median 174.9 ns max 176.5 ns
Tensor + (flex) min 265.1 ns median 267.8 ns max 269.8 ns
| 层 | 只开 flex | 开 flex metal autodiff |
|---|---|---|
Dispatch 比直接调 Flex 多 | 19 到 25 ns | 36 到 40 ns |
Tensor 比 Dispatch 多 | 45 到 47 ns | 约 93 ns |
表里的范围取自两遍的中位数,同一档两遍之间相差不到 8 ns。分派这一层是几十纳秒,和 16 个元素的加法本身(约 140 ns,大部分是分配)同一个量级;对 1024×1024 的矩阵乘(Flex 上约 4 ms)则可以忽略。Tensor 那一层还包括形状和设备检查、BridgeTensor 的转换,在后端多时同样变贵。这组数只说明「每个小 op 固定多出几十纳秒」,只适用于这台机器;它在什么场景下要紧,放到设计取舍里和 0.21 的泛型写法一起讨论。
动脑筋答案
flex Flex(Cpu) -> DeviceId(type=1024, index=0)
flex.autodiff Autodiff { device: Flex(Cpu), checkpointing: Disabled } -> DeviceId(type=1024, index=0)
ndarray NdArray(Cpu) -> DeviceId(type=1536, index=0)
metal Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal })) -> DeviceId(type=100, index=16384)
- Flex:
FlexDevice::to_id是(0, 0),高 8 位放Flex = 4,4 << 8 = 1024。NdArray 是6 << 8 = 1536。 flex.autodiff和flex完全相同:to_id对Autodiff设备直接用内层设备的编号,autodiff 不是硬件,不进编号。这和DispatchDevice的PartialEq一致:两个设备只差 autodiff 标记时相等。- Metal:
Cube = 0,高 8 位是 0,type=100全是 cubecl 给 wgpu 设备的编号,index也由 cubecl 编码,burn-dispatch不改。
小结
Dispatch是空结构体;后端信息在每个DispatchTensor的kind上,autodiff 信息在它的autodiff字段上。所有 cubecl 运行时共用Cube一个变体。- 变体由
build.rs算出的cube_backend、executing_backend、backend_enabled和各后端 feature 裁剪;一个后端都不开时只剩不可构造的NoBackend,到Device::default()才 panic。 - 后端名单只在
catalog.rs写一次,backend_catalog!导出全部、分布式、搬运矩阵三种视图;#[backend_dispatch]给每个 op 生成「合并 autodiff 标记 → 按第一个张量的 kind 选后端 → 拆包、调用、装包」,每个后端 5 份副本。 - 同后端搬运交给后端自己;跨后端一律经主机
into_data+from_data,两边都是 autodiff 时图不断。 - 实测分派层让 Flex 上一个小 op 多 19 到 40 ns,
Tensor层再多 45 到 93 ns,后端开得越多越贵。