16. GPU管理和Device Plugin工作機制
技術標籤:雲原生kubernetes
本文由 CNCF + Alibaba 雲原生技術公開課 整理而來
需求來源
2016 年,隨著 AlphaGo 的走紅和 TensorFlow 專案的異軍突起,一場名為 AI 的技術革命迅速從學術圈蔓延到了工業界,所謂 AI 革命從此拉開了帷幕。
經過三年的發展,AI 有了許許多多的落地場景,包括智慧客服、人臉識別、機器翻譯、以圖搜圖等功能。其實機器學習或者人工智慧,並不是什麼新鮮的概念,而這次熱潮的背後,雲端計算的普及以及算力的巨大提升,才是真正將人工智慧從象牙塔帶到工業界的一個重要推手。
與之相對應的,從 2016 年開始,Kubernetes 社群就不斷收到來自不同渠道的大量訴求,希望能在 Kubernetes 叢集上執行 TensorFlow 等機器學習框架。這些訴求中,除了像 Job
Kubernetes 管理 GPU 能帶來什麼好處呢?本質上是成本和效率的考慮。由於相對 CPU 來說,GPU 的成本偏高。在雲上單個 CPU 通常是一小時幾毛錢,而 GPU 的花費則是從單個 GPU 每小時 10 元 ~ 30 元不等,這就要想方設法的提高 GPU 的使用率。
為什麼要用 Kubernetes 管理以 GPU 為代表的異構資源?具體來說是 3 個方面:
加速部署:通過容器構想避免重複部署機器學習複雜環境 提升叢集資源使用率:統一排程和分配叢集資源 保障資源獨享:利用容器隔離異構裝置,避免互相影響
首先是加速部署,避免把時間浪費在環境準備的環節中。通過容器映象技術,將整個部署過程進行固化和複用,現在許許多多的框架都提供了容器映象,可以藉此提升 GPU 的使用效率。
通過分時複用,來提升 GPU 的使用效率。當 GPU 的卡數達到一定數量後,就需要用到 Kubernetes 的統一排程能力,使得資源使用方能夠做到用即申請、完即釋放,從而盤活整個 GPU 的資源池。
而此時還需要通過 Docker 自帶的裝置隔離能力,避免不同應用的程序運行同一個裝置上,造成互相影響。在高效低成本的同時,也保障了系統的穩定性。
GPU 的容器化
- 容器環境下使用 GPU 應用:
在容器環境下使用 GPU 應用,主要分為 2 步:
1. 構建支援 GPU 的容器映象
2. 利用 Docker 將該映象執行起來,並把 GPU 裝置和依賴庫對映到容器中
- 準備 GPU 容器映象:
有 2 個方法準備 GPU 容器映象:
1. 直接使用官方深度學習容器映象
2. 基於 Nvidia 的 CUDA 映象基礎構建
可以直接從 docker.hub 或阿里雲映象服務中尋找官方的 GPU 映象,包括像 TensorFlow、Caffe、PyTorch 等流行的機器學習框架,都有提供標準的映象。這樣的好處是簡單便捷,而且安全可靠。
當然如果官方映象無法滿足需求時,比如對 TensorFlow 框架進行了定製修改,就需要重新編譯構建自己的 TensorFlow 映象。這種情況下的最佳實踐是:依託於 Nvidia 官方映象繼續構建,而不要從頭開始。
- GPU 容器映象原理:
要了解如何構建 GPU 容器映象,首先要知道如何要在宿主機上安裝 GPU 應用。要在宿主機上安裝 GPU 應用,最底層是先安裝 Nvidia 硬體驅動;再到上面是通用的 CUDA 工具庫;最上層是 PyTorch、TensorFlow 這類的機器學習框架。上兩層的 CUDA 工具庫和應用的耦合度較高,應用版本變動後,對應的 CUDA 版本大概率也要更新;而最下層的 Nvidia 驅動,通常情況下是比較穩定的,它不會像 CUDA 和應用一樣,經常更新。
同時 Nvidia 驅動需要核心原始碼編譯,英偉達的 GPU 容器方案是:在宿主機上安裝 Nvidia 驅動,而在 CUDA 以上的軟體交給容器映象來做。同時把 Nvidia 驅動裡面的連結以 Mount Bind 的方式對映到容器中。這樣的一個好處是:當安裝了一個新的 Nvidia 驅動之後,就可以在同一個機器節點上執行不同版本的 CUDA 映象了。
- 利用容器執行 GPU 程式:
通過上面可以知道,在執行時刻一個 GPU 容器和普通容器之間的差別,僅僅在於需要將宿主機的裝置和 Nvidia 驅動庫對映到容器中。
示例:
docker run -it \
--volume=nvidia_driver_xxx_xx:/usr/local/nvidia:ro \
--device=/dev/nvidiactl \
--device=/dev/nvidia-uvm \
--device=/dev/nvidia-uvm-tools \
--device=/dev/nvidia0 \
nvidia/cuda nvidia-smi
通常會使用 Nvidia-docker 來執行 GPU 容器,而 Nvidia-docker 的實際工作就是來自動化做這兩個工作。其中掛載裝置比較簡單,而真正比較複雜的是 GPU 應用依賴的驅動庫。對於深度學習,視訊處理等不同場景,所使用的一些驅動庫並不相同。這又需要依賴 Nvidia 的領域知識,而這些領域知識就被貫穿到了 Nvidia 的容器之中。
Kubernetes 的 GPU 管理
- 部署 GPU Kubernetes:
首先安裝 Nvidia 驅動,
yum install -y gcc kernel-devel-$(uname -r)
/bin/sh ./NVIDIA-Linux-x86_64*.run
安裝 Nvidia Docker2,
yum install nvidia-docker2
pkill -SIGNUP dockerd
從 Nvidia 的 git repo 下去下載 Device Plugin 的部署宣告檔案,並且通過 kubectl create
命令進行部署。這裡 Device Plugin 是以 Deamonset
的方式進行部署的。
部署 Nvidia Device Plugin,
kubectl create -f nvidia-device-plugin.yml
- 在 Kubernetes 中使用 GPU:
站在使用者的角度,在 Kubernetes 中使用 GPU 容器還是非常簡單的。只需要在 Pod
資源配置的 limits
欄位中指定 nvidia.com/gpu
使用 GPU 的數量,然後再通過 kubectl create
命令將 GPU 的 Pod
部署完成。
cuda-vector-add Pod
yaml 檔案示例:
apiVersion: v1
kind: Pod
metadata:
name: cuda-vector-add
spec:
restartPolicy: OnFailure
containers:
- name: cuda-vector-add
image: nvidia/cuda-vector-add:v0.1
resources:
limits:
cpu: 250m
memory: 512Mi
nvidia.com/gpu: 1 #指定 GPU 數量
與普通容器不同的是,在 Kubernetes 中使用 GPU 容器只需要額外指定 .spec.containers.resources.limits
欄位,宣告 GPU 資源和對應的數量即可。
工作原理
- 通過擴充套件的方式管理 GPU 資源:
Kubernetes 本身是通過外掛擴充套件的機制來管理 GPU 資源的,有 2 個獨立的內部機制:
1. Extend Resources,允許使用者自定義資源名稱。而該資源的度量是整數級別,目的在於通過一個通用的模式支援不同的異構裝置,
包括 RDMA、FPGA、AMD GPU 等等,而不僅僅是為 Nvidia GPU 設計的
2. Device Plugin Framework,允許第三方裝置提供商以外接的方式對裝置進行全生命週期的管理,而 Device Plugin Framework 建立 Kubernetes 和 Device Plugin 模組之間的橋樑。
它一方面負責裝置資訊的上報到 Kubernetes,另一方面負責裝置的排程選擇
- Extended Resource 的上報:
Extend Resources 屬於 Node-level 的 api,完全可以獨立於 Device Plugin 使用。而上報 Extend Resources,只需要通過一個 PACTH API
對 Node
物件進行 status
部分更新即可,而這個 PACTH
操作可以通過一個簡單的 curl 命令來完成。這樣,在 Kubernetes 排程器中就能夠記錄這個節點的 GPU 型別,它所對應的資源數量是 1。
執行 PACTH
操作,
curl --header "Content-Type: application/json-patch+json" \
--request PATCH \
--data '[{"op": "add", "path": "/status/capacity/nvidia.com/gpu", "value": "1"}]' \
https://localhost:6443/api/v1/nodes/<node-name>/status
檢視對應資源數量,
kubectl describe node <node-name>
apiVersion: v1
kind: Node
...
Status:
Capacity:
nvidia.com/gpu: 1
如果使用的是 Device Plugin,就不需要做 PACTH
操作,只需要遵從 Device Plugin 的程式設計模型,在裝置上報的工作中 Device Plugin 就會完成這個操作。
- Device Plugin 工作機制:
整個 Device Plugin 的工作流程可以分成 2 個部分:
啟動時刻的資源上報
使用者使用時刻的排程和執行
Device Plugin 的開發比較簡單,主要包括最關注與最核心的 2 個事件方法:
1. ListAndWatch 對應資源的上報,同時還提供健康檢查的機制。當裝置不健康的時候,可以上報給 Kubernetes 不健康裝置的 ID,讓 Device Plugin Framework 將這個裝置從可排程裝置中移除
2. 而 Allocate 會被 Device Plugin 在部署容器時呼叫,傳入的引數核心就是容器會使用的裝置 ID,返回的引數是容器啟動時,需要的裝置、資料卷以及環境變數
- 資源上報和監控:
對於每一個硬體裝置,都需要它所對應的 Device Plugin 進行管理,這些 Device Plugin 以客戶端的身份通過 GRPC 的方式對 Kubelet
中的 Device Plugin Manager 進行連線,並且將自己監聽的 Unis socket api 的版本號和裝置名稱比如 GPU,上報給 Kubelet
。
總的來說,整個過程分為四步,其中前三步都是發生在節點上,第四步是 Kubelet
和 ApiServer
的互動。
1. Device Plugin 的註冊,需要 Kubernetes 知道要跟哪個 Device Plugin 進行互動。因為一個節點上可能有多個裝置,需要 Device Plugin 以客戶端的身份向 Kubelet 彙報 3 件事情:
1. 我是誰?就是 Device Plugin 所管理的裝置名稱,是 GPU 還是 RDMA
2. 我在哪?就是外掛自身監聽的 unis socket 所在的檔案位置,讓 Kubelet 能夠呼叫自己
3. 互動協議,即 API 的版本號
2. 服務啟動,Device Plugin 會啟動一個 GRPC 的 server。在此之後 Device Plugin 一直以這個伺服器的身份提供服務讓 Kubelet 來訪問,而監聽地址和提供 API 的版本就已經在第一步完成了
3. 當該 GRPC server 啟動之後,Kubelet 會建立一個到 Device Plugin 的 ListAndWatch 的長連線, 用來發現裝置 ID 以及裝置的健康狀態。當 Device Plugin 檢測到某個裝置不健康的時候,就會主動通知 Kubelet。
而此時如果這個裝置處於空閒狀態,Kubelet 會將其移除可分配的列表。但是當這個裝置已經被某個 Pod 所使用的時候,Kubelet 就不會做任何事情,如果此時殺掉這個 Pod 是一個很危險的操作
4. Kubelet 會將這些裝置暴露到 Node 節點的狀態中,把裝置數量傳送到 Kubernetes 的 ApiServer 中。後續排程器可以根據這些資訊進行排程
需要注意的是 Kubelet
在向 ApiServer
進行彙報的時候,只會彙報該 GPU 對應的數量。而 Kubelet 自身的 Device Plugin Manager 會對這個 GPU 的 ID 列表進行儲存,並用來具體的裝置分配。而這個對於 Kubernetes 全域性排程器來說,它不掌握這個 GPU 的 ID 列表,它只知道 GPU 的數量。
這就意味著在現有的 Device Plugin 工作機制下,Kubernetes 的全域性排程器無法進行更復雜的排程。比如說想做兩個 GPU 的親和性排程,同一個節點兩個 GPU 可能需要進行通過 NVLINK 通訊而不是 PCIe 通訊,才能達到更好的資料傳輸效果。在這種需求下,目前的 Device Plugin 排程機制中是無法實現的。
Pod
的排程和執行的過程:
Pod
想使用一個 GPU 的時候,只需要在 Pod
的 resources
下 limits
欄位中宣告 GPU 資源和對應的數量(比如 nvidia.com/gpu: 1
)。Kubernetes 會找到滿足數量條件的節點,然後將該節點的 GPU 數量減 1,並且完成 Pod
與 Node
的繫結。
繫結成功後,自然就會被對應節點的 Kubelet
拿來建立容器。而當 Kubelet
發現這個 Pod
的容器請求的資源是一個 GPU 的時候,Kubelet
就會委託自己內部的 Device Plugin Manager 模組,從自己持有的 GPU 的 ID 列表中選擇一個可用的 GPU 分配給該容器。此時 Kubelet
就會向本機的 DeAvice Plugin 發起一個 Allocate 請求,這個請求所攜帶的引數,正是即將分配給該容器的裝置 ID 列表。
Device Plugin 收到 AllocateRequest 請求之後,它就會根據 Kubelet
傳過來的裝置 ID,去尋找這個裝置 ID 對應的裝置路徑、驅動目錄以及環境變數,並且以 AllocateResponse 的形式返還給 Kubelet
。
AllocateResponse 中所攜帶的裝置路徑和驅動目錄資訊,一旦返回給 Kubelet
之後,Kubelet
就會根據這些資訊執行為容器分配 GPU 的操作,這樣 Docker
會根據 Kubelet
的指令去建立容器,而這個容器中就會出現 GPU 裝置,並且把它所需要的驅動目錄給掛載進來。
至此 Kubernetes 為 Pod
分配一個 GPU 的流程就結束了。