PyTorch + ABCI によるマルチノード分散学習のベストプラクティス
本記事では、産総研が提供するGPUクラウド計算基盤「ABCI」を用いて分散学習を行うためのシンプルな方法をまとめます。 以下のレポジトリに、マルチノード学習に対応した ABCI 向け学習コードの最小サンプルを公開しています。
https://github.com/yukara-ikemiya/abci-code-sample
大規模モデル学習に ABCI を活用する
AI Bridging Cloud Infrastructure (ABCI) は、国立研究開発法人 産業技術総合研究所(産総研)が構築・運用する世界初のオープンな人工知能処理向け大規模クラウド計算基盤です。 (https://abci.ai/ja/about_abci/)
Python 環境の構築
ABCI では、学習環境の構築に Singularity コンテナの利用が推奨されています。Singularity コンテナ(SIFファイル)をビルドする方法はいくつかありますが、上記レポジトリではまず Docker イメージを作成し、それを SIF ファイルに変換するスクリプトを提供しています。
SIF ファイルを作成したら、ABCI 上の任意のディレクトリに配置して学習実行時に読み込みます。
学習コードの準備
PyTorch で分散学習を行う際、一般的には DistributedDataParallel (DDP) を用いてコードを書きますが、ここでは DDP をラップしてよりシンプルに実装できる HuggingFace Accelerate の活用を推奨します。コードが簡潔になるだけでなく、バグの混入も防ぎやすくなります。
モデル(最小限のオートエンコーダ)
import torch
from torch import nn
class SimpleModule(nn.Module):
def __init__(self, dim_in, dim_hidden):
super().__init__()
self.dim_in = dim_in
self.dim_hidden = dim_hidden
self.net = nn.Sequential(
nn.Linear(self.dim_in, self.dim_hidden),
nn.Linear(self.dim_hidden, self.dim_in)
)
def forward(self, x):
return self.net(x)
トイデータセット
import numpy as np
import torch
from torch.utils.data import Dataset
class DummyDataset(Dataset):
def __init__(self, dim:int, num_data:int=10000):
super().__init__()
self.dim = dim
self.num_data = num_data
def get_item(self, idx):
data = np.linspace(0, idx, self.dim)
return torch.from_numpy(data.astype(np.float32))
def __len__(self):
return self.num_data
def __getitem__(self, idx):
return self.get_item(idx)
学習コード
import argparse
import torch
from torch.utils.data import DataLoader
from accelerate import Accelerator
from simple_module import SimpleModule
from dummy_dataset import DummyDataset
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument("--bs", type=int, default=50, help="batch size")
parser.add_argument("--lr", type=float, default=0.0001, help="learning rate")
parser.add_argument("--amp", type=str, default='fp16', help="autmatic mixed precision")
return parser.parse_args()
def main():
args = get_args()
# Accelerator の初期化
accelerator = Accelerator(mixed_precision=args.amp, split_batches=True)
# モデル
model = SimpleModule(dim_in=1000, dim_hidden=100)
# データセット
num_data = 10000
dataset = DummyDataset(model.dim_in, num_data)
dataloader = DataLoader(dataset, batch_size=args.bs, num_workers=4,
pin_memory=True, persistent_workers=True, shuffle=True)
# オプティマイザ
optimizer = torch.optim.Adam(model.parameters(), lr=args.lr, betas=[0.0, 0.99])
# 分散学習用に準備
model, dataloader, optimizer = accelerator.prepare(model, dataloader, optimizer)
for idx_e in range(200):
loss_epoch = 0.
for idx_d, x in enumerate(dataloader):
# forward
optimizer.zero_grad(set_to_none=True)
y = model(x)
# RMSE loss
loss = ((x - y) ** 2).mean().sqrt()
# backward
accelerator.backward(loss)
optimizer.step()
loss_epoch += loss.detach()
if accelerator.is_main_process:
loss_epoch /= idx_d + 1
print(f'Epoch {idx_e+1} : {loss_epoch}')
if __name__ == '__main__':
main()
実行スクリプトの準備
HuggingFace Accelerator が内部で参照する環境変数が自動取得されない場合があります。 これに対処するため、以下のように Python コマンドをラッパー bash でラップし、OpenMPI の環境変数をコピーして利用します。
python.bash
#!/bin/bash
# HuggingFace Accelerate 用の環境変数を定義・コピー
export WORLD_SIZE=$OMPI_COMM_WORLD_SIZE
export RANK=$OMPI_COMM_WORLD_RANK
export LOCAL_RANK=$OMPI_COMM_WORLD_LOCAL_RANK
export MASTER_PORT=11111
exec python "$@"
ノードあたりの GPU 数などの必要な設定の多くはスクリプトで自動検出できます。
train.bash
#!/bin/bash
#$-cwd
# モジュールの読み込み(ABCIの環境に応じて変更可能)
source /etc/profile.d/modules.sh
module load hpcx/2.12
module load singularitypro/3.11
module load cuda/11.6/11.6.2
module load nccl/2.11/2.11.4-1
# Singularity コンテナのパス
CONTAINER_PATH="/path/to/your/container.sif"
# ジョブID
JOB_NAME=$JOB_ID
# GPU タイプ(V100 または A100)の判定
GPU_INFO=$(nvidia-smi --query-gpu=gpu_name --format=csv)
if [[ $GPU_INFO =~ "V100" ]]; then
NUM_GPUS_PER_NODE=4
elif [[ $GPU_INFO =~ "A100" ]]; then
NUM_GPUS_PER_NODE=8
else
readonly PROC_ID=$!
kill ${PROC_ID}
fi
# GPU数の取得
GPUS_IN_ONE_NODE=$(nvidia-smi --list-gpus | wc -l)
NUM_GPU=$(expr ${NHOSTS} \* ${GPUS_IN_ONE_NODE})
echo "NUM_GPU = ${NUM_GPU}"
# MPI オプション
MPIOPTS="-np $NUM_GPU -N ${NUM_GPUS_PER_NODE} -x MASTER_ADDR=${HOSTNAME} -hostfile $SGE_JOB_HOSTLIST"
# マウント対象のディレクトリ
ROOT_SRC="/path/to/your/source/codes/"
# 設定値
batch_size=256 # 16 の倍数(2ノード構成時)
learning_rate=0.0001
amp=fp16
# 実行
mpirun $MPIOPTS \
singularity exec --nv --pwd ${ROOT_SRC}/src/ -B ${ROOT_SRC} \
${CONTAINER_PATH} \
${ROOT_SRC}/job/python.bash ${ROOT_SRC}/src/train.py \
--bs ${batch_size} \
--lr ${learning_rate} \
--amp ${amp}
最後に、ABCI 上のマルチノードでジョブを投入して実行します。
_run_job_on_abci_nodes.bash
# 2ノードでの学習実行ジョブ投入
qsub -j y -g gce12345 -l rt_AF=2 -l h_rt=0:30:00 ./job/train.bash