从我开发过的Tensorflow、飞桨、无量框架看深度学习这几年

点击下面卡片关注我呀,每天给你送来AI技术干货!

文:Peter潘欣@知乎

排版:夕小瑶的卖萌屋

知乎:https://zhuanlan.zhihu.com/p/363271864

和深度学习框架打交道已有多年时间。从Google的TensorFlow, 到百度的PaddlePaddle,再到现在腾讯的无量。很庆幸在AI技术爆发的这些年横跨中美几家公司,站在一个比较好的视角看着世界发生巨大的变化。在这些经历中,视角在不断切换,从最早的算法研究,到后来的框架开发,到机器学习平台和更多基础架构,每一段都有不同的感受和更深的领悟。

清明节这几天有些时间写了这篇文章,从我的视角,用几个深度学习框架串起来这些年历史上的一些有趣的插曲,和技术背后的一些故事,免得宝贵的记忆随着时间在脑中淡去。

Moonshots

Google Brain每年会组织一次Moonshots提案,许多后来比较成功的项目都是这样孵化出来的,比如AutoML,Neural Machine Translation等等。团队成员会提出一些当时技术比较难达到的项目,大家组成类似兴趣小组的形式投入到这些项目中。

动态图

快速成长的时间总是过得很快,Megan加入Brain后,我被安排向她汇 ,当时的RSWE团队已经有十几人,而Google Brain也从几十人变成了几百人。

2017年初,经Megan介绍,TensorFlow团队一位资深专家Yuan Yu找到我,问有没有关注Pytorch,约我调研后一块聊聊。于是我就去 上搜集了一下Pytorch的资料,又试用了一下。作为一个TensorFlow的深度用户,我的第一反应就是Pytorch解决了TensorFlow很大的痛点,用起来非常的“自然”。

和Yuan聊完后,我们快速的决定在TensorFlow上也尝试支持类似Pytorch的imperative programming用法。Demo的开发过程还算比较顺利,我大概花了一个多月的时间。记得当时我把项目命名为iTensorFlow, short for imperative TensorFlow。(后来被改名成eager,感觉好奇怪)。

Demo的设计思路其实也不复杂:1. TensorFlow graph可以被切分成任意粒度的Subgraph,可以通过函数调用的语法直接执行,2. TensorFlow对用户透明的记录执行过程以用于反向梯度计算。给用户的感觉就就类似Python native的运行。

进而产生几个推导:1. 当Subgraph的粒度是operator时,基本等价于Pytorch。2. 当Subgraph粒度由多个operator组成时,保留了graph-level optimization的能力,可以编译优化。

最后再埋个伏笔:1. tf.Estimator可以自动的去融合Subgraph,形成更大的Subgraph。用户在开发阶段基于imperative operator-level Subgraph可以简单的调试。用户在部署阶段,可以自动融合大的Subgraph,形成更大的optimization space。

做完之后,我非常兴奋的和Yuan演示成果。Yuan也说要帮我在TensorFlow里面推这个方案。当时Pytorch的成长速度非常的快,TensorFlow的Director也召集了多名专家级的工程师同时进行方案的探索。当时我还没能进入TensorFlow的决策层,最终得到的结论是1. 让我们成立一个虚拟组专门做这个项目。2. 之前的Demo全部推倒重新做,TensorFlow 2.0作为最重要Feature 发布,默认使用Imperative Mode (后改名叫Eager Mode,中文常常叫动态图)。我则作为团队的一员在项目中贡献来一些代码。

后面我逐渐转到了TensorFlow做开发。记得2017年还发生了一件印象深刻的事情,当TensorFlow收获海量用户时, 上一篇“TensorFlow Sucks”火了。虽然那篇文章很多观点我不能苟同,许多想法比较肤浅。但是,有一点不能否认,TensorFlow API是比较让人蛋疼的。1. 同一个功能往往几套重复的API支持。2. API经常变动,而且经常发生不向后兼容的问题。3. API的易用性不高。

为什么会发生这个问题呢能要从Google这个公司的工程师文化说起。Google是非常鼓励自由创新和跨团队贡献的。经常会有人给另一个团队贡献代码,并以此作为有影响力的论据参与晋升。所以在早期TensorFlow还不是特别完善的时候,经常有外部的团队给TensorFlow贡献代码,其中就包含了API。另外,在Google内部的统一代码仓库下,放出去的API是可以很容易的升级修改的,很多时候只需要grep和replace一下就行。但是github上放出去的API完全不一样,Google的员工不能去修改百度,阿里,腾讯内部的TensorFlow使用代码。对此TensorFlow团队早期的确没有非常有效的方案,后来才出现了API Committee对public API做统一的把关和规划。

在我做视觉的时候,和Google内部一个视觉团队有过很多合作,其中一个是slim API。这个视觉团队非常的强,当年还拿了CoCo的冠军。随着他们模型的推广流行,他们的tf.slim API也被广为流传。slim API的arg_scope使用了python context manager的特性。熟悉早期TensorFlow的人知道还有tf.variable_scope, tf.name_scope, tf.op_name_scope等等。with xxx_scope一层套一层,复杂的时候代码几乎没有什么可读性。另外就是各种global collection,什么global variable, trainable variable, local variable。这在传统的编程语言课里,全局变量这种东西可能是拿来当反面教材的。然而,算法人员的视角是不一样的,with xxx_scope和global collection能减少他们的代码量。虽然我们知道合理的程序设计方法也可以做到,但是算法专家估计需要把时间用来读paper,不太愿意研究这些程序设计的问题。

记得在早期内部还有两个流派的争论:面向对象和面向过程的API设计。

在TensorFlow动态图能力开发的早期,我们也反复讨论了2.0里面接口的设计方案。作为炮灰的我又接下了写Demo的工作。

闭关两周后,我给出了一个方案:1. 复用Keras的Layer接口。2. 但是不复用Keras的Network,Topology等其他更高层的复杂接口。

原因主要又两点:1. Layer是非常简洁优雅的,Layer可以套Layer,整个 络就是一个大Layer。Layer抽象成construction和execution两阶段也非常自然。2. Keras有很多历史上为了极简设计的高层接口。我个人经验觉得很难满足用户灵活的需求,并不需要官方提供。而且这样可能会导致TensorFlow API层过度复杂。

Paddle其实诞生时间比较早,据说是大约13~14年的时候徐伟老师的作品。后来据说Andrew Ng觉得Paddle叫一次不过瘾,就改名成了PaddlePaddle。Paddle和那个年代的框架Caffe有类似的问题,灵活性不够。很多地方用C++写成比较粗粒度的Layer,无法通过Python等简单的编程语言完成模型的快速构造。

后来17年下半年,团队开始完全从新写一个框架,但是继承了Paddle的名字。2017年底的时候,Paddle国内的团队找到了我,邀请我担任Paddle国内研发团队的负责人。抱着打造国产第一框架的理想,我接受了邀请,一个月后就在北京入职了。

早期设计

加入团队的时候,新的Paddle还是一个比较早期的原型系统,里面有一些设计已经被开发了出来。我发现其中有些设计理念和TensorFlow有明显的差异,但是实现的时候却又模仿了TensorFlow。

仿编程语言

设计者希望设计一种编程语言来完成深度学习模型的构建(有点类似Julia等把深度学习模型的特性嵌入到了编程语言中)。然而在实现上,我发现其实和TensorFlow比较类似。都是通过Python去声明一个静态模型结构,然后把模型结构交给执行器进行解释执行。并没有发明一种新的深度学习编程语言。

这块我基本没有对设计进行调整。本质上和TensorFlow早期静态图的没有区别。但是在细节上,TF基于Graph的模型可以通过feed/fetch选择性的执行任意一部分子图,更加灵活。Paddle中与Graph对应的是Program。Program就像正常程序一样,只能从头到尾完整的执行,无法选择性的执行。因此Paddle在这块相对简化了一些,但是可以通过在Python层构造多个Program的方式补全这部分灵活性的缺失,总体来说表达能力是足够的。

Transpiler

Transpiler是对Program进行直接改写,进而可以让模型能够被分布式运行,或者进行优化。初衷是比较好的,可以降低算法人员的使用难度。然而在实现上,最开始是在Python层直接对Program结构进行改写。后来我从新设计了IR+Pass的Compiler体系,通过一种更系统性的方式做了实现。

LoDTensor

可能是因为团队的NLP和搜索背景比较强,对于变长序列的重视程度很高。Paddle的底层数据是LoDTensor,而不是类似其他框架Tensor。LoDTensor相当于把变长序列信息耦合进了Tensor里面。这可能导致比较多的问题,比如很多Operator是完全序列无关的,根本无法处理序列信息在输入Tensor和输出Tensor的关系,进而比较随机的处理,给框架的健壮性埋下隐患。虽然我一直想推动序列信息和Tensor的解耦合,但是因为种种原因,没有彻底的完成这个重构的目标,希望后面能改掉。

性能

18年初的时候,Paddle还是个原型系统。由于OKR目标,团队已经开始初步接入一些业务场景。其实一个比较大的痛点就是性能太差。单机单卡速度非常慢,单机4卡加速比只有1.x。但是性能问题的定位却非常困难。我花了些时间写了些profile的工具,比如timeline。一些明显的性能问题可以被快速的定位出来并修复。

但是单机多卡的速度还是非常慢,timeline分析后发现其中有个ParallelOp,存在大量的Barrier。最后改写成了ParallelExecutor,把Program复制了N份部署在多张卡上,在其中插入AllReduce通信算子,然后这N倍的算子基于图依赖关系,不断把ready的算子扔进线程池执行。即使这样,我们也发现在多卡的性能上,不同模型需要使用不同的线程调度策略来达到最优。很难有一种完美的one-fits-all的方案。后面我们再聊如何通过IR+Pass的方法插件化的支持不同的算子调度策略。

分布式的训练也碰到不少的问题。一开始使用grpc,花了挺大的功夫做并行请求,然后又切成了brpc,在RDMA等方面做了不少的优化。分布式训练的性能逐步得到了提升。另外为了做到自动化分布式部署,前面提到的Transpiler随着场景的增加,Python代码也变得越来越复杂。

Imtermediate Representation&Pass

Imtermediate Representation+Pass的模式主要是从LLVM的架构上借鉴来的。在编译器上主要是用来解决把M个编程语言中任意一个编译到N个硬件设备中任意一个执行的问题。简单的解决方案是为每个编程语言和硬件单独写一个编译器。这需要M*N个编译器。显然这对于复杂的编译器开发来说,是非常高成本的。

Intermediate Representation是架构设计中抽象能力的典型体现。不同编程语言的层次不一样,或者仅仅是单纯的支持的功能有些差异。但是,这些编程语言终归需要在某种硬件指令集上执行。所以在编译的过程中,他们会在某个抽象层次上形成共性的表达。而IR+Pass的方法很好的利用了这一点。其基本思想是通过多层Pass (编译改写过程),逐渐的把不同语言的表达方式在某个层次上改写成统一的IR的表达方式。在这个过程中,表达方式逐渐接近底层的硬件。而IR和Pass可以很好的被复用,极大的降低了研发的成本。

深度学习框架也有着非常类似的需求。

  1. 用户希望通过高层语言描述模型的执行逻辑,甚至是仅仅声明模型的结构,而不去关心模型如何在硬件上完成训练或者推理。

  2. 深度学习框架需要解决模型在多种硬件上高效执行的问题,其中包括协同多个CPU、GPU、甚至大规模分布式集群进行工作的问题。也包括优化内存、显存开销、提高执行速度的问题。

更具体的。前文说到需要能够自动的将用户声明的模型Program自动的在多张显卡上并行计算、需要将Program拆分到多个机器上进行分布式计算、还需要修改执行图来进行算子融合和显存优化。

Paddle在一开始零散的开展了上面描述的工作,在分布式、多卡并行、推理加速、甚至是模型的压缩量化上各自进行模型的改写。这个过程非常容易产生重复性的工作,也很难统一设计模式,让团队不同的研发快速理解这些代码。

意思到这些问题后,我写了一个Single Static Assignment(SSA)的Graph,然后把Program通过第一个基础Pass改写成了SSA Graph。然后又写了第二个Pass把SSA Graph改写成了可以多卡并行的SSA Graph。

后面的事情就应该可以以此类推了。比如推理加速可以在这个基础上实现OpFusionPass, InferenceMemoryOptimizationPass, PruningPass等等,进而达到执行时推理加速的目的。分布式训练时则可以有DistributedTransPass。量化压缩则可以有ConvertToInt8Pass等等。这一套东西基本解决了上层Program声明到底层执行器的Compiler问题。

这个过程中的确碰到了不少的阻力。比如分布式早期通过Python完成了这个逻辑,需要迁移到C++层。压缩量化的研发更喜欢写Python,而IR&Pass是基于C++的。不同Pass间顺序依赖和Debug等。

全套深度学习框架工具

TensorFlow Everywhere原本是TensorFlow团队时的一个口 ,意思是TensorFlow需要支持深度学习模型在任意的场景下运行,进而达到AI Everywhere的目标。可以说深度学习框架希望成为AI的“操作系统”,就像鱼离不开水、App离不开iOS/Android一样。

Paddle作为全面对标TensorFlow的国产深度学习框架,自然也希望提供全套的解决方案。在早期的时候,Paddle和公司其他团队合作了PaddleMobile,提供了移动端的推理能力。后来又开展了Paddle.js,支持在H5、Web等场景的推理能力。为了在toB,在Linux的基础上又新增了Windows的支持。为了支持无人车等设备、又支持了在更多不同设备上运行。

举个PaddleMobile的例子。深度学习框架想再移动设备上部署面临这比较多的挑战。手机的空间和算力都比服务器小很多,而模型最开始在服务器训练好后体积相对较大,需要从很多角度下手。1. 使用较小的模型结构。2. 通过量化,压缩等手段削减模型体积。

另外移动段深度学习框架是通常基于ARM CPU,GPU则有Mali GPU, adreno GPU等等。为了最求比较极致性能,常常需要使用汇编语言。有个同学写到后面几乎怀疑人生,感觉自己大学学的东西不太对。为了不显著增加APP的体积,框架编译后的体积需要在KB~几MB的级别,因此需要基于部署的模型结构本身用到的算子进行选择性编译。极端的时候甚至需要是通过C++ Code Gen的方法直接生成前向计算必须的代码,而不是通过一个通用的解释器。

回顾

随着项目的复杂化,很多棘手的问题逐渐从深度学习的领域技术问题转变成了软件工程开发和团队管理分工的问题。随着团队的不断变化,自己有时候是作为一个leader的角色在处理问题,有的时候又是以一个independent contributor的角色在参与讨论。很庆幸自己经历过这么一段,有些问题在亲身经历后才能想得明白,想得开。时代有时候会把你推向风口浪尖,让你带船队扬帆起航,在更多的时候是在不断的妥协与摸索中寻找前进的方向。

调整

19年中这个项目时大概有2~3人。团队希望开发一个新的版本,基于TensorFlow进行扩展加强,使得无量可以复用TensorFlow已有的能力,并且能够支持推荐场景下的特殊需求。无量一开始采用的是基于参数服务器的架构。TensorFlow被复用来提供Python API以及完成基础算子的执行。而参数服务器,分布式通信等方面则是自己开发,没有复用TensorFlow。

这个选择在团队当时的情况下是比较合理的。如果选择另一种方向,基于TensorFlow底层进行改造,研发难度会比较大,而且很可能与 区版TensorFlow走向不同的方向,进而导致TensorFlow版本难以升级。而把TensorFlow作为一个本地执行的lib则可以在外围开发,不需要了解TensorFlow内部的复杂逻辑,也可以复用一些其他开源组件,比如pslib。

早期在软件开发的流程上相对比较欠缺。为了保障工程的推进,我先帮忙做了些基础工作,比如加上了第一个自动化测试和持续集成,对一些过度封装和奇怪的代码做了重构和简化。

另外,在接口层也做了一些调整。原来框架开始执行后就进入C++执行器,无法从python层提供或者返回任何执行结果,也无法在python层执行逻辑进行插件化的扩展。为了满足预期用户将来需要进行调试的需求,我模拟tf.Session和tf.Estimator对执行层的接口做了重构。这样用户可以通过feed/fetch的方式单步调试执行的过程。也可以通过Hook的方式在执行前后扩展任意的逻辑,提高框架的适用场景。

另外一个问题是python层基本完全是全局变量,很难进行多模型的封装。像TensorFlow有Graph实例或者Paddle有Program实例。因为python层需要重构的量比较大,我暂时先加入了Context的封装,勉强将各种状态和配置封装在了Context下。考虑到短期可能不会有更复杂的需求,暂时没有把这件事做完。

reader那块也做了一些重构。最开始那块的线程模型异常复杂,一部分是因为分布式文件系统等基础设施无法提供比较好的SDK,导致许多逻辑不得不在深度学习框架里面,比如文件的本地缓存。考虑到特征加工的逻辑比较复杂,以及一些老的TensorFlow用户可能习惯于tf.Example和tf.feature_column等基础算子库,我在reader层引入了基于TensorFlow的tf.dataset。不过后来发现用户似乎更关心性能问题,喜欢自定义C++ lib的方式来解决特征处理的问题。

API设计是个老大难的问题。TensorFlow,Paddle,无量都没能幸免。在一个多人协同的团队里,每个研发更多还是关注每个独立功能是否完成开发,而功能的接口往往需要考虑到整体的API设计风格,易用性,兼容性等许多因素,常常在高速迭代的过程中被忽略掉。不幸的是API常常不能像内部实现一样后期优化。当API被放给用户使用后,后续的修改往往会破坏用户代码的正确性。很多时候只能自己评审一下。

升级

投稿或交流学习,备注:昵称-学校(公司)-方向,进入DL&NLP交流群。

方向有很多:机器学习、深度学习,python,情感分析、意见挖掘、句法分析、机器翻译、人机对话、知识图谱、语音识别等。

从我开发过的Tensorflow、飞桨、无量框架看深度学习这几年

记得备注呦

点击上面卡片,关注我呀,每天推送AI技术干货~

声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2021年3月9日
下一篇 2021年3月9日

相关推荐