研究室で余ってた計算機サーバーを活用してGPUクラスタを構築してみた with kubernetes
1. 概要
本記事では、kubernetes を用いて、オンプレミスの計算機サーバー間で分散学習を行う方法・手順について紹介します。
kubernetesによりGPUクラスタを作成し、PytorchのDDPサンプルコードを実行することを目的とします。
環境
- master(control plane)1台 × worker 2台でクラスタを構築します。
(簡略化のために最小構成にしてます) - master, worker 共に ubuntu22.04 を使用
- すべてのノードにGPUが1台ずつ搭載
(masterはGPU不要) - GPU は NVIDIA を使用
- すべてのノードにGPUが1台ずつ搭載
2. master ノードのセットアップ
Kubernetes の動作要件を満たすように設定
- 指定したカーネルモジュール(overlay, br_netfilter)をシステム起動時に自動的にロードされるようにする
cat <<EOF | sudo tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
- kubernetes のネットワーク設定
cat <<EOF | sudo tee /etc/sysctl.d/kubernetes.conf
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
sudo sysctl --system
- swap を無効にする
sudo swapoff -a
sudo sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
※Dockerのインストールを済ませておく
kubernetes 関連コンポーネントのインストール
kubelet, kubeadm, kubectl のインストール
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key |sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
kubeadm で Kubernetes クラスタを構築
- kubeadm は実用最小限の Kubernetes クラスターを作成してくれる。
kubeadm init でクラスタ初期化
オプションの--pod-network-cidr
で Pod に割り当てる IP アドレスの範囲を指定する。
今回は以降でコンテナ間通信の仮想ネットワークとして flannel を使用するため、flannel のデフォルトネットワーク10.244.0.0/16
に合わせる。
sudo kubeadm init --pod-network-cidr=10.244.0.0/16
kubeadm init 後の output に worker がクラスタに参加するためのコマンド(kubeadm join ...
)が表示されるので控えておく。
- kube-api に接続するために以下を実行する
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
-
kubectl get node
でノードの状態が確認できれば OK。(この時点ではまだネットワーク関連の設定が完了していないため、マスターノードは NotReady 状態となっている)
flannel で仮想ネットワークを構築
- 設定ファイルを GitHub から取得し、実行する
wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
kubectl apply -f kube-flannel.yml
kubectl get node
でノードが Ready になっていることを確認する
参考サイト
- nvidia 周りの設定
- kubernetes の設定周り
3. worker ノードのセットアップ
master ノードと同様にセットアップを行う
NVIDIA やコンテナ関連のコンポーネントのインストールと containerd の設定
- NVIDIA driver のインストール
- docker, containerd のインストール
- nvidia-container-runtime のインストール、
/etc/docker/daemon.json
より、ランタイムオプションを設定
{
"runtimes": {
"nvidia": {
"path": "/usr/bin/nvidia-container-runtime",
"runtimeArgs": []
}
}
}
-
containerd の config 設定
-
デフォルトランタイムの設定
sudo apt-get install -y nvidia-container-toolkit sudo containerd config default | sudo tee /etc/containerd/config.toml sudo nvidia-ctk runtime configure --runtime=containerd
/etc/containerd/config.toml
より、default_runtime_name
をnvidia
に設定する -
systemd バックエンドな cgroup を有効にする
/etc/containerd/config.toml
より、 nvidia runtime options の書き換え[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options] 省略... SystemdCgroup = true
-
containerd 再起動
sudo systemctl restart containerd.service
-
※NVIDIA driver 設定後は再起動が必要
クラスタに参加
-
kubeadm join
コマンドを実行する- トークンには有効期限があり、通常は 24 時間で利用できなくなるらしい
- 失効した場合は、master ノードで
kubeadm token create --print-join-command
を実行すると再発行できる
- master ノードで
kubectl get node
を実行し、worker ノードがクラスタに参加したことを確認する
4. GPU-device-driver で worker の GPU を認識できるようにする
現状kubectl describe node your-node-name
を実行しても、gpuをリソースとして確認できないと思います。
なので、以下のようにnvidia-device-plugin を起動することで、gpuを認識できるようにします。
$ kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.15.0/deployments/static/nvidia-device-plugin.yml
kubectl describe node your-node-name
を実行してgpuが認識されたことを確認してください。
例:
...(省略)...
Capacity:
cpu: 12
memory: 131945888Ki
nvidia.com/gpu: 1
pods: 110
Allocatable:
cpu: 12
memory: 131843488Ki
nvidia.com/gpu: 1
pods: 110
...(省略)...
参考サイト
5. 分散学習用のマニフェストを作成
環境は整ったので、分散学習用のマニフェストを作成します。
- pod間通信を行うためのServiceと分散学習用のJobを作成します
- JOB_COMPLETION_INDEXが0のpodをマスターとして分散学習を行います(torchrunを使用)
# Service configuration for multinode.
apiVersion: v1
kind: Service
metadata:
name: multinode-svc
spec:
clusterIP: None # ClusterIP set to None for headless service.
ports:
- name: nccl # Port for torchrun master-worker node communication.
port: 29500
targetPort: 29500
selector:
job-name: multinode-job # Selector for pods associated with this service.
---
apiVersion: batch/v1
kind: Job
metadata:
name: multinode-job
spec:
completionMode: Indexed
completions: 2
parallelism: 2
template:
spec:
restartPolicy: Never
subdomain: multinode-svc # Subdomain for the headless service.
containers:
- image: "your-docker-image"
name: multinode
env:
- name: MASTER_ADDR
value: multinode-job-0.multinode-svc.default.svc.cluster.local # Node with rank 0 is chosen as the master node.
- name: MASTER_PORT
value: '29500'
- name: NNODES
value: '2' # Number of training nodes.
- name: NGPUS
value: '1' # Number of GPUs in the machine.
ports:
- containerPort: 29500
name: nccl
command: ["sh", "-c", "torchrun --nnodes=$NNODES --node_rank=$JOB_COMPLETION_INDEX --nproc_per_node=$NGPUS --master_addr $MASTER_ADDR --master_port $MASTER_PORT example.py"]
resources:
limits:
nvidia.com/gpu: 1
FROM pytorch/pytorch:latest
COPY ./practice/example.py ./
import argparse
import os
import sys
import tempfile
from urllib.parse import urlparse
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.optim as optim
from torch.nn.parallel import DistributedDataParallel as DDP
class ToyModel(nn.Module):
def __init__(self):
super(ToyModel, self).__init__()
self.net1 = nn.Linear(10, 10)
self.relu = nn.ReLU()
self.net2 = nn.Linear(10, 5)
def forward(self, x):
return self.net2(self.relu(self.net1(x)))
def demo_basic(local_world_size, local_rank):
# setup devices for this process. For local_world_size = 2, num_gpus = 8,
# rank 0 uses GPUs [0, 1, 2, 3] and
# rank 1 uses GPUs [4, 5, 6, 7].
n = torch.cuda.device_count() // local_world_size
device_ids = list(range(local_rank * n, (local_rank + 1) * n))
print(
f"[{os.getpid()}] rank = {dist.get_rank()}, "
+ f"world_size = {dist.get_world_size()}, n = {n}, device_ids = {device_ids} \n", end=''
)
model = ToyModel().cuda(device_ids[0])
ddp_model = DDP(model, device_ids)
loss_fn = nn.MSELoss()
optimizer = optim.SGD(ddp_model.parameters(), lr=0.001)
optimizer.zero_grad()
outputs = ddp_model(torch.randn(20, 10))
labels = torch.randn(20, 5).to(device_ids[0])
loss_fn(outputs, labels).backward()
optimizer.step()
def spmd_main(local_world_size, local_rank):
# These are the parameters used to initialize the process group
env_dict = {
key: os.environ[key]
for key in ("MASTER_ADDR", "MASTER_PORT", "RANK", "WORLD_SIZE")
}
if sys.platform == "win32":
# Distributed package only covers collective communications with Gloo
# backend and FileStore on Windows platform. Set init_method parameter
# in init_process_group to a local file.
if "INIT_METHOD" in os.environ.keys():
print(f"init_method is {os.environ['INIT_METHOD']}")
url_obj = urlparse(os.environ["INIT_METHOD"])
if url_obj.scheme.lower() != "file":
raise ValueError("Windows only supports FileStore")
else:
init_method = os.environ["INIT_METHOD"]
else:
# It is a example application, For convience, we create a file in temp dir.
temp_dir = tempfile.gettempdir()
init_method = f"file:///{os.path.join(temp_dir, 'ddp_example')}"
dist.init_process_group(backend="gloo", init_method=init_method, rank=int(env_dict["RANK"]), world_size=int(env_dict["WORLD_SIZE"]))
else:
print(f"[{os.getpid()}] Initializing process group with: {env_dict}")
dist.init_process_group(backend="nccl")
print(
f"[{os.getpid()}]: world_size = {dist.get_world_size()}, "
+ f"rank = {dist.get_rank()}, backend={dist.get_backend()} \n", end=''
)
demo_basic(local_world_size, local_rank)
# Tear down the process group
dist.destroy_process_group()
if __name__ == "__main__":
local_world_size = int(os.environ["LOCAL_WORLD_SIZE"])
local_rank = int(os.environ["LOCAL_RANK"])
spmd_main(local_world_size, local_rank)
kubectl apply -f job-manifest.yaml
にて、Jobを起動すると、分散学習がスタートします。
参考サイト
- Jobのpod間通信について
- マニフェスト作成参考サイト
- 分散学習参考サイト
- torchrunについて
6.まとめ
kubernetes を用いて GPU クラスタを作成し、分散学習を行う環境を構築しました。
今後は、分散学習における学習プロセスの最適化や、機械学習フローの自動化、クラスタ構成の冗長化等にチャレンジしてみたいと思います。
Discussion