3. 数据传输优化
数据传输,说白了就是显存和内存之间的搬运活。很多人觉得这有什么好讲的?不就是 cudaMemcpy 嘛。但我做了这么多年性能优化,可以负责任地告诉你:数据传输往往是整个深度学习训练的瓶颈。GPU 算得再快,数据喂不进去,一切都是白搭。
我见过太多团队,模型结构调得飞起,结果一看 Profiling,PCIe 传输占了 40% 以上的时间。嗯,这就是典型的「高射炮打蚊子」——GPU 在空转等数据。
同步传输:最朴素的搬运方式
cudaMemcpy 是大家最熟悉的 API。它的工作方式很简单:
- 调用后,CPU 线程阻塞
- 等数据全部搬完,才返回
- 默认是 Host 到 Device,或者 Device 到 Host
cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice);
// 这行执行完,数据才真正到了显存
这种模式的好处是「确定性」——你永远知道数据什么时候到位。但坏处也很明显:CPU 在等,GPU 也在等。整个流水线是串行的。
异步传输:让搬运和计算重叠
cudaMemcpyAsync 解决了同步传输的痛点。它不会阻塞 CPU 线程,调用后立即返回。但有个前提:必须配合流(Stream)使用。
cudaStream_t stream;
cudaStreamCreate(&stream);
cudaMemcpyAsync(d_data, h_data, size, cudaMemcpyHostToDevice, stream);
// 这里立即返回,CPU 可以干别的事
// 数据搬运在后台进行
为什么能异步?因为 GPU 有独立的 DMA 引擎。数据传输和计算可以并行。你想想看,这就像工厂里的传送带——一边在搬原料,一边在加工,互不干扰。
流与并发:真正的并行艺术
流(Stream)是 CUDA 里最优雅的设计之一。一个流就是一条命令队列,流之间的操作可以并发执行。
我画了一张图,帮你理解流的工作原理:
看到没?三个流在时间上完全重叠。Stream 1 在计算时,Stream 2 在传输数据。这就是真正的并发。
// 多流示例
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
// 流1:传输 + 计算
cudaMemcpyAsync(d_data1, h_data1, size, cudaMemcpyHostToDevice, stream1);
kernel<<<grid, block, 0, stream1>>>(d_data1);
// 流2:传输 + 计算(与流1并行)
cudaMemcpyAsync(d_data2, h_data2, size, cudaMemcpyHostToDevice, stream2);
kernel<<<grid, block, 0, stream2>>>(d_data2);
零拷贝技术:省掉搬运这一步
零拷贝(Zero-Copy)是个很有意思的技术。它让 GPU 直接访问主机内存,省去了显式的数据传输。
实现方式是通过 映射固定内存(Mapped Pinned Memory):
float* h_data;
cudaHostAlloc(&h_data, size, cudaHostAllocMapped);
float* d_data;
cudaHostGetDevicePointer(&d_data, h_data, 0);
// 现在 GPU 可以直接读写 h_data 指向的内存
kernel<<<grid, block>>>(d_data);
什么时候用零拷贝?我总结了几种场景:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 数据量小(< 1MB) | 零拷贝 | 传输开销大于计算开销 |
| 数据访问稀疏 | 零拷贝 | 只访问少量数据,全量传输浪费 |
| 数据量大且密集访问 | 传统传输 | 零拷贝的 PCIe 延迟更高 |
| 多 GPU 共享数据 | 零拷贝 | 避免多份拷贝,节省显存 |
实战建议:怎么选?
说了这么多,到底怎么选?我给出一个决策流程:
- 先看数据量:小于 1MB 且访问稀疏 → 零拷贝;大于 1MB → 走传输
- 再看是否可重叠:如果能用异步 + 流重叠传输和计算 → 用
cudaMemcpyAsync - 最后看显存:显存够用 → 预分配固定内存,避免运行时分配
cudaMemAdvise 可以告诉驱动你的访问模式。比如 cudaMemAdviseSetPreferredLocation 让数据尽量留在 GPU 端。这在多 GPU 场景下特别有用。
数据传输优化,说白了就是「让 GPU 别闲着」。同步传输是基础,异步传输是进阶,流是并发利器,零拷贝是特殊场景的奇兵。我个人习惯是:默认用异步 + 多流,小数据用零拷贝,同步传输只用于调试。
嗯,这一块内容不少,但核心就一句话:别让数据搬运成为瓶颈。下次你跑模型发现 GPU 利用率低,先看看是不是传输在拖后腿。