多进程最佳实践#
创建日期: 2017年1月16日 | 最后更新日期: 2025年6月18日
torch.multiprocessing
是 Python 内置 multiprocessing
模块的“即插即用”替代品。它支持完全相同的操作,但对其进行了扩展,因此通过 multiprocessing.Queue
传输的所有张量,其数据将被移至共享内存,并且只会将一个句柄发送到另一个进程。
注意
当 Tensor
被发送到另一个进程时,Tensor
数据是共享的。如果 torch.Tensor.grad
不是 None
,它也会被共享。在将一个没有 torch.Tensor.grad
字段的 Tensor
发送到另一个进程后,它会创建一个标准的、特定于进程的 .grad
Tensor
,它不会像 Tensor
数据那样被自动跨进程共享。
这使得实现各种训练方法成为可能,例如 Hogwild、A3C 或任何需要异步操作的方法。
多进程中的“毒性 fork”#
在使用带 加速器 的多进程时,可能会出现一个称为“毒性 fork”的已知问题。当加速器的运行时不安全且在进程 fork 之前初始化时,就会发生这种情况,从而导致子进程中的运行时错误。
- 为防止此类错误
在 fork 子进程之前,避免在主进程中初始化加速器。
使用替代的进程启动方法,例如
spawn
或forkserver
,这确保了每个进程都能进行干净的初始化。
多进程中的 CUDA#
在使用 fork
启动方法时,CUDA 运行时存在 多进程中的“毒性 fork” 中描述的限制;必须使用 spawn
或 forkserver
启动方法才能在子进程中使用 CUDA。
注意
启动方法可以通过创建具有 multiprocessing.get_context(...)
的上下文或直接使用 multiprocessing.set_start_method(...)
来设置。
与 CPU 张量不同,发送进程在接收进程保留张量副本时,必须保持原始张量。这在底层实现,但要求用户遵循最佳实践才能使程序正确运行。例如,发送进程必须在消费者进程仍然持有张量引用时保持活动状态,如果消费者进程由于致命信号异常退出,则引用计数无法挽救您。请参阅 本节。
另请参阅: 使用 nn.parallel.DistributedDataParallel 而不是 multiprocessing 或 nn.DataParallel
最佳实践和技巧#
避免和处理死锁#
在生成新进程时,很多事情都可能出错,死锁最常见的原因是后台线程。如果任何线程持有锁或导入了模块,并且调用了 fork
,那么子进程很可能处于损坏状态,并会导致死锁或其他故障。请注意,即使您没有,Python 的内置库也会如此——无需查找比 multiprocessing
更远。 multiprocessing.Queue
实际上是一个非常复杂的类,它会生成多个线程用于序列化、发送和接收对象,它们也可能导致上述问题。如果您发现自己处于这种情况,请尝试使用 SimpleQueue
,它不使用任何额外的线程。
我们正在尽最大努力让您轻松使用,并确保这些死锁不会发生,但有些事情是我们无法控制的。如果您遇到任何一段时间内无法解决的问题,请尝试在论坛上寻求帮助,我们将看看这是否是我们能够解决的问题。
重用通过 Queue 传递的缓冲区#
请记住,每次将 Tensor
放入 multiprocessing.Queue
时,它都必须被移动到共享内存中。如果它已经是共享的,则为无操作,否则将产生额外的内存复制,这会减慢整个过程。即使您有一个进程池将数据发送到一个进程,也要让它发送回缓冲区——这几乎是免费的,并且可以让您在发送下一批数据时避免复制。
异步多进程训练(例如 Hogwild)#
使用 torch.multiprocessing
,可以异步训练模型,参数要么一直共享,要么定期同步。在第一种情况下,我们建议传输整个模型对象,而在后一种情况下,我们建议只传输 state_dict()
。
我们建议使用 multiprocessing.Queue
在进程之间传递所有类型的 PyTorch 对象。例如,在使用 fork
启动方法时,可以继承已经在共享内存中的张量和存储,但这非常容易出错,应谨慎使用,并且仅供高级用户使用。Queue,即使它们有时解决方案不那么优雅,但在所有情况下都可以正常工作。
警告
您应该注意不要有全局语句,这些语句没有用 if __name__ == '__main__'
进行保护。如果使用了不同于 fork
的启动方法,它们将在所有子进程中执行。
Hogwild#
Hogwild 的具体实现可以在 examples 仓库 中找到,但为了展示代码的整体结构,下面还有一个最小示例。
import torch.multiprocessing as mp
from model import MyModel
def train(model):
# Construct data_loader, optimizer, etc.
for data, labels in data_loader:
optimizer.zero_grad()
loss_fn(model(data), labels).backward()
optimizer.step() # This will update the shared parameters
if __name__ == '__main__':
num_processes = 4
model = MyModel()
# NOTE: this is required for the ``fork`` method to work
model.share_memory()
processes = []
for rank in range(num_processes):
p = mp.Process(target=train, args=(model,))
p.start()
processes.append(p)
for p in processes:
p.join()
多进程中的 CPU#
不恰当的多进程可能导致 CPU 过载,使不同的进程争夺 CPU 资源,从而导致效率低下。
本教程将解释什么是 CPU 过载以及如何避免它。
CPU 过载#
CPU 过载是一个技术术语,指的是分配给系统的 vCPU 总数超过硬件可用 vCPU 总数的情况。
这会导致 CPU 资源严重争用。在这种情况下,进程之间会频繁切换,这会增加进程切换的开销并降低整体系统效率。
在 示例仓库 中找到的 Hogwild 实现的代码示例中,可以看到 CPU 过载。
在 CPU 上使用 4 个进程运行训练示例时,命令如下:
python main.py --num-processes 4
假设机器上有 N 个可用的 vCPU,执行上述命令将生成 4 个子进程。每个子进程将为自己分配 N 个 vCPU,导致需要 4*N 个 vCPU。但是,机器上只有 N 个可用的 vCPU。因此,不同的进程将争夺资源,导致频繁的进程切换。
以下观察结果表明存在 CPU 过载:
高 CPU 利用率:使用
htop
命令,您可以观察到 CPU 利用率持续很高,通常达到或超过其最大容量。这表明对 CPU 资源的需求超过了可用的物理核心,导致进程之间争夺 CPU 时间。频繁的上下文切换导致系统效率低下:在 CPU 过载的情况下,进程会争夺 CPU 时间,操作系统需要快速在不同进程之间切换以公平地分配资源。这种频繁的上下文切换会增加开销并降低整体系统效率。
避免 CPU 过载#
避免 CPU 过载的一个好方法是进行适当的资源分配。确保并发运行的进程或线程数量不超过可用的 CPU 资源。
在这种情况下,一种解决方案是指定子进程中的适当线程数。这可以通过在子进程中使用 torch.set_num_threads(int)
函数来设置每个进程的线程数来实现。
假设机器上有 N 个 vCPU,并且将生成 M 个进程,那么每个进程使用的最大 num_threads
值将是 floor(N/M)
。为了避免 mnist_hogwild 示例中的 CPU 过载,需要对 示例仓库 中的 train.py
文件进行以下更改。
def train(rank, args, model, device, dataset, dataloader_kwargs):
torch.manual_seed(args.seed + rank)
#### define the num threads used in current sub-processes
torch.set_num_threads(floor(N/M))
train_loader = torch.utils.data.DataLoader(dataset, **dataloader_kwargs)
optimizer = optim.SGD(model.parameters(), lr=args.lr, momentum=args.momentum)
for epoch in range(1, args.epochs + 1):
train_epoch(epoch, args, model, device, train_loader, optimizer)
使用 torch.set_num_threads(floor(N/M))
为每个进程设置 num_thread
。其中 N 是可用 vCPU 的数量,M 是选择的进程数。合适的 num_thread
值将因具体任务而异。但是,作为一般指南,num_thread
的最大值应为 floor(N/M)
以避免 CPU 过载。在 mnist_hogwild 训练示例中,在避免 CPU 过载后,可以实现 30 倍的性能提升。