更新2019年2月28日:我添加了一个new blog post with a slide deck包括我做了PyData蒙特利尔的表现。
介绍
这篇文章是围绕PyTorch代码库参观,它的目的是为PyTorch和其内部的建筑设计指导。我的主要目标是提供一些对于那些谁有兴趣了解发生什么事超出了面向用户的API,并展示一些新的东西超出了已经包含在其他的教程非常有用。
注意:PyTorch构建系统使用的代码生成广泛,所以我就不在这里重复了已经被别人描述。如果您有兴趣了解如何工作的,请阅读以下教程:
小号hort intro to Python extension objects in C/C++
正如你可能知道,你可以使用C和C的Python扩展++和发展所谓的“延伸”。所有PyTorch繁重的工作在C / C ++,而不是纯Python实现。在C / C定义一个新的Python对象类型++,可以定义像低于该一个例子的结构(其是用于autograd基Variable
类):
// Python对象即背torch.autograd.Variable结构THPVariable {PyObject_HEAD炬:: autograd ::可变CDATA;*的PyObject backward_hooks;};
正如你所看到的,有在定义的开始宏,叫PyObject_HEAD
,该宏的目标是Python中的标准化的对象和将扩大到包含一个指向类型的对象(其限定初始化方法,分配器等),也与参考计数器的场另一种结构。

有Python的API中的两个额外的宏叫Py_INCREF()
和Py_DECREF()
,这是用来递增和递减Python对象的引用计数器。多个实体可以借用或自己到其他对象的引用(引用计数器增加),只有当这个引用计数器达到零(当所有的引用被摧毁),Python将自动使用其垃圾回收对象中删除的记忆。
You can read more about Python C/++ extensionshere。
滑稽的事实:这是在许多应用亚洲金博宝中非常普遍使用小整数作为索引,计数器等为了提高效率,官方
CPython的解释Caches the integers from -5 up to 256. For that reason, the statement
a = 200; b = 200; a is b
将会
真正,while the statement
a = 300; b = 300; a is b
将会
假。
PyTorch张量零拷贝到numpy的,反之亦然
PyTorch有它自己的张量表示,该解耦对外交涉PyTorch内部表示。然而,因为它是很常见的,尤其是当数据亚洲金博宝从多种来源,装载有numpy的阵列无处不在,所以我们真的需要作出与NumPy和PyTorch张量之间的转换。出于这个原因,PyTorch提供了两个方法叫from_numpy()
和numpy的()
,其将一个numpy的阵列,反之亦然,分别PyTorch阵列和。如果我们看就是被称为一个numpy的数组转换成PyTorch张量的代码,我们可以得到在PyTorch的内部表示更多的见解:
在::张量tensor_from_numpy(*的PyObject OBJ){如果(!PyArray_Check(OBJ)){抛出类型错误( “预期np.ndarray(得到的是%S)”,Py_TYPE(OBJ) - > tp_name);}自动阵列=(PyArrayObject *)OBJ;INT NDIM = PyArray_NDIM(数组);自动调整大小= to_aten_shape(NDIM,PyArray_DIMS(阵列));自动步幅= to_aten_shape(NDIM,PyArray_STRIDES(阵列));// NumPy的进步使用字节。火炬大步使用元素计数。自动element_size_in_bytes = PyArray_ITEMSIZE(数组);为(自动&步幅:步幅){步幅/ = element_size_in_bytes;} //(...) - 为了简洁省略无效* DATA_PTR = PyArray_DATA(数组); auto& type = CPU(dtype_to_aten(PyArray_TYPE(array))); Py_INCREF(obj); return type.tensorFromBlob(data_ptr, sizes, strides, [obj](void* data) { AutoGIL gil; Py_DECREF(obj); }); }
(代码从tensor_numpy.cpp)
正如你可以从这个代码中看到,PyTorch是获得来自numpy的表示所有的信息(数组元数据),然后创建自己的。然而,如可以从标记线18注意,PyTorch是得到一个指针到内部numpy的阵列的原始数据而不是复制。这意味着PyTorch将创建该数据的参考,与用于原始数据张量的numpy的数组对象共享相同的存储器区域。

该re is also an important point here: when Numpy array object goes out of scope and get a zero reference count, it will be garbage collected and销毁,这就是为什么有在管线20中的numpy的阵列对象的引用计数的增量。
在此之后,PyTorch将创建从该numpy的数据blob一个新的张量对象,并且在创建新张量的它与存储器的大小和进展以及稍后将被使用的功能通过借用存储数据指针,一起the Tensor Storage (we’ll discuss this in the next section) to release the data by decrementing the reference counting to the Numpy array object and let Python take care of this object life cycle.
该tensorFromBlob()
method will create a new Tensor, but only after creating a new “Storage” for this Tensor. The storage is where the actual data pointer will be stored (and not in the Tensor structure itself). This takes us to the next section about张量储量。
张量存储
张量的实际原始数据不直接保存在张量结构,但在另一种结构称为存储,这又是张量结构的一部分。
正如我们在前面的代码看到从tensor_from_numpy()
,有一个呼叫tensorFromBlob()
将从原始数据blob创建一个张量。这最后一个功能将调用另外一个函数storageFromBlob(),这将反过来,对于这个数据根据其类型创建存储。在CPU浮点类型的情况下,它会返回一个新的CPUFloatStorage
instance.
该CPUFloatStorageis basically a wrapper with utility functions around the actual storage structure calledŤHFloatStorage
that we show below:
typedef结构THStorage {真实*数据;ptrdiff_t的大小;INT引用计数;焦标志;THAllocator *分配器;无效* allocatorContext;STRUCT THStorage *图;} THStorage;
(代码从THStorage。h)
As you can see, theTHStorage
具有指向原始数据,它的尺寸,标志和也是一个有趣的领域被称为分配器
我们马上要讨论的。同样重要的是要注意,关于如何解释里面的数据没有元数据THStorage
,这是由于这样的事实,存储是“哑”关于它的内容,它是张量的责任要懂得“视图”或解释这个数据。
From this, you already probably realized that we can have multiple tensors pointing to the same storage but with different views of this data, and that’s why viewing a tensor with a different shape (but keeping the same number of elements) is so efficient. This Python code below shows that the data pointer in the storage is being shared after changing the way Tensor views its data:
>>> tensor_a = torch.ones((3,3))>>> tensor_b = tensor_a.view(9)>>> tensor_a.storage()。DATA_PTR()== tensor_b.storage()。DATA_PTR()真
正如我们可以在上面的例子中看到的,在两个张量的存储数据指针是相同的,但张量表示存储的数据的不同的解释。
现在,我们的7号线锯ŤHFloatStorage
结构中,存在一个指向THAllocator
structure there. And this is very important because it brings flexibility regarding the allocator that can be used to allocate the storage data. This structure is represented by the following code:
typedef结构THAllocator {无效*(* malloc的)(无效*,ptrdiff_t的);无效*(* realloc的)(无效*,无效*,ptrdiff_t的);空隙(*免费)(无效*,无效*);} THAllocator;
(代码从THAllocator.h)
As you can see, there are three function pointer fields in this structure to define what an allocator means: a malloc, realloc and free. For CPU-allocated memory, these functions will, of course, relate to the traditional malloc/realloc/free POSIX functions, however, when we want a storage allocated on GPUs we’ll end up using the CUDA allocators such as thecudaMallocHost()
,就像我们可以在看THCudaHostAllocator
malloc function below:
静态无效* THCudaHostAllocator_malloc(无效* CTX,ptrdiff_t的大小){void *的PTR;如果(大小<0)THError( “无效的存储器大小:%LD”,大小);如果(大小== 0)返回NULL;THCudaCheck(cudaMallocHost(PTR,大小));返回PTR;}
(代码从THCAllocator.c)
你可能注意到在库组织模式,但要记住这些公约是很重要的导航信息库时,这里总结(取自PyTorch LIB自述):
- ŤH=ŤorcH
- THC=ŤorcHCuda
- 乡镇卫生院=ŤorcHCuda小号解析
- THCUNN=ŤorcHCUDAñeuralñetwork
- THD=ŤorcHdistributed
- THNN=ŤorcHñeuralñetwork
- THS=ŤorcH小号解析
Ťhis convention is also present in the function/class names and other objects, so it is important to always keep these patterns in mind. While you can find CPU allocators in the TH code, you’ll find CUDA allocators in the THC code.
最后,我们可以看到主要的张量的组成THTensor
结构体:
typedef结构THTensor {*的int64_t大小;*的int64_t步幅;INT n标注;THStorage *存储;ptrdiff_t的storageOffset;INT引用计数;焦标志;} THTensor;
(Code fromTHTensor.h)
正如你所看到的,主THTensor
结构保持的尺寸/步幅/尺寸/偏移/等以及存储(THStorage
),用于张量数据。
我们可以概括所有这种结构,我们在下图中看到:

现在,当我们有要求,如多处理,我们希望将多个不同进程之间共享数据的张,我们需要一个共享内存的方式来解决这个问题,否则,每次另一个进程需要一个张量,甚至当你想实现亚洲金博宝Hogwild训练过程,所有不同的进程将写入同一个存储区(其中的参数),你需要的进程之间进行复制,这是非常低效的。亚洲金博宝因此,我们将在下一节讨论一种特殊类型的存储共享内存的。
共享内存
共享内存可以根据平台支持多种不同的方式来实现。PyTorch支持其中的一些,但是为了简单起见,我会在这里讨论关于使用CPU(而非GPU)的MacOS会发生什么。由于PyTorch支持多种共享存储器方法,这部分是有点棘手把握成,因为它涉及间接在代码多个级别。
PyTorch provides a wrapper around the Pythonmultiprocessing
模块,并且可以从被导入torch.multiprocessing
。aro他们实现的改变在这个包装器und the official Python multiprocessing were done to make sure that everytime a tensor is put on a queue or shared with another process, PyTorch will make sure that only a handle for the shared memory will be shared instead of a new entire copy of the Tensor.
现在,很多人不知道从PyTorch张量方法称为share_memory_()
,however, this function is what triggers an entire rebuild of the storage memory for that particular Tensor. What this method does is to create a region of shared memory that can be used among different processes. This function will, in the end, call this following function below:
静态THStorage * THPStorage_(newFilenameStorage)(ptrdiff_t的大小){INT标志= TH_ALLOCATOR_MAPPED_SHAREDMEM |TH_ALLOCATOR_MAPPED_EXCLUSIVE;的std :: string手柄= THPStorage _(__ newHandle)();自动CTX = libshm_context_new(NULL,handle.c_str(),标志);返回THStorage_(newWithAllocator)(大小,&THManagedSharedAllocator,(无效*)CTX);}
(Code fromStorageSharing.cpp)
正如你所看到的,这个功能将创建使用称为特殊分配器另一个存储ŤHManagedSharedAllocator
。该功能首先定义一些标志,然后它创建一个手柄,其在格式的字符串/ torch_ [进程id] _ [随机数]
,之后,它就会使用特殊创建一个新的存储ŤHManagedSharedAllocator
。Ťhis allocator has function pointers to an internal PyTorch library calledlibshm,将实施Unix Domain SocketCommunication to share the shared memory region handles. This allocator is actual an especial case and it is a kind of “smart allocator” because it contains the communication control logic as well as it uses another allocator calledTHRefcountedMapAllocator
that will be responsible for creating the actual shared memory region and callmmap()的
于该区域映射到进程的虚拟地址空间。
注意:当与一个PyTorch下划线的方法结束时,如该方法称为share_memory_()
,这意味着,该方法具有就地效果,它将改变当前对象,而不是创建一个新的与改进的。
我现在将展示使用来自张量,是由手动交换共享存储器手柄上的另一过程中分配的数据的一个处理的一个Python例如:
这是在处理A执行:
>>> import torch >>> tensor_a = torch.ones((5, 5)) >>> tensor_a 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 [torch.FloatTensor of size 5x5] >>> tensor_a.is_shared() False >>> tensor_a = tensor_a.share_memory_() >>> tensor_a.is_shared() True >>> tensor_a_storage = tensor_a.storage() >>> tensor_a_storage._share_filename_() (b'/var/tmp/tmp.0.yowqlr', b'/torch_31258_1218748506', 25)
在该代码中,在执行进程A,we create a new Tensor of 5×5 filled with ones. After that we make it shared and print the tuple with the Unix Domain Socket address as well as the handle. Now we can access this memory region from another进程B如下所示:
Code executed in the process B:
>>>进口炬>>> tensor_a = torch.Tensor()>>> tuple_info =(B '/ var / tmp中/ tmp.0.yowqlr',B '/ torch_31258_1218748506',25)>>>存储=炬。Storage._new_shared_filename(* tuple_info)>>> tensor_a = torch.Tensor(存储)。查看((5,5))1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 11 1 [5×5大小的torch.FloatTensor]
正如你所看到的,使用关于Unix域套接字的地址和手柄的元组信息,我们能够从另一个访问的过程中张量存储。如果您更改此张量进程B,你也会看到它的反映进程Abecause these Tensors are sharing the same memory region.
DLPack:对于深度学习一个希望构架巴贝尔
ñow I would like to talk about something recent in the PyTorch code base, that is calleddLPack。DLPack是一个内存张量结构,其将允许交换张量数据的一个开放的标准化框架之间,什么是相当有趣的是,因为这个内存的表示是标准化和非常相似的内存中表示已经通过许多框架的使用,这将允许亚洲金博宝zero-copy data sharing between frameworks,which is a quite amazing initiative given the variety of frameworks we have today without inter-communication among them.
这必将有助于克服“孤岛模式”,我们今天在MXNet,PyTorch,等张量表示之间,并且将允许开发者框架和标准化能够给框架的所有好处之间的混合架构的操作。
DLPack的核心操作系统被称为结构非常简单亚洲金博宝DLTensor
, 如下所示:
/ *!* \简短的纯C张量的对象,不管理内存。* / typedef结构{/ *!* \介绍不透明数据指针指向分配的数据。*这将在CUDA的OpenCL设备指针或cl_mem手柄。*这个指针始终对齐到256个字节的CUDA。* / void *的数据;/ *!\介绍张量* / DLContext CTX的设备上下文/ *! \brief Number of dimensions */ int ndim; /*! \brief The data type of the pointer*/ DLDataType dtype; /*! \brief The shape of the tensor */ int64_t* shape; /*! * \brief strides of the tensor, * can be NULL, indicating tensor is compact. */ int64_t* strides; /*! \brief The offset in bytes to the beginning pointer to data */ uint64_t byte_offset; } DLTensor;
(代码从dlpack.h)
As you can see, there is a data pointer for the raw data as well as shape/stride/offset/GPU vs CPU, and other metadata information about the data that theDLTensor
指向。
还有那个叫做张量的托管版本DLManagedTensor
,其中框架可以提供一个框架,并且可以通过谁借了张量来通知资源不再需要其他的框架,框架调用也是一个“删除器”的功能。
在PyTorch,如果要转换或从DLTensor格式,你可以找到这样做,甚至在Python中,你可以做如下所示,两个C / C ++的方法:
从进口torch.utils炬导入dlpack吨= torch.ones((5,5))(DL)= dlpack.to_dlpack(t)的
这Python函数将调用toDLPack
从宏正函数,如下所示:
DLManagedTensor * toDLPack(常量张量&SRC){ATenDLMTensor * atDLMTensor(新ATenDLMTensor);atDLMTensor->手柄= SRC;atDLMTensor-> tensor.manager_ctx = atDLMTensor;atDLMTensor-> tensor.deleter =&删除器;atDLMTensor-> tensor.dl_tensor.data = src.data_ptr();的int64_t DEVICE_ID = 0;如果(src.type()is_cuda()。){DEVICE_ID = src.get_device();} atDLMTensor-> tensor.dl_tensor.ctx = getDLContext(src.type(),DEVICE_ID);atDLMTensor-> tensor.dl_tensor.ndim = src.dim();atDLMTensor-> tensor.dl_tensor.dtype = getDLDataType(src.type()); atDLMTensor->tensor.dl_tensor.shape = const_cast(src.sizes().data()); atDLMTensor->tensor.dl_tensor.strides = const_cast(src.strides().data()); atDLMTensor->tensor.dl_tensor.byte_offset = 0; return &(atDLMTensor->tensor); }
正如你所看到的,这是一个非常简单的转换,铸造从PyTorch格式的DLPack格式,元数据和分配的指针内部张量数据表示。
我真的希望有更多的框架,采用这种标准,必将给生态效益。这也是有趣的是,与潜在的整合Apache的箭将是惊人的。
就是这样,我希望你喜欢这个长的帖子!
– Christian S. Perone