torchrun (弹性启动) ¶
模块 torch.distributed.run
。
torch.distributed.run
是一个模块,可以在每个训练节点上启动多个分布式训练进程。
torchrun
是一个 Python 控制台脚本,用于在 setup.py 中声明的 entry_points
配置中的 torch.distributed.run 主模块。它相当于调用 python -m torch.distributed.run
。
torchrun
可用于单节点分布式训练,其中每个节点将启动一个或多个进程。它可以用于 CPU 训练或 GPU 训练。如果用于 GPU 训练,每个分布式进程将运行在单个 GPU 上。这可以实现单节点训练性能的显著提升。 torchrun
也可以用于多节点分布式训练,通过在每个节点上启动多个进程,以实现多节点分布式训练性能的显著提升。这对于具有多个 Infiniband 接口且支持直接 GPU 的系统尤其有益,因为所有这些接口都可以用于聚合通信带宽。
在单节点分布式训练或多节点分布式训练的两种情况下, torchrun
将在每个节点上启动指定数量的进程( --nproc-per-node
)。如果用于 GPU 训练,此数字需要小于或等于当前系统上的 GPU 数量( nproc_per_node
),并且每个进程将操作从 GPU 0 到 GPU(nproc_per_node - 1)的单个 GPU。
版本 2.0.0 中的变更: torchrun
将将 --local-rank=<rank>
参数传递给您的脚本。从 PyTorch 2.0.0 版本开始,首选使用破折号 --local-rank
而不是之前使用的下划线 --local_rank
。
为了保持向后兼容性,用户可能需要在他们的参数解析代码中处理这两种情况。这意味着在参数解析器中包括 "--local-rank"
和 "--local_rank"
。如果只提供 "--local_rank"
,则 torchrun
将触发错误:“错误:未识别的参数:–local-rank=”。对于仅支持 PyTorch 2.0.0+ 的训练代码,包括 "--local-rank"
应该足够。
>>> import argparse
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument("--local-rank", "--local_rank", type=int)
>>> args = parser.parse_args()
使用说明 ¶
单节点多工作进程 ¶
torchrun
--standalone
--nnodes=1
--nproc-per-node=$NUM_TRAINERS
YOUR_TRAINING_SCRIPT.py (--arg1 ... train script args...)
注意
--nproc-per-node
可能是 "gpu"
(每个 GPU 启动一个进程), "cpu"
(每个 CPU 启动一个进程), "auto"
(如果可用 CUDA 则等同于 "gpu"
,否则等同于 "cpu"
),或者指定进程数量的整数。有关详细信息,请参阅 torch.distributed.run.determine_local_world_size。
堆叠的单节点多工作线程
要在同一主机上运行多个实例(独立作业)的单节点多工作线程,我们需要确保每个实例(作业)在不同的端口上设置,以避免端口冲突(或者更糟,两个作业合并为一个作业)。为此,您需要使用 --rdzv-backend=c10d
并设置不同的端口 --rdzv-endpoint=localhost:$PORT_k
。对于 --nodes=1
,通常方便让 torchrun
自动选择一个空闲的随机端口,而不是手动为每次运行分配不同的端口。
torchrun
--rdzv-backend=c10d
--rdzv-endpoint=localhost:0
--nnodes=1
--nproc-per-node=$NUM_TRAINERS
YOUR_TRAINING_SCRIPT.py (--arg1 ... train script args...)
容错(固定大小的工作者数量,无弹性,容忍 3 次故障)
torchrun
--nnodes=$NUM_NODES
--nproc-per-node=$NUM_TRAINERS
--max-restarts=3
--rdzv-id=$JOB_ID
--rdzv-backend=c10d
--rdzv-endpoint=$HOST_NODE_ADDR
YOUR_TRAINING_SCRIPT.py (--arg1 ... train script args...)
HOST_NODE_ADDR
,格式为 [:](例如 node1.example.com:29400),指定 C10d rendezvous 后端应该实例化和托管在哪个节点和端口上。它可以是您的训练集群中的任何节点,但理想情况下,您应该选择一个具有高带宽的节点。
注意
如果未指定端口号,则默认为 29400。
弹性( min=1
, max=4
,容忍最多 3 次成员变更或失败)¶
torchrun
--nnodes=1:4
--nproc-per-node=$NUM_TRAINERS
--max-restarts=3
--rdzv-id=$JOB_ID
--rdzv-backend=c10d
--rdzv-endpoint=$HOST_NODE_ADDR
YOUR_TRAINING_SCRIPT.py (--arg1 ... train script args...)
HOST_NODE_ADDR
,格式为 [:](例如 node1.example.com:29400),指定 C10d rendezvous 后端应该实例化和托管在哪个节点和端口上。它可以是您的训练集群中的任何节点,但理想情况下,您应该选择一个具有高带宽的节点。
注意
如果未指定端口号,则默认为 29400。
关于 rendezvous 后端说明 ¶
对于多节点训练,您需要指定:
--rdzv-id
:一个唯一的作业 ID(由所有参与作业的节点共享)--rdzv-backend
: 一种torch.distributed.elastic.rendezvous.RendezvousHandler
的实现--rdzv-endpoint
: 遇见后端运行端点;通常形式为host:port
。
目前默认支持 c10d
(推荐)、 etcd-v2
和 etcd
(旧版)遇见后端。要使用 etcd-v2
或 etcd
,请设置一个启用了 v2
api 的 etcd 服务器(例如 --enable-v2
)。
警告
etcd-v2
和 etcd
遇见使用 etcd API v2。您必须启用 etcd 服务器上的 v2 API。我们的测试使用 etcd v3.4.3。
警告
对于基于 etcd 的会合,我们推荐使用 etcd-v2
而不是 etcd
,它们在功能上是等效的,但使用了改进的实现。 etcd
处于维护模式,将在未来的版本中删除。
定义
Node
- 一个物理实例或容器;映射到作业管理器工作的单元。Worker
- 分布式训练上下文中的工作者。执行相同函数的工人集合(例如,训练师)。
在同一节点上运行的工人组中的工人子集。
工人组内工人的排名。
工人组中工人的总数。
LOCAL_RANK
- 工作组内工人的排名。LOCAL_WORLD_SIZE
- 本地工作组的规模。rdzv_id
- 用户定义的唯一标识符,用于标识作业的工人组。该 ID 由每个节点使用,以加入特定的工人组。
rdzv_backend
- 集合点的后端(例如c10d
)。这通常是一个强一致性的键值存储。预约后端端点;通常形式为。
一个 Node
运行 LOCAL_WORLD_SIZE
个工作进程,这些工作进程构成了一个 LocalWorkerGroup
。作业中所有节点上的 LocalWorkerGroups
的并集构成了 WorkerGroup
。
环境变量 §
以下环境变量在您的脚本中可用:
LOCAL_RANK
- 本地排名。RANK
- 全局排名。GROUP_RANK
- 工作组排名。一个介于 0 到max_nnodes
之间的数字。当每个节点运行一个工作组时,这是节点的排名。ROLE_RANK
- 同一角色所有工作者中的工作者排名。工作者的角色在WorkerSpec
中指定。LOCAL_WORLD_SIZE
- 本地世界大小(例如本地运行的 worker 数量);等于在torchrun
中指定的--nproc-per-node
。WORLD_SIZE
- 世界大小(作业中所有 worker 的总数)。ROLE_WORLD_SIZE
- 与WorkerSpec
中指定的相同角色一起启动的总 worker 数量。MASTER_ADDR
- 运行 rank 0 worker 的主机的 FQDN;用于初始化 Torch 分布式后端。MASTER_PORT
- 可以用于托管 C10d TCP 存储的端口号。TORCHELASTIC_RESTART_COUNT
- 到目前为止的 worker 组重启次数。TORCHELASTIC_MAX_RESTARTS
- 配置的最大重启次数。TORCHELASTIC_RUN_ID
- 等于 rendezvousrun_id
(例如,唯一的作业 ID)。系统可执行文件覆盖。如果提供,Python 用户脚本将使用
PYTHON_EXEC
的值作为可执行文件。默认使用 sys.executable。
部署
对于 C10d 后端不需要。启动 rendezvous 后端服务器并获取端点(将作为
--rdzv-endpoint
传递给torchrun
)单节点多工作进程:在主机上启动
torchrun
以启动创建和监控本地工作组的代理进程。多节点多工作进程:在所有参与训练的节点上使用相同的参数启动
torchrun
。
使用作业/集群管理器时,多节点作业的入口命令应为 torchrun
。
失败模式
工作节点故障:对于有
n
个工作节点的训练作业,如果k<=n
个工作节点故障,则所有工作节点将停止并重新启动,最多重试max_restarts
次。代理故障:代理故障会导致本地工作节点组故障。是否使整个作业(群组语义)失败或尝试替换节点由作业管理器决定。这两种行为都由代理支持。
节点故障:与代理故障相同。
成员变更 _
节点离开(缩放):代理被通知离开,所有现有工作者停止,形成新的
WorkerGroup
,所有工作者以新的RANK
和WORLD_SIZE
启动。节点到达(扩容):新节点被纳入作业,所有现有工作者停止,形成新的
WorkerGroup
,所有工作者以新的RANK
和WORLD_SIZE
启动。
重要通知
此实用程序和多进程分布式(单节点或多节点)GPU 训练目前仅通过 NCCL 分布式后端实现最佳性能。因此,NCCL 后端是推荐用于 GPU 训练的后端。
初始化 Torch 进程组所需的环境变量由本模块提供,无需您手动传递
RANK
。要在您的训练脚本中初始化进程组,只需运行:
>>> import torch.distributed as dist
>>> dist.init_process_group(backend="gloo|nccl")
在您的训练程序中,您可以使用常规分布式函数或使用
torch.nn.parallel.DistributedDataParallel()
模块。如果您的训练程序使用 GPU 进行训练并希望使用torch.nn.parallel.DistributedDataParallel()
模块,以下是配置方法。
local_rank = int(os.environ["LOCAL_RANK"])
model = torch.nn.parallel.DistributedDataParallel(
model, device_ids=[local_rank], output_device=local_rank
)
请确保将 device_ids
参数设置为您的代码将操作的唯一 GPU 设备 ID。这通常是进程的本地 rank。换句话说, device_ids
需要是 [int(os.environ("LOCAL_RANK"))]
, output_device
需要是 int(os.environ("LOCAL_RANK"))
,以便使用此实用程序。
在失败或成员变更时,所有存活的工人将立即被杀死。请确保检查点进度。检查点的频率应取决于您的工作对丢失工作的容忍度。
此模块仅支持同构的
LOCAL_WORLD_SIZE
。也就是说,假设所有节点运行相同数量的本地工人(每个角色)。RANK
不稳定。在重启之间,节点上的本地工人可以分配与之前不同的范围等级。绝不要硬编码关于等级稳定性或RANK
与LOCAL_RANK
之间相关性的任何假设。当使用弹性(
min_size!=max_size
)时,绝不要硬编码关于WORLD_SIZE
的假设,因为世界大小可以随着节点允许离开和加入而改变。建议您的脚本具有以下结构:
def main():
load_checkpoint(checkpoint_path)
initialize()
train()
def train():
for batch in iter(dataset):
train_step(batch)
if should_checkpoint:
save_checkpoint(checkpoint_path)
(推荐) 在工作错误时,此工具将总结错误的详细信息(例如时间、排名、主机、pid、跟踪回溯等)。在每个节点上,第一个错误(按时间戳)被启发式地报告为“根本原因”错误。要获取作为此错误总结打印输出的一部分的跟踪回溯,您必须在下面的示例中装饰您的训练脚本中的主入口点函数。如果不装饰,则总结将不包含异常的跟踪回溯,而只包含退出代码。有关 torchelastic 错误处理的详细信息,请参阅:https://pytorch.org/docs/stable/elastic/errors.html
from torch.distributed.elastic.multiprocessing.errors import record
@record
def main():
# do train
pass
if __name__ == "__main__":
main()