分布式训练 DLC 快速入门

DLC可以快捷地创建分布式或单机训练任务。其底层基于Kubernetes,省去您手动购买机器并配置运行环境,无需改变使用习惯即可快速使用。本文以 MNIST 手写体识别为例,介绍如何使用DLC进行单机单卡训练,或多机多卡的分布式训练。

说明

MNIST手写体识别是深度学习最经典的入门任务之一,任务目标是通过构建机器学习模型,来识别10个手写数字(0~9)。

image

前提条件

使用主账号开通PAI并创建工作空间。登录PAI控制台,左上角选择开通区域,然后一键授权和开通产品。

计费说明

本文案例将使用公共资源创建DLC任务,计费方式为按量付费,详细计费规则请参见分布式训练(DLC)计费说明

单机单卡训练

创建数据集

数据集用于存储模型训练的代码、数据、以及训练结果。本文以对象存储OSS类型数据集为例进行说明。

  1. PAI控制台左侧菜单栏单击数据集 > 自定义数据集 > 新建数据集

    image

  2. 配置数据集参数。关键参数配置如下,其他参数默认即可。

    • 名称:如:dataset_mnist

    • 存储类型对象存储(OSS)

    • OSS路径:单击图标 image,选择Bucket并新建目录如:dlc_mnist

      如果您尚未开通OSS,或在当前地域下没有可选的Bucket,可参考如下步骤开通OSS,并新建Bucket:

      (可选)开通OSS,并新建Bucket

      1. 开通OSS服务

      2. 登录OSS管理控制台单击创建Bucket,填写Bucket名称地域选择与当前PAI相同的地域,其他参数默认即可,然后单击完成创建

        image

    单击确定创建数据集。

  3. 上传训练代码和数据。

    1. 下载代码。本文已经为您准备好了训练代码,单击mnist_train.py下载。为减少您的操作,代码运行时会自动将训练数据下载到数据集的dataSet目录中。

      您在后续实际业务使用时,可以预先把代码和训练数据上传到PAI的数据集中。

      单机单卡训练代码示例 mnist_train.py

      import torch
      import torch.nn as nn
      import torch.nn.functional as F
      import torch.optim as optim
      from torch.utils.data import DataLoader
      from torchvision import datasets, transforms
      from torch.utils.tensorboard import SummaryWriter
      
      # 超参数
      batch_size = 64  # 每次训练的数据量
      learning_rate = 0.01  # 学习率
      num_epochs = 20  # 训练轮次
      
      # 检查是否有可用的 GPU
      device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
      
      # 数据预处理
      transform = transforms.Compose([
          transforms.ToTensor(),
          transforms.Normalize((0.5,), (0.5,))
      ])
      
      train_dataset = datasets.MNIST(root='/mnt/data/dataSet', train=True, download=True, transform=transform)
      val_dataset = datasets.MNIST(root='/mnt/data/dataSet', train=False, download=False, transform=transform)
      
      train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
      val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
      
      
      # 定义简单的神经网络
      class SimpleCNN(nn.Module):
          def __init__(self):
              super(SimpleCNN, self).__init__()
              # 第一层卷积:输入通道1(灰度图像),输出通道10,卷积核5x5
              self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
              # 第二层卷积:输入通道10,输出通道20,卷积核3x3
              self.conv2 = nn.Conv2d(10, 20, kernel_size=3)
              # 全连接层:输入为20*5*5(卷积+池化后的特征图尺寸),输出128
              self.fc1 = nn.Linear(20 * 5 * 5, 128)
              # 输出层:128 -> 10(对应10个数字类别)
              self.fc2 = nn.Linear(128, 10)
      
          def forward(self, x):
              # 输入x形状: [batch, 1, 28, 28]
              x = F.max_pool2d(F.relu(self.conv1(x)), 2)  # [batch, 10, 12, 12]
              x = F.max_pool2d(F.relu(self.conv2(x)), 2)  # [batch, 20, 5, 5]
              x = x.view(-1, 20 * 5 * 5)  # 展平为[batch, 500]
              x = F.relu(self.fc1(x))      # [batch, 128]
              x = self.fc2(x)              # [batch, 10]
              return x
      
      
      # 实例化模型,并将其移动到 GPU 上(如果可用)
      model = SimpleCNN().to(device)
      criterion = nn.CrossEntropyLoss()
      optimizer = optim.SGD(model.parameters(), lr=learning_rate)
      
      # 创建 TensorBoard 的 SummaryWriter,可用于可视化的查看模型训练过程
      writer = SummaryWriter('/mnt/data/output/runs/mnist_experiment')
      
      # 用于保存最高准确率的模型的变量
      best_val_accuracy = 0.0
      
      # 训练模型并记录损失和准确率
      for epoch in range(num_epochs):
          model.train()
          for batch_idx, (data, target) in enumerate(train_loader):
              data, target = data.to(device), target.to(device)  # 将数据和目标移动到 GPU
      
              # 清零梯度
              optimizer.zero_grad()
              # 前向传播
              output = model(data)
              # 计算损失
              loss = criterion(output, target)
              # 反向传播
              loss.backward()
              # 更新参数
              optimizer.step()
      
              # 记录训练损失到 TensorBoard
              if batch_idx % 100 == 0:  # 每 100 个批次记录一次
                  writer.add_scalar('Loss/train', loss.item(), epoch * len(train_loader) + batch_idx)
                  print(f'Train Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)} ({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}')
      
          # 验证模型并记录验证损失和准确率
          model.eval()
          val_loss = 0
          correct = 0
          with torch.no_grad():  # 不计算梯度
              for data, target in val_loader:
                  data, target = data.to(device), target.to(device)  # 将数据和目标移动到 GPU
                  output = model(data)
                  val_loss += criterion(output, target).item()  # 累加验证损失
                  pred = output.argmax(dim=1, keepdim=True)  # 获取预测标签
                  correct += pred.eq(target.view_as(pred)).sum().item()  # 累加正确预测的数量
      
          val_loss /= len(val_loader)  # 计算平均验证损失
          val_accuracy = 100. * correct / len(val_loader.dataset)  # 计算验证准确率
          print(f'Validation Loss: {val_loss:.4f}, Accuracy: {correct}/{len(val_loader.dataset)} ({val_accuracy:.0f}%)')
      
          # 记录验证损失和准确率到 TensorBoard
          writer.add_scalar('Loss/validation', val_loss, epoch)
          writer.add_scalar('Accuracy/validation', val_accuracy, epoch)
      
          # 保存验证准确率最高的模型
          if val_accuracy > best_val_accuracy:
              best_val_accuracy = val_accuracy
              torch.save(model.state_dict(), '/mnt/data/output/best_model.pth')
              print(f'Model saved with accuracy: {best_val_accuracy:.2f}%')
      
      # 关闭 SummaryWriter
      writer.close()
      print('Training complete. writer.close()')
    2. 上传代码。在数据集详情页,单击查看数据跳转至OSS控制台。然后单击上传文件 > 扫描文件 > 上传文件,将训练代码上传至OSS中。

      image

创建DLC任务

  1. PAI控制台左侧菜单栏单击分布式训练DLC > 新建任务

    image

  2. 配置DLC任务参数。关键参数配置如下,其他参数默认即可。全量参数请参见创建训练任务

    • 节点镜像:选择镜像地址,然后根据您所在地域填写对应镜像地址。

      image

      地域

      对应镜像地址

      北京

      dsw-registry-vpc.cn-beijing.cr.aliyuncs.com/pai/modelscope:1.28.0-pytorch2.3.1tensorflow2.16.1-gpu-py311-cu121-ubuntu22.04

      上海

      dsw-registry-vpc.cn-shanghai.cr.aliyuncs.com/pai/modelscope:1.28.0-pytorch2.3.1tensorflow2.16.1-gpu-py311-cu121-ubuntu22.04

      杭州

      dsw-registry-vpc.cn-hangzhou.cr.aliyuncs.com/pai/modelscope:1.28.0-pytorch2.3.1tensorflow2.16.1-gpu-py311-cu121-ubuntu22.04

      其他

      查询地域ID,并替换镜像地址中的<地域ID>获取完整链接:

      dsw-registry-vpc.<地域ID>.cr.aliyuncs.com/pai/modelscope:1.28.0-pytorch2.3.1tensorflow2.16.1-gpu-py311-cu121-ubuntu22.04

      该镜像已在交互式建模 DSW 快速入门中验证没有环境问题。使用PAI建模时,通常先在DSW中验证环境、开发代码,然后再使用DLC训练。
    • 数据集:选择自定义数据集,选择上一步中创建的数据集。挂载路径默认/mnt/data

    • 启动命令python /mnt/data/mnist_train.py

      该启动命令与在DSW或本地运行时相同。但由于mnist_train.py 现已挂载至 /mnt/data/,因此仅需要修改代码的路径为/mnt/data/mnist_train.py。 
    • 资源来源:选择公共资源资源规格选择ecs.gn7i-c8g1.2xlarge即可。

      如果该规格实例库存不足,您也可以选择其他GPU实例。

    单击确定创建任务,任务大约需要执行15分钟。执行过程中可以单击日志查看训练过程。

    image

    执行完成后,会在挂载数据集的output路径下输出最佳的模型检查点,以及Tensorboard日志。

    image

(可选)查看Tensorboard

您可以借助可视化工具Tensorboard查看loss曲线,了解训练的具体情况。

重要

DLC任务如果想使用Tensorboard,必须配置数据集。

  1. 单击DLC任务详情页上方的Tensorboard > 新建Tensorboard

    image

  2. 挂载类型选择按任务,在Summary目录处填写训练代码中Summary存储的路径:/mnt/data/output/runs/,单击确定启动。

    对应代码片段:writer = SummaryWriter('/mnt/data/output/runs/mnist_experiment')
  3. 单击查看Tensorboard查看train_loss曲线(反映训练集损失)与 validation_loss曲线(反映验证集损失)。

    image

    (可选)根据loss图像,调整超参数,提升模型效果

    您可以根据损失值的变化趋势,初步判断当前模型的训练效果:

    • 在结束训练前 train_loss 与 validation_loss 仍有下降趋势(欠拟合)

      您可以增加 num_epochs(训练轮次,与训练深度正相关),或适当增大 learning_rate 后再进行训练,加大模型的对训练数据的拟合程度;

    • 在结束训练前 train_loss 持续下降,validation_loss 开始变大(过拟合)

      您可以减少 num_epochs,或适当减小 learning_rate 后再进行训练,防止模型过度训练;

    • 在结束训练前 train_loss 与 validation_loss 均处于平稳状态(良好拟合)

      模型处于该状态时,您可以进行后续步骤。

    受限于篇幅,本文无法对微调参数做过多讲解。您可以学习阿里云大模型 ACP 课程 来了解微调命令中的关键参数、以及如何通过损失曲线来决定是否应该继续微调等细节。

部署训练后的模型

详情请参见使用EAS将模型部署为在线服务

单机多卡或多机多卡分布式训练

当单个GPU的显存无法满足训练需求,或者想要加快训练速度时,您可以创建单机多卡或多机多卡的分布式训练任务。

本文以使用2台各有1GPU的实例为例进行说明,该示例同样适用于其他配置的单机多卡或多机多卡训练。

创建数据集

如果您在单机单卡训练时已经创建了数据集,只需单击下载代码mnist_train_distributed.py并上传。否则请先创建数据集,再上传该代码。

单机多卡或多机多卡训练代码示例 mnist_train_distributed.py

import os
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
from torch.utils.data.distributed import DistributedSampler
from torchvision import datasets, transforms
from torch.utils.tensorboard import SummaryWriter

class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=3)
        self.fc1 = nn.Linear(20 * 5 * 5, 128)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = F.max_pool2d(F.relu(self.conv1(x)), 2)
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, 20 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x

def main():
    rank = int(os.environ["RANK"])
    world_size = int(os.environ["WORLD_SIZE"])
    local_rank = int(os.environ["LOCAL_RANK"])
    dist.init_process_group(backend='nccl')
    torch.cuda.set_device(local_rank)
    device = torch.device('cuda', local_rank)

    batch_size = 64
    learning_rate = 0.01
    num_epochs = 20

    transform = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize((0.5,), (0.5,))
    ])

    # 只有主进程(rank=0)需要下载,其他进程需要等待它完成
    # 让 rank!=0 的进程先在屏障处等待
    if rank != 0:
        dist.barrier()

    # 所有进程都执行数据集的创建
    # 但只有 rank=0 的进程会实际执行下载
    train_dataset = datasets.MNIST(root='/mnt/data/dataSet', train=True, download=(rank == 0), transform=transform)

    # rank=0 进程下载完成后,也到达屏障处,从而释放所有进程
    if rank == 0:
        dist.barrier()

    # 此处,所有进程都已同步,可以继续执行后续代码
    val_dataset = datasets.MNIST(root='/mnt/data/dataSet', train=False, download=False, transform=transform)

    train_sampler = DistributedSampler(train_dataset, num_replicas=world_size, rank=rank, shuffle=True)
    train_loader = DataLoader(train_dataset, batch_size=batch_size, sampler=train_sampler, num_workers=4, pin_memory=True)
    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4, pin_memory=True)

    model = SimpleCNN().to(device)
    model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank])
    criterion = nn.CrossEntropyLoss().to(device)
    optimizer = optim.SGD(model.parameters(), lr=learning_rate)

    if rank == 0:
        writer = SummaryWriter('/mnt/data/output_distributed/runs/mnist_experiment')
    best_val_accuracy = 0.0

    for epoch in range(num_epochs):
        train_sampler.set_epoch(epoch)
        model.train()
        for batch_idx, (data, target) in enumerate(train_loader):
            data, target = data.to(device, non_blocking=True), target.to(device, non_blocking=True)
            optimizer.zero_grad()
            output = model(data)
            loss = criterion(output, target)
            loss.backward()
            optimizer.step()
            if batch_idx % 100 == 0:
                # 每个 rank 和 local_rank 都打印自己的 loss
                print(f"Rank: {rank}, Local_Rank: {local_rank} -- Train Epoch: {epoch} "
                      f"[{batch_idx * len(data) * world_size}/{len(train_loader.dataset)} "
                      f"({100. * batch_idx / len(train_loader):.0f}%)]\tLoss: {loss.item():.6f}")

                if rank == 0:
                    writer.add_scalar('Loss/train', loss.item(), epoch * len(train_loader) + batch_idx)

        # 验证
        model.eval()
        val_loss = 0
        correct = 0
        total = 0
        with torch.no_grad():
            for data, target in val_loader:
                data, target = data.to(device, non_blocking=True), target.to(device, non_blocking=True)
                output = model(data)
                val_loss += criterion(output, target).item() * data.size(0)
                pred = output.argmax(dim=1, keepdim=True)
                correct += pred.eq(target.view_as(pred)).sum().item()
                total += target.size(0)
        val_loss_tensor = torch.tensor([val_loss], dtype=torch.float32, device=device)
        correct_tensor = torch.tensor([correct], dtype=torch.float32, device=device)
        total_tensor = torch.tensor([total], dtype=torch.float32, device=device)
        dist.all_reduce(val_loss_tensor, op=dist.ReduceOp.SUM)
        dist.all_reduce(correct_tensor, op=dist.ReduceOp.SUM)
        dist.all_reduce(total_tensor, op=dist.ReduceOp.SUM)

        val_loss = val_loss_tensor.item() / total_tensor.item()
        val_accuracy = 100. * correct_tensor.item() / total_tensor.item()

        if rank == 0:
            print(f'Validation Loss: {val_loss:.4f}, Accuracy: {int(correct_tensor.item())}/{int(total_tensor.item())} ({val_accuracy:.0f}%)')
            writer.add_scalar('Loss/validation', val_loss, epoch)
            writer.add_scalar('Accuracy/validation', val_accuracy, epoch)
            if val_accuracy > best_val_accuracy:
                best_val_accuracy = val_accuracy
                torch.save(model.module.state_dict(), '/mnt/data/output_distributed/best_model.pth')
                print(f'Model saved with accuracy: {best_val_accuracy:.2f}%')
    if rank == 0:
        writer.close()
    dist.destroy_process_group()
    if rank == 0:
        print('Training complete. writer.close()')


if __name__ == "__main__":
    main()

创建DLC任务

  1. PAI控制台左侧菜单栏单击分布式训练DLC > 新建任务

    image

  2. 配置DLC任务参数。关键参数配置如下,其他参数默认即可。全量参数请参见创建训练任务

    • 节点镜像:选择镜像地址,然后根据您所在地域填写对应镜像地址。

      image

      地域

      镜像地址

      北京

      dsw-registry-vpc.cn-beijing.cr.aliyuncs.com/pai/modelscope:1.28.0-pytorch2.3.1tensorflow2.16.1-gpu-py311-cu121-ubuntu22.04

      上海

      dsw-registry-vpc.cn-shanghai.cr.aliyuncs.com/pai/modelscope:1.28.0-pytorch2.3.1tensorflow2.16.1-gpu-py311-cu121-ubuntu22.04

      杭州

      dsw-registry-vpc.cn-hangzhou.cr.aliyuncs.com/pai/modelscope:1.28.0-pytorch2.3.1tensorflow2.16.1-gpu-py311-cu121-ubuntu22.04

      其他

      查询地域ID,并替换镜像地址中的<地域ID>获取完整链接:

      dsw-registry-vpc.<地域ID>.cr.aliyuncs.com/pai/modelscope:1.28.0-pytorch2.3.1tensorflow2.16.1-gpu-py311-cu121-ubuntu22.04

      该镜像已在交互式建模 DSW 快速入门中验证没有环境问题。使用PAI建模时,通常先在DSW中验证环境及代码,然后再使用DLC训练。
    • 数据集:选择自定义数据集,并选择上一步中创建的数据集。挂载路径默认/mnt/data

    • 启动命令torchrun --nproc_per_node=1 --nnodes=${WORLD_SIZE} --node_rank=${RANK} --master_addr=${MASTER_ADDR} --master_port=${MASTER_PORT} /mnt/data/mnist_train_distributed.py

      DLC会自动注入MASTER_ADDRWORLD_SIZE通用环境变量,通过$环境变量名来获取。
    • 资源来源:选择公共资源节点数量2,资源规格选择ecs.gn7i-c8g1.2xlarge

      如果该规格实例库存不足,您也可以选择其他GPU实例。

    单击确定创建任务,任务大约需要执行10分钟。执行过程中可以在概览页面,查看两台实例的训练日志

    image

    执行完成后,会在挂载数据集的output_distributed路径下输出最佳的模型检查点,以及Tensorboard日志。

    image

(可选)查看Tensorbord

您可以借助可视化工具Tensorboard查看loss曲线,了解训练的具体情况。

重要

DLC任务如果想使用Tensorboard,必须配置数据集。

  1. 单击DLC任务详情页上方的Tensorboard > 新建Tensorboard

    image

  2. 挂载类型选择按任务,在Summary目录处填写训练代码中Summary存储的路径:/mnt/data/output_distributed/runs,单击确定启动。

    对应代码片段:writer = SummaryWriter('/mnt/data/output_distributed/runs/mnist_experiment')
  3. 单击查看Tensorboard查看train_loss曲线(反映训练集损失)与 validation_loss曲线(反映验证集损失)。

    image

    (可选)根据loss图像,调整超参数,提升模型效果

    您可以根据损失值的变化趋势,初步判断当前模型的训练效果:

    • 在结束训练前 train_loss 与 validation_loss 仍有下降趋势(欠拟合)

      您可以增加 num_epochs(训练轮次,与训练深度正相关),或适当增大 learning_rate 后再进行训练,加大模型的对训练数据的拟合程度;

    • 在结束训练前 train_loss 持续下降,validation_loss 开始变大(过拟合)

      您可以减少 num_epochs,或适当减小 learning_rate 后再进行训练,防止模型过度训练;

    • 在结束训练前 train_loss 与 validation_loss 均处于平稳状态(良好拟合)

      模型处于该状态时,您可以进行后续步骤。

    受限于篇幅,本文无法对微调参数做过多讲解。您可以学习阿里云大模型 ACP 课程 来了解微调命令中的关键参数、以及如何通过损失曲线来决定是否应该继续微调等细节。

部署训练后的模型

详情请参见使用EAS将模型部署为在线服务

相关文档