1. 从显存到内存:零拷贝为什么成了两个领域的共同答案
第一次意识到这两个概念之间存在深刻联系,是我在同时折腾嵌入式摄像头采集链路和大模型推理服务的时候。一边是 v4l2 到 dma-buf 再到 GPU 纹理的流水线,一边是张量并行(Tensor Parallel)下多卡通信的带宽瓶颈,表面上看八竿子打不着,但剥开外层,它们解决的是同一个问题:数据搬运太贵了,能不搬就不搬。
零拷贝(Zero-Copy)不是一个新词。二十年前网络协议栈就在玩这个,mmap、sendfile、RDMA,套路都差不多:减少用户态和内核态之间的内存复制次数,让数据在源头到目的地之间直达。但真正有意思的是,这个思想在异构计算和分布式计算的时代长出了两种完全不同的形态——dma-buf 解决的是单机内多个硬件设备共享内存的零拷贝,Tensor Parallel 解决的是多机多卡间同步张量的零拷贝。一个是物理内存的共享契约,一个是逻辑张量的切分与通信契约。
这篇文章我就把这两条线摊开对齐,讲讲各自的机制、背后的取舍,以及它们为什么值得放在一起看。适合正在搞 Linux 多媒体栈、异构计算,或者在做大模型训练推理优化的朋友,看完你会有一种“原来底层思路是通着的”的爽感。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 零拷贝的本质:不是不搬数据,而是让数据不用“路过”你
很多人对零拷贝有个误解,以为它代表完全没有数据移动。实际上,数据该从磁盘到网卡,或者从显存到计算单元,物理上总归要动。零拷贝真正省掉的,是数据在系统不同软件层次之间的反复拷贝,尤其是那些本来可以避免的中间副本。用一个生活化的类比来说:你从 A 房间搬家到 C 房间,零拷贝不是让你瞬移,而是让你不必先把所有东西搬到 B 房间堆一晚上,再从 B 搬到 C。如果 A 和 C 之间有条直通走廊,你当然希望别绕路。
在传统的数据通路里,数据至少要经历几次副本。以一次简单的摄像头采集为例:摄像头 DMA 把数据写进内核缓冲区,应用层要读,得从内核缓冲区 copy 到用户态缓冲区,如果要送去 GPU 做算法处理,还得再 copy 到显存。每次 copy 都意味着 CPU 参与、Cache 被污染、带宽被占用,而这一切在数据量变大之后会迅速变成瓶颈。
零拷贝的思路就是把这几次 copy 全部砍掉,改成共享缓冲区 + 所有权转移 + 同步机制。大家共用一块物理内存,谁要用谁直接拿地址去操作,配合 fence、同步对象或信号量来保证访问顺序。这样一来,数据的移动从“多个副本接力”变成了“一个副本多方共享”,省掉的是大量无意义的拷贝和上下文切换。
理解了这一层,再看 dma-buf 和 Tensor Parallel,就知道它们本质上是同一套方法论在不同尺度上的演绎。dma-buf 在物理内存层面做共享,Tensor Parallel 则在分布式系统的逻辑张量层面做切分与聚合,它们的共同出发点都是:数据在位,别乱搬,搬了就尽量只搬一次。
3. dma-buf 的零拷贝机制:异构设备共享同一块物理内存
3.1 从一次摄像头采集开始拆解
dma-buf 是 Linux 内核里的一套缓冲共享框架,最早由 System-on-Chip 社区推动,后来被 DRM 和 V4L2 这些子系统广泛采用。它要解决的场景非常具体:一个视频采集设备(比如 CSI 摄像头)和一个显示控制器(比如 DSI 屏幕)之间,要搬运一帧帧图像数据,如果每次都经过 CPU 中转,不仅慢,还会让 CPU 忙得没空干别的。
用 dma-buf 的典型流程是这样的:摄像头驱动在驱动初始化阶段申请一块 DMA 缓冲区,这块缓冲区由系统分配并导出成一个 dma-buf 对象。接着这块 dma-buf 会被传递到显示控制器驱动那里,显示控制器拿到它之后,通过映射接口拿到设备侧的物理地址,然后直接通过显示控制器的 DMA 引擎把数据从这块缓冲区拉到屏幕上。整个过程 CPU 只负责配置和调度,不搬数据。
在这个模型里,dma
