CUDA系统拆解-09-Streams、异步拷贝与Overlap:如何把拷贝和计算叠起来
CUDA系统拆解-09-Streams、异步拷贝与Overlap:如何把拷贝和计算叠起来
本文是「CUDA系统拆解」系列第 09 篇。
系列导读:CUDA系统拆解-00-导读:从编程模型到 AI 推理系统的学习路线
上一篇:CUDA系统拆解-08-Occupancy、寄存器压力与Launch参数:调优不是把占用率拉满
下一篇:CUDA系统拆解-10-Profiling、调试与瓶颈定位:先找到根因再谈优化
1. 这篇解决什么问题
这一篇要讲清 5 件事:
- 为什么 CUDA 程序不能只盯着“一个 kernel 算得快不快”,还要关心整条执行流水。
stream到底是什么,它和“主机端异步返回”是什么关系。- 为什么“主机端看起来异步”不等于“设备端真的并发执行”。
pinned memory为什么会影响异步拷贝和计算重叠。overlap到底在什么条件下才成立,以及它为什么在小 batch 和decode场景更重要。
如果这篇只记住一句话,那就是:
异步执行的目标不是把 API 写成异步,而是把拷贝、launch 和计算组织成真正能重叠的设备端流水。
2. 先记住的核心结论
stream可以理解成设备端的一条有序执行队列;同一个 stream 内通常按提交顺序执行,不同 stream 才有机会并发。- 主机端函数“提前返回”只说明 CPU 没被阻塞,不说明 GPU 上的拷贝和 kernel 一定并发。
- 默认
stream往往带来更强的顺序语义,如果所有操作都丢进默认 stream,就很难形成真正的 overlap。 pinned memory是页锁定内存。它更适合做高效 DMA 拷贝,也是很多异步 H2D / D2H 和重叠执行成立的前提。overlap通常需要同时满足:不同 stream、异步 API、合适的硬件能力、合适的内存类型,以及没有额外同步把流水打断。- 在大算子很重的场景里,launch 和流水组织问题可能被算力掩盖;在小 batch、短序列、
decode这类场景里,它们会更容易暴露出来。
3. 正文讲解
3.1 为什么需要异步执行
最朴素的 CUDA 执行方式是:
- 把数据从主机拷到设备
- launch 一个 kernel
- 等它跑完
- 再把结果拷回主机
这种方式逻辑简单,但资源利用率通常不高。原因是:
- CPU 在很多时刻只是等待
- PCIe / NVLink 拷贝和 GPU 计算没有尽量重叠
- 多个小任务之间容易出现空洞
所以异步执行的目标不是“代码看起来高级一点”,而是尽量把:
- 主机准备下一批工作
- H2D / D2H 拷贝
- kernel 执行
组织成一条连续流水,减少设备空转和主机等待。
3.2 stream 到底是什么
理解 stream 时,最稳的口径是:
stream 是设备端的一条有序工作队列。
你往一个 stream 里提交的操作,例如:
- H2D 拷贝
- kernel launch
- D2H 拷贝
通常会按顺序排队。于是:
- 同一个 stream 更强调顺序性
- 不同 stream 才有机会被设备调度成并发
这里要特别区分两件事:
- 主机端 API 是否阻塞
- 设备端任务是否并发
例如,一个异步 API 可能很快就返回给 CPU,但 GPU 端仍然只是把任务排进某条 stream,最后是否并发,还要看设备端队列关系和资源条件。
3.3 默认 stream 和普通 stream 的区别
很多初学者以为“用了异步 API 就天然并发”,问题常常出在默认 stream 上。
默认 stream 的直觉可以理解成:它常常带着更强的顺序语义。
如果所有拷贝和 kernel 都放在同一条默认 stream 里,那么即使主机端调用是异步的,设备端通常也还是顺序推进。
所以判断有没有 overlap,不能只看代码里有没有:
cudaMemcpyAsync- 异步 launch
还要看:
- 是否真的分到了不同 stream
- 中间是否插入了同步
- 默认 stream 语义是否把事情重新串行化了
3.4 为什么主机异步不等于设备端并发
这是这一篇最容易踩坑的地方。
“主机异步”只说明:
- CPU 提交任务后不用原地等
- CPU 可以继续做别的事
但设备端是否并发,还要继续问:
- 这些任务是否在不同 stream
- GPU 是否有 copy engine 和计算资源可同时工作
- 数据拷贝是否真的是异步可执行的
- 是否有
cudaDeviceSynchronize、事件等待、默认 stream 依赖把队列卡住
所以更准确的说法是:
异步是主机视角的接口行为,并发是设备视角的执行结果。两者相关,但不等价。
3.5 pinned memory 为什么重要
pinned memory 指的是页锁定内存,也叫 page-locked memory。它和普通 pageable host memory 的关键区别在于:
- 普通主机内存可能被操作系统换页
pinned memory不会被随意换出- 设备做 DMA 传输时更容易直接、高效地访问它
这对 CUDA 有两个直接意义:
- 主机到设备、设备到主机的拷贝效率通常更好
- 很多真正想做异步拷贝和 overlap 的场景,会更依赖
pinned memory
可以把它理解成:
如果 host memory 本身不适合直接做高效异步搬运,那么所谓“异步拷贝”在设备端很可能达不到你预期的重叠效果。
所以 pinned memory 的意义不只是“快一点”,而是它经常是异步数据通路能否真正成立的一块地基。
3.6 overlap 在什么条件下才成立
想让拷贝和计算重叠,通常要同时满足几类条件:
- 使用异步 API,而不是阻塞式拷贝
- 工作被放到合适的多个 stream,而不是全都塞进同一条队列
- host memory 类型合适,常见就是使用
pinned memory - GPU 具有相应的并发执行能力,例如 copy engine 与计算单元能协同
- 没有额外同步把流水切断
也就是说,真正的 overlap 是一条链路条件同时满足后的结果。
只满足其中一个条件,通常不够。
这也是为什么很多人代码里已经写了 cudaMemcpyAsync,实际 profile 一看仍然几乎全串行。问题不是“异步 API 没用”,而是整条流水没有真的被搭起来。
3.7 overlap 的工程意义
overlap 的价值,本质上是在隐藏不能消掉的时间:
- H2D / D2H 拷贝时间
- kernel launch 间隙
- 多阶段流水之间的等待
如果一段计算本身非常重,例如一个大 GEMM 跑很久,那么这些开销有时会被淹没。
但如果任务变碎,问题就会变明显:
- batch 很小
- 请求很多但每个都短
decode每步计算量小- 一轮里要频繁 launch 多个小 kernel
这时瓶颈可能不再只是某个 kernel 的 FLOPs,而是:
- launch overhead
- stream 组织
- 拷贝和计算有没有重叠
- CPU 和 GPU 是否形成稳定流水
所以在 AI 推理里,异步执行和 overlap 往往不是“高级优化项”,而是系统吞吐和尾延迟能不能做好的基础条件。
4. 和 AI 推理的关系
这一篇和推理系统的关系非常直接。
很多在线推理场景并不是“大 batch 大矩阵一路轰过去”,而是:
- 动态请求持续到达
- batch 经常变化
- 小 kernel 很多
decode单步计算偏小- 主机端调度、数据准备和设备端执行要持续配合
所以推理系统里常见的问题,最后都能翻译回这一篇:
- 为什么小 batch 吞吐上不去
- 为什么 GPU 利用率不稳定
- 为什么
decode阶段 launch 开销特别显眼 - 为什么有些系统要非常重视 stream、事件、异步 H2D、pinned buffer 和流水组织
换句话说,大模型推理不只是“kernel 快”,还要“整机流水顺”。
5. 常见误区
- 用了异步 API,不代表 GPU 端就一定并发。
stream不是 CPU 线程,也不是“随便分个标签”;它的本质是设备端队列关系。- 默认
stream往往会带来更强顺序语义,容易把你以为的并发重新串起来。 pinned memory不只是更快的 host memory,它经常决定异步拷贝和 overlap 能不能真正成立。- overlap 不是单点优化,而是拷贝、队列、硬件能力和同步策略共同作用的结果。
6. 复习自测
- 为什么说
stream更适合理解成设备端的有序执行队列? - 为什么“主机端异步返回”不等于“设备端并发执行”?
- 默认
stream为什么容易让程序重新串行化? pinned memory对异步拷贝和 overlap 的价值到底是什么?- overlap 通常需要满足哪些条件?
- 为什么小 batch 和
decode场景更容易暴露 launch overhead 和流水组织问题?
