簡介
一個 深度學習模型 (Deep Learning Model) 的開發牽涉到許多的步驟,包含從一開始的資料收集分析與處理、模型的開發與訓練
,到最後將模型進行部署並提供服務使用。這其中牽涉到許多開發環境與開發工具的轉換,對開發人員是一個不小的負擔。而 Kubeflow 便是一個建立在 Kubernetes 之上的模型開發平台,提供開發模型所需的所有工具,並且藉由 Kubernetes 達到資源、網路的彈性控管。
今天要來介紹的便是如何使用 Kubeflow 來完成深度學習模型開發、分散式訓練以及部署模型服務的典型應用情境,如下圖。
事前準備與前言
本篇教學牽涉使用 GKE (google kubernetes engine)
- 部署 Kubeflow
- 使用 Kubeflow/Jupyterhub 開啟 Notebook 開發模型
- 使用 Kubeflow/tf-operator 部署 TFJob 進行模型訓練
- 使用 Kubeflow/KFServing 部署訓練好的模型
本篇教學用到的程式碼與 yaml 檔,可以在我的 Github Repo 找到,歡迎參考。
另外本篇也大量參考此 範例,也可以至此參考。
使用 GKE 部署 Kubeflow
本篇文章範例將使用 Google Kubernetes Engine (GKE) 來部署 Kubernetes 與安裝 Kubeflow,同時將會使用 Google Cloud Storage (GCS) 來儲存訓練好之模型。
使用 GKE 部署 Kubernetes
GKE 在最近的版本中加入了 Workload Identity 的功能,使得Kubernetes 集群中的 Pod 可以使用代表 Google service account 的 Kubernetes service account 來使用其他的Google 雲端服務。詳細的介紹可以參考這邊: Workload Identity
因此在我們創建 Kubernetes 集群的設定中有幾點要注意。
- 選擇
1.15版本
,創建4個節點,每一個包含2 vcpu / 7.5GB mem
- 開啟 GCS (儲存空間) 的 API 存取權
- 於「可用性、網路、安全性和其他功能」的選項中開啟 Workload Identity
部署 Kubeflow
我們使用此文件部署 Kubeflow ,此版本為無認證功能的版本。
Kubeflow 目前透過子專案 kfctl
來發布與安裝 Kubeflow
- 下載 kfctl v1.0 發布版本 Kubeflow releases page
- 解壓縮
tar -xvf kfctl_v0.7.1_<platform>.tar.gz
設定環境變數,方便後續部署。
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 以利後需使用kfctl指令
$ export PATH=$PATH:"<path-to-kfctl>"
# 部署將會使用這個設定檔
$ export CONFIG_URI="https://raw.githubusercontent.com/kubeflow/manifests/v0.7-branch/kfdef/kfctl_k8s_istio.0.7.1.yaml"
# Kubeflow部署過程生成的設定檔存放的資料夾名,可以設定成`my-kubeflow`或是`kf-test`
$ export KF_NAME=<your choice of name for the Kubeflow deployment>
# 放置Kubeflow專案的資料夾路徑
$ export BASE_DIR=<path to a base directory>
# 此次部署Kubeflow的的完整路徑
$ export KF_DIR=${BASE_DIR}/${KF_NAME}使用官方的 kfctl 即可方便的一個指令部署。
1 | # 來到Kubeflow專案底下 |
- 檢驗部署完成
等候大約幾分鐘後,即會看到以下部署成功的字樣
1 | INFO[0193] Applied the configuration Successfully! |
可以檢查所有 Kubeflow namespace 底下的 pod 來確認是否部署成功。
1 | $ kubectl get po -n kubeflow |
因為此版本會同時部署 istio ,因此也可以檢查 istio 相關資源是否成功部署。
1 | $ kubectl get po -n istio-system |
創建 Service Account
Service Account 的目的是使得 Kubernetes 內部的 pod 可以直接使用 Google Service Account 來存取 Google 雲端服務。以下 Service Account 名稱可以自己替換。
1 | # 創建 Google Service Account |
Kubeflow 部署完成
至此 Kubeflow 即部署完成,你可以透過 Port-forward 來透過 istio-gateway 使用 Kubeflow UI。
1 | # 透過localhost:8080連上 |
使用 Notebook Servers
來打開 Jupyter Notebook 進行模型開發。
Jupyter Notebook
使用 tf-operator 進行分散式訓練
tf-operator 與 TFJob 介紹
tf-operator 可以說是 Kubeflow 裡面最早的服務,作為一個operator 其任務便是部署與管理一個 tensorflow 分散式訓練任務的生命週期。一個 tensorflow 分散式訓練可以分為幾個角色,
PS
: 儲存模型的參數,接收來自 Worker 的參數更新Worker
: 實際進行模型訓練的角色,每一次的 epoch 結束都會將更新的參數上傳至 PS ,並取得新的參數進行訓練。Chief
: 一樣也是 Worker 會進行模型訓練,但是同時負責儲存訓練好的模型或是 checkpoint 的角色,如果沒有特別指定則預設會是Worker0
。
而使用者可以透過 tfjob CRD 來定義一組分散式訓練,包含每一個角色所使用的 image 、資源與副本數量。
服務發現
如果曾經使用實體機部署過分散式訓練,有一個很大的重點在於如何讓上述的三個角色在開始執行後能夠發現彼此。
在 tensorflow distributed training 文件中提到,必須建立一個環境變數 TF_Config
,是先填寫每一個角色的所在的 host 與 port,如此一來 tensorflow 才能建立起 clusterspec
與 session
來開始訓練。
而當環境轉移到 Kuburnetes 上時,因為每一個角色以 pod 存在,並透過 service 建立服務與外界互動,因此在實體機上面必須手動設定位址這件事情,我們直接交由 service 與 kubernetes 內部的服務發現運作即可。
簡單來說,tf-operator 除了創建每一個角色的 pods/services 外,同時也會幫你在每一個 pod 的環境變數加上 TF_CONFIG
來記錄所有角色的位置,使得每一個角色可以參考此記錄來建立 tf.train.clusterspec
。
1 | # 舉例: |
而當 Chief
正常結束 ( exit 0
) ,tf-operator會判定此訓練成功結束,將根據資源釋放。
下圖為一個分散式訓練的示意圖:
進行 Mnist 分散式訓練
這邊我們使用大家都非常熟悉的手寫數字辨識範例,但是做了一點修改成為分散式訓練的版本: model.py
為了後續將會使用 TFServing 來進行 Inference ,我們做的修改包含
tf.estimator.train_and_evaluate
來進行訓練classifier.export_savedmodel
儲存模型
Note: 這篇教學不包含將模型程式碼打包成 docker image,詳細文件可以參考原文。此處我們直接使用範例包好的 image 。
部署 TFJob 進行訓練
首先我們先創建一個 GCS Bucket,將在最後作為儲存模型的空間。1
gsutil mb gs://$BUCKET/
接著至 Google Cloud Console 的 GCS 頁面,編輯你的 BUCKET 權限加入剛剛的 Google Service Account ( 以先前的例子來說即為 gcp-sa@XXX ) 成為 管理員
。
有了模型程式碼,且創建好物件儲存空間後後,接下來我們使用以下 yaml 檔來部署一個 TFJob
進行分散式訓練。
1 | apiVersion: kubeflow.org/v1 |
將上面的 TFJob 儲存成 mnist_tfjob.yaml ,進行部署
1 | kubectl apply -f mnist_tfjob.yaml |
且每一個 pod 會多一個環境變數 TF_CONFIG
1 | $ kubectl get pod/mnist-train-dist-worker-0 -n kubeflow -oyaml |
觀察 Chief
的 log,可以看到成功訓練完成,且將訓練好的模型上傳至 GCS
1 | INFO:tensorflow:Start Tensorflow server. |
最後在 GCS 上面可以看到剛剛成功訓練完的模型,包含一個 .pb
檔與參數。
至此便完成了使用 Kubeflow 執行分散式訓練任務。
當然如果你的應用情境是單機訓練,也可以使用 TFJob ,但是只需填寫 Worker spec 並且設定 replcas 為1即可。
Inference
訓練模型的最終目的就是讓模型自己找出模型內最佳參數,使得預測結果盡量符合訓練集。而訓練完成後我們便可以使用該模型,輸入不在訓練集內的同類型資料,可能是一段文字或是一張圖片,並期望得到一個準確的預測結果或是分類,這就稱為 Inference。
比方說輸入一個圖片,模型會產出一個描述圖片的文字。下圖是一個結合 CNN 與 RNN 的 Image Caption Generator 模型
TFServing 與使用
如何將訓練完的模型上線,其重要性不亞於訓練本身,畢竟模型可以被使用才有價值。但是隨著時間推移與資訊的更新,模型的準確率可能下降因為不符合現在的趨勢,模型可能每過一段時間就要再訓練一次再重新部署。也就如上面的圖,模型會不斷進行training並推出v1, v2…。
而為了優化整個部署與 Inference 流程,Google 推出了Tensorflow Serving,來解決模型部署問題。
使用方式非常簡單,有了剛剛的 .pb
模型檔後,即可直接使用 Tensorflow 提供的 Docker image 來部署該模型並提供一個服務端口,使得使用者可以直接透過該端口傳入一個請求,可能是一個圖片或是文字來得到預測結果。
一個簡單的範例,模型也僅僅是使用 Tensorflow 寫一個將矩陣中的每一個元素 除以2加上1
的模型。
1 | 下載 tensorflow serving image |
上面的 $TESTDATA/saved_model_half_plus_two_cpu 指到的目錄底下即是一個模型的 .pb
檔與參數。
以上就是如何使用 TFServing 與 Docker 來部署模型提供服務。更多介紹可以查看 官方文件。
KFServing
KFServing 介紹
既然 Tensorflow 已經可以做到模型部署,又為什麼需要 Kubeflow 提供的 KFServing 呢?原因如下,
直接透過 Docker 部署服務沒辦法維護服務的可用性與彈性。 KFServing 搭配 Kubernetes ,服務將以 pod/service 存在,即可直接交由 Kubernetes 來維護 pod 生命週期,並且可以做到
auto-sacling
( 支援 scale down to 0 ) 應付流量,或是rolling update
來 0-down-time 的更新模型。Tensorflow Serving 只能支援 Tensorflow 產出的模型,但是框架如此之多, KFServing 作為工具箱即盡力支援每一種框架。
KFServing 底層由 Knative 與 istio 實作,因此可以做到同時部署兩個版本的模型進行
金絲雀部署 (canary deployment)
進行A/B test
。一個輸入可能是一張圖或是一段文字,在輸入給模型前需要進行前處理轉成
np.array
或是tensor
,而原始的作法需要加上一個簡易的 HTTP Server,來做資料前處理在輸入進模型。而 KFServing 提供使用者簡單將前處理的函式實作完後,交由 KFServing 部署,並建立一個 pipeline 來串起前處理與實際的模型使用。如下圖,
事前準備
如同部署一個 TFJob 一樣,使用 KFServing 來部署剛剛訓練好的模型也只需要編寫一個 CRD spec,且更簡單的是只需提供 GCS 的位址, KFServing 會幫你下載模型檔案建立起 pod/service 提供服務。
在部署前須先在在 kubeflow namespace 標上 label
1 | kubectl label namespace kubeflow serving.kubeflow.org/inferenceservice=enabled |
Note:
出於某種原因 GCS 上面的資料夾可能出現同名的檔案,導致接下來的部署出錯,需要先手動刪除,如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15gs://${BUCKET}/my-model/export/:
gs://${BUCKET}/my-model/export/ # 多餘的,需刪除
gs://${BUCKET}/my-model/export/1579521445/:
gs://${BUCKET}/my-model/export/1579521445/ # 多餘的,需刪除
gs://${BUCKET}/my-model/export/1579521445/saved_model.pb
gs://${BUCKET}/my-model/export/1579521445/variables/:
gs://${BUCKET}/my-model/export/1579521445/variables/ # 多餘的,需刪除
gs://${BUCKET}/my-model/export/1579521445/variables/variables.data-00000-of-00001
gs://${BUCKET}/my-model/export/1579521445/variables/variables.index
使用以下指令刪除
gsutil rm gs://${BUCKET}/my-model/export/
...
使用 KFServing 部署模型
我們的模型是 Mnist 手寫數字辨識模型,因此我們需要一個前處理的步驟將圖片透過 openCV 來做處理,以下是前處理的程式碼,需要按照 KFServing 提供的 framework 來實作成一個 Module。
前處理的程式碼 image_transformer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25import kfserving
from typing import List, Dict
from PIL import Image
import numpy as np
import io
import base64
import cv2
def image_transform(instance):
byte_array = base64.b64decode(instance['image_bytes']['b64'])
image = Image.open(io.BytesIO(byte_array))
#img = cv2.imread(image, cv2.IMREAD_GRAYSCALE)
g = cv2.resize(255 - np.asarray(image), (28, 28))
g = g.flatten() / 255.0
return g.tolist()
class ImageTransformer(kfserving.KFModel): # 繼承 kfserving.KFModel
def __init__(self, name: str, predictor_host: str):
super().__init__(name)
self.predictor_host = predictor_host
self._key = None
def preprocess(self, inputs: Dict) -> Dict: # 實作 preprocess 函式
return {'instances': [image_transform(instance) for instance in inputs['instances']]}
主程式 main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import kfserving
import argparse
from .image_transformer import ImageTransformer
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()
if __name__ == "__main__":
transformer = ImageTransformer(
args.model_name, predictor_host=args.predictor_host)
kfserver = kfserving.KFServer()
kfserver.start(models=[transformer])
最後打包成 Docker image ,細節可以參考我的 github repo ,下面直接用我已經包好的 image 即可。
有了模型以及前處理的 Docker image,我們就可以完成 KFServing 的部署 spec。
將以下檔案存成 mnist_inference.yaml
1 | apiVersion: "serving.kubeflow.org/v1alpha2" |
部署該檔案,並等待該部署成功。
1 | kubectl apply -f mnist_inference.yaml |
使用模型進行 Inference
將測試用的圖片轉成base64,將下圖存成 0.png
1 | cat 0.png | base64 # 將結果複製到下面的 input.json 中之 b64 的值 |
將以下檔案存成 input.json1
2
3
4
5
6
7
8
9{
"instances": [
{
"image_bytes": {
"b64": "iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAAAAABXZoBIAAABC0lEQVR4nGNgGHCg0/NvOw8Oudqnf//+dcQq5bLk5j+g5OMlfphyDt/+/v33dxtQehuGrMcHoPA/F9FNX//+/WyCJvfi798f9714GRjKgYp8USUfAIUawCyVa3//vrBAkmJb+Pvv3xIWCEcNqC4DSbIJaGYDVI6BfRGKpMrNv3/vI7htf/8eQPAu//t3XwfBbf//7z+cEw70oTmSJUCdf+Gcor9/v0njkgQGWiuSXNh+YCAhSX4QgLFZvSuAQfTBEy455e/PXDEQQ0h+1o6/ILAPYU4mkHurDQjO/gNL/a2TQUiyXfkLBf9BxKcQRiQXMBjOg0q+uXC8cQlalDCwZ7z5+3fJoww7hoEGAMUNp28BRiGTAAAAAElFTkSuQmCC"
}
}
]
}
接著我們就可以送出請求,使用我們訓練好的mnist模型做預測吧
1 | MODEL_NAME=mnist |
小結
以上就是使用 Kubeflow 提供的 Notebook Servers 、 tf-operator 、 KFServing 來完成一個典型的模型開發流程。可以發現還有許多不足的地方,
- 比方說目前的模型不夠精準,可能需要進行 Hyperparameter Tuning , Kubeflow 提出了 Katib 服務。
- 開發流程的每一個步驟仍然需要手動的部署與執行,且步驟與步驟之間缺乏銜接,使用者體驗仍然不夠流暢。 Kubeflow 提出了 Pipeline 來建立一個工作流,使用者可以在一個工作流完成每一個階段的部署、執行與驗證。
- Jupyterhub Notebook 開發完模型後,使用者必須手動打包 Container image 來供後續使用, Kubeflow 提出了 Fairing ,使用者可以從 Notebook 直接打包 image 。
下面這張圖是完整個 Kubeflow Scope
而 Kubeflow 將在近期推出 v1.0 ,而此版本最重要的就是確保 Kubeflow Community 認為最重要的 Custom User Journey 可用且可靠。
本篇的教學已經基本涵括了大部分的 Custom User Journey , 將會在接下來幾篇針對 Pipeline 、與其餘服務做介紹與使用教學。