1. 问题起点:一条被反复点击的优化路径
做Linux内核驱动或者搞过多媒体框架的兄弟,对 dma-buf 应该都不陌生。做分布式训练的同学,对 tensor parallel 更是每天都要打交道。这两个东西放在一起看,表面上像两条永不相交的平行线——一个在内核态管物理内存,一个在深度学习框架里管张量计算。但我这两年两边项目都深度参与之后,越来越觉得它们骨子里的设计逻辑是同一套东西。
先讲个我自己遇到的场景。之前做视频采集设备驱动,ISP(图像信号处理器)采集到一帧原始图像,需要扔给GPU去做AI识别。最朴素的做法是:ISP先把数据写到内存,然后CPU用memcpy把数据从内核缓冲区拷到用户态,再拷到GPU显存。拷贝过程中CPU占用高,延迟大,一帧1080p30的图就能把CPU吃满十几个百分点。后来改成dma-buf方式,ISP直接把数据写到一块dma-buf里,设备fd一传,GPU那边直接拿这块物理内存映射成自己的显存,CPU全程零拷贝,延迟直接少了七八毫秒。
那边做训练的同学也在折腾类似的事。多机多卡训练,模型太大单卡放不下,就把一个Transformer层按head维度切开,每个GPU只算自己那部分,算完互相传一下。这个过程叫tensor parallel。最开始我们用的NCCL allreduce,每个step梯度同步一次,后来发现通信时间占比太高,就在算法层面做分片、做通信压缩,本质上也是要减少数据搬移量。
一个是内核态的设备间内存共享,一个是分布式训练的张量切分,名字差了十万八千里,底层逻辑却惊人地一致:谁都不想让数据在中间商手里多倒一次手。
这篇文章就把这两个领域拆开揉碎,看看它们各自的零拷贝实现思路,然后再拉通对比,聊聊设计模式层面能互相借鉴的地方。适合做驱动开发、多媒体框架、嵌入式优化、以及搞分布式训练框架的读者,读完你可能会跟我一样,有一种“原来大家都在解决同一个问题”的通透感。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. dma-buf 与零拷贝的内核实践
2.1 dma-buf 的诞生背景:从系统内存到外设跨域传输
先聊 dma-buf。这个东西在Linux内核里其实已经有十几年的历史了,最初是给GPU驱动、显示控制器、视频编解码器这些外设之间共享内存用的。
背景是这样的:以前多个外设(比如GPU和Display Controller)要共享一帧图像,大家各搞各的。GPU渲染完一帧,要显示到屏幕上,需要把数据从GPU显存拷贝到系统内存,再拷贝给显示控制器。每一轮拷贝都是按帧大小算的,4K分辨率60帧的显示输出,每一帧8MB左右,一秒要拷480MB数据过一遍系统总线,总线带宽被占掉一大截,延迟还高。
后来内核社区统一搞了 dma-buf 这个抽象层。核心思想很简单:一块物理内存(可能本身是设备显存,也可能是系统内存),由某个设备/驱动作为生产者创建出来,放进一个统一的buffer对象里,其他设备和驱动可以通过fd或者handle引用它,获得它的映射、同步、导出权利。本质上就是一个跨设备、跨驱动的共享内存管理器。
更底层一点说,dma-buf 屏蔽了“这块内存到底在物理上属于谁”的细节。它把这些统一抽象成 DMA地址和 sg_table(scatter-gather 表),别的设备要访问,就通过 dma_buf_map_attachment 拿到自己总线视角的地址。这个过程不需要任何数据搬移,只是把地址关系映射一下,开销是微秒级的。
2.2 生产者-消费者模型与所有权移交
dma-buf 的架构非常像一个典型的生产者-消费者模型。你去看内核源码,drivers/dma-buf/dma-buf.c 里核心数据结构就三块:
- dma_buf:这个是共享内存对象的“文件本体”,内部管理着 sg_table、ops 操作集、attach 列表。
- dma_buf_attachment:代表某个设备对这块 buffer 的一次“接入/引用”。一个 dma_buf 可以有多个 attachment,也就是多个设备同时引用。
- dma_buf_ops:定义了 map_dma_buf、unmap_dma_buf、mmap、begin_cpu_access 等回调。
有意思的是所有权移交。按传统思路,内存只归属于一个设备的地址空间,其他设备要用就得“借”。dma-buf 把这个过程规范成了 S2(同步)和 M2(互斥)两种状态管理,配合 fence 机制(下面会细说),保证数据在被消费者读到之前,生产者已经把数据写完了。
我在实际驱动开发中,最常用的流程是这样的:
c复制// 导出端:分配dma-buf,填充ops,创建fd
struct dma_buf *dmabuf = dma_buf_export(&exp_info, &dma_buf_ops);
int fd = dma_buf_fd(dmabuf, O_CLOEXEC);
c复制// 导入端:通过fd获取dma-buf,创建attachment并映射
struct dma_buf *dmabuf = dma_buf_get(fd);
struct dma_buf_attachment *attach = dma_buf_attach(dmabuf, dev);
struct sg_table *sgt = dma_buf_map_attachment
