Reference: https://github.com/ml-kubernetes/MNIST
GitHub - ml-kubernetes/MNIST: Simple example for learning and serving 'MNIST' in kubernetes cluster
Simple example for learning and serving 'MNIST' in kubernetes cluster - GitHub - ml-kubernetes/MNIST: Simple example for learning and serving 'MNIST' in kubernetes cluster
github.com
GCP Platform에서 Kubernetes를 이용해 MNIST 데이터셋을 훈련시켜 보기로 했다.
Reference가 되는 Git 프로젝트를 따라가며 실습을 진행하였다.
전체적인 진행 과정은 다음과 같다 : 설치 및 GKE 클러스터 생성 --> NFS 서버 생성 --> Splitter --> Trainer --> Aggregator --> Server
중요한 점은 Container를 생성해서 NFS 서버로 만들어 줄 것이고, 여기에 GCP Persistent Disk를 마운트 해서 split한 데이터를 저장하고, training 결과물을 저장하고, aggregator로 뽑아낸 최종 모델을 저장할 것이다.
1. 설치 및 GKE 클러스터 생성
gcloud CLI 설치는 이 페이지(https://cloud.google.com/sdk/docs/install-sdk?hl=ko)로 들어가서 진행하면 된다. 나는 Ubuntu에서 진행했고, 무료 GCP 계정을 사용했다. gcloud가 정상적으로 작동하면 kubernetes cluster를 생성해야 한다.
$ gcloud container clusters create my-kube-cluster \
--zone=us-central1-a \
--cluster-version=1.11.7-gke.12 \
--disk-size=20GB \
--disk-type=pd-standard \
--num-nodes=3
* command 실행시 발생할 수 있는 오류 1:
ERROR: (gcloud.container.clusters.create) ResponseError: code=400, message=Failed precondition when calling the ServiceConsumerManager: tenantmanager::185014: Consumer should enable service:container.googleapis.com before generating a service account
이와 유사한 에러가 발생한다면, API 서비스를 설치하지 않았기 때문이다. 아래의 커멘드를 실행시켜 해결해 준다.
$ gcloud services enable container.googleapis.com
* command 실행시 발생할 수 있는 오류2:
cluster-version이 지금은 업데이트 되었기 때문이다. 나는 1.11.7-gke.12 대신에 현재 default 값인 1.24.8-gke.2000를 사용했다.
다음은 gcloud 명령어를 이용해 kubeconfig 파일을 자동으로 업데이트 하기 위한 명령어이다.
$ gcloud container clusters get-credentials my-kube-cluster --zone us-central1-a
kubeconfig 파일을 사용하면 클러스터, 사용자, namespace, 및 인증 메커니즘에 대한 정보를 관리할 수 있다. kubectl 커맨드라인 툴은 kubeconfig 파일을 사용해 클러스터의 선택과 클러스터의 API 서버와의 통신에 필요한 정보를 찾는다.
이제 kubectl 명렁어가 제대로 작동하는지 확인한다.
$ kubectl get nodes
위에서 설명했던 persistent disk를 생성할 차례이다. 노드를 생성했던 Zone과 동일한 곳에 pd를 생성한다.
$ gcloud compute disks create --type=pd-standard \
--size=1GB --zone=us-central1-a ml-kube-disk
--size는 10GB로 변경해 주어야 에러가 안난다.
2. NFS 서버 생성
일단 환경 변수 값을 export 해주고,
$ export WORKER_NUMBER=3
$ export EPOCH=2
$ export BATCH=100
nfs 서버를 구성하기 위한 컨테이너를 만들어준다. 이 nfs 서버 컨테이너는 다른 pod에서 마운트하여 이용하게 될 것이다.
$ kubectl apply -f 1-nfs-deployment.yaml
$ kubectl apply -f 2-nfs-service.yaml
* command 실행시 발생할 수 있는 오류 1:
1-nfs-deployment.yaml 파일에서 apiVersion을 옛날 버전이 아닌 apps/v1 으로 변경해줘야 오류를 해결할 수 있다.
이제 다른 Node에서도 공유할 수 있게 PV와 PVC를 생성해준다.
$ export NFS_CLUSTER_IP=$(kubectl get svc/nfs-server -o jsonpath='{.spec.clusterIP}')
$ cat 3-nfs-pv-pvc.yaml | sed "s/{{NFS_CLUSTER_IP}}/$NFS_CLUSTER_IP/g" | kubectl apply -f -
sed는 편집에 특화된 명령어이다. sed의 유용한 점은 원본을 손상시키지 않고 편집을 할 수 있다는 점이다. 여기 sed 명령어의 의미는 3-nfs-pv-pvc.yaml 내의 {{NFS_CLUSTER_IP}}를 $NFS_CLUSTER_IP로 치환해 준다는 의미이다.
3. Splitter
이제 Splitter Pod를 하나 생성하여 MNIST Dataset을 다운받고, 3등분으로 나누고, NFS 서버에 마운트된 pd에 저장해줄 것이다.
$ cat 4-splitter.yaml | sed "s/{{WORKER_NUMBER}}/$WORKER_NUMBER/g" | kubectl apply -f -
Dockerfile에 ADD 된 splitter.py를 보면, keras를 이용해 간단히 mnist dataset을 load하고, 데이터 셋을 컨테이너 수만큼 나누어 지정된 경로에 저장하는 것을 볼 수 있다.
import argparse import numpy as np import tensorflow as tf import os if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--n_container", type=int, required=True) parser.add_argument("--savedir", type=str, required=True) args = parser.parse_args() # Create output path if not exists if not os.path.exists(args.savedir): os.makedirs(args.savedir) (train_x, train_y), (test_x, test_y) = tf.keras.datasets.mnist.load_data() n_data = len(train_x) // args.n_container for name, i in enumerate(range(0, len(train_x), n_data)): start, end = i, i + n_data np.savez(os.path.join(args.savedir, str(name)), x=train_x[start:end], y=train_y[start:end]) |
나누어진 파일이 잘 저장되었나 확인하기 위해 busybox 컨테이너를 하나 만들어서 nfs PVC를 마운트하고, 지정된 디렉토리를 살펴 볼 수 있다.
$ kubectl apply -f 9999-busybox.yaml
$ kubectl exec -it $(kubectl get pods | grep busybox | awk '{print $1}') sh
/ # ls /mnt
index.html lost+found
/ # exit
파일이 잘 나누어져 저장이 되었다면 다음과 같이 출력될 것이다.
$ kubectl exec $(kubectl get pods | grep busybox | awk '{print $1}') ls /mnt/data
0.npz
1.npz
2.npz
4. Trainer
트레이닝은 bash command를 생성해 $WORKER_NUMBER 만큼 돌면서 trainer pod를 생성하였다.
$ for (( c=0; c<=($WORKER_NUMBER)-1; c++ ))
do
echo $(date) [INFO] "$c"th Creating th trainer in kubernetes..
cat 5-trainer.yaml | sed "s/{{EPOCH}}/$EPOCH/g; s/{{BATCH}}/$BATCH/g; s/{{INCREMENTAL_NUMBER}}/$c/g;" | kubectl apply -f - &
done
여기서 주목할 것은 각 노드마다 하나씩 training이 돌아가게 해야하기 때문에 podAntiAffinity를 사용했다는 점이다.
5-trainer.yaml
affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: "job-type" operator: In values: - trainer topologyKey: "kubernetes.io/hostname" |
podAntiAffinity는 실행 중인 Pod들 중에, 선호하지 않은 Pod가 실행 중인 Node는 피해서 배치를 하겠다는 의미이다.
따라서, 이미 trainer Pod가 돌아가고 있는 Node는 피해서 다른 Node에 trainer Pod를 배치하게 된다.
컨테이너 이미지를 빌드할 때 쓰인 trainer.py 에서는 모델의 레이어를 볼 수 있는데, 아주 간단한 모델임을 알 수 있다.
model = tf.keras.models.Sequential([ Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)), Conv2D(64, (3, 3), activation='relu'), MaxPool2D(pool_size=(2,2)), Dropout(0.25), Flatten(), Dropout(0.5), Dense(10, activation='softmax') ]) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy']) |
제대로 모델이 저장이 되었는지 확인하기 위해, 다시 한번 busybox를 이용한다.
$ kubectl exec $(kubectl get pods | grep busybox | awk '{print $1}') ls /mnt/model
0-model.h5
1-model.h5
2-model.h5
5. Aggregator
생성된 3개 모델의 weights를 평균내서 하나의 모델로 통합시키는 단계이다. 그 과정에서 modelaverage라는 패키지를 이용하는데, 이 해당 프로젝트의 저자가 만든(?)것 같다.
https://github.com/graykode/modelaverage
GitHub - graykode/modelaverage: tf-keras, make the average of model weight in same model.
tf-keras, make the average of model weight in same model. - GitHub - graykode/modelaverage: tf-keras, make the average of model weight in same model.
github.com
aggregator Pod를 만들면, pd에 저장된 3개의 모델들을 이용해 통합된 모델을 생성한다.
$ kubectl apply -f 6-aggregator.yaml
역시 잘 만들어졌는지 확인하려면, busybox를 이용한다.
$ kubectl exec $(kubectl get pods | grep busybox | awk '{print $1}') ls /mnt
aggregated-model.h5
6. Server
서버는 간단한 Flask 코드로 짜여져있고, web으로 접속해서 이미지를 넣으면 prediction 결과를 띄워 주게 되어있다.
마찬가지로, server Pod를 생성하고, web에서 접속을 해야하기 때문에 external IP가 필요하다.
따라서, LoadBalancer 타입의 Service를 생성해준다.
$ kubectl apply -f 7-server.yaml
예를 들어, Cluster IP가 10.19.253.70이라면 10.19.253.70:80으로 접속해 이미지를 넣어보자.
Prediction 결과가 나온다.
다음 실습은 Kubeflow를 활용해 GCP에서 MNIST 데이터를 훈련시켜 볼 예정이다.
'DevOps > Practices for MLOps' 카테고리의 다른 글
GCP Free Tier에서 GPU 사용하기 (0) | 2023.02.15 |
---|