PyTorch - 内部建筑之旅

更新2019年2月28日:我加了一个带有幻灯片的新博客文章包含我为PyData Montreal做的演示。

介绍

这篇文章是关于PyTorch代码基的介绍,旨在指导PyTorch的架构设计及其内部结构。我的主要目标是为那些对理解面向用户的API之外发生的事情感兴趣的人提供一些有用的东西,并展示一些在其他教程中已经介绍过的新东西。

注意:PyTorch构建系统广泛地使用代码生成,因此我在这里不再重复其他人已经描述过的内容。如果您有兴趣了解这是如何工作的,请阅读以下教程:

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

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

//支持的Python对象torch.autograd.变量结构THPVariable{PyObject_HEAD torch::autograd::Variable cdata;PyObject*backward_hooks;};

如您所见,在定义的开头有一个宏,名为PyObject_HEAD,该宏的目标是使Python对象标准化,并将扩展到另一个结构,该结构包含指向类型对象(定义初始化方法、分配器等)的指针以及带有引用计数器的字段。

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

您可以阅读更多关于Python C/++extensions的内容这里

有趣的事实:在许多应用程序亚洲金博宝中,使用小整数作为索引、计数器等非常常见。为了提高效率,官方CPython解释器缓存从-5到256的整数。因此,声明a=200;b=200;a=b真的,而声明一个= 300;b = 300;a是b

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

PyTorch有自己的张量表示,它将PyTorch内部表示与外部表示解耦。然而,由于到处都有Numpy数组是很亚洲金博宝常见的,特别是当数据从各种数据源加载时,因此我们确实需要在Numpy和PyTorch张量之间进行转换。因此,PyTorch提供了两个被调用的方法来自_numpy()努比(),分别将Numpy数组转换为PyTorch数组和PyTorch数组。如果我们查看将Numpy数组转换为Pythorch张量所调用的代码,我们可以进一步了解Pythorch的内部表示:

在::张量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); }); }

(代码从张量_纽比cpp)

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

这里还有一点很重要:当Numpy数组对象超出范围并获得零引用计数时,它将被垃圾收集,并且摧毁,这就是第20行Numpy数组对象的引用计数增加的原因。

之后,PyTorch将从这个Numpy数据blob创建一个新的Tensor对象,在创建这个新的Tensor时,它将传递借用的内存数据指针,再加上内存大小和跨步,以及一个稍后将由Tensor存储器使用的函数(我们将在下一节中讨论),通过减少对Numpy数组对象的引用计数来释放数据,并让Python处理这个对象的生命周期。

tensorFromBlob()方法将创建一个新的张量,但只有创建一个新的“存储”这个张量之后。存储是其中实际数据指针将被存储(而不是在张量结构本身)。这需要我们去了解下部分张量存储器

张量存储

张量的实际原始数据不直接保存在张量结构中,而是保存在另一个称为存储器的结构上,而存储器又是张量结构的一部分。

正如我们在前面的代码看到从张量,有一个呼叫tensorFromBlob()这会从原始数据团中创建一个张量。最后一个函数将调用另一个名为storageFromBlob()的函数,该函数将根据数据的类型为该数据创建存储。对于CPU浮动类型,它将返回一个newCPU贷款存储实例。

CPUFloatStorage基本上是一个包装器,带有围绕实际存储结构调用的实用函数漂浮物贮存我们下面显示:

typedef struct THStorage{real*data;ptrdiff-t size;int refcount;char flag;thallolocator*allocator;void*allocatorContext;struct THStorage*view;}THStorage;

(代码从THStorage.h)

如你所见THStorage保存指向原始数据、其大小、标志和一个有趣的字段的指针分配器我们很快会讨论的。还需要注意的是,对于如何解释THStorage,这是因为存储对于其内容是“哑的”,而知道如何“查看”或解释这些数据是张量的责任。

由此,您可能已经意识到,我们可以有多个张量指向同一存储,但具有不同的数据视图,这就是为什么查看具有不同形状(但保持相同元素数)的张量非常有效的原因。下面的Python代码显示,在更改Tensor查看其数据的方式之后,正在共享存储中的数据指针:

>>> tensor_a = torch。((3))> > > tensor_b = tensor_a.view (9) > > > tensor_a.storage () .data_ptr () = = tensor_b.storage () .data_ptr()实现

正如我们在上面的示例中所看到的,两个张量的存储上的数据指针是相同的,但是张量表示对存储数据的不同解释。

现在,正如我们在第7行看到的漂浮物贮存结构,有一个指针指向THAllocator在那里建造。这一点非常重要,因为它为分配存储数据的分配器带亚洲金博宝来了灵活性。此结构由以下代码表示:

void struct THAllocator (*malloc)(void*, ptrdiff_t);void* (*realloc)(void*, ptrdiff_t);空白(*免费)(void *, void *);}THAllocator;

(代码从丘脑定位器)

正如你所看到的,也有在这个结构中三个功能指针字段定义什么分配器手段:一个malloc,realloc的和免费的。对于CPU分配的内存,这些功能将,当然,涉及到传统的malloc / realloc的/自由POSIX的功能,但是,当我们要对我们最终会使用CUDA的GPU分配器,如分配的存储库达马洛霍斯特(),就像我们在THCudaHostAllocator下面malloc函数:

静态无效* THCudaHostAllocator_malloc(无效* CTX,ptrdiff_t的大小){void *的PTR;如果(大小<0)THError( “无效的存储器大小:%LD”,大小);如果(大小== 0)返回NULL;THCudaCheck(cudaMallocHost(PTR,大小));返回PTR;}

(代码从THCAllocator.c)

您可能注意到了存储库组织中的一个模式,但是在导航存储库时记住这些约定是很重要的,这里总结了这些约定(摘自PyTorch库自述文件):

  • 真实航向=T兽人H
  • THC=T兽人HC乌达
  • 乡镇卫生院=T兽人HC乌达年代解析
  • 泰肯=T兽人H陆军部N欧拉N网络
  • =T兽人HD分发
  • 泰恩=T兽人HN欧拉N网络
  • 解说=T兽人H S公司解析

此约定也存在于函数/类名和其他对象中,因此务必记住这些模式。虽然可以在TH代码中找到CPU分配器,但在THC代码中会找到CUDA分配器。

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

typedef struct THTensor{int64 t*size;int64 t*stride;int nDimension;THStorage*storage;ptrdiff t storageOffset;int refcount;char flag;}THTensor;

(代码来自THTensor.h公司)

如你所见THTensor结构保存大小/跨距/尺寸/偏移量等以及存储(THStorage)对于张量数据。

我们可以在下图中总结所有这些结构:

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

共享内存

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

PyTorch为Python提供了一个包装器多处理模块,可以从火炬.多处理。他们实现的改变在这个官方Python包装器多处理是确保每次一个张量是队列或与另一个进程共享,PyTorch只能确保共享内存是共享的处理而不是一个新的整张量的副本。亚洲金博宝

很多人都不知道PyTorch有一个张量方法叫做share_memory_ ()但是,这个函数触发了该特定张量的存储内存的整个重建。这个方法的作用是创建一个共享内存区域,可以在不同进程之间使用。最后,此函数将调用以下函数:

静态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);}

(代码来自存储共享.cpp)

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

请注意:当方法在PyTorch中以下划线结尾时,例如share_memory_ (),这意味着此方法具有就地效果,它将更改当前对象,而不是通过修改创建新对象。

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

这在流程A中执行:

>>>导入火炬>>>张量a=手电筒((5,5) )>>>张量[焊炬浮子张量大小为5x5]>>>tensor_a.is_shared()False>>tensor_a=tensor_a.share_amemory_a()>>tensor_a.is_ared()True>>>tensor_a_a存储=tensor_a.storage()>>tensor_a存储。_share_afilename_au()(b'/var/tmp/tmp.0.yowqlr',b'/torch_a1218748506',25)

在本代码中,在处理一个,我们创建了一个5×5的新张量。之后,我们将其共享,并使用Unix域套接字地址和句柄打印元组。现在我们可以从另一个进程B如下所示:

过程B中执行的代码:

>>>导入火炬>>>张量a=火炬张量()>>>元组信息=(b'/var/tmp/tmp.0.yowqlr',b'/torch_31258_1218748506',25)>>存储=火炬.储存.u new_shared_filename(*tuple_info)>>>张量a=火炬张量(存储)视图((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 1 1 1 1[焊炬浮子张量尺寸5x5]

如您所见,使用有关Unix域套接字地址和句柄的元组信息,我们可以从另一个进程访问Tensor存储。如果你改变张量进程B,你还会看到它会在处理一个因为这些张量共享相同的记忆区域。

DLPack:深度学习框架的希望

现在我想谈谈Pythorch代码库中最近的一些东西,叫做数据包. DLPack是内存中张量结构的开放标准化,它允许交换张量数据框架之间,什么是相当有趣的是,因为这个内存的表示是标准化和非常相似的内存中表示已经通过许多框架的使用,这将允许亚洲金博宝框架之间的零拷贝数据共享,这是考虑到各种框架,我们今天有没有它们之间相互通信的相当惊人的举措。

这无疑将有助于克服我们今天在MXNet、PyTorch等中的张量表示之间的“孤岛模型”,并允许开发人员在框架之间混合框架操作以及标准化可以给框架带来的所有好处。

DLPack操作系统的核心是一个非常简单的结构,亚洲金博宝称为DLTensor,如下所示:

/*! *\brief Plain C Tensor对象,不管理内存。*/typedef结构{/*!*\brief不透明数据指针指向分配的数据。*这将是OpenCL中的CUDA设备指针或cl_mem句柄。*此指针始终与CUDA中的256字节对齐。*/空*数据;/*!\简要介绍张量的设备上下文*/DLContext ctx;/*!\简要的维度数*/int ndim;/*!\简要说明指针的数据类型*/DLDataType dtype;/*!\简要介绍张量的形状*/int64_t*shape;/*!*\brief steps of the tensor,*可以为空,表示张量很紧凑。*/国际64大踏步;/*!\将偏移量以字节为单位简述为指向数据的开始指针*/uint64_t byte_offset;}DLTensor;

(代码从dlpack.h)

正如可以看到,对于原始数据的数据指示字,以及形状/步幅/偏移/ GPU VS CPU,和其它元数据的信息有关的数据,该DLTensor指向。

还有一个张量的托管版本叫做数据管理传感器,其中框架可以提供一个上下文和一个“deleter”函数,框架可以调用该函数,该函数借用张量通知另一个框架不再需要资源。

在PyTorch,如果要转换或从DLTensor格式,你可以找到这样做,甚至在Python中,你可以做如下所示,两个C / C ++的方法:

从进口火炬火炬.utils导入dlpack t=手电筒(5,5)分升=dlpack.to\u数据包(吨)

这个Python函数将调用托德帕克来自ATen的功能,如下所示:

DLManagedTensor*toDLPack(const Tensor&src){ATenDLMTensor*atDLMTensor(新的ATenDLMTensor);atDLMTensor->handle=src;atDLMTensor->张量经理=atDLMTensor;atDLMTensor->张量删除器=&deleter;atDLMTensor->张量。数据=源数据();int64设备id=0;如果(型钢(0)是_cuda()){设备id=src.get_设备(); }ATDLM传感器->张量.ctx=getDLContext(型钢(,设备id);ATDLM传感器->张量.ndim=钢筋混凝土尺寸();ATDLM传感器->张量.dtype=获取数据类型(型钢();ATDLM传感器->张量.shape=常量转换(钢筋混凝土尺寸(二)数据(二);ATDLM传感器->张量.streams=const_cast(src.跨步(.data());atDLMTensor->张量.byte_offset=0;返回&(atDLMTensor->张量);}

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

我真的希望更多的框架采用这个标准,这肯定会给生态系统带来好处。值得注意的是阿帕奇箭头会很棒的。

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

- 基督教S. Perone

引用本文为:Christian S.Perone,“Pythorch——内部建筑之旅”,in亚洲金博宝未发现的地域2018年3月12日,//www.cpetem.com/2018/03/pytorch-internal-architecture-tour/

13对“Pythorch——内部建筑之旅”的思考

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

  2. 棒极了!不过,我认为您最好添加源代码版本,因为底层后端变化很快,有些链接已经断开。

  3. 你好,克里斯蒂安,谢谢你提供pytorch的内部细节。

    我有一个从火把到努比的转换问题,希望你能帮助我了解发生了什么以及如何修复它。

    简单地说,我将数组转换为pytorch,执行一个进程,然后转换回numpy,以便使用opencv进行后续处理。

    例子:
    火炬阵列=火炬。来自únumpy(numpyŠ数组)Š小于1毫秒
    在99%的GPU上对torch#u阵列进行小于1毫秒的处理
    numpy_array = np.array(torch_array

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

    问候h

    1. 您应该使用.numpy()。

      火炬阵列=火炬。来自únumpy(numpy_数组)
      ….
      ….
      numpy_array = torch_array.numpy()

  4. 谢谢你的一个伟大的职位!它真的帮助我理解了张量存储的工作原理。我现在可以检查两个张量是否共享相同的存储空间(通过't0.storage().date_ptr()==t1.storage().data_ptr()`),但如何检查numpy数组是否是张量的视图?有没有办法在Pythorch和Numpy之间做类似的检查?提前谢谢你的建议!

  5. 很好的帖子,它确实帮助我了解了火把的存储。

    我的理解是,我可以从STL向量创建一个C++ PyTrac张量,并通过pyBon将其公开到Python,而不需要复制。

    我不知道如果我能揭露从C ++ STL的一个载体向Python和从它创建一个张没有进行复印,尽管https://pytorch.org/docs/stable/tensors.html说torch.tensor总是拷贝数据

发表评论

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

这个网站使用Akismet来减少垃圾邮件。了解您的意见如何处理数据