TensorFlow计算图是由op和tensor组成,那么tensor一般都用来代表什么呢?显然,像模型的输入数据、网络权重、输入数据经op处理后的输出结果都需要用张量或特殊张量进行表达。既然tensor在TensorFlow体系架构中如此重要,因此本文将带领大家由浅入深地学习tensor的三个话题:用户眼中的tensor、TensorFlow系统中的tensor、tensor高阶用法DLPack(跨框架编程,如:TensorFlowPyTorch)。 注:本文基于TensorFlowv1。15。5进行编写。一、小白眼中的Tensor1。1TensorHelloWorld 定义两个张量,然后对其求加法,相关代码如下:segment1atf。constant(3。0,dtypetf。float32)btf。constant(4。0)alsotf。float32implicitlytotalabprint(a)print(b)print(total)三个print的输出如下:Tensor(Const:0,shape(),dtypefloat32)Tensor(Const1:0,shape(),dtypefloat32)Tensor(add:0,shape(),dtypefloat32)说明:此时的Tenosr尚不能产生真正的结果。以上代码创建了计算图,Tensor只是代表op运行的结果(但此时op未运行)。 如果想看到最终total的计算结果,则应该创建Session对象并运行计算图,具体代码如下(在segment1基础上增加代码):withtf。Session()assess:resultsess。run(total)print(result,type(result),type(total))输出结果7。0classnumpy。float32classtensorflow。python。framework。ops。Tensor 由此可见,Tensor代表尚未执行的结果表示,创建Session对象并运行计算图可得total结果7。0,并且结果的数据类型已变为numpy。最后说明一下,本小节代码输出的Tensor是指tf。Tensor,对应的代码实现是tensorflow。python。framework。ops。Tensor。1。2张量属性及特殊张量 从用户视角看tf。Tensor主要有三个属性:name、dtype、shape。除此之外,还有三个属性比较重要(不常用或者不直接可见):op、graph、device。其中op属性记录产生此Tensor的操作名称,graph属性记录包含此Tensor的计算图,device属性记录产生此Tensor的设备名称。 在TensorFlow体系中有四种特殊的张量(此处暂不严格区分Tensor与产生此Tensor的op),具体如下: tf。Variable:定义内容可变的张量,一般用来定义模型权重。 tf。constant:一般来说,张量内容不可变,此API可用来定义常规张量。 tf。placeholder:占位符张量,用于描述静态图输入规格。静态图采用先编译后执行的方式,因此在定义计算图时要知道输入规格。 tf。SparseTensor:为稀疏数据定制的张量结构。1。3Tensor与op的关系 我们多次提到,Tensor可以作为op的输入,经op一系列处理后产生新的Tensor作为输出。为了深入理解这一点,我们回头重新审视segment1中的代码片段(请大家注意Tensor的命名):segment1atf。constant(3。0,dtypetf。float32)btf。constant(4。0)alsotf。float32implicitlytotalabprint(a)print(b)print(total)三个print的输出如下:Tensor(Const:0,shape(),dtypefloat32)Tensor(Const1:0,shape(),dtypefloat32)Tensor(add:0,shape(),dtypefloat32)说明:此时的Tenosr尚不能产生真正的结果。以上代码创建了计算图,Tensor只是代表op运行的结果(但此时op未运行)。 针对上述代码,我们先来看看哪些是Tensor,哪些是op,然后基于此分别描述每一个操作的执行过程。为回答第一个问题,我们先看一段TensorFlow官方注释:tf。constantcreatesaConstnodeinthecomputationgraphwiththeexactvalueatgraphconstructiontime。 由此可见,segment1的代码中有两种op,分别为Const和add,前者出现了两次,而后者1次。基于此,我们得知segment1依次向计算图中添加了三个op,与此同时也可以回答第二个问题,即每个操作的过程。具体如下:三个print的输出如下(a,b,total):Tensor(Const:0,shape(),dtypefloat32)Tensor(Const1:0,shape(),dtypefloat32)Tensor(add:0,shape(),dtypefloat32)向计算图添加第一个op(Const),输入是一个标量,输出是Tensora,其名称由两部分组成,即op名称:a在op输出的索引位置。向计算图添加第二个op(Const1,因为op名称要唯一),输入标量,输出Tensorb,其命名规则同上。向计算图添加第三个op(add),输入是Tensora和b,输出Tensortotal,其命名规则同上。二、一探tensor究竟2。1前后端Tensor映射 在TensorFlow的白皮书〔7〕中提到CAPI是连接前端用户代码和后端执行引擎的桥梁,为深入理解这个概念,建议读者参照TensorFlow官网从头编译源代码。TensorFlowv1。15。5基于Bazel进行编译,前端python与后端C通过SWIG进行交互。实际上在系统编译之前会先启动SWIG代码生成过程,通过解析tensorflow。i自动生成两个wrapper文件:pywraptensorflowinternal。py和pywraptensorflowinternal。cc,前者对接前端python调用,后者对接后端CAPI调用。大家安装tensorflow官方二进制包后,只能看到py文件而没有cc文件。如果自己编译TensorFlow源码,可在项目根目录下的bazelbin中找到相应的py和cc文件,如下图所示: 上图红框中的so文件是由cc文件编译得到,黄框中的py模块首次被导入时,会自动加载so动态链接库。而在so对应的cc文件中,静态注册了一个函数映射表,实现python函数到C函数的映射。此映射表结构大致如下:staticPyMethodDefSwigMethods〔〕{{(char)SWIGPyInstanceMethodNew,(PyCFunction)SWIGPyInstanceMethodNew,METHO,NULL},{(char)TFOKswigconstant,TFOKswigconstant,METHVARARGS,NULL},{(char)TFCANCELLEDswigconstant,TFCANCELLEDswigconstant,METHVARARGS,NULL},{(char)TFUNKNOWNswigconstant,TFUNKNOWNswigconstant,METHVARARGS,NULL},{(char)TFINVALIDARGUMENTswigconstant,TFINVALIDARGUMENTswigconstant,METHVARARGS,NULL},此处省略许多代码}; 如果没有亲身实践,上面这些文字读起来多少有些吃力。为便于大家理解,我们把上述文字用如下简图进行总结: 有些好奇宝宝可能会说:上面讲得太宏观,好像懂了,又好像没懂。没关系,接下来我们以静态图的运行接口session。run()为例,结合TensorFlow源码详细梳理一下前后端的映射过程,具体过程见下图: 由上图我们可清晰看到CAPI层把前后端给隔离开了,当然CAPI层包括pywraptensorflowinternal。hcc、tfsessionhelper。hcc、capi。hcc。至此session。run()从前端映射到后端的流程讲完了,那接下来回答前端tensor如何映射至后端Tensor,请看如下代码:tfsessionhelper。ccline351voidTFSessionRunwrapperhelper(TFSessionsession,constcharhandle,constTFBufferrunoptions,conststd::vectorTFOutputinputs,conststd::vectorPyObjectinputndarrays,conststd::vectorTFOutputoutputs,conststd::vectorTFOperationtargets,TFBufferrunmetadata,TFStatusoutstatus,std::vectorPyObjectpyoutputs){DCHECKEQ(inputs。size(),inputndarrays。size());DCHECK(pyoutputs!nullptr);DCHECK(pyoutputsempty());Statuss;ConvertinputndarrayPyObjectstoTFTensors。WemaintainacontinuousarrayofTFTensorsaswellasscopedcontainerstomakesuretheyrecleanedupproperly。省略了很多代码,可以看到此处把前端类ndarray的对象转化成了TFTensors。}capi。ccline2274voidTFSessionRun(TFSessionsession,constTFBufferrunoptions,constTFOutputinputs,TFTensorconstinputvalues,intninputs,constTFOutputoutputs,TFTensoroutputvalues,intnoutputs,constTFOperationconsttargetopers,intntargets,TFBufferrunmetadata,TFStatusstatus){TODO(josh11b,mrry):ChangeSessiontobeabletouseaGraphdirectly,insteadofrequiringustoserializetoaGraphDefandcallSession::Extend()。if(sessionextendbeforerun!ExtendSessionGraphHelper(session,status)){return;}TFRunSetup(noutputs,outputvalues,status);ConvertfromTFOutputandTFTensortoastringandTensor。看这里,此外TensorFlow把TFTensor转化成cTensorstd::vectorstd::pairstring,Tensorinputpairs(ninputs);if(!TFRunInputs(inputvalues,inputpairs,status))return;for(inti0;ininputs;i){inputpairs〔i〕。firstOutputName(inputs〔i〕);}ConvertfromTFOutputtostringnames。std::vectorstringoutputnames(noutputs);for(inti0;inoutputs;i){outputnames〔i〕OutputName(outputs〔i〕);}}2。2CTensor类 查看参考文献5,我们找到了CTensor类的定义,其重要片段(seg1)如下:classTensor{public:Tensor序列化反序列化相关,在2。3节详细介绍boolFromProto(constTensorProtoother)TFMUSTUSERESULT;voidAsProtoField(TensorProtoproto)const;voidAsProtoTensorContent(TensorProtoproto)const;Tensor实际为底层数据的一种视图,可用vec或matrix进行展示templatetypenameTtypenameTTypesT::Vecvec(){returntensorT,1();}templatetypenameTtypenameTTypesT::Matrixmatrix(){returntensorT,2();}templatetypenameT,sizetNDIMStypenameTTypesT,NDIMS::Tensortensor();private:TensorShapeshape;维护Tensor的形状和数据类型TensorBufferbuf;底层数据的指针} 我们先来分析下两个私有成员。首先看一下TensorBuffer类,它是一个继承引用计数类的虚拟类,不包含任何实现。通过查看参考文献6,我们得知BufferBase继承TensorBuffer类,且维护了一个内存分配器指针。而Buffer类继承BufferBase类,且维护了指向实际数据的指针data和元素数量elem。上述类的继承关系如下图所示(为便于理解图中给出成员定义,而非标准的UML图): 接下来我们分析TensorShape类。它也有自己的类继承体系,其核心逻辑定义在父类TensorShapeRep中,相关的类继承体系如下图: 为深入理解TensorShape的作用,以下结合TensorShapeRep的部分代码(seg2)进行分析:classTensorShapeRep{private:如下buf共计16字节表示TensorShape,其中前12字节用来存储形状(Rep16、Rep32、Rep64)第13字节作用不清楚,第14、15、16字节分别表示数据类型编号、张量的维度数目、张量维度的表示类型union{uint8buf〔16〕;Rep64unusedaligner;Forcedatatobealignedenoughforapointer。}u;public:理论上可定义任意维的张量,但1维、2维、3维张量最常见。所以给出如下三种维度表示方法(12字节)structRep16{uint16dims〔6〕;最多可表示6维的张量,每一维的长度不超过2161};structRep32{uint32dims〔3〕;最多可表示3维的张量,每一维的长度不超过2321};structRep64{gtl::InlinedVectorint64,4dims;支持任意维度的张量};} 本小节最后,我们再来看一下Tensor类定义中的vector()和matrix()。查看两个方法的实现,发现调用了共同的方法tensor(),而tensor()的返回类型为TTypesT,NDIMS::Tensor,而TTypes正是衔接TFTensor与Eigen库的关键。请看如下代码(seg3):tensorflow1。15。5ensorflowcoreframeworkensor。hclassTensor{public:Returnstheshapeofthetensor。constTensorShapeshape()const{returnshape;}templatetypenameTtypenameTTypesT::Vecvec(){returntensorT,1();}templatetypenameTtypenameTTypesT::Matrixmatrix(){returntensorT,2();}templatetypenameT,sizetNDIMStypenameTTypesT,NDIMS::Tensortensor();}tensorflow1。15。5ensorflowcoreframeworkensortypes。htemplatetypenameT,intNDIMS1,typenameIndexTypeEigen::DenseIndexstructTTypes{RankNDIMStensorofscalartypeT。typedefEigen::TensorMapEigen::TensorT,NDIMS,Eigen::RowMajor,IndexType,Eigen::AlignedTensor;省略了许多代码}tensorflow1。15。5ensorflowcoreframeworkensor。hTFTensor的shape()返回TensorShape。base()返回指向实际数据的指针。templatetypenameT,sizetNDIMStypenameTTypesT,NDIMS::TensorTensor::tensor(){CheckTypeAndIsAligned(DataTypeToEnumT::v());returntypenameTTypesT,NDIMS::Tensor(baseT(),shape()。AsEigenDSizesNDIMS());} 由上述代码可见,调用tensor()是把TFTensor转化成了TTypesT,NDIMS::Tensor,而后者本质上是Eigen::TensorMap。至此,我们搞清楚了TFTensor与Eigen库的关系,可以认为TFCTensor是对Eigen::TensorMap的一种封装。因为Eigen::TensorMap构造函数的参数来自于TFTensor中保存的信息(base()和shape()对应的信息)。2。3CTensor序列化 在TensorFlow的分布式训练环境中涉及大量的跨机通信,通信的内容就是序列化后的张量(通过sendrecvop对协同工作)。本小节我们将一起学习Tensor的序列化机制,以及Tensor与序列化对象的互编程。TensorFlow中Tensor对应的序列化对象叫TensorProto,它是由对应的proto文件生成。具体代码如下(seg4):tensorflow1。15。5ensorflowcoreframeworkensor。protosyntaxproto3;messageTensorProto{DataTypedtype1;TensorShapePrototensorshape2;int32versionnumber3;bytestensorcontent4;repeatedint32halfval13〔packedtrue〕;DTFLOAT。repeatedfloatfloatval5〔packedtrue〕;DTDOUBLE。repeateddoubledoubleval6〔packedtrue〕;DTINT32,DTINT16,DTINT8,DTUINT8。repeatedint32intval7〔packedtrue〕;DTSTRINGrepeatedbytesstringval8;DTCOMPLEX64。scomplexval(2i)andscomplexval(2i1)arerealandimaginarypartsofithsingleprecisioncomplex。repeatedfloatscomplexval9〔packedtrue〕;DTINT64repeatedint64int64val10〔packedtrue〕;DTBOOLrepeatedboolboolval11〔packedtrue〕;DTCOMPLEX128。dcomplexval(2i)anddcomplexval(2i1)arerealandimaginarypartsofithdoubleprecisioncomplex。repeateddoubledcomplexval12〔packedtrue〕;DTRESOURCErepeatedResourceHandleProtoresourcehandleval14;DTVARIANTrepeatedVariantTensorDataProtovariantval15;DTUINT32repeateduint32uint32val16〔packedtrue〕;DTUINT64repeateduint64uint64val17〔packedtrue〕;}; 大家可用protoc编译器来编译tensor。proto文件,结果生成tensor。pb。h和tensor。pb。cc两个文件,他们分别声明了TensorProto类定义、TensorProto成员方法的实现。我们可以粗略地将TensorProto看作Tensor的二进制对象,基于此它们相互之间的转换代码如下所示(seg5):Tensor的序列化过程autotensorprotonewTensorProto();Fillsinprotowiththistensorscontent。AsProtoField()fillsintherepeatedfieldforproto。dtype(),whileAsProtoTensorContent()encodesthecontentinproto。tensorcontent()inacompactform。tensorAsProtoField(tensorproto);tensorAsProtoTensorContent(tensorproto);Tensor的反序列化过程Tensortensor;tensor。FromProto(tensorproto);三、跨框架编程通用内存张量DLPack3。1什么是DLPack DLPack是一种开放的内存张量结构,用于在AI框架之间共享张量。多框架整合解决AI问题,能充分发挥各框架优势(一些运算在某框架中支持更好),并最终取得整体最佳性能。但这里有一个关键问题要解决:如何将内存中的张量从一个框架传递到另一个框架,而不发生任何数据拷贝?幸运的是,陈天奇团队给出了DLPack这个答案。 DLPack的设计理念是尽可能的轻量化,它不考虑内存分配、设备API,仅仅关注张量数据结构。它可以运行在多个硬件平台上,目前支持的框架有:NumPy、CuPy、PyTorch、Tensorflow、MXNet、TVM、mpi4py。DLPack的开发者不打算实现Tensor和Ops,而是将其用作跨框架重用张量和操作的公共桥梁。深入理解DLPack,要掌握两大模块:CAPI与PythonAPI。DLPackCAPI体系结构如下: 上图中深蓝色的结构体均定义在〔13〕中。DLTensor代表普通CTensor对象,但不负责内存管理。DLManagedTensor也是一个CTensor对象,负责DLTensor的内存管理,它被设计用来帮助其他框架借用此DLTensor。接下来,我们将目光转向DLPack的PythonAPI。 DLPackPython接口是Pythonarray的标准API。用DLPackPython接口进行数据交换的接口有两个: fromdlpack(x):输入一个包含dlpack方法的数组对象,用这个方法构建一个包含x数据域的新数组对象。 dlpack(self,streamNone)anddlpackdevice():在fromdlpack(x)内部调用x的这两个方法,分别用于获取x的数据域以及定位x数组对象在哪个设备上。 从语义层面理解yfromdlpack(x)的话,生成x的库叫生产者,包含fromdlpack()的库叫做消费者。其中生产者提供了访问x数据域的途径,通常来说生产者和消费者之间关于相应的数据是零拷贝的,也即y可视为x的视图。如果深入fromdlpack(x)内部,则x。dlpack方法生成包含DLManagedTensor的PyCapsule对象(或称capsule),这个对象只能被消费一次。生产者必须将PyCapsule对象名称设为dltensor,以方便按名称检索。同时也要设置DLManagedTensor的deleter方法给PyCapsuleDestructor,这个设置是当名为dltensor的capsule对象不再需要时使用。消费者把DLManagedTensor的所有权从capsule对象转移至自己,这是通过把capsule对象改名为useddltensor以确保PyCapsuleDestructor不会被调用来实现的。但当capsule对象把DLManagedTensor所有权转移至消费者对象时,消费者对象的destructor方法仍然可以调用DLManagedTensor的deleter方法。3。2TensorFlow中的dlpack 笔者发现TensorFlow对DLPack的支持是从v2。2。0开始的,更早的版本没有dlpack相应的库。TensorFlow的dlpack接口与3。1遵守相同的语义描述,相应的API测试语句如下:importtensorflowastfxtf。constant(5)xtf。Tensor:shape(),dtypeint32,numpy5rtf。experimental。dlpack。todlpack(x)print(r,type(r))capsuleobjectdltensorat0x7f55a0431c30classPyCapsulexothertf。experimental。dlpack。fromdlpack(r)xothertf。Tensor:shape(),dtypeint32,numpy53。3TVM与DLPack的关系 如果你想开发一款跨AI框架的深度学习编译器,DLPack就是一种可行的方案(TVM就是这条技术路线)。比如,我们在TVM中声明并编译一个矩阵乘法算子,然后基于DLPack表示构建一个包装器,该包装器能让此矩阵乘法算子支持PyTorchTensor。对MxNet可以采用类似的操作。DLPack提供在AI框架和TVM之间共享的中间包装器的原理如下图所示: 上述原理可以参考如下代码举例:前提说明:在PyTorch中计算矩阵乘法importtorchxtorch。rand(56,56)ytorch。rand(56,56)zx。mm(y)第一步,定义并构建一个TVM矩阵乘法算子ntvm。convert(56)Xtvm。placeholder((n,n),nameX)Ytvm。placeholder((n,n),nameY)ktvm。reduceaxis((0,n),namek)Ztvm。compute((n,n),lambdai,j:tvm。sum(X〔i,k〕Y〔k,j〕,axisk))stvm。createschedule(Z。op)fmmtvm。build(s,〔X,Y,Z〕,targethostllvm,namefmm)第二步,对TVM函数进行包装以支持PyTorchTensor,并验证结果fromtvm。contrib。dlpackimporttopytorchfuncfmmisthepreviouslybuiltTVMfunction(Pythonfunction)fmmisthewrappedTVMfunction(Pythonfunction)fmmpytorchtopytorchfunc(fmm)z2torch。empty(56,56)fmmpytorch(x,y,z2)np。testing。assertallclose(z。numpy(),z2。numpy())第三步,参照第二步对MxNet进行类似包装处理importmxnetfromtvm。contrib。mxnetimporttomxnetfuncctxmxnet。cpu(0)xmxnet。nd。uniform(shape(56,56),ctxctx)ymxnet。nd。uniform(shape(56,56),ctxctx)zmxnet。nd。empty(shape(56,56),ctxctx)ftvm。build(s,〔X,Y,Z〕,targethostllvm,namef)fmxnettomxnetfunc(f)fmxnet(x,y,z)np。testing。assertallclose(z。asnumpy(),x。asnumpy()。dot(y。asnumpy()))第四步,topytorchfunc()的详细定义TVM提供了dlpacktensor和TVMNDArray互转的函数。TVM函数在最底层调用的是TVMNDArray。此包装器的大致流程是:AITensordlpacktensorTVMNDArraycallTVMfunctiondefconvertfunc(tvmfunc,tensortype,todlpackfunc):assertcallable(tvmfunc)defwrapper(args):argstuple(ndarray。fromdlpack(todlpackfunc(arg))ifisinstance(arg,tensortype)elseargforarginargs)returntvmfunc(args)returnwrapperdeftopytorchfunc(tvmfunc):importtorchimporttorch。utils。dlpackreturnconvertfunc(tvmfunc,torch。Tensor,torch。utils。dlpack。todlpack)四、总结 本文内容较多且烧脑,建议读者反复阅读几遍,定能有所收获。我们在此对通篇内容作个总结,本文主要讲了三个主题: 第一部分讲解小白眼中的Tensor,重点分析了Tensor的属性和OP的关系。 第二部分讲解系统开发者眼中的Tensor,重点讲解Tensor前后端映射,以及Tensor的C定义及序列化。 第三部分讲解通用内存张量DLPack,重点讲解了DLPack的定义及在TensorFlow中的使用,以及DLPack在TVM中扮演的角色。 作者:李杰参考文献 1。TensorFlowIntroduction:https:github。comtensorflowdocsblobmastersiteenr1guidelowlevelintro。md 2。TensorFlowTensors:https:github。comtensorflowdocsblobmastersiteenr1guidetensors。md 3。tf。constant源码:https:github。comtensorflowtensorflowblobv1。15。5tensorflowpythonframeworkconstantop。pyL165 4。tensorflow源码解析之frameworktensor:https:www。cnblogs。comjicanghaip9537282。html 5。TensorFlowcTensorsourcecode:https:github。comtensorflowtensorflowblobv1。15。5tensorflowcoreframeworktensor。h 6。TensorFlowcTensorsourcecode:https:github。comtensorflowtensorflowblobv1。15。5tensorflowcoreframeworktensor。cc 7。《TensorFlow:ASystemforLargeScaleMachineLearning》:https:www。usenix。orgsystemfilesconferenceosdi16osdi16abadi。pdf 8。tensorflowinternals。pdf:https:github。comhoranceliutensorflowinternals 9。DLPackdoc:https:dmlc。github。iodlpacklatest 10。DLPackgithub:https:github。comdmlcdlpack 11。DLPackCAPI:https:dmlc。github。iodlpacklatestcapi。html 12。PythonSpecificationforDLPack:https:dmlc。github。iodlpacklatestpythonspec。html 13。dlpack。h:https:github。comdmlcdlpackblobmainincludedlpackdlpack。h 14。BuildingaCrossFrameworkDeepLearningCompilerviaDLPack:https:tvm。apache。org20180810DLPackBridge