Pytorch - 内部架构之旅

2019年2月28日更新:我添加了一个带有幻灯片的新博客文章包含我为Pydata蒙特利尔的演示文稿。

介绍

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

注意:Pytorch Build System广泛使用代码,因此我不会在此处重复其他人已经描述的内容。如果您有兴趣了解这项工作,请阅读以下教程:

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

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

//返回torch.autograd.variable structiable {pyobject_head torch :: autograd ::变量cdata;pyobject * backward_hooks;};

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

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

你可以阅读更多关于Python C/++扩展的内容这里

有趣的事实:在许多应用程序亚洲金博宝中使用小整数数字作为索引,计数器等非常常见。CPython的翻译缓存-5 ~ 256的整数。因此,声明一个= 200;b = 200;a是b真正的,虽然陈述一个= 300;b = 300;a是b错误的

Zero-Copy Pytorch Tensor到Numpy,反之亦然

PyTorch有自己的张量表示,它将PyTorch内部表示与外部表示解耦。然而,由于这是非常常见的,特别是当数亚洲金博宝据从各种来源加载时,到处都有Numpy数组,因此我们真的需要在Numpy和PyTorch张量之间进行转换。因此,PyTorch提供了两个被调用的方法from_numpy ()numpy (),它分别将NUMPY数组转换为Pytorch阵列和反之亦然。如果我们查看正在调用的代码将numpy数组转换为pytorch tensor,我们可以对Pytorch的内部表示获得更多的见解:

在:: tensor tensor_from_numpy(pyobject * obj){if(!pyarray_check(obj)){throw typeerror(“预期的np.ndarray(got%s)”,py_type(obj) - > tp_name);}自动阵列=(pyarrayObject *)obj;int ndim = pyarray_ndim(阵列);自动尺寸= to_aten_shape(ndim,pyarray_dims(array));自动strides = to_aten_shape(ndim,pyarray_strates(array));// numpy strides使用字节。火炬迈出了元素数。自动元素_size_in_bytes= pyarray_itemsize(array);for(auto&stride:strides){stride / = componen_size_in_bytes;} //(...) - 为简洁void * data_ptr = pyarray_data(array)省略; 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数组对象共享同一个内存区域。

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

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

tensorFromBlob ()方法将创建一个新的张量,但仅在为此张量创建新的“存储”之后。存储是将存储实际数据指针的位置(而不是在张量结构本身中)。这使我们带到下一节张量议员

张量的存储

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

正如我们在以前的代码中看到的那样tensor_from_numpy(),有一个电话tensorFromBlob ()它将从原始数据团中创建一个张量。最后一个函数将调用另一个名为storageFromBlob()的函数,该函数将根据数据的类型为数据创建存储。在CPU浮点类型的情况下,它将返回一个newcpufloatstorage实例。

CPUFloatStorage基本上是一个包装器,包含围绕实际存储结构调用的实用函数THFloatStorage我们在下面显示:

typedef struct THStorage{真实*数据;ptrdiff_t大小;int refcount;char国旗;THAllocator *分配器;void * allocatorContext;struct THStorage *视图;} THStorage;

(代码从THStorage.h

正如你所看到的那样,尘埃保存一个指向原始数据的指针、它的大小、标志和一个有趣的字段分配器我们很快会讨论这个问题。控件中没有关于如何解释数据的元数据,注意这一点也很重要尘埃,这是由于存储对于其内容来说是“愚蠢的”,而知道如何“查看”或解释这些数据是张量的责任。

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

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

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

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

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

(代码从THAllocator.h

如您所见,此结构中有三个函数指针字段可定义分配器意味着什么:Malloc,Realloc和免费。对于CPU分配的内存,这些功能当然,与传统的MALLOC / REALLOC /免费POSIX函数有关,但是,当我们希望在GPU上分配的存储时,我们将结束使用CUDA分配器(如)cudamallochost(),就像我们在THCudaHostAllocatorMalloc功能下面:

静止void * thcudahostallocator_malloc(void * ctx,ptrdiff_t大小){void * ptr;如果(size <0)therror(“内存大小无效:%ld”,大小);if(size == 0)返回null;thcudacheck(Cudamallochost(&PTR,尺寸));返回PTR;}

(代码从THCAllocator.c

您可能会注意到存储库组织中的一个模式,但在导航存储库时务必记住这些约定,如下所示(摘自PyTorch自由自述):

  • THT兽人H
  • THCT兽人HC使用uda
  • Thcs.T兽人HC使用uda年代解析
  • THCUNNT兽人HNeuralNetwork
  • T兽人HD被分配
  • THNN.T兽人HNeuralNetwork
  • 解说T兽人H年代解析

此惯例也存在于函数/类名和其他对象中,因此始终始终保持这些模式非常重要。虽然您可以在TH代码中找到CPU分配器,但您将在THC代码中找到CUDA分配器。

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

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

(代码thtensor.h.

尽你所能,主要是THTensor结构包含大小/跨距/维度/偏移量/等以及存储空间(尘埃)为张量数据。

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

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

共享内存

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

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

现在,很多人都不知道调用了一个来自PyTorch的Tensor方法share_memory_ ()然而,这个函数触发了特定张量的存储内存的整个重建。这个方法所做的是创建一个可以在不同进程之间使用的共享内存区域。这个函数最终会调用下面这个函数:

静态thstorage * thpstorage_(newfilenamistorage)(ptrdiff_t大小){int flags = th_allocator_mapped_sharedmem |th_allocator_mapped_exclusive;std :: string handle = thpstorage _(__ newhandle)();自动CTX = libshm_context_new(null,handle.c_str(),标志);返回thstorage_(newwithallocator)(大小,和thmanagedsharedallocator,(void *)ctx);}

(代码StorageHaring.cpp.

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

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

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

这在进程A中执行:

>>> import torch >>> tensor_a = torch。((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(火炬。True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True True Trueyowqlr ' b ' / torch_31258_1218748506 ', 25)

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

在过程B中执行的代码:

>>>进口火炬>>> Tensor_A = TORCH.TENSOR()>>>>>>>>>>>>>>>>YOWQLR',B'/ TORCH_31258_1218748506',25)>>>存储=火炬。storage._new_shared_filename(* tuple_info)>>> tensor_a = torch.tensor(存储).view((5,5))1 1 1 1 1 1 1 1 1 1 11 11 1 1 1 1 1 1 11 1 1 1 1 1 11 1 [TORCH.FLOATTESOR大小5x5]

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

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

现在我想谈谈PyTorch代码库中最近的东西,叫做DLPack.DLPack是内存张量结构的开放标准化,它允许交换张量数据框架之间并且非常有趣的是,由于这种内存表示标准化并且与已经在许多框架使用的内存表示非常相似,因此它将允许a亚洲金博宝框架之间的零复制数据共享鉴于我们今天的框架各种框架,这是一个相当惊人的倡议,而没有他们之间的互通。

这肯定有助于克服我们今天在MXNet,Pytorch等的张量表示之间的“岛式模型”,并且将开发人员在框架之间混合框架操作以及标准化可以为框架带来的所有优势之间的框架操作。

DLPack os的核心结构非常简单,叫做亚洲金博宝DLTensor, 如下所示:

/ * !* \brief普通C张量对象,不管理内存。*/定义结构类型opaque data指针指向已分配的数据。*这将是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.

如您所见,原始数据以及形状/步幅/偏移/ GPU与CPU的数据指针以及关于数据的其他元数据信息DLTensor指向。

还有一个张量的管理版本叫做DLManagedTensor,框架可以提供上下文,也可以由借用Tensor借用的框架调用的“DELETET”功能通知其他框架不再需要资源。

在Pytorch中,如果要转换为或从DLTensor格式转换,可以找到C / C ++方法,以便在Python中执行此操作,您可以执行以下操作,如下所示:

从火炬进口火炬。从火炬导入dlpack t =火炬。((5,5))dl = dlpack.to_dlpack(t)

这个python函数将调用toDLPack来自Aten的功能,如下所示:

dlmanagedtensor * todlpack(const tensor&src){atendlmtensor * atdlmtensor(new areendlmtensor);atdlmtensor-> handle = src;atdlmtensor-> tensor.manager_ctx = atdlmtensor;atdlmtensor-> tensor.deleter =&deleter;atdlmtensor-> tensor.dl_tensor.data = src.data_ptr();int64_t device_id = 0;if(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 = getDLDattype(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

将本文引用:Christian S. Perone,“Pytorch - 内部架构之旅”亚洲金博宝未发现的地域12/03/2018,//www.cpetem.com/2018/03/pytorch-internal-architecture-tour/

“PyTorch -内部建筑之旅”的13个想法

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

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

  3. 你好,克里斯蒂安,谢谢你为我们提供关于pytorch的内幕信息。

    我有一个问题从pytorch转换成numpy,希望你能帮助我理解发生了什么,以及如何修复它。

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

    例子:
    Torch_array = torch.from_numpy(numpy_array) #小于1msec
    在GPU上,torch_array的处理时间小于1毫秒,99%
    Numpy_array = np.array(torch_array) #大于200毫秒

    GPU = Jetson TX1平台上的NVIDIA
    火炬= 0.4.0

    认为h

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

      torch_array = torch.from_numpy (numpy_array)
      ....
      ....
      numpy_array = torch_array.numpy()

  4. 写得好!现在我了解更多有关Pytorch内部的信息,如何代表/储存张量

  5. 谢谢你的精彩帖子!它真的帮助我理解张量存储是如何工作的。我现在可以检查是否两个张量共享相同的存储(通过' t0.storage().date_ptr() == t1.storage().data_ptr() '),但我如何检查numpy数组是张量的视图?是否有一种方法在PyTorch和Numpy之间做类似的检查?提前谢谢你的建议!

  6. 很棒的帖子,它确实帮助我了解Pytorch存储。

    我的理解是,我可以从STL向量创建一个c++ pytorch张量,并通过pybind将其公开给Python,不需要复制。

    我想知道我是否可以将STL矢量从C ++从C ++曝光到Python,并在没有制作副本的情况下从中创建一个张量,尽管如此https://pytorch.org/docs/stable/tensors.htmlTorch.Tensor说始终复制数据

答复蒂亚戈取消回复

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

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