ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

开源缓存一致性互连协议对比:从CHI七态到PBR路由的状态机深度解析

开源缓存一致性互连协议对比:从CHI七态到PBR路由的状态机深度解析 我记得三年前第一次在Rocket Chip上把两个RISC-V核通过TileLink接在一起时根本没有意识到Scale-up互连协议会占掉后面大半年的调试时间。当时我以为只要总线带宽够、延迟够低缓存一致性自然就能跑通结果一次共享数据的原子操作让整个SoC挂死。从那以后我养成了一个习惯拿到任何互连协议先不看它宣传的吞吐和延时而是先把状态机图画出来把每个事务的比特字段拆开看它在极端边界下怎么走。这篇文章就是基于这个习惯做的六个开源协议/开源实现层面的对比覆盖CHI七态、TileLink、AXI4-ACE、OCP、Wishbone还有NoC上常见的PBR路由机制适合正在做多核SoC集成、缓存一致性IP选型、NoC调优或者干脆被状态机折磨到秃头的朋友。1. 从一块“绕路的脏数据”说起Scale-up互连为什么必须死磕协议层很多做系统软件的朋友会把Scale-up和Scale-out搞混。Scale-out是加节点、加机器靠网络把一堆独立系统串起来Scale-up是指在同一个一致域里加核、加加速器、加内存控制器让所有处理器看到同一份内存视图。后者对互连协议的依赖远高于前者因为一旦同一份缓存行被多个核访问协议必须回答三个问题谁拥有这行数据这份数据是干净还是脏的当某个核想写的时候怎么让其他人手上的副本全部失效或更新这三个问题最终都落在状态机上。所以Scale-up互连协议不是在物理链路上传数据这么简单它本质上是分布式状态机协议。总线上的每个节点无论是CPU核、LLC、内存控制器还是PCIe桥都会为每个缓存行维护一个状态。事务之间互相耦合比如你在总线上看到一个ReadUnique请求它背后必须伴随着其他节点收到Snoop/Probe、返回响应、数据回家、完成确认这一整个闭环。任何一个环节的状态机写错了轻则性能回退重则死锁活锁。这也是为什么我对比协议时不做那种“口号式评测”。六个开源协议实现CHI、TileLink、AXI4-ACE、OCP、Wishbone再加上NoC层常见的PBR策略路由放到同一个框架里比比的是以下四件事缓存一致性状态模型状态集合有多大每个状态代表什么权限。状态迁移的触发事务读、写、探测、回写、唯一化各自怎么走。协议头的比特开销每个事务需要多少控制字段扩展性如何。状态机实现成本在RTL里用Verilog写出来是能用一段式草草糊弄还是必须上三段式加断言。这个框架比单纯跑benchmark更接近工程本质。带宽不够可以加线、加通道但状态机设计错是要返工的。下面的每一节我都会沿这条线拆一个协议或一类机制。2. CHI七态不是魔法是一张带“空/脏”编码的状态机大图ARM的AMBA CHI协议是这六个里一致性最强、也是状态机最复杂的一个。CHI的全称是Coherent Hub Interface原本是为多核服务器、移动SoC里那些动辄十几个核加一堆一致性代理设计的。它在协议栈里把节点分成Request NodeRN发起请求的核/加速器、Home NodeHN管理某段地址的节点通常是LLC切片或内存控制器、Subordinate NodeSN最终内存或外设。一个缓存行的状态并不只存在于RN侧HN侧还要维护一个“监听过滤/目录”状态两侧状态机互相配合。2.1 七态到底怎么来的很多文章直接说CHI七态但没说这七个状态为什么长这样。CHI的缓存状态本质上还是从MOESI那套语义长出来的M是ModifiedO是OwnedE是ExclusiveS是SharedI是Invalid。MOESI的问题在于没有表达“独占但数据无效/空”的情况也没有表达“部分字节脏”的情况。CHI在MOESI基础上做了两件事一是把Clean和Dirty进一步拆分二是引入了Empty和Partial偏移状态。在我看过的几个开源CHI实现里典型的七态集合可以归纳为状态缩写含义InvalidI本节点没有有效数据副本Shared CleanSC数据干净且其他节点也可能有副本Unique CleanUC本节点独占数据干净其他节点无副本Unique Clean EmptyUCE本节点独占地址但数据内容无效可被整块覆盖Unique DirtyUD本节点独占数据脏最终要把数据写回内存Shared DirtySD数据脏但可以共享本节点负责最终写回Unique Dirty PartialUDP部分字节有效且脏本节点独占不同实现会对状态集做裁剪有的把SD和UDP合到别的状态里因此并不是所有文档都写七个。但七态的价值在于它把两个经常被忽略的场景显式化了一个是“地址已经被我占住但数据还没来/已经被作废”典型场景是GPU要同一段地址反复做原子写另一个是“数据部分脏”典型场景是DMA只更新了缓存行中间的32字节。如果你不显式表示这两种情况协议就得用额外的“无效事务”去覆盖总线效率会难看很多。2.2 一条状态转换事务的全过程用一个例子说明七态怎么工作。假设RN-A对地址X发起一次ReadSharedHN收到请求后查目录发现RN-B手里有Unique Dirty副本。这时候HN不会直接回答“内存里有数据”因为内存里的数据是旧的真正的新数据在RN-B手里。于是HN向RN-B发起SnoopRN-B收到Snoop后把自己从UD状态变成SD或者SC把脏数据写回到HN再通过Data通道返回给HN。HN把数据转发给RN-A同时更新自己的目录状态RN-A从I变成SC。整个过程跨了REQ、SNP、RSP、DAT四条通道任何一个通道上的状态迁移错位数据就会在总线上“绕死”。如果RN-A想写地址X它必须先做“唯一化”。RN-A不知道别人有没有副本所以发ReadUnique请求。HN收到后向所有可能持有副本的RN发Snoop让它们把状态变回I或失效。RN-A收到Grant和数据后直接从I跳到UD或UDP之后就可以本地多次写而不必再发总线事务。这就是状态机的核心作用它在数据路径上拍板告诉每个节点“你能不能写什么时候能写”。2.3 为什么必须用“三段式状态机”来实现CHI节点用Verilog写CHI节点状态机时我一贯坚持三段式。第一段是时序逻辑纯寄存器决定当前状态第二段是组合逻辑做次态转移判断第三段是输出逻辑把当前状态翻译成对外的控制信号。CHI的状态转移条件有很多是跨通道的比如要等REQ通道收到请求、同时SNP通道没有pending的snoop、还要满足credit计数非零。这些条件如果用一段式状态机一股脑写在一个always块里代码会膨胀到根本没法review状态转移和输出信号耦合在一起出了问题都不知道是状态跳错了还是输出时序错了。三段式对验证也友好。我在波形里看当前状态寄存器再把组合逻辑里判断的转移条件拖出来能够很快定位是“状态该跳没跳”还是“状态跳了但输出没跟上”。CHI这种协议状态机代码写的规范程度直接决定了IP能不能集成进大系统。3. TileLink TL-C在同一条总线上完成获取/释放/探测状态机更瘦但边界要摸清TileLink是Rocket Chip/Chipyard生态里最常见的SoC互连协议最初由SiFive设计和RISC-V社区绑定得很深。TL-C是TileLink的缓存一致性扩展虽然很多使用默认配置的项目只用了TL-ULuncached或TL-UHmostly cached但不涉及复杂监听但只要涉及多核共享缓存TL-C就绕不开。3.1 TL-C的通道设计和状态机强相关TL-C总共有A、B、C、D、E五条通道五条通道的名字就暗示了它的状态机思路A通道用于发起Primary操作比如AcquireBlock、Get。B通道用于Manager主动发出的Probe也就是监听/探测。C通道用于释放数据或者响应Probe常见ReleaseData、ReleaseAck。D通道用于返回数据或应答包括GrantData、AccessAck。E通道只有一个作用客户端向Manager确认Grant叫GrantAck。这里和CHI的显著区别是TL-C用“获取-释放”语义代替了CHI那种更细的“读共享/读唯一/回写/清理”语义。客户端要一块数据就发AcquireBlock要释放数据或者收到Probe后要交出数据就发Release。状态机的核心状态大致收敛在Invalid、Clean、Dirty三类语义上再带上一些过渡状态。相比CHITL-C的状态集合更小实现起来更轻因此Rocket Chip里到处都是它的身影。这种更瘦的状态机在多核小系统里是优点因为验证棋盘小、逻辑简单。3.2 一个容易踩的状态机边界Release时数据归属TL-C一个容易让人迷糊的地方是Probe和Release之间的握手。Manager发出B通道Probe以后客户端必须在C通道上回ReleaseRelease里要带上数据有没有被改写过的状态。如果客户端手上是Clean它可以发ReleaseData不带Dirty标志如果手上是Dirty就必须带Dirty标志Manager拿到Dirty标志后要把数据更新到内存否则数据就等于丢了。这个边界在真实RTL里会变成一个很刁钻的bug。我见过一个开源RISC-V核实现客户端在Probe进来时正好处于一个“数据刚从A通道的响应里写进来、状态还没有刷成Clean”的时间窗口。它回了一个Clean的Release但数据其实已经被改过了一部分。结果就是内存里的数据被旧版本覆盖另一个核后读到的是一块被回滚的数据。幸好当时是FPGA验证抓波形抓到凌晨三点。后来我们在TL-C状态机的每个转移边上都加了一条断言Release中携带的Dirty标志必须和当前数据缓冲区的实际改写标记一致。3.3 在比特层面看TL-C的开销TL-C的通道字段包括Opcode、Param、Size、Source、Address、Data、Mask这些基本组成。和CHI类似它也要在事务里带Source ID因为一个请求从发出到最终收到响应之间可能有多个事务在飞行。Source ID的位宽直接决定了系统里能同时支持多少未完成事务。很多设计图省事把Source ID位宽设小结果多核压力测试一跑master因为拿不到新ID而stall总线利用率掉一半。这种比特层的参数调整比状态机本身更容易在实际项目里决定成败。4. AXI4-ACE与OCP一致性不是总线专利扩展通道各有各的取舍有些团队并不打算完全切换到CHI或TileLink他们手上已经有大把AXI或OCP的IP核想给旧总线加上缓存一致性能力。这时候AXI4-ACE和OCP的一致性扩展就成了实用选项。这两个方案不是从零设计的状态机协议而是在现有总线上打补丁所以它们的取舍很有意思。4.1 ACE是给AXI4戴上的“一致性帽子”AXI4-ACE的基本思想是保留AXI4的五个通道AW、W、B、AR、R再加上两个和缓存一致性相关的辅助通道。ACE把一致性相关操作分成“带一致性的访问”和“不带一致性的访问”通过AR/AW通道上的信号位来区分。当某个master想要共享读或者唯一写时它会在AR通道上发出带共享域属性的请求总线上的一致性互连单元会解析这个请求并通过辅助通道向持有副本的master发起监听。ACE里的缓存状态机同样可以回溯到MESI/MOESI语义但工程上很多开源实现通过一个“监听状态”寄存器来跟踪每个master的副本归属。因为ACE本身是建立在AXI的点对点流控上的它需要确保监听请求和普通读写请求不会在同一个master里乱序完成。这个要求让ACE实现里加入了大量屏障barrier逻辑比如一个master先读后写同一地址ACE必须保证先读到旧值再写入新值看不到旧值就绝不能执行写。4.2 OCP把“可裁剪信号”做到极致OCPOpen Core Protocol是OCP-IP组织推出的开放核接口标准。它和AXI最大的不同在于信号列表不是固定的而是按配置项生成。你不需要突发传输就可以砍掉Burst相关信号你需要一致性就打开一致性扩展。这套思路对IP复用很友好因为挂接IP核时可以精确控制接口面积。但OCP的灵活性也会反噬。一致性扩展打开后每个master和interconnect之间要协商一堆配置项比如“是否支持Atomic”“是否支持Exclusive”“监听是同步还是异步”。配置项组合爆炸验证时如果每个配置组合都测一遍测试矩阵根本写不完。我后来对OCP项目的建议是锁死一套固定配置不要允许每个IP分别选不同的一致性选项否则互连矩阵里的组合状态机会超出人脑能维护的范围。4.3 用表格快速对比一致性扩展能力维度CHITileLink TL-CAXI4-ACEOCP扩展状态模型七态/可裁剪三态过渡MOESI类多数可配置独立监听通道SNPBAC/CD可配置死锁避免协议内置靠排序规则屏障方式配置相关开源实现数量中等多多少适用于Scale-up大规模高中中低中低从这张表可以看出CHI在设计目标上就是冲着大规模可扩展去的TL-C在中小规模多核里最舒服ACE只是为了兼容AXI生态的一致性补丁OCP则更像是一个框架而不是一种固定协议。选型时如果目标核数超过八个且要求强一致模型我不会首选ACE和OCP。5. Wishbone当不了主角PBR路由才是让NoC“长脑子”的地方5.1 Wishbone极简状态机适合做外围通路Wishbone是开源SoC里资格最老的总线标准之一它没有缓存一致性扩展也没有复杂事务重排序机制。Wishbone的经典状态机非常简单Master发起周期等Slave返回ACK或ERR超时就报错。它非常适合做寄存器配置、启动ROM、调试接口这类低压力通路。但在Scale-up互连的语境里Wishbone只能当配角。如果把它当主角放进多核缓存一致域设计者必须自己用Wishbone拼出一套“软件一致性”机制比如给每个核加一层软件维护的目录表用中断让核主动刷cache。这种方案协议层面看不到状态机复杂度复杂度全被推给了软件延迟和不确定性完全不可控。所以我通常的建议是能用TileLink或CHI做主数据面就别用Wishbone硬扛Wishbone留给控制面足够体面了。5.2 PBR路由不是转发是让NoC节点做决策很多人看到PBR第一反应是网络设备里的策略路由Policy-Based Routing或者CG渲染里的PBR材质。确实容易混但在SoC NoC语境中我讲的PBR是一类基于策略的互连路由机制交换节点不只看目标地址的低位完成“一刀切”路由还会读事务头里的QoS属性、缓存一致性状态、事务类型、虚拟通道ID再根据配置好的策略表决定这个包该去哪个物理端口、走哪条虚拟通道、需不需要插入等待状态。开源NoC框架如OpenSoC Fabric、Chipyard自带的一些NoC互连里都能用类似思路做路由扩展。为什么PBR对Scale-up尤为重要因为互连网络里的流量不是一个齐刷刷的集合。一致性探测Probe可能要求固定延迟如果被普通读写流量堵在队列里可能导致请求超时原子操作要求顺序性如果被分配到不同路径上可能把两个原子操作的相对顺序打乱。PBR策略表存在的意义就是把这些“协议的软需求”翻译成“路由的硬决策”。我在一个四路RISC-V芯粒互连项目里用过类似PBR的机制。当时有两类流量一类是核之间的一致性探测一类是PCIe传入的大块DMA。普通哈希路由经常让PCIe大包和一致性探测混到同一个虚拟通道里导致探测延迟抖动。后来我们加了一条策略凡是带Coherence属性的包默认映射到VC0并在每个交换节点独占一个队列大块DMA映射到VC1容量大但延迟不敏感。这么一改探测延迟的最大值从800周期降到了140周期左右解决了一个线上同步性能问题。5.3 PBR和协议状态机的边界怎么切PBR本身也有一张状态机但它和缓存一致性状态机职责不同。缓存一致性状态机管理的是“一份数据被谁持有状态是I/C/D”PBR状态机管理的是“一个事务当前在互连网络中哪个队列、下一步朝哪个方向走”。前者是目录/缓存状态后者是流控/路由状态。这两个状态机必须在接口处对齐否则就会出现“事务已经完成但路由节点还在等策略表授权”这种互相等待的死锁。我的做法是在协议节点和NoC节点之间留一个统一的credit接口策略表授权必须在credit授权之前完成不允许路由节点在策略未决的状态下继续接受新包。这样虽然会牺牲一点队列利用率但换来了可证明的“无环等待”。对比项缓存一致性状态机PBR路由状态机管理对象缓存行/数据块数据包/事务状态代表数据在哪个节点的什么状态包位于哪个队列/走向哪个端口核心操作Read/Write/Probe/Release查表、分配VC、仲裁典型状态数3~74~16死锁风险等待数据/响应导致队列头阻塞/策略表未授权6. 虐过RTL之后说说状态机编码与验证的一点经验6.1 三段式状态机在这些协议里的实际写法不管用哪个协议在RTL里落地时都可以套三段式状态机。区别在于是否把“状态转移”和“输出”彻底分开。CHI节点往往有多个状态变量比如请求状态、监听状态、数据缓冲状态有人会把它们合并成一个大状态用one-hot编码但defense-in-depth的做法是拆成多个小状态机每个状态机用三段式实现再用一个顶层状态机协调它们之间的握手。我写这类模块时有一个固定套路当前状态用两个寄存器数组表示一个叫cs一个叫ns。cs在时钟上升沿更新ns由组合逻辑根据cs和输入信号算出输出信号再根据cs算一次。每个可能进入的死锁状态都写异常分支一旦ns变成不应出现的编码直接拉error输出并进入stall。这套做法不需要UVM也能在FPGA上跑出比较明确的问题。6.2 状态机覆盖率绝不能只看行覆盖率缓存一致性协议的状态机bug很多出现在“状态组合”上而不是单跳路径上。比如CHI中RN同时收到一个Data包和一个Snoop包它到底是先处理数据还是先处理Snoop这个顺序如果没有在状态机里显式建模验证时就算行覆盖率100%也测不出来。所以我在验证阶段最依赖的是交叉覆盖率和序列覆盖率要专门构造“读写和监听同时到达”“多个master同时对同一地址发起唯一化”这类恐怖用例。很多开源核的bug就是在这些用例下暴露的。还有一点状态编码本身要留出非法状态断言。如果你用3bit编码7个状态还剩下1个非法编码。很多人不管这1个编码结果综合后逻辑翻转出了非法状态系统并不立刻挂而是等这个非法状态被某个路径再次触发才爆炸。我现在的习惯是每个状态机模块都加一段assert property 禁止任何时刻ns等于非法编码。6.3 选型时先画状态图再跑benchmark如果让我给团队选型我会先让架构师把以下图画出来一跃状态机的节点角色图、二典型读共享/写唯一/监听失效的时序图、三跨节点死锁场景的示例图。三张图画完协议间的差异会非常直观。CHI画出来复杂但全面TileLink画出来清爽但对复杂拓扑的支撑力有限AXI-ACE画出来会看到一堆屏障OCP画出来你会发现很多空分支要填Wishbone画出来基本就是三四个状态转圈。我始终觉得Scale-up互连协议的对比不应该停留在“谁带宽高、谁延迟低”这种营销语言上。对于那些跑不到边界情况的benchmarkCHI和TileLink的差距可能微乎其微但一旦多核压力测试或一致性协议风暴上来状态机设计是否严谨会立刻决定项目是平稳交付还是进入漫长的调试期。以我个人的经验看协议先看状态机写代码先画状态图调试先抓非法状态永远是这三件事优先级最高。
返回列表