概述
得益于不同尺寸的大模型的發(fā)展,我們業(yè)務(wù)最近部署的 GPU 服務(wù)的規(guī)格越來(lái)越多樣了,有1卡、2卡、4卡以及8卡的類型,這跟以往大部分都是1卡的情況相差很大,如果還是以 Kubernetes 默認(rèn)的調(diào)度方式,那么在不同規(guī)格的 GPU 服務(wù)的情況下,很容易產(chǎn)生資源碎片的問(wèn)題,尤其是在資源比較緊張,也就是 GPU 分配率本身就比較高的情況下,按照默認(rèn)的打散的方式的話,如果大部分節(jié)點(diǎn)都分配了5卡、6卡的情況下,那些4卡的服務(wù)就很難找到合適的節(jié)點(diǎn)調(diào)度上去了。這種情況下,GPU 服務(wù)調(diào)度的 binpack 算法可以緩解這樣的問(wèn)題。
binpack算法
該用 binpack 算法之后,可以看到,GPU 分配的時(shí)候會(huì)先填滿一個(gè)節(jié)點(diǎn),然后再考慮下一個(gè)節(jié)點(diǎn),于是4卡的容器就調(diào)度上去了。
從 Kubernetes v1.19 開始,調(diào)度框架(Scheduler Framework)被引入,提供了一個(gè)更靈活和可擴(kuò)展的插件機(jī)制,允許開發(fā)者通過(guò)插件的形式自定義調(diào)度邏輯。調(diào)度框架包括多個(gè)階段(例如Filter、Score、Reserve、Permit等)來(lái)替代原來(lái)的 Predicates 和 Priorities。
公司 Kubernetes 版本比較低(v1.20),關(guān)于調(diào)度器的配置在 v1.23 版本有過(guò)不大不小的變化,因此目前最新的 Kubernetes 版本的配置方法與 v1.20 是不太相同的,這點(diǎn)在測(cè)試和正式環(huán)境的時(shí)候需要注意,以免在 v1.20 的集群使用最新版本 Kubernetes 的配置。
- Kubernetes v1.19: 調(diào)度框架引入,但Predicates和Priorities仍然存在,作為向后兼容的一部分
- Kubernetes v1.20: 調(diào)度框架繼續(xù)改進(jìn),官方開始建議使用調(diào)度框架插件來(lái)替代Predicates和Priorities
- Kubernetes v1.22: Predicates和Priorities被標(biāo)記為廢棄(Deprecated)
- Kubernetes v1.23: Predicates和Priorities正式被移除,調(diào)度框架成為唯一的調(diào)度機(jī)制
在 Kubernetes 的調(diào)度器中,RequestedToCapacityRatioResourceAllocation 參數(shù)允許用戶指定資源以及每類資源的權(quán)重,以便根據(jù)請(qǐng)求數(shù)量與可用容量之比率為節(jié)點(diǎn)評(píng)分。這就使得用戶可以通過(guò)使用適當(dāng)?shù)膮?shù)來(lái)對(duì)擴(kuò)展資源執(zhí)行裝箱操作,從而提高了大型集群中稀缺資源的利用率。RequestedToCapacityRatioResourceAllocation 優(yōu)先級(jí)函數(shù)的行為可以通過(guò)名為 requestedToCapacityRatioArguments 的配置選項(xiàng)進(jìn)行控制。該標(biāo)志由兩個(gè)參數(shù) shape 和 resources 組成。shape 允許用戶根據(jù) utilization 和 score 值將函數(shù)調(diào)整為最少請(qǐng)求(least requested)或最多請(qǐng)求(most requested)計(jì)算。 resources 包含由 name 和 weight 組成,name 指定評(píng)分時(shí)要考慮的資源,weight 指定每種資源的權(quán)重。如果我們要啟用 GPU 的裝箱(binpack),那么可以參考下面的 Policy 配置。
{
"kind": "Policy",
"apiVersion": "v1",
"predicates": [],
"priorities": [
{
"name": "RequestedToCapacityRatioPriority",
"weight": 2,
"argument": {
"requestedToCapacityRatioArguments": {
"shape": [
{
"utilization": 0,
"score": 0
},
{
"utilization": 100,
"score": 10
}
],
"resources": [
{
"name": "nvidia.com/gpu",
"weight": 10
}
]
}
}
}
]
}
如果想用 Profile 來(lái)給調(diào)度器配置插件,可以用下面的配置,注意這是 configmap 的配置,如果使用 Profile 的方式,那么啟動(dòng)命令要改成使用 --config
,詳細(xì)可以查看示例文件 v-1-20-3-scheduler-with-profile.yaml。
apiVersion: v1
kind: ConfigMap
metadata:
name: gpu-binpack-scheduler-config
namespace: kube-system
data:
gpu-binpack-scheduler-config.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1beta1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: true
leaseDuration: 15s
renewDeadline: 10s
retryPeriod: 2s
resourceLock: leases
resourceNamespace: kube-system
resourceName: gpu-binpack-scheduler
profiles:
- schedulerName: gpu-binpack-scheduler
plugins:
score:
enabled:
- name: NodeResourcesMostAllocated
pluginConfig:
- name: NodeResourcesMostAllocated
args:
resources:
- name: nvidia.com/gpu
weight: 10
- name: cpu
weight: 1
- name: memory
weight: 1
關(guān)于 RequestedToCapacityRatioResourceAllocation 優(yōu)先級(jí)函數(shù)如何對(duì)節(jié)點(diǎn)評(píng)分,可以參考下面的流程,看完之后應(yīng)該會(huì)更清晰。
請(qǐng)求的資源
nvidia.com/gpu: 2
Memory: 256MB
CPU: 2
資源權(quán)重
nvidia.com/gpu: 5
Memory: 1
CPU: 3
FunctionShapePoint {{0, 0}, {100, 10}}
節(jié)點(diǎn) Node1 配置
可用:
nvidia.com/gpu: 4
Memory: 1 GB
CPU: 8
已用:
nvidia.com/gpu: 1
Memory: 256MB
CPU: 1
節(jié)點(diǎn)得分:
nvidia.com/gpu = resourceScoringFunction((2+1),4)
= (100 - ((4-3)*100/4)
= (100 - 25)
= 75
= rawScoringFunction(75)
= 7
Memory = resourceScoringFunction((256+256),1024)
= (100 -((1024-512)*100/1024))
= 50
= rawScoringFunction(50)
= 5
CPU = resourceScoringFunction((2+1),8)
= (100 -((8-3)*100/8))
= 37.5
= rawScoringFunction(37.5)
= 3
NodeScore = (7 * 5) + (5 * 1) + (3 * 3) / (5 + 1 + 3)
= 5
節(jié)點(diǎn) Node2 配置
可用:
nvidia.com/gpu: 8
Memory: 1GB
CPU: 8
已用:
nvidia.com/gpu: 2
Memory: 512MB
CPU: 6
節(jié)點(diǎn)得分:
nvidia.com/gpu = resourceScoringFunction((2+2),8)
= (100 - ((8-4)*100/8)
= (100 - 50)
= 50
= rawScoringFunction(50)
= 5
Memory = resourceScoringFunction((256+512),1024)
= (100 -((1024-768)*100/1024))
= 75
= rawScoringFunction(75)
= 7
CPU = resourceScoringFunction((2+6),8)
= (100 -((8-8)*100/8))
= 100
= rawScoringFunction(100)
= 10
NodeScore = (5 * 5) + (7 * 1) + (10 * 3) / (5 + 1 + 3)
= 7
因此按照以上的結(jié)算過(guò)程,Node2 是優(yōu)選的節(jié)點(diǎn)。
Kubernetes配置
測(cè)試條件下,我們有如下配置的 Kubernetes 集群(沒(méi)有GPU節(jié)點(diǎn))。
# k get no -o wide
NAME STATUS ROLES VERSION INTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
master Ready control-plane,master v1.20.3 192.168.1.200 openEuler 22.03 LTS 5.10.0-60.125.0.152.oe2203.x86_64 docker://19.3.13
node1 Ready <none> v1.20.3 192.168.1.201 openEuler 22.03 LTS 5.10.0-60.139.0.166.oe2203.x86_64 docker://19.3.13
node2 Ready <none> v1.20.3 192.168.1.202 openEuler 22.03 LTS 5.10.0-60.139.0.166.oe2203.x86_64 docker://19.3.13
fake-gpu
雖然測(cè)試集群沒(méi)有可用的 GPU,但是可以通過(guò)下面的配置,可以讓給沒(méi)有 GPU 的節(jié)點(diǎn),虛假地上報(bào) GPU 資源,方便后面的測(cè)試。
kubectl label node node1 nvidia.com/gpu.deploy.device-plugin=true nvidia.com/gpu.deploy.dcgm-exporter=true --overwrite
kubectl label node node2 nvidia.com/gpu.deploy.device-plugin=true nvidia.com/gpu.deploy.dcgm-exporter=true --overwrite
kubectl label node 10.189.212.124 nvidia.com/gpu.deploy.device-plugin=true nvidia.com/gpu.deploy.dcgm-exporter=true --overwrite
kubectl label node 10.189.212.125 nvidia.com/gpu.deploy.device-plugin=true nvidia.com/gpu.deploy.dcgm-exporter=true --overwrite
helm repo add fake-gpu-operator https://fake-gpu-operator.storage.googleapis.com
# 模擬每個(gè)節(jié)點(diǎn)有8卡
helm upgrade -i gpu-operator fake-gpu-operator/fake-gpu-operator --namespace gpu-operator --create-namespace --set initialTopology.config.node-autofill.gpu-count=8
查看部署的結(jié)果和上報(bào)的資源,可以看到 node1 和 node2 當(dāng)前都上報(bào)了8卡的 GPU。
# k get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
device-plugin-ljwrv 1/1 Running 0 36m 10.244.1.54 node2
device-plugin-vncbc 1/1 Running 0 36m 10.244.2.77 node1
nvidia-dcgm-exporter-2t54q 1/1 Running 0 9h 10.244.2.10 node1
nvidia-dcgm-exporter-v8p5z 1/1 Running 0 9h 10.244.1.6 node2
status-updater-7dd49b7b5c-2ppq4 1/1 Running 0 17h 10.244.1.3 node2
topology-server-6845cbc768-wpbtm 1/1 Running 0 17h 10.244.2.8 node1
# kubectl-view-allocations -g node
Resource Requested Limit Allocatable Free
nvidia.com/gpu (62%) 10.0 (62%) 10.0 16.0 6.0
├─ node1 (100%) 8.0 (100%) 8.0 8.0 0.0
└─ node2 (25%) 2.0 (25%) 2.0 8.0 6.0
自定義調(diào)度器
參考我個(gè)人整理的 yaml 文件 v-1-20-3-scheduler.yaml 可以在 kube-system 的命名空間下部署一個(gè)自定義的調(diào)度器,名叫 gpu-binpack-scheduler,并且作為測(cè)試,部署了一個(gè)10個(gè)副本,一共需要10卡的 deployment,看看調(diào)度的最終情況,這里需要注意的是,測(cè)試 deployment 的 schedulerName 必須指定為 gpu-binpack-scheduler。
測(cè)試結(jié)果
從測(cè)試結(jié)果看,會(huì)先將 node1 填滿了,再往 node2 分配,這個(gè)結(jié)果是符合我們的預(yù)期的。
# k get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
sleepy-deployment-d557d79ff-5mtlp 1/1 Running 0 7h16m 10.244.2.73 node1
sleepy-deployment-d557d79ff-5p2k5 1/1 Running 0 7h16m 10.244.2.69 node1
sleepy-deployment-d557d79ff-6rbmj 1/1 Running 0 7h16m 10.244.1.52 node2
sleepy-deployment-d557d79ff-f8js8 1/1 Running 0 7h16m 10.244.2.75 node1
sleepy-deployment-d557d79ff-j2hg4 1/1 Running 0 7h16m 10.244.2.70 node1
sleepy-deployment-d557d79ff-jhx27 1/1 Running 0 7h16m 10.244.2.71 node1
sleepy-deployment-d557d79ff-lxj6v 1/1 Running 0 7h16m 10.244.2.72 node1
sleepy-deployment-d557d79ff-mzklc 1/1 Running 0 7h16m 10.244.2.76 node1
sleepy-deployment-d557d79ff-tpclt 1/1 Running 0 7h16m 10.244.2.74 node1
sleepy-deployment-d557d79ff-zaaa5 1/1 Running 0 7h16m 10.244.1.53 node2