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)。注意这时缺的参数保持初始化的随机值,加载算是成功了,只能用来诊断,不能直接拿去用。
#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)在加载时改名,确认四项都对上后用新名字重存。