目录

Pytorch Cuda Streams Introduction

本文探讨了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 计算和数据传输?一个简单的思路:

  1. 创建两个 Cuda 流,一个用于数据传输(流A),一个用于计算(流B)
  2. 在 A 上启动异步传输
  3. 在 B 上执行运算
  4. 由于操作在不同的流上,他们会并行执行(重叠),进而提高性能
  5. 通过同步或事件确保所有操作都完成

有的同学可能会问了,我们刚才介绍了依赖关系必须同步,比如先从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 事件完成

  1. 在执行op前,创建并记录一个事件(start
  2. 执行op
  3. 在执行op后,创建并记录事件(end
  4. 使用end.synchronize()同步确保op执行完成
  5. 使用start.elapsed_time(end)来获取时间差

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-reducebroadcast等原语,这部分内容在笔者之前的文章distribution-training有进行介绍,感兴趣的小伙伴可以进行扩展阅读。