iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Kubernetes

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

【Day 11】原生 gang scheduling:Kubernetes 自己的答案

  • 分享至 

  • xImage
  •  

前面三天都是外掛的解法:Kueue 用額度擋在門口,Volcano 換一套排程器,KAI 用佇列和可搶佔性。今天問的是另一個問題:Kubernetes 自己打算怎麼解?

答案是 scheduling.k8s.io 底下的兩個內建物件——Workload 與 PodGroup。Pod 上加一行 spec.schedulingGroup.podGroupName 就入群,不需要裝第二個排程器,但需要開兩個開關。這也是它和 Volcano、KAI 最根本的差別:那兩個是裝元件,這個是改叢集設定。


先講清楚版本與開關

Gang scheduling 自 Kubernetes v1.37 起為 Beta,而且預設停用。官方文件的功能狀態欄就是這樣寫的。

要使用它,叢集管理員必須在所有相關元件上啟用 GenericWorkload feature gate,而這個功能依賴 PodGroup API,所以對應的 API group 也要開。

節點方面沿用前幾天的 gpu-lab-worker,上面有 8 張 nvidia.com/gpu。


API 長什麼樣

PodGroup 用 schedulingPolicy 表達這組 Pod 要怎麼被排。實驗組用 gang:

apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata: { name: team-a, namespace: native-gang }
spec:
  schedulingPolicy:
    gang:
      minCount: 3          # 全有全無的門檻

對照組用 basic,也就是一個一個排,跟預設行為一樣:

apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata: { name: team-a, namespace: native-basic }
spec:
  schedulingPolicy:
    basic: {}

Pod 那邊只多一行:

apiVersion: v1
kind: Pod
metadata:
  name: team-a-0
  namespace: native-gang
  labels: { job: team-a }
spec:
  schedulingGroup:
    podGroupName: team-a          # 這一行就是「我屬於 team-a」
  restartPolicy: Never
  containers:
    - name: worker
      image: registry.k8s.io/pause:3.10
      resources:
        limits:
          nvidia.com/gpu: 2

沒有 CRD、沒有第二個排程器、沒有 annotation,schedulerName 也還是 default-scheduler。


機制:Pod 在哪裡被擋下來

官方文件把 gang 的生命週期拆成三步:

  1. 排程器先把 Pod 壓著,直到兩個條件都成立:PodGroup 物件存在,而且這個 PodGroup 已建立的 Pod 數量(含已排程與未排程)至少等於 minCount。兩個條件沒同時滿足,這個 PodGroup 不會進入 active 排程佇列。
  2. 湊到法定數量後,排程器為整組尚未排程的 Pod 尋找位置,做出單一的原子決定。
  3. 如果找得到至少 minCount 個 Pod 的位置,這些成功放置的 Pod 才會被綁定;找不到足夠的位置就一個都不排,整組移到 unschedulable 佇列等資源釋出,中間讓其他工作先跑。

值得注意的是第三步:綁定的是「成功放置的 Pod」,不是「minCount 個 Pod」。這個差別在實驗四會看得很清楚。

而「壓著等」這件事發生在兩個不同的時機,錯誤訊息用字也不同:等待 Pod 出現在排程佇列時是 waiting for minCount pods from a gang to appear in scheduling queue,等待隊友被排上去時是 waiting for minCount pods from a gang to be scheduled。實驗三拿到的是後者。


實驗設計

gpu-lab-worker 上 8 張卡
兩隊,每隊 3 個 worker,每個 worker 要 2 張卡
  → 每隊需要 6 張,兩隊合計 12 張 > 8 張

Pod 的建立順序刻意交錯(a-0, b-0, a-1, b-1, a-2, b-2),讓對照組落在「兩邊各搶到一半」,而不是碰運氣讓某一隊剛好湊滿。


實驗一:basic 與 gang 的差別

先跑對照組:

kubectl delete ns native-gang native-basic native-elastic --ignore-not-found --wait=true
kubectl apply -f ~/native-gang-lab/02-basic-control.yaml
namespace/native-basic created
podgroup.scheduling.k8s.io/team-a created
podgroup.scheduling.k8s.io/team-b created
pod/team-a-0 created
pod/team-b-0 created
pod/team-a-1 created
pod/team-b-1 created
pod/team-a-2 created
pod/team-b-2 created
kubectl -n native-basic get pods -o custom-columns='NAME:.metadata.name,GROUP:.spec.schedulingGroup.podGroupName,PHASE:.status.phase,NODE:.spec.nodeName'
NAME       GROUP    PHASE     NODE
team-a-0   team-a   Running   gpu-lab-worker
team-a-1   team-a   Running   gpu-lab-worker
team-a-2   team-a   Pending   <none>
team-b-0   team-b   Running   gpu-lab-worker
team-b-1   team-b   Running   gpu-lab-worker
team-b-2   team-b   Pending   <none>
kubectl describe node gpu-lab-worker | grep -E "^  nvidia.com/gpu "
  nvidia.com/gpu     8            8

兩隊各搶到 2 個,各差 1 個,8 張卡全部被佔住,但沒有任何一隊是完整的。資源全被吃光,卻沒有任何工作能開始——這正是 gang scheduling 要解決的那個場景。

跑實驗組之前一定要先把卡清回 0:

kubectl delete ns native-basic --wait=true
kubectl describe node gpu-lab-worker | grep -E "^  nvidia.com/gpu "
namespace "native-basic" deleted
  nvidia.com/gpu     0            0

接著換 gang:

kubectl apply -f ~/native-gang-lab/01-native-gang.yaml
kubectl -n native-gang get pods -o custom-columns='NAME:.metadata.name,GROUP:.spec.schedulingGroup.podGroupName,PHASE:.status.phase,NODE:.spec.nodeName'
NAME       GROUP    PHASE     NODE
team-a-0   team-a   Running   gpu-lab-worker
team-a-1   team-a   Running   gpu-lab-worker
team-a-2   team-a   Running   gpu-lab-worker
team-b-0   team-b   Pending   <none>
team-b-1   team-b   Pending   <none>
team-b-2   team-b   Pending   <none>
kubectl describe node gpu-lab-worker | grep -E "^  nvidia.com/gpu "
  nvidia.com/gpu     6            6
team-a team-b 卡被佔住 有隊伍在跑嗎
basic 2/3 2/3 8/8(100%) 0 隊
gang 3/3 0/3(等) 6/8(75%) 1 隊

利用率從 100% 掉到 75%,但真正在算的訓練從 0 個變成 1 個。


實驗二:basic 的 PodGroup status 會說謊

先看 basic 那兩個:

kubectl -n native-basic get podgroups.scheduling.k8s.io -o custom-columns='NAME:.metadata.name,POLICY:.spec.schedulingPolicy,STATUS:.status'
NAME     POLICY             STATUS
team-a   map[basic:map[]]   map[conditions:[map[lastTransitionTime:2026-09-25T11:00:19Z message: reason:Scheduled status:True type:PodGroupScheduled]]]
team-b   map[basic:map[]]   map[conditions:[map[lastTransitionTime:2026-09-25T11:00:19Z message: reason:Scheduled status:True type:PodGroupScheduled]]]

兩個都是 reason:Scheduled、status:True,message 後面是空的。但上一節才看到,這兩隊各有一個 Pod 卡在 Pending,誰都沒跑起來。

再看 gang 那兩個:

kubectl -n native-gang get podgroups.scheduling.k8s.io -o yaml \
  | grep -E "^    name:|minCount|reason:|status:|message:|type:"
        {"apiVersion":"scheduling.k8s.io/v1alpha2","kind":"PodGroup","metadata":{"annotations":{},"name":"team-a","namespace":"native-gang"},"spec":{"schedulingPolicy":{"gang":{"minCount":3}}}}
    name: team-a
        minCount: 3
  status:
      message: ""
      reason: Scheduled
      status: "True"
      type: PodGroupScheduled
        {"apiVersion":"scheduling.k8s.io/v1alpha2","kind":"PodGroup","metadata":{"annotations":{},"name":"team-b","namespace":"native-gang"},"spec":{"schedulingPolicy":{"gang":{"minCount":3}}}}
    name: team-b
        minCount: 3
  status:
      message: pod group is unschedulable
      reason: Unschedulable
      status: "False"
      type: PodGroupScheduled

gang 底下分得出 True 和 False,湊不齊的那隊還附上 pod group is unschedulable。basic 底下則兩個都是 True,而且 message 是空的——不是「狀態是 True 但訊息告訴你還缺人」,是連個說明都沒有,你完全無從察覺那兩隊其實都沒跑。

另外注意 basic 那兩個的 lastTransitionTime 是同一秒。它們幾乎是一建立就被標成 Scheduled,根本沒等排程結果。

所以 basic 底下的 Scheduled 不是「我這組跑起來了」的意思。拿這個欄位當監控指標會被騙。


實驗三:兩種 FailedScheduling 訊息

kubectl -n native-gang get events --field-selector reason=FailedScheduling \
  -o custom-columns='POD:.involvedObject.name,MSG:.message' | sort -u
POD        MSG
team-b-0   0/3 nodes are available: 1 node(s) had untolerated taint(s), 2 Insufficient nvidia.com/gpu. no new claims to deallocate, preemption: 0/3 nodes are available: 1 No preemption victims found for incoming pod, 2 Preemption is not helpful for scheduling.
team-b-1   0/3 nodes are available: 1 node(s) had untolerated taint(s), 2 Insufficient nvidia.com/gpu. no new claims to deallocate, preemption: 0/3 nodes are available: 1 No preemption victims found for incoming pod, 2 Preemption is not helpful for scheduling.
team-b-2   pod group is unschedulable, waiting for minCount pods from a gang to be scheduled, one or more plugins asked to wait and no plugin rejected pod

前兩個 Pod 的訊息是很典型的排不上去:三個節點,一個因為 taint 被排除(control-plane),另外兩個 Insufficient nvidia.com/gpu,而且搶佔也幫不上忙。

第三個完全不一樣,最值得逐字讀的是最後那句:one or more plugins asked to wait and no plugin rejected pod。這個 Pod 通過了所有檢查,沒有任何外掛說它不行,然後被要求等隊友。而且訊息用的是 be scheduled 而不是 appear in scheduling queue,也就是說它不是在等隊友出現,是在等隊友被排上去。

也就是說,gang 不是在 filter 階段把 Pod 擋掉的。四天下來的四種攔法擺在一起:

攔在哪 Pod 存在嗎
Kueue admission 不存在(spec.suspend)
Volcano bind 存在,Pending
KAI 佇列額度 存在,Pending
本篇(原生) 等隊友被排上 存在,Pending

實驗四:minCount 不等於「全部」

一隊 5 個 Pod,minCount: 3,卡還是 8 張,只夠 4 個 Pod 用。

kubectl delete ns native-gang --wait=true
kubectl describe node gpu-lab-worker | grep -E "^  nvidia.com/gpu "     # 確認 0
kubectl apply -f ~/native-gang-lab/03-elastic.yaml
kubectl -n native-elastic get pods -o custom-columns='NAME:.metadata.name,PHASE:.status.phase,NODE:.spec.nodeName'
NAME       PHASE     NODE
team-c-0   Running   gpu-lab-worker
team-c-1   Running   gpu-lab-worker
team-c-2   Running   gpu-lab-worker
team-c-3   Running   gpu-lab-worker
team-c-4   Pending   <none>
kubectl -n native-elastic get podgroups.scheduling.k8s.io \
  -o custom-columns='NAME:.metadata.name,MIN:.spec.schedulingPolicy.gang.minCount,STATUS:.status.conditions[0].reason'
kubectl describe node gpu-lab-worker | grep -E "^  nvidia.com/gpu "
NAME     MIN   STATUS
team-c   3     Scheduled

  nvidia.com/gpu     8            8

4 個跑起來、第 5 個等著、整組算 Scheduled。

這正好對應文件的說法:找得到至少 minCount 個位置時,那些成功放置的 Pod 就會被綁定。所以 minCount 是門檻,不是配額。

三組並排:

卡被佔住 有隊伍在跑嗎
basic 8/8(100%) 0 隊
gang,minCount = 全部 6/8(75%) 1 隊
gang,minCount < 全部 8/8(100%) 1 隊

所以「gang 用利用率換進度」這句話只對了一半。真正決定代價的不是有沒有開 gang,是 minCount 設多少。 設成全部就是全有全無,閒置是誠實的代價;設成一個下限,利用率和進度可以同時拿到。

第三列不是免費的:它要求工作負載少幾個也能跑。彈性訓練與推論服務吃得下這套,需要所有 rank 同時到齊的分散式訓練吃不下。


小結

今天看的是 Kubernetes 自己的 gang scheduling:PodGroup 加上 Pod 的 spec.schedulingGroup.podGroupName,不裝第二個排程器,schedulerName 仍然是 default-scheduler——代價是要開兩個 feature gate,外加在 apiserver 的 runtime-config 打開那個 API group。

basic 的行為就是一個一個排,兩隊各搶到一半、8 張卡全滿、沒有一隊在跑;gang 則是整組一起上,代價是 6/8 的利用率換到一個完整的訓練。而 minCount 設得比總數小的時候,兩邊可以同時拿到——前提是工作負載少幾個也能動。

四天下來,四種做法攔的位置各不相同:Kueue 在 admission、Volcano 在綁定前、KAI 在佇列額度、原生 gang 在等隊友被排上。外掛先做出來的東西,上游正在做成標準 API——今天看到的 PodGroup 就是其中一塊。


參考資料

Kubernetes — Gang Scheduling


上一篇
【Day 10】KAI Scheduler:用被搶佔的風險,換配額外的資源
下一篇
【Day 12】KubeRay:當 Kubernetes 轉角遇見 Ray
系列文
凌晨四點,女友帶著 GPU 來我家學習 Kubernetes:打造 K8s AI Infra 的 30 夜 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言