Rust in Everything
基于 burn 0.22.1

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 在开了 fusion feature 时是 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 都要把它按值搬进搬出。
💡TIP
打个比方:分拣口写死在传送带上的快递站

张量是包裹,DispatchTensorKind 的变体是包裹上的目的地标签(Flex、Cube),autodiff 字段是另一张「要留签收记录」的贴纸。每个 op 是一条传送带:先把同一单里所有包裹的贴纸对一遍(merge),再看第一件包裹的目的地标签,推进对应的分拣口;有签收贴纸的先过登记台(Autodiff<B, C>)。同一单里的其它包裹只核对标签,标签不一样就当场报错(input tensor rhs is on the wrong backend)。

比方到这里为止:真快递站的分拣口是运行期按表配置的;这里每条传送带的分拣口在编译期由宏按 feature 生成,没开的后端连分拣口都不存在,加一个后端要重新编译。

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。

📝NOTE
冷知识:没开后端时,曾经默默用 Flex

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 的 featuresB::float_matmul 出现次数crate 展开后行数
std,flex121052
std,flex,autodiff577881
std,flex,metal,autodiff10143573

每个后端贡献 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。

📝NOTE
没有蠢问题

问: 那用户把一个 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 内部的事
不同后端,都不带 autodifffloat_transfer:read_sync(Src::float_into_data(..)) 读到主机,再 Dst::float_from_data
不同后端,两边都是 autodiffAutodiff<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 部分。

📝NOTE
冷知识: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 拆出后端,再交给它。

❗IMPORTANT
动脑筋: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 ns36 到 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,后端开得越多越贵。

💬 关联讨论

发起讨论
加载中...