KFServing – 사용자 Predictor 를 이용한 InferenceService 배포와 예측

KFServing 은 사용자가 만든 이미지를 이용해서 InferenceService 를 생성할 수 있는 기능을 제공합니다. 이 기능을 이용하면, 사용자는 자신이 만든 모델을 컨테이너 내부로 가져와서 KFServing에서 제공 할 수 있도록 할 수 있습니다.

사용자 이미지를 이용하여 InferenceService 배포와 예측

모델 서버 만들기

웹 서버 코드 작성하기

파이썬의 플라스크를 이용하여 웹 서버를 작성합니다. 예측 엔드포인트인 /v1/models/custom-sample:predict 로 요청을 받아서 “Hello Worl”를 응답으로 반환합니다.

app.py

import os

from flask import Flask

app = Flask(__name__)


@app.route('/v1/models/custom-sample:predict')
def hello_world():
    greeting_target = os.environ.get('GREETING_TARGET', 'World')
    return 'Hello {}!\\n'.format(greeting_target)


if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=int(os.environ.get('PORT', 8080)))

컨테이너 이미지를 만들기

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

Dockerfile

FROM python:3.7-slim

ENV APP_HOME /app
WORKDIR $APP_HOME
COPY app.py ./

RUN pip install flask==1.1.1 gunicorn==20.0.4

CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:app

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

docker build -t kangwoo/kfserving-custom-hello:0.0.1 .

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

docker push kangwoo/kfserving-custom-hello:0.0.1

custom을 사용하는 InferenceService 로 예측 하기

InferenceService 생성

InferenceService 매니페스트를 작성합니다. predictor로 custom 을 사용합니다. container 섹션에서 사용자 이미지와 환경 변수 지정해 줍니다. containerPort 필드를 이용하여, 컨테이너에서 사용하는 포트도 지정해 줄 수 있습니다. 컨테이너 포드를 별도로 지정하지 않았을 경우에는, 기본값인 8080 포트를 사용합니다.

custom-hello.yaml

apiVersion: serving.kubeflow.org/v1alpha2
kind: InferenceService
metadata:
  name: custom-hello
spec:
  default:
    predictor:
      custom:
        container:
          image: kangwoo/kfserving-custom-hello:0.0.1
          ports:
            - containerPort: 8080
          env:
            - name: GREETING_TARGET
              value: "Python KFServing Sample"

InferenceService 를 생성합니다.

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

kubectl -n admin apply -f custom-hello.yaml

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

kubectl -n admin get inferenceservice

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

NAME               URL                                                                    READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
custom-hello       <http://custom-hello.admin.example.com/v1/models/custom-hello>           True    100                                52s

예측 실행하기

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

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

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

curl -v -H "Host: ${SERVICE_HOSTNAME}" http://$CLUSTER_IP/v1/models/$MODEL_NAME:predict

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

*   Trying 192.168.21.38...
* TCP_NODELAY set
* Connected to 192.168.21.38 (192.168.21.38) port 32380 (#0)
> GET /v1/models/custom-hello:predict HTTP/1.1
> Host: custom-hello.admin.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< content-length: 31
< content-type: text/html; charset=utf-8
< date: Sat, 04 Apr 2020 15:32:16 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 7650
< 
Hello Python KFServing Sample!
* Connection #0 to host 192.168.21.38 left intact
* Closing connection 0

사용자 모델을 이용하여 InferenceService 배포와 예측

토네이도 웹 서버를 사용하는 kfserving.KFModel 을 확장하여 사용자 이미지를 생성해 보겠습니다. KFModel을 확장하여 모델을 만드는 경우에는 엔드포인트를 직접 구현할 필요 없이, 사용할 메소드만 구현하면 됩니다. 예를 들어 예측을 처리할 경우 predict() 메소드를 구현하면 됩니다. kfserving.KFModel 은 KFServing SDK에 포함되어 있습니다.

모델 서버 만들기

모델 코드 작성하기

kfserving.KFModel 확장하여 모델 코드를 작성합니다. 앞서 구현한 파이토치를 이용해서 cifar10 데이터를 분류하는 모델을 재사용하겠습니다. 모델은 Net 클래스로 정의하고, 학습한 파라미터 값은 저장된 model.pt 파일로부터 읽어 옵니다. mdoel.pt 파일을 생성하고 싶으면, “KFServing InferenceService 배포와 예측 – PyTorch“를 참고 하시길 바랍니다.

model.py

import base64
import io
from typing import Dict

import kfserving
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.transforms as transforms
from PIL import Image


class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.pool = nn.MaxPool2d(2, 2)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


class KFServingSampleModel(kfserving.KFModel):
    def __init__(self, name: str):
        super().__init__(name)
        self.name = name
        self.ready = False

    def load(self):
        self.classes = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']

        net = Net()
        state_dict = torch.load("model.pt")
        net.load_state_dict(state_dict)
        net.eval()
        self.model = net

        self.ready = True

    def predict(self, request: Dict) -> Dict:
        inputs = request["instances"]

        data = inputs[0]["image"]["b64"]

        raw_img_data = base64.b64decode(data)
        input_image = Image.open(io.BytesIO(raw_img_data))

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

        input_tensor = preprocess(input_image)
        input_batch = input_tensor.unsqueeze(0)

        output = self.model(input_batch)

        scores = torch.nn.functional.softmax(output, dim=1)[0]

        _, top_3 = torch.topk(output, 3)

        results = {}
        for idx in top_3[0]:
            results[self.classes[idx]] = scores[idx].item()

        return {"predictions": results}


if __name__ == "__main__":
    model = KFServingSampleModel("custom-model")
    model.load()
    kfserving.KFServer(workers=1).start([model])

컨테이너 이미지를 만들기

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

Dockerfile

FROM python:3.6-slim

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

ENV APP_HOME /app
WORKDIR $APP_HOME
ADD model.py /app/
ADD model.pt /app/

CMD ["python", "model.py"]

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

docker build -t kangwoo/kfserving-custom-model:0.0.1 .

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

docker push kangwoo/kfserving-custom-model:0.0.1

custom을 사용하는 InferenceService 로 예측 하기

InferenceService 생성

InferenceService 매니페스트를 작성합니다. predictor로 custom 을 사용합니다. container 필드로 사용자 이미지를 지정해 줍니다.장 위치를 지정해 줍니다.

custom.yaml

apiVersion: serving.kubeflow.org/v1alpha2
kind: InferenceService
metadata:
  name: custom-hello
spec:
  default:
    predictor:
      custom:
        container:
          image: kangwoo/kfserving-hello:0.0.1
          env:
            - name: GREETING_TARGET
              value: "Python KFServing Sample"

InferenceService 를 생성합니다.

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

kubectl -n admin apply -f custom.yaml

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

kubectl -n admin get inferenceservice

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

NAME               URL                                                                    READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
custom-hello       <http://custom-hello.admin.example.com/v1/models/custom-hello>           True    100                                52s

예측 실행하기

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

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

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

cifar10-input.json

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

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

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

INPUT_PATH=@./cifar10-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/custom-model:predict HTTP/1.1
> Host: custom-model.admin.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 2611
> 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: 108
< content-type: application/json; charset=UTF-8
< date: Sat, 04 Apr 2020 07:15:56 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 7492
< 
* Connection #0 to host 192.168.21.38 left intact
{"predictions": {"airplane": 0.8815429210662842, "ship": 0.09650199115276337, "truck": 0.01203653123229742}}
* Closing connection 0

댓글 남기기

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