深度学习框架个别通过主动微分(autograd)机制计算梯度并反向流传。本文尝试通过一个简略的例子、浅显地察看一下OneFlow的autograd的实现机制。
OneFlow刚刚曾经公布了v0.8.0。这个文档仍是基于v0.7.0的代码。

1 主动微分根底

主动微分相干的材料比拟多,个人感觉主动微分的原理介绍这个系列及其援用的材料对相干背景常识的介绍比拟残缺清晰。
上面分几种状况对梯度流传的原理做一些直观解释。

1.1 stack网络的梯度流传

x -> f -> g -> z这个stack网络为例,依据链式法则:

∂z/∂x = ∂z/∂g * ∂g/∂f * ∂f/∂x

理论运行时,在梯度反向流传过程中:

  • z将∂z/∂g传给g。
  • 如果节点g有权重w须要计算梯度,就计算∂z/∂w = ∂z/∂g * ∂g/∂w
  • g须要计算∂g/∂f,再乘以z传过来的梯度,将后果传给f。g只须要给f传递链式乘积的后果,不须要传递各项明细。
  • 在训练阶段的前向计算时,g须要保留∂g/∂f计算依赖的两头后果、以供反向计算时应用。
  • 其它节点的流传状况顺次类推。

1.2 简略graph的梯度流传

以上面这个简略的graph拓扑为例。

在持续之前,须要理解一下多元复合函数微分的根本公式。
下图中,u和v都是对于x和y的函数,z是对于u和v的函数。

依据这个公式能够晓得,z对x的梯度别离沿两条链路流传,z -> u -> xz -> v -> x,节点x将两个梯度之和作为z对x的梯度。

1.3 简单graph的梯度流传

再看一个拓扑略微简单点的例子:

上图能够视为x -> U -> L,其中U是e -> ... -> h的子图。f -> g的子图能够视为V。
对于节点h来说,它须要把梯度传给g和k。
对节点e来说,它须要对f和k传来的梯度求和,才是∂L/∂e
这样,L对x的梯度,仍能够按链路拆解,一条链路前后节点间的梯度是乘积关系,传入的多条链路梯度是加和关系。

这篇blog中有一个简直一样的拓扑图,给出了局部权重参数的梯度公式。

2 autograd中tensor相干的一些基本概念

2.1 叶子节点

OneFlow的autograd文档中介绍了leaf node和root node的概念。只有输入、没有输出的是leaf node,只有输出、没有输入的是root node。
集体了解,如果把weight、bias、data视为计算图的一部分,这些节点就是叶子节点(op不是叶子节点)。尤其是从反向计算图的视角看,这些节点的grad_fn是空,反向流传到这些节点就会进行。

is_leaf和requires_grad有比拟亲密的关系,但二者又是独立的。PyTorch是这样解释的:

  • requires_grad=false的节点都是叶子节点。比方data。
  • requires_grad=true的节点如果是用户创立的,也是叶子节点。比方weight和bias。
  • 在梯度的反向计算过程中,只有叶子节点的梯度才会被填充。对于非叶子节点,如果要填充梯度信息,须要显式设置retain_grad=true
  • requires_grad=true才会计算、填充梯度。比方y = relu(x),y是op创立的、不是叶子节点。但如果x须要计算梯度,则y.requires_grad==true。但不须要为y填充梯度。

对于叶子节点这个概念,目前找到的次要是直观形容,还没看到严格、清晰的定义。也可能是因为用户个别不会间接应用is_leaf,这个概念只是在浏览代码的时候才会波及到。
上面的材料能够供进一步参考:

  • What is the purpose of is_leaf?
  • 叶子节点和tensor的requires_grad参数

2.2 tensor detach

Tensor的detach办法会创立一个新的tensor。新tensor的属性中

  • requires_grad = false
  • is_leaf = true

detach的意思是从grad的反向计算图中把tensor分离出来。新的tensor与原来的对象共享存储,但不参加反向图的拓扑结构。原有对象的requires_grad属性不变。
比方上面的代码,批改一个对象的数据,另一个对象的数据也会扭转。

import oneflow as flowy = flow.Tensor([1, 2, 3])x = y.detach()x[0] = 4assert(y[0] == 4)

3 示例代码

本文通过如下代码来察看OneFlow的autograd机制。

import oneflow as flow# y is scalarx = flow.tensor([-1.0, 2.0], requires_grad=True)y = flow.relu(x).sum()y.backward()print(x.grad)# y is not scalarx = flow.tensor([-1.0, 2.0], requires_grad=True)y = flow.relu(x)y.backward(flow.Tensor([1, 1]))print(x.grad)

y.backward办法有两种接口:

  • 如果y是一个标量(比方loss),不须要传递任何参数。
  • 如果y是一个向量,须要传入一个与y的shape统一的向量作为参数。

为什么会有这种区别呢?上面几篇参考资料中对这个问题做了比拟具体的解释。简略的说:

  • 如果函数的输入是向量,在反向流传的过程中会造成梯度tensor shape的维度收缩,实现简单、性能差。
  • 如果函数的输入是标量,反向流传梯度tensor的shape与参数变量的shape统一,不会呈现维度收缩,更容易实现。
  • 对于向量版本的backward,能够假想存在某个loss函数,backward的参数是loss流传到y这里的梯度。因为前后节点间的梯度是乘积关系,所以用ones代替这个假想的梯度,这样计算结果x.grad就是y对x的梯度。

后续将以y.backward(flow.Tensor([1, 1]))为例察看一下autograd的机制。其反向图只有x <- y这一步。

参考资料

  • 主动求梯度
  • PyTorch 的 backward 为什么有一个 grad_variables 参数?

3.1 梯度后果的存储

Tensor的grad属性,在读取值时调用的是acc_grad()办法(acc应该是accumulate的缩写)。这样就晓得梯度理论存储在哪里,读代码时能够重点关注相干局部。
调用流程如下:

4 autograd相干的类图关系

下图展现了autograd相干类的关系

在看autograd代码之前,能够参照这个类图,理解其中的构造和关系,有助于了解代码中各个局部的作用。

在eager模式下,用户通过op的组合逐渐构建出前向计算图。在执行前向计算的过程中,引擎会为autograd须要的反向计算图记录必要的信息,在调用backward办法时执行这个反向计算图。

对照下面的类图

  • 站在tensor的视角

    • 前向op输入一个tensor y,即TensorIf <- ReluFunctor这部分。
    • 从y能够找到反向计算图理论执行梯度计算的类,即TensorIf -> FunctionNode -> ReLU这个链路。

      • FunctionNode的backward_fn_蕴含了OpExprGradClosure。它只负责计算以后节点的梯度。
      • ReLU是执行梯度计算的类,它会调用ReluGradFunctor这个op来执行梯度计算。
  • 站在反向图存储的视角

    • 反向图相干的信息在FunctionNode中保留。
    • 反向计算图的root是tensor(比方y或loss)的grad_fn_node_变量。
    • FunctionNode的next_functions_示意反向图的上游节点,以后节点把梯度后果传给这些上游节点。这些FunctionNode的连贯就形成了反向图的拓扑构造。
    • tensor的梯度存储门路是TensorImpl.AutogradMeta.acc_grad_
    • AutogradMeta.current_grad_是反向图上游传递到以后节点的梯度共计。如果tensor t输出给op u和v,那么u和v反传的梯度会累加到current_grad_。current应该示意截至以后正在计算时的累加和。
    • FunctionNode尽管并不持有tensor实例,但它持有tensor的AutogradMeta成员变量指针。

      • 基于上述relu的例子中的节点y

        • output_meta_data_y.autograd_meta_
        • input_meta_data_x.autograd_meta_
        • 所以FunctionNode能获取到上下游的梯度数据并进行读写
    • AutoGradCaptureState能够存储一些梯度计算须要的状态信息,比方计算relu的梯度时须要用到它的前向输入后果y。
  • 站在反向图执行的视角

    • GraphTask负责反向图的执行。
    • FunctionNode只保留必要的数据。
    • GraphTask基于这些数据,本人结构遍历须要的数据结构,遍历所有节点、执行梯度计算。

5 前向计算过程中为autograd所做的筹备

反向图的执行过程是数据驱动的,数据的存储构造和内容决定了执行的具体动作。
以下探讨只针对eager模式。lazy模式下,反向图的构建是多轮优化passes的一部分。

之前在探讨Op、Kernel与解释器时曾经理解Interpreter的作用。只是过后重点关注op的执行,疏忽了grad相干的内容。
GetInterpreter返回的其实是一个AutogradInterpreter对象,在它的Apply办法中,调用内嵌Interpreter的同时,也会记录grad计算须要的信息。

AutogradInterpreter::Apply的次要流程如下:

Apply的第一步会先计算requires_grad。只有op的任一输出的requires_grad为true,op的输入的requires_grad也为true(前提是输入的数据类型反对梯度)。y的requires_grad就是在这里决定的。
比方y = relu(x),如果数据类型反对梯度,y.requires_grad就等于x.requires_grad。

而后会调用内嵌的解释器internal_执行相干计算。在调用内嵌解释器期间,会长期禁止梯度模式,比方有些op可能会嵌套、屡次调用解释器(ReluGradFunctor也会通过解释器执行),这些都不须要梯度逻辑。

须要阐明的是,结构x时不会执行grad相干的逻辑,因为inputs的requires_grad都是false,x的requires_grad是在结构的最初才设置的。

上面重点看一下几个外围函数的逻辑细节。

5.1 梯度闭包的构建

后面对类图的阐明中曾经提到,OpExprGradClosure只负责以后节点的梯度计算。
GetOrCreateOpGradClosure函数的外围代码如下:

op_grad_func_.reset(NewObj<std::string, OpExprGradFunctionIf>(proto().op_type_name()));JUST(op_grad_func_->Init(*this));return std::make_shared<OpExprGradClosure>(op_grad_func_);

NewObj会调用AutoRegistrationFactory获取事后注册的工厂、创建对象。之前在探讨Op指令在虚拟机中的执行时也看到过相似的注册机制。
这里op_type_name的值是relu,在代码中搜寻"relu",能够找到注册ReLU的宏。宏开展后的代码如下:

static AutoRegistrationFactory<std::string, OpExprGradFunctionIf>::CreatorRegisterType    g_registry_var4("relu", ([]() { return new ReLU; }));

所以理论返回的对象是ReLU。其Init函数是个空操作。
OpExprGradClosure只是简略的把ReLU存下来供backward执行时调用。

整个调用流程如下:

5.2 捕捉梯度计算须要的数据

调用流程如下:

Capture函数的作用就是为后续的梯度计算保留必要的数据。
须要留神的是,OpExprGradFunction::CaptureIf中保留的是detach的tensor。这些tensor与原来的tensor共享数据;能够读写梯度数据,但不会参加反向图的拓扑结构。这个函数把Interpreter传过来的op的detached outputs传给ReLU::Capture(就是relu的前向输入y),ReLU::Capture就[把output[0]存到ReLUCaptureState的saved_tensors_中](https://github.com/Oneflow-In...)。因为对于relu来说,依据y就能够计算梯度。

5.3 保留反向图构造信息

AutogradInterpreter::Apply中会结构一个lambada表达式backward_fn,其外围逻辑只有一行grad_closure->Apply
这个lambda的次要作用就是捕捉grad_closure这个智能指针。lambda表达式最终会作为FunctionNode的backward_fn_变量。这样才有类图中FunctionNode到OpExprGradClosure这条线,能力从FunctionNode找到closue、执行节点的梯度计算。

AddBackwardFuncPtr这个函数很要害,它的次要工作是为inputs和outputs创立FunctionNode、并保留反向图遍历须要的数据。其输出参数中的inputs/outputs,是前向计算的op的inputs/outputs。
对于relu来说,inputs就是x,outputs就是y。
在上述示例代码中,对于x,因为它是叶子节点、也须要计算梯度,在AddAccumulateFunctionNode会将grad_fn_node设置为一个空操作的函数。

之后会为y结构GraphFunctionNode并造成节点连贯、并保留到grad_fn_node。须要留神的是,这里的backward_fn就是AutogradInterpreter::Apply中的lambda表达式。
须要留神的是,AddBackwardFuncPtr中的inputs/outputs是针对op而言,GraphFunctionNode构造函数中同名变量的是针对FunctionNode而言,二者的含意和指向的对象是不一样的。
结构实现后,x和y的grad_fn_node_字段数据内容如下:

x.grad_fn_node_

op_type_name_: accumulate_gradnext_functions_: 空input_meta_data_: 空output_meta_data_: size=1,x.autograd_meta_,requires_grad=true,is_leaf=trueoutput_tensor_infos_: 对应x, relu前向op的inputbackward_fn_: 空函数,AddAccumulateFunctionNode中定义的

y.grad_fn_node_

op_type_name_: relu_backwardnext_functions_: size=1, x.grad_fn_node, 空操作, AddAccumulateFunctionNode中结构的GraphFunctionNodeinput_meta_data_: x.autograd_meta_, requires_grad=true, is_leaf=trueoutput_meta_data_: size=1, y.autograd_meta_, requires_grad=false, is_leaf=falseoutput_tensor_infos_: 对应y, relu前向op的outputbackward_fn_: AutogradInterpreter::Apply中定义的lambda函数

backward就是依据这些数据,从roots登程,实现反向图的遍历。

6 backward的入口

在local tensor的笔记中提到过,Tensor类在Python端通过一层包装,通过Python机制为Tensor类注册一些办法,backward就是包装的办法之一。
相干的源代码文件如下

  • python/oneflow/framework/tensor.py
  • python/oneflow/autograd/__init__.py
  • oneflow/python/oneflow/autograd/autograd.py
  • oneflow/api/python/autograd/autograd.cpp

C++的调用流程如下

这里反复一下本文应用的示例代码

import oneflow as flowx = flow.tensor([-1.0, 2.0], requires_grad=True)y = flow.relu(x)y.backward(flow.Tensor([1, 1]))print(x.grad)

上述示例代码执行时,Backward的主要参数的值如下:

  • outputs: y, relu输入的tensor
  • out_grads: [1, 1]

CheckAndInitOutGrads返回的是loss通过以后op、传到以后节点的梯度。其局部逻辑就是第3节探讨的

  • 如果y是一个向量,backward必须传入一个与y的shape统一的向量。
  • 如果y是一个标量,backward不要参数,框架会主动结构一个全1的tensor。

7 反向计算中GraphAutogradEngine的调用流程

反向图计算的流程剖析能够联合3类信息

  • 流程代码
  • 上述x和y的grad_fn_node_的值
  • 类图以及类之间的关系

RunBackwardAndSaveGrads4LeafTensor函数的几个参数是:

  • outputs: relu的输入y
  • out_grads: 用户本人结构的ones [1, 1]

7.1 反向传递过去的梯度的累加

RunBackwardAndSaveGrads4LeafTensor函数中,PushPartialTensor的作用就是将loss传过来的梯度累加到autograd_meta_.current_grad_.acc_tensor_。第4节中提到,TensorArg.acc_tensor_存储的就是loss传过来的梯度的共计。这就是roots(即y)接管到的梯度,要么是框架主动创立的ones,要么是用户提供的梯度(通常也是ones)。

这行代码的逻辑能够用如下伪码示意

outputs[i].impl_.autograd_meta_.current_grad_.acc_tensor_ += out_grads[i]

7.2 反向图计算工作的结构与执行

FunctionNode只是记录了反向图的根底信息。RunBackwardAndSaveGrads4LeafTensor中会再结构一个GraphTask对象来示意一次反向计算工作。
GraphTask的构造函数次要是初始化反向图的roots_节点,并将图中各个节点的依赖计数dependencies_置为0。依据示例代码,roots_就是y(通常是loss)。

ComputeDependencies会对反向图进行深度优先遍历、统计图中各个节点的依赖计数。

GraphTask::Apply中实现了反向图的遍历逻辑(传入的save_grad_for_leaf参数是true)。当FunctionNode的依赖为0时,节点才会被放入执行队列,所以对反向图执行的是广度优先遍历,FunctionNode::Apply执行时,它的依赖都执行结束了。

这个函数中,波及梯度计算逻辑次要包含两局部:

  • 调用node->Apply执行单个节点的梯度计算
  • 调用node->AccGrad4LeafTensor存储算好的梯度

7.3 节点的梯度计算

FunctionNode::Apply中,解决output_meta_data_的for循环的外围逻辑能够用如下伪码示意:

acc_tensor = output_meta_data_[i].current_grad_.acc_tensor_if (acc_tensor != nullptr) {  output_grads[i] = acc_tensor_} else {  output_grads[i] = zeros()}

从中能够看进去,output_grads的作用就是拷贝上游传过来的梯度数据(指针),作为backward_fn_的参数。

前面能够看到,backward_fn的外围逻辑是

// d(y)示意以后节点对y的梯度,比方relu对其输入y的梯度。input_grads = d(y) * output_grads

input_grads就是以后节点传给上游节点的梯度,调用backward_fn时会对它进行赋值。

解决input_meta_data的for循环的外围逻辑能够用如下伪码示意。本质就是将以后节点传给上游节点的梯度,累加到上游节点的current_grad上,从而实现梯度的流传。如果tensor输出给多个op,每个op的梯度会加起来。

input_meta_data_[i].current_grad_.acc_tensor_ += input_grads[i]

7.3.1 梯度计算的执行:backward_fn

以下只思考前述示例的root节点的执行。也就是y对应的FunctionNode。对于y来说,backward_fn就是AutogradInterpreter::Apply中定义的lambda表达式。对于relu来说,执行过程如下:

之前在5.1节曾经确认,OpExprGradClosure::impl_就是ReLU。
如前所述,backward_fn的参数中,output_grads是上游传过来的梯度数据,backward_fn须要计算relu的梯度,二者的乘积赋值给in_grads。这些参数会始终传递到ReLU::Apply。

functional::ReluGrad的dFunctor名字是ReluGrad。对应的Functor是ReluGradFunctor(命名空间是oneflow::one::functional::impl)。ReluGradFunctor的op名字是relu_grad,对应kernel的注册被包在一个宏定义中,activation_kernels.cpp中援用了这个宏,并别离针对float和double进行宏开展。
宏开展后的代码如下

static ::oneflow::user_op::UserOpRegisterTrigger<::oneflow::user_op::OpKernelRegistry>    g_register_trigger41 =        ::oneflow::user_op::UserOpRegistryMgr::Get()            .CheckAndGetOpKernelRegistry("relu_grad")            .SetCreateFn([]() {              return user_op::NewOpKernel<BinaryElemwiseXpuKernel<                  DeviceType::kCPU, ReluGradFunctor<double>, double, double, double>>(                  [](user_op::KernelComputeContext* ctx) { return ReluGradFunctor<double>(); },                  "dx", "y", "dy");            })            .SetIsMatchedHob((user_op::HobDeviceType() == DeviceType::kCPU)                             && (user_op::HobDataType("dx", 0) == GetDataType<double>::value))            .SetInplaceProposalFn(                [](const user_op::InferContext&,                   const user_op::AddInplaceArgPair& AddInplaceArgPairFn) -> Maybe<void> {                  for (auto&& maybe___LINE__ = AddInplaceArgPairFn("dx", 0, "dy", 0, true);                       !maybe___LINE__.IsOk();)                    return Error(maybe___LINE__.error())                        .AddStackFrame("oneflow/user/kernels/activation_kernels.cpp", 34,                                       __FUNCTION__);                  return Maybe<void>::Ok();                });

通过SetCreateFn能够晓得,理论执行的kernel是BinaryElemwiseXpuKernel(留神其第二个模版参数是oneflow::ReluGradFunctor,和functional::ReluGrad调用的Functor名字雷同、但命名空间不同)。其构造函数中的FunctorCreateFn参数就是下面宏开展代码中的

[](user_op::KernelComputeContext* ctx) { return ReluGradFunctor<double>(); }

在它的Compute办法中,会将FunctorCreateFn执行的后果(也就是oneflow::ReluGradFunctor)作为参数传给BinaryElemwiseXpuLauncher的operator(),所以其中的functor就是oneflow::ReluGradFunctor。这里实现了relu的梯度计算逻辑。

7.4 梯度的存储

FunctionNode::Apply执行结束后,GraphTask::Apply调用FunctionNode::AccGrad4LeafTensor为叶子节点拷贝梯度数据。
在上述例子中,因为y不是叶子节点,解决到y.grad_fn_node_时不会进行本质解决。对于x,会调用CopyOrAccGrad,这个函数逻辑的伪码模式如下

autograd_meta.acc_grad_ += autograd_meta.current_grad_

autograd_meta.acc_grad_就是Python端读到的x的梯度。

8 gdb断点示例

set breakpoint pending onbreak oneflow::one::AutogradInterpreter::Applybreak oneflow::one::BuiltinOpExprImpl<oneflow::UserOpConf>::GetOrCreateOpGradClosurebreak oneflow::one::OpExprGradFunction<oneflow::one::ReLUCaptureState>::CaptureIfbreak oneflow::one::GraphAutogradEngine::AddBackwardFuncPtrbreak autograd_engine.cpp:495break oneflow::one::AddAccumulateFunctionNodebreak oneflow::one::GraphFunctionNode::GraphFunctionNodebreak oneflow::one::AutogradEngine::RunBackwardAndSaveGrads4LeafTensorIfbreak oneflow::one::GraphAutogradEngine::RunBackwardAndSaveGrads4LeafTensorbreak oneflow::one::GraphTask::Applybreak oneflow::one::FunctionNode::Applybreak oneflow::one::TensorArg::GetAccTensorbreak oneflow::one::functional::impl::BinaryFunctor::operator()break oneflow::one::functional::TensorProcessor::Applybreak oneflow::one::(anonymous namespace)::CopyOrAccGradbreak elementwise_xpu_kernel.h:48break oneflow::ReluGradFunctor<float>::operator()

9 参考资料

  • oneflow v0.7.0
  • OneFlow学习笔记:Autograd解析
  • OneFlow: AUTOGRAD
  • 主动微分的原理介绍
  • 主动求梯度
  • PyTorch 的 backward 为什么有一个 grad_variables 参数?
  • PyTorch 101, Part 1: Understanding Graphs, Automatic Differentiation and Autograd