← Dwarkesh Podcast

Reiner Pope: 赖纳·波普——从底层开始的芯片设计

Reiner Pope – Chip design from the bottom up

第2026-05-22期2026-05-22简体中文

嘉宾:Reiner Pope

收听本集00:00 / 1:20:30
阅读设置
按你的习惯阅读
正文字号
下载播客精读
把这一篇,带走慢慢读

中文 · 精读 · 当前版本

下载功能还未开放

§01赖纳·波普——从底层开始的芯片设计

从最基本的逻辑门出发,一路讲到 GPU、TPU、FPGA 和人脑各自为何是现在这个样子。

这是与赖纳·波普(Reiner Pope)合作的一期新的黑板讲座:芯片究竟是如何工作的——从最基本的逻辑门讲起,逐层向上,直到解释 GPU、TPU、FPGA 和人脑各自为何呈现出现在的样子。

赖纳是新芯片创业公司 MatX 的 CEO(坦白说明一下:我是天使投资人)。他此前在谷歌工作,负责软件效率、编译器和 TPU 架构方面的工作。

请在 YouTube 上观看这一期,这样你就能看到黑板。

§02用逻辑门构建乘累加单元

00:00我又和赖纳·波普在一起了,他是新 AI 芯片公司 MatX 的 CEO。上次我们聊的是数据中心内部发生的事情。现在我想弄明白 AI 芯片内部发生了什么。芯片究竟是怎么工作的?顺便坦白说明一下:我是 MatX 的天使投资人。所以希望你们设计出了一块好芯片。

希望如此。我会从芯片设计中最小的基本单元开始,然后逐步构建到真正的量产芯片是什么样、它由哪些部件组成。在芯片的最底层,我们使用的基本单元是逻辑门,也就是非常简单的东西,比如与(AND)、或(OR)和非(NOT)。它们通过导线连接在一起,而这些导线必须在芯片上以金属走线的形式进行物理布局。

AI 芯片要计算的主要函数是矩阵乘法。在其内部,最基本的单元操作是对成对的数字做乘累加。我们先手工演示这个计算是什么样子,再推断出相应的电路会是什么样子。

最容易的做法是让一个四位数与另一个四位数做乘累加。最清晰的基本单元其实就是乘累加。也就是先把这两项相乘,然后再加上一个八位数。

我能问一个澄清性的问题吗?为什么这是计算机内部各种计算自然而然的基本单元?

有几个原因。它的效率会稍高一些,但对 AI 芯片来说之所以自然,是因为你看矩阵乘法过程中发生的事情……简单说,矩阵乘法是什么?就是对 i、j、k 做三重 for 循环,执行 output[i, k] += input[i, j] × other_input[j, k]。矩阵乘法的每一步都发生一次乘累加。

另一个观察是,累加步骤的精度几乎总是高于乘法步骤的精度。这是 AI 芯片特有的情况。你乘的是低精度的数,而在累加时误差会迅速累积,所以那里需要更高的精度。这就是我们选择做四位乘法和八位加法的原因。

让我确认一下我理解对了。对此有两种理解方式。一种是结果的值会比输入更大。另一种是,如果是浮点数,那么它会……也许这一部分我不太直观。但也许是同一个原理?

确实是同一个原理。另一个独立的原理是,你在对这个数求和时,其实是在把一大堆数加起来,所以会累积很多舍入误差。而在这里,链条中只有一次乘法,所以乘法中不会累积很多舍入误差。

为什么你要把一大堆数加起来?那里只有两个数啊。

这个求和会重复 j 次。

任何误差都会累积起来。我明白了。

那么我们如何手工完成这个计算呢?作为人类,我们大概会把它拆成两步,但我们可以用竖式乘法一次完成。

先看乘法项:我们要把这个四位数与另一个四位数的每一个比特位分别相乘。我们把它写出来。首先,1001 乘以这一位,结果就是这个数本身。然后向左移一位,这里乘的是 0,得到一个全 0 的数。再向左移一位,乘以这个 1,得到 1001。最后,对于最后这一位,我们又得到一个全 0 的数。

这样我们就得到了乘法需要相加的一批项。既然要做这个求和,我们不妨把真正的累加项也一起加进来。所以我们直接把它原样抄过来。这就是总和,是我们要计算的一个五路求和。

到达这个中间步骤需要用到哪些逻辑门?我们需要产生全部 16 个部分积。怎么产生其中一个部分积呢?以这里的这个 1 为例。我们用这个数乘以那边的这个 1 来产生它。这可以用一个与门(AND gate)实现。只有当这一位是 1 并且这一位也是 1 时,这个数才是 1。只要其中任何一个是 0,那么 0 乘以任何数都是 0。要产生所有这些,我们最终用掉了 16 个与门。在一般情况下,如果我做的是 p 位乘以 q 位的乘法,就需要 p × q 个与门。

最后,我把它们求和。大部分工作都发生在求和上。让我介绍一下这里用到的另一种逻辑门。与门几乎是芯片上最简单的逻辑门,几乎是最小的。在另一个极端,你通常会用到的最大的逻辑门叫做全加器。

如果你是软件出身,可能会以为全加器是把 32 位的数相加。在这里,它只是把三个单比特的数相加,所以你可以把它想成把 0、1 和 1 加在一起。把它们相加时,结果可能是 0、1、2 或 3,所以我只需要两个比特就能用二进制表示它。它的输入有三个比特,输出有两个比特。数字 2 的二进制是 10。因此它也被称为 3→2 压缩器,因为它接收三个比特的输入,产生两个比特的输出。

确认一下我理解对了:两个输入是一个 X 值和一个 Y 值,然后还有一个传进来的进位……

这三个输入都是同一个比特位上的比特,就像这里一列中的三个比特。两个输出,我把它们一个竖着画、一个横着画,以便与这种竖向与横向的排布相对应。这表示同一列中的东西处于同一个比特位,而相邻列中的东西则不同。这是进位输出,而这个是和。

所以如果全加器的输入是,比如说 101,那么输出就是 10。如果是 111,输出就是 11。如果是 000,输出就是 00。如果是 010,输出仍然是 01。明白了。

对。它本质上就是数一数有几个 1,然后用二进制表示出来。这个电路体现了我们人类沿着一列求和时自然而然做的事情。

我来演示一次用全加器求和的迭代。我在这里求和的方式对人类来说有点不自然。我们通常是沿着一列求和,然后记住进位,而这里我们不去记进位,而是把它明确地写出来。我们从最右边的一列开始,向左推进。在最右边这一列,我们把 1 和 1 相加,在这里得到一个 0,并产生一个进位 1。我们对这一对比特使用了这个全加器电路,并产生了一对比特作为输出。

现在我们可以对这一列做同样的事。这一列有四个数,所以我们取其中前三个,对它们运行一个全加器,得到的输出是一个 0 和一个 0。它们的和是 00。这就是对所有这些比特应用全加器。随着我用掉比特,我会把它们划掉,表示它们已经处理过了。

我们再继续一点。我取这三个数,把它们相加,得到一个 1 和一个 0。这三个数处理完了。现在我取这三个数相加,得到一个 1 和一个 0,这几个数也处理完了。

理解的方式是:我有一整个需要相加的数字网格。我会不断对这里所有的比特应用全加器,每次从一列中移除三个数,并输出两个数。一遍又一遍地继续,直到最终只剩下一个数。

这种方法叫做 Dadda 乘法器(Dadda multiplier)。这是使用全加器实现面积高效乘法器的标准方法。我们来试着量化一下这个电路的规模,这样我们就能对各部分的大小有个概念,以后也可以拿来比较。

我用了多少个全加器?我一开始有多少个数?我有 16 个部分积,也就是这些项与这些项两两相乘的所有积,再加上我在这里加进来的 8 项。所以我一开始有 24 个比特。最终在输出端产生了 8 个比特。在每一步中,我划掉三个数,并写出两个数作为结果。

每使用一次全加器,就会消去这里的一个比特。那么需要多少个全加器?一定是 24 减 8,所以这个电路中有 16 个全加器。这在一般情况下同样成立。这个电路中会有 p × q 个全加器。

让我确认一下我理解了这里的逻辑。输入比特数 24,等于 p × q 加上 p + q。输出比特数就是 p + q。所以 p × q 加 p + q,再减去 p + q,等于 p × q。

没错。我认为这解释了,或者至少暗示了,我们选择乘累加的第二个原因。第一个原因是它是矩阵乘法中实际出现的操作。第二个原因是,它让我们得到了非常漂亮、简洁的 p × q,代数运算非常简单。

我们已经描述了整个过程。我在这里采取的每一个原子步骤都变成一个逻辑门,然后把导线连接起来。当我有这三个输入用来产生这两个输出时,如果把它映射到一个物理器件上,就会有一根导线把这三个东西连接到一个逻辑门上,由这个逻辑门产生这个输出。

这就是 AI 芯片内部(在不同位宽下)的主要基本单元。我们将从这里出发,一步步构建出如何用它来运行你可能需要的所有其他运算。

现在问可能不是时候,但每当英伟达(Nvidia)报告说这块芯片能做多少次 FP4 运算、或者说 FP8 运算的数量减半时,这似乎意味着这些电路是可互换的,也就是说并不存在专用的 FP4 与专用的 FP8 电路。但按你在这里的映射方式,似乎如果必须映射到逻辑电路上,你就需要一个专用的 FP4 乘累加单元,再加一个专用的 FP8 累加单元。你能把它们“互换”使用吗?

按照现在画的样子,它们并不是特别可互换。这实际上是设计芯片时必须做出的主要选择之一:我要多少 FP4,多少 FP8?有时我会从客户需求的角度来考虑这个问题。另一个角度是让 FP4 和 FP8 的功耗预算相等。

当他们报告这些数字,而恰好 FP4 的数量是 FP8 的 2 倍时,他们只是选择给所有浮点格式分配了相当的芯片面积(die area),结果就变成了……?

为什么比例恰好是 2 倍?一部分原因是,它肯定不会恰好等同于芯片面积。还有数据搬运方面的原因。等我们看它如何进出存储器时,也许会再回到这个问题。从软件层面看,有一点非常好:我可以把两个四位数打包到与一个八位数相同的存储空间里。当我把它存到存储器时,我在芯片内部引出的总线的位宽设计会让这件事处理得非常顺畅。

这么一想,这不仅仅是 2 倍。它所需的面积听起来是随位宽呈二次方增长的。这就是为什么更低的精度比你想象的还要更有利。

这确实是一个很重要的原因。事实上,英伟达做出了改变。历史上,直到 B100 或 B200,每当位精度减半,FLOP 数就翻一番。出于你说的原因,也就是这种二次方的缩放关系,这个比例其实略有偏差。你本应获得比你原本想象的更大的加速。英伟达的产品规格已经开始承认这一点,在 B300 及之后的产品中,FP4 比 FP8 快三倍。

不过应该是 4 倍。

是的。我在这里展示的是整数乘法这一最简单的情形。当你处理浮点数时,比如 FP4 和 FP8,还有另一项,也就是指数,它会让计算变得更复杂。

从这里我们已经能看出什么?我认为你得出的最大观察是,面积随位宽呈二次方缩放,这非常有效,也是低精度算术在神经网络中表现如此出色的唯一原因。我们接下来要做的另一件事,是把花在乘法本身上的面积与它周围所有电路进行比较。

§03多路选择器与数据搬运的成本

16:31我们把时间稍微往回拨一点,看看 Tensor Core 出现之前的 GPU 是怎么工作的,实际上这与 CPU 的工作方式相同。我们把这个乘累加单元放在哪里?笼统地说,我来描述一个 CUDA 核心或一个 CPU。你会有一个寄存器堆(register file),它存储若干个条目,这里比如是 8 个条目、每个条目是 4 位的数,但通常是 32 位的数。

在 CUDA 核心内部,我会有一个一定深度的寄存器堆,然后有我的乘累加电路。它要做的是从这个寄存器堆中取出三个任意的寄存器,执行乘累加,然后写回寄存器堆。它会写入这一个,但它可以从这一个、这一个以及另一个随机的寄存器中读取。它会像这样接收三个输入。

这是许多处理器的核心数据通路。大多数处理器都长这样。你有一组寄存器,然后有一组逻辑单元,也就是 ALU。我们想分析从寄存器堆到 ALU 再返回的数据搬运的成本。

最终会有某个电路来说:“我不一定总是选择这一个。在任何时刻,我都可能选择任何一个寄存器。”第一个问题是:我该如何构建一个电路?我要找的这个电路是一个多路选择器(mux)。在这里,它有八个输入,分别来自寄存器堆的每一个条目,以及一个输出,也就是实际产生这个输出的那个。

这东西的成本是多少?我们手头用来构建它的只有与(AND)和或(OR)。怎么构建?我们用最笨的办法:构造一个掩码。当我们想读取第三个条目时,我们把每一个条目都与 1 或 0 做与运算,取决于是不是我们想读的那个,然后再把它们全部做或运算。

我先确认一下基本概念。多路选择器(mux)所做的就只是选择一个输入,对吗?

就是选择,软件是看不见的。你说“我要第三号输入”,这就意味着这里有一个多路选择器。

那么这个多路选择器的成本是多少?一个作用于 p 位的 n 输入多路选择器。我有 n 行。这里只有八行,每一行 p 位宽。我必须对每一个比特做与运算,所以得到 n × p 个与门。对于每一个输入,我都要决定是否把它屏蔽掉。然后我再把它们全部做或运算。

会有 (n – 1) × p 个或门。我有所有这些不同的东西,其中几乎全是 0,但我需要把它们从八个选项收拢成一个选项。每一步,我都需要把一行或到一个已有的行里。

其实挺有意思的:你平时不会在硬件层面去思考。你只是想“哦,我选第三个元素就好”,而像这样简单的事情,本身就是一个相当复杂的电路。

这是所有那些隐藏的数据搬运成本中出现的第一步。我们来做个比较。这个成本是我必须付出的。我这里有一个多路选择器,实际上乘累加运算的三个输入各需要一个,所以还有另外两份副本。这个成本是这里的 3 × n × p 个与门,而实际完成我所关心的那件事的电路里只有 p × q 个门。

如果代入实际数字,n 取 8,那么仅数据搬运就有 24 × p 个门,而仅乘加器(multiply-adder)——如果 q 是 4——只有 4 × p 个门。

这个三是从哪来的?

这里有三个不同的输入。我想暗示的是,所有这些工作都随寄存器堆的大小而增长——而这还是一个非常小的寄存器堆——所有这些仅仅把数据从寄存器堆搬到逻辑单元的工作,其成本比逻辑单元本身高出很多很多倍。

也许可以看看多路选择器长什么样,比如两位或四位的多路选择器。

我们来做一个二选一的多路选择器。我们有两个不同的数,也就是这两个输入。它们是被选择的对象,而选择信号可以是“我要这一个”,也可以是“我要另一个”。这是一种独热编码(one-hot encoding)。

这是我们的起点。我们聚焦这种情况。这是我们得到的实际输入,我们想产生这个作为结果。很费力地,我们把这一位与所有这些做与运算。这就得到这一位与这一行做与的结果。同样,我们把这一位与这一行做与。这得到全 0。这里有四个与运算。

最后,我们把这两个做或运算,得到 1。把这两个做或运算,得到 1。把这两个做或运算,得到 0。把这两个做或运算,得到 1。这就是四个或运算。这实际上看起来有点像加法。我们做了完全相同的一组与运算。我们把所有这些东西做了与运算,但接下来,我们不是用全加器电路来收拢,而是只用或门做非常简单的收拢。

但这看起来不像 n 乘以 p。

这里是 n=2 个输入的情况。在一般情况下,我们有 n 行,每行有 p 位。这就给出了 n × p 个与门。在我描述的这个电路中,几乎所有成本——八分之七的成本——都花在读写寄存器堆上,而逻辑单元本身只占极小的一部分成本。

这就是要解决的问题。这基本上就是英伟达 Volta 世代 GPU 之前的现状。CUDA 核心内部就是这种东西。正是这个问题描述,推动了 Tensor Core 的引入,Tensor Core 更一般的叫法是脉动阵列(systolic array)。

想想我们要怎么解决这个问题。我们把几乎所有的电路面积都花在了一个我们真的不在乎、而且对软件程序员隐藏的东西上,而我们真正在乎的东西却没占多少面积。想办法让这一部分变大,同时让这一部分保持同样的大小。这就是目标。

§04脉动阵列如何工作

26:10演进的过程是:在这个阶段,我们把这么多内容固化进了硬件。这一行是一次乘累加,而这一件事被固化进了硬件。脉动阵列的思路是向上跳两层循环,把这里整个循环固化进硬件。其想法是:如果我们有一块粒度大得多的固定功能逻辑,也许我们在输入和输出上付出的开销就会小得多。

有意思。听起来你的意思是,如果在矩阵乘法的循环中向上走一步,就可以让天平更偏向计算而不是通信。

没错。我们在这里会利用两种效应。一是每经过一次寄存器堆,我们可以做更多的事。二是在这个循环的某些部分,我们可以利用某些东西保持不变这一点。

直观地看,我们来看这个矩阵乘法。循环的这一部分对应于矩阵与向量的乘法。我们取一个矩阵,把它乘以一个向量。怎么做呢?每一列都与向量相乘,然后求和。我们要沿着列求和。

这个 0 和 3 与 3 和 7 相乘并求和,然后 1 和 2 与 3 和 7 相乘并求和。矩阵中的每一个条目都对应一次乘累加。我们把这四个乘累加画出来。

我确认一下为什么有四次乘累加:对应输出向量的列中的每个条目都是一个点积,在这里就是两次乘法,再加上这两次乘法结果的加法。你在累加……

严格来说每个点积只有一次加法,但我们喜欢从零开始。

但它包含了从零开始的初始化。

对。我们想要得到二次方量级更多的计算。我们的计算量是之前的 x 乘 y 倍。但我们希望通信量只有 x 倍。目的是让这个优势项按 y 增长。

我们已经铺好了乘法。我们想引入一个大小为二的向量,这已经符合我们对列的目标。这没问题。然而,我们需要处理这个矩阵的通信,它超出了我们 x 的预算。

思路是,在 AI 场景中,这个矩阵会在很长一段时间内保持不变。我们这边放着一些寄存器堆。从这个寄存器堆出来的东西量……在某种意义上,这就是我们希望按 x 增长的那一项。我们不想每个周期都把整个矩阵从寄存器堆里取进来,因为那样从寄存器堆出来的布线成本太高了。

我们的关键技巧是,这个矩阵可以存放在脉动阵列本地。我们把 0、1、2、3 这些数存放在一种叫做寄存器的门里,它们物理上存储这些数,然后我们会把这些数反复用于大量不同的向量。

这里的优化在于,矩阵乘法的本质使得你可以把这个方形的、二次方规模的东西直接存放在逻辑运算发生的地方,它相比你不断换入换出的输入多出了一个维度。

没错。

这就是矩阵乘法的本质。你做很多次乘法才得到一个值。点积是许多次乘法的结果。所以这个优化意味着,在得到一个值之前,你可以塞进去很多乘法。

对。为了把具体的样子补完整:我把这里的 3 和 2 交换了一下。就像这个 0 和 3 要与 3 和 7 相乘一样,我们要沿着这里的列形成点积。我们要把一个 3 和一个 7 送到这里。它送进这个乘法,也送进这个乘法。同样,3 送到这里,也送到这里。然后我们沿着这里求和。从一列的顶端开始,我们送入 0,然后从底部出来的就是结果。

直观地看,沿着矩阵的列执行一个点积,这恰好对应于脉动阵列中在空间上所做的事。这是一个向下求和的点积,这是第二个同样向下求和的点积。

需要进出寄存器堆的数据是什么?输出端有 x 量的数据出来,输入端也有 x 量的数据进来。至少就输入和输出向量而言,我们已经达到了目标:进出寄存器堆的数据量只有 x 这么多。

这就留下了一个问题:我说过权重矩阵存储在脉动阵列本地,那么它最初是怎么到那里的?在某个时刻,你需要启动芯片并填入这些数据,那么这些数据是从哪来的?

技巧在于我们做得非常慢。我们把它非常缓慢地一点点灌入脉动阵列。最简单的策略是,我们运行这样一条菊花链:在这里送入一个数,在下一个时钟周期它就会移动到脉动阵列的下一个条目。我们可以对每一列并行地这样做,这个也从这里来,这样就额外增加了大约 x 个单位的带宽进来。

你介意把那句话再重复一遍吗?

我们知道,我们只会很少地往矩阵里送入数字。我们只想想出任何一种构造,使得穿过脉动阵列边界的布线量被限制在 x,而不是按 x × y 增长。

一个特别简单的策略是,在一个时钟周期里往脉动阵列的最上面一行送入一个数。然后,在连续的 y 个时钟周期里,每次都往最上面一行送入数据,并把所有其他行向下移一位。这样就让需要从这个昂贵的寄存器堆出来的布线量只有 x 倍,而不是 x × y 倍。

我明白了。在通信方面有两个问题:通信时间和通信带宽。你的意思是,既然我们只会把它加载一次,那就让我们把带宽降到最低,因为带宽就等于芯片面积(die area)。我们通过更窄的通道缓慢地加载它,因为这个值会在里面保留一段时间。

完全正确。

我觉得有意思的是,上次我们谈多芯片推理时,我们要优化的那个高层大目标,是提高每单位内存带宽(也就是每单位通信)所对应的计算量。这里同样,我们也在努力提高实际乘法或加法的数量,相对于把信息从寄存器传输到逻辑电路的量。在这两种情况下,你都在最大化计算相对于通信的比例。

这在整个技术栈从上到下都会出现。这里接近最底层,接近门电路。还有一个版本也许更接近门电路,就是你选择使用的数字格式的精度。我们看到了同样的效应。无论是纯粹在 ALU 的精度上,还是在矩阵的大小上,都存在平方对线性的关系。

这个单元是更大一级的单元。我们有乘法电路,在它之上有一个相当大的脉动阵列。我画的是 2×2,但较早的 TPU 被描述为由这里所示电路组成的 128×128 阵列。这最终成为已知实现矩阵乘法最高效的电路。

我们谈过,显然你应该尽量最大化计算相对于通信的比例。那么有哪些不那么显而易见的权衡会让你夜不能寐,比如该做 X 还是做 Y,而答案并不明显?

芯片设计中的大多数决策都是规模(尺寸)决策。就在我们到目前为止画出的东西里……AI 芯片里都有这个电路。它们有一个脉动阵列,而在它附近的某个位置有一个寄存器堆来提供输入和输出。

即使在这个范围内,你要面对的规模问题是:我应该把脉动阵列做多大,把寄存器堆做多大?这两个问题是耦合的。一种思路是,设定一个预算,规定你想把芯片面积的多大比例花在数据搬运上。也许我说我想让这部分占 10%,脉动阵列占 90%。

然后我就可以确定寄存器堆的大小。更大的寄存器堆更灵活。它们能让我在应用层面获得更高的性能,但会占用本可用于脉动阵列的面积。

§05时钟周期与流水线寄存器

39:11芯片的时钟周期是在哪里起作用的?是什么决定了它?芯片的时钟周期又是什么?

首先值得注意的是,芯片是极度并行的。一块芯片里有 1000 亿个晶体管。每当你拥有大规模并行时,你需要做的一件关键事情,就是让不同的并行单元之间同步。

在软件中,通常你会用非常昂贵的同步方法,比如互斥锁(mutex)。一个线程完成它正在做的事情,获取存储在内存某处的锁,然后通知另一个线程自己已经完成。在芯片上,我们采用非常不同的方法。大约每一纳秒,芯片中所有电路都会暂停一下并同步。这就是时钟周期。整块芯片通常一下子齐步前进到下一个操作。

在电路上,它的样子是:时钟由寄存器来协调,寄存器就是我们在别处画过的那些存储器件。可以这样理解:我有一些存储单元保存着一个比特,它可能是 0 或 1。然后我有一团逻辑电路,也许是这个脉动阵列或乘法器。我有一批输入送进这团逻辑,最终它会写入某个输出寄存器。

有一个全局时钟信号驱动着所有这些寄存器。在某个确定的时刻,当时钟脉冲到来时,那一瞬间导线上的值是什么,就会被存储下来。

挑战在于,我希望时钟速度尽可能快。如果我以 2 GHz 运行,每秒完成的操作数就是以 1 GHz 运行时的两倍。但这最终意味着,我对穿过这团逻辑电路的延迟非常敏感,因为这里面发生的任何计算都必须在下一个时钟周期到来之前完成。任何芯片的一个主要优化点,就是让这个延迟尽可能短。

有意思。这里的约束似乎是:如果你加入太多逻辑,可能有赶不上时钟周期的风险;但如果加得不够,你又会白白放弃潜在的算力。有没有那种情况,是你赌一个概率,看计算能不能完成?还是说严格地要么在时钟周期内完成,要么就是没完成?

在标准的芯片设计中,你留出的裕量使得确实存在一个失败概率,但它在许多个标准差之外。无论从什么角度看,它都是一个可靠的部件,总能满足时钟要求。

有一些奇怪的例外,比如时钟域交叉,也就是从一个时钟跨到另一个时钟。那时你确实得去推算这个概率。但在主路径上,你留出的裕量是让信号提前大约 25% 个时钟周期到达,使得错过的可能性非常小。

时钟在哪里同步、寄存器放在哪里,这是你作为芯片设计者来决定的吗?还是说它是一种副产物:你想要某个逻辑序列,而你用来把 Verilog 转换成发给台积电(TSMC)的东西的软件,会自行判定为了让它工作,你必须在这里、这里和这里放寄存器,确保没有哪一步让整块芯片的时钟周期变得比必要的更长?

插入它们实际上是设计芯片的一大部分工作。它是由人工和自动方法结合完成的。

举一个最笨的做法为例:你可以把这团逻辑切成两半。不是只有一团逻辑,我可以有两团更小的逻辑完成同样的事情,只是用一个寄存器把它们隔开。如果你从中间切开,就能达到两倍的时钟频率。这很好,你得到两倍的性能,但代价是多了一个寄存器,也就意味着更多的存储。

退一步说,我们为什么需要让整块芯片同步?如果你想象玩《异星工厂》(Factorio)之类的游戏,那里没有全局时钟周期。东西做好了就是做好了。盘子里有铁,你想拿就可以拿。

沿用这个类比,你需要留意的是:如果我有两条穿过某些逻辑的不同路径。比如我在这里要做计算 f,在这里要做计算 g,然后它们要汇合进行计算 h。

会存在制造偏差。在有些芯片里 f 会稍微慢一点,在有些芯片里 g 会稍微慢一点。如果我有一个信号在传播,而 f 和 g 的结果必须在 h 处汇合,可能出错的情形是:f 先到了,于是它遇到的是 g 的上一个值,或者下一个值。

啊。而 h 需要知道什么时候开始,什么时候下一轮迭代已经……

完全正确。

这就解释了为什么在同一个工艺节点、同一种台积电技术上制造的不同芯片,可以有不同的时钟周期。两块用 3 纳米制造的芯片,可能有不同的时钟周期,取决于它们是否成功地优化到没有任何一条关键路径长到拖慢整块芯片的时钟周期。

没错。我在这里展示的这个优化叫做流水线寄存器插入。我们在流水线中间插入了一个寄存器。这纯粹是时钟速度与面积之间的权衡。这是简单的情形。还有更难的情形。我画的是一条逻辑流水线,但在其他情况下,你可能有一个计算实际上会反馈给它自己。它运行某个函数 f,然后写回自身。比如,这可能是一个加法,你在每个时钟周期加一个数。这个小电路本质上是对在不同时钟周期送来的所有数求和。

挑战在于,如果这个加法耗时太长,我能怎么办?如果我试图在它正中间放一个流水线寄存器,就会改变所做的计算。我得到的不再是对所有输入的累计和,而实际上会有两个不同的累计和。我最终会得到偶数项的累计和与奇数项的累计和。这个约束——也就是我的逻辑中有一个环路,所有芯片在某处都有这样的环路——是最难处理的,并且决定了时钟周期。

我不明白为什么有那个会成为问题。我甚至不确定在那里放一个寄存器意味着什么。它是一种原子操作吗?

嗯,加法其实并不是原子的。

正如你刚刚演示的那样。

做一次求和要做很多工作。你可以取这些工作的前半部分,在中间放一个寄存器,然后再取这些工作的后半部分。

好的。台积电提供一个 PDK,其中规定了他们能在芯片中提供给你的逻辑基本单元。由他们来确保没有哪个基本单元大于某个工艺节点所瞄准的时钟周期。但除此之外,你难道不能直接取台积电的所有基本单元,并根据需要在它们之间不断添加寄存器,直到达到你想要的时钟周期吗?

作为逻辑设计者,时钟周期是由芯片架构师设定的。比如,你从台积电拿到的基本单元,量级大致是与门或全加器。这在很大程度上取决于电压和你选择的库,但一般来说,一个时钟周期内你可以依次串联大约 10 个、20 个或 30 个这样的单元。这些基本单元非常快,可能是 10 皮秒。

作为逻辑设计者,原则上,如果你只有一个寄存器和一个与门构成的环路,你就能得到快得离谱的时钟速度,超过 4、5 或 6 GHz。但如果你拿这个非常简单的电路,看看你在这里花费的面积……这个叫做一个门当量(gate equivalent)的大小,也就是面积为一个单位。而这个东西大概是八个单位的面积。

同样,你几乎所有的成本都变成了同步或通信成本,而不是实际的逻辑。这是一个做过头了的例子。你把时钟速度做得非常快,代价是把几乎所有面积都花在了流水线寄存器上。

有意思。所以你是在暗示一种动态:你可以有非常快的时钟速度,但完成的工作并不多。你可以有低延迟但低吞吐量。

实际上这会损害你的吞吐量,因为芯片的吞吐量是每个时钟周期完成的工作量(这取决于面积效率)乘以每秒有多少个时钟周期。

这实际上与我们上次讨论的批量大小(batch size)非常相似:如果批量大小很低,任何一个用户都能很快收到自己的下一个 token,但比如一小时内处理的 token 总数会低于本来可以达到的水平。

完全正确。如果你把时钟速度推得很高,你得到的并行度就会变少。

§06FPGA 与 ASIC

51:51我记得和 Jane Street 的一位 FPGA 工程师克拉克(Clark)聊过,他帮我为我们上一次一起做的访谈做了准备。他当时在解释他们为什么使用 FPGA。我想,对于高频交易来说,吞吐量不如延迟重要,所以以确定性的方式对时钟周期进行非常具体的控制是最重要的事情。也许可以聊聊,为什么你不能直接用 ASIC 实现这一点,或者为什么你会用 FPGA 来获得确定性的时钟周期以用于高频交易。

我们来考虑 FPGA 与 ASIC 的商业论证。FPGA 和 ASIC 在概念模型上基本相同。你有一系列由小的基本单元(与、或、异或)构成的门,用导线连接起来,按固定的时钟周期运行。任何你能在 FPGA 中表达的东西,你也都能在 ASIC 中表达。在 ASIC 上,它的成本会比 FPGA 低大约一个数量级,能效也更好。

权衡在于,第一块 FPGA 花费你 1 万美元,而你做的第一块 ASIC 要花 3000 万美元,因为它需要一次完整的流片(tape-out)。FPGA 的商业用例是:你想要某种延迟非常确定、运行速度快、并行度高的东西,但你会频繁地改变工作负载,也许每个月一次。你不想每次都付那笔流片成本。

FPGA 究竟是如何在一块固定的硬件上模拟 ASIC 的编程模型的?从核心上讲,它有我们刚刚谈到的两个组成部分。它有作为存储器件的寄存器,还有提供所有门电路功能的查找表(LUT)。

然后还有第三个组成部分。我们有一大群这样的寄存器和查找表,它们由一大套多路选择器连接起来。在每一个这样的单元前面,我们都有一个多路选择器,从其他各处选择一个输入。我们有一大堆不同的选项送进所有这些单元。

这样做的本质效果是:当我对 FPGA 编程时,我可以取所有这些组件,并叠加上一种特定的连线:穿过这个查找表,送入另一个查找表,送到这个寄存器,然后再送入另一个查找表,诸如此类。

我用橙色画出来的是你如何……FPGA 的意思是现场可编程门阵列(Field-Programmable Gate Array)。橙色的是在现场被编程的部分,而白色的则是 FPGA 中为了首先把这个器件造出来而必须存在的所有导线。

“在现场被编程”是什么意思?

在现场被编程,意思是器件已经部署在数据中心里了。它就放在外面的世界里,然后你可以过去对它编程。

啊,这里的“field”不是电场的那个场。是“现场”的意思,就是在外面的世界里,明白了。

如果我看现场编程是如何从第一个查找表出来、再进入第二个查找表的,这是怎么工作的?

实现这一点的导线在哪里?我画这些的时候有点偷懒了。这里每一个器件前面都有一个多路选择器,可以从所有附近可用的电路中进行选择。FPGA 的实际配置就等同于多路选择器的控制。在这个多路选择器里,我们有数据输入,也有负责选择的控制信号。

每一个多路选择器旁边都有一个小存储器件,说明“你的输入要来自这里”。编程就是配置每一个这样的多路选择器。

明白了。那查找表内部发生了什么?

查找表也会有一点控制信号来告诉它该做什么。它的目的是可配置地充当与门、或门、异或门或其他任何这些不同的东西。你可以考虑很多种实现方式。在传统 FPGA 中的做法是……一个查找表有四位输入和一位输出。从四位到一位,有多少种不同的函数?有 16 种不同的函数。

你可以把它列成 16 个不同的数。你有一张表,比如 0111001……共 16 个条目。这张表就存储在这个蓝色的配置位中。它把这四个比特看作二进制数,查找表中相应的那一行,并输出那个比特。这本质上就是查找表的真值表视角。

好的,所以如果你想想与门、或门、或非门、异或门,这些都接受作为输入的……

那些是二输入函数。有时我们会有三输入函数,比如三路异或,或者四路异或。

在这种情况下,它是不是只取决于它有多大?

查找表的典型规模是四个输入。这算是一个最佳平衡点。这里还有另一种计算与通信之间的权衡。如果输入太少,你就需要使用更多的查找表。

基本上,查找表就是一张真值表。有了真值表,你可以编程出任何你想要的门。所以与其说是查找表,你也可以把它看成一个可编程的门。

没错。你在这里可以做的一件事,就是看看“FPGA 比 ASIC 贵一个数量级”这条经验法则是从哪来的。你数一数这个查找表里会有多少个门。

我们可以把这个查找表本质上看作这些多路选择器中的一个。它必须在 16 个不同的值之间选择,所以它是一个有 n=16 个选项、p=1 位的多路选择器。正如我们之前看到的,这个电路的成本是 n × p 个门。所以成本是 np,也就是 16 个与门,外加 16 个或门。

这个电路就是那个多路选择器?

没错,就是多路选择器。

是进入查找表的那个多路选择器?

查找表本身可以看成是一个大的多路选择器,从全部 16 行里选出一个输出。这就是查找表。

但按你在这里画的方式,是先有一个多路选择器,然后是一个查找表。

这里是一层层都是多路选择器。这里面还有第二个多路选择器。这个多路选择器就是这个多路选择器。

而另一个多路选择器只是在说……

它是从这一团门电路里的哪里来的。

对,而第二个多路选择器是说,“好,现在你有了一个值,但这个值仍然是一个四位的值。”

是的,我从这一团“汤”里选出了四个比特。然后我用这四个比特来选择我要使用查找表中的哪一个条目。

假设在第一个多路选择器里,你从附近八个寄存器中取输入。那么一共有 32 位进来。从中出来四位。这四位进入第二个多路选择器,它位于查找表内部。

在这种情况下,这些寄存器是单比特寄存器。如果附近有八个寄存器和查找表,那么附近一共有八个比特进来。我从八个中选出四个不同的值。实际上有四个不同的多路选择器,每个输入比特都配有一个小的多路选择器。每个都是八选一。

那八个是从哪来的?

来自附近的寄存器和其他查找表。

每个寄存器是一个比特。

是的。

我想 AMD 或者其他制造这些 FPGA 的厂商,仍然要对哪些寄存器连接到哪些寄存器有自己的设定。你可以编程实际的门,但他们在连接上加了导线……也就是通信拓扑,对吧?

你在局部粒度上拥有灵活性。有一个附近的邻域可以供你选择,但对于更粗粒度的长距离连接,他们会预先做出设定。

那它慢 10 倍的原因是什么?

看看构建这个查找表的成本,是 32 个门。它能给我的等效功能是——我能在这里做的一件有意思的事——一个四输入与门。四输入与门的意思是:与、与,然后再对两个与的结果做与。这个电路我在 ASIC 里直接用三个与门就能实现。用 LUT 我也能实现它,但需要 32 个门,而不是三个。

所以这个开销实际上来自这样一个事实:描述真值表有比逐一列出所有可能的输入组合更简洁的方式,那就是直接把门写出来。

对,也就是把多晶硅和导线等等布置出来。

§07缓存与暂存器

63:25有意思。你对我强调过的一个重要点是,他们之所以更喜欢 FPGA 而不是 CPU,是因为可以得到确定性的时钟周期。他们知道数据包什么时候进来、什么时候出去。为什么 CPU 里不能保证这一点?

实际上你也可以设计出延迟确定的 CPU。事实上,许多 AI 芯片内部的处理器也具有确定的延迟。Groq 就宣传过这一点。TPU 的核心里也是这样。

挑战在于同时获得确定的延迟和高速度。非确定的延迟来自 CPU 中特定的设计选择。实际上完全可以去掉这些设计选择,做出延迟确定的 CPU,但这类 CPU 在市场上不太有吸引力,所以人们不再制造这种 CPU 了。

从某种意义上说,确定的延迟是一个更简单的起点,而一些芯片设计者加入了一些东西使它变得不确定。举一个具体的例子,CPU 上最重要的非确定性来源可能就是 CPU 缓存本身。

在 CPU 中,你有 CPU 裸片本身,旁边是 DDR 内存。你在裸片内部有一个缓存系统,它记住最近对 DDR 的访问并把它们存起来。当我运行 CPU 指令时,每次遇到访问内存的指令,它都会先检查数据是否存放在缓存里。如果没有,就从 DDR 取。

这是一个巨大的优化。缓存比 DDR 快两个数量级。如果你从不使用缓存,基本上所有程序都会慢一百倍。缓存的存在对于 CPU 以合理速度运行是绝对必要的。

但你能否命中缓存,取决于 CPU 的周边环境:还有哪些其他程序在运行、最近运行过什么,以及缓存系统内部的随机数生成器在做什么。这是 CPU 运行时间非确定性的一个主要来源。

这就是 CPU 的内存系统。你可以做得不同的一大点是:不再由硬件说“我要读内存”,然后由硬件决定它是否来自缓存,而是你可以把这个决定固化进软件,这是另一种设计哲学。

比如在 TPU 中你就能看到这一点。我会画同样的图,但把它叫做暂存器(scratchpad)。主要区别在于……这是一个 TPU,这里用的是 HBM 而不是 DDR,但它仍然是片外存储器。不是软件说“先访问内存”然后让硬件决定,而是有一种指令去访问暂存器,另一种完全不同的指令去访问 HBM。

这种风格通常被称为暂存器,而不是缓存。关键区别在于,你有一种指令表示“读或写暂存器”,和另一种完全不同的指令表示“读或写 HBM”。

所以暂存器就是缓存。

对,这里这个东西就是暂存器。

§08为什么 CPU 核心比 GPU 核心大得多

67:27退一大步来说:人们说计算机采用“冯·诺依曼架构”,也就是对信息进行串行处理。也许只是因为我们一直在谈并行加速器,但 FPGA 是超级并行的。AI 加速器,也就是 TPU,是超级并行的。如果你想想它们有多少核心,甚至 CPU 也是超级并行的。那么现代硬件在什么意义上真的是冯·诺依曼架构?这样描述现代硬件真的公平吗?

我认为用它来描述 CPU 是公平的。CPU 上的并行度大约是 100 个核心乘以也许 16 路的向量单元,所以 CPU 上大约是 1000 路的并行度。

有一个问题:CPU 用的是一块裸片,如果线程更少,仅从晶体管电压开关的角度来说,是不是真的只有一条控制流——裸片上的一小部分——有电压在开关?

你究竟是怎么占满 CPU 的裸片面积的……

如果核心这么少,你把整块裸片都花在什么上了?

对,那里发生了什么?

核心只是大得多、复杂得多。我们应该拿一个占裸片百分之一的 CPU 核心和一个 LUT 比较。一个 LUT 只有 16 个门。这就清楚地说明了为什么 FPGA 里的 LUT 比 CPU 里的核心多那么多。

但为什么比如说 CUDA 核心比 CPU 核心多?CPU 和 GPU 有什么区别?在 CPU 内部,面积的一大用途是缓存。真正用于 ALU 的非常少。大部分是这些寄存器堆而不是逻辑单元。这两者在 GPU 中都有对应物,所以这不是一个很大的区别。

但在 GPU 中没有对应物的东西是分支预测器。CPU 里有一大片面积完全是一堆预测器,用来预测下一个分支在什么时候以及分支目标在哪里。去掉其中大部分内容,再加上把这些寄存器堆做得更紧凑,是 GPU 相对 CPU 获得的许多收益的来源。

分支预测器的作用是什么?是同时执行两个分支,还是它具体做什么?

问题在于,当我有一系列指令时,如果其中有一个分支,处理一条指令的实际步骤要花很长的时间。大概要五纳秒。

发现我遇到了一个分支、计算布尔值是否为真、把程序计数器更新到新的目标,然后从指令存储器中读取,这个过程可能需要五纳秒才能完成。所以实际上,这可能要到下面这里才完成。我想运行的时钟速度要比五纳秒所允许的快得多。五纳秒对应 200 MHz 的时钟速度。我希望以 1 GHz 或 2 GHz 运行。

所以我需要在分支被求值的同时运行其他指令。我只是想继续运行我后面的那些指令。但那也可能是错的。如果分支最终被采纳了,那么我就需要知道:我其实不该计算这些指令,而是需要跳转到目标位置去运行那些指令。分支预测器的作用,就是提前五个周期预测一个分支将要发生,甚至在你还没执行到那条指令之前。

§09大脑与芯片

72:00如果我想想大脑的工作方式,与你在这里描述的相比,在高层次上的差异可能是:在这些加速器中你可以做结构化稀疏,从而节省本来必须分配给门电路的一部分面积,而在大脑中是非结构化稀疏。任何神经元都可以连接到任何其他神经元,而且不是以那种列对齐的方式。

然后还有一点是存储与计算位于同一位置。不过我想你也可以说,在这些裸片上存储和计算在某种意义上也是共置的。

从某种意义上说,这恰恰就是存储与计算的共置。

所以也许这不是一个很大的区别。另一个很大的区别是,大脑的时钟周期比计算机慢得多。部分原因是为了节省能量,因为时钟周期越快,为了让信号稳定下来并识别晶体管处于什么状态,所需的电压就越大。

没错。

我不知道你对大脑可能在做什么,与这些芯片的工作方式相比,有没有什么评论。

我们先谈时钟速度。芯片上的时钟速度相当高,因为这能带来更高的吞吐量。当我们比较运行某个工作负载的 GPU 时,它运行的批量大小是 1000。而大脑运行的不是批量大小 1000,世上只有一个我。

你可以想象这样说:“拿一块 GPU,不让它以 GHz 运行,而是以 MHz 运行”,这看起来就会更像你说的大脑中的对应情形。但按硅的工作方式,这并不会给你带来 1000 倍的能效优势。

最终的情形是:你只是让这个电路运行一次直到稳定,然后它会闲置很长一段时间。它在闲置时并不会消耗很多能量,因为大部分能量消耗在把比特从零翻转到一再翻回来的过程中。

我们来谈谈这样一个电路的能耗。理解存储一个比特的方式是:你向芯片中某处隐含的一个电容里存入了一些电荷。当比特变成 1 时它被充电,而下一次变成 0 时它被放电。

给电容充电、再把这些电荷倒入地的这个循环,就是能量被消耗的地方。这叫做动态功耗或开关功耗,它占芯片能耗的大部分。还有一些其他的能耗,仅仅来自绝缘体并不完美这一事实,但我们先不考虑它。大部分能耗来自从零翻转到一再翻回零。

如果你让芯片运行得慢得多,每一千个时钟周期才给它一次时钟,那么翻转次数会少 1000 倍。能耗会大约少 1000 倍。但这并不是能效上的一个实质性优势。

§10GPU 不过是一堆微型 TPU

75:33好的,你已经在高层次上描述了 TPU 是如何工作的。那么 GPU 和 TPU 在高层次上的工作方式有什么区别?

有一个高层次的组织原则是不同的,然后在核心内部,东西也是不同的。从高层次来看,我们取一块 GPU 和一块 TPU,看看顶层的模块结构是什么样子。

如果你把它看作每种情况下的整块芯片,GPU 的组织方式主要是一堆几乎相同的单元,也就是 SM。中间有 L2 存储器,下面还有更多这样的 SM。所以有一个相当规则的核心网格。

相比之下,如果我们看 TPU,你会得到粒度粗得多的逻辑单元。最终只有少数几个矩阵单元,也就是那些大型脉动阵列。中间有一个向量单元,下面是你的矩阵单元。这些矩阵单元加上中间的向量单元,构成了整块 TPU 芯片。

你可以设想把它缩小成一个非常小的单元,带一个更小的矩阵单元和一个更小的向量单元,这大致就是一个 SM。从非常高层的角度看,GPU 就是在整块芯片上平铺了许多微型 TPU。

哦,有意思。你的意思是,流式多处理器(SM)内的 Tensor Core 类似于 MXU?

对,它们都非常相似。

我明白了。如果你的结构性更弱,有一堆微型 TPU 就很合理。而如果你只有巨大的矩阵乘法,你可能想避免让每个 SM 都有自己的寄存器和 warp 调度器所带来的成本。为什么不直接做一个巨大的东西,把这些成本摊销到整个东西上?

这体现在你能把东西做到多大上。我们已经看到过这个主题,尤其是脉动阵列:更大的脉动阵列能更好地摊销寄存器堆的成本。

这种设计允许你拥有更大的脉动阵列,而 GPU 的设计则限制你只能拥有各种小单元。不过这里有一个权衡。由于这种粗粒度的分离,你需要把大量数据从向量单元搬到矩阵单元,而这里只有两条边界线可通过。

如果你看 GPU 中的对应部分,到处都有向量单元,你可以通过许多不同的线路搬运数据。向量单元和矩阵单元之间可以搬运的数据量,在 GPU 中实际上比在 TPU 中高得多。不是必须让所有数据都通过仅仅两条线路,而是在 GPU 里通过 16 条连线来搬运。

对。而且你可能需要跨越的面积也更小。

这同样是一种节能。所以如果你能完全在一个 SM 内部运作,数据搬运就小得多。但一旦你想跨 SM 运作,就会变得更复杂、更昂贵。

所以你不必评论,但人们可能会预期 MatX 可能尝试做的是:获得 GPU 式的、被 SRAM 包围的更小结构的脉动阵列,同时让你在 SM 中为支持 CUDA 架构所需要的那些占用大量空间的东西可以被舍弃。

我们公开谈过一种我们称为可拆分脉动阵列(splittable systolic array)的东西,从某种意义上说,你可以把它想成既是大的脉动阵列、也可以是小的脉动阵列。

很好。好的,我想这是一个不错的收尾。赖纳,非常感谢你。

谢谢,德瓦克什。

— 读到这里,多懂一点。 —