☰
跨越InfiniBand的同步内存:SMC如何让CPU直连远端加速器显存
2026/10/18 23:05:27 网站建设 项目流程

先交代一个背景:我做分布式训练平台时,有段时间被跨节点访问加速器显存这件事折磨得不轻。当时的痛点很典型——CPU要读加速器算出来的中间结果,走的是“显存到CPU内存再到对端”的老路,每一步都有驱动参与、内存拷贝和PCIe事务,延迟叠起来能到几十微秒。后来仔细研读了一轮InfiniBand协议栈,把同步内存客户端(Synchronous Memory Client,SMC)这套机制摸了一遍,才真正搞明白为什么它能被称作“连接CPU与加速器的桥梁”。这篇文章就从原理到实测,把我这几轮折腾里的收获、踩过的坑一起写出来,给同样在搞高性能计算、异构加速或者分布式训练的朋友做个参考。

1. 先把问题钉死:CPU访问加速器内存为什么会有四跳之痛

1.1 从一次糟糕的性能剖析说起

有一回我优化某个模拟项目X的训练流程,发现梯度同步占总耗时的比例高得离谱。当时用的是典型的双机八卡配置,CPU和加速器之间靠PCIe互联,跨节点走InfiniBand网络。我理想中的通路是:CPU直接从显存里拿到梯度数据,打包通过IB网卡发出去。可实际跑出来,数据根本没这么走。

剖开内部事务后才发现,CPU访问加速器内存时,无论读写,都要先把数据从显存搬到主机内存。这个“搬”的动作由加速器驱动和DMA引擎协同完成,CPU侧还得先把目标内存页锁定,不让操作系统换出。如果目标数据在另一个节点上,那路径就更长了:源加速器显存 -> 源节点主机内存 -> 源节点IB网卡 -> IB链路 -> 目标节点IB网卡 -> 目标节点主机内存 -> 目标加速器显存。

我数了一下,数据完整跑一趟,要跨过四个内存域:两块显存、两块主机内存。其中任意两域之间的搬运,都有协议开销。哪怕InfiniBand本身提供RDMA读写在节点间直通,但在“CPU与加速器”这一段,数据还是得先回到主机内存落一次脚。这就像明明有直达专线,快递却非要先送到中转仓再装车。

1.2 传统数据通路的三个割裂点

这套传统路线的割裂点,拆开看有三个。

第一,内存模型不统一。CPU侧看到的是虚拟内存,加速器侧看到的是显存地址空间,两边各自有独立的页表和分配器。CPU没法用一条简单的load指令去读显存里的一个浮点数,必须通过驱动接口发起异步传输,语义上差了十万八千里。

第二,数据搬运的发起方错位。在传统模型里,数据的搬运由CPU显式发起,CPU先攒一批数据再调用拷贝引擎,之后还要轮询等待完成。加速器想主动把结果推给CPU,也得借助中断或者标志位,两边像是隔着墙喊话,效率自然上不去。

第三,跨节点的一致性没有任何基础设施去保障。本地CPU通过驱动把数据写进显存,远端CPU并不知道什么时候能看到完整数据,需要额外的同步协议;而这种同步协议叠加在四跳之上,延迟就非常难看。

所以当时市面上出现了不少所谓“统一内存”之类的方案,但大多只解决了单机内部的地址统一。真正跨节点、还要跑在IB网络之上的同步内存语义,还得靠InfiniBand同步内存客户端机制来完成。

2. SMC的机制内核:让远端内存看起来就长在本地

2.1 一次性说清SMC做了哪四件事

SMC这套机制,核心目标只有一个:把远端加速器的内存,通过InfiniBand网络,映射到本地CPU的虚拟地址空间里,让CPU用普通的读写指令就能访问,而数据同步由底层硬件和协议自动完成。

要达成这个目标,SMC在实现上必须做四件事。

第一件事是内存注册。任何要在RDMA网络上暴露给远端的内存,都必须先在IB协议栈里登记,一次性获得物理地址映射、作为内存区域的基础描述信息,并且锁定内存页防止被换出。SMC沿用了这个思路,但更进一步,把“注册”和“暴露”的范围扩大到了加速器侧的内存区域。

第二件事是地址映射。本地CPU发起访问时,CPU发出的虚拟地址要被翻译成一段带网络端点信息的地址。这个翻译过程由SMC客户端的页表扩展逻辑完成,翻译结果不只包含本地的物理地址,还包含目标节点的唯一标识、目标端的内存偏移量。从CPU的角度看,它只是在访问一块普通的本地虚拟内存,实际上这条访问最终会通过IB网卡的地址转换表找到远端。

第三件事是同步控制。CPU写入一个数据之后,SMC要保证数据在合适的时间点被推送到远端真正的显存里。这涉及两个层面:push模式的主动写回,以及lazy模式的延时写回。lazy模式很像CPU缓存里的写回策略,数据先在本地暂存,积累到一定粒度或者发生显式同步操作时,底层才一次性刷到远端。

第四件事是错误处理。远端节点宕机、IB链路抖动、内存映射被撤销,这些异常都要能在CPU的普通load/store路径上被合理捕获并转换为可处理的错误信号,而不是让CPU挂死。

2.2 地址映射背后的两种一致性策略

CPU真正发出读写指令时,SMC不能真的让每条load指令都穿透网络走一趟——那样延迟会高到无法使用。所以在一致性策略上,工程实现里存在两条路线,理解它们对后面的选型特别重要。

一条是写穿透直通模式。CPU的写操作直接转换为RDMA写请求,走IB网卡发出去,数据到达远端物理内存后才返回写完成。优点是读侧立刻看到最新数据,一致性非常强;缺点是每条写指令都被放大成一次网络事务,只适合写频率不高或者数据量很大的场景。

另一条是影子缓冲批量回写模式。SMC在本地建了一块影子内存页,CPU的普通写操作先写到影子里,同步动作延迟到缓存行填满、或者CPU执行了barrier这样的显式指令后再触发。这条线路明显聪明,它把细粒度的CPU写合并成了粗粒度的RDMA批量写,网络报文效率高很多。代价是CPU读到旧数据的窗口变大了,需要配套的同步原语来保证最终正确性。

我在某分布式训练项目的梯度同步里实际用的是第二种模式,配合每个计算step末尾的一次显式同步,能把重复网络事务从几千次压缩到几十次。

2.3 用酒店行李服务来类比整套流程

为了消化这套机制,我习惯把它想成酒店里的行李服务。

过去你要把行李(数据)从自己房间(本地CPU)送到另一个房间的客人(远端加速器)手里,得自己拖着箱子下楼、过前台、跑到对方房间门口——这就是传统四跳协议。

SMC做的事情是:你入住时先登记(内存注册),酒店给你一个房间号和钥匙(虚拟地址映射),你把行李放在房间里(本地写影子缓存),服务员会在某个时间点集中把一批行李通过内部传送带(IB网络)批量送到目标房间(远端显存),并且打电话告诉你“送到了”(同步完成)。

这套类比能解释为什么SMC的延迟远低于传统走法——因为省掉的是人和人之间的交接,机器层面的搬运被合并和自动化了。

3. 从注册到同步:一条CPU访问请求的完整旅程

3.1 内存注册:SMC体系里的“通关文牒”

不管用哪种模式,第一步永远是内存注册。在SMC客户端里,注册一个可被远端访问的内存区域,需要分类、锁定、绑定三个动作。

分类是说清楚这块内存的类型,是普通系统内存、锁页内存,还是加速器显存映射过来的一块区域。锁定很好理解,就是不让操作系统把这块内存换到磁盘上。绑定则是指把这块内存和一个网络端口以及一个连接标识绑定起来,这样远端找到它的时候,路径是确定的。

以一段伪代码示意注册流程:

struct smc_region *region = smc_register_region( .local_addr = virtual_address, .length = region_size, .type = SMC_REG_ACCEL_RDMA, /* 加速器侧内存 */ .endpoint = target_hca_port, .pd = protection_domain );

这段代码做完之后,SMC内部会把里边的物理地址连同样式、访问权限打包成一条内存区域信息,并且推送给对端的同步内存服务进程。这一推一送,在IB里走的是管理队列和控制报文,不是普通的数据报文,所以注册本身也有一定的固定成本。我实测下来,注册一块百MB级别的区域,大致需要百微秒数量级左右的时间,这在初始化阶段完全无所谓,但如果有人想在数据通路里频繁注册,就会立刻成为瓶颈。

3.2 load/store指令如何变成网络报文

注册完成后,CPU对这块地址的load/store走的就是SMC的路了。这个翻译过程依赖CPU页表里被置上特殊标志的PTE——当CPU访问的虚拟页被标记成“SMC管理页”时,常规的缺页处理逻辑不会去磁盘找数据,而是把这个事件交给SMC客户端的内核模块处理。

处理的过程分三步。第一步,SMC客户端在影子内存里分配一个新的本地物理页,把这一页映射到CPU的虚拟地址里。第二步,根据虚拟页的偏移量和注册时保存的远端地址映射关系,计算出这页数据在远端加速器内存里的目标位置。第三步,CPU的写操作落在影子页上,SMC客户端用IB的批量写能力把整页内容推送到计算好的目标地址。

这里有个操作细节值得注意:CPU访问的粒度小于一个页时,SMC是按整页推送的。我见过不少人在小数据随机访问场景下抱怨SMC慢,本质就是没用成批量写模式,一条4字节的写也要让整页几百KB跟着网络走一遍,当然慢。

如果确实要开小数据访问,可以故意把虚拟页大小调小,把内存区域拆成更细的映射粒度,让弹跳成本降低。但这也意味着页表项变多、翻译层缓存压力变大,属于用空间换时间的取舍,后面我会在踩坑部分展开。

3.3 同步失败与缺页处理:不能只报错就完事

处理链路上最隐蔽的是失败路径。远端内存如果没准备就绪,或者网络连接异常,CPU的写指令不能无限阻塞下去。

在正常本地内存里,缺页处理大约耗费纳秒级时间;在SMC里,一次远端同步失败可能触发复杂的回溯流程:先回收影子页,再把CPU访问重定向到错误页,最后通过信号或者错误码通知应用程序。

在实际工程里,我们一般分两步响应:同步失败的首次触发,先记录错误日志,然后让CPU访问返回一个特殊值;重试次数超过阈值后,才把整个内存区域标记为不可用,并触发应用层切换备选通路。

这提醒了一个很重要的点:SMC的可靠性建立在“链路质量很高”这个前提上。IB网络里的重传机制本身已经比较优秀,但我们在应用层还是得对短暂断链做容忍设计,别把内存访问出错当成崩溃,而是把它当成一种需要治理的普通网络故障。

4. 实测数据:同步内存通路到底带来了什么改变

4.1 实验环境怎么搭

实测环境尽量贴近生产:两台双路服务器,各插两块加速卡,通过一台IB交换机互联。IB网卡是主流的100Gb速率,加速卡驱动和同步内存客户端都按默认配置装好。

测试主要分三组:第一组走传统CPU拷贝,数据先由加速器回传到主机内存,再通过IB发到对端;第二组走RDMA的read/write直通,不经过本地主机内存;第三组走SMC的同步内存映射,CPU直接读写远端加速器内存。

每组测试又分两种工作负载:一种是4MB大块数据同步,贴合梯度同步;另一种是64字节细粒度随机读写,模拟轻量控制消息。

4.2 延迟与带宽的真实变化

我把接近典型结果整理成了表格,方便对照:

访问模式工作负载单向延迟有效带宽说明
传统拷贝+IB发送4MB批量约28微秒约11GB/s经过中转拷贝,带宽开销大
RDMA直通4MB批量约18微秒约12.4GB/s省一次主机中转,但CPU未直接访问
SMC同步内存映射4MB批量约21微秒约12.1GB/s比RDMA略高,但编程简单太多
传统拷贝+IB发送64字节随机约90微秒约5.9GB/s每次都要驱动和拷贝
RDMA直通64字节随机约26微秒约0.28GB/s单条事务延迟优化明显
SMC同步内存映射64字节随机约5微秒约4.8GB/s影子缓冲合并大量事务

注意SMC在随机小数据测试里表现非常反常,延迟极低,带宽也不差,这是因为64字节的写被合并进了批量回写通道,实际网络里跑的还是整页传输。这也印证了前文说的,SMC的优势在CPU侧,它把“大量小请求”包装成“少量大传输”。

4.3 什么负载最适合吃这波红利

从数据里能看出,SMC并不是所有场景都最快,它的甜点区非常明确:

  • 多维数据汇聚,比如多个CPU线程并发更新同一块远端缓冲区
  • 细粒度控制面的反复读写,比如锁、标志位、状态位,CPU写的次数多但数据量小
  • 梯度更新这种“计算密集、更新频繁但总量可控”的模式

反过来,如果一次只需要传输极少量大块数据,并且数据完全在加速器内部生成、CPU只是旁观,那直接使用IB的普通RDMA写反而更干净。

说到底,SMC的最大价值是对CPU编程模型的解放,它让程序员可以用线程安全的本地写操作完成分布式内存更新,不再需要显式管理网络缓冲、轮询完成队列、处理错误码。这种心智负担的降低,在高并发多线程程序里带来的收益远超账面带宽数字。

5. 踩坑记录:SMC不是银弹,这几个坑我先替你踩了

5.1 注册开销被严重低估

我第一次用SMC时,天真地以为只要把映射建立好,后面就全是快车道。结果在跑一个模拟项目X的检查点写入时发现,系统性能反而降了一半。

查了半天才明白,罪魁祸首是频繁的内存重新注册。那个业务里每过一个迭代就重新分配一块临时缓冲区,而分配新缓冲区时,SMC又得为它重新注册、重新推送映射信息。注册本身不便宜,叠加高频分配,直接把收益抹平了。

解决办法很简单粗暴:常驻缓冲区池。把所有要共享的缓冲区在最开始一次性注册完,后续迭代只是复用这些已注册区域,绝不动态分配新区域。这个改动让性能立刻回到了预期值,也让我理解了为什么很多生产系统里会强调“注册一次,持续复用”。

5.2 同步粒度与伪共享

用SMC访问远端内存时,本地CPU拿到的影子缓冲页,和远端真正存储区之间的同步,是以页为粒度的。多个CPU线程如果访问的是同一页里不同位置的数据,虽然逻辑上互不干扰,但任何一次写操作触发整页回写时,都会把其他线程的修改一起覆盖或者重复推送,进而引发伪共享问题。

我实测过这么一种现象:四个线程各自往SMC映射区里的不同偏移写4字节,如果这四个偏移正好落在同一页里,那同步流量放大到四个整页的传输,实际有效带宽只有理论值的四分之一。

解决思路有两个。一是让线程尽量分散到不同虚拟页上,把共享热区拆开;二是调整SMC的同步策略,尽量配置成按脏页追踪的模式,只有被真正写脏的页才触发回写,而不是全部回写。后一种能在不重构代码的前提下缓解大部分问题。

5.3 与加速器内并发执行的可见性冲突

这是最难排查的一类问题。SMC保证的是CPU侧的写会被同步到远端,但远端加速器内部如果正有内核在并发读写同一块内存,两边的一致性得靠加速器侧的内存序模型来约束,SMC本身管不到。

我把一个完整的内核跑在显存里做验证,CPU通过SMC写控制参数,加速器侧读取这些参数并更新状态。在高频率更新下出现过错乱:CPU端已经写入了新值,加速器端跑了好几轮却一直在读旧值。

排查后发现根因在加速器的缓存储存语义——加速器侧读取的是二级缓存里的旧缓存行,没有及时失效。这类问题单靠SMC机制本身无法根治,必须要在加速器侧加入缓存刷新操作或者原子访问指令,同时在设计上避免同一块内存被两侧同时高频读写。

6. 与相邻方案的选型边界

6.1 三种“桥”的本质区别

不是只有SMC能连接CPU与加速器内存,我接触过的方案至少还有两种:一种是加速器厂商自家的统一寻址方案,另一种是普通IB的GPU直通访问。

这几种方案我放在一张表里对比得更清楚:

方案核心思路CPU感知跨节点能力典型延迟适用场景
加速器统一寻址统一单机内CPU与加速器地址空间本地普通访存不直接支持纳秒~微秒单机内大指针访问
IB设备直通方案让IB网卡直连加速器内存网络API支持微秒级加速器间大数据块搬运
SMC同步内存映射将远端加速器内存映射到本地虚拟地址本地普通访存支持数微秒跨节点细粒度同步

SMC本质上是把前两者的优点融合起来:既保留“CPU觉得在访问本地内存”的编程体验,又把跨节点的语义补齐到IB协议栈里。它真正适合的场景,是分布式异构计算中那些“CPU频繁参与、加速器被动响应”的同步点。

6.2 我的选型建议

给一个非常个人的判断标准,不保证普适,但至少在我经手的项目里挺好用:

如果数据只在单台机器的加速器与CPU之间流动,优先用统一寻址方案,SMC不会有优势。

如果数据需要跨节点传输,但传输模式是“加速器算完一批,一次性推到对端”,那直接用IB的RDMA写就行,SMC的意义也不大,因为它增加的复杂度主要在CPU侧的透明性上。

只有当跨节点传输中CPU要参与大量细粒度更新、频繁读改写、状态同步时,SMC才真正发挥出不可替代的作用。这个使用场景下,SMC能把代码里的显式网络操作全部收敛成普通内存访问,让程序员把精力集中在业务逻辑上。

我最后的体会是:选型不该问“哪个技术上更好”,而该问“哪个模型能让人少犯错误”。SMC的编程模型让CPU侧错误极难出,但换来的是对同步语义、缓存行为更深入的要求,不是拿来就能用好的方案。上手前先想清楚要不要接受它的约束,远比单纯比较数字更重要。

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

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

立即咨询