unsupported dtype BF16 for op matmul
主线位置:推理主线第三步「前向,logits 对上」,为了省内存把权重留在 bf16 / f16,还在 CPU 上跑。
版本:PyTorch 2.15.0 / transformers 5.22.0(CPU)、candle 0.11.0(CPU,accelerate 和不开 accelerate;Metal)
flowchart TB
w["bf16 权重"] --> d{"VarBuilder dtype"}
d -->|"BF16 CPU"| e1["matmul 报错"]
d -->|"F16 CPU accelerate"| e2["f16 matmul 报错"]
d -->|"F16 CPU gemm"| bad["能跑 差 0.43"]
d -->|"F32 CPU"| ok1["差 3e-5"]
d -->|"BF16 F16 Metal"| ok2["和 torch 同量级"]
实验代码:PyTorch 侧 half_cpu.py,candle 侧 crate pitb-exp 的 bin p19_half,同一份代码用 --no-default-features(不开 accelerate)另编一次。release 构建,平台 M2 Max、macOS 27、rustc 1.99.0。权重是本地的 SmolLM2-135M-Instruct;PyTorch 固定 torch.set_num_threads(8)。各设备、各 dtype 的完整表格见半精度。
PyTorch 的写法
模型在 CPU 上,整个 .to(dtype):
model = AutoModelForCausalLM.from_pretrained(model_dir, dtype=torch.float32, attn_implementation="eager").eval()
with torch.inference_mode():
ref = model(ids).logits[0, -1]
ref_gen = model.generate(ids, max_new_tokens=20, do_sample=False)[0, 5:]
for dt in [torch.bfloat16, torch.float16]:
m = model.to(dt) # 习惯写法:整个模型换 dtype,还在 CPU 上
lg = m(ids).logits[0, -1].float()
gen = m.generate(ids, max_new_tokens=20, do_sample=False)[0, 5:]
cpu torch.bfloat16 max|diff| vs f32 = 4.336e-01 (max|ref| 20.6), greedy same prefix 18/20
cpu torch.float16 max|diff| vs f32 = 6.842e-02 (max|ref| 20.6), greedy same prefix 20/20
两种都能跑,误差是半精度该有的量级。(这两个数和半精度那页 from_pretrained(dtype=torch.bfloat16) 的 5.34e-1、4.86e-2 不同:.to(dtype) 把 RoPE 的 inv_freq buffer 也转成了 bf16 / f16,from_pretrained(dtype=...) 让它留在 float32。两种写法算的不是同一个模型。)
搬到 candle 的现象
candle 里选 dtype 的地方是 VarBuilder,Cache 也要同一个 dtype:
let model = Llama::load(st_vb(dt, &dev)?, &cfg)?;
println!("loaded {dt:?} accelerate={}", cfg!(feature = "accelerate"));
let (logits, greedy) = run(&model, &cfg, dt, &dev)?; // Cache::new(true, dt, ..),prefill + 19 步
println!("{}", check(&logits, &greedy)?);
st_vb 是 VarBuilder::from_mmaped_safetensors(&[".../model.safetensors"], dt, &dev),check 和 transformers f32 的参照比。CPU,开 accelerate(默认):
== acc cpu bf16
loaded BF16 accelerate=true
Error: unsupported dtype BF16 for op matmul
== acc cpu f16
loaded F16 accelerate=true
Error: the accelerate backend does not support f16 matmul
CPU,不开 accelerate:
== noacc cpu bf16
loaded BF16 accelerate=false
Error: unsupported dtype BF16 for op matmul
== noacc cpu f16
loaded F16 accelerate=false
logits max|diff|=4.28e-1 (max|ref| 20.6), greedy same prefix 20/20
- 加载都成功(
loaded那一行),报错在第一次前向。PyTorch 里 dtype 不支持通常在.to()时就知道,这里要等真正做矩阵乘。 - 不开
accelerate的 F16 不报错,误差 0.428,是 PyTorch CPU f16(0.068)的 6 倍,和 bf16 一个量级。这是三种情况里最难发现的一种。
原因
CPU 的矩阵乘是 impl Map2 for MatMul 里按 feature 二选一的 f:
不开
accelerate时是gemm那一支,开头一个白名单:match T::DTYPE { DType::F16 | DType::F32 | DType::F64 => {} _ => Err(Error::UnsupportedDTypeForOp(T::DTYPE, "matmul").bt())?, }没有 BF16。F16 交给
gemmcrate,它在 aarch64 上选 f16 的乘加指令,累加器本身是 f16,1536 项加下来误差就到了这个量级(单次矩阵乘的对照见半精度的「CPU 上的 F16」一节)。开
accelerate时是另一个f,只有 F32(sgemm)和 F64(dgemm),F16 单独bail!("the accelerate backend does not support f16 matmul"),其余落到同一个UnsupportedDTypeForOp。
Storage::matmul 在分派前只查两边 dtype 一致,不查后端支不支持,所以错误出现在 CPU 后端里面。全表和 Metal 一侧的情况见源码卷的坑 unsupported dtype BF16 for op matmul。origin/main 上 cpu_backend/mod.rs 另有几处修改(包括批维 stride 为 0 的 matmul,#3960),这两处 dtype 分支没有变。
改法
CPU 上载成 F32;要半精度就放到 Metal:
== acc cpu f32
loaded F32 accelerate=true
logits max|diff|=2.98e-5 (max|ref| 20.6), greedy same prefix 20/20
== acc metal bf16
loaded BF16 accelerate=true
logits max|diff|=6.02e-1 (max|ref| 20.6), greedy same prefix 20/20
== acc metal f16
loaded F16 accelerate=true
logits max|diff|=4.52e-2 (max|ref| 20.6), greedy same prefix 20/20
- CPU F32 就是 A3 的结果:差 2.98e-5,容差 1e-4。文件是 bf16,载成 F32 时内存翻倍(峰值 RSS 见什么时候选 candle)。
- Metal BF16 差 0.602、F16 差 0.045,和 PyTorch 同 dtype 的误差同一量级;bf16 下的贪心是否一致受近平局影响,不说明谁更准(半精度)。
- 不要用「CPU + F16 + 关掉 accelerate」来绕开报错:能跑,但误差是 f16 该有的 6 倍以上。
- 想要 CPU 上的小内存,用量化(GGUF),不是 bf16(GGUF)。
怎么检查
- dtype 按设备选,写在一处:
let dt = if dev.is_cpu() { DType::F32 } else { DType::BF16 };,VarBuilder和Cache::new用同一个dt。 - 加载后先跑一次很短的前向(一个 token)当冒烟测试,dtype 的问题在这里暴露,不要等到服务收到第一个请求。
- 看
Cargo.toml里candle-core有没有accelerate(或mkl)feature:同一份代码在两种构建下对 F16 的反应不同,一个报错、一个静默变差。
小结
- PyTorch CPU 上
model.to(torch.bfloat16)/float16能跑,logits 差 0.43 / 0.068。 - candle CPU:BF16 前向报
unsupported dtype BF16 for op matmul;F16 开accelerate报the accelerate backend does not support f16 matmul,不开时能跑但用 f16 累加,差 0.428。加载都成功,错在第一次 matmul。 - CPU 用 F32(差 2.98e-5),半精度放到 Metal(BF16 0.602、F16 0.045)。