Rust in Everything
基于 burn 0.22.1

部署形态

主线位置:部署主线的最后一步,不带自动求导地推理。参数已经存好(见保存与加载),本页比较怎样把「加载 + 推理」交付出去:交付哪些文件、要带什么运行时、启动多快、占多少内存。

版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备)
flowchart TB
  pm["PyTorch 模型"] --> eager["源码加 safetensors"]
  pm --> ts["TorchScript pt"]
  pm --> ex["torch.export pt2"]
  ex --> aoti["AOTInductor pt2"]
  eager --> py["Python 加 torch"]
  ts --> py
  ex --> py
  aoti --> py
  ts --> cpp["C++ 加 libtorch"]
  aoti --> cpp
  bm["burn 模型代码"] --> bin["一个二进制"]
  bin --> wf["读 bpk 或 safetensors"]
  bin --> emb["参数嵌在里面"]
  pm --> onnxf["ONNX 文件"]
  onnxf -->|"burn-onnx 构建时"| gen["生成 Rust 代码"]
  gen --> bin

任务和为什么要转一样:主线 MLP(4→8→3,ReLU)对 4 个样本做一次前向,打印 logits。那一页的 burn 程序按公式现算权重;这一页的每种形态都从文件或包里拿权重,更接近真实部署。

实验代码:PyTorch 侧 export_deploy.py(导出各种形态)、run_pt.py(在新进程里加载一种形态并推理),C++ 侧两个几十行的程序(用 PyTorch wheel 自带的 libtorch 编译),burn 侧 crate store-deploy(default-features = false,feature 只有 std、flex,读 safetensors 的那个二进制另加 store、safetensors;release,strip = true),ONNX 一节另有 crate store-onnx。平台 M2 Max、macOS 27、rustc 1.99.0。

PyTorch:四种形态

导出

model = a1_model()          # eval() 过的主线模型
x = a1_input()

sm = torch.jit.script(model)
sm.save(f"{out}/a1_script.pt")
tm = torch.jit.trace(model, (x,))
tm.save(f"{out}/a1_trace.pt")

ep = torch.export.export(model, (x,))
print(ep.graph_module.code)
torch.export.save(ep, f"{out}/a1_export.pt2")

path = torch._inductor.aoti_compile_and_package(ep, package_path=f"{out}/a1_aoti.pt2")

TorchScript 的两个入口在 2.15.0 里都会发出弃用警告:

warning: FutureWarning `torch.jit.script` is deprecated. Please switch to `torch.compile` or `torch.export`.
warning: FutureWarning `torch.jit.trace` is deprecated. Please switch to `torch.compile` or `torch.export`.

torch.export 录下来的图是 ATen 算子,参数是图的输入:

def forward(self, p_fc1_weight, p_fc1_bias, p_fc2_weight, p_fc2_bias, x):
    linear = torch.ops.aten.linear.default(x, p_fc1_weight, p_fc1_bias);  x = p_fc1_weight = p_fc1_bias = None
    relu = torch.ops.aten.relu.default(linear);  linear = None
    linear_1 = torch.ops.aten.linear.default(relu, p_fc2_weight, p_fc2_bias);  relu = p_fc2_weight = p_fc2_bias = None
    return (linear_1,)

AOTInductor 在本机调用 C++ 编译器,把这张图编译成一个动态库,打进 .pt2 包(整个导出脚本用了约 9 秒)。包里有生成的 C++ 源码和一个 301 424 字节的 wrapper.so。otool -L 只列出 libomp、libc++、libSystem,但 nm -u 显示它有 23 个未定义的 aoti_torch_* 符号(aoti_torch_cpu_addmm_out、aoti_torch_create_tensor_from_blob 等),由加载它的进程里的 libtorch 提供。编译成了本机代码,运行时仍然离不开 libtorch。

ONNX 是第五种导出,但加载它的不是 PyTorch,放到下面「ONNX:PyTorch 导出,burn 生成代码」一节和 burn 一起讲。

加载

每种形态在一个新的 Python 进程里加载、推理(torch.set_num_threads(1)):

if mode == "eager":           # 要模型的 Python 源码,权重从 safetensors 读
  model = Mlp().eval()
  model.load_state_dict(load_file(path))
elif mode == "jit":
  model = torch.jit.load(path)
elif mode == "export":
  model = torch.export.load(path).module()
elif mode == "aoti":
  model = torch._inductor.aoti_load_package(path)
with torch.inference_mode():
  y = model(x)

四种形态的输出和 PyTorch 直接前向的结果逐位相同(compare.py 报最大误差 0)。torch.jit.load 也带弃用警告:

FutureWarning: `torch.jit.load` is deprecated. Please switch to `torch.export`.

TorchScript 和 AOTInductor 的文件还能脱离 Python,用 C++ 加载。wheel 里带了 libtorch 的动态库和头文件,直接拿来编译(要 -std=c++20,用 C++17 编译 torch 的头文件报错):

torch::jit::script::Module m = torch::jit::load(argv[1]);
// ...
auto y = m.forward({x}).toTensor();
torch::inductor::AOTIModelPackageLoader loader(argv[1]);
// ...
auto out = loader.run({x});

两个程序的输出和 PyTorch 的最大差 2.72e-10,这是 C++ 侧按 9 位有效数字打印造成的,不是计算差异。它们链接 libtorch、libtorch_cpu、libc10,libtorch_cpu 又链接 libomp。

burn:一个二进制

burn 没有「把模型存成图、换个运行时加载」这一步。模型的结构和 forward 是 Rust 代码,部署就是把它和后端编译成一个程序。依赖只开推理要用的,设备是不带 autodiff 的 Device::flex(),整个程序里不记图(见不求导的区域):

[dependencies]
burn = { version = "=0.22.1", default-features = false, features = ["std", "flex"] }
serde_json = "1"

[features]
st = ["burn/store", "burn/safetensors"]

[profile.release]
strip = true

三个二进制,同一个 Mlp 和 run(建输入、前向、打印 JSON),只差参数从哪来:

// infer-file:从 save_file 写的 .bpk 读
let model = match Mlp::new(&device).try_load_file(&path) { /* ... */ };
run(&model, &device);
// infer-embed:参数用 include_bytes! 编进二进制
static WEIGHTS: &[u8] = include_bytes!("../../../store-out/a1.bpk");

let record = match ModuleRecord::from_bytes(Bytes::from_bytes_vec(WEIGHTS.to_vec())) { /* ... */ };
let model = match Mlp::new(&device).try_load_record(record) { /* ... */ };
run(&model, &device);
// infer-st:直接读 PyTorch 写的 safetensors
let mut model = Mlp::new(&device);
let mut st = SafetensorsStore::from_file(&path).with_from_adapter(PyTorchToBurnAdapter);
if let Err(e) = model.load_from(&mut st) { /* ... */ }
run(&model, &device);

ModuleRecord::from_bytes 从内存里的 burnpack 字节建记录,和 load_file 读文件是同一个格式。infer-embed 交付的就只有一个文件。三个程序的输出和 PyTorch 比,最大误差都是 1.49e-8(容差 1e-6),来自 matmul 的求和顺序(见导入 PyTorch 权重);otool -L 只列出 libSystem 和 libiconv。

ONNX:PyTorch 导出,burn 生成代码

ONNX 是两边都认的图格式。PyTorch 用 torch.onnx.export 导出(虚拟环境里装了 onnx 1.23.2 和 onnxscript 0.7.2,用默认的导出器,它内部先走 torch.export):

prog = torch.onnx.export(model, (x,), f"{out}/a1.onnx", input_names=["x"], output_names=["logits"])
m = onnx.load(f"{out}/a1.onnx")
print("opset:", [(o.domain or "ai.onnx", o.version) for o in m.opset_import])
print("inputs:", [(i.name, [d.dim_value for d in i.type.tensor_type.shape.dim]) for i in m.graph.input])
print("initializers:", [(t.name, list(t.dims)) for t in m.graph.initializer])
print("nodes:", [n.op_type for n in m.graph.node])
W1030 10:38:21.708000 11432 torch/onnx/_internal/exporter/_registration.py:107] torchvision is not installed. Skipping torchvision::nms
...
[torch.onnx] Obtain model graph for `Mlp([...]` with `torch.export.export(..., strict=False)`...
[torch.onnx] Obtain model graph for `Mlp([...]` with `torch.export.export(..., strict=False)`... ✅
[torch.onnx] Run decompositions...
[torch.onnx] Run decompositions... ✅
[torch.onnx] Translate the graph into ONNX...
[torch.onnx] Translate the graph into ONNX... ✅
[torch.onnx] Optimize the ONNX graph...
[torch.onnx] Optimize the ONNX graph... ✅
opset: [('ai.onnx', 20)]
inputs: [('x', [4, 4])]
initializers: [('fc1.weight', [8, 4]), ('fc1.bias', [8]), ('fc2.weight', [3, 8]), ('fc2.bias', [3])]
nodes: ['Gemm', 'Relu', 'Gemm']

开头四行 torchvision is not installed 的警告这里只留了第一行。导出的文件 4.1 KiB(另有一个 0 字节的 a1.onnx.data),图是 Gemm、Relu、Gemm 三个节点,两个 Gemm 都带 transB=1:权重按 PyTorch 的 [out, in] 存,乘之前转置。输入形状固定为导出时样例的 [4, 4]。

burn 一侧不在运行时解析 ONNX。ONNX 的导入不在 burn 仓库里,0.22.x 对应的是单独发布的 crate burn-onnx 0.22.0(依赖 burn ^0.22.0,和 0.22.1 一起用)。它在构建脚本里把 .onnx 生成 Rust 源码和一个 .bpk 权重文件,程序把生成的源码 include! 进来:

[dependencies]
burn = { version = "=0.22.1", default-features = false, features = ["std", "flex"] }
burn-store = { version = "=0.22.1", default-features = false, features = ["std"] }
serde_json = "1"

[build-dependencies]
burn-onnx = "=0.22.0"

[features]
embed = []
// build.rs
use burn_onnx::{LoadStrategy, ModelGen};

fn main() {
  let strategy = if std::env::var("CARGO_FEATURE_EMBED").is_ok() {
    LoadStrategy::Embedded
  } else {
    LoadStrategy::File
  };
  ModelGen::new()
    .input("model/a1.onnx")
    .out_dir("model/")
    .load_strategy(strategy)
    .run_from_script();
}
// src/main.rs
mod model {
  include!(concat!(env!("OUT_DIR"), "/model/a1.rs"));
}

let model = model::Model::default();
let xs: Vec<f32> = (0..16).map(|k| (0.1 * k as f64 - 0.5) as f32).collect();
let x = Tensor::<2>::from_data(TensorData::new(xs, [4, 4]), &device);
let y = model.forward(x);

生成的代码(默认的 LoadStrategy::File,2 198 字节,下面略去了加载函数的函数体):

// Generated from ONNX "model/a1.onnx" by burn-onnx
// ...
#[derive(Module, Debug)]
pub struct Model {
    linear1: Linear,
    linear2: Linear,
    #[module(skip)]
    device: Device,
}

impl Default for Model {
    fn default() -> Self {
        Self::from_file(
            "/.../target-store/release/build/store-onnx-03ca177b5fc039b9/out/model/a1.bpk",
            &Default::default(),
        )
    }
}
// ... from_file 和 from_bytes 用 BurnpackStore 加载,失败就 panic ...
impl Model {
    #[allow(unused_variables)]
    pub fn new(device: &Device) -> Self {
        let linear1 = LinearConfig::new(4, 8).with_bias(true).init(device);
        let linear2 = LinearConfig::new(8, 3).with_bias(true).init(device);
        Self {
            linear1,
            linear2,
            device: device.clone(),
        }
    }

    #[allow(clippy::let_and_return, clippy::approx_constant)]
    pub fn forward(&self, x: Tensor<2>) -> Tensor<2> {
        let linear1_out1 = self.linear1.forward(x);
        let relu1_out1 = burn::tensor::activation::relu(linear1_out1);
        let linear2_out1 = self.linear2.forward(relu1_out1);
        linear2_out1
    }
}

(路径里构建目录之前的部分换成了 /...。)生成的就是一个普通的 #[derive(Module)] 结构体,和手写的 Mlp 几乎一样:Gemm 加 transB=1 被认成 Linear,权重转成 burn 的 [in, out] 存进 1 292 字节的 a1.bpk。字段名来自 ONNX 节点(linear1、linear2),不是 PyTorch 的 fc1、fc2;输入形状 [4, 4] 没有进代码,forward 只要求秩是 2。构建时有一条警告,原文:

warning: associated function `from_bytes` is never used

前向和 PyTorch 比:

logits                   n= 12  max|diff|=1.49e-08
worst=1.49e-08 tol=1e-06 OK

和手写的 burn 模型一样,最大误差 1.49e-8,容差 1e-6。

部署时要注意 Model::default()。File 策略下它读的是构建时 OUT_DIR 里那个 .bpk 的绝对路径(burn-onnx 的 crate 文档也说「That is fine for development; ship the .bpk alongside your binary and call from_file (or use Embedded) for anything you distribute」)。把那个文件挪开再运行:

thread 'main' (8695750) panicked at /.../out/model/a1.rs:39:17:
Failed to load burnpack file /.../out/model/a1.bpk: I/O error: cannot open '/.../out/model/a1.bpk': No such file or directory (os error 2)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit 101

(同样把构建目录的前缀换成了 /...。)换到另一台机器上就是这个结果。打开 embed feature 让构建脚本用 LoadStrategy::Embedded:生成的代码用 include_bytes! 把 .bpk 编进二进制,Default 改为调用 from_embedded。release、strip 之后,File 版 3 078 352 字节,Embedded 版 3 078 768 字节,都只链接 libSystem 和 libiconv,结果都和 PyTorch 差 1.49e-8。Embedded 版连跑 20 次最短 2.9 毫秒(同一轮手写的 infer-embed 2.8 毫秒),峰值 RSS 3.0 MiB。

burn-onnx 0.22.0 另有一个默认关闭的 export feature,按 crate 文档是把 burn 模块的 forward 录下来写成 ONNX(实验性);本页没有试。

交付什么

形态模型文件程序运行时
PyTorch 源码 + safetensors540 B模型的 Python 源码Python 加 torch 及其依赖,728.5 MiB(为什么要转)
TorchScript,Python 加载5 507 B(trace:6 666 B)加载脚本同上
torch.export,Python 加载11 403 B加载脚本同上
AOTInductor,Python 加载356 163 B加载脚本同上
TorchScript,C++ 加载5 507 B68 KiB 的可执行文件libtorch 四个动态库 404.2 MiB
AOTInductor,C++ 加载356 163 B47 KiB 的可执行文件同上
burn infer-file.bpk 1 292 B2.76 MiB无(只用 macOS 自带的库)
burn-onnx 生成,Embedded无,在程序里2.94 MiB无
burn infer-embed无,在程序里2.74 MiB无
burn infer-stsafetensors 540 B2.97 MiB无

libtorch 那一行是 C++ 程序实际链接的四个库:libtorch_cpu.dylib 421 820 912 字节,libc10.dylib 1 142 496,libomp.dylib 856 096,libtorch.dylib 34 800,合计 423 854 304 字节。这是 macOS 上只带 CPU 的构建;Linux 上带 CUDA 的 libtorch 本页没有量。

几种 PyTorch 的模型文件都比 safetensors 大。它们都是 ZIP 包:TorchScript 里除了 4 个参数,还有 code/__torch__/ 下面序列化成类 Python 源码的 forward(a1.py、linear.py 等)和调试信息;torch.export 的包里有 5 629 字节的 models/model.json(图)和 1 641 字节的 sample_inputs/model.pt(导出时用的样例输入);AOTInductor 的包里主要是那个 .so。不管哪种,运行时都是同一个 libtorch。burn 一侧没有运行时要另装,代价是换一个模型就要重新编译一个程序。

启动要多久

每个命令先跑一次让文件进入系统缓存,再连续跑 20 次,量从启动子进程到它退出的墙钟时间。量的时候机器上还有别的任务(uptime 的 1 分钟负载在 5 到 15 之间)。

torch eager + safetensors    n=20  min=   649.4 ms  median=   659.3 ms
TorchScript (script)         n=20  min=   666.1 ms  median=   682.4 ms
torch.export .pt2            n=20  min=  1244.9 ms  median=  1264.5 ms
AOTInductor .pt2             n=20  min=  1280.6 ms  median=  1313.5 ms
burn infer-file + .bpk       n=20  min=     3.0 ms  median=     3.5 ms
burn infer-embed             n=20  min=     2.7 ms  median=     3.1 ms
burn infer-st + safetensors  n=20  min=     2.8 ms  median=     3.1 ms
C++ libtorch + TorchScript   n=20  min=   138.4 ms  median=   145.7 ms
C++ libtorch + AOTInductor   n=20  min=   117.8 ms  median=   129.5 ms
burn infer-file + .bpk       n=20  min=     2.9 ms  median=     3.5 ms

Python 进程里分段计时(time.perf_counter):

eager   import   525.5 ms  load     2.2 ms  forward#1   0.08 ms
jit     import   525.2 ms  load     5.9 ms  forward#1  14.37 ms
export  import   511.0 ms  load   475.4 ms  forward#1   0.27 ms
aoti    import   512.1 ms  load   482.2 ms  forward#1   0.61 ms
  • 四种形态都先付约 0.5 秒的 import torch。
  • torch.export.load 自己又花了约 0.48 秒:在一个已经 import 了 torch 的进程里数 sys.modules,加载前后多出 868 个模块,其中 sympy 420 个、torch.distributed 115 个、torch._dynamo 97 个。aoti_load_package 同样测一次:477 毫秒,多出 896 个模块。所以在 Python 里,「导出成图」比直接用源码加载慢一倍。
  • TorchScript 的第一次 forward 用了 14 毫秒,其余形态不到 1 毫秒。
  • 去掉 Python,C++ 加 libtorch 的启动约 120 到 140 毫秒;同一个前向,burn 约 3 毫秒。

对常驻服务,这些只付一次;对命令行工具、每个请求起一个进程的场合,就是每次都要付。

占多少内存

/usr/bin/time -l 的峰值 RSS 和 peak memory footprint,每个各跑 3 次,3 次之间差不到 2%,下面取第一次:

形态峰值 RSSpeak memory footprint
Python:源码 + safetensors208.7 MiB149.6 MiB
Python:TorchScript213.0 MiB152.9 MiB
Python:torch.export291.4 MiB231.5 MiB
Python:AOTInductor290.5 MiB231.0 MiB
C++ 加 libtorch:TorchScript101.1 MiB58.9 MiB
C++ 加 libtorch:AOTInductor94.1 MiB53.9 MiB
burn infer-file2.8 MiB1.3 MiB
burn infer-embed2.8 MiB1.3 MiB
burn infer-st3.0 MiB1.3 MiB

多出来的约 80 MiB 只出现在 torch.export 和 AOTInductor 两行,也就是加载时多 import 了近 900 个模块的那两种。这个模型只有 67 个参数,所有数字都是固定开销;模型大了,两边都要再加上参数和中间结果。

burn 也能录图

burn 0.22.1 并非完全没有「图」。打开 capture feature 后有一个只记录、不计算的设备 Device::capture,在 capture_scope 里跑一遍普通的 forward,得到一个 CapturedGraph:按执行顺序排列的操作,加上录下的参数值。用上面从 .bpk 读出的主线模型试一次(crate store-capture,feature std、flex、capture、extension,另加依赖 burn-capture = "=0.22.1"):

let device = Device::capture();
let captured = device.capture_scope(|scope| {
  let model = model.clone().to_device(&device);
  let input = Tensor::<2>::zeros([4, 4], &flex).to_device(&device);
  let output = model.forward(input.clone());
  let (i, o) = (id(&input), id(&output));
  scope.complete(i, o)
});
inputs [TensorId { value: 4 }] outputs [TensorId { value: 11 }]
12 operations, 5 recorded values
  Init(InitOperationIr { out: TensorIr { id: TensorId { value: 0 }, shape: Shape { dims: [4, 8] }, status: NotIn
  Init(InitOperationIr { out: TensorIr { id: TensorId { value: 1 }, shape: Shape { dims: [8] }, status: NotInit,
  Init(InitOperationIr { out: TensorIr { id: TensorId { value: 2 }, shape: Shape { dims: [8, 3] }, status: NotIn
  Init(InitOperationIr { out: TensorIr { id: TensorId { value: 3 }, shape: Shape { dims: [3] }, status: NotInit,
  Init(InitOperationIr { out: TensorIr { id: TensorId { value: 4 }, shape: Shape { dims: [4, 4] }, status: NotIn
  Float(F32, Matmul(MatmulOpIr { lhs: TensorIr { id: TensorId { value: 4 }, shape: Shape { dims: [4, 4] }, statu
  BaseFloat(Reshape(ShapeOpIr { input: TensorIr { id: TensorId { value: 1 }, shape: Shape { dims: [8] }, status:
  NumericFloat(F32, Add(BinaryOpIr { lhs: TensorIr { id: TensorId { value: 5 }, shape: Shape { dims: [4, 8] }, s
  Activation(Relu(UnaryOpIr { input: TensorIr { id: TensorId { value: 7 }, shape: Shape { dims: [4, 8] }, status
  Float(F32, Matmul(MatmulOpIr { lhs: TensorIr { id: TensorId { value: 8 }, shape: Shape { dims: [4, 8] }, statu
  BaseFloat(Reshape(ShapeOpIr { input: TensorIr { id: TensorId { value: 3 }, shape: Shape { dims: [3] }, status:
  NumericFloat(F32, Add(BinaryOpIr { lhs: TensorIr { id: TensorId { value: 9 }, shape: Shape { dims: [4, 3] }, s

(每行截到前 110 个字符。)和 torch.export 的图对比:PyTorch 录的是 aten.linear,burn 录的是 Linear 展开后的 matmul、偏置 reshape、add;参数(id 0 到 3)和 torch.export 一样不算图的输入,值记在 values 里,图的输入只有 id 4。GraphIr 实现了 serde 的 Serialize / Deserialize,但本页没有在 0.22.1 里找到像 torch.export.load 那样把它读回来执行的现成入口;burn-capture 的 crate 文档说它是给「tools that need a model's computation as a graph, such as exporters」用的。

📝NOTE
冷知识:burn 的 ONNX 导出写在这个 PR 里,又被搬走了

burn-capture 来自 #5377「feat(capture): add graph capture backend producing GraphIr」(首次发布于 v0.22.0-pre.3)。它的提交记录里先有「Add core ONNX export from existing GraphIr」「Add module ONNX export (static)」「Lower batch norm to ONNX BatchNormalization」「Add OnnxModel w/ save」等一串提交,最后两条是「Remove burn-onnx-export (moved into burn-onnx)」「rm burn-onnx-export」:导出器写好以后搬进了单独的 burn-onnx 仓库(burn-onnx 0.22.0 里默认关闭的 export feature),burn 仓库只留下录图的这一半。ONNX 的导入早就这样搬过一次:#4361「Move ONNX import to burn-onnx crate」把 burn-import 改名为 burn-onnx,一周后 #4393「Move ONNX crates to burn-onnx repository」把它移出 burn 仓库(两者都首次发布于 v0.21.0-pre.1)。所以 burn 0.22.1 的仓库和 burn crate 里都没有 ONNX 的导入导出;0.22.x 的导入单独发布为 crate burn-onnx 0.22.0,用法见上面的 ONNX 一节。

怎么选

  • 要在 Python 里服务、模型代码就在手边:直接用源码加 safetensors。2.15.0 里它比 torch.export 和 AOTInductor 启动快一倍、少占约 80 MiB,结果逐位相同。
  • 要脱离 Python:TorchScript 或 AOTInductor 加 C++ libtorch。TorchScript 在 2.15.0 已经标为弃用。运行时至少带上约 404 MiB 的 libtorch,启动约 120 毫秒。
  • 要一个不依赖任何运行时的文件:burn。模型可以用 Rust 重写(权重直接读 PyTorch 的文件,见导入 PyTorch 权重),也可以先导出 ONNX、让 burn-onnx 生成代码;两种都要求用到的算子 burn 有(见为什么要转),换模型要重新编译。生成代码时用 Embedded,或者自己带上 .bpk 调 from_file,不要依赖 Model::default() 记下的构建路径。

小结

  • PyTorch 四种形态(源码 + safetensors、TorchScript、torch.export、AOTInductor)结果逐位相同,运行时都是 libtorch;Python 里启动 0.65 到 1.3 秒,torch.export 和 AOTInductor 的加载额外 import 近 900 个模块,多约 0.48 秒、约 80 MiB。
  • C++ 加 libtorch 能跑 TorchScript 和 AOTInductor,要带约 404 MiB 的动态库,启动约 120 到 140 毫秒、峰值 RSS 约 100 MiB。AOTInductor 的 .so 有 23 个符号要 libtorch 提供。
  • burn 的部署是一个 2.7 到 3.0 MiB、只链接系统库的二进制,读 .bpk、读 PyTorch 的 safetensors、或用 include_bytes! 把参数嵌进去都行,启动约 3 毫秒、峰值 RSS 约 2.8 MiB,结果和 PyTorch 最大差 1.49e-8。
  • PyTorch 导出的 ONNX(Gemm、Relu、Gemm)经 burn-onnx 0.22.0 在构建时生成一个普通的 #[derive(Module)] 模型和 .bpk,结果和 PyTorch 最大差 1.49e-8;File 策略的 Model::default() 写死了构建目录里的路径,分发时用 Embedded。
  • burn 0.22.1 能用 Device::capture 把 forward 录成 GraphIr;ONNX 的导入导出不在 burn 仓库,在单独发布的 burn-onnx。

💬 关联讨论

发起讨论
加载中...