Accelerate 文档
从 Jupyter Notebook 启动分布式训练
并获得增强的文档体验
开始使用
从 Jupyter Notebook 启动分布式训练
本教程教你如何使用 🤗 Accelerate 从 Jupyter Notebook 在分布式系统上微调一个计算机视觉模型。你还将学习如何设置一些必要的前置条件,以确保你的环境配置正确、数据准备妥当,并最终启动训练。
本教程也提供 Jupyter Notebook 版本,请点击此处查看。
配置环境
在进行任何训练之前,系统中必须存在一个 Accelerate 配置文件。通常,这可以通过在终端中运行以下命令并回答提示来完成:
accelerate config
然而,如果通用默认设置可以接受,并且你*没有*在 TPU 上运行,Accelerate 提供了一个实用工具,可以通过 utils.write_basic_config() 快速将你的 GPU 配置写入配置文件。
以下代码将在写入配置后重启 Jupyter,因为执行此操作调用了 CUDA 代码。
在多 GPU 系统上,CUDA 不能被初始化超过一次。在 notebook 中调试并调用 CUDA 是可以的,但为了最终进行训练,需要执行完整的清理和重启。
import os
from accelerate.utils import write_basic_config
write_basic_config() # Write a config file
os._exit(00) # Restart the notebook
准备数据集和模型
接下来,你应该准备你的数据集。如前所述,在准备 DataLoaders
和模型时要特别小心,确保任何东西都不要放在*任何* GPU 上。
如果你确实需要这样做,建议将那部分特定代码放入一个函数中,并从 notebook 启动器界面中调用它,稍后会展示如何操作。
请确保根据此处的说明下载数据集。
import os, re, torch, PIL
import numpy as np
from torch.optim.lr_scheduler import OneCycleLR
from torch.utils.data import DataLoader, Dataset
from torchvision.transforms import Compose, RandomResizedCrop, Resize, ToTensor
from accelerate import Accelerator
from accelerate.utils import set_seed
from timm import create_model
首先,你需要创建一个函数,根据文件名提取类别名称。
import os
data_dir = "../../images"
fnames = os.listdir(data_dir)
fname = fnames[0]
print(fname)
beagle_32.jpg
在这里,标签是 beagle
。你可以使用正则表达式从文件名中提取标签。
import re
def extract_label(fname):
stem = fname.split(os.path.sep)[-1]
return re.search(r"^(.*)_\d+\.jpg$", stem).groups()[0]
extract_label(fname)
你可以看到它为我们的文件正确返回了正确的名称。
"beagle"
接下来,应该创建一个 Dataset
类来处理图像和标签的获取。
class PetsDataset(Dataset):
def __init__(self, file_names, image_transform=None, label_to_id=None):
self.file_names = file_names
self.image_transform = image_transform
self.label_to_id = label_to_id
def __len__(self):
return len(self.file_names)
def __getitem__(self, idx):
fname = self.file_names[idx]
raw_image = PIL.Image.open(fname)
image = raw_image.convert("RGB")
if self.image_transform is not None:
image = self.image_transform(image)
label = extract_label(fname)
if self.label_to_id is not None:
label = self.label_to_id[label]
return {"image": image, "label": label}
现在来构建数据集。在训练函数之外,你可以查找并声明所有的文件名和标签,并在启动的函数内部将它们作为引用使用。
fnames = [os.path.join("../../images", fname) for fname in fnames if fname.endswith(".jpg")]
接下来,收集所有的标签。
all_labels = [extract_label(fname) for fname in fnames]
id_to_label = list(set(all_labels))
id_to_label.sort()
label_to_id = {lbl: i for i, lbl in enumerate(id_to_label)}
接下来,你应该创建一个 get_dataloaders
函数,它会为你返回构建好的 dataloader。如前所述,如果在构建 DataLoaders
时数据会自动发送到 GPU 或 TPU 设备,那么它们必须使用这种方法来构建。
def get_dataloaders(batch_size: int = 64):
"Builds a set of dataloaders with a batch_size"
random_perm = np.random.permutation(len(fnames))
cut = int(0.8 * len(fnames))
train_split = random_perm[:cut]
eval_split = random_perm[cut:]
# For training a simple RandomResizedCrop will be used
train_tfm = Compose([RandomResizedCrop((224, 224), scale=(0.5, 1.0)), ToTensor()])
train_dataset = PetsDataset([fnames[i] for i in train_split], image_transform=train_tfm, label_to_id=label_to_id)
# For evaluation a deterministic Resize will be used
eval_tfm = Compose([Resize((224, 224)), ToTensor()])
eval_dataset = PetsDataset([fnames[i] for i in eval_split], image_transform=eval_tfm, label_to_id=label_to_id)
# Instantiate dataloaders
train_dataloader = DataLoader(train_dataset, shuffle=True, batch_size=batch_size, num_workers=4)
eval_dataloader = DataLoader(eval_dataset, shuffle=False, batch_size=batch_size * 2, num_workers=4)
return train_dataloader, eval_dataloader
最后,你应该导入稍后会用到的调度器。
from torch.optim.lr_scheduler import CosineAnnealingLR
编写训练函数
现在你可以构建训练循环了。notebook_launcher() 的工作方式是传入一个要调用的函数,该函数将在分布式系统上运行。
这是一个针对动物分类问题的基本训练循环。
代码被分成了几个部分,以便对每个部分进行解释。一个可以复制粘贴的完整版本将在最后提供。
def training_loop(mixed_precision="fp16", seed: int = 42, batch_size: int = 64):
set_seed(seed)
accelerator = Accelerator(mixed_precision=mixed_precision)
首先,你应该在训练循环中尽早设置种子并创建一个 Accelerator 对象。
如果在 TPU 上训练,你的训练循环应该将模型作为参数传入,并且模型应该在训练循环函数之外实例化。请参阅 TPU 最佳实践 了解原因。
接下来,你应该构建你的 dataloader 并创建你的模型。
train_dataloader, eval_dataloader = get_dataloaders(batch_size)
model = create_model("resnet50d", pretrained=True, num_classes=len(label_to_id))
你在这里构建模型,这样种子也可以控制新的权重初始化。
由于本例中你正在进行迁移学习,模型的编码器部分开始时是冻结的,以便首先只训练模型的头部。
for param in model.parameters():
param.requires_grad = False
for param in model.get_classifier().parameters():
param.requires_grad = True
对图像批次进行归一化将使训练速度稍快一些。
mean = torch.tensor(model.default_cfg["mean"])[None, :, None, None]
std = torch.tensor(model.default_cfg["std"])[None, :, None, None]
为了使这些常量在活动设备上可用,你应该将其设置为 Accelerator 的设备。
mean = mean.to(accelerator.device) std = std.to(accelerator.device)
接下来,实例化用于训练的其余 PyTorch 类。
optimizer = torch.optim.Adam(params=model.parameters(), lr=3e-2 / 25)
lr_scheduler = OneCycleLR(optimizer=optimizer, max_lr=3e-2, epochs=5, steps_per_epoch=len(train_dataloader))
在将所有东西传递给 prepare() 之前。
没有特定的顺序需要记住,你只需要按照你传递给 prepare 方法的相同顺序解包这些对象即可。
model, optimizer, train_dataloader, eval_dataloader, lr_scheduler = accelerator.prepare( model, optimizer, train_dataloader, eval_dataloader, lr_scheduler )
现在来训练模型。
for epoch in range(5):
model.train()
for batch in train_dataloader:
inputs = (batch["image"] - mean) / std
outputs = model(inputs)
loss = torch.nn.functional.cross_entropy(outputs, batch["label"])
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
评估循环与训练循环相比会稍有不同。传递的元素数量以及每个批次的整体总准确率将被加到两个常量中。
model.eval()
accurate = 0
num_elems = 0
接下来是标准的 PyTorch 循环的其余部分。
for batch in eval_dataloader:
inputs = (batch["image"] - mean) / std
with torch.no_grad():
outputs = model(inputs)
predictions = outputs.argmax(dim=-1)
最后是最后一个主要的不同之处。
在进行分布式评估时,预测和标签需要通过 gather() 传递,这样所有数据都可以在当前设备上可用,从而可以实现正确计算的指标。
accurate_preds = accelerator.gather(predictions) == accelerator.gather(batch["label"])
num_elems += accurate_preds.shape[0]
accurate += accurate_preds.long().sum()
现在你只需要计算这个问题的实际指标,然后可以使用 print() 在主进程上打印它。
eval_metric = accurate.item() / num_elems
accelerator.print(f"epoch {epoch}: {100 * eval_metric:.2f}")
此训练循环的完整版本如下所示。
def training_loop(mixed_precision="fp16", seed: int = 42, batch_size: int = 64):
set_seed(seed)
# Initialize accelerator
accelerator = Accelerator(mixed_precision=mixed_precision)
# Build dataloaders
train_dataloader, eval_dataloader = get_dataloaders(batch_size)
# Instantiate the model (you build the model here so that the seed also controls new weight initializations)
model = create_model("resnet50d", pretrained=True, num_classes=len(label_to_id))
# Freeze the base model
for param in model.parameters():
param.requires_grad = False
for param in model.get_classifier().parameters():
param.requires_grad = True
# You can normalize the batches of images to be a bit faster
mean = torch.tensor(model.default_cfg["mean"])[None, :, None, None]
std = torch.tensor(model.default_cfg["std"])[None, :, None, None]
# To make these constants available on the active device, set it to the accelerator device
mean = mean.to(accelerator.device)
std = std.to(accelerator.device)
# Instantiate the optimizer
optimizer = torch.optim.Adam(params=model.parameters(), lr=3e-2 / 25)
# Instantiate the learning rate scheduler
lr_scheduler = OneCycleLR(optimizer=optimizer, max_lr=3e-2, epochs=5, steps_per_epoch=len(train_dataloader))
# Prepare everything
# There is no specific order to remember, you just need to unpack the objects in the same order you gave them to the
# prepare method.
model, optimizer, train_dataloader, eval_dataloader, lr_scheduler = accelerator.prepare(
model, optimizer, train_dataloader, eval_dataloader, lr_scheduler
)
# Now you train the model
for epoch in range(5):
model.train()
for batch in train_dataloader:
inputs = (batch["image"] - mean) / std
outputs = model(inputs)
loss = torch.nn.functional.cross_entropy(outputs, batch["label"])
accelerator.backward(loss)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
model.eval()
accurate = 0
num_elems = 0
for batch in eval_dataloader:
inputs = (batch["image"] - mean) / std
with torch.no_grad():
outputs = model(inputs)
predictions = outputs.argmax(dim=-1)
accurate_preds = accelerator.gather(predictions) == accelerator.gather(batch["label"])
num_elems += accurate_preds.shape[0]
accurate += accurate_preds.long().sum()
eval_metric = accurate.item() / num_elems
# Use accelerator.print to print only on the main process.
accelerator.print(f"epoch {epoch}: {100 * eval_metric:.2f}")
使用 notebook_launcher
剩下的就是使用 notebook_launcher() 了。
你需要传入函数、参数(作为元组)以及用于训练的进程数。(更多信息请参阅文档)
from accelerate import notebook_launcher
args = ("fp16", 42, 64)
notebook_launcher(training_loop, args, num_processes=2)
在多节点上运行时,你需要在每个节点上设置一个 Jupyter 会话,并同时运行启动单元格。
对于一个包含 2 个节点(计算机),每个节点有 8 个 GPU,主计算机的 IP 地址为“172.31.43.8”的环境,它会是这样:
notebook_launcher(training_loop, args, master_addr="172.31.43.8", node_rank=0, num_nodes=2, num_processes=8)
在另一台机器的第二个 Jupyter 会话中:
注意 node_rank
是如何变化的。
notebook_launcher(training_loop, args, master_addr="172.31.43.8", node_rank=1, num_nodes=2, num_processes=8)
在 TPU 上运行时,它会是这样:
model = create_model("resnet50d", pretrained=True, num_classes=len(label_to_id))
args = (model, "fp16", 42, 64)
notebook_launcher(training_loop, args, num_processes=8)
要以弹性方式启动训练过程,以实现容错,你可以使用 PyTorch 提供的 elastic_launch
功能。这需要设置额外的参数,例如 rdzv_backend
和 max_restarts
。以下是如何使用具有弹性功能的 notebook_launcher
的示例:
notebook_launcher(
training_loop,
args,
num_processes=2,
max_restarts=3
)
在运行时,它会打印进度并说明你运行的设备数量。本教程是在两个 GPU 上运行的。
Launching training on 2 GPUs.
epoch 0: 88.12
epoch 1: 91.73
epoch 2: 92.58
epoch 3: 93.90
epoch 4: 94.71
就这样!
请注意,notebook_launcher() 会忽略 Accelerate 配置文件,若要根据配置启动,请使用:
accelerate launch
调试
运行 `notebook_launcher` 时的一个常见问题是收到 CUDA 已经初始化的错误。这通常源于 notebook 中的导入或之前的代码调用了 PyTorch 的 `torch.cuda` 子库。为了帮助缩小问题范围,你可以在环境中设置 `ACCELERATE_DEBUG_MODE=yes` 来启动 `notebook_launcher`,在生成进程时会进行额外检查,确保常规进程可以无误地创建并利用 CUDA。(你的 CUDA 代码之后仍然可以运行)。
结论
这个 notebook 展示了如何从 Jupyter Notebook 内部进行分布式训练。以下是一些需要记住的关键点:
- 确保将任何使用 CUDA(或 CUDA 导入)的代码保留在传递给 notebook_launcher() 的函数中。
- 将
num_processes
设置为用于训练的设备数量(例如 GPU、CPU、TPU 的数量等)。 - 如果使用 TPU,请在训练循环函数之外声明你的模型。