출처 : 변성윤님 블로그.
출처 : 부스트캠프 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부터 개발 시작해서 최근 가파른 성장
- 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
- /Users/philhoonoh/.docker/config.json 에서 credHelpers 삭제
- https://stackoverflow.com/questions/61933284/docker-compose-asking-for-gcloud-credentials
# 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()
- 다음 라이브러리의 Artifact를 지원
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/
- --target_dir flag를 사용
3.9 WEB UI
- @bentoml.web_static_content를 사용하면 웹 프론트엔드에 추가할 수 있음
- 참고 링크 : https://github.com/bentoml/gallery/tree/master/scikit-learn/iris-classifier
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 코드 리팩토링하기
- https://github.com/zzsza/Boostcamp-AI-Tech-Product-Serving/tree/main/part4/01-bentoml/app
- /Users/philhoonoh/Desktop/product_serving/Boostcamp-AI-Tech-Product-Serving/part4/01-bentoml/app
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)
- 기존 requests하는 URL을 5001로 변경하면 됨
'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 |
댓글