iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Kubernetes

凌晨四點,女友帶著 GPU 來我家學習 Kubernetes:打造 K8s AI Infra 的 30 夜系列 第 3

【Day 3】Device Plugin 的極限:一張卡一個整數,贏者全拿

  • 分享至 

  • xImage
  •  

你的部門剛採購了一張 RTX 4090

好不容易把預算批下來,你的部門買了一張 RTX 4090,24GB 的記憶體,插在那台大家共用的機器上。

第一個來申請的是隔壁組做資料分析的同事。他想開個 notebook 看看資料長什麼樣子,順便試跑一個小模型,於是他寫了一份再平常不過的 yaml:

apiVersion: v1
kind: Pod
metadata:
  name: data-explore
  namespace: ds-team
spec:
  containers:
    - name: notebook
      image: quay.io/jupyter/pytorch-notebook:latest
      ports:
        - containerPort: 8888
      resources:
        limits:
          nvidia.com/gpu: 1

透過 nvidia.com/gpu: 1 來申請一張 GPU 的寫法,就是 device plugin 在做的事。

那 Pod 也順利起來,一切正常。他進去下了 nvidia-smi 並看了一眼,24GB 的卡,用了 3GB。


越來越多人開始想一起用這張 GPU

隔天,另一組的同事也想跑點東西,他把同一份 yaml 改個名字 apply 了出去,但這次,他的 Pod 停在了 Pending

$ kubectl describe pod data-explore-2 -n ds-team
...
Events:
  Warning  FailedScheduling  ...  0/1 nodes are available:
           1 Insufficient nvidia.com/gpu.

這位同仁很納悶,明明那張卡上還有 21GB 是空閒的,但 Kubernetes 就是跟你說:沒有卡了。

要理解為什麼會這樣,得先知道 nvidia.com/gpu: 1 這一行背後到底發生了什麼事。


Device Plugin 在做什麼

Kubernetes 核心原生支援的運算資源只有 CPU、記憶體、ephemeral storage、hugepages 等少數幾種,沒有內建任何 GPU 專屬的資源型別,也不認得 CUDA 等廠商驅動。因此,kubelet 無法自行偵測節點上的 GPU,也無法直接將 GPU 分配給容器。要讓 Pod 使用這類硬體,必須先解決兩個問題:kubelet 如何得知節點上有哪些裝置,以及這些裝置在 Kubernetes 中以什麼名稱表示。

Device Plugin 就是為此設計的擴充框架,讓硬體廠商無須修改 Kubernetes 核心程式碼,就能將自家裝置接入叢集。廠商依照框架定義的 gRPC 介面實作 plugin,通常以 DaemonSet 部署在每個節點上。plugin 向 kubelet 註冊後,負責回報該節點上可用的裝置及其健康狀態。kubelet 將這些裝置登記為節點的 extended resource,寫入節點狀態的 capacityallocatable,供排程器分配時參考。

device plugin 回報的資源名稱採用以下格式:

vendor-domain/resourcetype

常見的例子包括 nvidia.com/gpuamd.com/gpugpu.intel.com/i915。Kubernetes 不維護資源名稱清單,廠商也不需要事先註冊。名稱只需帶有網域形式的前綴,且不得使用 Kubernetes 保留的 kubernetes.io 網域;斜線後方的資源類型名稱由廠商自行決定,Kubernetes 不解讀其語意。換言之,對 Kubernetes 而言,nvidia.com/gpu 只是一個可以計數的資源名稱,它並不知道這個名稱背後代表的是 GPU。


Device Plugin 實作總覽

整體流程概覽

Device Plugin 的運作分為四個階段:初始化 → 啟動 gRPC 服務 → 向 kubelet 註冊 → 服務模式

各階段說明

1. 初始化(Initialization)

  • Plugin 執行廠商自訂的初始化與設定,確保裝置處於可用(ready)狀態。

2. 啟動 gRPC 服務

  • 在主機路徑 /var/lib/kubelet/device-plugins/ 下建立 Unix socket,並提供 gRPC 服務。
  • 注意: 這個路徑是寫死的,不受 kubelet 的 --root-dir 或其他設定影響。

需實作的介面如下:

RPC 方法 用途 是否必須
GetDevicePluginOptions 回傳 plugin 的選項給 Device Manager 必須
ListAndWatch 以 stream 回傳裝置清單;裝置狀態改變或消失時,推送新清單 必須
Allocate 建立容器時呼叫,讓 plugin 執行裝置相關操作,並告訴 kubelet 如何讓容器使用該裝置 必須
GetPreferredAllocation 從可用裝置中回傳「偏好」的分配組合(僅供參考,不保證最終採用) 選用
PreStartContainer 若註冊時有聲明,會在每個容器啟動前呼叫,例如用來重置裝置 選用

重點:

  • GetPreferredAllocation()PreStartContainer() 可以不提供實際功能。
  • 是否支援這兩個選用方法,要在 GetDevicePluginOptions() 回傳的 DevicePluginOptions 中用旗標標示。
  • kubelet 在呼叫任何選用方法之前,一定會先呼叫 GetDevicePluginOptions() 確認。

3. 向 kubelet 註冊

  • 透過 Unix socket /var/lib/kubelet/device-plugins/kubelet.sock 向 kubelet 註冊。
  • 注意:順序很重要,必須先啟動 gRPC 服務,再註冊,否則註冊會失敗。

4. 服務模式(Serving Mode)

註冊成功後,plugin 持續執行兩件事:

(a) 監控裝置健康狀態

  • 裝置狀態有變化時,回報給 kubelet。

(b) 處理 Allocate 請求

  • 可進行裝置特定的準備工作,例如 GPU 清理、QRNG(量子亂數產生器)初始化。
  • 成功後回傳 AllocateResponse,內含容器 runtime 存取裝置所需的設定。
  • kubelet 會把這些資訊轉交給 container runtime。

AllocateResponse 結構

  • 一個 AllocateResponse 包含 0 個或多個 ContainerAllocateResponse
  • 每個 ContainerAllocateResponse 定義要對容器做哪些修改,才能讓容器存取裝置,可包含:
    • Annotations(註解)
    • Device nodes(裝置節點)
    • 環境變數
    • Mounts(掛載)
    • 完整格式的 CDI 裝置名稱(fully-qualified CDI device names)

CDI 相關注意事項:

  • Device Manager 要處理 CDI 裝置名稱,kubelet kube-apiserver 都必須啟用 DevicePluginCDIDevices feature gate。
  • 版本演進:v1.28 Alpha → v1.29 Beta → v1.31 GA

為什麼只能全拿或全不拿

extended resource 的兩項限制

Pod 透過 resources 欄位請求 device plugin 所提供的裝置,語法與 CPU、記憶體等原生資源相同。不過 Kubernetes 對這類 extended resource 另外訂有兩項限制:

  • Extended resources are only supported as integer resources and cannot be overcommitted.
  • Devices cannot be shared between containers.

第一項限制規範請求的形式:extended resource 只能以整數請求,且不可超額分配(overcommit),因此 request 與 limit 必須相等。第二項限制規範分配的結果:裝置一旦分配給某個容器,即由該容器獨占,不會同時分配給其他容器。

兩項限制合在一起,就形成「全拿或全不拿」的分配行為。容器無法請求一個裝置的一部分,也無法與其他容器共用同一個裝置。這不是尚未實作的功能,而是 device plugin 模型的設計前提。

模型中沒有「部分裝置」的表示方式

回顧 device plugin 的運作流程,從裝置回報、資源請求到裝置分配,每個環節都沒有表達「部分裝置」的機制。

裝置回報。 ListAndWatch 回傳的裝置清單中,每個 Device 只包含 ID、健康狀態,以及選填的 NUMA 拓撲資訊,沒有描述裝置容量(例如顯存大小)的欄位。kubelet 統計健康裝置的數量,登記為節點上的可分配資源,例如單卡節點上的 nvidia.com/gpu: 1

資源請求。 在 Pod spec 中,原生資源可以使用非整數值,例如 cpu: 500mmemory: 1.5Gi。extended resource 則只接受整數,nvidia.com/gpu: 0.5 是不合法的設定。

裝置分配。 Allocate 接收的參數是裝置 ID。每個 ID 代表一個完整的裝置,介面中沒有指定裝置某一比例的方式。

在 device plugin 模型中,裝置只以數量計算,沒有容量的概念。從資源計數的角度來看,一張 24GB 的 RTX 4090 和一張 80GB 的 H100 沒有差別,兩者都記為 nvidia.com/gpu,數量 1。使用者可以透過 node label 將 Pod 排程到特定型號的節點,但分配單位始終是一個完整裝置。

換言之,Kubernetes 在這個模型下不掌握 GPU 的顯存資訊,只記錄節點上可分配的裝置數量,以及每個裝置是否已被分配。由於每個裝置只有「已分配」和「未分配」兩種狀態,分配結果也只有兩種:全拿,或全不拿。


小結

老實說,Device Plugin 的限制就是 GPU 在 Kubernetes 上用不滿的大元兇。一張卡被一個 Pod 拿走,剩下的算力和 VRAM 就跟著一起鎖住,不管那個 Pod 實際上用了多少。

但這不是它設計得不好。它要解決的是「怎麼讓 Kubernetes 支援它不認識的硬體」,而它做到了,到今天為止我們還在用它,而且用了很多年。

然而,隨著 DRA 在 Kubernetes v1.34 正式 GA,這件事終於有了轉機,DRA 把「要一張卡」從填一個整數,換成提出一份可以描述條件的申請,這個神奇的功能也正是我們明天的主題。


參考資料

Kubernetes Documentation — Device Plugins


上一篇
【Day 2】AI Infra 前哨站:一句話丟進模型,GPU 上發生了什麼
下一篇
【Day 4】Kubernetes DRA:為你的 GPU 實現動態資源分配
系列文
凌晨四點,女友帶著 GPU 來我家學習 Kubernetes:打造 K8s AI Infra 的 30 夜9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言