Rust in Everything
基于 burn 0.22.1

use of moved value: x

主线位置:训练主线的 forward:同一个张量在一次前向里被用了两次(残差连接、循环里的固定输入、模块字段)。
版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备)
flowchart TB
  res["x + block(x)"] --> e1["E0382"]
  lp["循环里 block(x)"] --> e2["E0382 循环"]
  fw["forward 里 self.weight"] --> e3["E0507"]
  rf["两个借用相加"] --> e4["E0369"]
  e1 --> c["clone 只加计数"]
  e2 --> c
  e3 --> c
  e4 --> c

编不过的四个例子在 pita-ce 的 ce09a 到 ce09d(后两个从运算消费张量的实验复制),改好的版本是 pita-t 的 p09,PyTorch 侧 p09_reuse.py。平台 M2 Max、macOS 27、rustc 1.99.0。

PyTorch 的写法

class Block(torch.nn.Module):
  def __init__(self):
    super().__init__()
    self.weight = torch.nn.Parameter(torch.eye(3) * 2)

  def forward(self, x):
    return x @ self.weight

block = Block()
x = torch.ones(1, 3)
y = x + block(x)          # residual: x used twice
for step in range(2):     # same input every step
  out = block(x)
residual [[3.0, 3.0, 3.0]] | loop out [[2.0, 2.0, 2.0]]

Python 的名字只是引用,一个张量想用几次用几次。

搬到 burn 的现象

残差连接:

let x = Tensor::<2>::ones([1, 3], &device);
let y = x + block(x);
error[E0382]: use of moved value: `x`
  --> src/bin/ce09a.rs:10:21
   |
 9 |   let x = Tensor::<2>::ones([1, 3], &device);
   |       - move occurs because `x` has type `Tensor<2>`, which does not implement the `Copy` trait
10 |   let y = x + block(x);
   |           -         ^ value used here after move
   |           |
   |           value moved here
   |
help: consider cloning the value if the performance cost is acceptable
   |
10 |   let y = x.clone() + block(x);
   |            ++++++++

循环里每一步用同一个输入:

for _step in 0..2 {
  let out = block(x);
  println!("{out}");
}
error[E0382]: use of moved value: `x`
  --> src/bin/ce09b.rs:11:21
   |
 9 |   let x = Tensor::<2>::ones([1, 3], &device);
   |       - move occurs because `x` has type `Tensor<2>`, which does not implement the `Copy` trait
10 |   for _step in 0..2 {
   |   ----------------- inside of this loop
11 |     let out = block(x);
   |                     ^ value moved here, in previous iteration of loop
   |
note: consider changing this parameter type in function `block` to borrow instead if owning the value isn't necessary
  --> src/bin/ce09b.rs:3:13
   |
 3 | fn block(x: Tensor<2>) -> Tensor<2> {
   |    -----    ^^^^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
help: consider cloning the value if the performance cost is acceptable
   |
11 |     let out = block(x.clone());
   |                      ++++++++

forward(&self) 里直接用字段,报 E0507:

error[E0507]: cannot move out of `self.weight` which is behind a shared reference
 --> src/bin/ce09c.rs:9:14
  |
9 |     x.matmul(self.weight)
  |              ^^^^^^^^^^^ move occurs because `self.weight` has type `Tensor<2>`, which does not implement the `Copy` trait
  |
help: consider cloning the value if the performance cost is acceptable
  |
9 |     x.matmul(self.weight.clone())
  |                         ++++++++

想绕开所有权、改成借用 &a + &b,报 E0369:

error[E0369]: cannot add `&Tensor<1>` to `&Tensor<1>`
 --> src/bin/ce09d.rs:7:14
  |
7 |   let c = &a + &b;
  |           -- ^ -- &Tensor<1>
  |           |
  |           &Tensor<1>

原因

burn 的运算按值拿走操作数:Add<Self> 是 fn add(self, rhs: Self),matmul 是 fn matmul(self, other: Self),函数参数写成 Tensor<2> 也是拿走所有权。&Tensor 上没有实现运算符,所以借用也走不通。为什么这样设计、clone() 背后发生了什么,见运算消费张量;简单说,按值传递让后端能在引用计数为 1 时直接复用缓冲区(原地操作)。

改法

照 rustc 的 help 改,每个要再用一次的地方 clone():

impl Block {
  fn forward(&self, x: Tensor<2>) -> Tensor<2> {
    x.matmul(self.weight.clone())
  }
}

let y = x.clone() + block.forward(x.clone());
let mut out = None;
for _step in 0..2 {
  out = Some(block.forward(x.clone()));
}
residual [3.0, 3.0, 3.0] | loop out Some([2.0, 2.0, 2.0])

和 PyTorch 的 [[3.0, 3.0, 3.0]]、[[2.0, 2.0, 2.0]] 相同(小整数,误差 0)。

  • x.clone() + block(x) 也行:运算符左边先求值,x.clone() 拿到一份,然后 x 本身移进 block。rustc 给的就是这个建议。
  • 第二条报错的 note 建议把 block 的参数改成借用。对张量来说这一般不省事:函数体里做运算时还得 clone(),只是把 clone 挪到了函数里面。
  • 模块的字段用 Param 包着时,self.weight.val() 内部就是一次 clone,burn 自己的 Linear::forward 就这么写(运算消费张量的「模块里的 &self」一节)。

clone() 不复制数据:Flex 上只是 Arc 计数加一,64 MiB 的张量 clone 一次分配不到 0.001 MiB(运算消费张量实测)。不用为了「省一次 clone」去改结构。

怎么检查

这一类全部是编译错误,不会漏到运行期。要注意的只是改法:

  • 按 help 加 clone(),不要为了消掉报错把参数改成 &Tensor 再在里面 clone,那样语义一样、代码更长。
  • 一个值在最后一次使用时不必 clone,直接移走,后端才有机会原地复用它的缓冲区。

小结

  • burn 的运算和按值的函数参数都会拿走张量,再用一次是 E0382(循环里是「in previous iteration of loop」);&self 里用字段是 E0507;&a + &b 是 E0369。
  • 照 rustc 的建议 clone(),代价是一次引用计数,不是一次拷贝。
  • 最后一次使用不 clone,把所有权交出去。

💬 关联讨论

发起讨论
加载中...