撰文 | 郑建华
更新|赵露阳、王迎港
深度学习结构一般经过主动微分(autograd)机制核算梯度并反向传达。本文尝试经过一个简略的比方,粗浅地调查一下OneFlow的autograd的完成机制。
1
主动微分根底
主动微分相关的材料比较多,个人感觉主动微分的原理介绍(mp.weixin.qq.com/s/BwQxmNoSB… )这个系列及其引用的材料对相关背景知识的介绍比较完整明晰。
下面分几种状况对梯度传达的原理做一些直观解说。
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 -> x和z -> 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.paperspace.com/pytorch-101… )中有一个简直一样的拓扑图,给出了部分权重参数的梯度公式。
2
autograd中tensor相关的一些根本概念
2.1 叶子节点
OneFlow的autograd文档(docs.oneflow.org/en/master/b… )中介绍了leaf node和root node的概念。只要输出、没有输入的是leaf node,只要输入、没有输出的是root node。
个人了解,假如把weight、bias、data视为核算图的一部分,这些节点便是叶子节点(op不是叶子节点)。尤其是从反向核算图的视角(discuss.pytorch.org/t/what-is-t… )看,这些节点的grad_fn是空,反向传达到这些节点就会停止。
is_leaf和requires_grad有比较密切的联系,但二者又是独立的。PyTorch是这样解说的:(pytorch.org/docs/stable…)
- 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(discuss.pytorch.org/t/what-is-t… ),这个概念仅仅在阅览代码的时分才会触及到。
下面的材料能够供进一步参阅:
- What is the purpose of
is_leaf
? (discuss.pytorch.org/t/what-is-t…) - 叶子节点和tensor的requires_grad参数(zhuanlan.zhihu.com/p/85506092 )
2.2 tensor detach
Tensor的detach办法(github.com/Oneflow-Inc… )会创立一个新的tensor,新tensor的属性中
-
requires_grad = false
-
is_leaf = true
detach的意思是从grad的反向核算图中把tensor分离出来。新的tensor与本来的目标共享存储,但不参加反向图的拓扑结构。原有目标的requires_grad属性不变。
比方下面的代码,修改一个目标的数据,另一个目标的数据也会改变。
import oneflow as flow
y = flow.Tensor([1, 2, 3])
x = y.detach()
x[0] = 4
assert(y[0] == 4)
3
示例代码
本文经过如下代码来调查OneFlow的autograd机制。
import oneflow as flow
# y is scalar
x = flow.tensor([-1.0, 2.0], requires_grad=True)
y = flow.relu(x).sum()
y.backward()
print(x.grad)
# y is not scalar
x = 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这一步。
参阅材料
- 主动求梯度 (tangshusen.me/Dive-into-D… )
- PyTorch 的 backward 为什么有一个 grad_variables 参数?(zhuanlan.zhihu.com/p/29923090 )
3.1 梯度成果的存储
Tensor的grad属性(github.com/Oneflow-Inc… ),在读取值时调用的是acc_grad()办法(acc应该是accumulate的缩写)。这样就知道梯度实践存储在哪里,读代码时能够要点重视相关部分。
调用流程如下:
注:图片中的MirroredTensor在最新源码中,现已更名为LocalTensor,其实是一样的。
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的一部分(github.com/Oneflow-Inc… )。
之前在评论Op、Kernel与解说器(mp.weixin.qq.com/s/gXH7HZ9cF…) 时现已了解Interpreter的效果。仅仅其时要点重视op的履行,疏忽了grad相关的内容。
GetInterpreter(github.com/Oneflow-Inc… )回来的其实是一个AutogradInterpreter目标(github.com/Oneflow-Inc… ),在它的Apply办法中(github.com/Oneflow-Inc… ),调用内嵌Interpreter的一起,也会记载grad核算需求的信息。
AutogradInterpreter::Apply的首要流程如下:
Apply的第一步会先核算requires_grad。只要op的任一输入的requires_grad为true,op的输出的requires_grad也为true(github.com/Oneflow-Inc… )(前提是输出的数据类型支持梯度)。y的requires_grad便是在这儿决议的。
比方y = relu(x),假如数据类型支持梯度,y.requires_grad就等于x.requires_grad。
然后会调用内嵌的解说器internal_履行相关核算。在调用内嵌解说器期间,会暂时禁止梯度形式,比方有些op可能会嵌套、多次调用解说器(ReluGradFunctor也会经过解说器履行),这些都不需求梯度逻辑。
需求说明的是,结构x时不会履行grad相关的逻辑,因为inputs的requires_grad都是false,x的requires_grad是在结构的终究才设置的(github.com/Oneflow-Inc… )。
下面要点看一下几个中心函数的逻辑细节。
5.1 梯度闭包的构建
前面临类图的说明中现已提到,OpExprGradClosure只担任其时节点的梯度核算。
GetOrCreateOpGradClosure函数(github.com/Oneflow-Inc… )的中心代码如下:
template<>
Maybe<OpExprGradClosure> BuiltinOpExprImpl<UserOpConf>::GetOrCreateOpGradClosure() const {
if (!op_grad_func_.get()) {
...
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(github.com/Oneflow-Inc… )获取预先注册的工厂、创立目标。之前在评论Op指令在虚拟机中的履行(mp.weixin.qq.com/s/r5LOoEh-Q…) 时也看到过类似的注册机制。
这儿op_type_name的值是relu,在代码中查找”relu”,能够找到注册ReLU的宏(github.com/Oneflow-Inc… )。宏展开后的代码如下:
static AutoRegistrationFactory<std::string, OpExprGradFunctionIf>::CreatorRegisterTypeg_registry_var4("relu", ([]() { return new ReLU; }));
所以实践回来的目标是ReLU(github.com/Oneflow-Inc… )。其Init函数是个空操作。
OpExprGradClosure仅仅简略的把ReLU存下来供backward履行时调用。整个调用流程如下:
5.2 捕获梯度核算需求的数据
调用流程如下:
Capture函数(github.com/Oneflow-Inc… )的效果便是为后续的梯度核算保存必要的数据。
需求留意的是,OpExprGradFunction::CaptureIf(github.com/Oneflow-Inc… )中保存的是detach的tensor。这些tensor与本来的tensor共享数据;能够读写梯度数据,但不会参加反向图的拓扑结构。
这个函数把Interpreter传过来的op的detached outputs传给ReLU::Capture(github.com/Oneflow-Inc… )(便是relu的前向输出y),ReLU::Capture就把output[0]存到ReLUCaptureState的saved_tensors_中(github.com/Oneflow-Inc… )。因为关于relu来说,根据y就能够核算梯度。
5.3 保存反向图结构信息
AutogradInterpreter::Apply中会结构一个lambada表达式backward_fn(github.com/Oneflow-Inc… ),其间心逻辑只要一行grad_closure->Apply。
这个lambda的首要效果便是捕获grad_closure这个智能指针。lambda表达式终究会作为FunctionNode的backward_fn_变量。这样才有类图中FunctionNode到OpExprGradClosure这条线,才能从FunctionNode找到closue、履行节点的梯度核算。
GetThreadLocalAutogradEngine()->AddNode这个函数(github.com/Oneflow-Inc… )很要害,AddNode的首要任务(github.com/Oneflow-Inc… )是为inputs和outputs创立FunctionNode、并保存反向图遍历需求的数据。其输入参数中的inputs/outputs,是前向核算的op的inputs/outputs。关于relu来说,inputs便是x,outputs便是y。
在上述示例代码中,关于x,因为它是叶子节点、也需求梯度,在AddAccumulateFunctionNode会将grad_fn_node设置为一个空操作的函数(github.com/Oneflow-Inc… )。之所以是空操作,是因为叶子节点只需求存储梯度、不需求自己核算梯度;它所需求的梯度核算成果会由反向图的上游节点保存到x.autograd_meta_中。
之后会为y结构GraphFunctionNode并形成节点衔接(github.com/Oneflow-Inc… )、并保存到grad_fn_node(github.com/Oneflow-Inc… )。需求留意的是,这儿的backward_fn便是AutogradInterpreter::Apply中的lambda表达式(github.com/Oneflow-Inc… )。
需求留意的是,AddBackwardFuncPtr中的inputs/outputs是针对op而言,GraphFunctionNode结构函数中同名变量的是针对FunctionNode而言,二者的含义和指向的目标是不一样的。
结构完成后,x和y的grad_fn_node_字段数据内容如下:
x.grad_fn_node_
name_: accumulate_grad
next_functions_: 空
input_meta_data_: 空
output_meta_data_: size=1,x.autograd_meta_,requires_grad=true,is_leaf=true
output_tensor_infos_: 对应x, relu前向op的input
backward_fn_: 空函数,AddAccumulateFunctionNode中界说的
y.grad_fn_node_
name_: relu_backward
next_functions_: size=1, x.grad_fn_node, 空操作, AddAccumulateFunctionNode中结构的GraphFunctionNode
input_meta_data_: x.autograd_meta_, requires_grad=true, is_leaf=true
output_meta_data_: size=1, y.autograd_meta_, requires_grad=false, is_leaf=false
output_tensor_infos_: 对应y, relu前向op的output
backward_fn_: AutogradInterpreter::Apply中界说的lambda函数
backward便是根据这些数据,从roots出发,完成反向图的遍历。
6
backward的进口
在《OneFlow源码阅览4:tensor类型系统与local tensor》(segmentfault.com/a/119000004… )中提到过,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 flow
x = flow.tensor([-1.0, 2.0], requires_grad=True)
y = flow.relu(x)
y.backward(flow.Tensor([1, 1]))
print(x.grad)
上述示例代码履行时,Backward(github.com/Oneflow-Inc… )的首要参数的值如下:
- outputs: y, relu输出的tensor
- out_grads: [1, 1]
CheckAndInitOutGrads(github.com/Oneflow-Inc… )回来的是loss经过其时op、传到其时节点的梯度。其部分逻辑便是第3节评论的
- 假如y是一个向量,backward必须传入一个与y的shape共同的向量(github.com/Oneflow-Inc… )。
- 假如y是一个标量,backward不要参数,结构会主动结构一个全1的tensor(github.com/Oneflow-Inc… )。
7
autograd.grad
通常,我们都会经过tensor.backward或autograd.backward触发梯度核算和反向传达,但偶尔也会用到autograd.grad(oneflow.readthedocs.io/en/master/g… )这个接口。autograd.grad和autograd.backward很类似,不同之处首要在于:
-
autograd.backward以outputs(Tensor)作为起点,核算每一个叶子节点的梯度,并且梯度可累积,且保存于对应inputs(Tensor)的tensor.grad上。
-
而autograd.grad 接口则是从指定的 outputs为起点,以指定的 inputs为结尾核算梯度,并按 inputs 参数的顺序回来一个由inputs相对应的grads构成的TensorTuple。且梯度是直接获得的,不在inputs的tensor.grad中累积。
因为autograd.grad就只履行后向核算图中的一部分,在OneFlow 静态图形式下(lazy mode)TaskGraph 核算入度时就需求做一次剪枝,把不需求核算的结点去掉(参阅 TaskGraph::ComputeDependenciesAndPruneNode(github.com/Oneflow-Inc…) 接口),一起记载每个 inputs 序号,在 FunctionNode::Apply (github.com/Oneflow-Inc… )履行后,把需求保存的 grad 及时捕获,终究回来给用户。
8
反向核算中GraphAutogradEngine的调用流程
反向图核算的流程分析能够结合3类信息
- 流程代码
- 上述x和y的grad_fn_node_的值
- 类图以及类之间的联系
RunBackwardAndSaveGrads4LeafTensor(github.com/Oneflow-Inc… )函数的几个参数是:
- outputs: relu的输出y
- out_grads: 用户自己结构的ones [1, 1]
8.1 反向传递过来的梯度的累加
RunBackwardAndSaveGrads4LeafTensor(github.com/Oneflow-Inc… )函数中,PushPartialTensor(github.com/Oneflow-Inc… )的效果便是将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]
8.2 反向图核算任务的结构与履行
FunctionNode仅仅记载了反向图的根底信息。RunBackwardAndSaveGrads4LeafTensor中会再结构一个GraphTask目标来表明一次反向核算任务。
- GraphTask的结构函数(github.com/Oneflow-Inc… )首要是初始化反向图的roots_节点,并将图中各个节点的依靠计数dependencies_置为0。根据示例代码,roots_便是y(通常是loss)。
- ComputeDependencies(github.com/Oneflow-Inc… )会对反向图进行深度优先遍历、核算图中各个节点的依靠计数。
- GraphTask::Apply(github.com/Oneflow-Inc… )中完成了反向图的遍历逻辑(传入的save_grad_for_leaf参数是true)。当FunctionNode的依靠为0时,节点才会被放入履行行列(github.com/Oneflow-Inc… ),后续会对反向图履行按拓扑序遍历。FunctionNode::Apply履行时,它的依靠都履行结束了。GraphTack::Apply这个函数中,触及梯度核算逻辑首要包含两部分:
- 调用node->Apply履行单个节点的梯度核算(github.com/Oneflow-Inc… )
- 调用node->AccGrad4LeafTensor存储算好的梯度(github.com/Oneflow-Inc…)
8.3 节点的梯度核算
FunctionNode::Apply中(github.com/Oneflow-Inc… ),处理output_meta_data_的for循环(github.com/Oneflow-Inc… )的中心逻辑能够用如下伪码表明:
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(github.com/Oneflow-Inc… )的中心逻辑是:
// d(y)表明其时节点对y的梯度,比方relu对其输出y的梯度。
input_grads = d(y) * output_grads
input_grads便是其时节点传给下流节点的梯度,调用backward_fn时会对它进行赋值。
处理input_meta_data的for循环的中心逻辑(github.com/Oneflow-Inc… )能够用如下伪码表明。本质便是将其时节点传给下流节点的梯度,累加到下流节点的current_grad上,从而完成梯度的传达。假如tensor输入给多个op,每个op的梯度会加起来。
input_meta_data_[i].current_grad_.acc_tensor_ += input_grads[i]
8.3.1 梯度核算的履行:backward_fn
以下只考虑前述示例的root节点的履行。也便是y对应的FunctionNode。关于y来说,backward_fn便是AutogradInterpreter::Apply中界说的lambda表达式(github.com/Oneflow-Inc… )。关于relu来说,履行进程如下:
之前在5.1节现已承认,OpExprGradClosure::impl_便是ReLU(github.com/Oneflow-Inc… )。
如前所述,backward_fn的参数中,output_grads是上游传过来的梯度数据,backward_fn需求核算relu的梯度,二者的乘积赋值给in_grads。这些参数会一直传递到ReLU::Apply(github.com/Oneflow-Inc… )。
functional::ReluGrad(github.com/Oneflow-Inc… )的Functor姓名是ReluGrad。对应的Functor是ReluGradFunctor(github.com/Oneflow-Inc… )(命名空间是oneflow::one::functional::impl)。
ReluGradFunctor之后,是根据Primitive kernel完成的核算逻辑。 ReluGradFunctor中对应op姓名是”relu_grad”,这个relu_grad的注册被包在一个宏界说(github.com/Oneflow-Inc… )中,实践上会回来一个BinaryPrimitiveKernel,这是一种稍显特殊的根据Primitive的kernel,其详细为ep::primitive下的一种BroadcastElementwiseBinary工厂(github.com/Oneflow-Inc… ),其对应的cpu和cuda注册别离位于:
- oneflow/core/ep/cpu/primitive/broadcast_elementwise_binary.cpp
- oneflow/core/ep/cuda/primitive/broadcast_elementwise_binary.cu
终究完成位于binary_functor.h(github.com/Oneflow-Inc… ):
template<DeviceType device, typename Src, typename Dst>
struct BinaryFunctor<device, BinaryOp::kReluBackwardWithDyY, Src, Dst> {
OF_DEVICE_FUNC BinaryFunctor(Scalar attr0, Scalar attr1) {}
OF_DEVICE_FUNC Dst operator()(Src dy, Src y) const {
return static_cast<Dst>((y <= static_cast<Src>(0.0)) ? static_cast<Src>(0.0) : dy);
}
};
至此,完成了梯度核算的逻辑。
8.4 梯度的存储
FunctionNode::Apply履行结束后,GraphTask::Apply调用FunctionNode::AccGrad4LeafTensor(github.com/Oneflow-Inc… )为叶子节点复制梯度数据。
在上述比方中,因为y不是叶子节点,处理到y.grad_fn_node_时不会进行本质处理。关于x,会调用CopyOrAccGrad(github.com/Oneflow-Inc… ),这个函数逻辑的伪码形式如下
autograd_meta.acc_grad_ += autograd_meta.current_grad_
autograd_meta.acc_grad_便是Python端读到的x的梯度。
8.5 暂时梯度的开释机制
上述第5.点中,描绘了前向图构建进程中现已存放了对应的FunctionNode以及前向op所对应的反向backward_fn,实践求梯度、反向传达时,这一个个 backward_fn串联起来构成了反向核算图拓扑,关于其间的每个节点,backward_fn中都能够表明为output_grads、inputs/outputs(可选) -> inputs_grads的一个函数。
其间output_grads 便是链式法则中上游核算的累计梯度,其时节点backward_fn核算完成后,该节点的output_grads就不会再被运用到,从而变成了暂时梯度。之后会调用 FunctionNode->ReleaseOutTensorArgs()(github.com/Oneflow-Inc…) 来及时开释该暂时梯度。
参阅材料
- oneflow master(github.com/Oneflow-Inc…)
- OneFlow学习笔记:Autograd解析(mp.weixin.qq.com/s/6zm4xRpRk…)
- OneFlow: AUTOGRAD(docs.oneflow.org/en/master/b…)
- 主动微分的原理介绍(mp.weixin.qq.com/s/BwQxmNoSB…)
- 主动求梯度(tangshusen.me/Dive-into-D…)
- PyTorch 的 backward 为什么有一个 grad_variables 参数?(zhuanlan.zhihu.com/p/29923090)
- PyTorch 101, Part 1: Understanding Graphs, Automatic Differentiation and Autograd(blog.paperspace.com/pytorch-101…)
欢迎下载体验 OneFlow v0.8.0 最新版别: github.com/Oneflow-Inc…