
上个月调一个缓存一致性的死锁问题在一台满是逻辑分析仪和协议 trace 的台子上蹲了一周最后定位到的是一个很少有人注意的CHI Empty 状态在 snoop 命中的边界行为。这让我重新意识到一件事Scale-up 互连协议里真正值钱的细节往往不在 ppt 上的架构图里而在状态机跳转和报文比特位里。大模型把单个计算节点的内存墙和带宽墙逼到极限之后CHI、CXL、UCIe、TileLink 这些协议被频繁放到一起比较但CHI 七态PBR 路由具体解决什么问题、在链路层长什么样子很多人其实是模糊的。这篇文章把这六个开放互连协议放到同一张工作台上从状态机和比特两个维度做一次完整解剖重点放在 CHI 七态设计和 CXL 3.x 的 PBR 路由上适合正在做多 die 互连、异构计算、内存池化以及 SoC 一致性设计的工程师参考。1. Scale-up 卷土重来为什么一致性问题从片内蔓延到了机柜1.1 从 Scale-out 到 Scale-up算力互联的逻辑变了过去十年大家谈起大规模计算默认路线是 Scale-out一个集群里堆几千台服务器靠以太网或者 RDMA 把任务拆开。这个模式对训练吞吐还行但对单个大模型推理、图数据遍历这类强依赖任务并不友好。LLM 集群的瓶颈逐渐从能不能有足够算力变成一台机器能不能装下一个大模型、能不能低延迟访问起全部内存于是 Scale-up 重新成了主角。Scale-up 的典型诉求是多个芯片或者多台主机进入同一个一致性域看到同一份内存某个进程在前一个节点写入的数据后一个节点无需手动 Flush 就能读到。这比普通消息传递更贴近单机编程模型代价是协议复杂度从总线级上升到了网络级。Scale-up 和传统多路服务器里的 CPU 互联很不一样。多路服务器时期UPI 这类协议只管几个 CPU 插槽之间的缓存一致性拓扑基本固定距离短、节点少。AI 时代的 Scale-up 要面对的是 GPU、CPU、内存池、DPU 混合的拓扑可能有交换机可能有跨板卡甚至跨机柜的链路。拓扑从一条总线变成一张网光靠传统的一致性协议是扛不住的。1.2 协议栈出现了三明治结构现在的互连系统很少是一个协议从头包到尾普遍是底层物理传输 中间一致性/事务处理 上层内存语义的组合。一个 CXL 内存扩展设备下面跑的是 PCIe 的物理层和链路层上面跑的是 CXL.mem 的内存语义一颗复用 CHI 接口的加速器芯粒内部可能是 TileLink 或者 AXI 做组件互连到了 Die-to-Die 适配层再用 UCIe 把物理信号送出去。这种分层带来一个实际问题每个层级都可以选不同协议而跨层级组合时状态机、事务 ID、流控逻辑必须严格咬合。很多人只盯着某一层的协议比较放到整个 Scale-up 系统里就容易踩坑。所以下面先花一节把这个坐标系拉平再逐个深入。2. 六个常被放在一起比较的互连协议其实处在不同层级2.1 一份出身表看六协议标题里说的六个开放协议按我的习惯列一下协议来源/许可所属层级缓存一致性模型路由/寻址基础典型场景AMBA CHIARM 授权标准事务层链路层七态含 Empty 态目标 ID Home 节点路由片上 NoC、多核 Cluster、高性能 SoCAMBA ACEARM 授权标准总线协议事务层MESI 类地址总线译码AXI 生态内的一致性扩展TileLinkSiFiveBSD 开源协议层自定义TL-C地址分配器 客户端 IDRISC-V SoC、开源高性能核CXLCXL 联盟开放规范事务层链路层物理层MESI 类 BIR/Back-InvalidationID Based Routing Port Based Routing内存池化、多主机、Scale-up 机柜UCIeUCIe 联盟开放规范物理层适配层透传上层协议不定义路由芯粒 Die-to-Die 互连PCIePCI-SIG 开放规范物理层链路层事务层无一致性语义BDF 配置路由 地址路由CPU 与 IO、设备互连CXL 的物理载体有件事得先说清楚这里说的开源并不都是源代码级开源。TileLink 是真正 BSD 授权的开放协议RTL 直接放在 Rocket Chip 里可以随便看CHI、CXL、UCIe、PCIe 是规范公开的开放标准配套生态里有大量开源实现或者参考验证环境。所以横向对比的时候我更愿意叫开放协议族但多数讨论场景直接用开源两个字的语境也成立。从这张表能看出来六个协议并不是竞争关系。UCIe 和 PCIe 解决的是物理层问题CHI、ACE、TileLink、CXL 解决的是更高层的一致性和内存语义问题。真正的战场在后者但物理层的选型又会影响前者的效率和成本。2.2 CHI 和 CXL 为什么总被并列却不能互相替代CHI 和 CXL 被放一起比较是因为它们都在讨论缓存一致性都定义了一组比较完整的状态集合和 snoop 流程。但两者动机差别很大。CHI 诞生于 ARM 高性能 SoC 时代。多个 CPU 簇和一个统一的 LLC/Home 连在 NoC 上距离是片上毫米级节点数量有限带宽极高延迟极低。CHI 把请求、响应、数据、监听拆成独立通道让多个事务可以在 NoC 上并发流水这是它的一大优势。CXL 诞生于大数据中心。它要解决的是把远端的设备内存、内存池、甚至另一台主机的内存以缓存一致的方式接进来。距离从几十厘米到几十米中间要过交换机和重定时器。CXL 在事务层除了要维护一致性还必须处理足够大的连接数、跨越交换结构的路径选择、热插拔这类 PCIe 世界带来的问题。所以 CXL 在底层复用 PCIe 的物理/链路层在路由设计上明显比 CHI 走得更远。CHI 有清晰的 Home 节点概念所有复杂请求由 Home 统一协调适合片上集中式管理。CXL 的拓扑里没有一个假设中的中心 Home多级交换、多主机、多端口同时转发必须引入更灵活的路由机制。PBR 就是在这个背景下出现的。3. CHI 七态拆解MESI 到 MOESI再到带 Empty 的状态集合3.1 七态到底多出的是什么传统一致性协议大家最熟的是 MESI。四个状态Modified、Exclusive、Shared、Invalid核心是区分你独占改过你独占没改过大家共享不在你这。后来 MOESI 加了一个 OOwned允许一个持有脏数据的节点把数据借给别人读但最终由它负责回写省去了每次读都要先读到 Home 再转发的问题。CHI 在此基础上进一步演化形成了七态。通常指下面这七个状态含义有没有数据是不是唯一副本脏/干净IInvalid无--ScShared Clean有数据否干净UcUnique Clean有数据是干净UDUnique Dirty有数据是脏SDShared Dirty有数据否共享但由我负责回写脏UCEUnique Clean Empty没有数据是占位干净SCEShared Clean Empty没有数据否占位干净前五个其实是 MOESI 熟悉的变化关键是后两个 Empty 状态。Empty 的意思是这一块 cache line 的 tag 信息在但数据内容没有拿到。说白了就是一个空位。这个设计看着反直觉实际场景里非常有用。举个例子当一个 agent 想要执行一次 ReadUnique 去拿一块独占的缓存行来写但另一个 agent 还持有一份干净副本时常规做法是先把数据 snoop 回来再写入浪费一拍。如果允许先建立 UCE 占位等数据真正需要时才取或者配合某些不需要读旧值的写分配策略就能减少一次无效的数据搬移。再看 SCE它表示我参与共享这个地址但我手里没有数据这对 snoop 过滤器的维护很有价值——别人查一致性时知道这个节点也在群里但不会误以为它有数据而要求它回包。3.2 状态机里一个读请求的完整足迹拿一个最普通的 ReadShared 请求来走一遍。假设 RN-A 发起读Home 是 LLCRN-B 手里有一份 Sc 副本。RN-A 处于 I向 Home 发 REQ 通道的 ReadShared。Home 收到后判断这个地址存在共享副本于是向 RN-B 发 SNP 通道的 SnoopReadShared。RN-B 回 RSP说自己有 Sc 副本。Home 收集到所有响应后向 RN-A 发 DAT 通道的 CompData把数据带过来同时更新自己内部的目录信息。RN-A 收到数据状态从 I 变成 Sc。整个过程要经历 REQ、SNP、RSP、DAT 四种通道四个参与角色一拍都不能错。这也是为什么我在调试死锁时最怕看的情况一个事务卡在 Home 等 RSP 超时另一个事务卡在 RN 等 DAT互相等。如果是 ReadUnique 就复杂多了。RN-A 想独占写Home 需要确保其他副本全部失效。对持有 UD 的节点Home 会发 SnoopReadUnique要求对方回写数据并转成 I对持有 Sc/UCE 的节点可以只要求失效。RN-A 最终拿到的是 Uc 还是 UD取决于它接下来是否真写了数据。这里状态机还要配合缓存替换策略比如一个 UCE 状态的行被替换时由于没有数据它不需要产生回写直接丢弃 tag 就行。CHI 七态在实际工程里够用但费神。每个状态都要在 snoop 过滤器和数据 RAM 两边维护验证的状态转换矩阵比 MESI 大好几倍。尤其是 Empty 状态如果数据 RAM 里有 tag 位却没有有效数据位一旦 snoop 过滤逻辑写错很容易出现地址命中但从空 RAM 里读出了随机数据这种极难复现的 bug。3.3 七态对比 ACE 和 CXL 的一致性状态集合ACE 的一致性模型基本还是 MESI实现相对简单在总线带宽要求不太高的场景下很成熟。CXL.cache 的状态集合在我看来更接近一个工程化裁剪版它需要支持设备端缓存但设备的缓存不可能像 CPU 那样维护太多共享表项所以它把状态控制在比 MESI 多一些但比 CHI 简单很多的范围内并引入 Back-Invalidation 机制让 Home 可以主动通知设备端某行失效。CHI 的七态在片上 NoC 这样延迟极低、带宽极高的环境里能发挥最大价值。一旦距离拉长、延迟变大状态机每一步交互的代价都会被放大CXL 那套能少走一步就少走一步的设计就更实用。这是我在对比两个协议时最深的感觉状态集合的大小不是越全越好要看物理环境能不能支撑得起这么多状态带来的交互开销。4. 比特层面解剖CHI 报文在链路上究竟怎么组织4.1 REQ、RSP、DAT、SNP 四通道与事务流水账CHI 的事务层定义了四个逻辑通道分别承担不同职责REQ 是请求者向 Home 发起的操作RSP 是参与节点向 Home 返回的结果DAT 负责传输数据SNP 是 Home 向其他可能持有副本的节点发出的监听。四个通道相互独立意味着一个事务在 NoC 上可以拆成多个阶段同时跟其他事务交错流水线利用率和总线利用率都能提高。代价也很现实你必须为每个通道准备独立的 credit 流控通道之间一旦出现 credit 互相等待死锁排查就非常酸爽。链路层把事务层报文封装成 packet通常以 flit 为单位在物理链路上传输。一个 packet 包含 header 和可选的 data payload。header 里最关键的信息是事务 IDTxnID、源 IDSrcID、目标 IDTgtID和操作码Opcode这三者共同决定一个请求从哪里来、到哪里去、对应哪个事务。这里要特别强调当一个 REQ 因为超时被重发时TxnID 不能简单地重新利用否则新旧两个事务会同时在系统里乱窜数据一致性会出问题。4.2 把典型 REQ Header 的字段逐个拆开CHI 的字段位宽在不同实现里是可配置的我见过的最小配置和最大配置能差出一倍多。下面用一份比较典型的工程配置来展示字段设计逻辑字段位数示例作用Opcode7操作码表示 ReadShared、ReadUnique、WriteBack 等Size3传输字节大小QoS4服务质量等级用于 NoC 仲裁TgtID11目标节点 ID通常指向 Home/LLCSrcID11请求者 IDTxnID12事务 ID用于关联 RSP/DATReturnNID11数据/响应返回节点 ID可能不同于发起者Addr44物理地址或一致性地址CacheState3本次请求涉及的一致状态相关字段这些位加起来已经超过 100 位实际还会加上 tunable 的 metadata 位。从设计角度看每个字段都代表一次权衡。Addr 用 44 位可以把 16TB 物理地址空间都覆盖到但对一个只做 CXL 内存扩展的设备芯片42 位甚至 36 位就够了省下的位数可以转给 SrcID 或者 TxnID提高并发事务数。SrcID 和 TxnID 的位数决定了系统最多能同时承载多少在飞事务这是 Scale-up 系统估算带宽时一个关键参数。我遇到过一种情况为了降低布线压力把 SrcID 截短结果系统在某个聚合场景下大量事务因为 ID 冲突被 stalls。要定位这类问题最直接的办法是抓链路层 flit把每个 packet 的 SrcIDTxnID 拉出来做重叠统计。一旦发现某个 ID 对的重叠率超过一定阈值基本可以断定 ID 空间给少了。4.3 DAT 通道与链路效率的计算DAT 通道的 payload 里最核心的是 cache line 数据再加上字节有效位、CRC/ECC 校验位。以 64B cache line、128 位物理链路位宽为例一次 ReadShared 事务大概可以这样估算链路消耗REQ header 一个 flitSNP header 一个 flitRSP header 一个 flitDAT 需要 4 拍左右把 64B 数据带完加上校验整个事务在数据通路上要占用大约 7 到 8 个 flit 时间。如果链路上只有一个事务在跑利用率很低只有靠多事务流水交错才能把有效数据占比提上去。这也是为什么 CHI 这类协议强调多通道并发的原因。光看单事务延迟可能不如简单总线直接看吞吐流水线优势就非常明显了。比特层面的效率从来不是单看一拍能传多少数据而是看在重负载下协议控制头和数据 payload 的比例是否合理。5. PBR 路由当互连不再是一根总线而是一张网5.1 为什么 Scale-up 协议必须解决路由问题CHI 的七态解决的是一个缓存行可以处在什么状态但状态机必须建立在请求能送达正确节点的基础上。片上 NoC 的路由相对简单大多是固定的 mesh 或环形拓扑地址和节点 ID 跟物理位置有明确对应。一旦 Scale-up 拓扑引入交换机、多级组合和动态路径问题就变成一个包里装着的目标 ID经过中间的交换结构时谁来决定往哪个端口转发CXL 3.x 给出的答案之一是 PBRPort Based Routing。这个名字直译是基于端口的路由但它更多是一种转发思想而不是像 IP 路由那样维护一张全网路由表。PBR 的核心是每个交换端口维护一个转发表进端口之后根据包的目的信息查表确定要出哪个端口然后原样转发。5.2 ID-Based Routing 遇到规模问题PBR 如何破局早期 CXL 交换机的转发方式主要是 ID-Based Routing。它很直观每个目标组件分配一个 ID交换结构内部维护 ID 到端口的对应表。拓扑简单时非常好用一个包进来查一次表就知道往哪走。可一旦拓扑变成多级交换、多个内存池挂在不同层级、设备热插拔频繁ID-Based Routing 的表项数量和更新复杂度会迅速失控。每个交换机都要知道全局 ID 信息端口上有一点拓扑变化相关表项都得同步这在数据中心规模下很痛苦。PBR 的思路更像是二层交换机的 MAC 学习/转发表。它不要求每个交换机理解全局拓扑只要求在某个端口上收到包的时候根据包的关键字段对照本端口的转发表项决定下一跳方向。这样每一级交换机只需要维护与当前端口相关的表项规模大了以后表项不会无限膨胀拓扑变更的影响也被限制在局部端口。在实际实现里PBR 和 ID-Based Routing 并不是非此即彼。一个大型 CXL Fabric 可能用 PBR 做核心转发的骨架在请求路径上用 ID 做端到端关联在响应路径上依靠 PBR 表把完成数据准确定位到具体端口。这也解释了为什么在协议 trace 里你会看到同一个事务在入口和出口打上不同的转发标签这是交换结构在翻译路径信息。5.3 PBR 与一致性状态机在端到端路径上的配合这里有一个容易混淆的认知PBR 只解决包往哪走它不会改变 CHI 或 CXL 的状态机。一致性状态永远由链路端点的缓存管理器和 Home 节点维护PBR 只是一个透明的传送者。但 PBR 引入后带来一个非常实际的工程问题回包路径和请求路径可能不一致。一个请求从端口 1 进来PBR 表把它的响应留在端口 4 出去这是完全合法的。但如果表项更新滞后或者响应包的某些字段比如 SrcID在交换结构里被改写后没有正确还原回包就可能从另一个端口跑掉导致事务悬挂。所以你现在回头看 CHI 七态里那些等 RSP 超时的死锁场景在带 PBR 的 CXL Fabric 里会以更大规模的形式重演。调试这类问题我的习惯是先在交换结构入口给每个事务打一个全局时间戳看它进入和离开的端口时间线再结合端点的状态机 dump 做对齐比漫无目的地翻报文要快得多。从 CHI 七态到 PBR 路由背后的思维变化很清楚状态机把缓存行的正确性管好路由机制把请求的抵达管好两者分开解决又必须合在一起调试。这种事务状态和传输路径的分离是新一代互连协议最关键的设计哲学。6. 六协议横向对比延迟、带宽、实现成本的真实取舍6.1 一张表看关键指标把前面梳理的信息压缩成一张对比表方便做选型参考对比维度CHIACETileLinkCXLUCIePCIe一致性状态数七态MESI 类TL-C 可自定义裁剪后 MESI 类BIR不涉及不涉及路由机制Home目标 ID地址译码地址分配器IDRPBR由上层协议决定BDF地址路由Snoop 方式Home 集中发 SNP广播/目录分布式 ManagerHome/Device 双端不涉及不涉及典型距离片上 mm 级片内总线片内总线板卡到机柜芯粒间 mm 级板卡到机柜最大带宽趋势随 NoC 位宽扩展AXIl接口位宽通道数位宽扩展PCIe PHY 决定数十 GT/s/lane 量级64GT/sPCIe 6.0实现复杂度高中中低高含系统管理中物理层复杂中高这张表最大的价值不是判断谁更强而是帮你看清每个协议所处的位置。CHI 的性能上限极高但把它拿到机柜级距离上跑光是链路层重传和延迟补偿就够喝一壶CXL 在机柜级很方便但它依赖 PCIe 物理层端到端延迟比 CHI 这种片上协议高一到两个数量级UCIe 根本不回答一致性状态问题但它决定了上层协议到底能在多宽的物理管道上跑。6.2 选型逻辑按场景倒推协议我个人的选型习惯是先画拓扑再数跳数最后才做协议决策。如果是多 die 封装内的一致性互连die 之间距离在毫米级节点的数量在几个到几十个优先考虑 CHI 或者 TileLink。CHI 的成熟度高、验证生态全但授权和 IP 成本高TileLink 在开源 RISC-V 生态里非常好用源码就在手边改起来没有障碍。这里有个工程判断如果团队能接受开源的调试链路TileLink 的性价比往往比 CHI 更高如果目的是做成通用对外接口、跟第三方 IP 无缝对接CHI 的兼容性优势就体现出来了。如果是把多个主机和内存池拉进同一个一致域距离到板卡级甚至机柜级基本只能选 CXL。CXL 的 PBR 支持让多级交换机拓扑成为可能内存池化、多主机的热插拔和故障隔离也都有配套机制。现阶段做 Scale-up 系统CHI 和 CXL 很可能同时存在芯片内部走 CHI跨芯片/跨卡走 CXL中间用一层协议转换逻辑把两套状态机和事务 ID 映射起来。这个转换层是很多项目里最薄弱的环节也是最容易出死锁和性能拐点的地方。UCle 和 PCIe 更多是底座选择。UCIe 承载的是 die-to-die 物理链路如果上层跑 CHI、CXL 或者自定义协议UCIe 不挑食PCIe 则是 CXL 绕不开的底层。选择时重点看链路速率、封装形式、功耗和信号完整性需求而不是纠结它有没有一致性状态。7. 动手验证在开源工具链里把状态机跑起来7.1 我建议的复现路径理论说再多都不如把状态机跑起来来的直观。要实际看一个缓存一致性协议的状态转换最省事的是走 TileLink。Rocket Chip 和 Chipyard 里的 TL-C 实现是完全开源的配一个带 L2 缓存和多个 tile 的配置用 Verilator 仿真再加一个简单的 liveness 脚本触发多个核同时读写同一地址然后打开总线 trace 看 A、B、C、D、E 五个通道的握手序列。你能亲眼看见一个 request 如何从 A 通道进入。C 通道如何做 cache releaseD 通道如何带数据回到请求者。CHI 这边开源 RTL 相对少一些但可以走验证 IP 路线。不管是商业的还是开源的 UVM 环境都可以先构建一个最小的 RN 和 Home 对发一轮 ReadUnique再把 snoop 命中打开的覆盖场景扩展出来。重点观察当另一个 RN 持 UD 时SnoopReadUnique 的回写路径是否正确当持有的是 UCE 时它会不会错误地回写数据。后一种情况是 UVM 随机验证最容易砸出来的 bug因为很多测试激励根本没构造过没有数据的 tag 命中。CXL 的验证链路比较成熟的是用 QEMU 和 EDK2 做软件层模拟再配合 CXL 一致性测试套件。但要注意软件模拟里的 CXL 一致性模型和真实 RTL 的仲裁、credit、PBR 转发行为差得很远验证 Fabric 级别的 PBR 路由最好放到完整的硬件仿真环境里至少要有两个交换机级别节点才能把回包路径不一致这种问题暴露出来。7.2 我在实操中踩过的坑第一个坑是协议版本混用。CHI Issue B 和 Issue C 链路层的 flit 格式不兼容CXL 1.x 和 CXL 3.x 的许多报文字段也变了。不同小组各拿一个版本做集成接口对接时 port width 和字段位宽对不上跑起来直接卡死。我的习惯是集成前先做一次协议版本登记把每个接口用的 spec 版本、flit 大小、ID 位宽写成一张表挂在 wiki 上强制评审。第二个坑是 Empty 状态导致的幽灵数据。系统里同时有真实缓存数据 RAM 和 tag RAMEmpty 状态只在 tag RAM 里有记录。如果 snoop 过滤逻辑没有把 Empty 行单独排除一个读请求命中了这块 tag会把不属于该行的随机数据当成有效数据返回。这个问题在单核 debug 时很难暴露多核并发一多就神出鬼没。定位方法是在 tag RAM 里单独加一个 Empty 标志位并在读写数据 RAM 的所有路径上做断言。第三个坑是 PBR 表项和事务 ID 之间的时序。交换结构更新 PBR 表时在飞的旧事务如果还引用旧端口回包就会走错路。处理这类问题要么在更新表项前先阻塞新请求让旧事务全部 drain要么设计为允许旧事务通过独立的慢路径完成。我见过因为贪图性能不做 drain 导致整个 Fabric 挂死的案例性能优化可以后面再做先保证一致性逻辑在极端情况下是安全的。再分享一个小技巧不管做哪类一致性调试都值得在协议 trace 里加一个状态地址的二维日志。单纯抓字段只能知道有什么报文在跑加了状态和地址的联合日志才能一眼看出某个 cache line 从 I 到 UC 再到 UD 的完整轨迹配合地址过滤能迅速缩小可疑范围。这个习惯帮我在好几次死锁排查中省下一整天的纯机械比对时间希望对你有用。