Learning

Segment Anything: TorchServe로 배포하기 - code 포함

Code로 Segment Anything Torchserve 배포 과정을 살펴보기

2023
.
06
.
07
Segment Anything: TorchServe로 배포하기 - code 포함

본격적으로 Segment Anything 모델을 코드와 함께 Torchserve 배포하는 과정을 살펴보도록 하겠습니다. 이번 글은 코드 중심으로 진행될 예정이니 Torchserve에 대해 좀 더 자세한 설명이 궁금하시다면 저희 블로그의 Torchserve 시리즈를 읽어보시면 좋을 것 같습니다. 이제 시작해보도록 하겠습니다.

Torchserve handler 작성하기

추론을 위해 필요한 config.properties와 handler.py를 작성해보겠습니다.

config.properties의 address port는 이후 docker 실행 시 포트 포워딩을 해 docker 밖에서도 추론할 수 있도록 설정해 줄 예정입니다.  gpu_idbatch_size는 handler.py의 properties:dict 변수로 들어가는 값입니다.

# config.properties
inference_address=http://0.0.0.0:8070  # default: 8080
management_address=http://127.0.0.1:8071  # default: 8081
metrics_address=http://127.0.0.1:8072  # default: 8082
gpu_id=0
number_of_gpu=0
batch_size=1
model_store=.
default_workers_per_model=1

handler.py 기본적인 코드 구조는 Torchserve에서 제공하는 예시 코드와 동일합니다. 대신 후처리에서 추론 결과를 base64로 인코딩하는 과정을 추가해주었습니다. torch.Tensornp.array 그대로 반환해도 상관은 없지만, 이후 편리한 통신을 위해 string 타입으로 내보내겠습니다.

# handler.py
import io
import os
import cv2
import torch
import base64
import pickle
import logging
import requests
import numpy as np
from PIL import Image
from ts.torch_handler.base_handler import BaseHandler
from segment_anything import sam_model_registry, SamPredictor

logger = logging.getLogger(__name__)


class SegmentAnythingHandler(BaseHandler):
    def __init__(self):
        super(SegmentAnythingHandler, self).__init__()
        self.initialized = False

    def initialize(self, ctx):
        self.manifest = ctx.manifest
        properties = ctx.system_properties
        model_dir = properties.get("model_dir")
        serialized_file = self.manifest["model"]["serializedFile"]
        model_pt_path = os.path.join(model_dir, serialized_file)
        self.device = torch.device(f"cuda:{properties['gpu_id']}" if torch.cuda.is_available() else "cpu")
        sam = sam_model_registry[serialized_file.replace('.pth', '')](checkpoint=model_pt_path)
        sam.to(self.device)
        self.predictor = SamPredictor(sam)

        logger.debug("Model from path {0} loaded successfully".format(model_dir))
        self.initialized = True

    def handle(self, data, ctx):
        image_path = data[0].get("data")
        if image_path is None:
            image_path = requests.get(data[0].get("body")["dataUrl"]).content
        image = Image.open(io.BytesIO(image_path))
        image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)

        self.predictor.set_image(image)
        result_dict = {'original_size': self.predictor.original_size,
                       'input_size': self.predictor.input_size,
                       'features': post_process(self.predictor.features)}
        return [base64.b64encode(pickle.dumps(result_dict)).decode('utf8')]

모델 배포를 위한 준비를 마쳤으니 다음으로 docker를 사용해 추론 환경을 만들어보겠습니다.

Segment Anything Docker 환경 구축하기

Docker를 사용하지 않는다면 바로 모델 배포하기로 넘어가도 괜찮습니다.

먼저 DockerFile을 작성해보겠습니다. Base image는 원하시는 버전을 선택하시면 되고, Torchserve 사용 시 jdk가 필요하기 때문에 설치해주겠습니다. 마지막으로 필요한 라이브러리를 설치해주면 끝입니다. 편이에 따라 requirements.txt 파일을 만들어 RUN pip install -r requirements.txt로 설치하셔도 됩니다.

FROM pytorch/pytorch:1.13.1-cuda11.6-cudnn8-runtime

WORKDIR /workspace
RUN mkdir datahunt_segment_anything

# Install OpenJDK-11
RUN apt-get update && \
    apt-get install -y git && \
    apt-get install -y openjdk-11-jre-headless && \
    apt-get clean;

RUN apt-get -y install libgl1-mesa-glx && \
    apt-get install -y curl;

ADD datahunt_segment_anything /workspace/datahunt_segment_anything

RUN pip install git+https://github.com/facebookresearch/segment-anything.git
RUN pip install opencv-python matplotlib onnxruntime onnx
RUN pip install torchserve torch-model-archiver torch-workflow-archiver nvgpu validators tensorflow-cpu

이제 빌드해보도록 하겠습니다.

docker build -t segment-anything:v1 .

다음으로 컨테이너를 만들고 포트 설정을 해주겠습니다. 필요에 따라 volume 마운팅 설정을 해주면 로컬에서 작업하는 내용을 실시간으로 반영할 수 있습니다. 하지만 저희는 완성된 코드를 사용할 예정이니 따로 설정하진 않겠습니다. -p 8070은 위에 config.properties에서 설정해준 inference_address의 port 입니다.

docker run -it --gpus all --name segment-anything-v1 -p 8070:8070/tcp segment-anything:v1 /bin/bash

컨테이너에 접속한 걸 확인하셨나요? 그럼 이제 배포를 위한 모든 준비가 끝났으니 .mar 파일을 생성하고 Torchserve를 실행보겠습니다.

Segment Anything 모델 배포하기

현재 작업 디렉토리는 /workspace/datahunt_segment_anything이고, 내부 폴더 구조는 다음과 같습니다.

배포를 위한 스크립트는 아래와 같습니다. 직접 하나씩 실행해도 괜찮지만 이후 반복 사용을 위해 쉘 스크립트로 작성하겠습니다. 재배포 용도로 사용할 예정이기 때문에 실행 중인 서버가 있다면 종료하고, 기존에 만들어진 mar도 삭제하는 코드를 추가했습니다. 해당 과정이 필요하지 않다면 삭제해도 괜찮습니다.

#!/bin/bash

torchserve --stop

version=1.0
model_name="SAM"
model_path='../model/sam_vit_l.pth'

if [ -e ${model_name}.mar ]; then
  rm ${model_name}.mar
  echo 'Removed existing model archive.'
fi

# Create mar file
torch-model-archiver --model-name ${model_name} --version ${version} --serialized-file ${model_path} --handler "handler.py"

# Depolyment
torchserve --start --model-store . --models ${model_name}.mar --ts-config ./config.properties

이제 위에서 작성한 torchserve_deploy.sh 파일을 실행하면 모델 배포가 완료됩니다. 모델 추론을 해볼까요? 원하는 샘플을 준비하고 아래와 같이 실행해 보겠습니다.

curl -X POST "http://127.0.0.1:8070/predictions/SAM/1.0" -T "test/sample/sample.jpg"

그럼 아래와 같이 handler.py에서 설정한 형식대로 결과가 출력된 것을 확인할 수 있습니다.

Segment Anything handler 추론 결과, base64 encoding
handler 추론 결과 (base64 encoding)

Segment Anything Promptable Task 추론 테스트

미리 뽑은 임베딩 결과를 불러와 Promptable Task를 진행하는 과정입니다. Point와 Bounding box 좌표는 미리 가지고 있던 값을 사용했습니다. 여기서는 Python 코드를 사용했지만 Github Demo에 다른 언어를 사용해 추론하는 코드도 있으니 참고해주세요.

import cv2
import json
import torch
import base64
import pickle
import numpy as np
import matplotlib.pyplot as plt
from argparse import ArgumentParser
from segment_anything import sam_model_registry, SamPredictor
    
    
def show_mask(mask, ax=plt.gca(), random_color=False):
    if random_color:
        color = np.concatenate([np.random.random(3), np.array([0.6])], axis=0)
    else:
        color = np.array([30 / 255, 144 / 255, 255 / 255, 0.6])
    h, w = mask.shape[-2:]
    mask_image = mask.reshape(h, w, 1) * color.reshape(1, 1, -1)
    ax.imshow(mask_image)
    plt.show()
    
    
if __name__ == '__main__':
    parser = ArgumentParser()
    parser.add_argument('--device', default='cuda:0', type=str)
    parser.add_argument('--model_type', default='vit_l', type=str)
    parser.add_argument('--checkpoint', default='../model/sam_vit_l.pth', type=str)
    parser.add_argument('--input_path', default='test/sample.json', type=str)
    args = parser.parse_args()

    box_coords = np.array([198, 1369, 1369, 2665])
    point_labels = np.array([1, 1, 1])
    point_coords = np.array([[1000, 1700], [1200, 1500], [1300, 1700]])
    
    embed_dict = json.load(open(args.input_path, 'r'))
    image = cv2.imread(args.input_path.replace('json', 'jpg')
    image = cv2.cvtColor(image), cv2.COLOR_BGR2RGB)
    
    device = torch.device(args.device)
    
    sam = sam_model_registry[args.model_type](checkpoint=args.checkpoint)
    sam.to(device=device)
    predictor = SamPredictor(sam)
    input_dict = pickle.loads(base64.b64decode(embed_dict))
    
    predictor.is_image_set = True
    predictor.input_size = input_dict['input_size']
    predictor.original_size = input_dict['original_size']
    predictor.features = input_dict['features'].to(device)
    masks, scores, _ = predictor.predict(point_coords=point_coords,
                                         point_labels=point_labels,
                                         box=box_coords,
                                         multimask_output=False)
    show_mask(masks[0])

배포 후 실제 Segment Anything 의 결과 이미지
배포 후 실제 Segment Anything 의 결과 이미지

이후 프로세스는 자동화 할 수 있도록 CircleCI 배포를 진행해보려 합니다.

지금까지 Torchserve로 Segment-Anything의 Image Encoder 모델을 배포하고 추론하는 과정이었습니다. 마지막 남은 Datahunt의 SAM 기능 살펴보기[3/3] 글도 기대해주세요.

Talk to Expert