GPU設備管理
Kubernetes實現對GPU等硬件加速設備管理的支持
需求:
只要在Pod的YAML里面聲明某容器需要的GPU個數,那么Kubernetes為我創建的容器里就應該出現對應的GPU設備以及它對應的驅動目錄
原理:
當用戶的容器被創建之后這個容器里必須出現如下兩部分設備和目錄
1. GPU設備路徑,比如 /dev/nvidia0
2. GPU驅動目錄,比如 /usr/local/nvidia/*
GPU設備路徑正是該容器啟動時的Devices參數; 而驅動目錄則是該容器啟動時的Volume參數
在k8s的GPU支持的實現里,kubelet實際上就是將上述兩部分內容設置在了創建該容器的CRI(Container Runtime Interface)參數
這樣等到該容器啟動之后,對應的容器里就會出現GPU設備和驅動的路徑了
擴展資源類型字段(Extended Resource)
Kubernetes在Pod的API對象里沒有為GPU專門設置資源類型字段,而是使用了一種叫作Extended Resource的特殊字段來負責傳遞GPU的信息
而在kube-scheduler里面並不關心這個字段的具體含義,只會在計算的時候,一律將調度器里保存的該類型資源的可用量,直接減去Pod聲明的數值即可
Extended Resource其實是Kubernetes為用戶設置的一種對自定義資源的支持
調度器如何獲取各種自定義資源在每台宿主機上的可用量,宿主機節點本身就必須向APIserver匯報該資源的可用類型.在K8S中各種類型的資源可用量其實就是Node的Status
這樣在調度器里,它就能夠在緩存里記錄下在node上的nvidia.com/gpu類型的資源的數量是1
Device Plugin插件
在Kubernetes的GPU支持方案里用戶並不需要真正去手動做上述關於Extended Resource.在Kubernetes中對所有硬件加速設備進行管理的功能,都是由一種叫作Device Plugin的插件來負責的.這其中,當然也就包括了對該硬件的Extended Resource進行匯報的邏輯
對於每一種硬件設備都需要有它所對應的Device Plugin進行管理,這些Device Plugin都通過gRPC的方式和本節點上的kubelet連接起來
這個Device Plugin會通過一個叫作ListAndWatch的API,定期向kubelet匯報該Node上GPU的列表,kubelet 在拿到這個列表之后,就可以直接在它向APIServer發送的心跳里,以Extended Resource的方式加上這些GPU的數量,比如nvidia.com/gpu=3.用戶在這里是不需要關心GPU信息向上的匯報流程
需要注意的是ListAndWatch向上匯報的信息,只有本機上GPU的ID列表而不會有任何關於GPU設備本身的信息.而且kubelet在向API Server匯報的時候,只會匯報該GPU對應的Extended Resource的數量.當然,kubelet本身會將這個GPU的ID列表保存在自己的內存里,並通過ListAndWatch API定時更新
當一個Pod想要使用一個GPU的時候,只需要在Pod的limits字段聲明nvidia.com/gpu:1.那么接下來Kubernetes的調度器就會從它的緩存里尋找GPU數量滿足條件的Node,然后將緩存里的GPU數量減1,完成Pod與Node的綁定
調度成功后的Pod信息就會被對應的kubelet拿來進行容器操作.當kubelet發現這個Pod的容器請求一個GPU的時候.
kubelet就會從自己持有的GPU列表里,為這個容器分配一個GPU.此時,kubelet就會向本機的Device Plugin發起一個Allocate() 請求.這個請求攜帶的參數,正是即將分配給該容器的設備ID,也就是GPU列表中的某個元素
當Device Plugin收到Allocate請求之后,它就會根據kubelet傳遞過來的設備ID,從Device Plugin里找到這些設備對應的設備路徑和驅動目錄.這些信息,正是Device Plugin周期性的從本機查詢到的.比如,在NVIDIA Device Plugin的實現里,它會定期訪問nvidia-docker插件,從而獲取到本機的GPU信息
而被分配GPU對應的設備路徑和驅動目錄信息被返回給kubelet之后,kubelet 就完成了為一個容器分配GPU的操作.接下來kubelet會把這些信息追加在創建該容器所對應的CRI請求當中.當這個CRI請求發給 Docker,Docker創建出來的容器里就會出現這個GPU設備並把它所需要的驅動目錄掛載進去
對於其他類型硬件來說要想在k8s所管理的容器里使用這些硬件的話,也需要遵循上述DevicePlugin的流程來實現的Allocate和ListAndWatch API
流程總結:
1.kubelet通過對應的Device Plugin收集本機的數據僅僅是設備ID列表信息,然后上報給API Server. kubelet上報給API Server的數據僅僅是一個設備的總數 這個設備名稱和總數信息會緩存到Node的status列中
2.在pod中指定需要的設備名稱和數量后 調度器會把Pod和Node進行綁定 同時減少對應Node中status列設備對應的數量(status.設備數-pod.設備數)
3.在Node上的kubelet會根據pod要求的設備數 在自己的緩存中彈出對應數量的設備ID號
4.根據這些設備ID號 kubelet再次向Device Plugin發起一個Allocate() 請求 這些設備ID做為請求參數進行發送
5.Device Plugin根據具體的設備ID號就能找到這些設備對應的設備路徑和驅動目錄 然后把這些信息返回給kubelet
6.kubelet在啟動容器的時候把從Device Plugin獲取到的設備路徑和驅動目錄做為CNI參數 傳遞給docker run命令,容器啟動后就會出現相關設備
Device Plugin缺點
GPU等硬件設備的調度,實際上是由kubelet完成的.kubelet會負責從它所持有的硬件設備列表中,為容器挑選一個硬件設備,然后調用Device Plugin的Allocate API來完成這個分配操作. 在整條鏈路中,調度器扮演的角色僅僅是為Pod尋找到可用的支持這種硬件設備的節點而已.
這就使得Kubernetes 里對硬件設備的管理,只能處理“設備個數”這唯一一種情況 (只能找到節點級別)
一旦設備是異構的不能簡單地用“數目”去描述具體使用需求的時候.如:我的Pod想要運行在計算能力最強的那個GPU上Device Plugin就完全不能處理了
在很多場景下我們希望在調度器進行調度的時候,就可以根據整個集群里的某種硬件設備的全局分布,做出一個最佳的調度選擇 這種目前是無法實現的