Rust in Everything
基于 burn 0.22.1

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] 生成的 match panic「input tensor __tensor is on the wrong backend」,没有 op 名和设备。
  • 同为 cubecl、设备不同:cat 正常返回,读数据时后台线程 panic「Should have handle for tensor」,主线程拿到 ExecutionError。
  • 修法:拼之前 to_device 到同一个设备,最好一开始就建在同一个设备上。

💬 关联讨论

发起讨论
加载中...