KFServing InferenceService 배포와 예측 – ONNX Model with ONNX Runtime

ONNX를 사용하는 InferenceService

학습이 완료된 ONNX 모델을 저장 한 경우, KFServing에서 제공하는 ONNX 서버를 사용하여 간단히 배포 할 수 있습니다.

모델 생성

ONNX 서버를 테스트하려면 먼저 ONNX 모델을 생성해야 합니다.

파이토치로 학습한 모델을 ONNX 모델로 저장하겠습니다. 파이토치는 동적 그래프를 사용하므로, ONNX 로 export 할때 신경망 계산을 실제로 한 번 실행해야 합니다. 이를 위해서 실제 이미지 데이터 대신, 차원이 동일한 더미 데이터를 사용할 수도 있습니다.

앞서 PyTorch 에서 저장한 모델을 읽어와서 ONNX 모델로 저장하는코드를 작성해 보겠습니다. 파이토치의 모델 저장 경로는 /mnt/pv/models/pytorch/cifar10/model.pt 입니다.

import argparse
import os

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.onnx as onnx


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


if __name__ == "__main__":

    parser = argparse.ArgumentParser()
    parser.add_argument('--model_path', default='/mnt/pv/models/onnx/cifar10', type=str)
    args = parser.parse_args()

    model_path = args.model_path
    if not (os.path.isdir(model_path)):
        os.makedirs(model_path)

    model_file = os.path.join(model_path, 'model.onnx')

    net = Net()
    state_dict = torch.load("/mnt/pv/models/pytorch/cifar10/model.pt")
    net.load_state_dict(state_dict)
    net.eval()

    x = torch.empty(1, 3, 32, 32)
    torch.onnx.export(net, x, model_file)

생성 된 모델을 사용하여 ONNX 서버를 실행하고 예측을 수행 할 수 있습니다. 모델은 PV, S3 호환 가능 개체 저장소, Azure Blob 저장소 또는 Google Cloud Storage에 있을 수 있습니다.

모델 저장하기

쿠버네티스의 퍼시스턴스 볼륨에 모델을 저장해 보겠습니다. PVC 는 앞서 생성한 kfserving-models-pvc 을 사용하겠습니다. 모델을 학습시키기 위해서 쿠버네티스 잡(Job)을 사용하겠습니다. Job을 생성할 때 모델을 저장하기 위한 PVC를 마운트 해줍니다.

모델 코드 작성하기

cifa10 이미지를 분류하는 모델입니다. 모델을 저장할 위치를 --model_path 파라미터로 입력받게 하였습니다.

먼저 학습이 완료된 모델을 불러옵니다. 모델을 변환하기 전에 모델을 추론 모드로 바꾸기 위해서 torch_model.eval() 을 호출합니다. 모델을 변환하기 위해서는 torch.onnx.export() 함수를 호출합니다. 이 함수는 모델을 실행하여 어떤 연산자들이 출력값을 계산하는데 사용되었는지를 기록해줍니다. export 함수가 모델을 실행하기 때문에, 직접 텐서를 입력값으로 넘겨주어야 합니다. 이 텐서의 값은 알맞은 자료형과 모양이라면 더미 데이터를 사용해도 상관없습니다.

ipytorch_cifar10.py

import argparse
import os

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.onnx as onnx


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


if __name__ == "__main__":

    parser = argparse.ArgumentParser()
    parser.add_argument('--model_path', default='/mnt/pv/models/onnx/cifar10', type=str)
    args = parser.parse_args()

    model_path = args.model_path
    if not (os.path.isdir(model_path)):
        os.makedirs(model_path)

    model_file = os.path.join(model_path, 'model.onnx')

    net = Net()
    state_dict = torch.load("/mnt/pv/models/pytorch/cifar10/model.pt")
    net.load_state_dict(state_dict)
    net.eval()

    x = torch.empty(1, 3, 32, 32)
    torch.onnx.export(net, x, model_file, input_names=['input1'], output_names=['output1'])

컨테이너 이미지를 만들기

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

Dockerfile

FROM python:3.6-slim

RUN pip install torch torchvision

RUN mkdir -p /app
ADD onnx_cifar10.py /app/

쿠버네티스 잡 실행하기

컨테이너 이미지를 빌드하고, 컨테이너 이미지 레지스트리에 푸시 한 다음, 쿠버네티스 잡(Job)을 생성하겠습니다.

Job을 생성할 때는 모델을 저장하기 위해서 PVC를 마운트 해줍니다. 이 일련의 작업들은 직접 실행 할 수 있습니다. 하지만 좀 더 편하게 하기 위해서 앞서 배운 Kubeflow Fairing을 사용하겠습니다.

다음은 로컬 개발 환경에서 Fairing을 사용하여 컨테이너 이미지를 만들고, 쿠버네티스 잡을 실행하는 예제입니다.

fairing-local-docker.py

import uuid

from kubeflow import fairing
from kubeflow.fairing.kubernetes import utils as k8s_utils

CONTAINER_REGISTRY = 'kangwoo'

namespace = 'admin'
job_name = f'onnx-cifar10-job-{uuid.uuid4().hex[:4]}'

command = ["python", "onnx_cifar10.py", "--model_path", "/mnt/pv/models/onnx/cifar10"]
output_map = {
    "Dockerfile": "Dockerfile",
    "onnx_cifar10.py": "onnx_cifar10.py"
}

fairing.config.set_preprocessor('python', command=command, path_prefix="/app", output_map=output_map)

fairing.config.set_builder('docker', registry=CONTAINER_REGISTRY, image_name="onnx-cifar10",
                           dockerfile_path="Dockerfile")

fairing.config.set_deployer('job', namespace=namespace, job_name=job_name,
                            pod_spec_mutators=[
                                k8s_utils.mounting_pvc(pvc_name='kfserving-models-pvc', pvc_mount_path='/mnt/pv')],
                            cleanup=True, stream_log=True)

fairing.config.run()

fairing을 실행하면 쿠버네티스 잡이 생성되고, 학습이 완료된 모델이 지정한 경로에 저장됩니다.

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

InferenceService 생성

InferenceService 매니페스트를 작성합니다. predictor로 pytorch 을 사용합니다. storageUri 필드로 모델 저장 위치를 지정해 줍니다. pvc 의 이름이 kfserving-models-pvc 이고 저장 위치가 models/onnx/cifar10/ 이므로, pvc://kfserving-models-pvc/models/onnx/cifar10/model.onnx 라고 지정해 줍니다.

onnx.yaml

apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "onnx-cifar10"
spec:
  default:
    predictor:
      onnx:
        storageUri: "pvc://kfserving-models-pvc/models/onnx/cifar10/model.onnx"

InferenceService 를 생성합니다.

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

kubectl -n admin apply -f onnx.yaml

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

kubectl -n admin get inferenceservice

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

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

예측 실행하기

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

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

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

cifar10-input.json

{
  "inputs": {
    "input1": {
      "dims": [
        "1",
        "3",
        "32",
        "32"
      ],
      "dataType": 1,
      "rawData": "..."
    }
  }
}

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

MODEL_NAME=onnx-cifar10
SERVICE_HOSTNAME=$(kubectl -n admin get inferenceservice onnx-cifar10 -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 ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /v1/models/onnx-cifar10:predict HTTP/1.1
> Host: onnx-cifar10.admin.example.com
> User-Agent: curl/7.64.1
> Content-Type: application/json
> Accept: application/json
> Content-Length: 16533
> Expect: 100-continue
> 
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 200 OK
< content-length: 145
< content-type: application/json
< date: Sat, 04 Apr 2020 18:19:07 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 10068
< x-ms-request-id: 3f6c43cf-586a-416e-a55a-d0b3ac35057e
< 
* Connection #0 to host localhost left intact
{"outputs":{"output1":{"dims":["1","10"],"dataType":1,"rawData":"ejmIv2h5BsAdRVQ/PswYQCSLZL/Midc/rBSTP2QqU79gRCi/8NJQPQ==","dataLocation":"DEFAULT"}}}

댓글 남기기

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