前言

在昇腾NPU的计算体系中,CANN软件栈承担了从模型编译到硬件调度的全链路管理,而shmem仓库()则聚焦于其中一个基础而关键的环节——共享内存通信机制。Host与Device之间的数据传输效率,直接影响着端到端的推理和训练性能。很多开发者在优化昇腾NPU上的模型性能时,将注意力集中在算子融合和计算优化上,却忽视了数据传输路径对整体性能的制约。实际上,在大量小批量推理场景下,Host-Device数据传输的延迟往往比算子执行时间高出数倍,成为端到端性能的真正瓶颈。shmem的设计目标,正是通过共享内存机制来突破这一瓶颈,将Host-Device数据传输的开销降到最低。

共享内存通信的架构背景

要理解shmem的设计动机,首先需要理解昇腾NPU的Host-Device架构。在这种架构中,Host端(通常是CPU)负责模型加载、计算图编译和任务调度,Device端(昇腾NPU)负责实际的张量计算。两者之间的数据通路,是整个系统性能的关键约束。

传统的Host-Device数据传输采用"拷贝"模式:Host端将数据从自己的内存空间拷贝到Device的Global Memory,或者反过来。这种拷贝模式的瓶颈在于,数据需要经过PCIe总线(在独立加速卡场景下)或片上互联(在SoC场景下),传输带宽受限于物理链路。更关键的是,拷贝模式引入了两份数据副本——一份在Host内存,一份在Device内存——不仅浪费了内存资源,还带来了数据一致性管理的复杂度。

共享内存模式的核心思想是:让Host和Device共享同一块物理内存,通过地址映射而非数据拷贝来实现数据"传输"。当Host写入这块共享内存后,Device可以直接读取,反之亦然,无需任何数据搬运。在物理层面,共享内存依赖于Host和Device共享的物理内存区域,Host端和Device端分别通过各自的地址映射机制访问同一块物理内存。

shmem在CANN中的角色,就是管理这块共享内存的分配、映射、同步和回收。它为上层提供了统一的共享内存抽象,屏蔽了不同硬件平台(PCIe加速卡、SoC等)在共享内存实现上的差异。

shmem的核心抽象与接口设计

shmem将共享内存的管理抽象为几个核心概念:共享内存区域(SharedMemoryRegion)、地址映射(AddressMapping)和同步原语(SyncPrimitive)。

共享内存区域是shmem管理的基本单位。一个共享内存区域对应一块连续的物理内存,可以被Host和Device同时访问。shmem在分配共享内存时,需要与底层的内存管理器协作,确保分配的物理内存满足Device端的访问约束——例如,在某些硬件平台上,Device只能访问特定物理地址范围内的内存,这就要求shmem在分配时进行地址约束检查。

地址映射是将共享内存区域的物理地址映射到Host和Device各自的虚拟地址空间的过程。在Host端,地址映射通常通过操作系统的内存映射接口(如Linux的mmap)实现;在Device端,地址映射则依赖硬件的地址转换机制。shmem在映射过程中需要维护Host虚拟地址、Device虚拟地址和物理地址三者之间的映射关系,这是后续数据访问的基础。

同步原语解决的是Host和Device对共享内存的并发访问问题。当Host和Device共享同一块内存时,如果没有适当的同步机制,就会出现经典的并发问题——一方写入的数据可能被另一方覆盖,或者一方读取到另一方尚未写入完成的部分数据。shmem提供的同步原语包括内存屏障(Memory Barrier)和通知机制(Notification),前者确保写入操作的可见性,后者用于Host和Device之间的轻量级事件通知。

// shmem的共享内存分配与映射流程
// WHY讲解:共享内存需要同时映射到Host和Device的地址空间,
// 核心原因在于Host和Device使用不同的地址翻译机制——
// Host通过CPU的MMU进行虚拟地址到物理地址的翻译,
// Device通过昇腾NPU的SMMU(System MMU)进行翻译。
// 两套翻译机制独立运作,必须分别建立映射才能让
// 双方通过各自的虚拟地址访问同一块物理内存。
// shmem将这个双重映射过程封装为统一的接口,
// 避免开发者直接与两套MMU配置打交道。
SharedMemoryRegion* region = ShmemAllocator::Alloc(size, alignment, flags);
// Host端映射
void* host_ptr = ShmemMapper::MapToHost(region, PROT_READ | PROT_WRITE);
// Device端映射
uint64_t device_addr = ShmemMapper::MapToDevice(region, ACCESS_READ | ACCESS_WRITE);
// 使用完毕后解映射并释放
ShmemMapper::UnmapFromHost(host_ptr);
ShmemMapper::UnmapFromDevice(device_addr);
ShmemAllocator::Free(region);

WHY: shmem的同步原语设计遵循了昇腾CANN的内存模型语义,避免了竞争条件和内存一致性问题。

WHY: 零拷贝设计在昇腾NPU上尤为重要,因为NPU与Host之间的带宽远低于NPU内部带宽,减少数据搬运能显著提升性能。

WHY: shmem采用共享内存映射机制,避免了传统IPC的数据拷贝开销,让昇腾NPU与其他设备之间的通信更高效。

Host-Device数据传输的两种模式对比

shmem支持的共享内存模式与传统的拷贝模式各有适用场景。拷贝模式的优势在于语义清晰——Host和Device各自拥有独立的数据副本,不存在并发访问问题,数据一致性天然得到保证。其劣势在于传输延迟和内存占用。共享内存模式的优势在于零拷贝的传输延迟和节省的内存开销,但引入了并发同步的复杂度。

在实际应用中,两种模式并非互斥,而是互补的。对于大块数据的"预热"传输(如模型权重的初始加载),拷贝模式更为合适——数据只需传输一次,之后Device端独立使用,不存在并发问题。对于需要频繁在Host和Device之间交换的小块数据(如推理请求的输入输出),共享内存模式的优势则十分明显——避免了每次请求的拷贝开销。

shmem的设计也考虑了这种混合使用场景。开发者可以为不同的数据流选择不同的传输模式,shmem在同一套接口下统一管理共享内存区域和拷贝缓冲区,使得混合使用对上层透明。

内存一致性与可见性保证

共享内存模式下的内存一致性(Memory Consistency)是最容易出错的问题之一。CPU和昇腾NPU各自拥有缓存层次结构,写入操作可能暂时停留在缓存中,尚未刷新到共享的主存。如果一方的写入尚未到达主存,另一方就从主存读取,就会读到过期的数据。

shmem通过内存屏障来保证一致性。内存屏障是一条同步指令,它确保屏障之前的所有写入操作在屏障之后的读取操作之前完成。在Host端,shmem调用操作系统提供的内存屏障原语(如Linux的sync_file_range或用户态的__sync_synchronize);在Device端,shmem使用AICore的数据同步指令。

但内存屏障的使用并非没有代价。过度的内存屏障会抑制CPU和NPU的存储层次优化,降低访存性能。shmem在接口设计上采取的策略是"显式同步"——不自动插入内存屏障,而是要求开发者在需要保证可见性的位置显式调用同步接口。这种设计虽然增加了使用复杂度,但避免了隐式同步带来的性能损失,给予开发者对同步粒度的精确控制。

// shmem的同步原语使用示例
// WHY讲解:这里的同步采用"写入方刷出+读取方刷新"的配对模式,
// 而非简单地插入全量内存屏障。原因是全量内存屏障会使得
// CPU/NPU的整个缓存层次失效,代价极高。
// 配对模式只刷新特定共享内存区域对应的缓存行,
// 对其他内存访问无影响。
// 此外,通知机制(Notification)提供了比内存屏障更轻量的
// Host-Device事件同步手段——通知机制只传递"数据已就绪"的信号,
// 不涉及缓存操作,接收方在收到通知后再执行必要的缓存刷新。

// Host端写入数据
memcpy(host_ptr, input_data, data_size);
// 刷新Host端缓存,确保数据写入物理内存
ShmemSync::FlushCache(region, offset, data_size);
// 通知Device端数据已就绪
ShmemNotify::Send(device_id, notification_id);

// Device端等待通知后读取数据
ShmemNotify::Wait(notification_id);
// 刷新Device端缓存,确保读取到最新数据
ShmemSync::InvalidateCache(region, offset, data_size);
// 从共享内存读取数据到Device的Global Memory
DataCopy(device_tensor, device_addr, data_size);

共享内存的分配策略与碎片管理

shmem的内存分配策略需要应对共享内存场景下的特殊挑战。与普通内存分配不同,共享内存区域的物理内存必须满足Device端的访问约束,这大大缩小了可分配的物理内存范围。在某些硬件平台上,满足Device访问约束的物理内存只占总物理内存的一小部分,这使得共享内存成为稀缺资源。

shmem采用的分配策略是区域池化(Region Pooling)。shmem在初始化时向操作系统申请一大块满足Device访问约束的物理内存,作为共享内存池。后续的共享内存分配从这个池中进行,而不是每次都向操作系统申请新的物理内存。这种策略避免了频繁的操作系统内存分配调用,降低了分配延迟。

区域池化带来的问题是碎片管理。当不同大小的共享内存区域被分配和释放后,池中会产生外部碎片——剩余的空闲空间总和足够满足新的分配请求,但没有任何一块连续的空闲空间足够大。shmem通过合并相邻的空闲区域来缓解碎片问题,但在长时间运行的场景下,碎片化仍然是一个需要关注的问题。

shmem提供了一种碎片整理机制:当碎片率超过阈值时,shmem会将活跃的共享内存区域迁移到池的前部,将碎片化的空闲空间合并为一块大的连续区域。迁移过程需要暂时冻结受影响的共享内存区域的访问,因此shmem会在Host和Device都处于空闲状态时执行碎片整理,避免对正常业务的影响。

通知机制的硬件实现与软件抽象

shmem的通知机制是Host-Device通信的关键基础设施。通知机制允许一方在完成某个操作后通知另一方,实现轻量级的事件同步。

在PCIe加速卡场景下,通知机制的硬件基础是PCIe的Doorbell寄存器。Host端向Doorbell寄存器写入一个值,触发PCIe中断,Device端的中断处理器检测到中断后执行相应的回调。Device端到Host的通知则通过Device端的门铃机制实现,Host端的驱动轮询门铃状态或等待中断。

在SoC场景下,Host和Device共享同一块物理内存,通知机制可以利用硬件提供的事件寄存器(Event Register)实现,延迟更低。shmem在不同硬件平台上抽象出统一的通知接口,使得上层代码无需关心底层通知机制的实现差异。

通知机制的性能特征直接影响Host-Device协同的效率。PCIe Doorbell的通知延迟通常在微秒级别,对于需要频繁交换小数据的场景(如在线推理的请求-响应模式),这个延迟可能成为瓶颈。shmem针对这种场景提供了批量通知(Batch Notification)优化——将多个通知合并为一次硬件操作,分摊通知延迟。

使用前vs使用后:数据传输效率对比

在没有shmem的共享内存机制之前,所有Host-Device数据传输都必须通过CANN的内存拷贝接口完成。对于推理服务场景,每次推理请求都需要将输入数据从Host内存拷贝到Device内存,推理完成后将输出数据从Device内存拷贝回Host内存。当推理请求的输入输出数据量较小(如单张图片的分类请求)时,拷贝延迟在端到端延迟中的占比极高。

引入shmem的共享内存机制后,推理服务的输入输出可以通过共享内存传递,Host端将输入数据写入共享内存区域,通知Device端读取;Device端将推理结果写入共享内存区域,通知Host端读取。数据本身不再需要拷贝,省去了PCIe传输的时间开销。

在典型的在线推理服务中,请求的输入数据量通常在KB级别。在这种情况下,PCIe拷贝的延迟(包括DMA配置时间和传输时间)往往在数十微秒量级,而共享内存模式下的通知延迟在微秒量级。对于延迟敏感的推理服务,这种差异直接影响服务质量指标(如P99延迟)。在大批量数据场景下(如训练数据集的预取),拷贝模式的传输带宽可以得到充分利用,共享内存的优势不明显,但共享内存节省的内存开销(无需维护两份数据副本)仍然有价值。

多Device场景下的共享内存扩展

在多卡训练和多实例推理场景下,shmem的共享内存机制需要扩展到多Device环境。多Device场景的核心挑战是内存可见性——一块共享内存区域需要对所有参与的Device可见,还是只对特定Device可见?

shmem提供了两种共享模式:独占模式(Exclusive)和广播模式(Broadcast)。独占模式下,共享内存区域只对一个Device可见,适用于该区域的数据只被一个Device使用的场景。广播模式下,共享内存区域对所有Device可见,适用于数据需要被多个Device共享的场景(如多卡训练中的参数服务器模式)。

广播模式引入了新的一致性挑战——当一个Device写入共享内存后,其他Device何时能看到这个写入?shmem在广播模式下提供了全局内存屏障,确保一个Device的写入在所有Device上都可见后才继续执行。全局内存屏障的实现依赖于硬件的多播机制(如果可用)或软件的分布式屏障协议,前者延迟更低但依赖硬件支持,后者通用性更好但延迟较高。

// 多Device场景下的共享内存广播
// WHY讲解:广播模式下使用全局内存屏障而非单端屏障,
// 原因是数据一致性需要在对称的多个Device之间保证。
// 单端屏障只能保证写入方和单一读取方之间的一致性,
// 在多读取方场景下,一个读取方看到最新数据不代表
// 其他读取方也看到了。全局屏障通过与所有参与的
// Device进行一次同步握手,确保"全部可见或全不可见",
// 避免了部分读取方看到过期数据的不一致状态。
SharedMemoryRegion* region = ShmemAllocator::AllocBroadcast(
    size, alignment, {device_0, device_1, device_2});

// Device 0写入数据
ShmemMapper::MapToDevice(region, device_0);
memcpy(device_0_ptr, model_params, param_size);
// 全局内存屏障:确保所有Device都能看到写入
ShmemSync::GlobalBarrier(region, {device_0, device_1, device_2});
ShmemNotify::Broadcast(device_0, {device_1, device_2}, notification_id);

// Device 1和Device 2等待通知后读取
ShmemNotify::Wait(notification_id);
ShmemSync::InvalidateCache(region, 0, param_size);
// 读取共享内存中的模型参数

shmem在CANN软件栈中的层间协作

shmem并非孤立运作,它需要与CANN软件栈中的其他组件紧密协作。在模型加载阶段,CANN的模型管理器通过shmem将模型权重映射到Device的地址空间,避免权重数据的拷贝。在推理执行阶段,CANN的运行时通过shmem的通知机制来协调Host端的任务下发和Device端的执行完成。在多卡训练阶段,CANN的集合通信库利用shmem的广播模式来实现参数的共享。

这种层间协作的关键是接口边界的清晰划分。shmem只负责共享内存的管理和同步原语的提供,不涉及上层的语义——shmem不知道共享内存中存储的是模型权重、推理输入还是梯度数据。这种"无知"是刻意的设计:将语义理解留给上层组件,shmem保持通用性,可以在不同的应用场景中复用。

但通用性也带来了使用门槛的问题。开发者需要自行管理同步的时序,确保"写入完成后再通知,收到通知后再读取"的基本范式得到遵守。shmem在文档和示例中提供了最佳实践指南,帮助开发者正确使用共享内存和同步原语。

共享内存的安全边界

共享内存的使用引入了新的安全考虑。在多租户部署场景下,不同用户的推理请求可能共享同一个Device,如果共享内存区域的隔离不当,一个用户的数据可能被另一个用户的请求读取到。

shmem通过访问控制列表(ACL)来管理共享内存区域的访问权限。每个共享内存区域在创建时指定允许访问的Host进程和Device实例,shmem在映射请求时检查请求方的身份是否在ACL中。这种基于ACL的访问控制模型与操作系统的文件权限模型类似,易于理解和管理。

在SoC场景下,共享内存的安全性还涉及物理隔离问题。如果Host和Device共享同一块物理DRAM,那么恶意方可能通过物理内存探测手段读取共享内存中的数据。shmem在SoC场景下支持将共享内存分配到受硬件访问保护的物理区域,提供额外的安全保障。

从shmem看异构计算的通信范式

shmem的设计反映了一个更广泛的技术趋势:异构计算系统中的通信范式正在从"拷贝"向"共享"演进。这个趋势不仅体现在昇腾NPU中,也体现在其他异构计算平台中——AMD的HSA架构提供了类似的共享虚拟内存(SVM)机制,OpenCL 2.0也引入了共享虚拟内存的支持。

这一演进的根本驱动力是异构计算系统中数据移动成本的持续上升。随着计算单元算力的增长,计算时间不断缩短,但数据移动的延迟受限于物理链路和协议开销,改善幅度有限。当计算时间短到一定程度后,数据移动延迟成为系统性能的支配因素,此时"零拷贝"的共享内存通信就成为不可或缺的优化手段。

shmem在这一演进中提供了昇腾NPU平台上的共享内存基础设施。它的设计既吸收了业界共享内存机制的通用经验,又针对昇腾NPU的硬件特性(如AICore的地址映射机制、PCIe和SoC两种部署形态)做了专门的适配。这种"通用经验+平台特化"的设计方法,是异构计算基础设施设计的有效模式。

shmem作为昇腾CANN的共享内存通信基础设施,其设计涵盖了从物理内存分配到地址映射、从缓存一致性到通知机制、从单Device到多Device的完整技术栈。它将Host-Device数据传输从"拷贝"模式推进到"共享"模式,在延迟敏感的推理场景下带来了显著的性能提升。理解shmem的设计逻辑,不仅有助于开发者正确使用共享内存接口,更有助于理解异构计算系统中通信优化的深层规律。shmem的代码仓库在 ,对于关注Host-Device通信效率的开发者而言,是值得深入研究的参考实现。


仓库地址:https://atomgit.com/cann/hixl

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐