12.3.1 基于GPU的并行计算

预热的原因:

🧠 核心原因:GPU 的初始化和缓存机制会影响首次运行速度

1. 首次执行会有额外开销

  • 当你第一次在一个设备(如 cuda:0)上执行一个操作时,系统会进行一些初始化工作
    • 加载内核(kernel);
    • 分配内存;
    • 编译 PTX(Parallel Thread Execution)代码为特定 GPU 架构的指令;
    • 初始化 CUDA 上下文;
  • 这些操作只会在第一次运行时发生,后续调用就会快很多。

2. 缓存效应

  • 某些计算或内存访问模式会被硬件缓存(cache)优化;
  • 第一次运行可能命中率低,第二次就高了;
  • 如果你在测量时不考虑这些因素,第一次的“慢速”会扭曲你的平均性能数据

🔍 原因 1:缓存未命中(Cache Miss)

  • 第一次执行某个操作时,所需的数据不在缓存中;
  • 系统必须从内存甚至硬盘中读取,这会花费更多时间;
  • 这叫做 缓存未命中(cache miss)

✅ 原因 2:缓存命中(Cache Hit)

  • 第二次运行相同或类似的操作时,所需数据很可能已经缓存好了;
  • 系统可以直接从缓存中快速读取;
  • 这叫做 缓存命中(cache hit),速度快很多。

你的理解是正确的,这里的并行确实是因为 PyTorch 的 CUDA 操作是异步的,而不是因为 Python 本身的同步特性。

我来详细解释一下:


1. Python 是同步的,但 CUDA 操作是异步的

在 Python 中,默认情况下代码是同步执行的,即一行执行完才会执行下一行。
然而,当调用 PyTorch 的 CUDA 操作(如 .cuda() 上的张量计算)时,PyTorch 并不会阻塞 Python 线程,而是将计算任务提交到 GPU 的命令队列中,然后立即返回,这样 Python 就可以继续执行下一行代码。


2. GPU 有自己的并行执行能力

GPU 有多个流(streams)和多处理器,可以同时执行多个独立的计算任务。
当你在代码中连续调用两个 GPU 的计算任务时:

run(x_gpu1)  # 任务1提交到 GPU1
run(x_gpu2)  # 任务2提交到 GPU2

PyTorch 会:

  • 将第一个任务提交到 GPU1 的队列
  • 将第二个任务提交到 GPU2 的队列
  • 两个 GPU 可以同时执行各自的任务
  • Python 代码继续往下执行,不等待 GPU 计算完成

3. torch.cuda.synchronize() 的作用

如果你希望等待某个 GPU 完成所有任务,就需要调用:

torch.cuda.synchronize(device)

否则,你测量时间时可能会包含 GPU 计算时间 + Python 执行时间的不准确结果。


4. 为什么你的例子中删除 synchronize 能实现并行

原代码:

with d21.Benchmark('GPU1 time'):
    run(x_gpu1)
    torch.cuda.synchronize(devices[0])  # 等待 GPU1

with d21.Benchmark('GPU2 time'):
    run(x_gpu2)
    torch.cuda.synchronize(devices[1])  # 等待 GPU2

总时间 ≈ 0.46 + 0.47 ≈ 0.93 秒(串行测量)。

删除 synchronize 后:

with d21.Benchmark('GPU1 & GPU2'):
    run(x_gpu1)  # 提交到 GPU1
    run(x_gpu2)  # 立即提交到 GPU2
    torch.cuda.synchronize()  # 等待所有 GPU 完成

总时间 ≈ 0.458 秒(几乎等于单个 GPU 的时间),说明两个 GPU 是同时执行的。


5. 这就是“自动并行”

PyTorch 底层自动管理 GPU 任务调度,你不需要手动写多线程或多进程代码,只需按顺序提交任务到不同设备,系统会自动尝试并行执行。


✅ 总结:

  • Python 代码是同步的,但 PyTorch CUDA 操作是异步的
  • GPU 任务提交后立即返回,计算在后台进行。
  • 多个 GPU 可以同时执行各自的任务。
  • 这种设计让用户可以用同步的写法(一行接一行)获得异步并行的效果。
posted @ 2025-05-17 19:46  最爱丁珰  阅读(99)  评论(0)    收藏  举报