Failed to load burnpack file
主线位置:部署主线的「加载」一步,模型来自 PyTorch 导出的 ONNX。代码在构建时生成,权重文件在哪里读,也是构建时定的。
版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备)、burn-onnx 0.22.0
flowchart TB onnx["a1.onnx"] --> gen["build.rs ModelGen"] gen --> rs["a1.rs"] gen --> bpk["OUT_DIR 里的 a1.bpk"] rs -->|"File 策略"| def["default 读绝对路径"] def -->|"本机"| ok["能跑"] def -->|"别的机器"| boom["panic"] rs -->|"Embedded 策略"| emb["include_bytes"] emb --> ok2["到哪都能跑"]
实验代码:PyTorch 侧 pit_export.py(torch.export 存取);burn 侧 crate pitb-store-onnx,构建依赖 burn-onnx = "=0.22.0",build.rs 用 ModelGen 把 部署形态里导出的 a1.onnx 生成代码,feature embed 时用 LoadStrategy::Embedded(burn default-features = false,feature std、flex;release,strip = true)。平台 M2 Max、macOS 27、rustc 1.99.0。burn-onnx 不在 burn 仓库里,本页引用的是 crates.io 上 burn-onnx 0.22.0 的源码,没有行号链接。
PyTorch 的写法
PyTorch 的部署产物是「程序 + 模型文件」,模型文件的路径在运行时给出。用 torch.export 存一个 .pt2,复制到一个新目录,另一个进程按路径加载:
if sys.argv[1] == "save":
ep = torch.export.export(a1_model(), (a1_input(),))
torch.export.save(ep, sys.argv[2])
else:
ep = torch.export.load(sys.argv[2])
with torch.no_grad():
print(json.dumps({"logits": ep.module()(a1_input()).tolist()}))
在复制过去的目录里 load a1.pt2,结果和原模型逐位相同(max|diff|=0.00e+00)。换一个工作目录再用同样的相对路径:
...
W1030 11:14:50.525000 32070 torch/export/__init__.py:403] RuntimeError: open file failed because of errno 2 on fopen: No such file or directory, file path: a1.pt2
FileNotFoundError: [Errno 2] No such file or directory: 'a1.pt2'
(前面十几行是 torch.export.load 打印的回溯警告,这里略去。)找不到文件的情况只有一种:运行时给的路径不对,而路径是使用者自己给的。ONNX 那条路也一样,onnxruntime.InferenceSession(path) 在运行时接路径(虚拟环境里没有装 onnxruntime,这一条没有实测)。
搬到 burn 的现象
burn-onnx 在构建脚本里把 ONNX 变成 Rust 源码和一个 .bpk 权重文件,生成步骤见部署形态的 ONNX 一节。程序照 burn-onnx 文档的例子写:
let model = model::Model::default();
let y = model.forward(x);
release 构建之后,把二进制复制到实验目录下的 ship/,在那里运行:
logits n= 12 max|diff|=1.49e-08
worst=1.49e-08 tol=1e-06 OK
和 PyTorch 的前向差 1.49e-8,看起来已经是一个可以分发的二进制(3 078 912 字节)。看一下二进制里的字符串:
...linear1Struct:Modellinear2weightStruct:Linear/.../target-pitb/release/build/pitb-store-onnx-f1fb7cfbc1db81d1/out/model/a1.bpkShape
权重的位置是构建目录 OUT_DIR 里的绝对路径(本页的输出里,这个路径中构建目录之前的部分都换成了 /...)。在本机上,复制到哪里这个文件都在,所以测不出问题。模拟「另一台机器」:把构建目录里的 a1.bpk 改名,再运行 ship/ 里的同一个二进制:
thread 'main' (8794067) panicked at /.../target-pitb/release/build/pitb-store-onnx-f1fb7cfbc1db81d1/out/model/a1.rs:39:17:
Failed to load burnpack file /.../target-pitb/release/build/pitb-store-onnx-f1fb7cfbc1db81d1/out/model/a1.bpk: I/O error: cannot open '/.../target-pitb/release/build/pitb-store-onnx-f1fb7cfbc1db81d1/out/model/a1.bpk': No such file or directory (os error 2)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
容器镜像里只拷了二进制、在 CI 机器上构建再发到服务器、本机 cargo clean 之后运行旧的二进制,都是「这个路径上没有文件」,本页只用改名模拟了这一种情形。
另一个看起来能绕过去的写法更糟。生成的代码里有 Model::new(&device),不读任何文件,不会 panic:
logits n= 12 max|diff|=5.30e-01
worst=5.30e-01 tol=1e-06 MISMATCH
它返回的是随机初始化的参数,程序照常输出,结果和 PyTorch 差 0.53。
原因
ModelGen 默认的 LoadStrategy 是 File(burn-onnx 0.22.0 src/import/model_gen.rs 里 #[default] File),文档写着它「Generates from_file(), from_bytes(), and a Default impl that calls from_file()」。生成 Default 的是 src/import/burn/graph.rs 里的 register_burnpack_loaders,把构建时 .bpk 的路径原样写进源码:
LoadStrategy::File => {
let file = path_to_str(&file);
// ...
default_impl = quote! {
impl Default for Model {
fn default() -> Self {
Self::from_file(#file, &Default::default())
}
}
_blank_!();
};
在 build.rs 里用 run_from_script 时,这个路径是 OUT_DIR 下的绝对路径。生成出来的就是:
impl Default for Model {
fn default() -> Self {
Self::from_file(
"/.../target-pitb/release/build/pitb-store-onnx-f1fb7cfbc1db81d1/out/model/a1.bpk",
&Default::default(),
)
}
}
from_file 在 load_from 失败时直接 panic!("Failed to load burnpack file {}: {e}", ..),没有 Result 版本。burn-onnx 的 crate 文档把这两点都写明了:Model::default()「uses the default device and, with LoadStrategy::File, the .bpk path recorded at generation time」,「That is fine for development; ship the .bpk alongside your binary and call from_file (or use Embedded) for anything you distribute」;Model::new(&device)「only builds the module structure: layers get fresh random parameters and ONNX constants read as zeros. Never run a model built with new alone」。
和 PyTorch 的区别在于时机:PyTorch 的模型文件是运行时的输入;burn-onnx 的模型是编译进程序的 Rust 代码,权重文件的位置在构建时就定了,构建机器上的测试发现不了。
改法
把权重编进二进制
build.rs 里换成 LoadStrategy::Embedded(本页的 crate 用 feature embed 切换):
ModelGen::new()
.input("model/a1.onnx")
.out_dir("model/")
.load_strategy(LoadStrategy::Embedded)
.run_from_script();
生成的代码用 include_bytes! 在编译时读入 .bpk,Default 改为调用 from_embedded。同样复制到 ship/、把构建目录里的 a1.bpk 改名后运行:
logits n= 12 max|diff|=1.49e-08
worst=1.49e-08 tol=1e-06 OK
二进制 3 079 328 字节。注意 Embedded 不生成 from_file,原来调用它的代码会编译失败:
error[E0599]: no associated function or constant named `from_file` found for struct `Model` in the current scope
--> src/main.rs:17:32
|
17 | Some(p) => model::Model::from_file(p, &device),
| ^^^^^^^^^ associated function or constant not found in `Model`
...
自带 .bpk,按二进制所在的目录找
权重要单独更新、或者模型很大不想编进二进制时,保留 File 策略,把 .bpk 和二进制放在一起,不用 Model::default(),自己调 from_file。路径不要写相对路径:和 PyTorch 一样,相对路径按当前工作目录解析,从别的目录启动就找不到:
thread 'main' (...) panicked at /.../target-pitb/release/build/pitb-store-onnx-f1fb7cfbc1db81d1/out/model/a1.rs:39:17:
Failed to load burnpack file ./a1.bpk: I/O error: cannot open './a1.bpk': No such file or directory (os error 2)
(线程号换成了 ...。)按可执行文件所在的目录拼路径:
let dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf()));
match dir {
Some(d) => model::Model::from_file(d.join("a1.bpk"), &device),
None => {
eprintln!("cannot locate the executable");
std::process::exit(2);
}
}
构建目录里的 a1.bpk 改名、从另一个工作目录启动 ship/ 里的二进制(旁边放着 a1.bpk),结果和 PyTorch 差 1.49e-8;同一个二进制不带参数走 Model::default(),仍是上面那个 panic。from_file 的第二个参数是设备,这也顺带避开了 Default 只用默认设备的问题。
问: Embedded 版的二进制里还能搜到构建目录里 a1.bpk 的绝对路径,它运行时是不是还会去读?
答: 不会。那个字符串只出现在 from_embedded 的 panic 信息里(「Failed to load embedded burnpack (built from …)」),权重本身由 include_bytes! 在编译时放进二进制。上面那次运行时构建目录里的 a1.bpk 已经改名,结果照样对得上。它带出来的只是构建机器上的路径。
怎么检查
- 在构建机器上测不出来。测试分发产物时,先把构建目录挪开(或
cargo clean),或者在一台干净的机器、一个只放了二进制的容器里运行。 strings <二进制> | grep '\.bpk':File策略下能看到构建目录的绝对路径,就说明Model::default()会去读它。- 代码里搜
Model::default()和Model::new(:前者只该出现在Embedded策略下,后者不该出现在推理路径上。 - 部署后和 PyTorch 的输出对一次数(
compare.py),随机参数的模型也能正常输出,只有对数能发现。
小结
- burn-onnx 0.22.0 的默认
LoadStrategy::File让Model::default()读构建时OUT_DIR里.bpk的绝对路径;本机怎么复制都能跑,构建目录不在了就 panic:Failed to load burnpack file ...: No such file or directory。 Model::new不 panic,但参数是随机的,和 PyTorch 差 0.53。- 分发时用
LoadStrategy::Embedded(Default改调from_embedded,没有from_file),或者带上.bpk,按current_exe()所在目录调from_file;两种都和 PyTorch 差 1.49e-8。 - PyTorch 的模型文件是运行时输入,burn-onnx 的权重位置在构建时定死;两边的相对路径都按当前工作目录解析。