目录
- 从逻辑门到全加器 [00:00:00 - 00:10:00]
- MUX、寄存器文件与硬件成本 [00:10:00 - 00:20:00]
- 寄存器文件与本地化存储 [00:20:00 - 00:30:00]
- 时钟周期与流水线权衡 [00:30:00 - 00:40:00]
- FPGA、LUT 与 ASIC 成本 [00:40:00 - 00:50:00]
- CPU、GPU 与分支预测 [00:50:00 - 01:00:00]
- GPU、TPU 与数据搬运 [01:00:00 - 01:10:00]
- 从 SM 到可拆分 systolic array [01:10:00 - 01:20:00]
- 收尾:可拆分 systolic array [01:20:00 - 01:20:18]
从逻辑门到全加器
Dwarkesh Patel:我又请来了 Reiner Pope,他是 MADx 的 CEO,这是一家新的 AI 芯片公司。上一次我们聊的是数据中心里发生了什么,现在我终于搞清楚 AI 芯片内部到底在发生什么了。芯片究竟是怎么工作的?顺便声明一下,我是 MADx 的天使投资人。所以,希望你已经设计出一颗好芯片了。[00:00:18]
Reiner Pope:那我先从芯片设计里最基础的单元讲起,然后再一点点搭到一颗完整的量产芯片上去,看看它到底由什么组成。芯片最底层的原语是逻辑门,比如 AND、NOT 这种非常简单的东西。它们通过导线连接起来,而这些导线必须以金属走线的形式实际铺在芯片上。AI 芯片最主要要算的东西是矩阵乘法,而矩阵乘法里真正的基本原语,其实就是成对数字的乘加(multiply-accumulate)。所以我想先手工演示一下这个计算长什么样,再反推相应的电路会是什么样。最容易讲的办法,是先看一个 4 位数和另一个 4 位数怎么做乘加。真正最清晰的原语,其实就是乘加:先把这两个项相乘,再加上一个 8 位数。[00:01:36]
Dwarkesh Patel:我想先问个澄清问题。为什么这个原语对电脑里会发生的任何计算来说,都是最自然的基本单元?
Reiner Pope:有几个原因。它确实更高效,但对 AI 芯片来说之所以自然,是因为你看矩阵乘法本身,矩阵乘法其实就是对 i、j、k 的三重循环,输出 i,k 不断加上输入 i,j 乘以另一个输入 j,k。也就是说,乘加会出现在矩阵乘法的每一个步骤里。还有一个观察是,累加阶段的精度几乎总是要比乘法阶段更高。这一点也许是 AI 芯片特有的:你乘的是低精度数字,但一旦开始累加,误差会很快累积,所以这里需要更高的精度。这就是为什么我们选了 4 位乘法配 8 位加法。让我确认一下我是不是理解对了。这里有两种理解方式。[00:03:01]
Dwarkesh Patel:一种是结果会比输入更大;另一种是如果它是浮点数,那……也许这一点对我来说没那么直观,但本质上应该是同一个原理。
Reiner Pope:对,确实是同一个原理。我想补充的另一个角度是:当你在把这个数字一路加上去时,其实是在加很多很多项,所以会不断累积舍入误差;而在这里的乘法链里,只有一次乘法,所以乘法本身不会累积那么多舍入误差。你问为什么要加那么多项?其实不只是两个数字,对吧?我的意思是,求和这个过程会反复发生很多次。
Dwarkesh Patel:所以误差会累积。明白了。那如果我们手算这个计算过程,要怎么做?我的意思是,作为人,我们大概会把它拆成两步。
Reiner Pope:但我们可以用长乘法把它们合在一起做。先看乘法部分。我们要把这里这个四位数和另一个四位数的每一个比特位都乘一遍。具体展开的话,先把 1001 乘以这个比特位,那就是它本身;然后整体左移一位;再乘以 0,得到全 0;再左移一位去乘那个 1,就得到 1001;最后再乘最后那个比特位,又得到一个全 0。这样我们就得到了一堆要在乘法里相加的项。与此同时,我们也顺手把真正的累加项加进去,所以直接把它复制过来。这样得到的是一个五项求和。那我们连这一步中间结果都要得到,究竟用了哪些逻辑门呢?我们需要先生成这 16 个部分积。怎么生成其中一个部分积?比如这里这个 1。它为什么会是 1?因为只有这个比特和另一个比特都为 1 时,它才是 1;只要其中有一个是 0,0 乘以任何东西都是 0。于是我们一共用了 16 个 AND 门。一般来说,如果我做一个 p 位乘 q 位的乘法,那就会需要 p×q 个 AND 门。没错。接下来是求和。其实大部分工作都发生在求和这一步。让我再介绍一下我们这里用到的另一种逻辑门。AND 已经是芯片上最简单、最小的门之一了;另一端通常你会用到的最大逻辑门叫做全加器(full adder)。从软件角度你可能会以为全加器是给 32 位数字做加法,但在这里,它其实只是把三个单比特数字相加。你可以把它理解成 0、1、1 相加。加完之后结果可能是 0、1、2 或 3,所以我们可以用两个比特把它表示成二进制。也就是说,它的输入是三个比特,输出是两个比特,而 2 的二进制就是 10。所以它也叫三进二压缩器(three-to-two compressor)。[00:07:11]
Dwarkesh Patel:因为它接受三个输入比特,却输出两个比特。那三个输入分别是 x 和 y,还有一个进位,那个进位是从前面……抱歉。
Reiner Pope:这三个输入其实都是位于同一个比特位置上的比特,也就是这一列里的三个比特。然后我把两个输出分别画成了竖着和横着,是为了对应这里这个垂直和水平的布局,也就是说明同一列里的东西属于同一个比特位。
Dwarkesh Patel:对。相邻列里的东西就不一样了,比如这个是进位输出,而那个是和。如果全加器的输入,比如说是 101,那输出还是 10;如果是 111,输出就是 11;如果是 000,输出就是 00;如果是 010,输出还是 01。对,它本质上就是在数有多少个东西,然后把这个数量用二进制表示出来。[00:08:11]
Reiner Pope:所以这个电路其实很能捕捉我们人类在按列求和时自然会做的事。我来演示一下用全加器做一次求和的过程。这里的求和方式对人类来说有点不自然:我们平时是沿着一列一列往上加,然后记住进位;但这里我不去记住进位,而是把它直接写出来。我们从最右边那一列开始往左算。最右边那一列里,我们把 1 和 1 相加,就得到一个 0 和一个进位 1。于是我们就用这个全加器电路处理了这一对比特,输出了一对比特。接着我们照样处理下一列。这里有一列 1、2、3、4 个数,所以我们可以先拿其中三个来跑一个全加器,输出就是一个 0 和一个 0。也就是说,这些数的和是 00。这个过程就是把全加器应用到所有这些比特上。每用掉一些比特,我就把它们划掉,表示已经处理过了。我们再往前推进一点。我把这三个数加起来,得到一个 1 和一个 0;这三个数就处理完了。然后我再拿 1、2,甚至现在还能把这三个数也拿出来加一遍,结果还是一个 1 和一个 0,这些数也就处理完了。所以你可以这样理解:我面前有一整张需要相加的数字网格,我只需要不停地对这里面的比特应用全加器,每次从一列里拿掉三个数,输出两个数。[00:10:04]
MUX、寄存器文件与硬件成本
Reiner Pope:就这样一列一列地把三个数拿掉、输出两个数,重复、重复、再重复,直到最后只剩下一个数从这里出来,像这样;不过这个和可能还是不对。我们刚才描述的这套方法叫做数据乘法器(data multiplier),它基本上就是用全加器做面积效率很高的乘法器的标准做法。我们来量化一下这个电路的规模,这样之后才有一个能拿来比较的尺子。那我用了多少个全加器?我一开始有多少个数?这里有 16 个部分积,也就是这些项和这些项相乘得到的结果,再加上我这里要加进去的 8 个项。所以一开始总共有 24 位。最后我输出的是 8 位。每一步里,我都相当于划掉三个数、输出两个数。所以每次使用全加器,都会消去其中一个比特。那总共用了多少个全加器?答案一定是 24 减 8,也就是这个电路里有 16 个全加器。一般来说,推广到一般情况也是这样:这个电路里会有 p×q 个全加器。[00:11:37]
Dwarkesh Patel:让我确认一下这里的逻辑。输入比特 24,是 p×q 加上 p 再加 q。对吧。输出比特就是 p 加 q。于是 p×q + p + q - (p + q) 就等于 p×q。没错。[00:12:11]
Reiner Pope:所以我觉得这解释了,或者至少暗示了我们为什么要做乘加的第二个原因。第一个原因是它确实出现在矩阵乘法里,第二个原因则是它给了我们这个非常漂亮的 p×q 的简单代数式。所以我们等于把整个流程都描述了一遍:我在这里做的每一个原子步骤都会变成一个逻辑门,然后再由导线把它们连起来。比如我拿这三个输入,最后做成了这两个输出,如果把它映射到一台物理设备上,那就会有一根导线,把这三个东西连到一起,进入那个产生输出的逻辑门。好,这就是 AI 芯片内部在不同位宽下的主要原语。接下来我们要从这里往上走,看看如果要跑你想要的其他运算,该怎么用它。
Dwarkesh Patel:我可能挑了个不太合适的时间问这个问题,不过每次 NVIDIA 说这颗芯片能做 X 个 FP4,或者做一半数量的 FP8,这似乎意味着这些电路是可以互换的,好像并没有专门的 FP4 和 FP8 电路。但按照你现在的画法,如果它真的要映射到逻辑上,那你似乎就需要一个专门的 FP4 乘加器,再加一个专门的 FP8 累加器。也就是说,你们能把它们通用化吗?按这个画法看,它们其实并没有那么容易互换。[00:13:35]
Reiner Pope:这其实是设计芯片时要做的一个主要选择之一,也就是我要有多少 FP4、多少 FP8。做这个决定时,我有时候会从客户需求的角度去想:我觉得用户真正需要的是什么。还有一种思路是,从功耗预算的角度来平衡 FP4 和 FP8。那他们报告那些数字的时候,比如恰好说这颗芯片做 FP4 的速度是 FP8 的两倍,他们只是碰巧给所有浮点单元分配了等价的 die 面积吗?结果就变成了……为什么这个比例正好是 2 倍?对,没错。[00:14:15]
Reiner Pope:当然,实际情况不会完全等同于 die 面积。其实还有一个数据搬运方面的原因,我们等会儿看它如何进出内存时可能会再回来讲。单从软件层面看,有个很漂亮的地方:我可以把两个 4 位数字塞进和一个 8 位数字一样的存储空间里。所以当我把它存进内存之类的地方时,芯片内部总线的尺寸设计就会刚好非常合适。[00:15:04]
Dwarkesh Patel:说起来,其实不只是 2 倍。看起来它占用的面积应该是……是平方级的,对吧?和位宽是平方关系。
Reiner Pope:对,是平方关系。没错,这也是为什么更小精度的东西会更有优势。这其实是一个非常重要的原因。事实上,NVIDIA 历史上在 B100 或 B200 之前,一直都是每次把位精度减半,FP 计算量就翻倍。正如你刚才说的,因为这种平方级的缩放关系,这个比例其实还略微不对。按理说,你应该得到比你直觉里更大的提速。NVIDIA 的产品规格从 B300 往后开始,已经有点承认这一点了:FP4 比 FP8 快三倍。虽然理论上应该是 4 倍。是的。我这里画的是最简单的整数乘法情况。当你处理浮点数,也就是 FP4 和 FP8 时,还会多出一个指数项,这会让计算更复杂一些。不过我们现在已经能看出什么了?我觉得你已经抓住了一个关键观察:位宽的平方级缩放非常有效,而这也正是低精度算术在神经网络里这么成功的根本原因。接下来我们要做的,是比较乘法本身所占的面积,和它周围那些电路所占的面积。所以我们会稍微倒回去一点,看看在张量核心出现之前,GPU 是怎么工作的,实际上 CPU 也是同样的方式。也就是说,多路累加单元到底是怎么放进去的?所以我先泛泛地描述一下 CUDA core 或者 CPU 的结构。你会有一个寄存器文件,里面存着一定数量的条目。比如说这里可能是 8 个条目,在这个例子里我猜是 4 位数,不过通常会是 32 位数字之类的,都是数字。所以在 CUDA core 内部,我有一个有一定深度的寄存器文件,然后我还有一个乘加电路、乘法和累加电路。它要做的事情是:从这个寄存器文件里取出任意三个寄存器,执行乘加,然后再写回寄存器文件。也许会写到这个寄存器,但它读取时可以来自这个、这个,以及另外一个随机寄存器。也就是说,它会像这样取三个输入。于是这就是很多处理器的核心数据通路。大多数处理器都长这个样子:有一组寄存器,再有一组逻辑单元或 ALU。我们要分析的是,从寄存器文件到 ALU,再回去的这些数据搬运成本。最终会有这样一个电路:它会说,嗯,我并不总是要选这个寄存器。我随时可能选寄存器文件里的任何一个寄存器。所以第一步的问题就是,我该怎么搭一个这样的电路?我要找的这个电路就是一个 MUX(多路复用器)。在这里,它有 8 个输入,对应寄存器文件里的每一项,还有 1 个输出,也就是最后真正产生这个输出的那个。那这东西的成本是什么?它基本上只能靠 AND 和 OR 搭出来。那我们怎么搭?最笨的办法就是先做一个掩码:当我要读第三个条目时,就让每一项都和 1 或 0 做 AND,具体取决于这是不是我想读的那一项,然后把它们全部 OR 起来。[00:18:46]
Dwarkesh Patel:就是根据我想不想读它,给它一个 1 或 0 的掩码,然后再把它们全 OR 到一起。好,我先确认一下基础概念。MUX 做的事情其实就是选择,对吧?就是选择一个输入。对,就像对软件不可见一样,你说我要第 3 个输入,这就意味着背后有一个 MUX。对。那这个 MUX 的成本是什么?一个 n 输入的 MUX,[00:19:06]
Reiner Pope:如果它作用在 p 位上,那我就……我有 n 行,也就是这里的 8 行,而且每一行都有 p 位宽。那我就必须对每一个比特都做 AND。于是需要 n×p 个 AND 门。每一个输入我都得判断一下,要不要把它屏蔽掉;然后再把所有结果 OR 起来。所以还会有 (n-1)×p 个 OR 门。也就是说,我有这些不同的东西。几乎全都是 0,但我得把它们从 8 个选项压缩成 1 个选项。所以每一步我都需要把一行 OR 到当前已经有的结果里。[00:20:03]
Dwarkesh Patel:明白了,挺有意思的。你们平时其实不会从硬件这个层面去想这些东西。
寄存器文件与本地化存储
Dwarkesh Patel:其实挺有意思的是,你平时不会从硬件这个层面去想。你只是觉得,哦,我选第 3 个元素就行了,而这么简单的一件事,本身其实就是一套相当复杂的电路。对,我的意思是,这就是所有隐藏的数据搬运成本的第一步。[00:21:07]
Reiner Pope:所以,我们要比较的就是:我得为这个成本买单,而且我这里有一个掩码。实际上,对乘加操作的三个输入,我还要再复制两份,于是数据搬运这边的成本就变成了大约 3×n×p 个 AND 门;而真正做活的电路只有 p×q 这么多门。如果代入实际数字,比如 n=8,那么光数据搬运这边就会有 24×p 个门;如果 q=4,那么真正做乘加部分也只有 4×p 个门。
Dwarkesh Patel:等等,为什么这里会有一个 3?[00:22:47]
Reiner Pope:因为这里有三个不同的输入。
Dwarkesh Patel:明白了。
Reiner Pope:我真正想表达的是,所有这些工作都是随着寄存器文件大小一起缩放的,而这里的寄存器文件其实已经算很小了。单单把数据从寄存器文件搬到逻辑单元这件事,成本就已经比逻辑单元本身高得多了。这个问题基本上就是在 Volta 之前的 NVIDIA GPU 时代里大家面对的状态。CUDA core 里就是这种结构。也正是这个问题,催生了 Tensor Core,也就是更泛化地说的 systolic array。你可以把问题描述成:我们怎么解决它?我们把几乎所有芯片面积都花在了那些我们其实不关心、而且对软件程序员不可见的东西上,而真正关心的那部分面积却不多。那目标就是:想办法把后者做大,同时不增加前者。于是演进就变成了这样:在这个阶段,我们把这一小段东西直接焊进了硬件里。这里的一条乘加就是硬件里的一个固定单元。systolic array 的思路,则是把循环再往上提两层,把整个外层循环也一起焊进硬件里。也就是说,如果我们拥有一个更大粒度的固定功能逻辑块,进出它的“税”就可能小得多。
Dwarkesh Patel:也就是说,你是在暗示,如果把矩阵乘法循环再往上一层,就能把重心更多地从通信推向计算。对吧?[00:26:25]
Reiner Pope:没错。这里我们要利用两个效果。一个是,我们每次经过寄存器文件时,能做的事情变多了。另一个是,循环里有些东西其实可以保持不变。我们先从视觉上看矩阵乘法这一段。这里这一部分循环,实际上对应的是矩阵-向量乘法。也就是说,我们拿一个矩阵去乘一个向量。怎么做?就是让每一列都和向量相乘,然后再求和,所以我们是沿着列方向求和。比如这里的 0 和 3 要跟 3 和 7 相乘再求和;1 和 2 也要跟 3 和 7 相乘再求和。于是矩阵里的每一个元素都对应一个乘加器。我们就把这四个乘加器画出来。我只想确认一下,为什么这里会有四个乘加器。[00:27:56]
Dwarkesh Patel:因为对应输出向量的那一列里的每个条目,其实都是一个点积。在这个例子里,就是两次乘法,再把这两个乘法结果加起来,所以你是在做累加。对,也就是说加法其实只是在一个点积里做一次,但我们通常会先从零开始。[00:28:22]
Reiner Pope:对,从零初始化。我们要追求的是:计算量要平方级地增加。也就是说,我们的计算量要比之前多 X×Y 倍。但我们希望通信量只增加到 X 倍左右。这个设计意图就是让那个优势项跟着 Y 走。于是我们已经把乘法铺下去了。接下来要引入一个长度为 2 的向量,这已经和我们对通信量的预算吻合了,没问题。但问题在于,这个矩阵的通信量要怎么处理?它已经超出了我们 X 的预算。于是诀窍就来了:在 AI 场景里,这个矩阵实际上会在很长一段时间内保持不变。所以我们不需要从外面反复把它搬进来。比如这里我们有个寄存器文件放在这边,我们不希望寄存器文件里流出来的数据量太大;这就是我们希望压到 X 级别的那部分。我们不能每个周期都把整张矩阵从寄存器文件里搬进来,因为带宽不够。那样从寄存器文件到这里的布线成本会太高。所以我们改成:把这张矩阵本地存放在这个阵列里。关键技巧就是,像 0、1、2、3 这些数字,直接存到一个叫寄存器(register)的门里,让它在物理上把这些数存住,然后我们会在很长时间里反复重用它们,去配合很多很多不同的向量。[00:30:08]
时钟周期与流水线权衡
Dwarkesh Patel:这个优化的本质就是:矩阵乘法的特性允许你把这个更大、更像平方的东西直接存放在逻辑真正发生的地方;它比你不断换入换出的输入多了一个维度。对吧?
Reiner Pope:对。矩阵乘法本来就是这样:你要做很多次乘法,最后才得到一个值。一个点积,本质上就是很多次乘法的结果。所以这个优化意味着,你可以在把结果拿出来之前,先把很多乘法塞进去。没错,没错。为了把图画完整一点,我把这里的 3 和 2 交换了一下。3 和 2。[00:31:13]
Reiner Pope:比如这里的 0 和 3,会去乘 3 和 7。于是我们会沿着列方向形成点积。我们要把 3 和 7 喂进来,它们既参与这一处乘法,也参与那一处乘法。3 也是一样,既进这里,也进那里。然后我们会沿着这一列求和:从列顶端输入 0,最后从底部把结果输出。直观地看,这里就是矩阵里的点积沿着列方向执行,而这和 systolic array 里的空间布局是完全一致的。这里这一列完成的是一个点积,它是竖着求和的;那边是第二个点积,也同样竖着求和。那需要进出寄存器文件的数据是什么?输入这边会有 X 量的数据流出去,输出这边也会有 X 量的数据流进来。所以至少对于输入向量和输出向量来说,我们已经达成了目标:进出寄存器文件的数据量只有 X 级别。剩下的问题是:我刚才说权重矩阵是本地存放在 systolic array 里的,那它最开始是怎么进去的?毕竟你总得先给芯片“上电启动”,把数据灌进去,那这些数据从哪来?诀窍很简单:我们就是非常慢地把它灌进去。最朴素的策略就是搞一条串接链:先把一个数喂到这里,下一个时钟周期它就沿着阵列往下一格移动。这样每一列都可以并行地做,而这就意味着,流经这里的带宽也会相应带来一个大约 X 级别的输入带宽。[00:33:29]
Dwarkesh Patel:你能不能把刚才那句话再重复一遍?所以我们知道,我们其实只会很少地把数字灌进矩阵里。
Reiner Pope:所以我们只需要想出一种构造,让真正跨过这个 systolic array 边界的布线量,也就是这条边界这里的布线量,始终被限制在 X,而不要涨到 X×Y。一个特别简单的策略就是:我们把一个数字从 systolic array 的顶行灌进去。这就是我们在一个时钟周期里能做的事。然后接下来的 Y 个连续时钟周期里,我们每次都往顶行灌输入,同时把其他所有行都往下移一格。这样一来,从这个昂贵的寄存器文件里出来、真正需要的布线就只会是 X 量级,而不会是 X×Y。[00:34:24]
Dwarkesh Patel:明白了。那这里其实有两个通信问题:一个是通信时间,一个是通信带宽。对吧?你的意思是,既然我们只会把它加载一次,那就尽量压低带宽。没错,因为带宽就等于 die 面积,我们只要把它慢慢灌进去,用更窄的通道就行,因为这些值会在里面待很久。对,没错。有意思的是,我们上次聊多芯片推理时,真正要优化的高层目标,其实就是增加每单位内存带宽所能做的计算量,也就是每单位通信量所能做的计算量。这里也是一样,我们也在尽量提高实际乘法和加法相对于把信息从寄存器搬到逻辑单元的比例。也就是说,不管是上层还是下层,这个主题都在反复出现。[00:35:22]
Reiner Pope:对,这个模式几乎贯穿了整个栈。从很底层、接近逻辑门的地方开始,到更高一层也一样。甚至在位宽的选择上,也会出现一个更接近逻辑门的版本:我们刚才已经看到,仅仅是数值格式本身,就会出现平方级和线性项之间的差异。也就是说,既有这个 ALU 精度本身的平方律,也有矩阵尺寸上的平方律。是的,非常有意思。这个单元算是再高一层:我们先有乘法电路,然后再往上就是一个比较大的 systolic array。我这里画成了 2×2,但比如旧一点的 TPU,论文里描述的就是 128×128 这么大的电路。这个电路是目前已知最有效的矩阵乘法实现机制。明白了。[00:36:21]
Dwarkesh Patel:所以我们已经讨论到一个显而易见的结论:你应该尽量让计算相对于通信更大。那有哪些不那么显而易见、但真正会让你夜里睡不着的权衡?比如到底该选 X 还是选 Y,而答案又并不那么直观。对吧?
Reiner Pope:对。我觉得芯片设计里大多数决策,其实都是尺寸决策。就我们刚刚画出来的这些东西而言,AI 芯片里都会有这套电路:有一个 systolic array,旁边有个寄存器文件负责输入和输出。在这个范围里,你首先会问的两个尺寸问题就是:我的 systolic array 应该做多大?寄存器文件应该做多大?而 systolic array 的大小,实际上和寄存器文件大小是耦合的。换一种思路就是:我要给芯片面积里多少比例留给数据搬运,先定一个预算。比如我就说,数据搬运占 10%,systolic array 占 90%。然后我就可以反推寄存器文件的大小。更大的寄存器文件更灵活,它能让我跑更多……我能从应用层面拿到更高的性能。[00:37:48]
Dwarkesh Patel:但它也会从 systolic array 这部分面积里抢走空间。对吧,明白了。最近我办了一个征文比赛,邀请大家写我认为 AI 领域里几个最重要的开放问题。上周征稿结束后,我就用 Cursor 做了几个不同的界面来帮我审稿。一个界面会把投稿匿名化,隐藏掉不必要的信息。它让我可以按问题分类、加备注、记录分数。另一个界面是给那些也想申请我正在招的研究员岗位的人看的。这个 UI 会把申请人的 essay、简历和个人网站并排放在一起,这样我就能一次看到全部信息。Cursor 的 harness 对于让这些模型看懂并改进 UI 特别好用。我看着它在内置浏览器里渲染这些界面、截图、逐个点击模块,然后不停迭代。现在我大部分工作都在 Cursor 里完成。不管我是阅读和可视化一堆研究论文,还是在写一个审申请的界面,或者给我的 Blackboard 课做卡片,Cursor 都让 AI 很容易看见我正在看的东西,然后帮我理解、和我一起工作。所以,不管你在做什么,都应该在 Cursor 里做。去 cursor.com/dworkesh。芯片的时钟周期是怎么来的?它由什么决定?还有,芯片的时钟周期到底是什么?[00:39:10]
Reiner Pope:我觉得首先值得指出的是,芯片的并行度极其夸张,对吧?一颗芯片里有 1000 亿个晶体管。只要你有这么大规模的并行,就必须在不同的并行单元之间做同步。软件里通常会用 mutex 之类很贵的同步方法:一个线程做完自己的事,去内存里拿一个锁,然后通知另一个线程它已经结束了。芯片上我们则完全换了一种做法:大约每隔一纳秒,芯片里的所有电路都会暂停一下,然后同步一次。它实际上就是大概每一纳秒同步一次,这就是时钟周期。整颗芯片通常会在某个瞬间一起进入下一步操作,所有部分步调一致地推进。[00:40:00]
FPGA、LUT 与 ASIC 成本
Dwarkesh Patel:有趣。那这里看起来就像是在说,如果你加了太多逻辑,就可能赶不上时钟周期;但如果逻辑加得不够,又会把可做的计算量留在桌上。有没有一种情况,你会愿意接受一点概率上的风险,觉得某个计算“应该能完成”?还是说不是这样,它要么会在时钟边沿前完成,要么就不行?[00:42:28]
Reiner Pope:在标准芯片设计里,会给它留裕量。也就是说,确实有概率,但那个概率已经小到很多很多个标准差之外了,基本上可以忽略不计。对实际使用来说,它就是可靠的,都会赶上时钟。当然也有少数怪例外,比如跨时钟域的时候,你要从一个时钟域切到另一个时钟域,那时你确实得去考虑这个概率问题。但在主路径上,你会把裕量留得很大,让信号在时钟边沿前大约 25% 个周期就已经稳定下来,所以在这种情况下几乎不会出问题。
Dwarkesh Patel:也就是说,那个寄存器同步的时刻,不是你作为芯片设计者随便拍脑袋定的。更像是:你先有一串逻辑,然后把 Verilog 交给把它转换成发往 TSMC 的工具,工具会告诉你,为了让它成立,你得在这里、这里、这里插寄存器,避免某一步太长,把整颗芯片的时钟周期拖慢。
Reiner Pope:对,这其实是设计芯片工作里非常大的一部分,确实就是插这些寄存器。这个过程既有手工也有自动化。最简单的版本就是把这段逻辑一分为二:不再只是一个逻辑云,而是两个更小的逻辑云,做同样的事,中间用一个寄存器隔开。这样如果你从中间切开,就能把时钟频率翻倍。很好,你以增加一个寄存器、也就是多一点存储为代价,换来两倍性能。退一步看,[00:43:46]
Dwarkesh Patel:为什么我们需要整颗芯片同步?如果你把它想成在玩 Factorio 或者别的什么游戏,就没有全局时钟周期;事情做完就是做完了,铁板上有铁,你想拿就拿。
Reiner Pope:对,沿着这个类比去想,你得注意的是:如果有两条路径穿过一段逻辑。比如我这里做计算 F,那里做计算 G,然后它们要在某个地方汇合,去做 H。这里会有制造差异;有些芯片里 F 会慢一点,有些芯片里 G 会慢一点。如果有信号从这里传播过去,而 F 和 G 的结果必须在 H 处汇合,可能出错的地方就是:F 先到了,它可能会碰到 G 的前一个值,或者后一个值之类的东西,而 H 必须知道什么时候开始。[00:45:20]
Dwarkesh Patel:就像要明确知道下一轮迭代什么时候开始。那这也解释了为什么同一个工艺节点、同一种 TSMC 工艺做出来的不同芯片,会有不同的时钟周期。比如两颗 3nm 芯片,时钟周期可能不同,取决于它们是否把那条关键路径优化掉了,避免某一段太长而把整颗芯片拖慢。
Reiner Pope:对。刚才我展示的这个优化,就叫插入流水线寄存器。我们是在流水线中间插了一个寄存器。这本质上就是在时钟速度和面积之间做权衡。这个是简单情形。还有一个更难的情形:我画的是一条流水线逻辑,但有些时候,你会遇到一个计算,它会反馈回自身。也就是先运行函数 f,然后把结果写回自己。比如这里可能就是一个加法器,你每个时钟周期都往里加一个数。这个小电路本质上就是把不同时钟周期送进来的所有数字全部求和。问题是,如果这个加法太慢,我该怎么办?如果我试着把流水线寄存器插在中间,比如插在这里,就会改变计算结果。原本我应该得到所有输入的滚动和,但现在我会得到两条不同的滚动和:偶数项的和和奇数项的和。所以这种“逻辑里有个环”的约束,所有芯片某处都会有,它其实是最难处理、也最决定时钟周期的地方。[00:47:28]
Dwarkesh Patel:我不太明白,为什么在那里放一个寄存器会有问题,或者说我甚至不太确定那意味着什么。因为这不是一种原子操作吗?
Reiner Pope:对,但加法其实并不是原子操作。正如我们刚刚演示的那样,它要做很多步。没错,这里面有很多工作。于是你可以把这些工作的前半段拿出来,在中间插一个寄存器,再把后半段拿出来继续做。
Dwarkesh Patel:明白了。那接下来就取决于……TSMC 会给你一个 PDK,说明他们能在芯片里提供哪些逻辑原语。它要负责保证,没有哪个原语比他们希望这个工艺节点支持的时钟周期还大。但除此之外,你还会怎么进一步优化?你不能就说:给我 TSMC 的所有原语,然后在它们之间尽可能多地插寄存器,直到达到你想要的时钟周期吗?
Reiner Pope:从逻辑设计者的角度看,芯片架构师会先定时钟周期。举个例子,TSMC 给你的原语大概就是 AND 门或者全加器这个量级。它们会随着电压和你选的 library 不同而变化,不过一般来说,你通常可以在一个时钟周期里顺序串上大约 10、20、30 个这种原语。这些原语非常非常快,大概只有 10 皮秒左右。所以从逻辑设计者的角度,理论上如果你真的只是把一个寄存器和一个 AND 门做成这种循环,你甚至能得到夸张得离谱的时钟频率,四五六 GHz 都有可能。但如果你看这个很简单的电路在面积上的代价,这里大概是 1 个 gate equivalent,而那个寄存器大概就要占 8 个面积单位。也就是说,几乎所有成本都花在同步和通信上,而不是实际逻辑上。那就属于走太远了。[00:49:41]
Dwarkesh Patel:你把时钟速度做得很快,但代价是几乎把所有面积都用在流水线寄存器上。有意思。所以你暗示的是一种局面:时钟很快,但真正干成的活并不多。
Reiner Pope:对。所以你会得到很低的延迟,但带宽或者吞吐量就很低。[00:50:02]
CPU、GPU 与分支预测
Dwarkesh Patel:所以你会得到很低的延迟,但带宽或者吞吐量就很低。没错。实际上这会伤到吞吐量,因为你可以把芯片吞吐量理解成两个量的乘积:每个时钟周期能做多少事,这取决于这里的面积效率;再乘上每秒能跑多少个时钟。这个想法其实和我们上次聊的 batch size 很像:如果 batch size 很小,那么任意一个用户都能很快拿到下一个 token,但一个小时里总共处理的 token 数会比本来能做到的少。对,没错。你把时钟速度往上推,就会损失一部分并行性。我明白了。语言模型正在开始和最好的人工预测员竞争。我和 Jane Street 的两位资深交易员 Ron Minsky、Dan Pontecorvo 坐下来聊过,问过他们:AI 什么时候会真的做 Jane Street 在做的事?我们必须认真对待一种可能性,那就是我们会造出比地球上所有人类都更聪明、在所有认知任务上都更强的大语言模型或其他 人工智能系统。就交易这件事本身而言,我觉得它有点像 通用人工智能 完整问题,甚至有点像 NP 完整,因为归根到底,交易是在判断东西值多少钱,而这意味着你得对未来做预测。Jane Street 并不是在对 AI 押反向赌注。他们刚签了 60 亿美元的算力合同。但 Ron 的看法是,优势会不断转移。我从来没有像今天这样急切地想招更多工程师和交易员。那些我们还不会自动化的困难部分,最后往往就成了竞争优势所在。你可以去 jainestreet.com/slash/thwarkash 看这些开放职位,也可以看完整采访。
我记得我之前和 Jane Street 的一位 FPGA 工程师 Clark 聊过,他其实还帮我准备了上一次我们一起做的采访。他当时在解释他们为什么用 FPGA。我想,高频交易里,吞吐量也许不如延迟重要。所以,能以一种确定性的方式精确控制时钟周期,可能是最关键的事。也许可以聊聊,为什么不能直接用 ASIC 实现这一点,或者说,为什么 FPGA 会是那个选择。也就是,为什么你会为了高频交易,用 FPGA 来获得确定性的时钟周期?
Reiner Pope:首先,我们先看看 FPGA 和 ASIC 之间的商业逻辑。FPGA 和 ASIC 在概念模型上很像:我有一系列由 AND、OR、XOR 这些很小的原语构成的门,它们用固定的时钟周期连在一起,也通过固定时钟周期里的导线连在一起。所以,你在 FPGA 里能表达的任何东西,也都能在 ASIC 里表达出来;而且 ASIC 的能效会更好,成本大概会便宜一个数量级。代价是,第一块 FPGA 可能只要 1 万美元,而你做的第一块 ASIC 可能要 3000 万美元,因为它需要整套 tape-out。
所以,FPGA 的商业用途通常是:我想要一种延迟非常确定、运行速度快、并行度又高的东西,但我会经常改它,可能每个月就改一次甚至更频繁。那我就不想每次都付 tape-out 的成本。FPGA 到底是怎么实现这件事的?它有点像是在一块固定硬件里模拟 ASIC 的编程模型。它的底层有我们刚才说的两样东西:作为存储单元的寄存器,以及这些叫做查找表(lookup table,LUT)的东西,它们真正提供所有逻辑门。然后我们还会看到第三个组件:一大群这样的寄存器和 LUT 都摆在那里,而它们之间通过一大堆多路复用器(MUX)连接起来。
在每一个这样的单元前面,都有一个类似 MUX 的东西,它从周围所有可用输入里选一个。这里有很多不同的选项流入这些单元。所以,当我给 FPGA 编程时,我实际上是在说:我要把这些组件叠加成一张特定的布线图,让信号穿过这个 LUT,再进入那个 LUT,然后走到这个寄存器,再流进另一个 LUT,诸如此类。我这里用橙色画出来的部分,就是 FPGA 的含义,也就是 Field Programmable Gate Array,现场可编程门阵列。橙色代表那些已经在现场被编程好的连接,而白色则是为了让这块设备先被造出来,FPGA 内部必须存在的全部导线。
Dwarkesh Patel:什么叫“在现场编程”?就是设备已经部署到数据中心了,它放在现场,然后你可以过来给它编程?不是指电场那个 field 吧?
Reiner Pope:不是。这里的 field 是指“外面世界里、现场里”的那个 field。对,就是那个意思。
Dwarkesh Patel:那如果我看这种现场编程是怎么从第一个查找表出来、又进到第二个查找表的,那它到底是怎么做到的?这些线到底在哪儿?
Reiner Pope:我画图的时候偷了点懒,没有把所有线都画出来。这里的每一个器件前面都有一个 MUX,它可以从附近所有可用电路里选输入。所以,FPGA 的实际配置,本质上就是这些 MUX 的控制位。在这个 MUX 里,我们有数据输入,也有决定选哪一路的控制信号。每一个 MUX 旁边都有一个小存储单元,告诉它这次应该从哪里取输入。
Dwarkesh Patel:明白了。所以编程这件事,就是把这些 MUX 一个个配置好。这样就说得通了。那查找表里面到底发生了什么?
Reiner Pope:查找表本身也会有一点控制信号告诉它该做什么。查找表的目的,是让它能灵活地扮演 AND 门、XOR 门,或者别的各种门。实现方式有很多种。传统 FPGA 的做法是:它支持四个输入、一个输出。四个输入到一个输出,一共有多少种不同函数?一共是 16 种。所以你可以直接把它们制成一个 16 项的表。比如表里存着 0111001 之类的一串东西,总共 16 项。它做的事就是把这张表存到这个蓝色配置位里,然后把这 4 个比特当作二进制索引,查出表里对应的那一行,再输出那个比特。所以从本质上说,查找表就是一张真值表。
Dwarkesh Patel:明白。那如果你想到 AND 门、OR 门、NOR 门、XOR 门,它们都可以理解成输入……它们本质上就是二输入函数。有时候我们会有更复杂的,比如三输入函数,像三路 XOR,或者四路 XOR。那在这种情况下,LUT 的大小要看它有多大吗?通常 LUT 是四输入的,这是个比较合适的甜点位。因为这里又有一个计算/通信权衡:如果输入太少,就得用更多 LUT;如果输入太多,又会带来别的问题。但总之,查找表就是一张真值表。
Reiner Pope:对,你可以把它理解成一张可以编程的真值表,也就是一个可编程门。对。顺便说,这也是为什么经验法则里 FPGA 比 ASIC 贵一个数量级:你可以数一数,这个查找表里相当于塞进去了多少个门。
我们可以把这个查找表看成前面说的那种 MUX。它本质上就是一个要在 16 个不同值之间选择的 MUX。所以它是一个 n=16、p=1 的 MUX。我们前面已经看到,这种电路的成本大概是 n×p 个门,所以这里差不多就是 16 个 AND 门,再加 16 个 OR 门。
Dwarkesh Patel:这个电路就是那个 MUX,对吧?
Reiner Pope:对,就是 MUX。这个盒子是核心,那个进入查找表的 MUX 也是核心。也就是说,查找表本身可以理解成一个很大的 MUX,它从 16 行里选出一行,最后得到一个输出。对,这就是查找表。只是你画出来的时候……
GPU、TPU 与数据搬运
Dwarkesh Patel:就像从 16 行里选到 1 个输出。明白了,这就是查找表。不过你这里画法是一个 MUX 再接一个查找表,盒子一层一层往下套。也就是说,这里其实还有第二个盒子,第二个盒子就在里面。对,就是这个 MUX。明白了。然后另一个 MUX 只是说明它是从这堆乱七八糟的门里哪儿来的,对吧?第二个 MUX 的意思就是:现在你已经拿到了一个值,但那个值还是 4 比特的。也就是说,你先从一锅杂乱的输入里选出 4 个比特,然后再用这 4 个比特去决定我要用查找表里的哪一项。对吧?假设第一个 MUX 里有 8 个附近的寄存器可以选,你从中拿 8 个附近寄存器作为输入,那总共就是 32 个比特进来。然后其中 4 个比特出来,这 4 个比特再进入第二个 MUX,也就是查找表内部的那个 MUX。
Reiner Pope:不过我得说,在这个例子里,这些寄存器其实是单比特寄存器。所以如果有 8 个附近的寄存器和 LUT,那总共就是 8 个比特进来。我会从 8 个可能来源里选出 4 个不同的值。也就是说,其实有 4 个不同的 MUX,每个输入比特都对应一个小 MUX。
Dwarkesh Patel:也就是每个都在 8 个来源里选一个。那这 8 个来源又从哪来?附近的寄存器和其他 LUT。每个寄存器都是 1 比特?
Reiner Pope:对,每个寄存器都是 1 比特。
Dwarkesh Patel:所以我猜,不管是 AMD 还是别的 FPGA 厂商,还是得对“哪些寄存器连到哪些寄存器”这件事有明确意见。你可以把真正的门编进去,但他们在通信拓扑上已经先把线搭好了,对吧?
Reiner Pope:对。你得到的是局部粒度上的灵活性。附近有一个小范围,你可以从里面选;但更粗粒度、更远距离的连接,他们已经先替你做了取舍。
Dwarkesh Patel:那为什么说它会慢 10 倍?
Reiner Pope:如果你看建一个查找表的成本,大概就是 32 个门。然后它能给你的,只不过相当于做一个有趣的东西,比如四路 AND。也就是说,我用 32 个门的查找表,去实现一个四路 AND。那什么叫四路 AND?就是 and、and、and,然后再对前面这些结果做 and。这个电路我在 ASIC 里可以直接用这 3 个 AND 门实现;但用 LUT 也可以实现,只是它会花掉 32 个门,而不是 3 个。差距主要就来自这里:查找表里的那个 MUX。与其把所有输入组合都一一列出来,不如用更简洁的方式描述一张真值表,也就是直接把门画出来、把多晶硅和 AND 门放上去。
Dwarkesh Patel:有一个他给我讲的重要点是,他们偏爱 FPGA 而不是 CPU,是因为 FPGA 的时钟周期是确定的。他们知道一个包什么时候进、什么时候出。那为什么 CPU 里就不能保证这一点?
Reiner Pope:其实你完全可以设计一颗也有确定性延迟的 CPU。事实上,很多 AI 芯片里的处理器核心其实也都有确定性延迟。Grok 公开讲过这一点,TPU 的核心里也有。难点在于,你要同时拿到确定性延迟和高速。延迟不确定性从哪来?来自 CPU 里的具体设计选择。你完全可以把这些设计选择拿掉,做出一颗确定性延迟的 CPU。只是这类 CPU 在市场上没那么有吸引力,所以现在大家不太做了。但某种意义上,确定性延迟反而可能是更简单的起点,然后再往里面加东西,让它变得不确定。一个很具体的例子,大概也是最重要的例子,就是 CPU cache 本身。
在 CPU 里,你有 CPU 本体,旁边有外部内存,也就是 DDR。然后芯片内部还有一个 cache 系统。cache 会记住最近对 DDR 的访问并把数据存起来。所以,当我跑 CPU 指令时,每当某条指令要访问内存,它会先去 cache 里查:数据是不是已经在 cache 里了?如果不在,再去 DDR 里取。这是个非常大的优化。cache 比 DDR 快大约两个数量级。如果你完全不用 cache,几乎所有程序都会慢 100 倍。所以,cache 对 CPU 能否以合理速度运行来说是绝对必要的。
但你能不能命中 cache,取决于 CPU 运行时的环境:还有哪些程序在跑,最近跑过什么,cache 系统里的随机数生成器在干什么。所以这就是 CPU 运行时延迟不确定性的一个大来源。
所以这就是 CPU 的内存系统。你能做的一个重要变化是:不要让硬件自己说“我要去读内存,然后再决定”,也不要让硬件自己决定数据是从 cache 来还是不从 cache 来;你可以把这个决定写进软件里。另一种设计哲学就是这样,TPU 里就能看到。TPU 不是 cache,而是 scratchpad。我还是画同样的图,但把它叫做 scratchpad。区别在于,这里是 TPU,旁边是 HBM,不再是 DDR,但它依然是片外存储。软件不再是先发起一个内存访问,然后由硬件去决定;而是你有一类指令走这里,另一类完全不同的指令走 HBM。
这种风格通常就叫 scratchpad,而不是 cache。关键区别在于:你有一类指令说“读/写 scratchpad”,还有另一类完全不同的指令说“读/写 HBM”。
Dwarkesh Patel:所以 scratchpad 就是 cache 吗?
Reiner Pope:对,这里这个东西就是 scratchpad,先说清楚。
再往前追,人们常说计算机有“冯·诺依曼架构”,也就是信息是串行处理的。也许我们一直在聊并行加速器,所以我就不太……FPGA 很并行,AI 里的 TPU 也很并行。就算是 CPU,如果你把它有多少核心也算进去,其实也很并行。那现代硬件在什么意义上还算是冯·诺依曼架构?这种说法对现代硬件真的公平吗?
Reiner Pope:我觉得把它用来描述 CPU 是公平的。CPU 上你能得到的并行度,大概是 100 个核心,再乘一个 16 路向量单元,也就是大约 1000 路并行。
Dwarkesh Patel:还有一个问题。CPU 这块 die 是被用来干 CPU 活的。如果线程更少,是不是从晶体管电压或者开关状态的角度看,实际上就只是 die 上某一小块控制流在切换?或者说,如果核心这么少,那你到底是怎么占用一颗 CPU 的 die 面积的?我是不是在问:里面到底发生了什么?
Reiner Pope:核心只是更大、更复杂。比如我们应该把一个 CPU core 和一个 LUT 比一比:CPU core 大概占 die 的一百分之一到一百六十分之一,而一个 LUT 只有那 16 个门。这样就很容易看出,为什么 FPGA 里的 LUT 会比 CPU core 多得多。然后再往下一个问题,就是为什么 GPU 里会有更多 CUDA core,而不是更多 CPU core?这就涉及 CPU 和 GPU 的差别了。CPU 里很大一块面积花在 cache 上。真正用于 ALU 的面积其实很少,更多是寄存器文件,而不是逻辑单元。GPU 里也有这些东西,所以这不是主要差别。真正没有对应物的,是 CPU 里的 branch predictor,也就是分支预测器。所以 CPU 里有一大块面积,本质上就是一堆预测器,用来判断我的下一个分支什么时候出现、分支目标在哪里。把这些东西去掉……[01:10:00]
从 SM 到可拆分 systolic array
Reiner Pope:把这些东西去掉,再把这些寄存器文件做得更紧凑,在某种意义上,就是 GPU 能快起来的很大一部分原因。
Dwarkesh Patel:分支预测器的作用,是把两个分支都一起执行吗?还是它到底在做什么?
Reiner Pope:问题在于,当我有一串指令,比如指令、指令、指令、指令,如果这里有一个 branch,真正去处理一条指令这件事要花很长时间,大概需要 5 纳秒左右。也就是说,真正去发现“这里有个分支”、然后判断这个布尔值是真是假、把程序计数器更新到新的目标地址、再从指令存储器里把那个目标读出来,这整个过程实际上可能要 5 纳秒才能完成。于是现实里,这一步可能要到下面某个时刻才做完。但我想跑的时钟速度,比 5 纳秒允许的还要快得多。5 纳秒对应的也就 200MHz 左右,而我希望跑到 1GHz、2GHz 甚至更高。所以,在分支还在求值的时候,我得先继续跑后面的指令。我就是想把紧随其后的那些指令继续执行下去。可那有可能是错的。因为如果这个分支最后被 taken 了,我就得知道,不能继续执行这些指令,而应该跳到目标地址去执行另一批指令。所以分支预测器的作用,说白了就是提前做预测:在你真正走到这个分支之前,甚至提前 5 个周期,就预测这里会不会出现一个分支。
Dwarkesh Patel:如果我把大脑怎么工作和你刚才描述的东西做个类比,高层上的差异也许是:这些加速器可以做结构化稀疏,从而省掉原本要专门给这些门留出的面积;而在大脑里,稀疏是非结构化的。任何神经元都能连到任何其他神经元,而且不是那种按列对齐之类的连接。再一个区别是,存储和计算是同地的。我甚至可以说,从某种意义上,内存和计算本来就是共置的。
Reiner Pope:对,对,这本身就是一种共置。
Dwarkesh Patel:没错,也许这其实并不是特别大的差别。还有一个可能很大的差别是,大脑的时钟周期比计算机慢得多。部分原因是为了节能,因为时钟越快,为了让信号稳定下来、判断某个晶体管处于什么状态,就需要越高的电压。你对大脑和这些芯片之间的高层对比,还有别的看法吗?
Reiner Pope:有。先说时钟速度这一点。芯片上的时钟之所以高,是因为它能带来更高的吞吐量。比如一块 GPU 跑某个任务时,可能是 batch size 1000 之类,而大脑不是 batch size 1000。它只有一个我。所以你可以想象,把 GPU 的时钟从 GHz 降到 MHz,可能就会更像你刚才说的脑的那种东西。
但在硅里,这么做并不会让能效提升 1000 倍。最后看起来就是:你把电路跑一遍,让它稳定下来,然后它会在很长一段时间里保持空闲。空闲的时候它并不会消耗很多能量,因为大部分能量消耗都发生在位从 0 变 1、再从 1 变回 0 的切换上。
我们可以具体说说这类电路的能量消耗。把一个 bit 存起来,可以理解为你在芯片里某处的一个电容上沉积了电荷。它变成 1 的时候,这个电容被充电;下一次变成 0 的时候,这个电容放电。这个给电容充电、再把电荷放到地上的过程,就是能量消耗发生的地方。这叫动态功耗,或者开关功耗,是芯片能耗的主要部分。当然还有一些别的能耗,来自绝缘体并不是完美绝缘体,但我们先忽略掉它。实际上,大部分能耗都来自把 0 变 1、再变回 0 的充放电过程。
所以如果你把芯片跑得慢很多,比如每一千个时钟周期才打一拍,你的状态切换就会少一千倍,能耗大约也会少一千倍。
Dwarkesh Patel:但这不意味着能效会有一个特别夸张的提升,对吧?
Reiner Pope:对,不会带来特别大的能效优势。
Dwarkesh Patel:好,所以你已经从高层解释了 TPU 是怎么工作的。那 GPU 和 TPU 在高层上到底有什么差别?
Reiner Pope:我觉得这里有一个不同的高层组织原则,而且在核心内部也有差别。不过我们先看最外层的结构。假设这就是整颗芯片。GPU 的组织方式,主要是一堆几乎完全一样的单元,也就是这些 SM。中间有一个 L2 memory,下面又是一堆这样的 SM。所以它看起来就是一张非常规则的计算核心网格。
相比之下,TPU 的逻辑单元粒度更粗。你会看到的是一些大矩阵单元,也就是那些大号 systolic array。中间有一个向量单元,下面再是矩阵单元。所以整颗 TPU 芯片可以理解成“矩阵单元 + 中间的向量单元 + 矩阵单元”的组合。你也可以把它缩小成一个更小的单元:更小的矩阵单元、更小的向量单元。那其实就是一个 SM 的样子。
所以从非常高层的角度看,GPU 可以理解成把很多很多很小的 TPU 平铺在整颗芯片上。
Dwarkesh Patel:哦,有意思。你是在说,流式多处理器里的 tensor core,跟一个 MXU 类似吗?
Reiner Pope:对,非常非常像。
Dwarkesh Patel:明白了。所以如果你拥有更少的结构约束,做一堆小 TPU 就很合理。可如果你主要是做超大的矩阵乘法,你就会想:为什么不省掉每个 SM 自己那套寄存器、warp scheduler 之类的成本,直接做一个更大的东西,把这些成本摊到整个阵列上?
Reiner Pope:我觉得这会体现在你能把东西做多大的问题上。我们已经在 systolic array 上看到了这个主题:更大的 systolic array,能够更好地摊薄寄存器文件成本。这种设计允许你做更大的 systolic array,而 GPU 设计则逼着你把每一样东西都做得比较小。
不过这里有一个权衡。由于这种粗粒度的分工,你得把很多数据从向量单元搬到矩阵单元。这样你就需要通过两条边界线来搬数据。反过来看这里,虽然你有很多向量单元,但你得穿过这一条线、那一条线、再一条线、再一条线、再一条线、再一条线。也就是说,GPU 里向量单元和矩阵单元之间能搬的数据量,实际上会比 TPU 里更大,因为你不用把所有数据都挤过两条线,而是在 GPU 里通过十几条线来搬。
Dwarkesh Patel:对。但你也可能跨的面积更小,这本身也是一种节省。
Reiner Pope:这是能量上的节省。如果你能完全在一个 SM 里运算,数据移动就会小得多;但一旦你想跨 SM 运算,事情就会复杂、也会更贵。
收尾:可拆分 systolic array
Dwarkesh Patel:所以我不算是在提建议,但直觉上,Maddox 可能会想做的事情,是把 GPU 那种更小粒度的 systolic array 结构包在 SRAM 周围,同时又把 SM 里那些支持 CUDA 架构、但会占很多面积的东西尽量去掉。
Reiner Pope:对,我们公开聊过一件事,叫做 splittable systolic array。你可以把它理解成:在某种意义上,它既可以是很大的 systolic array,也可以拆成很小的 systolic array。
Dwarkesh Patel:酷。好,我觉得这句作为结尾很合适。Reiner,非常感谢。
Reiner Pope:谢谢,Rakesh。[01:20:18]