背景
在PyTorch的Tensor系列上一篇文章中:
Gemfield:PyTorch的Tensor(上)zhuanlan.zhihu.comGemfield介绍了一个Tensor的创建过程,特别是在创建一个Tensor的时候,调用栈从Python到C++再回到Python的过程。与此同时,在内存中对应的是一个Variable实例的创建(严格来说,Variable实例的某个field也是Variable实例)。
在本文,Gemfield将介绍PyTorch的Tensor中autograd相关的部分。autograd是PyTorch之所以是神经网络框架的一个重要原因。autograd机制提供了对Tensor上所有操作自动求微分的功能。我们知道,对于一个Variable来说,它的唯一数据成员就是impl_,这个impl_成员是TensorImpl 类型,在初始化阶段impl_会被实例化为一个Variable::Impl的实例(TensorImpl的子类):
Variable --> impl_ = Variable::Impl实例
对于一个Variable的autograd来说,autograd的部分就体现在impl_的autograd_meta_成员上。在初始化阶段,autograd_meta_会被初始化为一个Variable::AutogradMeta的实例(AutogradMetaInterface的子类),或者会被初始化为一个Variable::DifferentiableViewMeta的实例(Variable::AutogradMeta的子类),然后通过Variable的 get_autograd_meta()来访问。实际上,autograd_meta_正是一个Variable是普通tensor还是带autograd功能的tensor的唯一标识:
#1 Variable是个Tensor,没有requires_grad
Variable --> impl_ --> autograd_meta_ = None#2
Variable --> impl_ --> autograd_meta_ = Variable::AutogradMeta实例#3
Variable --> impl_ --> autograd_meta_ = Variable::DifferentiableViewMeta实例
而一个Variable::AutogradMeta实例有如下成员,这些成员正是PyTorch autograd系统的中坚:
# Variable::AutogradMeta 和 Variable::DifferentiableViewMeta
Variable grad_;
std::shared_ptr<Function> grad_fn_;
std::weak_ptr<Function> grad_accumulator_;
VariableVersion version_counter_;
std::vector<std::shared_ptr<FunctionPreHook>> hooks_;
bool requires_grad_;
bool is_view_;
uint32_t output_nr_;# 仅 Variable::DifferentiableViewMeta
Variable base_;
uint32_t attr_version;
- 1,grad_是另外一个Variable,存储当前Variable实例的梯度;
- 2,grad_fn是个Function的实例,非leaf variables才有。通过Variable的grad_fn()来访问,实际上,PyTorch中就是通过是否grad_fn_ == nullptr来判断一个Variable是否是leaf variable的;
- 3,grad_accumulator_是个Function的实例,只有leaf variables才有。通过Variable的grad_accumulator()来访问;
- 4,version_counter_里有个version number;
- 5,hooks_可以是一组;
- 6,requires_grad_ 是个flag,表明此Variable实例是否需要grad;
- 7,is_view_是个flag,表明此Variable实例是否是个view(没有实际存储,基于base的variable);
- 8,output_nr_是个数字;
- 9,base_是view的base variable;
- 10,attr_version是个数字。
我们通过下面这一小段代码来演示下这个能力:
gemfield = torch.ones(2, 2, requires_grad=True)
syszux = gemfield + 2
civilnet = syszux * syszux * 3
gemfieldout = civilnet.mean()
gemfieldout.backward()
特别的,对于在python会话中的每一步操作,gemfield都将映射到内存上类实例中的成员/结构体的变化。
Tensor创建:gemfield = torch.ones(2, 2, requires_grad=True)
我们使用gemfield = torch.ones(2, 2, requires_grad=True) 语句来创建了一个tensor。在https://zhuanlan.zhihu.com/p/54896021一文中已经介绍过了,这个调用会在内存中产生如下一个Variable实例:
#gemfieldVariable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = [[1., 1.],[1., 1.]]--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= None--> grad_accumulator_ (Function实例)= None--> version_counter_ = 0--> hooks_ len = 0--> requires_grad_ = True--> is_view_ = false--> output_nr_ = 0--> base_ = Not exist
这个gemfield变量就是图中的leaf,为什么呢?因为这是用户直接创建的(不是经过计算得到的),位于图中最“底端/外侧”的位置,没有子节点。这个时候,Tensor gemfield的grad是None,grad_fn是None。output_nr_为0,表明这个Variable是function的第1个输出。
Tensor的简单加法:syszux = gemfield + 2
我们使用 syszux = gemfield + 2 来得到一个新的Tensor,名字为syszux。这个加法嘛,在初始化的时候已经和C++中的THPVariable_add函数绑定上,并注册到Python的torch._C._TensorBase符号上了:
PyMethodDef variable_methods[] = {{"__add__", (PyCFunction)THPVariable_add, METH_VARARGS | METH_KEYWORDS, NULL},......
而THPVariable_add的定义如下:
static PyObject * THPVariable_add(PyObject* self_, PyObject* args, PyObject* kwargs)
{......return wrap(dispatch_add(self, r.tensor(0), r.scalar(1)));
}
1,scalar to tensor
在这个函数中,首先要将syszux = gemfield + 2 中的2从标量转换为tensor,这个转换逻辑如下:
auto tensor = scalar_to_tensor(scalar);
tensor.unsafeGetTensorImpl()->set_wrapped_number(true);
return autograd::make_variable(tensor);
现在scalar 2已经变成了内存中一个Variable的实例,在add真正执行之前,在内存中已经有2个Variable实例了,分别是gemfield和2:
#gemfield
Variable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = [[1., 1.],[1., 1.]]--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= None--> grad_accumulator_ (Function实例)= None--> version_counter_ = 0--> hooks_ len = 0--> requires_grad_ = True--> is_view_ = false--> output_nr_ = 0--> base_ = Not exist#scalar 2
Variable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = [2.]--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= None--> grad_accumulator_ (Function实例)= None--> version_counter_ = 0--> hooks_ len = 0--> requires_grad_ = False--> is_view_ = false--> output_nr_ = 0--> base_ = Not exist
不过要注意了,由于gemfield是一个leaf variable,因此在后文的加法运算中,gemfield会被触发Variable::grad_accumulator()调用,这会初始化gemfield的grad_accumulator_成员,因此在那之后,gemfield在内存中的样子就会变为:
#gemfield
Variable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = [[1., 1.],[1., 1.]]--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= None--> grad_accumulator_ (Function实例)= AccumulateGrad实例0x55ca7f304500--> ......
2,dispatch to add
既然是分发,肯定是为了分发到对应种类的Type上。分发的核心逻辑是:
dispatch_type().add(*this, other, alpha)
dispatch_type是at::Tensor类上的一个方法,根据其impl(TensorImpl类)的id、dtype等信息,推导出Type类型(VariableType)。在这个例子上,根据输入参数的类型,最终分发到torch/csrc/autograd/generated/VariableType.cpp(参考autograd的代码生成)中的VariableType的add方法上:
Tensor VariableType::add(const Tensor & self, const Tensor & other, Scalar alpha) const {std::shared_ptr<AddBackward0> grad_fn;grad_fn = std::shared_ptr<AddBackward0>(new AddBackward0(), deleteFunction);grad_fn->set_next_edges(collect_next_edges( self, other ));grad_fn->alpha = alpha;auto tmp = ([&]() {at::AutoNonVariableTypeMode non_var_type_mode(true);return baseType->add(self_, other_, alpha);})();auto result = as_variable(tmp);set_history(flatten_tensor_args( result ), grad_fn);return result;
}
匿名函数中的baseType->add最终又调用了如下的调用栈,只能说一个简单的加法在graph设计中也会变得比较复杂:
VariableType::add
|
V
baseType->add(self_, other_, alpha)
|
V
#ATen/TypeDefault.cpp
TypeDefault::add
|
V
#aten/src/ATen/native/BinaryOps.cpp
add(const Tensor& self, const Tensor& other, Scalar alpha)
|
V
#此处依赖初始化阶段的REGISTER_DISPATCH的工作
add_stub
|
V
#aten/src/ATen/native/cpu/BinaryOpsKernel.cpp.AVX2.cpp
add_kernel
|
V
#aten/src/ATen/native/cpu/BinaryOpsKernel.cpp.AVX2.cpp
binary_kernel_vec
|
V
binary_loop
3,构建autograd
3.1 构建grad_fn (AddBackward0)
AddBackward0是加法运算的反向传播算法,构建这个grad_fn实例是通过如下一行代码完成的:
std::shared_ptr<AddBackward0> grad_fn = std::shared_ptr<AddBackward0>(new AddBackward0(), deleteFunction);
可以看到,对于加法,对应的grad_fn是AddBackward0。在聊AddBackward0之前,你有必要先了解下PyTorch中的Function继承体系(可以参考 https://zhuanlan.zhihu.com/p/61765561 ),还记得吧:
class AddBackward0 : public TraceableFunction
class TraceableFunction : public Function
gemfield + 2 加法运算完成后,就会创建出来这个AddBackward0(一个Function)实例,并且使用collect_next_edges()搜集gemfield和2的grad_fn或者grad_accumulator。gemfield是leaf variable,因此搜集的就是grad_accumulator(2啥都没有),类型是AccumulateGrad(1个Function的实例),然后再向这个AddBackward0实例上注册:
grad_fn->set_next_edges(collect_next_edges( self, other ));
next edges就是self和other的gradient_edge():gradient_edge()函数返回的是Edge实例(通过Variable的grad_fn_构建)。当set_next_edges调用完成后,一个Function的next_edges_成员(类型为std::vector<Edge>)就会被初始化。
1,如果一个Variable是内部创建的(通过运算得到,比如syszux变量),那么grad_fn_就是这个Variable的gradient function;
2,如果一个Variable是用户创建的(比如gemfield变量),则grad_fn_就是这个Variable的gradient accumulator,也就是一个AccumulateGrad类(Function子类)的实例。
但不管怎样,Variable的grad_fn_成员在这里终归是要构建成一个Edge实例并以此作为gradient_edge()函数的返回:
Edge gradient_edge() const {if (const auto& gradient = grad_fn()) {return Edge(gradient, output_nr());} else {return Edge(grad_accumulator(), 0);}
}
对于leaf Variable来说,grad_fn是null,在这种情况下Variable将使用gradient accumulator, 用来累加输出给这个Variable的梯度。注意只有当Variable的requires_grad = True时才有gradient accumulators。经过构建后的AddBackward0的内存布局如下所示:
#grad_fn AddBackward0, requires_grad == True
Function实例 --> sequence_nr_ (uint64_t) = 0--> next_edges_ (edge_list) --> std::vector<Edge> = [(AccumulateGrad实例0x55ca7f304500, 0),(0, 0)]--> input_metadata_ --> [(type, shape, device)...] = None--> alpha (Scalar) = 1--> pre_hooks_ = None--> post_hooks_ = None--> anomaly_metadata_ = None--> apply() --> 使用 AddBackward0 的apply#grad_fn,requires_grad == False
Function实例 --> None
3.2 构建grad_accumulator_ (AccumulateGrad)
前文已经提到过,在grad_fn调用collect_next_edges去搜集输入参数(Variable实例)的edges时,对于leaf gemfield来说,会触发Variable::grad_accumulator()调用,在一个Variable第一次调用这个API的时候,会去初始化它的grad_accumulator_成员:
result = std::make_shared<AccumulateGrad>(Variable(std::move(intrusive_from_this)));
autograd_meta->grad_accumulator_ = result;
这会new一个AccumulateGrad对象,使用UINT64_MAX(也就是18446744073709551615)来初始化Function的sequence_nr_成员。构建完成后,grad_accumulator_(0x55ca7f304500)在内存中看起来是这个样子的:
# 类AccumulateGrad,继承自Function,是一个Function实例
Function实例 --> sequence_nr_ (uint64_t) = UINT64_MAX--> next_edges_ (edge_list) --> None--> input_metadata_ --> [(type, shape, device)...] = [(CPUFloatType, [2, 2], cpu)]--> pre_hooks_ = None--> post_hooks_ = None--> anomaly_metadata_ = None--> apply() --> 使用 AccumulateGrad 的apply--> variable = gemfield
初始化完毕后,此时gemfield这个Variable的grad_accumulator_已经被赋值为AccumulateGrad实例(0x55ca7f304500)。
4 构建Variable(syszux)
在加法表达式完成之后,内存中也就产生了syszux。刚产生的syszux实例在内存中看起来是这样的:
//syszux
Variable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = [[3., 3.],[3., 3.]]--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= None--> grad_accumulator_ (Function实例)= None--> version_counter_ = 0--> hooks_ len = 0--> requires_grad_ = False--> is_view_ = false--> output_nr_ = 0--> base_ = Not exist
之后会使用set_history(flatten_tensor_args( result ), grad_fn)来设置syszux的gradient_edge,这个函数做了2件事情:
1,AddBackward0实例中的input_metadata_ 追加了Variable syszux的(type, shape, device),追加完成后,syszux信息在input_metadata_中的index就是下文中会用到的output_nr_,此处为0(第一个元素嘛);这样AddBackward0实例在内存中看起来是这样:
//grad_fn->add_input_metadata(variable);
grad_fn_ --> input_metadata_ += (variable.type, variable.shape, variable.device) = []#AddBackward0实例, requires_grad == True
Function实例 --> sequence_nr_ (uint64_t) = 0--> next_edges_ (edge_list) --> std::vector<Edge> = [(AccumulateGrad实例, 0),(0, 0)]--> input_metadata_ --> [(type, shape, device)...] = [(CPUFloatType, [2, 2],cpu])]--> alpha (Scalar) = 1--> pre_hooks_ = None--> post_hooks_ = None--> anomaly_metadata_ = None--> apply() --> 使用 AddBackward0 的apply
2,syszux的autograd_meta中的grad_fn_ 被赋值成了上述AddBackward0实例,而autograd_meta中的output_nr_被赋值成了上文中的“当前Variable信息在input_metadata_中的index”。这样syszux实例在内存中看起来就是这样:
//as_variable_ref(variable).set_gradient_edge({grad_fn, output_nr});
Variable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = [[3., 3.],[3., 3.]]--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= AddBackward0实例0x55ca7f872e90(参考上面)--> grad_accumulator_ (Function实例)= None--> version_counter_ = 0--> hooks_ len = 0--> requires_grad_ = False--> is_view_ = false--> output_nr_ = 0--> base_ = Not exist
完成这一步后,如果你在python会话中打印syszux的grad_fn属性,就会调用THPVariable_get_grad_fn函数,接着调用Variable的grad_fn()函数拿到grad_fn_,然后使用 functionToPyObject()函数从cpp_function_types表中查找对应的Python表示:
>>> syszux.grad_fn
<AddBackward object at 0x7f1550f2e0d0>
civilnet = syszux * syszux * 3
现在进入civilnet = syszux * syszux * 3这个表达式的解析了,这个表达式执行期间,就会产生两次python object的__mul__(乘法)调用,并且会new一个新的Variable civilnet出来。
1,Python到C++
在初始化阶段,Variable的python绑定已经完成了下面的初始化:
PyMethodDef variable_methods[] = {{"__add__", (PyCFunction)THPVariable_add, METH_VARARGS | METH_KEYWORDS, NULL},{"__mul__", (PyCFunction)THPVariable_mul, METH_VARARGS | METH_KEYWORDS, NULL},......
}
因此这个表达式会导致对C++函数THPVariable_mul的调用,并且是2次:两次乘法。
2,dispatch to mul
和加法类似,这个乘法的调用栈如下所示(其中的一次):
#torch/csrc/autograd/generated/python_variable_methods.cpp
THPVariable_mul
|
V
#torch/csrc/autograd/generated/python_variable_methods_dispatch.h
dispatch_mul
|
V
#aten/src/ATen/core/TensorMethods.h
Tensor::mul
|
V
#需要dispatch type 了
#torch/csrc/autograd/generated/VariableType_4.cpp
Tensor VariableType::mul(const Tensor & self, const Tensor & other)
|
V
#运算符太简单了,扔回到base的default实现
#build/aten/src/ATen/TypeDefault.cpp
Tensor TypeDefault::mul(const Tensor & self, const Tensor & other)
|
V
#aten/src/ATen/native/BinaryOps.cpp
Tensor mul(const Tensor& self, const Tensor& other)
|
V
#aten/src/ATen/native/TensorIterator.cpp
TensorIterator::binary_op
|
V
#此处依赖初始化阶段的REGISTER_DISPATCH的工作
#build/aten/src/ATen/native/cpu/BinaryOpsKernel.cpp.AVX2.cpp(如果使用的是CPU版的pytorch,并且cpu支持AVX2)
void mul_kernel(TensorIterator& iter)
|
V
#aten/src/ATen/native/cpu/Loops.h
void binary_kernel_vec(TensorIterator& iter, func_t op, vec_func_t vop)
如果使用的是CPU版的PyTorch,那么这里的乘法运算最终分发到cpu的native实现上了。
3,构建Autograd信息
当乘法运算分发到VariableType::mul的时候,PyTorch将会在这个函数中构建autograd的信息。
Tensor VariableType::mul(const Tensor & self, const Tensor & other) const {std::shared_ptr<MulBackward0> grad_fn;grad_fn = std::shared_ptr<MulBackward0>(new MulBackward0(), deleteFunction);grad_fn->set_next_edges(collect_next_edges( self, other ));if (grad_fn->should_compute_output(1)) {grad_fn->self_ = SavedVariable(self, false);}if (grad_fn->should_compute_output(0)) {grad_fn->other_ = SavedVariable(other, false);}auto tmp = ([&]() {at::AutoNonVariableTypeMode non_var_type_mode(true);return baseType->mul(self_, other_);})();auto result = as_variable(tmp);set_history(flatten_tensor_args( result ), grad_fn);return result;
}
3.1 构建grad_fn (MulBackward0)
经过表达式 grad_fn = std::shared_ptr<MulBackward0>(new MulBackward0(), deleteFunction)后,一个Function的实例就产生了,类型为MulBackward0(0x55ca7ebba2a0)。
struct MulBackward0 : public TraceableFunction {variable_list apply(variable_list&& grads) override;SavedVariable self_;SavedVariable other_;
};
和AddBackward0不同的是,MulBackward0还有两个SavedVariable成员。比如使用syszux初始化SavedVariable self_的时候,进行了以下的拷贝:
#从syszux 拷贝到 SavedVariable self_
variable.output_nr() --> output_nr_
variable.requires_grad() --> requires_grad_
variable.data() --> data_
variable.grad_fn() --> grad_fn_
variable.version_counter() --> version_counter_
version_counter_.current_version() --> saved_version_
在内存中,这个MulBackward0实例的布局如下所示:
#grad_fn MulBackward0, requires_grad == True
Function实例 --> sequence_nr_ (uint64_t) = 1 (每个线程内自增)--> next_edges_ (edge_list) --> std::vector<Edge> = None--> input_metadata_ --> [(type, shape, device)...] = None--> self_ (SavedVariable) = syszux的浅拷贝--> other_ (SavedVariable) = syszux的另一个浅拷贝--> pre_hooks_ = None--> post_hooks_ = None--> anomaly_metadata_ = None--> apply() --> 使用 MulBackward0 的apply
3.2,初始化MulBackward0的next_edges_
前文已经提到过,在grad_fn调用collect_next_edges去搜集输入参数(Variable实例,此处为syszux)的edges时,对于“非leaf”的syszux来说,会触发Variable::grad_fn()调用,这会得到syszux的grad_fn,也就是AddBackward0实例。使用两个syszux的grad_fn组成的edges初始化完MulBackward0实例后,MulBackward0在内存中看起来是这个样子:
#grad_fn MulBackward0, requires_grad == True,0x55ca7ebba2a0
Function实例 --> sequence_nr_ (uint64_t) = 1(每个线程内自增)--> next_edges_ (edge_list) = [(AddBackward0实例0x55ca7f872e90,0),(AddBackward0实例0x55ca7f872e90,0)]--> input_metadata_ --> [(type, shape, device)...] = None--> pre_hooks_ = None--> post_hooks_ = None--> anomaly_metadata_ = None--> apply() --> 使用 AccumulateGrad 的apply
初始化完毕后,此时MulBackward0这个Function(0x55ca7ebba2a0)的next_edges_已经被赋值为syszux中的grad_fn组成的edges。
4 构建Variable(tmp)
在第一次乘法表达式完成之后,内存中也就产生了临时Variable tmp。刚产生的tmp实例在内存中看起来是这样的:
//tmp
Variable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = [[9., 9.],[9., 9.]]--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= None--> grad_accumulator_ (Function实例)= None--> version_counter_ = 0--> hooks_ len = 0--> requires_grad_ = False--> is_view_ = false--> output_nr_ = 0--> base_ = Not exist
之后会使用set_history(flatten_tensor_args( result ), grad_fn)来设置tmp的gradient_edge,这个函数做了2件事情:
1,MulBackward0实例中的input_metadata_ 追加了Variable syszux的(type, shape, device),追加完成后,syszux信息在input_metadata_中的index就是下文中会用到的output_nr_,此处为0(第一个元素嘛);这样MulBackward0实例在内存中看起来是这样:
//grad_fn->add_input_metadata(variable);
grad_fn_ --> input_metadata_ += (variable.type, variable.shape, variable.device) = []#MulBackward0实例, requires_grad == True
Function实例 --> sequence_nr_ (uint64_t) = 1--> next_edges_ (edge_list) = [(AddBackward0实例0x55ca7f872e90,0),(AddBackward0实例0x55ca7f872e90,0)]--> input_metadata_ --> [(type, shape, device)...] = [(CPUFloatType, [2, 2],cpu])]--> alpha (Scalar) = 1--> pre_hooks_ = None--> post_hooks_ = None--> anomaly_metadata_ = None--> apply() --> 使用 MulBackward0 的apply
2,Variable tmp的autograd_meta中的grad_fn_ 被赋值成了上述MulBackward0实例,而autograd_meta中的output_nr_被赋值成了上文中的“当前Variable信息在input_metadata_中的index”。这样tmp实例在内存中看起来就是这样:
//as_variable_ref(variable).set_gradient_edge({grad_fn, output_nr});
Variable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = [[9., 9.],[9., 9.]]--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= MulBackward0实例0x55ca7ebba2a0(参考上面)--> grad_accumulator_ (Function实例)= None--> version_counter_ = 0--> hooks_ len = 0--> requires_grad_ = False--> is_view_ = false--> output_nr_ = 0--> base_ = Not exist
5,第二次乘法
syszux * syszux后得到的tmp还要继续进行tmp * 3的运算,经过前面的一次加法(gemfield + 2)和一次乘法(syszux * syszux),我想我们目前已经能总结出规律了。每一次这样的运算,会经历以下的步骤:
- 1,加法/乘法的调用栈,最终派发到某种device的实现上,如果运算输入是个scalar,进行scalar到Variable的构建;
- 2,派发到VariableType上时,会顺便进行autograd信息的构建;
- 2.1,构建一个加法/乘法的反向计算函数实例(比如AddBackward0,MulBackward0);
- 2.2,初始化反向计算函数实例的next_edges_和其它相关成员,next_edges_成员的值来自前向时候的输入参数,如果输入Variable是leaf的话,则next_edges_来自输入Variable的grad_accumulator_;如果是非leaf的话,则来自Variable的grad_fn_;
- 2.3,使用步骤3中的Variable实例来初始化反向计算函数实例的input_metadata_,
- 3,运算后得到新的Variable,使用Variable::Impl进行构建,使用步骤2中的反向计算函数实例初始化该Variable实例的grad_fn_成员。
对于civilnet = tmp * 3的运算(civilnet = syszux * syszux * 3的第二步),上述步骤就是:
1,THPVariable_mul的分发,不再赘述;其间要使用scalar 3构建一个Variable;
2,在调用栈到达VariableType::mul的时候,构建又一个MulBackward0实例(0x55ca7fada2f0),并初始化其next_edges_成员:
#grad_fn MulBackward0, requires_grad == True,0x55ca7fada2f0
Function实例 --> sequence_nr_ (uint64_t) = 2 (每个线程内自增)--> next_edges_ (edge_list) = [(MulBackward0实例0x55ca7ebba2a0,0),(0,0)]--> input_metadata_ --> [(type, shape, device)...] = [(CPUFloatType, [2, 2],cpu])]--> self_ (SavedVariable) = tmp的浅拷贝--> other_ (SavedVariable) = 3的浅拷贝--> pre_hooks_ = None--> post_hooks_ = None--> anomaly_metadata_ = None--> apply() --> 使用 MulBackward0 的apply
注意sequence_nr_又自增了1。
3,构建Variable civilnet:
//as_variable_ref(variable).set_gradient_edge({grad_fn, output_nr});
Variable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = [[27., 27.],[27., 27.]]--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= MulBackward0实例0x55ca7fada2f0(参考上面)--> grad_accumulator_ (Function实例)= None--> version_counter_ = 0--> hooks_ len = 0--> requires_grad_ = False--> is_view_ = false--> output_nr_ = 0--> base_ = Not exist
gemfieldout = civilnet.mean()
步骤其实类似了:
1,python调用到C++的THPVariable_mean的调用:
static PyObject * THPVariable_mean(PyObject* self_, PyObject* args, PyObject* kwargs)
{......return wrap(dispatch_mean(self));
}
调用栈如下:
#torch/csrc/autograd/generated/python_variable_methods.cpp
static PyObject * THPVariable_mean(PyObject* self_, PyObject* args, PyObject* kwargs)
|
V
#aten/src/ATen/core/TensorMethods.h
Tensor Tensor::mean()
|
V
#torch/csrc/autograd/generated/VariableType_2.cpp
Tensor VariableType::mean(const Tensor & self)
|
V
#build/aten/src/ATen/TypeDefault.cpp
Tensor TypeDefault::mean(const Tensor & self)
|
V
#aten/src/ATen/native/ReduceOps.cpp
Tensor mean(const Tensor &self)
|
V
#aten/src/ATen/native/ReduceOps.cpp
static inline Tensor mean(const Tensor &self, optional<ScalarType> dtype)
|
V
#aten/src/ATen/native/ReduceOps.cpp
static inline Tensor mean(const Tensor &self, IntArrayRef dim, bool keepdim, optional<ScalarType> dtype)
|
V
#aten/src/ATen/native/ReduceOps.cpp
Tensor &mean_out(Tensor &result, const Tensor &self, IntArrayRef dim,bool keepdim, optional<ScalarType> opt_dtype)
|
V
at::sum_out(result, self, dim, keepdim, dtype).div_(dim_prod);
|
V
#build/aten/src/ATen/TypeDefault.cpp
Tensor & TypeDefault::sum_out(Tensor & out, const Tensor & self, IntArrayRef dim, bool keepdim, ScalarType dtype)
|
V
#aten/src/ATen/native/ReduceOps.cpp
Tensor& sum_out(Tensor& result, const Tensor& self, IntArrayRef dim, bool keepdim, ScalarType dtype)
|
V
#aten/src/ATen/native/ReduceOps.cpp
Tensor& sum_out(Tensor& result, const Tensor& self, IntArrayRef dim,bool keepdim, optional<ScalarType> opt_dtype)
|
V
sum_stub ,依赖初始化阶段的REGISTER_DISPATCH的工作
2,构建autograd
在调用栈到达 VariableType::mean时,开始顺便构建autograd的信息。主要是构建一个op的反向计算函数实例:MeanBackward0实例(0x55ca7eb358b0)。MeanBackward0类型定义如下:
struct MeanBackward0 : public TraceableFunction {variable_list apply(variable_list&& grads) override;std::vector<int64_t> self_sizes;int64_t self_numel = 0;
};
多了self_sizes和self_numel成员。构建完成后,MeanBackward0实例在内存中看起来如下所示:
#grad_fn MeanBackward0, requires_grad == True,0x55ca7eb358b0
Function实例 --> sequence_nr_ (uint64_t) = 3 (每个线程内自增)--> next_edges_ (edge_list) = [(MulBackward0实例0x55ca7fada2f0,0)]--> input_metadata_ --> [(type, shape, device)...] = [(CPUFloatType|[]|cpu])]--> self_sizes (std::vector<int64_t>) = (2, 2)--> self_numel = 4--> pre_hooks_ = None--> post_hooks_ = None--> anomaly_metadata_ = None--> apply() --> 使用 MulBackward0 的apply
注意sequence_nr_的值又自增了1,另外就是input_metadata_中的shape为空。
3,构建Variable gemfieldout
gemfieldout在内存中的布局如下所示:
//as_variable_ref(variable).set_gradient_edge({grad_fn, output_nr});
Variable实例 --> Variable::Imple实例 --> tensor data --> TensorImpl实例 --> Storage实例 = (27,)--> autograd_meta --> grad_ (又一个Variable实例) = None--> grad_fn_ (Function实例)= MeanBackward0实例0x55ca7eb358b0(参考上面)--> grad_accumulator_ (Function实例)= None--> version_counter_ = 0--> hooks_ len = 0--> requires_grad_ = False--> is_view_ = false--> output_nr_ = 0--> base_ = Not exist
总结
本文《PyTorch的Tensor(中)》主要介绍了Tensor的autograd部分,具体来说,就是在前向运算中,autograd的信息是如何构建和联系在一起的:
- 1,op的调用栈,最终派发到某种device的实现上,如果运算输入是个scalar,进行scalar到Variable的构建;
- 2,派发到VariableType上时,会顺便进行autograd信息的构建;
- 2.1,构建一个op的反向计算函数实例(比如AddBackward0,MulBackward0);
- 2.2,初始化反向计算函数实例的next_edges_和其它相关成员,next_edges_成员的值来自前向时候的输入参数,如果输入Variable是leaf的话,则next_edges_来自输入Variable的grad_accumulator_;如果是非leaf的话,则来自Variable的grad_fn_;
- 2.3,使用步骤3中的Variable实例来初始化反向计算函数实例的input_metadata_,
- 3,运算后得到新的Variable,使用Variable::Impl进行构建,使用步骤2中的反向计算函数实例初始化该Variable实例的grad_fn_成员。
而在下一篇文章《PyTorch的Tensor(下)》中,将主要介绍在backward的时候,Tensor中的autograd部分是怎么运行的。不像这篇文章中的Variable实例,那个时候其grad_成员将不再是None了。