根據 Gartner 對全球 CIO 的調查結果顯示,人工智能将成為 2019 年組織革命的颠覆性力量。對于人工智能來說,算力即正義,成本即能力,利用 Docker 和 Kubernetes 代表雲原生技術為 AI 提供了一種新的工作模式,将 GPU 機器放到統一的資源池進行排程和管理,這避免了GPU 資源使用率低下和人工管理的成本。是以,全球主要的容器叢集服務廠商 Kubernetes 都提供了 Nvidia GPU 容器叢集排程能力,但是通常都是将一個 GPU 卡配置設定給一個容器。這雖然可以實作比較好的隔離性,確定使用 GPU 的應用不會被其他應用影響;對于深度學習模型訓練的場景也非常适合,但是,針對模型開發和模型預測的場景還是會顯得比較浪費。基于此,大家有了共享 GPU 的叢集排程需求。
Kubernetes 共享 GPU 叢集排程
共享 GPU 的叢集排程就是能夠讓更多的模型開發和預測服務共享同一個 GPU 卡,進而提高叢集中 Nvidia GPU 的使用率。而這就需要提供 GPU 資源的劃分,而這裡 GPU 資源劃分的次元指的就是 GPU 顯存和 Cuda Kernel 線程的劃分。通常在叢集級别談支援共享 GPU 是以下兩件事情:
1.排程
2.隔離,我們這裡主要讨論的是排程,隔離的方案目前需要使用者通過應用限制(比如使用 Tensorflow 的per_process_gpu_memory_fraction 來控制),未來會提供基于 Nvidia 的 MPS 的可選項, 也會考慮 GPU 的方案。
而對于細粒度的 GPU 卡排程,目前 Kubernetes 社群并沒有很好的方案,這是由于 Kubernetes 對于 GPU 這類擴充資源的定義僅僅支援整數粒度的加加減減,無法支援複雜資源的配置設定。比如使用者希望使用 Pod A 占用半張 GPU卡,這在目前 Kubernetes 的架構設計中無法實作資源配置設定的記錄和調用。這裡挑戰是多卡 GPU 共享是實際矢量資源問題,而 Extened Resource 是标量資源的描述。
針對此問題,我們設計了一個 Out Of Tree 的共享 GPU 排程方案,該方案依賴于 Kubernetes 的現有的工作機制:
- Extended Resource 定義
- Scheduler Extender 機制
- Device Plugin 機制
- Kubectl 的擴充機制
這個 GPU 共享排程擴充的好處是:利用 Kubernetes 的擴充和插件機制實作,對于 API Server,Scheduler,Controller Manager 以及 Kubelet 等核心元件沒有侵入性。這就友善了使用者可以在不同 Kubernetes 版本上應用這個方案,無需 rebase 代碼和重新建構 Kubernetes 二進制包。
使用者場景
[]( https://www.atatech.org/articles/132268#2) 目标
https://www.atatech.org/articles/132268#3) 非目标
https://www.atatech.org/articles/132268#4) 設計原則
-
明确問題簡化設計,第一步隻負責排程和部署,後續再實作運作時顯存管控。
有很多的客戶明确的訴求是首先可以支援多AI應用可以排程到同一個 GPU 上,他們可以接受從應用級别控制顯存的大小,利用類似
控制應用的顯存使用量。那我們要解決的問題就先簡化到以顯存為排程标尺,并且把顯存使用的大小以參數的方式傳遞給容器内部。gpu_options.per_process_gpu_memory_fraction
-
不做侵入式修改
本設計中不會修改 Kubernetes 核心的 Extended Resource 的設計, Scheduler 的實作,Device Plugin 的機制以及 Kubelet 的相關設計。重用 Extended Resource 描述共享資源的申請 API。這樣的好處在于提供一個可以移植的方案,使用者可以在原生 Kubernetes 上使用這個方案。
- 按顯存和按卡排程的方式可以在叢集内并存,但是同一個節點内是互斥的,不支援二者并存;要麼是按卡數目,要麼是按顯存配置設定。
詳細設計
https://www.atatech.org/articles/132268#6) 前提:
- 依舊延用 Kubernetes Extended Resource 定義,但是衡量次元最小機關從 1 個 GPU 卡變為 GPU 顯存的 MiB。如果所節點使用的 GPU 為單卡 16GiB 顯存,它對應的資源就是 16276MiB;
- 由于使用者對于共享GPU的訴求在于模型開發和模型預測場景,在此場景下,使用者申請的GPU資源上限不會超過一張卡,也就是申請的資源上限為單卡。
而我們的工作首先是定義了兩個新的 Extended Resource: 第一個是 gpu-mem, 對應的是 GPU 顯存;第二個是 gpu-count,對應的是 GPU 卡數。 通過兩個标量資源描述矢量資源, 并且結合這一資源,提供支援共享 GPU 的工作機制。下面是基本的架構圖:

https://www.atatech.org/articles/132268#7) 核心功能子產品:
- GPU Share Scheduler Extender: 利用 Kubernetes 的排程器擴充機制,負責在全局排程器 Filter 和 Bind 的時候判斷節點上單個 GPU 卡是否能夠提供足夠的 GPU Mem,并且在 Bind 的時刻将 GPU 的配置設定結果通過 annotation 記錄到 Pod Spec 以供後續 Filter 檢查配置設定結果。
- GPU Share Device Plugin: 利用 Device Plugin 機制,在節點上被 Kubelet 調用負責 GPU 卡的配置設定,依賴 scheduler Extender 配置設定結果執行。
https://www.atatech.org/articles/132268#8) 具體流程:
-
資源上報
GPU Share Device Plugin 利用 nvml 庫查詢到 GPU 卡的數量和每張 GPU 卡的顯存, 通過
ListAndWatch()
将節點的 GPU 總顯存(數量 顯存)作為另外 Extended Resource 彙報給 Kubelet; Kubelet 進一步彙報給 Kubernetes API Server。 舉例說明,如果節點含有兩塊 GPU 卡,并且每塊卡包含 16276MiB,從使用者的角度來看:該節點的 GPU 資源為 16276 2 = 32552; 同時也會将節點上的 GPU 卡數量 2 作為另外一個 Extended Resource 上報。
2. 擴充排程
GPU Share Scheduler Extender 可以在配置設定 gpu-mem 給 Pod 的同時将配置設定資訊以 annotation 的形式保留在 Pod spec 中,并且在過濾時刻根據此資訊判斷每張卡是否包含足夠可用的 gpu-mem 配置設定。
2.1 Kubernetes 預設排程器在進行完所有過濾(filter)行為後會通過 http 方式調用 GPU Share Scheduler Extender的filter 方法, 這是由于預設排程器計算 Extended Resource 時,隻能判斷資源總量是否有滿足需求的空閑資源,無法具體判斷單張卡上是否滿足需求;是以就需要由 GPU Share Scheduler Extender 檢查單張卡上是否含有可用資源。
以下圖為例, 在由 3 個包含兩塊 GPU 卡的節點組成的 Kubernetes 叢集中,當使用者申請
gpu-mem=8138
時,預設排程器會掃描所有節點,發現 N1 所剩的資源為 (16276 * 2 - 16276 -12207 = 4069 )不滿足資源需求,N1 節點被過濾掉。
而 N2 和 N3 節點所剩資源都為 8138MiB,從整體排程的角度看,都符合預設排程器的條件;此時預設排程器會委托 GPU Share Scheduler Extender 進行二次過濾,在二次過濾中,GPU Share Scheduler Extender 需要判斷單張卡是否滿足排程需求,在檢視 N2 節點時發現該節點雖然有 8138MiB 可用資源,但是落到每張卡上看,GPU0 和分别 GPU1 隻有 4069MiB 的可用資源,無法滿足單卡 8138MiB 的訴求。而 N3 節點雖然也是總共有 8138MiB 可用資源,但是這些可用資源都屬于 GPU0,滿足單卡可排程的需求。由此,通過 GPU Share Scheduler Extender 的篩選就可以實作精準的條件篩選。
2.2 當排程器找到滿足條件的節點,就會委托 GPU Share Scheduler Extender 的 bind 方法進行節點和 Pod 的綁定,這裡 Extender 需要做的是兩件事情
- 以 binpack 的規則找到節點中最優選擇的 GPU 卡 id,此處的最優含義是對于同一個節點不同的 GPU 卡,以 binpack 的原則作為判斷條件,優先選擇空閑資源滿足條件但同時又是所剩資源最少的 GPU 卡,并且将其作為
儲存到 Pod 的 annotation 中;同時也儲存該 Pod 申請的 GPU Memory 作為ALIYUN_COM_GPU_MEM_IDX
和ALIYUN_COM_GPU_MEM_POD
儲存至 Pod 的 annotation 中,并且在此時進行 Pod 和所選節點的綁定。ALIYUN_COM_GPU_MEM_ASSUME_TIME
注意:這時還會儲存的 Pod annotation,它被初始化為“false”。它表示該 Pod 在排程時刻被指定到了某塊 GPU 卡,但是并沒有真正在節點上建立該 Pod。
ALIYUN_COM_GPU_MEM_ASSIGNED
代表了
ALIYUN_COM_GPU_MEM_ASSUME_TIME
時間。
指定
如果此時發現配置設定節點上沒有 GPU 資源符合條件,此時不進行綁定,直接不報錯退出,預設排程器會在 assume 逾時後重新排程。
- 調用 Kubernetes API 執行節點和 Pod 的綁定
以下圖為例,當 GPU Share Scheduler Extender 要把 gpu-mem:8138 的 Pod 和經過篩選出來的節點 N1 綁定,首先會比較不同 GPU 的可用資源,分别為 GPU0(12207),GPU1(8138),GPU2(4069),GPU3(16276),其中 GPU2 所剩資源不滿足需求,被舍棄掉;而另外三個滿足條件的 GPU 中, GPU1 恰恰是符合空閑資源滿足條件但同時又是所剩資源最少的 GPU 卡,是以 GPU1 被選出。
3. 節點上運作
當 Pod 和節點綁定的事件被 Kubelet 接收到後,Kubelet 就會在節點上建立真正的 Pod 實體,在這個過程中, Kubelet 會調用 GPU Share Device Plugin 的
Allocate
方法,
Allocate
方法的參數是 Pod 申請的 gpu-mem。而在
Allocate
方法中,會根據 GPU Share Scheduler Extender 的排程決策運作對應的 Pod
- 會列出該節點中所有狀态為 Pending 并且
為ALIYUN_COM_GPU_MEM_ASSIGNED
的 GPU Share Podfalse
- 選擇出其中 Pod Annotation 的
的數量與 Allocate 申請數量一緻的 Pod。如果有多個符合這種條件的 Pod,就會選擇其中ALIYUN_COM_GPU_MEM_POD
最早的 Pod。ALIYUN_COM_GPU_MEM_ASSUME_TIME
- 将該 Pod 的 annotation
設定為ALIYUN_COM_GPU_MEM_ASSIGNED
,并且将 Pod annotation 中的 GPU 資訊轉化為環境變量傳回給 Kubelet 用以真正的建立 Pod。true
https://www.atatech.org/articles/132268#9) 相關項目
目前項目已經開源到 github.com 上
gpushare-scheduler-extender gpushare-device-plugin部署
請參照
部署文檔https://www.atatech.org/articles/132268#11) 測試樣例
- 首先建立一個使用
的應用aliyun.com/gpu-mem
apiVersion: apps/v1
kind: Deployment
metadata:
name: binpack-1
labels:
app: binpack-1
spec:
replicas: 1
selector: # define how the deployment finds the pods it manages
matchLabels:
app: binpack-1
template: # define the pods specifications
metadata:
labels:
app: binpack-1
spec:
containers:
- name: binpack-1
image: cheyang/gpu-player:v2
resources:
limits:
# MiB
aliyun.com/gpu-mem: 1024
使用
使用文檔建構
如何建構視訊 Demo
https://www.atatech.org/articles/132268#15)Demo 1: 部署多個 GPU Share 的 Pod,發現他們以 binpack 的方式被放置到同一個 GPU 卡上
https://www.atatech.org/articles/132268#16)Demo 2: 避免錯誤排程申請資源超過單個 GPU 可用資源的 Pod
Roadmap
- 在 Device Plugin 中提供 Nvidia MPS 的可選支援;
- 支援該方案可以在由 kubeadm 初始化的 Kubernetes 叢集自動化部署;
- 提升 Scheduler Extener 的高可用性;
- 為 GPU, RDMA 和彈性網卡提供通用方案。