推理引擎异构内存分配器设计:ION/DMA-BUF 在 NPU 与 CPU 之间实现零拷贝共享的全链路剖析
推理引擎异构内存分配器设计ION/DMA-BUF 在 NPU 与 CPU 之间实现零拷贝共享的全链路剖析一、NPU 推理的看不见瓶颈一次 4K 输入图片的 12 次 CPU-GPU 拷贝与 70% 的内存带宽吃掉现象将模型部署到带有独立 NPU 的 SoC如瑞芯微 RK3588、海思 Hi3559A时一个完整的推理流水线涉及多次跨处理单元的数据搬运。以 YOLOv5 目标检测为例摄像头 ISP 输出 → DDRCPU 可访问→ 预处理Resize/NormalizeCPU 执行→ 拷贝到 NPU 专用内存 → NPU 推理 → 结果从 NPU 内存拷贝回 CPU → 后处理NMS→ 输出。整个过程数据在 CPU 和 NPU 之间至少来回三次每次拷贝消耗约 10-30ms取决于分辨率占推理总延迟的 60%-70%。问题的根源是 CPU 和 NPU 使用独立的地址空间。CPU 通过 MMU 映射 DDR 的特定区域NPU 通过 IOMMU或 SMMU映射其专用的物理内存区域。两者的页表互不共享——CPU 页表中的地址在 NPU 看来无意义反之亦然。传统方案是在两侧各分配一块内存通过memcpyCPU 侧或 DMA 引擎NPU 侧复制数据代价是双倍的内存占用和线性增长的拷贝延迟。ION 和 DMA-BUF 是 Linux 内核提供的用户态零拷贝内存共享框架。ION 负责内存的分配和管理支持从不同堆中申请不同类型的内存DMA-BUF 负责在多个设备驱动之间共享对同一块物理内存的访问权限。两者组合使用实现 CPU 和 NPU 在同一块物理内存上同时拥有可访问的映射。二、ION 堆的分级架构与 DMA-BUF 的跨设备共享协议从ion_alloc到dma_buf_attach的完整调用链sequenceDiagram participant App as 用户态应用 participant ION as ION Driver (/dev/ion) participant CMA as CMA Heap participant System as System Heap participant DMABUF as DMA-BUF Framework participant NPU as NPU Driver participant SMMU as IOMMU/SMMU App-ION: ioctl(ION_IOC_ALLOC)br/heap_maskCMA ION-CMA: cma_alloc 获取br/物理连续内存 CMA--ION: 返回 struct page[] ION-DMABUF: dma_buf_exportbr/创建共享句柄 DMABUF--ION: 返回 dma_buf fd ION--App: 返回 {fd, CPU 映射地址} App-NPU: ioctl(NPU_SET_INPUT_BUF, fd) NPU-DMABUF: dma_buf_get(fd) DMABUF--NPU: 返回 dma_buf 对象 NPU-SMMU: dma_buf_map_attachmentbr/建立 IOMMU 映射 SMMU--NPU: 返回 NPU 侧 IOVA NPU-NPU: 将 IOVA 写入 NPU 描述符寄存器 Note over App,NPU: CPU 通过 mmap 地址直接读写br/NPU 通过 IOVA 直接访问br/同一物理内存零拷贝 App-ION: ioctl(ION_IOC_FREE) ION-DMABUF: dma_buf_putbr/引用计数递减 DMABUF-SMMU: 解映射 IOMMU ION-CMA: cma_release 归还内存具体流程分解。首先应用通过/dev/ion的ION_IOC_ALLOCioctl 请求分配内存指定堆类型ION_HEAP_TYPE_DMA映射到 CMA 堆ION_HEAP_TYPE_SYSTEM映射到 Buddy Allocator。ION 核心层调用对应堆的allocate回调获取struct page数组。然后ION 调用dma_buf_export将struct page数组包装为struct dma_buf对象。dma_buf_export内部分配一个文件描述符fd并通过anon_inode_getfile将其与dma_buf对象绑定。此 fd 是用户态可见的句柄可以在进程间通过 Unix Domain Socket 传递也可以传递给其他内核驱动。当 NPU 驱动接收到这个 fd 时调用dma_buf_get(fd)获取内核态的dma_buf对象然后调用dma_buf_attach创建一个 attachment 记录。之后调用dma_buf_map_attachment该函数触发 IOMMU/SMMU 的map回调将scatterlist中的物理页面映射到设备 IO 虚拟地址空间IOVA。此后 NPU 通过 IOVA 访问该内存与 CPU 通过mmap返回的虚拟地址访问的是同一块物理内存。引用计数管理是 DMA-BUF 的核心机制。dma_buf_get增加引用计数dma_buf_put减少。当引用计数归零时dma_buf的release回调被触发将内存归还给 ION 堆。这保证了在 NPU 仍在使用该缓冲区时通过其 attachment 间接持有引用CPU 侧无法释放该内存。三、在嵌入式 Linux 上构建推理内存池封装 ION 分配的 API 与缓存一致性处理/* * npu_mem_pool.c — 基于 ION/DMA-BUF 的 NPU 推理内存池 * * 设计目标 * 1. 统一分配接口隐藏 ION ioctl 细节 * 2. 支持 CPU 和 NPU 同时访问的零拷贝模式 * 3. 缓存一致性在 CPU 写入后、NPU 读取前刷 cache * * 编译依赖: libion (Android) 或 手动 ioctl 封装 */ #include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include sys/mman.h #include errno.h /* ION ioctl 命令码Linux 主线内核 / Android 通用 */ #ifndef ION_IOC_ALLOC #define ION_IOC_ALLOC _IOWR(I, 0, struct ion_allocation_data) #define ION_IOC_FREE _IOW(I, 1, struct ion_handle_data) #define ION_IOC_MAP _IOWR(I, 2, struct ion_fd_data) #define ION_IOC_SHARE _IOWR(I, 4, struct ion_fd_data) #endif #define ION_HEAP_TYPE_DMA (1 0) /* CMA 堆 */ #define ION_HEAP_TYPE_SYSTEM (1 1) /* Buddy 系统堆 */ #define ION_FLAG_CACHED (1 0) /* 使用 CPU 缓存 */ struct ion_allocation_data { size_t len; size_t align; unsigned int heap_id_mask; unsigned int flags; int handle; }; struct ion_fd_data { int handle; int fd; }; struct ion_handle_data { int handle; }; /* * NPU 内存块描述符 * * 每个 NPUMemBlock 对应一块被 CPU 和 NPU 共享访问的物理内存 * fd 可以传递给 NPU 驱动用于 IOMMU 映射 */ typedef struct { int ion_fd; /* /dev/ion 文件描述符 */ int ion_handle; /* ION 内核句柄分配时获得 */ int dma_fd; /* DMA-BUF 文件描述符可传递给 NPU 驱动 */ void *cpu_addr; /* CPU 侧 mmap 虚拟地址 */ size_t size; bool is_cached; /* 是否启用 CPU 缓存 */ } NPUMemBlock; /* * 打开 ION 设备并分配 DMA-BUF 内存块 * * 参数: * heap_type: ION_HEAP_TYPE_DMACMA 连续内存推荐 NPU 使用 * 或 ION_HEAP_TYPE_SYSTEM系统非连续内存 * size: 请求大小字节 * cached: true 启用 CPU 缓存性能好但需手动刷 cache * false 使用 Non-Cached 映射性能低但无一致性问题 * * 返回: NPUMemBlock 指针失败返回 NULL */ NPUMemBlock* npu_mem_alloc(unsigned int heap_type, size_t size, bool cached) { NPUMemBlock* blk calloc(1, sizeof(NPUMemBlock)); if (!blk) return NULL; /* Step 1: 打开 ION 设备节点 */ blk-ion_fd open(/dev/ion, O_RDWR); if (blk-ion_fd 0) { fprintf(stderr, [NPU_MEM] open /dev/ion 失败: %s\n, strerror(errno)); free(blk); return NULL; } /* Step 2: 分配 ION 缓冲区 */ struct ion_allocation_data alloc_data { .len size, .align 4096, /* 页对齐 */ .heap_id_mask heap_type, .flags cached ? ION_FLAG_CACHED : 0, }; if (ioctl(blk-ion_fd, ION_IOC_ALLOC, alloc_data) 0) { fprintf(stderr, [NPU_MEM] ION_IOC_ALLOC 失败: %s, size%zu\n, strerror(errno), size); close(blk-ion_fd); free(blk); return NULL; } blk-ion_handle alloc_data.handle; blk-size size; blk-is_cached cached; /* Step 3: 获取 DMA-BUF fd用于传递给 NPU 驱动 */ struct ion_fd_data share_data { .handle blk-ion_handle }; if (ioctl(blk-ion_fd, ION_IOC_SHARE, share_data) 0) { fprintf(stderr, [NPU_MEM] ION_IOC_SHARE 失败: %s\n, strerror(errno)); /* 清理已分配的 handle */ struct ion_handle_data free_data { .handle blk-ion_handle }; ioctl(blk-ion_fd, ION_IOC_FREE, free_data); close(blk-ion_fd); free(blk); return NULL; } blk-dma_fd share_data.fd; /* Step 4: CPU 侧 mmap 映射 */ struct ion_fd_data map_data { .handle blk-ion_handle }; if (ioctl(blk-ion_fd, ION_IOC_MAP, map_data) 0) { fprintf(stderr, [NPU_MEM] ION_IOC_MAP 失败: %s\n, strerror(errno)); close(blk-dma_fd); struct ion_handle_data free_data { .handle blk-ion_handle }; ioctl(blk-ion_fd, ION_IOC_FREE, free_data); close(blk-ion_fd); free(blk); return NULL; } blk-cpu_addr mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, map_data.fd, 0); close(map_data.fd); /* mmap 后 fd 可以关闭 */ if (blk-cpu_addr MAP_FAILED) { fprintf(stderr, [NPU_MEM] mmap 失败: %s\n, strerror(errno)); close(blk-dma_fd); struct ion_handle_data free_data { .handle blk-ion_handle }; ioctl(blk-ion_fd, ION_IOC_FREE, free_data); close(blk-ion_fd); free(blk); return NULL; } return blk; } /* * CPU 写入数据后刷 cache 以确保 NPU 读取到最新数据 * * 关键在启用缓存的 ION 分配中CPU 写入的数据可能暂存在 L1/L2 * 缓存中NPU 通过 IOMMU 读取的是 DDR 中的旧数据。 * * __builtin___clear_cache 实际只做指令缓存刷新。 * 数据缓存刷新需要使用特定平台指令或 dma-buf 的 begin_cpu_access 回调。 * 此处提供示意实现。 */ void npu_mem_sync_for_device(NPUMemBlock* blk) { if (!blk || !blk-is_cached || !blk-cpu_addr) return; /* * 在 ARMv7/v8 平台上数据缓存刷新需要 * * 对于 Cortex-A 系列 * __builtin___clear_cache 仅影响 I-Cache不影响 D-Cache * * 推荐使用 dma_buf_begin_cpu_access / dma_buf_end_cpu_access * 但用户态通常不直接访问这些内核接口 * * 工程实践中推荐使用 ION 的 ION_IOC_SYNC ioctl * 或通过 ion_sync_fd 系统调用Android 专有 * * 此处以 ARM 平台 MVA 级 D-Cache 维护为例 */ #ifdef __ARM_ARCH_7A__ uintptr_t start (uintptr_t)blk-cpu_addr; uintptr_t end start blk-size; /* DCCMVAC: 按 MVA 清除数据缓存到 PoC */ for (uintptr_t addr start; addr end; addr 64) { /* ARM 缓存行大小通常为 32B (Cortex-A7) 或 64B (Cortex-A53) */ __asm__ volatile(mcr p15, 0, %0, c7, c10, 1 : : r(addr)); } /* 数据同步屏障确保缓存操作完成 */ __asm__ volatile(dsb sy : : : memory); #endif } /* * 释放 NPU 内存块解映射 CPU 地址、释放 ION handle、关闭 fd * 注意仅在确认 NPU 不再持有该 DMA-BUF 的引用时才能调用 */ void npu_mem_free(NPUMemBlock* blk) { if (!blk) return; if (blk-cpu_addr blk-cpu_addr ! MAP_FAILED) { munmap(blk-cpu_addr, blk-size); } if (blk-dma_fd 0) { close(blk-dma_fd); } /* 通过 ION 的 handle 告知内核释放物理内存 */ if (blk-ion_fd 0 blk-ion_handle 0) { struct ion_handle_data free_data { .handle blk-ion_handle }; /* * ION_IOC_FREE 可能失败如 handle 已被自动释放 * 不做错误检查——物理内存在 dma_fd 关闭时由 dma_buf_put 最终释放 */ ioctl(blk-ion_fd, ION_IOC_FREE, free_data); close(blk-ion_fd); } free(blk); }四、ION 在不同内核版本中的分裂与兼容性困境Android ION、主线 DMA-BUF Heaps 与 DMABUF 子堆ION 框架最初由 Android 贡献给 Linux 社区但主线内核长期拒绝合入完全版 ION因为它引入了从用户态直接控制内核物理内存分配的能力被认为违反了内核的内存抽象原则。结果是主线 Linux 4.14 之后引入了DMA-BUF Heaps作为替代方案通过/dev/dma_heap节点暴露不同的堆类型system、cma、cma-uncached等。而 Android 设备包括大多数商用嵌入式 SoC 的 BSP仍然使用/dev/ion接口。两者的用户态 API 不兼容。ION 使用ION_IOC_ALLOCION_IOC_SHARE两步操作DMA-BUF Heaps 使用单个DMA_HEAP_IOCTL_ALLOCioctl 直接返回 fd。将代码从一个接口迁移到另一个需要在分配层做抽象封装。另一个陷阱是 IOMMU 映射的粒度。ION 返回的scatterlist可能包含不连续的物理页面System Heap 的典型行为。如果 NPU 的 IOMMU 要求连续物理页面或只支持有限数量的 scatterlist 条目如底层 SoC 的 SMMU 仅支持 8 个条目一个 8MB 的 System Heap 分配展开成 2048 个 4KB 页面将超出硬件 IOTLB 容量。必须改用 CMA 堆保证物理连续或对 System Heap 分配额外进行dma_buf_attach 合并 scatterlist 条目的预处理。五、总结ION 和 DMA-BUF 共同构建了嵌入式 Linux 上跨处理单元的零拷贝内存共享机制。ION 负责从不同物理堆CMA/Buddy分配内存并导出 DMA-BUF fdDMA-BUF 负责在多个设备驱动之间管理对该内存的引用、建立 IOMMU 映射并保证缓存一致性。在 NPU 推理引擎的设计中推荐为模型权重静态、只读、输入/输出特征图流式、高频和中间张量临时、大块分别建立独立的内存池。权重池使用 CMA 堆预分配并持久映射到 NPU IOMMU特征图池使用 System Heap 及时刷 Cache 的方式降低 DMA 分配压力中间张量池在每帧推理完成后整体回收。这种三级池化架构可以将跨设备拷贝次数从传统的 6-12 次降至 0-1 次显著降低推理延迟和内存带宽占用。

相关新闻