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:
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.py
và batch_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
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
$ 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
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:
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