네이버 부스트캠프 ai tech 변성윤님의 model serving 강의자료를 보며 실습하고 정리하는 글입니다. 틀린점이나, 문제가 있다면 말씀해주세요 :)
변성윤님이 직접 정리해주신 bentoml !
후에는 위에걸로 다시 실습해보자.
BetoML
-- Introduction
-- BentoML 소개
-- BentoML 특징
-- BentoML이 해결하는 문제
BentoML 시작하기
-- BentoML 설치하기
-- BentoML 사용 Flow
BentoML Component
-- BentoService
-- Service Environment
-- Model Artifact
-- Model Artifact Metadata
-- Model Management & Yatai
-- API Function and Adapters
-- Model Serving
-- Labels
-- Retrieving BentoServices
-- WEB UI
BentoML으로 Serving 코드 리팩토링하기
-- BentoService 정의하기
-- Model Pack
FastAPI로 직접 머신러닝 서버 개발
-- 1~2개의 모델을 만들 때는 시도하면서 직접 개발 가능
만약 30개~50개의 모델을 만들어야한다면?
-- 많은 모델을 만들다보니 반복되는 작업이 존재(Config, FastAPI 설정 등)
여전히 Serving은 어렵다.
이 조차도 더 빠르게 간단하게 하고 싶다. 더 쉽게 만들고 싶다. 추상화 불가능할까?
더 쉬운 개발을 위해 본질적인 "serving"에 특화된 라이브러리를 원하게 됨
이런 목적의 라이브러리들이 점점 등장하기 시작
Serving Infra는 모든 회사에서 사용될 수 있는 Base Component
많은 라이브러리가 등장하고 있음
모든 라이브러리는 해결하고 하는 핵심 문제가 존재
어떻게 문제를 해결했는지가 다른 라이브러리
Serving에 집중하는 가벼운 Library, BentoML !
2019부터 개발 시작해서 최근 가파른 성장
관련 Library
문제 1: Model Serving Infra의 어려움
문제 2: Online Serving의 Monitoring 및 Error Handling
문제 3: Online Serving 퍼포먼스 튜닝의 어려움
pip install bentoml
모델 학습 코드 iris Classifier
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하기 위해 해야하는 작업
예측할 때 사용하는 API를 위한 Class
#bento_service.py
import pandas as pd
from bentoml import env, artifacts,
from bentoml.adapters import DataframeInput
from bentoml.frameworks.sklearn import SklearnModelArtifact
@env(infer_pip_packages=True)
@artifacts([SklearnModelArtifact('model')])
class IrisClassifier(BentoService):
@api(input=DataframeInput(), batch=True) # /predict로 접근 가능
def predict(self, df: pd.DataFrame):
"""
Doc String !!!
"""
return self.artifacts.model.predict(df)
@env : 파이썬 패키지, install script 등 서비스에 필요한 의존성을 정의
@artifacts : 서비스에서 사용할 Artifact 정의 => Sklearn
BentoService를 상속하면, 해당 서비스를 Yatai(모델 이미지 레지스트리)에 저장
@api : API생성
-- Input과 Output을 원하는 형태(Dataframe, Tensor, JSON 등)으로 선택할 수 있음
-- Doc String으로 Swagger에 들어갈 내용을 추가할 수 있음
@artifacts에 사용한 이름을 토대로 self.artifacts.model로 접근
-- API에 접근할때 해당 Method 호출
학습한 모델을 Prediction Service에 Pack
#bento_packer.py
#모델 학습
from sklearn import svm
from sklearn import datasets
clf = svb.SVC(gamma='scale')
iris = datasets.load_iris()
X,y= iris.data, iris.target
clf.fit(X,y)
from bento_service import IrisClassifier
#IrisClassifier 인스턴스 생성
iris_classifier_service= IrisClassifier()
iris_classifier_service.pack('model',clf)
#Model Serving을 위한 서비스를 Diskdp 저장
saved_path = iris_classifier_service.save()
bento_service에 정의한 Prediction Service
Model Artifact를 주입
BentoML Bundle : Prediction Service를 실행할 때 필요한 모든 코드, 구성이 포함된 폴더, 모델 제공을 위한 바이너리
CLI에서 bento_packer.py 실행 => Saved to ~ 경로가 보임
BentoML에 저장된 Prediction Service 확인
bentoml list
BentoML에 저장된 Prediction Service 폴더로 이동 후, 파일확인
cd /Users/{User name}/bentoml/repository
tree 명령어를 통해 파일 구조 확인
우리가 생성한 bento_service.py
Code와 동일
우리가 만들지 않은 init.py
bentoml.yml에 모델의 메타정보가 저장됨
Dockerfile도 자동으로 생성되며, 설정을 가지고 설치하는 코드
다른 이름의 모델이 Pack되면 어떻게 될까?!
bento_service.py의 IrisClassifier 클래스를 IrisClassifier1로 수정
(bento_packer.py의 import부분도 수정)한 후 코드 실행
새로운 IrisClassifier1 이 생성됨
bentoml list
다음 명령어로 Serving
bentoml serve IrisClassifier:latest
웹서버 실행
localhost:5000로 접근하면 Swagger UI가 보임
우리가 생성한 /predict를 클릭하면 코드에서 정의한 내용을 볼 수 있고, API Test도 가능
Try it out 클릭
Request Body에 임의로 Input을 넣고 Execute !
이번엔 파라미터를 맞춰서 Execute !
Response 200에 예측값 0 !
BentoML serve한 터미널에선 다음과 같은 로그가 발생
로그는 ~/bentoml/logs에 저장됨
cat predict.log
로 위의 로그 확인 가능
터미널에서 curl로 Request해도 정상적으로 실행됨
curl -i\
--header "Content-Type: application/json"\
--request POST\
--data'[[5.1,3.5,1.4,02.]]'\
http://localhost:5000/predict
bentoml yatai-service-start
Web UI : localhost:3000
bentoml containerize IrisClassifier:latest -t iris-classifier
조금 조금 오래 오래 기다리면 Container Image가 빌드됨
Docker Image로 빌드된 이미지 확인
docker images
docker명령어나 FastAPI를 사용하지 않고 웹 서버를 띄우고, 이미지 빌드 !
=> 예전보다 더 적은 리소스로 개발 가능
# 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'],\
docker_base_image="bentoml/model-server:0.12.1-py38-gpu")
@bentoml.artifacts([KerasModelArtifact('model'), PickleArtifact('tokenizer')])
class TensorflowService(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 our card
config.experimental.set_memory_growth(gpu[0],True)
model = load_model()
tokenizer=load_tokenizer()
bento_svc= TensorflowService()
bento_svc.pack('model',model)
bento_svc.pack('tokenizer', tokenizer)
saved_path= bento_svc.save()
@bentoml.env(
requirements_txt_file="./requirements.txt"
)
class ExamplePredictionService(bentoml.BentoService):
@bentoml.api(input=DataframeInput(), batch=True)
def predict(self, df):
return self.artifacts.model.predict(df)
#setup_sh
@bentoml.env(
infer_pip_package=True,
setup_sh="./my_init_script.sh"
)
class ExamplePredictionService(bentoml.BentoService):
...
@bentoml.evn(
infer_pip_packages=True,
setup_sh="""\
#!/bin/bash
set -e
apt-get install --no-install-recommands nvidia-driver-430
...
"""
)
class ExamplePredictionService(bentoml.BentoService):
...
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_classifer)
svc.set_version("2022-01.iteration20")
svc.save()
#The final produced BentoService bundle will have version:
# "1.4 2019-08.iteration20"
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 exam
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()
다음 라이브러리의 Artifact를 지원
svc=MyPredictionService()
svc.pack(
'model_a',
my_sklearn_model_object,
metadata={
'precision_score': 0.876,
'created_by': 'JODONG2'
}
)
svc.pack(
'model_b',
my_xgboost_model_object,
metadata={
'precision_score': 0.792,
'mean_absolute_error': 0.88
}
)
svc.save()
bentoml get model:version
bentoml serve 한 후, /metadata 로 접근
from bentoml import load
svc = load('path_to_bento_service')
print(svc.artifacts['model'].metadata)
bentoml list
bentoml get Iris Classifier
bentoml yatai-service-start
BentoService API는 클라이언트가 예측서비스에 접근하기 위한 End Point 생성
Adapter는 Input/Output을 추상화해서 중간 부분을 연결하는 Layer
-- 예) csv파일 형식으로 예측 요청할 경우 => DataframeInput을 사용하고 있으면 내부적으로 pandas.DataFrame객체로 변환하고 API함수에 전달함
@bentoml.api를 사용해 입력받은 데이터를 InputAdapter 인스턴스에 넘김
데이터 처리하는 함수도 작성할 수 있음
from my_lib import preprocessing,postprocessing, fetch_user_profile_from_database
class ExmplePredictionService(bentoml.BentoService):
@bentoml.api(input=DataframeInput(), batch=True)
def predict(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)
from typing import List
from bentoml import env, artifacts, api, BentoService
from bentoml.adapters import JsonInput
from bentoml.types import JsonSerializable, InfereceTask # type annotations are optional
@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' field")
results= self.artifacts.classifier(model_input)
return results
import bentoml
from bentoml.types import JsonSerializable, InferenceTask, inferenceError
class MyService(bentoml.BentoService):
@bentoml.api(input=JsonInput(), batch= False)
def predict(self,parsed_json: JsonSerializable, task: InferenceTask) -> InfereceResult:
if task.http_headers['Accept'] == 'application/json':
return InferenceResult(
data=predictions[0],
http_status=200,
http_headers={"Content-Type":"application/json"},
)
else:
return InferenceError(
err_msg="application/json output only", http_status =400
)
from my_lib import process_custom_json_format
class ExamplePredictionService(bentoml.BentomlService):
@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)
BentoService가 벤또로 저장되면, 여러 방법으로 배포할 수 있음
bentoml retrieve ModelServe --target_dir=~/bentoml_bundle/
bentoml --help
github boostcamp model serving repo 확인
import 된 모듈로 패키지 의존성을 추론해 추가
마스크 분류 모델에서 사용한 PytorchModelArtifact추가
image input을 사용해서 업르도된 이미지로부터 imageio.Array를 함수 인자로 주입
output은 json으로 클라이언트에게 제공
사용하기 전에 공식 Document를 보는 습관을 갖자
@api 데코레이터가 붙은 Method말고도 self.transform 같은 추가 Method도 사용가능
Service를 초기화하고 Model Load
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 = troch.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)