Rust in Everything
基于 burn 0.22.1

存储与加载

主线位置:训练主线的出口。训练出来的参数要离开进程(存盘、换机器、给推理用),别处训练的权重要进来(PyTorch、Hugging Face 的 safetensors)。Learner 的 checkpoint 也走这里,见 Learner 与数据。参数本身(Param、ParamId、#[derive(Module)] 生成的遍历)见 Module 派生。

flowchart TB
  model["Module"] -->|"into_record 遍历"| rec["ModuleRecord"]
  rec -->|"burn-pack Writer"| bpk[".bpk 文件"]
  bpk -->|"按路径对回"| model
  model -->|"collect"| pt["burn_pack Tensor 列表"]
  pt --> st["SafetensorsStore"]
  st --> sf[".safetensors"]
  torch[".pt 文件"] --> reader["pytorch-reader"]
  reader --> pt2["burn_pack Tensor 列表"]
  sf --> pt2
  pt2 --> remap["KeyRemapper 改名"]
  remap --> adapt["Adapter 转置 改名"]
  adapt -->|"apply 遍历"| model

0.22 里存取分两处。burn-core 的 store 模块很小,只做一件事:把模块参数存成 burnpack、原样读回;模块注释说它「intentionally tiny」,过滤、改名、PyTorch 和 safetensors 适配都在 burn-store crate(store feature)。两边共用 burn-pack crate 定义的文件格式和 burn_pack::Tensor 这个传输类型。实验 crate 是 train-store(从模板 base 复制,加了直接依赖 safetensors = "=0.8.0",Flex,debug),平台 M2 Max、macOS 27、rustc 1.99.0,输出存在 train-store/out-logs/。

Module 上的六个方法

方法做什么
into_record遍历模块,收集所有张量参数成一个 ModuleRecord
into_record_group只收一个 ParamGroup 里的参数,组外的不读数据
try_load_record / load_record把记录套回模块;后者出错就 panic("Failed to load record")
save_fileinto_record().save(path)
load_file / try_load_fileModuleRecord::load(path) 再 try_load_record

ModuleRecord 不泛型:0.21 的 burn-core/src/record/(Record trait、Recorder<B> 以及各种格式和精度的 recorder)和派生宏为每个模块生成的记录类型(burn-derive/src/module/record*.rs)都删掉了(#5083「Refactor Record - Serialization & Deserialization of Module, Optimizer & LrScheduler」)。Flag 一类模块自带的控制值不进记录,加载后保留目标模块自己的值。

ModuleRecord:按路径存,按路径对回

收集用一个 ModuleVisitor(Collector),进出子模块时压栈出栈字段名,每个参数记下「路径、ParamId、TensorData」三样:

    // Record the `on_save` form: it is what the load side validates against and
    // un-maps with `on_load`. Recording `val()` breaks any param whose mapper
    // changes the shape (a `Col`-layout `Linear` weight).
    fn visit_float<const D: usize>(&mut self, param: &Param<Tensor<D>>) {
        self.record(param.id, || param.transform_for_save().val().into_data());
    }

存的是 transform_for_save 之后的形状:Param 可以带保存、加载、初始化三个映射函数,LinearConfig::with_layout(LinearLayout::Col) 的 Linear 就靠它们在内存里和文件里用互为转置的两种布局(见 nn 模块)。读数据(val().into_data())放在闭包里,被 ParamGroup 过滤掉的参数不会去读。

加载是一个 ModuleMapper(ModuleRecordMapper):把记录按路径放进 HashMap<String, (ParamId, TensorData)>,map 走到每个参数时按当前路径 remove_entry,take 里分三种情况:

  • 路径不在表里:记进 missing,参数保持原样。
  • 形状和参数的 lazy_shape() 不同:记一条 "{path}: shape mismatch, expected .. but record has ..",参数保持原样。
  • 对上了:按 DTypePolicy 定 dtype(默认 FromRecord,用文件里的 dtype;CastToModule 转成参数现在的 dtype),建张量,再 transform_for_load(tensor, record_id):值换成记录里的,ParamId 也换成记录里的。

遍历完,表里剩下的就是没有参数认领的条目。apply 按顺序检查三类问题,各有一个开关:

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

默认三项都查:validate = true、allow_partial = false、allow_unused = false。allow_unused 的文档注释说明了为什么连「多出来的条目」也默认拒绝:记录里有条目落空,说明这不是取出记录的那个模块,看起来成功的加载其实少做了事。

恢复 ParamId 是为了续训:优化器状态按 ParamId 存(见优化器),新建模型时的 id 是随机的,只有换回记录里的 id,读回来的优化器状态才找得到自己的参数。这一点由 #5177(「fix(store): restore persisted ParamId on load_record and Applier apply」)补上,首次进入 v0.22.0-pre.1。

burnpack 格式

ModuleRecord::save 把每个条目变成 burn_pack::Tensor::new(path, dtype, shape, Some(id), bytes) 交给 burn_pack::Writer。格式写在 burn-pack 的 crate 文档里,三段:

段内容
头,10 字节MAGIC_NUMBER 0x4255524E("BURN",小端写出来是 NRUB)、u16 格式版本(现在是 1)、u32 元数据长度
元数据,CBOR每个张量的 dtype、shape、在数据段里的 (start, end)、可选的 param_id;字符串键值对;可选的标量表(优化器状态的 time 就存这里)
数据段先补齐到 256 字节边界(aligned_data_section_start),每个张量的起点也按 TENSOR_ALIGNMENT = 256 对齐,读文件时可以直接内存映射、零拷贝

256 的理由写在常量的注释里:f64 要 8 字节对齐、CPU 缓存行 64 字节、CUDA 偏好 256 字节合并访问,并列出 GGUF、MLX 等十几种格式也用 256,safetensors 是 64。读取方还有一组防恶意文件的上限:元数据最多 100 MB、单个张量最多 10 GB(32 位平台 2 GB)、最多 100 000 个张量、CBOR 嵌套最多 128 层、文件最多 100 GB。和 safetensors 比,多出来的是 param_id 和标量表:前者让模型记录能恢复 ParamId,后者让优化器和学习率调度器的状态也能用同一种文件。

实验 train-store roundtrip:两层的 Net { fc: Linear(4→3), head: Linear(3→2, 无偏置) },save_file 后读文件头,再用 BurnpackStore 列出条目,最后读回一个新建的模型:

#[derive(Module, Debug)]
struct Net {
  fc: Linear,
  head: Linear,
}
// ...
  model.clone().save_file("out/net.bpk").unwrap();
  // ...read the 10-byte header by hand, then list entries with BurnpackStore...
  let fresh = Net::new(device);
  println!("fresh fc.weight id = {}", fresh.fc.weight.id);
  let loaded = fresh.load_file("out/net.bpk");
ids: fc.weight=avq43klip2pc6 fc.bias=edntuk51pn2co head.weight=22h48qbgpqs2o
out/net.bpk: 792 bytes, magic "NRUB", version 1, metadata 210 bytes
  fc.bias: F32 Shape { dims: [3] } param_id=Some(14755144371902836595)
  fc.weight: F32 Shape { dims: [4, 3] } param_id=Some(14101554053607715927)
  head.weight: F32 Shape { dims: [3, 2] } param_id=Some(3222552515581354512)
fresh fc.weight id = etelnddevaf54
loaded fc.weight id = avq43klip2pc6, values equal: true

条目名就是模块里的字段路径。ParamId 在文件里是 u64,打印出来的 avq43klip2pc6 是同一个值的 base32 编码(ParamId 的 Display)。新建模型的 id 是 etelnddevaf54,加载后变回保存时的 avq43klip2pc6。Linear 的 weight 形状是 [d_input, d_output],所以 fc.weight 是 [4, 3]。

❗IMPORTANT
动脑筋:三个张量一共 (12 + 3 + 6) × 4 = 84 字节数据,为什么文件是 792 字节?头 10 字节、元数据 210 字节,上面的输出里都有。先算再看本页末尾的答案。

字段改了名

把 Net 的 fc 改名为 fc1(其余不变),读刚才的文件(train-store rename):

ModuleRecord: Record validation error: Missing tensors: ["fc1.weight", "fc1.bias"]

fc.weight、fc.bias 同时也是「没人认领」的条目,但 apply 先查 missing、查到就返回,报错里只有缺的这一半。ModuleRecord 没有改名的手段;要么把字段改回去,要么换 burn-store。

burn-store:ModuleSnapshot 和 ModuleStore

burn-store 给所有 Module 加了一个扩展 trait ModuleSnapshot:collect 把参数收成 Vec<burn_pack::Tensor>(数据是懒的,写盘时才从设备读回),apply 把一组张量按名字套回模块,save_into / load_from 交给一个 ModuleStore。现成的 store 有三个:BurnpackStore、SafetensorsStore、只读的 PytorchStore。它们都有同一组构造选项:按正则或路径过滤(with_regex、with_full_path)、改名(with_key_remapping、remap)、allow_partial、validate,safetensors 和 burnpack 还能附加元数据、选择是否覆盖已有文件。

用 safetensors 重做改名的实验:Net 存成 out/net.safetensors,读进 fc1 版本,先不改名,再加一条 with_key_remapping(r"^fc\.", "fc1."):

SafetensorsStore without remap: Err
Tensor not found: 
┌─ Tensor Loading Summary ─────────────────────────
│
│ ✓ Successfully applied: 1 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:
│    • fc1.bias
│        Did you mean: 'fc.bias'?
│    • fc1.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.
SafetensorsStore with remap ^fc\. -> fc1.: 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
│
└───────────────────────────────────────────────────
fc1.weight equals saved fc.weight: true

报错是一份 ApplyResult 的摘要,两边都列出来,还按名字相似度给了「Did you mean」。注意一个和 ModuleRecord 相反的默认:store 的 apply_to 只在有 errors(validate 开着时)或有 missing(没开 allow_partial 时)时返回 Err,unused 只出现在摘要里,不算失败。从别的框架导入时文件里常有模型用不到的张量,下面 PyTorch 的例子就是。

apply 里为了不复制整个模块(大模型会多占一倍内存)用了 unsafe:

        unsafe {
            // Read the module out of self (moves it, leaving self in undefined state)
            let module = core::ptr::read(self as *const Self);
            // ...
            let guard = AbortOnUnwind;

            // Map the module to create a new one with updated tensors
            let new_module = module.map(&mut applier);

            // Write the new module back to self
            core::ptr::write(self as *mut Self, new_module);

            // `self` holds a whole module again, so the window is closed.
            core::mem::forget(guard);
        }
📝NOTE
冷知识:加载权重时的 panic 曾经是 double free

AbortOnUnwind 来自 PR #5488(「fix(store): abort instead of double-dropping when a mapper unwinds」,首次进入 v0.22.0-pre.4,正式版 0.22.0)。之前 ptr::read 和 ptr::write 之间没有任何保护:map 里会跑调用方的代码(一个 ModuleAdapter 实现、后端的 from_data),其中一个 panic,self 就停在「已被移走」的状态,展开时被它的所有者再 drop 一次。提交说明强调 apply 是公开 trait 上的安全方法,所以「the double free was reachable without any unsafe」。修法是一个守卫:只有在展开途中被 drop 时,它打印一行说明然后 std::process::abort();成功路径上 mem::forget 掉它。为什么不恢复而是中止:原模块已经被 map 吃掉,没有合法的值可以放回去,Module 也没有 Default 约束可以放一个占位值;take_mut 和 replace_with 这类 crate 也是同样的处理。没有 std 时没有 abort,就在展开中再 panic 一次,效果相同。

safetensors 和 Adapter

PyTorch 的 nn.Linear 存 weight 的形状是 [out, in],burn 默认是 [in, out]。下面用 safetensors crate 自己写一个 PyTorch 布局的文件(train-store safetensors):fc.weight 是 [3, 4]、值 0 到 11,head.weight 是 [2, 3]。

  let tensors = vec![
    ("fc.weight", TensorView::new(Dtype::F32, vec![3, 4], &a).unwrap()),
    ("fc.bias", TensorView::new(Dtype::F32, vec![3], &b).unwrap()),
    ("head.weight", TensorView::new(Dtype::F32, vec![2, 3], &c).unwrap()),
  ];
  safetensors::serialize_to_file(tensors, Some(info), path).unwrap();
  // ...
  let mut st = SafetensorsStore::from_file("out/torch_like.safetensors");
  model.load_from(&mut st)
  // ...
  let mut st =
    SafetensorsStore::from_file("out/torch_like.safetensors").with_from_adapter(PyTorchToBurnAdapter);
  model.load_from(&mut st)
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] } }]
PyTorchToBurnAdapter: Ok
┌─ Tensor Loading Summary ─────────────────────────
...
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]

没有适配器时形状对不上,报 ShapeMismatch;加上 PyTorchToBurnAdapter 后 fc.weight 变成 [4, 3],第一行是原来第一列 0, 4, 8。适配器不看张量名猜,而是看它在模块里属于什么类型:遍历时维护一个容器栈(派生宏给每层起名 "Struct:Linear"、"Vec" 这样),ModuleContext::module_type 取最内层的 Struct: / Enum:。adapt_pytorch_tensor 只做两件事:

    // Linear: transpose weight (bidirectional - same operation both ways)
    if module_type == module_names::LINEAR && param == "weight" && tensor.shape.len() == 2 {
        return transpose_2d_tensor(tensor);
    }

    // Normalization layers: rename parameters based on direction
    if is_normalization_layer(module_type) {
        let renamed = match direction {
            PyTorchConversionDirection::PyTorchToBurn => pytorch_norm_param_to_burn(param),
            PyTorchConversionDirection::BurnToPyTorch => burn_norm_param_to_pytorch(param),
        };
        // ...
    }

Linear 的 weight 转置;BatchNorm、LayerNorm、GroupNorm、RmsNorm 的 weight / bias 改名为 burn 的 gamma / beta。卷积层不在其中:burn 的 Conv2d 权重本来就是 [channels_out, channels_in / groups, kernel_size_1, kernel_size_2],适配器不动它。转置是记在数据来源上的一个变换,真正读数据时才做。反方向有 BurnToPyTorchAdapter,另有按 dtype 转换的 HalfPrecisionAdapter(F32 ↔ F16,默认不碰 BatchNorm,注释说 running_var 在 F16 下会下溢)和 FloatCastAdapter,可以用 chain 串起来。

LinearLayout::Col 的 Linear 和这套机制配合得不好。Col 参数初始化时生成 [out, in],init_mapper 转置成内存里的 [in, out],save_mapper / load_mapper 在存取时各转置一次,文件里是 [out, in]。加载时的形状校验用 Param::lazy_shape:参数还没被读过(懒初始化)时它返回初始化值的形状 [out, in],读过之后返回内存里的形状 [in, out]。于是结果取决于参数有没有被碰过(train-store col 和 col2,fc 换成 Col 布局):

a) lazy Col + PytorchStore: Err
...
│  ⚠️  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] }"]
  • 没读过的 Col 参数配 PytorchStore:适配器已经把 [3, 4] 转成 [4, 3],和 [3, 4] 对不上,报错。
  • 自己存、自己读,目标参数没读过:成功(上游的测试 round_trip_a_shape_mapped_param 测的就是这种)。
  • 自己存、自己读,但目标模型先用过一次(参数已物化):同一个文件被拒绝。
  • 已物化的 Col 参数配 PytorchStore:校验通过,load_mapper 再转置一次,加载报告成功,形状却是 [3, 4],前向时才 panic:
Col fc.weight dims before [4, 3]
Ok
...
Col fc.weight dims after [3, 4]

thread 'main' (3289502) 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])

默认的 Row 布局没有映射函数,不受影响。

PyTorch .pt:pytorch-reader

PytorchStore 建在 pytorch-reader crate 上。这个 crate 的文档第一句:「Read PyTorch checkpoint files (.pt, .pth) without PyTorch or Burn」。它自己解析 pickle,按文件内容识别四种容器:PyTorch 1.6 起的 ZIP、0.1.10 到 1.5 的旧 pickle 流、更早的 TAR、纯 pickle;torch.save(model) 存的整个模型也能读,读出来是 state_dict() 的样子(非持久的 buffer 和其他属性不算);只支持小端文件,稀疏、量化、嵌套张量拒绝。数据是懒的:打开时只解析元数据,PytorchReader 给出名字、dtype 和形状,字节在要用时才从文件读。PytorchStore 不能写(collect_from 直接返回「Saving to PyTorch format is not yet supported」),apply_to 总是用 PyTorchToBurnAdapter。

实验 train-store pytorch 读的是 pytorch-reader 测试数据里的 full_model.pt(上游用 torch.save(model) 生成的,模型有 fc = nn.Linear(4, 3)、head = nn.Linear(3, 2, bias=False)、一个 nn.Sequential 和一个和 fc 是同一个对象的 tied),读进上面的 Net:

  blocks.0.bias: F32 Shape { dims: [2] }
  blocks.0.weight: F32 Shape { dims: [2, 2] }
  blocks.1.bias: F32 Shape { dims: [2] }
  blocks.1.num_batches_tracked: I64 Shape { dims: [] }
  blocks.1.running_mean: F32 Shape { dims: [2] }
  blocks.1.running_var: F32 Shape { dims: [2] }
  blocks.1.weight: F32 Shape { dims: [2] }
  fc.bias: F32 Shape { dims: [3] }
  fc.weight: F32 Shape { dims: [3, 4] }
  head.weight: F32 Shape { dims: [2, 3] }
  running_mean: F32 Shape { dims: [3] }
  tied.bias: F32 Shape { dims: [3] }
  tied.weight: F32 Shape { dims: [3, 4] }
┌─ Tensor Loading Summary ─────────────────────────
│
│ ✓ Successfully applied: 3 tensors
│ ⊘ Skipped (filtered):  0 tensors
│ ✗ Missing in source:    0 tensors
│ ? Unused in target:     10 tensors
│ ! Errors:               0 errors
...
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

没装 PyTorch,13 个条目都读出来了。tied 和 fc 是同一个子模块,pickle 里只存了一次,读出来照 state_dict() 的样子在两个名字下各出现一次;非持久的 buffer mask 和普通属性 scale 不在其中。Net 只用到 3 个,其余 10 个是 unused,不影响成功。burn 里的 fc.weight 正好是文件里那个矩阵的转置。

同一目录下的 full_model_state_dict.pt(同一个模型的 torch.save(model.state_dict()))在这个实验里打不开:Reader(Zip(InvalidArchive("Invalid Central Directory header")))。Python 的 zipfile 和 unzip 都能正常列出它的 17 个条目。新项目解析到的 zip 是 9.1.3,burn 仓库在 v0.22.1 的 Cargo.lock 里锁的是 9.0.4(离线缓存里没有,没能对照),所以只能确定:用 9.1.3 时这类文件读不了。

KeyRemapper

KeyRemapper 是一串 (Regex, String)。remap 对每个张量名按顺序试每条规则,匹配就 replace_all,后一条看到的是前一条改过的名字,所以规则的顺序有意义。store 的 with_key_remapping 每调一次追加一条,正则写错时 expect("Invalid regex pattern") 直接 panic;KeyRemapper::add_pattern 返回 Result。改名发生在 get_all_tensors 建缓存的时候,所以 keys()、get_tensor 看到的已经是新名字;过滤不在这里,在 apply 时做。

看一个真实文件:本地的 SmolLM2-135M model.safetensors(269 MB,只读它的头)。safetensors 的头是 8 字节小端长度加一段 JSON(train-store smollm):

header 30528 bytes, 273 entries (has __metadata__: true)
  model.embed_tokens.weight: "BF16" [49152,576]
  model.norm.weight: "BF16" [576]
  model.layers.0.input_layernorm.weight: "BF16" [576]
  model.layers.0.mlp.down_proj.weight: "BF16" [576,1536]
  model.layers.0.mlp.gate_proj.weight: "BF16" [1536,576]
  model.layers.0.mlp.up_proj.weight: "BF16" [1536,576]
  model.layers.0.post_attention_layernorm.weight: "BF16" [576]
  model.layers.0.self_attn.k_proj.weight: "BF16" [192,576]
  model.layers.0.self_attn.o_proj.weight: "BF16" [576,576]
  model.layers.0.self_attn.q_proj.weight: "BF16" [576,576]
  model.layers.0.self_attn.v_proj.weight: "BF16" [192,576]
tensors 272, layers 30
after remapping (272 keys), layers.0.*:
  layers.0.attn.k_proj.weight
  layers.0.attn.o_proj.weight
  layers.0.attn.q_proj.weight
  layers.0.attn.v_proj.weight
  layers.0.input_norm.gamma
  layers.0.mlp.down_proj.weight
  layers.0.mlp.gate_proj.weight
  layers.0.mlp.up_proj.weight
  layers.0.post_attention_norm.gamma

三条规则 ^model\. → 空、\.self_attn\. → .attn.、_layernorm\.weight$ → _norm.gamma,把 Hugging Face 的名字改成一个假想的 burn 模型的字段路径(layers: Vec<Block> 里的 attn、input_norm 等)。投影层的权重都是 [out, in](k_proj 是 [192, 576]),读进 burn 的 Linear 时同样要 PyTorchToBurnAdapter;文件里没有 lm_head(同目录的 config.json 写着 "tie_word_embeddings": true,输出层和 embed_tokens 共用权重)。数据是 BF16,要读进 F32 的模型,可以再 chain 一个 FloatCastAdapter::to(DType::F32)。

动脑筋答案

out/net.bpk: 792 bytes, magic "NRUB", version 1, metadata 210 bytes
  • 头 10 字节加元数据 210 字节是 220,数据段从下一个 256 的倍数开始:256。
  • 每个张量的起点也对齐到 256。三个张量依次放在数据段内偏移 0、256、512 处,最后一个从文件偏移 256 + 512 = 768 开始,长度是 head.weight 的 6 × 4 = 24 字节,文件到 792 结束。
  • 84 字节的数据占了 792 字节的文件,多出来的都是对齐的填充。参数大到几 MB 时,每个张量最多浪费 255 字节,可以忽略;这个格式是为大张量的零拷贝映射设计的。

小结

  • Module::save_file / load_file 走 burn-core 的 ModuleRecord:按字段路径记下 ParamId 和数据,加载时按路径对回,默认缺、多、形状不符都报错,并恢复记录里的 ParamId,让优化器状态续得上。
  • burnpack 文件是 10 字节头、CBOR 元数据、256 字节对齐的数据段,比 safetensors 多存了 param_id 和标量,模型、优化器、调度器都用它。
  • burn-store 的三个 store 共用过滤、改名、适配器;它们只把缺失当失败,多出来的张量只在摘要里列出。
  • PyTorchToBurnAdapter 按模块类型转置 Linear 的权重、给归一化层的参数改名;pytorch-reader 不需要 PyTorch 就能读 .pt,PytorchStore 只能读。
  • KeyRemapper 是按顺序执行的正则替换,后一条看到前一条的结果。
  • 实测两个问题:Col 布局的 Linear 加载时的形状校验取决于参数是否已物化,配 PytorchStore 时可能被多转置一次、加载成功而前向 panic;新项目解析到 zip 9.1.3 时,一部分 ZIP 格式的 .pt 打不开。

💬 关联讨论

发起讨论
加载中...