前面幾天討論的都是「怎麼切」。DRA 讓使用者說得出要哪一張卡、要多少;HAMi 讓那個數字在容器裡被真正執行。
但這些機制都建立在同一個前提上:Pod 已經被建立出來了。
真實的叢集並不是這樣運作的。八張卡、三個團隊、十幾個訓練工作同時想跑,就會出現幾個問題:誰先跑、誰該等、等的要等多久。前面幾天介紹的機制都無法回答這些問題,因為它們處理的是「這個 Pod 要拿什麼」,而這些問題屬於「這個 Job 現在該不該開始」。
今天的主角 Kueue 處理的就是這一層。
Kueue 負責管理配額,以及工作如何消耗這些配額。具體來說,它決定三件事:工作何時該等待、何時被准許開始,以及何時該被搶占。其中「被准許開始」指的是 Pod 可以被建立了;在那之前,Pod 根本不存在。「被搶占」則是指執行中的 Pod 會被刪除。
Kueue 不取代任何既有的 Kubernetes 元件。自動擴縮仍由 cluster-autoscaler 負責,Pod 放到哪個節點仍由 kube-scheduler 決定,Job 的生命週期仍由 kube-controller-manager 管理。Kueue 只在這些元件前面加上一道關卡。
因此整條鏈的分工如下:
| 元件 | 決定什麼 |
|---|---|
| Kueue | 這個 Job 現在能不能開始 |
| kube-scheduler | Pod 放在哪個節點 |
| DRA | 這個 Pod 拿哪一張卡 |
| HAMi | 拿到之後能用多少 |
由上往下,粒度一層比一層細。只有最上面這一層被擋下時,後面的事情完全不會發生。
Kueue 的做法相當直接:利用 Job 的 spec.suspend 欄位。
suspend 是 batch/v1 Job 原本就有的欄位,設為 true 的 Job 不會產生任何 Pod。Kueue 分兩個步驟使用它:
因為第一步由 webhook 自動處理,使用者不需要自己把 Job 建立成暫停狀態。本篇實驗的 yaml 仍然寫了 suspend: true,這樣寫沒有錯,但並非必要。
Kueue 的 API 物件不少,本篇只用到四個:
| 物件 | 範圍 | 定義 |
|---|---|---|
| ResourceFlavor | 叢集 | 描述叢集中有哪些可用資源 |
| ClusterQueue | 叢集 | 管理一池資源,定義使用上限與公平共享規則 |
| LocalQueue | namespace | 把屬於同一個租戶、關係密切的工作負載歸為一組 |
| Workload | namespace | 會執行到結束的應用程式,是 Kueue 中准入的基本單位 |
依關係排成一條線:
ResourceFlavor 資源的種類
↓
ClusterQueue 這一池有多少額度 ← 管理員建立,叢集層級
↓
LocalQueue 某個 namespace 的入口 ← 團隊使用的入口
↓
Workload 一次申請 ← Kueue 自動產生
Workload 是四者中唯一不需要自己建立的物件。 送出一個 Job 後,Kueue 會為它產生一個對應的 Workload,這才是 Kueue 實際在排隊處理的對象。
額度寫在 ClusterQueue 的 nominalQuota 欄位,代表這個 ClusterQueue 在任一時刻可以使用的該資源數量。
今天的實驗使用 day1 用 fake-gpu-operator 模擬假卡的叢集來測試:
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
只有一台 worker 上有 8 張假卡。 另外兩個節點沒有 run.ai/simulated-gpu-node-pool 標籤,所以 fake-gpu-operator 不會管理它們。
這個 8 是整個實驗的關鍵數字。
安裝 Kueue 只需要官方的 release manifest:
kubectl apply --server-side \
-f https://github.com/kubernetes-sigs/kueue/releases/download/v0.19.5/manifests.yaml
以本篇使用的版本來說,這份 manifest 會安裝十一個 CRD、數十個 RBAC 角色、一個 controller manager,以及兩個 webhook configuration。webhook 負責在 Job 建立時將它攔截並設為暫停。
kubectl get pods -n kueue-system
NAME READY STATUS RESTARTS AGE
kueue-controller-manager-5f685d7d6f-8gk4z 1/1 Running 0 23s
apiVersion: v1
kind: Namespace
metadata: { name: research }
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata: { name: gpu }
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata: { name: research-cq }
spec:
namespaceSelector: {} # 允許所有 namespace 透過 LocalQueue 使用
resourceGroups:
- coveredResources: ["nvidia.com/gpu"]
flavors:
- name: gpu
resources:
- name: "nvidia.com/gpu"
nominalQuota: 4 # 這個佇列只有 4 張卡的額度
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata: { name: team-queue, namespace: research }
spec: { clusterQueue: research-cq }
這裡的 ResourceFlavor 是空的,沒有 nodeLabels,也沒有 taint。叢集中的資源同質時,可以直接使用空的 ResourceFlavor。需要區分不同型號(例如 A100 與 T4)時,才會用 nodeLabels 等欄位把 ResourceFlavor 對應到特定節點。
kubectl apply -f 01-quota.yaml
namespace/research created
resourceflavor.kueue.x-k8s.io/gpu created
clusterqueue.kueue.x-k8s.io/research-cq created
localqueue.kueue.x-k8s.io/team-queue created
目前的狀態:叢集有 8 張卡,但這個佇列只准使用 4 張。 剩下的 4 張沒有故障,也沒有被佔用,只是不在這個佇列的額度內。
apiVersion: batch/v1
kind: Job
metadata:
name: train-a
namespace: research
labels: { kueue.x-k8s.io/queue-name: team-queue } # 交給 Kueue 管理
spec:
parallelism: 4
completions: 4
suspend: true
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: registry.k8s.io/pause:3.10
resources:
limits:
nvidia.com/gpu: 1
(train-b 內容相同,只有名稱不同。)
parallelism: 4 加上每個 Pod 需要一張卡,一個 Job 就需要 4 張,剛好用完整個配額。兩個 Job 同時送出,一定有一個進不來。
決定 Kueue 是否介入的是這個 label:
labels: { kueue.x-k8s.io/queue-name: team-queue }
它指定 Job 要送進哪個 LocalQueue。本篇的 research namespace 中,沒有這個 label 的 Job 不會被 Kueue 管理,會照常直接執行。
kubectl apply -f 02-twojob.yaml
job.batch/train-a created
job.batch/train-b created
兩個 Job 都顯示 created。但被建立不代表已經開始執行。
kubectl -n research get jobs \
-o custom-columns='JOB:.metadata.name,SUSPEND:.spec.suspend,ACTIVE:.status.active'
JOB SUSPEND ACTIVE
train-a true <none>
train-b false 4
兩個 Job 的 yaml 相同、同時送出,suspend 都寫成 true。但現在一個是 true、一個是 false:Kueue 解除了 train-b 的暫停,train-a 則維持原狀。
至於為什麼是 train-b 而不是 train-a:Kueue 預設依優先權、再依 Workload 的建立時間排序。兩個 Job 優先權相同,又是同一次 apply 送出,建立時間落在同一秒,排序比不出先後,所以誰先進場並不保證。
再看 Pod:
kubectl -n research get pods
NAME READY STATUS RESTARTS AGE
train-b-56x8m 1/1 Running 0 68s
train-b-5j8bs 1/1 Running 0 68s
train-b-h46j5 1/1 Running 0 68s
train-b-vfdcl 1/1 Running 0 68s
四個 Pod 全部屬於 train-b。
train-a 沒有任何 Pod。 不是 Pending,也不是 ContainerCreating,而是根本不存在。
這和一般資源不足的情況完全不同。一般情況下,Pod 會停在 Pending,describe 可以看到 Insufficient nvidia.com/gpu 的事件。這裡什麼都看不到,因為 kube-scheduler 從來沒有收到過這些 Pod。
train-a 目前以 Workload 物件的形式存在:
kubectl -n research get kwl
NAME QUEUE RESERVED IN ADMITTED FINISHED AGE
job-train-a-5db4e team-queue 2m35s
job-train-b-a44b6 team-queue research-cq True 2m35s
(kwl 是 workloads.kueue.x-k8s.io 的簡稱。)
兩個 Workload 都存在,名稱格式是 job-<Job 名稱>-<雜湊>,由 Kueue 自動產生,yaml 裡並沒有定義它們。
差別在中間兩欄:
RESERVED IN |
ADMITTED |
|
|---|---|---|
job-train-b |
research-cq |
True |
job-train-a |
空 | 空 |
這兩欄對應 Kueue 的兩個步驟:
RESERVED IN 顯示額度保留在哪個 ClusterQueue。train-a 的 RESERVED IN 是空的,代表它連額度都還沒取得,自然也不會被准入。
也可以從 condition 的角度確認:
kubectl -n research get workloads.kueue.x-k8s.io \
-o custom-columns='NAME:.metadata.name,QUEUE:.spec.queueName,ADMITTED:.status.conditions[?(@.type=="Admitted")].status'
NAME QUEUE ADMITTED
job-train-a-5db4e team-queue <none>
job-train-b-a44b6 team-queue True
Admitted 是 Workload 上的一個 condition。train-b 的值是 True,train-a 則沒有 Admitted 為 True 的紀錄,也就是尚未被准入。若想看 train-a 目前完整的 condition 與未准入的原因,可以用 kubectl describe 查看該 Workload。
最後看佇列本身的狀態:
kubectl get clusterqueue research-cq \
-o custom-columns='NAME:.metadata.name,PENDING:.status.pendingWorkloads,ADMITTED:.status.admittedWorkloads'
NAME PENDING ADMITTED
research-cq 1 1
一個排隊中,一個已准入。 這兩個數字可以直接從 ClusterQueue 的 status 看到。
把兩件事並列:
叢集上的卡 8 張
train-b 使用 4 張
尚未使用 4 張
train-a 需要 4 張 ← 數量剛好足夠
train-a 的狀態 排隊中
實體資源足夠,工作卻無法執行。
這不是錯誤,而是 Kueue 的設計目的。那 4 張卡之所以閒置,是因為 nominalQuota: 4 規定這個佇列只能使用 4 張。額度是人為設定的上限,與機器上實際有多少卡無關。
這種設計的用意在於保留資源給其他團隊。本實驗只有一個佇列,所以看起來像是浪費;實際環境中,research-cq、inference-cq、ci-cq 會各自持有一份額度,而「研究團隊不能用光所有卡」正是期望的行為。
kube-scheduler 無法做到這件事。 它只判斷「節點上還有沒有資源」,不判斷「這個團隊該不該使用」。kube-scheduler 看到的是實體資源,Kueue 看到的是已分配的額度。
今天介紹了 Kueue,它處理的是「這個工作現在該不該開始」。它不取代 kube-scheduler,而是在 Pod 建立之前加上一道准入關卡:Job 建立時被設為暫停,等 Workload 在 ClusterQueue 中取得額度、被准入之後才解除暫停,Pod 也要到那時才會出現。
不過,Kueue 只決定工作何時開始,Pod 放到哪裡仍然交給 kube-scheduler,而 kube-scheduler 是一次排一個 Pod。對需要多個 Pod 同時到齊的分散式訓練來說,這還不夠。接下來會介紹 gang scheduling,以及在排程階段處理整組 Pod 的 Volcano 和 KAI Scheduler。
Kueue — Overview
Kueue — Concepts
Kueue — ClusterQueue
Kueue — Resource Flavor
Kueue — Run A Kubernetes Job
Kueue — Setup default LocalQueue
Kubernetes — Jobs:Suspending a Job