背景
一个神经网络主要是由什么构成的呢?大概就是各种各样的op、op结合的方式、loss;这种形态对应到一个组织上,那就分别是组织中各种各样的人、组织架构、组织的奖惩机制。如此看来,小到一个人,大到一个国家,都具有这样的形态,就像自相似分形一样。在神经网络中,各种各样的op拥有或多或少的参数,而Tensor就是用来存储并计算这些参数的基础。
更近一步,PyTorch宣称自己是支持GPU运算的numpy,并且可以自动求微分,这究竟是什么意思呢?因此在本文中,gemfield将从以下几个方面来讲述Tensor:
1,如何创建一个tensor?创建一个tensor的时候发生了什么?
2,CUDA tensor和CPU tensor的区别是什么呢?这两个之间怎么互相转换呢?转换的时候发生了什么?
3,对于tensor上的方法调用,真正的执行逻辑是在哪里定义的呢?CPU tensor和CUDA tensor的执行有什么不一样呢?
4,更重要的是 ,一个tensor的requires_grad标志有什么含义呢?一个tensor的grad_fn属性又代表了什么呢?
5,最后逃避不了的问题就是,针对一个tensor的backward()调用发生了什么?每个tensor上的grad属性的意义又是什么呢?
那么欢迎来到PyTorch的Tensor系列,这个系列应该是PyTorch关于tensor讲解的最底层的文章了。此篇为本系列第一篇,专门阐述Tensor的创建。
PyTorch Tensor在Python中的继承体系
在Gemfield:详解Pytorch中的网络构造 一文中,gemfield提到过,所有可学习的参数(如weights和bias)的类型都是Parameter类,Parameter的父类正是torch.Tensor类(Parameter和torch.Tensor的区别只有4个:Parameter重新实现了序列化、如何print、deep copy、requires_grad默认True),而torch.Tensor的父类又是torch._C._TensorBase。看起来这个Tensor的继承体系是这样的:
#在python中定义了Parameter类
class Parameter(torch.Tensor)
#在python中定义了torch.Tensor类
class Tensor(torch._C._TensorBase)
#在C++中定义了Variable类
struct TORCH_API Variable : public at::Tensor
//PyObject* Py_InitModule(char *name, PyMethodDef *methods)
//创建torch._C
Py_InitModule("torch._C", methods.data())
//创建 torch._C._TensorBase
PyModule_AddObject(module, "_TensorBase", (PyObject *)&THPVariableType);
要了解Pytorch的Tensor,我们就肯定需要了解Tensor的继承体系以及父子之间的区别。Gemfield就从最基类torch._C说起。import torch的时候,按照Python规范,位于torch/__init__.py中的逻辑就会被执行:
from torch._C import *
......
__all__ += [name for name in dir(_C) if name[0] != '_' and not name.endswith('Base')]
这里的关键就是torch._C,因为Tensor类就是继承自torch._C._TensorBase。如果我们按照诞生顺序(初始化顺序)来描述这一过程的话,就是先有了torch._C,然后有了torch._C._TensorBase,然后有了torch.Tensor继承自torch._C._TensorBase。但这毕竟是C++部分,要在Python中能够import torch._C,则必定要使用Python的扩展规范来导出这个符号,PyTorch就是这么做的:如果是Python2,会使用Py_InitModule API;如果是Python3的话,则会使用PyModule_Create API;不管是哪个API,都会创建出torch._C这个python对象:
//name is "torch._C" PyObject* Py_InitModule(char *name, PyMethodDef *methods)
并在torch._C上注册一个list的function,这个list很长很长。每一个函数由一个PyMethodDef
代表,存放在几个很长很长的list里。这些符号的名字都会在dir(torch)里看到(除了那些符号名前带”_” prefix和”Base” suffix的)。 有了这么一个长长的methods list,我们就可以使用CPython的API来创建一个新的Python类:torch._C这个Python 类就诞生了。
下面的工作就是要往torch._C这个对象上注入一些(很多)成员。其中一个就是torch._C._TensorBase。torch._C的_TensorBase是通过下面的调用完成的初始化:
//来自civilnet的torch/csrc/autograd/python_variable.cpp
bool THPVariable_initModule(PyObject *module)
{
static std::vector<PyMethodDef> methods;
THPUtils_addPyMethodDefs(methods, torch::autograd::variable_methods);
THPUtils_addPyMethodDefs(methods, extra_methods);
THPVariableType.tp_methods = methods.data();
if (PyType_Ready(&THPVariableType) < 0)
return false;
Py_INCREF(&THPVariableType);
PyModule_AddObject(module, "_TensorBase", (PyObject *)&THPVariableType);
torch::autograd::initTorchFunctions(module);
return true;
}
执行THPVariable_initModule的时候,使用
PyModule_AddObject(module, "_TensorBase", (PyObject *)&THPVariableType);
来将THPVariableType注册成为torch._C._TensorBase。所以你现在知道了,torch._C._TensorBase就是c++中的THPVariableType(类型是PyTypeObject,Python对象系统中最重要的一个类)。现在我们注册了torch._C._TensorBase这个Python类,下面就要往这个类上注册一些函数:
THPUtils_addPyMethodDefs(methods, torch::autograd::variable_methods);
THPUtils_addPyMethodDefs(methods, extra_methods);
......
torch::autograd::initTorchFunctions(module);
其中,torch::autograd::variable_methods包含了下列358个方法:
//来自syszux的torch/csrc/autograd/generated/python_variable_methods.cpp
//的torch::autograd::variable_methods
"__add__", (PyCFunction)THPVariable_add
......
另外,在初始化完成torch._C._TensorBase后,紧接着立刻初始化了torch._C._VariableFunctions(包含了大量的方法),torch._C._VariableFunctions主要是暴露给torch/functional.py使用的符号。对于torch._C._TensorBase来说,初始化工作就要到此结束了。不过还有一个巨大的疑问没有解释,就是torch._C._TensorBase上的359个方法是在哪里实现的呢?难道这不应该是最关键的吗?再等等……好像从本文开头到现在,我们已经提到过三处函数区了(torch._C的方法、torch._C._TensorBase的方法、torch._C._VariableFunctions的方法)。Anyway,在本章节,我们就先聚焦torch._C._TensorBase的方法。这些方法都是torch::autograd::variable_methods,想起什么来了吗?在Gemfield:PyTorch Autograd代码的动态生成 一文中已经提到过,这些方法的实现都是动态生成的,并且由生成的python_variable_methods_dispatch.h中定义的inline dispatch函数将这些variable_methods的逻辑分发到Tensor类对应的方法上,比如native、cuda等等。
PyTorch Tensor在C++中的继承体系
一个tensor比较重要的特质主要有:tensor的维度信息、tensor的值内容、tensor的grad、tensor的type、tensor的backend等等。更重要的是,一个tensor需要精巧的内存管理。在C++中,一个tensor是由DataPtr、StorageImpl、Storage、TensorImpl、Tensor、Variable::Impl、Variable、AutogradMeta这些底层的类组成的。这个继承体系看起来是这样的:
#垂直表示继承,水平表示被包含,()表示为一个类
DataPtr -> StorageImpl -> Storage -> (TensorImpl) -> (Tensor)
| |
v v
(Tensor) -> Variable::Impl Variable -> AutogradMeta -> (TensorImpl)
这个继承体系设计的有些混乱,应该在最近的release中被重新设计下。Gemfield从底层到上层一一说来:
1,DataPtr
这个类位于最底层,用来直接维护tensor所需的内存。
class UniqueVoidPtr {
void* data_;
std::unique_ptr<void, DeleterFnPtr> ctx_;
}
class DataPtr {
c10::detail::UniqueVoidPtr ptr_;
Device device_;
}
device成员用来指定tensor的存储是在cpu还是cuda设备上。DataPtr使用构造函数DataPtr(void* data, void* ctx, DeleterFnPtr ctx_deleter, Device device)来构造一个实例,事实上,这个构造过程是由Allocator完成的。以CPU上的tensor为例,这个构造是由DefaultCPUAllocator类的allocate函数完成的:
at::DataPtr allocate(size_t nbytes) const override {
......
//DataPtr(void* data, void* ctx, DeleterFnPtr ctx_deleter, Device device)
return {data, data, &Delete, at::Device(at::DeviceType::CPU)}
}
假设这个调用是在x86的Linux上完成的,那么经此一役,最后return中的data来自posix_memalign调用(这是一个libc函数,用来申请对齐后的内存);Delete来自free()调用;device_就是at::Device(at::DeviceType::CPU)了。
2,StorageImpl和Storage
StorageImpl继承自intrusive_ptr_target,目的是借助父类实现的计数功能,然后结合智能指针c10::intrusive_ptr(其负责内存管理,但不负责计数)的帮助,就可以实现“侵入式”的引用计数指针。类的声明如下:
struct StorageImpl final : public c10::intrusive_ptr_target {
......
caffe2::TypeMeta data_type_;
DataPtr data_ptr_;
int64_t numel_;
bool resizable_;
Allocator* allocator_;
......
}
struct Storage{
......
protected:
c10::intrusive_ptr<StorageImpl> storage_impl_;
};
可以看到StorageImpl恰好就使用了上面的DataPtr。另外一个值得注意的成员就是allocator_,在PyTorch中,我们通过REGISTER_ALLOCATOR宏定义了2个allocator,cpu和cuda的:
REGISTER_ALLOCATOR(CPU, &g_cpu_alloc);
REGISTER_ALLOCATOR(CUDA, &g_cuda_alloc);
cpu上tensor的内存分配使用的是g_cpu_alloc,类型是DefaultCPUAllocator。
Storage类和StorageImpl之间使用了bridge设计模式,主要是为了保证ABI的兼容。
3,TensorImpl和Tensor
这两者之间也是bridge模式。另外值得注意的是这里的tensor指的是at tensor,pytorch由于merge了caffe2的缘故,目前有c10tensor、caffe2 tensor、aten tensor,比较混乱,以后肯定会统一,只不过目前有各种依赖,还不好merge。
struct TensorImpl : public c10::intrusive_ptr_target {
......
Storage storage_;
std::unique_ptr<c10::AutogradMetaInterface> autograd_meta_ = nullptr;
SmallVector<int64_t,5> sizes_;
SmallVector<int64_t,5> strides_;
int64_t storage_offset_ = 0;
int64_t numel_ = 1;
caffe2::TypeMeta data_type_;
TensorTypeId type_id_;
bool is_contiguous_ = true;
bool is_variable_ = false;
bool is_wrapped_number_ = false;
bool allow_tensor_metadata_change_ = true;
bool reserved_ = false;
......
};
class Tensor {
......
protected:
c10::intrusive_ptr<TensorImpl, UndefinedTensorImpl> impl_;
};
namespace detail {
template <typename T, typename... Args>
Tensor make_tensor(Args&&... args) {
return Tensor(c10::make_intrusive<T>(std::forward<Args>(args)...));
}
}
在TensorImpl中有一个成员是autograd_meta_,这是为自动微分服务的。如果一个tensor是Variable,那么这个成员将会被初始化(比如make_variable_consuming函数)并发挥后续作用;如果只是tensor,这个值就是nullptr。
make_tensor就是个helper function,使用了C++ template的perfect forwarding,来帮助用户简单创建一个实现了“侵入式”计数功能的tensor智能指针。
4,Variable和Variable::Impl
Variable使用PIMPL(Pointer to IMPLementation)模式来隐藏实现细节(就是Variable::Impl了):
struct Variable : public at::Tensor {
......
struct AutogradMeta;
struct Impl;
struct DifferentiableViewImpl;
struct DifferentiableViewMeta;
};
struct Variable::Impl : public at::TensorImpl {
......
at::Tensor data_;
};
//Variable的构造函数,by gemfield
inline Variable::Variable(c10::intrusive_ptr<Variable::Impl> self) : at::Tensor(std::move(self)) {}
经过构造后,一个Variable的实例中的impl_将指向一个TensorImpl的实例。
再来说说这个奇怪的Variable::Impl,它继承自at::TensorImpl,然后又包含了一个at::Tensor。。。这不就有两份TensorImpl里的东西了吗(比如Storage)?更近一步,在make_variable_consuming的时候,Variable::Impl实例中包含的at::Tensor被赋予了外部输入的tensor(有个storage实例),而Variable::Impl实例中还有一个通过继承得到的Storage实例,这不是有2份吗?设计真的够混乱的,同时也发现了官方代码里有相关的描述:
This field will be removed once VariableImpl and TensorImpl are merged.
嗯,看来以后Variable::Impl要合并到TensorImpl里。关键是现在也没看到Variable::Impl能有多大作用哎。
Variable是Tensor的子类,相比Tensor(本质还是多维数组),增加了自动微分系统,这个自动微分系统就是靠AutogradMeta类来实现的。Variable有2种:用户直接定义的tensor(比如权重参数等)是动态图中的leaf;由用户定义variable经过计算得到的中间值是内部variable。每个Variable还包含了一个variable(autograd_meta_中的grad_,由父类TensorImpl提供),称之为grad(gradient)。如果这个variable是leaf, 那么在动态图的运行中其gradient将会被累加。
5,Variable::AutogradMeta
所有和audograd相关的东西都定义在这个类,然后在TensorImpl/VariableImpl中将会包含这个类的实例:
struct Variable::AutogradMeta : public c10::AutogradMetaInterface {
std::string name;
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_;
PyObject* pyobj_ = nullptr;
std::mutex mutex_;
};
struct Variable::DifferentiableViewMeta : public Variable::AutogradMeta {
Variable base_;
uint32_t attr_version;
};
struct Variable::DifferentiableViewImpl : public Variable::Impl {};
每个Variable都有一个独一无二的(由std::unique_ptr智能指针来保证)AutogradMeta结构体,用来做和自动微分相关的一切工作。而一个Variable的实例化一般是通过make_variable helper函数(在torch::autograd命名空间里)来实现的,这个函数根据输入的Tensor、requires_grad来产生一个Variable的实例:
Variable(c10::make_intrusive<Variable::Impl>(data_copy, std::move(autograd_meta), requires_grad))
如何创建一个PyTorch的Tensor
在PyTorch中直接创建一个tensor的方法主要有2种:使用torch的方法(empty、rand、zeros、tensor…)、实例化torch.Tensor这个类。
#第一种
gemfield = torch.empty(7, 19)
gemfield = torch.rand(7, 19)
gemfield = torch.zeros(7, 19, dtype=torch.long)
gemfield = torch.tensor([7.0, 19])
#第二种
gemfield = torch.Tensor([1,2])
使用torch方法创建一个Tensor(requires_grad=false)
我们使用torch.empty(7,19)来作为这一小节的例子,它会创建一个7×19的二维矩阵,其它的方法也类似。那么这个empty的符号是怎么出现在torch模块上的呢?在torch/__init__.py中,有下面这样的代码:
for name in dir(_C._VariableFunctions):
if name.startswith('__'):
continue
globals()[name] = getattr(_C._VariableFunctions, name)
也就是_C._VariableFunctions中的符号全部被注入到torch模块下(除了__prefix的)。而在torch/csrc/autograd/generated/python_torch_functions.cpp(由模板生成)中我们定义了_C._VariableFunctions:
static PyMethodDef torch_functions[] = {
{"from_numpy", (PyCFunction)THPVariable_from_numpy, METH_STATIC | METH_O, NULL},
{"add", (PyCFunction)THPVariable_add, METH_VARARGS | METH_KEYWORDS | METH_STATIC, NULL},
{"empty", (PyCFunction)THPVariable_empty, METH_VARARGS | METH_KEYWORDS | METH_STATIC, NULL},
{"rand", (PyCFunction)THPVariable_rand, METH_VARARGS | METH_KEYWORDS | METH_STATIC, NULL},
{"tensor", (PyCFunction)THPVariable_tensor, METH_VARARGS | METH_KEYWORDS | METH_STATIC, NULL},
{"zeros", (PyCFunction)THPVariable_zeros, METH_VARARGS | METH_KEYWORDS | METH_STATIC, NULL},
......
}
static PyTypeObject THPVariableFunctions = {
PyVarObject_HEAD_INIT(NULL, 0)
"torch._C._VariableFunctions", /* tp_name */
......
torch_functions, /* tp_methods */
......
}
因此,当我们使用torch.empty/rand/zeros来初始化一个tensor的时候,实际上就调用了C++中的THPVariable_empty/THPVariable_rand/THPVariable_zeros函数。这个函数也是由模板文件生成的(gemfield专栏文章https://zhuanlan.zhihu.com/p/59425970中已经介绍过了):
gemfield@ThinkPad-X1C:~/github/pytorch$ python3 tools/setup_helpers/generate_code.py \
--declarations-path \
/home/gemfield/github/pytorch/build/aten/src/ATen/Declarations.yaml \
--nn-path aten/src/
这里我们还是以THPVariable_empty为例,在PyTorch中,任何一个像THPVariable_empty这样的函数——从python调用然后进入c++——都需要在函数实现的开头用PythonArgParser来解析入参,在THPVariable_empty中,解析入参的过程如下:
static PythonArgParser parser({
"empty(IntArrayRef size, *, Tensor out=None, ScalarType dtype=None, Layout layout=torch.strided, Device device=None, bool requires_grad=False)", }, /*traceable=*/true);
ParsedArgs<6> parsed_args;
auto r = parser.parse(args, kwargs, parsed_args);
可以看到,THPVariable_empty接收的入参为:size、dtype、layout、device、requires_grad,其中size为必须输入的参数(如果缺失的话会报错:TypeError: empty() missing 1 required positional arguments: “size”),其他参数都有默认值。这些参数会立即被TensorOptions封装起来:
const auto options = TensorOptions()
.dtype(dtype)
.device(device)
.layout(r.layout(3).layout)
.requires_grad(r.toBool(5));
// size is (7,19) here, by gemfield
return wrap(dispatch_empty(size, options));
因为一个tensor有这些特质:equires_grad, is_variable, device(cpu、CUDA…), dtype(int、float…), layout(Strided、sparse),这些属性都由TensorOptions封装。
接着说下dispatch_empty函数,就像它的名字一样,这个函数起得是分发作用。它将torch的empty调用分发到torch::empty C++调用上:
inline Tensor dispatch_empty(IntArrayRef size, const TensorOptions & options) {
return torch::empty(size, options);
}
torch::empty的实现在torch/csrc/autograd/generated/variable_factories.h中(参考gemfield专栏文章https://zhuanlan.zhihu.com/p/59425970),如下所示:
inline at::Tensor empty(at::IntList size, const at::TensorOptions & options = {}) {
at::Tensor tensor = at::empty(size, at::TensorOptions(options).is_variable(false));
at::Tensor result = autograd::make_variable_consuming(std::move(tensor),options.requires_grad());
return result;
}
先后调用了at::empty和autograd::make_variable_consuming。构建一个tensor的调用栈此刻已经完全展开了,这个调用栈主要有3步构成:1,at::empty;2,autograd::make_variable_consuming;3,wrap。
1,at::empty
对于at::empty来说,它实现在ATen模块中。这个时候,我们不由自主的想起了在gemfield专栏文章(Gemfield:PyTorch ATen代码的动态生成)中曾经提到过的Type继承体系,是的,我们现在到那里了。TypeExtendedInterface的getType会转而使用type_registry表去查询所要使用的Type,查询使用的索引就是backend(device)和scaler type(dtype),查询得到的结果将会是那几十个type中的某一个(type_registry表的初始化可以参考gemfield专栏文章:https://zhuanlan.zhihu.com/p/62481975):
//ATen/Functions.h 模板生成的, by civilnet
static inline Tensor empty(IntArrayRef size, const TensorOptions & options) {
return at::getType(options).empty(size, options);
}
//ATen/context.cpp
TypeExtendedInterface& getType(TensorOptions options) {
return globalContext().getType(
options.backend(), typeMetaToScalarType(options.dtype()), options.is_variable());
}
这里就假设使用的是其中的CPUFloatType,那么对empty的调用就会转发到CPUFloatType类上:
//来自syszux的ATen CPUFloatType.cpp
Tensor CPUFloatType::empty(IntArrayRef size, const TensorOptions & options) const {
const DeviceGuard device_guard(options.device());
return at::native::empty_cpu(/* actuals */ size, options);
}
//ATen/native/TensorFactories.cpp
Tensor empty_cpu(IntList size, const TensorOptions& options) {
auto* allocator = at::getCPUAllocator();
int64_t nelements = prod_intlist(size);
auto dtype = options.dtype();
auto storage_impl = c10::make_intrusive<StorageImpl>(
dtype,nelements,
allocator->allocate(nelements * dtype.itemsize()),
allocator,true);
auto tensor = detail::make_tensor<TensorImpl>(storage_impl, at::CPUTensorId(), false);
return tensor;
}
我们已经能够看到一个构建cpu上的float tensor的过程。cpu上tensor的内存分配使用的是DefaultCPUAllocator,在Linux上底层使用了posix_memalign(&data, gCaffe2Alignment, nbytes),这是libc中实现的一个内存分配函数;cuda的tensor则是使用cuda代码实现的。现在我们有了allocator、tensor中元素的类型、tensor中元素的数量,我们需要使用以下两个核心表达式来构建出tensor:
1,c10::make_intrusive<StorageImpl>(dtype,nelements, allocator->allocate(nelements * dtype.itemsize()), allocator,true);
2,detail::make_tensor<TensorImpl>(storage_impl, at::CPUTensorId(), false);
第一个表达式用来实例化一个StorageImpl,也就是说,一次torch.empty(7,19)的调用过程,实例化且仅实例化一个StorageImpl。这里使用了intrusive智能指针,该指针管理的还是StorageImpl类的实例。
第二个表达式用来实例化一个Tensor实例:
- 先new一个TensorImpl实例(构造函数接收3个参数,类型分别是Storage、TensorTypeId , bool ,和第一个表达式一样,同样使用intrusive智能指针管理TensorImpl实例),使用第一个表达式中实例化的StorageImpl来构造一个Storage实例(StorageImpl实例直接移动语义给Storage的storage_impl_成员);
- 构造完成后,这个TensorImpl实例的storage_成员被初始化成了上述的Storage实例(移动语义),其它成员也相应的初始化完毕;
- 使用上述的TensorImpl实例构造一个Tensor实例,其中TensorImpl实例直接赋值给Tensor实例的impl_成员(移动语义)。
经此一役,一个tensor实例已经产生了,这个tensor的impl_成员已经初始化为了一个TensorImpl的实例,而TensorImpl实例中的type、elements数量、allocator类型、分配的内存(DataPtr)也已经初始化完毕。既然调用栈下来是为了new一个tensor出来,那么到这里是不是就完成了?看起来tensor的构造函数也执行了啊?答案当然是No。
PyTorch 0.4之后创建的tensor默认就是Variable(是的,Variable已经从API中消失了),现在属于Tensor的部分构造完成了,但是,属于Variable的部分还没有开始构建!你已经想到了,torch.empty(7,19)的初始化到这里才构建出一个普通的tensor,后面还要将这个tensor变成Variable!相比于普通的Tensor,Variable最重要的区别就是autograd!好了,构造继续,欢迎进入make variable的环节!
2,autograd::make_variable_consuming
到这一步,我们需要通过make variable的调用,来将普通的Tensor变成具有autograd功能的tensor了。进行转换的函数就是autograd::make_variable_consuming,它的实现如下:
inline Variable make_variable_consuming(at::Tensor data,bool requires_grad = false,bool allow_tensor_metadata_change = true) {
if (data.defined()) {
data.unsafeGetTensorImpl()->set_allow_tensor_metadata_change(allow_tensor_metadata_change);
auto autograd_meta = c10::guts::make_unique<Variable::AutogradMeta>();
return Variable(c10::make_intrusive<Variable::Impl>(std::move(data), std::move(autograd_meta), requires_grad));
}
return Variable();
}
1,先将Tensor实例中的TensorImpl的allow_tensor_metadata_change设置为True;
2,new一个AutogradMeta实例:
struct Variable::AutogradMeta : public c10::AutogradMetaInterface {
std::string name;
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_;
std::mutex mutex_;
}
注意这个实例化的过程同时实例化了又一个Variable(继承自Tensor),不过使用的是缺省构造函数,所以此刻,这个Variable实例中的impl_尚且为空(impl_是c10::intrusive_ptr<TensorImpl, UndefinedTensorImpl>类型的成员)。
3,构建Variable::Impl的实例(父类是TensorImpl):
Variable::Impl::Impl(at::Tensor data, std::unique_ptr<Variable::AutogradMeta> autograd_meta, bool requires_grad, Edge gradient_edge)
: TensorImpl(data.type_id(), data.dtype(), data.device(), /* is variable */ true),
data_(std::move(data)) {
autograd_meta->grad_fn_ = std::move(gradient_edge.function);
autograd_meta->requires_grad_ = false;
autograd_meta->is_view_ = false;
autograd_meta->output_nr_ = gradient_edge.input_nr;
autograd_meta->set_requires_grad(requires_grad, this);
set_autograd_meta(std::move(autograd_meta));
}
构造函数的入参有at::Tensor data、 std::unique_ptr<Variable::AutogradMeta> autograd_meta、bool requires_grad、Edge gradient_edge,然后用这些传入的参数先来构造父类继承而来的成员和自身新定义的data_成员,这两部分的区别是:
- 父类继承而来的部分没有使用Storage部分(也即storage_impl_为空),只使用了AutogradMeta部分;
- 自己新增的data_成员用来接收入参的tensor data;
4,用上述Variable::Impl实例来构建Variable实例,
inline Variable::Variable(c10::intrusive_ptr<Variable::Impl> self) : at::Tensor(std::move(self)) {}
可以看到,Variable中的impl_被初始化为了上述构建的Variable::Impl实例。我们来自上而下再回顾下,当前构建出来的这个Variable实例含有TensorImpl类型的impl_(从Tensor继承而来),而这个TensorImpl类型的impl_初始化为了Variable::Impl实例,而这个Variable::Impl实例中的tensor data初始化为了上一章节中new出来的tensor,这个Variable::Impl实例中的autograd_meta初始化为了上述new出来的AutogradMeta实例。可以简单表示为这样:
Variable实例 --> Variable::Imple实例 --> tensor data --> autograd_meta --> grad_ (又一个Variable实例) --> grad_fn_ / grad_accumulator_ (Function实例) --> requires_grad_ (默认为False)
至此,Variable实例构建完毕。哈哈哈哈,你开心欢颜,你觉得一个有autograd能力的Tensor终于构建出来了。可是,可是,我是C++啊!现在还是在C++的逻辑里啊……这里构建的Tensor还不能返回给你的Python代码!我们还得包装成一个Python对象,这个Python对象的payload才是这里构建出来的Tensor实例——这就是wrap函数的意义。
3,wrap
最后dispatch_empty函数的返回由wrap函数封装,wrap函数的本职工作就是将C++的tensor包装成一个Python对象。wrap定义如下:
inline PyObject* wrap(at::Tensor tensor) {
return THPVariable_Wrap(Variable(std::move(tensor)));
}
PyObject * THPVariable_Wrap(Variable var)
{
return THPVariable_NewWithVar((PyTypeObject *)THPVariableClass, std::move(var));
}
可以看到wrap函数调用了THPVariable_Wrap函数,而THPVariable_Wrap进行一些简单的判断后(主要看是否已经创建过同一个tensor),接着调用THPVariable_NewWithVar来进行python中tensor的创建,这个函数用来将Variable实例变成真正python对象中的payload再返回。
4,THPVariable_NewWithVar
THPVariable_NewWithVar函数就是PyTorch的Tensor从C++通往Python之路。既然是Python中的tensor对象,那在Python的世界中总得有个对应的符号名吧。没错,这个名字就是大名鼎鼎的torch._C._TensorBase。
所以,THPVariable_NewWithVar函数就是将一个C++中的tensor(class Variable)转换为Python中的tensor(torch._C._TensorBase),这个函数定义如下:
static PyObject* THPVariable_NewWithVar(PyTypeObject* type, Variable var)
{
PyObject* obj = type->tp_alloc(type, 0);
if (obj) {
auto v = (THPVariable*) obj;
new (&v->cdata) Variable(std::move(var));
v->cdata.set_pyobj(obj);
......
}
return obj;
}
简单来说就是,C++中的tensor(class Variable)是python tensor对象(torch._C._TensorBase)中的payload,对,正是cdata(python中的_cdata)。
struct THPVariable {
torch::autograd::Variable cdata;
PyObject* backward_hooks = nullptr;
};
最后我们来总结下torch.empty(7,19)的调用栈:
简要来说就是:torch.empty(7,19)调用、从Python到C++、C++多态分发到对应的type上(如CPUFloatType)、从CPUFloatType分发到native的实现上、构建对应的Allocator、构建Stroage、构建Tensor、构建Variable、从C++到Python、torch._C._TensorBase诞生:
//syszux -> torch/__init__.py torch.empty(7,19)
|
V
//syszux -> torch/csrc/autograd/generated/python_torch_functions.cpp static PyObject * THPVariable_empty(PyObject* self_, PyObject* args, PyObject* kwargs)
|
V
//syszux -> 来自torch/csrc/autograd/generated/python_torch_functions_dispatch.h inline Tensor dispatch_empty(IntArrayRef size, const TensorOptions & options)
|
V
//syszux -> torch/csrc/autograd/generated/variable_factories.h inline at::Tensor empty(at::IntArrayRef size, const at::TensorOptions & options = {})
|
V
//syszux -> build/aten/src/ATen/Functions.h static inline Tensor empty(IntArrayRef size, const TensorOptions & options)
|
V
//syszux -> build/aten/src/ATen/CPUFloatType.cpp Tensor CPUFloatType::empty(IntArrayRef size, const TensorOptions & options) const
|
V
//syszux -> aten/src/ATen/native/TensorFactories.cpp Tensor empty_cpu(IntArrayRef size, const TensorOptions& options)
|
V
----构建StorageImpl实例
----使用StorageImpl实例构建TensorImpl实例
----使用TensorImpl实例构建Tensor实例
----(Tensor完成,尚无autograd)
|
V
----构建AutogradMeta实例
----构建Variable::Impl实例
----使用Variable::Impl实例构建Variable实例
|
V
//syszux -> torch/csrc/autograd/python_variable.cpp THPVariable_NewWithVar(PyTypeObject* type, Variable var)
|
V
一个Python中的torch._C._TensorBase对象诞生
使用torch方法创建一个Tensor(requires_grad=true)
啊,和上面小节的内容一样——除了设置requires_grad为true。那么这样创建的tensor会有什么不一样呢?没什么不一样——除了一个flag:requires_grad_的值为True:
Variable实例 --> Variable::Imple实例 --> tensor data
--> autograd_meta --> grad_ (又一个Variable实例)
--> grad_fn_ / grad_accumulator_ (Function实例)
--> requires_grad_ = True
使用torch.Tensor类实例化一个tensor
当使用下面这种方式构建tensor时,我们实际上实例化了torch.Tensor这个python类。
gemfield = torch.Tensor([7,19])
torch.Tensor这个类的父类正是torch._C._TensorBase,torch._C._TensorBase是按照CPython的语法定义的:
PyTypeObject THPVariableType = {
PyVarObject_HEAD_INIT(nullptr, 0)
"torch._C._TensorBase", /* tp_name */
sizeof(THPVariable), /* tp_basicsize */
0, /* tp_itemsize */
(destructor)THPVariable_dealloc, /* tp_dealloc */
&THPVariable_as_mapping, /* tp_as_mapping */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* tp_flags */
(traverseproc)THPVariable_traverse, /* tp_traverse */
(inquiry)THPVariable_clear, /* tp_clear */
0, /* tp_weaklistoffset */
THPVariable_properties, /* tp_getset */
THPVariable_pynew /* tp_new */
......
};
tp_new的字段指向的是THPVariable_pynew函数,因此,当new一个torch.Tensor的时候,THPVariable_pynew函数被调用:
static PyObject *THPVariable_pynew(PyTypeObject *type, PyObject *args, PyObject *kwargs)
{
auto& default_type = torch::tensors::get_default_tensor_type();
auto tensor = torch::utils::legacy_tensor_ctor(default_type, args, kwargs);
return THPVariable_NewWithVar(type, std::move(tensor));
}
可以看到使用torch::utils::legacy_tensor_ctor方法来创建C++中的Variable实例,内部先后调用了legacy_new_from_sequence、internal_new_from_data,在internal_new_from_data中,我们看到了之前见到过的熟悉调用:
auto tensor = autograd::make_variable(at::empty(sizes, at::initialTensorOptions().dtype(inferred_scalar_type)), /*requires_grad=*/false);
一个Variable实例诞生了,和之前不同的是,这个Variable需要使用to方法去放置到合适的device上:
//internal_new_from_data函数
return tensor.to(device, inferred_scalar_type, /*non_blocking=*/false, /*copy=*/false);
to的调用栈是这样的:
//1, aten/src/ATen/core/TensorMethods.h
inline Tensor Tensor::to(Device device, ScalarType dtype, bool non_blocking, bool copy) const {
return dispatch_type().to(*this, device, dtype, non_blocking, copy);
}
//2, torch/csrc/autograd/generated/VariableType_2.cpp
Tensor VariableType::to(const Tensor & self, Device device, ScalarType dtype, bool non_blocking, bool copy) const {
auto result = TypeDefault::to(self, device, dtype, non_blocking, copy);
return result;
}
//3, build/aten/src/ATen/TypeDefault.cpp
Tensor TypeDefault::to(const Tensor & self, Device device, ScalarType dtype, bool non_blocking, bool copy) const {
return at::native::to(/* native_actuals */ self, device, dtype, non_blocking, copy);
}
//4, aten/src/ATen/native/TensorConversions.cpp
Tensor to(const Tensor& self, Device device, ScalarType dtype, bool non_blocking, bool copy) {
device = ensure_has_index(device);
if (self.device() == device && self.dtype() == dtype && !copy) {
return self;
}
return to_impl(self, self.options().device(device).dtype(dtype), non_blocking);
}
//5, aten/src/ATen/native/TensorConversions.cpp
static inline Tensor to_impl(const Tensor& self, const TensorOptions& options, bool non_blocking) {
return self.dispatch_type().toBackend(options.backend()).toScalarType(typeMetaToScalarType(options.dtype()))
.copy(self, non_blocking, options.device());
}
当然,gemfield这种场景下,tensor的type和默认的一样,所以到调用栈第4个to之后,就停止了。Variable实例然后被送入THPVariable_NewWithVar,还记得这个名字吗?创建Python中tensor的道路殊途同归,最后都来到了THPVariable_NewWithVar,最终产生了torch._C._TensorBase对象。
总结
让我们再次回顾一下创建一个PyTorch tensor的过程:
第一步是创建tensor,根据tensor的sparse/strided、backend、scaler type、大小等等,被dispatch到Type继承体系中的某一个type上,然后选择合适的内存allocator,分配内存,创建StorageImpl,然后创建Tensor;
第二步是make variable,函数名是make_variable_consuming,最重要的初始化内容就是加入了自动微分系统,这一系统由类AutogradMeta封装;
第三步是从C++ tensor 到Python tensor,C++的tensor成为了Python tensor的payload后,Python世界中的torch._C._TensorBase对象诞生了。