使用标准slurm集群调度容器作业

本文介绍了从集群创建到作业调度的全过程,适用于需要在SLURM集群中运行容器化任务的用户。

步骤一:创建集群

  1. 创建一个标准版SLURM集群

    本文使用的集群配置示例如下,未提及的参数请根据需要填写。

    配置项

    配置

    集群配置

    系列

    标准版

    部署模式

    公共云集群

    集群类型

    SLURM

    管理节点

    • 实例规格:采用ecs.r7.xlarge实例规格,该规格配置为4 vCPU,32 GiB内存。

    • 镜像:centos_7_6_x64_20G_alibase_20211130.vhd

    计算节点与队列

    队列节点数

    初始节点1

    节点间互联

    VPC网络

    实例规格组

    • 实例规格:采用ecs.gn7i-c56g1.14xlarge实例规格,该规格配置为56 vCPU、346 GiB内存。

      重要

      需使用支持GPU的实例规格。更多内容,请参见实例规格族

    • 镜像:centos_7_6_x64_20G_alibase_20211130.vhd

    共享文件存储

    /home 集群挂载目录

    默认情况下,管理节点的/home/opt将挂载文件系统,作为共享存储目录。

    /opt 集群挂载目录

    软件与服务组件

    待安装软件

    选择docker

    可安装服务组件

    登录节点

    • 实例规格:采用ecs.r7.xlarge实例规格,该规格配置为4 vCPU,32 GiB内存。

    • 镜像:centos_7_6_x64_20G_alibase_20211130.vhd。

  2. 创建一个集群用户

    本文中以usertest用户为例。

步骤二:搭建基础软件环境

  1. 为计算节点绑定弹性公网IP。具体操作,请参见弹性公网IP

  2. 远程连接计算节点

  3. 下载并安装CUDA。

    1. 下载CUDA安装包。

      cd /opt
      wget https://developer.download.nvidia.com/compute/cuda/12.4.1/local_installers/cuda_12.4.1_550.54.15_linux.run
    2. 安装CUDA。

      yum install -y git
      sh /opt/cuda_12.4.1_550.54.15_linux.run

      显示如下图所示时,说明CUDA已安装。

      image

    3. 配置环境变量。

      echo 'export PATH=/usr/local/cuda-12.4/bin:$PATH' >> ~/.bashrc
      echo 'export LD_LIBRARY_PATH=/usr/local/cuda-12.4/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
      
      source ~/.bashrc
    4. 查看NVIDIA CUDA工具包和GPU驱动的安装状态及版本信息。

      # NVIDIA CUDA编译驱动程序的版本信息
      nvcc --version
      
      # GPU的详细状态信息
      nvidia-smi

      显示如下图所示时,说明CUDAGPU驱动正常。

      image

  4. 安装并配置 NVIDIA Container Toolkit。

    distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
    curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo
    sudo yum install -y nvidia-container-toolkit
    sudo systemctl restart docker
  5. 下载并安装Singularity。

    Singularity是一个容器化工具,它允许在不改变用户环境的情况下运行容器,常用于HPC环境。

    cd /opt
    wget https://public-ehs.oss-cn-hangzhou.aliyuncs.com/softwares/packages/CentOS_7.2_64/singularity-3.8.3-1.el7.x86_64.rpm
    yum install -y /opt/singularity-3.8.3-1.el7.x86_64.rpm
  6. 创建作业依赖数据。

    1. 拉取PyTorch容器镜像。

      docker pull ac2-registry.cn-hangzhou.cr.aliyuncs.com/ac2/pytorch:2.4.0-cuda12.1.1-py310-alinux3.2104
    2. 使用usertest 用户,创建main.py文件 。

      vim /home/usertest/main.py
    3. main.py文件脚本内容如下。

      # -*- coding: utf-8 -*-
      
      import torch
      import torchvision
      import torchvision.transforms as transforms
      from torch import nn
      from torch.utils.data import DataLoader
      from torch.optim import SGD
      
      class SimpleNet(nn.Module):
          def __init__(self):
              super(SimpleNet, self).__init__()
              self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
              self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
              self.fc1 = nn.Linear(64 * 8 * 8, 128)
              self.fc2 = nn.Linear(128, 10)
              self.pool = nn.MaxPool2d(2, 2)
              self.relu = nn.ReLU()
              self.dropout = nn.Dropout(0.5)
      
          def forward(self, x):
              x = self.pool(self.relu(self.conv1(x)))
              x = self.pool(self.relu(self.conv2(x)))
              x = x.view(-1, 64 * 8 * 8)
              x = self.relu(self.fc1(x))
              x = self.dropout(x)
              x = self.fc2(x)
              return x
      
      device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
      print(f"Using device: {device}")
      
      transform = transforms.Compose([
          transforms.ToTensor(),
          transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
      ])
      
      train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
      test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
      
      train_loader = DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)
      test_loader = DataLoader(dataset=test_dataset, batch_size=64, shuffle=False)
      
      model = SimpleNet().to(device)
      criterion = nn.CrossEntropyLoss()
      optimizer = SGD(model.parameters(), lr=0.001, momentum=0.9)
      
      num_epochs = 10
      for epoch in range(num_epochs):
          model.train()
          running_loss = 0.0
          for i, data in enumerate(train_loader, 0):
              inputs, labels = data
              inputs, labels = inputs.to(device), labels.to(device)
      
              optimizer.zero_grad()
              outputs = model(inputs)
              loss = criterion(outputs, labels)
              loss.backward()
              optimizer.step()
      
              running_loss += loss.item()
              if i % 100 == 99: 
                  print(f"[{epoch + 1}, {i + 1}] loss: {running_loss / 100:.3f}")
                  running_loss = 0.0
      
          model.eval()
          correct = 0
          total = 0
          with torch.no_grad():
              for data in test_loader:
                  images, labels = data
                  images, labels = images.to(device), labels.to(device)
                  outputs = model(images)
                  _, predicted = torch.max(outputs.data, 1)
                  total += labels.size(0)
                  correct += (predicted == labels).sum().item()
      
          print(f"Accuracy on test set: {100 * correct / total:.2f}%")
      
      print("Training finished.")

步骤三:调度作业

调度Docker作业

展开查看Docker脚本介绍

#!/bin/bash
#SBATCH --job-name=your_job_name
#SBATCH --output=your_output
#SBATCH --nodes=1
#SBATCH --gres=gpu:1
#SBATCH --ntasks=2

# args
iamges="your_image"
run_cmd="your_command"
work_dir="your_workdir"
share_dir="/ehpcdata/:/mnt"
devices="device=$CUDA_VISIBLE_DEVICES"

# cleanup docker handle
function cleanup {
    echo "Caught signal, stopping Docker container: " $SLURM_JOB_NAME
    docker ps -q --filter label=$SLURM_JOB_NAME | xargs -r docker stop
    docker ps -qa --filter label=$SLURM_JOB_NAME | xargs -r docker rm
}
trap cleanup SIGINT SIGTERM

# start docker
docker pull $iamges
srun docker run \
  --label $SLURM_JOB_NAME \
  --rm --net=host \
  --gpus '"'$devices'"' \
  -v $share_dir \
  $iamges \
  /bin/bash -c "$run_cmd" &

# wait to complete
wait
cleanup
说明

脚本说明:

  1. docker运行资源由slurm调度器分配。

  2. GPU ID(CUDA_VISIBLE_DEVICES)由slurm分配,并通过--gpus传递给docker runtime。这种方式支持GPU物理隔离与GPU ID索引映射(nvidia-smi0开始)。

  3. docker镜像内仅支持root执行,Slurm调度参数环境变量无法自动传到内部,需要通过Docker run中的command显性传递到内部,这点跟Singularity不一样。

  4. 由于scancel作业时docker容器不会退出,这里设计信号量机制保障slurm作业启动的镜像在作业完成或退出时自动停止容器。这个机制通过作业名称作为镜像的标签,当作业退出时根据作业名称过滤,停止对应镜像。

  5. 作业脚本包括以下部分:

    1. slurm调度参数,包括资源需求,作业名称(必填),输入输出等信息。

    2. 设置环境变量,包括运行命令,镜像地址(厂库或本地),这部分作为docker启动参数。

    3. cleanup handle,这部分固定不需要修改。

    4. docker 启动命令。

    5. 退出(wait to complete)。

  6. docker run命令:

    1. --name:容器名称,同slurm作业名称。

    2. --gpus:容器使用GPU ID,由slurm调度器分配。

    3. -v:指定容器共享目录,建议把包括代码,模型工作目录映射到容器。注意:容器内部使用root权限,默认产生的文件权限属于root。

    4. 镜像&命令:设定镜像名称与执行命令。

通过E-HPC Portal提交作业

  1. 登录E-HPC Portal

  2. 提交NCCL作业。

    1. 在顶部导航栏,选择任务管理,在页面上方,单击submitter,在创建作业页面,填写作业计算节点数1任务数2Gpu1

    2. 作业脚本内容如下。

      使用命令docker images获取镜像名称和版本号,替换第三行your_image

      #!/bin/bash
      
      image="your_image"
      run_cmd="python main.py"
      share_dir="/home/usertest/:/root"
      
      # cleanup docker handle
      function cleanup {
          echo "Caught signal, stopping Docker container: " $SLURM_JOB_NAME
          docker ps -q --filter label=$SLURM_JOB_NAME | xargs -r docker stop
          docker ps -qa --filter label=$SLURM_JOB_NAME | xargs -r docker rm
      }
      trap cleanup SIGINT SIGTERM
      cleanup
      
      # start docker
      # docker pull $image
      
      docker run \
        --label $SLURM_JOB_NAME \
        --gpus "device=0" \
        -v $share_dir \
        $image \
        /bin/bash -c "$run_cmd" &
      
      # wait to complete
      wait
      cleanup
  3. 查询作业。

    进入任务管理页面,可以查询作业列表,包含作业状态,作业操作等。更多内容,请参见查询作业

通过命令行提交作业

  1. 通过命令行提交作业。具体操作,请参见SLURM

  2. 作业脚本内容如下。

    使用命令docker images获取镜像名称和版本号,替换第十三行your_image

    #!/bin/bash
    
    #SBATCH --job-name=tf_sample_job
    #SBATCH --nodes=1
    #SBATCH --ntasks=2
    #SBATCH --gpus-per-task=1
    #SBATCH --time=01:00:00
    #SBATCH --partition=comp
    #SBATCH --output=tf_sample_job_%j.out
    #SBATCH --error=tf_sample_job_%j.err
    
    # 定义变量
    image="your_image"
    run_cmd="python main.py"
    share_dir="/home/usertest/:/root"
    
    # 清理 Docker 进程
    function cleanup {
        echo "Caught signal, stopping Docker container: " $SLURM_JOB_NAME
        docker ps -q --filter label=$SLURM_JOB_NAME | xargs -r docker stop
        docker ps -qa --filter label=$SLURM_JOB_NAME | xargs -r docker rm
    }
    trap cleanup SIGINT SIGTERM
    cleanup
    
    # 启动 Docker 容器
    docker pull $image
    docker run \
      --label $SLURM_JOB_NAME \
      --gpus "device=$CUDA_VISIBLE_DEVICES" \
      -v $share_dir \
      $image \
      /bin/bash -c "$run_cmd" &
    
    # 等待任务完成
    wait
    cleanup
  3. 通过slurm查询作业。

    使用 squeue 命令可以查询当前正在运行和排队中的作业列表。

    squeue

    使用 sacct 命令可以查询作业的历史记录,包括已完成的作业。

    sacct

调度Singularity作业

  1. Docker2Singularity。

    为了简化Singularity镜像管理,可以复用云上Docker镜像仓库,根据以下操作步骤,对镜像进行格式转换。

    # 方案1:通过local docker package转换sif镜像
    [root@compute006 opt]# docker images
    REPOSITORY                                               TAG               IMAGE ID       CREATED       SIZE
    ac2-registry.cn-hangzhou.cr.aliyuncs.com/ac2/pytorch   2.4.0-cuda12.1.1-py310-alinux3.2104   19301a07d7fd   4 months ago   6.33GB
    [root@compute006 opt]# docker save -o docker.tar 19301a07d7fd 
    [root@compute006 opt]# ll docker.tar 
    -rw------- 1 root root 3021202432 2月  12 15:03 docker.tar
    [root@compute006 opt]# singularity build pytorch.sif docker-archive:///opt/docker.tar
    
    
    # 方案2:通过docker容器仓库build sif镜像
    singularity build xx.sif docker://ac2-registry.cn-hangzhou.cr.aliyuncs.com/ac2/pytorch:2.4.0-cuda12.1.1-py310-alinux3.2104

展开查看Singularity脚本介绍

#!/bin/bash
#SBATCH --job-name=my_job_name
#SBATCH --output=output.log
#SBATCH --nodes=1
#SBATCH --gres=gpu:1
#SBATCH --ntasks=2

# args
image=your_image.sif
run_cmd="your cmd"
share_dir="/ehpcdata/:/mnt"

singularity exec --nv --bind $share_dir $image $run_cmd
说明

脚本说明:

  1. Singularity支持root与非root用户,启动后容器用户的上下文保持不变,镜像内外的用户环境都是相同的,这点不同于docker镜像需要把slurm环境变量主动传递到镜像内部。

  2. GPU ID不支持物理隔离,需要应用自己通过调度器分配的GPU ID(CUDA_VISIBLE_DEVICES)完成GPU选择,例如:"CUDA_VISIBLE_DEVICES=0 ./deviceQuery"。

  3. 作业脚本包括以下部分:

    1. slurm调度参数,包括资源需求,作业名称(必填),输入输出等信息。

    2. 设置环境变量,包括运行命令,镜像地址(厂库或本地),这部分作为docker启动参数。

    3. Singularity 启动命令。

  4. Singularity exec命令:

    1. --nv:用于启用对 NVIDIA GPU 的支持。

    2. --bind:指定容器共享目录,建议把包括代码,模型工作目录映射到容器。注意:容器内部使用root权限,默认产生的文件权限属于root。

    3. 镜像&命令:设定镜像名称与执行命令。

通过E-HPC Portal提交作业

  1. 登录E-HPC Portal

  2. 提交NCCL作业。

    1. 在顶部导航栏,选择任务管理,在页面上方,单击submitter,在创建作业页面,填写作业计算节点数1任务数2Gpu1

    2. 作业脚本内容如下。

      image=/opt/pytorch.sif
      run_cmd="python main.py"
      share_dir="/home/usertest/:/root"
      
      singularity exec --nv --bind $share_dir $image $run_cmd
  3. 查询作业。

    进入任务管理页面,可以查询作业列表,包含作业状态,作业操作等。更多内容,请参见查询作业

通过命令行提交作业

  1. 通过命令行提交作业。具体操作,请参见SLURM

  2. 作业脚本内容如下。

    #!/bin/bash
    #SBATCH --job-name=singularity_TF
    #SBATCH --output=output.log
    #SBATCH --nodes=1
    #SBATCH --gres=gpu:1
    #SBATCH --partition=container
    #SBATCH --ntasks=2
    
    
    image=/opt/pytorch.sif
    run_cmd="python main.py"
    share_dir="/home/usertest/:/root"
    
    singularity exec --nv --bind $share_dir $image $run_cmd
  3. 通过slurm查询作业。

    使用 squeue 命令可以查询当前正在运行和排队中的作业列表。

    squeue

    使用 sacct 命令可以查询作业的历史记录,包括已完成的作业。

    sacct