https://zhuanlan.zhihu.com/p/699754357
好的,这是一个非常核心且重要的概念。你提到的 "legacy stream" 是理解 CUDA Stream 演进和正确使用它们的关键。
我们来详细解释一下 Legacy Stream(遗留流)。
1. 什么是 Legacy Stream?
Legacy Stream 指的是在 CUDA 7.0 版本之前 ,默认流 (Default Stream) 的那种特殊的、具有隐式同步 (implicit synchronization) 行为的流。
当你调用一个 CUDA API(如 cudaMemcpyAsync 或 kernel<<<...>>>) 但不指定一个具体的 cudaStream_t 对象 时,这个操作就会被放入默认流中。在现代 CUDA 中,你也可以通过传递 0 或 NULL 来显式指定使用默认流。
- Legacy Stream 不是一种新的流,而是旧版本 CUDA 中默认流的一种特定行为模式。
2. Legacy Stream 最核心的特性:全局同步屏障
Legacy Stream 的行为与我们自己创建的普通流(Non-Default Stream)完全不同。它的关键特性是:
它与所有其他流之间存在隐式的、自动的同步关系。
这可以从两个方面理解:
- 启动一个 Legacy Stream 操作 :当你在 Legacy Stream 中启动一个操作(比如
kernel_L)时,这个操作会等待 GPU 上所有 其他普通流中已经提交的操作全部完成之后,才会开始执行。 - 在其他流中启动操作 :当你在一个普通流(比如
stream_A)中启动一个操作(比如kernel_A)时,这个操作会等待 Legacy Stream 中所有已经提交的操作全部完成之后,才会开始执行。
简单来说,Legacy Stream 就像一个全局的同步点或一道屏障。 任何进出 Legacy Stream 的操作都会强制GPU进行一次"大同步",从而破坏了不同流之间的并行性。
3. 图解 Legacy Stream 的行为
假设我们有以下代码序列:
c
// 1. 在 stream_A 中启动 Kernel A
kernel_A<<<..., stream_A>>>();
// 2. 在 Legacy Stream (默认流) 中启动 Kernel L
kernel_L<<<..., 0>>>();
// 3. 在 stream_B 中启动 Kernel B
kernel_B<<<..., stream_B>>>();
如果是在 Legacy Stream 模式下,执行的时间线会是这样的:
时间线分析:
- CPU: 快速连续地提交了 Kernel A, L, B。
- Stream A: Kernel A 开始执行。
- Legacy Stream : 虽然 CPU 已经提交了 Kernel L,但它必须等待 Stream A 中的 Kernel A 执行完毕。它像一道墙,拦住了自己。
- Stream B : 虽然 CPU 已经提交了 Kernel B,但它也必须等待 Legacy Stream 中的 Kernel L 执行完毕。Legacy Stream 也像一道墙,拦住了别人。
结果: Kernel A、L、B 变成了串行执行!我们使用多个流来并行执行任务的初衷被完全破坏了。这就是为什么它的行为模式是"遗留的 (Legacy)"并且通常是有害的。
4. 现代 CUDA 的改进:Per-thread Default Stream
由于 Legacy Stream 的行为严重阻碍了性能优化,从 CUDA 7.0 开始 ,NVIDIA 引入了新的默认流模式:Per-thread Default Stream (每线程默认流)。
这是现在所有现代 CUDA 的默认行为。
在这种新模式下:
- 默认流 (Default Stream) 的行为与任何其他普通流完全一样!
- 它不再与其他流有任何隐式的同步关系。
- 不同流(包括默认流和其他你创建的流)之间可以真正地并行执行。
在 Per-thread Default Stream 模式下,同样的代码序列会产生如下的时间线:
时间线分析:
- Kernel A, L, B 在提交后,如果 GPU 资源足够,它们会同时开始执行。
- 流与流之间是独立的,实现了真正的并发。
总结与对比
| 特性 | Legacy Stream (旧模式, CUDA < 7.0) | Per-thread Default Stream (新模式, CUDA >= 7.0) |
|---|---|---|
| 别名 | 默认流 (Default Stream), NULL Stream, Stream 0 | 默认流 (Default Stream), NULL Stream, Stream 0 |
| 与其它流的关系 | 隐式同步 (Implicit Synchronization) | 独立 (Independent) |
| 行为 | 像一个全局同步屏障,会阻塞其他流,也被其他流阻塞。 | 和任何普通创建的流行为一致。 |
| 对并发的影响 | 严重破坏不同流之间的并发性。 | 允许默认流和其他流之间实现真正的并发。 |
| 如何启用 | 在现代 CUDA 中,需要使用编译器标志 nvcc --default-stream legacy |
默认行为,无需任何特殊设置。 |
给你的建议:
- 在编写新的 CUDA 代码时,你应该始终基于 Per-thread Default Stream 的行为模式来思考,因为这是现代标准。
- 忘记 Legacy Stream 的存在,除非你在维护非常古老的代码或者被明确要求使用那个编译选项。
- 为了代码的清晰和可维护性,最佳实践是 :如果你需要并发,就显式地创建和使用你自己的流 (
cudaStreamCreate),而不是依赖默认流的行为。
早期的GPU硬件上只有一个execution engine, 是一个GPU一个 execution engine吗?
这是一个非常好的问题,它触及了CUDA硬件演进的核心。你提出的"一个GPU一个execution engine吗?"这个问题,答案是:在那个文本所描述的早期、简化的模型中,是的。但在现代GPU中,情况要复杂得多。
让我们来彻底剖析 "execution engine" 这个概念。
1. "Execution Engine" 是一个概念模型
首先要明确,"Execution Engine"(执行引擎)并不是一个官方的、精确的NVIDIA硬件术语,比如"Streaming Multiprocessor (SM)"或"CUDA Core"。它更多的是一种为了教学和理解而创造的概念性简化。
在这个简化的模型中,"Execution Engine" 代表了GPU一次能够处理和调度一个独立工作序列(即一个流 stream)的能力。
所以,当文章说"早期的GPU硬件上只有一个execution engine"时,它真正的意思是:
早期的GPU硬件,在任何一个时间点,其内部的调度器只能从一个流(Stream)中提取命令并执行。即使你在软件层面创建了多个流,硬件也只能串行地服务这些流,而不能让它们真正地并发执行。
2. 早期GPU的真实情况 (为什么是"一个引擎")
在像NVIDIA的 Fermi 和 Kepler 架构(大约在GeForce 400系列到700系列)的时代,情况确实如此。
- 硬件限制 :这些GPU拥有一个单一的硬件工作队列 (Hardware Work Queue) 或调度器。CPU将命令提交到不同的软件流(
cudaStream_t),但这些软件流最终都必须通过这唯一一个硬件入口点来将工作分发给GPU上的SM(Streaming Multiprocessors)。 - 行为后果 :这个单一的调度器一次只能关注一个软件流。它会执行完一个流中的所有已提交任务,然后才会切换到下一个流。这就导致了流之间的假并发 。你以为
kernelA(在stream A中)和kernelB(在stream B中)可以同时运行,但实际上硬件会先运行完kernelA,再运行kernelB,反之亦然。它们无法在时间上重叠。
把这个单一的硬件调度器想象成一个银行里只有一个柜员。 即使有10个客户(软件流)在排队,这个柜员(硬件调度器)一次也只能服务一个客户。他必须处理完一个客户的所有业务(流中的所有命令),才能接待下一个客户。
这就是文章中"一个execution engine"所代表的真实场景。
3. 现代GPU的巨大飞跃:Hyper-Q技术
从 Maxwell 架构 (GeForce 900系列)开始,特别是 Pascal 架构 (GeForce 10系列)及之后,NVIDIA引入了一项革命性的技术:Hyper-Q。
- 硬件进化 :Hyper-Q技术为GPU提供了多个硬件工作队列 (例如,最初的Hyper-Q支持32个)。这意味着GPU的顶层调度器现在可以同时从多个软件流中拉取命令。
- 行为后果 :有了Hyper-Q,来自不同流的Kernel现在可以真正地并发执行 (truly concurrent execution) 。只要GPU上有足够的空闲SM,调度器就可以把
kernelA(来自stream A)分派给一组SM,同时把kernelB(来自stream B)分派给另一组SM。
回到银行的例子,Hyper-Q就像是银行增设了多个柜台(比如32个)。 现在,32个客户(软件流)可以同时被不同的柜员(硬件工作队列)服务。这极大地提高了银行(GPU)的吞吐量和效率。
总结
| 早期GPU (如Fermi/Kepler) | 现代GPU (如Pascal/Volta/Ampere/Hopper) | |
|---|---|---|
| "Execution Engine" 概念 | 一个 (代表单一硬件调度能力) | 多个 (代表多路并发调度能力) |
| 硬件实现 | 单一硬件工作队列/调度器 | 多个硬件工作队列 (Hyper-Q技术) |
| 多流行为 | 伪并发/串行化:流之间不能同时执行 | 真并发:不同流中的任务可以同时在不同SM上执行 |
| 适用场景 | 文本中的简化模型,用于解释流的基本顺序性 | 现代高性能CUDA编程,利用多流实现指令级并行 |
所以,你的理解是正确的。文章作者使用"一个execution engine"这个说法,是为了建立一个最基础的模型,来解释单个流内部的顺序执行特性。这个模型对于理解早期GPU的行为或者只使用单个流的场景是完全准确的。但是,要进行高效的现代CUDA编程,就必须知道现代GPU已经突破了这个限制,拥有了"多个引擎"的能力,可以实现真正的多流并发。