3. CXL.cache 消息类型:Request、Response、Snoop 详解

好,我们进入第三章。这章是 CXL.cache 协议的「骨架」——消息类型。我个人习惯把 CXL.cache 的消息分成三大类:Request、Response、Snoop。你想想看,任何缓存一致性协议,其实都是在做三件事:发请求、收回应、处理探测。CXL.cache 也不例外。

我之前刚接触 CXL Trace 时,看到一大堆 D2H、H2D 的消息头就头疼。后来发现,只要搞懂这三种消息类型,Trace 里 80% 的乱码都能读顺。咱们今天就把它彻底拆开。

小提示: 本章所有消息格式都基于 CXL 3.0/3.1 规范。你在实际 Trace 中看到的字段名可能略有差异,但核心分类不会变。

3.1 消息类型的整体分类

CXL.cache 的 Device(比如加速器)和 Host(处理器)之间,通过两种物理通道通信:Request 和 Response。而 Snoop 是 Host 发往 Device 的特殊消息,用来查询或更新 Device 侧缓存的状态。

下面这张图是我经常在内部培训时用的分类框架,你可以先看个全貌:

CXL.cache 消息类型分类框架 CXL.cache 消息(共 8 种) Req (3 种) RdCurr / RdOwn / RdOnce Rsp (3 种) RspData RspHit / RspI Snp (2 种) SnpData SnpCurr 关键术语说明: • M2S (Master to Subordinate) = Host → Device • S2M (Subordinate to Master) = Device → Host • 所有 Snp 消息走 M2S 方向;所有 Req 消息走 S2M 方向

3.2 Request 类消息(S2M 方向)

Request 是 Device 主动发给 Host 的消息。Device 要读数据、要写数据、或者想知道某个缓存行的最新状态,都得靠 Request 开口。在 Trace 里,你会看到方向标记为 S2M。

CXL.cache 定义了三种 Request:

消息名Opcode功能简述常见场景
Req_RdCurr 0x00 读当前数据,不改变缓存状态 Device 想做一次「只读窥探」,比如查表
Req_RdOwn 0x01 读数据并获取该行的所有权(写权限) Device 要修改数据前,必须发这个
Req_RdOnce 0x02 读数据,但只请求一次有效副本 流式访问,数据只用一次,不需要后续缓存一致性维护

我记得第一次看 Trace 时,搞不清 RdCurr 和 RdOnce 有什么区别。后来在 FPGA 验证平台上跑了一个实际案例才明白:RdCurr 要求 Host 返回当前时刻的「有效数据」,哪怕这个数据是脏的;而 RdOnce 只要求 Host 返回一个「可用的副本」,Host 甚至可以返回一个过时的值(只要它保证一致性协议没问题)。所以 RdOnce 的延迟通常更低,但 Device 不能对它做写回。

实战注意: 你在 Trace 里看到大量 RdOnce 时,说明 Device 在做流式处理(比如 AI 推理的权重读取)。如果看到大量 RdOwn,说明 Device 有频繁的写操作(比如训练梯度的更新)。这个规律在性能分析时非常有用。

3.3 Response 类消息(S2M 方向)

Response 是 Device 对 Host 此前 Snoop 探测的回复。注意,Response 和 Request 一样都是从 Device 到 Host,但 Response 的触发条件是被动的——Host 先发了一个 Snoop 问 Device「你那边的缓存行状态是什么?」,Device 才回一个 Response。

CXL.cache 定义了三类 Response:

消息名Opcode含义附带数据?
RspData 0x04 Device 持有该行的有效副本,并将数据返回给 Host 是(带 64B 数据体)
RspHit 0x05 Device 命中缓存行,但不需要返回数据(状态已足够) 否
RspI 0x06 Device 不持有该行(Miss),或状态无效(I 状态) 否

很多人会把 RspData 和 RspHit 弄混。我教你一个记忆法:RspData 是「我不仅要告诉你我命中了,我还把数据直接塞给你」;RspHit 是「我命中了,但数据还在我手里,你别问我要」。前者多用于 Device 帮忙写回(比如 Device 有脏数据需要回写),后者用于状态确认。

3.4 Snoop 类消息(M2S 方向)

Snoop 是 Host 主动发给 Device 的探测消息。这是整个 CXL.cache 协议里真正体现「缓存一致性」的地方。Host 要维护自己的缓存视图,必须时不时地问 Device:「你那边那个缓存行现在是什么状态?是 M 还是 S 还是 I?」

CXL.cache 只定义了两种 Snoop:

消息名Opcode功能典型触发条件
SnpData 0x07 要求 Device 返回该行的数据副本 Host 需要读取该行,而 Device 可能是唯一拥有者
SnpCurr 0x08 仅查询状态,不要求返回数据 Host 只想知道状态,比如做目录转移

我在调试过程中发现,很多人会因为不理解 Snoop 的方向而陷入困惑。你看,SnpData/SnpCurr 是 M2S,而它们对应的回复 RspData/RspHit/RspI 却是 S2M。这构成了一个典型的事务对:

Host → Device: SnpData (你想干嘛?)
Device → Host: RspData (我把数据给你,我转移所有权)

在 Trace 中找这种配对关系,是分析一致性协议是否正确的基础。

曾经踩过的坑: 我之前在一个项目中,Device 收到 SnpData 后总是回 RspHit 而不是 RspData。协议规定,SnpData 要求必须返回数据,如果 Device 状态是 Modified,必须写回数据再回复 RspData。那个项目因为把 SnpData 当成 SnpCurr 处理,导致 Host 永远拿不到脏数据,最终数据一致性崩溃。排查了一周才发现这个低级错误。

3.5 消息类型与 Opcode 编码表

为了方便你对照 Trace 日志,我整理了一份完整的 Opcode 编码表:

Opcode (4-bit)方向消息类型是否带数据
0x00S2MReq_RdCurr否(仅地址)
0x01S2MReq_RdOwn否
0x02S2MReq_RdOnce否
0x04S2MRspData是(64B)
0x05S2MRspHit否
0x06S2MRspI否
0x07M2SSnpData否
0x08M2SSnpCurr否

注意,Opcode 0x03、0x09~0x0F 是保留的。如果你在 Trace 里看到这些值,要么是协议版本不匹配,要么是 Trace 工具误解析。我建议遇到这种情况直接抓原始 bit 流对照看。

3.6 如何从 Trace 中快速识别消息类型

给你几条实战经验。拿到一条 CXL.cache Trace 后,我一般按这个步骤扫:

  1. 看方向: S2M 是 Device→Host,只可能是 Req 或 Rsp;M2S 是 Host→Device,只可能是 Snp。
  2. 看 Opcode 高 4 位: 0x0~0x2 是 Req;0x4~0x6 是 Rsp;0x7~0x8 是 Snp。几乎一秒就能定位。
  3. 看是否带数据: 只有 RspData 在 Reply 通道带 64B 负载。Req 和 Snp 都不带数据。
  4. 配合事务 ID(TxnID)配对: 同一个事务的 Snp 和 Rsp 共享相同的 TxnID。这是硬件做的关联,永远不会错。

好了,这一章其实就讲一件事:CXL.cache 的 8 种消息,底层逻辑就是 Req/Rsp/Snp 三足鼎立。把这个框架刻进脑子里,后面的 Trace 分析就是在它上面加肉。