Rust in Everything
基于 burn 0.22.1

ShapeMismatch expected [4, 3] found [3, 4]

主线位置:训练主线的入口之外。权重从 PyTorch 那边来,要先经 burn-store 套进 #[derive(Module)] 的参数里,模型才能进 forward。加载机制见存储与加载。

flowchart TB
  file["PyTorch 权重 out x in"] --> store["SafetensorsStore"]
  store -->|无适配器| check["形状对比 lazy_shape"]
  check --> err["ShapeMismatch"]
  store -->|PyTorchToBurnAdapter| tr["按模块类型转置"]
  tr --> ok["in x out 套进 Linear"]

实验 crate pc-store(从 train-store 复制),burn 0.22.1,Device::flex(),debug 构建。平台 M2 Max、macOS 27、rustc 1.99.0。

现象

模型是两层 Linear:fc = LinearConfig::new(4, 3)、head = LinearConfig::new(3, 2).with_bias(false)。用 safetensors crate(0.8.0)按 PyTorch 的习惯写一个文件,fc.weight 是 [3, 4]、head.weight 是 [2, 3],数据是 0..12,然后直接读:

let mut model = Net::new(device);
let mut st = SafetensorsStore::from_file("out/torch_like.safetensors");
match model.load_from(&mut st) {
  Ok(r) => println!("no adapter: Ok\n{r}"),
  Err(e) => println!("no adapter: Err\n{e}"),
}
wrote out/torch_like.safetensors (340 bytes)
no adapter: Err
Validation failed: Import errors: [ShapeMismatch { path: "fc.weight", expected: Shape { dims: [4, 3] }, found: Shape { dims: [3, 4] } }, ShapeMismatch { path: "head.weight", expected: Shape { dims: [3, 2] }, found: Shape { dims: [2, 3] } }]

两个权重都差一个转置,fc.bias 是一维的,对上了。方阵的 Linear 不会报这个错,会静悄悄地读进一个转置了的权重。square 模式:LinearConfig::new(4, 4),文件里按 PyTorch 布局写 [4, 4] 的 0..16,用 [1, 0, 0, 0] 做一次 forward,PyTorch 算出来应当是 W 的第 0 列:

no adapter: ok = true
forward([1,0,0,0]) = [0.0, 1.0, 2.0, 3.0]  (PyTorch: W @ x = column 0 of W = [0, 4, 8, 12])
with adapter: forward([1,0,0,0]) = [0.0, 4.0, 8.0, 12.0]

不加适配器,加载成功,结果是第 0 行。

出处

两边约定不同。burn 的 Linear 算 O = IW + b,weight 的文档写着「Matrix of shape [d_input, d_output]」;PyTorch 的 nn.Linear 算 x @ W.T + b,weight 是 [out_features, in_features]。

报错在 burn-store 的 Applier 里:它遍历模块,每个参数先取 Param::lazy_shape(不触发懒初始化的形状),读出文件里的数据之后逐个比较:

        // Validate shape
        if *data.shape() != target_shape {
            self.errors.push(ApplyError::ShapeMismatch {
                path: path.clone(),
                expected: target_shape,
                found: data.shape().clone(),
            });
            return None; // Signal caller to fall back to initialization
        }

错误先攒着,全部遍历完一起报,所以一次能看到所有对不上的参数。burn-store 不按名字猜「这是一个要转置的权重」,转不转由适配器决定,见存储与加载里 safetensors 和 Adapter 一节。

修法

给 store 挂上 PyTorchToBurnAdapter:

let mut st =
  SafetensorsStore::from_file("out/torch_like.safetensors").with_from_adapter(PyTorchToBurnAdapter);
match model.load_from(&mut st) {
  Ok(r) => println!("PyTorchToBurnAdapter: Ok\n{r}"),
  Err(e) => println!("PyTorchToBurnAdapter: Err\n{e}"),
}
println!("fc.weight dims {:?}", model.fc.weight.val().dims());
println!("fc.weight {:?}", vals(model.fc.weight.val()));
PyTorchToBurnAdapter: Ok
┌─ Tensor Loading Summary ─────────────────────────
│
│ ✓ Successfully applied: 3 tensors
│ ⊘ Skipped (filtered):  0 tensors
│ ✗ Missing in source:    0 tensors
│ ? Unused in target:     0 tensors
│ ! Errors:               0 errors
│
└───────────────────────────────────────────────────
fc.weight dims [4, 3]
fc.weight [0.0, 4.0, 8.0, 1.0, 5.0, 9.0, 2.0, 6.0, 10.0, 3.0, 7.0, 11.0]

第一行 0, 4, 8 是原来 [3, 4] 的第一列。适配器看的是模块类型(参数属于 Struct:Linear),只转置 Linear 的 weight,顺带把归一化层的 weight / bias 改名为 gamma / beta。

读 .pt 文件的 PytorchStore 不用自己挂:它的 apply_to 总是用 PyTorchToBurnAdapter。读 pytorch-reader 测试数据里的 full_model.pt(fc = nn.Linear(4, 3)),和原始数据对一下:

  fc.weight: F32 Shape { dims: [3, 4] }
...
burn fc.weight dims [4, 3]
raw [3,4] row 0: [-0.00374341, 0.2682218, -0.41152257, -0.3679695]
burn [4,3] col 0: [-0.00374341, 0.2682218, -0.41152257, -0.3679695]
burn == transpose(raw): true

LinearLayout::Col 的 Linear:另一个坑

LinearConfig::with_layout(LinearLayout::Col) 让 Linear 换一种存法:初始化时生成 [d_output, d_input],靠 Param 上的 init_mapper 转置成内存里的 [d_input, d_output],save_mapper / load_mapper 在存取时各转置一次(LinearConfig::init 的 LinearLayout::Col 分支)。问题出在 lazy_shape:参数还没被读过时,它返回的是初始化值的形状 [out, in];读过一次(物化)之后返回内存里的 [in, out]。同一个文件,结果取决于参数有没有被碰过(col2 模式,fc 换成 Col,读 full_model.pt 和自己存的 burnpack):

a) lazy Col + PytorchStore: Err
Validation failed: Import errors:
┌─ Tensor Loading Summary ─────────────────────────
...
├─ Errors
│
│  ⚠️  Shape mismatch for 'fc.weight': expected Shape { dims: [3, 4] }, found Shape { dims: [4, 3] }
│
└───────────────────────────────────────────────────
b) lazy target: Ok, dims [4, 3]
c) materialized target: Record validation error: Apply errors: ["fc.weight: shape mismatch, expected Shape { dims: [4, 3] } but record has Shape { dims: [3, 4] }"]

更糟的是「已物化 + PytorchStore」(col 模式,加载前先 val() 看了一眼形状):校验按 [4, 3] 通过,load_mapper 再转置一次,加载报告「Successfully applied: 3 tensors」,形状却成了 [3, 4],直到 forward 才 panic:

Col fc.weight dims before [4, 3]
Ok
┌─ Tensor Loading Summary ─────────────────────────
│
│ ✓ Successfully applied: 3 tensors
...
Col fc.weight dims after [3, 4]

thread 'main' (3406440) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-nn-0.22.1/src/modules/linear.rs:136:35:
assert_shape!(input, [.., d_input]): axis 1 expected 3, got 4 (dims [1, 4])

这是 burn 0.22.1 的上游问题(本页在 0.22.1 上复现):lazy_shape 在有 init_mapper 的参数上,物化前后给出的形状不一致,而校验和 load_mapper 对「文件里是哪种形状」的假设不一样。在它修好之前,从 PyTorch 导入的模型用默认的 LinearLayout::Row。

怎么确认没中招

  • 打印 load_from 返回的 ApplyResult(它实现了 Display,就是上面的方框),看 Errors 是 0、Unused in target 和 Missing in source 是不是预期的数。
  • 方阵权重不会报错。挑一个 Linear,把 burn 里的 weight 转置后和文件里的原始数据逐元素比(上面 burn == transpose(raw) 的做法)。
  • 加载之后马上用一个假输入跑一次 forward,形状问题当场暴露,不要等到训练里。
  • 用了 LinearLayout::Col 的,加载之后再打印一次 weight.val().dims()。

小结

  • burn 的 Linear.weight 是 [in, out],PyTorch 是 [out, in];burn-store 只比形状,不猜转置。
  • safetensors / burnpack store 要 .with_from_adapter(PyTorchToBurnAdapter);PytorchStore 自带这个适配器。
  • 方阵不报错但会读反,用数值比对确认。
  • 0.22.1 里 Col 布局的 Linear 加载结果取决于参数是否已物化,可能加载成功而 forward panic;导入的模型用 Row。

💬 关联讨论

发起讨论
加载中...