Deploy with BentoML Yatai
이번글에서는 BentoML의 Yatai라는 component를 쿠버네티스 클러스터에 설치하여 Yatai Web UI를 통해 간편하게 모델을 배포 및 관하는 방법에 대해 소개하려고 한다.
얼마전에 BentoML v1.0.0이 preview release 되었는데 눈 여겨볼 변경점은 다음과 같다.
- bentoml 커맨드에 yatai login 이라는 하위 명령어와 옵션을 통해 외부 컨테이너에서 쿠버네티스 환경의 yatai에 접근하여 모델을 push할 수 있다. (API 토큰, Yatai endpoint 활용)
- model과 bentos(bento service)를 구분지어 관리하고 runner라는 원격 python worker에서 inference를 실행할 수 있도록 한다.
위와 같은 변경점을 새로 적용하면서 파이프라인을 아래와 같이 수정하였다.
v0.13.1(https://docs.bentoml.org/en/v0.13.1/)
- Local에서 BentoService를 정의하고 MLflow에서 불러온 모델을 pack & save
- 자동 생성된 dockerfile에 접근하여 Image build & push
- 쿠버네티스 Deployment, Service manifest 작성 후 배포
v1.0.0(https://docs.bentoml.org/en/v1.0.0/index.html)
- Yatai 서버로 모델과 서비스를 push하는 kubeflow pipeline 작성
- Yatai Web UI에서 배포
즉 v0.13.1 에서는 모델을 배포할 때 마다 Local에서 이미지를 빌드하고 manifest를 작성 해야하는 반복작업이 필요했지만 v1.0.0 부터 kubeflow pipeline만 잘 구축한다면 쿠버네티스 환경의 Yatai UI에서 간편하게 모델을 배포할 수 있게 되었다. 이제 본문으로 들어가서 Kubernetes에 Yatai 설치부터 배포까지 천천히 살펴보자.
Yatai install
Prerequisites
- Yatai를 설치하기 위해서는 쿠버네티스(v1.18+)와 Helm(v3+)이 설치되어있어야 한다. 공식 문서에는 minikube 환경에서 설치하는 방법이 자세히 나와있으니 참고하면 좋을 것 같다.
- Install latest kubeadm: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/
- Install latest Helm: https://helm.sh/docs/intro/install/
Install Yatai Helm Chart
helm repo add yatai https://bentoml.github.io/yatai-chart
helm repo update
helm install yatai yatai/yatai -n yatai-system --create-namespace
명령어를 수행하면 yatai-component, yatai-operator, yatai-system 네임스페이스가 각각 생성된다. yatai-system 네임스페이스를 확인해보면 Yatai Web UI 서비스가 NodePort로 배포되어 있는 것을 확인할 수 있다.
Create Account
- Yatai를 사용하기 위해서는 초기 관리자 계정설정을 해야한다. 아래 명령어를 통해 초기화 토큰을 구하고 관리자 계정을 생성할 수 있다.
export YATAI_INITIALIZATION_TOKEN=$(kubectl get secret yatai --namespace yatai-system -o jsonpath="{.data.initialization_token}" | base64 --decode)
echo "Visit: http://{NODE IP}:30080/setup?token=$YATAI_INITIALIZATION_TOKEN"
# echo 출력 url을 복사하여 웹 페이지 접속
- 왼쪽 이미지에서 사용할 이름, 이메일, 암호를 입력 후 제출을 누르면 오른쪽 이미지와 같이 로그인되어 메인 UI 화면으로 넘어간다.
Create API Token
- 외부에서 쿠버네티스에 배포된 Yatai에 접근하기 위해 API 토큰을 생성해야 한다.
1. 우측 상단 관리자 이름아래 API 토큰 클릭
2. 생성 클릭 -> 토큰 이름을 작성하고 권한 부여 -> 제출
3. API 토큰 복사
Uninstall Yatai
- yatai 설치에 문제가 발생한 경우 아래 명령어로 Yatai를 완전히 삭제할 수 있다.
bash -c "$(curl https://raw.githubusercontent.com/bentoml/yatai-chart/main/delete-yatai.sh)"
Yatai 설정을 마쳤으니 이제 Kubeflow에 사용될 serve pipeline 코드를 작성해보자.
코드작성
Serving을 제외한 나머지 pipeline의 코드와 데이터셋 정보는 여기 참고.
mlflow_model.py
- MLflow에 저장된 모델의 이름과 버전을 통해 모델을 불러오는 역할
import os
import mlflow
from mlflow.tracking import MlflowClient
def load_model(model_name, version):
os.environ["AWS_ACCESS_KEY_ID"] = "minio"
os.environ["AWS_SECRET_ACCESS_KEY"] = "minio123"
os.environ["MLFLOW_S3_ENDPOINT_URL"] = "http://minio-service.kubeflow.svc:9000"
client = MlflowClient("http://mlflow-server-service.mlflow-system.svc:5000")
filter_string = f"name='{model_name}'"
results = client.search_model_versions(filter_string)
for res in results:
if res.version == str(version):
model_uri = res.source
break
reconstructed_model = mlflow.pytorch.load_model(model_uri)
return reconstructed_model
- Artifact Store인 minio의 계정정보와 endpoint url을 환경변수로 지정하고 MLflow의 endpoint url을 통해 MLflowClient를 초기화한다. pipeline은 쿠버네티스에서 동작하기 때문에 endpoint url은 DNS lookup이 가능하다.
- 모델은 PyTorch로 작성되었기 때문에 mlflow.pytorch.load_model 함수를 사용하며 파라미터로 특정 모델의 s3 endpoint를 넘겨 모델을 load 한다.
bento_push.py
- MLflow에서 불러온 모델을 bentoml로 감싼 모델로 저장하고 shell script를 실행하는 역할
import argparse
import subprocess
import shlex
from mlflow_model import load_model
import bentoml
def bento_serve(opt):
model = load_model(model_name=opt.model_name, version=opt.model_version)
for param in model.parameters():
param.requires_grad = False
bentoml.pytorch.save_model("surface_classifier", model)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--model-name', type=str, help='MLflow model name')
parser.add_argument('--model-version', type=int, help='MLFlow model version')
parser.add_argument('--api-token', type=str, help='MLFlow model version')
opt = parser.parse_args()
bento_serve(opt)
subprocess.run(["chmod", "+x", "bento_command.sh"])
subprocess.call(shlex.split(f"./bento_command.sh {opt.api_token} http://{NODE IP}:30080"))
- bento_serve 함수 안에서 모델의 파라미터에 requires_grad=False를 주지 않으면 runner에서 아래와 같은 에러가 발생한다.
- RuntimeError: Inference tensors cannot be saved for backward. To work around you can make a clone to get a normal tensor and use it in autograd.
- bentoml.pytorch.save_model 함수로 모델을 bentoml에 저장한다. (bentoml models list 로 저장된 모델 검색 가능)
bento_command.sh
- 컨테이너에서 bentoml 명령어를 수행하는 역할
#!/bin/bash
TOKEN=$1
URL=$2
bentoml yatai login --api-token $TOKEN --endpoint $URL
bentoml build
bentoml push surface_convnext:latest
- bentoml yatai login: 쿠버네티스에 배포된 Yatai에 통신가능하도록 로그인
- bentoml build: 저장되어 있던 모델과 bentos(bento service)를 build하는 명령어 (bentoml list 로 bentos 검색 가능), build 를 하기 위해서는 bentofile.yaml 파일이 현재경로에 반드시 존재해야 한다.
- bentoml push: 모델과 bentos(bento service)를 Yatai에 등록하는 명령어
- “surface_convnext”는 bentoml service 이름
bentofile.yaml
- 서비스와 Inference 과정에 필요한 정보를 가지고 있는 yaml 파일
# bentofile.yaml
service: "service.py:svc" # A convention for locating your service: <YOUR_SERVICE_PY>:<YOUR_SERVICE_ANNOTATION>
labels:
owner: jeff
stage: demo
include:
- "*.py" # A pattern for matching which files to include in the bento
python:
packages: # inference에 필요한 라이브러리
- torch
- torchvision
- pillow
- numpy
- albumentations
- timm
service.py
- Service api와 inference 코드가 정의된 파일
import bentoml
import numpy as np
from bentoml.io import Image, NumpyNdarray
import torch
import albumentations as A
from albumentations.pytorch import ToTensorV2
transform = A.Compose([
A.Resize(224, 224),
A.Normalize((0.6948057413101196, 0.6747249960899353, 0.6418852806091309), (0.1313374638557434, 0.12778694927692413, 0.12676562368869781)),
ToTensorV2(),
])
SURFACE_CLASSES = ['negatieve', 'positive']
surface_clf_runner = bentoml.pytorch.get("surface_classifier:latest").to_runner()
svc = bentoml.Service("surface_convnext", runners=[surface_clf_runner])
@svc.api(input=Image(), output=NumpyNdarray())
def classify(imgs):
# inference preprocess
imgs = np.array(imgs)
imgs = transform(image=imgs)['image']
imgs = imgs.unsqueeze(0)
result = surface_clf_runner.run(imgs)
return np.array([SURFACE_CLASSES[i] for i in torch.argmax(result, dim=1).tolist()])
- bento_push.py 파일에서 저장한 surface_classifier를 불러와 runner로 정의한다. 이때 save당시 tag값은 hash값으로 지정되었기 때문에 latest를 붙여 불러온다.
- svc 변수에 bentoml service를 초기화하며 이때의 이름(surface_convnext)이 bentos의 이름이 된다.
- inference 코드인 classify 함수를 정의하고 데코레이터로 api를 붙여준다 이때 bentoml.io 모듈에 있는 클래스로 inference input과 output의 타입 정의한다. (Input: Image, Output: numpy ndarray)
Dockerfile
- kubeflow 파이프라인을 수행하는 Docker Image를 build하기 위한 용도
FROM pytorch/pytorch:latest
RUN apt-get -y update && apt-get install -y libzbar-dev
RUN pip install -U mlflow boto3 protobuf~=3.19.0 bentoml==1.0.0 timm sqlalchemy==1.3.24 albumentations
RUN mkdir -p /app
COPY . /app/
WORKDIR /app
ENTRYPOINT ["python", "bento_push.py" ]
# Dockefile 폴더 경로로 이동
docker build -t {DOCKER ID}/{IMAGE NAME}:{TAG} .
docker push {DOCKER ID}/{IMAGE NAME}:{TAG}
기존에 kfp 모듈로 작성한 pipeline에 serve pipeline을 추가하는 과정은 생략. (작성 방법은 여기 참조)
Kubeflow Pipeline Run
Explain
위 DAG(Directed Acyclic Graph)가 전체 파이프라인 구조이다. 이미 지난 포스팅에서 test-model 파이프라인까지 수행하였으므로 MLflow에 모델이 저장되어 있는 상태이다.
MLflow dashboard에 surface라는 이름의 모델이 2개의 버전으로 저장되어 있다. 다시 kubeflow로 돌아와 pipeline create run을 클릭해 아래와 같이 파라미터를 주었다.
- MODE_hyp_train_test_serve: 수행할 파이프라인 선택
- SERVE_model_name: MLflow에 저장된 모델의 이름
- SERVE_model_version: MLflow에 저장된 모델의 버전
- SERVE_api_token: Yatai의 API Token (위에서 복사한 API Token을 여기서 사용)
Run Result
- Run 수행 결과 로그를 보면 최초에 yatai login이 수행되고 모델과 bentos에 대해 build와 push가 순차적으로 진행되는 것을 볼 수 있다.
- Yatai Web UI를 확인해보면 모델과 Bentos가 동시에 등록된 것을 볼 수 있다.
- 코드작성에서 설명한 대로 모델의 이름은 surface_classifier이며 bentos(서비스) 이름은 surface_convnext 이다.
Deploy with Yatai
Yatai 공식문서에 따르면 배포를 하기 위한 방법으로 두 가지가 존재한다.
- Web UI에서 버튼을 통해 간편하게 배포하는 방법
- BentoDeployment 오브젝트를 가지는 yaml 파일을 작성하여 kubectl 커맨드로 배포하는 방법
여기서는 Web UI에서 배포하려고한다. Web UI에서 배포를 클릭하고 생성 버튼을 클릭한다.
- 클러스터: Yatai를 설치하면서 자동 생성된 default 클러스터 선택
- Kuberentes 네임스페이스: 배포될 네임스페이스 위치
- 배포이름: Kubernetes 서비스 이름
- Bento 저장소: Yatai에 등록된 bentos 선택
- Bento: surface_convnext라는 이름의 bentos에서 배포할 태그 선택
- 복제 수: replicas 수
- 복제자원
- CPU 자원 요청: cpu request
- CPU 자원 제한: cpu limit / 기본 1000m 이지만 infernece 속도를 고려해 4000m으로 설정
- 메모리 자원 요청: memory request
- 메모리 자원 제한: memory limit
작성 후 위에 제출 버튼을 클릭하면 배포가 시작된다.
해당 배포를 클릭해서 들어가 복제를 클릭해 보면 아래와 같이 쿠버네티스에 배포된 pod의 현재 상태를 확인할 수 있다.
배포가 정상적으로 완료되면 request를 보내어 예측 결과를 얻을 수 있다. 현재 쿠버네티스 1.21 kubeadm에서 Nginx ingress가 정상적으로 동작하지 않기 때문에 서비스 타입을 NodePort로 변경하여 Inference를 수행해보자.
kubectl edit svc surface-convnext-1 -n yatai
# type을 ClusterIP에서 NodePort로 변경
http://{Node IP}:{Node Port}에 접속하면 아래와 같은 Swagger UI로 접속이 된다.
Swagger Inference
[Post]- [try-out]-[파일 선택]-[Execute] 를 순차적으로 클릭하면 예측결과를 얻을 수 있다. Inference에 사용한 이미지와 예측 결과는 아래와 같다.
Python Inference
import requests
url = "http://{NODE IP}:{NODE PORT}/classify"
test_files = {
"test_file_1": open("{IMAGE PATH}", "rb")
}
response = requests.post(url, files=test_files)
print(response.json())
output:
End
이번 포스팅에서는 Bentoml v1.0.0에 대해 간략하게 소개하고 Kubeflow-MLflow-Yatai를 연계하여 모델 배포를 해보았다. Yatai Web UI 에서는 kubectl CLI 명령어에서 작업할 수 있는 기능을 어느정도 구현해 놓아 손쉽게 배포 및 관리를 할 수 있다. 이 글에서는 다루지 않은 Adaptive Batching 기능과 request, response 모니터링에 대해서는 다음 포스팅에서 다룰 예정이다.
keep going
Reference
- BentoML Doc: https://docs.bentoml.org/en/v1.0.0/guides/migration.html
- Yatai GitHub: https://github.com/bentoml/Yatai
댓글남기기