iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Kubernetes

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

【Day 9】Volcano:你是否在火山救過一組永遠拿不到 GPU 的 Pod

  • 分享至 

  • xImage
  •  

昨天介紹的 Kueue 決定「這個 Job 現在該不該開始」:Job 建立時被設為暫停,取得額度並被准入後才解除暫停。但 Kueue 不負責 Pod 放到哪個節點,這件事仍然交給 kube-scheduler。

而 kube-scheduler 一次只看一個 Pod。它不知道哪幾個 Pod 是一組的。

對分散式訓練來說,這就是問題的開始:一組 worker 只要少一個,其他的就只能一直等下去。今天要看的,就是那些等不到的 Pod,以及 Volcano 怎麼讓它們不必等。


kube-scheduler 解不了的問題

kube-scheduler 每次排程的對象是一個 Pod。每一次排程嘗試分成排程週期與綁定週期兩個階段,而排程週期是依序執行的。它不知道哪幾個 Pod 屬於同一個工作,也不知道少了其中一個,其他的就沒有意義。

假設叢集有 8 張卡,兩個訓練工作同時送出,每個都需要 8 張。kube-scheduler 一個一個排的結果,可能會是這樣:

訓練 A 的 worker-0   → 拿到卡
訓練 B 的 worker-0   → 拿到卡
訓練 A 的 worker-1   → 拿到卡
訓練 B 的 worker-1   → 拿到卡
...
卡用完了

兩邊都只拿到一部分,而分散式訓練的 worker 需要彼此配合。以 PyTorch 為例,torch.distributed.init_process_group阻塞到所有行程都加入為止。所以已經拿到卡的 worker 會停在原地等待同伴,也不會釋放手上的卡。

結果是卡被佔滿,卻沒有任何工作在前進。Volcano 的文件也指出,同一組容器之間高度相關,可能發生資源競爭,而整體排程配置可以有效解決這類死結。


Volcano 是什麼

Volcano 是 Kubernetes 原生的批次排程系統,用來擴充與強化標準 kube-scheduler 的能力。它的排程器建立在 kube-batch 的基礎上,目前是 CNCF 的 incubating 專案,支援 Spark、TensorFlow、PyTorch、Flink、Ray 等計算框架。

Volcano 並不修改 kube-scheduler,而是另外部署一個排程器。Kubernetes 以 Pod 的 spec.schedulerName 判斷由哪個排程器負責;Volcano 自己的 Job 型別(VolcanoJob)的 schedulerName 可以是 volcanodefault-scheduler,預設就是 volcano

Volcano 提供的排程策略很多,包括 DRF 公平排程、以佇列為基礎的配額與搶占、網路拓撲感知、binpack 等。本篇只看其中最核心的一個:gang scheduling

https://ithelp.ithome.com.tw/upload/images/20260923/20183759oU5o5pfaw8.png


Gang scheduling:全有或全無

Gang scheduling 是批次排程領域的一個概念,字面上的 gang 指的是「一夥人」:把一組必須同時執行的工作視為一個單位,一起排程、一起執行,而不是各自獨立地被安排。

這個概念要解決的,就是開頭那個問題。一個分散式訓練由多個 worker 組成,它們之間互相依賴,少一個就沒有意義。如果排程器一個一個排,就可能出現「每個工作都拿到一部分資源、卻都湊不齊」的情況。Gang scheduling 的做法是把判斷單位從單一 Pod 換成整組 Pod:湊得齊就全部放行,湊不齊就一個都不放

Volcano 把這個策略實作成排程器的核心演算法之一,目標是滿足排程過程中「全有或全無」的需求,避免 Pod 被任意排程而浪費叢集資源。

它的判斷方式是:觀察一個 Job 底下已排程的 Pod 數量,是否達到最低執行數量。達到了,就對 Job 底下所有 Pod 執行排程;達不到,就不執行。

在資源不足時,這個策略可以避免「部分工作被分配到資源、卻在等待其他任務而浪費資源」的情況。

https://ithelp.ithome.com.tw/upload/images/20260923/20183759twJSjucI7y.png


PodGroup:排隊的單位

Volcano 用一個 CRD 來表達「這幾個 Pod 是一組的」,叫做 PodGroup:一組有強烈關聯的 Pod,主要用於批次排程,例如 TensorFlow 的 ps 與 worker。

建立 VolcanoJob 時若沒有指定 PodGroup,Volcano 會自動建立一個對應的 PodGroup。這和 Kueue 為 Job 自動建立 Workload 的做法相似:使用者送出工作,系統產生一個用來排隊的物件。

PodGroup 有五個狀態:

狀態 意義
Pending 已被 Volcano 接受,但資源需求尚未滿足
Inqueue 已通過驗證,等待綁定到節點。這是 Pending 與 Running 之間的過渡狀態,controller 在這個狀態才開始建立 Pod
Running 至少有 minMember 個 Pod 在執行
Unknown minMember 個 Pod 中,部分在執行、部分無法排程
Completed PodGroup 的所有 Pod 都已完成

Gang 的判斷發生在兩個地方

第一關:enqueue。 排程器的 enqueue 動作會判斷叢集的閒置資源能否滿足工作的最低需求。滿足了,PodGroup 從 Pending 變成 Inqueue;不滿足,就維持 Pending。只有 PodGroup 進入 Inqueue 之後,controller 才會為它建立 Pod。

第二關:allocate。 allocate 動作負責為 Pod 挑選節點,而且採用 commit 機制:一個 Pod 的排程需求被滿足時,不一定會立刻綁定,還要看它所屬 Job 的 gang 條件是否滿足。只有 gang 條件滿足,Pod 才會被綁定到節點。

所以在 Volcano 裡,「在等」可能有兩種樣子:PodGroup 停在 Pending 時,Pod 還不存在;PodGroup 已進入 Inqueue 但 gang 條件不滿足時,Pod 存在,但沒有被綁定到節點。


和 Kueue 的差別

Kueue Volcano
部署內容 controller manager + webhook 排程器 + controller manager + admission
等待時的樣子 Job 被暫停,Pod 不存在 PodGroup 為 Pending 時 Pod 不存在;已 Inqueue 但 gang 未滿足時,Pod 存在但未綁定
排隊的物件 Workload PodGroup
本篇關注的能力 額度 一組 Pod 的原子性

Volcano 本身也有佇列與配額管理,但本篇只看 gang scheduling。


實驗環境

沿用昨天 fake-gpu-operator 的叢集,一台 worker 上有 8 張假卡

kubectl get nodes -o custom-columns=NODE:.metadata.name,GPU:.status.allocatable.'nvidia\.com/gpu'
NODE                    GPU
gpu-lab-control-plane
gpu-lab-worker          8
gpu-lab-worker2

以官方提供的 development manifest 安裝 Volcano(本篇指定 v1.15.2):

kubectl apply -f https://raw.githubusercontent.com/volcano-sh/volcano/v1.15.2/installer/volcano-development.yaml
kubectl -n volcano-system rollout status deploy/volcano-scheduler
kubectl -n volcano-system get pods
NAME                                  READY   STATUS      RESTARTS   AGE
volcano-admission-7b4c5dc49c-5mq9v    1/1     Running     0          18s
volcano-admission-init-z4gd9          0/1     Completed   0          18s
volcano-controllers-cc5cbbd9f-hzfbm   1/1     Running     0          18s
volcano-scheduler-548886dbfd-bqqxw    1/1     Running     0          18s

這三個元件對應 Volcano 架構中的三個部分:

  • scheduler:依照 actions 與 plugins,把工作排到最合適的節點
  • controller manager:管理 Queue、PodGroup、VolcanoJob 等 CRD 的生命週期
  • admission:負責 CRD 的 API 驗證

相較於 Kueue 只有一個 controller manager,Volcano 多了一個 volcano-scheduler,也就是前面提到的「另外部署的排程器」。它和 kube-scheduler 同時存在,各自處理 schedulerName 指定給自己的 Pod。


送出兩個各需要 8 張卡的訓練工作

apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: gang-a
spec:
  minAvailable: 4                 # 至少 4 個 Pod 能被排程才放行
  schedulerName: volcano          # 交給 Volcano 排程
  queue: default
  tasks:
    - replicas: 4
      name: worker
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: w
              image: busybox:1.36
              command: ["sh", "-c", "sleep 120"]
              resources:
                limits: { nvidia.com/gpu: 2 }

gang-b 內容相同,只有名稱不同。)

4 個 worker,每個需要 2 張卡,一個工作需要 8 張。叢集剛好 8 張,兩個工作同一時間只可能有一個執行。

這份 YAML 的 apiVersionbatch.volcano.sh/v1alpha1,是 Volcano 自己的 Job 型別,不是 batch/v1minAvailabletasks 都是它的欄位。


觀察 PodGroup

送出之前,先在一個終端持續觀察 PodGroup:

kubectl get podgroups.scheduling.volcano.sh -w \
  -o custom-columns=NAME:.metadata.name,MIN:.spec.minMember,RUNNING:.status.running,PHASE:.status.phase

再從另一個終端執行 kubectl apply -f gang.yaml

輸出如下:

NAME      MIN   RUNNING   PHASE
gang-a-…  4
gang-b-…  4
gang-b-…  4               Inqueue
gang-a-…  4               Inqueue
gang-a-…  4               Running
gang-a-…  4     1         Running     ← gang-a 開始執行
gang-a-…  4     2         Running
gang-a-…  4     3         Running
gang-a-…  4     4         Running     ← 全部到齊
gang-a-…  4     3         Running     ← 開始有 Pod 執行完畢
gang-b-…  4               Inqueue
gang-a-…  4     2         Running
gang-a-…  4     1         Running     ← 只剩一個在執行,6 張卡閒置
gang-b-…  4               Inqueue     ← 仍然沒有進場
gang-b-…  4     4         Running     ← 卡全部釋放後才一次進場

PodGroup 名稱帶有後綴,MIN 欄位的 4 則來自 Job 的 minAvailable

兩件事值得注意。

一、兩個 PodGroup 一開始就都進入了 Inqueue。 依照前面 PodGroup 一節的說明,這代表兩者都通過了 enqueue,controller 因此為兩者都建立了 Pod。enqueue 的判斷會受排程器設定影響,例如 overcommit-factor 預設為 1.2,本篇沒有深入探討。

二、gang-a 只剩一個 Pod 在執行時,有 6 張卡閒置,gang-b 仍然沒有進場。 這對應前面提到的第二關(allocate):gang-b 需要 8 張卡,6 張不夠,gang 條件不滿足,allocate 就不會綁定它的任何一個 Pod。它不會先放兩個 Pod 進去佔位。

昨天的 Kueue 也出現過「卡空著卻不能跑」,原因是額度;今天的原因是整組放不下


時間戳記

工作結束後,Pod 的時間紀錄仍然保留:

POD               CREATED               STARTED               FINISHED
gang-a-worker-0   2026-09-18T14:14:50Z  2026-09-18T14:14:56Z  2026-09-18T14:16:56Z
gang-a-worker-1   2026-09-18T14:14:50Z  2026-09-18T14:14:57Z  2026-09-18T14:16:57Z
gang-a-worker-2   2026-09-18T14:14:50Z  2026-09-18T14:14:55Z  2026-09-18T14:16:55Z
gang-a-worker-3   2026-09-18T14:14:50Z  2026-09-18T14:14:54Z  2026-09-18T14:16:54Z
gang-b-worker-0   2026-09-18T14:14:50Z  2026-09-18T14:17:00Z  2026-09-18T14:19:00Z
gang-b-worker-1   2026-09-18T14:14:50Z  2026-09-18T14:17:00Z  2026-09-18T14:19:00Z
gang-b-worker-2   2026-09-18T14:14:50Z  2026-09-18T14:17:00Z  2026-09-18T14:19:00Z
gang-b-worker-3   2026-09-18T14:14:50Z  2026-09-18T14:17:00Z  2026-09-18T14:19:00Z

八個 Pod 在同一秒被建立(14:14:50),這和上一節兩個 PodGroup 一開始就進入 Inqueue 的觀察一致。

gang-b 的四個 Pod 在同一秒啟動(14:17:00),時間點在 gang-a 最後一個 Pod 結束(14:16:57)之後。在那之前,gang-b 的 Pod 已經存在了兩分多鐘,卻沒有任何一個先啟動。

gang-a 的四個 Pod 啟動時間相差三秒。要注意 STARTED 是容器的啟動時間,除了排程之外還包含拉取映像檔等步驟,所以這三秒的差距不能直接解讀為排程順序。如果要比較排程發生的時間,應該看 Pod 的 PodScheduled condition。


對照組:不使用 Volcano

上面說明了 Volcano 的行為,接著確認不使用它時會發生什麼。

同樣的兩個工作,改用 batch/v1 的 Job,並拿掉 schedulerNameminAvailable,其餘條件相同:一樣兩個工作、每個需要 8 張卡。

kubectl apply -f plain.yaml
watch -n2 "kubectl get pods -o wide | grep plain"
plain-a-29glv   0/1   Pending   104s
plain-a-6vjnj   1/1   Running   104s   gpu-lab-worker
plain-a-jwkrr   0/1   Pending   104s
plain-a-qpl49   0/1   Pending   104s
plain-b-j7gt5   1/1   Running   104s   gpu-lab-worker
plain-b-kb945   1/1   Running   104s   gpu-lab-worker
plain-b-llj75   0/1   Pending   104s
plain-b-xlrsx   1/1   Running   104s   gpu-lab-worker
Running 尚缺
plain-a 1 / 4 3 個
plain-b 3 / 4 1 個

兩個工作都沒有到齊,而 4 個 Running 的 Pod 各使用 2 張卡,8 張卡全部被佔滿。這就是開頭描述的情況。

最後仍然執行完畢

POD             CREATED    STARTED    FINISHED
plain-a-6vjnj   14:45:56   14:45:56   14:47:56
plain-b-j7gt5   14:45:56   14:45:56   14:47:56
plain-b-kb945   14:45:56   14:45:56   14:47:56
plain-b-xlrsx   14:45:56   14:45:56   14:47:56

plain-a-29glv   14:45:56   14:47:58   14:49:58
plain-a-jwkrr   14:45:56   14:47:58   14:49:58
plain-a-qpl49   14:45:56   14:47:58   14:49:58
plain-b-llj75   14:45:56   14:47:58   14:49:58

第一批在 14:47:56 結束,第二批在 14:47:58 啟動,總共約四分鐘,和使用 Volcano 時差不多。

差別在於這四分鐘裡的狀態:

前兩分鐘 後兩分鐘
Volcano gang-a 4/4 gang-b 4/4
不使用 gang a 1 個 + b 3 個,兩個都不完整 a 3 個 + b 1 個,兩個都不完整

使用 Volcano 時,任何時刻都有一個完整的工作在執行;不使用時,8 張卡全程滿載,但沒有任何一個工作是完整的。

為什麼對照組還是完成了

因為 busybox sleep 120 不需要等其他 Pod。每個 Pod 睡完就結束,卡被釋放,下一批才能接上。

真實的分散式訓練不是這樣。如開頭所述,PyTorch 的 init_process_group 會阻塞到所有行程都加入為止。換成真正的訓練,plain-b 的三個 worker 會等待第四個,而第四個拿不到卡,因為卡被 plain-a 的那一個 Pod 佔著,而那一個也在等它的三個同伴。雙方都在等對方釋放的資源,這種情況會一直持續,直到初始化逾時失敗為止。

所以這個對照組呈現的只是較溫和的版本。


補充:結果不保證每次相同

kube-scheduler 分配的結果不保證每次相同。這次跑出 1/3 的分配,重跑一次可能是 2/2 或 3/1,也可能剛好是 4/0,那樣其中一個工作就能完整執行。

也就是說,沒有 gang scheduling 時,工作能不能完整執行取決於當下的排程結果,而不是由機制保證。


小結

今天介紹了 Volcano 的 gang scheduling。它以 PodGroup 表示一組相關的 Pod,並在兩個地方把關:enqueue 判斷資源是否足以讓 PodGroup 進入 Inqueue,只有進入 Inqueue 才會建立 Pod;allocate 則要等 gang 條件滿足,才把整組 Pod 綁定到節點。實驗中,gang-b 的 Pod 雖然早就存在,卻一直等到 8 張卡全部釋放才同時啟動;而不使用 Volcano 的對照組,8 張卡全程滿載,卻沒有一個工作是完整的。

Kueue 和 Volcano 都讓工作排隊,但判斷依據不同:Kueue 看額度夠不夠,Volcano 看整組放不放得下。接下來會介紹同樣處理這一層、但以 GPU 使用率與公平共享為重點的 KAI Scheduler。


參考資料

Volcano — GitHub README
Volcano — Introduction
Volcano — Architecture
Volcano — Scheduler Introduction
Volcano — Actions
Volcano — Gang plugin
Volcano — PodGroup
Volcano — VolcanoJob
Volcano — How to configure scheduler
volcano-sh/apis — PodGroup phase 定義
Kubernetes — Scheduling Framework
Kubernetes — Configure Multiple Schedulers


上一篇
【Day 8】Kueue:Kubernetes 上的工作准入與配額管理
系列文
凌晨四點,女友帶著 GPU 來我家學習 Kubernetes:打造 K8s AI Infra 的 30 夜9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言