본문 바로가기
MLOps

MLOps - 18. BentoML

by cocacola0 2022. 6. 2.

출처 : 변성윤님 블로그.
출처 : 부스트캠프 AI Tech.

1. BentoML

1.1 Introduction

  • FastAPI로 직접 머신러닝 서버 개발

    • 1~2개의 모델을 만들 때는 시도하면서 직접 개발 가능
  • 만약 30개~50개의 모델을 만들어야 한다면?

    • 많은 모델을 만들다보니 반복되는 작업이 존재(Config, FastAPI 설정 등)
    • 여전히 Serving은 어렵다
    • 이 조차도 더 빠르게 간단하게 하고 싶다. 더 쉽게 만들고 싶다. 추상화 불가능할까?
  • 더 쉬운 개발을 위해 본질적인 “Serving”에 특화된 라이브러리를 원하게 됨

    • 이런 목적의 라이브러리들이 점점 등장하기 시작
    • 모든 라이브러리는 해결하려고 하는 핵심 문제가 존재
    • 어떻게 문제를 해결했는지가 다른 라이브러리

1.2 BentoML이 해결하는 문제

  • 문제 1: Model Serving Infra의 어려움

    • Serving을 위해 다양한 라이브러리, Artifact, Asset 등 사이즈가 큰 파일을 패키징
    • Cloud Service에 지속적인 배포를 위한 많은 작업이 필요
    • BentoML은 CLI로 이 문제의 복잡도를 낮춤(CLI 명령어로 모두 진행 가능하도록)
  • 문제 2: Online Serving의 Monitoring 및 Error Handling

    • Online Serving으로 API 형태로 생성
    • Error 처리, Logging을 추가로 구현해야 함
    • BentoML은 Python Logging Module을 사용해 Access Log, Prediction Log를 기본으로 제공
    • Config를 수정해 Logging도 커스텀할 수 있고, Prometheus 같은 Metric 수집 서버에 전송할 수 있음
  • 문제 3: Online Serving 퍼포먼스 튜닝의 어려움

    • BentoML은 Adaptive Micro Batch 방식을 채택해 동시에 많은 요청이 들어와도 높은 처리량을 보여줌

1.3 BentoML 소개

  • Serving에 집중하는 가벼운 Library, BentoML

    • 2019부터 개발 시작해서 최근 가파른 성장
  • Bento[벤또] : 일본의 도시락 요리

  • Yatai[야타이] : 일본식 포장마차

1.4 BentoML 특징

  • 쉬운 사용성
  • Online / Offline Serving 지원
  • Tensorflow, PyTorch, Keras, XGBoost 등 Major 프레임워크 지원
  • Docker, Kubernetes, AWS, Azure 등의 배포 환경 지원 및 가이드 제공
  • Flask 대비 100배의 처리량
  • 모델 저장소(Yatai) 웹 대시보드 제공
  • 데이터 사이언스와 DevOps 사이의 간격을 이어주며 높은 성능의 Serving이 가능하게 함

2. BentoML 시작하기

2.1 BentoML 설치하기

  • BentoML은 python 3.6 이상 버전을 지원
  • pyenv 등으로 python version을 3.8으로 설정
  • 가상 환경 설정(virutalenv or poetry)
    pip install bentoml

2.2 BentoML 사용 Flow

step1. 모델 학습 코드 생성

from sklearn import svm
from sklearn import datasets

clf = svm.SVC(gamma='scale')
iris = datasets.load_iris()
X,y = iris.data, iris.target
clf.fit.(X,y)
  • 위 코드를 (기존에) Serving하기 위해 해야하는 작업
    • FastAPI Web Server 생성
    • Input, Output 정의
    • 의존성 작업(requirements.txt, Docker 등)

step2. Prediction Service Class 생성

  • BentoService를 활용해 Prediction Service Class 생성
  • 예측할 때 사용하는 API를 위한 Class

step3. Prediction Service에 모델 저장(Pack)

  • CLI에서 bento_packer.py 실행 => Saved to ~ 경로가 보임
    $ python bento_packer.py

  • BentoML에 저장된 Prediction Service 확인
    $ bentoml list

  • BentoML에 저장된 Prediction Service 폴더로 이동 후, 파일 확인
  • tree 명령어를 통해 파일 구조 확인

  • 우리가 생성한 bento_service.py Code와 동일
  • /Users/philhoonoh/bentoml/repository/IrisClassifier/20220530135632_6D5EF0/IrisClassifier/bento_service.py

  • 우리가 생성하진 않은 init.py
  • /Users/philhoonoh/bentoml/repository/IrisClassifier/20220530135632_6D5EF0/IrisClassifier/init.py
  • create_bento_service_cli

  • bentoml.yml에 모델의 메타정보가 저장됨
    • 패키지 환경, API Input / Output, Docs 등
  • /Users/philhoonoh/bentoml/repository/IrisClassifier/20220530135632_6D5EF0/IrisClassifier/bentoml.yml

  • Dockerfile도 자동으로 생성되며, 설정을 가지고 설치하는 코드
  • /Users/philhoonoh/bentoml/repository/IrisClassifier/20220530135632_6D5EF0/Dockerfile

  • 다른 이름의 모델이 Pack 되면 어떻게 되는지 확인하기 위해 bento_service.py의 IrisClassifier 클래스를 IrisClassifier1로 수정(bento_packer.py의 import 부분도 수정)한 후 코드 실행
  • IrisClassifier1이 새로 생성됨
  • bentoml list로 확인
$ python bento_packer.py
$ bentoml list

step4. Serving

  • 다음 명령어로 Serving
  • bentoml serve IrisClassifier:latest 웹서버 실행
    $ bentoml serve IrisClassifier:lastest

  • localhost:5000로 접근하면 Swagger UI가 보임

  • 우리가 생성한 /predict을 클릭하면 코드에서 정의한 내용을 볼 수 있고, API Test도 가능 Try it out 클릭
  • 임의로 hi를 넣고 Execute 클릭
  • Curl, Request URL이 보이며, Response 400

  • 이번엔 파라미터를 맞춰서 Execute

  • BentoML serve한 터미널에선 다음과 같은 로그가 발생

  • 로그는 ~/bentoml/logs에 저장됨

  • prediction.log를 확인하면 예측 로그를 확인할 수 있음
$ cat prediction.log
  • 터미널에서 curl로 Request해도 정상적으로 실행됨
$ curl -i --header "Content-Type: application/json" --request POST --data '[[5.1, 3.5, 1.4, 0.2]]' http://localhost:5000/predict

step5. Yatai Service 실행

  • bentoml yatai-service-start

  • Web UI : localhost:3000

step6. Docker Image Build(컨테이너화)

  • bentoml 사용 Docker Image Build
    $ bentoml containerize IrisClassifier1:latest -t iris-classifier

ERROR

Error: bentoml-cli containerize failed: InitializationError('docker-credential-gcloud not installed or not available in PATH') has type InitializationError, but expected one of: bytes, unicodeential-gcloud not installed or not available in PATH      

ERROR Fix

# before
{
  "credsStore": "desktop",
  "credHelpers": {
    "gcr.io": "gcloud",
    "us.gcr.io": "gcloud",
    "eu.gcr.io": "gcloud",
    "asia.gcr.io": "gcloud",
    "staging-k8s.gcr.io": "gcloud",
    "marketplace.gcr.io": "gcloud"
  }
}

# after
{
  "credsStore": "desktop",
}
  • Docker Image로 빌드된 이미지 확인
    docker images

  • docker 명령어나 FastAPI를 사용하지 않고 웹 서버를 띄우고, 이미지 빌드! => 예전보다 더 적은 리소스로 개발 가능

3. BentoML Component

3.1 BentoService

  • bentoml.BentoService는 예측 서비스를 만들기 위한 베이스 클래스
  • @bentoml.artifacts : 여러 머신러닝 모델 포함할 수 있음
  • @bentoml.api : Input/Output 정의
    • API 함수 코드에서 self.artifacts.{ARTIFACT_NAME}으로 접근할 수 있음
  • 파이썬 코드와 관련된 종속성 저장
# bento_svc.py
import bentoml
from bentoml.adapters import JsonInput
from bentoml.frameworks.keras import KerasModelArtifact
from bentoml.service.artifacts.common import PickleArtifact

@bentoml.env(pip_packages=['tensorflow','scikit-learn','pandas'], dokcer_base_image="bentoml/model-server:0.12.1-py38-gpu")
@bentoml.artifacts([KerasModelArtifact('model'), PickleArtifact('tokenizer')])
class TensorflowServic(bentoml.BentoService):
    @api(input=JsonInput())
    def predict(self, parsed_json):
        return self.artifacts.model.predict(input_data)

# bento_packer.py
from bento_svc import TensorflowService

# OPTIONAL : to remove tf memory limit on your card
config.experimental.set_memory_grouth(gpu[0], True)

model = load_model()
tokenizer = load_tokenizer()

bento_svc = TensorflowService()
bento_svc.pack('model', model)
bento_svc.pack('tokenizer', tokenizer)

save_path = bento_svc.save()

3.2 Service Environment

  • 파이썬 관련 환경, Docker 등을 설정할 수 있음

  • @betoml.env(infer_pip_packages=True) : import를 기반으로 필요한 라이브러리 추론

  • requirements_txt_file을 명시할 수도 있음

    @bentoml.env(requirements_txt_file="./requirements.txt")
    class ExamplePredictionService(bentoml.BentoService):
      @bentoml.api(input=DataframeInput(), batch = True)
      def predict(self, df):
          reutrn self.artifacts.model.predict(df)
  • pip_packages=[] 를 사용해 버전을 명시할 수 있음

    @bentoml.env(
      pip_packages = [
          'scikit-learn==0.24.1',
          'pandas @https://github.com/pypa/pip/archive/1.3.1.zip',
      ]
    )
    class ExamplePredictionService(bentoml.BentoService):
      @bentoml.api(input=DataframeInput(), batch = True)
      def predict(self, df):
          reutrn self.artifacts.model.predict(df)
  • docker_base_image를 사용해 Base Image를 지정할 수 있음

import bentoml
from bentoml.adapters import JsonInput
from bentoml.frameworks.keras import KerasModelArtifact
from bentoml.service.artifacts.common import PickleArtifact

@bentoml.env(
    pip_packages=['tensorflow','scikit-learn','pandas'], 
    dokcer_base_image="bentoml/model-server:0.12.1-py38-gpu"
)
@bentoml.artifacts([KerasModelArtifact('model'), PickleArtifact('tokenizer')])
class TensorflowServic(bentoml.BentoService):
    @api(input=JsonInput())
    def predict(self, parsed_json):
        return self.artifacts.model.predict(input_data)
  • setup_sh를 지정해 Docker Build 과정을 커스텀할 수 있음
# example1.py
@bentoml.env(
    infer_pip_packages=True,
    setup_sh="./my_init_script.sh"
)

@bentoml.artifacts([KerasModelArtifact('model'), PickleArtifact('tokenizer')])

class TensorflowServic(bentoml.BentoService):
    @api(input=JsonInput())
    def predict(self, parsed_json):
        return self.artifacts.model.predict(input_data)

# example2.py    
@bentoml.env(
    infer_pip_packages=True,
    setup_sh = 
    """
    #!/bin/bash
    set -e 
    apt-get install --no-install-recommends nvidia-driver-430
    """
)
class TensorflowServic(bentoml.BentoService):
    @api(input=JsonInput())
    def predict(self, parsed_json):
        return self.artifacts.model.predict(input_data)
  • @bentoml.ver를 사용해 버전 지정할 수 있음
from bentoml import ver, artifacts
from bentoml.service.artifacts.common import PickleArtifact

@ver(major=1, minor=4)
@artifacts([PickleArtifact('model')])
class MyMLService(BentoService):
    pass

svc = MyMLService()
svc.pack("model", trained_classifier)
svc.set_version("2019-08.iteration20")
svc.save()

# The final produced BentoService bundle will have version:
# "1.4.2019-08.iteration20"

3.3 Model Artifact

  • @bentoml.artifacts : 사용자가 만든 모델을 저장해 pretrain model을 읽어 Serialization, Deserialization
  • 여러 모델을 같이 저장할 수 있음
  • A 모델의 예측 결과를 B 모델의 Input으로 사용할 경우
  • 보통 하나의 서비스 당 하나의 모델을 권장
import bentoml
from bentoml.adapters import DataframeInput
from bentoml.frameworks.sklearn import SklearnModelArtifact
from bentoml.frameworks.xgboost import XgboostModelArtifact

@bentoml.env(infer_pip_packages=True)
@bentoml.artifacts([
    SklearnModelArtifact("model_a"),
    XgboostModelArtifact("model_b")
])
class MyPredictionService(bentoml.BentoService):
    @bentoml.api(input=DataframeInput(), batch=True)
    def predict(self, df):
        # assume the output of model_a will be the input of model_b in this example
        df = self.artifacts.model_a.predict(df)
        return self.artifacts.model_b.predict(df)

svc = MyPredictionService()
svc.pack('model_a', my_sklearn_model_object)
svc.pack('model_b', my_xgboost_model_object)
svc.save()

3.4 Model Artifact Metadata

  • 해당 모델의 Metadata(Metric - Accuracy, 사용한 데이터셋, 생성한 사람, Static 정보 등)

  • Pack에서 metadata 인자에 넘겨주면 메타데이터 저장

  • 메타데이터는 Immutable

    svc = MyPredictionService()
    svc.pack(
      'model_a',
      my_sklearn_model_object,
      metadata = {
          'precision_score' : 0.876,
          'created_by' : 'joe'
      }
    )
    svc.pack(
      'model_b',
      my_xgboost_model_object,
      metadata = {
          'precision_score' : 0.792,
          'mean_absolute_error' : 0.88
      }
    )
    svc.save()
  • Metadata에 접근하고 싶은 경우

    • 1) CLI

      bentoml get model:version
    • 2) REST API

      • bentoml serve 한 후, /metadata로 접근
    • 3) Python

      from bentoml import load
      svc = load("path_to_bento_service')
      print(svc.artifacts['model'].metadata)

3.5 Model Management & Yatai

  • BentoService의 save 함수는 BentoML Bundle을 ~/bentoml/repository/{서비스 이름}/{서비스 버전}에 저장

  • 모델 리스트 확인

    $ bentoml list

  • 특정 모델 정보 가져오기

    $ bentoml get IrisClassifier

  • YataiService : 모델 저장 및 배포를 처리하는 컴포넌트

    $ bentoml yatai-service-start

3.6 API Function and Adapters

  • BentoService API는 클라이언트가 예측 서비스에 접근하기 위한 End Point 생성

  • Adapter는 Input / Output을 추상화해서 중간 부분을 연결하는 Layer

    • 예) csv 파일 형식으로 예측 요청할 경우 => DataframeInput을 사용하고 있으면 내부적으로 pandas.DataFrame 객체로 변환하고 API 함수에 전달함
  • @bentoml.api를 사용해 입력 받은 데이터를 InputAdapter 인스턴스에 넘김

  • 데이터 처리하는 함수도 작성할 수 있음

from bentoml.adapters import DataframeInput
from my_lib import preprocessing, postprocessing, fetch_user_profile_from_database

class ExamplePredictionService(bentoml.BentoService):
    @bentoml.api(input=DataframeInput(), batch=True)
    def precit(self, df):
        user_profile_column = fetch_user_profile_from_database(df['user_id'])
        df['user_profile'] = user_profile_column
        model_input = preprocessing(df)
        model_output = self.artifacts.model.predict(model_input)
        return postprocessing(model_output)
  • Input 데이터가 이상할 경우 오류 코드를 반환할 수 있음
from typing import List
from bentoml import env, artifacts, api, BentoService
from bentoml.adapters import JsonInput
from bentoml.types import JsonSerializable, InferenceTask

@env(infer_pip_packages=True)
@artifacts([SklearnModelArtifact('classifier')])
class MyPredictionService(BentoService):

    @api(input=JsonInput(), batch=True)
    def predict_batch(self, parsed_json_list: List[JsonSerializable], tasks: List[InferenceTask]):
        model_input = []
        for json, task in zip(parsed_json_list, tasks):
            if "text" in json:
                model_input.append(json['text'])
            else:
                task.discard(http_status=400, err_msg="input json must contain 'text' filed")

        results = self.artifacts.classifier(model_input)
           return results
  • 세밀하게 Response를 정의할 수 있음
import bentoml
from bentoml.types import JsonSerializable, InferenceTask, InferenceError, InferenceResult

class MyPredictionService(BentoService):

    @bentoml.api(input=JsonInput(), batch=False)
    def predict_batch(self, parsed_json: JsonSerializable, tasks: InferenceTask) -> InferenceResult:
        if task.http_headers['Accept'] == "application/json":
            predictions = self.artifact.model.predict([parsed_json])
            return InferenceResult(
                data = predictions[0],
                http_status = 200,
                http_headers={"Content-Type" : "application/json"}
                )
        else:
            return InferenceError(err_msg="application/json input only", http_status=400)
  • BentoService가 여러 API를 포함할 수 있음
from my_lib import process_custom_json_format

class ExamplePredictionService(bentoml.BentoService):
    @bentoml.api(input=DataframeInput(), batch=True)
    def predict(self, df: pandas.Dataframe):
        return self.artifacts.model.predict(df)

    @bentoml.api(input=JsonInput(), batch=True)
    def predict_json(self, json_arr):
        df = process_custom_json_format(json_arr)
        return self.artifacts.model.predict(df)

3.7 Model Serving

  • BentoService가 벤또로 저장되면, 여러 방법으로 배포할 수 있음
    • 1) Online Serving : 클라이언트가 REST API Endpoint로 근 실시간으로 예측 요청
    • 2) Offline Batch Serving : 예측을 계산한 후, Storage에 저장
    • 3) Edge Serving : 모바일, IoT Device에 배포

3.8 Retrieving BentoServices

  • 학습한 모델을 저장한 후, Artifact bundle을 찾을 수 있음
    • --target_dir flag를 사용
      bentoml retrieve ModelServe --target_dir=~/bentoml_bundle/

3.9 WEB UI

from bentoml import env, artifacts, api, BentoService, web_static_content
from bentoml.adapters import DataframeInput
from bentoml.artifact import SklearnModelArtifact

@evn(auto_pip_dependencies=True)
@artifacts([SklearnModelArtifact('model')])
@web_static_content('./static')
class IrisClassifier(BentoService):
    @api(input=DataframeInput(), batch=True)
    def test(self, df):
        return self.artifacts.model.predict(df) 

4. BentoML으로 Serving 코드 리팩토링하기

4.1 BentoService 정의하기

  • import 된 모듈로 패키지 의존성을 추론해 추가
  • 마스크 분류 모델에서 사용한 PytorchModelArtifact 추가
  • image input을 사용해서 업로드된 이미지로부터 imageio.Array를 함수 인자로 주입
  • output은 json으로 클라이언트에게 제공
  • FastAPI에서 썼던 코드와 거의 동일
  • @api 데코레이터가 붙은 Method 말고도 self.transform 같은 추가 Method도 사용 가능
from bentoml import BentoService, api, env, artifacts
from bentoml.adapters import ImageInput, JsonOutput

@env(infer_pip_packages=True)
@artifacts([PytorchModelArtifact("model")])
class MaskAPIService(BentoService):
    @api(input=ImageInput(), output=JsonOutput())
    def predict(self, image_array:Array):
        transformed_image = self.transform(image_array)
        outputs = self.artifacts.model.forward(transforemd_image)
        _, y_hats = outputs.max(1)
        return self.get_label_from_class(class=y_hats.item())

4.2 Model Pack

  • Service를 초기화하고 Model Load
  • 서비스를 Pack하고 Yatai에 저장
if __name__ == "__main__":
    import torch

    bento_svc = MaskAPIService()
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = MyEfficientNet().to(device)
    state_dict = torch.load(
    "../../../assets/mask_task/model.pth", map_location=device
    )
    model.load_state_dict(state_dict = state_dict)
    bento_svc.pack("model",model)
    saved_path = bento_svc.save()
    print(saved_path)
  • bentoml 로 모델 porting
  • http://localhost:$port로 접속하면 Swagger를 확인할 수 있음
    • Test Execute하면 정상적으로 값이 Return!
# model bentoml 로 packing
$ python main.py

# bundle 확인
$ bentoml list

# 해당 서비스 실행
$ bentoml serve MaskAPIService:lastest --port 5001

Error

bentoml TypeError: expected str, bytes or os.PathLike object, not NoneType

Error Fix

- https://github.com/bentoml/BentoML/issues/2084
$ pip install imageio==2.9.0

  • Frontend Code
    • 기존 requests하는 URL을 5001로 변경하면 됨
      response = requests.post("http://localhost:5001/predict", files=files)

'MLOps' 카테고리의 다른 글

MLOps - 20. 머신러닝 디자인 패턴  (0) 2022.06.04
MLOps - 19. Airflow  (0) 2022.06.03
MLOps - 17. MLFlow  (0) 2022.06.01
MLOps - 16.Logging  (0) 2022.05.31
MLOps - 15. Docker  (0) 2022.05.30

댓글