1. 程式人生 > 其它 >16. GPU管理和Device Plugin工作機制

16. GPU管理和Device Plugin工作機制

技術標籤:雲原生kubernetes

本文由 CNCF + Alibaba 雲原生技術公開課 整理而來

需求來源

2016 年,隨著 AlphaGo 的走紅和 TensorFlow 專案的異軍突起,一場名為 AI 的技術革命迅速從學術圈蔓延到了工業界,所謂 AI 革命從此拉開了帷幕。

經過三年的發展,AI 有了許許多多的落地場景,包括智慧客服、人臉識別、機器翻譯、以圖搜圖等功能。其實機器學習或者人工智慧,並不是什麼新鮮的概念,而這次熱潮的背後,雲端計算的普及以及算力的巨大提升,才是真正將人工智慧從象牙塔帶到工業界的一個重要推手。

與之相對應的,從 2016 年開始,Kubernetes 社群就不斷收到來自不同渠道的大量訴求,希望能在 Kubernetes 叢集上執行 TensorFlow 等機器學習框架。這些訴求中,除了像 Job

這些離線任務的管理之外,還有一個巨大的挑戰:深度學習所依賴的異構裝置及英偉達的 GPU 支援。

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 APINode 物件進行 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

總的來說,整個過程分為四步,其中前三步都是發生在節點上,第四步是 KubeletApiServer 的互動。

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 的時候,只需要在 Podresourceslimits 欄位中宣告 GPU 資源和對應的數量(比如 nvidia.com/gpu: 1)。Kubernetes 會找到滿足數量條件的節點,然後將該節點的 GPU 數量減 1,並且完成 PodNode 的繫結。

繫結成功後,自然就會被對應節點的 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 的流程就結束了。