Device
主线位置:算子主线的分岔点,也是求导主线的入口。张量在哪个设备上,
Dispatch就把a.matmul(b)交给哪个后端;设备上带不带.autodiff()标记,决定这次调用要不要先经过Autodiff<B>。本页讲burn-tensor/src/device.rs里的Device:它怎么造出来、默认是谁、怎么比较、能做哪些设备级的操作。DispatchDevice的各个变体怎么在match里分到后端,见 Dispatch。
flowchart TB dev["Device 门面"] --> blob["Opaque 定长存储"] blob --> dd["DispatchDevice"] dd --> cube["Cube 各 cubecl 运行时"] dd --> flex["Flex"] dd --> nd["NdArray 弃用"] dd --> remote["Remote"] dd --> cap["Capture"] dd --> ad["Autodiff"] ad --> inner["Box 内层设备"] ad --> ck["检查点策略"] dd --> un["Unavailable 无后端"]
本页实验 crate tensor-device(--release,模板的 feature 去掉了 autotune-checks,它会让 GPU 计时失真)、tensor-nobackend(只依赖默认 feature 的 burn,debug),平台 M2 Max、macOS 27、rustc 1.99.0。Metal 是实测;CUDA、ROCm、Vulkan 只读了源码。
门面:又一个藏起来的枚举
Device 和 Tensor 一样只有一个不透明字段:
pub struct Device {
blob: device_opaque::Opaque,
}
// Aligned, type-erased storage for `DispatchDevice`. See `crate::macros` for
// why this indirection exists (it keeps the dispatch type tree out of
// downstream MIR).
burn_std::obfuscate!(
type: DispatchDevice,
module: device_opaque,
derives: [Send, Sync]
);
为什么要藏起来见 BridgeTensor 与 *_impl。藏起来的 DispatchDevice 每个开启的后端一个变体:Cube 一个变体包下 CUDA、ROCm、Metal、Vulkan、WebGPU、wgpu 和 cubecl 的 CPU 运行时,Flex、NdArray、LibTorch、Remote、Capture 各一个,Autodiff 包着另一个 DispatchDevice。一个后端都没开时只剩一个隐藏的 Unavailable(NoBackend) 变体。
Device 不是 Copy,Clone 是把内部的 DispatchDevice 克隆一份再装进一个新的 Opaque;本卷 feature 组合下它占 128 字节、按 64 对齐(上一页的实验)。所以 API 里到处传的是 &Device。Tensor::device() 每次调用也都新建一个 Device:它从后端张量取出 DispatchDevice,再用 Device::new 包起来。
构造函数随 feature 出现
每个后端的构造函数各自带着 #[cfg(feature = ...)]:
| 构造函数 | feature | 参数 | 里面装的 |
|---|---|---|---|
Device::flex() | flex | 无 | FlexDevice |
Device::cpu() | cpu | 无 | cubecl CPU 运行时 |
Device::cuda(i) / rocm(i) | cuda / rocm | impl Into<DeviceIndex>,整数或 DeviceIndex::Default | CudaDevice::new(i) |
Device::wgpu(kind) | wgpu | DeviceKind | WgpuDevice,图形 API Auto |
Device::metal(kind) / vulkan / webgpu | 同名 feature | DeviceKind | WgpuDevice,图形 API 固定 |
Device::ndarray()、libtorch*() | ndarray / tch | 无或索引 | 标了 #[deprecated(since = "0.22.0")] |
Device::capture() | capture | 无 | 只记录不执行的设备 |
DeviceIndex::Default 解析成 0;从 i32 / i64 转换时负数会 expect("device index must be non-negative")。wgpu、metal、vulkan、webgpu 四个构造函数共用 wgpu_device:把 burn 自己的 DeviceKind(DiscreteGpu(i)、IntegratedGpu(i)、VirtualGpu(i)、Cpu、DefaultDevice、Existing(id))一一映射成 cubecl 的 WgpuDeviceKind,再 .on(backend) 定下图形 API。DeviceKind 自己定义一份而不直接用 cubecl 的,注释说是「so callers don't have to depend on cubecl」。metal feature 在 macOS 上实际走哪条编译路径属于 wgpu 与 Metal。
没开对应 feature 时,这些函数根本不存在,错误发生在编译期。只依赖 burn = "=0.22.1"(默认 feature)、调用 Device::flex():
error[E0599]: no associated function or constant named `flex` found for struct `burn::tensor::Device` in the current scope
--> src/bin/noflex.rs:4:19
|
4 | let _ = Device::flex();
| ^^^^ associated function or constant not found in `burn::tensor::Device`
|
note: if you're trying to build a new `burn::tensor::Device`, consider using `burn::tensor::Device::new` which returns `burn::tensor::Device`
...
Default:按顺序挑一个,没有就 panic
Device::default() 只是 Self::new(DispatchDevice::default())。DispatchDevice::default 先看环境变量 BURN_DEVICE,再按固定顺序取第一个开启的后端:
fn default() -> Self {
// BURN_DEVICE selects one compiled backend or reports a configuration error.
#[cfg(feature = "std")]
{
if let Ok(device_str) = std::env::var("BURN_DEVICE") {
match device_str.to_lowercase().as_str() {
// ...
"metal" => {
#[cfg(feature = "metal")]
return Self::Cube(CubeDevice::Wgpu(
WgpuDevice::default().on(WgpuBackend::Metal),
));
panic!(
"BURN_DEVICE=metal requested, but the 'metal' feature is not enabled."
);
}
// ...
_ => panic!("Unknown BURN_DEVICE override: '{}'.", device_str),
}
}
}
// Spelled out per feature rather than left to `CubeDevice::default()`: that answers for
// the runtimes *cubecl* compiled in, and cargo unifies features across a build, so a
// workspace that also builds `burn-cuda` would hand this crate a CUDA default even when
// it was built with only `wgpu`. The order is the one a caller who did not choose would
// want — a discrete accelerator, then the portable path, then the CPU.
#[cfg(feature = "cuda")]
return Self::Cube(CubeDevice::Cuda(Default::default()));
#[cfg(feature = "metal")]
return Self::Cube(CubeDevice::Wgpu(Default::default()));
// ... rocm、vulkan、webgpu、wgpu、cpu、tch
// Preserve the preference for Flex over the deprecated NdArray backend.
#[cfg(feature = "flex")]
return Self::Flex(FlexDevice);
#[cfg(feature = "remote")]
return Self::Remote(RemoteDevice::default());
#[cfg(feature = "ndarray")]
return Self::NdArray(NdArrayDevice::default());
panic!(
"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()."
);
}
顺序是 CUDA、Metal、ROCm、Vulkan、WebGPU、wgpu、CPU、LibTorch、Flex、Remote、NdArray。一连串带 cfg 的 return 只有第一个开启的会被编译成可达代码,函数上的 #[allow(unreachable_code)] 就是为此。注释解释了为什么不直接用 CubeDevice::default():cargo 会在整个构建里统一 feature,cubecl 那一层的默认值可能被同一工作区里别的 crate 打开的 CUDA 带偏。
注意 metal 那一行:没有 BURN_DEVICE 时,metal feature 下的默认设备是 WgpuDevice::default(),图形 API 是 Auto,并没有像 BURN_DEVICE=metal 或 Device::metal(..) 那样固定成 Metal。
metal 和 wgpu),在 M2 Max 上 Device::default() == Device::metal(DeviceKind::DefaultDevice) 是 true 还是 false?两者最后都会用这台机器上同一块 GPU。答案和解释在页末。实验:默认设备和 BURN_DEVICE
tensor-device 开着 flex、ndarray、wgpu、metal 等 feature,程序只打印 Device::default():
--- default
Device::default() = Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Auto }))>
--- BURN_DEVICE=flex
Device::default() = Device<Flex(Cpu)>
exit=0
--- BURN_DEVICE=metal
Device::default() = Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))>
exit=0
--- BURN_DEVICE=cuda
thread 'main' (3205663) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-dispatch-0.22.1/src/device.rs:261:25:
BURN_DEVICE=cuda requested, but the 'cuda' feature is not enabled.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
--- BURN_DEVICE=foo
thread 'main' (3205665) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-dispatch-0.22.1/src/device.rs:337:26:
Unknown BURN_DEVICE override: 'foo'.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
tensor-nobackend 只写 burn = { version = "=0.22.1" }。burn 的默认 feature 里没有任何后端,程序照样能编译,Device::default() 在运行期 panic:
fn main() {
println!("building a device...");
let device = Device::default();
let t = Tensor::<1>::zeros([2], &device);
println!("{t}");
}
building a device...
thread 'main' (3149547) 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().
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Device 的文档把这一点写在了「Backend selection」一节:「Default Cargo features do not enable an execution backend. Select one explicitly ... there is no implicit CPU fallback.」
0.22.0 之前的预发布版本里,一个后端都没开时会悄悄编进 Flex 当默认后端;#5722 改成了现在这样,前后的差别见 Dispatch 里讲 NoBackend 的部分。
相等:只比硬件
Device 的 PartialEq 转给 DispatchDevice 的实现:
fn eq(&self, other: &Self) -> bool {
match (self, other) {
// If both are Autodiff, compare the inner devices
#[cfg(feature = "autodiff")]
(DispatchDevice::Autodiff(a), DispatchDevice::Autodiff(b)) => {
a.inner.as_ref() == b.inner.as_ref()
}
// If one is Autodiff, compare it to the raw device
#[cfg(feature = "autodiff")]
(DispatchDevice::Autodiff(a), b) => a.inner.as_ref() == b,
#[cfg(feature = "autodiff")]
(a, DispatchDevice::Autodiff(b)) => a == b.inner.as_ref(),
#[cfg(cube_backend)]
(Self::Cube(a), Self::Cube(b)) => a == b,
// ... Flex、NdArray、LibTorch、Remote、Capture 同样两两比较
#[allow(unreachable_patterns)]
(_, _) => false,
}
}
autodiff 标记和检查点策略都不参与比较,AutodiffDevice 自己的 PartialEq 也只比 inner,文档说这是为了和外层保持一致。实测:
== equality
flex == flex: true
flex == flex.autodiff(): true
flex.autodiff() == flex ckpt: true
flex == ndarray: false
metal == wgpu: false
metal == default: false
wgpu == default: true
这个定义直接影响检查与报错:TensorCheck 用 == 判断两个张量在不在同一设备上,所以一个 .autodiff() 设备上的张量和同一硬件上的普通张量可以一起运算;而 metal 和 wgpu 两种构造出来的设备即使指向同一块 GPU,也被当成不同设备。
autodiff()、without_autodiff()、inner()
autodiff 在 0.22 里是设备上的一个标记,不是类型。autodiff 把设备包进 DispatchDevice::Autodiff,已经包过的原样返回:
#[cfg(feature = "autodiff")]
#[must_use]
pub fn autodiff(self) -> Self {
match self.into_dispatch() {
device @ DispatchDevice::Autodiff(_) => Self::new(device),
other => Self::new(DispatchDevice::autodiff(other)),
}
}
without_autodiff 反过来取出内层;inner 就是调 without_autodiff,文档说前者「reflects the historical backend-decorator model」,后者直接描述设备的属性。gradient_checkpointing() 把策略换成 Balanced,只能在 autodiff 设备上调。实测的 Debug 输出:
== debug
flex: Device<Flex(Cpu)> is_autodiff=false
ndarray: Device<NdArray(Cpu)> is_autodiff=false
metal: Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))> is_autodiff=false
wgpu: Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Auto }))> is_autodiff=false
default: Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Auto }))> is_autodiff=false
flex.autodiff(): Device<Autodiff { device: Flex(Cpu), checkpointing: Disabled }> is_autodiff=true
flex.autodiff().autodiff(): Device<Autodiff { device: Flex(Cpu), checkpointing: Disabled }> is_autodiff=true
flex.autodiff().gradient_checkpointing(): Device<Autodiff { device: Flex(Cpu), checkpointing: Balanced }> is_autodiff=true
flex_ckpt.inner(): Device<Flex(Cpu)>
flex.without_autodiff(): Device<Flex(Cpu)>
--- checkpoint-plain
thread 'main' (3205667) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-tensor-0.22.1/src/device.rs:555:18:
Gradient checkpointing requires autodiff; use Device::autodiff().gradient_checkpointing()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
调两次 autodiff() 不会套两层,文档专门说明这不会带来高阶导数:「only first-order autodiff is supported」。AutodiffDevice 用一个 Box<DispatchDevice> 装内层设备,burn-dispatch 里的注释说这个包装类型「mainly restricts users from creating Autodiff(Autodiff) devices」。标记怎么在分派时变成 Autodiff<B, C> 类型,见 Autodiff 装饰器;检查点策略见梯度检查点。
sync、flush、profile、seed
这几个方法都是一行:把 self.as_dispatch() 交给 Dispatch 上的同名函数,由 Dispatch 分到具体后端。
| 方法 | 做什么 | 返回 |
|---|---|---|
sync | 等设备上所有已提交的工作完成 | Result<(), ExecutionError> |
flush | 把缓冲着的 op(fusion 攒着的、remote 待发的)推出去执行,不等结果;即时执行的后端是空操作 | Result<(), ExecutionError> |
profile | 测闭包放到设备上的工作用了多少设备时间 | Result<(O, ProfileDuration), ExecutionError>,时长是之后再 resolve().await 的 future |
seed | 设随机数种子;文档说有的后端是全局生效 | () |
sync 和 flush 返回 Result,这是设备层把执行期错误交给用户的地方之一;什么错误是 Result、什么是 panic,见检查与报错。GPU 计时为什么要先 sync,见 autotune。
实测(profile 之前先在同一形状上跑一次 matmul,让内核编译和 autotune 不落进窗口):
for (name, d) in [("flex", &flex), ("metal", &metal)] {
d.seed(42);
let a = Tensor::<1>::random([4], Distribution::Default, d).into_data();
d.seed(42);
let b = Tensor::<1>::random([4], Distribution::Default, d).into_data();
let c = Tensor::<1>::random([4], Distribution::Default, d).into_data();
// 打印 a、a == b、b == c
}
let (out, dur) = metal.profile(|| a.clone().matmul(b.clone()).into_data()).unwrap();
let ticks = read_sync(dur.resolve());
let (_, dur_lazy) = metal.profile(|| a.clone().matmul(b.clone())).unwrap();
let (_, dur) = flex.profile(|| fa.clone().matmul(fa.clone())).unwrap();
== sync / flush
flex: sync Ok(()), flush Ok(())
metal: sync Ok(()), flush Ok(())
== seed
flex: a [0.13340974, 0.5265574, 0.24873835, 0.5427252]
same seed equal: true, next draw equal: false
metal: a [0.33610713, 0.3398847, 0.14931732, 0.5403774]
same seed equal: true, next draw equal: false
== profile
metal profile: out len 1048576, ticks Some(1.318208ms)
metal profile without read: None
flex profile: Some(5.285959ms)
- 同一个种子在同一设备上给出同样的序列,Flex 和 Metal 之间不同。
- Metal 上闭包只做
matmul、不读回时,resolve得到None:设备开着 fusion,这次 matmul 还攒在队列里,窗口关上时设备上什么也没跑。文档里正好写了这种情况:「a window over lazy work alone can read as empty」,解决办法是闭包以一次读取结束,或用profile_with加ProfileOptions::flush。fusion 什么时候真正执行见 Fusion 装饰器。带读回的那次读到 1.3ms,是 1024×1024 matmul 加读回在设备上的时间。 - Flex 上同样形状的 matmul 读到 5.3ms;Flex 的
profile用的是哪种计时,本页没有追。
设置与 configure
每个设备有一份 DeviceSettings(settings 返回它):没有指定 dtype 时用的默认浮点、整数、布尔类型。supports_dtype 问设备支不支持某个 dtype。三个设备的默认值:
== default dtypes per device
flex: float F32 int I32 bool Native; supports F64 true F16 true BF16 true
ndarray: float F32 int I64 bool Native; supports F64 true F16 false BF16 false
metal: float F32 int I32 bool U8; supports F64 false F16 true BF16 true
同样是 Tensor<2, Int>,ndarray 上是 I64,Flex 和 Metal 上是 I32;Metal 不支持 F64。configure 可以改默认值,但只能改一次,而且要在这个设备上建任何张量之前:
== configure
configure ndarray F64 before use: Ok(())
tensor dtype after configure: F64
second configure: Err(Device NdArray(Cpu) settings have already been initialized and cannot be changed.
Default data types lock on the first tensor operation for a device (or on a previous `set_default_dtypes`/`configure` call). Configure the device before creating any tensor on it, or create tensors with an explicit dtype instead.)
错误类型是 DeviceError::AlreadyInitialized。设置按设备存、第一次张量操作就锁定,所以 configure 要 &mut self,但它改的不是这个 Device 值本身,而是全局按设备登记的设置。
to_device:跨后端搬运
Tensor::to_device 只有两行:
pub fn to_device(self, device: &Device) -> Self {
let target_device = device.clone().with_autodiff_context_from(&self.device());
Self::new(K::to_device(self.primitive, &target_device))
}
with_autodiff_context_from 把目标设备的 autodiff 标记换成源张量的:源张量带 autodiff,目标就加上(连同检查点策略);源张量不带,目标就去掉。也就是说,to_device 只换硬件,不换「要不要求导」。文档还写了另外两点:在带梯度追踪的浮点张量上,to_device 是图里的一个 op,结果不是叶子;在两个不同后端之间搬运时,数据先读进主机内存成为 TensorData,再上传到目标后端。哪些后端之间能直接搬、哪些要经主机,属于 Dispatch 的跨后端矩阵,见 Dispatch。
实测 Flex 和 Metal 之间来回:
let x = Tensor::<2>::from_floats([[1.0, 2.0], [3.0, 4.0]], &flex);
let on_metal = x.clone().to_device(&metal);
let back = (on_metal * 2.0).to_device(&flex);
let i = Tensor::<1, Int>::from_ints([1, 2, 3], &flex).to_device(&metal);
let xa = Tensor::<2>::from_floats([[1.0, 2.0], [3.0, 4.0]], &flex_ad); // flex.autodiff()
let xa_metal = xa.to_device(&metal); // 目标是不带 autodiff 的 metal
== to_device
to metal: device Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))>
back to flex: device Device<Flex(Cpu)> data [2.0, 4.0, 6.0, 8.0]
int to metal: dtype I32 device Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))>
autodiff tensor to plain metal device: is_autodiff true device Device<Autodiff { device: Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal })), checkpointing: Disabled }>
最后一行:目标传的是不带 autodiff 的 metal,结果张量的设备却是 Autodiff { device: Cube(...) },标记跟着张量走了。要去掉它,在张量上调 without_autodiff()。
动脑筋答案
false。实验的相等表里就有这一行:
metal == default: false
wgpu == default: true
metal feature 下默认设备是 CubeDevice::Wgpu(Default::default()),打印出来是 WgpuDevice { kind: DefaultDevice, backend: Auto };Device::metal(DeviceKind::DefaultDevice) 是 backend: Metal。WgpuDevice 的比较包含图形 API 这一项,Auto 和 Metal 不等,尽管在这台 Mac 上两者最后用的是同一块 GPU。默认设备和 Device::wgpu(DeviceKind::DefaultDevice) 才相等。后果是:一部分张量用 Device::default()(包括 Tensor::from(..) 和反序列化)建、一部分用 Device::metal(..) 建,放在一起运算会被 TensorCheck 当成跨设备而 panic,见检查与报错。想让默认设备就是固定 Metal 的那个,设 BURN_DEVICE=metal。
小结
Device是用obfuscate!包住的DispatchDevice,128 字节,不是Copy;构造函数随后端 feature 出现,没开就编译不过。Default先看BURN_DEVICE,再按 CUDA、Metal、ROCm、Vulkan、WebGPU、wgpu、CPU、LibTorch、Flex、Remote、NdArray 的顺序挑;一个后端都没开时编译能过、运行期 panic,不会回退到 Flex。metalfeature 下的默认设备是backend: Auto的 wgpu 设备,和Device::metal(..)不相等。- 相等只比硬件,autodiff 标记和检查点策略都不看;
autodiff()幂等,inner()等于without_autodiff()。 sync/flush/profile返回Result;fusion 下只含惰性工作的profile窗口读到None;configure只能在设备第一次使用前调一次。to_device只换硬件,保留张量自己的 autodiff 标记;跨后端搬运经主机内存。