Rust in Everything
基于 burn 0.22.1

Record validation error: Missing tensors

主线位置:训练主线的两头,开始时加载 checkpoint、结束时保存模型。记录里每个张量以字段路径(fc.weight)为键,加载时按路径对回参数,默认缺一个就失败。机制见 存储与加载。

flowchart TB
  file["文件里是 fc.weight"] --> applyn["按路径对回"]
  model["模型要 encoder.weight"] --> applyn
  applyn --> miss["missing 两个"]
  miss --> rec["load_file 只列名字"]
  miss --> st["safetensors 摘要 Did you mean"]
  file --> remap["with_remap_pattern 改名"]
  remap --> okn["加载成功"]

实验 crate exp-burn/pb-pit,bin rename(参数选情形),Device::flex(),debug,M2 Max、macOS 27、rustc 1.99.0。

现象

上周存的模型有一个字段叫 fc,今天把它改名为 encoder,其余不变:

// What was saved last week.
#[derive(Module, Debug)]
struct Net {
  fc: Linear,
  head: Linear,
}

// Today: `fc` was renamed to `encoder`.
#[derive(Module, Debug)]
struct NetV2 {
  encoder: Linear,
  head: Linear,
}

旧模型用 save_file("out/net.bpk") 存过,再用 burn-store 的 SafetensorsStore 存一份 out/net.safetensors。新模型 load_file:

thread 'main' (3487568) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/burn-core-0.22.1/src/module/base.rs:574:14:
Failed to load module from file: Validation("Missing tensors: [\"encoder.weight\", \"encoder.bias\"]")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=101

try_load_file 返回同样的错误,Display 是:

try_load_file: Record validation error: Missing tensors: ["encoder.weight", "encoder.bias"]

用 burn-store 的 BurnpackStore 读同一个文件,多了每个张量所在的模块类型:

BurnpackStore: Validation error: Missing tensors: [("encoder.bias", "Struct:NetV2.Struct:Linear"), ("encoder.weight", "Struct:NetV2.Struct:Linear")]

三种写法都只告诉你模型要什么,没说文件里有什么。文件里的 fc.weight、fc.bias 同时也是「没人认领」的条目,但检查按顺序进行,先查到缺的就返回了。

出处

load_file 是 try_load_file 加 .expect("Failed to load module from file"),后者把记录交给 ModuleRecord::apply。apply 用一个 ModuleMapper 走遍模型,走到每个参数时按当前路径从记录里取;取不到的记进 missing,最后依次检查形状错误、缺失、多余三类,默认都不放过:

        if !allow_partial && !mapper.missing.is_empty() {
            return Err(RecordError::Validation(format!(
                "Missing tensors: {:?}",
                mapper.missing
            )));
        }

路径就是字段名拼起来的,#[derive(Module)] 生成的遍历在进出子模块时压栈字段名(存储与加载)。改字段名就是改键;ParamId 也存在记录里,但只用来在加载后还原 id,不参与匹配。

burn-store 的两个 store 失败时给的信息不同。BurnpackStore 的 apply_to 和上面一样只 {:?} 打印缺失列表;SafetensorsStore 在同样的位置把整个 ApplyResult 的 Display 放进错误:

SafetensorsStore: Tensor not found: 
┌─ Tensor Loading Summary ─────────────────────────
│
│ ✓ Successfully applied: 2 tensors
│ ⊘ Skipped (filtered):  0 tensors
│ ✗ Missing in source:    2 tensors
│ ? Unused in target:     2 tensors
│ ! Errors:               0 errors
│
├─ Missing Tensors (requested by model but not found in source)
│
│  First 10 missing tensors:
│    • encoder.bias
│        Did you mean: 'fc.bias'?
│    • encoder.weight
│        Did you mean: 'fc.weight'?
│
├─ Unused Tensors (in source but not used by model)
│
│  First 10 unused tensors:
│    • fc.bias
│    • fc.weight
│
└───────────────────────────────────────────────────

Hint: Use `.allow_partial(true)` on the store to get an `ApplyResult` with structured missing/error info instead of a hard failure.

缺的、多的并排列出,「Did you mean」按路径相似度给出候选。BurnpackStore 想看这份摘要,就按提示开 .allow_partial(true):加载返回 Ok(ApplyResult),打印它得到同样的表格(输出见实验的 partial 情形,内容与上面一致,只是没有最后的 Hint)。注意这时缺的参数保持初始化的随机值,加载算是成功了,只能用来诊断,不能直接拿去用。

📝NOTE
冷知识:那行 Hint 只加在两个 store 上

#5032「fix(store): add .allow_partial(true) hint in TensorNotFound error w/ docs」(首次进入 v0.22.0-pre.1,正式版 0.22.0)的提交说明是:「When a load fails due to missing tensors, the error now includes a hint suggesting .allow_partial(true) for structured diagnostic info.」改动只碰了 pytorch/store.rs 和 safetensors/store.rs 两处 TensorNotFound(摘要表格之前就有,这次在后面加了 Hint)以及 burn-book 的一页。BurnpackStore 的缺失错误不在其中,到 0.22.1 仍是一行 {:?},这就是上面两种输出差别这么大的原因。

修法

字段改名不改文件,就在加载时把键改过来。burn-store 的每个 store 都能挂一个 KeyRemapper(KeyRemapper 的规则见 存储与加载),BurnpackStore 上的写法是 with_remap_pattern,save_file 写出的 .bpk 可以直接用它读:

let mut model = net_v2(&device);
let mut st = BurnpackStore::from_file("out/net.bpk").with_remap_pattern(r"^fc\.", "encoder.");
match model.load_from(&mut st) {
  Ok(r) => println!("with_remap_pattern(^fc\\. -> encoder.): Ok\n{r}"),
  Err(e) => println!("Err {e}"),
}
let same = model.encoder.weight.val().to_data() == old.fc.weight.val().to_data();
println!("encoder.weight == saved fc.weight: {same}");
with_remap_pattern(^fc\. -> encoder.): Ok
┌─ Tensor Loading Summary ─────────────────────────
│
│ ✓ Successfully applied: 4 tensors
│ ⊘ Skipped (filtered):  0 tensors
│ ✗ Missing in source:    0 tensors
│ ? Unused in target:     0 tensors
│ ! Errors:               0 errors
│
└───────────────────────────────────────────────────
encoder.weight == saved fc.weight: true

四个张量全部对上,值和保存时相同。正则要锚定(^fc\.),否则 fc 也会匹配到路径中间别的字段名。之后用新模型 save_file 重存一份,以后的加载就不再需要这条规则。

Module::load_file / ModuleRecord 本身没有改名的手段:要么把字段名改回去,要么像上面这样走 burn-store。

怎么确认没中招

  • 改了任何 #[derive(Module)] 结构体的字段名、或把一个子模块挪进另一个结构体,都等于改了记录的键;改完用旧的 checkpoint 跑一次加载。
  • 加载失败时先换 SafetensorsStore 或 allow_partial(true) 看一眼摘要:「Missing」和「Unused」两栏并排,改名一目了然。
  • 加载成功后打印 ApplyResult:Missing in source 和 Unused in target 都应当是 0。

小结

  • 记录按字段路径匹配参数,字段改名后旧文件读不进来,load_file 报「Missing tensors」,只列模型要的名字。
  • SafetensorsStore 的错误和 allow_partial(true) 的 ApplyResult 带摘要和「Did you mean」;BurnpackStore 的错误只有一行。
  • 用 with_remap_pattern(或 KeyRemapper)在加载时改名,确认四项都对上后用新名字重存。

💬 关联讨论

发起讨论
加载中...