
做了四年互连验证最怕听到的问题就是“CHI和CXL都是做一致性的到底有啥不一样”。每次我都想反问你关心的是协议能干什么还是协议在硅片上怎么动如果只关心前者MESI加一个窥探过滤器就够聊了如果关心后者就必须把CHI的七个缓存态、CXL的HDM解码、UCIe的flit映射一个个掰开揉碎。这篇东西按Scale-up互连里能合法拿到协议的六份开放规范——CHI、CXL、UCIe、OpenCAPI、Gen-Z、CCIX——做一次协议级对比重点放在比特位和状态机最后专门聊PBR路由在不同协议里到底解决什么问题。适合做SoC集成、NoC设计、CXL交换机和FPGA原型验证的人如果你只是听过Scale-up这个词也能从状态机和路由的视角把这张图拼起来。1. Scale-up互连的地基状态机、比特位和路由策略纠缠在一起1.1 Scale-up到底up的是什么Scale-out是加机器Scale-up是加“同一台机器”里的资源。CPU节点之间、CPU和加速器之间、内存池和交换机之间靠的都不是以太网那种软件纠错弱一致模型而是硬件链路上以纳秒为单位的读改写、窥探、写回、原子操作。这个场景下协议栈每多一层打包解包延迟就多一拍每少一个缓存状态一致性就可能出错。所以Scale-up互连协议本质上是在同一个时钟节拍里同时回答三个问题这笔请求是什么类型、它要写到哪里、当前所有缓存里这块数据是什么状态。前两个问题落到比特字段第三个问题落到状态机。这也是为什么协议对比不能只停留在“谁延迟低、谁带宽高”必须往比特和状态机层面钻。早期大家习惯用MESI四个状态描述缓存行归属但单核时代的东西搬到多核、多芯片、多节点之后很多特殊情况靠四态表达不了。比如一个缓存行只有在缓存里但没有数据只有部分字节是脏的多个节点共享且其中一个拥有最新数据。这些问题不解决硬件就无法在数据竞争时选出正确的响应者。1.2 六份开放协议名单我这次对比的六个协议市面上经常被叫成“开源互连协议”。更准确的说法是开放标准规范——不是源码开放而是任何人都能下载规范文本、按规范实现。它们分别是协议典型范围自己定义缓存一致性状态机一句话定位AMBA CHISoC/NoC/CPU簇是7个稳定状态ARM体系里的全一致性主干CXLCPU到设备/内存池CXL.cache/CXL.mem各自有状态机把PCIe扩展成一致性和内存语义UCIeChiplet裸片到裸片否只做传输层把多个die拼成一个逻辑芯片OpenCAPI加速器一致性是MESI类地址翻译最早做开放加速器一致性的方案之一Gen-Z内存语义Fabric是内存语义一致面向内存池化的开放fabricCCIXPCIe上的加速器一致性是简化MESI想在PCIe物理层上“顺手”做一致性CHI和UCIe放在一起比确实有点跨界因为UCIe根本不关心cache line它只关心die之间的flit搬运。但Scale-up系统今天恰恰是这些协议叠起来用的CPU核之间走CHI多个裸die之间走UCIe外部内存扩展走CXL。横向对比能帮你分清楚每一层的职责边界。1.3 状态机是协议的“量子态”写过RTL状态机的人都知道三段式写法是把组合逻辑、时序逻辑和输出逻辑分开调试时才能快速定位是哪个跳转条件出了问题。互连协议里的缓存一致性状态机比普通控制逻辑更难因为它不是单一FSM而是“每一个缓存行都在跑一个FSM”。比如CHI里一个cache line从SC变成UD中间要经过snoop请求、响应、数据回传、完成确认任何一步掉链子整个cache line的状态就可能和后端内存不一致。更麻烦的是同一个请求节点可能同时挂着多笔outstanding事务每笔事务都对应一个pending状态。这些pending状态和缓存稳定状态互相交叉才是真实硬件里最难验证的部分。所以后面聊状态机我不会只列“有哪几个状态”而是把状态转移背后的消息流讲清楚。状态不是画出来的是消息一步一步推出来的。2. CHI七态拆到比特位从I到SD每一步都不能乱2.1 为什么MESI不够用要搞出七个状态CHI的七态是下面这七个I、UC、UCE、UD、UDP、SC、SD。MESI里的Modified、Exclusive、Shared、Invalid基本都能在这七态里找到影子但CHI多出来的几个状态全是针对多核和系统总线场景的“边角情况”。状态数据有效唯一拥有脏典型含义I否否否无效没有缓存行UC是是否唯一且干净UCE否是否唯一但空数据UD是是是唯一且脏UDP部分有效是部分字节脏只有部分数据有效SC是否否共享且干净SD是否是共享且脏UCE是我最早觉得多余的状态。后来做过一次读unique不发数据的优化场景才明白某些协议事务比如DMA描述符预取只需要拿到缓存行的所有权不需要真正搬数据。这时候给一个“唯一但空”的状态可以避免一次无效的数据搬运。UDP则更现实。一次写操作可能只改了64B缓存行里的32B如果状态机只允许全脏判定那其他节点读取时就要多承担无效的写回流量。UDP配合字节使能让partial dirty的写回只搬真正变脏的部分流量能省不少。2.2 CHI消息通道和REQ flit里的关键比特CHI协议定义了四条逻辑通道REQ、RSP、SNP、DAT。名字很直白请求、响应、窥探、数据。这四条通道不是简单的命名而是为了防死锁刻意分出来的独立缓冲域。请求通道吃不下数据通道的流量snoop通道永远有高优先级这就是死锁避免的硬件基础。REQ flit里最重要的几个位域按常见配置看大致是Opcode、QoS、SrcID、TgtID、Address和Size。可以理解成下面这样// 示意不是某个版本的精确位域 typedef struct { uint8_t opcode; // 读共享/读唯一/写回/原子操作等 uint8_t qos; // 服务质量等级影响仲裁 uint16_t src_id; // 请求节点ID uint16_t tgt_id; // 目标节点ID uint64_t addr; // 物理地址/系统地址 uint8_t size; // 访问字节数 } chi_req_flit_t;TgtID在这条消息里异常关键。它可能是某个Home Node的编号也可能是PBR路由查表后的输出端口编号。SrcID用于响应回来时找请求方QoS则参与NoC仲裁。DAT flit也别轻视。CHI的数据通道会携带DBIDData Buffer ID和DataSource等字段因为多笔事务可以乱序返回接收方必须靠DBID把数据放回对应的pending事务槽位而不能简单假设“谁先请求谁先回”。2.3 从RN发起到HN命中的状态转移实例拿一笔读共享举例。某个RNRequest Node发现自己缓存里数据是I于是发ReadShared给负责这块地址的HNHome Node。HN收到后先在自己维护的directory里查看到另一个RN-A缓存行状态是UD说明唯一副本在RN-A且是脏的。HN向RN-A发snoopRN-A收到后进入过渡状态把数据通过DAT通道返回给请求方同时自己从UD变成SD或SC。请求方拿到数据后登记为SCHN更新directory一笔完整事务结束。这个例子里有三个状态机在同时跑请求方的pending FSM从“等数据”到“完成”RN-A的一致性FSM从UD到SDHN侧的目录FSM更新共享列表。任何一个节点的状态转移和消息到达顺序不匹配都是验证要抓的bug。很多人在FPGA上第一次调CHI看到“系统挂死”时第一反应是查DDR其实往往是某个snoop响应没按协议顺序回来。3. CXL、UCIe、OpenCAPI、Gen-Z、CCIX另外五套字里行间的差异3.1 CXL把缓存一致性和内存池化揉进PCIeCXL最聪明的地方是复用PCIe物理层然后在上层分了三类协议CXL.io负责枚举、错误报告和类似PCIe的IO语义CXL.cache负责加速器与Host之间的缓存一致性CXL.mem负责让CPU直接访问设备挂载的内存而不是只能通过驱动搬运数据。CXL.cache的缓存状态比CHI简单不少。工程实现上通常围绕M/S/I来转少了CHI里UCE、UDP这种极致优化状态。这不是CXL做不了而是CXL的主要场景是设备侧一致性设备没有CPU那么频繁的原子操作和读改写流量没必要把状态机做得那么细。CXL.mem里还有个很重要的bias机制。一块被CXL设备托管的内存可以被bias到Host侧也可以bias到Device侧。bias到谁那边谁访问就快另一边访问可能要触发迁移。这个机制本质上也是一组状态只不过维护的不是cache line数据而是“这块内存在逻辑上更贴近谁”。3.2 UCIe不关心cache line只关心die-to-dieUCIe做的是chiplet之间的物理互连。它不处理缓存一致性也没有CHI那种REQ/RSP/SNP/DAT通道语义。它更像一个适配器把上层的CXL、PCIe或者其他die-to-die协议打包成统一flit通过很短的die间链路搬运过去。因为UCIe不管上层协议它自己的状态机主要集中在链路训练、加扰、CRC校验和低速边带通信上。你可以把它看作一个极其可靠的高速隧道。CHI和CXL的信号跑到die边界时UCIe负责把格式转换成芯片间能传输的电气和协议形式到对面再还原出来。做CXL多die扩展时CXL协议负责一致性UCIe负责物理拼装两者不冲突各管一段。3.3 OpenCAPI、Gen-Z、CCIX殊途同归的三个开放协议OpenCAPI的核心卖点是允许加速器直接访问CPU的虚拟地址并且参与一致性。它引入了一套地址翻译机制加速器带TLB而不是简单拿物理地址发请求。状态机层面是MESI类加上和地址翻译相关的一组pending状态。Gen-Z走的是内存语义fabric路线。它把整个系统看成一个巨大的内存池所有CPU、加速器、存储设备都挂在同一个fabric上通过逻辑地址寻址。Gen-Z的状态机更强调内存语义比如read、write、atomic operation对传统缓存状态的维护相对弱一些。CCIX则是在PCIe物理层上做缓存一致性。它用PCIe的包格式承载一致性消息利用额外定义的协议层完成MESI状态流转。问题在于PCIe物理层为IO场景设计承载一致性snoop时需要额外的协议开销和延迟。CCIX在早期有一定生态后来OpenCAPI、Gen-Z、CCIX的很多设计想法都被并进了CXL现在新设计选CCIX的时代基本过去了。3.4 一张总表把六个协议的状态和路由摆一起协议稳定缓存态自己定义完整一致性FSM主要路由依据流控特点CHI7个是地址Hash到HN TgtID PBRCredit端到端独立通道CXLM/S/I Bias是HDM解码/CXL Switch路由Credit 重试UCIe无数据缓存态否die/die间端口映射CRC 重传OpenCAPIMESI类 TLB态是虚拟地址翻译后路由CreditGen-Z内存语义一致态是逻辑地址到SliceCredit/重试CCIXMESI类是PCIe BDF/地址路由PCIe流控这张表是帮你在选型时拉主线的。CHI擅长SoC内部CXL擅长外部扩展UCIe擅长chiplet拼接剩下三个协议现在更多是历史遗产和借鉴来源。4. PBR路由策略表和协议头共同决定的下一跳4.1 先厘清PBR的三个重名PBR在网络工程师嘴里是Policy-Based Routing在图形学里是Physically Based Rendering在互连协议团队里又没有严格统一的定义。有些团队的PBR是Port Based Routing有些是Protocol Based Routing还有些是Packet Buffer Routing。为了避免争议这里我按Scale-up互连上下文把它读成Protocol/Policy-Based Routing——也就是“协议头加策略表共同决定路由”。这种路由和传统按地址查表不一样。地址路由只问“这个地址在哪个区域”而PBR路由还会问“这笔消息是什么协议、什么Opcode、什么QoS、需不需要走特殊通道”。同一个地址范围可能同时存在一致性请求和IO请求如果都走一条路径一致性请求可能被IO流量堵死系统直接死锁。4.2 PBR在底层怎么工作底层执行时可以抽象成一次查表从包头取出若干比特作为key查一张策略路由表输出下一跳端口和优先级。key不是简单把地址拿过来而是把协议类型、事务类型、QoS、目标ID组合在一起。// 伪代码示意 route_result pbr_lookup(packet_hdr hdr) { uint64_t key hash( hdr.protocol_id, hdr.opcode, hdr.qos, hdr.tgt_id, hdr.addr ); route_result ret route_table_lookup(key); if (ret.valid 0) { ret.port default_routing(hdr.addr); } return ret; }默认路由兜底非常重要。Scale-up系统里策略表不可能覆盖全部组合漏查表时返回默认端口比直接丢包更容易在验证阶段暴露问题。当然安全要求高的场景会希望漏查表直接报错这取决于产品定义。PBR的关键不在查表动作本身而是table entry的维护。当系统发生热迁移、内存分区调整、CXL Switch策略变化时PBR表项必须同步更新。如果更新不是原子的某些包可能用旧表项进了错误端口就会产生数据一致性风险。4.3 CHI和CXL/Gen-Z中PBR的形态差异CHI里的PBR主要体现在NoC/Crossbar对消息的调度和路由。CHI本身已经给出了TgtIDNoC可以把它当硬路由键但如果NoC想做协议隔离就需要再解析Opcode和QoS把这些字段也塞进查找键。比如snoop类消息永远走最高优先级VC写回应消息和普通读请求分开避免缓冲互占。CXL里的PBR更接近“地址解码协议分派”。CXL Switch面对一个包时先判断是CXL.io/CXL.cache还是CXL.mem再查HDM寄存器确定这个地址属于哪个下游端口。CXL 3.0的增强型交换路由逻辑本质上就是一张更大、更动态的策略路由表。Gen-Z从一开始就按fabric思路设计所有节点都是逻辑地址空间的一部分。它的路由表把逻辑地址映射到具体的memorizer/slice并且支持多径。这和你去查一张大型路由表非常像只是查表延迟必须在亚纳秒到几纳秒级别。4.4 路由表本身也是一个状态机很多人以为只有缓存一致性才有状态机其实是错的。PBR路由表在更新期间每个entry可以处于“稳定”“待更新”“回滚”等状态。如果一笔请求刚好落在正在更新的entry上必须在旧状态和新状态之间做出原子选择不能看到一半的表项。更实际的一点路由与流控强相关。CXL Switch里经常要为不同端口和VC维护Credit计数器查表后输出端口没有Credit这笔包就得被暂存并等待。这个等待过程也是在跑一个状态机从“路由完成”到“等待Credit”再到“发送完成”。一旦Credit返还逻辑写错链路就可能停摆。5. 比特级走查一笔跨协议读请求如何穿越状态机5.1 场景构造假设一个CPU Cluster内部走CHI外部通过CXL访问一个挂在CXL Switch上的远端内存池中间的die与die通过UCIe拼接。CPU对0x8000_1000地址发起一次ReadShared请求。这个过程不是一次简单的“读内存”而是多级协议栈接力。第一步CPU侧L2 missCHI的RN-F识别到地址不在本地生成REQ flit其中Opcode是ReadSharedTgtID指向负责远端地址的HN或桥接节点。第二步HN/PBR查表后发现这个地址被映射到CXL域于是把CHI请求转换成CXL.mem的读请求目标端口记为CXL Switch下游端口。第三步UCIe把转换后的消息封装成flit加上CRC和链路层信息通过die间物理层送出去。到这一步缓存一致性已经从CHI的七态语义切换成了CXL.mem的内存语义。CXL Switch收到包后根据HDM解码和PBR策略把请求转发给挂着内存池的端口。内存控制器读取数据后按原路返回数据回到CPU ClusterCHI的RN-F把cache line状态更新为SC。5.2 关键节点状态变化节点初始状态消息事件最终状态CPU L2I收到ReadShared响应数据SCCHI目录无记录记录远端映射和共享者有效条目CXL设备内存无缓存态响应CXL.mem读无变化UCIe链路L0稳定承载flit传输L0稳定这里要注意CHI的“SC”和CXL.mem的“普通读”不是一回事。CXL.mem并不维护缓存状态它只保证“读到一致的数据”。以此类推如果在CXL域里还存在另一个缓存代理那代理就必须维护自己的M/S/I状态并通过CXL.cache与Host交互。所以“缓存状态”不是全链路通用的而是每一跳各自维护。5.3 流控、重试和死锁避免这笔读请求在穿越多个协议域时每个域都有自己的流控。CHI通道靠Credit发给RN之前要先确认对端有Credit否则包不能发出。UCIe用协议层和链路层的缓冲die间如果接收端满只能流控回压。CXL Switch则可能因为同一个下游端口有大量请求对部分请求进行重试。真正要防的是死锁。CHI、CXL、OpenCAPI这些协议都把请求、响应、snoop、数据分成独立通道或独立VC就是为了避免“请求占满缓冲后等响应响应又被请求堵死”的环路。设计多协议桥接IP时最忌把不同通道合并成一个FIFO省面积的结果就是随机死锁。6. 从状态机到工程选型我在这六个协议上踩过的坑6.1 选型建议做CPU Cluster级的全一致性互连CHI基本是标准答案状态丰富时序收敛工具链也成熟。做服务器外部扩展CXL是当前生态最稳的方向。做Chiplet多die集成就绕不开UCIe。OpenCAPI、Gen-Z、CCIX除非有存量IP或者特殊客户要求否则新项目我建议尽量靠向CXL。选型时另一个容易被忽略的点是“死锁域”的划分。CHI在SoC内部划分通道CXL在PCIe物理层上划协议域UCIe在die间划物理链路。三者的死锁边界如果不一致桥接处就可能出现协议规范没覆盖到的循环等待。提前画清每层有几个缓冲域比选哪份协议更重要。6.2 状态机实现里的三个坑第一个坑是partial dirty。UDP状态最容易被新手当成普通UD处理结果写回整行数据浪费带宽只是小事如果字节使能没处理对还会把没写的旧字节当成有效数据传出去。务必在data flit里显式维护byte enable。第二个坑是稳定状态和过渡状态混在一个状态寄存器里。CHI这种协议一个cache line要同时记录稳定状态和outstanding事务状态两者本来就是两个维度。强行合成一个状态枚举会产生大量枚举组合验证用例呈指数级膨胀。分开写稳定状态一个regpending事务状态一个reg再用约束保证组合合法。第三个坑是PBR表更新和事务并发。我踩过最严重的一次是CXL Switch热更新路由表时没有先暂停相关端口的事务结果有一笔请求读到新旧混杂的路由entry被送到错误的内存设备。解决办法是给路由表加上类似“更新隔离窗口”的机制或者用双buffer原子切换。6.3 验证建议多协议互连项目建议一开始就用断言做每条状态机的合法转移约束。比如CHI里UD只能被snoop转成SD或SC不能直接跳UDPCXL Switch里某个端口正在回包就不能立刻把该端口的HDM entry置为无效。断言虽然写起来烦但能在回归测试里救你无数次。对PBR路由我更推荐做“协议感知”的定向测试构造同一地址不同Opcode的包看它们是否走同一个VC构造高QoS的snoop请求确认它不会被低优先级写回堵死。随机测试能覆盖地址空间但覆盖不了策略语义策略语义必须靠人肉设计cases。最后再分享一个联调习惯每加一个新协议桥接我都会先让两侧跑最短的读写环路比如单地址read-modify-write等这条环路稳定后再开outstanding并发。一次并发开太大状态机同时跑到wild状态时你根本分不清是CHI目录错、CXL Switch错还是UCIe丢包。串行定位永远比并行猜错快。