Appearance
异步计算
一、什么是异步计算
我们现在用的计算机都是高度并行的系统,有多个 CPU 核、多个 GPU,每个设备都能同时处理很多事情。但 Python 本身是单线程的,不擅长并行和异步代码,所以深度学习框架(比如 PyTorch、MXNet、飞桨)都用了异步计算的方式来提高性能。
简单来说,异步计算就是:当我们调用一个 GPU 操作的时候,这个操作不会立刻执行,而是先排队到 GPU 的任务队列里,Python 代码会继续往下跑,不用等这个操作完成。这样 CPU 和 GPU 可以同时干活,提高整体的计算效率。
比如我们用 PyTorch 做矩阵乘法,当我们写b = torch.mm(a, a)的时候,Python 只是把这个任务交给 GPU 的后端队列,然后就继续执行下一行代码了,不用等 GPU 算完这个乘法。
二、异步计算的好处:为什么要这么做
我们先看一个例子,对比 NumPy 和 PyTorch 的矩阵乘法速度:
python
import numpy as np
import torch
from d2l import torch as d2l
# 用GPU计算
device = d2l.try_gpu()
# 测试NumPy(CPU计算)
with d2l.Benchmark('numpy'):
for _ in range(10):
a = np.random.normal(size=(1000, 1000))
b = np.dot(a, a)
# 测试PyTorch(GPU异步计算)
with d2l.Benchmark('torch'):
for _ in range(10):
a = torch.randn(size=(1000, 1000), device=device)
b = torch.mm(a, a)运行结果大概是:
Plain
numpy: 1.0704 sec
torch: 0.0013 secPyTorch 快了好几个数量级,除了 GPU 本身比 CPU 快,还有一个重要原因是 PyTorch 用了异步计算:Python 把任务交给 GPU 后,就继续干别的了,不用等 GPU 算完,所以看起来耗时特别短。
如果我们强制让 Python 等 GPU 算完(同步),耗时就会变长:
python
with d2l.Benchmark():
for _ in range(10):
a = torch.randn(size=(1000, 1000), device=device)
b = torch.mm(a, a)
# 强制等待GPU计算完成
torch.cuda.synchronize(device)运行结果大概是Done: 0.0049 sec,比异步的时候长了一点,但还是比 NumPy 快很多,因为 GPU 本身的计算速度快。
三、异步计算的原理:前端和后端
深度学习框架分为两部分:
前端:就是我们写代码的地方,比如 Python,负责发出计算命令。
后端:是框架的 C++ 实现,负责实际执行计算,管理自己的线程,不断从队列里拿任务执行。
前端把计算任务发给后端后,就继续执行自己的代码,不用等后端完成。后端会跟踪任务之间的依赖关系,比如如果z = x * y + 2,那么后端会先算x*y,再算+2,不会把顺序搞乱。
四、阻塞操作:什么时候会强制等待
有时候我们需要拿到计算结果,这时候就必须等后端算完,这种操作叫阻塞操作,比如:
打印变量:
print(z),因为要打印结果,必须等 z 算完。把张量转换成 NumPy:
z.numpy(),因为 NumPy 没有异步的概念,必须拿到实际的数值。把张量转换成标量:
z.item(),同样需要拿到实际的数值。显式等待:
torch.cuda.synchronize(device)或者z.wait_to_read()(MXNet 里的方法)。
比如我们测试一下阻塞操作的耗时:
python
# 测试转换成NumPy
with d2l.Benchmark('numpy conversion'):
b = torch.mm(a, a)
b.numpy()
# 测试转换成标量
with d2l.Benchmark('scalar conversion'):
b = torch.mm(a, a)
b.sum().item()这些操作都会让 Python 等待 GPU 计算完成,所以耗时会比纯异步的时候长。
五、异步计算的性能提升
异步计算可以让前端和后端并行工作,减少等待时间。比如我们做 10000 次加法操作,对比同步和异步的耗时:
python
# 同步执行:每次都等计算完成
with d2l.Benchmark('synchronous'):
for _ in range(10000):
y = x + 1
# 每次都等y算完
torch.cuda.synchronize(device) # 强制等待
# 异步执行:最后再等所有计算完成
with d2l.Benchmark('asynchronous'):
for _ in range(10000):
y = x + 1
# 最后等所有计算完成
torch.cuda.synchronize(device)运行结果大概是:
Plain
synchronous: 3.5791 sec
asynchronous: 0.7830 sec异步的耗时只有同步的 1/4 左右,因为同步的时候,Python 每次都要等 GPU 算完才能继续下一次循环,而异步的时候,Python 可以一次性把所有任务都发给 GPU,然后 GPU 自己慢慢算,Python 不用等。
假设每个任务的三个阶段耗时分别是 t1(前端发任务)、t2(后端计算)、t3(后端返回结果):
同步的总时间是:10000*(t1 + t2 + t3)
异步的总时间是:t1 + 10000t2 + t3(只要 10000t2 > 9999*t1,异步就更快)
六、注意事项
不要过度异步:如果把太多任务都放到队列里,会占用太多内存,导致内存不够用。建议每个小批量训练完后,做一次同步,让前端和后端的进度保持一致。
避免频繁的阻塞操作:如果频繁把张量转换成 NumPy 或者打印结果,会破坏异步的性能,因为每次都要等 GPU 算完。
注意任务依赖:后端会自动跟踪任务的依赖关系,所以不用担心顺序错了,比如
z = x*y + 2,后端会先算x*y,再算+2,不会乱序。
七、小结
异步计算让 Python 前端和框架后端并行工作,提高计算效率,尤其是在 GPU 计算的时候。
阻塞操作会强制等待计算完成,比如打印、转换 NumPy、显式同步,要合理使用。
不要过度异步,避免内存占用过多,建议每个小批量同步一次。
(注:文档部分内容可能由 AI 生成)