이번글에서는 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 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 공식문서에 따르면 배포를 하기 위한 방법으로 두 가지가 존재한다.

  1. Web UI에서 버튼을 통해 간편하게 배포하는 방법
  2. 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

업데이트:

댓글남기기