从逻辑门解析GPU/TPU/FPGA与人脑芯片设计差异Reiner Pope – Chip design from the bottom up
Reiner Pope演讲从基础逻辑门出发,对比分析了GPU、TPU、FPGA及人脑芯片的设计哲学,揭示不同架构(如并行计算vs模拟信号)如何导致它们在性能、能耗和应用场景上的显著差异。
Dwarkesh Patel
与Reiner Pope的新黑板讲座:芯片究竟如何工作——从基础逻辑门讲起,逐步解析为何GPU、TPU、FPGA和人类大脑各自呈现特定形态。
Reiner是初创芯片公司MatX的CEO(需说明:本人是其天使投资人)。他此前曾在谷歌工作,负责软件效率、编译器及TPU架构相关项目。
请在YouTube观看这场讲座,亲眼看看黑板上的讲解。
00:00:00 —— 用逻辑门构建乘积累加单元
00:16:31 —— 多路复用器与数据传输成本
00:26:10 —— 脉动阵列工作原理
00:39:11 —— 时钟周期与流水线寄存器
00:51:51 —— FPGA vs ASIC
01:03:25 —— 缓存 vs scratchpad
01:07:27 —— CPU核心为何远大于GPU核心
01:12:00 —— 大脑 vs 芯片
01:15:33 —— GPU本质是大量微型TPU集群
Dwarkesh Patel
再次邀请MatX CEO Reiner Pope,这家新成立的AI芯片公司。上次我们聊了数据中心内部运作,这次想深入芯片内部——芯片究竟如何工作?需声明:我是MatX的天使投资人,希望你们设计出了好芯片。
Reiner Pope
希望能做到。我将从芯片设计的最小基本单元讲起,逐步展开实际生产芯片的组成结构。芯片最底层的基本元件是逻辑门(如AND/OR/NOT),这些元件通过物理金属导线连接构成电路。
AI芯片的核心计算任务是矩阵乘法,其基础运算单元是数值对的乘积累加(multiply-accumulate)。下面将手动演示这一计算过程,并推导出对应电路实现方式。
以四位数相乘为例会更清晰。乘积累加是最直观的基础运算单元——先计算两个项的乘积,再累加到一个八位数中。
Dwarkesh Patel
能否请教一个问题?为何乘积累加是计算机内部运算的自然基础单元?
Reiner Pope
有几个原因。它效率略高,但 AI 芯片之所以自然采用这种设计是因为矩阵乘法的本质:矩阵乘法是什么?简单来说就是三重循环——对输出 [i, k] += input[i, j] × other_input[j, k],每一步都会进行一次乘积累加操作。
另一个观察是:在累加阶段精度几乎总是高于乘法阶段。这是 AI 芯片的特性——你先用低精度数相乘,再累加时误差会快速累积,因此需要更高精度。这就是我们选择四比特乘法与八比特加法的原因。
Dwarkesh Patel
让我确认下理解是否正确。有两种理解方式:一是结果值比输入大;二是如果是浮点数的话……这部分可能不太直观。但原理是不是类似?
Reiner Pope
确实原理相同。另一条独立原理是:当你把这些数不断累加时,会有大量舍入误差累积。而这里只有一次乘法链中的乘法运算,因此乘法过程中累积的舍入误差较少。
Dwarkesh Patel
为什么要把一堆数累加起来?那里只有两个数啊。
Reiner Pope
这个求和过程要重复执行 j 次。
Dwarkesh Patel
误差会累积。我明白了。
Reiner Pope
如果手工计算怎么做呢?人类通常会拆成两步,但我们可以用长乘法一次性完成。
先处理乘法项:我们要把四位数分别乘以另一个四位数的每一位。比如 1001 乘第一位(自身),左移一位乘第二位(得全零),再左移一位乘第三位(得 1001),最后一位又得全零。
这些乘积项需要相加,同时直接累加初始累加器项。最终要计算的是五项之和。
实现这一中间步骤用了多少逻辑门?我们需要生成这 16 个部分积。以这里的 1 为例:它是这两个比特位都为 1 时的 AND 结果。若任一为 0,则结果必为 0。因此生成所有部分积共需 16 个 AND 门。通用情况下 p 位乘 q 位乘法需要 p×q 个 AND 门。
最后进行求和。主要工作都在加法环节。这里用到的另一种逻辑门是芯片上最简单的 AND 门,而最大规模的逻辑门通常是全加器(full adder)。
如果你来自软件领域,可能会认为全加器是把两个32位数相加。但实际上它只是将三个单比特数相加,可以想象成把0、1和1相加。把这些数加在一起的结果可能是0、1、2或3,因此用二进制只需要两位就能表示。输入有三个比特位,输出有两个比特位。二进制中的2是10。这也称为一个3→2压缩器,因为它接收三个比特的输入并产生两个比特的输出。
Dwarkesh Patel
为了确保我理解正确:两个输入是一个X值和一个Y值,再加上之前进位的……
Reiner Pope
这三个输入是同一比特位置上的比特,比如这里的三列比特。我把两个输出画成垂直和水平的形式,以匹配这种垂直与水平的布局。这表示同一列中的东西处于相同的比特位置,而相邻列中的东西则不同。这是进位输出,而这个是求和结果。
Dwarkesh Patel
所以如果全加器的输入是101,那么输出将是10。如果是111,输出就是11。如果是000,输出则是00。如果是010,输出仍然是01。明白了。
Reiner Pope
没错。它本质上只是在计数并将这个数以二进制形式表达出来。这个电路捕捉了我们人类在逐列求和时的自然操作。
我将展示如何使用全加器进行求和的一个迭代过程。这里的求和方式对人类来说可能有点不自然。我们会逐列求和并记住进位,但在这里我们不会记住进位,而是明确地把它写出来。我们从最右边的列开始向左进行。在最右边的列上,我们把1和1相加,得到这里的0和进位1。我们已经在这个比特对上使用全加器电路,并得到了两个比特作为输出。
现在我们可以对这个列做同样的事情。我们有四个数字组成的列,所以我们取前三个,运行全加器,得到的输出是0和0。这些的和是00。这就是对这些比特应用的全加器。每用完一些比特,我就划掉它们,表示我已经处理过这些比特了。
让我们继续前进一点。我取这三个数字,相加后得到1和0。我已经处理了这三个数字。现在我取这些三个数字相加,得到1和0,我也处理了这些数字。
看待这个问题的方式是我有一个需要相加的整个数字网格。我会不断在这个网格上应用全加器,从某一列中不断取出三个数字,并写出两个数字作为输出。反复进行这个过程,直到最终只剩下一个单独的数字输出。
这种方法被称为Dadda乘法器。这是使用全加器实现面积高效乘法的标准方法。让我们量化一下这个电路的规模,以便了解其大小,并在以后进行比较。
我用了多少个全加器?开始时有多少个数字?我有16个部分积,这是所有这些项与所有这些项的乘积,加上这里要添加的八项。我一开始有24个比特。最终我产生了8个比特输出。在每一步,我都划掉三个数字,并写出两个数字作为结果。
全加器的每一次使用都会消除这里的其中一个比特。那么需要多少个全加器呢?应该是24减去8,因此这个电路中共有16个全加器。在一般情况下也是如此,这个电路中将会有p乘以q个全加器。
Dwarkesh Patel
让我确保我理解其中的逻辑。输入比特数是24,即p乘以q,再加上p和q。输出比特数仅仅是p加上q。所以p乘以q加上p和q,再减去p和q就等于p乘以q。
Reiner Pope
没错。我认为这解释了至少暗示了我们选择做乘积累加的第二个原因。第一个原因是它在矩阵乘法中出现。第二个原因是它给我们带来了非常简洁、简单的p乘以q的代数运算。
我们已经描述了整个流程。这里我所做的每一个原子步骤都变成了一个逻辑门,然后导线连接在一起。当我用这三个输入产生这两个输出时,如果我将此映射到物理设备上,就会有导线将这三者连接到一个产生输出的逻辑门上。
这是AI芯片内部的主要基本构建模块,在不同位宽下。我们将从这里开始,说明如何使用它来运行所有其他可能需要的操作。
Dwarkesh Patel
这可能不是问这个问题的好时机,但每当Nvidia报告该芯片可以执行X次FP4或一半次数FP8时,似乎意味着这些电路是可互换的,没有专用的FP4与FP8之分。但根据你在这里的描述,如果需要映射到逻辑中,你可能需要一个专用的FP4乘积累加器和一个专用的FP8累加器。它们是否可以“互换”?
Reiner Pope
如所绘制的,它们并不是特别可互换。实际上,设计芯片时你需要做出的一项主要决定是:我应该分配多少FP4和多少FP8?有时我会从客户需求的角度考虑这一点。另一个角度是在FP4和FP8之间均衡功耗预算。
Dwarkesh Patel
当他们报告这些数字,恰好FP4的数量是FP8的两倍时,他们只是选择给所有浮点数分配等效的芯片面积,结果最终变成……?
Reiner Pope
为什么比例正好是2倍?部分原因肯定不会完全等同于芯片面积。还有数据传输的原因。我们可能在查看数据如何进出内存时再来讨论这一点。从软件层面来看,我可以将两个四位数打包进与一个八位数相同的存储空间。当我存储到内存时,芯片内线的总线尺寸安排使得这一过程非常高效。
Dwarkesh Patel
想想看,不仅仅是2倍。所需面积听起来是与比特长度成二次方关系。这就是为什么更小的精度比你可能认为的更有优势。
Reiner Pope
这是一个非常重要的原因。事实上,Nvidia曾做过更改。直到B100或B200之前,每当你将比特精度减半,FLOP计数就会翻倍。由于你提到的这种二次缩放,这个比例实际上是稍微错误的。你应该获得比你原先想象的更大的加速效果。Nvidia的产品规格从B300开始已经承认这一点,其中FP4比FP8快三倍。
Dwarkesh Patel
不过应该是4倍才对。
Reiner Pope
没错。我在这里展示的是最简单的整数乘法运算。当处理浮点数(如FP4和FP8格式)时,还有一个指数项会使得计算变得复杂化。
从这一点我们已经能看出什么?我认为你观察到的主要结论是:位宽存在二次方缩放效应,这种效应非常有效,也是低精度算术在神经网络中表现良好的唯一原因。接下来我们要比较乘法运算本身与其周围电路所占的面积。
Reiner Pope
让我们稍微回顾一下历史,看看在Tensor Core出现之前的GPU是如何工作的——其实这和CPU的工作原理完全一样。我们将把乘加单元放在哪里?这里以CUDA核心或CPU为例。设备会有一个寄存器文件,存储若干条目,比如这里的4位数字,通常是32位数字。
在CUDA核心内部,我将有一个深度为N的寄存器文件,然后是我的乘加电路。它的功能是从寄存器文件中读取三个任意寄存器,执行乘加操作,然后将结果写回寄存器文件。它会写入这个寄存器,但可以从这三个寄存器中的任意一个读取。
这是许多处理器核心的数据通路。大多数处理器都类似:一组寄存器和一组逻辑单元(ALU)。我们需要分析从寄存器文件到ALU再返回的数据传输成本。
最终,电路需要决定:“我不总是必须选择这个寄存器,在任何时刻都可以选择任意寄存器。”第一个问题是:如何构建这样的电路?我们要寻找的是一个多路复用器(mux),它有八个输入(每个寄存器文件条目一个输出),产生一个输出。
这东西的成本是多少?我们只需要用AND和OR门来构建它。怎么做呢?用最笨的方法。我们生成一个掩码。如果要读取第三个条目,就将每个条目与1或0进行AND操作(取决于是否需要读取该条目),然后将所有结果进行OR操作。
Dwarkesh Patel
为了确保我理解基本概念:这个mux的作用只是选择输入吗?
Reiner Pope
只是选择,对软件不可见。你说“我想要第三个输入”,就意味着这里有一个mux。
那么这个mux的成本是多少?一个n输入、p位的mux。我有n行(这里是八行),每行宽p位。我需要对所有位进行AND操作,因此有n x p个AND门。对于每个输入,我必须决定是否屏蔽它。然后我会将所有结果进行OR操作。
会有(n - 1) x p个OR门。我需要将这八个选项压缩成一个选项。每一步,我都需要将一行数据合并到现有行中。
Dwarkesh Patel
有趣的是,你并不考虑硬件层面的细节。你只是简单地说“我要选第三个元素”,而如此简单的操作背后却是一个相当复杂的电路。
Reiner Pope
这是所有隐藏数据移动成本显现的第一步。我们只需进行比较。我需要支付这笔费用。这里有一个多路复用器(mux),实际上,对于我的乘积累加操作的三个输入源,我还有另外两个这样的副本。这里的成本是3 × n × p个与门,而实际执行我关心的操作的电路中只有p × q个门。
如果我们代入具体数值,当n为8时,仅数据移动就需要24 × p个门,相比之下——如果q为4——乘加操作仅需4 × p个门。
Dwarkesh Patel
这个“三”是从哪里来的?
Reiner Pope
这里有三个不同的输入源。我想强调的是,所有这些工作随着寄存器文件大小成比例增长——而这是一个非常小的寄存器文件——仅仅将数据从寄存器文件传输到逻辑单元的工作量,比逻辑单元本身昂贵得多。
Dwarkesh Patel
也许看看一个多路复用器的样子会有所帮助,比如一个二进制的或四进制的mux。
Reiner Pope
我们将做一个二路mux。我们有两个不同的数字,这两个输入是我们要在两者之间选择的,选择器可以是“我想要这个”或“我想要另一个”。这是一热编码。
我们从这里开始。让我们专注于这种情况。这是我们实际得到的输入,我们希望产生这个结果。我们通过将这个位与所有这些进行AND运算来非常费力地实现这一点。这产生了与这一行的AND运算。同样,我们将这个位与这一行进行AND运算。这产生了全零。这里有四个AND门。
最后,我们将这两者进行OR运算,得到1。将这两者进行OR运算,得到1。将这两者进行OR运算,得到0。将这两者进行OR运算,得到1。这就是这四个OR门。这最终看起来有点像加法。我们完全做了相同的AND运算。我们已经将这些东西全部进行了AND运算,但并不是通过全加器电路来压缩它,而是仅仅使用OR门进行非常简单的压缩。
Dwarkesh Patel
但这不像n乘以p。
Reiner Pope
这是在n=2个输入的情况下。在一般情况下,我们将有n行,每行有p位。这将给我们n乘以p个AND门。在我描述的电路中,几乎全部的成本,即八分之七的成本,在于读取和写入寄存器文件,而逻辑单元本身的成本只占极小一部分。
要解决的问题是:在Volta一代Nvidia GPU之前,这种状况基本如此。类似的东西就藏在CUDA核心里。这个问题陈述正是推动了Tensor Cores的引入,它们更一般地称为脉动阵列(systolic arrays)。
想想我们要如何解决这个问题。我们几乎把整个电路面积用在了程序员并不关心且对软件透明的某事上,而我们真正关心的事情却只占用了很少的面积。如何使这部分变大一些,同时保持这部分大小不变?这就是目标。
Reiner Pope
当时的演进是,我们已经将这些功能固化到硬件中。这一单行代码就是一个乘积累加操作,而这一整个功能也被直接固化在硬件里。脉动阵列(systolic array)的思路是向上提升两层循环,并将这个完整循环全部固化到硬件中。其核心思想是:如果采用更大粒度的固定功能逻辑单元,或许输入和输出的开销会显著降低。
Dwarkesh Patel
有意思。听起来你是在建议,如果在矩阵乘法循环中再推进一步,就能更偏向计算而非通信?
Reiner Pope
没错。这里我们要利用两个效应。一是每次通过寄存器文件时能处理更多数据;二是循环中的某些部分可以借助某些保持不变的特性来优化。
直观来看,我们要分析的是矩阵乘法。这段循环对应一个矩阵-向量乘法。我们取一个矩阵和一个向量相乘怎么做呢?每列分别乘以该向量,然后求和。我们是沿着列方向求和的。
这里的0和3分别与3和7相乘后求和,1和2也分别与3和7相乘后求和。矩阵中的每个元素都对应一个乘积累加操作。我们将画出这四个乘积累加过程。
Dwarkesh Patel
为了确保我理解为什么有四个乘积累加:输出向量对应的每一列元素是一个点积,这种情况下会有两次乘法运算,然后将这两个乘积相加。你是说累加...
Reiner Pope
严格来说每个点积只涉及一次加法,但我们习惯从零开始初始化。
Dwarkesh Patel
但这也包括了初始化为零的操作。
Reiner Pope
是的。我们希望计算量呈平方级增长,即现在的计算量是之前的x乘以y倍。但目标是将通信开销控制在x倍以内。我们希望这个优势项达到y的效果。
我们已经完成了乘法运算。现在需要引入大小为2的向量,这与我们的列目标一致。没问题。但我们需要管理这个矩阵的通信,它超出了我们的预算x。
在AI场景中,这个矩阵会在很长一段时间内保持不变。我们这边有一些寄存器文件。从这个寄存器文件出来的数据量……我们希望这部分开销控制在x级别。我们不希望每个周期都从寄存器文件中拉入完整的矩阵,因为这样会导致寄存器文件的连线开销过大。
关键技巧在于,这个矩阵可以直接存储在脉动阵列附近。我们把数字0、1、2、3存入一个称为寄存器的物理存储单元,并反复复用这些数值来处理大量不同的向量。
Dwarkesh Patel
这里的优化点是,矩阵乘法的特性允许将这个方形二次结构直接存储在逻辑运算的位置上,相比不断交换输入的维度,这里多了一个额外的维度。
Reiner Pope
没错。
Dwarkesh Patel
这就是矩阵乘法的本质。你需要进行大量乘法运算才能得到一个结果值。点积是多次乘法的结果。因此这种优化意味着你可以在获取某个输出值之前塞入大量乘法运算。
Reiner Pope
没错。为了具体说明这个过程:我把这里的3和2交换了。就像这里的0和3会与3和7相乘一样,我们将在列方向上形成点积。我们将把3和7输入到这里。这个数值会参与这里的乘法运算,也会参与那里的乘法运算。同理,3会同时参与这里的运算和那里的运算。然后我们沿着这里求和。从一列的顶部开始输入0,最后底部会得到计算结果。
直观来看,矩阵中的列方向进行了点积运算,这与脉动阵列的空间处理方式完全对应。这是垂直求和的第一个点积,这也是垂直求和的第二个点积。
寄存器文件需要输入和输出的数据是什么?我们有x个数据量从输出端流出,也有x个数据量从输入端流入。至少对于输入输出向量而言,我们已经实现了寄存器文件的数据进出量仅为x的目标。
这引出了另一个问题:我提到权重矩阵存储在脉动阵列中,那么这些数据最初是如何进入的呢?芯片启动时需要加载这些数据,它们最初来自哪里?
关键在于我们非常缓慢地传输数据。通过菊花链方式极慢地将数据注入脉动阵列:最简单的策略是在这里输入一个数,下个时钟周期它会移动到下一组脉动阵列单元。我们可以并行地在所有列执行此操作,这也来自这里,这样我们就获得了约x倍带宽的输入。
Dwarkesh Patel
能否再重复一遍刚才的话?
Reiner Pope
我们知道矩阵中只会偶尔引入新数字。我们只需设计任何结构,使得穿过脉动阵列边界的连线数量不超过x,而不会达到xy级别。
特别简单的策略是:在一个时钟周期内将一个数输入到脉动阵列的最上行。随后连续y个时钟周期每次都将最上行数据输入,并让其他行整体下移一位。这样,来自昂贵寄存器文件的连线数量仅保持为x而非xy。
Dwarkesh Patel
明白了。关于通信有两个考量因素:通信时间和通信带宽。你表示由于我们只需要加载一次,所以应最小化带宽——因为带宽等于芯片面积。我们通过更小的通道缓慢加载,因为这些值会被长期保留在阵列中。
Reiner Pope
正是如此。
Dwarkesh Patel
有趣的是,上次我们讨论跨芯片推理时,需要优化的核心目标其实是提升每单位内存带宽(即每单位通信)的计算量。换句话说,我们要尽量减少寄存器到逻辑单元间数据传输的次数,转而实际增加乘法或加法运算的比例。在这两种情况下,关键都是最大化计算量相对于通信量的比值。
Reiner Pope
这种优化贯穿整个技术栈的各个层级。这里接近底层——晶体管层面。甚至还有更贴近晶体管的优化方式,取决于你选择的数值精度格式。我们观察到了同样的效果:无论是ALU运算精度还是矩阵规模,都存在平方项与线性项的权衡。
这个单元是更大的下一级模块。基础是乘法电路,其上层是一个相当大的脉动阵列(systolic array)。我画的是2x2结构,但早期TPUs描述的却是128x128规模的此类电路。事实证明这是目前实现矩阵乘法的最高效电路设计。
Dwarkesh Patel
我们已经谈到"最大化计算/通信比"似乎是显而易见的准则。那么有哪些反直觉的权衡因素会让你彻夜难眠?比如面对X和Y的选择时,答案并不明显。
Reiner Pope
芯片设计中大多数决策本质上是尺寸选择问题。当前已绘制的AI芯片架构都包含这个基本电路——脉动阵列,以及附近提供输入输出的寄存器文件。
即便在这个范围内,仍需解决两个关键尺寸问题:我的脉动阵列该多大?寄存器文件该多宽?这两个问题相互耦合。一种思路是为数据移动分配芯片面积预算,比如设定10%用于数据通路,90%留给脉动阵列。
基于此预算即可确定寄存器文件大小。更大的寄存器文件能提升应用层性能,但会挤占脉动阵列的面积资源。
Dwarkesh Patel
芯片的时钟周期从何而来?它又由什么决定?什么是芯片时钟周期?
Reiner Pope
首先必须注意到芯片具有惊人的并行度——单颗芯片就有1000亿个晶体管。当存在大规模并行时,关键挑战在于同步这些并行单元。
在软件中,我们依赖昂贵的同步机制如互斥锁(mutex):线程完成工作后需从内存获取锁并通知其他线程。而芯片采用截然不同的方案:每隔几纳秒,所有电路单元都会短暂暂停以实现全局同步——这就是时钟周期的定义。整个芯片通常以严格同步的方式推进到下一个操作。
在电路层面,时钟通过寄存器(我们在别处画出的存储设备)传递。可以这样理解:某个存储单元保存着0或1的二进制位,连接着一团逻辑单元(可能是脉动阵列或乘法器),这些逻辑单元接收大量输入,最终将结果写入输出寄存器。
有一个全局时钟信号驱动所有这些寄存器。在某一时刻,当时钟响起时,该瞬间出现在导线上的任何值都会被存储。
挑战在于我希望时钟速度尽可能快。如果运行频率为两千兆赫兹,每秒完成的操作数就比一千兆赫兹时多一倍。但这意味着我对这一逻辑云中的延迟非常敏感,因为其中任何计算都必须在下个时钟周期到来前完成。芯片优化的重点之一就是让这种延迟尽可能短。
Dwarkesh Patel
有意思。这里的约束似乎是:如果加入过多逻辑,可能会错过时钟周期;但如果不够,又浪费了潜在算力。是否存在一种情况需要冒险尝试计算能否完成?还是说它必须严格在时钟周期内完成,否则就不行?
Reiner Pope
在标准芯片设计中,会预留余量使得失败概率极低——远超出多个标准差范围。实际上它是可靠的,几乎总能满足时钟要求。
有些例外情况,比如跨时钟域(clock domain crossings)时从一个时钟切换到另一个时钟,这时确实需要考虑概率问题。但在主要路径上,会预留25%的时钟周期余量,使其极大概率不会超时。
Dwarkesh Patel
时钟同步和寄存器的位置是芯片设计者确定的吗?还是说当需要特定逻辑序列时,将Verilog转换为TSMC可制造文件的工具会自动决定这些位置,以确保单步操作不会延长整个芯片的时钟周期?
Reiner Pope
插入寄存器实际上是芯片设计工作的核心部分,这需要结合人工和自动方法共同完成。
用最简单的方式说明:你可以将这段逻辑一分为二。不再用一大片逻辑,而是分成两片较小的逻辑,中间插入一个寄存器。若取中间位置分割,就能实现两倍时钟频率。性能翻倍,代价是多了一个寄存器,意味着更多存储空间。
Dwarkesh Patel
退一步思考,为什么整个芯片需要同步?比如在《Factorio》这类游戏中,没有全局时钟周期,事情做完就自然结束,铁板上的资源想拿就拿。
Reiner Pope
用这个类比需注意:如果有两条不同的逻辑路径。比如这里要执行运算f,那里执行运算g,最终汇合到运算h。
制造工艺存在差异:某些芯片上f稍慢,某些芯片上g稍慢。若信号传播过程中,f和g的结果要在h处汇合,可能出现的问题是:f过早到达并遇到g的旧值或新值。
Dwarkesh Patel
啊。h需要知道何时开始,下个迭代何时……
Reiner Pope
没错。
Dwarkesh Patel
这解释了为何在同一制程节点(如台积电相同技术)生产的不同芯片,时钟周期可能不同。3 nm工艺下生产的两颗芯片,若优化程度不同——确保没有单一关键路径过长拖累整体时钟周期——其时钟周期也会出现差异。
Reiner Pope
没错。这里展示的优化称为流水线寄存器插入。我们在流水线中段插入了一个寄存器。这是纯粹的时钟速度与面积之间的权衡。属于简单情况。还有更复杂的情况:我画出的是一条逻辑流水线,但某些情况下计算可能需要反馈自身。比如执行函数f后写回自身,例如每时钟周期累加一个数。这个小电路本质上会汇总不同时钟周期输入的所有数值。
挑战在于,如果加法耗时过长,我能做什么?若在中间直接插入流水线寄存器,会改变运算逻辑。原本形成连续累加,现在会得到两个独立累和——偶数和与奇数和。这种约束(所有芯片逻辑中必然存在的循环结构)是最难处理的问题,它决定了时钟周期上限。
Dwarkesh Patel
我不明白为何这会成为问题。甚至不确定寄存器放在那里的含义——是否类似原子操作?
Reiner Pope
嗯,加法并非原子操作。
Dwarkesh Patel
正如您刚才演示的。
Reiner Pope
完成累加需要大量工作。可将前半部分工作、插入寄存器、再处理后半部分。
Dwarkesh Patel
明白了。台积电提供PDK(工艺设计套件),规定了芯片可用的基础逻辑单元。他们需确保每个基础单元不超过目标时钟周期的门延迟。但除此之外,能否直接用台积电提供的所有基础单元,按需插入足够多的寄存器,直到达到所需时钟周期?
Reiner Pope
作为逻辑设计师,芯片架构师设定时钟周期。例如台积电提供的基础单元可能是与门或全加器,具体取决于电压和库选择,通常一个时钟周期可串联约10-30个此类单元。这些单元速度极快,仅约10皮秒。
作为逻辑设计师,理论上若只有寄存器和与门构成环路,时钟频率可飙升至4-6 GHz以上。但若看这个极简电路的面积开销……此处称为1个门等效面积,即面积为1单位。而该结构实际占用约8单位面积。
几乎全部成本都变成了同步或通信开销,而非实际逻辑运算。这就是过度设计的案例:为追求超快时钟,几乎将所有面积用于流水线寄存器。
Dwarkesh Patel
有意思。所以您暗示存在一种动态:时钟速度极快,但完成的任务量却有限。低延迟伴随低吞吐量。
Reiner Pope
这实际上会损害吞吐量,因为芯片的吞吐量是每时钟周期完成的工作量(基于面积效率)乘以每秒的时钟频率。
Dwarkesh Patel
这和之前讨论的批量大小问题其实很像:如果批量较小,单个用户可以很快获得下一个token,但比如在一小时内处理的总token数会比批量设置得更大时更少。
Reiner Pope
没错。如果你将时钟速度提升到非常高,获得的并行度反而会更低。
Dwarkesh Patel
我记得在Jane Street和FPGA工程师Clark聊过,他帮我准备上次面试时解释了为什么他们使用FPGA。我推测在高频交易中,吞吐量不如延迟重要,因此以确定性的方式对时钟周期进行精确控制才是关键。或许可以探讨一下为何ASIC无法实现这一点,或者为何在高频交易中使用FPGA来获得确定性时钟周期。
Reiner Pope
让我们对比FPGA和ASIC的商业案例。FPGA和ASIC在概念模型上基本相同。它们由一系列小原语——AND、OR、XOR等门电路组成,通过固定时钟周期的导线连接。FPGA能表达的内容ASIC也能表达,但ASIC的成本约为FPGA的十分之一,且能效更高。
两者的权衡在于:首个FPGA成本约$10,000,而首个ASIC需$3000万,因为它需要完整的流片流程。FPGA适用于需要极低延迟、快速运行和高并行性,但工作负载频繁变化(如每月变更)的场景,你不想每次支付流片费用。
FPGA如何在固定硬件中模拟ASIC编程模型?其核心包含两部分:寄存器作为存储设备,查找表(LUT)提供所有逻辑门功能。
还有第三部分:大量寄存器和LUT通过一组多路复用器(muxes)连接。每个组件前都有一个mux,从中选择来自其他位置的输入。这些组件有众多选项可供选择。
这意味着编程FPGA时,我可以将这些组件叠加特定布线:信号经过LUT,再输入另一个LUT,送至寄存器,然后进入另一LUT,以此类推。
橙色部分表示FPGA现场可编程的部分(Field-Programmable),白色则是制造FPGA时必须存在的原始连线。
Dwarkesh Patel
“现场编程”是什么意思?
Reiner Pope
现场编程指设备部署到数据中心后,你可以在实际环境中对其进行编程。
Dwarkesh Patel
哦,不是电场中的“field”,而是“存在于现实世界”的意思,明白了。
如果看第一个查找表的输出如何进入第二个查找表,具体如何实现?
Reiner Pope
是什么让这些线路实现功能?我在绘制这些时偷懒了。这里的每个设备前面都接有一个多路复用器(mux),可以从所有可用附近电路中选择。FPGA的实际配置就是由这些mux的控制信号决定的。这个mux有数据输入端,也有选择控制端。
每个这样的多路复用器旁边都有一个小型存储设备,用于指示“从这里获取输入”。编程过程就是配置所有这些多路复用器。
Dwarkesh Patel
这样合理。查找表内部发生了什么?
Reiner Pope
查找表中还包含一些控制信号,告诉它该做什么。它的作用是配置性地充当AND门、OR门、XOR门或其他类似功能。实现方式有很多。传统FPGA的做法是……查找表有四个输入位和一个输出位。从四位到一位的函数有多少种?共有16种不同函数。
你可以将这些函数表示为16个不同的数字,比如表格0111001,共16项。这个表格存储在这个蓝色配置位中,它将这四位视为二进制数,查找相关行并输出对应位。这就是查找表的真值表视图。
Dwarkesh Patel
好的,如果你考虑AND门、OR门、NOR门、XOR门,它们的输入都是……
Reiner Pope
这些都是二输入函数。有时我们会有三输入函数,比如三路XOR,或四路XOR。
Dwarkesh Patel
这种情况下,是否只取决于其大小?
Reiner Pope
LUT的典型输入数量是四位。这是一个平衡点。这里存在计算与通信的权衡。如果输入太少,就需要更多LUT。
Dwarkesh Patel
基本上,查找表就是一个真值表。通过真值表,你可以编程实现任何需要的逻辑门。因此,你也可以将查找表看作一个可编程门。
Reiner Pope
没错。这里可以解释为什么FPGA比ASIC贵一个数量级的原因。你只需计算查找表中有多少个门。
我们可以将这个查找表看作一个多路复用器。它需要在16个选项中选择,即n=16个选项和p=1位的mux。如前所述,这个电路的成本是n乘以p个门,也就是16个AND门和16个OR门。
Dwarkesh Patel
这个电路是指那个多路复用器吗?
Reiner Pope
没错,就是那个多路复用器。
Dwarkesh Patel
进入查找表的多路复用器?
Reiner Pope
你可以把查找表本身看作一个大mux,从16行中选出一个输出。这就是查找表的工作原理。
Dwarkesh Patel
但按你的画法,这里有一个多路复用器和一个查找表。
Reiner Pope
一路多用下去。这里面还有第二个多路复用器。这个多路复用器就是这个多路复用器。
Dwarkesh Patel
另一个多路复用器只是在说……
Reiner Pope
它来自这一堆门中的某个地方。
Dwarkesh Patel
对,第二个多路复用器的功能是,“现在你得到一个值,但这个值仍然是四位。”
Reiner Pope
是的,我从一堆数据中选择了四位,然后用这四位来选择查找表中的哪一项使用。
Dwarkesh Patel
假设第一个多路复用器(mux)从八个附近的寄存器获取输入,总共是32位。其中输出四位,这四位进入第二个多路复用器,而第二个多路复用器位于查找表(lookup table)内部。
Reiner Pope
在这种情况下,这些寄存器是单比特寄存器。如果有八个附近的寄存器和查找表,那么我总共有八位来自附近的数据。我从八种值中选择到四种。实际上有四个不同的多路复用器,每个输入比特都关联一个小型的多路复用器。它们各自从八种中选择一种。
Dwarkesh Patel
那八个数据是从哪里来的?
Reiner Pope
来自附近的寄存器和其他查找表(LUTs)。
Dwarkesh Patel
每个寄存器是一比特。
Reiner Pope
是的。
Dwarkesh Patel
我猜想 AMD 或其他制造 FPGA 的公司仍然需要决定哪些寄存器连接到哪些寄存器。你可以在实际的门电路中编程,但它们会在连接中添加导线……对吧?通信拓扑结构。
Reiner Pope
你在局部粒度上获得了灵活性。你可以选择附近的邻居,但对于更粗粒度的长距离连接,它们会做出决定。
Dwarkesh Patel
为什么它慢10倍的原因是什么?
Reiner Pope
如果你看一下构建这个查找表的成本,它是32个门。它可以给我等效的——这里有什么有趣的事情可以做——一个四输入与门。四输入与门意味着 AND、AND,然后是一个 AND 的 AND。这是一个我可以直接在 ASIC 中使用三个与门实现的电路。使用查找表,我也可以实现它,但需要32个门而不是三个。
Dwarkesh Patel
所以开销主要来自于存在一种比列出所有可能的输入组合更简洁的方式来描述真值表,即直接写出逻辑门。
Reiner Pope
是的,为了放置多晶硅和导线等。
Dwarkesh Patel
有意思。你向我强调的一个重要观点是,他们更喜欢 FPGA 而不是 CPU 的原因是 FPGA 能获得确定性的时钟周期。他们知道数据包何时到达和离开。为什么 CPU 不能保证这一点?
Reiner Pope
实际上,你可以设计具有确定性延迟的 CPU。事实上,许多 AI 芯片中的处理器也具有确定性延迟。Groq 曾宣传过这一点。TPU 的核心也有这种特性。
挑战在于同时获得确定性延迟和高速度。CPU 中的非确定性延迟来自特定的设计选择。实际上,可以移除这些设计选择并制造具有确定性延迟的 CPU,但这些在市场上并不吸引人,因此人们不再生产这类 CPU。
某种意义上,确定性延迟是一个更简单的起点,一些芯片设计师添加了某些东西使其变为非确定性。以具体例子来说,CPU 中最重要的非确定性来源可能是 CPU 缓存本身。
在 CPU 中,你有 CPU 芯片本身,以及旁边独立的 DDR 内存。你有一个缓存系统,记住最近访问过的 DDR 数据并将其存储起来。当我运行 CPU 指令时,每次访问内存的指令都会先检查数据是否在缓存中。如果不在,就从 DDR 中获取。
这是一次巨大的优化。缓存的速度比DDR快两个数量级。如果你从未使用过缓存,几乎所有程序都会运行得慢一百倍。对于CPU以合理速度运行来说,缓存的存在是绝对必要的。
但是你是否能获得缓存命中取决于CPU的周围环境:正在运行的其他程序、最近运行的程序以及缓存系统内部随机数生成器的状态。这是导致CPU运行时非确定性的一大来源。
这就是CPU的内存系统。你可以做的一个重要不同之处在于,硬件不再说“我要读取内存”然后由硬件决定是否来自缓存,而是将这个决策融入软件中,这是一种不同的设计哲学。
例如在TPU中你会看到这一点。我会画出相同的图,但称之为scratchpad。主要区别在于……这将是TPU,你在这里有HBM而不是DDR,但它仍然是片外存储器。不再是软件说“首先访问内存”并让硬件决定,而是一种指令去访问scratchpad,另一种完全不同的指令去访问HBM。
这种风格通常被称为scratchpad而非cache。关键区别在于,你有一种指令表示“读取或写入scratchpad”,而另一种完全不同的指令表示“读取或写入HBM”。
Dwarkesh Patel
所以scratchpad就是缓存。
Reiner Pope
没错,这里的东西就是scratchpad。
Dwarkesh Patel
退后一步:人们常说计算机采用“冯·诺依曼架构”,其中信息是串行处理的。也许只是因为我们在讨论并行加速器,FPGA极其并行。AI加速器TPU也极其并行。即使考虑到所有核心,现代CPU也是高度并行的。那么,现代硬件在什么意义上实际上是冯·诺依曼架构?这真的能公平地描述现代硬件吗?
Reiner Pope
我认为这可以公平地描述CPU。CPU上获得的并行度大约是100个核心乘以可能16路向量单元,因此大约1,000路的并行度。
Dwarkesh Patel
一个问题:CPU使用的芯片上,如果线程较少,仅从晶体管电压开关的角度来看,是否确实只有一小部分芯片——即控制流——在电压开关?
如何实际占用CPU芯片的面积……
Reiner Pope
如果核心如此之少,你却在芯片上花费了这么多面积?
Dwarkesh Patel
是的,那里到底发生了什么?
Reiner Pope
核心只是更大更复杂。我们应该比较一个占芯片面积百分之一的CPU核心与LUT。LUT只有16个门。显然,FPGA中的LUT比CPU中的核心多得多是有道理的。
但是为什么例如CUDA核心比CPU核心更多?CPU和GPU之间的区别是什么?在CPU内部,面积的一个大用途是缓存。实际上ALU所占比例很小。主要是寄存器文件而非逻辑单元。两者在GPU中都有对应,这不是主要区别。
但 GPU 上没有等效部件的是分支预测器。CPU 中有一大块专门用于预测下一条分支何时发生及目标地址的预测器。移除这些预测器并收紧寄存器文件,是导致 GPU 相比 CPU 性能提升的关键因素之一。
Dwarkesh Patel
分支预测器的作用是什么?是同时执行两条分支,还是其他功能?
Reiner Pope
问题在于,当我有连续指令序列时遇到分支,实际处理指令的步骤会变得非常耗时。可能需要五纳秒。
从发现分支、评估布尔条件是否为真、更新程序计数器到新的目标地址,再到读取指令存储器,整个过程可能耗费五纳秒。因此,实际完成时间可能在此之后。我希望时钟频率远高于五纳秒允许的范围——五纳秒对应 200 MHz 时钟速度,而我希望运行在 1-2 GHz 频率。
所以我需要在分支被评估期间继续执行其他指令。我只是想继续执行后续指令。但如果预测错误,分支最终会被跳转,那么我需要知道此时应跳转到目标地址而非继续评估这些指令。分支预测器的作用是提前五个周期预测分支行为,甚至在到达该指令前就做出判断。
Dwarkesh Patel
若对比大脑工作机制与你描述的内容,高层次差异可能是:加速器可通过结构化稀疏性节省本需分配给门的面积,而大脑采用非结构化稀疏性——任意神经元可连接至任意其他神经元,且无需列对齐。
此外,内存与计算单元紧密集成。虽然某种程度上说,芯片上的内存和计算也是共存的。
Reiner Pope
这正体现了内存与计算的某种紧密集成。
Dwarkesh Patel
或许这不是主要区别。另一显著差异是大脑的时钟周期远慢于计算机。部分原因是节能需求:时钟越快,晶体管状态稳定所需的电压越高,能耗随之增加。
Reiner Pope
没错。
Dwarkesh Patel
不知你对大脑运作方式与芯片设计有何见解?
Reiner Pope
先从时钟频率说起。芯片的高时钟频率是为了提升吞吐量。例如,GPU 运行批量大小为 1,000 的任务,而大脑仅处理单一输入(即“我”)。
你可以设想:“将 GPU 从 GHz 降频到 MHz”,这样会更接近大脑的工作模式。但从硅的物理特性看,这种降频无法带来千倍能效优势。
最终效果是,你只需运行一次电路使其稳定,之后它会长时间保持空闲状态。由于大部分能量消耗在比特从0到1及反向切换的过程中,空闲时电路耗电量很低。
我们来谈谈这类电路的能耗。存储一个比特可以理解为在芯片某个电容中注入电荷:当比特变为1时充电,再变为0时放电。
电容充电后向地线释放电荷的循环过程就是能量消耗所在,这被称为动态功耗或开关功耗,占芯片总能耗的大部分。虽然绝缘体并非完美会额外产生一些能耗,但可忽略不计。绝大多数能耗仍来自0-1-0的切换。
若将芯片运行速度大幅降低,比如每千个时钟周期才触发一次,则切换次数减少约1000倍,能耗也会降至约1/1000。但这种能效提升并不显著。
Dwarkesh Patel
好的,你已概括了TPU的工作原理。那么GPU和TPU在高层设计上的核心区别是什么?
Reiner Pope
高层组织原则不同,且核心内部结构也有差异。我们先对比GPU和TPU的整体块级结构。
若将整个芯片视为整体,GPU的组织形式由大量几乎相同的单元(即流式多处理器SMs)构成,中间配有L2缓存,底部还有更多SMs,形成规整的核心网格。
相比之下,TPU采用更粗粒度的逻辑单元,仅有少数矩阵运算单元(即大型脉动阵列),中间搭配向量单元,底部则是矩阵单元。这些带中间向量单元的矩阵单元构成了整个TPU芯片。
若将这种结构缩小为极小单元——含更小矩阵单元和向量单元——这就是SM的实质。从高层看,GPU本质上是布满全芯片的大量微型TPU阵列。
Dwarkesh Patel
哦,有意思。你是说流SM中的张量核与MXU类似吗?
Reiner Pope
是的,两者非常相似。
Dwarkesh Patel
明白了。若数据缺乏规律性,大量微型TPU的设计确实合理。而若仅进行超大矩阵乘法,或许无需每个SM都配备独立寄存器和线程调度器,直接构建单一巨型单元摊薄成本岂不更好?
Reiner Pope
这与可扩展性密切相关。尤其是脉动阵列,更大的尺寸能更高效摊薄寄存器文件的开销。
这种设计允许使用更大规模的脉动阵列,而GPU架构强制所有单元必须小型化。不过也存在权衡:因粗粒度分离特性,向量单元与矩阵单元间需通过此处两条外围线路频繁传输大量数据。
如果在 GPU 上观察类似的结构,你会发现矢量单元无处不在,数据可以通过多条线路传输。GPU 中矢量单元与矩阵单元之间可传输的数据量实际上远高于 TPU。在 GPU 中,你无需仅通过两条线路传输所有数据,而是能通过 16 条线路并行传输。
Dwarkesh Patel
没错。而且你可能需要跨越更小的面积。
Reiner Pope
这也能节省能耗。因此如果能在单个流式多处理器(SM)内完成操作,数据传输量会小得多。但一旦需要在多个 SM 间操作,情况会变得复杂且成本更高。
Dwarkesh Patel
当然不用多说,但可以推测 MatX 可能会尝试实现类似 GPU 的小规模脉动阵列结构,并用 SRAM 包围它,同时舍弃支持 CUDA 架构所需的 SM 中占用大量空间的组件。
Reiner Pope
我们曾公开讨论过一种称为“可拆分脉动阵列”的技术,从某种意义上说,它可以既是大型脉动阵列,也可视为小型脉动阵列。
Dwarkesh Patel
太好了。我认为这是个合适的结束点。Reiner,非常感谢!
Reiner Pope
谢谢,Dwarkesh。
需要完整排版与评论请前往来源站点阅读。