2. cudaMemcpy基础:同步拷贝的三种模式

好,咱们今天聊聊 cudaMemcpy。这个函数可以说是 CUDA 编程里最常用的 API 之一,没有它,CPU 和 GPU 之间就是「老死不相往来」的状态。

我个人习惯把 cudaMemcpy 叫做「同步拷贝三兄弟」——HostToDevice、DeviceToHost、DeviceToDevice。名字虽然长,但逻辑其实很简单:数据从哪来,到哪去。

2.1 API 签名与参数解析

先看函数签名,这是咱们吃饭的家伙:

cudaError_t cudaMemcpy(
    void* dst,          // 目标地址
    const void* src,    // 源地址
    size_t count,       // 拷贝字节数
    cudaMemcpyKind kind // 拷贝方向
);

四个参数,一个返回值。嗯,这里要注意:dst 和 src 的类型都是 void*,意味着你可以传任意类型的指针。但千万别传错了方向——我曾经在项目里把 dst 和 src 写反了,结果调试了整整一个下午才发现。

返回值 cudaError_t 也很关键。我建议每次调用后都检查一下返回值,或者用 cudaGetLastError() 兜底。你想想看,如果拷贝失败了,后续计算全白搭。

2.2 三种拷贝模式详解

2.2.1 HostToDevice(主机到设备)

这是最常用的模式。CPU 把数据准备好,然后「喂」给 GPU。说白了,就是 cudaMemcpy(d_gpu, h_cpu, size, cudaMemcpyHostToDevice)。

我在项目中遇到过一个问题:主机内存是 pageable(可分页)的,拷贝速度特别慢。后来改用 cudaHostAlloc 分配 page-locked 内存,速度直接翻倍。这个后面会细讲,先记住结论——page-locked 内存对 HostToDevice 拷贝有奇效。

关键点:HostToDevice 拷贝会阻塞 CPU 线程,直到数据传输完成。这意味着 CPU 在拷贝期间啥也干不了。

2.2.2 DeviceToHost(设备到主机)

GPU 算完了,结果得拿回 CPU 做后续处理。这就是 cudaMemcpy(h_cpu, d_gpu, size, cudaMemcpyDeviceToHost)。

这个方向有个坑:它也是阻塞的。而且因为 PCIe 带宽的限制,从 GPU 往 CPU 拷数据往往比反方向慢一些。我记得有一次做图像处理,每帧都要把结果拷回 CPU,结果帧率直接腰斩。后来改成异步拷贝才解决。

避坑指南:我曾经在 DeviceToHost 拷贝后直接访问主机内存,以为数据已经到位了。结果发现 GPU 内核还没执行完——因为 cudaMemcpy 虽然是同步的,但它只保证拷贝本身完成,不保证之前提交的内核已经执行完毕。所以记得在拷贝前加 cudaDeviceSynchronize()。

2.2.3 DeviceToDevice(设备到设备)

这个模式有意思:数据在 GPU 内部搬来搬去,不经过 PCIe。比如 cudaMemcpy(d_dst, d_src, size, cudaMemcpyDeviceToDevice)。

你可能会问:直接在 GPU 上操作指针不就行了,为啥还要拷贝?嗯,场景其实不少——比如做数据重排、多 GPU 间的数据交换(虽然这个场景更推荐用 cudaMemcpyPeer),或者某些库函数要求输入输出地址不同。

DeviceToDevice 拷贝的速度非常快,因为它走的是 GPU 内部的高速总线。我建议能用这个模式就别绕到 CPU 再拷回来,白白浪费带宽。

2.3 阻塞特性分析

为什么说 cudaMemcpy 是同步的?因为它会阻塞调用它的 CPU 线程,直到数据传输完成。说白了,就是 CPU 得「等」着。

来看个例子:

cudaMemcpy(d_gpu, h_cpu, size, cudaMemcpyHostToDevice);
// 这行代码执行时,CPU 被阻塞在这里
// 直到数据全部拷到 GPU 显存里,才会继续往下走
kernel<<<grid, block>>>(d_gpu);
// 内核启动是异步的,但数据拷贝已经完成了

这种阻塞特性有好有坏:

  • 好处:编程简单,逻辑清晰。你不需要操心数据什么时候到位,拷贝完就是到位了。
  • 坏处:浪费 CPU 时间。如果数据量很大(比如几百 MB),CPU 就干等着,啥也干不了。

我刚开始学 CUDA 时,总觉得阻塞拷贝是「天经地义」的。后来发现,很多高性能场景下,CPU 和 GPU 应该「各干各的」——CPU 准备下一批数据,GPU 算当前这批。这时候就需要异步拷贝了,那是后面章节的内容。

2.4 知识体系图

下面这张图总结了 cudaMemcpy 的核心逻辑,我建议你多看几遍:

cudaMemcpy 同步拷贝三种模式 CPU 主机内存 (pageable / page-locked) GPU 设备显存 (global memory) HostToDevice CPU → GPU DeviceToHost GPU → CPU DeviceToDevice GPU 内部拷贝 ⚠ 阻塞特性:CPU 线程等待拷贝完成 HostToDevice / DeviceToHost 均阻塞 CPU 三种模式覆盖了 CPU↔GPU 数据交换的全部场景,是 CUDA 编程的基石

2.5 性能对比表

最后给个表格,方便你快速对比:

模式 方向 是否阻塞 CPU 典型带宽 适用场景
HostToDevice CPU → GPU 是 ~12 GB/s (PCIe 3.0 x16) 输入数据准备
DeviceToHost GPU → CPU 是 ~12 GB/s (PCIe 3.0 x16) 结果回传
DeviceToDevice GPU 内部 是 ~900 GB/s (HBM2e) 显存内数据搬移

小提示:如果你发现 DeviceToDevice 拷贝很慢,先检查一下两个地址是不是在同一个 GPU 上。跨 GPU 拷贝要用 cudaMemcpyPeer,带宽会差很多。

好了,同步拷贝的基础就这些。记住一句话:同步拷贝简单可靠,但会浪费 CPU 时间。后面我们会讲怎么用异步拷贝来解决这个问题。

可可精品资料库