input tensor __tensor is on the wrong backend
主线位置:算子主线的第一步没拦住,问题漏到后面几层。
Tensor::cat的TensorCheck不查设备,调用进入Dispatch(Dispatch);后端不同时#[backend_dispatch]生成的match在那里 panic,后端相同、设备不同时一路进到Fusion的流(Fusion 装饰器),直到读数据才出错。
flowchart TB cat["Tensor::cat 或 stack"] --> chk["TensorCheck 只查形状"] chk --> disp["Dispatch 按第一个张量选后端"] disp -->|"后端不同"| wb["wrong backend panic"] disp -->|"同为 Cube 设备不同"| fus["Fusion 登记"] fus --> rd["into_data 时执行"] rd --> dsu["DSU 线程 Should have handle"] fix["先 to_device 到同一设备"] --> ok["cat 正常"]
本页实验 crate pa-dev(模板的 feature,debug 构建),平台 M2 Max、macOS 27、rustc 1.99.0。metal 是 Device::metal(DeviceKind::DefaultDevice)。
现象
Flex 上一个张量、Metal 上一个,拼起来:
let a = Tensor::<1>::ones([2], &Device::flex());
let b = Tensor::<1>::zeros([2], &metal);
let c = Tensor::cat(vec![a, b], 0);
println!("cat: {:?}", c.into_data().to_vec::<f32>().unwrap());
thread 'main' (3523528) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-dispatch-0.22.1/src/ops/tensor.rs:11:1:
input tensor `__tensor` is on the wrong backend
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
换成 Tensor::stack(vec![a, b], 0) 得到一模一样的两行(stack-backends 用例)。报错里没有 op 名,没有设备,__tensor 也不是你代码里的变量名。
两个张量都在 cubecl 后端上、只是设备不同(默认设备是 backend: Auto,见 The provided tensors are not on the same device),cat 本身成功返回,形状也对:
let a = Tensor::<1>::ones([2], &Device::default());
let b = Tensor::<1>::zeros([2], &metal);
let c = Tensor::cat(vec![a, b], 0);
println!("cat built, dims {:?}", c.dims());
println!("cat: {:?}", c.into_data().to_vec::<f32>().unwrap());
cat built, dims [4]
thread 'DSU-100-0' (3523578) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-ir-0.22.1/src/handle.rs:333:32:
Should have handle for tensor TensorId { value: 1 }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' (3523550) panicked at src/main.rs:63:31:
Error while reading data: use `try_into_data` instead to catch the error at runtime: An error happened during execution
Caused by:
Should have handle for tensor TensorId { value: 1 }
exit=101
panic 了两次:先是名为 DSU-100-0 的后台线程,然后主线程在 into_data 那一行。用 try_into_data 接住,主线程不 panic,后台线程那一段照样打到 stderr:
thread 'DSU-100-0' (3523598) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-ir-0.22.1/src/handle.rs:333:32:
Should have handle for tensor TensorId { value: 1 }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
try_into_data: Err(An error happened during execution
Caused by:
Should have handle for tensor TensorId { value: 1 })
case cat-cube-try: done
exit=0
出处
二元运算的检查以 binary_ops_device 开头,cat 的检查 TensorCheck::cat 只有两条:列表不能为空,除拼接维以外形状相同。TensorCheck::stack 同样没有设备,Tensor::stack 最后也是调 cat。所以跨设备的列表能通过 Tensor::cat 的第一行。
接下来是 Dispatch 的 float_cat。#[backend_dispatch] 宏按第一个张量选定后端,再把列表里每个张量取成这个后端的原语。列表参数的展开在 extract_input 里,逐个元素绑定到名为 __tensor 的变量:
InputKind::VecTensor(kind) => {
let tensor = format_ident!("__tensor");
let extract = extract_tensor(&tensor, *kind, false, extraction);
// ...
quote! {
let #name = #name.into_iter().map(|__tensor| {
#extract
__tensor
}).collect::<alloc::vec::Vec<_>>();
}
}
extract_tensor 给每个张量生成一个 match,变体不是选定的后端就落到 _ => panic!("input tensor `{}` is on the wrong backend", stringify!(#name))。这就是报错里 __tensor 的来历,也是为什么位置显示成 burn-dispatch/src/ops/tensor.rs:11:1:那是 #[backend_dispatch] 属性所在的行,宏生成的代码都算在这一行上。宏怎么展开见 Dispatch。
Flex 和 Metal 是两个后端,这个 match 能发现;默认设备和 Device::metal(..) 都属于同一个 Cube 变体,match 放行。Fusion 把 cat 记成一个 op 收进流里,等 into_data 执行时,服务线程在 HandleContainer 里找另一个设备的张量句柄,找不到(burn-ir/src/handle.rs:333):
let (id, handle) = self
.take(id)
.unwrap_or_else(|| panic!("Should have handle for tensor {id:?}"));
服务线程的 panic 被转成挂在结果张量上的错误,读的时候才交给调用方。为什么错误挂在张量上、和它同一条流里不相干的张量为什么还能正常读,见 Fusion 装饰器 的「错误挂在张量上」。
修法
拼之前把所有张量搬到同一个设备:
let parts = vec![
Tensor::<1>::ones([2], &Device::flex()),
Tensor::<1>::zeros([2], &Device::default()),
Tensor::<1>::full([2], 2.0, &metal),
];
let parts: Vec<_> = parts.into_iter().map(|t| t.to_device(&metal)).collect();
let c = Tensor::cat(parts, 0);
println!("cat: {:?} on {:?}", c.clone().into_data().to_vec::<f32>().unwrap(), c.device());
cat: [1.0, 1.0, 0.0, 0.0, 2.0, 2.0] on Device<Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))>
case cat-fix: done
exit=0
跨后端的 to_device 经主机内存(Device),在循环里对大张量这样做有代价,最好从源头就把张量建在同一个设备上。
怎么确认没中招
- 看到「input tensor
...is on the wrong backend」,位置在burn-dispatch/src/ops/*.rs:某个 op 的输入来自不同后端。RUST_BACKTRACE=1找自己代码里的调用点(panic 的位置在 burn-tensor 里)。 - 看到
DSU-...线程 panic「Should have handle for tensor」:查有没有把同一类后端的两个不同设备(比如默认设备和Device::metal(..))上的张量放进同一个 op。 - 把来源不同的张量收进
Vec交给cat/stack之前,打印一遍每个张量的device(),或者统一to_device。
小结
cat和stack的TensorCheck不查设备,跨设备的列表能进到Dispatch。- 后端不同:
#[backend_dispatch]生成的matchpanic「input tensor__tensoris on the wrong backend」,没有 op 名和设备。 - 同为 cubecl、设备不同:
cat正常返回,读数据时后台线程 panic「Should have handle for tensor」,主线程拿到ExecutionError。 - 修法:拼之前
to_device到同一个设备,最好一开始就建在同一个设备上。