ETHZ-计算机体系结构基础笔记-全-

ETHZ 计算机体系结构基础笔记(全)

1:现代微处理器设计

概述

在本节课中,我们将学习计算机架构的基础概念,特别是现代微处理器的设计原理。我们将从计算机架构的定义开始,探讨指令集架构与微架构的区别,并分析单周期、多周期以及流水线处理器的设计思想、优缺点和性能考量。课程旨在为理解更高级的处理器设计(如乱序执行)打下坚实基础。


计算机架构的定义与重要性

计算机架构是设计和选择硬件组件、定义硬件-软件接口,以构建满足特定功能、性能、能耗和成本目标的计算系统的科学与艺术。

为什么学习计算机架构?我们致力于构建更好的系统:使其更快、更便宜、更小、更可靠。通过利用底层电路的进步并分析工作负载,我们可以以更优的方式支持新的应用,例如机器学习、大型语言模型和个性化基因组学。理解计算机的工作原理对于优化现有系统和推动创新至关重要。

当前的计算领域与10到20年前大不相同,应用和技术需求催生了新的架构。每个组件及其接口,乃至整个系统设计,都在被重新审视。


指令集架构与微架构

指令集架构 是软件与硬件之间的契约,它规定了程序员对硬件行为的假设。微架构 则是指令集架构在特定设计约束和目标下的具体硬件实现。

一个简单的类比是汽车:油门踏板是司机(程序员)的接口,而引擎的内部机械或电子实现则是微架构。司机无需了解引擎内部,但了解内部机制可能有助于更高效地驾驶。

  • ISA示例:加法指令的操作码、通用寄存器的数量。
  • 微架构示例:执行乘法指令所需的周期数、寄存器文件的端口数量、是否采用流水线。

微架构的变化通常比ISA更快,因为改变微架构无需修改庞大的上层软件栈。


冯·诺依曼模型与指令处理

经典的冯·诺依曼模型有两个关键属性:

  1. 存储程序:指令和数据存储在同一存储器中。
  2. 顺序执行:指令严格按顺序执行,前一条指令完成后才能开始下一条。

处理一条指令意味着根据ISA的规范,将程序可见的架构状态 转换为新的架构状态。架构状态包括内存、寄存器和程序计数器。

微架构则定义了如何实现这种转换。它可以使用程序不可见的微架构状态 来优化执行速度。


单周期微处理器

在单周期微处理器设计中,每条指令在一个时钟周期内完成。整个数据通路是组合逻辑,在周期结束时更新架构状态。

关键问题:时钟周期时间由执行时间最长的指令决定。例如,如果访存指令需要600皮秒,而加法指令只需400皮秒,时钟周期也必须设为600皮秒。

设计原则分析

  • 关键路径设计:违反。为最坏情况优化,拖累了常见情况。
  • 常见情况设计:违反。未针对执行频率高的指令进行优化。
  • 平衡设计:可能违反。资源可能未被均衡利用。

单周期设计时钟频率低,硬件成本高(需要多个ALU等资源以避免复用)。


多周期微处理器

多周期设计将指令处理分解为多个阶段(状态),每个阶段在一个较短的时钟周期内完成。不同指令所需的周期数不同。

优势

  • 更高的时钟频率:周期时间由最慢的阶段决定,而非最慢的指令。
  • 硬件复用:例如,单个ALU可以在不同周期用于不同目的。
  • 灵活性:易于处理耗时不确定的操作(如远程内存访问),只需在等待状态中循环即可。

劣势

  • 硬件开销:需要额外的寄存器存储中间结果。
  • 时序开销:每个周期都有锁存数据的开销,随着周期变短,开销占比增大。
  • 有限并发性:任何时刻只使用了机器的一小部分硬件。

多周期设计遵循冯·诺依曼顺序执行模型,通过一个有限状态机来控制指令处理的各个阶段。


从多周期到流水线

多周期处理器的主要局限是硬件利用率低。当一条指令处于解码阶段时,取指硬件是空闲的;当它执行时,访存硬件是空闲的。

核心思想:流水线化。像工厂装配线一样,让不同的指令同时处于处理过程的不同阶段。

  • 当指令I在解码时,取指下一指令I+1。
  • 当指令I在执行时,解码指令I+1,取指指令I+2。
  • 以此类推。

理想情况下,一个K级流水线可以将吞吐量提高K倍(每个周期完成一条指令),但每条指令的延迟(完成所需时间)可能因流水线寄存器开销而略有增加。


流水线处理器的挑战与理想条件

实现高效流水线需要满足几个理想条件,但指令处理往往并不完美:

  1. 相同操作:流水线应重复执行相同的操作序列。但不同指令(如加法与加载)需要的阶段可能不同,导致某些流水段空转(外部碎片)。
  2. 操作独立:流水线中的指令应彼此独立。但实际程序中指令间存在数据依赖和控制依赖,这会导致流水线冲突,必须通过停顿或转发技术解决。
  3. 均匀划分:处理过程应能被均匀地划分为耗时相等的阶段。但硬件模块(ALU、内存访问)的延迟不同,导致时钟周期由最慢阶段决定,其他阶段出现空闲时间(内部碎片)。

性能评估框架

一个通用的性能公式是:
程序执行时间 = 指令数 × 平均每条指令周期数 × 时钟周期时间

  • 单周期:CPI = 1,但时钟周期时间很长。
  • 多周期:CPI > 1(每条指令需多个周期),但时钟周期时间短。
  • 理想流水线:CPI ≈ 1(吞吐量高),时钟周期时间短(但比多周期略长,因流水线寄存器开销)。

设计微架构时,需要在CPI和时钟周期时间之间进行权衡。


总结

本节课我们一起学习了现代微处理器设计的基础。我们从计算机架构的广义定义出发,明确了ISA与微架构的角色。接着,我们深入分析了三种基本的处理器设计模型:

  1. 单周期设计:概念简单,但因最坏情况决定性能而效率低下。
  2. 多周期设计:通过将指令分解为阶段提高了时钟频率和硬件复用率,但硬件利用率和并发性仍有限。
  3. 流水线设计:通过重叠执行多条指令大幅提高了吞吐量,是高性能处理器的基础,但也引入了依赖冲突和流水线碎片等挑战。

理解这些基础模型及其权衡,是接下来学习更复杂技术(如流水线冲突解决、分支预测和乱序执行)的关键。在下一讲中,我们将深入探讨流水线处理器的具体实现及其面临的问题。

2:流水线与乱序微处理器设计 (Spring 2025)

概述

在本节课中,我们将深入学习流水线处理器的核心概念,探讨其理想模型与实际实现之间的差距。我们将重点分析导致流水线停顿(Stall)的各种原因,特别是数据依赖性问题,并介绍如何通过数据旁路(Bypassing)等技术来缓解这些问题。最后,我们将引入乱序执行(Out-of-Order Execution)的基本思想,作为提升处理器吞吐率、容忍长延迟操作的关键技术。


理想流水线与现实挑战

上一节我们介绍了多周期处理器和流水线的基本概念。理想流水线假设各阶段工作量均衡、操作相互独立。然而,现实中的指令流水线并非如此理想。

首先,不同流水线阶段的延迟可能不同。为了统一时钟控制,我们必须让所有阶段遵循最慢阶段的时钟周期,这导致了内部碎片化(Internal Fragmentation)。

其次,不同指令执行不同的操作,但被强制通过相同的流水线阶段。例如,不访问内存的指令仍需经过内存访问阶段,这造成了硬件资源的浪费,即外部碎片化(External Fragmentation)。

更重要的是,指令之间并非相互独立。它们之间存在依赖关系,流水线必须能够检测并解决这些依赖,以确保最终结果的正确性。


流水线停顿的原因

流水线停顿是指流水线停止流动的状态。理想情况下,流水线应持续流动。停顿主要由以下原因引起:

  1. 资源竞争(Resource Contention):当多个流水线阶段需要同时使用同一硬件资源时发生。
  2. 依赖关系(Dependencies):指令间的数据依赖或控制依赖导致后续指令无法继续执行。
  3. 长延迟操作(Long Latency Operations):某些操作(如访存)需要多个周期才能完成,会阻塞流水线。

依赖关系有时也被称为“冒险”(Hazards),它规定了指令间必须遵守的执行顺序。主要有两种基本类型:

  • 数据依赖(Data Dependence):一条指令需要使用另一条指令产生的结果。
  • 控制依赖(Control Dependence):一条指令是否执行取决于前一条分支指令的结果。

本节我们将主要关注数据依赖。


资源竞争的处理

资源竞争,有时也称为资源依赖,并非程序语义所固有,而是由硬件资源限制引起。

以下是处理资源竞争的两种基本方法:

  • 消除竞争根源:例如,复制资源或提高资源吞吐率。一个典型例子是使用独立的指令缓存(I-Cache)和数据缓存(D-Cache),或者为内存提供多端口访问,以支持取指和加载/存储操作的并发进行。
  • 检测竞争并停顿:如果无法增加资源,则需检测资源竞争,并停顿其中一个竞争阶段。通常,会停顿流水线中较晚的阶段(如取指阶段),以便让较早的、正在使用关键资源的指令(如处于内存访问阶段的加载指令)能够继续执行。

数据依赖的类型

数据依赖是影响流水线性能的关键因素。主要分为三类:

  1. 真数据依赖 / 流依赖(Flow Dependence, Read After Write - RAW)

    • 含义:后续指令需要读取前导指令写入的结果。这是真正的生产者-消费者关系。
    • 示例指令I: ADD R1, R2, R3 后跟 指令J: SUB R4, R1, R5。指令J依赖于指令I写入R1的结果。
    • 公式表示:如果指令 I_j 使用指令 I_i 写入的寄存器,且 i < j,则存在流依赖。
  2. 反依赖(Anti Dependence, Write After Read - WAR)

    • 含义:后续指令要写入一个寄存器,而前导指令需要读取该寄存器的旧值。
    • 示例指令I: ADD R1, R2, R3 后跟 指令J: SUB R2, R4, R5。指令J写入R2不能发生在指令I读取R2之前。
    • 性质:这是一种“假依赖”,源于架构寄存器数量有限。如果寄存器命名空间足够大,可以消除。
  3. 输出依赖(Output Dependence, Write After Write - WAW)

    • 含义:两条指令都要写入同一个寄存器。
    • 示例指令I: ADD R1, R2, R3 后跟 指令J: SUB R1, R4, R5。必须保证R1最终的值是指令J写入的结果。
    • 性质:同样是一种“假依赖”,源于寄存器名称冲突,可通过重命名消除。

核心区别:流依赖是的依赖,必须严格遵守以保证程序正确性。反依赖和输出依赖是名称的依赖,由有限的架构寄存器引起,在硬件层面可以通过寄存器重命名技术消除。


处理流依赖的方法

处理流依赖有六种基本思路:

  1. 检测并停顿:检测到依赖后,停顿流水线,直到所需值在寄存器文件中可用。这是性能最低的方案。
  2. 检测并旁路(Bypassing / Forwarding):检测到依赖,但不立即停顿。当生产者指令产生结果后,通过专用通路直接“旁路”给消费者指令,无需等待写回寄存器文件。
  3. 软件调度(编译时):编译器在生成代码时,了解底层流水线结构,通过重排指令顺序或插入空操作(NOP)来避免硬件停顿。
  4. 乱序执行:检测到依赖后,将后续无法立即执行的指令移开,让独立的指令继续执行。这是本节后半部分的重点。
  5. 值预测(Value Prediction):预测所需的值并继续执行,待实际值产生后再验证预测是否正确。这增加了复杂性。
  6. 细粒度多线程(Fine-Grained Multithreading):每个周期从不同的线程取指,利用线程间的天然独立性来填充流水线。这种方法以牺牲单线程性能为代价,常见于GPU。

数据旁路(Bypassing/Forwarding)

数据旁路是减少流水线停顿的关键硬件技术。

问题:在基本流水线中,如果消费者指令需要等待生产者指令将结果写回寄存器文件后才能读取,将导致多个周期的停顿。

解决方案:增加额外的数据通路和检测逻辑,使得生产者指令的结果一旦在流水线中产生(例如在ALU执行阶段末或访存阶段),就可以直接转发给需要该结果的消费者指令的输入端口,而无需经过寄存器文件。

旁路路径示例(以经典5级流水线为例):

  • 从EX/MEM寄存器旁路到ALU输入:将刚计算出的结果直接用于下一条指令的运算。
  • 从MEM/WB寄存器旁路到ALU输入:将刚从内存读取的数据或更早的计算结果用于当前指令的运算。
  • 寄存器文件内部转发:在同一时钟周期内,前半段写回的结果在后半段可以被读取。

旁路逻辑:硬件需要比较消费者指令的源寄存器ID与流水线中所有后续指令的目的寄存器ID。如果匹配且该结果已产生(位于某个流水线寄存器中),则通过多路选择器(MUX)选择旁路数据作为输入。

局限性:并非所有依赖都能通过旁路解决。例如,加载指令(LOAD)的数据在访存阶段结束时才可用,无法直接旁路给当前周期正处于执行阶段的下一条指令的ALU,否则会延长关键路径。这种情况下,仍需插入一个停顿周期(气泡)。


精确异常(Precise Exceptions)

在支持乱序执行或长延迟操作之前,必须保证处理器的异常行为符合冯·诺依曼模型的顺序语义,即精确异常

为什么需要精确异常?

  1. 维护ISA语义:顺序执行是编程模型的基础。
  2. 便于调试:当程序因异常(如除零错误)停止时,程序员希望看到的是异常指令之前的所有指令已生效,之后的指令都未生效的状态。否则调试将极其困难。
  3. 操作系统支持:进程的上下文切换、中断处理和恢复都需要一个明确定义的、一致性的机器状态。

精确状态的定义:当异常发生时,硬件必须保证:

  • 所有在异常指令之前的指令都已执行完毕并更新了架构状态(寄存器、内存)。
  • 所有在异常指令之后的指令都像从未执行过一样,没有更新任何架构状态。

多周期操作带来的挑战:在流水线中,一条长延迟指令(如除法)可能还未完成,后续独立的短指令(如加法)可能已经执行完毕。如果允许加法指令直接写回寄存器文件,就破坏了顺序提交的语义。一旦除法指令发生异常,架构状态将处于不一致的中间状态。


重排序缓冲区(Reorder Buffer, ROB)

为了解决多周期操作和乱序执行下的精确异常问题,引入了重排序缓冲区(ROB)

核心思想:允许指令在功能单元中乱序执行完成,但在更新架构状态(寄存器文件、内存)之前,先将结果按顺序写入一个缓冲区(ROB)。只有当一条指令在ROB中成为最旧的指令,且已确认执行无异常时,才将其结果提交(Commit/Retire)到架构状态。

ROB的工作流程

  1. 分配(Allocate):指令译码后,按程序顺序在ROB中分配一个条目。
  2. 执行(Execute):指令在功能单元中乱序执行。完成后,将结果和目的寄存器ID写入其ROB条目。
  3. 提交(Commit/Retire):ROB头部指针指向最旧的指令。检查该指令:
    • 如果已完成且无异常,则将其结果从ROB写入架构寄存器文件或内存,然后释放该ROB条目。
    • 如果检测到异常,则清空流水线和ROB,跳转到异常处理程序,并将该指令的PC等状态提供给处理器。

ROB的优势

  • 以相对简单的方式实现了精确异常。
  • 自然地消除了反依赖(WAR)和输出依赖(WAW),因为所有写操作都通过ROB顺序提交,寄存器重命名可以在此基础上进行。

ROB的挑战

  • 数据获取:一条指令需要操作数时,该操作数可能还在ROB中(未写回寄存器文件)。需要一种机制来从ROB中读取数据。
  • 实现复杂度:ROB是一个循环队列,但根据寄存器ID查找值是一种内容寻址(CAM)操作,硬件开销较大。

优化:寄存器重命名与ROB指针
为了高效地从ROB读取数据,可以将寄存器文件扩展。每个架构寄存器对应一个条目,包含:

  • 有效位(Valid Bit):指示寄存器文件中的值是否是最新且可用的。
  • 值(Value):如果有效位为1,则为实际数据值。
  • 标签(Tag):如果有效位为0,则该标签指向即将产生该寄存器值的ROB条目ID。

当指令需要读取寄存器时:

  1. 检查寄存器文件对应条目的有效位。
  2. 如果有效位为1,直接使用该值。
  3. 如果有效位为0,则使用存储的标签(ROB ID)去索引ROB(这是一个RAM操作,而非CAM操作),从中获取值或持续监听该ROB条目的完成广播。

这实质上是一种寄存器重命名:将架构寄存器(如R3)动态地映射到物理寄存器(一个ROB条目)。这消除了假依赖,并为后续的乱序执行奠定了基础。


乱序执行(Out-of-Order Execution)概念

在引入了ROB保证了顺序提交和精确异常后,我们可以进一步优化流水线前端的效率,这就是乱序执行

核心问题:即使有旁路,在顺序发射(In-Order Issue)的流水线中,一条长延迟指令或数据未就绪的指令,会阻塞后续所有指令的发射,即使这些后续指令是独立的。

核心思想:借鉴数据流(Data Flow) 计算模型。在数据流机中,指令在其所有操作数就绪时立即“发射”(Fire)执行。我们将此思想引入到冯·诺依曼架构的内部。

基本机制

  1. 顺序取指与译码:指令按程序顺序从缓存中取出并译码。
  2. 寄存器重命名与分发:译码后,进行寄存器重命名以消除假依赖。然后,指令被分发到保留站(Reservation Stations) 中。保留站是指令等待执行的地方。
  3. 乱序发射(调度):在保留站中,每条指令监控其所有源操作数是否就绪(来自寄存器文件或已完成的指令广播)。一旦某条指令的所有操作数就绪,它就被标记为“就绪”。
  4. 唤醒与选择:每个功能单元从对应保留站的就绪指令中选择一条(根据优先级策略,如年龄)发射到执行单元。这就是乱序发射
  5. 乱序完成与顺序提交:指令在执行单元中乱序完成,将结果写回ROB,并通过公共数据总线(Common Data Bus, CDB) 广播结果及其标签(ROB ID)。所有正在等待该标签的保留站中的指令会捕获这个值。最后,指令在ROB中按程序顺序提交。

类比:将流水线比作高速公路。顺序发射时,一辆慢车(长延迟指令)会阻塞整条车道。乱序执行相当于为慢车设置了“休息区”(保留站),让它驶离主道,让其他快车(独立指令)先行通过。当慢车准备好后(操作数就绪),再从休息区驶入执行车道。

乱序执行处理器的结构

     顺序前端                    乱序核心                          顺序提交
[取指 -> 译码 -> 重命名] -> [保留站(调度窗口) -> 功能单元] -> [重排序缓冲区(ROB) -> 提交]
       |                          |                                |
    寄存器文件                   结果广播(CDB)                   架构状态更新
       |__________________________|
                旁路/唤醒
  • 顺序前端:保证控制流的正确性。
  • 乱序核心:是一个基于数据流原理执行的引擎,负责挖掘指令级并行。
  • 顺序提交:通过ROB保证最终结果的顺序语义和精确异常。

总结

本节课我们一起深入探讨了流水线处理器的核心挑战与进阶设计。

我们首先分析了理想流水线与现实之间的差距,重点讲解了导致流水线停顿的三大原因:资源竞争、数据依赖和长延迟操作。我们详细剖析了三种数据依赖(RAW、WAR、WAW),并指出只有RAW是真依赖。

接着,我们学习了处理依赖的多种方法,特别是数据旁路技术,它能有效减少RAW依赖引起的停顿。然后,我们探讨了实现精确异常的必要性,并引入了重排序缓冲区(ROB) 这一关键结构。ROB通过顺序提交机制,在支持乱序完成的同时,保证了处理器的状态始终满足顺序语义,便于调试和异常处理。

最后,我们引出了乱序执行的概念。通过将指令分发到保留站,并基于操作数就绪情况进行调度,乱序执行引擎允许独立的指令越过被阻塞的指令先执行,从而更好地容忍长延迟操作,挖掘程序中的指令级并行。ROB和寄存器重命名技术为乱序执行提供了坚实的基础。

下一节,我们将通过具体例子,深入观察乱序执行引擎的详细工作流程。

3:乱序微处理器设计 II 与分支预测

概述

在本节课中,我们将继续学习乱序执行微处理器的设计细节,并深入探讨现代处理器中一个至关重要的性能优化技术:分支预测。我们将从乱序执行的硬件实现机制开始,然后分析控制流依赖带来的挑战,并介绍如何通过分支预测来缓解这些挑战。

乱序执行回顾与算法形式化

上一节我们介绍了乱序执行的基本概念,本节中我们来看看其核心算法——Tomasulo算法的具体形式化描述。

Tomasulo算法的核心思想是:如果保留站条目可用,则在重命名指令后,将重命名后的操作插入到保留站中。这仅在保留站条目可用时发生。保留站条目本身可能成为资源依赖的来源。如果保留站条目不可用,则流水线会停顿。

指令在保留站中等待,直到其所有源操作数就绪。它通过监听公共数据总线来跟踪源操作数的就绪状态。每当一条指令执行完毕,它就会在总线上广播其结果标签。任何等待该结果的指令都会捕获该值,并标记其对应的源操作数为就绪。一旦指令的所有源操作数就绪,它就会被分派到功能单元开始执行。执行完成后,该指令会再次广播其结果,并更新寄存器重命名表,回收其标签。

以下是Tomasulo算法中保留站条目的关键字段描述:

  • Op: 操作码。
  • Qj, Qk: 将产生源操作数的保留站编号。如果为0,则表示源操作数已就绪或在Vj/Vk中。
  • Vj, Vk: 源操作数的值。
  • Busy: 指示该保留站条目是否被占用。

乱序执行示例分析

让我们通过一个具体的程序片段来模拟Tomasulo算法的执行过程。

考虑以下指令序列:

  1. MUL R3, R1, R2 // R3 = R1 * R2
  2. ADD R5, R3, R4 // R5 = R3 + R4
  3. ADD R7, R2, R6 // R7 = R2 + R6
  4. ADD R10, R8, R9 // R10 = R8 + R9
  5. MUL R12, R7, R10 // R12 = R7 * R10
  6. ADD R5, R5, R11 // R5 = R5 + R11

假设初始时所有寄存器值有效,且保留站为空。模拟过程如下:

  • 周期1: 取指MUL
  • 周期2: 解码MUL。检查保留站(乘法单元)有空闲条目。检查源操作数R1和R2,在寄存器重命名表中均为有效值。因此,该指令源操作数立即可用,被分派到乘法单元开始执行(假设乘法延迟为6周期)。同时,将目标寄存器R3重命名为该保留站条目的标签(例如X),并在寄存器重命名表中将R3标记为无效,指向标签X
  • 周期3MUL继续执行。解码ADD R5, R3, R4。检查加法保留站有空闲条目。检查源操作数:R3在寄存器重命名表中无效,其标签为X,因此该源操作数未就绪;R4有效。将目标寄存器R5重命名为新条目标签(例如A)。该指令因等待R3而无法执行。
  • 周期4MUL继续执行。ADD R5, R3, R4在保留站中等待。解码ADD R7, R2, R6。其源操作数R2和R6均有效,因此该指令被分派到加法单元开始执行(假设加法延迟为4周期)。目标寄存器R7被重命名为标签B
  • 周期5-7: 指令继续执行或等待。ADD R10, R8, R9被解码,其源操作数均有效,被分派执行。目标寄存器R10被重命名为标签C
  • 周期8MUL指令执行完成。它在公共数据总线上广播标签X和结果值(R1*R2)。正在等待源操作数标签X的指令(即ADD R5, R3, R4)捕获该值,并更新其对应的源操作数值。现在该指令的两个源操作数均已就绪。
  • 周期9ADD R5, R3, R4因其操作数就绪而被分派执行。同时,ADD R7, R2, R6执行完成,广播标签B和结果值。MUL R12, R7, R10指令正在等待标签B,因此捕获该值。但它仍在等待另一个源操作数(标签C)。
  • 后续周期: 过程以此类推。ADD R10, R8, R9完成后广播标签C,使MUL R12, R7, R10就绪并执行。最后一条ADD R5, R5, R11需要注意:它读取R5,而R5的最新生产者是标签A(第二条指令),但该指令自身又写R5。因此,在解码时,它首先获取R5的当前重命名标签A作为源操作数,然后立即将R5重命名为自己的保留站标签D,作为新的生产者。

通过这个例子,我们可以看到指令如何根据操作数就绪情况而非程序顺序被动态调度执行,从而隐藏了长延迟操作(如乘法)带来的停顿。

支持精确异常的乱序处理器

上一节我们看到了基本的乱序执行机制,本节中我们引入对精确异常的支持,这是现代处理器的必备特性。

在仅支持乱序执行的模型中,指令一旦执行完成就立即更新架构状态(寄存器文件)。但这会导致问题:如果一条后续的、在程序顺序上更早的指令发生异常,处理器状态将无法回滚到一个一致的、所有先前指令已提交的状态。

解决方案是引入重排序缓冲区。ROB是一个循环缓冲区,指令在解码后被按程序顺序分配一个ROB条目。指令乱序执行完毕后,其结果并不直接写回架构寄存器文件,而是写入其ROB条目。仅当指令到达ROB头部(即所有之前的指令都已提交)时,才将其结果从ROB写回架构寄存器文件并提交。这保证了指令的提交(即架构状态的更新)严格按程序顺序进行。

因此,现代乱序处理器通常维护两个寄存器文件:

  • 前端寄存器文件/重命名寄存器文件: 存储着包括推测结果在内的最新寄存器值。指令解码时从这里读取源操作数,指令执行完成后将结果写回这里。它支持乱序读写。
  • 架构寄存器文件: 存储已提交的、确定的架构状态。只有当指令从ROB提交时,才将结果写回这里。它只按顺序更新。

当发生分支预测错误或异常时,处理器需要刷新流水线中该错误路径之后的所有指令。恢复过程包括:将架构寄存器文件的内容复制到前端寄存器文件(从而回滚所有未提交的推测状态),然后从正确的地址重新开始取指。

分支预测导论

控制流依赖,尤其是条件分支,是限制处理器指令级并行性的主要瓶颈。分支指令的下一条指令地址在分支条件被解析之前是未知的,这会导致流水线停顿。

分支预测的目标是在分支指令被解码甚至取指之前,就预测其执行方向(跳转或不跳转)以及可能的跳转目标地址,从而让取指单元能够不间断地工作。

分支预测不准确的代价非常高。假设一个20级流水线、5路超标量的处理器,一次分支预测错误需要刷新后续20个周期内已取入的指令(20 * 5 = 100条指令槽位)。即使预测准确率达到99%,性能损失也可能高达20%。因此,高精度的分支预测对性能至关重要。

分支预测基础

一个完整的分支预测机制需要在取指阶段提供三类信息:

  1. 当前取指的指令是否是分支指令?
  2. 如果是条件分支,它的方向是跳转(taken)还是顺序执行(not taken)?
  3. 如果预测为跳转,它的目标地址是什么?

以下是解决这些问题的基本硬件结构:

  • 分支目标缓冲区: BTB是一个缓存结构,以指令地址(PC)的一部分作为索引。它存储之前遇到的分支指令的跳转目标地址。如果在BTB中命中,则表明当前指令很可能是分支指令,并直接给出预测的目标地址。
  • 方向预测器: 同样以PC索引,预测条件分支的方向。最简单的形式是1位“上次结果”预测器。
  • 下一地址选择逻辑: 结合BTB命中信号和方向预测器的结果,决定下一个取指地址是PC+4(顺序地址)还是BTB提供的目标地址。

静态分支预测

静态分支预测在编译时或由程序员指定,无需运行时硬件支持。其方法简单但灵活性有限。

以下是几种常见的静态预测策略:

  • 总是预测不跳转: 预测所有分支都不跳转,继续顺序执行。适用于编译器将大概率执行的路径布局为fall-through的情况。
  • 总是预测跳转: 预测所有分支都跳转,使用BTB中的目标地址。对于循环结束分支(向后跳转)通常有效。
  • 向后跳转/向前不跳转: 基于观察:循环结束的分支(目标地址小于当前PC)通常跳转;条件判断分支(目标地址大于当前PC)通常不跳转。
  • 基于剖析的预测: 编译器使用代表性的输入集运行程序,收集分支行为剖面,并根据此剖面在编译时决定每个分支的预测方向,并将提示信息编码在指令中。
  • 基于程序分析的启发式预测: 编译器根据代码模式应用启发式规则,例如预测“小于零”的分支为不跳转,或保护循环的条件分支为跳转。

动态分支预测

动态分支预测在处理器运行时进行,能够根据程序的实际执行历史自适应地调整预测,通常能获得比静态预测高得多的准确率,但需要额外的硬件资源。

基本动态预测器

  • 上次结果预测器: 使用1位饱和计数器记录每个分支上次的执行结果,并预测本次相同。对于稳定(总是跳转或总是不跳转)的分支效果很好,但对于交替跳转的分支(如T, N, T, N...)准确率只有0%。

    • 状态机预测T -> 实际T -> 保持预测T预测T -> 实际N -> 切换为预测N
  • 两位饱和计数器预测器: 也称为双模态预测器。使用2位状态机,为预测改变引入了“惯性”,避免因单次结果波动而立即翻转预测。

    • 状态强不跳转 (SN) -> 弱不跳转 (WN) -> 弱跳转 (WT) -> 强跳转 (ST)
    • 规则: 只有当实际结果与当前弱状态相反时,才会切换到另一个弱状态;实际结果与强状态相同则保持,相反则退化为弱状态。这提高了对轻微波动分支的预测稳定性。

利用相关性的高级预测器

基本预测器只考虑分支自身的近期历史。许多分支的行为与其他分支或自身更早的历史相关。

  • 全局历史预测器: 发现分支结果可能与其他分支的结果相关(全局相关)。它维护一个全局历史寄存器,记录最近所有分支的执行结果(例如,每位记录一个分支是T还是N)。用GHR的内容作为索引,去访问一个模式历史表,该表的每个条目是一个2位饱和计数器。这样,预测基于“在特定的全局分支历史上下文下,该分支通常如何行为”。

    • 公式表示Prediction = PHT[GHR],其中PHT是模式历史表,GHR是全局历史寄存器。
  • 局部历史预测器: 发现分支结果可能与其自身过去多次执行的结果相关(局部相关)。它为每个分支维护一个局部历史寄存器,记录该分支最近几次的执行结果。用LHR的内容索引一个局部模式历史表,进行预测。

    • 公式表示Prediction = PHT_i[LHR_i],其中PHT_i是分支i的私有模式历史表,LHR_i是分支i的局部历史寄存器。

现代高性能处理器的分支预测器通常是这些基本结构的复杂组合(如锦标赛预测器、感知机预测器等),并具有多级结构,以同时捕获局部和全局相关性,实现极高的预测准确率(>95%)。

总结

本节课中我们一起学习了乱序微处理器设计的核心算法——Tomasulo算法的具体工作流程,并通过示例加深了理解。我们看到了如何通过重排序缓冲区在乱序执行中实现精确异常,这是现代处理器可靠性的基石。随后,课程转向了另一个性能关键主题:分支预测。我们分析了分支预测的必要性和巨大性能影响,介绍了静态和动态预测的基本方法,并探讨了如何利用局部历史和全局历史相关性来构建更精准的动态分支预测器。理解这些机制是掌握现代高性能CPU设计精髓的关键。

4:分支预测 II 与预取

在本节课中,我们将继续学习分支预测技术,并探讨如何通过预取来缓解内存延迟问题。我们将深入了解全局与局部历史预测、混合预测器、感知机预测器等高级概念,并介绍预取的基本原理、关键问题以及几种基础算法。

概述

上一节我们介绍了分支预测的基本概念和简单的两级计数器预测器。本节中,我们将深入探讨更高级的分支预测技术,包括利用分支间的全局相关性、同一分支的局部相关性,以及结合多种预测器的混合方法。随后,我们将转向另一个提升处理器性能的关键技术——预取,学习其核心思想、设计挑战和基础实现方法。

全局与局部历史预测

我们之前讨论了两级全局历史分支预测器(GAg)。它使用一个全局历史寄存器(GHR)记录最近N个分支的方向,并用此GHR值索引一个模式历史表(PHT,内含两比特计数器)来进行预测。其核心思想是:一个分支的结果可能与之前执行的其他分支结果相关。

然而,仅使用GHR会丢失“当前正在预测的是哪个分支”这一上下文信息。因此,McFarling提出了Gshare预测器,其改进在于将程序计数器(PC)全局历史寄存器(GHR) 进行哈希(例如异或操作)后,再用于索引PHT。公式表示为:
索引 = PC XOR GHR
这样做增加了上下文信息,提高了预测准确性,并更好地利用了PHT表项。

除了全局相关性,同一分支的结果也可能与其自身过去的结果相关,这称为局部相关性。例如,一个循环末尾的条件分支会呈现“1110”(取、取、取、不取)的固定模式。为了捕捉这种模式,我们可以使用局部历史预测器。

以下是局部历史预测器的关键组件:

  • 局部历史表(LHT):由PC索引,每个表项是一个记录该分支最近N次方向的历史寄存器。
  • 模式历史表(PHT):由局部历史值索引,每个表项是一个两比特(或更多)的饱和计数器,用于给出最终预测。

局部历史预测器能很好地预测具有固定模式的循环分支。

混合预测与感知机预测

不同的分支可能适合不同的预测策略。例如,循环分支适合局部历史预测,而存在条件依赖的分支可能适合全局历史预测。因此,混合预测器应运而生,它结合多种预测器,并通过一个元预测器(或选择器)动态选择当前最可能准确的预测结果。Alpha 21264处理器就采用了结合全局和局部历史预测器的混合方案。

另一种思路是使用简单的机器学习模型。感知机预测器将分支预测视为一个二分类问题。其核心操作是计算一个权重向量 W 与输入向量 X(由GHR的位构成,取值为+1或-1)的点积,并加上一个偏置权重 w0。如果结果大于0,则预测分支跳转。公式表示为:
预测输出 = w0 + Σ (wi * xi)
其中,xi 是GHR的第i位(+1表示跳转,-1表示不跳转),wi 是对应的权重。预测器会在线学习并更新这些权重,以捕捉分支结果与历史之间的复杂线性关系。

预取技术基础

分支预测旨在保持指令流水线充满,而预取则旨在在处理器真正需要数据之前,就将其从慢速内存提前加载到高速缓存中,从而隐藏内存访问延迟。

设计一个预取器需要回答四个关键问题:

  1. 预取什么(What):预测哪些内存地址将被访问。这依赖于对程序访存模式(如步长、流)的识别。
  2. 何时预取(When):发起预取的时机。过早可能导致数据在被使用前就被换出缓存(缓存污染),过晚则无法完全隐藏延迟。
  3. 存放在哪(Where):预取的数据放在何处。通常是直接放入缓存,但可能引发污染;也可以放入独立的预取缓冲区,但这增加了设计复杂性。
  4. 如何实现(How):由谁、以何种方式执行预取。可以是软件(编译器插入预取指令)、硬件(专用逻辑监控访存)或基于执行(例如,派发一个辅助线程进行预取)。

步长预取器

一种经典且常用的硬件预取器是步长预取器。它监控连续的访存地址,如果发现稳定的地址差值(步长),就预测后续访问将遵循这一模式并发起预取。

步长预取器主要有两种实现方式:

  • 基于指令(PC)的步长预取:为每个加载/存储指令(通过PC标识)维护上一次访问的地址和观测到的步长。当同一指令再次执行且步长稳定时,则预取 当前地址 + 步长。这种方式能精确区分不同指令的访存模式。
  • 基于内存区域的步长预取:将内存划分为区域,为每个区域记录访存步长。无论哪个指令访问该区域,都使用相同的步长进行预测。这种方式能捕捉由多个不同指令共同形成的步长访问模式。流预取是步长预取的一个特例,其步长为1(连续地址访问),在IBM Power等处理器中广泛应用。

预取器的性能通常用几个指标衡量:准确性(预取的数据中被实际使用的比例)、覆盖率(预取消除的缓存缺失占所有缺失的比例)和及时性(数据在需要时已存在于缓存中的比例)。设计时需要在侵略性(高覆盖率、高及时性)和保守性(高准确性、低缓存污染/带宽开销)之间取得平衡。

总结

本节课我们一起深入学习了高级分支预测技术。我们看到了如何利用全局和局部历史信息来提升预测精度,以及如何通过混合预测器和感知机等机器学习模型来应对不同类型的分支。随后,我们转向了预取技术,了解了其解决内存延迟问题的核心思想,探讨了设计预取器必须考虑的四个关键问题,并介绍了基础的步长预取器及其两种实现方式。这些技术是现代高性能处理器不可或缺的组成部分,对于充分挖掘硬件潜力至关重要。

5:预取技术 II (Spring 2025)

概述

在本节课中,我们将继续探讨计算机架构中的一个基础且迷人的主题——预取技术。我们将深入硬件预取器的设计思路,并重点介绍一种强大的预取方法:基于执行的预取。课程将涵盖从简单的模式检测到复杂的、利用程序执行来预测未来内存访问的各种技术。


回顾:预取的基本问题与指标

上一节我们介绍了预取技术中的核心问题:预取什么、何时预取、预取到哪里以及如何预取。这些都是在内存层次结构中进行有效数据预取的关键考量。

在评估预取器性能时,我们主要关注三个辅助指标:

  • 准确率:预取的数据被实际使用的比例。
  • 覆盖率:预取器能够消除的缓存缺失的比例。
  • 及时性:预取的数据在需要之前多久到达缓存。

此外,设计预取器时还需考虑额外的带宽消耗和可能引起的缓存污染,因为带宽是宝贵资源,而无效的预取会挤占有用的缓存行。


基于局部性的预取器

我们讨论过步长预取和流预取,它们能很好地处理规则的内存访问模式。然而,实际应用中并非所有访问都具有完美的步长。

一种更通用的方法是基于局部性的预取。其核心思想是监控特定内存区域内的访问模式,而非固定的步长。

  1. 将内存地址空间划分为区域。
  2. 当某个区域发生缓存缺失时,开始监控该区域附近的一系列访问。
  3. 通过观察访问地址的变化方向(例如,持续增长),建立“方向”置信度。
  4. 当置信度足够高时,预取器开始预取该方向上的多个地址,并以滑动窗口的方式持续监控和预取。

这种方法可以捕捉非步长的、但具有空间局部性的访问流,在现实处理器中非常有效。其攻击性(预取数量、窗口大小)可以通过参数调节,并可与步长检测结合以提高效率。


复杂模式与关联预取

实际工作负载可能展现出更复杂的模式,例如重复的“差值”序列。假设连续内存访问的地址差序列为 +7, -6, +12, +6, -5, ...,并且这个序列会重复出现。

差值关联预取器 旨在学习和预测这种模式:

  • 记录历史差值序列作为“签名”。
  • 当观察到特定签名时,预测接下来可能出现的差值。
  • 例如,看到模式 +7, -6, +12 后,预测下一个差值是 +6

这可以看作是更一般的关联预取的特例。最早的关联预取器基于内存地址本身进行关联。

地址关联预取 的基本概念是:

  • 记录历史缓存块地址的访问序列(例如 A, B, C, D)。
  • 构建一个概率模型(如马尔可夫模型),描述看到地址X后,地址Y出现的概率。
  • 当再次遇到地址A时,根据历史记录预取与它关联的地址B、C、D。

优势:能够覆盖任意访问模式,包括不规则的数据结构遍历。
挑战

  • 若使用地址关联,存储开销巨大,因为需要为大量地址记录关联信息。
  • 无法减少强制性缺失,因为必须见过该地址才能建立关联。
  • 可能产生大量带宽消耗。

差值关联 通过操作地址差而非完整地址,显著降低了存储需求,并有可能预取从未见过地址的数据,从而减少强制性缺失。


面向指针的预取器

对于指针密集型应用(如链表遍历),一种创新的预取思路是内容导向预取

核心思想:当缓存块被载入时,用硬件扫描块内的数据,识别出哪些值可能是指针(即内存地址),然后预取这些指针指向的数据。

如何动态识别指针?一个巧妙的方法是:

  • 检查缓存块内所有指针大小(如8字节)的对齐值。
  • 比较这些值的虚拟地址高位(例如前12位)是否与当前缓存块地址的高位匹配。
  • 如果匹配,则该值很可能是一个指向同一虚拟地址空间区域的指针,进而对其发起预取。

优势

  • 无需记录历史信息,硬件开销相对较小。
  • 可以预取从未访问过的指针数据,消除强制性缺失。
  • 实现简单直接。

劣势:可能会盲目预取块内所有类似指针的值,不够精确,可能造成带宽浪费和缓存污染。

后续研究通过结合编译器或性能剖析信息,为硬件提供提示,指明哪些指针更可能被访问,从而提高了预取的精确性。


混合预取器与学习型预取器

单一预取器难以覆盖所有内存访问模式。因此,现代处理器通常采用混合预取器,集成多种预取策略(如步长预取、流预取、关联预取),类似于混合分支预测器。

挑战

  • 需要决策不同预取器之间的优先级。
  • 多个预取器可能相互干扰,竞争缓存空间和内存带宽。
  • 需要更复杂的机制来管理和节制总体预取行为。

更前沿的方向是引入机器学习来指导预取。例如,使用强化学习构建自优化预取器。

  • 智能体:预取器本身。
  • 状态:当前内存请求的特征(如程序计数器、历史地址差等)。
  • 动作:选择预取的偏移量(例如,从当前地址A预取 A+offset)。
  • 奖励:根据预取是否有用、是否及时以及系统带宽使用情况等因素给出正/负反馈。

预取器通过不断尝试,学习在特定状态下应选择哪个偏移量能获得最大长期奖励,从而自适应不同程序的行为。这类方法在复杂和非规则工作负载上展现出潜力。


基于执行的预取

前述预取器主要基于观察到的访问模式进行推断。而基于执行的预取采取了一种更直接的方法:通过提前执行一段程序代码来生成精确的预取请求。

基本理念:创建一个推测执行线程,其唯一目的是预取数据。

  • 这个线程可以是主线程在遇到长延迟缓存缺失时,在空闲硬件上下文中发起的。
  • 它“提前”执行程序中的代码,特别是那些会导致未来缓存缺失的指令链。
  • 当主线程真正执行到那些代码时,数据已经被预取到缓存中。

前瞻执行

一种著名的基于执行的预取技术是 “前瞻执行”

  1. 当乱序执行窗口中最旧的指令是一个长延迟缓存缺失时,处理器对架构状态进行检查点保存。
  2. 进入“前瞻模式”。在此模式下,处理器继续推测性地执行后续指令,但目的不是提交结果,而是生成预取
  3. 对于缺失数据的指令,将其标记为无效,使其不阻塞执行流水线,从而为后续独立指令让出空间,快速触及后续的缓存缺失。
  4. 当最初引发前瞻的缓存缺失返回时,处理器回滚到之前保存的检查点,恢复正常执行。此时,后续缺失的数据很可能已在缓存中。

优势

  • 高精度:沿实际程序路径执行,预取准确率极高。
  • 覆盖复杂模式:能处理指针追逐等不规则访问。
  • 硬件利用:复用现有的乱序执行硬件,无需完全独立的硬件上下文。

挑战

  • 执行额外指令:可能执行许多最终无用的指令,消耗能量。
  • 受分支预测限制:如果前瞻线程走在错误路径上,预取可能无效。
  • 前瞻距离有限:受限于最初缓存缺失的解决时间。

前瞻执行及其变体已被证明能有效提升性能,其思想在工业界和学术界持续产生影响,后续研究致力于提高其效率、扩展其前瞻距离(如在内存控制器中实现连续前瞻),并探索其在向量化等场景下的应用。


总结

本节课我们一起深入探讨了硬件预取技术的多个高级主题:

  1. 我们学习了如何通过基于局部性的预取来捕捉非步长的空间访问模式。
  2. 我们分析了关联预取器如何通过记录和预测地址或差值序列来处理复杂、重复的访问模式。
  3. 我们探讨了专为指针数据结构设计的内容导向预取器及其优化思路。
  4. 我们了解到现代系统采用混合预取器以覆盖多样化的访问模式,并面临新的管理挑战。
  5. 我们介绍了利用强化学习的自适应预取器这一前沿方向。
  6. 最后,我们重点学习了基于执行的预取,特别是前瞻执行技术,它通过推测性地提前执行程序来生成高精度的预取,是处理不规则内存访问的强大工具。

预取技术仍然是计算机架构中一个活跃且基础的研究领域,在提升系统性能、降低内存延迟方面至关重要。随着工作负载和体系结构的不断演进,新的预取思想和技术将持续涌现。

6:数据流、超标量、VLIW

概述

在本节课中,我们将学习数据流执行模型、超标量处理器以及超长指令字架构。我们将从数据流的基本概念开始,探讨其在乱序执行引擎中的应用,然后分析超标量和VLIW架构的设计哲学与实现挑战。

数据流执行模型

上一节我们介绍了乱序执行的核心机制,本节中我们来看看其背后的理论基础——数据流执行模型。

在数据流模型中,数据的可用性决定了指令的执行顺序,这与控制流驱动的冯·诺依曼模型截然不同。数据流节点(可视为指令)在其所有输入数据就绪时“触发”,即被获取并执行。

数据流程序被表示为节点和连接弧组成的数据流图。节点代表操作,弧代表数据依赖。一个节点只有在所有输入弧上都有数据“令牌”时才能触发,并在执行后在其输出弧上产生新的令牌。

以下是数据流节点的几种基本类型:

  • 运算节点:如乘法、加法。当两个输入数据就绪时触发,执行运算并输出结果。
  • 条件节点:根据一个布尔输入,决定将数据令牌传递到“真”路径还是“假”路径。
  • 关系节点:如大于比较。比较两个输入数据,输出一个布尔令牌。
  • 屏障同步节点:具有多个输入,仅当所有输入令牌都就绪时,才将所有输入复制到输出。用于同步并行任务。

数据流模型在指令集架构层面并未获得广泛应用,但在微架构层面,通过乱序执行引擎实现受限的数据流却极为成功。乱序执行引擎动态地将顺序程序转换为数据流图执行,同时保持了顺序执行语义,对程序员透明。

乱序执行中的内存操作处理

上一节我们探讨了数据流模型,本节中我们来看看在乱序执行中一个最复杂的挑战:加载/存储操作的处理。

处理内存操作比处理寄存器操作复杂得多,原因如下:

  1. 内存依赖是动态的:寄存器依赖在解码时即可确定,而内存地址(通常是基址+偏移)需在指令执行时计算。
  2. 内存状态巨大且可共享:寄存器状态小且私有,内存状态庞大且可能被多线程/多处理器共享。
  3. 内存歧义问题:一个较新的加载指令可能先于一个较旧的存储指令计算出地址。此时,加载指令无法确定自己是否依赖于那个地址未知的存储。

这引出了内存歧义问题:当加载指令地址就绪时,如何处理那些地址尚未就绪的、更旧的存储指令?

以下是几种处理策略:

  • 保守策略:等待所有更旧的存储指令都提交(或至少地址计算完成)后再执行加载。性能损失大。
  • 激进策略:假设加载独立于所有地址未知的存储,立即执行加载。若预测错误(即存在依赖),则需刷新流水线并重新执行。
  • 预测策略:使用预测器判断加载是否依赖于某个未知地址的存储,并据此决定等待或执行。

现代处理器通常采用预测策略,并辅以复杂的硬件结构进行验证和恢复。

为了实现依赖检测和数据转发,处理器需要加载队列存储队列

  • 加载指令:计算地址后,需搜索存储队列,检查是否有更旧的存储写入相同地址,并从最新的匹配存储中转发数据。
  • 存储指令:计算地址后,需搜索加载队列,检查是否有更年轻的加载错误地提前读取了该地址,若发现则触发恢复。

这种搜索逻辑非常复杂,涉及基于地址和操作大小的范围匹配年龄比较(确保只从更旧的存储转发),以及可能从多个存储和缓存中合并数据

细粒度多线程

上一节我们讨论了乱序执行的复杂性,本节中我们来看一种通过简化硬件来容忍延迟的技术:细粒度多线程。

细粒度多线程的核心思想是:每个时钟周期从不同的线程获取指令,并确保同一线程的任何两条指令不会同时在流水线中。这样,硬件就无需处理同一线程指令间的控制和数据依赖。

其工作方式如下:

  • 硬件需要为每个线程复制线程上下文(程序计数器和寄存器文件)。
  • 流水线的每个阶段都包含来自不同独立线程的指令。
  • 通过在不同线程的指令间切换,用其他线程的有用工作来覆盖单个线程的延迟(如缓存缺失、分支误预测)。

优势在于:

  • 无需复杂的乱序执行硬件即可容忍延迟。
  • 提高了流水线利用率和线程级吞吐量。

劣势在于:

  • 单线程性能下降(同一线程的指令间隔变长)。
  • 需要额外的硬件来存储多个线程上下文。
  • 当活跃线程不足时,流水线会出现空闲。
  • 线程间仍会争用共享的缓存和内存资源。

这种模型在图形处理器中得到了广泛应用,因为图形负载天然具有大量可并行执行的线程。

超标量执行

上一节我们介绍了通过多线程提升吞吐量的方法,本节中我们回到提升单线程指令级并行性的另一种途径:超标量执行。

超标量处理器每个时钟周期可以获取、解码、执行并提交多条指令。硬件负责检测这些并发指令间的依赖关系,并调度它们到相应的功能单元。

这与VLIW形成对比:超标量的依赖检查和调度由硬件动态完成;而VLIW则由编译器静态完成

实现超标量需要:

  • 多份数据通路副本:多个取指/解码单元、多个功能单元、多端口寄存器文件和缓存。
  • 依赖检查逻辑:在解码或重命名阶段,快速判断并发指令间的依赖,决定哪些指令可以同时发射。

超标量与乱序执行是正交概念:

  • 顺序超标量:硬件多路发射,但指令按程序顺序执行。
  • 乱序超标量:硬件多路发射,且指令可乱序执行。这是现代高性能处理器的典型设计。

优势是更高的指令吞吐量和更低的平均CPI。劣势是硬件复杂度显著增加,尤其是依赖检查、重命名和调度逻辑随着发射宽度的增加而急剧复杂化。

超长指令字

上一节我们看到了硬件复杂度高的超标量设计,本节中我们探讨一种截然不同的哲学:超长指令字架构。

VLIW架构的核心思想是:硬件应保持简单,智能应置于软件中。编译器将多条独立的指令打包成一个很长的指令束。硬件每个周期获取并执行这样一个指令束,且不进行任何动态依赖检查,指令束中的操作被锁定步调执行。

其特点是:

  • 编译器负责调度:编译器寻找指令级并行性,并将无依赖的指令打包。
  • 硬件简单:无需复杂的依赖检查、重命名或动态调度逻辑。指令束中的操作直接对应到特定的功能单元。
  • 锁定步调执行:如果指令束中任何一条指令停顿(如缓存缺失),整个指令束(包括其他独立操作)都必须停顿。

VLIW的优势在于硬件简单、功耗低、设计容易。但其劣势也很明显:

  • 编译器负担重:需要编译器发现足够的并行指令来填充宽指令束,否则会出现空操作,降低代码密度和性能。
  • 难以容忍可变延迟:对于像加载这样延迟不确定的操作,锁定步调执行会导致严重性能损失。
  • 二进制代码兼容性差:静态调度与微架构细节(如功能单元数量、延迟)紧密耦合,为新的微架构重新编译代码可能带来性能问题。

尽管纯VLIW在通用计算领域未成主流,但其编译器技术(如静态调度、循环展开、软件流水线)对现代优化编译器产生了深远影响,并且在数字信号处理器等嵌入式领域取得了成功。

总结

本节课我们一起学习了多种提升处理器并行性能的技术路线。我们从数据流模型出发,理解了乱序执行是其受限的、对程序员透明的实现。我们深入探讨了乱序执行中棘手的内存操作处理问题。接着,我们了解了通过细粒度多线程在简化硬件的同时容忍延迟的方法。然后,我们对比了超标量执行(硬件动态调度)和超长指令字(软件静态调度)两种提升指令级并行的不同哲学,分析了它们各自的优势、挑战及应用场景。这些架构思想共同塑造了现代处理器的设计格局。

7:SIMD架构

概述

在本节课中,我们将要学习单指令多数据(SIMD)处理范式。这种范式对计算机架构产生了深远影响,如今几乎所有计算机架构都包含某种形式的SIMD支持。我们将探讨SIMD如何通过单一指令操作多个数据元素来提升性能和能效,并深入了解其两种主要实现方式:向量处理器和阵列处理器。

脉动阵列回顾

上一节我们介绍了脉动阵列和超长指令字(VLIW)。脉动阵列是用于加速特定计算(如矩阵乘法)的专用架构。例如,谷歌的TPU(张量处理单元)本质上就是一个脉动阵列,它通过专门的硬件高效地执行大规模的乘累加运算,以加速神经网络中的卷积和矩阵乘法操作。

SIMD架构简介

现在,我们来看看SIMD架构。SIMD代表“单指令,多数据”。其核心思想是:一条指令可以同时对多个数据元素执行相同的操作。这非常适合于数据并行性高的应用,例如图像处理、科学计算和机器学习。

计算机分类法(弗林分类法)

在深入SIMD之前,了解计算机的分类很有帮助。弗林分类法根据指令流和数据流的数量将计算机分为四类:

  • SISD(单指令单数据):传统的标量处理器,如我们之前讨论的MIPS处理器。每条指令处理一个数据元素。
  • SIMD(单指令多数据):本节课的重点。一条指令同时处理多个数据元素。
  • MISD(多指令单数据):多个指令处理同一个数据元素。这类似于脉动阵列或流处理器。
  • MIMD(多指令多数据):多个指令流处理多个数据流。例如多处理器或多线程系统。

现代处理器(如苹果M1芯片)通常结合了这四种范式。

SIMD的优势

SIMD通过利用数据并行性来提升效率。其核心优势在于分摊控制开销。考虑一个将两个包含10亿个元素的向量相加的例子:

  • 标量处理器中,需要获取、解码并执行10亿条相同的ADD指令。
  • SIMD处理器中,只需获取和解码一条向量ADD指令,然后用它来控制10亿个数据元素的加法操作。

因此,当向量长度很大时,SIMD在性能和能效方面具有巨大优势。它减少了指令获取和解码的需求,并将能量更多地用于实际计算而非控制逻辑。

向量处理器 vs. 阵列处理器

SIMD主要有两种实现方式,区别在于并行性是在时间还是空间上展开。

向量处理器(时间并行)

向量处理器使用少量功能单元,但通过深度流水线在时间上处理向量。

  • 工作原理:一条向量指令控制一个功能单元。该单元在连续的时钟周期内,依次对向量中的每个元素执行相同操作。
  • 示例:一个向量加法指令控制一个加法器流水线。第一个周期处理元素0,第二个周期处理元素1,依此类推。
  • 优点:硬件效率高,不需要大量复制功能单元。
  • 缺点:处理整个向量所需的总时间较长。

关键公式:完成一个长度为 N 的向量操作所需时钟周期数 ≈ 功能单元延迟 + (N - 1)

阵列处理器(空间并行)

阵列处理器使用大量相同的功能单元,在空间上同时处理向量。

  • 工作原理:一条指令同时广播到多个处理单元(PE)。每个PE在同一时钟周期内处理向量中不同的数据元素。
  • 示例:一个有4个PE的系统,一条向量加法指令可以同时让4个PE分别计算元素0、1、2、3的和。
  • 优点:速度极快,理想情况下可以在一个周期内完成整个短向量的操作。
  • 缺点:硬件成本高,需要复制大量功能单元。

现代处理器(如GPU)通常是向量和阵列处理的结合体,在时间和空间两个维度上同时利用并行性。

SIMD编程核心概念

为了支持SIMD操作,需要对传统的标量ISA进行扩展。

1. 向量寄存器

与传统标量寄存器存储单个值不同,向量寄存器存储的是一个向量(即一组值)。

  • 表示V0, V1, V2...
  • 结构:每个向量寄存器包含 N 个元素,N最大向量长度。例如,V0[0], V0[1], ..., V0[N-1]

2. 向量长度寄存器 (VL)

用于设置当前操作的向量实际长度,因为程序处理的向量长度可能小于最大向量长度。

3. 向量步长寄存器 (VS)

向量元素在内存中可能不是连续存储的。步长定义了向量中相邻元素在内存地址上的间隔。

  • 步长为1:元素在内存中连续存储。这是最理想的情况,有利于内存访问。
  • 步长不为1:例如在矩阵运算中,访问一列数据时,元素在内存中相隔多行。步长需要相应调整。

示例:矩阵乘法中的步长
假设矩阵按行优先存储。计算行向量与列向量的点积时:

  • 加载行向量:元素连续,步长 = 1
  • 加载列向量:元素间隔一行的大小,步长 = 矩阵列数

4. 向量掩码寄存器 (VM)

用于实现条件执行。掩码是一个位向量,每一位对应向量中的一个元素。

  • 1:对该元素执行操作。
  • 0:不对该元素执行操作(或结果不写回)。

示例代码:向量化条件语句 if (A[i] != 0) C[i] = A[i] * B[i]

VLVL, #50        ; 设置向量长度为50
VLD V0, A        ; 加载向量A到V0
VLD V1, B        ; 加载向量B到V1
VCMPNE V0, #0    ; 比较 V0 != 0,结果存入向量掩码VM
VMUL V2, V0, V1  ; 条件乘法:仅在VM为1的对应位置计算 V0*V1,结果存V2
VST V2, C        ; 条件存储:将V2存回C

通过掩码,我们消除了显式的分支指令,将控制依赖转化为数据依赖,提高了效率。

内存系统:存储体交错访问

SIMD处理器需要高速的内存带宽来喂饱其计算单元。关键挑战是:如何实现每个周期提供(或接收)一个数据元素的稳定吞吐量,尤其是在内存访问延迟(例如11个周期)较高的情况下?

解决方案是:多存储体 + 交错存储

工作原理

  1. 存储体划分:将单片内存划分为多个独立的存储体(例如16个)。每个存储体有自己的地址和数据寄存器,但共享地址总线和数据总线(以控制芯片引脚成本)。
  2. 交错存储:将向量元素依次分布到不同的存储体中。例如,元素0存于存储体0,元素1存于存储体1,元素2存于存储体2,...,元素15存于存储体15,元素16又回到存储体0,依此类推。
  3. 流水线访问:当以步长1访问一个长向量时,可以在第一个周期向存储体0发起访问,第二个周期向存储体1发起访问……尽管每个存储体的访问仍需11个周期,但由于访问是交错进行的,从整个内存系统来看,从第11个周期开始,每个周期都能完成一个元素的访问,实现了稳定的流水线吞吐。

关键条件

要维持每个周期一个元素的吞吐量,需满足:

  • 存储体数量 ≥ 存储体访问延迟
  • 访问步长与存储体数量互质(以避免访问冲突集中到少数存储体)。当步长为1时,自然满足此条件。

如果数据布局不当(例如,所有要访问的元素都映射到同一个存储体),性能将急剧下降,退化为标量内存访问。

SIMD性能示例分析

让我们通过一个具体的例子来感受SIMD的性能提升:计算两个50维向量的元素平均值 C[i] = (A[i] + B[i]) / 2

标量实现(简化版)

for (int i = 0; i < 50; i++) {
    C[i] = (A[i] + B[i]) >> 1; // 假设用右移1位代替除以2
}

在标量处理器上,这需要执行约300条动态指令(包括循环控制),并产生大量指令获取和解码开销。

向量化实现

SETVL #50       ; 设置向量长度=50
SETVS #1        ; 设置步长=1
VLD V0, A       ; 向量加载A
VLD V1, B       ; 向量加载B
VADD V2, V0, V1 ; 向量加法 V2 = V0 + V1
VSRA V3, V2, #1 ; 向量算术右移1位 V3 = V2 >> 1
VST V3, C       ; 向量存储到C
  • 动态指令数:从~300条减少到7条(2条设置指令+5条计算指令)。
  • 消除分支:完全消除了循环分支。
  • 性能提升:在配备了多端口存储体内存系统和向量链式转发(类似数据前推)的向量处理器上,执行时间可比优化后的标量处理器提升19倍以上。
  • 能效提升:大幅降低了控制开销(取指、译码、分支预测)的能耗。

SIMD的挑战与扩展

尽管SIMD强大,但也面临一些挑战:

1. 向量长度处理

当数据元素数量超过硬件最大向量长度时,需要使用条带挖掘技术:将长循环分解为多个步长为最大向量长度的内部循环,以及一个处理剩余元素的尾部循环。

2. 不规则数据访问:分散-收集操作

对于稀疏数据或间接访问,需要使用分散收集操作。

  • 收集:根据一个索引向量,从内存的非连续地址读取数据,并打包到一个连续向量寄存器中。
  • 分散:将一个向量寄存器中的数据,根据索引向量,写回到内存的非连续地址中。
    这些操作对于处理稀疏矩阵等数据结构至关重要。

3. 可向量化性

SIMD的性能提升依赖于程序中存在规则的数据级并行。编译器或程序员需要进行循环依赖分析,以确保循环的每次迭代是独立的,从而安全地进行向量化。不规则的控制流或数据依赖会阻碍向量化。

4. 阿姆达尔定律的限制

即使并行部分可以无限加速,系统的整体加速比也会受到串行部分比例的限制。
公式加速比 ≤ 1 / (S + P/N),其中 S是串行比例,P是并行比例(S+P=1),N是处理器数量。
因此,为了充分利用SIMD,必须努力最大化程序中的可并行化部分。

现代架构中的SIMD

  • SIMD指令集扩展:现代CPU(如x86的SSE/AVX,ARM的NEON/SVE)都集成了SIMD指令,作为标量指令集的扩展。它们使用宽度为128位、256位或512位的向量寄存器。
  • GPU:图形处理器是SIMD思想的集大成者。它们结合了向量处理(线程束内的SIMT执行)和阵列处理(拥有成百上千个核心),形成了大规模并行架构。GPU的编程模型(如CUDA/OpenCL)也围绕数据并行思想构建。
  • AI加速器:许多专用AI芯片(如TPU)的核心也采用了类似SIMD或脉动阵列的架构来高效处理张量运算。

总结

本节课我们一起学习了SIMD架构。我们从弗林分类法入手,理解了SIMD在计算机范式中的地位。然后深入探讨了其两种实现方式——在时间上展开的向量处理器和在空间上展开的阵列处理器,并指出现代处理器通常是两者的结合。

我们学习了SIMD编程的核心概念:向量寄存器、向量长度、步长以及用于条件执行的向量掩码。我们分析了SIMD通过分摊控制开销带来的性能和能效优势,并通过具体示例看到了显著的提升。

同时,我们也探讨了支持SIMD所需的高带宽内存系统,其关键是存储体交错访问技术。最后,我们审视了SIMD面临的挑战,如不规则数据访问、向量化条件以及阿姆达尔定律的根本性限制。

SIMD是挖掘数据并行性、提升计算能效的关键技术,从CPU的指令集扩展,到GPU的大规模并行架构,再到各种AI加速器,其思想无处不在,是现代计算不可或缺的基石。

8:GPU架构 🚀

概述

在本节课中,我们将学习图形处理单元(GPU)的架构。我们将基于上周关于SIMD执行、向量处理器和阵列处理器的内容,深入探讨GPU如何结合这些概念,并通过编程模型的创新使其更易于使用。GPU是现代高性能计算和图形处理的核心,理解其工作原理对于计算机架构的学习至关重要。


回顾:SIMD与数据级并行

上一节我们介绍了SIMD(单指令多数据)架构,它是利用数据级并行的有效方式。向量处理器和阵列处理器是实现SIMD的两种主要硬件形式。

  • 阵列处理器:拥有多个强大的处理单元,每个单元可以独立执行操作,实现空间上的并行。
  • 向量处理器:通过流水线化的功能单元,对向量数据的不同元素依次执行相同操作,实现时间上的并行。

这两种架构都依赖于高带宽的内存和寄存器访问,以保持流水线充满数据。它们的性能优势在于能够分摊指令获取和解码的开销,但在处理不规则数据并行时效率会降低。


从SIMD到SIMT:编程模型的演进

本节中,我们来看看GPU如何通过改变编程模型来简化并行编程。关键在于区分编程模型执行模型

  • 编程模型:程序员如何表达代码,例如顺序执行、数据并行(SIMD)或多线程(MIMD)。
  • 执行模型:硬件如何实际执行代码,例如乱序执行、向量处理或多线程处理。

GPU采用了一种称为SPMD(单程序多数据)的编程模型。程序员编写一个看似标量的程序(一个“内核”),这个程序将被成千上万个线程同时执行,每个线程处理不同的数据片段。硬件则动态地将这些线程分组,以SIMD(在GPU术语中常称为SIMT,单指令多线程)的方式执行。

核心优势

  1. 线程独立性:每个线程可以独立执行,甚至执行不同的控制流路径,这比传统的SIMD编程更灵活。
  2. 灵活的线程分组:硬件可以动态地将执行相同指令的线程分组,形成线程束(Warp,NVIDIA术语)波前(Wavefront,AMD术语),这是GPU执行SIMD操作的基本单位。

GPU架构核心:线程束与细粒度多线程

以下是GPU架构如何利用线程束和细粒度多线程来隐藏延迟并提高利用率。

  • 线程束(Warp):一组(通常是32个)执行相同指令的线程。它是GPU调度和执行的基本单位。
  • 细粒度多线程:当一个线程束因内存访问等长延迟操作而停滞时,GPU调度器会立即切换到另一个就绪的线程束,从而保持计算单元的忙碌,有效隐藏延迟。

为了实现快速的上下文切换,GPU拥有巨大的寄存器文件,用于存储所有活跃线程的上下文。例如,一个GPU核心可能拥有256KB的寄存器文件。

执行流程示例
假设一个线程束有32个线程,而SIMD执行单元宽度为8个通道(即8个处理单元)。那么,完成这个线程束的一条指令需要4个周期(32/8=4)。通过在不同的周期交错执行不同线程束的指令,GPU可以同时利用空间(多个处理单元)和时间(流水线)上的并行性。


GPU编程模型:从内核到线程束

程序员使用如CUDA或OpenCL等框架进行GPU编程。以下是编程层次结构:

  1. 网格(Grid):一个内核调用的所有线程块。
  2. 线程块(Block):一组线程,它们可以共享一块快速的内存(共享内存)。线程块被分配到GPU的流多处理器(SM)上执行。
  3. 线程(Thread):最基本的执行单元。程序员为单个线程编写内核代码。

硬件负责将线程块内的线程分组为线程束。关键约束是:一个线程束中的所有线程必须来自同一个线程块,以确保内存访问和同步的正确性。

地址计算:每个线程通过其唯一的线程ID来访问不同的数据元素,例如访问数组 A[thread_id]。这使得内存地址计算非常简单高效。


处理控制流分歧

在传统的SIMD中,处理分支(如if-else语句)是复杂的,因为所有数据通道必须执行相同的指令路径。GPU的SIMT模型通过硬件处理分支分歧(Branch Divergence) 使其对程序员透明。

工作原理
当一个线程束遇到分支指令时,线程可能根据各自的数据选择不同的路径。GPU硬件会:

  1. 先执行选择路径A的所有线程,暂时禁用选择路径B的线程。
  2. 然后执行选择路径B的所有线程,暂时禁用选择路径A的线程。
  3. 在分支重新汇合点,所有线程恢复同步执行。

虽然这简化了编程,但会导致线程束利用率下降(部分线程被禁用)。因此,编写分支较少的GPU内核对性能至关重要。


高级调度与优化技术

为了进一步提升性能,研究人员和工业界提出了多种优化技术。

  • 动态线程束形成/压缩:将来自不同线程束但执行相同指令且线程ID不冲突的活跃线程合并成一个新的、利用率更高的线程束。
  • 两级轮询调度:为了避免大量线程束同时到达长延迟操作(如内存访问)而导致调度器“饥饿”,将线程束分组调度,确保总有可用的计算密集型线程束来隐藏延迟。

这些技术旨在提高SIMD单元的利用率和整体系统吞吐量。


GPU架构实例与发展

GPU架构在不断演进,核心数量、内存带宽和计算能力持续增长。我们以NVIDIA的几代架构为例:

  • Fermi(2009):引入了L2缓存,支持更高效的细粒度多线程。
  • Volta(2017):引入了张量核心(Tensor Core),专门用于加速矩阵乘加运算,极大提升了深度学习性能。
  • Ampere(2020):增强了对稀疏矩阵运算的支持。
  • Hopper(2022):支持更低精度(如FP8)的数据类型,以适应AI训练和推理的需求。
  • Blackwell(2023):通过芯片间高速互连(如NVLink),将多个GPU连接成强大的计算集群,以应对大模型对内存容量和算力的双重需求。

现代GPU不仅是图形处理器,更是通用的高性能并行计算平台。


总结

本节课中我们一起学习了GPU架构的核心原理。我们从SIMD和向量处理器的回顾开始,探讨了GPU如何通过SIMT编程模型将数据并行性抽象为多线程,从而简化了并行编程。我们深入了解了线程束、细粒度多线程、分支分歧处理等关键概念,并看到了GPU如何通过巨大的寄存器文件、高带宽内存层次结构以及先进的调度策略来实现极高的吞吐量。最后,我们回顾了GPU架构的演进历程,看到了它如何通过集成专用加速器(如张量核心)和增强互连技术来持续满足日益增长的计算需求。理解GPU架构对于设计高效并行算法和系统至关重要。

9:脉动阵列与仿真

概述

在本节课中,我们将要学习一种特殊的计算范式——脉动阵列。这是一种为特定计算任务设计的高效、规则化硬件加速结构。我们还将探讨计算机架构研究中一个至关重要的工具:仿真。通过仿真,我们可以在不实际构建硬件的情况下,评估新架构想法的性能与效率。


脉动阵列简介

上一节我们介绍了GPU和SIMD等数据并行范式,本节中我们来看看另一种截然不同的执行模型:脉动计算。

脉动阵列是一种更接近专用集成电路(ASIC)的加速器。其核心思想是用一个规则的处理单元(PE)阵列,取代单个处理单元,并精心编排数据在这些PE间的流动。数据从内存流入,流经多个PE,在每个PE处进行一部分计算,最终结果才被写回内存。这种方式最大限度地提高了单次内存访问所执行的计算量,从而平衡了计算与内存带宽。

核心动机与原理

脉动阵列的原始动机是设计简单、规则的加速器,以实现高并发和高性能,同时缓解内存瓶颈问题。其设计原则是:最大化对单次从内存获取的数据元素所执行的计算量,或者说,最大化程序的算术强度。

一个形象的比喻是:内存如同心脏,数据如同血液,处理单元如同细胞。数据从内存(心脏)有节奏地“脉动”流过各个处理单元(细胞),完成计算。

与流水线的区别

脉动阵列与指令流水线有本质不同:

  • 流水线:将单条指令的执行拆分为多个阶段,以提高指令吞吐率。
  • 脉动阵列:将数据流的计算拆分为多个阶段,分布在不同的PE上。每个PE可以是一个功能强大的计算单元,阵列结构可以是多维和非线性的,PE间可以相互通信。

脉动阵列实例:卷积与矩阵乘法

卷积计算

卷积是图像处理、信号处理和卷积神经网络中的核心操作。其基本形式是向量乘加运算。

假设我们计算输出向量 Y,其中每个元素 Yi = Σ (Wj * Xi+j-1)。我们可以设计一个一维脉动阵列来实现它。

以下是处理单元(PE)的核心操作:

// 每个PE存储一个固定的权重 W
// 输入:x_in, y_in
// 输出:x_out, y_out
y_out = y_in + W * x_in; // 执行乘累加
x_out = x_in;            // 将数据传递给下一个PE

通过精心安排输入数据 Xi 和部分和 Yi 进入阵列的时机,当数据流过整个PE阵列后,就能在输出端得到完整的卷积结果。

矩阵乘法

矩阵乘法是机器学习中更常见的操作。我们可以设计一个二维脉动阵列来计算 C = A * B

每个PE的设计如下:

// 输入:来自上方(north)的数据 `a_in`,来自左方(west)的数据 `b_in`
// 输出:向下传递 `a_out = a_in`,向右传递 `b_out = b_in`
// 内部累加器:r(初始为0)
r = r + a_in * b_in; // 执行乘积累加

通过将矩阵A的行元素从顶部输入、矩阵B的列元素从左部输入,并错开它们的输入时序,当所有数据流过阵列后,每个PE中累加器 r 的值就是结果矩阵C的一个对应元素。


脉动阵列的优缺点

优点

  • 高能效:无指令开销,计算密集。
  • 高并发与高性能:规则设计易于实现大规模并行。
  • 内存带宽友好:单数据项被多次复用,算术强度高。

缺点

  • 专用性强:仅适用于具有规则、可脉动化数据流模式的计算。
  • 编程与映射复杂:需要精心设计数据流和时序。
  • 缺乏通用性:不适合不规则或控制密集型任务。

现代应用:谷歌TPU

脉动阵列并非过时的概念。谷歌的张量处理单元(TPU)的核心就是一个大型的脉动阵列,用于加速神经网络中的矩阵乘法。现代TPU已经演变为混合架构:

  1. 脉动阵列:用于密集矩阵计算。
  2. 向量单元(SIMD):用于激活函数等向量操作。
  3. VLIW标量核心:用于控制流和标量操作。
    这印证了现代加速器往往是多种计算范式的结合。

架构仿真概述

在提出和评估像脉动阵列这样的新架构想法时,我们无法总是立即构建硬件。这时,仿真是架构师的关键工具。

仿真的目的与挑战

仿真的根本目的是评估尚不存在的系统。主要挑战在于:

  • 工作负载依赖性强:不同程序性能表现差异大。
  • 设计空间巨大:需要探索大量参数组合。
  • 准确性、速度与灵活性的权衡:通常难以三者兼得。

仿真方法层次

架构评估有多种方法,形成一个从抽象到具体的谱系:

  1. 理论证明/分析建模:适用于规律性强的计算或特定属性(如安全)分析。
  2. 仿真:在不同抽象级别上进行,是设计空间探索的核心。
  3. 原型实现(如FPGA):更接近真实硬件,用于验证和深入研究。
  4. 实际流片:最准确,也最昂贵、最耗时。

对于前瞻性研究,分析建模高层仿真是最常用的手段。

高层仿真的权衡

高层仿真通过提升抽象级别来换取速度和灵活性。

  • 优点:快速探索设计空间,把握相对趋势。
  • 缺点:可能因模型不准确而导致错误决策。
    正确的做法是采用渐进精化的策略:从高层抽象模型开始,逐步增加细节,最终用实际设计或原型进行验证和校准。

仿真中的关键问题

设计或使用仿真器时,需要考虑:

  • 模拟什么:全系统还是组件?功能还是时序?
  • 如何模拟:执行驱动还是踪迹驱动?周期精确还是事件驱动?
  • 输入/输出:输入是程序二进制、踪迹还是检查点?输出性能、能耗还是统计信息?
  • 工作负载采样:如何从冗长的实际工作负载中选取有代表性的片段进行模拟?
  • 验证:如何确保仿真结果的准确性?对于未来系统,这可能尤其困难。

实例:内存系统仿真器 Ramulator

Ramulator 是一个专注于内存子系统(如DRAM)的快速、可扩展仿真器。它允许研究人员在统一的框架下评估DDR3、DDR4、LPDDR、HBM等不同内存标准的表现,而无需拥有所有硬件的物理实体。

通过此类仿真器,我们可以快速回答诸如“将DDR3升级为HBM能带来多少性能提升?”之类的问题,从而指导架构设计决策。


总结

本节课中我们一起学习了:

  1. 脉动阵列:一种通过规则PE阵列和数据流编排来实现高效计算的专业化执行模型,广泛应用于现代AI加速器(如TPU)中。
  2. 架构仿真:计算机架构研究的基础工具,它使我们在硬件实现前就能评估想法的优劣。我们理解了仿真在准确性、速度与灵活性之间的权衡,以及高层仿真和渐进精化策略的重要性。

掌握脉动阵列的原理有助于理解现代异构计算的核心,而理解仿真方法则是进行任何严肃架构创新的必备技能。

9c:解耦访存执行 (DAE) 🧠

在本节课中,我们将学习一种名为“解耦访存执行”的处理器设计范式。这种设计旨在通过分离内存访问和计算执行来提升性能,同时保持硬件设计的相对简单性。

概述与动机 🎯

上一节我们讨论了VLIW和脉动阵列。本节中,我们来看看解耦访存执行。其基本动机是,像Tomasulo算法这样的乱序执行机制在1980年代(奔腾Pro之前)被认为过于复杂,难以实现。人们希望系统不要如此复杂。VLIW也可以被置于类似的定位,因为它与乱序执行有着截然不同的设计哲学。

解耦访存执行拥有非常相似的设计哲学,但它没有在硬件上走到极致。我们将看到,它从根本上改变了硬件设计。

核心思想 💡

基本思想是将操作访问(即内存访问)与执行(即计算)解耦。我们有两个独立的指令流,它们通过指令集架构可见的队列进行通信。

以下是其系统架构的直观描述:

  • 你有一个访存处理器和一个执行处理器
  • 访存处理器的任务仅仅是获取内存数据并供给执行处理器。
  • 执行处理器的任务是将所需的内存地址提供给访存处理器。
  • 它们通过队列进行通信。

这种设计的美妙之处在于,这是两种不同类型的任务。内存访问可能受限于内存带宽,而计算可能不受限。因此,你可以在等待内存的同时继续进行计算。反之亦然,有时你可能在等待长时间的计算,但可以继续进行内存访问。这样,无需实现完整的乱序执行,访存处理器和执行处理器之间也无需停顿。当内存操作进行时,你可以进行计算,反之亦然。这就是它的优势所在。

这个概念由Jim Smith在1982年的开创性论文中提出,其基本原理至今仍应用于计算系统中,尽管形式不完全相同。

架构细节与优势 ⚙️

首先,指令集架构需要改变,因为通信通过这些队列进行。这些是指令集架构可见的队列。因此,队列的长度决定了你能容忍的内存端和执行端的延迟量。

以下是队列的优势:

  • 这些队列可以具有很高的可扩展性。它们不像重排序缓冲站或加载存储队列的标签匹配逻辑那样难以扩展。这里的队列是FIFO队列,易于扩展。
  • 还有一个分支队列,用于保持两个处理器同步。

本质上,基本思想是将一个单一的指令流(例如著名的Livermore循环)分解为两个指令流:访存流和执行流。它们执行相同的功能,但内存访问操作在访存处理器中执行,计算和分支操作在执行处理器中执行。当需要将内存访问结果传递给执行处理器时,就将其放入“访存到执行”队列。执行引擎从该队列取出数据,并可能将结果放入“执行到访存”队列。通信通过这些队列进行。

主要优势在于,执行流可以领先于访存流运行,反之亦然。如果访存处理器在等待内存,执行处理器可以执行有用的工作。如果访存处理器命中缓存,无需等待内存,它就可以为落后的执行处理器提供数据。通常内存访问耗时更长,因此执行处理器通常可以在访存处理器等待时执行独立的指令。

关键思想是队列减少了对大量寄存器的需求。这些不是寄存器,你不需要像乱序执行引擎那样拥有数千个物理寄存器。通信通过这些FIFO队列进行。因此,你获得了有限的乱序执行能力,但没有唤醒和选择逻辑的复杂性,也无需庞大的物理寄存器文件。

面临的挑战与解决方案 🔧

当然,任何设计都有缺点。编译器的支持在这里至关重要,就像它对VLIW和Tomasulo算法一样。你需要编译器支持来划分程序和管理队列。这决定了你能获得多大的解耦程度。人们为此开发了许多有趣的编译技术。

另一个缺点是分支指令需要在访存处理器和执行处理器之间进行同步。因为你实际上是将一个指令流分离成两个,那么分支会怎样?它们在执行处理器中执行,但你需要通知访存处理器,以确保访存处理器不会永远走在错误的分支路径上。

此外,多指令流也是一个挑战。你需要生成两个指令流或为两个处理器编程,这可能很繁琐。但后来的研究表明,这可以通过单一指令流动态地分流到多个处理器来实现。例如,Astronautics Z1处理器就是这样做的:它有一个单一的取指单元,然后动态地分离成访存指令流水线和执行指令流水线。每个流水线内部都是顺序执行的,这非常重要。乱序执行的能力来自于一个流水线与另一个流水线的异步操作,直到它需要来自另一个流水线的数据。

以下是Astronautics Z1处理器的关键组件:

  • 访存寄存器和执行寄存器。
  • 用于在两个流之间通信的队列。
  • 复制单元,用于将一个流水线的操作复制到另一个流水线。
  • 重启堆栈单元,用于处理分支。

编译器技术:循环展开 🔄

分支处理是一个大问题。因此,许多编译器使用循环展开来消除分支。循环展开通过在一个迭代中复制循环体多次来工作。你可能已经了解过循环展开,它是一种非常基础的编译器技术,旨在尽可能消除分支,因为分支在VLIW、解耦访存执行以及脉动阵列中都会带来问题。

循环展开的思想是在一个迭代内多次复制循环体。当然,现在你在一个原始迭代中执行了四次迭代,因此需要确保正确地递增索引值,这会带来一些问题。但这样做之后,你执行的循环控制指令(如分支、条件测试)就减少了,从而降低了循环维护开销。基本块变大了,这为代码优化和调度创造了机会。

问题通常出现在迭代次数不是展开因子的倍数时。例如,展开因子是4,但n不是4的倍数,那么你就需要额外的代码来处理这种情况,这会增加最终代码的大小。但循环展开是一种非常简单的基于编译器的技术,有助于我们今天讨论的所有处理器,解耦访存执行是其中重要的一环。它在Astronautics Z1处理器中大量使用以提升性能。

实际应用与影响 🚀

现在,让我们看看解耦访存执行在实际处理器中的影响,然后结束本节。虽然描述的形式不完全相同,但解耦访存执行的原则被应用在我所知的一些旧处理器中。例如,在奔腾4处理器内部,在指令被重命名并分配到重排序缓冲区和寄存器之后,它们会经过一个内存部分和一个执行部分。你可以看到这就是解耦:处理器有一个专门处理内存操作的部分和一个专门处理执行操作的部分,它们彼此解耦。即使在像这样的乱序超标量执行处理器中,也解耦了访存和执行,这样你可以在不同组件上获得专业化,并且这些不同组件之间也能实现一定程度的乱序执行。

从更简化的视角看奔腾4,你可以看到内存和整数执行的解耦。这个概念甚至可以扩展到不同类型的执行,如浮点执行和整数执行。

总结与展望 📚

本节课我们一起学习了三种主要的设计思想:VLIW、脉动阵列和解耦访存执行。你可以思考一下未来它们可能在哪些领域发挥作用。

一个很好的问题是:解耦访存执行是否特别适合与VLIW结合?答案是肯定的。原则上,你可以将VLIW指令束的一部分作为内存束,另一部分作为执行束,从而在VLIW引擎中解耦访存和执行。这样,你既能摆脱VLIW的锁步执行限制,又能获得部分乱序执行的益处。这正是我喜欢将这些主题放在一起讲解的原因,因为解耦访存执行的原则可以应用于VLIW引擎,从而在不使硬件过于复杂的情况下,显著提升性能潜力。

希望你对这三种主要思想有所了解。我们下周将讨论另一个引人入胜且极具影响力的主题:向量处理器和GPU。届时再见。

10:虚拟内存 🧠

概述

在本节课中,我们将要学习计算机系统中一个至关重要的概念——虚拟内存。虚拟内存为程序员提供了一个看似无限大的内存空间,而实际上,物理内存的容量要小得多。我们将探讨虚拟内存的工作原理、它带来的好处、以及为了实现它,硬件和操作系统需要如何协同工作。


为什么需要虚拟内存?🤔

上一节我们介绍了程序员视角下的理想内存。然而,现实中的物理内存是有限的。虚拟内存的出现,正是为了解决物理内存的局限性所带来的诸多问题。

在仅有物理内存的系统中(如早期计算机),程序员需要直接管理物理地址,这带来了以下困难:

  • 内存管理复杂:程序员需要自己处理数据在有限物理内存中的存放和移动。
  • 缺乏隔离性:多个程序可能使用相同的物理地址,导致数据被意外覆盖,无法保证进程间的安全隔离。
  • 难以共享:进程间共享代码或数据需要显式的、复杂的同步机制。
  • 可移植性差:程序需要为不同物理内存大小的系统进行专门适配。

虚拟内存的核心思想,就是为每个程序提供一个独立的、巨大的虚拟地址空间,并通过一个映射机制,将其动态地映射到有限的物理地址空间上。这样,程序员就无需关心物理内存的具体细节。


虚拟内存的基本概念 🧩

本节中,我们来看看虚拟内存是如何组织和管理地址空间的。

分页机制

虚拟内存将虚拟和物理地址空间都划分为固定大小的块,称为。虚拟地址空间中的块叫虚拟页,物理地址空间中的块叫物理页帧。操作系统负责维护虚拟页到物理页帧的映射关系。

一个典型的页大小是 4 KB。这意味着一个 32 位的虚拟地址(4 GB 空间)会被划分为:

  • 虚拟页号:高 20 位,用于查找映射。
  • 页内偏移:低 12 位,因为 2^12 = 4096 (4 KB),偏移量在映射过程中保持不变。

公式物理地址 = 物理页帧号 * 页大小 + 页内偏移

页表

操作系统使用一个称为页表的数据结构来存储所有虚拟页到物理页帧的映射关系。每个进程都有自己的页表。

页表的每个条目称为页表项,通常包含:

  • 有效位:指示该虚拟页是否已加载到物理内存中。
  • 物理页帧号:如果有效位为 1,则指向对应的物理页帧。
  • 访问控制位:如读/写/执行权限位,用于内存保护。

当 CPU 执行一条加载或存储指令时,它使用的是虚拟地址。内存管理单元需要“查阅”页表,将虚拟地址翻译成物理地址,然后才能访问真正的物理内存。


页表带来的挑战与优化 🚀

我们知道了页表是地址翻译的关键。但一个简单的线性页表会非常巨大。例如,对于一个 64 位虚拟地址空间(使用 48 位有效地址)和 4 KB 页,仅一个进程的页表就可能需要 2^(48-12) * 8字节 ≈ 32 TB 的空间,这显然不现实。

多级页表

为了解决页表过大的问题,现代系统使用多级(层次)页表。其核心思想是:只为进程实际使用的虚拟地址区域分配页表结构,而不是为整个地址空间预先分配。

以 x86-64 系统常见的四级页表为例:

  1. 虚拟地址被分成多个索引字段(例如,各 9 位)。
  2. CR3 寄存器指向顶级页目录(PML4)。
  3. 依次使用各级索引在页表层次结构中“行走”,最终找到末级的页表项(PTE),获得物理页帧号。

优势:节省了大量内存,因为未使用的虚拟地址区域对应的中间页表根本不会被创建。
代价:一次地址翻译可能需要多次内存访问(页表行走),导致延迟增加。

加速翻译:TLB

由于页表存储在内存中,每次地址翻译都进行多级页表行走会非常缓慢。为了加速,CPU 中集成了一个硬件缓存,专门用于存放最近使用过的虚拟页到物理页帧的映射,这就是转址旁路缓冲器

工作原理

  • 当需要翻译虚拟地址时,首先查询 TLB。
  • 如果 TLB 命中,则直接获得物理页帧号,速度极快。
  • 如果 TLB 缺失,则必须启动硬件页表行走器去内存中查找页表,找到映射后,不仅用于本次访问,还会将其存入 TLB 以备后用。

TLB 是虚拟内存性能的关键,其命中率通常非常高。


处理页错误 ⚠️

当程序访问一个虚拟地址时,可能遇到页不在物理内存中的情况,这会触发一个页错误。以下是处理页错误的步骤:

  1. 触发:CPU 访问一个虚拟页,其页表项中的有效位为 0,表示该页不在物理内存中(可能在磁盘上)。
  2. 异常:CPU 触发一个页错误异常,操作系统内核的页错误处理程序被调用。
  3. 处理:操作系统执行以下操作:
    • 检查访问是否合法(例如,是否有写入权限)。
    • 在物理内存中寻找一个空闲的页帧。如果内存已满,则需要根据某种页面置换算法(如时钟算法)选择一个“牺牲”页帧,将其内容写回磁盘(如果被修改过)。
    • 从磁盘(如交换空间或文件)中将所需的页读入到上一步找到的物理页帧中。这个 I/O 操作通常由 DMA 控制器完成,以减轻 CPU 负担。
  4. 更新:操作系统更新页表,建立新的虚拟页到物理页帧的映射,并设置有效位和权限位。
  5. 恢复:页错误处理程序返回,导致页错误的指令被重新执行,此时翻译成功,访问得以继续。

页错误处理是虚拟内存实现“无限内存”幻觉的核心机制,它允许系统将不常用的页暂时移出物理内存,为更急需的页腾出空间。


内存保护与共享 🛡️

除了地址翻译,虚拟内存的另一个重要功能是提供内存保护。页表项中的访问控制位(读、写、执行)使得操作系统可以控制每个页的访问权限。

以下是内存保护与共享的关键点:

  • 进程隔离:每个进程有自己的页表,因此它们的虚拟地址空间是隔离的。一个进程无法访问或破坏另一个进程的内存,除非通过操作系统显式设置的共享机制。
  • 权限控制:例如,代码页可以标记为“只读+可执行”,防止被意外修改;栈页可以标记为“可读可写但不可执行”,防止代码注入攻击。
  • 共享内存:通过让不同进程的页表项指向同一个物理页帧,可以高效地实现进程间的代码共享(如共享库)或数据共享。

现代优化与研究方向 📈

虚拟内存系统仍在不断演进以应对新的挑战。以下是一些重要的优化和研究方向:

  • 大页:使用更大的页(如 2 MB 或 1 GB 的“大页”)可以减少页表项的数量和 TLB 缺失率,从而提升大数据集应用的性能。Linux 中的透明大页特性可以自动尝试将小页合并为大页。
  • TLB 预取与管理:更智能的硬件或软件策略来预测并预加载可能需要的 TLB 条目。
  • 异构内存系统:随着新型非易失性内存、加速器自带内存的出现,虚拟内存系统需要更高效地管理这些异构的物理内存资源。
  • 虚拟化环境:在虚拟机中,存在“客户机虚拟地址 -> 客户机物理地址 -> 主机物理地址”的两层翻译,带来了额外的复杂性和开销,需要硬件(如 Intel EPT/AMD NPT)和软件的协同优化。

总结 🎯

本节课中我们一起学习了虚拟内存这一计算机架构的基石。我们了解到:

  1. 目标:虚拟内存通过硬件和操作系统的协作,为每个进程提供了巨大、私有且受保护的地址空间幻觉。
  2. 核心机制分页机制将地址空间划分成页,通过页表维护虚拟页到物理页帧的映射。
  3. 关键硬件MMU 负责地址翻译,TLB 作为翻译缓存极大提升了性能。
  4. 关键软件事件页错误处理程序负责动态地将数据在磁盘和物理内存之间调度,并更新页表。
  5. 重要功能:除了地址扩展,虚拟内存还提供了至关重要的内存保护共享功能。
  6. 持续演进:面对新的硬件和工作负载,虚拟内存系统通过大页、更智能的算法等不断优化。

虚拟内存完美体现了计算机系统中软硬件协同设计的智慧,它使得我们的程序能够更简单、更安全、更高效地运行。

11:以内存为中心的计算 (2025年春季)

概述

在本节课中,我们将探讨以内存为中心的计算这一新兴范式。我们将了解为何传统的以处理器为中心的计算架构在性能和能效上面临巨大挑战,并学习如何通过将计算能力移至数据所在之处(即内存或存储设备)来从根本上解决这些问题。我们将介绍两种主要方法:近内存计算内存内计算,并通过具体的研究案例和原型系统来展示其潜力。


为什么需要以内存为中心的计算?

上一节我们回顾了传统架构中处理器与内存分离带来的问题。本节中,我们来看看当前计算系统面临的核心挑战。

当今的计算系统严重受限于数据。重要的工作负载(如机器学习、基因组学、视频处理)都是数据密集型的,需要快速高效地处理海量数据。然而,数据量的增长速度远超我们的处理能力。

我们目前的设计是以处理器为中心的:处理器在这里,内存在那里,数据需要在两者之间频繁移动。

  • 巨大的能量浪费:数据移动消耗的能量远高于计算本身。例如,一次32位整数加法消耗的能量,可能只有从内存读取一个数据所需能量的1/6400。
  • 严重的性能损失:处理器大部分时间都在等待数据从内存返回。研究表明,在尖端处理器上,工作负载只有10%-20%的时间在执行指令,其余时间都在等待。
  • 系统复杂性激增:为了掩盖内存延迟,我们添加了巨大的乱序执行引擎、多级缓存、复杂的预取器等多线程技术。这导致超过90%的硬件资源被用于存储和移动数据,而非计算。

如果我们以“十岁孩童”的视角来看这个系统,会得到一个诚实的答案:这太不合理了。我们需要一种不同的方法。


以内存为中心的计算范式

上一节我们指出了以处理器为中心架构的弊端。本节中,我们来看看解决方案的核心思想。

以内存为中心的计算旨在从根本上最小化数据移动。其核心理念是:在数据所在之处进行计算

这并非全新的想法,早在20世纪60年代就有相关论文。但今天它变得尤为重要的原因在于:

  1. 应用与系统需求:数据密集型工作负载的能效和性能瓶颈已无法忽视。
  2. 内存技术挑战:随着内存单元尺寸缩小,其可靠性下降,需要更智能的内存控制器来管理,这为增加计算功能提供了契机。
  3. 新兴技术与集成工艺:如3D堆叠、混合键合等集成技术,使得将逻辑层和内存层紧密耦合成为可能。

我们可以通过两种主要方式实现以内存为中心的计算:

  1. 近内存计算:将计算逻辑(处理器或加速器)放置在物理上靠近内存芯片或内存层的位置。逻辑和内存仍是独立设计的,但通过高带宽、低延迟的互连紧密耦合。
  2. 内存内计算:直接利用内存器件本身的模拟操作特性进行计算(例如,利用DRAM中的电荷共享原理)。这是一种更根本的变革,逻辑和内存的界限变得模糊。

近内存计算

上一节我们区分了两种以内存为中心的方法。本节中,我们首先深入探讨近内存计算

近内存计算通过在内存芯片内部或旁边添加处理单元来减少数据移动。以下是几个关键的研究方向和实例:

3D堆叠与图处理加速

利用3D堆叠技术,可以将逻辑层和多个DRAM层垂直集成。在一项早期研究中,我们设计了一个名为“Tesseract”的系统,用于加速图处理应用。

  • 核心思想:将图数据分区存储在多个3D堆叠“立方体”的内存中。每个立方体的逻辑层包含一个简单的处理器阵列。计算时,不是将数据移动到函数,而是将函数(以消息形式)发送到数据所在的位置
  • 结果:这种架构为图处理算法带来了显著的性能提升(后来的一些工作甚至达到100倍加速)和能效提升。

机器学习推理的异构加速

机器学习模型的不同层具有迥异的特性:有些层计算密集,有些层数据移动密集。

  • 核心思想:设计一个异构加速系统,包含:
    • 以计算为中心的加速器:处理计算密集型层。
    • 以数据为中心的加速器(位于内存逻辑层):处理数据密集型层。
  • 运行时系统:智能地将模型的不同层调度到最合适的加速器上执行。
  • 结果:与单一的大型加速器相比,这种异构系统能以更小的总面积,实现约3倍的能效提升和更高的吞吐量。

存储内计算

对于超大规模数据集(如数百TB的基因组数据库),将数据全部移动到计算单元是不现实的。

  • 核心思想:在存储系统(如SSD控制器)中加入简单的计算能力,执行初步的过滤操作。例如,在基因组比对中,先在存储端过滤掉那些与参考基因组完全匹配或明显不匹配的序列片段。
  • 结果:只有少量无法轻易判断的数据需要被传送到主系统进行复杂计算,从而极大地减少了数据移动量和总处理时间。

总结:近内存计算通过将传统计算单元“拉近”内存,已经在特定领域(图处理、ML、基因组学)显示出巨大潜力,并且已有商业原型(如UPM的DRAM芯片内处理器)。


内存内计算

上一节我们看到了将逻辑靠近内存的威力。本节中,我们探讨一种更激进的方法:直接利用内存进行计算

内存内计算的核心是利用内存阵列固有的物理特性来执行逻辑操作。我们以DRAM为例进行说明。

基础原理:利用电荷共享

DRAM的基本操作单元是电容和存取晶体管。关键发现是,通过违反标准时序参数、以特定方式激活多行字线,可以利用电荷共享效应实现计算。

1. 数据复制(RowClone)

  • 操作:先激活源行(将数据读入行缓冲器),然后快速连续地激活目标行。
  • 原理:行缓冲器中的强驱动信号会将数据“推”入目标行的电容中,实现快速复制。
  • 优势:在内存内部复制一个4KB页面的延迟可降至约90纳秒,能耗极低,且无需经过处理器和缓存。

2. 位运算(例如AND, OR)

  • 操作并发激活三行(或多行)DRAM单元。
  • 原理:被激活单元的电荷会在位线上共享。最终位线的电压状态取决于多数单元的电荷状态,这实现了多数表决函数。通过将其中一行固定为特定值(如逻辑1或0),多数表决可以衍生出ANDOR操作。
  • 公式表示(概念性)
    • 多数表决:MAJ(A, B, C)
    • 若设 C = 1,则 MAJ(A, B, 1) = A OR B
    • 若设 C = 0,则 MAJ(A, B, 0) = A AND B
  • 应用:这种位级并行性非常适合数据库查询(位图索引)、集合运算、加密算法等。

从原理到系统:挑战与进展

将内存内计算集成到可编程系统中面临诸多挑战:

  • 编程模型:如何向程序员暴露这些操作?一种方法是通过新的指令或函数调用,编译器将其转换为发送给内存控制器的微程序序列。
  • 粒度匹配:DRAM一次操作一整行(例如8KB),但程序通常需要更细的粒度。研究提出了细粒度DRAM架构,通过分割字线,允许对更小的数据块(如512位)独立操作,从而更好地匹配现代向量化指令。
  • 可靠性:在未修改的商用DRAM上通过“滥用”时序实现的计算,其成功率并非100%。要构建可靠系统,可能需要轻微修改DRAM设计或结合纠错技术。

其他有趣的原语

研究还表明,利用DRAM的时序特性还能实现其他功能:

  • 真随机数生成:通过提前读取未稳定的DRAM单元,利用其固有的随机噪声来生成随机比特。
  • 物理不可克隆函数:利用那些在特定时序下总是出错的DRAM单元,生成独特的硬件指纹,用于安全认证。

总结:内存内计算是一种极具潜力的底层原语,能够以极高的能效和并行度执行特定操作。尽管将其集成到通用计算系统仍面临挑战,但它在专用领域和作为加速器方面前景广阔。


采用挑战与未来展望

上一节我们探讨了内存内计算的技术原理。本节中,我们来看看要实现以内存为中心的计算范式所面临的更广泛的系统级挑战

设计新的硬件只是第一步,真正的挑战在于整个软件栈和生态系统的适配:

  1. 编程与编译:需要新的编程模型、语言扩展、编译器支持,让开发者能轻松利用近内存或内存内计算资源。
  2. 数据映射与放置:操作系统和运行时需要智能地将数据分配到适合进行近内存/内存内计算的位置(例如,将需要频繁复制的两个页面放在同一个DRAM子阵列中)。
  3. 一致性:当处理器和内存端加速器同时访问或修改数据时,如何维护缓存一致性?这是一个棘手的问题。
  4. 虚拟内存:内存内计算单元如何理解并处理虚拟地址?这可能需要重新审视整个虚拟内存系统设计。
  5. 调度与资源管理:系统如何决定将哪部分工作负载卸载到内存端执行?如何管理内存计算单元与主机处理器之间的资源竞争?
  6. 安全与隔离:确保不同应用在共享内存计算资源时的安全隔离。

这些挑战既是障碍,也是研究机遇。解决它们需要计算机体系结构、操作系统、编程语言等多个领域的协同创新。


总结

在本节课中,我们一起学习了以内存为中心的计算这一重要范式。

  • 问题根源:我们首先分析了传统以处理器为中心架构的致命弱点——数据移动已成为性能和能效的主要瓶颈。
  • 核心思想:解决方案是让计算靠近数据,最小化不必要的数据搬运。
  • 两种路径:我们深入探讨了实现这一目标的两种主要技术路径:
    • 近内存计算:在物理上将处理逻辑放置在内存附近(如3D堆叠的逻辑层),已有多项研究和商业原型展示其价值。
    • 内存内计算:直接利用内存器件的物理特性(如DRAM的电荷共享)进行计算,这是一种更底层、能效潜力更高的方法,但编程和系统集成挑战更大。
  • 系统级挑战:我们认识到,要广泛采用这种新范式,需要克服从编程模型、编译器、操作系统到系统架构的一系列重大挑战。

以内存为中心的计算不是一时的潮流,而是应对数据洪流和能效墙的必然发展方向。它要求我们像“十岁孩童”一样,跳出固有思维,重新思考计算系统的根本设计。未来,我们有望看到计算、存储和通信功能更紧密融合的智能系统。

12:内存鲁棒性

在本节课中,我们将学习内存鲁棒性的核心概念,特别是由内存单元物理特性引发的比特翻转问题。我们将从基础的内存技术讲起,探讨其可靠性挑战,并深入分析一个著名的安全漏洞案例——Rowhammer。课程将涵盖该漏洞的原理、影响、利用方式以及业界提出的各种缓解方案。

概述:内存技术的可靠性挑战

现代计算机系统的主内存主要采用动态随机存取存储器技术。其基本存储单元由一个电容和一个访问晶体管构成。电容存储电荷,电荷的有无代表比特值“1”或“0”。为了追求更高的存储密度和能效,DRAM的制造工艺不断微缩,单元尺寸持续减小。

然而,技术微缩带来了显著的可靠性挑战。随着电容尺寸变小,其存储的电荷量减少,更容易受到噪声干扰,数据保持时间也缩短。这导致内存单元更容易发生非预期的比特翻转,即存储的值从0变为1或从1变为0。

DRAM单元结构与微缩挑战

上一节我们介绍了DRAM的基本原理,本节中我们来看看其具体的物理结构以及微缩带来的问题。

DRAM单元的核心是一个用于存储电荷的电容器。访问晶体管则控制着电容与位线之间的连接,以便进行读写操作。为了可靠地感知电荷状态,电容器必须足够大,访问晶体管也必须足够大以确保低泄漏和较长的数据保持时间。

随着工艺节点进入10纳米级别,将电路特征尺寸缩小到35纳米以下变得极具挑战性。虽然业界通过垂直堆叠电容(类似建造摩天大楼)等创新技术实现了微缩,但代价是单元可靠性下降。电荷减少、保持时间缩短,单元更容易因噪声效应而受到干扰。

大规模研究:错误率与密度的关联

为了量化可靠性问题,我们与Facebook合作进行了一项大规模研究,分析了其所有数据中心中记录的内存错误。

研究发现,DRAM芯片的密度与其在服务器中观察到的错误率之间存在相关性。随着内存芯片变得更密集(即单元更小),故障率也随之上升。这项2015年的工作揭示了技术微缩对可靠性的直接影响,尽管如今芯片密度更高,但这一趋势依然存在。

深入研究:构建测试基础设施

为了更深入地理解这些技术缩放问题,我们构建了专门的FPGA测试基础设施。最初的目的是研究数据保持时间,因为我们预测随着单元变小,可保留的电子数量减少,数据保持将成为重大问题。

我们观察到,由于工艺差异,不同DRAM单元的数据保持时间存在很大差异。当时的标准是每64毫秒刷新所有单元。我们提出了一个想法:根据单元不同的保持时间,以不同的速率刷新它们。这被称为利用异构保持时间。

然而,实现这一想法非常困难,因为保持时间依赖于位置、存储的数据模式和时间。除非添加纠错码等机制,否则难以轻松实现。我们后续在DSN 2015上发表的AVATAR方案,结合了ECC和内存清理,可靠地解决了这个问题,并因此获得了Test of Time奖。

新问题的发现:Rowhammer漏洞

我们的测试基础设施不仅用于研究保持时间,还被用来探究其他类型的内存干扰错误。在闪存研究中,我们发现了“读干扰”现象,即访问一个单元会干扰其他单元。

这引出了一个关键问题:DRAM中是否也存在类似问题?我们确信可能存在,但不清楚在正常操作模式下是否会发生。于是,我们利用基础设施研究了在何种操作条件下会引发这种导致比特翻转的干扰。

这就是Rowhammer漏洞的发现过程。其原理是:反复激活(打开并关闭)DRAM中的一行(称为“攻击行”),会对物理上相邻的“受害行”中的脆弱单元产生电气干扰。这种干扰会导致受害单元中的电荷逐渐泄漏,经过足够次数的激活后,其存储的比特值就会发生翻转。

我们在2014年的ISCA论文中表明,通过对一个攻击行进行成千上万次的合法激活操作,可以预测性地诱发相邻行的比特翻转。更关键的是,我们证明了这可以在正常操作条件下,通过用户级程序实现。

Rowhammer的原理与影响

上一节我们介绍了Rowhammer的发现,本节中我们来看看其具体工作原理和深远影响。

Rowhammer利用了DRAM的基础操作。激活一行会将其数据读入感应放大器,同时对该字线施加高电压。关闭(预充电)该行则施加低电压。反复进行“激活-预充电”操作是合法操作,但只要在单元被刷新之前进行足够多次,就会对相邻行的脆弱单元造成累积性电荷泄漏,最终导致比特翻转。

我们对来自三大主要DRAM制造商的129个模块进行了测试,发现2010年后制造的芯片中,超过80%存在此漏洞。这是一个随着技术微缩而恶化的问题:单元越小,越脆弱。

这个漏洞的深远意义在于,它首次展示了简单的硬件故障机制如何引发广泛的系统安全漏洞。安全研究人员随后展示了利用这些比特翻转可以进行各种强大的攻击。

用户级攻击与安全漏洞

我们开发了一个简单的用户级程序来演示Rowhammer攻击。该程序使用特定的汇编指令序列,确保两个地址X和Y映射到相同的DRAM存储区,并且不被缓存。通过反复访问(“锤击”)这些地址,可以在易受攻击的芯片上引发比特翻转。

更有效的是一种称为“双侧锤击”的技术,即用两个攻击行夹击一个受害行,干扰来自两侧,成功率更高。

谷歌的安全团队在看到我们的论文后,基于此开发了两种权限提升攻击。其中一种攻击通过Rowhammer诱发页表项中的比特翻转。攻击者通过“页表喷洒”技术,在物理内存中填充大量指向同一物理文件的页表项。如果Rowhammer恰好在某个PTE的关键位(如物理页号)上引发翻转,就可能使该PTE指向一个攻击者本无权限访问的物理页。如果这个被错误指向的页恰好是页表本身,攻击者就获得了读写页表的能力,从而完全控制系统。

此后,出现了更多利用Rowhammer的攻击,包括通过JavaScript进行远程攻击、利用Android内存分配模式的确定性攻击、甚至通过GPU或网络远程直接内存访问发起的攻击。

缓解方案与行业应对

发现漏洞后,需要立即和长期的解决方案。我们提出了多种缓解思路:

  1. 增加刷新率:更频繁地刷新所有行,减少攻击窗口。这是最简单的方法,但会显著增加能耗、降低性能。
  2. 物理隔离:隔离关键数据与普通数据。实施成本高,且难以保证安全。
  3. 纠错码:ECC能有效防护随机比特翻转,但对Rowhammer这种可诱发多位翻转的定向攻击效果有限,强ECC成本高昂。
  4. 反应式刷新:检测频繁激活的行(攻击行),并刷新其相邻行(受害行)。
  5. 主动式限制:检测并限制对频繁激活行的访问速率。

在我们的ISCA论文中,我们提出的最佳方案是概率性相邻行激活。其思想是:内存控制器在关闭一行后,以很低的概率(例如0.5%)主动刷新其相邻行。这是一种低开销、低复杂性的统计性防护方案。英特尔曾在其内存控制器中实现了类似方案。

行业最初的应急响应是普遍增加内存刷新率(例如苹果公司)。随后,DRAM制造商试图通过“目标行刷新”(一种内部机制,可能包含用于跟踪频繁激活行的表)来解决问题,并声称已修复漏洞。

漏洞的持续演进与挑战

然而,后续研究表明问题远未解决,且更加严重。

我们的后续研究发现,新一代DRAM芯片对Rowhammer更加脆弱,比特翻转所需的激活次数(即“锤击计数”)大幅降低。例如,某些LPDDR4芯片在仅4800次双侧锤击后就会失效。模拟表明,随着锤击计数降低,许多缓解方案(包括PARA)的性能开销会变得不可接受。

我们与另一研究团队合作,通过“多面锤击”攻击成功绕过了DRAM制造商的内置防护机制。攻击原理是同时锤击大量行(例如19行),以溢出芯片内部用于跟踪攻击行的容量有限的计数器或表格。

更深入的研究揭示了Rowhammer特性的复杂性,它受温度、攻击行激活保持时间、单元物理位置等多种因素影响,且存在空间差异(少数单元极度脆弱)。这为设计更具针对性的防护机制提供了思路。

新的威胁:RowPress

除了Rowhammer,我们还发现了另一种干扰现象:RowPress。其原理不是高频激活,而是将一行保持开启状态很长时间。

研究表明,将一行激活并保持开启数十毫秒(这超出了标准但用于研究),仅需单次激活就能导致相邻行比特翻转。即使在标准允许的70.8微秒最大开启时间内,也能将引发比特翻转所需的激活次数降低一两个数量级。

这意味着一些正常的高局部性工作负载也可能无意中诱发比特翻转。防护RowPress需要调整现有方案,在更低的激活计数阈值下触发防护,从而可能带来更高的性能开销。

根本性解决方案:重新思考内存接口

现有的缓解方案大多是在现有内存架构上的修补。我们提出了一个更根本的解决方案:Self-Managed DRAM

其核心思想是改变DRAM芯片与内存控制器之间的接口,允许DRAM芯片在需要时向控制器说“不”(即推迟命令),以便自主执行维护操作(如刷新、Rowhammer缓解、内存清理等)。

这样做的优势在于:

  • 解耦创新:DRAM制造商可以在芯片内部实现更先进的维护和优化算法,无需等待漫长且复杂的内存接口标准更新。
  • 更优的防护:芯片内部更了解自身的物理布局和脆弱性,可以实施更高效的防护。
  • 为存内计算铺路:这种自主性也有利于未来在DRAM内实现处理功能。

这一设想旨在推动内存系统设计从“以处理器为中心”向更“以内存为中心”的架构演进。

总结

本节课中我们一起学习了内存鲁棒性的核心挑战。我们从DRAM技术微缩带来的可靠性问题出发,深入探讨了Rowhammer这一由物理干扰导致的安全漏洞。我们分析了其原理、利用方式以及对系统安全的严重威胁。随后,我们回顾了业界提出的各种缓解方案,从简单的增加刷新率到复杂的内部计数器机制,并指出了这些方案在面临越来越脆弱的芯片和RowPress等新威胁时的局限性。最后,我们探讨了通过改变内存接口(Self-Managed DRAM)来从根本上提高内存系统自主性和鲁棒性的未来方向。这一领域的研究跨越了器件、电路、架构、安全等多个学科,是系统设计中持续面临的重要挑战。

13:闪存与固态硬盘 🚀

在本节课中,我们将要学习闪存(Flash Memory)和固态硬盘(Solid-State Drives, SSD)的基本原理与现代架构。闪存作为一种非易失性存储技术,是当今智能手机、笔记本电脑和数据中心存储系统的核心。我们将从闪存单元的工作原理开始,逐步深入到复杂的SSD控制器架构、地址映射、垃圾回收等关键技术,并探讨如何通过先进的错误处理技术来提升闪存的可靠性与寿命。


闪存概述:从新兴技术到主流存储 💡

上一节我们介绍了计算机架构中存储系统的多样性。本节中,我们来看看一种成功从“新兴技术”转变为现代计算基石的存储介质——闪存。

闪存曾被认为是一种前景不明朗的电荷存储技术。经过数十年的研发与优化,它如今已无处不在。我们口袋里的智能手机就是闪存技术影响力的最佳例证。闪存属于电荷存储型内存,其核心是在浮栅(Floating Gate)中存储电荷。然而,随着存储单元尺寸的缩小,电荷的可靠感知变得困难。

为了解决电荷存储的局限性,业界探索了两种主要路径:

  1. 新型内存架构:通过以内存为中心的系统设计、新颖的内存架构与接口,以及更好的损耗管理来克服内存短板。
  2. 新兴存储技术:例如相变存储器(PCM),它通过改变材料相位来存储数据,并通过检测电阻来读取,被认为比DRAM更具可扩展性。

尽管新兴技术前景广阔,但它们也存在许多不足。相比之下,闪存通过持续的技术迭代和智能的控制器设计,成功解决了可靠性、性能和成本问题,成为了主流存储方案。


现代固态硬盘(SSD)架构 🏗️

上一节我们了解了闪存的技术背景。本节中,我们来看看如何将闪存颗粒组织成一个高性能、高可靠的存储设备,即固态硬盘。

现代SSD是一个复杂的系统,由多个核心硬件控制器、DRAM以及非易失性闪存封装组成。其核心是SSD控制器,它内部包含多个处理器核心、硬件闪存控制器以及用于缓存的DRAM。

以下是SSD处理写入请求时,数据流经的主要组件:

  1. 主机接口层(Host Interface Layer, HIL):负责与主机操作系统通信,接口可能是SATA或更高性能的NVMe。NVMe允许应用程序直接与SSD队列交互,无需操作系统深度介入,从而提升吞吐量。
  2. 闪存转换层(Flash Translation Layer, FTL):这是SSD固件的“大脑”,负责将主机看到的逻辑地址映射到闪存上的物理地址,并管理闪存的诸多特性。
    • 写入缓冲区(Write Buffer):临时存放待写入数据,以便快速向主机返回确认,降低写入延迟。它还能实现灵活的I/O调度。缓冲区大小通常限制在几十MB,并依赖电容在突然断电时将脏数据写回闪存。
    • 逻辑到物理地址映射(L2P Mapping):由于闪存采用“异地更新”(Out-of-Place Write)策略,更新数据时需写入新位置,并将旧位置标记为无效。因此,需要一个映射表来记录逻辑页地址(LPA)到物理页地址(PPA)的对应关系。此映射表通常占用SSD总容量的约0.1%。
    • 元数据管理:FTL还维护着用于垃圾回收、磨损均衡、数据刷新等功能的元数据。
  3. 闪存控制器(Flash Controller):负责与闪存颗粒进行物理通信。
    • 随机化(Randomization):对数据进行加扰,避免特定数据模式加剧闪存错误。
    • 纠错码(Error-Correcting Code, ECC):为数据添加冗余信息,以检测和纠正读取时产生的错误。例如,每1KB数据可能附加72位ECC。
    • 发送闪存命令:最终将数据和命令发送给具体的闪存封装。

当处理读取请求时,FTL会先检查写入缓冲区(可视为缓存)。若未命中,则查询L2P映射表找到数据物理位置,再通过闪存控制器读取。随着SSD老化,ECC解码可能失败,此时需要调整参考电压进行重读,这会导致读取性能下降。


闪存单元:工作原理与特性 ⚡

上一节我们介绍了SSD的整体架构。本节中,我们深入其核心存储单元——闪存细胞(Flash Cell),看看它是如何存储数据的。

闪存细胞本质上是一种特殊的晶体管。与普通MOSFET不同,它在控制栅(Control Gate)和沟道之间增加了一个浮栅(Floating Gate),该浮栅被绝缘体包围,可以长期 trapped 电子。

其工作原理基于阈值电压(Threshold Voltage, Vth) 的改变:

  • 编程(Program):在控制栅施加高电压(如+20V),电子从衬底隧穿进入浮栅并被 trapped。浮栅带负电,使得细胞的阈值电压升高
  • 擦除(Erase):在控制栅施加负高压(如-20V),将电子从浮栅推回衬底,阈值电压降低
  • 读取(Read):在控制栅施加一个参考电压(Vref)。若Vref高于细胞当前Vth,晶体管导通,读为‘1’(对应擦除状态);若Vref低于Vth,晶体管截止,读为‘0’(对应编程状态)。

核心公式数据状态 = (Vref > Vth) ? ‘1’ : ‘0’

闪存的重要特性包括:

  • 多级单元(MLC/TLC/QLC):通过精确控制浮栅电荷量,使Vth落在多个不同区间,从而在一个细胞中存储多个比特(如2, 3, 4 bits)。这提升了密度,但牺牲了可靠性和性能。
  • 数据保持(Retention):浮栅中的电子会随时间缓慢泄漏,导致Vth漂移(通常左移),造成保持错误。即使不供电,数据也可能在数年后丢失。
  • 耐久性(Endurance):每次编程/擦除(P/E)循环都会对氧化层造成轻微损伤。经过一定次数的P/E循环后(如SLC 10万次,QLC仅1千次),细胞将无法可靠存储数据。

NAND闪存阵列:组织与操作 🧱

上一节我们了解了单个闪存细胞。本节中,我们看看如何将海量细胞组织起来,并进行高效的读写操作。

多个闪存细胞以特定方式连接,形成可寻址的存储阵列:

  • NAND串(NAND String):多个(如128个)细胞串联,类似一个NAND门。读取时,只有目标细胞施加参考电压,其他细胞施加更高的通过电压(Vpass)使其强制导通,这样整个串的电流仅由目标细胞的状态决定。
  • 页(Page):共享同一字线(Wordline)的所有细胞构成一个页,是读写操作的基本单位(通常为4KB-16KB)。对于MLC/TLC,一个字线对应多个逻辑页(如LSB页、MSB页)。
  • 块(Block):共享位线(Bitline)的一组NAND串构成一个块,是擦除操作的基本单位(通常包含数百个页)。擦除一个块会将其中所有细胞重置为‘1’状态。
  • 平面(Plane)晶圆(Die)封装(Package):多个块组成平面,多个平面组成晶圆,多个晶圆封装在一起形成闪存颗粒。不同平面可以并行操作以提升带宽。

编程操作采用增量步进脉冲编程(ISPP)

  1. 施加一个适中的编程电压脉冲。
  2. 验证细胞的Vth是否达到目标。
  3. 对已达到目标的细胞,通过拉高其位线电压来抑制(Inhibit) 进一步编程。
  4. 略微提高编程电压,重复步骤1-3,直到所有待编程细胞达到目标Vth。
    这种方法可以精确控制Vth分布,对于MLC/TLC至关重要。

读取操作对于SLC只需一次比较。对于MLC/TLC,则需要施加多个不同的参考电压进行多次感测,再通过解码逻辑确定存储的比特值。比特编码方式会影响读取延迟,例如,某种编码可能让MSB页的读取快于LSB页,这可以被FTL利用进行数据放置优化。


闪存转换层(FTL):关键算法 🔄

上一节我们看到了闪存物理操作的限制。本节中,我们来看看SSD的“智能”核心——FTL,如何通过软件算法管理闪存,提供一个简单、高效的块设备接口。

FTL的核心任务之一是地址映射。由于闪存块必须在写入新数据前被擦除,且擦除粒度大、耗时长,“就地更新”效率极低。因此,SSD采用异地更新策略:将数据更新写入新的空闲位置,并将旧位置标记为无效。这就需要FTL维护一个动态的逻辑到物理地址映射表

以下是一个简化的SSD初始状态与写入示例:

  • 假设物理容量(5个块,每块4页)大于逻辑容量(16个逻辑页)。
  • 收到对逻辑页LPA 0的写入,FTL将其写入PPA (Block0, Page0),并更新映射表。
  • 收到对LPA 4-15的连续写入,FTL会顺序填充当前打开的块(Block0),写满后再打开新块(Block1)继续写入。此时,LPA 4实际存储在PPA 1
  • 当对LPA 4进行更新时,FTL将其写入新的空闲位置(如PPA 16),并将原PPA 1标记为无效,同时更新映射表指向PPA 16

持续的异地更新会产生大量无效页,耗尽空闲页。此时需要垃圾回收(Garbage Collection, GC)

  1. 选择受害块(Victim Block):通常选择无效页比例最高的块,以最小化有效数据搬移开销。
  2. 搬移有效页:读取受害块中所有仍有效的页,将其写入新的空闲位置,并更新映射表。
  3. 擦除受害块:擦除整个块,使其变为空闲块可供后续写入。
    GC会带来显著的性能开销(读、写、擦除)和写放大,因此FTL会在SSD空闲时在后台进行GC,或采用渐进式GC来降低尾延迟影响。TRIM命令允许操作系统通知SSD哪些数据已被删除,帮助FTL提前识别无效页,提升GC效率。

FTL的另一项重要任务是磨损均衡(Wear Leveling),确保所有闪存块的P/E循环次数尽量平均,避免部分块过早报废。此外,还需定期进行数据刷新(Refresh),在数据因电荷泄漏而出错前,读取、纠错并重写,以对抗保持错误。


闪存可靠性:错误表征与缓解 🛡️

上一节我们学习了FTL如何管理闪存。本节中,我们直面闪存的核心挑战——可靠性问题,并探讨如何通过表征错误和设计缓解技术来应对。

闪存本质上是一个噪声信道。其错误主要来源于:

  1. 操作错误:编程干扰、读取干扰、擦除错误。
  2. 保持错误:电荷随时间泄漏,是主导性错误源,占总错误99%以上。
    随着P/E循环增加和工艺缩微,原始误码率(RBER)呈指数增长。

为了构建可靠的存储系统,需要结合多种技术:

  • 纠错码(ECC):必需,但能力有限且复杂度随纠错能力增强而提高。
  • 数据刷新:周期性读取数据,利用ECC纠正已积累的保持错误,然后重写(异地或原位)。刷新频率可根据块已经历的P/E周期数自适应调整。
  • 读取参考电压优化:闪存阈值电压分布会随P/E循环和干扰发生变化。通过建模预测分布漂移,并动态调整读取时使用的参考电压,可以显著降低读取错误率。

研究成果示例:通过实验表征,建立了闪存阈值电压分布的数学模型(如高斯模型、学生t分布模型)。基于此模型,可以预测编程干扰导致的电压漂移。通过每1K次P/E循环采样学习一次,并据此优化读取参考电压,能够在相同ECC强度下将闪存寿命延长30%,或者说在相同寿命要求下降低对ECC能力的要求。


总结 📚

本节课中,我们一起深入探讨了闪存与固态硬盘的技术全景。我们从闪存细胞的基本物理原理出发,了解了其编程、擦除、读取机制以及多级单元、数据保持、耐久性等关键特性。接着,我们剖析了现代SSD的复杂架构,包括主机接口、闪存转换层(FTL)和闪存控制器,并深入讲解了FTL的核心算法:异地更新、地址映射、垃圾回收、磨损均衡和数据刷新。最后,我们探讨了闪存面临的可靠性挑战,以及如何通过错误表征、数据刷新和动态电压优化等技术来有效缓解这些挑战,从而构建出高性能、高可靠、大容量的现代存储系统。闪存技术的发展是硬件架构与智能控制软件紧密结合、共同解决物理限制的典范。

14:互连网络

在本节课中,我们将要学习计算机系统中的互连网络。互连网络是连接处理器、内存、缓存和I/O设备等组件,并实现它们之间通信的关键基础设施。我们将探讨不同的网络拓扑结构、路由算法以及流控制机制,理解它们如何影响系统的性能、可扩展性和成本。

概述

互连网络对于任何包含多个组件的计算系统都至关重要。它决定了组件之间通信的速度、带宽和可靠性。在本节中,我们将介绍互连网络的基本概念及其重要性。

为什么需要互连网络?

我们需要互连网络来连接不同的组件并实现它们之间的通信。例如,处理器需要与内存通信,缓存之间需要同步数据,I/O设备需要与处理器交互。所有这些都需要某种形式的互连。

互连网络影响系统的多个方面:

  • 可扩展性:决定了系统可以扩展到多大规模。
  • 带宽:决定了组件间通信的数据吞吐量。
  • 成本:连接组件的硬件成本。
  • 性能与能效:直接影响通信延迟和能耗。
  • 可靠性与安全性:影响消息传递的保证和协议的正确性。

在计算机架构中,内存瓶颈和互连瓶颈常常交织在一起,因为数据不仅需要被访问,还需要在组件间移动。

网络拓扑结构

网络拓扑定义了网络中节点(如处理器、内存块)和链路(连接线)的物理或逻辑布局。它决定了通信路径、延迟和成本。

以下是评估拓扑的一些关键属性:

  • 直径:网络中任意两个节点之间最长的最短路径长度。
  • 平均跳数:所有有效通信路径的平均跳数。
  • 对分带宽:将网络切成两半时,被切断的链路的带宽总和。这粗略反映了网络可维持的吞吐量。

接下来,我们来看看几种常见的拓扑结构。

总线

总线是最简单的拓扑结构。所有节点都连接到一组共享的物理线路上。

优点

  • 实现简单,成本低,适用于少量节点。
  • 易于实现缓存一致性协议(如侦听协议)。

缺点

  • 不可扩展:随着节点增加,电气负载和仲裁复杂性上升,导致频率降低。
  • 高争用:同一时间只能有一对节点通信,容易饱和。

总线适用于小规模多处理器系统。

点对点全连接

每个节点都直接连接到其他所有节点。

优点

  • 无争用:不同节点对可以同时通信。
  • 最低延迟:无需经过中间节点。

缺点

  • 成本极高:每个节点需要 O(n) 个端口,总链路数为 O(n²)
  • 布局困难:在芯片上布线极其复杂。

这种结构不具备可扩展性。

交叉开关

交叉开关是一种折中方案。它有一组输入节点和一组输出节点,通过一个开关矩阵连接,允许任何输入连接到任何输出。

优点

  • 高并发:允许并发传输到非冲突的目的地。
  • 低延迟,高吞吐

缺点

  • 成本较高:仍然是 O(n²) 的复杂度。
  • 仲裁复杂:随着规模增大,仲裁器变得复杂。

交叉开关常用于核心到缓存网络等小规模系统。实现时可以选择有缓冲无缓冲设计。有缓冲设计更灵活,支持可变大小的数据包(可分割为微片),但需要额外的存储空间。

多级间接网络

为了降低交叉开关的成本,人们设计了多级间接网络(如Omega网络、蝶形网络)。在这种网络中,节点通过多级交换机连接,交换机本身不是通信节点。

特点

  • 成本降低:成本约为 O(n log n)
  • 延迟增加:需要经过 log n 级交换机。
  • 可能产生争用:即使目的地不同,数据包也可能在中间交换机上冲突。

多级网络在成本和性能之间取得了平衡。

环形网络

节点连接成一个环,每个节点只与两个邻居相连。

优点

  • 成本低:复杂度为 O(n)
  • 实现简单

缺点

  • 高延迟:平均跳数为 O(n/2)
  • 可扩展性差:对分带宽恒定,增加节点会降低性能。

为了提高性能,通常使用双向环。为了进一步扩展,可以引入层次化环,即用更高速的“全局环”连接多个“局部环”。

网格与环面网络

在网格中,每个节点与上下左右四个邻居直接相连(边缘节点除外)。

优点

  • 成本低O(n)
  • 布局规整:易于在芯片上实现。
  • 路径多样性:两点间存在多条路径。
  • 平均跳数:约为 O(√n),优于环。

缺点

  • 边缘拥塞:通信容易向中心汇聚,导致中心区域拥塞。
  • 非对称性:边缘节点性能可能较差。

环面 通过将网格的边缘连接起来解决了非对称性问题,使每个节点都有四个邻居,提高了路径多样性,但布线稍复杂。

胖树与其他拓扑

胖树 是一种层次化拓扑,越靠近树根,链路带宽越宽(因此叫“胖”树)。它适合聚合通信模式(如规约操作)。

超立方体 是一个 n 维立方体,具有 log n 的延迟和 n log n 的链路数,可扩展性好,但布局复杂。

拓扑结构的选择没有绝对最优,需要根据系统规模、成本、性能目标和应用特征来权衡。

上一节我们介绍了各种网络拓扑,本节中我们来看看数据包如何在选定的拓扑中传输,即路由算法。

路由机制与算法

路由决定了数据包从源节点到目的节点的具体路径。

路由机制

  1. 算术路由:基于简单算术计算下一跳。例如,在网格中使用 XY 路由,先沿X方向移动,再沿Y方向移动。
    • 下一跳方向 = sign( dest.x - current.x ),若为0则计算Y方向。
  2. 基于源的路由:源节点指定路径上每个交换机的输出端口。交换机简单,但报文头开销大。
  3. 查表路由:每个交换机根据数据包的目的地址查找路由表决定输出端口。灵活,支持容错,但交换机更复杂。

路由算法类型

  1. 确定性路由:对给定的源-目的对,总是选择相同的路径(如XY路由)。简单,但无法利用路径多样性,可能导致拥塞。
  2. 随机路由:随机选择路径,不考虑网络状态。例如 Valiant 算法:先随机路由到一个中间节点,再路由到目的地。旨在平衡网络负载,但可能增加延迟。
  3. 自适应路由:根据网络状态(如拥塞、故障)动态选择路径。
    • 最小自适应:只在所有能减少到目的地距离的输出端口(“生产性端口”)中选择。
    • 非最小自适应:有时可以选择不减少距离的端口(“非生产性端口”)以绕过拥塞。性能潜力高,但需解决活锁问题。

死锁与活锁

  • 死锁:多个数据包相互等待对方占用的资源,形成循环依赖,导致无法前进。解决方法包括:设计无死锁的路由算法(如限制转弯)、增加缓冲区、或检测并解除死锁。
  • 活锁:数据包在网络中不断绕行,永远无法到达目的地。这在非最小自适应路由中可能发生。解决方法包括基于数据包年龄进行优先级调度。

自适应路由还可以用于容错,通过查表路由绕过故障的链路或路由器。

上一节我们讨论了数据包如何选择路径,本节中我们来看看当多个数据包竞争同一资源时,网络如何管理这些数据流,即流控制。

流控制与缓冲

流控制决定了网络在争用情况下如何管理数据包。

处理争用的方法

当两个数据包需要同一输出链路时:

  1. 缓冲:将其中一个数据包存入缓冲区,稍后发送。需要缓冲区管理。
  2. 丢弃:丢弃一个数据包,并通知发送方重传。适用于广域网(如TCP/IP),但在片上网络可能引入额外延迟和复杂性。
  3. 偏转:将冲突的数据包通过另一个非生产性端口发送出去,让其绕行。这是无缓冲路由的核心思想。

电路交换 vs. 分组交换

这是一个更高层的设计选择:

  • 电路交换:在数据传输前,预先在源和目的地之间建立一条专用路径。建立后,数据可以无缓冲地流式传输。
    • 优点:无仲裁开销,无缓冲,适合大数据流。
    • 缺点:链路利用率低,建立和拆除路径有开销。
  • 分组交换:每个数据包(或微片)在每个路由器独立做出路由决策。
    • 优点:灵活,链路利用率高。
    • 缺点:需要每跳仲裁,可能需缓冲,延迟可能较高。

无缓冲(偏转)路由

这是一种极端的设计选择,完全消除路由器中的缓冲区。发生争用时,强制偏转一个数据包。

  • 优点:路由器设计简单,面积和功耗低。
  • 挑战:需解决活锁问题;在高负载下,由于缺乏缓冲,吞吐量可能下降。

偏转路由与某些拓扑(如层次化环)结合能取得良好效果。

网络性能分析

评估网络性能通常使用负载-延迟曲线

  • X轴:注入负载(如每个节点每周期注入的平均数据包数)。
  • Y轴:平均数据包延迟。

曲线特征:

  1. 零负载延迟:当注入负载为0时的延迟。由拓扑路由算法流控制的开共同决定。
  2. 饱和吞吐量:延迟开始急剧上升时的注入负载。它代表了网络的最大可持续吞吐量,同样受拓扑、路由和流控制效率的限制。

重要提示:网络性能最终必须放在整个应用和系统背景下评估。特定的通信模式可能使某些网络设计表现得更好或更差。

总结

本节课中我们一起学习了计算机互连网络的基础知识。我们从互连网络的重要性开始,深入探讨了多种拓扑结构(总线、点对点、交叉开关、多级网络、环、网格、环面、胖树等)及其权衡。接着,我们了解了路由机制和算法(确定性、随机、自适应),以及它们面临的死锁和活锁挑战。然后,我们研究了流控制方法,包括电路交换、分组交换以及有趣的偏转路由。最后,我们介绍了如何使用负载-延迟曲线来分析网络性能。互连网络是一个丰富而复杂的领域,其设计需要综合考虑拓扑、路由和流控制,以满足特定系统在性能、成本、可扩展性和能效方面的需求。

15:并行性、异构性与瓶颈加速 (Spr 2025)

概述

在本节课中,我们将学习指令预取、异构计算系统设计以及如何通过识别和加速并行程序中的瓶颈来提升性能和可扩展性。我们将从指令预取的基本概念开始,然后深入探讨异构多核架构的原理、优势与挑战,最后介绍一系列通过软硬件协同来加速关键路径(如临界区、屏障等)的技术。

指令预取

上一节我们详细讨论了数据预取。本节中,我们来看看指令预取。指令预取的核心思想与数据预取类似:预测程序即将需要的指令,并主动将它们取入缓存。两者的主要区别在于,指令访问通常是顺序的(遵循冯·诺依曼模型的顺序执行原则),而数据访问则不一定。

顺序指令预取

由于指令执行的顺序性,简单的顺序流预取通常非常有效。其基本方法是:在取出一条指令后,总是预取其后的 N 条指令。这种方法在程序顺序执行且没有分支时效果很好。

一个早期的例子是 IBM 360/91 处理器,它使用了指令取指缓冲区来实现预取。虽然现代处理器更为复杂,但这种顺序预取的思想至今仍然适用。

更高级的指令预取技术

以下是几种更高级的指令预取技术:

  • 基于使用历史的预取:为缓存行添加一个“预取位”元数据,记录之前预取的指令块是否被实际使用过。根据历史信息,决定是否预取后续指令块,以提高预取准确性。
  • 关联预取:识别指令缓存缺失地址,并将其与更早的、足够提前的指令访问相关联。当下次看到这个早期访问时,就提前启动对缺失地址的预取,以覆盖预取延迟。
  • 取指导向的指令预取:利用处理器前端(取指/译码阶段)的信息,在处理器后端(执行阶段)因内存访问等原因停顿(stall)时,前端可以继续预测分支并预取指令,填充到一个指令缓冲区(如取指目标队列)中。这实际上将前端与后端解耦,允许前端“超前运行”,从而重叠后端停顿的延迟和指令缓存缺失的延迟。

研究表明,如果取指导向的预取与一个足够大的取指目标队列以及高精度的分支预测相结合,可能就不再需要其他复杂的指令预取器。

指令预取与数据预取的对比

指令缓存缺失的发生频率通常低于数据缓存缺失,因为代码的工作集通常更小、局部性更高。然而,在某些工作负载(如大型商业数据库)中,指令工作集可能很大,导致严重的指令缓存缺失。此外,指令缺失的后果可能更严重,因为它会直接导致流水线“断流”。

异构性与并行性瓶颈

现在,让我们转向一个更核心的话题:并行性、异构性和瓶颈加速。异构性(或称非对称性)是实现专业化的关键途径。

为什么需要异构性?

对称(同构)设计试图用单一类型的核心满足所有工作负载和设计指标,这非常困难。异构设计则通过提供多种不同类型的核心或硬件单元,使系统能够:

  1. 适应不同的工作负载行为:不同应用,甚至同一应用的不同执行阶段,其行为(如局部性、分支可预测性、指令级并行度)可能差异巨大。
  2. 同时优化多个设计指标:系统设计通常需要权衡性能、能效、公平性、复杂度等多个目标。单一设计难以在所有目标上都达到最优。

异构性的本质是在通用性和专用性之间架起桥梁:它不像通用设计那样“一刀切”,也不像专用设计那样为每个任务定制硬件,而是提供一组优化的子设计,并将它们组合在一起。

并行性的挑战:阿姆达尔定律

理想情况下,我们希望并行程序的加速比能随着核心数量线性增长。然而,阿姆达尔定律指出,程序的加速比受限于其串行部分的比例

公式Speedup = 1 / (F + (1 - F) / N)
其中,F 是串行部分的比例,N 是处理器数量。

即使串行部分很小,它也会成为性能扩展的根本瓶颈。此外,并行部分本身也并非完美并行,会受限于:

  • 同步开销:如锁、屏障。
  • 负载不均衡:工作未能在各核心间均匀分配。
  • 资源争用:核心在共享资源(如缓存、内存控制器)上的竞争。

异构多核:结合大核与小核

基于以上分析,我们可以得出一个直观的解决方案:

  • 串行代码段:需要一个强大的大核来快速执行。
  • 可并行代码段:可以使用多个能效高的小核来执行。

异构多核架构(Asymmetric Chip Multi-Processor, ACMP)正是结合了这两种核心。例如,在一个芯片上放置一个高性能大核和多个高能效小核。

  • 串行部分和关键瓶颈(如高争用的临界区)被调度到大核上执行,以获得高性能。
  • 可并行部分被调度到多个小核上执行,以获得高吞吐量。

这样,我们就能在串行性能上媲美“全大核”设计,在并行吞吐量上接近“全小核”设计,从而接近“两者兼得”的理想状态。

瓶颈加速技术

接下来,我们探讨几种具体的软硬件协同技术,用于动态识别和加速并行程序中的瓶颈。

1. 加速临界区

核心思想:将争用严重的临界区代码迁移到异构多核系统中的大核上执行。

工作原理

  1. ISA 扩展:添加 CRITICAL_SECTION_CALLCRITICAL_SECTION_DONE 指令。
  2. 执行流程
    • 当小核上的线程进入一个临界区时,它不直接执行,而是通过 CRITICAL_SECTION_CALL 指令,将临界区ID、入口程序计数器、栈指针等信息发送到临界区请求缓冲区
    • 大核从缓冲区中取出请求,开始执行临界区代码。执行完毕后,通过 CRITICAL_SECTION_DONE 指令通知发起请求的小核。
    • 小核接收到完成信号后继续执行。
  3. 优势
    • 更快执行:大核更快地执行临界区。
    • 更好的共享数据局部性:锁和共享数据驻留在大核缓存中,避免了在多小核间“乒乓”移动,减少了缓存一致性开销。
  4. 挑战
    • 虚假串行化:独立的临界区可能被发送到同一个大核而被迫串行。可通过多线程或基于历史计数器进行限流来缓解。
    • 私有数据通信开销:临界区所需的输入数据(线程私有)需要从小核传输到大核。

2. 瓶颈识别与调度

核心思想:将加速对象从“临界区”泛化到任何导致线程等待的“瓶颈”,包括临界区、屏障、流水线阶段等。

工作原理

  1. 软件注解:编译器或库在潜在的瓶颈代码段(如锁获取、屏障点)插入 BNECK_CALLBNECK_RETURN 指令。在等待点插入 BNECK_WEIGHT 指令。
  2. 硬件监控:硬件维护一个瓶颈表。当线程执行 BNECK_WEIGHT 时,硬件会计数该瓶颈导致的线程等待周期数
  3. 动态决策:小核在执行 BNECK_CALL 前,会查询瓶颈表。如果该瓶颈历史等待周期数高(表明是关键瓶颈),则将其迁移到大核执行;否则,就在本地小核执行。
  4. 优势:更通用,能识别和加速多种类型的同步瓶颈,并根据其实际关键性动态决策。

3. 基于效用的瓶颈加速

核心思想:在瓶颈识别的基础上,引入更精细的“效用”模型来决定加速优先级,而不仅仅是等待周期数。

效用公式(概念性):
Utility(加速代码段C) ≈ LocalSpeedup(C) × Importance(C) × Criticality(C)
其中:

  • LocalSpeedup(C):该代码段在大核上相对于小核的本地加速比。
  • Importance(C):该代码段在执行时间中所占的比例。
  • Criticality(C):该代码段在全局范围内的关键程度。

通过估计每个瓶颈的效用值,系统可以优先加速那些能带来最大整体性能提升的瓶颈,决策更加精准。

4. 数据编组:缓解通信开销

核心思想:在将代码段(如临界区)迁移到大核执行时,主动将其所需的“生产者”数据(由之前代码段生成)从源小核“推送”到大核的缓存,以避免迁移后的缓存缺失。

工作原理

  1. 识别生成者指令:通过离线剖析,编译器识别出在代码段中最后写入特定缓存块(这些块会被后续代码段读取)的指令,称为“生成者”指令。
  2. 插入编组指令:在生成者指令后插入特殊的编组指令
  3. 硬件支持:小核配备编组缓冲区。当执行到生成者指令时,记录产生的地址。当执行到编组指令时,硬件主动从本地缓存读取这些地址的数据,并通过互连网络发送到大核的缓存中。
  4. 优势:当大核开始执行迁移来的代码段并访问这些数据时,它们很可能已经在大核的缓存中,从而将“缓存缺失”转化为“缓存命中”,显著降低了代码迁移带来的通信延迟。

异构性的其他实现方式与总结

除了设计不同微架构的核心,还有其他方式实现异构性:

  • 动态电压频率缩放:根据线程并行度,动态调整部分核心的电压和频率。在高单线程性能需求时,提升单个核心的频率;在高吞吐需求时,让多个核心以较低频率运行。
  • 核心融合/形态核心:探索将多个小核动态组合成一个更强大核心的研究思路。

总结

本节课我们一起学习了:

  1. 指令预取:利用指令的顺序性,通过顺序预取、关联预取、取指导向预取等技术来隐藏取指延迟。
  2. 异构计算:通过结合高性能大核和高能效小核,异构多核架构旨在同时优化串行性能和并行吞吐量,应对阿姆达尔定律和并行开销的挑战。
  3. 瓶颈加速:一系列软硬件协同技术(加速临界区、瓶颈识别与调度、基于效用的加速、数据编组)被用于动态识别并行程序中的关键路径(瓶颈),并将它们调度到更强大的硬件资源上执行,从而提升整体性能和可扩展性。这些技术的关键在于权衡加速收益迁移开销共享数据局部性私有数据通信成本

异构性和智能的瓶颈管理是应对现代及未来计算挑战,尤其是在“后摩尔定律”时代提升系统效率和性能的关键方向。

17:多处理器、内存排序与缓存一致性 (2025春季)

概述

在本节课中,我们将学习多处理器系统的基础知识,重点关注内存排序和缓存一致性这两个核心概念。我们将探讨如何设计支持并行执行的硬件,同时确保程序的正确性。课程将从并行计算的分类开始,逐步深入到内存一致性模型和缓存一致性协议的具体实现。

多处理器系统概述

上一节我们回顾了计算机架构的基本概念,本节中我们来看看多处理器系统的分类。Michael Flynn在1966年的论文中,根据指令流和数据流的数量对计算机进行了分类。

以下是四种主要的计算类型:

  • SISD (单指令流单数据流):这是传统的顺序处理器。一条指令操作一个标量数据元素。其核心是顺序执行线程
  • SIMD (单指令流多数据流):一条指令同时操作多个数据元素,例如向量处理或阵列处理。其核心优势是单条指令能启动大量并行操作,公式表示为:一条指令 -> 对向量 V 的所有元素执行操作 OP。这显著降低了指令获取和解码的开销。
  • MISD (多指令流单数据流):多条指令操作同一个数据元素。一个接近的例子是脉动阵列处理器,数据像流水一样经过一系列处理单元,每个单元对其执行不同的操作。
  • MIMD (多指令流多数据流):多个独立的指令流同时操作多个数据。这是现代多核处理器和众核处理器的基础,也是本节课的重点。

现代系统通常是这些类型的组合。例如,一个多核处理器(MIMD)中的每个核心可能是一个支持SIMD扩展的SISD处理器。

并行计算的目标

我们为什么要进行并行计算?主要目标有以下几点:

  1. 提升性能:通过同时执行多个任务来减少总执行时间。
  2. 提高能效:使用多个低频、简单的核心可能比单个高频、复杂的大核心更节能。动态功耗公式为:P_dynamic = C * V^2 * F。降低频率F通常允许降低电压V,从而带来立方的功耗收益。
  3. 改善成本效益与可扩展性:设计并复制多个简单的处理单元通常比设计一个极高性能的单一复杂单元更容易且成本更低。
  4. 增强可靠性:通过冗余执行相同的任务并比较结果,可以提高系统的容错性。

并行性的类型

并行性可以从不同层面进行挖掘:

  • 指令级并行 (ILP):在单个指令流中挖掘并行性,例如流水线、乱序执行、推测执行。
  • 数据级并行 (DLP):对不同的数据元素并行执行相同的操作,例如SIMD和向量处理。
  • 任务级并行 (TLP):并行执行不同的任务或线程,例如多线程和多处理。

本节课我们将重点讨论任务级并行,特别是在共享内存多处理器环境下的情况。

多处理器系统的类型与挑战

根据处理器是否共享全局内存地址空间,多处理器系统可分为两类:

  • 松散耦合多处理器:无共享全局内存地址空间。通常通过消息传递进行进程间通信。
  • 紧密耦合多处理器:共享全局内存地址空间。通常通过加载和存储指令访问共享数据进行通信和同步。也称为对称多处理器(SMP),是现代多核处理器的基础。

需要注意的是,编程模型和硬件执行模型可以解耦。例如,可以在共享内存硬件上运行消息传递的程序,反之亦然,但这通常需要软件层进行转换并可能带来开销。

对于紧密耦合多处理器,主要的设计挑战包括:

  • 共享内存同步:如何高效、正确地实现锁、屏障等同步原语。
  • 缓存一致性:当多个私有缓存保存了同一内存地址的副本时,如何确保所有处理器看到的数据是一致的。
  • 内存一致性:定义多处理器系统中内存操作的全局可见顺序,为程序员提供可预测的语义。
  • 共享资源争用:多个线程对共享硬件资源(如缓存、互连网络)的竞争访问。
  • 互连网络设计:如何高效连接大量处理器和内存。

并行加速的限制

并行化并非总能带来线性的性能提升。阿姆达尔定律描述了并行加速的上限。

α 为程序中可并行部分的比例,P 为处理器数量,则加速比 S 为:
S = 1 / ((1-α) + α/P)

P 趋近于无穷大时,最大加速比 S_max = 1 / (1-α)。这表明,程序的串行部分(1-α)是性能提升的根本瓶颈

此外,在评估并行算法时,必须进行公平的比较:应该将并行算法与最优的串行算法进行比较,否则可能夸大并行带来的收益。超线性加速(加速比超过处理器数量)通常意味着比较不公平(如使用了更差的串行算法),或者在并行化时带来了额外的正面效应(如总缓存容量增大,减少了缓存缺失)。

内存一致性

在单处理器中,内存操作顺序由冯·诺依曼模型保证,即程序顺序执行。但在多处理器中,不同处理器发出的内存操作以何种全局顺序被其他处理器观察到,这就是内存一致性问题。

一个不正确的内存顺序可能导致严重的错误。以Dekker算法为例,两个处理器(P1和P2)试图通过标志变量F1和F2进入临界区。如果写操作(设置标志)和读操作(检查对方标志)的全局顺序在不同处理器看来不一致,可能导致两个处理器同时进入临界区,违反了互斥原则。

顺序一致性是一种强一致性模型,由Leslie Lamport提出。它满足两个条件:

  1. 单个处理器的操作按其程序顺序执行。
  2. 所有处理器观察到所有内存操作的全序相同。

顺序一致性简化了编程,但严格限制了硬件优化(如乱序执行、写缓冲),可能带来较大的性能开销。

弱一致性模型

为了在性能和正确性之间取得平衡,提出了多种弱一致性模型。其核心思想是:只在必要时(如同步操作时)强制进行内存排序

常见的弱一致性模型包括:

  • 弱一致性:使用内存栅栏指令显式界定需要保证顺序的区域。栅栏之前的所有内存操作必须完成后,才能执行栅栏;栅栏之后的所有内存操作必须等待栅栏完成。
  • 释放一致性:进一步区分同步操作的类型(如获取锁和释放锁),允许在非临界区进行更灵活的重排序。

这些模型将保证正确性的部分责任转移给了程序员,但为硬件实现提供了更大的优化空间,从而可能获得更高的性能。

缓存一致性

在具有私有缓存的多处理器中,同一内存块可能存在于多个缓存中。当一个处理器修改其缓存中的副本时,必须确保其他处理器不会读到过时的数据,这就是缓存一致性问题。

缓存一致性协议需要保证:

  1. 写传播:对某个内存位置的写操作,最终必须对其他持有该位置副本的处理器可见。
  2. 写串行化:对所有处理器而言,对同一内存位置的所有写操作必须被观察到相同的顺序。

软件维护一致性非常困难且低效。因此,现代多处理器主要依靠硬件协议来实现缓存一致性。

基于侦听的缓存一致性协议

在基于总线的小规模多处理器中,常使用侦听协议。所有缓存都连接到共享总线,并“侦听”总线上所有的读写请求。

一个简单的协议是MSI协议,每个缓存行有三种状态:

  • 修改:缓存行是唯一的副本,且已被修改(脏数据)。
  • 共享:缓存行是多个可能副本之一,是干净的(与内存一致)。
  • 无效:缓存行数据无效。

协议通过状态转换来维护一致性。例如,当处理器要写入一个处于“共享”状态的缓存行时,它会在总线上发出“读独占”请求,其他缓存看到后将自己的副本置为“无效”,然后该处理器将状态转为“修改”并进行写入。

MESI协议是MSI的优化,增加了独占状态。该状态表示缓存行是唯一的副本,但是干净的。这允许处理器在从“独占”状态写入时,无需通知其他缓存,从而减少了总线事务。

基于目录的缓存一致性协议

侦听协议依赖于广播,难以扩展到大量处理器。基于目录的协议更具可扩展性。

目录是一个集中式或分布式的数据结构,记录每个缓存块被哪些处理器缓存。当处理器需要读写一个缓存块时,它先查询目录。目录负责向所有持有该块副本的处理器发送失效或更新消息,并确保写操作的串行化。

目录协议避免了广播,但引入了目录访问的延迟和存储开销。可以通过使用位向量、链表或布隆过滤器等技术来压缩目录信息。

总结

本节课我们一起学习了多处理器系统的核心挑战:内存排序和缓存一致性。

  • 内存一致性定义了多线程程序中内存操作的全局可见顺序,顺序一致性模型简单但性能代价高,弱一致性模型在性能和编程复杂性之间进行了权衡。
  • 缓存一致性确保了多个缓存中同一数据副本的同步,MSI/MESI等侦听协议适用于小规模系统,而目录协议更适合大规模可扩展系统。

理解这些概念对于设计高效、正确的并行硬件和软件至关重要。下一节课,我们将探讨连接这些处理器和内存的互连网络技术。

17b:片上网络 (Spring 2025) 🧠

概述

在本节课中,我们将深入学习片上网络的设计原理、关键挑战以及前沿优化技术。我们将从回顾网络基础知识开始,逐步探讨如何针对现代芯片架构的特点,重新思考并优化网络设计,特别是围绕缓冲、流控制和调度等核心概念。


网络性能回顾与设计挑战

上一节我们介绍了互连网络的基础知识。本节中,我们来看看如何衡量网络性能,以及现代系统对网络设计提出的新挑战。

网络性能通常用负载-延迟曲线来描述。X轴代表注入网络的负载(例如,每秒注入的数据包速率),Y轴代表数据包从源到目的地的平均或最大延迟。

  • 零负载延迟:由拓扑结构、路由算法以及流控制和缓冲机制共同决定的最小延迟。
  • 饱和吞吐量:网络能够维持而不导致延迟急剧上升的最大注入速率。

这个曲线的形状受到多种设计选择的影响:

  • 拓扑结构:决定了节点间连接的基本方式和最小跳数。
  • 路由算法:决定了数据包在给定拓扑上的具体路径。
  • 流控制与缓冲:决定了数据包在遇到竞争时的处理方式,直接影响延迟和吞吐量。

然而,这些基于合成流量模式(如均匀随机流量)的曲线,可能无法准确反映真实应用程序的性能。随着架构、工作负载和技术的演变,我们需要重新思考通信网络的设计。


片上网络的特点与机遇

与片外网络相比,片上网络具有独特的优势和约束,这为设计优化提供了新的方向。

以下是片上网络的关键特点:

  • 优势
    • 低延迟:节点间距离短,通信延迟低。
    • 丰富的布线资源:多层金属层允许高带宽、密集的连线。
    • 易于全局协调:在单一芯片上更容易实现复杂的协调机制。
  • 约束
    • 二维基底限制:拓扑结构的物理实现受限于芯片布局。
    • 功耗与面积限制:总功耗和冷却能力限制了复杂算法的使用。
    • 大缓冲区的成本高:缓冲区消耗显著的芯片面积和静态/动态功耗。

这些特点促使我们探索更高效、更简单的网络设计,特别是减少或消除昂贵的缓冲区。


迈向无缓冲网络:偏转路由

鉴于缓冲区的成本,一个极端的思路是:能否完全消除缓冲区?这引出了无缓冲偏转路由的概念。

在传统的缓冲网络中,当多个数据包竞争同一输出端口时,其中一个会被缓冲。无缓冲偏转路由则采用不同的策略:如果没有可用的“生产性”方向(即朝向目的地的方向),数据包会被偏转到一个非生产性方向。

核心思想:使用链路本身作为临时“缓冲区”,让数据包绕远路到达目的地,从而避免在路由器内进行缓冲。

这种方法,也称为“热土豆路由”,其路由器操作可以简化为两个核心步骤,用伪代码描述如下:

for each incoming flit in ranked_order:
    if productive_output_port is free:
        assign flit to productive_port
    else:
        assign flit to any free non-productive_port # 偏转

优势与挑战

  • 优势:显著降低面积和功耗,简化流控制(无需信用机制),能自适应地绕过局部拥塞。
  • 挑战:在高负载下吞吐量下降,数据包可能乱序到达导致目的端需要重组缓冲区,并且需要解决活锁问题(数据包被无限偏转)。

研究表明,在真实工作负载(其网络注入率通常不高)上,无缓冲设计能带来显著的能耗和面积节省,且性能损失较小。


提升无缓冲网络的实用性

基本的无缓冲设计存在活锁和复杂重组缓冲区的问题。本节我们看看如何通过更巧妙的设计来解决这些问题,使其更实用。

解决活锁:黄金数据片机制

保证活锁自由不一定需要全局的、基于年龄的排序。一个更简单的方法是:网络保证每个周期都有一个特定的数据片(称为“黄金数据片”)被优先路由,直到其到达目的地。

实现方式:采用一个静态、轮转的调度表,为每个可能的源节点和请求ID分配一个成为“黄金数据片”的时间窗口。路由器只需识别并优先路由当前周期的黄金数据片,其他数据片则进行随机或简单仲裁。

优化重组缓冲区:与处理器集成

大型重组缓冲区用于应对最坏情况(所有节点向同一目的地发送数据)。我们可以通过两种方式优化:

  1. 乐观注入与重传:发送方乐观地注入数据。如果接收方缓冲区满,则丢弃数据包并记录。当缓冲区有空闲时,接收方主动通知发送方重传,避免了不必要的多次重传。
  2. 利用现有硬件:对于缓存未命中请求,处理器核心已经需要MSHR来跟踪未完成的请求。我们可以将MSHR直接用作网络的重组缓冲区,从而消除额外的专用缓冲。

通过这些优化,我们可以在保持低面积、低功耗优势的同时,简化路由器设计(用更快的置换网络替代复杂的排序网络),并有效管理数据包交付。


折中之道:最小缓冲偏转路由

完全无缓冲的设计在高负载下性能下降较大。本节我们探讨一个折中方案:最小缓冲偏转路由,旨在用最少的缓冲开销换取显著的性能提升。

核心思路是识别并缓冲那些原本会被偏转的数据片,给它们第二次机会,而不是让它们立即去占用网络链路。

关键技术

  1. 侧缓冲区:在路由器中添加一个很小的FIFO缓冲区。当数据片在仲裁中失败即将被偏转时,将其存入侧缓冲区,而不是立即偏转。随后在注入端口空闲时,将其重新注入网络管道。
  2. 双出口端口:允许每个路由器每个周期弹出两个数据片,减少因出口竞争导致的偏转。
  3. 银数据片优先级:在黄金数据片优先级之下,引入一个“银数据片”优先级。每个路由器每周期保证至少一个非黄金数据片(银数据片)不被偏转,减少因分布式随机仲裁导致的不必要偏转。

这些技术协同工作,能有效降低偏转率。研究表明,仅需相当于传统缓冲网络25%的缓冲区大小,最小缓冲偏转路由就能达到相近的性能,同时保持了接近无缓冲网络的低功耗和面积优势。


应用感知的网络调度

前面的优化主要关注网络微观机制。本节我们将视角提升到应用层面,探讨如何通过智能调度来优化整体系统性能。

在共享的片上网络中,不同应用对网络延迟的敏感度不同。简单的、局部化的路由器调度策略(如轮询、基于年龄)可能做出全局次优的决策。

应用感知的全局调度

思路是引入一个全局排名机制。系统周期性地根据应用程序的特性(如请求数量、关键性)为所有运行的应用生成一个优先级排名。这个排名被广播到所有路由器,每个路由器都依据此全局排名来调度数据包。

效果:这类似于在内存控制器中使用的“最短作业优先”思想。网络资源会优先服务于那些能够更快完成或对延迟更敏感的应用,从而提高系统整体的吞吐量和公平性。

利用松弛时间进行调度

一个更精细的概念是数据包的松弛时间——即一个数据片可以被延迟而不影响应用性能的周期数。

核心思想:优先调度松弛时间短(更关键)的数据包,延迟那些松弛时间长(其延迟可被其他未完成请求重叠)的数据包。

如何估算松弛时间:在数据包注入时,根据历史信息预测其访问延迟(例如,通过预测是否为L2缓存命中)以及与其并行的其他未完成请求的延迟,从而估算出其松弛时间。路由器根据数据包头中携带的松弛时间估计值进行调度决策。

结合应用感知排名和松弛时间调度,可以进一步优化网络资源分配,在提高性能的同时改善应用间的公平性。


面向大规模系统的网络拓扑重构

当芯片上集成大量处理单元和加速器时,提供服务质量保证变得重要,但传统的QoS支持会在所有路由器中引入开销。本节介绍一种通过拓扑设计来高效提供QoS的思路。

传统方法是在网络所有路由器中实现复杂的QoS机制。新的思路是:将需要QoS支持的共享资源访问限制在网络的特定区域

核心设计

  1. QoS区域:只在网络的一部分路由器中实现完整的QoS支持。
  2. 高连通性拓扑:采用多跳快速通道等低直径、高连通性的拓扑。这种拓扑允许从网络任何节点到QoS区域有直接或接近直接的路径
  3. 隔离访问:利用高连通性拓扑提供的“快速通道”,不同应用或虚拟机可以隔离地访问QoS区域,彼此路径不重叠,从而自然提供了流量隔离和性能保证。

这种方法通过巧妙的拓扑设计,将复杂的QoS硬件需求限制在局部,同时利用丰富的片上连线资源实现全局的隔离访问,从而以较低的成本实现了强大的服务质量保障。


总结

本节课中我们一起深入探讨了片上网络的设计与优化。我们从回顾负载-延迟曲线和片上网络特点出发,探索了从完全无缓冲的偏转路由,到引入最小缓冲的折中方案,再到应用感知的智能调度和面向QoS的拓扑重构等一系列创新思路。

关键要点包括:

  1. 片上网络的设计需充分利用其低延迟、高带宽特性,同时规避其面积、功耗限制。
  2. 减少或消除缓冲区是降低面积功耗的有效途径,但需妥善解决活锁、重组和性能问题。
  3. 最小缓冲智能偏转仲裁可以在缓冲与无缓冲之间取得良好平衡。
  4. 应用语义(如关键性、松弛时间)引入网络调度,可以显著提升系统整体性能与公平性。
  5. 通过创新的拓扑设计,可以在不显著增加硬件复杂度的情况下,实现诸如服务质量保证等高级功能。

这些研究体现了计算机架构中一个重要的方法论:随着工作负载、技术和架构范式的变化,重新审视和挑战那些被视为理所当然的设计选择,往往能带来显著的收益。

18:新兴存储技术 (Spring 2025) 🚀

概述

在本节课中,我们将探讨新兴存储技术。我们将看到一些案例研究,以及我们和其他研究者为启用这些技术而开发的方法。正如我们过去讨论过的,DRAM存在许多可扩展性问题,我们需要通过智能内存控制器等技术使其更智能,以突破极限。但同时,思考其他可能替代DRAM或其他存储模块的技术也是有益的。

从闪存到新兴技术

闪存在过去也曾是新兴存储技术。在20世纪80年代,人们努力使其工作,当时也有很多人对这种架构持悲观态度。但在某个时刻,所有的研发努力都得到了回报。现在,闪存可以说是我们口袋里的智能手机能够存在的关键原因之一。否则,我们如何携带一个装有旋转硬盘的手机呢?下周我们将详细讨论闪存和固态硬盘,敬请期待。今天,我们将关注更多仍处于“新兴”阶段的存储技术。

电荷存储的局限性

让我们快速回顾一下电荷存储的局限性。

  • 在闪存中,我们有一个浮栅,需要控制其中的电荷。
  • 在DRAM中,我们有一个存储电荷的电容器。

本质上,存在晶体管漏电问题。在DRAM中,需要频繁刷新。在闪存中,最终也需要刷新。例如,如果你将数据存储在闪存中几年不碰它,很可能无法完好地恢复数据。幸运的是,我们的闪存现在被集成到SSD中,SSD使用相当智能的控制器来使闪存正常工作。

可靠的感测变得困难,因为电荷存储单元的尺寸减小了。当你想缩小尺寸时,它会变得越来越小,最终你将无法可靠地存储电荷和感测电荷。这就是为什么这些架构从根本上存在可扩展性问题。

解决方案概览

我们讨论过不同的解决方案。

  • 解决方案一:新的内存架构,通过以内存为中心的系统设计来克服内存缺点,例如新颖的内存架构、接口、功能、更好的浪费管理等。
  • 解决方案二:新兴存储技术。存在一些新兴的电阻式存储技术,它们似乎比DRAM更具可扩展性,并且也是非易失性的。

DRAM是易失性的,需要电力来保持电荷并需要刷新。但这些电阻式存储器是非易失性的,不需要电力来保持数据。

新兴电阻式存储技术示例

以下是几个例子。

  • 相变存储器:通过改变材料相态来存储数据。数据通过检测材料电阻来读取。PCM早在20世纪60年代就已开发,并已用于可重写CD。后来,人们意识到材料的相态变化也会导致电阻变化,并可以转换为电流,从而设计出感测电路来解码材料状态。
  • 自旋转移矩磁阻随机存取存储器:通过注入电流来改变磁极方向,电阻由磁极方向决定。
  • 忆阻器:通过注入电流来改变原子结构,电阻由原子距离决定。

这些技术提供了许多优点,因为电阻是非易失性的,并且从根本上更容易缩小尺寸。但同时,当你想注入电流来改变这些状态时,需要消耗大量能量和产生高温,这会导致许多问题。

相变存储器详解

PCM是一种电阻式存储器。在高电阻状态下可以编码为0,在低电阻状态下可以编码为1。PCM可以在状态之间可靠且快速地切换,但这个“快速”并非像DRAM那样快。

PCM工作原理

写入操作需要通过电流注入来改变相态。有两个基本操作:置位和复位。

  • 置位操作:需要持续电流,将单元加热到结晶温度以上,并保持较长时间。
  • 复位操作:注入相当高的电流,将单元加热到熔化温度以上,然后快速淬火,从而得到非晶态。
  • 读取操作:通过检测材料电阻来检测相态。

有趣的是,在结晶态时,电阻约为1k到10k欧姆;在非晶态时,电阻很高,约为1兆欧或10兆欧。这两种电阻状态之间存在明显差异,这对于可靠地从这些存储器中读取数据非常重要。

PCM的优势与挑战

PCM提供了一些机会。

  • 比DRAM和闪存具有更好的可扩展性。
  • 需要电流脉冲,其大小随特征尺寸线性缩放。
  • 预计可扩展到9纳米。
  • 可以比DRAM更密集,因为它可以更好地缩小尺寸,并且由于电阻范围大,可以在每个单元中存储多个比特。

当然,这里存在权衡。当你增加每个单元的比特数时,电阻值之间的差异就不那么大了。如果你想把电阻窗口分成16个状态来存储4个比特,那么差异就不大了,问题在于你的感测电路需要相当精确才能检测出差异。

PCM也是非易失性的,可以在85摄氏度下保持数据超过10年,无需刷新,静态功耗低。

PCM的多级单元

我们可以将这个电阻值窗口分割。例如,分割成四个区域,就可以在每个单元中存储2个比特。当然,与单级单元PCM相比,你需要花费更多的时间和能量来读写。

PCM的特性调查

我们想了解PCM的特性,并对2003年至2008年的原型进行了调查。我们查看了所有这些数字并整理成表格。例如,有些原型的置位时间为100纳秒,置位电流为200微安,复位时间为50纳秒,复位电流为600微安。此外,还有写入耐久性。

耐久性本质上是指你可以可靠地向存储单元写入多少次。例如,如果你向PCM单元写入超过10^7次,那么你的单元可能会失效。有时单元并非完全失效,但变得不可靠,这意味着为了避免错误,你需要更强大的纠错码,这会导致很多问题。耐久性实际上是所有新兴存储技术的不幸问题之一。

从调查来看,PCM的耐久性范围从104到109不等。在进行新兴存储技术研究时,你并不确切知道这些数字,因为人们使用了不同的方法,设计了不同的感测电路。因此,使用参数范围进行研究总是一个好主意。

PCM在系统中的定位

我们可以在系统中考虑PCM的位置:主存、缓存和存储。我们想考虑将其作为主存。PCM的延迟接近DRAM,但不如DRAM好。PCM的读取延迟约为50纳秒,是DRAM的4倍,但比非易失性闪存快。写入延迟约为1150纳秒,是DRAM的12倍,但低于非易失性闪存。动态能耗也高于DRAM,读取动态能耗是DRAM的2倍,写入动态能耗是DRAM的43倍。耐久性大约是DRAM的10^-8倍。单元尺寸比DRAM大,但可以通过特征尺寸和多级单元技术更好地扩展。

这里的要点是,PCM不太可能取代DRAM,因为它存在许多缺点。让我们再次总结一下PCM相对于DRAM的优缺点。

  • 优点:更好的技术可扩展性、非易失性、持久内存、低静态功耗。
  • 缺点:更高的延迟、更高的动态能耗、更低的耐久性、可靠性问题。

在启用PCM作为DRAM替代品方面存在挑战,因此需要减轻PCM的缺点,并找到在系统中放置PCM的正确方式。

用PCM完全替代DRAM的研究

在这项工作中,我们考虑完全用PCM替代DRAM。我们考虑了从调查中得出的所有数字:延迟是DRAM的4倍,写入延迟是12倍,能耗读取是2倍,写入是43倍,耐久性是10^8倍。

我们进行了模拟,在一个四核、4MB L2缓存系统中用PCM替换了DRAM。PCM的组织方式与DRAM相同。长话短说,结果并不好。执行时间延长了约60%,能耗增加了2.2倍,平均寿命为500小时。有些应用的性能开销甚至更大。

结果不理想,这在预料之中。现在的问题是,我们能否克服这些问题?我们对此进行了研究。

改进思路一:多行窄行缓冲器

在DRAM中,我们有一个大的感测放大器,因为读取操作是破坏性的,需要这个感测放大器进行电荷放大以确保正确。但PCM没有这个问题,所以你不需要感测放大器像整个阵列的行那么大。你可以使用更小尺寸的感测放大器,从而节省面积预算,并用这些预算来添加一些锁存器和缓冲器。这些锁存器和缓冲器本质上可以隐藏延迟,充当缓存。

改进思路二:细粒度写入

在DRAM中,写入时首先写入感测放大器,然后需要等待该值在DRAM行中恢复。但在PCM中,你不需要更新整行。你可以只更新发生变化的部分,也许只更新一个字节或两个字。这可以显著减少写入周期,从而有助于耐久性问题。

通过实施这两个想法,你可以获得性能改进,但执行时间仍然增加了20%,能耗相当,平均寿命更长。但这里有一些注意事项:最坏情况下的寿命要短得多,因此没有保证;密集型应用会遭受较大的性能和能耗损失;我们甚至考虑的是乐观的PCM参数吗?事实证明,其中一些参数实际上非常乐观。

如果你想完全用PCM替换主存,你不会获得性能改进,反而会得到性能下降,并且还存在许多其他问题。

混合主存架构

现在让我们看看另一种方法:混合主存。记住我们过去讨论过的图片:你有CPU和不同的控制器,例如DRAM控制器控制快速且耐久的DRAM,但它容量小、有漏电、易失且成本高。另一种存储技术如相变存储器,容量大、非易失且成本低,但速度慢、耐久性差且动态能耗高。

通过将两者结合使用,希望你能从多种技术中获得最佳效果。这就是使用混合内存的想法,但这当然也带来许多挑战,例如如何映射数据,如何决定哪些数据应该去DRAM,哪些应该去PCM,如何迁移它们。或者,你是否将其架构为缓存,即DRAM作为PCM的缓存?或者DRAM是内存地址空间的一部分,这样DRAM和相变存储器共同构成整个内存地址空间。如果是这样,程序员也需要处理它,例如使用一些指令将数据存储在DRAM部分,或使用一些指令将数据存储在PCM部分。编译器也可以提供帮助,但也可以作为缓存,即DRAM可以作为PCM的缓存。

目标是通过多种存储技术提供多种指标的最佳组合。当然,正如我所说,有许多问题或挑战需要解决,例如我们是设计为缓存还是主存?数据移动或管理的粒度应该是细粒度还是粗粒度?应该是硬件控制还是软硬件协同?何时迁移数据?因为迁移总是有成本的,所以本质上何时调度它?以及如何设计可扩展且高效的大型缓存?

一个选择是我们可以将DRAM视为PCM的缓存。PCM是主存,DRAM缓存内存行和块。好处是减少了DRAM缓存命中的延迟,并且还进行写入过滤,从而可以改善或消除PCM的写入耐久性问题。内存控制器硬件应管理DRAM缓存,这消除了系统软件开销。但至少存在三个问题:哪些数据应放置在DRAM中,哪些应保留在PCM中?如何智能地管理你的缓存?数据在它们之间移动的粒度是多少?以及如何设计低成本的硬件来管理DRAM缓存?

这里有两个想法方向:数据放置的局部性,以及芯片标签和粒度。

DRAM作为PCM缓存的研究

目标如前所述,是实现DRAM和PCM或NVM的最佳结合。它最大限度地减少了DRAM的用量,同时不牺牲性能和耐久性。DRAM作为缓存来容忍PCM的延迟和写入带宽。当然,PCM作为主存以良好的成本和功耗提供大容量。

这里的关键是如何管理DRAM缓冲区。有一些技术,比如写入过滤技术,你可以采用延迟写入的方法。当你从闪存或硬盘驱动器带来数据时,不要立即写入PCM,而是首先将它们写入这个DRAM缓冲区作为延迟写入页。然后,如果你意识到它们被需要,或者因为你在该DRAM缓冲区中捕获了一些写入而变脏,你需要将它们写回PCM。但在某些时候,你甚至不需要将它们写回PCM,只需将它们写回闪存或硬盘驱动器。你也可以进行页面绕过,即丢弃使用率低的页面。

这项研究进行了一些分析。他们考虑了一个16核系统,具有8GB DRAM,主存访问延迟为320个周期,还有延迟为2毫秒的硬盘驱动器,以及延迟为32微秒的闪存,并假设闪存命中率为99%。他们的假设是PCM的密度是DRAM的4倍,这考虑了PCM的优良特性。PCM比DRAM慢4倍,块大小等于PCM页大小。

结果显示,拥有32GB PCM加上1GB DRAM作为缓存,你实际上可以获得性能优势,平均性能非常接近32GB DRAM基线。这很好,这正是我们从混合内存系统中所期望的。

数据放置与混合内存管理

让我们在更高抽象层次上思考数据放置和混合内存。本质上,你有这些核心缓存和内存控制器,以及多个通道。一个通道可以是内存A,它快速但容量小;通道B可以是内存B,它容量大但速度慢。想法是,你希望将你的页面分配到这些不同的内存中。

在某个时刻,你想将页面2迁移到通道A,因为你想要更快的访问。但问题是,这会导致一些数据移动。此外,你现在在通道A上也有一些争用。因此,如果你这样实现,也可能以降低带宽为代价。

为了最大化系统性能,我们将每个页面放置在哪个内存中?但同时,你还应该回答其他问题,例如你不应该让通道B闲置,并可能导致负载均衡问题。

基于访问模式的数据放置

DRAM和PCM之间的数据放置,其想法是如何表征数据访问模式并指导混合内存中的数据放置。

有些工作负载具有流式访问。当你有流式访问时,它们在PCM中可以和在DRAM中一样快。然而,对于随机访问,它们在DRAM中要快得多。因为对于随机访问,你总是需要支付访问延迟。但当你有流式访问时,希望你可以通过利用带宽来隐藏这些延迟。因此,对于具有流式访问的应用程序,你可以将它们映射到PCM。对于随机访问,你可以将它们映射到DRAM。这就是这项工作的想法:将具有一些重用性的随机访问数据映射到DRAM,将流式数据映射到PCM。

这里有更多细节:行缓冲器同时存在于DRAM和PCM中,因此行命中的延迟在DRAM和PCM中是相似的。然而,行未命中的延迟在DRAM中很小,在PCM中很大。这就是为什么行冲突对PCM来说代价高昂,所以你不喜欢随机访问。而当你有流式访问时,你实际上可以从行缓冲器局部性中受益,所以你可以将它们映射到DRAM或PCM,两者都可以工作。但当你有随机访问时,很可能会有很多行冲突,这就是为什么最好使用具有较低未命中延迟的内存。

因此,将数据放置在DRAM中,这些数据很可能在行缓冲器中未命中,行缓冲器局部性低,这意味着在DRAM中P很小,并且被多次重用,这值得移动成本,并且在DRAM中有空间。如果满足这两个标准,你实际上可以移动或迁移该数据到DRAM。

现有解决方案的一些弱点是,它们都是启发式的,只考虑了内存访问行为的有限部分,并没有直接捕捉数据放置决策对整体系统性能的影响。

大型DRAM缓存的设计挑战

这是一个问题:当我们有大型DRAM缓存时,它需要大量的元数据,例如基于标签的块信息存储。我们如何设计高效的DRAM缓存?当你发出加载请求时,你首先检查数据是否可以从这个DRAM缓存中获取,所以你需要检查元数据,然后访问它。如果命中,你可以从DRAM获取数据;否则,你应该去PCM。所以想法是我们如何存储标签。

一个想法是,我们可以将标签存储在DRAM中与数据相同的行中。在你的DRAM行中,你有缓存块,也有标签。好处是你不需要片上标签存储开销,因为标签阵列需要相当高,你可以在家做一些计算。但本质上,这是因为你的DRAM缓存在这里相当大,然后你需要相当大的标签元数据。所以你不需要为此付费,但也有一些缺点:缓存命中只有在DRAM访问之后才能确定,这相当耗时;缓存命中需要两次DRAM访问,所以你需要访问DRAM来获取标签,然后在检查标签之后,如果命中,你需要再次访问并读取块。所以这是一个问题。

第二个想法是,我们仍然可以将所有元数据存储在DRAM中,但我们可以减少元数据存储开销。我们还可以在片上SRAM中缓存一部分频繁访问的元数据。那些你更频繁访问的块,你可以将它们的元数据缓存在SRAM中。这样实际上可以降低访问元数据的成本,现在你可能同时拥有快速的元数据访问和较低的存储开销。

第三个想法可以是动态数据传输粒度。有些应用程序受益于缓存更多数据,所以它们可能具有良好的空间局部性,你可以考虑它们具有较大的缓存块。其他应用程序则不然,大粒度会浪费带宽并降低缓存利用率。所以第三个想法是,我们可以通过成本效益分析来确定基本数据和缓存块大小的简单动态缓存粒度策略。然后,他们可以将主存分组为行集,例如,不同的样本行集遵循不同的固定缓存粒度。其余的内存遵循最佳情况。

结合所有这些技术,你实际上可以获得非常接近理想情况的性能,但复杂度要低得多,因为存储开销更低。在能效方面,你可以获得更好的能效,因为你没有减少太多性能,但由于存储更低,你降低了很多成本和功耗。

DRAM缓存方面也有很多最新的选择。人们也做了很多工作,我们和其他人都做了很多工作,这里可以看到许多不同的研究,比如不同的方案。这篇论文实际上对它们进行了很好的比较,比较了所有这些方案或技术,例如它们如何处理DRAM缓存命中、DRAM缓存未命中、如何处理替换流量、替换决策等等。如果你感兴趣,你也可以查看这篇论文。

新兴存储技术的其他机遇

现在我想看看新兴存储技术的其他机遇。这里我将列出其中一些基本机遇,我们将涵盖其中几个。

机遇一:内存与存储的融合

目前它们没有融合,我们有DRAM和SSD或硬盘驱动器。潜在地,由于新兴存储技术是非易失性的,你实际上可以将它们融合,并使用单一接口来管理所有数据。

你还可以考虑新的应用程序,例如超快速检查点和恢复,这对于持久内存非常重要。通过减少数据丢失,你可以拥有更健壮的系统设计。

你也可以考虑与内存紧密耦合的处理,例如使用内存中处理技术。我记得我们在关于使用内存的第一讲中,我不再赘述,例如我们可以使用DRAM做很多计算,比如按位操作和行克隆,同样的DRAM。

人们已经证明,例如,新兴存储技术也可以同时进行按位操作和行克隆。有趣的是,这些架构还可以执行一些使用DRAM处理无法轻易完成的操作。这是因为它们的构建方式,例如用于数组操作的内存交叉阵列。

这些架构设计成交叉阵列方式,例如一些新兴非易失性存储技术具有交叉阵列结构,如忆阻器、电阻式RAM、相变存储器、自旋转移矩磁阻随机存取存储器。这些交叉阵列可用于执行点积操作,利用模拟计算能力。通过执行点积操作,潜在地你可以进行矩阵向量乘法,潜在地你可以进行神经网络卷积神经网络等等。它们可以使用基尔霍夫定律对多个数据片段进行操作。

计算是在模拟域中进行的,在交叉阵列内部,所以你需要前置电路,如数模转换和模数转换来处理输入和输出。

这里有一个例子:你可以看到我们这些数模转换器,这里是你的交叉阵列区域。这是一个采样保持器,你可以把它想象成一个锁存器,你在这里采样你的模拟数据,这里有一个模数转换器。

本质上,这里发生的是,你在这里施加的电压,可以基于你的输入向量确定,所以你有一些输入值,如0和1,你使用这些数模转换器将它们映射到一些电压值。这些电压会导致一些电流流经这条线,取决于这个电阻的电导。然后,这条位线上的电流值将是这些电流值的总和,基于基尔霍夫定律。这就是为什么你实际上可以看到这实际上是一种点积,所以你可以使用这个模拟视角,然后将其转换为数字,以轻松进行点积操作。人们已经做到了。

所以本质上,你可以认为,例如,你有这个输入数组,这是你的矩阵,你的权重矩阵。你可以将你的权重矩阵作为电阻值放在你的交叉阵列中。然后你将这些值输入到输入电压中,然后你得到输出。

这可以实现,是的。当然,你需要支付编程能量、编程延迟,例如编程自旋转移矩磁阻随机存取存储器或忆阻器。这也是耐久性问题的一部分,例如,人们已经证明,使用忆阻器阵列进行推理相当好,你只需编程一次电阻,然后进行大量推理。但一旦你想进行需要更新权重的训练,那么你的交叉阵列将很快磨损。

这实际上首先在...中提出,但你知道,人们已经将其用于加速卷积神经网络,例如在ISCA论文中。请记住那篇论文非常清楚,因为ISCA论文的第一作者是我在2011年的第一位导师。当时我们一起研究自适应路由算法或互连网络,但我们稍后也会看到一些关于这方面的讲座。

所以你可以考虑如何将这个想法用于卷积。这里有一个卷积的例子,本质上你有输入特征图,有一个窗口在你的输入上移动或滑动。你在每一步进行一些计算,然后计算输出特征图。当然,你还需要进行一些填充,以确保你总是有一些数据,因为有些区域你的窗口可能会超出你的输入特征。

所以你可以考虑它,然后你可以用它来进行卷积神经网络的操作。但在卷积神经网络中,你不仅要做点积,还有其他层,如非线性层,如ReLU。所以,在你的芯片中,本质上,你可以有这个基于非易失性存储器的交叉阵列来进行这些点积,你还需要做一些非线性函数阵列,你可以将其视为近内存处理引擎,它们也集成在这里,以便加速非线性神经元操作。

通过这样,他们基本上可以提高性能。我们也在这个主题上做了工作,比如GenPx。John明天在我们的基因组学讲座中可能也会稍微讨论一下这项工作。

所以,在这项工作中,还有Swordfish,我不确定我们明天是否会讨论它,但它也将成为你阅读的一部分。这也很有趣,因为人们已经大量使用忆阻器来进行神经网络,基本上用于进行卷积神经网络或深度神经网络等应用。

但问题是这些内存资源不理想。因此,每当你进行计算时,都会有一些错误。这项工作实际上试图研究这些错误如何使你的假设出错,以及这些错误如何导致使用这些内存时的精度损失或性能损失。了解所有这些问题是很好的,这样你就可以设计技术来克服它们。

这是提到的ISCA论文,以及该领域的其他论文。人们已经开发出来。这也是历史领域和卷积讲座的一部分,如果你感兴趣,这是一个美丽的主题,你也可以从Mos教授的讲座中查看。

机遇二:内存与存储的统一接口

我们讨论了内存与存储的融合,让我们更深入地探讨一下。你可以有一个统一的接口。

这是一个传统系统,本质上,你有内存和存储。处理器通过加载和存储指令访问DRAM,然而要访问这些,我们需要进入I/O子系统,基本上I/O块栈,你需要打开文件,读取I/O,然后更新等等。

这表明,每当你想要访问存储时,你都需要支付操作系统开销,这在早期的计算机系统中并非如此,当时我们使用磁芯存储器。那是人们用作主存和存储的一切,虽然不是很大的内存,但那是人们拥有的唯一内存。但后来,随着技术的进步,人们开发了DRAM,然后这些接口实际上逐渐分道扬镳。所以现在我们有不同的接口来访问主存和访问存储。

我们知道硬盘驱动器是非易失性的,但速度慢,并且是块可寻址的。我们知道DRAM是易失性的,速度快,并且是字节可寻址的。

同时,我们也知道有一些非易失性尝试内存,如PCM或自旋转移矩磁阻随机存取存储器,它们相对较快,至少与闪存或硬盘驱动器相比,并且它们是字节可寻址的,并且是非易失性的。所以现在的问题是,我们能否使用这些非易失性存储器来合并内存和存储,以便我们在它们之间拥有一个共享接口?

这就是两级内存和存储模型的想法。传统的两级存储模型对于非易失性存储器来说是一个瓶颈。原因是,当你考虑这个例子,比如两级存储。你有这个处理器和缓存,每当你想要访问你的存储时,你需要进入操作系统和文件系统进程。

但这种开销可能还可以,因为硬盘驱动器本身就很慢,所以你可以支付这种开销,因为音频块延迟可能不会比访问磁盘的延迟长。但事实证明,对于SSD来说,情况并非如此,这就是为什么人们也开发了新兴接口来访问SSD,比如我们下周将要学习的NVMe接口。

然而,如果我们考虑这里我们有PCM,那么PCM比SSD快得多,例如比闪存和硬盘驱动器快。所以现在你需要支付很多延迟来通过操作系统,然后你访问该PCM的延迟相当快。所以显然这不是一个好的决定,所以现在你可以看到,当我们使用非易失性存储器作为存储时,这种两级设计是次优的。

所以目标是我们希望在单个单元中统一内存和存储管理,以消除在定位、传输和转换数据方面的浪费。这当然可以提高能源和性能,并且简化了编程模型。编程模型不需要处理不同的内存类别、是否持久化或不同的接口。一切只是统一的。这自然提供了编程的便利性。

通过非易失性存储器,我们可以提供持久内存,正如我们讨论的,这是直接在非易失性存储器内存中操作持久数据的机会。

这里有一个例子。你有不同的内存模块,比如DRAM、闪存、非易失性存储器和硬盘驱动器,比如不同的存储技术,它们都在硬件中,但你正在统一接口,作为一个持久内存管理器。

你通过加载和存储访问这个持久内存管理器,这个管理器将访问DRAM、闪存、非易失性存储器和硬盘驱动器。所以处理器最终只看到这些加载和存储,还有一些来自软件和运行时的提示,你可以使用这些提示在DRAM、闪存、非易失性存储器和硬盘驱动器之间智能地映射你的数据。

所以这不是一个非常容易的例子,比如持久对象,有一个文件,你将其分配为持久对象,然后你可以根据其特性将该文件作为持久对象映射到闪存、非易失性存储器或硬盘驱动器。

持久内存管理器使用访问和提示信息来分配、定位、迁移和访问异构设备阵列中的数据。持久内存管理器暴露一个加载存储接口来访问持久数据,这是这里的关键区别,应用程序可以直接访问持久内存,没有持久数据的转换、翻译和定位开销。

它管理数据放置、位置、持久性和安全性,以获得多种存储形式的最佳效果。它还管理元数据存储和检索。这当然会导致需要管理的开销,并为系统软件暴露接口、钩子和接口。

为了在异构设备之间进行高效的数据映射,给你一些思路:持久内存暴露一个大的持久地址空间,但它可能使用许多不同的设备来实现这一目标,从快速、低容量、易失性的DRAM到慢速、高容量、非易失性的硬盘驱动器或闪存。你还有介于两者之间的其他非易失性存储器设备,所以性能和能源可以从这些设备之间的良好数据放置中受益。这就是为什么你真的需要从这些提示中学习,并很好地映射你的数据,利用每个设备的优势,并尽可能避免其弱点。

例如,考虑两个重要的应用程序特征:局部性和持久性。这里有一个例子:你有像数据库应用程序这样的应用程序,比如列存储中的列,它们只是偶尔被扫描,放置在闪存上。显然,它的局部性较差,因为不频繁,并且你使用扫描。但它是持久的,因为它是数据库的一部分。所以你可以将其特征分类为这个节点。同时,可能有一些应用程序,如内容分发网络的频繁更新索引,它具有更多的局部性并且是临时的,所以你需要将其放置在这里,这就是你实际上可以管理存储、持久内存的方式。

我们使用这些系统评估了这个想法。我们考虑硬盘驱动器基线,即具有易失性DRAM内存和持久硬盘驱动器存储的传统系统。当然,这具有操作系统和文件系统代码以及缓冲的开销,以及硬盘驱动器访问延迟。

我们还考虑了非易失性存储器基线,它与硬盘驱动器基线完全相同,但硬盘驱动器被非易失性存储器取代。好处是,由于非易失性存储器访问速度更快,但仍然存在两级存储模型的问题。不幸的是,当时我们还没有SSD基线,这也是这项工作的一个局限性。

持久内存,我们只使用非易失性存储器,并且没有DRAM来确保全系统持久性。所有数据访问都使用加载和存储,所以本质上没有两级存储模型。它不会在系统调用上浪费时间,数据直接在非易失性存储器设备上操作。

所以你可以看到,从硬盘驱动器两级到非易失性存储器两级,我们获得了24倍的加速,这相当好,因为非易失性存储器相当快。从非易失性存储器两级到持久内存,我们获得了大约5倍的加速。这也显示了这种想法的有效性。我相信闪存在这里某个位置,因为闪存的延迟比硬盘驱动器好得多。例如,访问硬盘驱动器大约需要10毫秒,但考虑到所有其他延迟,访问SSD的读取延迟可能在200微秒左右,所以使用闪存访问速度要快几个数量级。

在能源方面,当你使用持久内存时,你也可以观察到相当大的加速。如果你想了解更多,你也可以查看这篇论文以获取更多细节。

持久内存的挑战

这些是我们在这里讨论的挑战和机遇,例如将内存和存储结合在一个统一接口中以访问所有数据。所以人们工业界已经在这方面做了工作,这是我今天多次展示的幻灯片,但这些Optane内存。人们实际上已经展示了如何将这些Optane内存作为系统的一个DIMM添加,你可以看到它实际上就像DDR内存条。然后他们将其用作持久内存,人们开发了库,可以使用这个Optane内存作为持久内存,并报告了与将DRAM作为非持久内存和SSD作为持久内存相比的结果。

人们还添加了处理数据引擎,我不确定为什么在这里,但无论如何。

持久内存存在许多挑战,我想在这里指出的一个关键挑战是:如果所有内存都是持久的,如何确保系统数据的一致性。

有两个极端:程序员透明,让系统处理。你不想让程序员做任何事情,你想提供相当的程序便利性,所以系统应该处理它。这对程序员来说非常好,但当然很难实现。另一个极端是程序员处理。

我认为好的解决方案可能介于两者之间。但在研究中,有时采取极端行动也是好的,因为你想看看你将面临哪些挑战,以及当你想突破界限时你将如何应对它们。这就是为什么在新兴存储技术领域进行研究也很有趣。

这是一个例子,比如崩溃一致性问题,这实际上是众所周知的。例如,你想向链表添加一个节点。有一个指向下一个节点的链接,也有一个指向前一个节点的链接,你总是希望以原子方式完成。因为如果你不这样做并且发生崩溃,那么你可能会得到这种链表,即指向前一个节点的连接存在,但指向下一个节点的连接不存在。然后你的链表基本上就断了。这实际上是一个已知问题,并导致内存状态不一致。

因此,有相应的解决方案,基本上有明确的接口来管理一致性。本质上,你需要以原子方式完成,所以有一些原子开始和结束,你在这里插入你的代码以确保所有这些操作将以原子方式发生,要么两者都发生,要么都不发生。这是你在原子写入时需要确保的关键。

但当然,这限制了非易失性存储器的采用,因为你必须重写代码,明确区分易失性和非易失性数据。这就是为什么将负担放在程序员和编译器上并不是一个好主意。

因此,也有一些引用,我稍后不会展示,如果你感兴趣,可以稍后查看。但本质上,有一些。让我看看。是的,所以你需要使用事务内存。为了确保你以原子方式完成。

这里还有一些列表,在库访问中,你需要进行事务内存获取,以确保获取以原子方式完成。但你基本上已经明白了,所以它应该像手动声明持久组件。有时你需要新的实现,第三方代码可能处于不一致状态。所以你需要将它们放在事务内存访问中。但本质上,你需要从这些库和代码中受益以提供一致的代码。

但我们希望有一种方法,比如软件透明的持久内存一致性。我们希望在持久内存中完全软件透明。

关键想法是,我们定期检查状态,并在崩溃发生时恢复到先前的检查点。这是一种基于硬件的检查点机制,以多种粒度进行检查点,以减少检查点延迟和元数据开销,并且我们重叠检查点和执行以减少检查点延迟,并且它适应DRAM和非易失性存储器的特性。

我们在这方面做了很多工作,这并不容易,也不容易。我不确定我们是否真的应该推荐人们使用它,因为它给系统增加了很多复杂性。当然,在这项工作中,我们还考虑了一个单一节点,你只需要处理你的内存和存储,但假设你正在考虑一个分布式系统,并且你有来自网络的数据,例如你的网卡。那么你将如何检查点,你知道,人们也在研究分布式系统中的检查点,比如在数据库中。

但你可以想象,检查点会导致多少开销,比如所有检查点也会导致大量数据移动和能源消耗。这就是为什么在完全放在程序员身上和完全程序透明之间找到一个中间解决方案可能是最好的方法。

但我们也得到了很好的结果,比如我们的性能在理想化的具有零成本一致性的DRAM的5%、4.9%以内。

我们还做了关于如何重叠检查点和执行的工作,因为如果你这样做,然后花时间检查点,然后运行和检查点,那是相当愚蠢的,因为你为检查点增加了很多开销。所以想法是,你希望通过运行其他线程或其他什么来重叠这些检查点。所以本质上,你应该能够很好地调度它们,以便尽可能多地重叠检查点和执行。

是的,如果你想了解更多,你可以查看这篇论文,它也将成为你作业中的阅读材料之一。

但持久内存中还有另一个关键挑战,比如利用持久性的程序。人们在这方面做了很多工作,如果你有兴趣了解更多,你也可以查看这些论文。

安全与隐私问题

现在我们谈到安全和数据隐私问题,你的同事实际上提到了。本质上,在非易失性存储器中,我们存在这些安全和隐私问题。首先,由于耐久性问题,我们存在这些磨损攻击。

人们实际上可以设计攻击,导致你的芯片快速磨损,例如。例如,如果你在这里攻击,例如,发出或发出大量写入到你的PCM内存。这会导致,你知道,该芯片的磨损。当然,有一些技术,如磨损均衡,人们一直在使用,试图限制写入次数,例如,在你的PCM中。

假设耐久性是108,如果你只向一个单元写入108次,那么该单元将会失效。但如果你平衡你的写入,你使用一些技术,如磨损均衡,你基本上尝试在整个内存阵列中以平衡的方式写入。那么希望你可以实际上容忍更多的写入。但是,当你设计攻击技术时,攻击者实际上也可能足够聪明或足够有创造力,并试图,我不知道,欺骗你的磨损均衡技术,从而本质上导致大量写入到你的单元。

在混合内存中,你也可能存在性能攻击,例如,人们也在这方面做了工作。你的同事提到的另一件事是,断电后数据不会被擦除。在DRAM中,当你关闭电源时,你的数据被清除,除非你想进行这种冷启动攻击,并且你知道将你的DRAM冷却很多,但一般来说,当你关闭电源时,事情就完成了。但在非易失性存储器中,情况并非如此,你可能在这里存在一些隐私问题,需要研究。

总结与未来展望

让我们总结一下,有什么问题吗?我在一些幻灯片上有点快,但我试图尽可能传达想法。希望你们从这次讲座中获得了一些很好的见解。

新兴技术的未来是光明的,无论底层技术和上层问题需求存在何种挑战。它可以实现数量级的改进以及计算系统中的新应用程序。

然而,我们必须像我们一直讨论的那样,跨堆栈思考,并设计启用系统。这里有一个很好的例子:你有一个新设备,这个新兴存储技术。你可以设计不同的技术,你知道,就像我们已经观察到的那样,比如微架构软件技术。

人们也在算法设计方面做了工作,例如,如何改变你的算法以减少写入次数。当你设计算法时,通常你根本不会考虑这一点,比如使用DRAM,你从未想过,好吧,我有多少次写入,但有了这个新的角度,比如计算机科学也需要意识到,从事理论计算机科学工作的人说,这些算法也应该考虑写入次数,你知道,人们也在那个方向上做了有趣的工作。当然,如果有疑问,请参考闪存,一个至少二十年来备受怀疑的新兴技术。

但现在你可以看到,闪存无处不在,我的意思是,在我们所有人的口袋里。下周我们还将学习很多关于闪存的知识。

在这个领域有许多研究和设计机会,比如实现完全持久内存,正如我们也讨论过的,使用基于非易失性存储器的内存进行计算,混合内存系统,持久内存中的安全和隐私问题,可靠性和耐久性问题,非易失性存储器的虚拟内存系统,以及例如,虚拟块接口。

这也是一个有趣的思考点。虚拟内存出现的原因是,基本上你的主存不够大。这实际上使程序员的生活相当困难,你知道,他们想要设计程序的方式,所以人们开发了这个虚拟内存的绝妙想法,基本上给人一种更大内存的错觉。

但使用这种非易失性存储器,你实际上可以拥有相当大的内存,比如几十TB、几百TB。所以我们需要重新思考所有这些事情,我们真的还需要虚拟内存吗?当你想要研究新兴存储技术时,所有这些事情都相当有趣。

为了克服这些问题,你真的需要重新思考整个堆栈,并重新思考你几十年来所做的所有假设,你知道,这很有见地,并且你知道你可以突破界限,并且你知道你可以用许多开箱即用的解决方案来克服问题。

这个虚拟块接口也是我们做过的一项工作,旨在让你对虚拟内存有不同的思考,如果你感兴趣,可以查看它,或者你可以在Naran的讲座中观看,那是四年前2024年秋季计算机架构讲座之一。

到此,我结束这次讲座。我们提前了五分钟。意见。基督徒。我们可以休息五分钟。

好的,那么明天见,玩得开心。

19:基因组分析入门与加速 🧬

在本节课中,我们将学习基因组分析的基础知识、核心流程以及如何通过算法和硬件协同设计来加速这一过程。基因组分析对于疾病研究、精准医疗和公共卫生等领域至关重要。

概述:什么是基因组? 🧬

基因组决定了生物体的不同性状,例如眼睛颜色和表型。对于真核生物而言,基因组是位于细胞核内的一整套DNA或染色体。人类基因组通常包含23对染色体,这些染色体由核苷酸字母(A、C、T、G)组成。

一个人类基因组有多大?
如果将单个细胞中所有染色体的核苷酸字母写在A4纸上并堆叠起来,其高度大约相当于苏黎世的一座100米高的建筑——安德烈亚斯塔。具体来说,一个单倍体人类基因组包含约32亿个碱基对。

基因组分析的重要性与应用 💡

我们需要更快、可扩展且准确的基因组分析,原因如下:

  • 疾病研究与治疗:揭示与基因组变异相关的疾病,并直接编辑基因组以应对生命科学的基本挑战。
  • 病原体检测:快速识别病原体,以便在疾病爆发早期采取行动。
  • 基因组编辑:精确定位并修改基因组中的特定区域,例如纠正可能导致癌症的功能失调基因。这项技术获得了诺贝尔奖。
  • 大规模分析:对整个人群进行快速基因组分析。
  • 数据安全:基因组数据是高度敏感的隐私信息,需要严格保护以防滥用(例如在保险评估中受到歧视)。

基因组分析的核心流程 🔄

标准的基因组分析流程通常包括以下步骤:

1. 数据生成:测序 📊

首先,我们需要将生物分子(DNA/RNA)转化为人类可读的数字形式。这通过高通量测序机完成,它们将生物分子片段转化为称为“读段”的核苷酸序列。

面临的挑战包括:

  • 读段在基因组中的原始位置信息丢失。
  • 序列可能包含变异或测序错误。
  • 数据量巨大,需要与庞大的参考基因组空间进行比较。

目前有多种测序技术,我们将重点介绍纳米孔测序技术

纳米孔测序技术简介

纳米孔测序仪是一种便携式设备。其核心原理是:当带负电的DNA单链在电压驱动下穿过一个纳米孔时,会 disrupt 孔内的离子电流。不同碱基通过时会产生独特的电流信号变化,通过检测这些信号即可实现测序。

该技术的优势包括:

  • 长读长:可产生长达数百万碱基的读段。
  • 便携性与实时分析:支持在数据生成的同时进行实时分析,并能根据初步分析结果决定是否继续测序当前分子,从而节省时间和成本。

测序后,我们得到原始电信号数据。传统方法会通过一个称为“碱基识别”的步骤(通常使用复杂的神经网络)将电信号翻译成A/C/T/G序列。但这个过程计算成本高。新兴研究方向是直接分析原始信号,避免翻译步骤,以提高效率和可扩展性。

2. 数据比对:解决“生命拼图” 🧩

获得读段后,我们需要知道它们来自基因组的哪个位置,这个过程称为读段比对。可以将其理解为解决一个巨大的拼图:读段是拼图碎片,而参考基因组是完整的参考图片。

面临的挑战是:

  • 搜索空间巨大:需要将数百万个短读段定位到包含数十亿字符的参考基因组上。
  • 允许错配:由于个体差异和测序错误,读段与参考基因组之间并非完全匹配。

为了高效解决这个问题,现代比对工具通常采用以下流程:

  1. 种子定位:从读段中提取短子序列(种子),在预先构建的参考基因组哈希索引中快速查找匹配位置,生成候选区域。
  2. 过滤与链化:过滤掉错误的候选匹配,并将相关的匹配连接起来。
  3. 精确比对:在剩余的候选区域上执行计算密集型的动态规划算法,进行精确比对,找出所有差异(如突变、插入、缺失)。

这个流程中,精确比对(动态规划) 是最耗时的步骤。

超越线性参考基因组:图基因组

传统的线性参考基因组存在“参考偏差”,无法代表物种内的所有遗传变异。解决方案是使用图基因组,它将多个个体的基因组变异整合到一个图中,节点代表共有序列,边和替代路径代表变异。这能提供更准确的比对,但计算也更复杂。

3. 下游分析:从比对结果中获取洞见 🔬

根据比对结果,可以进行多种下游分析:

  • 变异检测:通过对比对结果进行“堆叠”和一致性分析,识别个体基因组中的单核苷酸多态性、插入、缺失等变异。
  • 宏基因组学:当样本中包含未知生物的混合DNA时(如环境样本、肠道菌群),目标是从读段中识别出所有存在的生物体。通常通过提取读段中的k-mer(短子序列),并与包含大量物种信息的大型数据库进行比对来实现。

算法与硬件加速 🚀

基因组分析面临两大瓶颈:

  1. 数据生成与处理的速度不匹配:测序仪生成数据很快,但通用计算硬件分析数据较慢。
  2. 巨大的数据移动开销:数据在存储、内存、处理器之间频繁移动,其能耗和延迟远高于实际计算。

解决方案是软硬件协同设计,将计算尽可能靠近数据所在的位置。以下是几个研究案例:

案例1:JAM-PiP(内存处理加速)

目标:整合碱基识别和读段比对两个步骤,减少中间数据移动和无效计算。
关键思想

  • 基于数据块的流水线:不是等整个读段的碱基识别完成再做比对,而是识别一小块(数据块)后就立即对其进行质量检查和初步比对。
  • 早期拒绝:根据当前数据块的分析结果,预测该读段是否值得继续分析。如果不值得,则立即停止对该读段的后续处理,节省计算资源。
    效果:通过让两个步骤紧密通信,显著提升了分析速度并降低了能耗。

案例2:GenStore(存储内计算)

目标:将部分计算下推至存储设备(如SSD)内部进行,减少数据向CPU/加速器的移动。
关键思想:在SSD内部添加简单的加速逻辑,对存储其中的读段进行快速过滤(例如,识别与参考基因组完全匹配或完全不匹配的读段)。只将需要复杂计算的读段传输给主机处理器。
效果:通过“在数据所在地进行计算”,大幅减少了数据移动量,提升了整体性能。

案例3:GenASM(近似字符串匹配硬件加速器)

目标:加速读段比对中最耗时的精确比对步骤。
关键思想:协同设计算法与硬件。采用适合硬件实现的Bitap算法进行快速编辑距离计算,并对其进行修改以支持生成具体的比对操作(回溯)。在硬件上使用脉动阵列结构高效执行比特操作,并优化数据布局。
效果:为基因组分析提供了首个灵活的近似字符串匹配硬件加速框架,显著提升了比对的性能。

案例4:RawHash(原始信号直接分析)

目标:绕过耗时的碱基识别步骤,直接对纳米孔产生的原始电信号进行实时读段比对。
关键思想

  • 将参考基因组也转换为“预期信号”。
  • 对原始信号和参考信号进行量化(分桶)和哈希处理,使得相似的电信号产生相同的哈希值。
  • 使用哈希匹配快速定位读段可能来源的区域。
    效果:首次实现了在无需碱基识别的情况下,对大型基因组(如人类)进行实时比对,降低了延迟和计算需求。

未来机遇与总结 🌟

基因组分析领域正在快速发展,未来机遇包括:

  • 更紧密的跨层协同设计:生物技术公司(如Illumina)已在测序仪中集成FPGA进行实时分析。科技公司(如NVIDIA)也在为其GPU添加针对动态规划等基因组核心操作的专用指令。
  • 新兴计算范式:存内计算、近内存计算、 wafer-scale AI引擎(如Cerebras)等新型硬件架构为加速基因组分析提供了新的可能性。
  • 拓展分析边界:直接从原始信号进行基因组组装、结合原始信号与碱基识别信息进行混合分析、将分析流程应用于蛋白质组学等新型生物数据。

本节课中,我们一起学习了基因组分析的基础流程、关键挑战以及通过算法与硬件协同设计来加速分析的前沿方法。实现快速、准确、低成本、便携且可扩展的基因组分析,对于推动生命科学研究和医疗健康事业至关重要。

20:实现以内存为中心的计算 (Spring 2025) 🧠

在本节课中,我们将深入探讨以内存为中心的计算,这是解决现代系统中数据移动瓶颈的关键方法。我们将回顾处理内存架构的分类,并详细介绍两种主要方法:内存内处理近内存处理。课程将涵盖具体的研究案例、硬件设计原则以及当前市场上可用的商业原型芯片。最后,我们将介绍实验三的内容,你将有机会在模拟的UPMEM近内存处理系统上进行编程实践。


课程回顾与问题定义

上一节我们介绍了处理内存的基本概念。本节中,我们来看看其背后的核心问题。

数据移动是当今系统性能的主要瓶颈。这源于多种原因:应用程序可能缺乏足够的数据局部性来有效利用CPU或加速器上的深层缓存层次结构;主内存设备可能无法提供足够的带宽来维持高计算吞吐量;或者访问主存或存储的延迟过高。

这个问题并非新出现,但随着应用需求的增长,它变得日益关键。例如,神经网络和Transformer模型的数据使用量呈指数级增长。即使在手机或笔记本电脑等设备中,数据移动也占据了系统总能耗的显著部分。

其根本原因在于,我们历史上一直以计算为中心 设计系统。在芯片上进行一次精确计算仅消耗皮焦耳级别的能量,但若需要访问片外主存,能耗将高出两到三个数量级。这种不平衡导致了当前的系统瓶颈。


缓解数据移动瓶颈的现有方案

为了缓解数据移动瓶颈,业界已尝试多种方案。例如,每一代iPhone都引入了新的硬件预取器,试图预测数据访问并将其提前加载到CPU核心。缓存层次结构也变得越来越深,容量越来越大。

然而,这些以计算为中心 的架构只是在修补问题,并未从根本上解决。问题的核心在于,计算发生在远离数据存储位置的地方。

一种根本性的解决方案是转向以内存为中心 的设计思路,将计算资源移动到数据附近。


处理内存架构的分类

处理内存架构主要分为两大类:

1. 近内存处理
这种架构类似于传统的冯·诺依曼架构,区分逻辑单元和内存单元。不同之处在于,它将逻辑单元移动到更靠近内存阵列的位置。逻辑单元离内存阵列越近,可用的内存带宽就越大,内存访问延迟也越短。

2. 内存内处理
这种方法与我们熟悉的架构完全不同。它不再区分系统的计算和存储组件,而是直接利用内存器件本身的模拟操作原理来执行计算。内存本身被用于计算。


深入探讨:内存内处理架构

上一节我们回顾了基本分类,本节中我们来看看内存内处理架构的具体实现与优化。

DRAM组织与操作回顾

要理解内存内处理,首先需要了解DRAM的组织方式。DRAM是分层组织的:

  • 最高层是DRAM阵列,由水平和垂直连接的存储单元组成。
  • 下一层是DRAM子阵列,包含多个DRAM阵列,共享全局行解码器和全局敏感放大器。
  • 访问DRAM主要涉及两个操作:激活预充电/写入

基础内存内操作

利用DRAM的操作原理,可以执行简单的内存内操作:

  • 行复制:通过连续两次激活命令,可以将源行的数据复制到目标行,无需CPU介入。公式可表示为:目标行数据 = 源行数据
  • 三取二多数操作:通过同时激活三行DRAM,利用敏感放大器的差分特性,可以实现布尔逻辑中的多数表决功能。例如,若三行数据为蓝、黄、蓝,则结果为

这些基础操作能显著降低数据复制和布尔运算的延迟与能耗。

扩展内存内处理能力:SIMDRAM框架

基础操作功能有限。SIMDRAM框架旨在支持更复杂的操作,其关键思想包括:

  1. 垂直数据布局:将数据按列存储,而非传统的按行存储。这允许通过行操作隐式地移位数据,并将DRAM子阵列视为一个大规模单指令多数据引擎。
  2. 基于多数表决的计算:直接使用多数门而非传统的与/或门来构建算术逻辑,可以减少所需的激活次数,提高性能。

SIMDRAM框架的工作流程分为三步:

  1. 将用户输入的与/或逻辑图转换为优化的多数-反相图。
  2. 将该图映射为一系列DRAM行复制和多数操作序列,生成微程序
  3. 通过内存控制器执行微程序,在DRAM芯片内完成计算。

支持复杂函数:Pluto框架

对于超越函数(如三角函数、指数函数),SIMDRAM难以直接处理。Pluto框架采用查找表 方法,将复杂计算预先计算并存储在DRAM中,通过内存读取(查找表访问)来替代计算。

Pluto通过特殊的“行扫描”操作来执行查找表查询。它比较输入向量与当前激活的行地址,匹配时则将数据复制到输出向量。Pluto提供了三种硬件设计,在性能、能效和面积效率之间进行权衡。

提升灵活性与利用率:MIMDRAM框架

现有内存内处理架构存在利用率低、不支持归约操作、编程复杂等问题。MIMDRAM框架通过以下方式解决:

  • 细粒度DRAM:将全局字线分段,允许独立激活不同的DRAM阵列部分,实现灵活的并行度。
  • 支持归约操作:利用DRAM内部已有的放大路径和互连网络,在阵列间移动数据,实现树状归约计算。
  • 编译器自动向量化:重用CPU的向量指令集(如AVX-512)和编译器支持,自动将循环映射到内存内处理硬件,对程序员透明。

深入探讨:近内存处理架构

上一节我们深入了解了内存内处理,本节中我们来看看近内存处理架构的设计挑战与商业实现。

近内存处理的设计挑战

在内存芯片附近或内部集成逻辑并非易事,需权衡以下关键因素:

  1. 面积限制:内存芯片的首要目标是存储密度。添加计算逻辑会占用存储空间,可能加剧内存容量瓶颈。
  2. 热约束:内存芯片通常采用被动散热。集成高功耗逻辑单元可能导致过热,需要更昂贵的主动冷却方案。
  3. 制造工艺:逻辑工艺优化速度,内存工艺优化密度。用内存工艺制造逻辑单元,其性能通常低于专用逻辑工艺。

尽管面临挑战,但通过精心设计,近内存处理架构仍能在特定应用上取得优于传统CPU或加速器的性能与能效。

商业近内存处理架构实例

目前已有多个商业或原型近内存处理架构:

1. UPMEM

  • 简介:首个商业化近内存处理方案,采用DRAM DIMM形态,内部集成多个简单处理器核。
  • 架构:遵循松散耦合加速器模型。主机CPU需显式将数据移至UPMEM DIMM,并启动核上执行的任务。核间无直接通信,需通过主机内存中转。
  • 核心:顺序、多线程处理器,频率较低(~425 MHz),反映了内存工艺集成逻辑的挑战。每个处理器核配有本地暂存存储器。
  • 应用:适用于内存密集型应用,如数据库操作、机器学习训练等。

2. 三星 HBM-PIM

  • 简介:利用3D堆叠的HBM内存,将计算逻辑集成在缓冲层或替换部分存储层。
  • 架构:针对神经网络推理的乘累加操作进行优化,采用固定功能单元。通过特定的读写命令序列触发计算模式。
  • 特点:保持与标准HBM接口的兼容性,计算单元位于存储阵列的外围。

3. SK海力士 AiM

  • 简介:基于GDDR6内存,针对AI推理设计。
  • 架构:每个存储体配有处理单元,支持新的DRAM命令以触发计算。专注于低精度浮点运算和激活函数。

4. 阿里巴巴 HB-PNM

  • 简介:采用晶圆对晶圆混合键合技术,将逻辑晶圆和内存晶圆面对面集成。
  • 目标:推荐系统等应用,使用低功耗DRAM。

实验三介绍:UPMEM近内存处理编程

在本课程的最后部分,我们将介绍实验三,你将动手在UPMEM架构上进行编程。

UPMEM编程模型

UPMEM SDK提供了一系列API来管理加速器:

  • 分配DPUdpu_alloc 分配一组处理单元。
  • 加载二进制文件dpu_load 将内核代码加载到DPU。
  • 数据传输
    • dpu_copy_to / dpu_copy_from:与单个DPU进行串行数据传输。
    • dpu_push / dpu_prepare_xfer:与多个DPU进行并行数据传输。
    • dpu_broadcast_to:将数据广播到多个DPU。
  • 启动内核dpu_launch 在DPU上启动执行。
  • 同步:提供互斥锁、信号量、屏障等原语,用于DPU内线程间的协调。

实验任务

实验三包含以下任务:

  1. 任务一:熟悉UPMEM SDK,练习使用不同的数据传输API,并测量其性能。
  2. 任务二:实现一个简单的向量加法内核,并测试不同数据类型下的性能。
  3. 任务三:实现一个向量归约操作,需要使用线程同步机制。
  4. 附加任务:实现一个图像RGB亮度调整内核。

实验将通过Docker容器提供的模拟器环境进行,无需访问真实硬件。

编程挑战与高级框架

直接使用UPMEM SDK编程较为繁琐,需要手动处理数据划分、传输、同步等细节。

为此,研究人员开发了更高级的编程框架,如DAP。它采用基于并行模式的抽象,允许用户通过组合MapReduce等模式来描述计算,框架则自动处理底层的数据传输和任务调度,大大简化了编程难度。


总结与展望 🚀

本节课中,我们一起深入学习了以内存为中心的计算。

  • 我们回顾了数据移动瓶颈的问题根源,以及从计算中心转向内存中心的设计思路。
  • 我们详细探讨了内存内处理的多种框架,它们利用DRAM物理特性执行计算,从简单的行操作发展到支持复杂算术和超越函数。
  • 我们也分析了近内存处理的设计挑战,并介绍了UPMEM、三星、SK海力士等公司的商业或原型架构,展示了该领域的活跃发展。
  • 最后,我们介绍了实验三,你将有机会在UPMEM模拟系统上实践近内存处理编程。

以内存为中心的计算是一个充满活力的研究领域,随着更多原型和产品的出现,未来将有更多机会推动内存和计算架构的协同创新。

22:前沿内存鲁棒性研究 (Spring 2025) 🧠

概述

在本节课中,我们将学习关于内存鲁棒性的前沿研究,特别是针对DRAM(动态随机存取存储器)中“行锤击”和“行压迫”等干扰现象的防御机制。我们将探讨几篇关键论文,它们从不同角度提出了高效、低开销的解决方案,以应对随着技术缩放而日益严重的内存干扰问题。


1. 空间变异感知的鲁棒性防御 🛡️

上一节我们介绍了内存干扰的基本问题。本节中,我们来看看如何利用DRAM行之间鲁棒性的空间变异来改进现有防御方案。

1.1 背景与动机

内存隔离是计算机系统中确保安全、隐私和可靠性的基本原则。它要求对一个内存地址的访问不应在其他地址上产生意外的副作用。然而,在现代DRAM芯片中,由于高密度和访问延迟等权衡,实现严格的内存隔离变得困难。

DRAM芯片的基本组织包括多个存储体(Bank),每个存储体内有多个子阵列(Sub-array),子阵列中的存储单元(Cell)以二维行列形式组织。访问数据需要先激活(Activate)一行,将其内容读取到行缓冲器(Row Buffer),然后进行列访问,最后预充电(Precharge)关闭该行。

反复快速打开和关闭相邻的DRAM行会导致邻近存储单元发生比特翻转,这种现象被称为“行锤击”(Row Hammer)。它是DRAM干扰问题的一个典型例子。

随着技术缩放,新芯片的干扰脆弱性加剧,现有解决方案已带来显著的性能、能耗和成本开销。遗憾的是,此前没有严谨的工作研究不同DRAM行之间干扰鲁棒性的空间变异,以及这种变异对未来解决方案的影响。

1.2 研究目标与方法

本工作的目标是理解不同DRAM行之间干扰鲁棒性的空间变异,并利用这种理解来改进现有的防御方案。

我们测试了来自三大制造商的144个DDR4 DRAM芯片,表征了单个存储体内所有行以及芯片内多个存储体的特性。我们发现,不同DRAM行之间的行锤击阈值(即诱发首次比特翻转所需的最小激活次数)存在巨大且不规则的变异。

以下是关键发现:

  • 变异显著:在同一存储体内,最脆弱的行和最坚固的行所需的锤击阈值可能相差数个数量级。
  • 缺乏规律:行的脆弱性与其物理地址(如存储体地址、行地址、行间距等)之间没有明显的、可预测的规律性模式。

1.3 核心机制:Sword

基于上述观察,我们提出了一个关键思想:根据受害者行的干扰脆弱性水平,动态调整现有防御方案的激进程度

为此,我们提出了一种名为 Sword(Spatial-Variation-Aware Robustness Defenses)的新机制。Sword动态调整解决方案的阈值或参数,从而调整其预防性操作的频率。

核心操作

  1. 行分类:Sword将DRAM行按照其脆弱性划分为若干等级(例如16个等级),每个等级对应一个不同的防御阈值。
  2. 元数据存储:每个DRAM行仅需存储少量比特(例如4比特)来标识其所属的脆弱性等级。这些元数据可以存储在内存控制器中(使用SRAM结构),也可以存储在DRAM芯片内部(利用ECC或空闲引脚)。
  3. 动态决策:当内存访问发生时,防御机制(如Para、BlockHammer、Hydra)根据目标行对应的脆弱性等级,使用调整后的阈值来决定是否执行预防性刷新。对于更坚固的行,减少不必要的刷新操作。

公式描述
设原有防御机制的刷新阈值为 T,行为 i 的脆弱性等级为 L_iL_i 值越高代表越脆弱)。Sword调整后的阈值 T_i' 可以表示为:
T_i' = f(T, L_i)
其中函数 f 根据具体防御机制和分级策略定义,通常使脆弱行的 T_i' 小于或等于 T,而坚固行的 T_i' 大于 T

1.4 评估与总结

我们将Sword与五种先进的防御机制(Para, BlockHammer, Hydra等)集成,并使用Ramulator 2.0仿真框架在120个八核工作负载下进行评估。

结果

  • 降低开销:Sword显著降低了现有解决方案的性能开销。在极低的锤击阈值下,系统吞吐量下降幅度大幅减少。
  • 提升性能:通过减少不必要的预防性刷新,系统整体性能得到提升。

总结:我们首次对DRAM芯片内部干扰鲁棒性的空间变异进行了严谨实验,揭示了其显著且不规则的特性。基于此,我们提出了Sword机制,它能动态调优现有方案的激进程度,显著降低其性能开销,为未来高脆弱性DRAM系统提供了高效的增强方案。

遗留问题:行的脆弱性特征可能随时间(老化效应)或数据模式变化,因此需要周期性地重新分析或设计在线分析机制来更新元数据,这是一个开放的研究方向。


2. 低成本、可扩展的行锤击缓解方案 ⚙️

上一节我们介绍了利用空间变异优化防御的思路。本节中,我们来看看两种旨在以低面积、低性能开销解决行锤击问题的新型机制。

2.1 问题与现有方案分类

随着DRAM芯片变得更脆弱,缓解行锤击的防御技术需要同时具备高效性和低开销。现有的行激活次数跟踪方案可分为三类:

  1. 每行计数器:为每个DRAM行分配一个专用计数器。精度高、性能开销低,但面积成本极高。
  2. 每攻击行计数器:利用“攻击者只能锤击少量行”的观察,减少计数器数量,但需使用标签(Tag)进行映射。在低锤击阈值下,所需计数器数量增加,面积成本依然较高。
  3. 少于每行计数器:通过共享计数器或引入概率性刷新来进一步减少计数器数量。面积成本低,但在低锤击阈值下,由于预防性刷新频繁,性能和能耗开销很大。

理想方案需要在面积、性能和能耗之间取得良好平衡,但现有方案难以同时满足。

2.2 机制一:Camel 🐫

Camel(Count-Min Sketch Based Row Hammer Mitigation)的目标是在高脆弱性DRAM系统中,以低面积、性能和能耗开销防止行锤击比特翻转。

关键思想:结合低成本的基于哈希的计数器和高精度的基于标签的计数器,取二者之长。

  • 计数器表(Counter Table):使用一种称为Count-Min Sketch的流式算法,将DRAM行映射到一组低成本的哈希计数器上。它尽可能唯一地映射各行,并在计数器组达到阈值时触发对受害者行的预防性刷新。这以低成本实现了大范围的激活跟踪。
  • 近期标签表(Recent-Tagger Table):为近期被频繁激活的少量DRAM行动态分配高精度的、每行专用的标签计数器。这提高了对“热行”的跟踪精度,从而减少误报和性能损失。

操作流程

  1. 当行A被激活时,Camel同时查询计数器表(得到估计值E_hash)和近期标签表(如果匹配,则得到精确值E_tag)。
  2. 优先采用标签表提供的精确值(若存在)。
  3. 将此激活次数估计值与预防性刷新阈值比较,若达到阈值,则刷新受害者行。

评估结果

  • 面积:Camel的面积开销显著低于Graphene,与Hydra相似。
  • 性能与能耗:在低行锤击阈值下,Camel的性能和DRAM能耗开销很小,且优于其他低面积开销的解决方案(如Hydra)。

2.3 机制二:Abacus 🧮

Abacus(All-Bank Activation Counters for Scalable and Low-Overhead Row Hammer Mitigation)从工作负载与内存的交互模式中获得了关键观察。

关键观察:许多工作负载倾向于在相近的时间点访问不同存储体中相同行地址的行(称为兄弟行)。这是由于:

  1. 工作负载内存访问模式固有的空间局部性(如遍历数组)。
  2. 现代物理地址到DRAM的映射策略,它将相邻的缓存行映射到不同存储体的同一行,以利用存储体级并行性。

关键思想:使用一个计数器来跟踪所有兄弟行(跨所有存储体)的最大激活次数,从而将计数器数量减少为存储体数量的分之一。

设计目标:保持Abacus计数器的值始终等于所有兄弟行中的最大激活次数。若估计值小于真实最大值,则不安全;若大于,则会导致不必要的性能开销。

实现:Abacus借鉴了Graphene的激活计数机制,并进行了扩展以实现跨存储体的兄弟行跟踪。

评估结果

  • 性能与能耗:Abacus在低行锤击阈值下引入了很小的性能和能耗开销,平均性能优于Hydra和Para,能耗低于它们,与Graphene相近。
  • 面积:Abacus的面积开销显著低于Graphene,在极低阈值下略高于Hydra,但Hydra的性能开销更大。

总结:Camel和Abacus都通过创新的计数器组织方式,在低行锤击阈值下实现了面积、性能和能耗之间的良好权衡,为未来高密度、高脆弱性DRAM系统提供了可行的缓解方案。


3. 工业界解决方案:DDR5标准中的缓解机制 🏭

前面我们探讨了学术界的解决方案。本节中,我们来看看工业界如何应对行干扰问题,特别是最新的DDR5标准中引入的机制。

3.1 背景与演进

早期工业界解决方案(如某些DRAM的“目标行刷新”功能)曾依赖“安全通过隐匿”的原则,但被许多研究证明可被绕过。如今,行业标准组织JEDEC正更开放地更新规范以应对干扰。

最新的DDR5标准(截至2024年4月)引入了一种名为 Per-Row Activation Counting (PRAC) 的新机制。我们的目标是严格分析这些新机制的安全性和性能开销。

3.2 两种工业界方案

两种方案都基于一个关键命令:RFM (Refresh Management)。RFM命令允许内存控制器授予DRAM模块一段时间窗口,使其能够安全地执行预防性刷新操作,而不会被内存控制器的访问打断。

  1. 周期性RFM (Periodic RFM, PRFM)

    • 内存控制器为每个DRAM存储体维护一个激活计数器。
    • 当计数达到设定的RFM阈值时,控制器发送一个RFM命令给DRAM模块,并清零计数器。
    • DRAM模块在RFM授予的时间内决定刷新哪个(些)行。
    • 问题:精度低,一个存储体内所有行共享一个计数器,导致大量不必要的刷新,开销大。
  2. 每行激活计数 (PRAC / “Track”)

    • DRAM芯片内部为每个行维护一个激活计数器(但非精确并行更新,而是在行关闭时更新)。
    • 当某行的计数达到“备份阈值”时,DRAM芯片向内存控制器发送一个“备份”信号。
    • 控制器收到信号后,有一段“正常流量窗口”可继续调度请求,随后必须进入“恢复期”并发送一个或多个RFM命令。
    • 问题:启用PRAC会增加关键的DRAM时序参数(如tRP, tRC),从而增加访问延迟。

3.3 安全性与性能分析

我们通过最坏情况访问模式分析(如“波浪攻击”)来评估这些机制的安全性,并使用Ramulator 2.0进行性能和能耗仿真。

安全性

  • PRFM:需要非常频繁地发送RFM命令(例如,每8次激活发一次)才能防御较低的行锤击阈值(如128),配置复杂。
  • Track (PRAC):可配置为防御低至2次激活的行锤击阈值,但需要仔细配置以避免被波浪攻击绕过。

性能与能耗

  • PRFM:由于频繁的RFM命令和大量预防性刷新,在低行锤击阈值下性能和能耗开销显著增加。
  • Track (PRAC):由于时序参数增加,即使在没有触发预防性刷新时也有基础性能开销。在低阈值下,其性能优于Graphene和Hydra,但能耗较高。然而,其开销随着阈值降低而急剧增加,可扩展性不佳。
  • 攻击面:攻击者可以故意触发大量“备份”信号,消耗高达79%的DRAM带宽,导致系统性能严重下降,这构成了新的内存性能攻击面。

总结:最新的工业界解决方案PRAC提供了可配置的安全性,但对当今的DRAM芯片已带来不可忽视的性能和能耗开销,且对未来更脆弱的芯片扩展性差。它还可能被用作性能攻击的载体。未来研究需要致力于降低其时序参数开销、解决波浪攻击下的性能恶化问题,并防止其预防性操作被利用。


4. 新型干扰现象:行压迫 (RowPress) 及其组合攻击 🔥

之前我们主要关注行锤击。本节中,我们将探讨一种新发现的、不同的DRAM干扰现象——“行压迫”,以及将其与行锤击结合的组合攻击。

4.1 行压迫的发现与表征

行压迫是指在锤击行的同时,长时间保持该行处于打开(激活)状态,这会对相邻行产生额外的干扰效应,导致比特翻转。

关键特性(基于对164个DDR4芯片的实验):

  1. 放大干扰:行压迫能显著减少诱发比特翻转所需的最小激活次数,降幅可达一至两个数量级。在极端情况下,仅一次激活(但保持长时间打开)就足以引发翻转。
  2. 温度敏感性:温度升高,行压迫效应加剧。
  3. 与行锤击不同
    • 影响的存储单元集合有较大不同。
    • 比特翻转的方向性不同(行锤击主要是0->1,行压迫主要是1->0)。
    • 访问模式有效性不同:随着行打开时间增长,单侧攻击比双侧攻击更有效,这与行锤击相反。

4.2 系统级演示与缓解

我们在一台搭载易受攻击的Samsung DRAM模块的Intel i5系统上实现了概念验证攻击。通过精心设计的内存访问模式(反复访问同一行内的不同缓存块以诱使内存控制器保持该行打开),我们成功诱发了仅靠行锤击无法产生的比特翻转。

缓解策略:调整现有的行锤击缓解机制,将行打开时间纳入考虑。

  • 方法一(主动):在内存控制器中实施更积极的行策略,限制行的最大打开时间。
  • 方法二(被动):在行保持打开期间,即使没有新激活,也持续增加其激活计数,从而更快地触发现有防御机制的刷新阈值。
    这两种方法都能在不增加额外性能开销的情况下防御行压迫。

4.3 组合攻击:行锤击 + 行压迫

一个自然的问题是:能否组合行锤击和行压迫来发动更强大的攻击?

实验:我们测试了84个DRAM芯片,采用一种混合攻击模式——反复“锤击”一个攻击行,同时长时间“压迫”另一个攻击行(两者位于受害者行两侧)。

发现

  • 更快的攻击:组合模式诱发首次比特翻转所需的总时间显著少于单纯的行锤击或行压迫。
  • 更少的激活:组合模式所需的总激活次数也更少。
  • 假设:从一侧进行的行压迫对受害者行的干扰效应比另一侧的行锤击更强,二者结合产生了协同效应。

意义:组合攻击进一步降低了攻击的难度和时间,对现有的、仅针对固定激活阈值的行锤击防御机制构成了严峻挑战。

4.4 电路级机理简介

干扰现象的根源在于DRAM单元的高密度布局。关键因素包括:

  • 存取晶体管沟道:反复激活相邻字线会导致电荷在共享的活性区(Active Region)内迁移。
  • 电荷陷阱:迁移的电子可能被氧化物层中的电荷陷阱捕获,改变晶体管的阈值电压,加剧电荷泄漏。
  • 字线耦合:紧密排列的字线之间的电容耦合也会导致干扰。
    行锤击主要与电荷在相邻单元间的快速往复迁移有关,而行压迫则与长时间打开一行导致的电荷持续注入和陷阱填充有关。对这些电路级机理的深入理解有助于设计更根本的缓解方案。

总结

本节课中,我们一起深入探讨了内存鲁棒性领域的前沿研究:

  1. 利用空间变异:Sword机制通过感知并利用DRAM行之间干扰脆弱性的不规则差异,动态优化现有防御方案,显著降低性能开销。
  2. 创新计数器设计:Camel和Abacus通过混合哈希/标签计数器或利用工作负载的访问模式(兄弟行),实现了低面积、低开销的行锤击缓解。
  3. 评估工业方案:分析了DDR5标准中的PRAC等工业界解决方案,指出其在安全性、可扩展性和新攻击面方面面临的挑战。
  4. 揭示新现象与组合攻击:介绍了“行压迫”这一新型干扰现象及其特性,并展示了将其与行锤击结合的组合攻击的更强威力。

这些研究共同指向一个方向:随着DRAM技术缩放,内存干扰问题日益复杂,需要从电路特性、系统架构、安全策略等多个层面进行协同创新,才能设计出高效、安全、可扩展的未来内存系统。

23:推测执行(2025年春季)

在本节课中,我们将要学习推测执行,特别是其在并行机器(如多核、多处理器或多线程机器)中的应用。这是一个吸引了众多研究者的迷人领域,许多相关研究已经展开。

以下是本课所需的阅读材料:

  • Sohi 等人的《多核推测》是一篇关于推测的奠基性论文。
  • Zilles 的《双核执行》工作,我们稍后会讨论。
    推测执行有两个方面:一是提升单线程程序的性能,二是提升并行程序的性能。前两篇论文主要讨论如何利用推测来并行化单线程程序。后两篇论文则关注如何利用推测来提升并行应用程序的性能,包括如今已在一些系统中使用的事务内存,以及推测锁消除(本质上是在程序员无需或只需少量支持的情况下实现的事务内存)。

还有一些推荐的阅读材料,例如 Steffan 等人的《一种可扩展的线程级推测方法》,这是较早的线程级推测论文之一。此外还有 Hammond 和 Olukotun 的《动态多线程处理器》论文等。本课将讨论更多相关研究,希望你能阅读。

本课的重点是推测执行与并行机器。什么是推测?推测就是在知道某件事是否需要之前就去做。这是推测最普遍的形式。例如,在单线程机器中,为了保持流水线满载,在知道是否需要取指之前就进行取指。这主要用于提升单处理器环境下的性能。

你已经学习过许多技术,特别是如果你上过我的 447 计算机架构课程(其讲座可在网上找到)。例如,分支预测就是一种推测形式。在单处理器上下文中,你在知道是否应该取指之前就获取下一条指令,这是理想的分支预测,目的是保持流水线满载。数据值预测是另一个概念。当加载一个数据值时,可能需要一段时间,为什么不预测该值,以便推测性地处理依赖指令?这打破了指令间的数据依赖,实现了指令级并行。当然,你需要验证分支预测和数据值预测。如果分支预测错误,当分支实际解析时,你需要通过重定向取指到正确的分支目标来恢复。同样,如果数据值预测错误,所有依赖指令可能都已执行,你需要通过重新执行依赖指令或刷新流水线并重新开始来恢复,就像处理分支预测错误一样。预取是另一种推测形式。处理器在真正需要某个地址之前就请求它,可以根据当前发生的地址模式来推测。例如,如果程序生成的地址是可预测的,预取器可以开始预取这些块。这是一种推测,因为预取器只是猜测处理器将继续以流式或跨步方式访问内存。在这种情况下,可能不需要恢复,因为推测本身没有危害。而分支预测则不同,如果不纠正分支结果,会影响正确性。

以上是我们在单处理器上下文中讨论过的推测技术。今天我们的重点将是并行机器或多处理器系统。在多处理器上下文中,也有许多推测方法,如线程级推测、事务内存和辅助线程等。基本思想是推测性地并行化程序以提升多处理器系统的性能。

如果你想了解更多关于单处理器上下文的内容,我鼓励你去观看 447 课程中关于分支预测的讲座(大概是第 10 或 11 讲),它也在 YouTube 上。数据值预测和预取也在 447 课程的其他讲座中有所涉及,你同样可以在 YouTube 上找到,或者我也很乐意在答疑课上讨论。

让我们看看多处理器上下文。同样,这里有许多推测方法,如线程级推测、事务内存和辅助线程。基本思想是推测性地并行化程序以提升性能。为此,我们希望不安全地并行执行线程。“不安全”意味着我们不知道线程是否应该并行执行。线程可以来自顺序或并行应用程序。这是推测性并行化最普遍的形式。我们希望并行化一个单线程应用程序,或者从一个并行应用程序中挖掘更多并行性。

一旦你开始在不清楚是否可以并行的情况下并行执行线程,它们之间可能存在数据依赖。你需要以某种方式检查这些数据依赖。假设你生成了线程 0 并开始推测性地派生线程 1,同时继续执行线程 0。在开始执行线程 1 之前,你不知道线程 1 是否与线程 0 存在数据依赖。但线程 1 可能写入一个稍后被线程 0 读取的位置,这意味着它们至少不应该纯粹地并行执行。因此,硬件或软件需要监控这些数据依赖违规。一旦检测到数据依赖顺序被违反,违规的线程就会被取消并重新启动。

例如,这是一种数据依赖推测违规。可能的情况是,一个线程进行加载,而另一个逻辑上更早的线程应该进行存储,但存储发生得更晚。由于推测性并行化,加载发生得更早,从而得到了错误的数据值。硬件或软件检测到这种数据依赖顺序违规(加载本应在存储之后发生),并确定违规线程(本例中是线程 1,因为它在逻辑上更晚,但执行过早)。因此,该线程被取消并重启。这是一种监控和从数据依赖违规中恢复的方法。之所以需要这样做,是因为线程的执行是不安全的,我们在不确定其与程序其他部分是否存在跨依赖的情况下就过早地启动了它。

如果没有数据依赖,你派生了线程,结果证明它完全独立,那么线程就可以提交。例如,假设你有一个单线程程序正在执行,其中有一个函数调用。在单处理器上下文中,你通常会顺序执行。但你可以做的是,在另一个处理器上启动另一个线程(线程 1)来执行这个函数调用,而原线程(线程 0)则从函数返回点继续执行。如果这个函数与原线程完全独立,并且没有检测到数据依赖违规,那么你就可以提交这个线程的结果。你需要以某种方式将结果合并到架构状态中,但实际上可以提交。因为不存在依赖,所以推测性并行化在这种情况下是有效的。例如,如果是一个清理任务函数,原线程可能不需要其结果;或者如果是一个其结果在程序很晚才需要的函数,你可以提前并行执行它,这样当其他指令需要其结果时,该函数的结果已经可用。这是推测性并行化可以工作的一种情况。

例如,如果数据依赖未被违反,推测线程就提交。如果线程最初来自顺序顺序,并且你正在推测性地并行化一个单线程,那么需要保持该顺序,使得线程按顺序一个接一个地提交。

基本上,当线程推测执行时,需要进行线程间值通信。假设你决定在此处启动一个函数调用线程。那么这些线程需要通过寄存器或内存进行通信。让我简要讨论一下不同类型的通信,因为这将在不同种类的线程级推测机制中反复出现。如果线程需要通过寄存器通信,这需要处理器间的硬件支持。寄存器依赖和内存依赖之间的根本区别在于,线程间的寄存器依赖是编译器已知的。编译器在编译程序时就知道这些依赖。例如,假设你有一个顺序程序,其中一部分代码写入寄存器 R2,而另一部分(比如一个函数调用)从寄存器 R2 读取并对其进行操作。如果你将这个函数调用作为单独的线程执行,那么这个依赖需要被满足。这个 R2 现在在单独的线程上执行,因为函数与函数返回点之后的代码并行执行。当函数执行时,它读取 R2,而 R2 依赖于正在此处执行的原线程。编译器可以检测到这一点,这是寄存器与内存的根本区别。寄存器依赖可以被编译器分析和检测,因为它们是静态的。当然,它们在某种程度上是动态的,因为它们依赖于分支,但至少你知道可能存在或不存在依赖,只有分支决定该依赖。寄存器地址是静态已知的,依赖也是静态已知的。因此,当线程间发生寄存器通信时,可以由寄存器的生产者(原线程)或消费者(函数线程)发起,具体取决于哪个先执行。编译器可以说:“这个寄存器有依赖,你需要等待它的值”,或者“你需要发送这个寄存器的数据值,因为其他线程需要它”。编译器在分析后可以插入这些提示。

如果消费者先执行(假设是线程 1),并且它与前一个线程(线程 0)并行执行(如果你进行激进的并行化,可能会发生这种情况),那么如果寄存器中的值尚未就绪,消费者就会停顿。它知道需要等待,但值在寄存器文件中尚未就绪。当生产者执行时,它会转发值。你如何知道值是否就绪?基本上,你可以与寄存器文件通信,寄存器文件可以为每个寄存器设置就绪位。你可以为该特定线程在硬件中添加一个就绪位。如果值可用,就绪位被设置;如果值尚不可用(当该线程启动时 R2 的值尚未产生),则该 R2 的就绪位被初始化为零。只有当生产者执行时,它才能将该就绪位设置为一。这些就绪位也称为满/空位。这是在线程间进行细粒度同步所需的一种构造,用于指示寄存器的值是否已产生。生产者设置就绪位,消费者等待就绪位变为一。这基本上是线程间的数据流和满/空位同步。早期引入这一概念的是 Burton Smith 关于流水线共享资源 MIMD 计算机的奠基性论文。我鼓励你阅读那篇论文,其中很好地使用了满/空位。这是在 Burton Smith 当时设计的异构元素处理器中,发表于 ICPP 1978 年。这是在线程间通信寄存器的一个重要构造,你可以使用满/空位,内存也可以。这是一个重要的同步构造,使一个线程能够产生寄存器或内存值,另一个线程消费它,从而实现同步。这基本上是一个同步变量,可以在硬件中支持。如果你在进行线程级推测,你应该在寄存器中支持这一点。

你可以阅读这篇论文以了解满/空位的良好应用。另一方面,如果生产者先执行,生产者只需写入结果并发送就绪位。当消费者实际执行时,消费者读取该值。当然,生产者在设置就绪位后可以继续执行。我们将在多标量处理器中看到一个例子。多标量处理器实际上就是以这种方式在线程间通信。寄存器文件中有满/空位,当一个任务(他们称之为任务)产生一个需要发送到其他处理器的值时,编译器会分析任务以知道该值是否需要发送出去。然后,任务在产生该值时,通过寄存器文件环发送该值,而需要该值的另一个处理器在其寄存器文件中捕获它。我有点超前了,但这是你应该阅读的另一篇必读材料:Sohi 等人在 ISCA 1995 年发表的《多标量处理器》。这是线程级推测的一个奠基性例子,以硬件-软件协同的方式实现。

我已经说过,这可以通过寄存器中的满/空位来实现。另一方面,内存通信与寄存器通信非常不同。编译器通常不知道内存依赖,至少困难的动态内存依赖编译器不知道。为什么编译器不知道?因为编译器不知道操作的内存地址,而且由于指针和动态内存分配的使用,并非所有内容都是静态可分析的。你为指针分配内存,可以为指针分配任何值,因此这些依赖依赖于程序的动态输入以及程序基于这些输入的行为,编译器无法分析动态执行。因此,在推测性并行化中,满足不同线程间的内存通信通常更困难。

但如果你在推测性地并行化程序,你希望知道这里是否有一个存储,那里是否有一个加载。如果这个存储和加载在并行执行时实际重叠,你希望知道这个加载是否获得了正确的值,并且没有获得旧值。同样,这里可能有存储,那里可能有加载,你需要知道存储 A 是否与加载 B 重叠,因为如果它们重叠,你可能不希望存储 A 在加载 B 之前发生,因为加载 B 在逻辑上更晚。你需要保持依赖,遵守依赖。一般来说,在线程级推测方法中,线程推测性地执行加载,并从最近的前驱线程获取数据。它记录它已读取该数据。它在 L1 缓存或另一个结构中保持这个记录,我们将看到一些例子。另一方面,存储也是推测性地执行的。当线程执行存储时,它不等待。当线程执行加载时,通常也不等待。正如我之前所说,当线程执行存储时,它可能还不是机器中最老的线程。因此,在它尚未提交时,它缓冲更新,将更新放在写缓冲区或 L1 缓存中。当它执行存储时,它会检查后继线程是否有过早的读取。这意味着线程之间存在顺序。你有线程 0、1、2、3、4、5,它们都可以并行执行,但存在一个顺序,即顺序顺序。如果你正在并行化一个单线程程序并并行执行它们,在这种情况下,你正在获取本应顺序执行的代码块,并并行执行它们。线程 4、线程 5,检查它们是否在加载和存储上存在依赖,是否违反了任何加载-存储依赖。

因此,每个线程都可以推测性地执行加载。例如,假设加载实际上独立于任何存储。并记录它们已读取数据。例如,从地址 A 加载,并记录“我从地址 A 读取了数据”。这可以在 L1 缓存中通过设置一个位来完成,表示“我已读取数据”,比如设置一个推测读取位。后来,例如线程 0 执行一个到位置 A 的存储。在那一刻,它缓冲该存储。它还检查是否有后继线程已经读取了位置 A。如果后继线程进行了过早的读取,那么在这种情况下确实发生了,这个加载 A 发生在存储 A 之前,而它本不应该以那个顺序发生,因为它们是逻辑上顺序的线程。仅仅因为我们不安全地并行执行了它们,加载发生在了存储之前。因此,当存储实际执行时,我们需要检测到这种依赖。存储基本上以某种方式找出目标加载发生得更早,而它本不应该在存储完成之前发生。因此,如果后继线程在这种情况下进行了过早的读取,它需要被取消。取消意味着线程需要重新启动,也许其他所有线程也需要重新启动,因为现在有一个错误的数据值依赖,因为其他线程可能读取了由这个加载后来产生的其他数据值,并用错误的数据值执行了。通常,当你检测到这样的数据依赖违规,发现该线程推测读取了错误的值时,你会取消违规线程及其所有后继线程。当然,你可以做一些更聪明的事情,但复杂性会增加。你可以只找出那些获得错误值的依赖指令,并选择性地重新执行那些依赖指令。这是一个难题,很难有选择地找出哪些指令需要重新执行。人们已经研究过这个问题并找到了一些解决方案,但总的来说这是一件困难的事情。

我已经给了你很多线程级推测的概念。基本上,当你获取一个单线程程序并不安全地并行化它时,你会遇到这些依赖问题,关键依赖问题是寄存器通信和内存通信,你需要满足这些,内存通信需要动态满足。这种通信也可以在这里发生,不一定只与最老的线程,这里是最老的线程和最年轻的线程。但它可能发生在线程 3 和线程 4 之间,线程 4 进行加载 A,后来线程 3 进行存储到加载 A,这需要被检测,线程 4 及其所有后继线程需要被刷新或取消。

依赖违规:只有真正的数据依赖违规才应该导致线程取消。存在不同类型的依赖违规。假设在这种情况下,我表示加载 A 和存储 A,这是较晚的线程。假设较早的线程进行加载 A,较晚的线程进行存储 A。在这种情况下,这实际上是一个名依赖,硬件应该能够处理,意味着这里没有真正的依赖,这是一个反依赖,而不是真依赖。基本上,假设你有线程 0 和线程 1,线程 0 进行加载 A,线程 1 进行存储 A,并且这个存储发生在这个加载之前。你绝对应该确保这个加载不会获得来自这个存储的值。你应该能够通过消除名依赖来做到这一点。你通过寄存器重命名来做到这一点,但这里不是寄存器,所以你可以进行内存重命名。我们将看到一个进行内存重命名的结构,无论何时你想消除非真依赖(在这种情况下是写后读依赖,或者读后读依赖)。你可以通过重命名这些位置来消除这种依赖,因为存储实际上是在该位置创建一个新值。你可以认为存储杀死了该位置的旧值并创建了一个新值,所以实际上没有依赖,它只是一个名依赖,因为我们没有足够的内存位置。或者,如果需要,你可以创建该内存位置的新版本。第二个是写后写依赖,如果一个线程存储到 A,下一个线程也存储到 A,硬件应确保存储按顺序出现。我们之前已经讨论过这个主题。当真依赖发生时,即较老的线程存储到内存位置,而较年轻的线程通过加载从该内存位置读取,在这种情况下,如果较年轻的线程在存储发生之前读取,那么这就是真依赖,需要被检测并导致取消,因为较年轻的线程读取了错误的值。

因此,一般来说,名依赖(前两种依赖)可以通过版本控制来解决。基本上,每次对内存位置的存储都可以创建一个新版本,正如我刚刚描述的,因为存储实际上是在使用该内存位置作为地址,它是在创建一个新值。一旦你有了不同的版本,例如,每当这个存储写入时,它创建版本 1,而这个加载是从版本 0 加载,所以这个加载可以获得正确的值,因为它实际上是在该线程执行时从版本 0 读取。当这个线程执行时,它创建该位置的版本 1。当下一个线程执行时,假设它不存储 A,它创建版本 2。这个线程创建所有版本,一旦这个线程创建版本 2,名依赖就被消除了。当然,这需要一些硬件成本来用不同的版本号标记内存位置。这方面的一个例子在 1998 年 HPCA 的《推测版本缓存》论文中描述,我鼓励你阅读那篇论文。这是处理非真依赖的名依赖的一种方法。

设计推测性并行化概念时的另一个问题是:在哪里保存推测性内存状态? 让我们看看。基本上,什么是推测性内存状态?每当一个线程推测性地执行存储指令时,你不知道该存储指令是否应该实际提交到架构状态,因为该线程可能还不是机器中逻辑顺序上最老的线程。这意味着你需要以某种方式缓冲该存储指令。有两种方法:你可以把它放在一个单独的缓冲区,例如一个在线程间共享的共享队列(存储队列)。想法是,当你执行存储时,你有一个缓冲区,线程将其版本号和线程 ID 放入该缓冲区。另一个线程后来检查该缓冲区,看是否有来自更老线程的存储到相同位置。这是存储缓冲区方法,即单独的存储缓冲区方法。这是一个额外的数据结构,但当然是可行的。这类似于我们将在多标量处理器中讨论的地址解析缓冲区,地址解析缓冲区是确保在多标量处理器中满足存储-加载依赖的一种方式。这也类似于我们在 447 课程中讨论过的提前运行缓存和提前运行执行。提前运行执行的基本思想是,你推测性地执行程序,当你推测性地执行程序时,一些存储指令实际上推测性地写入内存,你不想将它们暴露给架构状态,因为记住提前运行执行是纯粹推测性的,在提前运行模式下发生的模式指令从提前运行缓存获取数据值。如果你对此更感兴趣,可以阅读之前讨论过的提前运行论文。

另一方面,你可以不设这样的单独缓冲区,而是决定将推测性数据、推测性存储块放在 L1 缓存中。在这种情况下,你可以在标签存储中标记一个额外的位,称为推测位或推测修改位。当我们讨论线程级推测时,会看到这个例子。这个推测修改位基本上表示该缓存块被该线程推测性地写入。通常,这对其他线程不可见。当线程提交时,你需要使它们非推测性;当线程被取消时,你需要使它们无效。基本上,你正在做的是将存储缓冲区放入缓存并与缓存集成,但你需要确保缓存仍然正确运行。这修改了缓存,但如果你真的想保持推测性内存状态,也许你不需要额外的存储缓冲区。因此,在哪里保持推测性内存状态实际上存在有趣的权衡,我鼓励你思考这个问题,特别是当我们讨论多标量和不同类型的线程级推测方法时。

现在,我将深入探讨推测性并行化单线程程序,但在开始之前,我先休息一下,很快回来。


好的,我们继续。我们将讨论推测性并行化单线程程序,这是推测讲座的一部分,然后我们将重点讨论如何利用推测提升并行程序的性能。这是一个吸引了许多人的话题,有许多阅读材料可以推荐,这些都是参考阅读材料,其中一些是必读的。当然,你读得越多越好,阅读和理解这些思想并评估不同思想之间的权衡总是好的。这可以给你很多好的项目想法,也能让你进行出色的研究。因此,我实际上建议阅读所有这些论文,但其中一些将是必读的,正如我之前告诉你的,并且我们将发布在网站上。我会尽量涵盖其中一些,这些是参考阅读材料,但还有许多其他阅读材料我们可以讨论。

将单线程程序并行化到多个硬件上下文的基本思想称为线程级推测。这也称为推测性多线程,有许多例子。一个例子是动态多线程,我简要讨论过,这里给出这个参考,但那是它的一个例子。Hammond 和 Olukotun 在 MICRO 1998 年写了一篇关于动态多线程处理器的论文。这是推测的一个例子,我们不会详细讨论,但我把它写在这里是因为它也是一种有趣的方法。基本思想是,每当一个线程遇到函数调用时,在单独的硬件上下文上启动该函数调用。因此,核心 0 执行常规线程,核心 1 执行函数调用。此时,核心 0 并非无所事事,而是从函数调用的返回点继续执行。因此,考虑代码,你有一些代码在这里,有一个调用在这里,这个调用带你到某个函数块,该函数块在这里执行,而调用之后的下一条指令(假设是一个加法)在核心 0 上执行。这基本上是函数级推测。在那个函数处,你启动一个线程,如果里面还有另一个函数,你可以在另一个单独的硬件上下文上启动另一个线程,如果还有另一个函数,你再启动一个线程。这就是你如何在函数级别动态多线程化程序。他们在这里所做的是,如果某些值不可用(例如,被预测为由该函数产生),他们基本上预测这些值。实际上,他们预测函数的返回值,因为函数通常产生一个返回的寄存器值。如果是这种情况,那么该寄存器值被预测,并且该返回点用该预测值执行。在该函数结束时,该函数产生一些结果,它也产生寄存器值,将该寄存器值与这个预测值进行比较,如果预测正确,那么这里的执行就是正确的(假设所有由程序较早部分产生且必须在返回点之前执行的值都被正确产生)。因此,需要进行一些检查以确保那发生了,最终结果被合并,他们使用称为预结果缓冲区的东西合并结果。我不会详细讨论,这是机制中更复杂的部分,但他们能够以这种方式在函数级别并行化程序。这是另一个例子,该论文通过循环迭代并行化程序。例如,如果你有一个 for 循环,你可以做的是,当你到达迭代开始时,在另一个处理器上启动下一个迭代。因此,基本上你可以推测性地说,迭代 0 在这里(核心 0),迭代 1 在这里(核心 1),迭代 2 在这里(核心 2),迭代 3 在这里(核心 3),依此类推,有多少核心就用多少。这是推测性并行化的另一种形式。基本上,你甚至可以预测迭代的输入,即使迭代间存在依赖。当然,你需要做我之前说过的所有其他事情,你需要确保真正的数据依赖被正确满足,迭代正确执行。如果存在依赖违规,实际看到依赖违规的后续迭代被取消。这就是推测性多线程(线程级推测)的思想。人们特别研究了在遇到函数调用时的多线程,以及在循环中的多线程。在循环中,你可以尝试推测性地并行化循环的不同迭代,我们将在本讲座中反复讨论这些概念。

但基本思想我已经告诉你了:在编译时或运行时将单个指令流推测性地划分为多个线程。执行是推测性的。你可以在多个硬件上下文中执行推测线程,正如我在这里展示的。最终,你需要将结果合并到单个流中。例如,这个函数调用需要将其结果合并到单个流中,使得结果看起来像是你顺序执行了那个单指令流,这是关键思想。当然,硬件和软件有时需要协同检查在推测执行期间是否有任何真依赖被违反,并确保顺序语义,因为记住,我们在单线程程序中有顺序语义,我们需要确保该语义,只是在底层利用硬件中存在的多个硬件上下文并行化该单线程程序。

因此,一种可能的方法是假设线程是独立的。例如,这里函数调用执行另一个线程,函数的返回点执行另一个线程。假设它们是独立的。你实际上可以使用值预测和分支预测来打破线程间的依赖。在这种情况下,可以预测函数的返回值,使得返回点之后的代码可以假设其独立性而执行。你不需要等待那个数据值,等待那个数据值可能是另一种选择,但这会减少线程间的并行性。如果它是可预测的,预测该值可能更好,这样该线程可以继续执行。因此,基本上你可以使用值预测来打破线程间的依赖,同样也可以使用分支预测。当然,你最终需要验证这些预测,因为记住这是一个我们推测性并行化的单指令流,你可以通过以某种方式执行安全版本来验证这些预测,也许总是执行安全版本,并通过检查一些不变量来验证。我们将看到其中一种方法,你可以思考其他方法。

线程级推测的早期工作之一实际上是在卡内基梅隆大学完成的,这是 Greg Steffan 的论文《一种可扩展的线程级推测方法》,来自 Todd Mowry 的小组。这展示了一个例子,我将在幻灯片上讲解这个例子。这是一个难以并行化的循环。假设这个 while 循环执行多次,基本上你有两个数组,一个使用一个索引从哈希表加载,另一个以某种方式写入这个哈希表并产生一些值。如果你想并行化这个 while 循环,作为程序员,你不知道一个循环迭代(迭代 1)是否独立于另一个循环迭代。可能第一个迭代写入一个索引,而下一个迭代读取该索引,或者第一个迭代写入一个索引,而第 25 个迭代读取该索引。但你不知道,因为这些索引依赖于程序的输入数据值。因此,作为程序员,很难通过将循环迭代作为不同核心上的不同线程执行来并行化程序。但使用线程级推测,可能发生的情况是,基本思想是推测性地执行连续迭代和连续处理器。因此,处理器 1 在这里执行迭代 1(称为 E1),处理器 2 执行 E2(第二次迭代),处理器 3 开始执行 E3,处理器 4 开始执行 E4。它们都动态并行执行。基本上,这些依赖被跟踪。例如,处理器 1 从哈希索引 3 读取,这没问题,没有其他处理器写入索引 3。处理器 2 从 19 读取,处理器 3 从 33 读取,处理器 4 从 10 读取。因此,第四次迭代从索引值 10 读取,结果第一次迭代实际上写入索引值 10。因此,需要以某种方式检测到这种依赖,所以第四次迭代不能与第一次迭代真正并行执行,因为它们之间存在依赖。第四次迭代应该获得正确的值,而它可能执行并获得错误的值。因为在这里,如果你看时间,第四次迭代的哈希 10 读取执行得比第一次迭代的哈希 10 存储更早。因此,在这种情况下,需要检测到这种违规,因为在这个 while 循环的顺序执行中,第一次迭代应该在第四次迭代之前执行。这就是线程级推测的思想。你如何实际检测这种违规?想法是使用一致性协议来检测这种违规,我们会回到这一点,但假设没有违规,假设动态地这些值使得没有线程写入一个后来被其他线程读取的索引。在这种情况下,所有这些线程在迭代结束时尝试提交,这个尝试提交函数基本上检查是否有违规。如果完全没有违规(违规意味着该线程读取的数据值实际上是由其他线程写入的,并且该线程由于推测性过早执行而获得了错误的值),那么这些线程可以提交。这种提交需要按顺序进行,因为你需要保持程序的顺序语义,记住这又是一个串行程序。这些线程实际上按顺序提交,线程提交假设前一个线程已提交且没有违规。如果存在违规,需要被检测,并通过一致性协议检测到。当这个处理器实际读取哈希 10 时,它不知道其他人已经写入。但当这个处理器后来写入这个索引 10(内存地址索引 10)时,它基本上向这个处理器发送一个无效请求,而这个处理器如果已经读取(记住原则:当你进行推测读取时,你在缓存或某个位置标记表示“我实际上推测读取了这个值”),如果后来从更早的 E(更早的处理器)收到无效请求(因为处理器也可以执行更晚的迭代,而更早的 E 实际上写入了一个被更晚的 E 推测读取的位置),你可以通过一致性协议确定这一点。一致性消息协议向该地址发送无效消息,该缓存接收该无效消息,并检查该块是否实际上被推测读取。如果该块实际上被推测读取(在本例中就是这种情况),并且你收到来自更早 E 的无效请求,这意味着你推测性地读取了一个值,而其他人本应更早写入它,因此这是一个数据依赖违规,你读取了错误的值。一旦你读取了错误的值,现在你需要取消这个线程,这个 Epoch。这展示了实际发生的情况。因此,我将再次讲解这张幻灯片。基本上,这是处理器 1 和处理器 2,假设 E5 和 E6 正在执行,E6 是这里更早的 E(逻辑顺序上更早),E5 是更早的。E6 推测性执行,它基本上从这个内存位置加载。一旦它进行加载,它需要发送读取请求并在其缓存中获取数据缓存块。每个缓存块添加了两个位,表示该缓存块是否被推测加载,以及是否被推测修改,正如我们之前讨论存储和加载以及内存通信时所说的。当这个处理器进行加载时,这个推测加载位被设置为一。它被设置为一,后来,假设这个处理器继续用加载的值执行,它获得加载的值,但这是推测性的,可能不是正确的值。后来,E5 在处理器 1 上执行。它基本上进行存储到相同的位置(Q 和 P 相同,但这些都是指针,所以你无法静态知道位置,因此无法并行化这个程序)。它进行存储,将值从 1 改为 2(本例中加载的值被设置为 1)。它将推测修改位设置在其缓存中,但这在这里不重要,那是用于其他目的。因为它进行存储,它需要向所有其他拥有该位置的处理器发送无效消息。记住,这是一个缓存一致性的共享内存机器,当你对缓存进行存储时,这是一个基于无效的协议。它基本上向所有通过目录协议或侦听协议拥有该位置的其他处理器发送无效消息。这个无效消息到达执行 E6 的处理器的 L1 缓存。无效消息还包含关于哪个 E 正在使其无效的信息,因此它附加了这个 E5 标签。当缓存收到这个消息时,它检查:“哦,这个消息是发送给一个推测加载的块,该块设置了这位,并且它来自一个逻辑上比我所拥有的 Epoch 更早的 Epoch,这意味着我可能推测性地加载了不正确的值。” 在这种情况下,你检测到这是一个违规。一旦收到这个无效消息,你就知道你读取了错误的值,违规被检测到,因此违规信号在这里被设置为真。此时,你可以开始取消所有内容,但在这篇论文中,他们没有这样做。他们做的是等待这个尝试提交时间,这个尝试提交时间实际上检查缓存中的违规位是否被设置,如果违规位被设置,则开始恢复过程。但基本上,它刷新整个 Epoch 并取消所有后续的 Epoch。这就是你如何使用缓存一致性通过无效消息检测读后写依赖违规。如果这是一个基于更新的协议,你可以做同样的事情,甚至可以更聪明。如果是基于更新的协议,这个 E 会向该位置发送一个带有其 Epoch ID 和数据的更新请求。你仍然会知道这是推测加载的,并且你可以说,如果你加载的数据值与发送的数据值不匹配,那么你得到了错误的值,存在违规。如果数据值恰好匹配,那么你碰巧得到了正确的值,因为实际上有很多存储是静默的(这些称为静默存储)。Kevin Lepak 在 MICRO 2001 年有一篇关于静默存储的论文,我鼓励你阅读。这基本上意味着存储将相同的值存储到该位置(与该位置之前的值相同)。因此,基于更新的协议可以有这种优化,它可以检查数据值是否已更改,而不是只检查地址是否将被修改为某个未知值。这就是思想。这就是你如何通过利用底层的一致性协议来检测线程级推测中的冲突。

这篇论文实际上展示了一些结果,我简要介绍一下。我鼓励你阅读这篇论文,实际上有一些有希望的结果。这些是一些应用程序,这是在单芯片多处理器(四处理器单多核机器)上的结果。你可以看到加速比,并行区域在这里,你在并行区域获得的加速比,这只是并行覆盖率。因此,如果你看到并行区域的覆盖率不是很高,这意味着阿姆达尔定律从一开始就限制了加速比。但这些结果显示,例如,在一些应用程序中,使用四个处理器可以获得显著的性能收益,高达 46% 的性能收益,并且随着添加更多处理器,性能收益持续增加。记住,这纯粹是通过线程级推测完成的,它是一个在单线程应用程序上并行化的应用程序。例如,在 JPEG 压缩中,整个程序上收益较少(8%),但在并行区域上收益很高(94%)。你可以看到随着处理器数量增加的速度曲线,这是单线程版本,最多到 8 个处理器,这是程序并行区域的执行时间。你可以看到这个应用程序在并行区域实际上扩展了,但有些情况下,例如这里,性能增加到四个线程,但随着增加到六个和八个线程,性能开始下降,执行时间开始增加。事实上,八个线程的执行时间比一个线程更差。为什么会发生这种情况?这可能是由于多种原因,但在这种情况下,线程间存在大量依赖,因此有大量取消。这是一个原因。由于依赖,数据分布在私有缓存中,你得到大量无效,并且还会发生带宽争用。这些基本上是由于我们在并行区域瓶颈中讨论的问题,论文中实际上讨论了这些。但像这样的线程级推测方法相当有希望。缺点是,正如你所见,优点是你可以在并行区域获得显著的加速,缺点是有些加速实际上很低,并且系统中的复杂性可能很高。因此,我鼓励你思考优点和缺点。

让我们转到另一篇奠基性论文,这实际上是一篇更早的讨论线程级推测的论文,称为多标量处理器。关键思想是挖掘串行程序中的隐式线程级并行性,就像我们之前讨论的那样,但这里的方法不同。在这种情况下,编译器将程序划分为任务,任务可以定义不清,但我们会讨论。这些任务基本上是单线程程序的部分。如果你将单线程程序视为执行一系列由静态指令块组成的动态指令,这些任务就是这些块的任意子集。因此,假设这是你的程序,这些是一些指令块,你的任务基本上是程序中的块,任务 0、1、2、3、4,它们可以是循环迭代,也可以是函数。但关键点是,这里是顺序执行。任务被调度到独立的处理资源上,而不是在相同的处理资源上执行。线程 0 去一个处理器,线程 1 去另一个,线程 2 去另一个,等等,类似于之前。硬件处理任务间的寄存器依赖,因为这些来自同一个程序,可能存在寄存器依赖(这里写入寄存器 R2,那里读取寄存器 R2),可能有许多依赖。硬件处理寄存器依赖,但编译器指定哪些寄存器应在任务间通信,因为编译器知道这些信息。记住,寄存器依赖是编译器已知的。编译器在创建任务时,基本上会说:“这些是我需要的寄存器,这些是我可能产生或可以产生的寄存器。” 这些信息被传达给硬件,使得硬件可以在线程间通信,我们将看到一个例子。内存依赖通过内存推测处理,硬件检测并解决任务间的错误推测,并通过我们将讨论的称为地址解析缓冲区的东西实现。

因此,我实际上推荐这里的这两篇论文,但必读论文是 Sohi 等人在 ISCA 1995 年发表的《多标量处理器》,这是一篇较早的论文。《用于挖掘细粒度隐式并行性的可扩展分割窗口范式》论文中的许多概念还不够成熟,但也发表在 ISCA 1992 年。当然,“多标量”这个名字更吸引人。“用于挖掘细粒度隐式并行性的可扩展分割窗口范式”听起来不那么吸引人,但当你说“多标量”时,它开始变得更吸引人。总之,这是个玩笑。但基本上,多标量论文中的这个图清楚地描述了它们试图做什么。

如果你想拥有一个单线程程序并想并行执行操作,你希望一个接一个地执行指令,并拥有一个大的窗口或执行范围来实际提升程序的性能,通过乱序执行提升程序的性能,通过查看大的指令窗口来提升程序的性能。我们已经讨论过为什么构建大窗口更困难,这导致了向多核发展以提升性能,因为通过构建大指令窗口来提升性能是困难的。现在,如果你有一个大的指令窗口,并且你想每个周期执行多条指令,其中一个限制是,例如,如果你想每个周期执行 100 条指令,你需要查看指令流中的许多指令,并且还需要每个周期 100 条指令的宽度,还需要一个支持每个周期执行 100 条指令的寄存器文件,这意味着你需要有 200 个端口(假设每条指令有两个输入值,两个源寄存器)。构建这种多端口寄存器文件是困难的,这是使单核单处理器不具吸引力的技术推动因素之一,类似的原因在 1992 年也出现了。基本上,200 端口的寄存器文件不可扩展。那么,为什么我们不将这个大的指令窗口分割成小块,并在不同的处理器中并行执行不同的部分呢?处理器 0 执行任务 0,处理器 1 执行任务 1,处理器 2 执行任务 2,处理器 3 执行任务 3。现在每个处理器查看一个较小的窗口,但你可以并行处理整个窗口,也许是类似大小的窗口。额外的好处是每个处理器有一个更简单的寄存器文件。为什么?因为现在为了获得相同的带宽,假设你想每个周期总共执行 100 条指令(我这里非常激进),现在你每个处理器只需要执行 25 条指令,这意味着你只需要 50 个端口。当然,如果你将其缩放到合理的值,假设你想总共执行 16 条指令,如果你有四个处理器,每个处理器只需要执行 4 条指令,这意味着每个寄存器文件只需要 8 个端口,这实际上更容易接受。这就是思想。基本上,现在我们有一个分布式寄存器文件,更小的窗口,你将大窗口分割成更小的窗口,通过多个硬件上下文提取并行性。你拥有这个分布式寄存器文件,但你需要以某种方式确保寄存器文件中的通信正确进行,我们将看看多标量是如何做到这一点的。我已经给了你基本思想。

关键思想是通过将顺序指令

posted @ 2026-03-29 09:13  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报