上一篇介绍了 Tensor 的核心模型:它不是一组孤立的数字,而是由 Storage、Shape、Stride、Storage Offset、dtype、device 和 layout 共同描述的一种数据抽象。
但 Tensor 只有数据和布局,还不能完成模型训练。训练还需要回答一个问题:
模型输出发生变化时,参数应该沿着什么方向、以多大的幅度变化?
这需要计算梯度。PyTorch 通过 Autograd 把数学上的求导过程变成了一个可以执行的运行时系统:
Tensor 运算
↓
动态记录运算关系
↓
形成计算图
↓
从结果反向遍历
↓
计算并累积梯度
↓
Optimizer 更新参数
初学者通常只需要记住:
loss.backward()
optimizer.step()
但 AI-Infra 工程师还需要理解:
requires_grad决定了什么?- leaf Tensor 和 non-leaf Tensor 有什么区别?
grad_fn指向什么?- 梯度为什么会累积?
- 计算图什么时候创建,什么时候释放?
- 为什么保存一个 Tensor 可能让整张图无法回收?
detach()、no_grad()和inference_mode()有什么区别?- 自定义算子如何向 Autograd 提供 backward?
- 为什么某些 in-place 操作会破坏反向传播?
本文会先从链式法则和向量—雅可比积开始,再逐层解释动态计算图、Autograd Node、梯度累积、图的生命周期和自定义 autograd.Function。最后会实现一个 Mini-Autograd,用一个很小的系统复现 PyTorch 反向传播的核心思想。
一、从数学求导到自动求导
1. 梯度解决什么问题?
假设有一个非常简单的函数:
y = x²
当 x = 3 时,y = 9。如果希望调整 x 让 y 变小,就需要知道:
y 对 x 的变化有多敏感?
这个敏感程度就是导数:
dy/dx = 2x
当 x = 3 时:
dy/dx = 6
如果目标是最小化 y,就可以沿着负梯度方向调整 x:
x_new = x - learning_rate × gradient
神经网络中的参数通常有数百万甚至数十亿个,手工为每个参数推导和实现梯度是不现实的。Autograd 的任务就是自动完成这个过程。
2. 链式法则
对于复合函数:
z = f(y)
y = g(x)
有:
dz/dx = dz/dy × dy/dx
例如:
y = x²
z = 3y + 1
可以拆成:
dz/dy = 3
dy/dx = 2x
dz/dx = 3 × 2x = 6x
这正是反向传播的基本结构:从结果开始,沿着计算关系反向应用局部导数。
3. 从标量到向量
实际模型很少只有一个标量输入和一个标量输出。更常见的情况是:
y = f(x)
其中 x 和 y 都是向量或 Tensor。
严格来说,这时的导数由 Jacobian 描述:
J[i, j] = ∂y[i] / ∂x[j]
如果直接保存完整 Jacobian,内存和计算成本可能非常高。神经网络训练中通常不需要显式构造完整 Jacobian,而是计算向量—雅可比积:
vᵀJ
其中 v 是从后续计算传回来的梯度。PyTorch 的 backward() 正是沿着计算图计算这种反向量积。
4. 为什么 backward 通常从标量 Loss 开始?
loss.backward()
这里的 loss 通常是一个标量。标量的反向传播可以理解为:
∂loss / ∂每个参数
如果对一个非标量 Tensor 直接调用 backward():
x = torch.randn(3, requires_grad=True)
y = x * 2
y.backward()
通常会报错,因为 PyTorch 不知道应该从哪个方向将 y 的梯度传回来。
可以显式提供外部梯度:
y.backward(torch.ones_like(y))
这相当于计算:
sum(y) 对 x 的梯度
所以 backward() 不只是“计算这个 Tensor 的梯度”,更准确地说是:
从当前结果出发,给定一个上游梯度,沿图计算各个输入的向量—雅可比积。
二、动态计算图:每次执行都记录一条新路径
1. 什么是计算图?
考虑下面的计算:
x = torch.tensor(2.0, requires_grad=True)
y = x * x
z = y + 3
loss = z.sum()
可以表示为:
x ─────┐
* → y ───┐
x ─────┘ + → z → sum → loss
3 ───┘
图中的节点可以表示:
- 输入 Tensor;
- 中间 Tensor;
- 算子;
- 输出 Tensor;
- 反向传播需要的局部信息。
2. Eager Mode 下的图是动态创建的
上一篇介绍过,PyTorch 默认采用 Eager-first 的编程体验。在 Eager Mode 下,代码执行到哪一行,对应的 Tensor 操作就可以立即执行;如果需要梯度,Autograd 同时记录必要的反向关系。
x = torch.randn(3, requires_grad=True)
y = x * 2
z = y.relu()
loss = z.sum()
大致过程是:
执行 x * 2
↓
创建乘法结果 y
↓
记录 y 如何由 x 得到
执行 relu
↓
创建 z
↓
记录 z 如何由 y 得到
执行 sum
↓
创建 loss
↓
记录 loss 如何由 z 得到
计算图不是预先写死的一张全局静态图,而是当前这次 forward 执行产生的运行时结构。
3. 每次 forward 通常都会创建新图
for inputs, targets in loader:
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
通常每个训练 step 都会经历:
本次 forward 创建图
↓
本次 backward 遍历图
↓
图中的中间信息被释放
↓
下一次 forward 创建新图
这种设计让 Python 控制流可以直接影响计算图:
def f(x: torch.Tensor) -> torch.Tensor:
if x.sum() > 0:
return x * x
return x + x
不同输入可能走不同分支,因此不同执行过程可能产生不同的计算关系。
4. 动态图的优点和成本
优点:
- Python 控制流自然可用;
- 中间结果容易检查;
- 模型结构容易修改;
- 失败位置通常比较直观;
- 适合研究和快速迭代。
成本:
- 每次执行都可能构建运行时图;
- Python 调度和对象创建有额外开销;
- 保存中间 Tensor 可能延长图生命周期;
- 编译器需要额外捕获和分析;
- 动态控制流可能导致 graph break。
torch.compile() 的一个重要目标,就是在保留这种编程体验的同时,捕获其中适合优化的计算部分。它并不改变 Autograd 的基本数学含义,但可能改变计算图被捕获和执行的方式。
三、requires_grad、Leaf Tensor 与 grad_fn
1. requires_grad
x = torch.tensor(2.0, requires_grad=True)
这表示:只要后续计算在梯度记录开启的上下文中进行,并且结果参与反向传播,Autograd 就需要追踪与 x 有关的计算关系。
x = torch.tensor(2.0, requires_grad=True)
y = x * x
z = y + 1
z.backward()
print(x.grad) # tensor(4.)
这里:
y = x²
z = y + 1
∂z/∂x = 2x = 4
requires_grad=True 并不意味着 Tensor 现在已经有梯度,也不意味着每个后续操作都一定会被记录。是否记录还会受到:
- 输入是否需要梯度;
- 是否处于
no_grad()或inference_mode(); - 运算是否可微;
- Tensor 是否参与当前计算结果;
等条件影响。
2. Leaf Tensor
一个常见的 leaf Tensor 是用户直接创建、并设置了 requires_grad=True 的 Tensor:
x = torch.tensor(2.0, requires_grad=True)
print(x.is_leaf) # True
由运算产生的结果通常不是 leaf:
y = x * 2
print(y.is_leaf) # False
模型参数通常也是 leaf Tensor:
from torch import nn
layer = nn.Linear(4, 2)
for parameter in layer.parameters():
print(parameter.is_leaf)
Autograd 默认会把最终梯度保存在 leaf Tensor 的 .grad 中。对于 non-leaf Tensor,虽然它参与了计算图,但默认不一定保留 .grad。
3. 为什么 non-leaf 的 .grad 可能是 None?
x = torch.tensor(2.0, requires_grad=True)
y = x * 2
z = y * 3
z.backward()
print(x.grad) # tensor(6.)
print(y.grad) # 通常为 None
如果确实需要查看 non-leaf Tensor 的梯度,可以在 backward 前调用:
y.retain_grad()
z.backward()
print(y.grad)
这会要求 Autograd 额外保留该梯度,因此调试时可以使用,生产热路径中不要无差别对大量中间 Tensor 调用 retain_grad()。
4. grad_fn
x = torch.tensor(2.0, requires_grad=True)
y = x * 2
print(x.grad_fn) # None
print(y.grad_fn) # 类似 <MulBackward0 ...>
leaf Tensor 通常没有由其他 Tensor 运算产生的 grad_fn;non-leaf Tensor 通常会带有指向反向节点的 grad_fn。
可以通过 next_functions 观察部分反向关系:
print(y.grad_fn.next_functions)
这些对象属于 Autograd 的运行时实现细节,不应该依赖具体类名编写业务逻辑。它们的价值主要在于调试和理解计算图。
5. requires_grad 与 Parameter
nn.Parameter 是 Tensor 的一个特殊封装,目的是告诉 nn.Module:
这个 Tensor 是模型参数
from torch import nn
weight = nn.Parameter(torch.randn(4, 2))
print(weight.requires_grad) # True
当 Parameter 被赋值为 Module 的属性时,Module 会将它注册到参数集合中。Optimizer 再从 Module 的参数中取得它们并更新。
这说明两个机制是分开的:
requires_grad
→ 是否参与梯度计算
Parameter 注册
→ 是否被 Module 和 Optimizer 发现
一个普通 Tensor 可以参与梯度计算,但不会因为 requires_grad=True 就自动成为模型参数。
四、backward():反向传播与梯度累积
1. 一次 backward 的过程
x = torch.tensor(2.0, requires_grad=True)
y = x * x
loss = y + 1
loss.backward()
可以抽象为:
loss
↓ 反向
加法节点
↓ 反向
乘法节点
↓ 反向
x
每个节点需要做两件事:
- 接收从后继节点传来的上游梯度;
- 使用自己的局部导数,计算传给前驱节点的梯度。
对于:
y = x * x
局部导数是:
∂y/∂x = 2x
2. 梯度默认会累积
x = torch.tensor(2.0, requires_grad=True)
y = x * 3
y.backward()
print(x.grad) # tensor(3.)
y = x * 4
y.backward()
print(x.grad) # tensor(7.)
第二次 backward 没有覆盖第一次梯度,而是累加了新的梯度:
3 + 4 = 7
这也是为什么训练循环通常需要清空梯度:
optimizer.zero_grad()
loss.backward()
optimizer.step()
或者:
optimizer.zero_grad(set_to_none=True)
3. 为什么梯度累积是有用的?
梯度累积并不只是一个容易忘记清零的陷阱,它也可以用于模拟更大的 batch:
optimizer.zero_grad(set_to_none=True)
for micro_batch in micro_batches:
loss = model_loss(micro_batch)
loss.backward()
optimizer.step()
如果每个 micro-batch 的 loss 已经按需要缩放,那么多个 micro-batch 的梯度可以累积后再更新一次参数。
这在显存不足以容纳大 batch 时很有用,但需要同时考虑:
- loss 是否需要除以累积步数;
- BatchNorm 等状态是否仍按 micro-batch 更新;
- 梯度裁剪应该发生在累积之后还是每个 micro-batch;
- AMP 的 scaler 如何配合;
- 分布式训练中的梯度同步成本。
4. optimizer.zero_grad(set_to_none=True)
把梯度置零和把梯度设为 None 不是完全相同的操作:
optimizer.zero_grad(set_to_none=True)
设为 None 可以避免不必要的填零,并让后续 backward 在需要时重新分配梯度。但业务代码不能无条件假设:
parameter.grad is always a Tensor
正确性和性能都需要根据训练循环验证。
5. 多次 backward 与计算图释放
x = torch.tensor(2.0, requires_grad=True)
y = x * x
y.backward()
y.backward()
第二次调用通常会失败,因为第一次 backward 后,为了节省内存,图中保存的中间信息已经被释放。
如果确实需要对同一张图多次反向传播,可以使用:
y.backward(retain_graph=True)
y.backward()
但 retain_graph=True 会延长计算图生命周期、增加内存占用。它应该是有明确理由的选择,而不是遇到错误时盲目添加。
五、计算图中的保存值与生命周期
1. backward 为什么需要保存中间值?
有些算子的局部导数依赖 forward 阶段的输入或输出。
例如:
y = x²
反向时需要:
∂y/∂x = 2x
因此 Autograd 需要保留 x,或者保留足以计算梯度的信息。
对于 ReLU:
y = max(0, x)
反向时需要知道 forward 阶段哪些位置大于 0。不同算子会保存不同的中间信息。
2. 保存输出可能保存整张图
losses = []
for inputs, targets in loader:
loss = criterion(model(inputs), targets)
losses.append(loss)
如果这些 loss 仍然连接着 Autograd 图,那么列表可能间接持有每个 step 的计算图和中间 Tensor,导致显存持续增长。
如果只需要记录数值,应转换为不再连接图的标量:
losses.append(loss.detach().item())
如果需要保留 Tensor 但不需要梯度:
losses.append(loss.detach().cpu())
3. saved_tensors_hooks
在某些内存受限场景,保存的 Tensor 可以通过 hooks 进行自定义处理,例如:
from torch.autograd.graph import saved_tensors_hooks
概念上可以实现:
保存到 GPU 的中间 Tensor
↓ pack
压缩或搬到 CPU
↓ unpack
backward 时恢复
这类机制可以换取显存空间,但会引入:
- 额外的数据搬运;
- 压缩和解压成本;
- 更复杂的生命周期;
- 可能的数值变化。
它适合明确的内存优化场景,不应作为默认编程方式。
4. 计算图与 Tensor 生命周期
需要同时区分两类生命周期:
Tensor 对象生命周期
由 Python 引用和 Storage 关系影响
Autograd 图生命周期
由输出、grad_fn、保存值和 backward 过程影响
一个 Tensor 可能:
- 与其他 Tensor 共享 Storage;
- 通过
grad_fn连接到图; - 被某个 Python 容器长期持有;
- 让 Autograd 保存的中间值继续存活。
因此,内存问题不能只看变量名是否被删除,还要看 Tensor 是否仍然连接着 Storage 或计算图。
六、detach()、no_grad() 与 inference_mode()
1. detach():切断一个 Tensor 的 Autograd 关系
x = torch.tensor(2.0, requires_grad=True)
y = x * 2
z = y.detach()
print(y.requires_grad) # True
print(z.requires_grad) # False
detach() 返回一个不再沿原计算图传播梯度的 Tensor。它通常与原 Tensor 共享底层数据,因此:
detach
→ 改变 Autograd 关系
→ 不等于 clone
→ 不等于数据复制
如果需要既切断梯度又创建独立数据,可以使用:
z = y.detach().clone()
2. torch.no_grad():临时关闭梯度记录
with torch.no_grad():
output = model(inputs)
在这个上下文中,通常不会为 Tensor 运算构建 Autograd 图,适合不需要训练的计算。
model.eval()
with torch.no_grad():
output = model(inputs)
这里的两个操作职责不同:
model.eval()
→ 改变 Dropout、BatchNorm 等 Module 行为
no_grad()
→ 关闭梯度记录
不能用其中一个代替另一个。
3. torch.inference_mode():更强的推理上下文
with torch.inference_mode():
output = model(inputs)
inference_mode() 面向纯推理场景,除了不记录梯度,还会进一步减少 Autograd 相关的元数据和版本计数开销。
它的约束也更强:在该模式中创建的 Tensor 不应被当作普通的、可以随时参与梯度计算的 Tensor 使用。适合明确隔离的推理路径,不适合包住一段之后还需要训练的混合逻辑。
4. 三者的区别
| 机制 | 是否创建新 Tensor | 是否复制数据 | 是否连接原计算图 | 典型用途 |
|---|---|---|---|---|
detach() |
是一个新的 Tensor 视图 | 通常否 | 否 | 将结果交给日志、缓存或非梯度逻辑 |
no_grad() |
由内部操作决定 | 由内部操作决定 | 不记录新图 | 验证、评估、临时关闭梯度 |
inference_mode() |
由内部操作决定 | 由内部操作决定 | 不记录新图且约束更强 | 独立的高性能推理路径 |
detach().clone() |
是 | 是 | 否 | 需要独立数据和独立梯度关系 |
5. 与 model.eval() 的完整组合
推理代码通常写成:
model.eval()
with torch.inference_mode():
predictions = model(inputs)
训练代码通常写成:
model.train()
with torch.enable_grad():
predictions = model(inputs)
loss = criterion(predictions, targets)
loss.backward()
train()、eval()、enable_grad()、no_grad() 和 inference_mode() 分别控制不同的状态和上下文,不应该把它们当成同一类 API。
七、自定义 autograd.Function
1. 为什么需要自定义 Autograd?
如果使用已有的 PyTorch 算子,Autograd 通常已经知道它们的反向规则:
y = torch.sin(x)
z = y * y
z.sum().backward()
但当你实现了一个新的算子,或者希望用特殊方式计算 backward,就需要告诉 Autograd:
forward 如何计算结果?
backward 如何根据上游梯度计算输入梯度?
2. 一个平方算子
class Square(torch.autograd.Function):
@staticmethod
def forward(ctx, input):
ctx.save_for_backward(input)
return input * input
@staticmethod
def backward(ctx, grad_output):
(input,) = ctx.saved_tensors
return grad_output * 2 * input
调用:
x = torch.tensor(3.0, requires_grad=True)
y = Square.apply(x)
y.backward()
print(x.grad) # tensor(6.)
这里:
forward:y = x²
局部导数:dy/dx = 2x
上游梯度:grad_output = dLoss/dy
输入梯度:dLoss/dx = grad_output × 2x
3. ctx.save_for_backward()
forward 阶段可以把 backward 所需的 Tensor 保存到 ctx:
ctx.save_for_backward(input)
保存的 Tensor 会影响计算图和内存生命周期,因此不能把所有中间结果都无条件保存。
如果 backward 只需要一个标量配置,也可以保存普通属性:
ctx.alpha = alpha
Tensor 和非 Tensor 状态的保存方式不同,应根据 backward 的需要选择。
4. 多输入和不可导输入
一个函数可能有多个输入:
class Scale(torch.autograd.Function):
@staticmethod
def forward(ctx, input, alpha):
ctx.alpha = alpha
return input * alpha
@staticmethod
def backward(ctx, grad_output):
return grad_output * ctx.alpha, None
这里 alpha 如果是普通数值,就不需要返回梯度;backward 返回值的位置必须与 forward 输入对应。
5. 自定义 Autograd 的工程边界
自定义 backward 至少需要验证:
- forward 数值是否正确;
- backward 梯度是否正确;
- 多种 shape;
- 多种 dtype;
- CPU 和 CUDA;
- contiguous 和 non-contiguous;
- 空 Tensor 和边界输入;
- 一阶梯度;
- 二阶梯度是否支持;
- 异常输入;
- 内存是否正确释放。
可以使用:
torch.autograd.gradcheck
和:
torch.autograd.gradgradcheck
进行数值验证。
八、实现一个 Mini-Autograd
1. 实践目标
PyTorch 的 Autograd 内部包含复杂的 C++ 和 Python 组件,但可以用一个很小的系统复现核心思想:
每个值知道它的父节点
每个运算保存局部梯度函数
从结果开始反向拓扑遍历
把梯度累加到父节点
为了聚焦计算图,本实践使用 Python 标量,不实现 Tensor、dtype、device 和广播。
2. Value 节点
class Value:
def __init__(self, data, parents=(), op=""):
self.data = data
self.grad = 0.0
self.parents = set(parents)
self.op = op
self.backward_fn = lambda: None
每个 Value 包含:
data
当前数值
grad
当前累积梯度
parents
当前节点依赖的父节点
backward_fn
当前节点如何把梯度传给父节点
3. 实现加法
def add(left, right):
result = Value(left.data + right.data, (left, right), "+")
def backward():
left.grad += result.grad
right.grad += result.grad
result.backward_fn = backward
return result
因为:
d(left + right)/dleft = 1
d(left + right)/dright = 1
所以两个父节点都接收相同的上游梯度。
4. 实现乘法
def multiply(left, right):
result = Value(left.data * right.data, (left, right), "*")
def backward():
left.grad += right.data * result.grad
right.grad += left.data * result.grad
result.backward_fn = backward
return result
因为:
d(left × right)/dleft = right
d(left × right)/dright = left
5. 拓扑排序
反向传播需要从结果开始,沿依赖关系逆序访问节点:
def build_topological_order(root):
visited = set()
order = []
def visit(node):
if node in visited:
return
visited.add(node)
for parent in node.parents:
visit(parent)
order.append(node)
visit(root)
return order
如果计算是:
a → multiply → b → add → c
拓扑序是:
a, b, c
反向遍历时则使用:
c, b, a
6. 实现 backward
def backward(root):
for node in build_topological_order(root):
node.grad = 0.0
root.grad = 1.0
for node in reversed(build_topological_order(root)):
node.backward_fn()
一个更完整的实现通常会避免重复构建拓扑序,这里为了突出概念保持简单。
7. 运行一个例子
a = Value(2.0)
b = Value(3.0)
c = multiply(a, b)
d = add(c, a)
backward(d)
print(d.data) # 8.0
print(a.grad) # 4.0
print(b.grad) # 2.0
数学上:
d = a × b + a
∂d/∂a = b + 1 = 4
∂d/∂b = a = 2
Mini-Autograd 只有很少的代码,却已经包含了 Autograd 的核心结构:
Value
↓
父节点关系
↓
局部导数
↓
拓扑排序
↓
逆序传播
↓
梯度累积
8. Mini-Autograd 与 PyTorch Autograd 的差异
Mini-Autograd 没有实现:
- 多维 Tensor;
- Storage 和 stride;
- dtype 和 device;
- 广播;
- 原地操作检查;
- 高阶梯度;
- 多线程和异步设备执行;
- C++ Autograd Node;
- saved Tensor 生命周期管理。
它不是 PyTorch 的替代品,而是一个帮助理解反向传播的数据结构实验。
九、Autograd 常见问题与排查方法
1. element 0 of tensors does not require grad
常见原因:
- 输入没有设置
requires_grad; - 模型参数被错误地冻结;
- forward 在
no_grad()中执行; - 中间结果被
detach(); - 使用了不可导操作;
- 取出了 Python 标量后继续计算。
排查时可以沿路径打印:
print(inputs.requires_grad)
print(outputs.requires_grad)
print(outputs.grad_fn)
print(loss.requires_grad)
print(loss.grad_fn)
2. grad is None
需要先区分:
这是正常的 None
还是梯度链路断了?
可能原因:
- Tensor 是 non-leaf,默认不保留
.grad; - Tensor 没有参与最终 loss;
requires_grad=False;- 中间调用了
detach(); - backward 尚未执行;
- 梯度被设为
None。
对模型参数,可以检查:
for name, parameter in model.named_parameters():
print(name, parameter.requires_grad, parameter.grad is None)
3. 计算图被意外保留
典型风险包括:
history.append(loss)
outputs_cache.append(outputs)
metrics[step] = hidden_state
如果这些对象还连接着图,可能造成内存不断增长。
根据需求选择:
loss.item()
loss.detach()
loss.detach().cpu()
不要无条件使用 detach(),因为它会切断之后可能需要的梯度关系;也不要无条件使用 clone(),因为它会产生数据复制。
4. In-place 操作导致 backward 失败
x = torch.randn(3, requires_grad=True)
y = x * x
x.add_(1)
如果 backward 需要 x 的旧值,而 x 已经被修改,Autograd 可能检测到版本不一致并报错。
遇到 in-place 相关错误时,先移除 in-place 操作验证正确性,再判断是否有必要通过更安全的方式优化内存。
5. 梯度异常和数值稳定性
Autograd 只负责按照定义计算梯度,不保证梯度一定数值稳定。出现 NaN 或 Inf 时,还要检查:
- 输入是否包含 NaN/Inf;
- loss 是否溢出;
- dtype 是否过低;
- 学习率是否过大;
- 指数、对数和除法是否处在危险范围;
- 混合精度和 GradScaler 是否配置正确。
可以使用:
torch.autograd.set_detect_anomaly(True)
定位异常 backward,但它会增加大量开销,只适合调试阶段使用。
十、Java 工程师如何理解 Autograd
1. Autograd 不是普通事件回调
事件回调通常回答:
某个事件发生后,调用哪些函数?
Autograd 回答的是:
当前输出由哪些 Tensor 运算得到?
给定上游梯度,如何按照局部导数把梯度传回去?
它是一个带数学语义的计算图系统,不是任意业务事件的监听器。
2. Autograd 更接近运行时构建的反向程序
可以把一次 forward 和 backward 粗略理解为:
forward
产生数值结果
同时记录反向所需的结构和数据
backward
沿记录的结构逆序执行
将上游梯度转换为下游梯度
这和 Java 编译器在编译期生成字节码不同,也和普通运行时调用栈不同。Autograd 图只描述当前计算中与梯度有关的部分。
3. 梯度累积类似显式状态管理
Java 工程师通常习惯把局部变量和对象状态区分开。在 PyTorch 中,.grad 是与参数生命周期相关的显式状态:
forward 计算
↓
backward 写入 parameter.grad
↓
optimizer 读取 grad
↓
zero_grad 清理状态
如果不理解这个状态流,就容易出现:
- 梯度重复累积;
- 参数没有梯度;
- 梯度清零时机错误;
- 梯度裁剪作用在错误的阶段。
4. Tensor 布局和 Autograd 是两个正交维度
上一篇讨论了 Tensor 的:
Storage / Shape / Stride / Offset / Dtype / Device
本文讨论了:
requires_grad / grad_fn / Graph / Gradient
一个 Tensor 可以:
- 共享底层 Storage,但不共享 Autograd 关系;
- 连接同一张计算图,但拥有不同的 view 布局;
- 发生 dtype 或 device 转换,同时改变数据存储和梯度路径。
分析问题时,需要明确自己正在观察的是:
数据布局问题
还是
梯度关系问题
十一、本文小结
Autograd 的核心任务,是把数学上的链式法则变成一次沿动态计算图执行的反向遍历。
1. 计算图的基本结构
Tensor 运算
↓
记录父节点和局部导数
↓
形成动态计算图
↓
从结果节点逆序遍历
↓
计算和累积梯度
2. 关键对象
requires_grad
→ 是否需要追踪梯度
leaf Tensor
→ 通常是用户或模型直接持有的参数节点
grad_fn
→ 当前 Tensor 对应的反向关系
.grad
→ 保存到 Tensor 或 Parameter 上的梯度状态
3. 三种常用的梯度控制方式
detach()
→ 切断一个 Tensor 与原计算图的连接
no_grad()
→ 临时关闭新的梯度记录
inference_mode()
→ 面向纯推理的更强优化上下文
它们都不等价于 model.eval()。
4. 分析 backward 问题的顺序
输入是否 requires_grad?
↓
中间结果是否仍有 grad_fn?
↓
loss 是否参与了目标参数的计算?
↓
是否被 detach 或 no_grad 截断?
↓
是否发生了 in-place 修改?
↓
是否正确处理梯度累积和清零?
5. Mini-Autograd 的核心
父节点
+ 局部导数
+ 拓扑排序
+ 逆序遍历
+ 梯度累积
= 自动求导的核心骨架
下一篇将进入 Tensor 和 Autograd 之上的模型组织层:
nn.Module如何管理模型层次、Parameter、Buffer、state_dict,并把这些对象连接到训练循环?