M l ops Kubernetes Docker

Tìm hiểu về Kubernetes và áp dụng vào bài toán AI - Phần 2: Kubernetes Job

Tìm hiểu về Kubernetes và áp dụng vào bài toán AI - Phần 2: Kubernetes Job

Trong bài trước, chúng ta đã tìm hiểu về Pod, cách tương tác với Pod và hạn chế của nó. Bài này, chúng ta sẽ làm việc với một thành phần ở mức high level hơn của Kubernetes, đó là Job. Cụ thể, mình sẽ cùng nhau tạo ra các Job để train model và thực hiện Batch Inference.

1. Kubernetes Job là gì?

Theo định nghĩa từ trang chủ của Kubernetes thì:

A Job creates one or more Pods and will continue to retry execution of the Pods until a specified number of them successfully terminate. As pods successfully complete, the Job tracks the successful completions. When a specified number of successful completions is reached, the task (ie, Job) is complete. Deleting a Job will clean up the Pods it created.

Hiểu một cách đơn giản thì Jobs chịu trách nhiệm quản lý một hoặc nhiều Pods để thực hiện một công việc nào đó. Trong quá trình làm việc, các Pods có thể chạy song song với nhau, và nếu một Pod bị chết thì Job sẽ tạo ra một Pod khác để thay thể. Job chỉ được coi là hoàn thành thì tất cả các Pod của nó hoàn thành. Khi xóa Job, các Pods được quản lý bởi nó cũng bị xóa theo.

Job rất phù hợp để chạy các tác vụ kiểu Batch, tức là các tác vụ mà chạy trong một khoảng thời gian nào đó rồi kết thúc. Trong AI, có khá nhiều tác vụ kiểu như vậy, có thể kể ra như Feature Engineering, Cross-Validation, Model Training, Batch Inference. Ví dụ, chúng ta tạo ra một Job để train một model, sau đó lưu model đó vào Storage. Một Job khác sẽ sử dụng model đó để thực hiện Batch Inference.

2. Sử dụng Job cho các tác vụ AI

Chúng ta sẽ thử tạo 2 Jobs:

  • Job thứ nhất để train ML model, lưu model ra file trên AWS S3.
  • Job thứ hai sử dụng model đã trained để thực hiện Batch Inference.

Hãy xem cấu trúc thư mục làm việc:

kubernetes_job
   ├── docker
      ├── batch_inference.py
      ├── Dockerfile
      └── train.py
   └── job
       ├── job-inference.yaml
       └── job-train.yaml

2.1 Code train & inference model

Tạo thư mục docker và copy 2 file train.pybatch_inference.py đã sử dụng trong các bài trước vào thư mục vừa tạo. Sử a lại nội dung của file train.py như sau:

import json
import os

import boto3
from joblib import dump
import matplotlib.pyplot as plt
import numpy as np
from sklearn import ensemble
from sklearn import datasets
from sklearn.utils import shuffle
from sklearn.metrics import mean_squared_error


MODEL_DIR = os.environ["MODEL_DIR"]
MODEL_FILE = os.environ["MODEL_FILE"]
METADATA_FILE = os.environ["METADATA_FILE"]
BUCKET_NAME = os.environ["BUCKET_NAME"]
MODEL_PATH = os.path.join(MODEL_DIR, MODEL_FILE)
METADATA_PATH = os.path.join(MODEL_DIR, METADATA_FILE)


# #############################################################################
# Load data
print("Loading data...")
boston = datasets.load_boston()

print("Splitting data...")
X, y = shuffle(boston.data, boston.target, random_state=13)
X = X.astype(np.float32)
offset = int(X.shape[0] * 0.9)
X_train, y_train = X[:offset], y[:offset]
X_test, y_test = X[offset:], y[offset:]

# #############################################################################
# Fit regression model
print("Fitting model...")
params = {'n_estimators': 500, 'max_depth': 4, 'min_samples_split': 2,
          'learning_rate': 0.01, 'loss': 'ls'}
clf = ensemble.GradientBoostingRegressor(**params)

clf.fit(X_train, y_train)
train_mse = mean_squared_error(y_train, clf.predict(X_train))
test_mse = mean_squared_error(y_test, clf.predict(X_test))
metadata = {
    "train_mean_square_error": train_mse,
    "test_mean_square_error": test_mse
}

print("Serializing model to: {}".format(MODEL_PATH))
dump(clf, MODEL_PATH)

print("Serializing metadata to: {}".format(METADATA_PATH))
with open(METADATA_PATH, 'w') as outfile:  
    json.dump(metadata, outfile)

print("Moving to S3")
s3 = boto3.client('s3')
s3.upload_file(MODEL_PATH, BUCKET_NAME, MODEL_FILE)

Sửa lại code của file batch_inference.py như sau:

import os

import boto3
from joblib import load
import numpy as np
from sklearn import datasets
from sklearn.utils import shuffle


MODEL_DIR = os.environ["MODEL_DIR"]
MODEL_FILE = os.environ["MODEL_FILE"]
METADATA_FILE = os.environ["METADATA_FILE"]
BUCKET_NAME = os.environ["BUCKET_NAME"]
MODEL_PATH = os.path.join(MODEL_DIR, MODEL_FILE)
METADATA_PATH = os.path.join(MODEL_DIR, METADATA_FILE)

def load_model():
    s3 = boto3.resource('s3')
    try:
        s3.Bucket(BUCKET_NAME).download_file(MODEL_FILE, MODEL_PATH)
    except Exception as e:
        if e.response['Error']['Code'] == "404":
            print("The object does not exist.")
        else:
            raise
    return load(MODEL_PATH)

def get_data():
    """
    Return data for inference.
    """
    print("Loading data...")
    boston = datasets.load_boston()
    X, y = shuffle(boston.data, boston.target, random_state=13)
    X = X.astype(np.float32)
    offset = int(X.shape[0] * 0.9)
    X_train, y_train = X[:offset], y[:offset]
    X_test, y_test = X[offset:], y[offset:]
    return X_test, y_test

print("Running inference...")
X, y = get_data()

# #############################################################################
# Load model
print("Loading model from: {}".format(MODEL_PATH))
clf = load_model()

# #############################################################################
# Run inference
print("Scoring observations...")
y_pred = clf.predict(X)
print(y_pred)

2.2 Tạo Docker Images

  • Tạo file Dokerfile

Cũng trong cùng thư mục docker, tạo file Dockerfile với nội dung như sau:

FROM jupyter/scipy-notebook
USER root
WORKDIR /docker
ADD . /docker

RUN pip install awscli joblib boto3
RUN mkdir /docker/model

# Env variables
ENV MODEL_DIR=/docker/model
ENV MODEL_FILE=clf.joblib
ENV METADATA_FILE=metadata.json
ENV BUCKET_NAME=kubernetes-job
  • Build Docker Image này:
$ docker build -t docker-ml .
Sending build context to Docker daemon  6.656kB
Step 1/10 : FROM jupyter/scipy-notebook
 ---> c1a7c7ef5e27
Step 2/10 : USER root
 ---> Using cache
 ---> 0c1dbc43bef8
Step 3/10 : WORKDIR /docker
 ---> Running in fdae735976d0
Removing intermediate container fdae735976d0
 ---> b795fe3bbd80
Step 4/10 : ADD . /docker
 ---> 16082b6c9bda
Step 5/10 : RUN pip install awscli joblib boto3
 ---> Running in e4f9036ae9fc
Collecting awscli
  Downloading awscli-1.18.221-py2.py3-none-any.whl (3.5 MB)
Requirement already satisfied: joblib in /opt/conda/lib/python3.8/site-packages (1.0.0)
Collecting boto3
  Downloading boto3-1.16.61-py2.py3-none-any.whl (130 kB)
Collecting s3transfer<0.4.0,>=0.3.0
  Downloading s3transfer-0.3.4-py2.py3-none-any.whl (69 kB)
Collecting botocore==1.19.61
  Downloading botocore-1.19.61-py2.py3-none-any.whl (7.2 MB)
Collecting PyYAML<5.4,>=3.10
  Downloading PyYAML-5.3.1.tar.gz (269 kB)
Collecting colorama<0.4.4,>=0.2.5
  Downloading colorama-0.4.3-py2.py3-none-any.whl (15 kB)
Collecting rsa<=4.5.0,>=3.1.2
  Downloading rsa-4.5-py2.py3-none-any.whl (36 kB)
Collecting docutils<0.16,>=0.10
  Downloading docutils-0.15.2-py3-none-any.whl (547 kB)
Collecting jmespath<1.0.0,>=0.7.1
  Downloading jmespath-0.10.0-py2.py3-none-any.whl (24 kB)
Requirement already satisfied: urllib3<1.27,>=1.25.4 in /opt/conda/lib/python3.8/site-packages (from botocore==1.19.61->awscli) (1.26.3)
Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in /opt/conda/lib/python3.8/site-packages (from botocore==1.19.61->awscli) (2.8.1)
Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.8/site-packages (from python-dateutil<3.0.0,>=2.1->botocore==1.19.61->awscli) (1.15.0)
Collecting pyasn1>=0.1.3
  Downloading pyasn1-0.4.8-py2.py3-none-any.whl (77 kB)
Building wheels for collected packages: PyYAML
  Building wheel for PyYAML (setup.py): started
  Building wheel for PyYAML (setup.py): finished with status 'done'
  Created wheel for PyYAML: filename=PyYAML-5.3.1-cp38-cp38-linux_x86_64.whl size=44618 sha256=421030371a2f82fdfd722d0b032ce5b0c8d01e02a5ca379c9a0e2eea3a03fd78
  Stored in directory: /tmp/pip-ephem-wheel-cache-cs65titp/wheels/13/90/db/290ab3a34f2ef0b5a0f89235dc2d40fea83e77de84ed2dc05c
Successfully built PyYAML
Installing collected packages: jmespath, pyasn1, botocore, s3transfer, rsa, PyYAML, docutils, colorama, boto3, awscli
  Attempting uninstall: PyYAML
    Found existing installation: PyYAML 5.4.1
    Uninstalling PyYAML-5.4.1:
      Successfully uninstalled PyYAML-5.4.1
Successfully installed PyYAML-5.3.1 awscli-1.18.221 boto3-1.16.61 botocore-1.19.61 colorama-0.4.3 docutils-0.15.2 jmespath-0.10.0 pyasn1-0.4.8 rsa-4.5 s3transfer-0.3.4
Removing intermediate container e4f9036ae9fc
 ---> 0f7e7ec1c6a0
Step 6/10 : RUN mkdir /docker/model
 ---> Running in 7f4c3bd5253a
Removing intermediate container 7f4c3bd5253a
 ---> 0b2c845fbb40
Step 7/10 : ENV MODEL_DIR=/docker/model
 ---> Running in 643ef25a50eb
Removing intermediate container 643ef25a50eb
 ---> 339c22e0a4f9
Step 8/10 : ENV MODEL_FILE=clf.joblib
 ---> Running in d81f810dc092
Removing intermediate container d81f810dc092
 ---> cd7ecdc2f380
Step 9/10 : ENV METADATA_FILE=metadata.json
 ---> Running in 701460e9b463
Removing intermediate container 701460e9b463
 ---> 7646e477d5a9
Step 10/10 : ENV BUCKET_NAME=kubernetes-job
 ---> Running in ce92e3cdbc3b
Removing intermediate container ce92e3cdbc3b
 ---> 31d25c8be720
Successfully built 31d25c8be720
Successfully tagged docker-ml:latest
  • Push Docker Image lên Docker Hub:

Sử dụng các lệnh sau để push Docker Image vừa build lên Docker Hub

$ docker tag docker-ml:latest tiensu/docker-ml:latest
$ docker push tiensu/docker-ml:latest
The push refers to repository [docker.io/tiensu/docker-ml]
76fba3826ca9: Pushed 
59928edb97b5: Pushed 
b5e012598fbb: Pushed 
c4e3257e6eb5: Pushed 
5f70bf18a086: Mounted from tiensu/ml-model-batch-infer 
6f5a41ae77fd: Mounted from tiensu/ml-model-batch-infer 
5a1b9a3f9355: Mounted from tiensu/ml-model-batch-infer 
b1d7816bac14: Mounted from tiensu/ml-model-batch-infer 
c91fed2d1998: Mounted from tiensu/ml-model-batch-infer 
cc70098d00e3: Mounted from tiensu/ml-model-batch-infer 
88727e93cbac: Mounted from tiensu/ml-model-batch-infer 
cadaf24035f3: Mounted from tiensu/ml-model-batch-infer 
8f170f4774e3: Mounted from tiensu/ml-model-batch-infer 
33bd52db887f: Mounted from tiensu/ml-model-batch-infer 
21e5dd010f50: Mounted from tiensu/ml-model-batch-infer 
ea370ab22368: Mounted from tiensu/ml-model-batch-infer 
421d1408f872: Mounted from tiensu/ml-model-batch-infer 
18fd1ca0de51: Mounted from tiensu/ml-model-batch-infer 
8f01aab6d756: Mounted from tiensu/ml-model-batch-infer 
e18a1c4e1d31: Mounted from tiensu/ml-model-batch-infer 
8552f27c3cd8: Mounted from tiensu/ml-model-batch-infer 
1a4c57efcc23: Mounted from tiensu/ml-model-batch-infer 
94b8fe888eac: Mounted from tiensu/ml-model-batch-infer 
02473afd360b: Mounted from tiensu/ml-model-batch-infer 
dbf2c0f42a39: Mounted from tiensu/ml-model-batch-infer 
9f32931c9d28: Mounted from tiensu/ml-model-batch-infer 
latest: digest: sha256:40678bdd8d763129322db38be9f83bc70d1278b7836c7c7f4f4ac3ef6af20e5e size: 6582

2.3 Tạo Kubernetes Job để train ML model

Tương tự như tạo Pod, để tạo Job ta cũng cần khai báo các thông tin cần thiết trong file cấu hình job-train.yaml:

apiVersion: batch/v1
kind: Job
metadata:
  name: job-train-ml-model
spec:
  template:
    spec:
      containers:
      - name: train-container
        imagePullPolicy: Always
        image: tiensu/docker-ml:latest
        command: ["python3",  "train.py"]
        env:
        - name: AWS_ACCESS_KEY_ID
          value: ""
        - name: AWS_SECRET_ACCESS_KEY
          value: ""
      restartPolicy: Never
  backoffLimit: 0

Một số thông tin như sau:

  • apiVersion: Phiên bản của Kubernetes API.
  • kind: Loại tài nguyên của Kubernetes cần tạo, ở đây là Job.
  • metadata: Danh sách các nhãn, các thuộc tính tùy ý mà người phát triển có thể gắn cho Job. Thường các thông tin về Metadata của ML model được gắn ở đây. Kubernetes cũng khuyến nghị một số nhãn ở đây.
  • spec.template: Chính là phần cấu hình của Pod mà ta cần khai báo, tương tự như cấu hình của Pod mà ta đã tạo ở bài trước.
    • imagePullPolicy: Cho phép Kubernetes luôn luôn sử dụng Docker Image từ Docker Hub thay vì Cache Image.
    • env: Danh sách các biến môi trường để Pod sử dụng. Ở đây, chúng ta khai bào 2 biến liên quan đến AWS để làm việc với AWS S3. Mình đã xóa các key mà mình sử dụng. Nếu bạn muốn chạy thử thì hãy thêm key của bạn vào nhé!
  • restartPolicy: Có khởi động lại Container khi nó bị chết hay không?
  • backoffLimit: Số lần cố gắng thực hiện lại Job khi nó bị thất bị.

Chạy lệnh sau để tạo và kiểm tra trạng thái của Job:

$ kubectl create -f job-train.yaml
job.batch/job-train-ml-model created

$ kubectl get jobs
NAME                 COMPLETIONS   DURATION   AGE
job-train-ml-model   1/1           58s        2m19s

Kiểm tra xem các pods của Job là gì và trạng thái của chúng:

$ kubectl get pods --selector=job-name=job-train-ml-model
NAME                       READY   STATUS      RESTARTS   AGE
job-train-ml-model-6fkcd   0/1     Completed   0          2m19s

Xem logs Job/Pod:

$ kubectl logs job-train-ml-model-6fkcd
Loading data...
Splitting data...
Fitting model...
Serializing model to: /docker/model/clf.joblib
Serializing metadata to: /docker/model/metadata.json
Moving to S3

Như vậy, có thể thấy là Job đã chạy xong, file model đã được lưu trên S3.

Cuối cùng, ta có thể xóa Job sau khi chúng đã hoàn thành nhiệm vụ của mình:

$ kubectl delete job job-train-ml-model
job.batch "job-train-ml-model" deleted

2.4 Tạo Kubernetes Job để thực hiện Batch Inference

Chúng ta sẽ sử dụng lại Docker Image đã tạo ở trên cho Job này.

File cấu hình của Job (job-inference.yaml) như sau:

apiVersion: batch/v1
kind: Job
metadata:
  name: job-inference-ml-model
spec:
  template:
    spec:
      containers:
      - name: inference-container
        imagePullPolicy: Always
        image: tiensu/docker-ml:latest
        command: ["python3",  "batch_inference.py"]
        env:
        - name: AWS_ACCESS_KEY_ID
          value: ""
        - name: AWS_SECRET_ACCESS_KEY
          value: ""
      restartPolicy: Never
  backoffLimit: 0

So với cấu hình của Job phía trên, chỉ có các thông tin sau thay đổi: Job name, container name, container command.

Để tạo và liểm tra trạng thái của Job, chạy lệnh sau:

$ kubectl create -f job-inference.yaml
job.batch/job-inference-ml-model created

$ kubectl get jobs
NAME                     COMPLETIONS   DURATION   AGE
job-inference-ml-model   1/1           13s        66s

Kiểm tra xem các Pods của Job và trạng thái tương ứng:

$ kubectl get pods --selector=job-name=job-inference-ml-model
NAME                           READY   STATUS      RESTARTS   AGE
job-inference-ml-model-sk2m4   0/1     Completed   0          2m11s

Chú ý: Tên của Pod = Tên của Job + chuỗi ngẫu nhiên.

Xem logs của Job/Pod:

$ kubectl logs job-inference-ml-model-sk2m4
Running inference...
Loading data...
Loading model from: /docker/model/clf.joblib
Scoring observations...
[15.32448686 27.68741572 24.21374322 31.94786177 10.40175849 34.31050209
 22.05210667 11.58265489 13.19650094 42.84036647 33.03218733 15.77635169
 23.93521876 19.85532224 25.43466604 20.55132127 13.67707622 47.44313586
 17.6460682  21.51806638 22.57388848 16.97645106 16.25503893 20.57862843
 14.57438158 11.81385445 24.78353556 37.77877263 30.23411048 19.67713185
 23.19380271 24.96712102 18.65459129 30.35476911  8.9560549  13.8130382
 14.18848318 17.3840622  19.83840166 24.09904134 20.52649052 15.32433651
 25.8157052  16.47533793 19.2214524  19.86928427 21.47113681 21.56443118
 24.64517965 22.43665872 22.1020877 ]

Như vậy là Job đã thực hiện Batch Inference thành công bằng model nhận được từ S3.

Cuối cùng, xóa Job sau khi nó đã hoàn thành nhiệm vụ để tiết kiệm tài nguyên server:

$ kubectl delete job job-inference-ml-model
job.batch "job-inference-ml-model" deleted

3. Kết luận

Như vậy là mình đã cùng các bạn tìm hiểu và sử dụng Kubernetes Job để thực hiện các tác vụ của một bài toán AI. Có một lưu ý dành cho các bạn đó là trong trường hợp việc thực hiện tạo Job thất bại, hãy nhớ sử dụng lệnh kubectl describe pod <pod_name>, trong đó pod_name là tên Pod của Job để xem đầy đủ logs. Dựa vào logs này, các bạn có thể dễ dàng phát hiện ra nguyên nhân lỗi và cách khắc phục chúng.

bài viết tiếp theo, chúng ta sẽ tìm hiểu và thực hành với CronJob. Mời các bạn đón đọc!

Source code của bài này các bạn tham khảo tại đây.

4. Tham khảo