오늘은 BentoML을 이용한 model serving 방법을 설명드리려고 합니다. 정확히는 BentoML을 사용하여 model serving을 위한 model prediction api를 생성하는 것을 목표로 하겠습니다.
1. BentoML이란?
Model serving 방법을 설명드리기 전 BentoML에 대해 간단히 알아보죠. BentoML의 Bento는 일본어이며 한국어로는 도시락을 의미합니다.
- BentoML: ML-powered prediction service 생성을 쉽게 해주는 framework
- BentoML의 Bento는 일본어이며 한국어로는 도시락을 의미
- 도시락이 밥과 반찬이 모두 있는 것처럼 BentoML은 model serving에 필요한 요소들을 모아주기 때문에 이와 같이 naming
- BentoML 장점
- 실제 production service에 ML model을 serving하는 데 있어서 필요한 지식과 시간을 최소화시켜주는 도구
- 다수의 ML 개발자들은 API, docker 사용법을 잘 모른다는 문제를 해소시켜줌
- ML model의 production에 deploy하기까지의 process를 accelerate 및 standarize 함
- Scalable하고 high performance의 prediction service 제공
- 지속적으로 prediction service에 대해 deploy, monitor, operate 가능
- 실제 production service에 ML model을 serving하는 데 있어서 필요한 지식과 시간을 최소화시켜주는 도구
2. Environment Setup
Model serving에 필요한 BentoML version 및 사용한 enviornment입니다. 오늘 serving할 모델은 YOLOv8을 사용할 것입니다.
- BentoML version: 1.0.19 (중요!)
- Torch version: 1.9.0
- Model: YOLOv8s
- Docker
- CPU: Intel(R) Xeon(R) Gold 5120 CPU @ 2.20GHz
- GPU: Tesla V100-PCIE-32GB
3. Model serving with BentoML
BentoML을 이용한 YOLOv8s을 serving하는 방법을 설명드리도록 하겠습니다. 전체 코드는 여기서 확인 가능하십니다.
3.1 Saving a Model
BentoML을 이용하여 YOLOv8s 모델을 BentoML전용 model store 저장합니다. 따로 model store(in local dir)에 저장하는 이유는 모델 버전 관리 및 meta data를 같이 저장하기 위함입니다.
# bentoml_packer.py
import bentoml
from ultralytics import YOLO
model = YOLO("yolov8s.pt").model
model.eval()
saved_model = bentoml.pytorch.save_model(name='yolov8s_model',
model=model,
signatures={"__call__": {"batchable": False}})
print(saved_model)
위와 같이 bentoml.pytorch.save_model
함수를 통해 load한 YOLOv8s model을 yolov8s_model
이라는 이름으로 저장합니다. signatures
parameter에 batchable을 False로 하여 batch를 1만 사용하도록 제한하였습니다.
위 파일을 실행시키면 아래와 같이 정상적으로 model saving되는 것을 확인가능합니다. Tag부분을 보면 docker에 image_name:tag와 똑같이 model_name:tag(yolov8s_model:ukgv3lhwxstqdibw
)형태로 저장됩니다. 그리고 bentoml models list cli를 통해 저장된 model size나 생성 시간 등을 확인가능합니다.
3.2 Creating a Service
Service는 BentoML의 core component이며 model serving의 logic을 담는 주체입니다. YOLOv8 모델을 통해 detection (inference)할 수 있도록 하는 api endpoint를 쉽게 만들 수 있으며 해당 endpoint의 input과 output의 type, shape을 정의가능합니다. 또한 model inference에 필요한 코드도 아래와 같이 작성하면 됩니다.
# service.py
... 생략 ...
import bentoml
from bentoml.io import Image, JSON
yolov8s_runner = bentoml.pytorch.get("yolov8s_model:latest").to_runner()
svc = bentoml.Service("yolov8s_svc", runners=[yolov8s_runner])
def encode_image(input_img):
ratio = 3 # 0~9
encode_param = [cv2.IMWRITE_PNG_COMPRESSION, ratio]
encoded_img = base64.b64encode(cv2.imencode(".png", input_img, encode_param)[1])
return encoded_img.decode("utf8")
... 생략 ...
@svc.api(input=Image(),
output=JSON())
def predict(f: Image):
img_origin, img_tensor = pre_processing(f=f)
out = yolov8s_runner.run(img_tensor)
out_bbox_info, out_img = post_processing(img_origin=img_origin,
img_tensor=img_tensor,
out=out)
enc_out_img = encode_image(out_img)
cls = out_bbox_info.cls.detach().cpu().numpy()
conf = out_bbox_info.conf.detach().cpu().numpy()
coord = out_bbox_info.data[:,:4].detach().cpu().numpy()
res = {'enc_out_img': enc_out_img, 'cls': cls, 'conf': conf, 'coord': coord}
return res
BentoML 코드에만 초점을 맞추어 설명드리기 위해 YOLOv8을 위한 process는 위 코드에서 생략하였습니다.
- 저장한 yolov8s_model을 load하기위해
bentoml.pytorch.get
함수를 사용하였습니다. 그리고to_runner
함수를 통해 모델을 실행(inference)할 수 있는 하나의 computation unit로 만듦- Runner는 remote python worker에서 실행되며 scaling기능을 가지고 있음
bentoml.Service
함수를 통해 yolov8s_svc이름의 service 생성함- Service를 handling하는 주체는
svc
variable - runners인자에 위의 정의한 runner인
yolov8s_runner
를 넣어줌
- Service를 handling하는 주체는
@svc.api(input=Image(), output=JSON())
을 통해 svc service의 inference api endpoint를 만듦- Input의 type을
Image
형태로 받을 것이고 output은Json
형태임을 명시함 predict(f: Image)
을 통해 inference api endpoint 이름은 predict로 정의하고 f라는 parameter를 통해 Image을 받음
- Input의 type을
predict
함수 내에서 inference를 위한 service logic을 구현함pre_processing
함수를 통해 PIL형태의 image를 torch tensor로 바꿈- 위의
bentoml.Service
에서 runners의 인자로 들어간yolov8s_runner
의run
함수를 실행하여 inference진행 post_processing
함수를 통해 detection result image와 detection result info(bbox, class, confidence)를 출력- endpoint선언 시 output은 JSON형태로 보내기로 선언했기 때문에 encoding한 detection result image와 detection result info를 json형태로 보냄
- detection result image인
out_img
를 그대로 json형태로 보낼 경우에out_img
의 data size가 크기 때문에 response time이 느려지는 문제가 발생하므로 encoding하여 보냄
- detection result image인
위 코드에 대해 service 테스트해보도록 하겠습니다. bentoml serve service:svc
cli 을 입력하면 serving 테스트진행하게 됩니다. service:svc
에서 service는 service.py
를 의미하고 svc는 service.py내의 service 주체인 svc
variable을 의미합니다.
위와 같이 출력된다면 정상적으로 service가 실행 중입니다. 위의 log를 살펴보면 http://0.0.0.0:3000
으로 service:svc
을 listening(요청을 받음)하는 것을 알 수 있습니다. 기본적으로 bentoml service에서 사용되는 port는 3000
입니다. 그렇다면 http://0.0.0.0:3000
에 inference를 담당하는 predict api가 정상적으로 작동하는지 테스트하기 위해 다른 terminal를 실행시켜 아래 코드를 실행시켜 봅니다.
# request.py
... 생략 ...
PREDICT_API = "http://0.0.0.0:3000/predict"
ORI_IMG_PATH = './bus.jpeg'
data = subprocess.run(shlex.split(f"curl -F 'fileobj=@{ORI_IMG_PATH};type=image/jpeg' {PREDICT_API}"), stdout=subprocess.PIPE).stdout
dict = json.loads(data)
def decode_image(input_img):
output_img = np.frombuffer(base64.b64decode(input_img.encode('utf8')), np.uint8)
output_img = cv2.imdecode(output_img, cv2.IMREAD_COLOR)
return output_img
out_img = decode_image(dict['enc_out_img'])
cv2.imwrite('./recv_out_img.jpg', out_img)
for coord, cls, conf in zip(dict['coord'], dict['cls'], dict['conf']):
print(f'bbox: {[int(x) for x in coord ]}, class: {int(cls)}, confidence: {conf:.2f}')
- curl 을 통해 image를 predict endpoint에 보냄
- predict endpoint인
http://0.0.0.0:3000/predict
에 image(bus.jpeg)를 post - response받은
dict['enc_out_img']
은 encoded image이므로 decoded하여 저장하였음
- predict endpoint인
위와 같이 정상적으로 predict
api에 image가 post되어 YOLOv8s모델을 통해 detection 된 결과를 받을 수 있습니다. 왼쪽 log에는 detection result info을, 오른쪽 사진은 detection result image를 나타내었습니다. (Class 0은 person, class 5은 bus를 의미합니다.)
3.3 Building a Bento
위의 테스트가 A라는 서버에서 정상적으로 이루어졌다고 가정하고 만약 해당 서비스를 B라는 서버에서 하고 싶다면 어떻게 해야 할까요? B에 가서 A와 똑같은 환경을 만들고 위의 과정을 반복해야 할까요? 이렇게 하는 것은 불필요한 작업 및 시간을 필요로 하고 에러도 발생시킬 수 있습니다.
그래서 이를 해결하기 위해 BentoML을 이용하여 service를 실행시키기 위한 모든 것을 dockerizing하게 됩니다. 구체적으로 dockerizing은 model, service 파일, source code, service에 필요한 환경(PyTorch, Numpy 등등)을 모두 모아 놓는 것이기 때문에 Bento(도시락)를 만든다고도 말할 수 있습니다.
# bentofile.yaml
service: "service:svc" # Same as the argument passed to `bentoml serve`
labels:
owner: da2so
stage: dev
include:
- "*.py" # A pattern for matching which files to include in the bento
- "*.yaml"
exclude:
- "*.pyc"
python:
packages:
- torch==1.9.0+cu111
- torchvision==0.10.0+cu111
- PyYAML==6.0
- loguru
- pandas==1.5.2
- Pillow==9.3.0
- numpy==1.23.5
- opencv-python==4.5.3.56
- thop
- py-cpuinfo
- psutil
- seaborn==0.12.2
- tensorboard==2.8.0
- pybboxes==0.1.6
- tqdm
extra_index_url:
- "https://download.pytorch.org/whl/cu111"
docker:
distro: debian
python_version: "3.8"
cuda_version: "11.2.2"
setup_script: "./setup.sh"
위의 yaml 파일을 통해 bento만드는 데 필요한 것을 모두 명시해야합니다.
- service: "service:svc"
- 서비스하고자 하는 service file:service class(
service:svc
)을 명시
- 서비스하고자 하는 service file:service class(
- labels
- meta data를 입력
- include / exclude
include
는bentofile.yaml
가 존재하는 directory 위치에 있는 파일 들중 포함하고자 하는 파일을 의미하고exclude
는 그 반대
- python
- 서비스에 필요한 python package를 명시
- docker
- docker base image에 대한 내용으로
debian
os를 사용할 것이며 python, cuda version을 명시할 수 있음 setup_script
에는 docker image에 setup되어야 하는 명령어들이 포함된 쉘 스크립트를 의미함
- docker base image에 대한 내용으로
bentoml build cli
입력하면 benfile.yaml
을 기반으로 bento를 만들어 줍니다.
Log를 보면 model store에 저장된 yolov8s_model:ukgv3lhwxstqdibw
를 load하여 packing하는 것을 알 수 있고 service:svc
에서 정의한 service 이름인 yolov8s_model을 기반으로 tag(slops5xxc2yhdibw
)도 생성되었음을 알 수 있습니다. 위의 과정을 통해 무엇이 생성되었는 지 알려드리기 위해 ~/bentoml/bentos/yolov8s_svc/slops5xxc2yhdibw/
(~/bentoml/bentos/${SERVICE_NAME}/${SERVICE_TAG}) 디렉토리로 가봅니다.
위와 같은 파일들이 docker build를 통해 생성되는 것을 알 수 있습니다.
3.4 Generating Docker Image from Bento
생성된 bento 파일들을 기반으로 최종적으로 docker image를 만들어봅니다. DOCKER_BUILDKIT=0 bentoml containerize ${service_name}:${service_tag}
명령어를 사용합니다.
성공적으로 완료되었으니 docker images
명령어를 통해 생성된 docker image를 확인합니다.
해당 docker image로 container를 생성하여 container에서도 서비스가 정상적으로 작동되는 지 확인해 보겠습니다. 저는 docker run -it --gpus "device=0" --ipc=host --name yolov8s_model -p 3000:3000 yolov8s_svc:slops5xxc2yhdibw
명령어로 container를 생성하였습니다.
docker image를 통해 서비스를 생성하니 전과 다르게 [api_server:${number}]
부분과 [runner:yolov8s_model:${number}]
부분이 추가되었습니다. api_server는 request를 받는 api server를 의미하고 그에 대한 ${number}는 몇번째 api server인지를 나타냅니다. 그리고 runner:yolov8s_model는 runner를 의미하고 그에 대한 ${number}는 몇번째 runner인지를 나타냅니다. 아래 사진은 api server가 3개인 경우와 runner가 1개인 경우의 서비스를 의미합니다. (다음 글에서는 서비스 성능 최적화를 위해 api server개수와 runner개수를 조절하는 방법을 알아보도록 할게요!)
3.2에서 만든 request.py
으로 테스트 다시 해보면 이전과 똑같이 inference api가 정상적으로 작동하는 것을 확인할 수 있습니다.
서비스가 실행되는 container(request 받는 side)의 log를 보면 api_server:48
를 통해 정상적으로 request받아서 runner:yolov8s_model:1
으로 inference진행완료한 것을 알 수 있습니다!
'AI Engineering > MLOps' 카테고리의 다른 글
Airflow (2) - DAG workflow 작성 및 실행 (0) | 2022.03.16 |
---|---|
Airflow (1) - Airflow 이해 및 설치 (0) | 2022.03.16 |
Docker/Kubernetes - (12) Kubernetes Ingress (0) | 2022.03.15 |
Docker/Kubernetes - (11) Kubernetes 리소스의 관리와 설정 (0) | 2022.03.15 |
Docker/Kubernetes - (10) Kubernetes 이해 및 사용 (0) | 2022.03.15 |