Categories
程式開發

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制


導讀:2016 年,隨著 AlphaGo 的走紅和 TensorFlow 項目的異軍突起,一場名為 AI 的技術革命迅速從學術圈蔓延到了工業界,所謂 AI 革命從此拉開了帷幕。該熱潮的背後推手正是雲計算的普及和算力的巨大提升。

需求來源

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

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 1

與之相對應的,從 2016 年開始,Kubernetes 社區就不斷收到來自不同渠道的大量訴求:希望能在 Kubernetes 集群上運行 TensorFlow 等機器學習框架。這些訴求中,除了之前文章所介紹的,像 Job 這些離線任務的管理之外,還有一個巨大的挑戰:深度學習所依賴的異構設備及英偉達的 GPU 支持。

我們不禁好奇起來:Kubernetes 管理 GPU 能帶來什麼好處呢?

本質上是成本和效率的考慮。由於相對 CPU 來說,GPU 的成本偏高。在雲上單 CPU 通常是一小時幾毛錢,而 GPU 的花費則是從單 GPU 每小時 10 元 ~ 30 元不等,這就要想方設法的提高 GPU 的使用率。

為什麼要用 Kubernetes 管理以 GPU 為代表的異構資源?

具體來說是三個方面:

  • 加速部署:通過容器構想避免重複部署機器學習複雜環境;

  • 提升集群資源使用率:統一調度和分配集群資源;

  • 保障資源獨享:利用容器隔離異構設備,避免互相影響。

首先是加速部署,避免把時間浪費在環境準備的環節中。通過容器鏡像技術,將整個部署過程進行固化和復用,如果同學們關注機器學習領域,可以發現許許多多的框架都提供了容器鏡像。我們可以藉此提升 GPU 的使用效率。

通過分時復用,來提升 GPU 的使用效率。當 GPU 的卡數達到一定數量後,就需要用到 Kubernetes 的統一調度能力,使得資源使用方能夠做到用即申請、完即釋放,從而盤活整個 GPU 的資源池。

而此時還需要通過 Docker 自帶的設備隔離能力,避免不同應用的進程運行同一個設備上,造成互相影響。在高效低成本的同時,也保障​​了系統的穩定性。

GPU 的容器化

上面了解到了通過Kubernetes 運行GPU 應用的好處,通過之前系列文章的學習也知道,Kubernetes 是容器調度平台,而其中的調度單元是容器,所以在學習如何使用Kubernetes 之前,我們先了解一下如何在容器環境內運行GPU 應用。

1. 容器環境下使用 GPU 應用

在容器環境下使用 GPU 應用,實際上不復雜。主要分為兩步:

  • 構建支持 GPU 的容器鏡像;

  • 利用 Docker 將該鏡像運行起來,並且把 GPU 設備和依賴庫映射到容器中。

2. 如何準備 GPU 容器鏡像

有兩個方法準備:

  • 直接使用官方深度學習容器鏡像

比如直接從 docker.hub 或者阿里雲鏡像服務中尋找官方的 GPU 鏡像,包括像 TensorFlow、Caffe、PyTorch 等流行的機器學習框架,都有提供標準的鏡像。這樣的好處是簡單便捷,而且安全可靠。

  • 基於 Nvidia 的 CUDA 鏡像基礎構建

當然如果官方鏡像無法滿足需求時,比如你對 TensorFlow 框架進行了定制修改,就需要重新編譯構建自己的 TensorFlow 鏡像。這種情況下,我們的最佳實踐是:依託於 Nvidia 官方鏡像繼續構建,而不要從頭開始。

如下圖中的 TensorFlow 例子所示,這個就是以 Cuda 鏡像為基礎,開始構建自己的 GPU 鏡像。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 2

3. GPU 容器鏡像原理

要了解如何構建 GPU 容器鏡像,先要知道如何要在宿主機上安裝 GPU 應用。

如下圖左邊所示,最底層是先安裝 Nvidia 硬件驅動;再到上面是通用的 Cuda 工具庫;最上層是 PyTorch、TensorFlow 這類的機器學習框架。

上兩層的CUDA 工具庫和應用的耦合度較高,應用版本變動後,對應的CUDA 版本大概率也要更新;而最下層的Nvidia 驅動,通常情況下是比較穩定的,它不會像CUDA和應用一樣,經常更新。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 3

同時 Nvidia 驅動需要內核源碼編譯,如上圖右側所示,英偉達的 GPU 容器方案是:在宿主機上安裝 Nvidia 驅動,而在 CUDA 以上的軟件交給容器鏡像來做。同時把 Nvidia 驅動裡面的鏈接以 Mount Bind 的方式映射到容器中。

這樣的一個好處是:當你安裝了一個新的 Nvidia 驅動之後,你就可以在同一個機器節點上運行不同版本的 CUDA 鏡像了。

4. 如何利用容器運行 GPU 程序

有了前面的基礎,我們就比較容易理解 GPU 容器的工作機制。下圖是一個使用 Docker 運行 GPU 容器的例子。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 4

我們可以觀察到,在運行時刻一個 GPU 容器和普通容器之間的差別,僅僅在於需要將宿主機的設備和 Nvidia 驅動庫映射到容器中。

上圖右側反映了 GPU 容器啟動後,容器中的 GPU 配置。右上方展示的是設備映射的結果,右下方顯示的是驅動庫以 Bind 方式映射到容器後,可以看到的變化。

通常大家會使用 Nvidia-docker 來運行 GPU 容器,而 Nvidia-docker 的實際工作就是來自動化做這兩個工作。其中掛載設備比較簡單,而真正比較複雜的是 GPU 應用依賴的驅動庫。

對於深度學習,視頻處理等不同場景,所使用的一些驅動庫並不相同。這又需要依賴 Nvidia 的領域知識,而這些領域知識就被貫穿到了 Nvidia 的容器之中。

Kubernetes 的 GPU 管理

1. 如何部署 GPU Kubernetes

首先看一下如何給一個 Kubernetes 節點增加 GPU 能力,我們以 CentOS 節點為例。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 5

如上圖所示:

  • 首先安裝 Nvidia 驅動

由於 Nvidia 驅動需要內核編譯,所以在安裝 Nvidia 驅動之前需要安裝 gcc 和內核源碼。

  • 第二步通過 yum 源,安裝 Nvidia Docker2

安裝完 Nvidia Docker2 需要重新加載 docker,可以檢查 docker 的 daemon.json 裡面默認啟動引擎已經被替換成了 nvidia,也可以通過 docker info 命令查看運行時刻使用的 runC 是不是 Nvidia 的 runC。

  • 第三步是部署 Nvidia Device Plugin

從 Nvidia 的 git repo 下去下載 Device Plugin 的部署聲明文件,並且通過 kubectl create 命令進行部署。

這裡 Device Plugin 是以 deamonset 的方式進行部署的。這樣我們就知道,如果需要排查一個Kubernetes 節點無法調度GPU 應用的問題,需要從這些模塊開始入手,比如我要查看一下Device Plugin 的日誌,Nvidia 的runC 是否配置為docker 默認runC 以及Nvidia 驅動是否安裝成功。

2. 驗證部署 GPU Kubernetes 結果

當 GPU 節點部署成功後,我們可以從節點的狀態信息中發現相關的 GPU 信息。

  • 一個是 GPU 的名稱,這裡是 nvidia.com/gpu;

  • 另一個是它對應的數量,如下圖所示是 2,表示在該節點中含有兩個 GPU。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 6

3. 在 Kubernetes 中使用 GPU 的 yaml 樣例

站在用戶的角度,在 Kubernetes 中使用 GPU 容器還是非常簡單的。

只需要在 Pod 資源配置的 limit 字段中指定 nvidia.com/gpu 使用 GPU 的數量,如下圖樣例中我們設置的數量為 1;然後再通過 kubectl create 命令將 GPU 的 Pod 部署完成。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 7

4. 查看運行結果

部署完成後可以登錄到容器中執行 nvidia-smi 命令觀察一下結果,可以看到在該容器中使用了一張 T4 的 GPU 卡。說明在該節點中的兩張GPU 卡其中一張已經能在該容器中使用了,但是節點的另外一張卡對於改容器來說是完全透明的,它是無法訪問的,這裡就體現了GPU的隔離性。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 8

工作原理

1. 通過擴展的方式管理 GPU 資源

Kubernetes 本身是通過插件擴展的機制來管理 GPU 資源的,具體來說這裡有兩個獨立的內部機制。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 9

  • 第一個是 Extend Resources,允許用戶自定義資源名稱。而該資源的度量是整數級別,這樣做的目的在於通過一個通用的模式支持不同的異構設備,包括 RDMA、FPGA、AMD GPU 等等,而不僅僅是為 Nvidia GPU 設計的;

  • Device Plugin Framework 允許第三方設備提供商以外置的方式對設備進行全生命週期的管理,而 Device Plugin Framework 建立 Kubernetes 和 Device Plugin 模塊之間的橋樑。它一方面負責設備信息的上報到 Kubernetes,另一方面負責設備的調度選擇。

2. Extended Resource 的上報

Extend Resources 屬於 Node-level 的 api,完全可以獨立於 Device Plugin 使用。而上報 Extend Resources,只需要通過一個 PACTH API 對 Node 對象進行 status 部分更新即可,而這個 PACTH 操作可以通過一個簡單的 curl 命令來完成。這樣,在 Kubernetes 調度器中就能夠記錄這個節點的 GPU 類型,它所對應的資源數量是 1。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 10

當然如果使用的是 Device Plugin,就不需要做這個 PACTH 操作,只需要遵從 Device Plugin 的編程模型,在設備上報的工作中 Device Plugin 就會完成這個操作。

3. Device Plugin 工作機制

介紹一下 Device Plugin 的工作機制,整個 Device Plugin 的工作流程可以分成兩個部分:

  • 一個是啟動時刻的資源上報;

  • 另一個是用戶使用時刻的調度和運行。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 11

Device Plugin 的開發非常簡單。主要包括最關注與最核心的兩個事件方法:

  • 其中 ListAndWatch 對應資源的上報,同時還提供健康檢查的機制。當設備不健康的時候,可以上報給 Kubernetes 不健康設備的 ID,讓 Device Plugin Framework 將這個設備從可調度設備中移除;

  • 而 Allocate 會被 Device Plugin 在部署容器時調用,傳入的參數核心就是容器會使用的設備 ID,返回的參數是容器啟動時,需要的設備、數據卷以及環境變量。

4. 資源上報和監控

對於每一個硬件設備,都需要它所對應的Device Plugin 進行管理,這些Device Plugin 以客戶端的身份通過GRPC 的方式對kubelet 中的Device Plugin Manager 進行連接,並且將自己監聽的Unis socket api 的版本號和設備名稱比如GPU,上報給kubelet。

我們來看一下 Device Plugin 資源上報的整個流程。總的來說,整個過程分為四步,其中前三步都是發生在節點上,第四步是 kubelet 和 api-server 的交互。

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 12

  • 第一步是 Device Plugin 的註冊,需要 Kubernetes 知道要跟哪個 Device Plugin 進行交互。這是因為一個節點上可能有多個設備,需要 Device Plugin 以客戶端的身份向 Kubelet 匯報三件事情:我是誰?就是 Device Plugin 所管理的設備名稱,是 GPU 還是 RDMA;我在哪?就是插件自身監聽的 unis socket 所在的文件位置,讓 kubelet 能夠調用自己;交互協議,即 API 的版本號;

  • 第二步是服務啟動,Device Plugin 會啟動一個 GRPC 的 server。在此之後 Device Plugin 一直以這個服務器的身份提供服務讓 kubelet 來訪問,而監聽地址和提供 API 的版本就已經在第一步完成了;

  • 第三步,當該 GRPC server 啟動之後,kubelet 會建立一個到 Device Plugin 的 ListAndWatch 的長連接, 用來發現設備 ID 以及設備的健康狀態。當 Device Plugin 檢測到某個設備不健康的時候,就會主動通知 kubelet。而此時如果這個設備處於空閒狀態,kubelet 會將其移除可分配的列表。但是當這個設備已經被某個 Pod 所使用的時候,kubelet 就不會做任何事情,如果此時殺掉這個 Pod 是一個很危險的操作;

  • 第四步,kubelet 會將這些設備暴露到 Node 節點的狀態中,把設備數量發送到 Kubernetes 的 api-server 中。後續調度器可以根據這些信息進行調度。

需要注意的是 kubelet 在向 api-server 進行匯報的時候,只會匯報該 GPU 對應的數量。而 kubelet 自身的 Device Plugin Manager 會對這個 GPU 的 ID 列表進行保存,並用來具體的設備分配。而這個對於 Kubernetes 全局調度器來說,它不掌握這個 GPU 的 ID 列表,它只知道 GPU 的數量。

這就意味著在現有的 Device Plugin 工作機制下,Kubernetes 的全局調度器無法進行更複雜的調度。比如說想做兩個 GPU 的親和性調度,同一個節點兩個 GPU 可能需要進行通過 NVLINK 通訊而不是 PCIe 通訊,才能達到更好的數據傳輸效果。在這種需求下,目前的 Device Plugin 調度機制中是無法實現的。

5. Pod 的調度和運行的過程

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 13

Pod 想使用一個 GPU 的時候,它只需要像之前的例子一樣,在 Pod 的 Resource 下 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 的流程就結束了。

本文思考與實踐

1. 本文總結

在本文中,我們一起學習了在 Docker 和 Kubernetes 上使用 GPU。

  • GPU 的容器化:如何去構建一個 GPU 鏡像;如何直接在 Docker 上運行 GPU 容器;

  • 利用 Kubernetes 管理 GPU 資源:如何在 Kubernetes 支持 GPU 調度;如何驗證 Kubernetes 下的 GPU 配置;調度 GPU 容器的方法;

  • Device Plugin 的工作機制:資源的上報和監控;Pod 的調度和運行;

  • 思考:目前的缺陷;社區常見的 Device Plugin。

2. Device Plugin 機制的缺陷

最後我們來思考一個問題,現在的 Device Plugin 是否完美無缺?

需要指出的是 Device Plugin 整個工作機制和流程上,實際上跟學術界和工業界的真實場景有比較大的差異。這裡最大的問題在於 GPU 資源的調度工作,實際上都是在 kubelet 上完成的。

而作為全局的調度器對這個參與是非常有限的,作為傳統的 Kubernetes 調度器來說,它只能處理 GPU 數量。一旦你的設備是異構的,不能簡單地使用數目去描述需求的時候,比如我的 Pod 想運行在兩個有 nvlink 的 GPU 上,這個 Device Plugin 就完全不能處理。

更不用說在許多場景上,我們希望調度器進行調度的時候,是根據整個集群的設備進行全局調度,這種場景是目前的 Device Plugin 無法滿足的。

更為棘手的是在 Device Plugin 的設計和實現中,像 Allocate 和 ListAndWatch 的 API 去增加可擴展的參數也是沒有作用的。這就是當我們使用一些比較複雜的設備使用需求的時候,實際上是無法通過 Device Plugin 來擴展 API 實現的。

因此目前的 Device Plugin 設計涵蓋的場景其實是非常單一的, 是一個可用但是不好用的狀態。這就能解釋為什麼像 Nvidia 這些廠商都實現了一個基於 Kubernetes 上游代碼進行 fork 了自己解決方案,也是不得已而為之。

3. 社區的異構資源調度方案

從零開始入門 K8s:GPU 管理和 Device Plugin 工作機制 14

  • 第一個是 Nvidia 貢獻的調度方案,這是最常用的調度方案;

  • 第二個是由阿里雲服務團隊貢獻的 GPU 共享的調度方案,其目的在於解決用戶共享 GPU 調度的需求,歡迎大家一起來使用和改進;

  • 下面的兩個 RDMA 和 FPGA 是由具體廠商提供的調度方案。

本文轉載自阿里巴巴雲原生微信公眾號(ID:Alicloudnative)。

相關閱讀:

從零開始入門 K8s:調度器的調度流程和算法介紹

從零開始入門 K8s:Kubernetes 調度和資源管理

從零開始入門 K8s:etcd 性能優化實踐

從零開始入門 K8s:手把手帶你理解 etcd

從零開始入門 K8s:深入剖析 Linux 容器

從零開始入門 K8s:Kubernetes 中的服務發現與負載均衡

從零開始入門 K8s:Kubernetes 網絡概念及策略控制

從零開始入門 K8s:監控與日誌的可觀測性

從零開始入門 K8s:應用存儲和持久化數據卷:存儲快照與拓撲調度

從零開始入門 K8s:應用存儲和持久化數據卷的核心知識

從零開始入門 K8s:應用配置管理

從零開始入門 K8s:應用編排與管理:Job & DaemonSet

從零開始入門 K8s:應用編排與管理

從零開始入門 K8s:K8s 的應用編排與管理

從零開始入門 K8s:詳解 Pod 及容器設計模式

從零開始入門 K8s:詳解 K8s 容器基本概念

從零開始入門 K8s:詳解 K8s 核心概念