Posts 온프레미스 GPU 서버로 쿠버네티스 클러스터 구성하기
Post
Cancel

온프레미스 GPU 서버로 쿠버네티스 클러스터 구성하기

학습 작업을 손으로 배분하는 것의 한계

GPU 서버가 세 대쯤 되면 누가 어느 서버를 쓰는지 메신저로 조율하게 된다. 서버가 늘거나 사람이 늘면 이 방식은 곧 무너진다. 학습 작업을 파드로 던지고 스케줄러가 빈 GPU를 찾아 배치하게 만드는 편이 결국 더 싸게 먹힌다.

구성은 마스터 1대 + 워커 3대다. 마스터는 GPU가 없어도 되고 메모리도 적게 잡는다. 워커만 GPU를 단다.

1
2
3
4
마스터   172.16.10.90    (GPU 없음, 16GB)
워커 1   172.16.10.100   (GPU, 64GB)
워커 2   172.16.10.101   (GPU, 64GB)
워커 3   172.16.10.102   (GPU, 128GB)

모든 노드에 먼저 해둘 것

NTP 동기화

클러스터 노드 간 시각이 어긋나면 인증서 검증과 리더 선출이 간헐적으로 실패한다. 증상이 “가끔 안 된다”로 나타나서 원인 찾기가 유독 오래 걸린다.

1
2
3
sudo apt install -y ntp
sudo systemctl enable --now ntp
timedatectl status

스왑 해제

kubelet은 스왑이 켜져 있으면 아예 기동을 거부한다. 메모리 압박 상황에서 스왑으로 넘어가면 파드의 메모리 제한이 의미를 잃기 때문이다.

1
2
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab   # 재부팅 후에도 유지

/etc/fstab 수정을 빼먹으면 재부팅 한 번에 노드가 NotReady로 떨어진다. 그리고 그 재부팅은 대개 몇 달 뒤 아무 맥락 없는 순간에 일어난다.

커널 모듈과 sysctl

1
2
3
4
5
6
7
8
9
10
11
12
cat <<'EOT' | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOT
sudo modprobe overlay && sudo modprobe br_netfilter

cat <<'EOT' | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOT
sudo sysctl --system

도커 cgroup 드라이버를 systemd로

kubelet의 기본 cgroup 드라이버가 systemd인데 도커는 cgroupfs가 기본이다. 둘이 다르면 노드가 부하 상태에서 불안정해진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat <<'EOT' | sudo tee /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": { "max-size": "100m" },
  "storage-driver": "overlay2",
  "default-runtime": "nvidia",
  "runtimes": {
    "nvidia": {
      "path": "nvidia-container-runtime",
      "runtimeArgs": []
    }
  }
}
EOT
sudo systemctl daemon-reload && sudo systemctl restart docker

default-runtime을 nvidia로 두는 것이 중요하다. 이렇게 해두지 않으면 GPU 디바이스 플러그인 자체가 GPU를 못 보는데, 정작 파드 로그에는 “device not found”만 찍혀 원인이 드러나지 않는다.

클러스터 기동

마스터에서:

1
2
3
4
5
6
7
sudo kubeadm init \
  --apiserver-advertise-address=172.16.10.90 \
  --pod-network-cidr=10.244.0.0/16

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

--pod-network-cidr은 쓸 CNI에 맞춰야 한다. Flannel이면 10.244.0.0/16, Calico면 192.168.0.0/16이 기본값이다. 여기서 사내 네트워크 대역과 겹치면 파드에서 사내 서버로 가는 트래픽이 통째로 사라진다. 이건 나중에 고치려면 클러스터를 다시 만들어야 하므로 처음에 확인해야 한다.

CNI 설치 후 워커에서 join:

1
2
sudo kubeadm join 172.16.10.90:6443 --token <TOKEN> \
  --discovery-token-ca-cert-hash sha256:<HASH>

토큰은 24시간 뒤 만료된다. 잃어버렸으면 마스터에서 다시 만든다.

1
kubeadm token create --print-join-command

GPU를 스케줄 가능한 자원으로 만들기

NVIDIA 디바이스 플러그인을 데몬셋으로 올리면 각 노드의 GPU가 nvidia.com/gpu 자원으로 보이기 시작한다.

1
2
kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.0/nvidia-device-plugin.yml
kubectl describe node 172.16.10.100 | grep -A5 Capacity

이제 학습 잡은 이렇게 던진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: batch/v1
kind: Job
metadata:
  name: detector-train
spec:
  backoffLimit: 0
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: trainer
        image: registry.internal/trainer:cu118
        command: ["python", "train.py", "--cfg", "/cfg/exp01.yaml"]
        resources:
          limits:
            nvidia.com/gpu: 2
        volumeMounts:
        - { name: dataset, mountPath: /data }
        - { name: ckpt,    mountPath: /ckpt }
      volumes:
      - name: dataset
        nfs: { server: 172.16.10.90, path: /export/dataset, readOnly: true }
      - name: ckpt
        nfs: { server: 172.16.10.90, path: /export/ckpt }

backoffLimit: 0으로 두는 이유가 있다. 기본값에서는 학습이 실패하면 쿠버네티스가 알아서 재시도하는데, 설정 오류나 OOM으로 죽는 잡이 같은 조건으로 여섯 번 다시 돌면서 GPU를 계속 붙잡는다. 학습 잡은 자동 재시도가 도움이 되는 경우가 거의 없다.

GPU는 limits에만 쓸 수 있고 requests에는 못 쓴다. 그리고 GPU는 분할되지 않으므로 nvidia.com/gpu: 2는 정확히 GPU 2장을 통째로 점유한다. 한 장을 두 잡이 나눠 쓰는 구성이 필요하면 MPS나 MIG를 따로 붙여야 한다.

데이터셋을 어디에 둘 것인가

이 구성에서 가장 오래 고민한 부분이다. 결론은 NFS 한 대에 몰고 읽기 전용으로 마운트하는 것이었다. 노드마다 데이터를 복사해두면 읽기는 빠르지만 데이터셋이 갱신될 때마다 세 곳을 맞춰야 하고, 결국 어느 노드가 어떤 버전을 갖고 있는지 아무도 모르게 된다.

NFS가 느려서 학습이 데이터 로딩에 묶이면, 그때 노드 로컬 SSD에 캐시를 두는 방식을 추가한다. 순서를 반대로 하면 안 된다. 정합성을 먼저 확보하고 속도는 나중에 붙인다. 반대로 하면 속도는 얻었는데 어떤 데이터로 학습한 모델인지 말할 수 없게 된다.

This post is licensed under CC BY 4.0 by the author.