問題背景
全球主要的容器集群服務廠商的Kubernetes服務都提供了Nvidia GPU容器調度能力,但是通常都是將一個GPU卡分配給一個容器。這可以實現比較好的隔離性,確保使用GPU的應用不會被其他應用影響;對于深度學習模型訓練的場景非常適合,但是如果對于模型開發和模型預測的場景就會比較浪費。 大家的訴求是能夠讓更多的預測服務共享同一個GPU卡上,進而提高集群中Nvidia GPU的利用率。而這就需要提供GPU資源的劃分,而這里GPU資源劃分的維度指的就是GPU顯存和Cuda Kernel線程的劃分。通常在集群級別談支持共享GPU,通常是兩件事情:
1.調度?
2.隔離,我們這里主要討論的是調度,隔離的方案未來會基于Nvidia的MPS來實現。
而對于細粒度的GPU卡調度,目前Kubernetes社區并沒有很好的方案,這是由于Kubernetes對于GPU這類擴展資源的定義僅僅支持整數粒度的加加減減,無法支持復雜資源的分配。比如用戶希望使用Pod A占用半張GPU卡,這在目前Kubernetes的架構設計中無法實現資源分配的記錄和調用。這里挑戰是多卡GPU共享是實際矢量資源問題,而Extened Resource是標量資源的描述。
針對此問題,我們設計了一個outoftree的共享GPU調度方案,該方案依賴于Kubernetes的現有工作機制:
- Extended Resource定義
- Scheduler Extender機制
- Device Plugin機制
用戶場景
- 作為集群管理員,我想提高集群的GPU使用率;在開發過程中,多個用戶共享模型開發環境
- 作為應用開發人員,我希望能夠同時在Volta GPU上運行多個推理任務
目標
- 能夠讓使用者通過API描述對于一個可共享資源的申請, 并能實現該種資源的調度
非目標
- 不支持該共享資源的隔離
- 不支持超賣
設計原則
- 明確問題簡化設計,第一步只負責調度和部署,后續再實現運行時顯存管控。
有很多的客戶明確的訴求是首先可以支持多AI應用可以調度到同一個GPU上,他們可以接受從應用級別控制顯存的大小,利用類似gpu_options.per_process_gpu_memory_fraction
控制應用的顯存使用量。那我們要解決的問題就先簡化到以顯存為調度標尺,并且把顯存使用的大小以參數的方式傳遞給容器內部。 - 不做侵入式修改
本設計中不會修改Kubernetes核心的Extended Resource的設計, Scheduler的實現,Device Plugin的機制以及Kubelet的相關設計。重用Extended Resource描述共享資源的申請API。這樣的好處在于提供一個可以移植的方案,用戶可以在原生Kubernetes上使用這個方案。 - 按顯存和按卡調度的方式可以在集群內并存,但是同一個節點內是互斥的,不支持二者并存;要么是按卡數目,要么是按顯存分配。
詳細設計
前提:
- 依舊延用Kubernetes Extended Resource定義,但是衡量維度最小單位從1個GPU卡變為GPU顯存的MiB。如果所節點使用的GPU為單卡16GiB顯存,它對應的資源就是16276MiB
- 由于用戶對于共享GPU的訴求在于模型開發和模型預測場景,在此場景下,用戶申請的GPU資源上限不會超過一張卡,也就是申請的資源上限為單卡
而我們的工作首先是定義了兩個新的Extended Resource: 第一個是gpu-mem, 對應的是GPU顯存;第二個是gpu-count,對應的是GPU卡數。 通過兩個標量資源描述矢量資源, 并且結合這一資源,提供支持共享GPU的工作機制。下面是基本的架構圖:
核心功能模塊:
- 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分配結果執行。
具體流程:
1. 資源上報
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卡,并且將其作為
ALIYUN_COM_GPU_MEM_IDX
保存到Pod的annotation中;同時也保存該Pod申請的GPU Memory作為ALIYUN_COM_GPU_MEM_POD
和ALIYUN_COM_GPU_MEM_ASSUME_TIME
保存至Pod的annotation中,并且在此時進行Pod和所選節點的綁定。
注意:這時還會保存ALIYUN_COM_GPU_MEM_ASSIGNED
的Pod annotation,它被初始化為“false”。它表示該Pod在調度時刻被指定到了某塊GPU卡,但是并沒有真正在節點上創建該Pod。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
3.1 會列出該節點中所有狀態為Pending并且ALIYUN_COM_GPU_MEM_ASSIGNED
為false
的GPU Share Pod
3.2 選擇出其中Pod Annotation的ALIYUN_COM_GPU_MEM_POD
的數量與Allocate申請數量一致的Pod。如果有多個符合這種條件的Pod,就會選擇其中ALIYUN_COM_GPU_MEM_ASSUME_TIME
最早的Pod。
3.3 將該Pod的annotation?ALIYUN_COM_GPU_MEM_ASSIGNED
設置為true
,并且將Pod annotation中的GPU信息轉化為環境變量返回給Kubelet用以真正的創建Pod。
相關項目
目前項目已經開源到github.com上
gpushare-scheduler-extender
gpushare-device-plugin
部署
請參照部署文檔
測試樣例
1. 首先創建一個使用aliyun.com/gpu-mem
的應用
apiVersion: apps/v1
kind: Deploymentmetadata:name: binpack-1labels:app: binpack-1spec:replicas: 1selector: # define how the deployment finds the pods it managesmatchLabels:app: binpack-1template: # define the pods specificationsmetadata:labels:app: binpack-1spec:containers:- name: binpack-1image: cheyang/gpu-player:v2resources:limits:# MiBaliyun.com/gpu-mem: 1024
使用
請參照使用文檔
構建
請參照如何構建
視頻Demo
Demo 1: 部署多個GPU Share的Pod,發現他們以binpack的方式被放置到同一個GPU卡上
視頻地址:http://cloud.video.taobao.com...
Demo 2:避免錯誤調度申請資源超過單個GPU可用資源的Pod
視頻地址:http://cloud.video.taobao.com...
Roadmap
- 利用nvidia MPS實現隔離
- 支持該方案可以在由kubeadm初始化的Kubernetes集群自動化部署
- Scheduler Extener的高可用性
- 為GPU, RDMA 和彈性網卡提供通用方案
本文作者:必嘫
閱讀原文
本文為云棲社區原創內容,未經允許不得轉載。