2. cudaMemcpy基础:同步拷贝与异步拷贝的API详解,默认流与多流场景下的行为差异

说到CUDA编程,内存拷贝是绕不开的基本功。我刚开始接触CUDA时,觉得cudaMemcpy不就是把数据从CPU搬到GPU嘛,有啥好研究的?结果第一次做大规模数据处理,性能直接拉胯,排查了半天才发现是拷贝方式没选对。今天咱们就把这块掰开揉碎了讲清楚。

2.1 同步拷贝:最直接的cudaMemcpy

先看最基础的API:

cudaError_t cudaMemcpy(void* dst, const void* src, size_t count, 
                       enum cudaMemcpyKind kind);

这个函数大家肯定都用过。它的行为是阻塞式的——调用返回时,数据一定已经拷贝完成了。说白了,CPU会一直等在那里,直到GPU把活干完。

关键点:cudaMemcpy是同步的,它会阻塞当前CPU线程。

我有个习惯,在写原型代码时先用cudaMemcpy,因为它简单、不容易出错。但生产环境里,我几乎不用它做大批量数据传输——太慢了,CPU白白等着。

2.2 异步拷贝:cudaMemcpyAsync的威力

真正干活的是这个:

cudaError_t cudaMemcpyAsync(void* dst, const void* src, size_t count,
                            enum cudaMemcpyKind kind, cudaStream_t stream);

注意最后一个参数——stream。这是异步拷贝的灵魂。调用cudaMemcpyAsync后,函数会立即返回,CPU不用等。拷贝操作被提交到指定的流中,由GPU在后台执行。

我的经验:异步拷贝配合流水线操作,能把数据传输和计算重叠起来。我曾经在一个视频处理项目中,用异步拷贝把整体延迟降低了40%。

但有个坑——异步拷贝要求源地址和目的地址必须是页锁定内存(pinned memory)。如果你传了普通malloc分配的内存,cudaMemcpyAsync会默默退化成同步拷贝,而且不会报错。嗯,这里要注意。

2.3 默认流 vs 多流:行为天差地别

为什么要把流单独拎出来讲?因为流决定了操作之间的执行顺序。

默认流(NULL stream)

如果你不显式创建流,所有操作都在默认流中执行。默认流有个特殊性质——它会对所有其他流产生隐式同步。什么意思?

  • 默认流中的操作必须等所有其他流中的操作完成才能开始
  • 其他流中的操作也必须等默认流中的操作完成才能开始

说白了,默认流就是个"霸道总裁",谁都得让着它。这会导致你明明用了多流,却得不到并行效果。

避坑指南:我曾经在项目里同时用了默认流和自定义流,以为能并行执行,结果性能纹丝不动。排查了半天才发现是默认流在作祟。从那以后,我只要用多流,就坚决不用默认流。

多流场景

创建多个非默认流,才能真正实现操作并行:

cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);

// 异步拷贝到不同流
cudaMemcpyAsync(dst1, src1, size, cudaMemcpyHostToDevice, stream1);
cudaMemcpyAsync(dst2, src2, size, cudaMemcpyHostToDevice, stream2);

// 不同流中的kernel可以并行执行
kernel1<<<grid, block, 0, stream1>>>(dst1);
kernel2<<<grid, block, 0, stream2>>>(dst2);

同一个流中的操作是串行的,不同流中的操作可以并行。你想想看,如果数据拷贝和计算能同时进行,那效率能提升多少?

2.4 知识体系总览

下面这张图把核心逻辑串起来了:

cudaMemcpy 核心知识体系 cudaMemcpy(同步) cudaMemcpyAsync(异步) 阻塞CPU线程 立即返回,不阻塞CPU 默认流:隐式同步所有流 多流:操作可并行执行 ⚠ 异步拷贝要求页锁定内存(pinned memory) ⚠ 多流场景避免使用默认流,否则无法真正并行

2.5 实际场景对比

为了让你更直观地理解差异,我整理了一个对比表:

特性 cudaMemcpy(同步) cudaMemcpyAsync(异步)
CPU行为 阻塞等待 立即返回
内存要求 任意内存 必须页锁定内存
流支持 默认流(隐式) 可指定任意流
并行能力 无(串行执行) 可与计算重叠
适用场景 小数据量、原型验证 大数据量、性能优化

个人建议:刚开始学CUDA时,先用cudaMemcpy把功能跑通。等代码稳定了,再逐步替换成cudaMemcpyAsync做性能优化。一步到位反而容易踩坑。

好了,这一章的内容就到这里。记住核心:同步拷贝简单但低效,异步拷贝灵活但需要小心内存类型和流管理。下一章我们会深入页锁定内存的分配和管理,那是异步拷贝的基石。


可可精品资料库