冷开场:工位已经坐满,工作却还在路上
0:00二〇一七年,谷歌公布第一代张量处理器的性能分析。这块为神经网络计算设计的芯片里,六万多个计算单元整齐排开,可以同时完成大量乘法和加法。
听上去,它最不缺的就是速度。像一间刚扩建完的工厂,工位、机器和电力都已准备妥当,只要订单一到,产量就该直线上升。
可论文记录了另一幅画面:这一轮要用的权重还没有送到,或者上一轮的中间结果还没准备好,整片计算阵列就会停下来。权重是模型训练后保存、生成时反复使用的大量数值;中间结果则是前一步必须交给后一步的数值。它们不能靠猜,也不能临时拿一份相近数据顶替。
于是,六万多个计算单元同时面对同一件事:不是不会算,而是还不能算。机器最强的部分已经就位,决定下一秒有没有结果的,却是一份尚未抵达的依赖。
这也提醒我们区分两种常被混在一起的速度。峰值能力回答“条件理想时一秒最多能算多少”,实际完成时间回答“从请求进来到结果出来究竟等了多久”。前者可以靠增加工位迅速变大,后者还要把取数据、等待前序结果和交回输出全部算进去。本期关心的是第二种速度。
[001]
引用 [001]
冷开场只保留计算阵列、权重、中间结果与等待,不展开内部队列和存储器名称。
PDF pp. 3-5 and §4: systolic matrix unit, weight / activation paths and weight stalls
做过应用服务的人,大概见过相似的假繁忙:线程池还有空闲线程,请求却全卡在同一条数据库查询上。再加一倍线程,只会多出一批等待者;查询没有返回,后面的业务仍然不能开始。
这幅图能解释“执行者空闲,依赖未到”,却不能解释芯片内部怎样工作。计算单元没有网络连接,权重读取也不是数据库查询。两边共有的只是依赖关系:后一步必须拿到前一步的产物,执行者数量不会取消这个先后条件。
本期要追的,正是这份等待如何形成。答案不只藏在二〇一七年的芯片里。它会把我们带回七十多年前:那时电子线路已经把计算变快,改变任务的速度,却还牢牢握在人手里。
[002]
引用 [002]
线程池类比只解释共同依赖未到,正文立即排除服务器与芯片机制等同。
PDF pp. 4-5 and §4: matrix unit waits when required input or weight data is unavailable
第一章:把下一步移进机器
2:11一九四三年,美国宾夕法尼亚大学摩尔电气工程学院开始建造 ENIAC。它要用电子线路完成原本由人和机械设备承担的大量计算。机器一旦进入运行,速度非常快;可要换成另一类问题,操作人员往往要重新设置大量开关和电缆,安排各部分先做什么、把结果交给谁。
这有点像一座剧场每换一出戏,不只替换剧本,还要拆掉布景、挪动舞台、重新安排演员从哪扇门进出。演出本身可以很快,换戏却成了另一项工程。
ENIAC 并非没有程序,它有自己的程序控制方式。昂贵之处在于,任务步骤有很大一部分仍留在机器外面,凝固在接线、开关状态和人的准备劳动里。电子速度解决了“算得慢”,却把“改得慢”照得更加明显。
[003]
引用 [003]
保留 ENIAC 有程序控制方式的边界,剧场类比只解释换题劳动。
- J. Presper Eckert / John W. Mauchly, Automatic High Speed Computing: A Progress Report on the EDVAC, 1945
- John von Neumann, First Draft of a Report on the EDVAC, 1945-06-30
progress report scan pp. 2-3; First Draft §§1.1-2.9 on electronic speed, organs and automatic control
摩尔电气工程学院的团队正在设计下一台机器 EDVAC。J. Presper Eckert 和 John Mauchly 已经讨论新的存储器、数字表示和自动安排步骤。John von Neumann 加入以后,参与逻辑控制、指令编码、用具体问题检验方案,并把此前讨论整理进报告。
他们的工程选择可以压缩成一句话:不仅把要计算的数值存进机器,也把告诉机器“下一步做什么”的指令存进去。换任务时,人不必主要依靠重新接线,而可以修改和装入另一串指令。
剧场也就从“每换一出戏重搭舞台”,变成“在同一座舞台上换剧本”。这个类比到这里为止:机器指令不是自然语言剧本,控制部分不会理解人物和情节。它只会按编码取出动作。类比真正说明的是,改变工作顺序的东西终于可以被保存和替换。
这个变化还让修改变得可以积累。接线方案当然也能记录,但程序一旦成为内容,就能像其他内容一样被复制、比较、修订和再次装入。一次改进不必只留在某次机器布置里,它可以进入下一份程序。计算机的通用性因此不只表现在“能做很多题”,也表现在换题的方法开始具有软件的可重复性。
[004]
引用 [004]
同段固定团队贡献与存储指令动作,剧本类比明确排除理解语义。
- J. Presper Eckert / John W. Mauchly, Automatic High Speed Computing: A Progress Report on the EDVAC, 1945
- John von Neumann, First Draft of a Report on the EDVAC, 1945-06-30
progress report scan pp. 2-5; First Draft §§2.2-2.9 and §§15.1-15.6
一条指令进入机器以后,发生的事情并不神秘。控制部分从存储器取出指令,按照它指出的位置取得数值,运算部分完成计算,再把结果写回;控制部分接着取下一条,机器继续向前。
指令和数值能够使用同一套保存与传送能力,不表示它们作用相同。可以想象一间仓库既保存物料,也保存作业单:两者都占货位、都能被取出,工位拿到物料时用它加工,调度员拿到作业单时按它安排动作。机器里没有调度员理解文字,这个类比只解释“做什么”和“拿什么来做”可以一起成为可定位的内容。
如果把视线停在零件名称上,这很容易变成一张教材框图。更值得记住的是四种关系:程序保存了可以更换的安排,状态保存了当前工作走到哪里,控制决定下一步,执行部分完成眼前动作。输入把新内容送进来,输出把结果交出去。
存储器不会理解题目,运算部分也不会自己决定今天算天气还是工资。改变机器用途的力量,来自可替换程序和这几种角色之间的交接。所谓架构,首先是这份分工怎样成立,而不是机箱里每根线必须画在什么位置。
[005]
引用 [005]
用程序、状态、控制与执行重述最小动作,不口播原始字母缩写或位格式。
- John von Neumann, First Draft of a Report on the EDVAC, 1945-06-30
- Arthur W. Burks / Herman H. Goldstine / John von Neumann, Preliminary Discussion..., 1946-06-28
First Draft §§2.2-2.8 and §§15.1-15.6; IAS report §§1.2-1.4
后来,人们用“存储程序”概括这种做法,又用“冯·诺伊曼架构”指向一组更宽泛的关系。这两个名称都带着后见视角:《EDVAC 报告初稿》没有使用“存储程序”这个词,标题页也只列了 John von Neumann。
标题页的一人署名不能抹去 Eckert、Mauchly 和团队的前置工程,也不能抹去 von Neumann 在逻辑组织、检验与写作中的具体参与。把这段历史讲成单人灵光一现,或者反过来讲成一个人只是抄写,都把协作压扁了。
更关键的是,后来的机器很快采用不同的存储器、指令和并行方式。真正延续下来的不是一九四五年的统一施工图,而是一项承诺:程序可以成为可保存、可复制、可修改的对象,同一套硬件因而能在不重搭整座舞台的情况下改变用途。
这项承诺也改变了投入的去向。人们可以把更多时间花在编写、检查和改进程序,而不是为每次任务重新布置机器;同一份程序还可以在同类机器上重复运行。它没有让程序开发变得容易,却让开发劳动第一次能够稳定地沉淀在一个可修改对象里。
[006]
引用 [006]
后见名称只承担关系框架,明确拒绝单人发明和统一物理蓝图。
- John von Neumann, First Draft of a Report on the EDVAC, 1945-06-30
- J. Presper Eckert / John W. Mauchly, Automatic High Speed Computing: A Progress Report on the EDVAC, 1945
- Arthur W. Burks / Herman H. Goldstine / John von Neumann, Preliminary Discussion..., 1946-06-28
- Thomas Haigh / Mark Priestley / Crispin Rope, Reconsidering the Stored-Program Concept, 2014
First Draft title page and §§15.1-15.6; progress report scan pp. 2-5; IAS report title / Preface; Haigh / Priestley / Rope pp. 4-13
第二章:同一组关系,换到不同尺度
7:02先把电脑缩小,放进一把智能门锁。芯片里长期保存着一份控制程序:怎样读取按键,怎样核对密码,什么时候驱动锁舌。门锁运行以后,还会产生另一批内容:刚输入的数字、传感器读数、失败次数和开门记录。
程序断电以后仍要保留,平时很少改;临时状态变化频繁,外设读数还要随时进入。两类内容需求不同,一款微控制器便让程序和运行数据使用分开的区域和通路。处理器执行眼前指令时,还能准备下一条。
这看上去不再是“指令和数值共用一块存储器”,却没有放弃存储程序。门锁仍由可保存、可更新的程序决定行为,同一块硬件可以通过更新规则修复漏洞。变化的是道路,保留的是程序、状态、控制和执行之间的关系。具体手册只证明这一款实现,不代表所有嵌入式设备。
为什么要分路,不是一场概念之争,而是需求不同。验证程序一旦意外损坏,门锁可能彻底失效;临时输入却必须快速变化,断电后也未必需要保留;传感器还要在外部事件发生时及时送来数据。把它们分开安排,是在掉电保存、更新风险、成本和响应时间之间做选择,不是在决定自己属于哪一种教材分类。
[007]
引用 [007]
门锁是编辑场景,程序与数据分路只绑定具体微控制器手册。
- Microchip Technology, AVR32DA28/32/48 Data Sheet, Rev. B, 2021
- Thomas Haigh / Mark Priestley / Crispin Rope, Reconsidering the Stored-Program Concept, 2014
Microchip PDF p. 28, §6.3 and pp. 39-40, §§7.1-7.5; Haigh / Priestley / Rope pp. 9-13
再把镜头反过来放大。你在购物应用里打开一件商品,屏幕上只有一个页面,背后却可能有不同服务读取账户、库存、价格和推荐。入口接住请求,调度系统选择机器,网络传递问题,存储交回数据,多个结果最后汇合。
对用户来说,这是一项服务;对数据中心来说,它是许多程序、服务器和数据副本的一次合作。Barroso、Holzle 和 Ranganathan 因此建议把仓库规模的系统当成一个完整设计单位,因为性能、成本和可靠性已经取决于整体协作。
但数据中心不是一颗仓库大小的中央处理器。每台服务器仍有自己的处理器、内存和程序,彼此通过网络交换消息。跨过单机边界以后,系统还要面对消息迟到、局部故障、重新调度和状态暂时不同步。这里复现的是角色之间需要配合,不是一条统一指令流被放大了几千倍。
单机控制部分可以假定下一条指令就在既定位置,云端服务却不能假定每台机器始终可达。一次库存查询超时,入口可能重试,也可能返回降级结果;重试又可能把同一动作执行两次。系统不仅要安排“下一步做什么”,还要说明“我不知道上一步是否成功时怎么办”。尺度变大以后,控制从确定动作变成带着不确定性的协调。
[008]
引用 [008]
购物请求是编辑场景;明确排除巨型中央处理器和统一指令流。
PDF pp. 22-24, printed pp. 2-4, §§1.1-1.3: interacting programs, thousands of nodes, network, storage and warehouse-scale design
软件公司也能借来做一次更大胆、也更危险的类比。共享文档和业务系统保存项目状态,管理层或项目负责人决定一部分优先级,各团队执行任务,客户反馈与监控不断形成输入,产品和服务成为输出。所有决定如果都要经过一个审批点,很多人就会一起等待。
这个类比只能帮助我们看见信息通道,不能把公司解释成计算机。Herbert Simon 讨论复杂系统时指出,层级关系不只等于组织图上的上下级。真实团队里,人会解释、协商、拒绝和创造,也会越过正式路径核对信息;公司没有机器式的唯一控制器、统一时钟和确定指令。
因此,不能从“冯·诺伊曼架构”推出集中管理或分权管理必然更好。我们只借它提出工程问题:状态放在哪里,决定经过几道门,现场的人能否处理局部变化,哪个审批点正在让所有执行者等待?
[009]
引用 [009]
组织是有边界的编辑类比,不把层级等同正式权威,也不推出管理定律。
- Herbert A. Simon, The Architecture of Complexity, 1962
- Luiz Andre Barroso / Urs Holzle / Parthasarathy Ranganathan, The Datacenter as a Computer, 3rd ed., 2018
Simon scan p. 2 / printed p. 468 and scan p. 10 / printed p. 476; Barroso et al. PDF p. 22
门锁、云端和公司摆在一起,外形几乎没有共同点。它们也不是 EDVAC 在不同尺度上的物理复刻。门锁可以把程序与运行数据分路;云端把控制和状态分散到许多机器;公司里的人甚至会重新解释目标。
它们仍然值得并置,因为同一组问题能暴露不同系统的选择:什么内容可以被保存和修改,谁根据这些内容决定下一步,谁真正执行,依赖经过多远才能抵达,失败以后由谁恢复。
这就是本期采用“架构”一词的边界。它不是一张祖传蓝图,而是一份关系检查表。蓝图要求尺寸和接线相同;关系检查表允许实现完全不同,却逼我们说清每一种安排把速度、可靠性和复杂度交给了谁。
用这份检查表看门锁,我们会追问程序更新失败后怎样恢复;看云端,我们会追问状态分散以后谁能做决定;看组织,我们会追问哪些信息必须上报、哪些选择可以留在现场。问题相似,答案却受材料、网络和人的能力限制。关系框架的价值正是在比较差异,而不是抹平差异。
[010]
引用 [010]
综合段只收束关系观察法,不声称三个尺度共享同一物理架构。
- Thomas Haigh / Mark Priestley / Crispin Rope, Reconsidering the Stored-Program Concept, 2014
- Microchip Technology, AVR32DA28/32/48 Data Sheet, Rev. B, 2021
- Luiz Andre Barroso / Urs Holzle / Parthasarathy Ranganathan, The Datacenter as a Computer, 3rd ed., 2018
- Herbert A. Simon, The Architecture of Complexity, 1962
Haigh / Priestley / Rope pp. 9-13; Microchip PDF p. 28; Barroso et al. pp. 22-24; Simon printed pp. 468 and 476
第三章:语言把劳动移给谁
12:22存储程序让换任务更多变成换指令,新的困难随即落到程序员身上:怎样写出数量越来越多、又不能出错的指令?
可以把早期编程想成安排一批投递。机器语言要求你亲手写下每一次停车、转弯、装卸和下一个地址,机器可以直接照做,但任何路线变化都可能牵动后面的编号。汇编语言先给道路、站点和动作起了名字,再由汇编器翻译成数字指令。编号负担减轻了,路线仍主要由人逐步安排,也仍紧贴一台具体机器。
高级语言再往前走一步:程序员说明要把哪批货送到哪些目的地、满足哪些条件,工具负责规划大量局部路线。三者不是严格的高低等级,同一项目也可以混用。投递类比更不能解释语言的全部能力,它只揭示一件事:描述越接近目标,越多重复安排就有机会交给工具。
被交出去的不只是敲键盘的数量。地址变化以后哪些跳转要调整、一个表达式怎样拆成机器支持的动作、同一段循环里哪些结果可以复用,这些原本需要人逐项维护的关系,也开始由工具统一处理。程序规模越大,这种可重复安排越有价值,因为人的注意力可以留给问题本身和工具无法知道的约束。
[011]
引用 [011]
投递类比统一解释数字指令、符号名字和较高层目标,并排除语言等级表。
- John Backus 等, The FORTRAN Automatic Coding System, 1957
- Dennis M. Ritchie, The Development of the C Language, 1993
Backus et al. PDF p. 11 / printed p. 197 compares machine language, assembly program language and FORTRAN; Ritchie PDF pp. 2-3
编译器因此更像路线规划者,而不是逐字抄写员。开发者给出目的地、顺序约束和可以重复的计算,编译器查看表达式与循环,选择机器指令,避免一部分重复工作,再把局部选择拼成完整程序。
这份分工之所以重要,是因为局部路线会随着机器改变。让工具承担可重复的翻译和检查,同一份意图才更容易修改,也更可能被带到另一台机器。程序员放弃逐条指定的权力,换回了开发速度、可读性和工具可以反复执行的安排。
但编译器也不知道没有写进程序的业务约束。它不知道某项结果能否延迟,不知道某个外部调用其实可以合并,更不能保证规划出的每条路线都优于了解全部现场的专家。抽象的价值不是消灭判断,而是把普遍、重复的局部劳动交给工具,把真正依赖业务和算法的选择留给人。
这也解释了为什么“更高级”不等于“更慢”。如果工具看见了更完整的循环和数据关系,它有时反而能做出单看一条指令时做不到的安排。反过来,如果重要意图被藏在工具看不懂的写法里,再聪明的编译器也只能保守处理。运行效率既取决于抽象层,也取决于抽象是否把有用信息表达出来。
[012]
引用 [012]
路线规划是有边界类比;编译器能力只绑定已表达约束,不承诺普遍最优。
PDF p. 1 / printed p. 188 on project goals; PDF p. 11 / printed p. 197 on coding effort, output and translation
一九五〇年代,John Backus 带领的团队开发 FORTRAN 时,这笔交换已经非常明确。科学和工程程序的大量时间耗在规划指令、编码和找错上;团队希望程序员用接近数学公式的方式描述计算,再由编译器生成高效的机器程序。
这不是先追求“好写”,后来才补救“跑得慢”。项目从一开始就必须同时证明两件事:人的编码劳动能显著减少,生成程序的运行效率又足以让使用者接受。早期报告中,一条 FORTRAN 语句可以变成多条机器指令,一些样例的准备时间明显下降,手工重写也没有获得值得一提的提速。
这些结果只属于当时那台机器和那些样例,不能证明编译器永远追平最好的人工优化。它更深的意义在于改变了责任边界:程序员开始用更大的计算意图工作,工具则接过大量容易重复、也容易出错的机器安排。
当时真正需要跨过的还有信任门槛。机器时间昂贵,如果方便书写的程序运行代价大幅上升,节省下来的开发时间可能很快被计算成本吃掉。FORTRAN 团队必须让“少写很多指令”和“机器仍然跑得足够好”同时成立,高级语言才不只是更友好的记号,而会成为可投入生产的工具。
[013]
引用 [013]
开发与性能结果严格绑定早期具体机器和样例,不外推为所有高级语言。
PDF p. 1 / printed p. 188 on goals and case history; PDF p. 11 / printed p. 197 on conciseness, coding time and output length
十多年后,Dennis Ritchie 和同事们在资源紧张的小型计算机上开发 Unix。最初的系统用汇编语言写成,他们却需要一种更容易编写和理解、又能处理操作系统工作的语言。
C 给出了另一种平衡。它的类型和操作能落到真实机器提供的能力,让程序员在必要时关心内存和数据表示;同时又不把每段程序锁死在某台机器的指令名字上,使 Unix 可以向不同机器迁移。系统软件既要接近硬件,也要不断增长、修改和移植,这两项压力缺一不可。
这里的关键不是把所有机器差异藏掉,而是把差异压缩到少数必须面对的位置。大部分系统逻辑可以继续用同一种语言表达,真正依赖设备的部分再单独处理。于是换机器不再等于从第一条指令开始重写,维护者也能把注意力集中到那些确实无法通用的边界上。
这不表示任何 C 程序都比其他语言快,也不表示越靠近机器越专业。FORTRAN 和 C 共同说明,高级表达与运行效率并非只能二选一。语言和编译器真正改变的,是获得一份可接受性能所需付出的人力,以及程序换机器、换团队以后还要承担多少维护成本。
[014]
引用 [014]
C 只作为历史平衡案例,不获得跨场景永久性能优先权。
PDF pp. 2-3 on assembler, writing ease and machine-grounded abstractions; p. 14 on efficiency, abstraction and portability
第四章:性能压力,让被隐藏的细节重新出现
17:47图形处理器把大量计算单元放到同一块设备上以后,新的问题出现了。只告诉机器“把这段算完”,并不能保证所有计算单元都有活干,也不能保证它们需要的数据已经在近处。
可以把 CUDA 这样的编程方式想成管理一座分区仓库。开发者要把大任务切成许多可以同时处理的小批次,决定哪些工人在同一作业区,哪些物料放在区内共享,什么时候必须等同组工作完成,还要安排普通处理器和图形处理器之间怎样交接任务与数据。
分区的价值,是让同一批近处物料服务多次计算,减少每个工位都回中央库房取货。边界也很清楚:计算单元不会像真正的工人那样协商、换岗或发现业务目标。任务切错、共享安排错误,它们只会精确地低效,或者精确地给出错误结果。
[015]
引用 [015]
仓库作业区解释任务分组、近处共享、同步和设备交接,并排除工人自主性。
PDF p. 23, chapter 3; pp. 33-34, §§5.3-5.4; pp. 39-40, §6.2.2
这些选择会直接影响速度。假如每做一点计算,就把数据在普通处理器和图形处理器之间来回搬运,路上的时间可能超过真正计算的时间;假如不同小组频繁互相等待,任务切得再碎,也不会自然变快。
这并不是退回机器语言。开发者不需要亲手写每个数字指令,编译器和运行系统仍然安排大量细节。变化只是抽象层向下开了一扇窗:为了让并行执行者持续工作,开发者重新接手“工作怎样分组、哪些数据留在近处、何时发生同步”这些会改变算法表现的选择。
为什么这些选择难以完全自动隐藏?因为最合适的分组常常取决于算法里哪些数据会被共同使用,哪些步骤真的互不依赖。硬件看得见一次读取和一次计算,却未必知道开发者接下来还会怎样使用同一份数据。语言把这部分意图暴露出来,是为了让人的算法知识与机器的执行能力在同一处相遇。
更多控制也意味着更多责任。代码更依赖特定设备,测试组合增加,硬件换代时旧安排可能失效。看见底层并不自动带来性能;只有看见的恰好是真实瓶颈,新增复杂度才可能换回收益。
[016]
引用 [016]
控制权与复杂度的交换是证据约束下综合,不声称显式控制必然更快。
PDF pp. 23 and 33-40: heterogeneous execution, work grouping, synchronization, data transfer and memory hierarchy
近几年,Triton 又把一部分劳动交还给编译器。它面对的处境是:人工智能研究者提出一种新计算,现成库未必支持;请少数专家手写高度优化的底层程序,成本高、迁移难;只用很高层的表达,又可能隐藏影响性能的数据复用。
它的做法可以类比成处理一张巨大的表格。开发者不再指定每个格子由哪个计算单元读取,而是说明怎样把表格分成一块一块,每块要完成什么计算、哪些数据会在块内反复使用;编译器再把这些块分给具体执行小组,处理更细的机器安排。
这个类比只解释两层分工,表格本身不会执行程序。分块方法仍然要由开发者选对,数据块之间的关系也必须符合算法。块太大、太小或复用不足,编译器不能把任意算法自动变快。
数据块之所以成为合适的交界面,是因为它比单个计算动作更接近算法,又比完整模型更接近硬件。开发者可以表达“这一块数据会一起参与计算”,编译器则根据具体设备决定怎样分派、怎样使用近处存储。双方都不必接管对方最擅长的全部知识。
[017]
引用 [017]
表格分块解释算法结构与机器映射分工,明确开发者仍需选择正确分块。
PDF pp. 1-2, Abstract / §1 on expert kernels, portability and tile abstraction
Triton 的原始论文在列出的矩阵与卷积实验中,得到与成熟厂商库相当的结果。这个范围很重要:它证明一种分工在特定实验里可行,不证明新语言会让任何程序自动加速。
从机器语言走到这里,语言史就不再是一座只向上攀登的楼梯。更像一条会移动的边界:工具先隐藏编号和局部路线,让程序更容易写、更容易迁移;当并行数量和数据距离成为瓶颈,部分结构又向开发者显形;当这些结构能够被稳定描述,工具再接回更细的映射劳动。
所以,抽象不是远离机器,深入底层也不是能力证明。两者都在回答同一个分工问题:哪些选择可以由工具可靠重复,哪些选择必须由理解算法与业务的人承担,又有哪些选择带来的收益根本覆盖不了维护代价?
[018]
引用 [018]
语言演进收束为劳动边界移动,不建立抽象层级或普遍性能排名。
- John Backus 等, The FORTRAN Automatic Coding System, 1957
- Dennis M. Ritchie, The Development of the C Language, 1993
- NVIDIA, CUDA C++ Programming Guide, release 12.4, 2024
- Philippe Tillet / H. T. Kung / David Cox, Triton: An Intermediate Language and Compiler for Tiled Neural Network Computations, 2019
FORTRAN PDF pp. 1 and 11; Ritchie PDF pp. 2-3 and 14; CUDA guide pp. 23 and 33-40; Triton PDF pp. 1-2 and 8
第五章:让数据留下,或者一路向前
22:13把时间拨回一九六五年。处理器越来越快,程序和数据越来越大,工程师却很难同时得到容量大、速度快、价格低的存储器。Maurice Wilkes 描述了一种务实办法:在处理器附近放一块较小、较快的存储,把刚从远处取回的内容暂时留下。
这像工匠把马上还要用的螺丝刀和量尺留在工作台,而不是每完成一个动作都走回库房。工具恰好在手边,下一步就省下一次往返;需要的工具不在,仍然要去远处取。
今天我们把这类近处小型快速存储称为高速缓存。它没有让远处库房消失,也不保证下一次一定命中。它只是利用程序经常重复使用近期内容或相邻内容的特点,为依赖缩短常走的那段路。
这项下注有两种直觉。一种是刚用过的内容很快还会再用,例如循环反复查看同一组数据;另一种是程序取了一个位置以后,往往还会取它附近的位置。高速缓存利用的是常见访问规律,不是预知未来。程序一旦跳到完全不同的数据,近处准备就可能落空。
[019]
引用 [019]
工作台类比解释近处留存、命中与远处仍存在,不展开替换算法。
printed pp. 270-271, Summary / Introduction and instruction / large slave-memory schemes
工作台类比很直观,也容易让人误以为是程序员挑选每一件工具。硬件高速缓存通常自动管理,程序照常读取原来的位置,背后究竟走近路还是远路,往往由硬件决定。它和人工整理工作台、应用服务选择缓存内容都不是同一种机制。
真正相同的只有复用逻辑:一份内容取回以后,如果很快还要用,让它留在近处就比反复长途搬运便宜。命中时,等待像是消失了;没有命中,远近差异便重新暴露。
也正因为它靠访问规律下注,同样的代码量不代表同样的速度。两个程序做的计算次数相近,一个总在重复使用小范围数据,另一个每一步都跳到远处的新位置,实际等待可能完全不同。性能分析不能只数运算,还要看访问顺序是否让近处内容真正被用上。
这项设计也解释了一个常见错觉。软件看到的是稳定的读取方式,机器下面却可能铺着多层距离和速度完全不同的存储。抽象替我们藏住了路线,却不能取消路线;性能问题出现时,隐藏的距离就会重新成为原因。
[020]
引用 [020]
明确硬件缓存、工作台和应用缓存机制不同,只保留近处复用动作。
- M. V. Wilkes, Slave Memories and Dynamic Storage Allocation, 1965
- T. Kilburn / D. B. G. Edwards / M. J. Lanigan / F. H. Sumner, One-Level Storage System, 1962
Wilkes printed pp. 270-271; Kilburn printed pp. 223-226 on automatic transfers behind a one-level view
一九八二年,H. T. Kung 从另一侧追问:如果专用处理器每做一次运算,都要回存储器重新取数,计算单元越快,取货口供应不上就越明显。继续增加工位,只会让更多工位一起空着。
他的规则阵列像一条流水线。数据从一端进入,经过相邻计算单元;每到一个工位完成一小部分计算,再把数据交给下一处。同一份数据沿途多次参与工作,不必每算一步都返回中央存储器。这种安排后来称为脉动阵列。
流水线真正节省的是返回库房的次数。一个工位刚用过的数据直接交给相邻工位,另一组数据按固定节奏从另一方向进入;计算发生在交接途中。只要节奏稳定,入口不必为每一个乘法重新供应完整数据,有限的通道便能支撑更多计算。
类比的边界正是流水线的边界。任务一旦不规则,步骤频繁分叉,或者数据只用一次就丢,固定阵列就未必合适。它用任务的规则性换取复用和持续供给,不是通用处理器的替代品。
[021]
引用 [021]
流水线解释沿途计算与复用,并以规则任务边界限制适用范围。
printed pp. 37-40: balancing computation with I/O, rhythmic data flow through arrays and multiple computations per memory access
高速缓存和脉动阵列看起来是两种完全不同的设计。前者把可能再用的内容留在近处,后者让内容沿着规则路径继续前进。它们却共同改变了一笔账:为完成一次计算,依赖要走多远;一次搬运以后,又能换回多少次有用工作。
这笔账比“有多少计算单元”更接近真实效率。工位数量决定理想上限,供料速度和复用次数决定多少工位此刻真的有活干。增加计算单元是在扩大消费能力,缩短路程、提高复用则是在改善供给,两者不能互相代替。
可以把它写成一个不用公式的判断:一份数据远道而来,只使用一次,路费就由一次计算独自承担;如果沿途使用十次,同样路费便摊到十次计算上。硬件设计、编译器安排和算法重排都在寻找这种摊薄机会,只是它们能控制的层次不同。
代价也没有消失。近处存储容量有限,固定阵列只擅长规则任务,软件和编译器还要把问题整理成硬件能高效执行的形状。性能提升从来不是免费午餐,而是用容量、通用性或开发复杂度交换更少的等待。
[022]
引用 [022]
综合近处留存与沿途复用,明确容量、通用性和软件复杂度代价。
- M. V. Wilkes, Slave Memories and Dynamic Storage Allocation, 1965
- H. T. Kung, Why Systolic Architectures?, 1982
- John L. Hennessy / David A. Patterson, A New Golden Age for Computer Architecture, 2019
Wilkes printed pp. 270-271; Kung printed pp. 37-40; Hennessy / Patterson PDF pp. 6-10
第六章:硬件上限,为什么没有全部兑现
27:02现在回到二〇一七年的谷歌张量处理器。主机先安排任务,模型权重保存在离计算阵列较远的位置,当前输入和中间结果被送到近处。内容准备好以后,同一份权重与中间结果沿规则路径前进,在许多计算单元之间反复使用。
这正是脉动阵列的长处:数据一旦进入,沿途完成多次计算,不必每做一次乘法都回远处取数。可它也解释了开场的停顿。阵列只能充分利用已经到手的数据,不能使用尚未抵达的权重。专用硬件提高了消费速度,没有把供给变成无限。
因此,六万多个计算单元是能力上限,不是持续兑现的保证。任务是否规则、每份数据能复用几次、远处权重供应得多快,共同决定实际有多少工位保持忙碌。
这也解释了为什么同一块芯片运行不同模型时,离峰值数字会有不同距离。需要大量重复计算的任务,取回一次权重便能支撑更多工作;频繁更换权重的任务,通道更早成为限制。峰值没有变,任务让供给与消费之间的比例变了。
[023]
引用 [023]
回收规则阵列、数据复用和权重等待,不口播内部部件英文名。
- Norman P. Jouppi 等, In-Datacenter Performance Analysis of a Tensor Processing Unit, 2017
- H. T. Kung, Why Systolic Architectures?, 1982
TPU PDF pp. 3-10, especially §4; Kung printed pp. 37-40
论文比较不同任务后发现,有些任务取回一份数据便能完成很多计算,阵列可以得到充分利用;另一些任务很快又要换一批权重,等待供给的时间更突出。对后一类任务,提高数据抵达速度,可能比继续扩大计算阵列更有效。
应用服务里也有同样的诊断顺序。接口如果慢在一条共同的数据库查询上,增加工作线程可以接住更多请求,却不会让查询自动返回得更快。先减少往返、合并请求或改变数据位置,往往比继续增加等待者有效。
诊断时要看等待发生在哪里,而不是只看哪个部件最醒目。计算单元长期忙满,可能确实需要更多执行能力;计算单元经常空闲、数据通道持续繁忙,问题更可能在供给;两边都不忙,系统也许正在等待外部输入。不同迹象指向不同动作,不能用同一种“扩容”覆盖。
芯片不是服务器,权重也不是数据库结果。这个类比不提供同一种解决方案,只保留一种判断习惯:扩充执行者以前,先确认瓶颈究竟在消费能力,还是在所有执行者共同等待的供给。
[024]
引用 [024]
服务类比只保留容量与共同供给的诊断顺序,不等同具体机制。
PDF §4, especially pp. 6-10: operations per weight byte, weight stalls and bandwidth / clock sensitivity by workload
硬件不是唯一能缩短路程的地方。二〇二二年,一种注意力加速算法改变了计算顺序。普通做法会产生很大的中间结果,把它写到较远存储,随后为了下一步再读回来;新算法把工作切成较小数据块,让更多步骤在近处完成,只把必要结果送回远处,最终计算结果保持不变。
它像数据库优化器重新安排执行计划:问题没有变,最后答案没有变,但中间结果可以少落盘、少读回。边界是,这里处理的是数学计算,不是数据库查询;结果等价也不是类比保证的,而要由算法本身证明。
这件事把第三条主张推深了一层。数据位置不只由硬件接线决定,也受算法怎样拆分和排序影响。你可以换一块更快的芯片,也可以让同一块芯片少等几次。前者提高上限,后者减少为了得到同一结果所走的路。
[025]
引用 [025]
数据库执行计划类比解释计算重排与减少往返,明确算法和结果边界。
PDF pp. 1-4, Abstract / Figure 1 / §2.1: IO awareness, tiling, exact attention and reduced HBM-SRAM reads / writes
尾声:每生成一步,依赖都要重新集合
30:26现在,把镜头从一项算法拉回整次大语言模型生成。你输入一句话,按下发送。模型要依靠训练后保存下来的大量权重,读取这段对话此前留下的上下文记录,还必须等待前一个字词单位生成,才能继续下一个。
权重可以粗略想成部署后基本固定的一套庞大只读配置:每次请求都要参考。可它不是普通配置文件,每个数值都直接参加计算,数量大到无法全部留在最快位置。上下文记录又有点像会话状态,只属于当前对话并不断增长;但它同样直接参加后续计算,规模和用途都远超普通会话。
还有一条无法跳过的顺序依赖。模型没有生成前一个字词单位,就不知道下一个位置究竟要接在哪段文字后面。它可以在每一步内部并行完成大量计算,却不能提前把尚未确定的整段回答一次算完。阵列内部很宽,生成过程仍要一步一步向前。
两个类比只帮助我们分清“长期保存的模型内容”和“随请求增长的当前状态”。真正发生的事是,每生成一步,权重、上下文记录和前一步结果都要按计算需要重新集合。用户看到文字一点点出现,机器支付的是依赖一次次抵达的时间。
[026]
引用 [026]
配置与会话类比均紧邻数值直接参与计算、规模和用途边界。
PDF pp. 1-3: autoregressive dependency, weights / KV cache in HBM and stage-dependent memory costs
这也给普通软件团队一条更实际的性能路线。第一步,先用能清楚表达业务、容易测试维护,而且性能已经足够的语言与框架。不要为了证明“懂底层”,提前承担一笔尚未产生收益的复杂度。
第二步,用测量找到等待发生在哪里:算法是不是做了无用工作,数据是不是走了太多往返,并行分工是否让执行者互相等待,还是大部分时间其实耗在数据库、网络或外部服务。一个接口九成时间都在等远程查询,把其中一小段业务判断改成更底层语言,用户几乎感觉不到。
测量还要对应用户真正关心的目标。批处理可能在意单位时间完成多少任务,交互服务更在意一次请求多久返回,移动设备还要计算耗电和发热。只追一个局部循环的速度,可能让整体成本上升,或者把等待转移到另一处。瓶颈必须放回完整工作流里判断。
第三步,只把证据确认的热点下沉,并把代码复杂度、硬件绑定、迁移难度和长期维护一起计价。如果一段模型计算每天执行数百万次,优化数据分块可能值得;如果编译器升级或硬件换代已经消除了收益,旧优化也该有退出方案。深入到底层是一种有价格的工具,不是一场职业忠诚测试。
[027]
引用 [027]
三步路线是证据约束下的工程综合,不给出普遍语言或硬件性能排名。
- John Backus 等, The FORTRAN Automatic Coding System, 1957
- Dennis M. Ritchie, The Development of the C Language, 1993
- NVIDIA, CUDA C++ Programming Guide, release 12.4, 2024
- Philippe Tillet / H. T. Kung / David Cox, Triton: An Intermediate Language and Compiler for Tiled Neural Network Computations, 2019
- Norman P. Jouppi 等, In-Datacenter Performance Analysis of a Tensor Processing Unit, 2017
- Tri Dao 等, FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness, 2022
Language and AI sources at the locators recorded in C-012-V6-013 through C-012-V6-018 and C-012-V6-023 through C-012-V6-025
回头看,这八十年的变化没有排成一条从 EDVAC 直达大语言模型的发明链。它们只是让三个反复出现的问题越来越清楚。
第一,架构不是必须复印的物理蓝图,而是程序、状态、控制和执行怎样配合。第二,编程语言不会单向远离机器,它不断在开发者、编译器、运行系统和硬件之间重新分配劳动。第三,性能不只看有多少执行者,还要看依赖何时抵达、一次抵达能被使用多少次,以及为了缩短等待付出的复杂度是否值得。
摩尔电气工程学院团队把“下一步做什么”移进可保存的程序,让换题不再主要依靠人的双手。今天,我们仍在移动同一类边界:把哪些决定交给工具,把哪些数据留在近处,把哪些热点交给专用硬件。
架构让符号可以被保存和执行,却不决定下一个符号更可能是什么。下一期,我们去贝尔实验室,跟随 Claude Shannon 看语言怎样变成一串可以猜测、计数和度量的符号。这是《大语言模型前史》的下一站。
[028]
引用 [028]
结尾只收束三条主张,明确排除 EDVAC 到大语言模型的直接因果。
- John von Neumann, First Draft of a Report on the EDVAC, 1945-06-30
- Thomas Haigh / Mark Priestley / Crispin Rope, Reconsidering the Stored-Program Concept, 2014
- John Backus 等, The FORTRAN Automatic Coding System, 1957
- Dennis M. Ritchie, The Development of the C Language, 1993
- NVIDIA, CUDA C++ Programming Guide, release 12.4, 2024
- Philippe Tillet / H. T. Kung / David Cox, Triton: An Intermediate Language and Compiler for Tiled Neural Network Computations, 2019
- M. V. Wilkes, Slave Memories and Dynamic Storage Allocation, 1965
- H. T. Kung, Why Systolic Architectures?, 1982
- Norman P. Jouppi 等, In-Datacenter Performance Analysis of a Tensor Processing Unit, 2017
- Tri Dao 等, FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness, 2022
- Reiner Pope 等, Efficiently Scaling Transformer Inference, 2023
First Draft §§2.2-2.8 and §§15.1-15.6; later architecture, language and AI sources at the separately bounded locators above
独立片尾:单集识别与系列说明
34:22你刚刚收听的是《原代码》第十二期,《“冯·诺依曼架构”:从 EDVAC 到大语言模型》。
本期也是《原代码》系列《大语言模型前史》的第二期。
系列副标题是,从图灵、冯·诺依曼到 GPT-1。
这个系列关心的不是一条注定抵达今天的发明直线,而是一代代研究者怎样把关于机器与语言的大问题,拆成计算、记忆、概率、知识、学习和预测这些可以动手解决的问题。
欢迎订阅《原代码》,继续收听《大语言模型前史》的后续更新。