Rust in Everything
基于 candle 0.11.0

dtype mismatch in mul, lhs: F32, rhs: F64

主线位置:推理主线的「建模块」一步。照着 transformers 的 RoPE 代码算 inv_freq,再和位置相乘。

版本:PyTorch 2.15.0(CPU 和 MPS)、candle 0.11.0(CPU,accelerate;Metal)
flowchart TB
  cfg["rope_theta f64"] --> v["Vec f64"]
  lit["new 0.125"] --> f64["F64 张量"]
  v --> f64
  f64 --> mix["和 F32 相乘"]
  mix --> err["dtype mismatch"]
  f64 -->|Metal| op["F64 自己运算"]
  op --> berr["badd_f64 加载失败"]
  f64 -->|"写 f32"| ok["F32 张量"]

本页的对照:PyTorch 侧是实验代码里的 p01.py(torch.set_num_threads(1)),candle 侧是实验 crate pita-t 的 p01_f64(features metal accelerate,release)。平台 M2 Max、macOS 27、rustc 1.99.0。

PyTorch 的写法

transformers 的 Llama 系模型这样算 RoPE 的频率:

d, theta, seq = 8, 100000.0, 4
inv = 1.0 / (theta ** (torch.arange(0, d, 2).float() / d))
pos = torch.arange(seq).float()
pos[:, None] * inv[None, :]
scale = torch.tensor([0.125])
pos * scale
torch.tensor([0.125], dtype=torch.float64, device="mps")
inv_freq torch.float32 [1.0, 0.05623412877321243, 0.003162277862429619, 0.00017782794020604342]
pos[:, None] * inv[None, :] torch.float32 [4, 4]
torch.tensor([0.125]) torch.float32 pos * scale torch.float32 [0.0, 0.125, 0.25, 0.375]
mps tensor([0.125]) torch.float32
mps float64: TypeError Cannot convert a MPS Tensor to float64 dtype as the MPS framework doesn't support float64. Please use float32 instead.

theta 是 Python 的 float(双精度),但只要它和张量运算,结果就是默认 dtype float32;torch.tensor([0.125]) 也是 float32。PyTorch 用户很少需要想「这个数是几位的」。MPS 不支持 float64,PyTorch 在建张量时就拒绝。

搬到 candle 的现象

逐行照搬,rope_theta 从 config.json 读成 f64(serde 的默认选择):

let theta: f64 = 100000.0; // rope_theta read from config.json as f64
let inv: Vec<_> = (0..d).step_by(2).map(|i| 1.0 / theta.powf(i as f64 / d as f64)).collect();
let inv = Tensor::new(inv.as_slice(), &dev)?;
let pos = Tensor::arange(0u32, seq as u32, &dev)?.to_dtype(DType::F32)?;
pos.unsqueeze(1)?.broadcast_mul(&inv.unsqueeze(0)?);
pos.unsqueeze(1)?.matmul(&inv.unsqueeze(0)?);
pos.broadcast_mul(&Tensor::new(&[0.125], &dev)?);
&inv + &inv;
inv.to_dtype(DType::F32);
== cpu
inv_freq                           F64 [4] [1.0, 0.056234132, 0.0031622776, 0.00017782794]
pos[:, None] * inv[None, :]        Err: dtype mismatch in mul, lhs: F32, rhs: F64
pos.matmul(inv)                    Err: dtype mismatch in matmul, lhs: F32, rhs: F64
x.broadcast_mul(new(&[0.125]))     Err: dtype mismatch in mul, lhs: F32, rhs: F64
inv + inv (both F64)               F64 [4] [2.0, 0.112468265, 0.0063245553, 0.00035565588]
inv.to_dtype(F32)                  F32 [4] [1.0, 0.056234132, 0.0031622776, 0.00017782794]
== metal
inv_freq                           F64 [4] [1.0, 0.056234132, 0.0031622776, 0.00017782794]
pos[:, None] * inv[None, :]        Err: dtype mismatch in mul, lhs: F32, rhs: F64
pos.matmul(inv)                    Err: dtype mismatch in matmul, lhs: F32, rhs: F64
x.broadcast_mul(new(&[0.125]))     Err: dtype mismatch in mul, lhs: F32, rhs: F64
inv + inv (both F64)               Err: Metal error Error while loading function: badd_f64
inv.to_dtype(F32)                  Err: Metal contiguous to_dtype F64 F32 not implemented

(show 把结果读回主机打印;Err: 后面是错误的完整文本。)

  • 在 CPU 上,F64 张量本身能算,问题只出在和 F32 相遇的那一步。
  • 在 Metal 上,Tensor::new 把 F64 数据传上去不报错、to_vec1::<f64>() 也能读回(上面第一行),但 F64 自己和自己相加时报内核 badd_f64 加载失败,想在 Metal 上转成 F32 也报错。和 PyTorch 在建张量时就拒绝不同,candle 的错误推迟到了第一次运算。

原因

两件事叠在一起:

  1. dtype 由 Rust 类型定。 1.0、theta.powf(..) 的结果在没有别的约束时都是 f64,Tensor::new 按元素类型的 DTYPE 建张量,于是是 F64。candle 没有默认 dtype,见创建与 dtype。
  2. 没有类型提升。 二元运算进后端之前先过 Storage::same_dtype,dtype 不同就返回 dtype mismatch in {op}。broadcast_mul 最后调的是 mul,所以报错里写的是 mul。

Metal 那两条报错来自后端缺内核:MetalStorage::binary 用 format!("{op}_{}{}", dtype.as_str(), suffix) 拼出内核名 badd_f64,库里没有这个函数;MetalStorage::to_dtype 的转换表里没有任何 F64 的行,落到最后一支:

                (left, right) => {
                    crate::bail!("Metal contiguous to_dtype {left:?} {right:?} not implemented")
                }

上传和读回只是复制字节,不需要内核,所以能成功。各 dtype 在各设备上能做什么,见设备的「dtype 和设备要一起选」和源码卷的 DType 与 Device。origin/main 上 Metal 后端这两处没有改。

改法

在数据变成张量的那一刻定成 F32。三种写法:

// 1. compute the Vec in f32
let inv32: Vec<f32> = (0..d).step_by(2).map(|i| 1f32 / (theta as f32).powf(i as f32 / d as f32)).collect();
pos.unsqueeze(1)?.broadcast_mul(&Tensor::new(inv32.as_slice(), &dev)?.unsqueeze(0)?);
// 2. F64 data you must keep: convert on the CPU, then move
let via_cpu = Tensor::new(&[0.125], &Device::Cpu)?.to_dtype(DType::F32)?.to_device(&dev)?;
pos.broadcast_mul(&via_cpu);
// 3. a single number is not a tensor: use the f64 scalar ops
&pos * 0.125;
fix: f32 vec                       F32 [4, 4] [0.0, 0.0, 0.0, 0.0, 1.0, 0.05623413, 0.0031622779, 0.00017782794, 2.0, 0.11246826, 0.0063245557, 0.00035565588, 3.0, 0.1687024, 0.009486834, 0.0005334838]
fix: cpu to_dtype then to_device   F32 [4] [0.0, 0.125, 0.25, 0.375]
fix: scalar affine (pos * 0.125)   F32 [4] [0.0, 0.125, 0.25, 0.375]

CPU 和 Metal 上这三行相同。第一种写法和 PyTorch 一样在 float32 里做 theta ** (..),第三个频率两边都是 0.0031622779;在 CPU 上先按 f64 算完再 to_dtype(F32),得到的是 0.0031622776,差一个 ulp。candle-transformers 的 llama.rs 也是第一种写法:1f32 / cfg.rope_theta.powf(..),它的 rope_theta 字段本来就是 f32。&pos * 0.125 的系数虽然是 f64,但走的是 affine,后端先把系数转成张量的类型,结果仍是 F32,见创建与 dtype的「和标量运算:只认 f64」。

怎么检查

  • 看报错里的 rhs: F64:自己的模型里没有 F64 时,它几乎总是来自一个没写后缀的字面量或一个 Vec<f64>。
  • Tensor::new、from_vec、from_slice 的参数是浮点数时,写出类型:&[0.125f32]、let v: Vec<f32>。配置结构体里的 f64 字段在用之前 as f32。
  • 在 Metal 上调试时,先在 CPU 上跑同一段代码:CPU 上 F64 能算,报错会指向真正混用 dtype 的那一步,而不是之后某个 F64 自己的运算。

小结

  • candle 的 1.0、theta.powf(..) 是 F64,PyTorch 的同样写法是 float32;F64 和 F32 相遇报 dtype mismatch in mul, lhs: F32, rhs: F64。
  • Metal 上 F64 能上传、能读回,第一次运算报 badd_f64 加载失败,to_dtype 报 Metal contiguous to_dtype F64 F32 not implemented。
  • 改法:在 f32 里算好再建张量;F64 数据先在 CPU 上转 F32 再搬;单个系数用 &x * 0.125。

💬 关联讨论

发起讨论
加载中...