☰
Arm Neoverse CMN-700缓存分区实战:SLC隔离与性能调优指南
2026/10/5 1:37:56 网站建设 项目流程

去年年底我帮一个做边缘云的朋友调优他们基于Neoverse平台的AI推理节点,发现一个很奇怪的现象: GPU在疯狂吃数据,CPU侧却经常卡在等内存上,整体吞吐死活上不去。后来排查到最后,问题竟然出在系统级缓存(SLC)的分配策略上——多个加速器核眼巴巴等着同一块最后一级缓存的数据,互相踩踏,性能自然被拽下来了。

这就是我要写这篇东西的原因。CMN-700是Arm Neoverse服务器SoC的互连骨架,而SLC和缓存分区(Cache Partitioning)是其中最能直接决定真实业务延迟、吞吐和无扰行为的关键机制。这篇文章会从架构角色、SLC内部组织,到缓存分区的配置、性能调优与踩坑排查,把所有核心细节一次讲透。适合正在做Arm服务器平台验证、异构加速器整合或性能调优的架构师、BSP工程师和应用开发者。

1. CMN-700到底管什么:为什么现代Arm服务器离不开它

1.1 从单核霸主到“拼核心”时代:互连才是瓶颈

早年间做嵌入式,一颗Cortex-A53就够了,总线简单,缓存小,不存在太复杂的互连问题。但到了服务器领域,Arm Neoverse平台走的是“多核、众核、异构加速”的路线,一颗SoC上可能集成了几十到上百个CPU核心,还挂着多块GPU、NPU、DPU,甚至各种CXL设备。这么多单元之间要共享数据、访问内存,就不能再靠简单的总线了。

CMN-700就是Arm Neoverse平台下的缓存一致性互连网络(Cache Coherent Interconnect Network),它相当于整个SoC的数据高速公路网。所有核心、内存控制器、IO设备、加速器都要接入CMN-700,通过这套网状互连交换数据。上一代CMN-600主要面向16核或者更小的服务器规模,而CMN-700针对的是更大规模、更高带宽的云和数据中心场景,支持更多节点、更多内存通道,并且加入了CXL(Compute Express Link)支持。

1.2 CMN-700架构里的关键角色

CMN-700由几类Node组成,每个Node负责不同功能:

  • HN-F(Home Node - Fully Coherent):一致性家庭节点,负责管理内存访问和跨节点的缓存一致性,SLC就挂在HN-F里。
  • SN(Subordinate Node):连接内存控制器、PCIe Root Complex等从属设备。
  • RN(Request Node):连接CPU核心、GPU等请求方。
  • CCG(Crosspoint Group):负责网格交换和路由,数据在这里转发。

这套架构用网状网络(Mesh)把节点串起来,不同节点间有多条路径,不会像老式总线那样一堵全堵。CMN-700支持多个HN-F节点,每个HN-F节点都有自己局部的SLC切片,多个SLC切片拼在一起,从软件视角看就是一块统一的系统级缓存。

我见过很多人第一次接触CMN-700时,最容易困惑的问题就是:SLC到底是“一块”还是“很多块”?答案是物理上分散、逻辑上统一。SLC切片分布在SoC的各个mesh节点旁,但协议和地址映射让它们对外表现为一个整体,这跟x86处理器的LLC(Last Level Cache)设计思路类似。

2. SLC内存系统深度拆解:你以为的“最后一级缓存”没那么简单

2.1 为什么非要有SLC:主存的“长尾延迟”救不了

可以考虑一个类比:把CPU核心里的L1/L2缓存比作厨房的操作台,食材(数据)就在手边。主存是仓库,东西全但离得远。如果做一道菜就要跑一趟仓库,厨师肯定疯掉。SLC就是厨房和仓库之间的中央备菜间——常用食材先放在这里,厨师(各个核心和加速器)来取的时候不用跑远路。

但是SLC的角色比“备菜间”更复杂一点。在Neoverse平台,SLC是所有IE(Intelligent Engine)、IO设备、CPU核心共享的最后一层缓存。它的存在有三个直接作用:

  • 降低平均访问延迟:主存延迟动辄上百ns,SLC命中时只要几十ns,对延迟敏感型负载非常关键。
  • 减少对内存带宽的占用:很多数据其实在多个核心之间反复共享,有了SLC,内存控制器就不用反复搬到同一块数据了。
  • 吸收带宽尖峰:云上业务最大的麻烦是流量突刺。一小段突刺流量如果直接怼到内存,内存带宽可能直接被耗尽,而SLC能先把数据暂存下来,平滑掉尖峰。

2.2 SLC的容量、组织与替换策略

CMN-700的SLC容量不是固定的,芯片设计阶段通过配置实例化SLC切片数量和每个切片的Size来决定。典型配置从几MB到几十MB不等,具体取决于SoC面向的场景。例如高密度多核云实例可能配更大的SLC,而边缘低功耗场景可能砍掉一部分SLC切片来节省面积和功耗。

SLC内部按Cache Line组织,通常一行是64字节。为了提高带宽,SLC被划分成多个Bank和多个Slice,不同的地址被哈希到不同的Slice/Bank上,这样多个请求可以并行访问不同Slice,不至于争抢同一个端口。我在实际测试中看到,性能差距很多时候不是容量不够,而是哈希算法在特定访问模式下出现了热点,导致某些Slice比其他Slice忙得多。

替换策略上,CMN采用接近LRU的伪LRU算法。真LRU在几百路(Way)的SLC里代价太高,所以用树型伪LRU,精度略低但硬件成本小很多。这个细节对性能调优的影响在于:即使你手动给某个请求设置了高优先级,它也不能保证把别人挤出去,替换还是按照伪LRU的全局视角来跑。

2.3 SLC的访问路径:CHI协议下的数据流

想搞懂SLC,绕不开AMBA CHI协议。CMN-700一致性互连使用的CHI协议定义了一套完整的消息类型,用来描述不同节点之间的读写、共享、监听、回写等操作。

当CPU核A要读一个地址时,请求会从RN出发,经过网格送到归属该地址的HN-F。HN-F先查SLC,如果命中,就直接把数据回给CPU核A;如果未命中,HN-F需要去内存取数据填充SLC,再把数据返回。如果其他核也缓存了这个数据,HN-F还要发起监听操作,从其他核的缓存里拿最新的一份数据。

这里最容易让普通工程师踩坑的地方是“SLC与CPU核内L2的关系”。Linux perf工具里看到的一些PMU事件会把L2 miss和SLC hit分开统计。很多人以为L2 miss之后一定会走到内存,其实不对——L2 miss之后还有SLC这一道缓存,只有SLC也miss了才会真正访问DDR/HBM。

另外,CHI协议的Cache State和回写策略也影响性能。比如IO设备或GPU如果用的是non-cacheable或device memory访问,一般不会在SLC里留缓存,性能就差很多;但如果配置了“stashing(缓存预置)”,加速器就能主动把数据放进SLC,让CPU后续读取更快。这种差异在异构计算里非常明显。

3. 缓存分区的原理:从“共享池”到“专属车道”

3.1 共享缓存的“群居困境”

SLC默认是所有请求方共享的。从利用率角度看,共享缓存是数学上最优的方案——A没用到容量,B可以全部用掉。但在真实世界里,共享意味着干扰。

比如一个8核数据库实例和一个8核Web前端跑到同一颗SoC上,数据库的延迟敏感,Web前端可能瞬时吃掉大量SLC容量,把数据库的热数据从缓存里挤出去。结果数据库的P99延迟飙升,SLA就完蛋了。

这种问题在物理机、裸金属和多租户云环境里尤其突出。服务器供应商要对客户承诺“性能隔离”,但共享缓存天生做不到完全隔离。CMN-700为此提供了硬件级的QoS机制,其中最核心的就是缓存分区(Cache Partitioning)。

3.2 缓存分区如何工作:Way Partitioning

CMN-700的缓存分区基于Way粒度。它把SLC的所有Way划分成多个分区(Partition),每个分区由一组Way组成。然后给不同的请求节点或请求流分配不同的分区ID。

当一个请求被标记为使用分区P时,它在SLC里分配缓存行时只能在分区P对应的Way里找位置。如果分区P已经满了,即使其他分区还有大量空Way,也不能去占用。这就实现了真正的物理隔离——不会出现“看起来有空间,但重要数据反而被挤掉”的尴尬局面。

实现上,CMN-700通过配置QM(QoS Manager)相关的寄存器来维护分区属性和请求者映射。通常的做法是给CPU集群、GPU、网络控制器等分配不同的分区ID,再配合Weight值来调整各分区对SLC的占用比例。

这里关键的一个理解是:分区的隔离是“容量隔离”,不是“带宽隔离”。即便一个分区占满了,它的请求在向内存发送时还是要和别人争抢内存带宽和Mesh带宽。所以缓存分区解决的是缓存资源争用,内存带宽问题还需要配合内存带宽分配策略。

3.3 什么场景真正需要缓存分区

有人可能问,默认共享不好吗,为什么要自我限制容量?

举几个实际场景:

  • 电信NFV场景:vDPA、vSwitch数据面要求稳定的低延迟,语音包不能让后台的分析任务干扰。
  • 云端多租户:一台物理主机上跑多个不同优先级的虚拟机,VIP客户的核心数据库和低优先级的批处理任务必须隔离。
  • 异构AI推理机:CPU和NPU同时访问SLC时,如果把关键模型的权重放在固定分区里,推理延迟会更稳定。
  • 车载或工业实时控制:实时控制任务的响应时间必须可预测,不能因为多媒体任务刷缓存而产生抖动。

在这些场景里,牺牲一点峰值吞吐,换取稳定性和SLA,是绝对划算的。

4. 实操配置:从平台固件到Linux驱动,手把手启用SLC分区

4.1 启用缓存分区的前提条件

不是所有基于CMN-700的平台都默认开放SLC分区配置,而且CMN-700的QoS功能配置路径一般需要满足三个条件:

  1. SoC的Trusted Firmware/BIOS固件里把QoS功能编译并使能起来。
  2. 相关的驱动(通常是平台厂商提供的CMN QoS驱动,或ACPI的Generic QOS描述)正确加载。
  3. 需要知道每个CPU集群、加速器在CMN拓扑中的Node ID,或者至少有办法通过设备树/ACPI表找到这些ID。

如果你是BSP工程师,而且需要从固件侧验证,那么建议用WAYPD(Way Partitioning)配置接口。一般通过读写CMN的QM寄存器实现,不同SoC手册的寄存器偏移会略有差异,但配置逻辑一致。

4.2 在Linux下查看CMN-700和SLC状态

Linux内核从某个版本开始提供了访问CMN拓扑的方法。最直接的方式是使用lscpu和lsmstat来查看拓扑,但要看SLC容量,更靠谱的是通过dmidecode或者平台的sysfs节点来读。

例如有的平台在/sys/devices/platform/arm-cmn/节点下暴露了slc_size,可以直接读取:

cat /sys/devices/platform/arm-cmn/slc_size

如果平台驱动没有暴露这个文件,也可以去读CMN节点的性能计数器映射的sysfs目录:

ls /sys/devices/arm_cmn/events/

这里一般能列出slc_miss、slc_hit之类的事件名,利用perf就能采集SLC命中率。

perf stat -e arm_cmn/slc_hit/,arm_cmn/slc_miss/ -a sleep 10

实测下来,这个方式比从datasheet推算SLC行为要直观得多。如果你拿到的平台内核版本太老,可能没有CMN PMU驱动,那就只能借助JTAG或者平台厂商提供的调试工具了。

4.3 配置Way Partitioning的实操流程

假设你的平台固件已经开启了QoS功能,并且驱动导出了qos相关的接口,一般可以通过如下几步设置分区:

第一步:确认分区粒度和总Way数

SLC的总Way数通常是固定的,比如16 Way或者32 Way。你需要确认当前SLC配置为多少Way、多少组。分区的最小粒度通常是一个Way,但某些平台驱动要求对齐到两个Way或四个Way。

可以通过读取CMN的SLCDEF寄存器或者查看驱动文档获得。如果手头没有文档,可以先读一下平台固件导出的/sys/kernel/debug/cmn/slc_way_total(如果有的话)。

第二步:为每个请求方分配分区

不同的平台驱动接口不同,典型做法是在设备树或ACPI表中设定请求方对应的QoS分组。以某个参考平台为例,它把CMN的QoS接口映射到了一组debugfs文件:

echo 0x00 > /sys/kernel/debug/cmn/qos/cpu_cluster_0 echo 0x01 > /sys/kernel/debug/cmn/qos/gpu_cluster echo 0x02 > /sys/kernel/debug/cmn/qos/pcie_controller_0

这里写入的值是请求方使用的QoS ID(分区ID),0x00、0x01、0x02表示把不同请求方映射到不同分区。

第三步:设置每个分区的大小

用debugfs接口设置各分区的Way数:

echo 8 > /sys/kernel/debug/cmn/qos/partition/0/ways echo 4 > /sys/kernel/debug/cmn/qos/partition/1/ways echo 4 > /sys/kernel/debug/cmn/qos/partition/2/ways

这样,分区0占8个Way,分区1占4个Way,分区2占4个Way,总数为16个Way。如果Way总数是32,剩余未分配的Way则保持默认共享状态。

这个配置生效后,不同请求方只能使用各自分区内的Way,系统级缓存从“大通铺”变成了“单间+公共区域”混合模式。

4.4 配置后的验证方法

配置不等于生效,一定要验证。

最直接的方法是用perf分别给两个CPU集群跑一个缓存压力测试,然后看各自的SLC命中率。如果分区生效,那么两个集群互不干扰,命中率曲线会比较稳定;如果没生效,你会发现一个集群的命中率会明显受另一个集群的影响。

也可以用固定大小的内存块在两个集群间反复交换数据,观察先前配置的分区内是否有足够的容量容纳热数据。如果热数据集大小接近分区容量,命中率会出现悬崖式下降,这反过来能帮助你校准分区大小。

如果平台驱动不支持sysfs/debugfs配置,那就只能改固件源码了。在Trusted Firmware的plat/arm/board/目录下,通常有平台对CMN的初始化配置,可以在cmn700_init()之前调用QoS配置函数。具体寄存器操作需要参考对应CMN-700的TRM(Technical Reference Manual),留意CMN_QOS_CTRL这类寄存器就行。

5. 实战调优与踩坑指南:缓存分区不是调了就完事

5.1 怎么判断SLC容量到底够不够

调优之前先回答一个基本问题:我的SLC该配多大分区?

我个人习惯用这个经验法则:观察系统跑的典型混合负载,找到每个关键消费者的“工作集大小”(Working Set Size,WSS)。这可以用perf mem或pmu-tools的采样工具来估计。分区大小设置为工作集大小的1.5到2倍比较合适,留一些缓冲余量。

常见的错误是把分区切得太小。比如确认关键应用的WSS是6MB,但SLC总共只有8MB,这时候你强行给这个应用分4MB,命中率会惨不忍睹,实际延迟反而比共享缓存更差。分区隔离是有代价的,它牺牲了聚合容量。

建议的做法是按优先级排队:先满足最高优先级应用的WSS余量,剩下的容量再分给低优先级应用。如果容量实在不够,就需要考虑通过减少活跃数据集、修改NUMA/亲和性等方式降低WSS。

5.2 我踩过的三个真实大坑

第一个坑:配置了分区,但平台的路由和QoS ID没对齐。

有次在某个平台上配置了CPU集群0的分区ID为0x00,GPU的为0x01,跑出来的数据和没配置一样。折腾了很久才发现,该SoC的CMN-700将GPU请求节点分成了多个RN,每个RN有自己的QoS ID,我只配置了其中一个。所以配置前务必确认请求方的Node ID范围,不能只看“GPU”这个逻辑概念。

第二个坑:启用分区后,总SLC利用率下降,整体吞吐反而掉了。

这是正常的。分区隔离会引入“碎片化”——低优先级分区的Way用不满,高优先级分区却在排队。解决方法是不要把所有Way全部分完,保留一些Way作为公共缓冲池,让低优先级应用在紧急情况下可以借用公共Way,这样既保留隔离性又不至于浪费配额。

第三个坑:SLC命中了,但内存延迟没降。

排查后发现是TLB miss占了主。SLC能缓存数据,但地址翻译还是要靠页表缓存(TLB)。如果TLB命中率低,CPU会花大量时间做地址翻译,SLC命中也没用。建议把关键应用的大页(HugePage)打开,或者用Linux的透明大页机制,让TLB覆盖范围变大。

5.3 常见问题速查表

现象可能原因解决思路
配置分区后性能不变QoS未使能/请求者Node ID未全部覆盖检查固件开关、核对RID映射
高优先级应用延迟反弹分区容量小于WSS增大分区、压缩工作集
整体SLC利用率下降静态分区碎片化保留公共Way
SLC命中率高但总带宽上不去Mesh路由热点或DDR带宽瓶颈结合CMN PMU查看节点占用
IP/子网/集群之间互相“看不到”对方的数据一致性域配置问题检查CMN的Address Map和HN-F归属

5.4 调优时如何选择监控指标

调优最重要的不是“调”,而是“量”。CMN-700的PMU会提供SLC命中率、SLC miss率、各Node间流量等事件,结合perf工具可以精确定位瓶颈。

建议至少监控以下四类指标:

  • SLC Hit Ratio:整体命中率和各QoS分区的命中率。
  • DRAM带宽利用率:确认瓶颈在内存还是互连。
  • Mesh Node Utilization:观察哪个CCG节点繁忙。
  • L2 Hit Ratio:确认SLC缓存是否被合理使用。

一个可用的perf命令组合是:

perf stat -e arm_cmn/slc_hit/,arm_cmn/slc_miss/,arm_cmn/mesh_read/,arm_cmn/dram_read/ -a sleep 10

注意不同平台的event name可能不同,需要先perf list | grep arm_cmn查看平台支持的事件名。

5.5 缓存分区与内存带宽QoS的配合

如果确认瓶颈不在SLC容量而在内存带宽,那还要关注CMN-700的带宽分配机制。缓存分区管“容量”,而带宽分配管“流量”。两者配合才能实现完整的性能隔离。

我的经验是,对关键负载先做SLC分区,保障命中率,再通过QoS策略限制低优先级应用的内存带宽占用,避免它们频繁刷DRAM。Bandwidth Partitioning的配置思路类似,也是给不同的QoS ID分配带权重的带宽配额。权重会按接在CMN上的接口数量来折算,配置时最好从较小的配额开始逐步上调。

这块在底层固件里通常有一套平台自定义的默认配置,如果把它们误修改,可能导致整机不稳定,建议每次只调整一个分区,并在调整后跑满测试负荷,确认没有引入稳定性问题再继续。

6. 从性能验证到实际体验:分享几个我常用的调优命令

说太多理论容易头大,这里整理几个我平时在基于CMN-700的服务器上最常用的调试命令,基本是开箱即用的。

检查CMN单元是否被内核识别:

ls /sys/bus/event_source/devices/ | grep cmn

看到arm_cmn就说明PMU驱动已经加载了。如果没看到,可能需要确认内核配置CONFIG_ARM_CMN是否打开。

查看Chiplet或Die的SLC相关性能事件:

perf list | grep cmn

有的平台会暴露arm_cmn/slc_rd_lookup、arm_cmn/slc_wr_lookup之类的事件,非常详细。

快速抓一块SLC命中率:

perf stat -e arm_cmn/slc_hit/,arm_cmn/slc_miss/ -a sleep 5

通过hit / (hit + miss)即可得到粗略命中率。如果命中率低于60%,先不要折腾缓存分区,优先检查代码局部性或内存访问模式。

如果想看某个进程占用SLC的情况,可以使用Coresight?

perf record -e arm_cmn/slc_hit/ -a -- sleep 10 perf report

不过要注意,这类采样在大型SoC上数据量大,建议缩小采样周期和事件范围。

调试过程中还有一个很有用的技巧:用taskset把负载固定在特定CPU集群,然后通过修改QoS分区配置观察吞吐变化。这个过程可以帮你快速定位该负载是否受SLC容量约束。

我通常会写一个简单的bash测试脚本,不断变化分区Way数,同时记录关键性能指标,这样就能画出“性能-分区大小”曲线。真实项目里,这条曲线基本都能找到一个明显的拐点,拐点之后再加Way,性能提升就很有限了。

7. 最后再分享一个关于Neoverse生态的想法

CMN-700不是孤立存在的,它与Neoverse V1、N2等CPU核心,以及各种自研加速器、DPU、CXL内存扩展设备深度绑定。做性能调优时,如果只盯着SLC,不关注内存拓扑、CXL设备的位置,很容易出现“缓存调好了,延迟反而更高”的情况。

比如CXL外接内存和DDR本地内存在地址空间上可能连续,但物理距离完全不同。SLC为CXL内存做缓存和做本地DDR缓存的延迟差异巨大。配置缓存分区时,最好先了解哪些地址属于CXL扩展内存,哪些属于本地DDR,避免把大量热数据刷到远端内存里。

根据我个人踩过的几次坑,做Neoverse平台优化一定要像剥洋葱一样一层层来:先看CPU核心的缓存行为,再看SLC的命中率,最后看DDR/CXL的带宽延迟。每到一层,都要用性能计数器验证,而不是靠感觉猜。

CMN-700和SLC这套体系,给了软件工程师前所未有的控制力——你能决定谁可以占用系统级缓存,谁能独占多少容量,谁必须去走慢速路径。这种能力的代价是配置复杂度上升。但只要方法对路,从“共享缓存大乱斗”切换到“关键负载专属通道”之后,你会看到P99延迟和平均吞吐出现立竿见影的改善。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询