Pytorch Cuda Streams Introduction
Summary
本文探讨了Cuda **流(Stream)的基本概念、并行执行和多GPU同步策略。我们分析了使用多个Cuda流的优势,以及如何通过Cuda事件(Event)**确保任务同步,利用Cuda流优化程序性能。
基本概念
Q:Cuda 流是什么?它的主要用途是什么?
- Cuda流可以理解为一个顺序执行任务队列,当提交运行cuda的任务到流上时,它会严格按照提交任务的顺序执行。
- 不同流的任务可以并行执行
- 注意cuda流上任务执行和cpu上的任务执行默认是异步的。
- 主要用途:允许开发者更细粒度地控制并行、同步和任务执行顺序,以充分利用GPU,优化程序运行效率。
Q:默认情况下,PyTorch Cuda 操作在哪个流上执行?
- 每个GPU都有一个默认的Cuda流(default stream)
- 如果用户没有显式指定其他流,pytorch的Cuda操作将在这个流上执行
并行执行
Q:我们为什么可能需要使用多个 Cuda 流?
- 我们可以使用多个Cuda流来充分利用GPU,特别是存在多个独立并行执行的操作时
- 这种方式不仅可以用于独立的计算操作(如没有依赖关系的乘法和加法),还可以用于计算和数据传输(如
copy
),以进一步提高性能。 - 值得指出的是,开发者必须确保将互相依赖的操作放在相同的流上,否则计算结果可能会错误。
Q:为什么说实际并行度取决于GPU的硬件资源?
原因很多,我们选取主要的说明:
- 流多处理器(Streaming Multiprocessors, SMs) 的数量:SMs是并行处理任务的基本单元,每个SM有一定数量的计算单元(ALUs),更多的SMs意味着更多的并行线程。例如V100有80个SM,GTX 1050只有6个
- 硬件功能级别:NVIDIA GPU有不同计算能力级别(Compute Capability),如3.5的设备可能支持 Hyper-Q 和动态并行等特性,而2.0的设备就不支持。
- 全局内存带宽和缓存:更快的数据传输速度(主机到设备、设备到设备)和内存访问速度能有效提升并行性。
Q:为什么我们需要同步?
- 数据准确性:互相依赖的操作(计算和拷贝)如果在不同的流上,我们必须同步才能得到正确结果。
- 测量性能:如果想准确测量GPU 操作执行时间,我们必须同步。
- 错误检查:异步时报错可能无法得到正确的报错,开启同步定位问题
数据传输
Q:如何使用 Cuda 流重叠 GPU 计算和数据传输?一个简单的思路:
- 创建两个 Cuda 流,一个用于数据传输(流A),一个用于计算(流B)
- 在 A 上启动异步传输
- 在 B 上执行运算
- 由于操作在不同的流上,他们会并行执行(重叠),进而提高性能
- 通过同步或事件确保所有操作都完成
有的同学可能会问了,我们刚才介绍了依赖关系必须同步,比如先从host copy tensor到gpu,需要确保数据完整传输后才能开始计算。那同步了的话和在一个流上执行有什么区别呢?
这是因为我们可以在计算过程中提前开始下一个批次的数据传输:
如 B 正在运算,如果在一个流上执行,此时我们只运算,没有数据传输。而如果在两个流上执行,B 运算时我们就可以利用 A 来进行数据传输,这样就起到了最基本的重叠并行效果,能够大大加快深度学习训练效率。
注意事项:
- 并非所有GPU都支持并发的数据传输和计算,较旧的GPU可能就不支持
- 使用太多的流或没有恰当地管理流,可能会出现资源争抢或者调度的问题,影响并行效果。
- 需要确保数据依赖关系被正确处理
事件
Q:Cuda 事件 (torch.cuda.Event
) 与 Cuda 流有何关系?
Event 是标记 stream 中特定点的工具,我们使用 Event来监控和同步流的执行。它可主要用于:
- 同步:相对于
cuda.syncronize
(阻塞CPU,确保设备上所有流的所有操作都完成),我们可以通过事件来进行更细粒度的同步控制。例如我们可以在流 A 中记录一个事件,在流 B 中等待该事件完成,实现同步。 - 性能测量:可以使用 Event来测量 Cuda 操作的时间,以进一步了解和优化程序性能。
Q:如何使用 Cuda 事件精确测量 Cuda 操作(op)的时间?
一个简单的思路:使用两个 Cuda 事件完成
- 在执行op前,创建并记录一个事件(
start
) - 执行op
- 在执行op后,创建并记录事件(
end
) - 使用
end.synchronize()
同步确保op执行完成 - 使用
start.elapsed_time(end)
来获取时间差
多个GPU流同步
Q:当使用多个 GPU 时,如何保证每个 GPU 上的流操作正确地同步?
- 设置当前设备:使用
torch.cuda.set_device(device_id)
来确保你正在与合适的GPU交互。 - 使用Cuda 事件:事件可以在不同GPU间同步,如在 GPU0 的流A上记录事件,然后再GPU2 的流B 上同步事件
- 明确某个设备的同步,
torch.cuda.synchronize(device_id)
- 考虑流的依赖关系,确保数据依赖被解决。
- 避免不必要的同步,只在确实需要时进行同步来提升效率
此外,我们也可以使用 直接设备间通信(Peer-to-Peer,P2P) 优化同步效率:通过 P2P 我们可以直接将数据从一个GPU转移到另一个GPU,无需经过host中转,节省时间和带宽。(如NVIDIA的NVLink技术)
为了在复杂的多GPU应用中实现准确运算和高性能,我们除了考虑流的同步外,还需要考虑高效地进行设备间通信,使用all-reduce
,broadcast
等原语,这部分内容在笔者之前的文章distribution-training有进行介绍,感兴趣的小伙伴可以进行扩展阅读。