做者 | 車漾 阿里巴巴高級技術專家git
本文整理自《CNCF x Alibaba 雲原生技術公開課》第 20 講。
關注「阿里巴巴雲原生」公衆號,回覆關鍵詞「入門」,便可下載從零入門 K8s 系列文章 PPT。docker
導讀:2016 年,隨着 AlphaGo 的走紅和 TensorFlow 項目的異軍突起,一場名爲 AI 的技術革命迅速從學術圈蔓延到了工業界,所謂 AI 革命今後拉開了帷幕。該熱潮的背後推手正是雲計算的普及和算力的巨大提高。
通過近幾年的發展,AI 有了許許多多的落地場景,包括智能客服、人臉識別、機器翻譯、以圖搜圖等功能。其實機器學習或者說是人工智能,並非什麼新鮮的概念。而此次熱潮的背後,雲計算的普及以及算力的巨大提高,纔是真正將人工智能從象牙塔帶到工業界的一個重要推手。編程
與之相對應的,從 2016 年開始,Kubernetes 社區就不斷收到來自不一樣渠道的大量訴求:但願能在 Kubernetes 集羣上運行 TensorFlow 等機器學習框架。這些訴求中,除了以前文章所介紹的,像 Job 這些離線任務的管理以外,還有一個巨大的挑戰:深度學習所依賴的異構設備及英偉達的 GPU 支持。json
咱們不由好奇起來:Kubernetes 管理 GPU 能帶來什麼好處呢?api
本質上是成本和效率的考慮。因爲相對 CPU 來講,GPU 的成本偏高。在雲上單 CPU 一般是一小時幾毛錢,而 GPU 的花費則是從單 GPU 每小時 10 元 ~ 30 元不等,這就要千方百計的提升 GPU 的使用率。安全
爲何要用 Kubernetes 管理以 GPU 爲表明的異構資源?服務器
具體來講是三個方面:框架
首先是加速部署,避免把時間浪費在環境準備的環節中。經過容器鏡像技術,將整個部署過程進行固化和複用,若是同窗們關注機器學習領域,能夠發現許許多多的框架都提供了容器鏡像。咱們能夠藉此提高 GPU 的使用效率。
經過分時複用,來提高 GPU 的使用效率。當 GPU 的卡數達到必定數量後,就須要用到 Kubernetes 的統一調度能力,使得資源使用方可以作到用即申請、完即釋放,從而盤活整個 GPU 的資源池。less
而此時還須要經過 Docker 自帶的設備隔離能力,避免不一樣應用的進程運行同一個設備上,形成互相影響。在高效低成本的同時,也保障了系統的穩定性。curl
上面瞭解到了經過 Kubernetes 運行 GPU 應用的好處,經過以前系列文章的學習也知道,Kubernetes 是容器調度平臺,而其中的調度單元是容器,因此在學習如何使用 Kubernetes 以前,咱們先了解一下如何在容器環境內運行 GPU 應用。
在容器環境下使用 GPU 應用,實際上不復雜。主要分爲兩步:
有兩個方法準備:
好比直接從 docker.hub 或者阿里雲鏡像服務中尋找官方的 GPU 鏡像,包括像 TensorFlow、Caffe、PyTorch 等流行的機器學習框架,都有提供標準的鏡像。這樣的好處是簡單便捷,並且安全可靠。
固然若是官方鏡像沒法知足需求時,好比你對 TensorFlow 框架進行了定製修改,就須要從新編譯構建本身的 TensorFlow 鏡像。這種狀況下,咱們的最佳實踐是:依託於 Nvidia 官方鏡像繼續構建,而不要從頭開始。
以下圖中的 TensorFlow 例子所示,這個就是以 Cuda 鏡像爲基礎,開始構建本身的 GPU 鏡像。
要了解如何構建 GPU 容器鏡像,先要知道如何要在宿主機上安裝 GPU 應用。
以下圖左邊所示,最底層是先安裝 Nvidia 硬件驅動;再到上面是通用的 Cuda 工具庫;最上層是 PyTorch、TensorFlow 這類的機器學習框架。
上兩層的 CUDA 工具庫和應用的耦合度較高,應用版本變更後,對應的 CUDA 版本大機率也要更新;而最下層的 Nvidia 驅動,一般狀況下是比較穩定的,它不會像 CUDA 和應用同樣,常常更新。
同時 Nvidia 驅動須要內核源碼編譯,如上圖右側所示,英偉達的 GPU 容器方案是:在宿主機上安裝 Nvidia 驅動,而在 CUDA 以上的軟件交給容器鏡像來作。同時把 Nvidia 驅動裏面的連接以 Mount Bind 的方式映射到容器中。
這樣的一個好處是:當你安裝了一個新的 Nvidia 驅動以後,你就能夠在同一個機器節點上運行不一樣版本的 CUDA 鏡像了。
有了前面的基礎,咱們就比較容易理解 GPU 容器的工做機制。下圖是一個使用 Docker 運行 GPU 容器的例子。
咱們能夠觀察到,在運行時刻一個 GPU 容器和普通容器之間的差異,僅僅在於須要將宿主機的設備和 Nvidia 驅動庫映射到容器中。
上圖右側反映了 GPU 容器啓動後,容器中的 GPU 配置。右上方展現的是設備映射的結果,右下方顯示的是驅動庫以 Bind 方式映射到容器後,能夠看到的變化。
一般你們會使用 Nvidia-docker 來運行 GPU 容器,而 Nvidia-docker 的實際工做就是來自動化作這兩個工做。其中掛載設備比較簡單,而真正比較複雜的是 GPU 應用依賴的驅動庫。
對於深度學習,視頻處理等不一樣場景,所使用的一些驅動庫並不相同。這又須要依賴 Nvidia 的領域知識,而這些領域知識就被貫穿到了 Nvidia 的容器之中。
首先看一下如何給一個 Kubernetes 節點增長 GPU 能力,咱們以 CentOS 節點爲例。
如上圖所示:
因爲 Nvidia 驅動須要內核編譯,因此在安裝 Nvidia 驅動以前須要安裝 gcc 和內核源碼。
安裝完 Nvidia Docker2 須要從新加載 docker,能夠檢查 docker 的 daemon.json 裏面默認啓動引擎已經被替換成了 nvidia,也能夠經過 docker info 命令查看運行時刻使用的 runC 是否是 Nvidia 的 runC。
從 Nvidia 的 git repo 下去下載 Device Plugin 的部署聲明文件,而且經過 kubectl create 命令進行部署。
這裏 Device Plugin 是以 deamonset 的方式進行部署的。這樣咱們就知道,若是須要排查一個 Kubernetes 節點沒法調度 GPU 應用的問題,須要從這些模塊開始入手,好比我要查看一下 Device Plugin 的日誌,Nvidia 的 runC 是否配置爲 docker 默認 runC 以及 Nvidia 驅動是否安裝成功。
當 GPU 節點部署成功後,咱們能夠從節點的狀態信息中發現相關的 GPU 信息。
站在用戶的角度,在 Kubernetes 中使用 GPU 容器仍是很是簡單的。
只須要在 Pod 資源配置的 limit 字段中指定 nvidia.com/gpu 使用 GPU 的數量,以下圖樣例中咱們設置的數量爲 1;而後再經過 kubectl create 命令將 GPU 的 Pod 部署完成。
部署完成後能夠登陸到容器中執行 nvidia-smi 命令觀察一下結果,能夠看到在該容器中使用了一張 T4 的 GPU 卡。說明在該節點中的兩張 GPU 卡其中一張已經能在該容器中使用了,可是節點的另一張卡對於改容器來講是徹底透明的,它是沒法訪問的,這裏就體現了 GPU 的隔離性。
Kubernetes 自己是經過插件擴展的機制來管理 GPU 資源的,具體來講這裏有兩個獨立的內部機制。
Extend Resources 屬於 Node-level 的 api,徹底能夠獨立於 Device Plugin 使用。而上報 Extend Resources,只須要經過一個 PACTH API 對 Node 對象進行 status 部分更新便可,而這個 PACTH 操做能夠經過一個簡單的 curl 命令來完成。這樣,在 Kubernetes 調度器中就可以記錄這個節點的 GPU 類型,它所對應的資源數量是 1。
固然若是使用的是 Device Plugin,就不須要作這個 PACTH 操做,只須要聽從 Device Plugin 的編程模型,在設備上報的工做中 Device Plugin 就會完成這個操做。
介紹一下 Device Plugin 的工做機制,整個 Device Plugin 的工做流程能夠分紅兩個部分:
Device Plugin 的開發很是簡單。主要包括最關注與最核心的兩個事件方法:
對於每個硬件設備,都須要它所對應的 Device Plugin 進行管理,這些 Device Plugin 以客戶端的身份經過 GRPC 的方式對 kubelet 中的 Device Plugin Manager 進行鏈接,而且將本身監聽的 Unis socket api 的版本號和設備名稱好比 GPU,上報給 kubelet。
咱們來看一下 Device Plugin 資源上報的整個流程。總的來講,整個過程分爲四步,其中前三步都是發生在節點上,第四步是 kubelet 和 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 調度機制中是沒法實現的。
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 的流程就結束了。
在本文中,咱們一塊兒學習了在 Docker 和 Kubernetes 上使用 GPU。
最後咱們來思考一個問題,如今的 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 了本身解決方案,也是不得已而爲之。
「 阿里巴巴雲原生關注微服務、Serverless、容器、Service Mesh 等技術領域、聚焦雲原生流行技術趨勢、雲原生大規模的落地實踐,作最懂雲原生開發者的技術圈。」