计算机体系结构

Memory Consistency and Cache Coherence

Memory Consistency and Cache Coherence

Memory Consistency and Cache Coherence 定义

1. Memory Consistency(内存一致性)


2. Cache Coherence(缓存一致性)


3. 主要区别

特性 Memory Consistency(内存一致性) Cache Coherence(缓存一致性)
关注点 内存操作的全局顺序和可见性 多个缓存副本之间的数据一致性
范围 涉及所有内存操作(读/写)的顺序 仅涉及缓存和内存之间的数据一致性
问题背景 多个处理器对共享内存的访问顺序是否一致 多个处理器的缓存中数据是否一致
解决方案 内存一致性模型(如顺序一致性、弱一致性) 缓存一致性协议(如MESI、写失效、写更新)

4. 总结

Memory Consistency and Cache Coherence

TSO(Total Store Ordering)内存模型

  1. TSO(Total Store Ordering)是一个被广泛使用的内存模型
  2. 并在x86架构中使用,RISC-V也提供了TSO扩展,即RVTSO,人们普遍认为x86内存模型等同于TSO,然而Intel和AMD从来没有保证这一点
  3. x86选择放弃SC(顺序一致性sequential consistency),以更好地支持基于FIFO的write buffer,用于加速性能
  4. TSO和SC最关键的区别就是store可能被放入write buffer中且允许load的bypass
    1. 对于单核视角来说,和SC没有区别,执行顺序和程序顺序一致
    2. 对于多核来说,需要额外的手段来保证memory consistency
  5. 由于TSO只允许store-load的重排,因此需要使用FENCE的场合不多,FENCE的实现方式也不是很重要。一种简单的实现是,当FENCE被执行时,清空write buffer,并在FENCE提交之前不再执行后面的load指令。
Memory Consistency and Cache Coherence

x86的多核宽松内存一致性模型

  1. 被修饰的汇编指令成为“原子的”

    1. 本身是原子指令,比如“XCHG”和“XADD”汇编指令
    2. 本身不是原子指令,但是被LOCK指令前缀修饰后成为原子指令,比如LOCK CMPXCHG
    3. 被修饰的汇编指令A在执行期间,会在内存总线上声言一个#LOCK信号,该信号导致内存被锁住,此时内存不能再被其他汇编指令存取,直到A执行完成。经过分析可知,A的执行效果与“暂停执行其他所有汇编指令直到A执行完成等价,因此此时A是原子的
  2. fence

    1. sfence: 在sfence指令前的写操作当必须在sfence指令后的写操作前完成
    2. lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成
    3. mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成
  3. C++11中支持的内存模型

    1. Acquire-Release能保证不同线程之间的Synchronizes-With关系,同时也约束到同一个线程中前后语句的执行顺序。
    2. Release-Consume只约束有明确的carry-a-dependency关系的语句的执行顺序,同一个线程中的其他语句的执行先后顺序并不受这个内存模型的影响
enum memory_order {
    memory_order_relaxed, // Relaxed
    memory_order_consume, // Release-Consume
    memory_order_acquire, // Acquire-Release 用来修饰一个读操作,表示在本线程中,所有后续的关于此变量的内存操作都必须在本条原子操作完成后执行
    memory_order_release, // Acquire-Release 用来修饰一个写操作,表示在本线程中,所有之前的针对该变量的内存操作完成后才能执行本条原子操作
    memory_order_acq_rel, // Acquire-Release 同时包含memory_order_acquire和memory_order_release标志
    memory_order_seq_cst  // 顺序一致性模型
};

einsum

两个基本概念

自由索引(Free indices)和求和索引(Summation indices):

三条基本规则

特殊规则

特殊规则有两条:

a = torch.randn(2,3,5,7,9)
# i = 7, j = 9
b = torch.einsum('...ij->...ji', [a])

二次变换(bilinear transformation)

np_a = a.numpy()
np_b = b.numpy()
np_c = c.numpy()
np_out = np.empty((2, 5), dtype=np.float32)

np_out = torch.einsum('ik,jkl,il->ij', [a, b, c]).numpy()
# ik broadcast成ikl
# il broadcast成 ikl
# 'ik,jkl,il->ij'可以理解成'ikl,jkl,ikl->ij'

for i in range(0, 2):
    for j in range(0, 5):
        # 求和索引内循环  这里是 k 和 l
        sum_result = 0
        for k in range(0, 3):
            for l in range(0, 7):
                sum_result += np_a[i, k] * np_b[j, k, l] * np_c[i, l]
        np_out[i, j] = sum_result

总结

a = torch.rand(2,3)
b = torch.rand(3,4)
c = torch.einsum("ik,kj->ij", [a, b])
# 等价操作 torch.mm(a, b)

equation 中的字符也可以理解为索引,就是输出张量的某个位置的值,是怎么从输入张量中得到的,比如上面矩阵乘法的输出 c 的某个点 c[i, j] 的值是通过 a[i, k] 和 b[k, j] 沿着 k 这个维度做内积得到的。

RAM

DRAM

  1. 电容
  2. 带宽不是很高
  3. 需要刷新,会有颠簸

SRAM

  1. 面积和功耗不能和工艺平行
  2. 类型
    1. Cpu register Flip Flops 每个bit都有一读一写
    2. L1/L2 SRAM 6个晶体管,一般最多每个bank一读一写
    3. L3/L4 eDRAM/GCRAM 4晶体管/电容 读写速度明显降低,也会有使用上的问题

NoC

OpenSMART

https://github.com/hyoukjun/OpenSMART/tree/master

connect

https://users.ece.cmu.edu/~mpapamic/connect/

https://github.com/crossroadsfpga/connect/tree/main

Flexnoc

is a commercial NoC generator by Arteris which generates a customized topology for each SoC,

Cache写机制 Write-through与Write-back

Cache写机制分为write through和write back两种。

Write-through: Write is done synchronously both to the cache and to the backing store. Write-back (or Write-behind) : Writing is done only to the cache. A modified cache block is written back to the store, just before it is replaced. Write-through(直写模式)在数据更新时,同时写入缓存Cache和后端存储。此模式的优点是操作简单;缺点是因为数据修改需要同时写入存储,数据写入速度较慢。

Write-back(回写模式)在数据更新时只写入缓存Cache。只在数据被替换出缓存时,被修改的缓存数据才会被写到后端存储。此模式的优点是数据写入速度快,因为不需要写存储;缺点是一旦更新后的数据未被写入存储时出现系统掉电的情况,数据将无法找回。

Write-misses写缺失的处理方式 对于写操作,存在写入缓存缺失数据的情况,这时有两种处理方式:

Write allocate (aka Fetch on write) – Datum at the missed-write location is loaded to cache, followed by a write-hit operation. In this approach, write misses are similar to read-misses. No-write allocate (aka Write-no-allocate, Write around) – Datum at the missed-write location is not loaded to cache, and is written directly to the backing store. In this approach, actually only system reads are being cached. Write allocate方式将写入位置读入缓存,然后采用write-hit(缓存命中写入)操作。写缺失操作与读缺失操作类似。

No-write allocate方式并不将写入位置读入缓存,而是直接将数据写入存储。这种方式下,只有读操作会被缓存。

无论是Write-through还是Write-back都可以使用写缺失的两种方式之一。只是通常Write-back采用Write allocate方式,而Write-through采用No-write allocate方式;因为多次写入同一缓存时,Write allocate配合Write-back可以提升性能;而对于Write-through则没有帮助。

处理流程图

Write-through模式处理流程:A Write-Through cache with No-Write Allocation

[![image.png](Cache写机制 Write-through与Write-back/BLAimage-png.png)](Cache写机制 Write-through与Write-back/BLAimage-png.png)

Write-back模式处理流程:A Write-Back cache with Write Allocation

[![image.png](Cache写机制 Write-through与Write-back/aCyimage-png.png)](Cache写机制 Write-through与Write-back/aCyimage-png.png)

人工智能的产业

  1. 模型算法
    1. 科研
    2. 企业商用
    3. 数据收集、标注
  2. 软件框架
    1. 科研
    2. 商业部署
  3. 加速芯片
    1. 云训练芯片
    2. 云推理
    3. 边沿推理
  4. 云服务
    1. 基础软件框架
    2. 硬件
    3. 服务
  5. 应用
    1. 各种AI云服务

    2. 边缘AI

    3. 特定领域

      1. 基础设施:安防
      2. 自动驾驶、机器人
      3. 工业、医疗、教育、金融

Nand flash

LUN → CE → Die → Plane → Bank → Block → Page​

  1. Block 是擦除操作的最小单位
  2. Page 是读写操作的最小单位,常见的Nand Flash多数是2KB,最新的是4KB、8KB
  3. 硬件电路只支持 “一次性操作一个 Page 的所有单元”,因此读写必须以 Page 为单位
  4. 写入前必须先擦除对应的Block
  5. 栅极堆叠(Gate-Stacked) 和沟道堆叠(Channel-Stacked)两种结构均需解决 “垂直互联”,如何连接不同层的字线 WL 和位线 BL
  6. WL BL SSL 三个控制线构成三维浮栅晶体管的索引信号,确定WL 和SSL,读写所有的BL,也就是一个Page
  7. 不同WL的晶体管是串联在一起的,除了需要读取的管,其他的会在读取的时候被强制导通,避免影响需要读取的晶体管
WL(Word Line,字线)
BL(Bit Line,位线)
SSL GSL(源极选择,漏级选择)
MAC计算
  1. input :SSL
    1. 每个SSL表示input的一位
  2. output :BL
    1. 每个BL表示output的1位,
    2. 8个连续的BL组成一个8位的输出
    3. BL经过高精度ADC量化后,再根据每个bit的二进制单位分别进行左移,再累加,得到乘法的结果
  3. weight : cell中存储的数据
  4. WL :选中一批权重
单次MAC计算规模
  1. 矩阵乘法 MNK
  2. SSL input: 所有乘完需要累加的input的指定位,input矩阵的一行(K个)的每个指定位
    1. 拆分input的 K
  3. BL output:一个BL表示一批input的一位 和weight的位乘加的结果,一次性最多输出 N个结果的1位
    1. 拆分output的 N
  4. WL weight:选中一批权重的指定位,可以用来选定权重批次,或者权重的位index
    1. 支持用于拆分权重的 N 或者 bit index

[![b81dc059-2002-4e2b-932e-073924c856af.jpg](Nand flash/b81dc059-2002-4e2b-932e-073924c856af.jpg)](Nand flash/b81dc059-2002-4e2b-932e-073924c856af.jpg)

acquire release 实现内存一致性

背景

  1. 在单线程场景中,CPU 通常会保证程序顺序(Program Order) 的可见性,即单线程内的指令会按照代码编写的顺序执行(或看起来像是按顺序执行)存储器读写的结果也会符合单线程的预期
    1. 即使CPU有乱序功能,也会通过scoreboard等方式来处理data hazard,address hazard等,确保单线程内的内存访问都是保续的。即使现代的CPU都是超标量处理器。
  2. 但在多线程或多处理器(multi-hart)场景中,要实现多线程同时正确的对一个内存操作就会遇到问题
    1. 乱序执行(Out-of-Order Execution)
    2. 缓存分层(Cache Hierarchy),
    3. 存储缓冲区(Store Buffer)**,**CPU有各级的存储器cache,in-flight指令暂存器
    4. 其他线程观察到的实际操作顺序与当前线程的程序顺序不一定一致
    5. 希望实现一个功能,利用一片多核都可以访问到的存储器来实现同步,这个存储器实现**“锁”**功能
      1. 处理器1,通过读“锁”,确定没人用,写“锁”,确保占用,不被别人使用
      2. 在处理器,读-写中间,处理器2可能就会写“锁”,从而造成错误
      3. 处理器1,准备通过写“锁”,来释放占用。但是写操作可能被乱序到当前线程的前面去执行,从而达不到保护的目的。
    6. 所以希望实现“原子指令”,实现获取“锁”,和释放“锁”的中间不会被其他的线程干扰,产生错误。
  3. 在多线程间建立 “同步关系”,通过限制 CPU 的重排序和缓存行为,确保
    1. 使用acquirereleasey语义,确保单线程内,和 LR.W 和 SC.W 指令之间是保序的。单线程内的指令实际执行顺序能被LR.W 和 SC.W控制,不收缓存和乱序的影响。
    2. 然后通过 LR.W 和 SC.W 指令,通过特殊的多线程都唯一的硬件,实现多线程之间的竞争。
  4. 从而确保
    1. 一个线程的写操作结果能被另一个线程的读操作正确感知
    2. 跨线程的操作顺序符合预期(避免 “先写后读” 变成 “先读后写” 的错乱)

LR.W(Load-Reserved)​

​指令格式​
​行为描述​
  1. ​加载值​​:从内存地址 rs1读取32位数据到寄存器 rd

  2. ​设置保留标记​​:

    • 在缓存子系统(如L1 Cache)中标记该地址为 ​​Reserved​​(保留状态)。

    • 硬件会记录当前核心(Hart)对该地址的独占访问权。

  3. ​隐式acquire语义​​(若未显式指定):

    • 后续内存操作不会被重排序到 LR.W之前(保证加载操作的可见性)。
​硬件实现细节​

SC.W(Store-Conditional)​

​指令格式​
​行为描述​
  1. ​检查保留标记​​:

    • 若地址 rs1的保留标记仍被当前核心持有(未被其他核心修改):

      • 执行存储:将 rs2的值写入 rs1

      • 清除保留标记。

      • rd写入 0(成功)。

    • 若保留标记已失效(其他核心修改了地址 rs1):

      • 放弃存储。

      • rd写入 非0值(通常为1,失败)。

  2. ​隐式release语义​​(若未显式指定):

    • 之前的内存操作不会被重排序到 SC.W之后(保证存储操作的全局可见性)。
​硬件实现细节​

典型用例:自旋锁实现​

# 加锁(使用aq/rl语义确保屏障)
lock:
  lr.w.aq t0, (a0)       # 带acquire的加载保留
  bnez    t0, lock       # 检查锁是否被占用(t0!=0则重试)
  li      t1, 1
  sc.w.rl t0, t1, (a0)   # 带release的条件存储
  bnez    t0, lock       # 若存储失败(t0!=0),重试

# 解锁
unlock:
  sw.rl   zero, (a0)     # 带release的存储,释放锁

多核交互场景示例​

Core 0                          Core 1
======                          ======
1. lr.w t0, (x)                3. lr.w t0, (x)
   - 加载x=0,设置保留标记          - 加载x=0,设置保留标记
2. sc.w t1, 1, (x)             4. sc.w t1, 1, (x)
   - 保留有效,存储成功(x=1)       - 保留已失效(因Core 0修改x),存储失败
   - t1=0                         - t1=1