打开求导
主线位置:训练主线的 forward 之前:决定哪些张量要梯度、前向要不要记图。
版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备)
flowchart TB
subgraph pt ["PyTorch"]
t1["torch.tensor"] -->|"requires_grad=True"| l1["叶子要梯度"]
end
subgraph bn ["burn"]
d["Device flex"] -->|".autodiff()"| d2["autodiff 设备"]
d2 --> t2["Tensor from_floats"]
t2 -->|".require_grad()"| l2["叶子要梯度"]
end
l1 --> fwd["前向记图"]
l2 --> fwd
实验代码:PyTorch 侧 rg.py,burn 侧 crate autodiff-exp 的 bin rg(burn =0.22.1,feature std、flex、ndarray、autodiff、optim,debug)。平台 M2 Max、macOS 27、rustc 1.99.0。
一个参数变成两步
PyTorch 里任何浮点张量都能要梯度,requires_grad=True 一个参数就够了:
a = torch.tensor([1.0, 2.0])
w = torch.tensor([3.0, 4.0], requires_grad=True)
aa = a * a
aw = a * w
burn 要两步。先让设备带上 autodiff 标记,再对要梯度的叶子调 require_grad():
let ad = Device::flex().autodiff();
let a = Tensor::<1>::from_floats([1.0, 2.0], &ad);
let w = Tensor::<1>::from_floats([3.0, 4.0], &ad).require_grad();
let aa = a.clone() * a.clone();
let aw = a.clone() * w.clone();
Device::flex() 本身不会记图。.autodiff() 只是在设备值上打一个标记,Dispatch 在运行期看到标记,才把算子交给 autodiff 那一层(机制见 Autodiff 装饰器)。在 autodiff 设备上新建的张量默认不要梯度,和 PyTorch 新建的张量默认 requires_grad=False 一样。Tensor::require_grad 按值拿走张量、返回一个新的,所以要接住返回值;它带 #[must_use],丢掉返回值会有警告。
已经建在普通设备上的张量,也可以单独抬上去:Tensor::autodiff 只挂标记,不打开梯度,要梯度还得再 require_grad():
let lifted = Tensor::<1>::from_floats([1.0, 2.0], &plain).autodiff().require_grad();
一个标志对三个查询
两边各打印一遍上面四个张量的状态:
def flags(name, t):
print(f"{name:10} requires_grad={t.requires_grad!s:5} is_leaf={t.is_leaf!s:5} grad_fn={type(t.grad_fn).__name__ if t.grad_fn else None}")
pub fn flags<const D: usize>(name: &str, t: &Tensor<D>) {
println!(
"{name:10} is_autodiff={:5} is_tracked={:5} is_require_grad={}",
t.is_autodiff(),
t.is_tracked(),
t.is_require_grad()
);
}
a requires_grad=False is_leaf=True grad_fn=None
w requires_grad=True is_leaf=True grad_fn=None
a*a requires_grad=False is_leaf=True grad_fn=None
a*w requires_grad=True is_leaf=False grad_fn=MulBackward0
a is_autodiff=true is_tracked=false is_require_grad=false
w is_autodiff=true is_tracked=true is_require_grad=true
a*a is_autodiff=true is_tracked=false is_require_grad=false
a*w is_autodiff=true is_tracked=true is_require_grad=false
PyTorch 的 requires_grad 会沿着运算传下去:a*w 也是 True,它不是叶子,有 grad_fn。burn 的 is_require_grad() 只对 require_grad() 过的叶子为真,a*w 是 false;「参与了要求导的计算」要看 is_tracked。对照起来:
| 想问的事 | PyTorch | burn |
|---|---|---|
| 这个张量的运算会不会被记下来 | 没有对应,CPU 张量总能求导 | is_autodiff() |
| 反向会不会经过它 | requires_grad | is_tracked() |
| backward 之后能不能取到它的梯度 | requires_grad and is_leaf | is_require_grad() |
三个查询背后是 burn 节点上的 Requirement 三个取值:require_grad() 的叶子是 Grad,有输入要梯度的算子输出是 GradInBackward,其余是 None(见 Autodiff 装饰器的「Requirement」一节)。从 PyTorch 搬代码时,if t.requires_grad: 多半应该换成 t.is_tracked(),不是 is_require_grad()。
错误在哪报
把几种常见的误用在两边各跑一遍:
attempt("int requires_grad", lambda: torch.tensor([1, 2], requires_grad=True))
attempt("backward on constant", lambda: (a * 2).sum().backward())
attempt("requires_grad_(True) on a*w", lambda: aw.requires_grad_(True))
attempt("requires_grad_(False) on a*w", lambda: aw.requires_grad_(False))
attempt("requires_grad_() on a*a", lambda: flags("a*a after", aa.requires_grad_()))
int requires_grad: RuntimeError: Only Tensors of floating point and complex dtype can require gradients
backward on constant: RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
requires_grad_(True) on a*w: ok
requires_grad_(False) on a*w: RuntimeError: you can only change requires_grad flags of leaf variables. If you want to use a computed variable in a subgraph that doesn't require differentiation use var_no_grad = var.detach().
a*a after requires_grad=True is_leaf=True grad_fn=None
requires_grad_() on a*a: ok
burn 侧用 std::panic::catch_unwind 包住每一次尝试,panic 原文由默认的 panic hook 打到 stderr:
let plain = Device::flex();
attempt("require_grad on Device::flex()", || {
Tensor::<1>::from_floats([1.0, 2.0], &plain).require_grad()
});
attempt("backward on constant", || (a.clone() * 2.0).sum().backward());
attempt("require_grad on a*w", || aw.clone().require_grad());
attempt("set_require_grad(false) on a*w", || {
flags("a*w off", &aw.clone().set_require_grad(false))
});
attempt("require_grad on a*a", || flags("a*a after", &aa.clone().require_grad()));
thread 'main' (8533721) panicked at .../burn-tensor-0.22.1/src/tensor/api/float.rs:1185:13:
Tensor::require_grad requires autodiff; call Tensor::autodiff first
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
require_grad on Device::flex(): panicked
thread 'main' (8533721) panicked at .../burn-tensor-0.22.1/src/tensor/api/autodiff.rs:75:9:
Tensor::backward requires a tracked autodiff tensor; call Tensor::autodiff().require_grad() on the source leaf before computing the output
backward on constant: panicked
thread 'main' (8533721) panicked at .../burn-autodiff-0.22.1/src/tensor.rs:190:17:
Can't convert a non leaf tensor into a tracked tensor
require_grad on a*w: panicked
a*w off is_autodiff=true is_tracked=false is_require_grad=false
set_require_grad(false) on a*w: ok
a*a after is_autodiff=true is_tracked=true is_require_grad=true
require_grad on a*a: ok
逐条对照:
| 操作 | PyTorch | burn |
|---|---|---|
| 普通设备上的张量要梯度 | 不存在这种情况 | panic:Tensor::require_grad requires autodiff |
| 对常量算出来的 loss 调 backward | RuntimeError | panic:Tensor::backward requires a tracked autodiff tensor |
| 对中间结果打开梯度 | 本来就是 True,什么也不做 | panic:Can't convert a non leaf tensor into a tracked tensor |
| 对中间结果关掉梯度 | RuntimeError,建议用 detach() | 成功,结果和 detach() 一样成为不追踪的新起点 |
对没有历史的算子结果(a*a)打开梯度 | 成功,它是叶子 | 成功,它是 None,可以变成 Grad |
| 整数张量要梯度 | RuntimeError | 编译错误 |
中间结果那两行正好反过来。burn 的 AutodiffTensor::require_grad 只在 Requirement::None 时新建一个 Grad 节点,GradInBackward 一律 panic,不区分「本来就在追踪」;set_require_grad(false) 落到 float_set_require_grad,直接用原语建一个新的 AutodiffTensor,于是和前面的图断开。想拿中间结果的梯度,PyTorch 用 retain_grad(),burn 没有,见 backward 与梯度。
整数张量是唯一在编译期挡住的一条:require_grad 只定义在浮点张量上(crate autodiff-intgrad):
let ad = Device::flex().autodiff();
let i = Tensor::<1, Int>::from_ints([1, 2], &ad).require_grad();
error[E0599]: no method named `require_grad` found for struct `Tensor<1, burn::tensor::Int>` in the current scope
--> src/main.rs:5:52
|
5 | let i = Tensor::<1, Int>::from_ints([1, 2], &ad).require_grad();
| ^^^^^^^^^^^^
|
help: there is a method `is_require_grad` with a similar name
|
5 | let i = Tensor::<1, Int>::from_ints([1, 2], &ad).is_require_grad();
| +++
其余都是运行期的 panic。0.21 时 backward 只定义在 autodiff 后端的张量类型上,在不能求导的后端上调它编译不过;0.22 把后端从类型挪到了设备值上,这类检查就变成了运行期的(来龙去脉见 burn 解决什么问题的冷知识)。从这一点看,0.22 的 burn 反而和 PyTorch 更像:都是跑起来才知道。
require_grad() 曾经什么也不做PR #5571(「feat(tensor): add is_autodiff and is_tracked state inspection」,首次发布于 v0.22.0)给 require_grad 写的文档是「This function does nothing when autodiff isn't enabled or when the tensor is quantized.」:在 Device::flex() 上建的张量调 require_grad() 不报错,backward 之后梯度是 None,看不出哪里漏了 .autodiff()。五天后的 PR #5624(「feat(autodiff): support graph-preserving cross-backend transfers」,同样首次发布于 v0.22.0)的提交列表里有一条「Enforce runtime gradient preconditions」,在 set_require_grad_impl 里加了上面那条 assert!,文档改成「For non-quantized tensors, panics if autodiff is disabled or the tensor is a non-leaf.」。所以 0.22.0 之前的预发布版本里,这一行是静默的;0.22.0 起它 panic,信息里直接告诉你先调 Tensor::autodiff。
先 require_grad,再参与计算
两边有一点完全一样:要梯度的标记必须在张量参与前向之前打上。
x = torch.tensor([1.0, 2.0])
used = x * w
x.requires_grad_()
used.sum().backward()
print("late x.grad =", x.grad, "| w.grad =", w.grad)
let x = Tensor::<1>::from_floats([1.0, 2.0], &ad);
let used = x.clone() * w.clone();
let x = x.require_grad();
let grads = used.sum().backward();
println!("late x.grad = {:?} | w.grad = {:?}", v(x.grad(&grads)), v(w.grad(&grads)));
late x.grad = None | w.grad = tensor([1., 2.])
late x.grad = None | w.grad = Some([1.0, 2.0])
乘法在执行的那一刻决定记不记 x 这个输入;之后再打标记,已经记好的图不会回头补。burn 这边的出处见 grad 返回 None 的 #3。
模型参数不用手动标
PyTorch 的 nn.Linear 参数默认 requires_grad=True。burn 的 Linear 在 autodiff 设备上初始化时,参数也已经要梯度;在普通设备上初始化则不要:
fc = torch.nn.Linear(2, 2)
flags("fc.weight", fc.weight)
let fc = burn::nn::LinearConfig::new(2, 2).init(&ad);
flags("fc.weight", &fc.weight.val());
let fc_plain = burn::nn::LinearConfig::new(2, 2).init(&plain);
flags("plain fc.w", &fc_plain.weight.val());
fc.weight requires_grad=True is_leaf=True grad_fn=None
fc.weight is_autodiff=true is_tracked=true is_require_grad=true
plain fc.w is_autodiff=false is_tracked=false is_require_grad=false
LinearConfig::init 经 Initializer::init_with 建一个延迟初始化的 Param::uninitialized,要梯度的设置是 true;第一次 val() 时 initialize 把 is_require_grad && self.device.is_autodiff() 交给初始化函数,为真才 require_grad()。普通设备上这个值是假,「训练时要梯度」只记在 Param 上,等 Module::train() 再打开。手动替换参数用的 Param::from_tensor 也一样,对 autodiff 张量直接打开梯度,对普通张量只记下设置。所以训练主线里,模型建在 Device::flex().autodiff() 上就够了,不用逐个参数 require_grad();冻结部分参数(PyTorch 的 requires_grad_(False))见参数与遍历。
小结
requires_grad=True在 burn 里是两步:设备.autodiff()(或张量.autodiff()),叶子再.require_grad(),并接住返回值。- PyTorch 的
requires_grad对中间结果也为真,对应 burn 的is_tracked();is_require_grad()只对要梯度的叶子为真。 - 对中间结果打开梯度:PyTorch 不做事,burn panic;关掉梯度:PyTorch 报错,burn 断开成新起点。
- 除了整数张量没有
require_grad(E0599),其余误用都是运行期 panic;0.22.0 起普通设备上的require_grad()也 panic。 - 两边都要在张量参与前向之前打标记;模型建在 autodiff 设备上,参数自动要梯度。