Model Serving 관련 라이브러리를 찾아보면 항상 나오는 BentoML. 사용하기로 마음먹고 이용법을 찾아봐도 한국어 자료가 많지 않을 뿐만 아니라, 있어도 예전 버전(0.13.1.) 기준으로 작성되어 있는 자료가 대부분이다(심지어 로고도 바뀜). 이번 포스트에서는 v1.0 이후 BentoML 사용법에 대해서 알아본다.
What is BentoML?
BentoML은 기본적으로 Model Serving에 특화된 라이브러이다. 만들어진 머신러닝 모델에 대해 API를 뚫고, 패키징하고, 컨테이너화 하는 것을 표준화하는 동시에 매우 편하게 작업을 수행할 수 있도록 지원한다. 백엔드나 데브옵스 지식이 없더라도 데이터 사이언티스트 혹은 ML 엔지니어가 모델을 쉽게 서빙하고 배포하는 것을 지원하는 라이브러리이다.
엄청 쉬운 사용성과 Online/Offline Serving 지원, Adaptive Batching 지원, 메이저 ML 프레임워크(Tensorflow, PyTorch, XGBoost, Detetron 등) 대부분 지원, 클라우드(AWS, GCP, Azure 등) 지원 등등등... 정말 안 쓸 이유가 없이 편리하고 연동하기에도 좋다.
(개인적으로 써보면서 확실히 ML 엔지니어 혹은 데이터 사이언티스트 타깃인 제품이라고 느꼈다. 여담으로 (이후 좀 더 자세히 보겠지만) 이전 v0에서는 코드 위주로 작성해야 했던 것들이, v1에서는 yaml 파일 작성하는 것으로 대체되는 것을 보면서 BentoML의 컨셉이 개발에서 옵스 위주로 설정되었다는 것을 느꼈다.)
기본적인 사용 흐름 + 찍먹해보기
기본적인 사용 흐름을 먼저 보고 가는 것이 좋을 것 같다
- 학습된 모델 가져오기 [김밥 원재료 가져오기]
- 학습된 모델을 Runner(모델 실행단위 정도로 우선 생각) 인스턴스로 만들기 [원재료 바탕으로 김밥 재료 만들기 ex. 지단]
- Runner 인스턴스들을 bentoml.Service 안에 넣고 인스턴스화 [내가 원하는 재료들을 김+밥 위에 올리기 ex. 참치김밥이면 참치 + 깻잎 +...]
- 만들어진 Service instance를 데코레이터(@서비스명.api)로 감싸 api 정의 [김밥 말기]
- bentofile.yaml 파일을 생성하여 Service를 포함, 의존성들 다 적어주고 bento build하여 단일 아티팩트 bento 만들기 [김밥 도시락에 넣기]
- 빌드된 bento를 dockerize, cloud로 push 등등으로 배포하기 [김밥 도시락 배달하기]
기본 흐름을 알았으니 찍먹해보자. 공식 문서에 있는 Deploy a Transformer model with BentoML 예제를 참고하여 작성하였다.
python 3.8이상으로 가상환경을 만들어준 후 bentoml을 pip install 해준다 (실습을 위해 transformers와 torch도 받는다)
$ conda create -n bentoml python=3.8
$ conda activate bentoml
$ pip install bentoml transformers torch
bentoml을 설치하면 기본적으로 ~/bentoml 폴더가 기본 폴더로 설정된다. 만약 다른 곳에서 작업하고 싶다면 export로 디렉토리 설정
우선 bentoml이 제공하는 로컬 모델 스토어에 모델을 다운받기 위한 코드 파일 작성. (허깅페이스에 있는 distilbart 모델을 하나 다운받음)
# download_model.py
import transformers
import bentoml
bentoml.transformers.save_model(
task,
transformers.pipeline("summarization", model="sshleifer/distilbart-cnn-12-6"),
metadata=dict(model_name=model),
)
스크립트를 실행시켜 모델을 다운받는다 (시간 좀 걸림)
$ python download_model.py
모델이 다 다운로드 받으면 CLI 명령을 통해 모델 리스트 확인도 가능하다. 지금은 python 스크립트로 모델을 다운받았지만, 실제 사용할 때는 자체적으로 제작한 모델이나 ML팀에서 제작한 모델을 import해와서 사용하면 된다.
$ bentoml models list
이제 본격적으로 위에서 소개한 1~6단계를 실행한다. 우선 1번(모델 가져오기), 2번(runner 인스턴스 만들기), 3번(service 안에 runner을 넣고 인스턴스화), 4번(만들어진 service 데코레이터로 감싸고 api 따기)를 해보겠다.
# service.py
import bentoml
# 1번: bentoml.models.get으로 모델 가져오기
# 2번: .to_runner()로 runner 인스턴스 만들기
sum_runner = bentoml.models.get("summarization:latest").to_runner()
# 3번: bentoml.Service에 runner 인스턴스 넣기
svc = bentoml.Service(
name="summarization", runners=[sum_runner]
)
# 4번: service 인스턴스 데코레이터로 감싸고 api 따기
@svc.api(input=bentoml.io.Text(), output=bentoml.io.Text())
async def model_inference(text: str) -> str:
result = await sum_runner.async_run(text, max_length=3000)
return result[0]["summary_text"]
4번까지해서 만들어진 김밥을 도시락에 넣지 않고 바로 서빙할 수도 있는데, 이는 bentoml serve로 가능하다. 아래 커맨드라인 명령을 통해 로컬호스트 3000번 포트로 서버가 하나 띄워진다
$ bentoml serve service:svc # 서비스모듈명:서비스인스턴스명
-d 에 원하는 내용을 넣어 서버로 보내보자
curl -X 'POST' \
'http://0.0.0.0:3000/summarize' \
-H 'accept: text/plain' \
-H 'Content-Type: text/plain' \
-d 'Korea is a peninsular region in East Asia. Since 1945, it has been divided at or near the 38th parallel, with North Korea comprising its northern half and South Korea comprising its southern half. Korea consists of the Korean Peninsula, Jeju Island, and several minor islands near the peninsula.'
# result
Korea consists of the Korean Peninsula, Jeju Island, and several minor islands near the peninsula . Since 1945, it has been divided at or near the 38th parallel . North Korea comprises its northern half and South Korea its southern half . The peninsula is a peninsular region in East Asia
하지만 내가 만든 김밥은 외부에 있는 사람은 먹지 못한다... 외부 사람에게도 내가 만든 김밥을 맥이기 위해 도시락에 넣어보자. 이 도시락을 Bento라고 하며 service를 돌리기 위한 소스 코드, 모델 파일, 여러 종속성들을 하나의 아티팩트로 빌드한 것을 말한다.
먼저 bentofile.yaml 파일을 작성한다.
# bentofile.yaml
service: 'service:svc'
include:
- '*.py'
python:
packages:
- torch
- transformers
models:
- summarization:latest
커맨드라인으로 bentoml build (--docker 옵션 사용하면 컨테이너 하나를 띄워서 그 안에서 빌드 시키는 것도 가능함)
$ bentoml build
만들어진 bento는 로컬의 bento store에 저장된다. (즉 모델 스토어, 벤토 스토어 두 개의 스토어가 있는 것) bento 확인은 아래와 같이 커맨드라인으로 가능하다
$ bentoml list
빌드 완료된 벤토는 바로 서버로 띄워볼 수도 있고 (참고로 bento의 이름은 bentoml.Service의 이름으로 설정된다)
# 바로 띄워보기
$ bentoml serve summarization:latest
도커라이즈하거나 yatai를 통해 쿠버네티스로 배포할 수도 있다. 혹은 BentoML에서 개발하는 SaaS 플랫폼인 BentoCloud에 push 가능하다. 충격적이게도 containerize 명령 한 번 내리면 BentoML에서 알아서 Dockerfile을 작성해서 이미지로 빌드해준다...
# dockerize
$ bentoml containerize summarization:latest
# for Apple Silicon
$ bentoml containerize --platform/linux/amd64 summarization:latest
추가: ver0 vs ver1
BentoML의 버전이 1 이상으로 되면서 많은 부분이 변화하였다. 개인적으로는 엄청 사용성이 좋아졌다고 생각하긴 하는데, 그 방향성은 위에서도 이야기 했던 것처럼 스크립트 코드 베이스에서 yaml 베이스로 넘어간 느낌. 최근 MLOps 제품들과 비슷하게 Ops 혹은 DevOps의 관점에서 조금 더 서빙 방법을 고도화한 느낌이었다.
1. 자체적인 모델 스토어 추가
.save_model API를 통해 학습된 모델을 로컬 모델 스토어에 저장할 수 있다. 또한 모델 스토어를 관리하는 커맨드를 추가하였다. (export하여 외부 모델 스토어에 저장하는 것도 가능)
$ bentoml models list # 모델 리스트 출력
$ bentoml models get {모델이름:태그} # 모델에 대한 상세 정보 출력
$ bentoml models delete {모델이름:태그} -y # 모델 스토어에서 모델 지우기
# model export, import (.bentomodel 파일로 다뤄짐)
$ bentoml models export {모델이름:태그} {export 결과 저장 경로}
$ bentoml models import {import할 파일의 상대경로}
# 외부 모델 스토어에 export 예시
$ bentoml models export iris_clf:latest s3://my_bucket/my_prefix/
2. Service를 정의하는 방법
기존 여러 데코레이터를 사용하여 정의하는 방법에서 runner 개념 도입, 메소드를 사용, yaml 파일 등을 이용한 정의 방법으로 변경되었다.
이전에는 정의하고자 하는 Service 클래스 위에 해당 service에서 사용할 ML 모델을 @artifacts로 아티팩트를 등록, service의 환경을 @env로 설정해주는 방법을 사용하였다. 또한 API의 입출력 데이터 형태를 bentoml.adapters를 통해 import 하여 사용하였다.
import pandas as pd
from bentoml import env, artifacts, api, BentoService
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)
def predict(self, df: pd.DataFrame):
return self.artifacts.model.predict(df)
ver1.0 이후에는 모델 스토어에서 사용하고자 하는 ML 모델을 Runner 인스턴스들로 만들고, 이 Runner 인스턴스들을 Service 안에 리스트 형태로 넣는 방식으로 바뀌었다. (Runner란 서빙 로직 단위, 음... 추상적으로는 service 안에서 실제 돌아가는 프로세스 혹은 워커 같다고 느꼈다. 예를 들어 하나의 service안에 text detection 모델로 만든 runner1, text recognizer 모델로 만든 runner2, parser 모델로 만든 runner 3을 리스트로 묶어서 넣을 수 있다)
api 데코레이터의 경우 service 클래스 내에서 정의되는 것이 아닌, service 클래스로 인스턴스를 만들고 후에 인스턴스이름@api 데코레이터 형태로 사용하는 것으로 변경되었다.
bentoml.adapters로 조금 더 직관적인 이름인 bentoml.io로 변경되었으며 추가적으로 multiple 입출력 또한 지원하게 되었다
import numpy as np
import pandas as pd
import bentoml
from bentoml.io import NumpyNdarray, PandasDataFrame
iris_clf_runner = bentoml.sklearn.get("iris_clf:latest").to_runner()
svc = bentoml.Service("iris_classifier", runners=[iris_clf_runner])
@svc.api(input=PandasDataFrame(), output=NumpyNdarray())
def predict(input_series: pd.DataFrame) -> np.ndarray:
result = iris_clf_runner.predict.run(input_series)
return result
환경설정의 경우 스크립트가 아닌 bento build를 위한 bentofile.yaml에서 기재한다. 이후 보겠지만, bento 또한 기존 스크립트에서 작성한 반면, v1부터는 프로젝트 폴더에 yaml 파일을 작성하고 build 커맨드를 통해 만든다.
# v0.13.1
@env(pip_packages=["scikit-learn", "pandas"])
# v1.0
service: "service.py:svc"
labels:
owner: bentoml-team
project: gallery
include:
- "*.py"
python:
packages:
- scikit-learn
- pandas
3. Bento 만드는 방법
Bento를 만드는 방법이 도커 이미지 빌드 하듯이 yaml 파일을 작성하고 bentoml build 커맨드를 통해 빌드할 수 있게 되었다. (확실히 뭔가 dev -> ops 로 컨셉이 명확해진 느낌이 있는 것 같다)
기존: 스크립트 코드 기반
# 작성한 서비스 클래스를 import 해온다
from bento_service import IrisClassifier
# import 해온 클래스로 서비스 인스턴스를 만든다
iris_classifier_service = IrisClassifier()
# 서비스 인스턴스를 활용하여 모델 아티팩트를 Pack한다
from sklearn import svm
from sklearn import datasets
iris = datasets.load_iris()
X, y = iris.data, iris.target
clf = svm.SVC(gamma='scale')
clf.fit(X, y)
iris_classifier_service.pack('model', clf)
# pack한 서비스를 저장한다
saved_path = iris_classifier_service.save()
변경: 뭔가 도커스러워진? 사용법 (yaml 작성 -> build)
# bentofile.yaml
service: "service.py:svc"
labels:
owner: bentoml-team
project: gallery
include:
- "*.py"
python:
packages:
- scikit-learn
- pandas
$ bentoml build
# --docker 옵션을 사용하여 컨테이너를 생성, 거기에서 build 시킬 수도 있다
$ bentoml build --docker
4. Bento 관리와 배포
컨테이너화는 이전과 마찬가지로 bentoml containerize 명령으로 가능
# dockerize
$ bentoml containerize {벤토이름:태그}
# for Apple Silicon
$ bentoml containerize --platform/linux/amd64 {벤토이름:태그}
이전에 Bento 관리는 Yatai에 의존했다면, 이제는 로컬 벤토 스토어에서 관리가 가능하다
$ bentoml list # 벤토 리스트 출력
$ bentoml get {벤토이름:태그} # 벤토에 대한 상세 정보 출력
$ bentoml delete {벤토이름:태그} -y # 벤토 스토어에서 모델 지우기
Yatai는 쿠버네티스를 통한 모델 서빙, 배포에 조금 더 특화하는 느낌
BentoCloud라는 조금 더 상위 제품을 만들어서 여기에서 벤토를 pull push, 여러 설정과 협업이 가능해졌다.
또한 bentoctl이라는 커맨드라인 툴을 통해 외부 클라우드 플랫폼 제품에서 벤토를 해들링할 수 있게 되었다.
셋에 대한 자세한 비교는 공식 문서에서
'MLOps' 카테고리의 다른 글
6. API Serving (0) | 2023.02.19 |
---|---|
5. FastAPI (0) | 2023.02.11 |
2. Model Development (0) | 2023.01.27 |