Rust in Everything
基于 burn 0.22.1

同一段代码,Int 在不同设备上位宽不同

主线位置:算子主线第一步之前,张量被创建的那一刻。没写 dtype 时,Tensor::from_data 用设备的默认设置决定元素类型(Device 的「设置与 configure」),之后每个 op 都按这个 dtype 走到后端。同一段代码换一个设备,Tensor<1, Int> 里装的就可能是另一种整数。

flowchart TB
  code["Tensor 1 Int 没写 dtype"] --> set["设备的 DeviceSettings"]
  set --> flex["Flex: I32"]
  set --> nd["ndarray: I64"]
  set --> mtl["Metal: I32"]
  flex --> wrap["4e9 回绕成负数"]
  mtl --> wrap
  nd --> okv["4000000000"]
  fix["显式 dtype 或 configure"] --> i64["三处都是 I64"]

本页实验 crate pa-dev(模板的 feature,含 flex、ndarray、metal;debug 构建),每个用例单独起一个进程。平台 M2 Max、macOS 27、rustc 1.99.0。ndarray 后端在 0.22 标为弃用,这里只用来对照(不少旧代码还在用它)。

现象

同一段代码在三个设备上跑:arange 的 dtype,四个 10 亿的和:

for (name, device) in [("flex", Device::flex()), ("ndarray", Device::ndarray()), ("metal", metal.clone())] {
  let i = Tensor::<1, Int>::arange(0..3, &device);
  let s = Tensor::<1, Int>::full([4], 1_000_000_000, &device).sum();
  let data = s.into_data();
  println!(
    "{name:<8} arange {:?} | sum of 4 x 1e9: {:?} {:?} | to_vec::<i64>: {:?}",
    i.dtype(),
    data.dtype(),
    data.clone().convert::<i64>().to_vec::<i64>().unwrap(),
    data.to_vec::<i64>()
  );
}
flex     arange I32 | sum of 4 x 1e9: I32 [-294967296] | to_vec::<i64>: Err(DTypeMismatch { expected: I64, actual: I32 })
ndarray  arange I64 | sum of 4 x 1e9: I64 [4000000000] | to_vec::<i64>: Ok([4000000000])
metal    arange I32 | sum of 4 x 1e9: I32 [-294967296] | to_vec::<i64>: Err(DTypeMismatch { expected: I64, actual: I32 })
case int-dtype: done
exit=0

三个后果:

  • Flex 和 Metal 上和溢出,回绕成 -294967296(4 000 000 000 − 2³²),没有任何报错。
  • 在 ndarray 上能用的 to_vec::<i64>(),换到 Flex / Metal 上返回 DTypeMismatch;代码里如果是 .unwrap(),就变成 panic。
  • 放进一个装不下的值,在 I32 设备上直接 panic。Tensor::<1, Int>::from_data([3_000_000_000i64], &device),三个设备分别跑:
### int-big flex

thread 'main' (3524870) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-std-0.22.1/src/element/cast.rs:218:1:
Element cannot be represented in the target type: "i64"(3000000000) => "i32"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101
### int-big ndarray
ndarray: I64 [3000000000]
case int-big: done
exit=0
### int-big metal

thread 'main' (3524904) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-std-0.22.1/src/element/cast.rs:218:1:
Element cannot be represented in the target type: "i64"(3000000000) => "i32"
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101

浮点也有同类问题。TensorData 是 f64,不写 dtype 时被转成设备默认的 F32;Metal 的 supports_dtype(F64) 是 false,可显式要 F64 时张量照样建了出来:

for (name, device) in [("flex", Device::flex()), ("metal", metal.clone())] {
  let a = Tensor::<1>::from_data(TensorData::from([0.1f64, 0.2]), &device);
  println!("{name:<6} from f64 data -> {:?}, supports F64 {}", a.dtype(), device.supports_dtype(DType::F64));
}
let a = Tensor::<1>::from_data(TensorData::from([0.1f64, 0.2]), (&metal, DType::F64));
println!("metal explicit F64 -> {:?}", a.dtype());
println!("{:?}", a.into_data());
flex   from f64 data -> F32, supports F64 true
metal  from f64 data -> F32, supports F64 false
metal explicit F64 -> F64
TensorData { bytes: Bytes { data: [154, 153, 153, "..."], len: 16 }, shape: Shape { dims: [2] }, dtype: F64 }
case f64: done
exit=0

只是上传、读回,不经过内核,所以没出事。一旦要在 GPU 上算,比如把 F32 张量 cast 成 F64:

let a = Tensor::<1>::from_floats([0.1, 0.2], &metal).cast(FloatDType::F64);
println!("cast -> {:?}", a.dtype());
println!("{:?}", a.into_data());
cast -> F64

thread 'main' (3523777) panicked at src/main.rs:131:26:
Error while reading data: use `try_into_data` instead to catch the error at runtime: An error happened during execution
Caused by:
  Several failures at once
Caused by:
  The bytes were never written (failure #2, still claiming 2 buffer(s))
Caused by:
  A launch error happened
Caused by:
  A compilation error happened during launch
Caused by:
  An error caused the compilation to fail
Caused by:
  the device refused the MSL module of kernel `cast_element_i_f32_o_f64_n_2_0e1143f3`: Validation Error

Caused by:
  In Device::create_shader_module_passthrough, label = 'cast_element_i_f32_o_f64_n_2_0e1143f3'
    Failed to generate the backend-specific code
...
exit=101

cast 那一行返回了,dtype() 也说是 F64;错误要到读数据、内核真正编译时才出现,而且是 Metal 拒绝了生成的 MSL。

出处

0.22 里 dtype 的默认值不在类型上,在设备上。每个后端设备实现 DeviceOps::defaults:

设备实现默认 float / int
Fleximpl DeviceOps for FlexDeviceF32 / I32
ndarrayimpl DeviceOps for NdArrayDeviceF32 / I64(注释还写着 // E = f32, I = i64)
cubecl(Metal、CUDA……)impl DeviceOps for DeviceF32 / I32

Tensor::from_data 没拿到 dtype 时用 opt.resolve_dtype::<K>() 取设备默认值,然后把数据转成这个类型;i64 的 3e9 转 i32 失败,就是 burn-std 里那条「cannot be represented」。整数运算按元素类型的语义在后端执行,I32 的和就在 32 位里回绕。

设置按设备登记在一个全局表里,每个设备一份、只能初始化一次:第一次建张量就锁定成默认值。所以 Device::configure 只能在第一次使用之前调,之后返回 AlreadyInitialized;要求不支持的 dtype 返回 UnsupportedDType。Device::settings() 只读,不会锁定。

📝NOTE
冷知识:位宽以前写在后端的类型参数里

#5000(「refactor(backends)!: remove associated element types & replace with device defaults」,2026-05,首次发布于 v0.22.0-pre.1)之前,BackendTypes 有 type FloatElem、type IntElem、type BoolElem 三个关联类型:ndarray 是 NdArray<E = f32, I = i64, Q = i8>,Flex 是 type IntElem = i32,cubecl 后端是 CubeBackend<R, F, I, BT>。那时整数位宽同样因后端而异,但它是类型的一部分,换后端时编译器至少会让你写出新的类型。这个 PR 删掉了三个关联类型,换成每个设备的 defaults(),ndarray 那行 // E = f32, I = i64 的注释就是旧类型参数留下的痕迹。同一时期的 #4717 把后端从 Tensor 的类型里拿掉,dtype 从此和后端一样,是运行期的值。

修法

需要 64 位整数就在创建时写出来,元组 (&device, DType::I64) 也是 TensorCreationOptions:

let big = Tensor::<1, Int>::from_data([3_000_000_000i64], (&device, DType::I64));
flex     I64 [3000000000]
ndarray  I64 [3000000000]
metal    I64 [3000000000]
case int-explicit: done
exit=0

想让一个设备上所有没写 dtype 的整数都是 I64,在建任何张量之前 configure。Metal 上同时试一下 F64:

let mut device = Device::metal(DeviceKind::DefaultDevice);
println!("configure F64: {:?}", device.configure(FloatDType::F64));
println!("configure I64: {:?}", device.configure(burn::tensor::IntDType::I64));
let s = Tensor::<1, Int>::full([4], 1_000_000_000, &device).sum();
println!("sum {:?} {:?}", s.dtype(), s.into_data().to_vec::<i64>().unwrap());
configure F64: Err(UnsupportedDType { device: "Cube(Wgpu(WgpuDevice { kind: DefaultDevice, backend: Metal }))", dtype: F64 })
configure I64: Ok(())
sum I64 [4000000000]
case configure-int: done
exit=0

F64 被拒绝时没有占掉这次初始化,后面的 I64 照样成功。反过来,设备用过之后再 configure 就晚了(Flex 上先建一个张量):

first tensor F32
configure after use: Err(AlreadyInitialized { device: "Flex(Cpu)" })
configure before use: Ok(())
then tensor F64
case configure: done
exit=0

(后两行是一个新的 ndarray 设备:用之前 configure(FloatDType::F64) 成功。)

读数据时不要假设元素类型:TensorData::convert::<i64>() 先转换再取,或者用会做转换的 try_to_vec_as(检查与报错)。浮点要 F64 时先问 device.supports_dtype(DType::F64),Metal 上答案是 false,就别依赖它。

怎么确认没中招

  • 对计数、索引、求和这类可能超过 2³¹ 的整数,打印一次 dtype();I32 就意味着最大 2 147 483 647。
  • 代码里的 to_vec::<i64>()、as_slice::<i64>() 都在假设 I64,换到 Flex / Metal 上会失败。
  • 同一套测试在两个后端上各跑一遍,结果不一致时先比 dtype。
  • configure 的返回值要检查:AlreadyInitialized 说明某处已经在这个设备上建过张量(包括初始化模型参数)。

小结

  • dtype 默认值属于设备:Int 在 Flex、Metal 上是 I32,在 ndarray 上是 I64;I32 的和溢出时静默回绕。
  • to_vec::<i64>() 在 I32 张量上返回 DTypeMismatch;放进 32 位装不下的值直接 panic。
  • Metal 不支持 F64:configure 会拒绝,显式 F64 的张量能建能读,真要算时内核编译失败,错误到读数据才出现。
  • 修法:需要位宽就显式写 (&device, DType::I64),或在第一次使用前 configure;读数据时先转换。

💬 关联讨论

发起讨论
加载中...