Skip to content

CXL (Compute Express Link)

CXL 的标准是公开下载的:https://www.computeexpresslink.org/download-the-specification,下文参考的是 2022 年 8 月 1 日的 CXL 3.0 版本。

CXL 设备类型

CXL 对 PCIe 的重要的扩展,一是在于让设备可以和 CPU 实现缓存一致性(CXL.cache),二是可以做远程的内存(CXL.mem)。

具体下来,CXL 标准主要定义了三类设备:

  • CXL Type 1: 设备带有与 CPU 一致的缓存,实现 CXL.io 和 CXL.cache
  • CXL Type 2: 设备带有自己的内存和与 CPU 一致的缓存,实现 CXL.io,CXL.cache 和 CXL.mem
  • CXL Type 3: 设备带有自己的内存,实现 CXL.io 和 CXL.mem

CXL 传输层

CXL.io

CXL.io 基本上就是 PCIe 协议:

CXL.io provides a non-coherent load/store interface for I/O devices. Figure
3-1 shows where the CXL.io transaction layer exists in the Flex Bus layered
hierarchy. Transaction types, transaction packet formatting, credit-based
flow control, virtual channel management, and transaction ordering rules
follow the PCIe* definition; please refer to the “Transaction Layer
Specification” chapter of PCIe Base Specification for details. This chapter
highlights notable PCIe operational modes or features that are used for
CXL.io.

CXL 3.0 速度是 64.0 GT/s,使用 PAM4 编码,对应的是 PCIe 6.0。

CXL.cache

CXL.cache 每个方向上有三个 channel:请求,响应和数据。考虑到 Host 和 Device 的传输方向,就是六个 channel:D2H Req,D2H Resp,D2H Data,H2D Req,H2D Resp,H2D Data。在 Data channel 上传输的缓存行大小是 64 字节。

CXL.cache 的缓存行状态采用的是 MESI。

CXL.cache 传输有三种模式:68B Flit,256B Flit 和 PBR Flit。

H2D Request 的字段:

  • Valid
  • Opcode
  • Address[51:6]: 物理地址
  • UQID: Unique Queue ID
  • CacheID: Only in 256B Flit
  • SPID/DPID: Only in PBR Flit

D2H Response 的字段:

  • Valid
  • Opcode
  • UQID: Unique Queue ID
  • DPID: Only in PBR Flit

D2H Data 的字段:

  • Valid
  • UQID: Unique Queue ID
  • ChunkValid: Only in 68B Flit
  • Bogus
  • Poison: data is corrupted
  • BEP: Only in 256B Flit & PBR Flit
  • DPID: Only in PBR Flit

D2H Request 的字段:

  • Valid
  • Opcode
  • CQID: Command Queue ID
  • NT: Non Temporal
  • CacheID: Only in 256B Flit
  • Address: 46 位物理地址
  • SPID/DPID: Only in PBR Flit

H2D Response 的字段:

  • Valid
  • Opcode
  • RspData
  • RSP_PRE
  • CQID: Command Queue ID
  • CacheID: Only in 256B Flit
  • DPID: Only in PBR Flit

H2D Data 的字段:

  • Valid
  • CQID: Command Queue ID
  • ChunkValid: Only in 68B Flit
  • Bogus
  • GO-Err
  • CacheID: Only in 256B Flit
  • DPID: Only in PBR Flit

请求类型

首先考虑 Host 会发送的请求。

第一种是 SnpData,例如在 Host 在读取的时候出现缺失,此时需要向 Device 发送 Snoop,获取最新的 Dirty 的 Data,或者让 Device 的缓存行降级为 Shared 状态。

Device 收到 SnpData 后,如果发现缓存行不在缓存中(状态是 I),会回复一个 RspIHitI;如果缓存行在缓存中且数据没有修改(状态是 S 或者 E),降级到 S,会回复一个 RspSHitSE;如果缓存行是 dirty(状态是 M),可以选择降级到 S,然后回复 RspSFwdM 以及缓存行的数据,也可以选择变成 Invalid,回复 RspIFwdM 以及缓存行的数据。

可以看到,这些 D2H Response 的 Opcode 的名字格式很有规律,Rsp+A+Hit/Fwd+B,A 表示新的缓存行状态,B 是原来的缓存行状态,Hit 不附带数据,Fwd 附带数据。

第二种是 SnpInv,例如 Host 要写入缓存,就要 invalidate 其他缓存。Device 收到以后,可能返回 RspIHitI、RspIHitSE 和 RspIFwdM,分别对应不同的初始状态,最终都是 Invalid 态。

第三种是 SnpCur,获取当前的缓存行状态。Device 可以修改缓存行状态,但是不建议。可能的返回有 RspIHitI,RspVHitV,RspSHitSE,RspSFwdM,RspIFwdM 和 RspVFwdV。这里的 V 表示 Valid,对应 MESI 中的 MES 三种状态。所以如果缓存行状态不变的话,就是 RspIHitI,RspVHitV 和 RspVFwdV 三种响应。

再考虑 Device 会发送的请求。首先,请求可以分为四类:

  1. Read:发送 D2H Request,接收 H2D Response 和 H2D Data
  2. Read0:发送 D2H Request,接收 H2D Response
  3. Write:发送 D2H Request,接收 H2D Response,发送 D2H Data,可选接收 H2D Response
  4. Read0-Write:发送 D2H Request,接收 H2D Response,发送 D2H Data

  5. RdCurr(Read),Device 读取 Host 的缓存行,不造成任何的缓存状态的修改。Device 缓存还是处于 Invalid 状态。

  6. RdOwn(Read),Device 读取 Host 的缓存行,可以进入 E 态或者 M 态。Host 响应 GO-Err/GO-I/GO-E/GO-M。
  7. RdShared(Read),Device 读取 Host 的缓存行,进入 S 态。Host 响应 GO-Err/GO-I/GO-S。
  8. RdAny(Read),Device 读取 Host 的缓存行,进入 M 态,E 态或 S 态。Host 响应 GO-Err/GO-I/GO-S/GO-E/GO-M。
  9. RdOwnNoData(Read0),Device 不读取现在缓存行的数据,进入 E 态。一般用于整个缓存行的数据都要更新的情况,所以不需要或许当前缓存行的数据。
  10. ItoMWr(Read0-Write),Device 写入新的完整缓存行到 Host 中,并且进入 M 态。Host 响应 GO_WritePull/GO_ERR_WritePull。
  11. WrCur(Read0-Write),和 ItoMWr 基本一样,区别在于,如果缓存行命中了,就写入到缓存中;如果缺失了,就写入到内存中。Host 响应 GO_WritePull/GO_ERR_WritePull。
  12. CLFlush(Read0),要求 Host Invalidate 一个缓存行。Host 响应 GO-Err/GO-I。
  13. CleanEvict(Write),Device 要 Evict 一个 Exclusive 的缓存行。Host 响应 GO_WritePull/GO_WritePull_Drop。
  14. DirtyEvict(Write),Device 要 Evict 一个 Modified 的缓存行。Host 响应 GO_WritePull/GO_ERR_WritePull。
  15. CleanEvictNoData(Write),Device 要 Evict 一个 Exclusive 的缓存行,但是不传输数据,只用于更新 Snoop Filter。Host 响应 GO-I。
  16. WrInv(Write),Write Invalidate Line,向 Host 写入 0-64 字节的数据,并且 Invalidate 缓存。Host 响应 WritePull/GO-Err/GO-I。
  17. WOWrInv(Write),Weakly Ordered 版本的 WrInV,写入 0-63 字节的数据。Host 响应 ExtCmp/FastGO_WritePull/GO_ERR_WritePull。
  18. WOWrInvF(Write),Weakly Ordered 版本的 WrInv,写入 64 字节的数据。Host 响应 ExtCmp/FastGO_WritePull/GO_ERR_WritePull。
  19. CacheFlushed(Read0),告诉 Host 自己的缓存都被清空了,所有缓存行都在 I 状态。Host 响应 GO-I。

和其他协议的对比

缓存一致性协议 中分析过 TileLink 的缓存一致性实现方法,如果某一个缓存(Master A)出现了缺失,需要经过如下的过程:

  • Master A -> Slave: Acquire
  • Slave -> Master B: Probe
  • Master B -> Slave: ProbeAck
  • Slave -> Master A: Grant
  • Master A -> Slave: GrantAck

在 TileLink Cached 里面,所有的 Master 都是平等的。而在 CXL 中,需要维护缓存一致性的,有 CPU 内部的各个缓存之间,还有 CPU 和设备之间。而 CXL.cache 主要负责的是与设备的缓存一致性部分,维护缓存一致性的核心是在 CPU 一侧,Host 相当于 TileLink 的 Slave,Device 相当于 TileLink 的 Master A。可以说 CXL.cache 是不对称的缓存一致性协议。

例如 CXL 中设备读取缓存的时候,出现了缺失,那么需要经过如下的过程:

  • Device -> Host: RdShared/RdOwn
  • Host -> CPU Caches: Custom Snoop Messages
  • Host -> Other CXL Device: SnpData
  • Other CXL Device -> Host: RspSHitSE/RspSFwdM
  • Host -> Device: GO-S

可以看到,整体的流程也是差不多的。

CXL.mem

CXL.mem 用于扩展内存,根据类型的不同,它可能单独使用,也可能和 CXL.cache 配合使用。具体来说,有三种一致性模型:

  1. HDM-H(Host-only Coherent):仅 Type 3 设备,也就是无 CXL.cache
  2. HDM-D(Device Coherent):仅 Legacy Type 2 设备,也就是有 CXL.cache
  3. HDM-DB(Device Coherent using Back-Invalidation):Type 2 或 Type 3 设备

在 CXL.cache 中,两端是 Host 和 Device;而 CXL.mem,两端是 Master 和 Subordinate。

从 Master 到 Subordinate 的消息(M2S)有三类:

  1. Request(Req)
  2. Request with Data(RwD)
  3. Back-Invalidation Response(BIRsp)

从 Subordinate 到 Master 的消息(S2M)有三类:

  1. Response without data(NDR, No Data Response)
  2. Response with Data(DRS, Data Response)
  3. Back-Invalidation Snoop(BiSnp)

其中比较特别的是 Back-Invalidation,这个的目的是让 Device 可以通过 Snoop 修改 Host 中缓存了 Device 内存中的数据的缓存行。

对于 Type 3 的设备(无 CXL.cache)来说,Device 就是一个扩展的内存,比较简单,只需要支持读写内存就可以了。Host 发送 MemRd*,Device 响应 MemData;Host 发送 MemWr*,Device 响应 Cmp。

对于 Type 2 的设备(有 CXL.cache)来说,Device 既有自己的缓存,又有自己的内存,所以这时候就比较复杂了。例如 Host 在读取数据的时候(MemRd,SnpData/SnpInv/SnpCur),还需要对 Device Cache 进行 Snoop(SnpData/SnpInv/SnpCur),保证缓存的一致性。Host 想要写入数据到 Device Memory 的时候,如果此时 Device Cache 中有 Dirty 数据,需要进行写合并,再把合并后的数据写入到 Device Memory。当 Device 想要从自己的缓存读取数据,又缺失的时候,首先需要判断数据在 Host 端的缓存中,还是在 Device Memory 中,不同的偏置(Bias)模式决定了数据应该放在 Host 还是 Device 的缓存上。Device 要写入数据的时候,如果 Host 中缓存了该缓存行,则需要 Back-Invalidation。为了支持这些场景,CXL.cache 和 CXL.mem 会比较复杂。

CXL 链路层

前面提到过,CXL.io 实际上就是 PCIe,所以 CXL.io 的链路层采用的就是 PCIe 的链路层。

而对于 CXL.cache 和 CXL.mem,CXL 定义了 68B Flit 模式和 256B Flit 模式。

其中 68B Flit 拆分成四个 Slot,每个 Slot 16 字节,可以保存不同组合的 CXL 请求/响应,其中第一个 Slot(Slot 0)开头还有 Flit Header。最后还有 2 个字节的 CRC,没有 FEC。也是因为 68B Flit 没有 FEC,所以只用于不高于 32GT/s 的链路,更高就需要用包括 FEC 的 256B Flit 模式。

256B Flit 模式有两种:

  1. Standard 256B Flit 包括开头 2 个字节的头部,然后是 15 个 Slot,最后是 2 字节的 Credit Return,8 字节的 CRC 和 6 字节的 FEC。
  2. Latency Optimized 256B Flit 包括开头 2 个字节的头部,然后是 8 个 Slot(共 120 字节),紧接着是 6 字节的 CRC,接着是剩下的 7 个 Slot,2B 的 Credit Return,Slot 8 的最后 2 字节,6 字节的 FEC 和 6 字节的 CRC。

两种的格式的区别在于,Latency Optimized 256B Flit 把 CRC 拆散成了两部分,放在中间和结尾,并且花费了更多的空间(6+6B)。这样,每收到 128B 就可以做 CRC 检查,不用等到收到完整的 256B。下面是这两种 CXL 256B Flit 和 PCIe 256B Flit 的对比示意图:

CXL 和 PCIe FLIT 格式对比(图源 CXL 3.0 Scales the Future Data Center

具体地,256B Flit 被分为了前半部分(Even Flit)和后半部分(Odd Flit)。前半部分(Even Flit)包括 2 字节的头部,120 字节的数据和 6 字节的 CRC,这 6 字节的 CRC 保护的就是 Even Flit 的数据。后半部分(Odd Flit)包括 116 字节的数据,6 字节的 FEC 和 6 字节的 CRC,其中 FEC 保护的是完整的 256B 数据,CRC 保护的是后 128 字节数据。

接受方收到前 128 字节的时候,如果发现 CRC 正确,就可以提前进行处理。类似地,收到后 128 自己的时候,如果发现 CRC 正确,就可以进行处理,不需要验证 FEC。如果任何一部分的 CRC 校验失败,再用 FEC 来纠正整个 256B Flit。如果 FEC 还是纠正失败,那就重传。

当然了,这种设计在减少延迟的同时,也带来了复杂性:例如前半部分 CRC 验证正确,后半部分 CRC 验证错误,这时候用 FEC 进行纠错,纠错之后发现,前半部分 CRC 错误,后半部分 CRC 正确。这时候应该相信谁呢?重传的时候,前半和后半的 CRC 如果又出现校验错误了呢?这里有很多情况需要讨论。

Comments