内部建筑之旅

介绍

这篇文章是关于PyTorch代码库的,为火炬及其内部构件的建筑设计提供指导。我的主要目标是为那些有兴趣了解在面向用户的API之外发生的事情的人提供一些有用的东西,并展示一些其他教程中没有介绍的新内容。

注意:PyTorch构建系统广泛地使用代码生成,因此我不会在这里重复其他人已经描述过的内容。如果你对它的原理感兴趣,请阅读以下教程:

C/ c++中Python扩展对象的简短介绍

你们可能知道,您可以使用C和c++扩展Python,并开发所谓的“扩展”。所有繁重的PyTorch工作都是用C/ c++实现的,而不是纯python。要在C/ c++中定义新的Python对象类型,您可以定义如下面的示例所示的结构(这是autograd的基础变量类):

// Python object that backs torch.autograd.Variablestruct THPVariable {    PyObject_HEAD    torch::autograd::Variable cdata;PyObject * backward_hooks;};

如你所见,在定义的开始有一个宏,被称为PyObject_HEAD,该宏的目标是Python对象的标准化,并将扩展到另一个结构,该结构包含一个指向类型对象的指针(该指针定义了初始化方法,分配器,以及一个带有参考计数器的字段。

Python API中有两个额外的宏被调用Py_INCREF ()Py_DECREF (),用于递增和递减Python对象的引用计数器。多个实体可以借用或拥有对其他对象的引用(引用计数器增加),只有当这个引用计数器达到0时(当所有引用都被销毁时),Python将使用垃圾收集器自动删除该对象中的内存。

您可以阅读有关Python C/ c++扩展的更多信息在这里

有趣的事实:在许多应用程序bepaly亚洲中,使用小整数作为索引是很常见的,计数器,等。为了提高效率,官方的 CPython的翻译缓存从-5到256的整数。出于这个原因,该声明 一个= 200;b = 200;a是b真正的,在声明中 一个= 300;b = 300;a是b

零拷贝PyTorch张量到Numpy,反之亦然

PyTorch有自己的张量表示,它将PyTorch的内部表示与外部表示解耦。然而,因为这很常见,bepaly亚洲特别是当数据从各种来源加载时,要让Numpy数组无处不在,bepaly亚洲因此,我们确实需要在Numpy和PyTorch张量之间进行转换。出于这个原因,PyTorch提供了两个方法from_numpy()numpy (),将Numpy数组转换为PyTorch数组,反之亦然,分别。如果我们看一下将一个Numpy数组转换成PyTorch张量的代码,我们可以对火炬的内部表现有更多的了解:

at::Tensor tensor_from_numpy(PyObject* obj) {  if (!PyArray_Check(obj)) {    throw TypeError("expected np.ndarray (got %s)",Py_TYPE(obj)- > tp_name);} auto array = (PyArrayObject*)obj;int ndim = PyArray_NDIM(array);自动大小= to_aten_shape(ndim,PyArray_DIMS(数组));自动步长= to_aten_shape(ndim,PyArray_STRIDES(数组));// NumPy跨步使用字节。火炬跨步使用元素计数。auto element_size_in_bytes = PyArray_ITEMSIZE(array);for (autoandstride: stride) {stride /= element_size_in_bytes;} //(…)-为简洁起见省略* data_ptr = PyArray_DATA(array);auto&type = CPU(dtype_to_aten(PyArray_TYPE(array)));Py_INCREF(obj);返回type.tensorFromBlob(data_ptr,大小,的进步,[obj](void* data) {AutoGIL gil;Py_DECREF (obj);});}

(代码从tensor_numpy.cpp)

从这段代码可以看出,PyTorch从Numpy表示中获取所有信息(数组元数据),然后创建自己的信息。然而,从第18行可以看出,PyTorch将获得指向内部Numpy数组原始数据的指针,而不是复制它。这意味着PyTorch将为该数据创建一个引用,与原始张量数据的Numpy数组对象共享相同的内存区域。

这里还有一点很重要:当Numpy数组对象超出作用域并获得零引用计数时,它将被垃圾收集和摧毁了,这就是为什么Numpy数组对象的引用计数在第20行有一个增量。

在这之后,PyTorch将从这个Numpy数据块创建一个新的张量对象,在这个新张量的创建过程中,它传递了借来的内存数据指针,与内存大小和进步以及张量函数将使用后的存储(我们将在下一节讨论这个)发布的数据递减Numpy数组对象的引用计数,让Python照顾这个对象的生命周期。

tensorFromBlob()方法会产生一个新的张量,但是只有在为这个张量创建一个新的“存储”之后。存储是存储实际数据指针的地方(而不是存储在张量结构本身中)。这让我们进入下一节张量存储

张量的存储

张量的实际原始数据并不直接保存在张量结构中,但是在另一种叫做存储的结构中,这也是张量结构的一部分。

正如我们在前面的代码中看到的tensor_from_numpy (),有一个要求tensorFromBlob()这将从原始数据blob中创建一个张量。最后一个函数将调用另一个名为storageFromBlob()的函数,反过来,根据数据类型为其创建存储。在CPU浮点类型的情况下,它将返回一个新的CPUFloatStorage实例。

CPUFloatStorage基本上是一个包装器,它具有围绕实际存储结构调用的实用函数THFloatStorage我们展示如下:

typedef struct THStorage{real *data;ptrdiff大小;int refcount;char国旗;THAllocator *分配器;void * allocatorContext;struct THStorage *view;

(代码从THStorage.h)

如你所见,的THStorage持有指向原始数据的指针,它的大小,还有一个有趣的字段叫做分配器我们很快就会讨论这个问题。还需要注意的是,没有关于如何解释内部数据的元数据THStorage,这是因为存储的内容是“愚蠢的”,张量有责任知道如何“查看”或解释这些数据。

从这个,你可能已经意识到我们可以有多个张量指向相同的存储但是有不同的数据视图,这就是为什么用不同的形状(但是保持相同的元素数量)来观察一个张量是如此有效。下面的Python代码显示,在改变了张量查看数据的方式之后,存储中的数据指针正在被共享:

>>> tensor_a =电筒.ones((3,3))> > > tensor_b = tensor_a.view (9) > > > tensor_a.storage () .data_ptr () = = tensor_b.storage () .data_ptr()实现

正如我们在上面的例子中看到的,两个张量存储上的数据指针是相同的,但是张量代表了对存储数据的另一种解释。

现在,正如我们在第7行看到的THFloatStorage结构,有一个指向a的指针THAllocator结构。这一点非常重要,因为它为bepaly亚洲分配程序带来了灵活性,可以用来分配存储数据。该结构由以下代码表示:

typedef struct THAllocator{void* (*malloc)(void*,ptrdiff);void * * realloc () (void *,void *,ptrdiff);空白(*免费)(void *,void *);} THAllocator;

(代码从THAllocator.h)

如你所见,在这个结构中有三个函数指针字段来定义分配器的含义:realloc和自由。分配cpu内存,这些函数将当然,与传统的malloc/realloc/free POSIX函数相关,然而,当我们需要在gpu上分配一个存储时,我们最终将使用CUDA分配器,例如cudaMallocHost(),就像我们在THCudaHostAllocatormalloc函数如下:

静态void* THCudaHostAllocator_malloc(void* ctx,ptrdiff_t size) {void* ptr;if (size < 0) THError("无效内存大小:%ld")大小);if (size == 0)返回NULL;THCudaCheck(cudaMallocHost(ptr,大小));返回ptr;}

(代码从THCAllocator.c)

您可能注意到了存储库组织中的一个模式,但是在浏览存储库时,一定要记住这些约定,总结如下(摘自PyTorch自由自述):

  • TH=T兽人H
  • THC=T兽人H C使用uda
  • thc=T兽人H C使用uda年代解析
  • THCUNN=T兽人H NeuralNetwork
  • =T兽人H Distributed
  • THNN=T兽人H NeuralNetwork
  • 解说=T兽人H年代解析

这个约定也存在于函数/类名和其他对象中,所以一定要记住这些模式。虽然你可以在TH代码中找到CPU分配器,您将在THC代码中找到CUDA分配器。

最后,我们可以看到主张量的复合THTensor结构:

typedef struct th张量{int64_t *size;int64_t *步;int nDimension;THStorage *存储;ptrdiff storageOffset;int refcount;char国旗;}THTensor;

(代码从THTensor.h)

你可以看到,主要的THTensor结构保存大小/跨距/尺寸/偏移量/等等以及存储(THStorage)的张量数据。

我们可以总结一下我们在下面的图表中看到的所有这些结构:

现在,一旦我们有了需求,比如多处理我们想要在多个不同的过程中共享张量数据,我们需要一个共享内存的方法来解决它,否则,每bepaly亚洲当另一个过程需要一个张量,甚至当你想要实现的时候Hogwild训练过程中,所有不同的进程将写入相同的内存区域(参数所在),你需要在进程之间复制,这是非常低效的。bepaly亚洲因此,我们将在下一节中讨论一种用于共享内存的特殊存储。

共享内存

根据平台的支持,共享内存可以以多种不同的方式实现。PyTorch支持其中一些,但为了简单起见,我将在这里讨论使用CPU(而不是GPU)在MacOS上发生的事情。由于PyTorch支持多种共享内存方法,这一部分比较难理解,因为它涉及到代码中更多的间接层。

PyTorch为Python提供了一个包装器多处理模块,可以导入torch.multiprocessing。他们在这个官方Python多处理的包装器中实现的更改是为了确保每次将一个张量放到队列中或与另一个进程共享时,bepaly亚洲PyTorch将确保只共享共享内存的句柄,而不是张量的整个新副本。

现在,很多人不知道PyTorch中的张量方法share_memory_ (),然而,这个函数是触发那个特定张量的整个存储内存重建的原因。这个方法的作用是创建一个可以在不同进程之间使用的共享内存区域。这个函数将最后,调用以下函数:

{int flags = TH_ALLOCATOR_MAPPED_SHAREDMEM | TH_ALLOCATOR_MAPPED_EXCLUSIVE;std::string handle = THPStorage_(__newHandle)();auto ctx = libshm_context_new(NULL,handle.c_str (),旗帜);返回THStorage_(newWithAllocator)(大小,&THManagedSharedAllocator,(void *) ctx);}

(代码从StorageSharing.cpp)

你可以看到,这个函数将使用一个名为的特殊分配器创建另一个存储THManagedSharedAllocator。这个函数首先定义一些标志,然后创建一个句柄,它是一个字符串的格式/ torch_[进程id] _(随机数),在那之后,然后,它将使用special创建一个新的存储THManagedSharedAllocator。这个分配器具有指向内部PyTorch库的函数指针libshm,这将实现Unix域套接字共享共享内存区域句柄的通信。这个分配器实际上是一个特殊的情况,它是一种“智能分配器”,因为它包含通信控制逻辑,并使用另一个分配器调用THRefcountedMapAllocator这将负责创建实际的共享内存区域和调用mmap()将此区域映射到进程虚拟地址空间。

请注意:当一个方法以PyTorch中的下划线结束时,例如调用的方法 share_memory_ (),这意味着该方法具有就地效应,它将改变当前对象,而不是创建一个新的修改。

现在我将展示一个Python示例,其中一个处理使用了一个张量的数据,这个张量是通过手动交换共享内存句柄分配给另一个进程的:

这是在流程A中执行的:

>>>导入电筒>>> tensor_a =电筒.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)

在这段代码中,中执行处理一个,我们创建一个新的5×5张满的。然后,我们将其共享,并使用Unix域套接字地址和句柄打印元组。现在我们可以从另一个内存区域访问这个内存区域进程B如下所示:

进程B中执行的代码:

>>>导入火炬>>> tensor_a =火炬。张量()>>> tuple_info = (b'/var/tmp/tmp.0.yowqlr',b ' / torch_31258_1218748506 ',25)>>>存储=火炬。store ._new_shared_filename(*tuple_info)>>> tensor_a =火炬。张量(storage).view((5,5)) 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]

如你所见,通过使用Unix域套接字地址和句柄的元组信息,我们能够从另一个进程访问张量存储。如果你改变这个张量进程B,你也会看到它会在处理一个因为这些张量共享相同的内存区域。

DLPack:对深度学习框架Babel的希望

现在我想谈谈PyTorch代码库中最近的一些东西,,被称为DLPack。DLPack是内存张量结构的开放标准化,它允许交换张量数据之间的框架,有趣的是,由于这种内存表示是标准化的并且与许多框架已经使用的内存表示非常相似,bepaly亚洲它将允许框架之间的零拷贝数据共享,这是一项非常了不起的倡议,因为我们今天有各种各样的框架,但它们之间没有相互交流。

这肯定会帮助我们克服MXNet中张量表示之间的“岛屿模型”PyTorch,等,并且允许开发人员在框架之间混合框架操作以及标准化可以给框架带来的所有好处。

DLPack os的核心是一个非常简单的结构,称bepaly亚洲为DLTensor,如下所示:

/ * !*简单的C张量对象,不管理内存。*/typedef struct {/*!*简要说明不透明数据指针指向分配的数据。*这将是CUDA设备指针或OpenCL中的cl_mem句柄。*这个指针总是对齐到256字节的CUDA。* / void *数据;/ * !\简述张量*/ DLContext ctx的设备上下文;/ * !\简单的尺寸数量*/ int ndim;/ * !\简述指针*/ DLDataType dtype的数据类型;/ * !简略张量*/ int64_t*的形状;/ * !张量的简单步长,*可以为空,表示张量是紧的。* / int64_t *进步;/ * !\将偏移量以字节表示为指向data */ uint64_t byte_offset;} dl张量;

(代码从dlpack.h)

如你所见,有一个数据指针的原始数据以及形状/跨距/偏移量/GPU对CPU,和其他元数据信息有关的数据DLTensor指向。

还有一个张量的管理版本叫做DLManagedTensor,其中框架可以提供一个上下文和一个“删除器”函数,框架可以调用这个函数,框架借用这个张量来通知另一个框架不再需要这些资源。

在PyTorch,如果你想转换成dl张量格式,你可以找到C/ c++两种方法来实现,甚至在Python中你也可以这样做,如下所示:

import torchfrom torch.utils import dlpackt=torch.one((5,5))dl = dlpack.to_dlpack(t)

这个Python函数将调用toDLPack函数从阿托恩如下所示:

ATenDLMTensor * toDLPack(const tensor&src) {atendlm张量* atdlm张量(new atendlm张量);atDLMTensor - >处理= src;atDLMTensor->tensor.manager_ctx = atDLMTensor;atDLMTensor - > tensor.deleter =删除人;atdlmtensor->tensor.dl_tensor.data=src.data_ptr();int64_t device_id = 0;if (sr .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.steps=const_cast
           
            (src.strides(). data());atDLMTensor->tensor.dl_tensor.byte_offset = 0;返回& (atDLMTensor - >张量);}
           
          

如你所见,这是一个非常简单的转换,将元数据从PyTorch格式转换为DLPack格式,并为内部张量数据表示分配一个指针。

我真的希望更多的框架采用这个标准,这肯定会给生态系统带来好处。值得注意的是,一个潜在的集成Apache箭头将是惊人的。

就是这样,希望你喜欢这篇长文!

——基督教。腓骨

基督教的年代。腓骨

9日评论

  1. 好的文章!bepaly亚洲看到Pytorch的细节以及它的良好实现非常有趣。

  2. 好帖子!然而,我认为您最好添加源代码版本,因为底层后端变化很快,一些链接已经断开。

  3. 嗨,基督徒,感谢pytorch的内幕消息。

    我在将pytorch转换成numpy时遇到了一个问题,希望您能帮助我理解发生了什么,以及如何修复它。

    简而言之,我把数组转换成pytorch,做一个过程,然后使用opencv将其转换回numpy以进行后续处理。

    例子:
    torch_array =火炬。from_numpy(numpy_array) #小于1msec
    对GPU @ 99%上小于1 msec的torch_array进行处理吗
    numpy_array = np.array(torch_array) #大于200msec

    jetson TX1平台上的GPU = nvidia
    火炬= 0.4.0

    认为h

  4. 写的好!现在我知道了更多关于pytorch内部结构的知识,它如何表示/存储张量

留下你的评论

您的电子邮件地址将不会公布。

这个网站使用Akismet来减少垃圾邮件。了解如何处理注释数据