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,把所有权交出去。
💬 关联讨论
发起讨论加载中...