PyTorch C++ Autograd 梯度计算完全指南:backward 与 grad 函数深入解析
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
梯度计算是 PyTorch C++ 前端中最核心的能力之一。本文将系统讲解 torch::autograd::backward 与 torch::autograd::grad 两个高层梯度计算函数的完整签名、参数语义与底层实现原理,并演示 Tensor 上内置的backward()、.grad()、.detach()、.requires_grad_()等方法在自动微分(Automatic Differentiation)流程中的实际用法。读完本文,你能够在纯 C++ 环境下完成前向图构建、反向传播、梯度清零、高阶导数计算等常见训练与调试任务,并对链式法则背后的 Jacobian-vector product(雅可比向量积)机制形成源码级认知。
梯度计算概览:C++ 前端中的自动微分
PyTorch 为张量计算提供了自动求导能力:它会在 Tensor 上记录所有算子操作,构建一张有向无环的计算图,随后通过反向传播(backpropagation)自动计算图中各节点相对于叶子张量的梯度。与 Python 端的torch.autograd.backward/torch.autograd.grad相对应,纯 C++ 环境(可暴露给 C++ API 与 TorchScript)提供了两个顶层函数:
torch::autograd::backward(...):对给定张量执行反向传播,并把梯度累加到叶子张量的.grad中;torch::autograd::grad(...):计算输出关于指定输入的梯度并作为返回值返回,不写回.grad。
从源码注释可以看到(torch/csrc/autograd/autograd.cpp),这份 C++ 实现复刻了torch/autograd/__init__.py与torch._C._EngineBase.run_backward中的既有逻辑,是一个不依赖 Python 的纯 C++ 自动微分入口,二者必须长期保持语义一致。
在 Autograd 索引文档 中,官方给出了使用场景定位:训练神经网络(梯度自动计算)、为特殊算子实现自定义反向传播、以及需要细粒度控制梯度计算过程时,都应使用这一套 C++ 自动微分设施。
torch::autograd::backward:向叶子累加梯度
函数签名与完整参数表
backward的完整声明位于 torch/csrc/autograd/autograd.h:
void torch::autograd::backward( const variable_list& tensors, const variable_list& grad_tensors = {}, std::optional<bool> retain_graph = std::nullopt, bool create_graph = false, const variable_list& inputs = {});| 参数 | 类型 | 默认值 | 语义 |
|---|---|---|---|
tensors | variable_list | —(必填) | 需要求导的输出张量,即被微分函数的结果。 |
grad_tensors | variable_list | {} | Jacobian-vector product 中的"向量",通常是对应张量的预计算梯度;长度须与tensors匹配。 |
retain_graph | std::optional<bool> | std::nullopt | 为false时,反向计算结束后用于求梯度的计算图会被释放;默认取create_graph的值。 |
create_graph | bool | false | 为true时构造导数图,从而允许计算更高阶导数。 |
inputs | variable_list | {} | 只针对这些输入把梯度累加到at::Tensor::grad,其余张量被忽略;为空时累加到所有用于计算tensors的叶子张量。 |
语义细节
backward计算的是给定张量关于图叶子(graph leaves)的梯度之和,整个图用链式法则求导:
- 若
tensors中存在非标量(数据元素多于一个)且需要梯度的张量,则计算 Jacobian-vector product,此时必须额外提供grad_tensors。它应是长度匹配的序列,保存 JVP 中的"向量",通常是被微分函数输出关于对应张量的梯度;对不需要梯度的张量,可用torch::Tensor()(未定义张量)占位。 - 该函数把梯度累加到叶子张量上,因此多次调用前通常需要先将
.grad清零(对应 Python 中的optimizer.zero_grad()/grad.zero_()语义)。 - 传入
inputs时,梯度只会被累加到这些输入上;当某个输入不是叶子时,当前实现仍会调用其grad_fn(虽然严格来说不必如此),这是官方明确标注的"实现细节",用户不应依赖该行为(参见 autograd.h 中的注释)。
核心示例
#include <torch/torch.h> auto x = torch::randn({2, 2}, torch::requires_grad()); auto y = x * x; auto z = y.sum(); // 计算梯度 z.backward(); std::cout << x.grad() << std::endl; // dz/dx = 2x,被写入 x.grad()torch::autograd::grad:返回梯度而非累加
grad的声明同样位于 autograd.h:
torch::autograd::variable_list grad( const variable_list& outputs, const variable_list& inputs, const variable_list& grad_outputs = {}, std::optional<bool> retain_graph = std::nullopt, bool create_graph = false, bool allow_unused = false);| 参数 | 类型 | 默认值 | 语义 |
|---|---|---|---|
outputs | variable_list | —(必填) | 被微分函数的输出。 |
inputs | variable_list | —(必填) | 需要返回梯度的输入;梯度返回而不累加进at::Tensor::grad。 |
grad_outputs | variable_list | {} | JVP 中的"向量",通常是对应每个输出的梯度;未定义张量可占位,语义同backward的grad_tensors。 |
retain_graph | std::optional<bool> | std::nullopt | 同backward,默认取create_graph的值。 |
create_graph | bool | false | 构造导数图以便计算更高阶导数。 |
allow_unused | bool | false | 为false时,如果指定了未参与计算outputs的输入(其梯度恒为零),会报错;为true则容忍此情况。 |
语义要点与示例
grad计算并返回输出关于输入的梯度之和,其中grad_outputs长度应与outputs匹配;某输出若不requires_grad,其梯度可用torch::Tensor()表示。grad与backward最本质的区别在于:它不修改任何张量的.grad,而是把结果以variable_list形式返回,因此非常适合在无需持有"参数"对象的工具函数或分析代码中使用:
#include <torch/torch.h> auto x = torch::randn({2, 2}, torch::requires_grad()); auto y = x * x; auto z = y.sum(); // 用 grad() 针对特定输出-输入对求梯度(不会写回 x.grad()) auto grads = torch::autograd::grad({z}, {x}); std::cout << grads[0] << std::endl; // 2xTensor 内置的梯度相关方法
除顶层函数外,torch::Tensor自身还提供一组梯度计算方法。文档在 gradient.md 中给出的用法可扩展如下:
// 启用梯度追踪 auto x = torch::randn({2, 2}).requires_grad_(true); // 检查该张量是否要求梯度 bool needs_grad = x.requires_grad(); // backward 之后访问梯度 auto grad = x.grad(); // 从计算图中分离出来(detach 后共享数据但不参与反向传播) auto x_detached = x.detach();配合标量输出的便捷反向调用,最常用的组合是:
auto loss = model->forward(input); // 某种标量损失 loss.backward(); // 自动将梯度累加到所有叶子(模型参数) for (auto& p : model->parameters()) p.grad().zero_(); // 下一轮前清零从实现上看,这些方法最终汇聚到at::Tensor的 autograd hooks。在 torch/csrc/autograd/variable.h 中定义的VariableHooks(实现at::impl::VariableHooksInterface)提供了_backward、requires_grad_、retain_grad、retains_grad、grad_fn、is_leaf、is_view、set_data等一系列回调——Tensor::backward()、.grad()、.detach()、.is_leaf()等在 dispatch 层即通过这些 hooks 落到 autograd 运行时。其中Variable的requires_grad()返回requires_grad_ || grad_fn_(variable.h),即一个非叶子中间张量即便其标志位为假,只要存在grad_fn也会被视为参与求导。
在不需要梯度的推理阶段,应使用torch::NoGradGuard关闭梯度记录(对应 Python 的with torch.no_grad()):
torch::NoGradGuard no_grad; // 进入该作用域后不构建计算图 auto result = model->forward(input); // 不计算梯度源码级原理:backward 与 grad 的内部执行流程
两份高层 API 的实现集中在 torch/csrc/autograd/autograd.cpp,可拆解为三个步骤。
1. _make_grads:构造种子梯度
backward与grad都会首先调用静态函数_make_grads(autograd.cpp)把用户输入规范化为参与反向的梯度序列,其行为包括:
grad_outputs为空时,对每个requires_grad()的输出做两条TORCH_CHECK校验:输出必须为标量(output.numel() == 1,否则报"grad can be implicitly created only for scalar outputs"),且数据类型必须是浮点类型(否则报"grad can be computed only for real scalar outputs...");通过后用at::ones_like生成全 1 种子梯度。grad_outputs非空时,先校验num_tensors == num_gradients;随后逐元素处理:未定义的grad_output走与上面相同的隐式创建逻辑;已定义者直接沿用,但若输出为复数,则校验grad_output与output的复数属性一致(要求 dtype 匹配)。
这段逻辑解释了为什么对非标量tensors调用backward()(不传grad_tensors)会报错——必须显式给出与输出同形状的grad_tensors作为"向量"。
2. run_backward:把任务交给求导引擎
规范化的梯度随后进入run_backward(autograd.cpp):
- 确定根节点(roots):对每个输出调用
impl::gradient_edge(output)取其梯度边,并校验该边确有grad_fn,否则报"element ... of tensors does not require grad and does not have a grad_fn"。 - 确定输出边(output_edges):当显式传入
inputs时,为每个输入定位其grad_fn(无则退回try_get_grad_accumulator);backward因accumulate_grad = true还会对输入调用retain_grad(),以便反向时写入.grad;对计算图中不可达的输入,用Identity节点占位(参见源码中的NOTE [ Autograd Unreachable Input ])。 - 调用引擎:以
Engine::get_default_engine().execute(roots, grad_outputs, keep_graph, create_graph, accumulate_grad, output_edges)真正执行反向传播。 - 未使用输入检查:仅当非空
inputs且allow_unused = false时,校验grad_inputs[i].defined(),对未参与图的输入报错并提示设置allow_unused = true。
3. backward 与 grad 的分流
两个公开函数(autograd.cpp)的差别在run_backward的调用参数上一目了然:
backward:retain_graph缺省时回退为create_graph;调用run_backward(..., /*allow_unused=*/true, /*accumulate_grad=*/true),把梯度累加到叶子。grad:同样回退retain_graph,但以allow_unused的显式传入值、accumulate_grad = false调用,因此梯度只返回、不累加。
参数选型的工程要点
结合文档与上述实现,可归纳出以下工程性结论(默认值语义均有源码依据):
- 非标量输出的 JVP 规则:只要
tensors/outputs中有非标量且requires_grad()的张量,就必须按序提供与其等长的grad_tensors/grad_outputs;对不参与求导的输出可用torch::Tensor()占位,_make_grads会为其自动回退到隐式创建分支。 retain_graph与create_graph的联动:二者缺省时retain_graph取create_graph的值。由于默认create_graph = false,意味着常规一次反向后计算图即被释放;如需多次backward()(例如共享中间结果的多个损失),才显式传retain_graph = true——官方提醒,大多数情况下应通过把中间结果作为输入单独调用grad()等更高效的方式规避,而不是无脑保留整张图。inputs参数与内存效率:backward传入inputs可把反向"裁剪"到仅关注这些输入的梯度(其余张量被忽略,仅在计算路径上被回溯),run_backward只会为其构造 output edges。- 清空梯度:
backward采用累加语义,训练循环中应在每轮迭代前对.grad清零(例如param.grad().zero_(),语义对应 Python 训练循环的optimizer.zero_grad())。 - 高阶导数:把
create_graph置true会构造导数图,从而支持二阶及以上的导数计算(double backward),代价是额外的图内存开销。 - 推理模式:非训练场景务必用
torch::NoGradGuard(或等价机制)关闭自动微分图的构建,避免不必要的内存与算力开销。
参考与延伸阅读
- 本主题原文:docs/cpp/source/api/autograd/gradient.md
- Autograd 模块总览与使用时机:docs/cpp/source/api/autograd/index.md
- 函数声明与逐参数文档:torch/csrc/autograd/autograd.h
- 纯 C++ 实现(
_make_grads/run_backward/backward/grad):torch/csrc/autograd/autograd.cpp - Tensor 方法背后的 autograd hooks(
VariableHooks):torch/csrc/autograd/variable.h - 自定义反向传播与求导模式扩展:docs/cpp/source/api/autograd/custom_functions.md、docs/cpp/source/api/autograd/modes.md
【免费下载链接】pytorchTensors and Dynamic neural networks in Python with strong GPU acceleration项目地址: https://gitcode.com/GitHub_Trending/py/pytorch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考