Post

모델 배포 - 모델 로드 패턴





유스케이스

모델 로드 패턴은 모델을 서버 이미지에 빌트인하지 않고 추론 모듈을 기동할 때 다운로드 받는 방식입니다. 서버 이미지 버전보다 추론 모델의 버전을 더 빈번하게 갱신하는 경우나 동일한 서버 이미지로 여러 종류의 추론 모델 가동이 가능한 경우에 사용합니다.

추론 모델을 자주 빌드하게 된다면 앞선 포스트에서 다룬 모델-인-이미지 패턴을 사용했을 때 서버 이미지를 자주 빌드하게 되므로 운영적인 측면에서 효율적이라고 할 수 없습니다. 적당한 하이퍼파라미터를 선정하였고 새로운 데이터만 바꿔넣는 형태라면 모델 로드 패턴이 최적의 솔루션이 될 수 있습니다.

아키텍처

이 패턴에서는 서버 이미지와 모델을 별도로 관리함에 따라 서버 이미지의 빌드와 모델의 학습이 분리됩니다. 따라서 서버 이미지를 경량화할 수 있습니다. 또한 서버 이미지를 범용적으로 사용할 수 있으므로 동일한 이미지를 여러 개의 추론 모델에 응용할 수 있습니다.

모델 로드 패턴에서는 추론 모듈을 배치할 때 서버 이미지를 Pull하고 난 다음 추론 모듈을 기동합니다. 그리고나서 모델 파일을 불러와 추론 모듈을 본격적으로 가동합니다. 이때 환경변수 등을 사용하여 추론 서버에서 가동하는 모델을 유연하게 변경할 수 있습니다.

모델 로드 패턴의 단점이라고 하면 모델이 라이브러리의 버전에 의존적일 때 서버 이미지의 버전 관리와 모델 파일의 버전 관리를 별도로 수행해야 한다는 점입니다. 따라서 모델이 많아질 수록 복잡해지고 운용 부하가 커질 위험이 있습니다.

구현

구현할 때 가장 중요한 점은 모델 파일을 도커 이미지에 포함시키지 않는 것입니다. 여기에서는 쿠버네티스 클러스터를 Google Kubernetes Engine에 구축하고, 모델 파일은 GCP Storage에 보존하여 컨테이너 기동 시마다 불러오도록 합니다. 이하의 코드는 모두 출판사에서 공식적으로 제 공하는 소스코드 저장소에서 발췌하였습니다.

우선 아래 메인 스크립트는 파일의 다운로드를 실행하는 내용을 담고 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# model_loader/main.py

import os
from logging import DEBUG, Formatter, StreamHandler, getLogger

import click
from google.cloud import storage

logger = getLogger(__name__)
logger.setLevel(DEBUG)
strhd = StreamHandler()
strhd.setFormatter(Formatter("%(asctime)s %(levelname)8s %(message)s"))
logger.addHandler(strhd)


@click.command(name="model loader")
@click.option("--gcs_bucket", type=str, required=True, help="GCS bucket name")
@click.option("--gcs_model_blob", type=str, required=True, help="GCS model blob path")
@click.option("--model_filepath", type=str, required=True, help="Local model file path")
def main(gcs_bucket: str, gcs_model_blob: str, model_filepath: str):
    logger.info(f"download from gs://{gcs_bucket}/{gcs_model_blob}")
    dirname = os.path.dirname(model_filepath)
    os.makedirs(dirname, exist_ok=True)

    client = storage.Client.create_anonymous_client()
    bucket = client.bucket(gcs_bucket)
    blob = bucket.blob(gcs_model_blob)
    blob.download_to_filename(model_filepath)
    logger.info(f"download from gs://{gcs_bucket}/{gcs_model_blob} to {model_filepath}")


if __name__ == "__main__":
    main()

그리고 Dockerfile에는 모델 파일을 포함시키지 않은 채 모델을 실행하기 위한 코드를 빌드합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Dockerfile

FROM python:3.8-slim

ENV PROJECT_DIR model_load_pattern
WORKDIR /${PROJECT_DIR}
ADD ./requirements.txt /${PROJECT_DIR}/
RUN apt-get -y update && \
    apt-get -y install apt-utils gcc && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    pip install --no-cache-dir -r requirements.txt

COPY ./src/ /${PROJECT_DIR}/src/
COPY ./models/label.json /${PROJECT_DIR}/models/label.json

ENV LABEL_FILEPATH /${PROJECT_DIR}/models/label.json
ENV LOG_LEVEL DEBUG
ENV LOG_FORMAT TEXT

COPY ./run.sh /${PROJECT_DIR}/run.sh
RUN chmod +x /${PROJECT_DIR}/run.sh
CMD [ "./run.sh" ]

이제 다운로드용 도커 이미지와 추론 API의 도커 이미지를 이용해 쿠버네티스 클러스터에 웹 API를 배포합니다. 이때 쿠버네티스에는 initContainers를 지정해 초기화용 컨테이너를 기동합니다. 이를 위한 매니페스트는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# manifests/deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: model-load
  namespace: model-load
  labels:
    app: model-load
spec:
  replicas: 4
  selector:
    matchLabels:
      app: model-load
  template:
    metadata:
      labels:
        app: model-load
    spec:
      containers:
        - name: model-load
          image: shibui/ml-system-in-actions:model_load_pattern_api_0.0.1
          ports:
            - containerPort: 8000
          resources:
            limits:
              cpu: 500m
              memory: "300Mi"
            requests:
              cpu: 500m
              memory: "300Mi"
          volumeMounts:
            - name: workdir
              mountPath: /workdir
          env:
            - name: MODEL_FILEPATH
              value: "/workdir/iris_svc.onnx"
      initContainers:
        - name: model-loader
          image: shibui/ml-system-in-actions:model_load_pattern_loader_0.0.1
          imagePullPolicy: Always
          command:
            - python
            - "-m"
            - "src.main"
            - "--gcs_bucket"
            - "ml_system_model_repository"
            - "--gcs_model_blob"
            - "iris_svc.onnx"
            - "--model_filepath"
            - "/workdir/iris_svc.onnx"
          volumeMounts:
            - name: workdir
              mountPath: /workdir
      volumes:
        - name: workdir
          emptyDir: {}

---
apiVersion: v1
kind: Service
metadata:
  name: model-load
  namespace: model-load
  labels:
    app: model-load
spec:
  ports:
    - name: rest
      port: 8000
      protocol: TCP
  selector:
    app: model-load

---
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: model-load
  namespace: model-load
  labels:
    app: model-load
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: model-load
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 50

코드 스니펫 중간을 보면 initContainers로 모델 파일을 다운로드하고, iris_svc.onnx를 추론 모듈 컨테이너에서 불러오는 구조입니다. 모델-인-이미지 패턴과 다른 점은 imagePullPolicy: Always 설정을 하지 않는다는 것입니다. 모델의 업데이트는 반드시 initContainers를 통해서 이루어집니다.

이제 kubectl 명령어를 통해 추론 요청을 시도합니다.

1
kubectl apply -f manifests/namespace.yml

그리고 모델이 올바르게 다운로드 되었는지 로그를 통해 확인할 수 있습니다.

1
kubectl -n model-load logs deployment.apps/model-load

이제 클러스터 내부 엔드포인트에 포트를 포워딩하고 테스트 데이터를 POST 요청합니다.

1
2
3
4
5
6
7
8
9
10
kubectl \
  -n model-load \
  port-forward deployment.apps/model-load \
  8000:8000 &

curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"data": [[1.0, 2.0, 3.0, 4.0]]}' \
  localhost:8000/predict

장점

장점을 요약하자면 다음과 같습니다.

  • 서버 이미지 버전과 모델 파일의 버전 분리가 가능함
  • 서버 이미지의 응용성이 향상되며 서버 이미지가 가벼워짐


This post is licensed under CC BY 4.0 by the author.