Python的语法非常灵活,并结合了其他语言的许多方便的特性。然而,Python的优点也暗示着它的缺点。蚂蚁研究员王毅在工业系统中对Python的亲身体验,让他对Python的局限性有了更深刻的认识,而Go+是弥补这一点的最可靠方案。那么Python有哪些缺点呢?Go+如何弥补?本文就Go+补足Python的局限性分享王毅的看法和尝试。前不久,徐世伟(江湖人称老徐)的Go+项目在HackerNews上掀起了波澜[1]。一见倾心,参与投稿。最近老徐和社区组织了一个视频交流,让我说说你为什么关注Go+,你想看什么。现场交流后,根据弹幕反馈和两位好友——洪明生(TenosrFlowRuntime负责人)和王宇(沉雕魔)的建议,进行了修改。我已经在分布式深度学习系统上工作了十三年,尤其是在2016年徐伟先生让我接替他担任他原来的PaddlePaddle项目的负责人之后。我个人在工业系统中使用Python的经验让我了解其局限性。深的。而Go+是我见过最靠谱的补偿方案。期待Go+对标Python,弥补Python的不足。在此基础上,有一个类似numpy的项目(姑且称之为numgo+)来支持张量运算,满足数据科学的需求;在numgo+之上,构建一个类似于PyTorch的深度学习基础库(姑且称之为GoTorch)。如果可能,它将进一步成为深度学习编译器生态系统的前端语言。我现在在蚂蚁集团工作,负责一个开源的SQL编译器SQLFlow——将扩展语法支持AI的SQL程序翻译成Python程序。同事说,如果Go+生态能够成熟,他们很乐意让SQLFlow输出Go+程序。很多读者可能觉得我在胡说八道——Python这么火的语言,为什么还要“补”?Python的优势Python的语法很灵活,并结合了许多其他语言的便利性。比如Python和C++一样,允许运算符重载,所以numpy的作者重载了算术运算符来做张量运算。和Lisp一样,Python的eval函数递归实现了Python解释器,可以解释和执行Python表达式,因此Python程序可以自己生成。这种灵活性让程序员可以为所欲为,所以特别适合探索性的工作。比如研究生用Python做科研;数据科学家以前用它来代替各种昂贵的商业系统;在后来诞生的深度学习领域,Python也迅速蓬勃发展。Python的局限性Python的优势也暗示着它的弱点。我个人感觉有两个痛点。很难保证代码质量的另一种说法是语法灵活:有多种方法可以编写程序。现代软件工程没有孤胆英雄,全靠大家的通力合作。多种可能的写法往往意味着团队在代码审查时很容易发生争吵——而且很难平静下来,因为可能不一定有客观的选择标准。其他很多语言也有类似的问题,比如Java。解决方案是在社区中定义一些设计模式。在编写程序之前,程序员应该检查是否有可以应用的设计模式。如果是这样,请跟随他们。因此,Java程序员除了学习Java语法外,还需要学习设计模式。C++也有类似的问题。其中一个解决方案是,谷歌设定了一套代码风格——哪些语法可以使用,哪些语法不允许——根据RobPike的解释,挑出一些允许的语法是Go设计的初衷。Python非常灵活,代码风格不能像C++那样详细定义——PEP8几乎只讲排版要求,对语法的选择几乎没有限制。Python也没有办法定义模式——要写的太多了。Python灵活地采用了动态类型,所以我们在看Python函数的时候,一定要仔细阅读它的代码,否则不知道它有没有返回值,不知道返回值是什么。Python也有语法扩展,要求程序员指定输入和输出的数据类型,但用的人不多——毕竟大家都以“灵活”为目标;如果灵活性有限,那么使用静态类型语言确实更好。结果就是每个Python函数都不能太长,否则看不懂。但是Python程序员是来求灵活的,他们要的是信马由缰的感觉。不管你看不懂,我自己都能看懂。不管怎样,论文发表后我就毕业了。split函数细化了粒度?不可能,这辈子都不可能。有没有写得很好的Python代码?一些。比如谷歌切线。这是一个非常小的项目。只有两个作者。它的代码结构清晰——每个功能基本都在十行代码以内,代码和注释一样长,很容易理解。但这也与很多Python用户的印象背道而驰。在我负责PaddlePaddle项目的时候,除了学习和总结Python模式,我还配置CI调用各种工具进行源码检查。但是,这些工具不够智能,无法自动注释代码,也不会自动拆分太长的函数定义。计算效率难以优化Python语法丰富,灵活性强,因此解释器编写起来非常复杂,而且性能难以优化。相比之下,Go语言语法简洁,表达能力远超C,但关键字总数却少于C,这种简洁使得Go程序的性能优化更容易。Go诞生几年后,Go编译器对代码的性能优化水平迅速逼近GCC对C++程序的优化水平,而C++和Python一样语法丰富,所以编译器中的代码性能优化功能是不容易开发。有些人试图写一个Python编译器而不是解释器,这样可以在程序执行之前进行性能优化。但是Python语法比C++更灵活,以至于几乎不可能编写出完全支持Python标准语法的编译器。因此放弃了几次尝试。目前普遍的做法是让解释器做执行时优化(JIT编译)。由于运行时信息,它比编译器更容易。在AI中,深度学习训练的计算成本很高。TensorFlow的图模式的解决方案是:用户编写的Python程序在执行时并不真正进行训练,而是将训练过程输出到一个名为“计算图”的数据结构中,交给“解释器”TenosrFlowruntime”来执行。只要保证TensorFlow运行时的执行效率,就不受Python解释器效率的限制。TensorFlow图模式用心良苦,画蛇添足——源程序、层层IR、二进制代码是人们一直以来用来描述计算过程的表达方式。TensorFlow项目早年发明的计算图屡屡造轮子,造就了非专业——图很难表达if-else、循环、函数定义和调用,更不用说闭包等高级控制流结构,协程和线程。人工智能工程师非专业的编译器设计让LLVM的作者ChrisLattener苦笑不已,于是他尝试用Swift代替Python作为TensorFlow的前端语言,用MLIR代替TensorFlow中的“计算图”[2]].试图弥补局限在我负责PaddlePaddle的时候,为了验证PaddleFluid的能力,我和同事陈曦做了一艘无人船,尝试用Fluid写了模仿学习的方法,这样,这艘船可以学习人类司机的驾驶技术。有关详细信息,请参阅博客系列[3]。但是如果我们带一台运行Python程序的MacBookPro上车,它会非常耗电,而且嵌入式设备不适合运行用Python编写的训练程序。如果每次停船都将数据上传到服务器进行训练,那么船向人类学习的迭代进度就太慢了。为此,当时另一位同事杨洋写了PaddleTape,用C++实现了PyTorch的自动微分能力。结合PaddleFluid积累的众多用C++编写的基础计算单元(算子),Tape是完全用C++实现的。深度学习系统与Python无关。2019年初,我的朋友洪明生在谷歌负责SwiftforTensorFlow项目,这也是AI基础设施去Python化的一次尝试。他拉着我与ChrisLattener的团队分享PaddleTape和无人船的故事,并修改了幻灯片[4]。我在蚂蚁集团负责的开源分布式深度学习训练系统ElasticDL,尝试过调用TensorFlow图模式、eager执行模式、PyTorch、SwiftforTensorFlow,深受SwiftforTensorFlow设计理念和与Python生态系统启发的共荣战略。Go+与数据科学以上尝试提醒我,语言的选择标准必须包括:清晰简洁的语法和稳定易学的语法。我也希望语言使用者是一个更有探索精神的群体。Go+及其基于Go社区的用户群符合要求。在Go+出现之前,也有使用Go进行数据科学的尝试,也有使用Go实现的张量计算库(如gonum),但没有使用numpy的Python程序简洁。一个最直接的原因就是Go的常量需要指定数据类型,而Python的则不需要。我写了几个比较[5]。要在Go中定义一个ndarray类型的常量,用户需要这样写:x:=numgo.NdArray([][]float64{{1.0,2.0,3.0},{1.0,2.0,3.0}})在Python中:x=numpy.ndarray([[1.0,2.0,3.0],[1.0,2.0,3.0]])Go+自动推导数据类型,写法和Python差不多:x:=numgo.NdArray([[1.0,2.0,3.0],[1.0,2.0,3.0]])更进一步,XuJia添加了一条评论,解释说Go+将支持MATLAB的张量定义语法。这样,程序就更简单了:x:=numgo.NdArray([1.0,2.0,3.0;1.0,2.0,3.0])类似方便的语法改进在Go+中积累了很多,例子在[6]中。这些语法扩展足以大大简化数据科学编程。Go+编译器负责将使用这些语法糖编写的Go+程序翻译成Go程序。这样,它就可以与其他Go语言编写的库一起编译,从而在Go生态系统中复用代码。重用Go生态系统是Go+语言的优势之一。在围棋的发展过程中,积累了很多科学计算的基础技术,比如围棋数据类型对张量的封装。这些数据类型的计算也有高效的Go实现,部分原因是Go程序可以轻松调用C/C++程序,包括LAPACK等科学计算领域经过验证的基础库,甚至NVIDIAGPU接口库CUDA。值得注意的是,这些基于C/C++的基础库也是Python数据科学生态的基础,所以本文的标题是Go+CompletingthePythonEcosystem。Go+和DeepLearningCompiler上面提到了深度学习技术。这是Python广泛使用的另一个领域,与数据科学有着天然的联系,比如PyTorch和TensorFlow的张量数据结构与numpy的ndarray相同。在深度学习领域,编译器是最新的主流研究方向。在Go社区中,目前有很多后台系统开发人员;直播过程中,有听众在弹幕中表示自己不是AI工程师,不关注AI。如果真这么想,恐怕不仅仅是技术理想问题,也是对自己工作的不负责任。后台系统和AI系统的界限越来越模糊,因为后台系统指的是互联网服务的后台系统;而整个互联网经济是建立在用不眠的服务器代替人服务大众的基础上的,而AI就是以这个逻辑为基础的,详见我的一篇旧文[7],里面统计了人类被淘汰的职业在过去的二十年中,通过人工智能技术。而这个界限在不久的将来会彻底消失,因为随着在线学习、强化学习、模仿学习、联邦学习技术取代监督学习成为互联网智能(包括传统的搜索、广告、推荐,以及新兴的无人驾驶和主流技术)金融智能),AI系统将不再分为训练和预测,AI工程师不再负责前者,后台工程师则负责后者。在AI领域,深度学习超越传统机器学习的一个重要原因是,传统机器的每个模型(可以理解为对知识结构的描述)往往对应一种或多种训练算法;而在深度学习中,几乎所有的模型都是用一种叫做随机梯度下降(SGD)的算法或其具有相似性的变体来训练的。这样,基础设施工程师负责培训系统的开发;模型研究人员重复使用,大大减轻了科研的工程负担,提高了模型开发的效率。深度学习系统的核心问题是autodiff,这是由SGD算法的数学特性决定的。SGD算法通过交替执行前向传播和反向传播,可以从训练数据中推导出模型的参数。模型加上参数就是知识。这里的工程挑战在于,模型研究人员在定义模型时,顺带描述了正向计算过程,而逆向计算过程是人类难以描述的。最好有一个程序,自动从正向计算过程中推导出反向计算过程。这种自动推导称为autodiff。autodiff目前有两种策略。第一种是在运行时派生的,也称为基于动态网络和磁带的方法。其基本思想是,无论前向计算过程多么复杂,即使包括if-else、循环、函数定义和调用,甚至协程、多线程,只要记录下顺序执行的基本操作(算子)即可在一个磁带中,那么逆向计算过程就是追溯这个磁带中的记录,依次调用每个算子对应的导数算子(梯度算子)。这是PyTorch、TensorFloweagerexecution和PaddleTape采用的策略。这种策略与编译器关系不大,但与JIT编译有一点关系。另一种策略是在运行前推导反向计算过程,为此需要引入autodiff的专用编译器。TensorFlow图形模式、Caffe/Caffe2、PaddleFluid、GoogleTangent、Julia、SwiftforTensorFlow都使用这种策略。编译器通常将源语言描述的源程序翻译成目标语言描述的目标程序。不过前三种技术比较偷懒,没有引入源语言,而是让用户通过调用Python库来描述前向计算过程。GoogleTangent、Julia和SwiftforTensorFlow允许用户分别在Python、Julia和Swift中定义函数,以描述正向计算过程,并将正向计算函数转化为反向计算函数。严格来说,Julia的作者已经实现了各种自动差异方案:运行时、编译时或两者的混合。修改这篇文章时,明生提醒我:对于不同的愿景,即使用相同的语言来实现内核和基于内核构造+执行程序/图形,请参见[8]。这里的内核是指深度学习中基本运算单元算子的实现。编译时和运行时autodiff策略同样适用于Go+,并不妨碍Go+复用现有技术。就像LAPACK的基础库应该在数据科学领域复用一样,基础算子和梯度算子也应该在深度学习领域复用。在运行时使用磁带实现autodiff的策略更易于实现。我记得杨洋用一周的时间开发了PaddleTape。编译策略要复杂得多。基于TensorFlow团队袁宇老师的工作[9],PaddleFluid20多人花了数月时间搞定了if-else、循环、函数定义和调用的autodiff。这些尝试提醒我们复用社区核心技术的重要性。比如用MLIR代替计算图可以描述更复杂的控制流——计算图肯定不能描述goroutine和select。使用TVM作为编译器的后端,并使用深度学习技术来学习如何优化深度学习程序。所有这些技术的输出都是对基本运算符的调用。从这个角度来看,之前深度学习技术生态中积累的算子类似于内置函数。这也是洪明生在修改本文时反复提醒的。希望在不久的将来,Go+可以作为一种新的深度学习前端语言,与Python、Julia、Swift一起,共同复用底层IR、编译器后端和基础算子。总结我理解未来Go+项目的核心战术工作是:在保持Go语法简洁性的基础上,合理承认简化语法——不要像Python和C++那样融入太多的灵活性,同时利用Go的极简语法在规范之上,适当更灵活。此外,通过社区合作开发numgo+、GoTorch等探索性项目,丰富技术生态是社区的战略方向。它甚至更进一步,成为深度学习编译器的前端语言,可以复用社区多年来积累的深度学习底层计算技术。最后感谢老徐和Go+核心贡献者柴树山和陈东坡,Go社区杰出贡献者AstaXie,以及我的同事ONNX社区核心贡献者张科的审稿。
