Rust in Everything
基于 burn 0.22.1

the trait bound X: Module is not satisfied

主线位置:训练主线的起点,#[derive(Module)] 为结构体生成遍历、搬设备、valid / train 的代码。宏只看字段的类型名来决定一个字段是子模块、常量还是跳过,规则见 Module 派生 的「字段:子模块、常量和 #[module(skip)]」。

flowchart TB
  field["字段 pooling: Pooling"] --> name{"类型名是常量吗"}
  name -->|"usize f32 String 等"| constant["常量"]
  name -->|"写了 module skip"| skip["跳过"]
  name -->|"其他一律"| sub["子模块"]
  sub --> need["要求 Module 和 ModuleDisplay"]
  need --> e0277["E0277"]

实验 crate exp-burn/pb-pit,bin skip_err、clone_err(故意编译失败)和 skip_ok,debug,M2 Max、macOS 27、rustc 1.99.0。

现象

一个分类头,带一个选择池化方式的枚举:

#[derive(Clone, Debug)]
enum Pooling {
  Mean,
  Max,
}

#[derive(Module, Debug)]
struct Head {
  linear: Linear,
  pooling: Pooling,
}

cargo build --bin skip_err(节选,三个错误里的第一个和结尾):

error[E0277]: the trait bound `Pooling: burn::module::Module` is not satisfied
  --> src/bin/skip_err.rs:10:10
   |
10 | #[derive(Module, Debug)]
   |          ^^^^^^ unsatisfied trait bound
   |
help: the trait `burn::module::Module` is not implemented for `Pooling`
  --> src/bin/skip_err.rs:5:1
   |
 5 | enum Pooling {
   | ^^^^^^^^^^^^
   = note: If `Pooling` is used as configuration or runtime state in a `#[derive(Module)]` type, mark that field with `#[module(skip)]`.
   = help: the following other types implement trait `burn::module::Module`:
             (L0, L1)
             (L0, L1, L2)
...
           and 124 others
   = note: this error originates in the derive macro `Module` (in Nightly builds, run with -Z macro-backtrace for more info)
...
error: could not compile `pb-pit` (bin "skip_err") due to 10 previous errors

另外两个错误分别指向字段那一行(同样是 Module)和 ModuleDisplay,每个都带同一句 note。rustc 说有 10 个错误,只展开了 3 个,其余是重复的。

0.21 的代码升级到 0.22 时最容易遇到它:那时字段类型里没有 B 的自定义类型会被自动当成配置,0.22 没有后端参数了,这条判断不复存在(见 type provided when a constant was expected)。

另一个顺手加上去就会报错的写法:

#[derive(Module, Clone, Debug)]
struct Head {
  linear: Linear,
}
error[E0119]: conflicting implementations of trait `std::clone::Clone` for type `Head`
 --> src/bin/clone_err.rs:4:18
  |
4 | #[derive(Module, Clone, Debug)]
  |          ------  ^^^^^ conflicting implementation for `Head`
  |          |
  |          first implementation here

For more information about this error, try `rustc --explain E0119`.
error: could not compile `pb-pit` (bin "clone_err") due to 1 previous error

出处

派生宏的 parse_module_field_type 按类型路径的名字分类:bool、整数、浮点、String 和由它们组成的容器算常量,写了 #[module(skip)] 的跳过,其余全部当子模块,生成的代码对它调用 Module 和 ModuleDisplay 的方法。宏在展开时拿不到 trait 实现的信息,只能这样按名字分;Pooling 不在常量名单里,就成了「子模块」,编译器随后发现它没有实现 Module。

那句 note 来自 Module 和 ModuleDisplay 上的 #[diagnostic::on_unimplemented],0.22.1 才加上(Module 派生 的冷知识);0.22.0 只报 trait 不满足。

E0119 的原因:Module 要求 Clone,派生宏自己生成了一个 impl Clone(codegen.rs,结构体的实现在 codegen_struct.rs 的 gen_clone),再写 derive(Clone) 就是两个实现。

修法

字段上标 #[module(skip)],Pooling 只需要 Clone + Debug + Send(bin skip_ok):

#[derive(Module, Debug)]
struct Head {
  linear: Linear,
  #[module(skip)]
  pooling: Pooling,
}

impl Head {
  fn forward(&self, x: Tensor<3>) -> Tensor<2> {
    let pooled = match self.pooling {
      Pooling::Mean => x.mean_dim(1),
      Pooling::Max => x.max_dim(1),
    };
    self.linear.forward(pooled.squeeze_dim(1))
  }
}

fn main() {
  let device = Device::flex();
  let head = Head { linear: LinearConfig::new(4, 2).init(&device), pooling: Pooling::Max };
  let out = head.forward(Tensor::<3>::ones([3, 5, 4], &device));
  println!("params {}, out {:?}", head.num_params(), out.dims());
  // the skipped field survives the module transforms, untouched
  let moved = head.valid().to_device(&Device::flex().autodiff());
  println!("after valid().to_device(..): pooling = {:?}", moved.pooling);
  println!("{moved}");
}
params 10, out [3, 2]
after valid().to_device(..): pooling = Max
Head {
  linear: Linear {d_input: 4, d_output: 2, bias: true, params: 10}
  pooling: Max
  params: 10
}

参数个数只算 Linear 的 10 个;valid()、to_device 之后 pooling 原样保留,显示时用它的 Debug。跳过的字段也不进保存的记录:要随模型一起保存的配置,放进模型的 Config(见 Config),加载时由配置重建。

E0119 的修法是删掉 Clone,#[derive(Module, Debug)] 就够了。

怎么确认没中招

  • 0.21 的代码升级时,把每个 #[derive(Module)] 结构体里不是子模块、不是 Param、也不是基本类型的字段都列出来,逐个决定是 #[module(skip)] 还是挪到 Config。
  • {model} 打印的模块树里,跳过的字段显示成 Debug 的样子,子模块显示参数个数,一眼能看出宏把哪个字段当成了什么。

小结

  • 0.22 的 #[derive(Module)] 按类型名分字段,认不出的一律当子模块;配置枚举、运行状态要 #[module(skip)]。
  • 0.22.1 的错误信息里有一句 note 直接提示加 #[module(skip)]。
  • derive(Module) 自带 Clone,再写 derive(Clone) 是 E0119。

💬 关联讨论

发起讨论
加载中...