KFServing – Explainer를 포함한 InferenceService

Anchors

KFServing 은 Alibi의 Anchors 알고리즘을 이용한 Explainer를 제공하고 있습니다.

앵커 알고리즘은 Ribeiro et al.의 Anchors : High-Precision Model-Agnostic Descriptions 논문을 기반으로 하고 있습니다. 이 알고리즘은 이미지, 텍스트 및 테이블 형식 데이터에 적용되는 분류 모델에 적합한 모델 독립적 (블랙 박스) 인 설명을 제공합니다. 앵커의 기본 개념은 앵커라는 고정밀 규칙을 사용하여 복잡한 모델의 동작을 설명하는 것입니다.

Alibi

Alibi는 학습한 머신 러닝 모델이 왜 그런 결정을 내었는지에 대해 설명하기 위한 오픈 소스 파이썬 라이브러리입니다. 블랙 박스인 머신 러닝 모델을 설명하고 해석할 수 있는 알고리즘을 제공합니다. 그리고 해석 가능한 머신 러닝 메소드에 대한 일관된 API를 지원하기 때문에 사용하기 편합니다. Alibi는 테이블 형식(tabular), 텍스트 그리고 이미지 데이터 분류 등 같은 다양한 사용 사례를 지원합니다.

지원하는 알고리즘

Alibi에서 지원하는 알고리즘들은 머신러닝 모델 예측에 대한 설명을 제공합니다. 모델 예측을 요청하면, 왜 이러한 예측 결과를 도출하게 되었는지에 대해 설명합니다. 다음 알고리즘은 모두 블랙 박스 모델에서 작동하기 때문에, 예측 함수(prediction function)에 접근할 수 있어야합니다.

Model explanations

사용한 Alibi 패키비의 버전은 0.2.2 입니다. KFServing에서 Alibi 서빙을 위해서 사용하는 이미지 버전이 0.2.2 이기 때문입니다. 다른 버전을 사용하고 싶다면, KFServing의 설정을 바꾸면 됩니다. Alibi 0.2.2 버전은 텐서플로우 2 버전을 지원하기 않기 때문에, 테스트를 위해서 텐서플로우 1 버전을 사용하였습니다.


Alibi Image Explainer

Alibi Image Explainer 를 사용하는 InferenceService 를 작성해 보겠습니다. Alibi Image Explainer 는 블랙 박스 설명자 알고리즘으로서, Alibi 오픈 소스 라이브러리의 Anchors 이미지 버전입니다.

Anchors 이미지는 먼저 슈퍼 픽셀로 분할되어 로컬 이미지 구조를 유지하고 있습니다다. 해석 가능한 표현은 앵커에 각 수퍼 픽셀의 존재 또는 부재로 구성됩니다. 해석 가능한 설명에 도달하려면 의미있는 수퍼 픽셀을 생성하는 것이 중요합니다. 이 알고리즘은 여러 표준 이미지 분할 알고리즘 (felzenszwalb, slic 및 quickshift)을 지원하며 사용자가 사용자 정의 분할 기능을 제공 할 수 있습니다.

이 알고리즘에 대한 자세한 내용은 Seldon Alibi 설명서를 참조하십시오.

모델 생성

Alibi Image Explainer 를 사용하기 위해서 Explainer 모델을 저장해야합니다. AnchorImage 인스턴스를 생성한 후, dill을 이용해서 저장하면 됩니다. Alibi Image Explainer 를 사용하는 InferenceService를 생성하기 위해서는 예측 모델과 설명 모델 둘 다 필요하기 때문에 같이 작성하겠습니다.

케라스의 데이터셋 중의 하나인 패션 mnist 데이터를 분류하는 모델을 작성해 보겠습니다.

먼저, 예측 모델을 만들고 훈련하기 위한 텐서플로우의 고수준 API인 tf.keras를 사용합니다. tf.saved_model.save() 메소드를 이용하여, 전체 모델을 지정한 위치에 저장합니다. 여기에는 가중치, 모델 구성 등이 포함됩니다. 모델을 저장할 때 주의해야할 점은 모델 저장 위치의 마지막 디렉토리에 모델의 버전이 포함되어야 합니다. 모델 버전은 숫자를 사용해야합니다.

def model():
    x_in = Input(shape=(28, 28, 1))
    x = Conv2D(filters=64, kernel_size=2, padding='same', activation='relu')(x_in)
    x = MaxPooling2D(pool_size=2)(x)
    x = Dropout(0.3)(x)

    x = Conv2D(filters=32, kernel_size=2, padding='same', activation='relu')(x)
    x = MaxPooling2D(pool_size=2)(x)
    x = Dropout(0.3)(x)

    x = Flatten()(x)
    x = Dense(256, activation='relu')(x)
    x = Dropout(0.5)(x)
    x_out = Dense(10, activation='softmax')(x)

    cnn = Model(inputs=x_in, outputs=x_out)
    cnn.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

    return cnn

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

그 다음, AnchorImage 인스턴스를 생성합니다. 모델의 예측 함수와 이미지 크기를 지정해 줍니다. 수퍼 픽셀은 표준 이미지 분할 알고리즘인 slic를 사용하여였습니다. 생성한 인스턴스를 dill을 이용해서 지정한 위치에 저장합니다.

predict_fn = lambda x: model.predict(x)

image_shape = (28, 28, 1)
segmentation_fn = 'slic'
explainer = AnchorImage(predict_fn, image_shape=image_shape, segmentation_fn=segmentation_fn)
explainer.predict_fn = None  # Clear explainer predict_fn as its a lambda and will be reset when loaded
with open(explainer_export_path, 'wb') as f:
    dill.dump(explainer, f)

모델 저장하기

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

모델 코드 작성하기

패션 mnist 이미지를 분류하는 모델입니다. 예측 모델을 저장할 위치를 --model_path 파라미터로 explainer 모델의 저장할 위치를 --explainer_path 파라미터로 입력받게 하였습니다.

tensorflow_fashion_mnist.py

from __future__ import absolute_import, division, print_function, unicode_literals

import argparse
import os

import dill
import numpy as np
import tensorflow as tf
from alibi.explainers import AnchorImage
from tensorflow.keras.layers import Conv2D, Dense, Dropout, Flatten, MaxPooling2D, Input
from tensorflow.keras.models import Model
from tensorflow.keras.utils import to_categorical

print("TensorFlow version: ", tf.__version__)

parser = argparse.ArgumentParser()
parser.add_argument('--model_path', default='/mnt/pv/tensorflow/fashion_mnist/model', type=str)
parser.add_argument('--explainer_path', default='/mnt/pv/tensorflow/fashion_mnist/explainer', type=str)

args = parser.parse_args()

version = 1
export_path = os.path.join(args.model_path, str(version))

(x_train, y_train), (x_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()
print('x_train shape:', x_train.shape, 'y_train shape:', y_train.shape)

x_train = x_train.astype('float32') / 255
x_test = x_test.astype('float32') / 255
x_train = np.reshape(x_train, x_train.shape + (1,))
x_test = np.reshape(x_test, x_test.shape + (1,))
print('x_train shape:', x_train.shape, 'x_test shape:', x_test.shape)
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
print('y_train shape:', y_train.shape, 'y_test shape:', y_test.shape)


def model():
    x_in = Input(shape=(28, 28, 1))
    x = Conv2D(filters=64, kernel_size=2, padding='same', activation='relu')(x_in)
    x = MaxPooling2D(pool_size=2)(x)
    x = Dropout(0.3)(x)

    x = Conv2D(filters=32, kernel_size=2, padding='same', activation='relu')(x)
    x = MaxPooling2D(pool_size=2)(x)
    x = Dropout(0.3)(x)

    x = Flatten()(x)
    x = Dense(256, activation='relu')(x)
    x = Dropout(0.5)(x)
    x_out = Dense(10, activation='softmax')(x)

    cnn = Model(inputs=x_in, outputs=x_out)
    cnn.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

    return cnn


cnn = model()
cnn.summary()

cnn.fit(x_train, y_train, batch_size=64, epochs=3)

# Evaluate the model on test set
score = cnn.evaluate(x_test, y_test, verbose=0)
print('Test accuracy: ', score[1])

tf.saved_model.save(cnn, export_path)
print('Saved model to {}'.format(export_path))

predict_fn = lambda x: cnn.predict(x)

image_shape = (28, 28, 1)
segmentation_fn = 'slic'
kwargs = {'n_segments': 15, 'compactness': 20, 'sigma': .5}
explainer = AnchorImage(predict_fn, image_shape, segmentation_fn=segmentation_fn, segmentation_kwargs=kwargs)


explainer.predict_fn = None  # Clear explainer predict_fn as its a lambda and will be reset when loaded
explainer_export_path = os.path.join(args.explainer_path, 'explainer.dill')
if not (os.path.isdir(os.path.dirname(explainer_export_path))):
    os.makedirs(os.path.dirname(explainer_export_path))
with open(explainer_export_path, 'wb') as f:
    dill.dump(explainer, f)
print('Saved explainer to {}'.format(explainer_export_path))

컨테이너 이미지를 만들기

컨테이너 이미지를 만들기 위한 Dockerfile 입니다. 텐서플로우 1.15 를 기본 이미지로 사용합니다. 사용한 Alibi 패키비의 버전은 0.3.2 입니다. KFServing에서 Alibi 서빙을 위해서 사용하는 이미지 버전이 0.3.x 이기 때문입니다. 다른 버전을 사용하고 싶다면, KFServing의 설정을 바꾸면 됩니다. Alibi 0.3.2 버전은 텐서플로우 2 버전을 지원하기 않기 때문에, 텐서플로우 1.15 버전을 사용하였습니다.

Dockerfile

FROM tensorflow/tensorflow:1.15.2-py3

RUN pip install alibi==0.3.2
RUN pip install numpy joblib dill

RUN mkdir -p /app
ADD tensorflow_fashion_mnist.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'tensorflow-fashion-mnist-job-{uuid.uuid4().hex[:4]}'

command = ["python", "tensorflow_fashion_mnist.py", "--model_path", "/mnt/pv/tensorflow/fashion-mnist/model",
           "--explainer_path", "/mnt/pv/tensorflow/fashion-mnist/explainer"]
output_map = {
    "Dockerfile": "Dockerfile",
    "tensorflow_fashion_mnist.py": "tensorflow_fashion_mnist.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="tensorflow-fashion-mnist",
                           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을 실행하면 쿠버네티스 잡이 생성되고, 학습이 완료된 모델이 지정한 경로에 저장됩니다.

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

InferenceService 생성

InferenceService 매니페스트를 작성합니다.

predictor로 tensorflow 를 사용합니다. storageUri 필드로 모델 저장 위치를 지정해 줍니다. pvc 의 이름이 kfserving-models-pvc 이고 저장 위치가 tensorflow/fashion-mnist/model 이므로, pvc://kfserving-models-pvc/tensorflow/fashion-mnist/model 라고 지정해 줍니다.

explainer로 alibi 를 사용합니다. typeAnchorImages 로 지정합니다. 그리고 storageUri 필드로 Explainer 의 모델 저장 위치인 pvc://kfserving-models-pvc/tensorflow/fashion-mnist/explainer 를 지정해 줍니다.

alibi-fashion-mnist.yaml

apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "alibi-fashion-mnist"
spec:
  default:
    predictor:
      tensorflow:
        storageUri: "pvc://kfserving-models-pvc/tensorflow/fashion-mnist/model"
    explainer:
      alibi:
        type: AnchorImages
        storageUri: "pvc://kfserving-models-pvc/tensorflow/fashion-mnist/explainer"
        config:
          batch_size: "25"

InferenceService 를 생성합니다.

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

kubectl -n admin apply -f alibi-fashion-mnist.yaml

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

kubectl -n admin get inferenceservice

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

NAME                  URL                                                                          READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
alibi-fashion-mnist   <http://alibi-fashion-mnist.admin.example.com/v1/models/alibi-fashion-mnist>   True    100                                2m

예측 실행하기

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

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

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

fashion-mnist-input.json

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

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

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

INPUT_PATH=@./fashion-mnist-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/alibi-fashion-mnist:predict HTTP/1.1
> Host: alibi-fashion-mnist.admin.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 29557
> 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: 181
< content-type: application/json
< date: Sat, 04 Apr 2020 12:10:22 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 8701
< 
{
    "predictions": [[3.36351741e-06, 6.70785425e-07, 8.033673e-07, 5.26102497e-07, 3.26495325e-07, 0.00148782798, 6.07064408e-07, 0.0196282454, 4.8258451e-05, 0.978829265]
    ]
* Connection #0 to host 192.168.21.38 left intact
}
* Closing connection 0

설명 실행하기

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

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

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

fashion-mnist-input.json

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

다음은 admin 네임스페이스의 tensorflow-mnist InferenceService 에 설명을 요청하는 예제입니다.

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

INPUT_PATH=@./fashion-mnist-input.json
curl -v -H "Host: ${SERVICE_HOSTNAME}" http://$CLUSTER_IP/v1/models/${MODEL_NAME}:explain -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/alibi-fashion-mnist:explain HTTP/1.1
> Host: alibi-fashion-mnist.admin.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 29557
> 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: 101253
< content-type: text/html; charset=UTF-8
< date: Sat, 04 Apr 2020 10:51:23 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 10601
<
< 
{
  "anchor": [...],
  "segments": [...],
  "precision": 1.0,
  "coverage": 0.5106,
  "raw": {
    "feature": [
      13
    ],
    "mean": [
      1.0
    ],
    "precision": [
      1.0
    ],
    "coverage": [
      0.5106
    ],
    "examples": [
      {
        "covered": [...],
        "covered_true": [...],
        "covered_false": [],
        "uncovered_true": [],
        "uncovered_false": []
      }
    ],
    "all_precision": 0,
    "num_preds": 250026,
    "instance": [...],
    "prediction": 9
  },
  "meta": {
    "name": "AnchorImage"
  }
}
* Closing connection 0

설명 확인하기

설명 요청의 응답 결과를 이미지로 확인하기 위해서 테스트 코드를 작성해 보겠습니다.

test_fashion_mnist.py

import argparse
import json
import os
import ssl

import matplotlib.pyplot as plt
import numpy as np
import requests
import tensorflow as tf

ssl._create_default_https_context = ssl._create_unverified_context

HOST = 'alibi-fashion-mnist.admin.example.com'
PREDICT_TEMPLATE = 'http://{0}/v1/models/alibi-fashion-mnist:predict'
EXPLAIN_TEMPLATE = 'http://{0}/v1/models/alibi-fashion-mnist:explain'


def get_image_data(idx):
    (_, _), (x_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()

    return x_test[idx]


def preprocess_input(input):
    data = input.astype('float32') / 255
    data = np.reshape(data, data.shape + (1,))
    data = data.reshape(1, 28, 28, 1)
    return data


CLASSES = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat', 'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle boot']


def decode_predictions(predictions):
    labels = np.argmax(predictions, axis=1)
    return CLASSES[labels[0]]


def predict(cluster_ip):
    image_data = get_image_data(0)
    data = preprocess_input(image_data)
    payload = json.dumps({"instances": data.tolist()})

    headers = {'Host': HOST}
    url = PREDICT_TEMPLATE.format(cluster_ip)
    print("Calling ", url)
    r = requests.post(url, data=payload, headers=headers)
    resp_json = json.loads(r.content.decode('utf-8'))
    preds = np.array(resp_json["predictions"])
    label = decode_predictions(preds)

    plt.imshow(image_data)
    plt.title(label)
    plt.show()


def explain(cluster_ip):
    image_data = get_image_data(1)
    data = preprocess_input(image_data)
    payload = json.dumps({"instances": data.tolist()})

    headers = {'Host': HOST}
    url = EXPLAIN_TEMPLATE.format(cluster_ip)
    print("Calling ", url)
    r = requests.post(url, data=payload, headers=headers)
    if r.status_code == 200:
        explanation = json.loads(r.content.decode('utf-8'))

        exp_arr = np.array(explanation['anchor'])

        f, axarr = plt.subplots(1, 2)
        axarr[0].imshow(image_data)
        axarr[1].imshow(exp_arr[:, :, 0])
        plt.show()
    else:
        print("Received response code and content", r.status_code, r.content)


parser = argparse.ArgumentParser()
parser.add_argument('--cluster_ip', default=os.environ.get("CLUSTER_IP"), help='Address of istio-ingress-gateway')
parser.add_argument('--op', choices=["predict", "explain"], default="predict",
                    help='Operation to run')
args, _ = parser.parse_known_args()

if __name__ == "__main__":
    if args.op == "predict":
        predict(args.cluster_ip)
    elif args.op == "explain":
        explain(args.cluster_ip)

다음 명령어를 실행하면, 설명을 요청할 수 있습니다.

python test_fashion_mnist.py --luster_ip ${CLUSTER_IP} --op explain

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

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

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

KFServing InferenceService 배포와 예측 – NVIDIA Triton Inference Server

Triton은 이전에 TensorRT Inference Server로 알려져 있습니다.

이번에는 모델을 직접 생성하지 않고, 기존에 만들어진 모델을 사용하겠습니다.

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

InferenceService 생성

InferenceService 매니페스트를 작성합니다. predictor로 tensorrt 를 사용합니다. storageUri 필드로 모델 저장 위치를 지정해 줍니다.

tensorrt.yaml

apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "tensorrt-simple-string"
spec:
  default:
    predictor:
      tensorrt:
        storageUri: "gs://kfserving-samples/models/tensorrt"

InferenceService 를 생성합니다.

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

kubectl -n admin apply -f tensorrt.yaml

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

kubectl -n admin get inferenceservice

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

NAME                     URL                                                                                READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
tensorrt-simple-string   <http://tensorrt-simple-string.admin.example.com/v1/models/tensorrt-simple-string>   True    100                                33s

예측 실행하기

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

https://docs.nvidia.com/deeplearning/sdk/tensorrt-inference-server-guide/docs/client.html#section-client-api 에 나와 있는 클라이언트를 사용해서 요청을 하겠습니다. tensorrtserver_client 컨테이너를 실행시킨 후, 컨테이너 안에서 클라이언트는 사용합니다. 그래서 컨테이너 안에서 CLUSTER_IP 로 접근이 가능해야 합니다.

/etc/hosts 파일을 편집해서 CLUSTER_IP 를 tensorrt-simple-string.default.example.com 과 맵핑시킵니다.

클라이언트를 사용하기 위해서, 컨테이너를 실행시킵니다.

docker run -it --rm --net=host kcorer/tensorrtserver_client:19.05

컨테이너 안에서 다음 명령어를 실행시킵니다.

root@trantor:/workspace# ./build/simple_string_client -u tensorrt-simple-string.default.example.com

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

0 + 1 = 1
0 - 1 = -1
1 + 1 = 2
1 - 1 = 0
2 + 1 = 3
2 - 1 = 1
3 + 1 = 4
3 - 1 = 2
4 + 1 = 5
4 - 1 = 3
5 + 1 = 6
5 - 1 = 4
6 + 1 = 7
6 - 1 = 5
7 + 1 = 8
7 - 1 = 6
8 + 1 = 9
8 - 1 = 7
9 + 1 = 10
9 - 1 = 8

KFServing InferenceService 배포와 예측 – Tensorflow

Tensorflow를 사용하는 InferenceService

학습이 완료된 Tensorflow 모델을 저장 한 경우, KFServing은 TensorFlow Serving을 사용하여 모델 서버를 배포 해 줍니다.

모델 생성

Tensorflow 서버를 테스트 하려면 먼저 파이썬을 사용하여 간단한 Tensorflow 모델을 생성해야 합니다.

텐서플로우 모델을 만들고 훈련하기 위한 고수준 API인 tf.keras를 사용합니다. 모델의 save() 메소드를 이용하여, 전체 모델을 지정한 위치에 저장합니다. 여기에는 가중치, 모델 구성 등이 포함됩니다. 모델을 저장할 때 주의해야할 점은 모델 저장 위치의 마지막 디렉토리에 모델의 버전이 포함되어야 합니다. 모델 버전은 숫자를 사용해야합니다.

케라스의 데이터셋 중의 하나인 mnist 데이터를 분류하는 모델을 작성해 보겠습니다.

from __future__ import absolute_import, division, print_function, unicode_literals

import argparse
import os

import tensorflow as tf


def train():
    print("TensorFlow version: ", tf.__version__)

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

    version = 1
    export_path = os.path.join(args.model_path, str(version))

    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
    x_train, x_test = x_train / 255.0, x_test / 255.0

    model = tf.keras.models.Sequential([
        tf.keras.layers.Flatten(input_shape=(28, 28)),
        tf.keras.layers.Dense(128, activation='relu'),
        tf.keras.layers.Dropout(0.2),
        tf.keras.layers.Dense(10, activation='softmax')
    ])

    model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=0.01),
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])

    print("Training...")
    training_history = model.fit(x_train, y_train, batch_size=64, epochs=10,
                                 validation_split=0.2)

    print('\\nEvaluate on test data')
    results = model.evaluate(x_test, y_test, batch_size=128)
    print('test loss, test acc:', results)

    model.save(export_path)
    print('"Saved model to {}'.format(export_path))


if __name__ == '__main__':
    train()

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

모델 저장하기

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

모델 코드 작성하기

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

tensorflow_mnist.py

from __future__ import absolute_import, division, print_function, unicode_literals

import argparse
import os

import tensorflow as tf


def train():
    print("TensorFlow version: ", tf.__version__)

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

    version = 1
    export_path = os.path.join(args.model_path, str(version))

    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
    x_train, x_test = x_train / 255.0, x_test / 255.0

    model = tf.keras.models.Sequential([
        tf.keras.layers.Flatten(input_shape=(28, 28)),
        tf.keras.layers.Dense(128, activation='relu'),
        tf.keras.layers.Dropout(0.2),
        tf.keras.layers.Dense(10, activation='softmax')
    ])

    model.compile(optimizer=tf.keras.optimizers.SGD(learning_rate=0.01),
                  loss='sparse_categorical_crossentropy',
                  metrics=['accuracy'])

    print("Training...")
    training_history = model.fit(x_train, y_train, batch_size=64, epochs=10,
                                 validation_split=0.2)

    print('\\nEvaluate on test data')
    results = model.evaluate(x_test, y_test, batch_size=128)
    print('test loss, test acc:', results)

    model.save(export_path)
    print('"Saved model to {}'.format(export_path))


if __name__ == '__main__':
    train()

컨테이너 이미지를 만들기

컨테이너 이미지를 만들기 위한 Dockerfile 입니다. 텐서플로우를 기본 이미지로 사용합니다.

Dockerfile

FROM tensorflow/tensorflow:2.1.0-py3

RUN mkdir -p /app
ADD tensorflow_mnist.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'tensorflow-mnist-job-{uuid.uuid4().hex[:4]}'

command = ["python", "tensorflow_mnist.py", "--model_path", "/mnt/pv/models/tensorflow/mnist"]
output_map = {
    "Dockerfile": "Dockerfile",
    "tensorflow_mnist.py": "tensorflow_mnist.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="tensorflow-mnist",
                           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을 실행하면 쿠버네티스 잡이 생성되고, 학습이 완료된 모델이 지정한 경로에 저장됩니다.

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

InferenceService 생성

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

tensorflow.yaml

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

만약 GPU 리소스를 사용하려고 한다면, resource 필드에 GPU 를 할당해 주면됩니다.

tensorflow_gpu.yaml

apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "tensorflow-mnist"
spec:
  default:
    predictor:
      tensorflow:
        storageUri: "pvc://kfserving-models-pvc/models/tensorflow/mnist/"
        resources:
          limits:
            cpu: 100m
            memory: 1Gi
            nvidia.com/gpu: "1"

InferenceService 를 생성합니다.

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

kubectl -n admin apply -f tensorflow.yaml

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

kubectl -n admin get inferenceservice

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

NAME               URL                                                                    READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
tensorflow-mnist   <http://tensorflow-mnist.admin.example.com/v1/models/tensorflow-mnist>   True    100                                70s

예측 실행하기

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

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

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

mnist-input.json

{
  "instances": [
    [
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0,
      0.0
    ],
...
  ]
}

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

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

INPUT_PATH=@./mnist-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/tensorflow-mnist:predict HTTP/1.1
> Host: tensorflow-mnist.admin.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 9866
> 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: 185
< content-type: application/json
< date: Sat, 04 Apr 2020 06:38:13 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 8726
< 
{
    "predictions": [[0.000235743544, 1.21317851e-06, 0.000527939294, 0.00202324125, 1.88633135e-06, 3.09452735e-05, 2.20991225e-07, 0.995925665, 2.64503233e-05, 0.00122672203]
    ]
}

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"}}}

KFServing InferenceService 배포와 예측 – PyTorch

PyTorch를 사용하는 InferenceService

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

전제 조건

  • state_dict() 사용하여 모델을 저장해야 합니다.

모델 생성

PyTorch 서버를 테스트하려면 먼저 파이썬을 사용하여 간단한 PyTorch 모델을 생성해야 합니다.

추론을 위해 모델을 저장할 때, 학습이 완료된 모델의 학습 매개 변수만 저장하면 됩니다. torch.save()함수를 사용하여 모델의 state_dict 를 저장하면 나중에 모델을 복원 할 때 유연성이 가장 높아 지므로 모델 저장에 권장되는 방법입니다. 현재 KFServing은 PyTorch가 추론을 위해 권장하는 모델 저장 방법인 state_dict() 메소드를 사용하여 저장된 PyTorch 모델을 지원합니다.

PyTorch의 KFServing 인터페이스는 사용자가 PyTorch 모델과 동일한 위치에 model_class_file을 업로드 할 것으로 예상하고 선택적 model_class_name을 런타임 입력으로 전달하도록 허용합니다. 모델 클래스 이름을 지정하지 않으면 ‘PyTorchModel’을 기본 클래스 이름으로 사용합니다. 다른 인터페이스를 사용하여 저장된 PyTorch 모델을 지원하기 위해, 이 인터페이스가 발전함에 따라 현재 인터페이스가 변경 될 수 있습니다.

PyTorch의 데이터셋 중의 하나인 cifar10 데이터를 분류하는 모델을 작성해 보겠습니다.

cifar10.py

import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim


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__":

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

    trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                            download=True, transform=transform)
    trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
                                              shuffle=True, num_workers=2)

    testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                           download=True, transform=transform)
    testloader = torch.utils.data.DataLoader(testset, batch_size=4,
                                             shuffle=False, num_workers=2)

    classes = ('plane', 'car', 'bird', 'cat',
               'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

    net = Net()

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

    for epoch in range(2):  # loop over the dataset multiple times

        running_loss = 0.0
        for i, data in enumerate(trainloader, 0):
            # get the inputs; data is a list of [inputs, labels]
            inputs, labels = data

            # zero the parameter gradients
            optimizer.zero_grad()

            # forward + backward + optimize
            outputs = net(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            # print statistics
            running_loss += loss.item()
            if i % 2000 == 1999:    # print every 2000 mini-batches
                print('[%d, %5d] loss: %.3f' %
                      (epoch + 1, i + 1, running_loss / 2000))
                running_loss = 0.0

    print('Finished Training')

    # Save model
    torch.save(net.state_dict(), "model.pt")

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

모델 저장하기

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

모델 코드 작성하기

cifa10 이미지를 분류하는 모델입니다. 모델을 저장할 위치를 --model_path 파라미터로 입력받게 하였습니다. state_dict() 메소드를 사용하여 모델을 저장하게 되면, 학습된 파라미터만 저장이 됩니다. 그래서 모델이 정의된 파이썬 파일이 별도로 필요합니다. 모델이 정의된 파이썬 파일도 같이 저장소에 복사해 줍니다.

ipytorch_cifar10.py

import argparse
import os
import shutil

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms


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/pytorch/iris', 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.pt')

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

    trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                            download=True, transform=transform)
    trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
                                              shuffle=True, num_workers=2)

    testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                           download=True, transform=transform)
    testloader = torch.utils.data.DataLoader(testset, batch_size=4,
                                             shuffle=False, num_workers=2)

    classes = ('plane', 'car', 'bird', 'cat',
               'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

    net = Net()
    if torch.cuda.is_available():
        print('Use GPU')
        net = net.cuda()

    criterion = nn.CrossEntropyLoss()
    optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

    for epoch in range(2):  # loop over the dataset multiple times

        running_loss = 0.0
        for i, data in enumerate(trainloader, 0):
            # get the inputs; data is a list of [inputs, labels]
            inputs, labels = data

            # zero the parameter gradients
            optimizer.zero_grad()

            # forward + backward + optimize
            outputs = net(inputs)
            loss = criterion(outputs, labels)
            loss.backward()
            optimizer.step()

            # print statistics
            running_loss += loss.item()
            if i % 2000 == 1999:  # print every 2000 mini-batches
                print('[%d, %5d] loss: %.3f' %
                      (epoch + 1, i + 1, running_loss / 2000))
                running_loss = 0.0

    print('Finished Training')

    # Save model
    torch.save(net.state_dict(), model_file)

    shutil.copy(os.path.abspath(__file__), os.path.join(model_path, __file__))

컨테이너 이미지를 만들기

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

Dockerfile

FROM python:3.6-slim

RUN pip install torch torchvision

RUN mkdir -p /app
ADD pytorch_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'sklean-iris-job-{uuid.uuid4().hex[:4]}'

command=["python", "pytorch_cifar10.py", "--model_path", "/mnt/pv/models/pytorch/cifar10"]
output_map = {
    "Dockerfile": "Dockerfile",
    "pytorch_cifar10.py": "pytorch_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="pytorch-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/pytorch/cifar10/ 이므로, pvc://kfserving-models-pvc/models/pytorch/cifar10/ 라고 지정해 줍니다.

앞서 학습 할때 사용한 모델 클래스의 이름이 Net 입니다. modelClassName 에 모델 클래스의 이름인 Net 를 지정해 줍니다. 만약 모델 클래스 이름을 별도로 지정해 주지 않으면, 기본값인 PyTorchModel 을 모델 클래스 이름으로 사용합니다.

pytorch.yaml

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

만약 GPU 리소스를 사용하려고 한다면, resource 필드에 GPU 를 할당해 주면됩니다.

pytorch_gpu.yaml

apiVersion: "serving.kubeflow.org/v1alpha2"
kind: "InferenceService"
metadata:
  name: "pytorch-cifar10-gpu"
spec:
  default:
    predictor:
      pytorch:
        storageUri: "pvc://kfserving-models-pvc/models/pytorch/cifar10/"
        modelClassName: "Net"
        resources:
          limits:
            cpu: 100m
            memory: 1Gi
            nvidia.com/gpu: "1"

InferenceService 를 생성합니다.

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

kubectl -n admin apply -f pytorch.yaml

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

kubectl -n admin get inferenceservice

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

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

예측 실행하기

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

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

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

cifar10-input.json

{
   "instances":[
      [
         [
            [
               0.23921573162078857,
               0.24705886840820312,
               0.29411768913269043,
               0.301960825920105,
               0.2549020051956177,
               0.22352945804595947,
               0.2705882787704468,
               0.24705886840820312,
               0.23921573162078857,
               0.24705886840820312,
               0.2627451419830322,
               0.2549020051956177,
               0.2627451419830322,
               0.301960825920105,
               0.32549023628234863,
               0.3333333730697632,
               0.30980396270751953,
               0.2705882787704468,
               0.2549020051956177,
               0.2549020051956177,
               0.22352945804595947,
               0.16862750053405762,
               0.17647063732147217,
               0.16078436374664307,
               0.16862750053405762,
               0.12156867980957031,
               0.09803926944732666,
               0.10588240623474121,
               0.12156867980957031,
               0.07450985908508301,
               -0.011764705181121826,
               -0.09019607305526733
            ]
....
         ]
      ]
   ]
}

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

MODEL_NAME=pytorch-cifar10
SERVICE_HOSTNAME=$(kubectl -n admin get inferenceservice pytorch-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/pytorch-cifar10:predict HTTP/1.1
> Host: pytorch-cifar10.admin.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 110681
> 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: 224
< content-type: text/html; charset=UTF-8
< date: Sat, 29 Mar 2020 13:36:23 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 8857
< 
* Connection #0 to host localhost left intact
{"predictions": [[-1.0642542839050293, -2.1011602878570557, 0.829179584980011, 2.3874664306640625, -0.8927481174468994, 1.6838929653167725, 1.1490685939788818, -0.8248656988143921, -0.6572933197021484, 0.05098217725753784]]}

KFServing InferenceService 배포와 예측 – XGBoost

XGBoost를 사용하는 InferenceService

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

전제 조건

  • 모델 피클의 이름은 model.bst 여야 합니다.
  • xgboost v0.82 버전을 사용해야 합니다.

모델 생성

XGBoost 서버를 테스트하려면 먼저 파이썬을 사용하여 간단한 XGBoost 모델을 생성해야 합니다.

Scikit-learn의 기본적인 데이터셋 중의 하나인 아이리스 꽃 데이터를 사용하여, 아이리스 꽃을 분류하는 모델을 작성해 보겠습니다. 모델 피클의 이름 model.bst 이어야 합니다.

import xgboost as xgb
from sklearn.datasets import load_iris

iris = load_iris()
X = iris['data']
y = iris['target']
dtrain = xgb.DMatrix(X, label=y)
param = {'max_depth': 6,
         'eta': 0.1,
         'silent': 1,
         'nthread': 4,
         'num_class': 10,
         'objective': 'multi:softmax'
         }
xgb_model = xgb.train(params=param, dtrain=dtrain)
xgb_model.save_model('model.bst')

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

모델 저장하기

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

모델 코드 작성하기

아이리스 꽃을 분류하는 간단한 모델입니다. 모델을 저장할 위치를 --model_path 파라미터로 입력받게 하였습니다.

iris.py

import argparse
import os

from joblib import dump
from sklearn import datasets
from sklearn import svm


def train():
    parser = argparse.ArgumentParser()
    parser.add_argument('--model_path', default='/mnt/pv/models/sklearn/iris', type=str)
    args = parser.parse_args()

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

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

    clf = svm.SVC(gamma='scale')
    iris = datasets.load_iris()
    X, y = iris.data, iris.target
    clf.fit(X, y)
    dump(clf, model_file)


if __name__ == '__main__':
    train()

컨테이너 이미지를 만들기

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

Dockerfile

FROM python:3.6-slim

RUN pip install xgboost==0.82 scikit-learn

RUN mkdir -p /app
ADD xgboost_iris.py.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'xgboost-iris-job-{uuid.uuid4().hex[:4]}'

command=["python", "iris.py", "--model_path", "/mnt/pv/models/xgboost/iris"]
output_map = {
    "Dockerfile": "Dockerfile",
    "xgboost_iris.py": "xgboost_iris.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="xgboost-iris", dockerfile_path="Dockerfile")

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

fairing.config.run()

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

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

InferenceService 생성

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

xgboost.yaml

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

InferenceService 를 생성합니다.

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

kubectl -n admin apply -f xgboost.yaml

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

kubectl -n admin get inferenceservice

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

NAME           URL                                                            READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
xgboost-iris   <http://xgboost-iris.admin.example.com/v1/models/xgboost-iris>   True    100                                58s

예측 실행하기

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

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

iris-input.json

{
  "instances": [
    [6.8,  2.8,  4.8,  1.4],
    [6.0,  3.4,  4.5,  1.6]
  ]
}

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

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

INPUT_PATH=@./iris-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/xgboost-iris:predict HTTP/1.1
> Host: xgboost-iris.admin.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 76
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 76 out of 76 bytes
< HTTP/1.1 200 OK
< content-length: 27
< content-type: text/html; charset=UTF-8
< date: Sat, 28 Mar 2020 18:54:46 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 10165
< 
* Connection #0 to host localhost left intact
{"predictions": [1.0, 1.0]}

KFServing InferenceService 배포와 예측 – Scikit-Learn

Scikit-Learn를 사용하는 InferenceService

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

전제 조건

  • 모델 피클은 joblib을 사용하여 저장해야 합니다. 그리고 파일명은 model.joblib 이어야 합니다.
  • 현재 sklearn 0.20.3 버전을 사용합니다. 피클 모델은 이 버전과 호환되어야 합니다.

모델 생성

SKLearn 서버를 테스트하려면 먼저 파이썬을 사용하여 간단한 scikit-learn 모델을 생성해야합니다.

Scikit-learn의 기본적인 데이터셋 중의 하나인 아이리스 꽃 데이터를 사용하여, 아이리스 꽃을 분류하는 모델을 작성해 보겠습니다. 모델 피클은 joblib을 사용하여 저장해야 하고, 파일명은 model.joblib 이어야 합니다.

from joblib import dump
from sklearn import datasets
from sklearn import svm

clf = svm.SVC(gamma='scale')
iris = datasets.load_iris()
X, y = iris.data, iris.target
clf.fit(X, y)
dump(clf, 'model.joblib')

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

모델 저장하기

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

모델 코드 작성하기

아이리스 꽃을 분류하는 간단한 모델입니다. 모델을 저장할 위치를 --model_path 파라미터로 입력받게 하였습니다.

iris.py

import argparse
import os

from joblib import dump
from sklearn import datasets
from sklearn import svm


def train():
    parser = argparse.ArgumentParser()
    parser.add_argument('--model_path', default='/mnt/pv/models/sklearn/iris', type=str)
    args = parser.parse_args()

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

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

    clf = svm.SVC(gamma='scale')
    iris = datasets.load_iris()
    X, y = iris.data, iris.target
    clf.fit(X, y)
    dump(clf, model_file)


if __name__ == '__main__':
    train()

컨테이너 이미지를 만들기

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

Dockerfile

FROM python:3.6-slim

RUN pip install scikit-learn==0.20.3 joblib

RUN mkdir -p /app
ADD iris.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'sklean-iris-job-{uuid.uuid4().hex[:4]}'

command=["python", "iris.py", "--model_path", "/mnt/pv/models/sklearn/iris"]
output_map = {
    "Dockerfile": "Dockerfile",
    "iris.py": "iris.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="sklean-iris", 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=False, stream_log=True)

fairing.config.run()

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

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

InferenceService 생성

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

sklearn.yaml

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

InferenceService 를 생성합니다.

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

kubectl -n admin apply -f sklearn.yaml

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

kubectl -n admin get inferenceservice

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

NAME           URL                                                            READY   DEFAULT TRAFFIC   CANARY TRAFFIC   AGE
sklearn-iris   <http://sklearn-iris.admin.example.com/v1/models/sklearn-iris>   True    100                                19s

예측 실행하기

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

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

iris-input.json

{
  "instances": [
    [6.8,  2.8,  4.8,  1.4],
    [6.0,  3.4,  4.5,  1.6]
  ]
}

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

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

INPUT_PATH=@./iris-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/sklearn-iris:predict HTTP/1.1
> Host: sklearn-iris.admin.example.com
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 76
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 76 out of 76 bytes
< HTTP/1.1 200 OK
< content-length: 23
< content-type: text/html; charset=UTF-8
< date: Sat, 29 Mar 2020 15:20:23 GMT
< server: istio-envoy
< x-envoy-upstream-service-time: 9032
< 
* Connection #0 to host localhost left intact
{"predictions": [1, 1]}

KFServing InferenceService 배포와 예측

InferenceService를 사용하여 모델 서버를 제공하려면, 사용할 네임스페이스가 다음과 같은지 확인해야합니다.

  • [serving.kubeflow.org/inferenceservice=enabled](<http://serving.kubeflow.org/inferenceservice=enabled>) 레이블이 네임스페이스 추가 되어 있어야 합니다.
  • 쿠버네티스 클러스터의 Istio IngressGateway에 접근할 수 있어야 합니다.

레이블 추가

Kubeflow의 대시보드나 프로필 컨트롤러(Profile Controller)를 사용하여, 사용자 네임스페이스를 만드는 경우에는 KFServing에서 모델을 배포할 수 있도록 serving.kubeflow.org/inferenceservice: enabled 레이블이 자동으로 추가됩니다. 만약 네임스페이스를 직접 생성하는 경우에는 해당 네임스페이스에 serving.kubeflow.org/inferenceservice: enabled 레이블을 추가해야만 합니다.

다음은 my-namespace 라는 네임스페이스에 레이블을 추가하는 예제입니다.

kubectl label namespace my-namespace serving.kubeflow.org/inferenceservice=enabled

Istio IngressGateway에 접근하기

InferenceService 가 정상적으로 생성되면, istio의 ingressgateway 를 모델 서버에 접속할 수 있습니다. KFServing에서 사용하는 ingressgateway의 이름을 알려면, config-istio 라는 ConfigMap을 조회하면 됩니다.

다음은 knative-serving 네임스페이스에 있는 config-istio 을 조회하는 예제입니다.

kubectl -n knative-serving get cm config-istio -o yaml

정상적으로 조회 되면 다음과 같은 결과를 얻을 수 있습니다.

apiVersion: v1
data:
  gateway.knative-serving.knative-ingress-gateway: kfserving-ingressgateway.istio-system.svc.cluster.local
  local-gateway.knative-serving.cluster-local-gateway: cluster-local-gateway.istio-system.svc.cluster.local
  local-gateway.mesh: mesh
  reconcileExternalGateway: "false"
kind: ConfigMap
metadata:
  ...
  name: config-istio
  namespace: knative-serving

data 섹션의 gateway.knative-serving.knative-ingress-gateway 필드가 현재 KFServing에서 사용하는 ingressgateway 를 설정하는 부분입니다. 위의 예제에서는 kfserving-ingressgateway를 사용하고 있습니다.

kfserving-ingressgateway를 조회해 보겠습니다.

다음은 istio-system 네임스페이스에 있는 kfserving-ingressgateway을 조회하는 예제입니다.

kubectl -n istio-system get service kfserving-ingressgateway 

KFServing이 설치된 쿠버네티스 클러스터에 따라 결과가 다르게 나옵니다. 응답 결과에 따른 크게 세가지 방법으로 접근 할 수 있습니다.

  • LoadBalancer 를 통해서 접근하기
  • NodePort를 통해서 접근하기
  • kubectl port-forward를 통해서 접근하기

LoadBalancer

쿠버네티스 클러스터가 LoadBalancer 를 지원하면 다음과 같은 결과를 얻을 수 있습니다. 서비스의 타입이 LoadBalancer 이고, EXTERNAL-IP 에 IP가 할당되어 있습니다. 이럴 경우에는 EXTERNAL-IP 를 통해서 ingressgateway에 접근할 수 있습니다.

NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                                                                                                                                                                                   AGE
kfserving-ingressgateway   LoadBalancer   10.101.141.37   10.201.121.4  15020:30543/TCP,80:32380/TCP,443:32390/TCP,31400:32400/TCP,15011:30263/TCP,8060:32119/TCP,853:32180/TCP,15029:32156/TCP,15030:30674/TCP,15031:30230/TCP,15032:32563/TCP,15443:30995/TCP   2d23h

앞으로 만들 예제에서 사용하기 위해서 ingressgateway 의 접근 주소를 다음과 같이 정의하겠습니다. EXTERNAL-IP 주소를 사용합니다.

CLUSTER_IP=10.201.121.4

NodePort

쿠버네티스 클러스터가 LoadBalancer 를 지원하지 않거나, 서비스의 타입이 NodePort 인 경우 EXTERNAL-IP 의 값이 비어 있습니다. 이럴 경우에는 클러스터의 노드 IP 와 NodePort를 통해서 접근할 수 있습니다.

NAME                       TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                                                                                                                                                                                   AGE
kfserving-ingressgateway   LoadBalancer   10.101.141.37   <pending>     15020:30543/TCP,80:32380/TCP,443:32390/TCP,31400:32400/TCP,15011:30263/TCP,8060:32119/TCP,853:32180/TCP,15029:32156/TCP,15030:30674/TCP,15031:30230/TCP,15032:32563/TCP,15443:30995/TCP   2d23h

노드 IP는 노드를 조회하면 알 수 있습니다.

다음은 노드를 조회 하는 예제입니다.

kubectl get node -o wide

정상적으로 조회되면 다음과 같은 응답 결과가 나옵니다.

NAME     STATUS   ROLES    AGE   VERSION    INTERNAL-IP     EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
mortar   Ready    master   13d   v1.15.10   192.168.21.38   <none>        Ubuntu 18.04.3 LTS   4.15.0-91-generic   docker://18.9.9

노드가 한 개가 아닌 경우에는 여러개의 노드 정보가 출력됩니다. 해당 노드들 중에서 아무 노드의 INTERNAL-IP 를 사용하면 됩니다.

앞으로 만들 예제에서 사용하기 위해서 ingressgateway 의 접근 주소를 다음과 같이 정의하겠습니다. 노드의 IP 와 80 PORT(80:32380/TCP)의 노드 포트인 32380을 포트로 사용합니다.

CLUSTER_IP=192.168.21.38:32380

port-forward

외부에서 쿠버네티스 클러스터의 서비스에 접근할 수 없는 경우, kubectl 의 port-forward를 사용할 수 있습니다. 접근 하려는 외부 시스템에서 다음 명령어 실행하면 로컬 포트를 경유 해서 쿠버네티스 서비스에 접근할 수 있습니다.

kubectl -n istio-system port-forward svc/kfserving-ingressgateway 8080:80

포트 포워딩이 정상적으로 실행되면, 로컬포트로 ingressgateay 서비스로 접근할 수 있습니다. http://localhost:8080 처럼 선언한 로컬 포트의 주소로 접근하면, 쿠버네티스 ingressgateway 의 80 포트로 포워딩 됩니다.

앞으로 만들 예제에서 사용하기 위해서 ingressgateway 의 접근 주소를 다음과 같이 정의하겠습니다.

CLUSTER_IP=localhost:8080

PVC 생성하기

InferenceService 에 사용할 모델은 PVC에 저장하겠습니다. 만약 클라우드 스토리지와 같은 다른 저장소를 사용하려면, “클라우드 저장소를 이용하여 InfeerneceService 배포와 예측”을 참조하시기 바랍니다.

kfserving-models-pvc라는 PVC 매니페스트를 작성합니다.

kfserving-models-pvc.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: kfserving-models-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

다음 명령어를 실행하여, admin 네임스페이스에 kfserving-models-pvc라는 PVC를 생성하겠습니다.

kubectl -n admin apply kfserving-models-pvc.yaml

KFServing 설정

KFServing 에서 사용하는 여러 설정 정보들은 inferenceservice-config 라는 쿠버네티스 ConfigMap에 정의되어 있습니다.

이 설정 정보에는 다음과 같은 내용이 정의되어 있습니다.

  • credentials : S3나 GCS를 사용할 때 참조할 값들.
  • explainers : explainer를 실행할 때 사용할 컨테이너의 이미지 정보.
  • ingress : KFServing에서 사용할 Istio ingress 정보.
  • predictors : predictor를 실행할 때 사용할 컨테이너 이미지의 정보.

다음 명령어를 실행하면, 설정 정보를 조회할 수 있습니다.

kubectl -n kubeflow get cm inferenceservice-config -o yaml

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

apiVersion: v1
data:
  credentials: |-
    {
       "gcs": {
           "gcsCredentialFileName": "gcloud-application-credentials.json"
       },
       "s3": {
           "s3AccessKeyIDName": "awsAccessKeyID",
           "s3SecretAccessKeyName": "awsSecretAccessKey"
       }
    }
  explainers: |-
    {
        "alibi": {
            "image" : "gcr.io/kfserving/alibi-explainer",
            "defaultImageVersion": "0.2.2",
            "allowedImageVersions": [
               "0.2.2"
            ]
        }
    }
  ingress: |-
    {
        "ingressGateway" : "knative-ingress-gateway.knative-serving",
        "ingressService" : "kfserving-ingressgateway.istio-system.svc.cluster.local"
    }
  logger: |-
    {
        "image" : "gcr.io/kfserving/logger:0.2.2",
        "memoryRequest": "100Mi",
        "memoryLimit": "1Gi",
        "cpuRequest": "100m",
        "cpuLimit": "1"
    }
  predictors: |-
    {
        "tensorflow": {
            "image": "tensorflow/serving",
            "defaultImageVersion": "1.14.0",
            "defaultGpuImageVersion": "1.14.0-gpu",
            "allowedImageVersions": [
               "1.11.0",
               "1.11.0-gpu",
               "1.12.0",
               "1.12.0-gpu",
               "1.13.0",
               "1.13.0-gpu",
               "1.14.0",
               "1.14.0-gpu"
            ]
        },
        "onnx": {
            "image": "mcr.microsoft.com/onnxruntime/server",
            "defaultImageVersion": "v0.5.1",
            "allowedImageVersions": [
               "v0.5.1"
            ]
        },
        "sklearn": {
            "image": "gcr.io/kfserving/sklearnserver",
            "defaultImageVersion": "0.2.2",
            "allowedImageVersions": [
               "0.2.2"
            ]
        },
        "xgboost": {
            "image": "gcr.io/kfserving/xgbserver",
            "defaultImageVersion": "0.2.2",
            "allowedImageVersions": [
               "0.2.2"
            ]
        },
        "pytorch": {
            "image": "gcr.io/kfserving/pytorchserver",
            "defaultImageVersion": "0.2.2",
            "allowedImageVersions": [
               "0.2.2"
            ]
        },
        "tensorrt": {
            "image": "nvcr.io/nvidia/tensorrtserver",
            "defaultImageVersion": "19.05-py3",
            "allowedImageVersions": [
               "19.05-py3"
            ]
        }
    }
  storageInitializer: |-
    {
        "image" : "gcr.io/kfserving/storage-initializer:0.2.2",
        "memoryRequest": "100Mi",
        "memoryLimit": "1Gi",
        "cpuRequest": "100m",
        "cpuLimit": "1"
    }
  transformers: |-
    {
    }
kind: ConfigMap
metadata:
...
  name: inferenceservice-config
  namespace: kubeflow