# 计算机体系结构 *计算机体系结构是关于如何构建执行指令的机器。本文涵盖数制、逻辑门、CPU设计、指令集架构、流水线、存储器层次结构和虚拟内存——每个程序、框架和AI模型最终运行其上的硬件基础。* - 每个神经网络、每个训练循环、每次推理调用最终都会变成流经晶体管的电信号序列。对于严肃的机器学习从业者来说,理解硬件不是可选的:它解释了为什么矩阵乘法很快,为什么内存是瓶颈,为什么GPU主导AI训练,以及为什么缓存友好的代码可以比朴素代码快100倍。 ## 数制 - 计算机将所有内容表示为**二进制**(基2):0和1的序列。每个数字是一个**比特**。8个比特为一组称为一个**字节**。二进制数 $b_{n-1} b_{n-2} \ldots b_1 b_0$ 的值为 $\sum_{i=0}^{n-1} b_i \cdot 2^i$。 - 例如,$1011_2 = 1 \cdot 8 + 0 \cdot 4 + 1 \cdot 2 + 1 \cdot 1 = 11_{10}$。 - **十六进制**(基16)是二进制的紧凑表示法。每个十六进制数字代表4个比特:$0\text{-}9$ 映射到 $0000\text{-}1001$,$A\text{-}F$ 映射到 $1010\text{-}1111$。因此 $\text{0xFF} = 1111\,1111_2 = 255_{10}$。内存地址和颜色代码通常用十六进制书写。 - **补码**表示有符号整数。对于$n$位数字,最高有效位的权重为 $-2^{n-1}$ 而非 $+2^{n-1}$。8位补码的范围为 $-128$ 到 $+127$。要取一个数的相反数:翻转所有位然后加1。这种表示使加法和减法使用相同的硬件电路,这就是它被普遍采用的原因。 - **IEEE 754浮点数**将实数表示为 $(-1)^s \times 1.m \times 2^{e-\text{bias}}$,其中$s$是符号位,$m$是尾数(小数部分),$e$是移码指数。 ![IEEE 754 float32布局:1个符号位、8个指数位、23个尾数位](../images/ieee754_float.svg) - **float32**(单精度):1个符号 + 8个指数 + 23个尾数 = 32位。范围:$\approx \pm 3.4 \times 10^{38}$,精度:$\approx 7$位十进制数字。 - **float64**(双精度):1个符号 + 11个指数 + 52个尾数 = 64位。范围:$\approx \pm 1.8 \times 10^{308}$,精度:$\approx 15$位十进制数字。 - **float16**(半精度):1 + 5 + 10 = 16位。范围和精度有限,但使用一半的内存和带宽。广泛用于ML训练(混合精度,第6章)。 - **bfloat16**:1 + 8 + 7 = 16位。与float32相同的指数范围但精度更低。由Google专门为ML设计:完整的指数范围可防止训练期间溢出,降低的精度对梯度更新是可以接受的。 - 浮点算术**不精确**。在float64中,$0.1 + 0.2 \neq 0.3$(它等于 $0.30000000000000004$)。这是因为$0.1$没有精确的二进制表示,就像$1/3$没有精确的十进制表示一样。在数百万次操作(如梯度下降)中积累这些误差可能导致数值不稳定,这就是为什么存在像损失缩放(第6章)和Kahan求和法这样的技术。 ## 逻辑门 - 所有计算都可以归结为**逻辑门**:实现布尔运算(来自文件1的命题逻辑)的物理电路。 - 基本门: - **与门**(AND):仅当两个输入都为1时输出为1。 - **或门**(OR):至少一个输入为1时输出为1。 - **非门**(NOT,反相器):翻转输入。 - **与非门**(NAND,NOT-AND):通用门。任何其他门都可以仅由与非门构建。这就是为什么与非门是数字电路的基本构建块。 - **异或门**(XOR,异或):输入不同时输出为1。对于加法(二进制加法的和位就是XOR)和加密至关重要。 - **半加器**使用XOR(和)和AND(进位)相加两个单比特。**全加器**相加两个比特加上一个进位输入,可以串联起来创建$n$位加法器。这就是CPU执行整数加法的方式:一系列简单逻辑门的级联。 - **多路选择器**(MUX)根据控制信号从多个输入中选择一个。使用$n$个控制位,可以从$2^n$个输入中选择。多路选择器是if-else链的硬件等价物,广泛用于CPU数据通路中路由数据。 - 现代处理器包含数十亿个晶体管,每个晶体管充当一个微小的开关。晶体管要么导通(导电,表示1),要么不导通(不导电,表示0)。门由晶体管构成,加法器由门构成,ALU由加法器构成,CPU由ALU构成。整个计算层级就建立在这个基础之上。 ## CPU架构 - **中央处理器(CPU)**执行指令。其核心组件: - **ALU**(算术逻辑单元):执行整数算术(加、减、乘)和逻辑运算(AND、OR、XOR、移位)。这里是实际计算发生的地方,由上述逻辑门构建而成。 - **寄存器**:CPU内部微小、超快的存储位置。现代CPU有数十个通用寄存器,每个寄存器保存一个字(在64位CPU上为64位)。寄存器是系统中速度最快的存储器:访问时间约~0.3纳秒。 - **程序计数器(PC)**:保存下一条要执行指令的内存地址。 - **控制单元**:解码指令并编排数据通路,告诉ALU执行什么操作以及使用哪些寄存器。 - **指令周期**(取指-译码-执行)每秒重复数十亿次: 1. **取指**:从PC中的地址读取指令。 2. **译码**:确定指令的功能(加法?从内存加载?分支?)及其使用的操作数。 3. **执行**:执行操作(ALU计算、内存访问或分支)。 4. 增加PC(除非指令是分支/跳转)。 - 运行在4 GHz的CPU每秒执行40亿个周期。每个周期耗时0.25纳秒。在这段时间内,光传播约7.5厘米,这就是芯片物理大小重要的原因:信号无法在一个周期内穿过大芯片。 ## 指令集架构 - **指令集架构(ISA)**是硬件和软件之间的契约:它定义了CPU能理解的指令、寄存器集、内存模型和编码格式。 - **CISC**(复杂指令集计算机):指令可以复杂、变长,并可以直接访问内存。一条指令可以乘法两个内存值并存储结果。**x86**(Intel/AMD)是占主导地位的CISC ISA,驱动着大多数桌面和服务器。其向后兼容性(现代x86 CPU仍然运行1980年代的代码)既是优势也是负担。 - **RISC**(精简指令集计算机):指令简单、定长,且仅操作寄存器。内存访问需要单独的加载/存储指令。更简单的指令可实现更快的时钟速度和更易实现的流水线。 - **ARM**:移动设备的主要RISC ISA,并越来越多地用于服务器和笔记本电脑(Apple M系列芯片就是ARM)。ARM的能效使其非常适合电池供电和热受限设备。 - **RISC-V**:一个开源的RISC ISA。任何人都可以设计RISC-V芯片而无需许可费。在嵌入式系统、研究和AI加速器中的采用正在增长。 - CISC与RISC的区别已经模糊:现代x86 CPU内部将复杂的CISC指令解码为更简单的微操作(本质上是内部RISC),从而获得两方面的优势。 ## 流水线 - 没有流水线时,CPU完全完成一条指令后才开始下一条。这会浪费硬件:当ALU执行时,取指和译码单元处于空闲状态。 ![CPU流水线:指令在取指、译码、执行、访存和写回阶段重叠](../images/cpu_pipeline.svg) - **流水线**使指令执行重叠,如同装配线。当指令1在执行时,指令2在译码,指令3在被取指。一个5级流水线(取指、译码、执行、访存、写回)可以同时有5条指令在执行中。 - 吞吐量接近每周期一条指令(尽管每条指令需要5个周期才能完成)。这与ML中的流水线原理相同:数据并行性使计算和通信重叠(第6章)。 - **冒险**是流水线被破坏的情况: - **数据冒险**:指令2需要指令1尚未产生的结果。"Add R1, R2, R3"后跟"Sub R4, R1, R5"——第二条指令需要R1,而第一条指令仍在计算。**转发**(旁路)通过将结果直接从一级流水线路由到另一级,无需等待写回阶段来解决这个问题。 - **控制冒险**:分支指令(if-else)意味着CPU在分支解析之前不知道应该取指哪条下一条指令。**分支预测**猜测分支将走哪条路径,并推测性地沿预测路径取指。现代预测器准确率超过95%,使用历史表和类似神经网络的模式匹配。一次预测错误代价约~15个周期(流水线必须被清空并重启)。 - **结构冒险**:两条指令同时需要相同的硬件资源(例如,都需要内存端口)。通过复制资源或插入停顿来解决。 ## 存储器层次结构 - 计算机内存中的根本矛盾:快速内存昂贵且容量小,廉价内存缓慢但容量大。**存储器层次结构**通过利用**局部性**来弥合这一差距:程序倾向于重复访问相同的数据(时间局部性)并访问附近的数据(空间局部性)。 ![存储器层次结构金字塔:寄存器在顶部(快速、小)到HDD在底部(慢、大)](../images/memory_hierarchy.svg) - 层次结构,从最快到最慢: - **寄存器**:~0.3 ns访问,总容量~KB。位于CPU内。 - **L1缓存**:~1 ns,每核心32-64 KB。分为指令缓存和数据缓存。 - **L2缓存**:~4 ns,每核心256 KB-1 MB。 - **L3缓存**:~10 ns,跨核心共享8-64 MB。 - **RAM(DRAM)**:~50-100 ns,8-512 GB。主内存。 - **SSD**:~10-100 μs,256 GB-8 TB。持久存储。 - **HDD**:~5-10 ms,1-20 TB。机械式,随机访问非常慢。 - 寄存器和RAM之间的速度差距约为300倍。寄存器和磁盘之间约为30,000,000倍。缓存层次结构隐藏了这一差距:如果CPU需要的数据在L1缓存中(**缓存命中**),访问很快。如果不在(**缓存未命中**),CPU停顿,同时从更慢的层级获取数据。 - **缓存关联度**决定内存地址可以存储在缓存中的位置: - **直接映射**:每个地址映射到恰好一个缓存行。简单但会导致冲突。 - **全关联**:任何地址可以放在任何位置。灵活但搜索成本高。 - **组关联**($k$路):每个地址映射到一组$k$个位置。实际CPU中使用的实用折衷方案(通常为4路或8路)。 - **缓存一致性**确保所有CPU核心看到一致的内存视图。当核心1写入一个核心2已缓存的内存地址时,一致性协议(如MESI)会使核心2的副本失效或更新。这对并发编程(文件4)至关重要,也是共享内存并行性困难的原因之一。 - 对于ML从业者,存储器层次结构解释了为什么: - 矩阵运算应按顺序访问内存(行优先与列优先的布局很重要)。 - 批量大小会影响性能:更大的批次分摊内存延迟。 - 混合精度(float16/bfloat16)使有效内存带宽翻倍,而内存带宽往往是瓶颈。 ## 虚拟内存 - **虚拟内存**使每个进程仿佛拥有自己独立、连续的大内存空间,即使物理RAM是有限的并在进程间共享。 - 地址空间被划分为固定大小的**页**(通常为4 KB)。**页表**将虚拟页号映射到物理帧号。当程序访问虚拟地址0x1234时,CPU通过查找页表将其转换为物理地址。 - **转译后备缓冲器(TLB)**是页表项的缓存。由于页表位于RAM中(慢速),TLB在快速硬件中存储最近使用的转译结果。TLB未命中需要遍历内存中的页表,耗费数百个周期。 - 当程序访问一个不在物理RAM中的页时,发生**缺页**。OS从磁盘加载该页(交换),耗费数百万个周期。过多的缺页(**系统颠簸**)会严重损害性能。这就是为什么ML训练需要足够的RAM来容纳模型、优化器状态和合理的数据批次。 - **页面置换**算法决定当RAM满时应换出哪个页面: - **LRU**(最近最少使用):换出最长时间未被访问的页面。在实践中对大多数工作负载最优。在硬件中通过**时钟算法**(带引用位的循环链表)近似实现。 - **FIFO**:换出最旧的页面。简单但可能换出频繁使用的页面。 - **最优**(Bélády算法):换出将在最长时间内不被使用的页面。无法实现(需要未来知识)但可作为理论基准。 - 虚拟内存还提供了**隔离**:每个进程都有自己的虚拟地址空间。一个进程中的错误不会破坏另一个进程的内存,因为它们的虚拟地址映射到不同的物理帧。这是OS安全性和稳定性的基础。 ## I/O、中断和DMA - CPU需要与外部世界通信:磁盘、网卡、键盘、GPU。这就是**I/O子系统**。 - **程序控制I/O**(轮询):CPU在一个循环中反复检查设备的状态寄存器,等待数据就绪。简单但浪费CPU周期做空转而不是有用工作。 - **中断驱动I/O**:设备在数据就绪时发送一个硬件**中断**。CPU继续正常执行直到中断到达,然后运行一个**中断处理程序**(内核函数)来处理数据。这比轮询高效得多,因为CPU在等待时不会空闲。 - 中断机制: 1. 设备通过硬件线路发出中断信号。 2. CPU完成当前指令,将当前状态(寄存器、程序计数器)保存到堆栈。 3. CPU在**中断向量表**(每个中断类型对应一个函数指针的表)中查找中断处理程序地址。 4. 处理程序在内核模式下运行,处理I/O,然后返回。 5. CPU恢复保存的状态并恢复被中断的程序。 - 这与上下文切换(文件3)的保存/恢复模式相同,但由硬件而非定时器触发。 - **DMA**(直接存储器访问):对于大数据传输(磁盘读取、网络数据包、GPU内存复制),让CPU逐字节复制数据是浪费的。**DMA控制器**直接在设备和RAM之间传输数据,无需CPU参与。CPU设置传输(源地址、目标地址、大小),DMA控制器处理传输,完成后CPU收到一个中断。 - DMA对ML至关重要:当你调用 `model.to('cuda')` 时,数据通过PCIe总线上的DMA从系统RAM传输到GPU内存。在训练期间,跨GPU的梯度同步使用基于DMA的RDMA(远程DMA)进行高带宽、低延迟传输(第6章)。 - **总线**将CPU连接到内存和I/O设备。现代系统使用**PCIe**(快速外设组件互连)连接高速设备(GPU、NVMe SSD、网卡)。PCIe 4.0在每个x16插槽上提供约~32 GB/s;PCIe 5.0将其翻倍。总线带宽通常是GPU训练的瓶颈:GPU的计算速度可能快于数据送达的速度。 - **MMIO**(内存映射I/O):设备寄存器被映射到内存地址。CPU使用普通的加载/存储指令对这些地址进行读写,硬件将访问路由到设备而不是RAM。这统一了内存和I/O访问为一个单一机制,简化了硬件和软件。 ## 编程任务(使用CoLab或笔记本) 1. 探索IEEE 754浮点数表示。将浮点数转换为二进制表示,观察符号、指数和尾数字段。 ```python import struct def float_to_bits(f): """显示float32的IEEE 754二进制表示。""" packed = struct.pack('>f', f) bits = ''.join(f'{byte:08b}' for byte in packed) sign = bits[0] exponent = bits[1:9] mantissa = bits[9:] return sign, exponent, mantissa for val in [1.0, -1.0, 0.1, 0.5, 3.14, float('inf'), float('nan')]: s, e, m = float_to_bits(val) print(f"{val:>10} sign={s} exp={e} ({int(e, 2) - 127:>4d}) mantissa={m[:10]}...") ``` 2. 模拟直接映射缓存。跟踪一系列内存访问的命中与未命中。 ```python def simulate_cache(accesses, cache_size=8, block_size=1): """模拟直接映射缓存。""" cache = [None] * cache_size hits, misses = 0, 0 for addr in accesses: cache_line = addr % cache_size if cache[cache_line] == addr: hits += 1 status = "HIT " else: misses += 1 cache[cache_line] = addr status = "MISS" print(f" Access {addr:3d} → line {cache_line}: {status}") print(f"\nHits: {hits}, Misses: {misses}, Hit rate: {hits/(hits+misses):.1%}") # 顺序访问(良好的局部性) print("顺序访问:") simulate_cache([0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3]) # 跨步访问(冲突未命中) print("\n跨步访问(stride = cache size):") simulate_cache([0, 8, 0, 8, 0, 8]) ``` 3. 演示为什么浮点算术不满足结合律。展示 $(a + b) + c \neq a + (b + c)$ 的情况。 ```python import jax.numpy as jnp a = jnp.float32(1e8) b = jnp.float32(1.0) c = jnp.float32(-1e8) left = (a + b) + c # (1e8 + 1) + (-1e8) right = a + (b + c) # 1e8 + (1 + (-1e8)) print(f"(a + b) + c = {left}") # 应为 1.0 print(f"a + (b + c) = {right}") # 可能会丢失 1.0 print(f"Equal: {left == right}") print(f"\n当 1.0 加到 1e8 上时被丢失,因为 float32 只有约 7 位精度") ```