本指导适用于在 TencentOS Server 3上使用 Distributed DataParallel(DDP)训练框架运行 ResNet 模型,以 Docker 方式启动。
环境准备
1. 从 GitHub 下载 DDP 的 Demo 开源仓库到本地。
git clone https://github.com/tczhangzhi/pytorch-distributed.gitcd pytorch-distributed
2. 由于 DDP 是基于 Pytorch 开发的,我们使用 Pytorch 的基础镜像即可包含大部分运行环境。这里我们拉取 Nvidia 的基础镜像来启动容器。
docker run -it --gpus all --name=DDP --ipc=host -v $PWD:/workspace nvcr.io/nvidia/pytorch:23.06-py3 /bin/bash
此时会从 nvcr 拉取 docker 镜像,请确保网络环境较好,直到镜像里所有层下载成功。成功后会直接进入容器内部。
数据集下载
我们使用 DDP 训练框架运行 ResNet-101 模型,官方预训练 ResNet-101 模型最常用的数据集是 ImageNet-1K,所以这里我们使用 ImageNet-1K 训练模型。
1. 首先创建数据集的存放地址,放在 /workspace/datasets/imagenet 文件夹下,首先创建文件夹。
mkdir -p datasets/imagenetcd datasets/imagenet
2. 在该目录下直接下载 ImageNet-1K 数据集,包括训练集,验证集和标签映射文件三个文件。
下载训练集:
wget https://image-net.org/data/ILSVRC/2012/ILSVRC2012_img_train.tar --no-check-certificate
下载验证集:
wget https://image-net.org/data/ILSVRC/2012/ILSVRC2012_img_val.tar --no-check-certificate
下载标签映射文件:
wget https://image-net.org/data/ILSVRC/2012/ILSVRC2012_devkit_t12.tar.gz --no-check-certificate
注意:
训练集包含1000个类,共计1281167张图片,大小有138G;验证集包含1000个类,共计50000张图片,大小有6G。请确保机器拥有较大的容量下载数据集。此外请确保网络环境较好,ImageNet 训练集较大需要较长时间下载。
训练集解压
1. 将训练集解压到 train 目录下。
mkdir train && tar -xvf ILSVRC2012_img_train.tar -C train && for x in `ls train/*tar`; do fn=train/`basename $x .tar`; mkdir $fn; tar -xvf $x -C $fn; rm -f $fn.tar; done
2. 进入 train 目录下。
cd train
3. 查看该目录下的文件夹数量,若解压成功,正常情况下应该返回1000(代表1000个文件夹,数据集的1000个类)。
ls -lR|grep "^d"|wc -l
4. 查看该目录下的所有图片的数量,若解压成功,正常情况下应该返回1281167(代表一共有1281167张训练图像)。
ls -lR|grep "^-"|wc -l
验证集解压
1. 将验证集解压到 train 目录下。
mkdir val && tar xvf ILSVRC2012_img_val.tar -C ./val
2. 解压完成后,所有图像都在 val 文件夹里了。但是此时 val 下全是图像,没有被分到1000个文件夹里,需要将所有验证集图像分类到1000个类里去。
3. 解压标签映射文件 ILSVRC2012_devkit_t12.tar.gz,里面内容为每张图像对应的类别的映射关系。
tar -xzf ILSVRC2012_devkit_t12.tar.gz
4. 在 /imagenet 目录下创建 unzip.py 文件,写入以下代码:
from scipy import ioimport osimport shutildef move_valimg(val_dir='./val', devkit_dir='./ILSVRC2012_devkit_t12'):"""move valimg to correspongding folders.val_id(start from 1) -> ILSVRC_ID(start from 1) -> WINDorganize like:/val/n01440764images/n01443537images....."""# load synset, val ground truth and val images listsynset = io.loadmat(os.path.join(devkit_dir, 'data', 'meta.mat'))ground_truth = open(os.path.join(devkit_dir, 'data', 'ILSVRC2012_validation_ground_truth.txt'))lines = ground_truth.readlines()labels = [int(line[:-1]) for line in lines]root, _, filenames = next(os.walk(val_dir))for filename in filenames:# val image name -> ILSVRC ID -> WINDval_id = int(filename.split('.')[0].split('_')[-1])ILSVRC_ID = labels[val_id-1]WIND = synset['synsets'][ILSVRC_ID-1][0][1][0]print("val_id:%d, ILSVRC_ID:%d, WIND:%s" % (val_id, ILSVRC_ID, WIND))# move val imagesoutput_dir = os.path.join(root, WIND)if os.path.isdir(output_dir):passelse:os.mkdir(output_dir)shutil.move(os.path.join(root, filename), os.path.join(output_dir, filename))if __name__ == '__main__':move_valimg()
5. 执行该文件。
python unzip.py
这样验证集就被分到了1000个文件夹之下。
运行模型
DDP 运行的文件为 distributed.py,其中有以下几处需要修改。
代码第74行,将 --local_rank 参数的 default 改为 int(os.environ["LOCAL_RANK"],以便启用最新的 torchrun 方法启动。
#原有代码default=-1,#更改为default=int(os.environ["LOCAL_RANK"]),
代码第375行会报错,将 view() 方法更改为 reshape() 方法即可。
#原有代码correct_k = correct[:k].view(-1).float().sum(0, keepdim=True)#更改为correct_k = correct[:k].reshape(-1).float().sum(0, keepdim=True)
以上修改完之后,即可运行模型。这里我们测试的是 ResNet-101 模型通过 ImageNet-1K 数据集训练。由于我们有8张L40 gpu,所以 --nproc_per_node 设置为8。
torchrun --nproc_per_node=8 distributed.py --data datasets/imagenet --arch resnet101 --epochs 120 --batch-size 256
此时会使用 ResNet-101 模型架构,从0开始训练120个 epochs,数据集在 datasets/imagenet 文件夹下,batchsize为256,学习率 lr 为0.1,优化方法为 SGD 使用 momentum=0.9,权重衰退 weight decay=1e-4。参数设置来源于 ResNet 原论文:ResNet,从而尽可能的复现出论文里的结果。
说明:
如需要更改 batchsize,学习率,momentum 参数和权重衰减参数,只需加上对应参数
--batch-size,--learning-rate,--momentum,--weight-decay并设置想要的值即可。此时会开始训练模型,由于使用的是 DDP 训练模式,所以各个 gpu 之间的显存占用相差不大。运行模型过程中会保存当前 epoch 训练结束之后的模型权重以及到当前 epoch 为止在验证集上最好性能的 epoch 的模型权重。
训练时命令行会出现训练时每一个 iteration 和测试时每一个 iteration 的性能以及每一个 epoch 测试完成之后的总的性能(例如最后一个 epoch 的参考如下,top1 Acc 复现的结果还是比较接近论文中的结果):
...Test: [180/196] Time 0.021 ( 0.066) Loss 9.2615e-01 (9.0108e-01) Acc@1 78.91 ( 77.86) Acc@5 92.58 ( 93.87)Test: [180/196] Time 0.021 ( 0.066) Loss 9.2615e-01 (9.0108e-01) Acc@1 78.91 ( 77.86) Acc@5 92.58 ( 93.87)Test: [180/196] Time 0.021 ( 0.066) Loss 9.2615e-01 (9.0108e-01) Acc@1 78.91 ( 77.86) Acc@5 92.58 ( 93.87)Test: [180/196] Time 0.021 ( 0.066) Loss 9.2615e-01 (9.0108e-01) Acc@1 78.91 ( 77.86) Acc@5 92.58 ( 93.87)Test: [180/196] Time 0.021 ( 0.066) Loss 9.2615e-01 (9.0108e-01) Acc@1 78.91 ( 77.86) Acc@5 92.58 ( 93.87)Test: [180/196] Time 0.021 ( 0.066) Loss 9.2615e-01 (9.0108e-01) Acc@1 78.91 ( 77.86) Acc@5 92.58 ( 93.87)Test: [180/196] Time 0.021 ( 0.066) Loss 9.2615e-01 (9.0108e-01) Acc@1 78.91 ( 77.86) Acc@5 92.58 ( 93.87)Test: [180/196] Time 0.021 ( 0.066) Loss 9.2615e-01 (9.0108e-01) Acc@1 78.91 ( 77.86) Acc@5 92.58 ( 93.87)Test: [190/196] Time 0.021 ( 0.066) Loss 8.5628e-01 (8.9837e-01) Acc@1 77.34 ( 77.88) Acc@5 94.14 ( 93.88)Test: [190/196] Time 0.021 ( 0.066) Loss 8.5628e-01 (8.9837e-01) Acc@1 77.34 ( 77.88) Acc@5 94.14 ( 93.88)Test: [190/196] Time 0.021 ( 0.066) Loss 8.5628e-01 (8.9837e-01) Acc@1 77.34 ( 77.88) Acc@5 94.14 ( 93.88)Test: [190/196] Time 0.021 ( 0.066) Loss 8.5628e-01 (8.9837e-01) Acc@1 77.34 ( 77.88) Acc@5 94.14 ( 93.88)Test: [190/196] Time 0.021 ( 0.066) Loss 8.5628e-01 (8.9837e-01) Acc@1 77.34 ( 77.88) Acc@5 94.14 ( 93.88)Test: [190/196] Time 0.021 ( 0.066) Loss 8.5628e-01 (8.9837e-01) Acc@1 77.34 ( 77.88) Acc@5 94.14 ( 93.88)Test: [190/196] Time 0.021 ( 0.066) Loss 8.5628e-01 (8.9837e-01) Acc@1 77.34 ( 77.88) Acc@5 94.14 ( 93.88)Test: [190/196] Time 0.021 ( 0.066) Loss 8.5628e-01 (8.9837e-01) Acc@1 77.34 ( 77.88) Acc@5 94.14 ( 93.88)* Acc@1 77.890 Acc@5 93.886* Acc@1 77.890 Acc@5 93.886* Acc@1 77.890 Acc@5 93.886* Acc@1 77.890 Acc@5 93.886* Acc@1 77.890 Acc@5 93.886* Acc@1 77.890 Acc@5 93.886* Acc@1 77.890 Acc@5 93.886* Acc@1 77.890 Acc@5 93.886
同时目录下会出现 model_best.pth.tar 和 checkpoint.pth.tar,记录模型权重。可以看到测试时同一个 iteration 有多条记录,这是因为 DDP 在每一张卡上都有一个进程,每一张卡都会打印一条记录。
参考文档
ResNet