前阵子有个做加速器芯片的朋友跑来找我,说他们主核数量一上来,系统性能非但没涨,反而整体吞吐还掉了。我问他内部互连用的什么,他理所当然地说“AMBA总线啊”,我再追问一句:是AMBA CHI协议,还是更老的AXI/ACE?他就卡住了。这种反应我见得挺多——很多工程师把AXI当成AMBA家族的全部,直到核多了、一致性问题炸了,才开始正视CHI。
这篇不打算写成协议规范的翻译,我想从一个做互连系统设计的工程师视角,把AMBA CHI协议拆开揉碎讲清楚:它到底解决了什么架构问题,几条关键机制是怎么工作的,到了真实的高性能芯片里又是怎么落地和取舍的。适合三类人看:准备上多核/大小核SoC的架构师,做互连验证或SoC集成的工程师,以及想把缓存一致性概念补扎实的读码人。
1. 从AXI到CHI:总线式一致性为什么卡住了多核SoC
1.1 所谓“一致性”,到底是一笔什么账
先回到最根本的问题。
CPU、GPU、NPU各自带着一级二级缓存跑,主存里同一份数据,可能同时在好几个缓存里都有副本。某个核把这份数据改了,其他核手里的旧副本如果还继续用,程序立刻错乱。解决这个问题的机制,就叫缓存一致性(Cache Coherence)。
拿生活里的事类比:一个项目组共用一份计划表,会议室有一份纸质原版,每个人桌上都有复印件。甲把“上线时间”改成了周五,没通知别人;乙看自己桌上的复印件还是下周一,最后按错版本汇报了。一致性的本质,就是确保任何人改动后,其他人手里拿到的要么是最新版本,要么能立刻知道版本变旧、需要重新去问。
经典MESI协议把这层关系收敛成四种状态:Modified、Exclusive、Shared、Invalid。每个缓存行知道自己是改了还没回写、独占一份、共享一份、还是已经失效。多核之间围绕这四种状态互相协作。听起来不复杂,但问题在于——靠什么通道协作、协作一次要花多少时间,这是总线架构的分水岭。
1.2 ACE那条路,为什么人多就塌
早期的AMBA AXI把主存读写做得很好,但它本身不管一致性。后来ARM在AXI基础上加了ACE扩展,用一套附加信号在总线上广播“谁有这份数据”的请求。
ACE的思路是好的,实际跑起来却有几个死穴。
第一,总线是单刷的。AXI/ACE本质还是共享总线结构,所有一致性事务都要经过中央仲裁,核一多,仲裁器和总线带宽立刻变成瓶颈。
第二,snoop是广播式的。一个核想读某块数据,ACE会让互连把查询请求发给所有可能持有该数据的副本。核数量从4涨到16,一次查询的开销并不是线性涨,而是近似平方级暴涨。多核SoC上经常出现“一致性风暴”,一大半能力都耗在互相确认版本上。
第三,物理时序扛不住。总线结构在芯片版图上走线长、扇出大,工作频率很难往上提。低功耗移动SoC还能忍,到了服务器基准频率2GHz以上、核心几十个的场景,ACE基本就是不可持续方案。
我在一个八核项目里当时实测过:核数翻倍后,ACE互连上的一致性事务延迟没有下降,反而因为仲裁排队被拉高了将近三成。这就是总线式一致性的天花板。
1.3 CHI的思路:把“总线”改成“网络”
CHI全称Coherent Hub Interface,是AMBA 5的核心互连协议。它从根上换了一套思路:
- 互连不再是总线,而是点对点网络;每个节点通过通道收发独立报文。
- 一致性事务被拆成标准报文,像快递一样沿路径送抵目的地。
- 引入明确的Home Node(主机节点)承担秩序裁定和目录记忆,替代无脑广播。
- 物理层、链路层、协议层分离,各层可以独立优化。
打个比方:AXI/ACE是公司内线电话,人一多声音就糊;CHI是标准邮政体系,每个包裹写清楚地址、走路由、有签收凭证。单体延迟略高,但整个系统可以扩展到几十上百个节点。
ARM从CHI-A一路演进到现在的CHI-E、CHI-F,每次大版本都在补新能力:更低延迟的响应、更灵活的缓存状态、跨Die扩展语义、更细粒度的QoS控制。这些能力不是锦上添花,而是现代高性能SoC真刀真枪逼出来的需求。
2. 先把名词理清楚:节点、通道和流控这三件事
学CHI最容易劝退的点,是规范里一堆缩写:RN、SN、HN、MN、REQ、RSP、DAT、Credit、Flit…… 我建议一次只抓三件事:谁在说话、走什么通道、怎么控制流量。
2.1 节点角色:谁负责干活,谁负责裁判
一套CHI系统里,角色划分非常清晰:
| 节点类型 | 全称 | 典型职责 |
|---|---|---|
| RN | Request Node | 发出读/写请求的节点,典型是CPU cluster、GPU、AI accelerator |
| SN | Slave Node | 响应访问的末端,典型是内存控制器、外围寄存器 |
| HN | Home Node | 一致性事务的协调者,持有目录信息,负责裁定缓存状态和转发Snoop |
| MN | Miscellaneous Node | 系统级配置和维护接口 |
你可以把HN理解成物业公司:每栋楼(RN)报修、装修、改动都要先到物业登记;物业知道谁家有钥匙副本,出了纠纷由物业按规章裁定。SN就是仓库,存着最原始的那份货物。
实际芯片里,一个RN可能对应一个CPU簇,一个HN可能对应一层末级缓存或内存控制器所在的节点。分配关系很灵活,但逻辑职责不变:RN负责发起,HN负责裁判,SN负责存储。
2.2 三条通道:请求、响应、数据各走各的
CHI节点之间有三条独立的单向通道,分别是:
- REQ通道:承载读写请求和Snoop消息,RN发给HN的是新请求,HN发给RN的通常是Snoop和维护类消息。
- RSP通道:承载对请求和Snoop的响应。比如“我这里有数据”“我完成失效了”。
- DAT通道:专门传数据块,比RSP通道更宽,针对Cache Line大小优化。
三条通道分开有什么意义?最直接的好处是避免堵车。读请求可以走在REQ上,数据响应可以并行从DAT回来,写数据和响应也能同时流动。如果所有消息挤在同一条通道,一条大数据块可能把后面几十个轻量响应全部堵死,那延迟就没法看了。
我自己调试中经常遇到的一种情况是:REQ通道压力不大,但DAT通道因为连续回写大块数据被打满,导致后续读命令排队。这种情况在AXI时代不太容易意识到,因为总线上所有流量共用一个物理仲裁,互连实现会默默帮你排队;CHI的通道分离让问题被看得清清楚楚,也让大家开始认真做通道级QoS。
2.3 Credit流控:防止把对方邮箱塞爆
通道有了,还要防止发送方无节制发报文把接收方缓冲区打爆。CHI用的是经典信用机制(Credit)。
大致逻辑是:每个方向有一组信用额度,每发出一份报文,发送方的可用额度减一;接收方处理完报文,会通过返回Credit的方式把额度补回来。发送方看到额度不够就停下来。这样一来,双方不需要时刻握手确认,只知道“我现在还有几个可用的邮包号”,就能安全地持续发数据。
这套机制我最喜欢的一点是它的可预测性。信用不足时,通道自然背压,延迟可计算;信用充足时,报文可以流水线式全速跑,几乎感觉不到协议开销。性能调优时,查“是带宽受限还是信用受限”比乱调优先级有效得多。
3. 缓存状态机与一致性域:谁有权限、谁说了算
通道把话传起来了,接下来最硬核的问题:一个缓存行在系统里到底处于什么状态?谁有权限改?改完谁能看到?这些都要靠状态机和一致性域来回答。
3.1 CHI的缓存状态:比MESI多了“脏共享”这一档
经典的MESI是四状态,CHI在通用缓存场景下常用的状态包括:
| 状态 | 含义 | 大致对应MESI |
|---|---|---|
| I | 无效副本 | I |
| SC | 共享且干净,多个核可能都有副本 | S |
| SD | 共享但本副本是脏的,最新主体数据在这里 | M/S的组合折中 |
| UC | 唯一且干净,只有当前核有这个副本 | E |
| UD | 唯一且脏,当前核是唯一拥有最新数据的副本,需要回写 | M |
为什么要把共享状态细分成SC和SD?因为现实里出现过一个微妙场景:A核和B核同时持有某块数据的副本,但脏数据只能有一个人背着。如果A改了数据还没来得及回写,B也想读,这时候如果互连不区分“谁手里是脏副本”,就必须让B强制回写或强制失效,白白增加流量。CHI的SD状态允许互连在知道某个共享副本是脏的前提下,把数据凑到一块、直接决定谁来回写,减少一次无谓的内存访问。
这个细节初看不重要,但我们在做低功耗设计时发现,精确识别“脏副本归属在哪里”,能省掉相当可观的回写流量。高核心密度场景下,这种省法非常值钱。
3.2 POC与POS:一致性和顺序性的最终裁判
CHI规范里有两个概念,我建议所有读协议的人第一时间弄懂:POC(Point of Coherence)和POS(Point of Serialization)。
POC是某个地址的“权威副本汇聚点”。在POC这个点,无论数据在哪个缓存里、还是已经回到主存,都能被看出来谁是最新、谁改过。POS则是所有对该地址访问的排序点——谁先谁后,在这里被一锤定音。
大多数实现里,HN就是特定地址范围的POC和POS。CPU核发来的所有对同一地址的访问,都会先到HN“报到”,由HN按顺序安排:先处理谁的,再处理谁的,哪些Snoop要发出去,数据从哪里取。有了这个仲裁点,多核并发时“两个核同时改同一个变量”这类争议就有了统一的裁决依据。
3.3 目录与Snoop Filter:从“广播问”变成“精准问”
传统总线式一致性是广播问询:每笔事务问所有节点。CHI则借助HN里的目录来缩小询问范围。
HN会记录“这个地址可能被谁拿着副本”。有新请求进来时,HN查目录,只向真正可能持有副本的RN转发Snoop,其余节点完全不需要介入。这个设计在真实芯片里通常体现为Snoop Filter或分布式目录,常见实现会用哈希、部分Tag来做规模压缩。
我见过不少团队第一次从ACE切换到CHI时,最直观感受就是“写Snoop的次数变少了”。不是因为协议魔法,而是因为目录把冗余问询过滤掉了。
4. 跟着一次ReadShared走完一致性事务全程
纸上谈兵再多,不如拆一条真实事务。这里以最常见的共享读ReadShared为例,完整走一遍。
4.1 请求方打包:RN的第一次动作
假设CPU0要读地址0x8000_4000的数据,而且允许别人也保存共享副本,于是它发出ReadShared请求。
这颗请求不是像AXI那般在总线上“抬电平”,而是组织成一份CHI报文,装进REQ通道。报文里携带地址、访问大小、请求类型、目标节点ID、事务ID等信息。节点ID用于路由,帮助报文沿着互连拓扑到达对应的HN。
在发出之前,RN要检查REQ通道的信用额度。额度够,报文才能上通道;额度不够,就得等。这个细节很多刚上手的人忽略,等到链路层credit溢出才发现问题,那是已经晚了。
4.2 HN的裁判动作:查目录、发Snoop
HN收到ReadShared后,先查自己的目录:0x8000_4000这个地址,系统里有谁可能持有副本?
如果只有CPU0自己碰过,那目录清空,HN直接跳去SN取数据。但如果目录显示CPU1也可能有副本,HN就会向CPU1发一个Snoop请求,问:你手里有这块数据吗?是什么状态?如果是脏副本,请把数据带上回话。
这里有个工程抉择:Snoop是按目录精确派发,还是按阉割策略盲目广播?ARM公版CMN走的是带目录过滤的网格Snoop,自己设计互连时就要权衡目录容量、命中率和过滤精度。目录太小过滤不住,Snoop流量爆炸;目录太大,访问延迟和面积又飙起来。
4.3 数据怎么回来:CompData与通道分工
被Snoop的CPU1返回SnpResp,说明自己有没有数据、数据是否干净。HN据此汇总所有信息,决定最终状态:
- 如果CPU1持有脏副本,HN会让脏数据直接或间接回到请求方,同时更新SN里的主存内容;
- 如果各副本都是干净的,HN可以直接从SN读一份干净数据送给CPU0。
数据返回时走DAT通道,带有完成语义时是CompData报文,不带数据只确认时是Comp报文。CPU0拿到数据后,把这一行标记为SC(Shared Clean),整个共享事务完成。
把这一步消化掉,CHI的大半个框架就通了。后面所有复杂事务,本质上都是在这个“请求-查目录-Snoop-收集-返回”的骨架上增加不同约束。
5. 写路径上的关键操作:独占、回写与目录派发
读事务相对简单,写路径才真正考验一致性协议。这里拆几个高频写相关操作。
5.1 改写前的“拿资格”:ReadUnique与MakeReadUnique
CPU想改某个地址的数据,不允许别人手里还有可用副本,于是就必须先“清扫现场”。最典型的就是ReadUnique:HN收到后会把所有其他RN的副本通过Snoop置为Invalid,然后返回给请求方一份数据,状态标记为UD或UC。
这个流程本质上是把“别人手里的旧副本”全部销毁。如果某个RN持有脏副本,Snoop会把它逼出来,脏数据得先还给HN,再由HN交给请求方,保证请求方拿到的就是全系统最新版本。此时请求方才能放心改写。
另有MakeReadUnique一类操作,适合请求方已经持有共享副本、且不想再从内存读一遍数据的场景。它把别人的副本全置无效,但返回路径可以不带数据,节省带宽。这是一类典型的协议优化:读懂语义,才能给系统省流量。
5.2 什么时候回写:WriteBack与WriteEvict
缓存行被替换时,脏副本不能直接扔,必须把最新数据交还到POC处,这个过程叫回写。常用的回写报文包括WriteBack(带回写数据)、WriteEvict(干净副本的逐出,不携带数据,仅做目录更新)。
回写的时序设计是性能敏感点。如果让回写事务全走完整的“请求-响应”往返,会产生大量排队延迟;CHI允许部分场景下数据提前走DAT通道,甚至用管道化方式,让后一笔回写能接着前一笔尾巴,不必等前一笔完全落定。我们优化多核跑spec时,经常就是调回写通道的缓冲深度和写合并策略,收益非常直接。
5.3 目录派发与一致性三要素
前面讲过,同一地址的所有事务最终都要汇聚到POS那里定序。围绕写操作,一致性有三个可检验的标准:
- 原子性:一件事在别人看来要么全发生,要么没发生;
- 顺序性:同一地址的操作,在POS上能看到一致的前后序列;
- 可见性:某个核写入后,其他后续访问能看到这个结果。
这三点听起来是理论,实际调试时全都会变成具体bug。最常见的“看不见写入”问题,往往不是数据没写,而是请求方和观察者的访问没经过同一个POS,或者Snoop派发漏了一个副本。对着CHI的报文日志排查时,我会先看Snoop是否覆盖了所有RN,再查响应是否带回正确状态,最后才怀疑数据通路。
6. 量产层面的取舍:CMN网格、QoS与跨Die扩展
协议再漂亮,落到底层还是面积、功耗与带宽的博弈。这一节聊量产芯片里CHI常见的高性能落地形态。
6.1 CMN网格互连:把节点铺成地图
ARM公版的高性能互连CMN系列,本质就是用网格拓扑把CHI的RN、HN、SN节点串起来。每个节点在网络里有固定坐标,报文按最短路径在网格上跳转,延迟由跳数决定。
地址到HN的映射也不是随便定的。CMN采用哈希机制,把地址空间分散到多个HN上,让不同地址的流量并行处理。哈希选得好不好,直接决定多核访问主存时的热点分布。我们在早期原型上吃过亏:哈希太简单,导致某一片HN排队严重,其他HN闲得没事干;换成动态哈希后,整体带宽提升接近两成。
6.2 QoS:给报文划分等级
多租户、多级缓存、多类加速器共存的场景里,不能所有报文一视同仁。CHI报文里带有QoS相关信息,互连节点可基于优先级进行带宽分配、整形和调度。
我举个实际场景:实时性要求高的GPU渲染帧,如果它的一笔读请求排在大量后台批量写后面,帧率就会肉眼可见地抖动。利用QoS把GPU相关请求设为更高服务等级,把后台批量写调度到低优先级队列,延迟尾巴立刻收敛。
6.3 Chiplet时代:CHI怎么跨Die连
近几年Chiplet是绕不开的话题。多个Die要共享内存一致视图,光靠PCIe那种松耦合远远不够。ARM顺应这个趋势,把CHI做了跨芯片扩展(C2C),让CHI报文能封装在Die-to-Die接口上跑,典型落地就是UCIe物理层承载CHI语义。
跨Die一致性比片内多了两道坎:延迟高出几个数量级,物理链路还可能出错。所以C2C版本普遍会强化重传、错误检测、宽限期与调试机制。这个话题我在实际评估中觉得最有意思的地方在于:它把片内一致性协议直接暴露到了封装边界上,很多原本只在NoC内部的调试手段,现在得面向多Die系统重构一遍。
7. 自学CHI的正确姿势:路径、工具与常见误区
最后聊点学习层面的经验。
7.1 先补ACE再啃CHI
直接打开CHI规范靠硬啃,非常容易劝退。我推荐的学习路线:先读ARM AMBA ACE规范,把MESI、一致性域、snoop这些老概念建立起来;再读AMBA 5 CHI规范,边读边对照ACE概念看差异,重点关注状态机、通道语义和事务流程图。
读规范时别追求逐章精读。先抓住三张图:缓存状态转换图、事务序列图、通道字节布局图。这三张图看懂,协议半边天就算拿下了。
7.2 用验证环境反推协议行为
纯读规范是纸上谈兵,有条件的一定要搭验证环境。UVM是主流,CHI agent可以买商业VIP,也可以基于开源模型改。无论哪种,都建议先跑一条简单ReadShared事务,把完整的协议交互日志导出来,对照时序图走一遍。
我见过不少同学卡在“能写测试但看不懂波形”。这个阶段最有效的办法是盯着三个点看:请求从哪个通道发出、HN在哪一拍发出Snoop、数据从哪个通道回来。把这三条主线对清楚,事务就透明了。
7.3 常见认知误区
- 误区一:CHI只是AXI的升级款。错。两者模型差异巨大,CHI是报文网络,AXI是共享总线。
- 误区二:用了公版CMN就万事大吉。错。CMN只提供节点骨架,哈希策略、QoS配置、缓冲深度全都要自己做。
- 误区三:事务乱序越多性能越好。错。乱序能提升利用率,但会付排序和缓存成本,边界要测试。
- 误区四:CHI只服务于移动端。实际上服务器、车规、数据中心加速器都在用,Chiplet推动下它的阵地还在扩大。
我在实际项目中感受到最深的一点是:互连已经不再是SoC的配角,而是定义性能边界的核心不动产。谁能把CHI的协议语义吃透,谁就掌握了在几十个节点之间精准调度数据流向的能力;这东西不需要多少花活,但确实需要把状态机、通道、流控这些基本功像肌肉记忆一样刻进脑子,才能真正调出一个既一致、又快、又省电的系统。
好了,技术就聊到这里。接下来如果你手头正好要上手一个多核互连设计,我建议不要急着看五花八门的新特性,先把ReadShared和ReadUnique两条事务,在自己环境里跑透,后面一切都顺了。