一、引言

本文主讲体验,方便深入理解为什么 GPU 这么高效,以及相关的对应措施。

二、开始编码

本节内容可汇总至同一个 py 脚本中,本次文件名为 pytorch_test_gpu.py,现依次说明如下:

2.1、引入并打印

本小节代码用于引入 torch,并打印检查是否有可用的 GPU。

import torch

if torch.cuda.is_available():
    device = torch.device("cuda")
    print("GPU 可用")
else:
    device = torch.device("cpu")
    print("没有可用的 GPU,将使用 CPU")

2.2、创建张量并移到相应设备上

请记住下面的 1000,后面的进一步测试代码会分成 1000,2000,4000,7000,10000 测试。

tensor = torch.rand(1000, 1000)

tensor = tensor.to(device)

2.3、在 CPU 上进行矩阵计算

该小节代码中添加了时间事件,用于计算所消耗的时间。

start_cpu = torch.cuda.Event(enable_timing=True)
end_cpu = torch.cuda.Event(enable_timing=True)
start_cpu.record()
result_cpu = torch.mm(tensor, tensor)
end_cpu.record()
torch.cuda.synchronize()
cpu_time = start_cpu.elapsed_time(end_cpu)

2.4、在 GPU 上进行矩阵计算

该小节代码中也添加了时间事件,用于计算所消耗的时间。

start_gpu = torch.cuda.Event(enable_timing=True)
end_gpu = torch.cuda.Event(enable_timing=True)
start_gpu.record()
result_gpu = torch.mm(tensor, tensor)
end_gpu.record()
torch.cuda.synchronize()
gpu_time = start_gpu.elapsed_time(end_gpu)

2.5、汇总打印时间

print(f"CPU 计算时间: {cpu_time:.3f} 毫秒")
print(f"GPU 计算时间: {gpu_time:.3f} 毫秒")

三、开始测试

以下 docker run 皆在 windows 的 wsl 窗口的传统 cmd 命令行界面。

3.1、第一次冲动的惩罚

$ docker run --gpus all --rm nvcr.io/nvidia/pytorch:24.01-py3 python pytorch_test_gpu.py

得到错误如下:

pytorch 如何检测能不能找到GPU 验证pytorch能用gpu_CUDA

很明显,这说明对 docker 的挂载不太了解,没关系,下一步就让容器能识别到这个 py 脚本。

3.2、第二次正确挂载执行测试

按比较方便的来,我假设该 py 脚本就在当前命令行的路径:

# $ cd 到 pytorch_test_gpu.py 当前的工作目录
$ docker run --gpus all -v %cd%:/workspace -it --rm nvcr.io/nvidia/pytorch:24.01-py3 python pytorch_test_gpu.py

pytorch 如何检测能不能找到GPU 验证pytorch能用gpu_CUDA_02

3.3、汇总测试结果

下面测试结果,按前面说的分了五组,每组两次:

pytorch 如何检测能不能找到GPU 验证pytorch能用gpu_pytorch_03

游戏结束,供反思,有兴趣想了解更多,可继续看扩展知识。

四、扩展知识

4.1、Windows 下的 Docker 正确挂载的办法

一般情况下,通用的编写命令规则如下:

$ docker run -v <host_directory>:<container_directory> <image_name>

其中参数对应如下:

  • <host_directory>
  • <container_directory>
  • <image_name>

那么在 Windows 上使用时,要注意以下几个要点:

  1. 在主机上指定的目录要真实存在;
  2. 使用反斜杠 `\` 作为目录分隔符,而不是正斜杠 `/`
  3. 可以使用多个 `-v`
  4. 可以使用绝对路径或相对路径。如果使用相对路径,则相对于当前工作目录进行解析。

4.2、为什么张量越大越慢?

当你增加张量的大小时,例如从 torch.rand(1000, 1000) 到 torch.rand(10000, 10000),你可能会发现 GPU 的加速效果逐渐降低,甚至变得不如 CPU。这可能有几个原因:

  • 数据传输开销:当张量的大小增加时,在 CPU 和 GPU 之间传输数据所需的时间也会增加。对于较小的张量,数据传输的时间可能相对较短,因此 GPU 的加速效果明显。但是,当张量变得非常大时,数据传输的开销可能会成为性能瓶颈,减少了 GPU 的加速效果。
  • GPU 内存限制:GPU 的内存容量有限,当张量的大小超过 GPU 的可用内存时,可能会导致内存不足的问题。在这种情况下,PyTorch 可能会将部分计算转移回 CPU,或者触发内存交换,这会显著降低性能。
  • 并行性的限制:虽然 GPU 擅长并行计算,但是当张量的大小增加到一定程度时,并行性可能会受到限制。这是因为 GPU 的并行处理单元(如 CUDA 核心)的数量是有限的。当张量的大小超过了 GPU 可以高效处理的阈值时,加速效果可能会下降。
  • CPU 的优化:对于某些操作,PyTorch 在 CPU 上可能有高度优化的实现。当张量的大小较小时,GPU 的加速效果可能会掩盖这些优化的影响。但是,当张量的大小增加时,CPU 优化的效果可能会变得更加明显,从而减少了 GPU 相对于 CPU 的优势。
  • 算法的特点:某些算法或操作可能更适合在 CPU 上执行,而不是在 GPU 上。这取决于算法的特点、数据访问模式以及 CPU 和 GPU 的架构差异。

为了获得更好的性能,你可以尝试以下几点:

  • 将大张量分割成较小的批次,以减少数据传输的开销和内存占用。
  • 使用适当的批次大小,以充分利用 GPU 的并行处理能力,同时避免超出 GPU 内存限制。
  • 对于某些操作,考虑使用 PyTorch 提供的优化库或者自定义的 CUDA 内核,以充分利用 GPU 的性能。
  • 根据任务的特点选择合适的设备。有些任务可能更适合在 CPU 上执行,而有些任务可能在 GPU 上表现更好。

总的来说,这就是程序员的价值所在了,去调整代码,优化代码后,可以充分发挥 GPU 的加速潜力,同时避免不必要的性能损失。

4.3、解释一下 torch.cuda.Event

torch.cuda.Event 是 PyTorch 中用于测量 CUDA 操作时间的一个类。它允许你在 CUDA 设备上记录事件的时间戳,并计算事件之间的经过时间。通过使用 torch.cuda.Event,你可以精确地测量 CUDA 操作的执行时间,从而优化和分析 GPU 上的性能瓶颈。

4.3.1、创建事件

start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)

4.3.2、记录事件

start_event.record()
# CUDA 操作...
end_event.record()

4.3.3、同步等待事件

torch.cuda.synchronize()

4.3.4、计算事件之间的经过时间

elapsed_time_ms = start_event.elapsed_time(end_event)