第 381 期·中文精读

克里斯·拉特纳:编程与 AI 的未来

Chris Lattner: Future of Programming and AI

LLVM 与 Swift 的创造者谈编程语言的演进、编译器技术,以及 AI 将如何改变写代码的方式。

官方音频00:00 / --:--
中文精读由 AI 基于原始访谈改写,非官方翻译,可能存在偏差或错误;表述以原音频与官方文稿为准。说明

Mojo 官方给出的性能数字是:某些场景下比 Python 快 35000 倍。

说这个数字的人是克里斯·拉特纳(Chris Lattner)。他造过 LLVM 和 Clang,发明了 Swift,在 Google 参与过 TensorFlow 和 TPU,在特斯拉当过 Autopilot 软件副总裁。现在他联合创办了 Modular,一家做 AI 基础设施的公司,Mojo 是这家公司的编程语言,一门 Python 的超集。

这是他第三次上 Lex Fridman 的播客。两年不见,他手里多了一门新语言。

一门语言的文件扩展名,可以是一个火苗

Mojo 还有个设计,比 35000 倍更能说明这个人的审美:源文件的后缀名可以是一个 emoji,比如 🔥。

他半开玩笑地说,这大概是这门语言最重要的决定。理由倒不全是玩笑:文件扩展名本质上是元数据,既然是元数据,为什么要让它占掉屏幕上一块地方,还要和一堆 txt、pdf 挤在一起,长得灰扑扑的。想酷一点,就得在视觉上跳出来。

实际用起来有一点小麻烦。Git 不认这个,它把火苗当成不可打印字符,在命令行里吐出一串奇怪的十六进制。GitHub、VS Code、Windows 资源管理器都没问题,因为它们早就为国际化路径做过准备,emoji 只是再往前迈一步。

拉特纳对这事的态度是:Git 可以被修好。他把整个反应看成一条光谱,一端是“哦这个挺酷”,另一端是“你在杀我的孩子”。他判断世界会走到前面那一端,而且不必赌上全部家当,两条路可以同时铺。

这个细节值得多看一眼。一个人愿意在文件扩展名这种毫秒级的体验上花心思,说明他做语言不是在做规范,是在做产品。

两头都在动,中间就成了一团乱麻

要理解 Mojo 为什么存在,得先看 Modular 想解决的问题。

拉特纳的观察是,TensorFlow 和 PyTorch 这些框架,设计于七八年前。那时候大语言模型还不存在。今天人们拿它们做的事情,和它们被造出来时的目标已经差了一大截。这是软件这一头的变化。

硬件那一头的变化更剧烈。新芯片、新加速器、各种 ASIC,几乎每家像样的公司都在发布自己的芯片,TPU、GPU、NPU、IPU,各种 P 打头的处理器。拉特纳自己在 Google 做过 TPU,那是人类部署过最大规模的 AI 计算系统之一。

一边是移动的系统,另一边也是移动的系统,中间就变成了一团巨大的乱麻。(克里斯·拉特纳)

乱麻的代价落在使用者身上。搞软件的人面对一堆自己用不上的硬件,理论上能编程和实际上能把全世界聪明人的创造力释放出来,是两件完全不同的事。

Modular 想做的就是中间那层软件栈,让研究者把想法送进生产环境的路径短一点。Mojo 是这层栈里最关键的一块砖。

Python 到底好在哪

拉特纳在节目里反问 Lex:你喜欢 Python 什么?

Lex 的回答很感性:直观,写起来像写英文,读别人的代码也比读 C++ 快,同样的意思用更少的字。还有生态,想干什么都有现成的包。

拉特纳接着往下挖了一层。他认为机器学习确实帮了 Python 大忙,TensorFlow 和 PyTorch 拥抱 Python,直接把 Python 推到了今天的位置。但真正让它长成这样的,是它扮演的角色。

Python 是那个通用连接器。它能把很多不同的系统拼在一起,让你组合出更大的东西,而不必先搞懂每个零件内部怎么转。(克里斯·拉特纳)

这个定位挺准确。Python 更像一块乐高底板,形状各异的积木都能插上去,虽然每块积木本身不是它造的。生态和易用性互相喂养,滚成一个正向循环,一旦滚起来就很难被替代。

那问题在哪?

慢。

35000 倍这个数字,一层层拆开

这个倍数听着像营销话术,其实拆开看,每一层都有具体的技术来源,加起来还真能到这个量级。

第一层,把解释器拿掉。大家平时敲 python3 跑的那个叫 CPython,是个解释器。代码要先变成字节码,再被读、被解析、被解释执行,这中间多了一整层。改用编译器,光是消掉这一层,根据代码不同就是 2 倍到 5 倍、10 倍的提升。

第二层,把数据从盒子里拿出来。CPython 有个设计决定:一切皆对象,为了让所有对象的内存表示统一,每个对象都带一个头,后面跟载荷。这意味着你传递一个整数,传的其实是一个指向数据的指针。每次分配要记账,每次引用计数要加减,现代 CPU 最讨厌的就是这种追着指针满内存跑的行为,因为缓存会被打穿。把这层间接去掉,让数字直接躺在寄存器里,又是一个 10 倍。两层乘起来,量级已经很可观。

第三层,用上向量。现代 CPU 一条指令能同时处理 4 个、8 个、16 个甚至 32 个数据。Python 不暴露这个能力。Mojo 暴露了。

第四层,用上多线程。这里牵扯到 GIL。很多人抱怨 Python 的全局解释器锁让并行变得半残,拉特纳指出,GIL 和引用计数是死死绑在一起的,你没法只扔一个留一个。

第五层,控制内存层次。数据放在哪一级缓存、怎么布局,直接决定跑多快。这些在 Python 里根本没法表达。

这几层叠起来,就是那个吓人的数字。Mojo 给这些能力的语法还相当省事,比如你想让一段循环并行,写一句 parallelize 就行。

性能的意义不止是快。拉特纳给了两个理由,第二个更值得琢磨。

对企业和环境来说,性能等于成本和能耗。他开了个玩笑:你可以说 Python 对环境不好,换到 Mojo 至少立刻好上十倍。

对产品来说,性能等于体验。模型延迟降下来,产品团队就敢把模型做大;模型做大,用户感受到的东西就更好。这条因果链在机器学习行业里反复出现,很多时候模型能力的上限不取决于算法,取决于推理成本。

把解释器搬进编译器

Mojo 最有意思的设计在这里。

Python 的表达力很大一部分来自动态特性。你定义一个类,重载它的 __add__,加号就能用在这个类上。PyTorch 的自动微分、eager 模式,底下全是这类运行期的魔法。

问题是这些魔法在 GPU 上跑不了,就算能跑,性能也不划算。

Mojo 的做法是:既然 Python 内部本来就有一个解释器,那就让这个解释器在编译期运行。于是你得到编译期元编程。API 写法一点不变,那些动态特性被整体抬到编译期执行,生成的代码再去跑在加速器上。

他拿 Lisp 做类比。Lisp 最强大的地方在于,宏和普通代码是同一套东西,同一套语法、同一套概念。这让它简单、一致、可组合,你写的库在运行期和编译期都能用。

C++ 走的是反方向。模板系统跟运行期编程是两个平行宇宙,语法不同,概念不同,调试方式也不同。

它是意外变得强大的。它并不是被设计成图灵完备的,那是后来一路上被发现的事情。(克里斯·拉特纳)

他说自己写过足够多的 C++,有资格抱怨几句。这话不假,一个维护过 Clang 的人对 C++ 的感情大概最复杂。

他提到 Zig 也在这个方向上探索,把运行期能做的事挪到编译期。这条路这几年确实在聚拢共识。

技术上难在哪?Mojo 底下是一套全新的编译器设计,建在 MLIR 之上,还塞进了缓存、解释器、JIT。也就是说,编译器内部装了一个解释器,这个解释器现在还得把代码拿到目标机器上真跑一遍,看哪个版本最快,再缝回编译流程里继续。

编译过程本身变成了机器相关的搜索。这在传统编译器里是很少见的形态。

让人去记 TPU 的 tile size 是 128,本来就不合理

自动调优(auto-tuning)是 Mojo 另一个被低估的特性。

先看现实。硬件专家脑子里装着一堆魔数:缓存多大、寄存器多少个、向量多长最划算、GPU 有多少个 SM、warp size 是多少、TPU 的 tile 是 128。这些数字确实重要,写对了代码能快好几倍。

我的看法是,大多数普通程序员其实不想知道这些东西。(克里斯·拉特纳)

而且这些魔数还在变。A100、H100、Volta,算法是同一套,参数各不相同。再往远处看,你的代码半年前写在笔记本上,今天要搬到服务器上跑,明天可能换了张显卡。每次都要重调一遍,谁受得了。

自动调优的思路很朴素:既然外面有的是算力,何必让人去猜。你把算法结构写清楚,比如切成块、并行处理、按 128 或者别的粒度切,然后告诉编译器“这些方案你挑一个”,编译器在真实硬件上跑一遍,谁快用谁,结果缓存下来,下次直接用。

一步到位,写完就不用管了。换机器的时候它自己重新试。

这套机制解决的其实是个老问题:可移植性和极致性能长期矛盾。以前的做法是 #ifdef 一层套一层,代码越来越难看,测试的组合数爆炸。自动调优把这件事从“人手写分支”变成“机器做实验”,本质上是用算力换人的注意力。

缩进这件事,他愿意吵

中间有一段岔得很远,但挺有意思。

Lex 提到自己从 Emacs 换到 VS Code,顺带说了句考虑过 Vim 但没选,结果收到一堆 hate mail。拉特纳接话说,缩进就是比花括号好,而且他能给出理性论证。

他的论证有三条。Python 有几百万人用,占住了机器学习,这是事实层面的理由。花括号语言最后也要跑格式化工具,跑完的结果是缩进加花括号两套并存,既然要分组,为什么留两套。还有那些“只有一条语句时可以省略花括号”的设计空间,Python 式的缩进根本不需要面对。

Lex 想看数据:因为缩进写错导致的 bug,Python 里多还是 C++ 里多。两个人都觉得大概率不多,因为编辑器会替你缩进,你甚至不需要跟它较劲。

拉特纳承认这是宗教问题,但他乐于捅一下熊。捅完还是回到正题:

我们不想破坏 Python,也不想改它,或者打引号地“修复”它。我们爱 Python 本来的样子。我们的看法是,Python 只是还没写完。(克里斯·拉特纳)

这句话是理解 Mojo 定位的钥匙。它不打算取代 Python,它打算接着写。

类型是可选项,不是教条

Python 有类型,字符串、整数、字典,但全都活在运行期。所以你写函数不用标类型,代价是所有东西必须是同一种表示:指向带对象头的对象的指针。方法分发永远走同一条路径,不管接收者实际是什么。

Mojo 允许你渐进式地加类型。这个整数是 32 位还是 64 位,这个浮点是双精度还是单精度,编译器拿到这些信息就能做更好的优化,代码补全也更准,因为它知道这个对象上有哪些操作。

整套设计的重心落在“渐进”两个字上。你可以在任何地方加任意多的类型,不想加就不加,Python 那套动态语义照样完整保留。

拉特纳的立场是:类型不是对的东西,也不是错的东西,它是有用的东西。这个说法比两边阵营的吵架都更接近工程现实。

Mojo 到底是什么

回到最基本的问题。它是编译型还是解释型?

答案是全都干。它可以是解释执行的,可以是 JIT 编译的,也可以是静态编译的。三种模式共存,因为不同场景的需求不一样:试验阶段要快,部署阶段要极致。

它是不是只服务 AI?AI first,是的,AI 的需求在推着它往前走。但目标是通用语言。判断标准他给的很简单:只要某个地方出现“上面一层 Python 做 API、下面一层 C 和 C++ 做计算”这种结构,那就是 Mojo 的机会。

这个模式在今天的软件世界里到处都是。科学计算、数据处理、金融建模、图像处理,全是这个形状。用一门语言从头写到尾,把两层合成一层,收益不只是省掉一门语言的学习成本,还有调试、部署、团队协作上的简化。

至于那个火苗后缀,拉特纳说得轻描淡写:不必赌上全部家当,两条路可以同时铺。这话听着像在讲文件扩展名,其实也是在讲 Mojo 和 Python 的关系。

这场对话发生的时候,Mojo 还没开放。但从他描述的技术路径看,方向是清楚的:不要让使用者为了性能离开 Python,而是让 Python 长出自己的性能。

至于能不能成,得看接下来几年有多少人愿意把生产代码搬过去。

  • 去 mojolang.org 申请早期访问,找一段你熟悉的 Python 数值代码,原样跑一遍,再给热点函数加上类型标注跑一遍,自己看差距,别信二手数字。
  • 翻一遍你手上的项目,找出“Python 调 C++ 扩展”的地方。这些地方就是最值得试点的位置,收益最直接,风险最可控。
  • 如果暂时用不上 Mojo,也可以先借它的思路:把代码里那些手调的魔数(分块大小、向量长度、线程数)抽出来,写成可配置的参数,让程序自己在目标机器上试。哪怕用 Python 写脚本做这件事,也比凭经验拍脑袋强。
  • 想理解编译期元编程,别从 C++ 模板入手,先看 Lisp 的宏,理解“代码即数据”之后再看 Mojo 的设计会顺很多。
  • 把 GIL 和引用计数当成同一个问题看。这样你就能判断某个并发方案的宣传是真解决了问题,还是绕过去了。
  • 缩进还是花括号,别在这上面花时间吵架了。真的,两个人都说这是宗教问题,信哪个都不影响你写出好代码。

本集资源与出处