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 입니다. 파이썬을 기본 이미지로 사용하고, torch
와 torchvision
패키지를 추가로 설치합니다.
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을 사용하여 컨테이너 이미지를 만들고, 쿠버네티스 잡을 실행하는 예제입니다.
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"}}}