KFServing – Transformer를 포함한 InferenceService 배포

대부분의 모델 서버는 텐서를 입력 데이터로 이용합니다. 다시 말해서, 모델을 학습할 때 사용한 데이터의 형태로 이용하는 것입니다. 이 데이터들은 효율적인 학습을 위해서 사전 처리 단계를 거칩니다. 그래서 원시 데이터와는 다릅니다. 예를 들어서 원시 데이터를 이미지로 사용한다고 가정하면, 학습을 위해서 이미지의 크기를 조절하고, 각 필셀 값의 범위를 조정합니다. 그래서 모델 서버에 예측을 요청할 때도 사전 처리를 거친 데이터를 입력해야만 정상적인 결과가 나오게 되는 것입니다. 즉 사용자가 원시 입력 형식으로 전송하는 경우 예측 요청을 하기 전에 사전 처리 단계가 필요합니다. 이럴 경우 Transformer를 사용할 수 있습니다.

Transformer를 이용하면, 사전/사후 처리 코드를 사용자가 구현할 수 있습니다. 앞서 구현한 파이토치 예제에서는 텐서를 입력 받아서 예측을 하였습니다. 이 예제에 전처리 단계를 추가한 Transformer를 사용하여, 사용자가 원시 이미지 데이터를 보내 예측을 할 수 있도록 하겠습니다.

Transformer 이미지 생성하기

사전/사후 처리를 할 Transformer 이미지를 생성해 보겠습니다.

사전/사후 처리 코드 작성하기

먼저 사전 처리에서 사용할 image_transform() 메소드를 구현합니다. 사용자가 보내온 BASE64 로 인코딩된 원시 이미지 데이터를 텐서로 변환합니다. transforms.Compose()를 이용하여 이미지 크기도 조절하고, 정규화도 시킵니다.

사후 처리에서 사용할 top_5() 메소드를 구현합니다. 이미지 분류값을 사람이 보기 쉽게 레이블을 붙이주고, TOP 5 로 정리해서 보여줍니다.

kfserving.KFModel 를 상속 받아서 preprocess() 와 postprocess() 메소드를 구현해 줍니다.

image_transformer.py

import argparse
import base64
import io
import logging
from typing import Dict

import kfserving
import numpy as np
import torch
import torchvision.transforms as transforms
from PIL import Image

logging.basicConfig(level=kfserving.constants.KFSERVING_LOGLEVEL)

transform = transforms.Compose([
    transforms.Resize(32),
    transforms.CenterCrop(32),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])


def image_transform(instance):
    byte_array = base64.b64decode(instance['image']['b64'])
    image = Image.open(io.BytesIO(byte_array))
    im = Image.fromarray(np.asarray(image))
    res = transform(im)
    logging.info(res)
    return res.tolist()


CLASSES = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']


def top_5(prediction):
    pred = torch.as_tensor(prediction)
    scores = torch.nn.functional.softmax(pred, dim=0)

    _, top_5 = torch.topk(pred, 5)

    results = {}
    for idx in top_5:
        results[CLASSES[idx]] = scores[idx].item()

    return results


class ImageTransformer(kfserving.KFModel):
    def __init__(self, name: str, predictor_host: str):
        super().__init__(name)
        self.predictor_host = predictor_host

    def preprocess(self, inputs: Dict) -> Dict:
        return {'instances': [image_transform(instance) for instance in inputs['instances']]}

    def postprocess(self, inputs: Dict) -> Dict:
        return {'predictions': [top_5(prediction) for prediction in inputs['predictions']]}


if __name__ == "__main__":
    DEFAULT_MODEL_NAME = "model"

    parser = argparse.ArgumentParser(parents=[kfserving.kfserver.parser])
    parser.add_argument('--model_name', default=DEFAULT_MODEL_NAME,
                        help='The name that the model is served under.')
    parser.add_argument('--predictor_host', help='The URL for the model predict function', required=True)

    args, _ = parser.parse_known_args()

    transformer = ImageTransformer(args.model_name, predictor_host=args.predictor_host)
    kfserver = kfserving.KFServer()
    kfserver.start(models=[transformer])

컨테이너 이미지를 만들기

컨테이너 이미지를 만들기 위한 Dockerfile 입니다. 파이썬을 기본 이미지로 사용하고, torchtorchvision 패키지를 추가로 설치합니다.

ENTRYPOINT를 이용하여 image_transformer.py 를 실행합니다. Transformer 컨테이너가 실행 될때 --model_name--predictor_host 같은 인자 값들이 넘어오기 때문에, 꼭 ENTRYPOINT를 사용해야합니다.

Dockerfile

FROM python:3.6-slim

RUN pip install torch torchvision
RUn pip install kfserving==0.3.0 image numpy

ENV APP_HOME /app
WORKDIR $APP_HOME
ADD image_transformer.py /app/

ENTRYPOINT ["python", "image_transformer.py"]

다음 명령어를 실행하면 kangwoo/kfserving-transformer:0.0.1 라는 이름의 컨테이너 이미지를 빌드 할 수 있습니다.

docker build -t kangwoo/kfserving-transformer:0.0.1 .

빌드한 컨테이너 이미지를 컨테이너 레지스트리로 업로드 하겠습니다.

docker push kangwoo/kfserving-transformer:0.0.1

Transoformer를 포함한 InferenceService 로 예측 하기

InferenceService 생성

InferenceService 매니페스트를 작성합니다. 예측을 위한 predictor 는 파이토치에서 사용한 것을 그대로 재사용하겠습니다. transformer 필드를 이용하여서 생성한 Transformer 이미지를 지정해 줍니다.

transformer.yaml

apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "pytorch-cifar10-transformer"
spec:
  default:
    predictor:
      pytorch:
        storageUri: "pvc://kfserving-models-pvc/models/pytorch/cifar10/"
        modelClassName: "Net"
    transformer:
      custom:
        container:
          image: kangwoo/kfserving-transformer:0.0.1

InferenceService 를 생성합니다.

다음은 admin 네임스페이스 InferenceService 를 생성하는 예제입니다.

kubectl -n admin apply -f transformer.yaml

생성한 InferenceService를 조회해 보겠습니다.

kubectl -n admin get inferenceservice

InferenceService 가 정상적으로 생성되면 다음과 같은 응답 결과를 확인할 수 있습니다.

NAME                          URL                                                                                          READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
pytorch-cifar10-transformer   <http://pytorch-cifar10-transformer.admin.example.com/v1/models/pytorch-cifar10-transformer>   True    100                                21s

예측 실행하기

예측을 요청하기 위해서는 모델 서버에 접근해야 합니다. 모델 서버는 ingressgateway 를 통해서 접근할 수 있습니다. ingressgateway 는 모델 서버들을 구분하기 위해서 호스트 이름을 사용합니다. ingressgateway에 접근하 기 위한 주소는 앞서 정의한 CLUSTER_IP 를 사용하겠습니다.

예측을 요청할 데이터를 json 파일로 작성합니다.

데이터의 크기가 크기 때문에 git 에 있는 파일을 다운받아서 사용해주세요.

input.json

{
  "instances": [
    {
      "image": {
        "b64": "..."
      }
    }
  ]
}

b64 필드에는 BASE64로 인코딩된 이미지가 들어 있습니다. 만약 다른 이미지로 테스트해 보고 싶다면, 다음 코드를 참조하여 값을 변경하면 됩니다.

import base64
with open("airplane.jpg", "rb") as image_file:
    encoded_string = base64.b64encode(image_file.read())

print(encoded_string.decode())

다음은 admin 네임스페이스의 pytorch-cifar10-transformer InferenceService 에 예측을 요청하는 예제입니다.

MODEL_NAME=pytorch-cifar10-transformer
SERVICE_HOSTNAME=$(kubectl -n admin get inferenceservice pytorch-cifar10-transformer -o jsonpath='{.status.url}' | cut -d "/" -f 3)

INPUT_PATH=@./input.json
curl -v -H "Host: ${SERVICE_HOSTNAME}" http://$CLUSTER_IP/v1/models/$MODEL_NAME:predict -d $INPUT_PATH

정상적으로 실행되면 다음과 같은 응답 결과를 확인 할 수 있습니다.

*   Trying 192.168.21.38...
* TCP_NODELAY set
* Connected to 192.168.21.38 (192.168.21.38) port 32380 (#0)
> POST /v1/models/pytorch-cifar10-transformer:predict HTTP/1.1
> Host: pytorch-cifar10-transformer.admin.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 4551
> Content-Type: application/x-www-form-urlencoded
> Expect: 100-continue
> 
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 200 OK
< content-length: 176
< content-type: application/json; charset=UTF-8
< date: Sat, 04 Apr 2020 09:43:33 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 45
< 
* Connection #0 to host 192.168.21.38 left intact
{"predictions": [{"airplane": 0.9271695017814636, "ship": 0.06180185452103615, "bird": 0.004641484934836626, "deer": 0.003963686991482973, "automobile": 0.000884002773091197}]}
* Closing connection 0

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다