一文搞懂 GPU 共享方案: NVIDIA Time Slicing
本文主要分享 GPU 共享方案,包括如何安裝、配置以及使用,最后通過分析源碼了 TImeSlicing 的具體實現(xiàn)。通過配置 TImeSlicing 可以實現(xiàn) Pod 共享一塊物理 GPU,以提升資源利用率。
1.為什么需要 GPU 共享、切分等方案?
開始之前我們先思考一個問題,為什么需要 GPU 共享、切分等方案?
或者說是另外一個問題:明明直接在裸機環(huán)境使用,都可以多個進程共享 GPU,怎么到 k8s 環(huán)境就不行了。
推薦閱讀前面幾篇文章:這兩篇分享了如何在各個環(huán)境中使用 GPU,在 k8s 環(huán)境則推薦使用 NVIDIA 提供的 gpu-operator 快速部署環(huán)境。
GPU 環(huán)境搭建指南:如何在裸機、Docker、K8s 等環(huán)境中使用 GPU
GPU 環(huán)境搭建指南:使用 GPU Operator 加速 Kubernetes GPU 環(huán)境搭建
這兩篇則分析了 device-plugin 原理以及在 K8s 中創(chuàng)建一個申請 GPU 的 Pod 后的一些列動作,最終該 Pod 是如何使用到 GPU 的。
Kubernetes教程(二一)---自定義資源支持:K8s Device Plugin 從原理到實現(xiàn)
Kubernetes教程(二二)---在 K8S 中創(chuàng)建 Pod 是如何使用到 GPU 的:device plugin&nvidia-container-toolkit 源碼分析
看完之后,大家應該就大致明白了。
資源感知
首先在 k8s 中資源是和節(jié)點綁定的,對于 GPU 資源,我們使用 NVIDIA 提供的 device-plugin 進行感知,并上報到 kube-apiserver,這樣我們就能在 Node 對象上看到對應的資源了。
就像這樣:
root@liqivm:~# k describe node gpu01|grep Capacity -A 7
Capacity:
cpu: 128
ephemeral-storage: 879000896Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 1056457696Ki
nvidia.com/gpu: 8
pods: 110
可以看到,該節(jié)點除了基礎(chǔ)的 cpu、memory 之外,還有一個nvidia.com/gpu: 8 信息,表示該節(jié)點上有 8 個 GPU。
資源申請
然后我們就可以在創(chuàng)建 Pod 時申請對應的資源了,比如申請一個 GPU:
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
containers:
- name: gpu-container
image: nvidia/cuda:11.0-base # 一個支持 GPU 的鏡像
resources:
limits:
nvidia.com/gpu: 1 # 申請 1 個 GPU
command: ["nvidia-smi"] # 示例命令,顯示 GPU 的信息
restartPolicy: OnFailure
apply 該 yaml 之后,kube-scheduler 在調(diào)度該 Pod 時就會將其調(diào)度到一個擁有足夠 GPU 資源的 Node 上。
同時該 Pod 申請的部分資源也會標記為已使用,不會在分配給其他 Pod。
到這里,問題的答案就已經(jīng)很明顯的。
- 1)device-plugin 感知到節(jié)點上的物理 GPU 數(shù)量,上報到 kube-apiserver
- 2)kube-scheduler 調(diào)度 Pod 時會根據(jù) pod 中的 Request 消耗對應資源
即:Node 上的 GPU 資源被 Pod 申請之后,在 k8s 中就被標記為已消耗了,后續(xù)創(chuàng)建的 Pod 會因為資源不夠?qū)е聼o法調(diào)度。
實際上:可能 GPU 性能比較好,可以支持多個 Pod 共同使用,但是因為 k8s 中的調(diào)度限制導致多個 Pod 無法正常共享。
因此,我們才需要 GPU 共享、切分等方案。
2. 什么是 Time Slicing 方案
NVIDIA 提供的 Time-Slicing GPUs in Kubernetes 是一種通過 oversubscription(超額訂閱) 來實現(xiàn) GPU 共享的策略,這種策略能讓多個任務在同一個 GPU 上進行,而不是每個任務都獨占一個 GPU。
雖然方案名稱叫做 Time Slicing,但是和時間切片沒有任何關(guān)系,實際上是一個 GPU 超賣方案。
比如節(jié)點上只有一個物理 GPU,正常安裝 GPU Operator 之后,device plugin 檢測到該節(jié)點上有 1 個 GPU,上報給 kubelet,然后 kubelet 更新到 kube-apiserver,我們就可以在 Node 對象上看到了:
root@liqivm:~# k describe node gpu01|grep Capacity -A 7
Capacity:
cpu: 128
ephemeral-storage: 879000896Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 1056457696Ki
nvidia.com/gpu: 1
pods: 110
此時,創(chuàng)建一個 Pod 申請 1 個 GPU 之后,第二個 Pod 就無法使用了,因為 GPU 資源不足無法調(diào)度。
但是 Time Slicing 可以進行 oversubscription 設(shè)置,將 device-plugin 上報的 GPU 數(shù)量進行擴大。
比如將其數(shù)量放大 10 倍,device plugin 就會上報該節(jié)點有 1*10 = 10 個 GPU,最終 kube-apiserver 則會記錄該節(jié)點有 10 個 GPU:
root@liqivm:~# k describe node gpu01|grep Capacity -A 7
Capacity:
cpu: 128
ephemeral-storage: 879000896Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 1056457696Ki
nvidia.com/gpu: 10
pods: 110
這樣,就可以供 10 個 Pod 使用了。
當然了,Time Slicing 方案也有缺點:多個 Pod 之間沒有內(nèi)存或者故障隔離,完全的共享,能使用多少內(nèi)存和算力全靠多個 Pod 自行競爭。
ps:就和直接在宿主機上多個進程共享一個 GPU 基本一致
3. Time Slicing Demo
Time Slicing 由于是 NVIDIA 的方案,因此使用起來比較簡單,只需要在部署完成 GPU Operator 之后進行配置即可。
首先參考這篇文章完成 GPU Operator 的部署 --> GPU 環(huán)境搭建指南:使用 GPU Operator 加速 Kubernetes GPU 環(huán)境搭建
然后即可開始配置 TimeSlicing。
整體配置分為以下 3 個步驟:
1)創(chuàng)建 TimeSlicing 配置
根據(jù)官方文檔描述,修改了 TimeSlicing 配置之后,device plugin Pod 不會自動重啟,因此新的配置不會生效,需要手動重啟對應 Pod。
kubectl rollout restart -n gpu-operator daemonset/nvidia-device-plugin-daemonset
2)修改集群策略開啟 Time Slicing,并指定讓 device-plugin 使用第一步中創(chuàng)建的配置
- 這里則是通過 Configmap 名稱來指定
3)(可選)給要使用 GPU TimeSlicing 的節(jié)點打上對應 label,實現(xiàn)不同 Node 使用不同策略
- 比如不同節(jié)點上的 GPU 不同,那么可以根據(jù) GPU 的算力或者內(nèi)存情況設(shè)置不同的副本數(shù)以合理利用資源
- 如果都是統(tǒng)一 GPU,則使用集群級別的統(tǒng)一配置即可
配置開啟 TimeSlicing
創(chuàng)建 TimeSlicing 配置
使用一個單獨的 Configmap 來存放 TimeSlicing 的配置。
這里使用集群級別的統(tǒng)一配置,配置文件 time-slicing-config-all.yaml 完整內(nèi)容如下:
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config-all
data:
any: |-
version: v1
flags:
migStrategy: none
sharing:
timeSlicing:
renameByDefault: false
failRequestsGreaterThanOne: false
resources:
- name: nvidia.com/gpu
replicas: 4
具體配置含義參考官方文檔:about-configuring-gpu-time-slicing
data.<key>: 配置的名字,可以為不同 Node 設(shè)置單獨配置,后續(xù)通過名稱引用對應配置。- 后續(xù)開啟 TimeSlicing 時則根據(jù) key 指定使用不同配置
- 這里我們使用集群統(tǒng)一配置,因此創(chuàng)建一個 key 即可
flags.migStrategy:配置開啟時間片之后如何處理 MIG 設(shè)備,默認為 nonerenameByDefault:是否對 GPU 資源改名。- 設(shè)置為 true 之后,會使用
<resource-name>.shared替代原本的<resource-name>。例如nvidia.com/gpu會變成nvidia.com/gpu.shared,顯式告知使用者這是共享 GPU。 - 默認為 false,即不改資源類型名,不過 Node 上的 label 也會改,比如使用時間片之前是
nvidia.com/gpu.product=Tesla-T4, 使用后就會變成nvidia.com/gpu.product=Tesla-T4-SHARED這樣依舊可以通過 nodeSelector 來限制 Pod 調(diào)度節(jié)點,來控制是否使用共享的 GPU - 推薦使用 fasle 即可
- 設(shè)置為 true 之后,會使用
failRequestsGreaterThanOne:開啟后,當 Pod 請求 1 個以上的 shared GPU 時直接報錯 UnexpectedAdmissionError。這個字段是通過報錯的方式告訴使用者,請求多個 shared GPU 并不會增加 Pod 對該共享 GPU 的占用時間。resources.name:要通過時間分片提供訪問的資源類似,比如nvidia.com/gpuresources.replicas:可共享訪問的資源數(shù)量,比如這里指定的 4 也就是 1 個該類型的 GPU 可以供 4 個 Pod 共享訪問,也就是最終 Pod 上看到的 GPU 數(shù)量是物理 GPU 數(shù)量的 4 倍。
將配置 Apply 到 gpu-operator 所在的 namespace
kubectl create -n gpu-operator -f time-slicing-config-all.yaml
修改集群策略
修改clusterpolicies.nvidia.com/cluster-policy 對象,讓 device plugin 使用上一步創(chuàng)建的配置。
kubectl patch clusterpolicies.nvidia.com/cluster-policy \
-n gpu-operator --type merge \
-p '{"spec": {"devicePlugin": {"config": {"name": "time-slicing-config-all", "default": "any"}}}}'
- name:time-slicing-config-all 指定了配置文件對應的 Configmap 名稱
- default:any:表示默認配置為這個 Configmap 中的 key 為 any 的配置
修改后 gpu-feature-discovery 和 nvidia-device-plugin-daemonset pod 會重啟,使用以下命令查看重啟過程
kubectl get events -n gpu-operator --sort-by='.lastTimestamp'
驗證 TimeSlicing 是否生效
查看 Node 上的 GPU 信息
首先查看一下 Node 信息,確認 TimeSlicing 生效了
kubectl describe node xxx
正常結(jié)果如下
...
Labels:
nvidia.com/gpu.count=4
nvidia.com/gpu.product=Tesla-T4-SHARED
nvidia.com/gpu.replicas=4
Capacity:
nvidia.com/gpu: 16
...
Allocatable:
nvidia.com/gpu: 16
...
增加了幾個 label,
- nvidia.com/gpu.product=Tesla-T4-SHARED
- nvidia.com/gpu.replicas=4
根據(jù)nvidia.com/gpu.count=4 可知,節(jié)點上有 4 張 GPU,然后由于使用了時間片,且配置的nvidia.com/gpu.replicas=4 副本數(shù)為 4,因此最終節(jié)點上 device plugin 上報的 GPU 數(shù)量就是 4*4 = 16 個。
驗證 GPU 能否正常使用
創(chuàng)建一個 Deployment 來驗證,GPU 能否正常使用。
這里副本數(shù)指定為 5,因為集群里只有 4 張 GPU,如果 TimeSlicing 未生效,那么有一個 Pod 肯定會應為拿不到 GPU 資源而 pending。
apiVersion: apps/v1
kind: Deployment
metadata:
name: time-slicing-verification
labels:
app: time-slicing-verification
spec:
replicas: 2
selector:
matchLabels:
app: time-slicing-verification
template:
metadata:
labels:
app: time-slicing-verification
spec:
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
hostPID: true
containers:
- name: cuda-sample-vector-add
image: "nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda11.7.1-ubuntu20.04"
command: ["/bin/bash", "-c", "--"]
args:
- while true; do /cuda-samples/vectorAdd; done
resources:
limits:
nvidia.com/gpu: 1
會啟動 5 個 Pod,
查看情況
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
time-slicing-verification-7cdc7f87c5-lkd9d 1/1 Running 0 23s
time-slicing-verification-7cdc7f87c5-rrzq7 1/1 Running 0 23s
time-slicing-verification-7cdc7f87c5-s8qwk 1/1 Running 0 23s
time-slicing-verification-7cdc7f87c5-xhmb7 1/1 Running 0 23s
time-slicing-verification-7cdc7f87c5-zsncp 1/1 Running 0 23s
5 個 Pod 都啟動了,說明時間片時成功的。
隨便查看一個 Pod 的日志
$ kubectl logs deploy/time-slicing-verification
Found 5 pods, using pod/time-slicing-verification-7cdc7f87c5-s8qwk
[Vector addition of 50000 elements]
Copy input data from the host memory to the CUDA device
CUDA kernel launch with 196 blocks of 256 threads
Copy output data from the CUDA device to the host memory
Test PASSED
Done
[Vector addition of 50000 elements]
Copy input data from the host memory to the CUDA device
CUDA kernel launch with 196 blocks of 256 threads
Copy output data from the CUDA device to the host memory
...
有 Test PASSED 則說明成功了。
說明 TimeSlicing 配置生效了。
4. 使用 Node 級別的單獨配置
前面只創(chuàng)建了一個名稱為 any 的配置,并在 clusterpolicy 中指明了使用該配置為默認配置,因此集群中的全部節(jié)點都會使用該配置來做時間片。
但是可能集群中不同節(jié)點上的 GPU 型號不同,因此需要共享分副本數(shù)可以調(diào)整,性能好的副本數(shù)就調(diào)大一點,性能差的就小一點。
本章主要記錄怎么為不同的節(jié)點使用不同的配置。
實際上是為不同的 GPU 準備不同的配置。
創(chuàng)建時間片配置
同樣的創(chuàng)建 TimeSlicing 配置,不過這次 Configmap 中寫了兩個配置,而且是以 GPU 型號命名的
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config-fine
data:
a100-40gb: |-
version: v1
flags:
migStrategy: mixed
sharing:
timeSlicing:
resources:
- name: nvidia.com/gpu
replicas: 8
- name: nvidia.com/mig-1g.5gb
replicas: 2
- name: nvidia.com/mig-2g.10gb
replicas: 2
- name: nvidia.com/mig-3g.20gb
replicas: 3
- name: nvidia.com/mig-7g.40gb
replicas: 7
tesla-t4: |-
version: v1
flags:
migStrategy: none
sharing:
timeSlicing:
resources:
- name: nvidia.com/gpu
replicas: 4
可以看到,分別對 A100 和 Tesla T4 這兩種 GPU 做了配置。
- a100-40gb:A100 支持 MIG,因此增加了 MIG 部分的配置,若沒有則指定為 none 即可
- 然后根據(jù) MIG 實例分別指定不同的 replicas 數(shù)
- tesla-t4:Tesla T4 GPU 性能比較差,因此 replicas 指定為 4 即可
將配置 Apply 到 gpu-operator 所在的 namespace
kubectl create -n gpu-operator -f time-slicing-config-all.yaml
修改集群策略
同樣的,修改一下 cluster-policy 指定 device plugin 使用的 Configmap,這次與之前的區(qū)別在于,這里沒有指定 default 配置。
kubectl patch clusterpolicies.nvidia.com/cluster-policy \
-n gpu-operator --type merge \
-p '{"spec": {"devicePlugin": {"config": {"name": "time-slicing-config-fine"}}}}'
沒有指定 default 時,device-plugin 則會根據(jù) node 上的 label (nvidia.com/device-plugin.config)來獲取要使用的配置。
為節(jié)點打 label
在節(jié)點上打上下面的 label,這樣該節(jié)點上的 device plugin 就會根據(jù)該 label 的 value 來使用對應名字的配置了。
比如這里,就是有這個 label 的節(jié)點就使用名叫 tesla-t4 的配置。
kubectl label node <node-name> nvidia.com/device-plugin.config=tesla-t4
一般都是以 GPU 型號命名,然后給使用該 GPU 的節(jié)點都打上對應 label,這樣便于查看。
5. 關(guān)閉 TimeSlicing
想關(guān)閉 TimeSlicing 配置也很簡單,直接更新 集群策略 把 device plugin 下的 config 這一段去掉即可。
devicePlugin:
config:
default: any
name: time-slicing-config-all
enabled: true
env:
- name: PASS_DEVICE_SPECS
value: "true"
- name: FAIL_ON_INIT_ERROR
value: "true"
命令如下:
kubectl patch clusterpolicies.nvidia.com/cluster-policy -n gpu-operator --type json -p '[{"op": "remove", "path": "/spec/devicePlugin/config"}]'
然后重啟一下 device-plugin pod
kubectl rollout restart -n gpu-operator daemonset/nvidia-device-plugin-daemonset
不出意外的話就關(guān)掉了,再次查看 Pod 信息,GPU 就變成了物理 GPU 數(shù)量,說明關(guān)閉成功。
kubectl get node xxx -oyaml
addresses:
- address: 172.18.187.224
type: InternalIP
- address: izj6c5dnq07p1ic04ei9vwz
type: Hostname
allocatable:
cpu: "4"
ephemeral-storage: "189889991571"
hugepages-1Gi: "0"
hugepages-2Mi: "0"
memory: 15246720Ki
nvidia.com/gpu: "1"
pods: "110"
6.源碼分析
簡單看下源碼,分析 TimeSlicing 是怎么實現(xiàn)的。
首先是 device-plugin 可以接收的配置
// api/config/v1/config.go#L32
// Config is a versioned struct used to hold configuration information.
type Config struct {
Version string `json:"version" yaml:"version"`
Flags Flags `json:"flags,omitempty" yaml:"flags,omitempty"`
Resources Resources `json:"resources,omitempty" yaml:"resources,omitempty"`
Sharing Sharing `json:"sharing,omitempty" yaml:"sharing,omitempty"`
}
這也就是我們在 clusterPolicy 中配置的:
apiVersion: v1
kind: ConfigMap
metadata:
name: time-slicing-config-all
data:
any: |-
version: v1
flags:
migStrategy: none
sharing:
timeSlicing:
renameByDefault: false
failRequestsGreaterThanOne: false
resources:
- name: nvidia.com/gpu
replicas: 4
這里我們關(guān)注 resources 中的 replicas 參數(shù),正是這個參數(shù)定義了 **oversubscription(超額訂閱) ** 的額度。
resources:
- name: nvidia.com/gpu
replicas: 4
看下代碼中是什么生效的
// internal/rm/device_map.go#L282
// updateDeviceMapWithReplicas returns an updated map of resource names to devices with replica
// information from the active replicated resources config.
func updateDeviceMapWithReplicas(replicatedResources *spec.ReplicatedResources, oDevices DeviceMap) (DeviceMap, error) {
devices := make(DeviceMap)
// Begin by walking replicatedResources.Resources and building a map of just the resource names.
names := make(map[spec.ResourceName]bool)
for _, r := range replicatedResources.Resources {
names[r.Name] = true
}
// Copy over all devices from oDevices without a resource reference in TimeSlicing.Resources.
for r, ds := range oDevices {
if !names[r] {
devices[r] = ds
}
}
// Walk shared Resources and update devices in the device map as appropriate.
for _, resource := range replicatedResources.Resources {
r := resource
// Get the IDs of the devices we want to replicate from oDevices
ids, err := oDevices.getIDsOfDevicesToReplicate(&r)
if err != nil {
return nil, fmt.Errorf("unable to get IDs of devices to replicate for '%v' resource: %v", r.Name, err)
}
// Skip any resources not matched in oDevices
if len(ids) == 0 {
continue
}
// Add any devices we don't want replicated directly into the device map.
for _, d := range oDevices[r.Name].Difference(oDevices[r.Name].Subset(ids)) {
devices.insert(r.Name, d)
}
// Create replicated devices add them to the device map.
// Rename the resource for replicated devices as requested.
name := r.Name
if r.Rename != "" {
name = r.Rename
}
for _, id := range ids {
for i := 0; i < r.Replicas; i++ {
annotatedID := string(NewAnnotatedID(id, i))
replicatedDevice := *(oDevices[r.Name][id])
replicatedDevice.ID = annotatedID
replicatedDevice.Replicas = r.Replicas
devices.insert(name, &replicatedDevice)
}
}
}
return devices, nil
}
核心部分如下:
for _, id := range ids {
for i := 0; i < r.Replicas; i++ {
annotatedID := string(NewAnnotatedID(id, i))
replicatedDevice := *(oDevices[r.Name][id])
replicatedDevice.ID = annotatedID
replicatedDevice.Replicas = r.Replicas
devices.insert(name, &replicatedDevice)
}
}
可以看到,這里是雙層 for 循環(huán),對 device 數(shù)量進行了一個復制的操作,這樣每張 GPU 都可以被使用 Replicas 次了。
其他屬性都沒變,只是把 deviceID 進行了處理,便于區(qū)分
// NewAnnotatedID creates a new AnnotatedID from an ID and a replica number.
func NewAnnotatedID(id string, replica int) AnnotatedID {
return AnnotatedID(fmt.Sprintf("%s::%d", id, replica))
}
然后在真正掛載時則進行 split 拿到 id 和 replicas 信息
// Split splits a AnnotatedID into its ID and replica number parts.
func (r AnnotatedID) Split() (string, int) {
split := strings.SplitN(string(r), "::", 2)
if len(split) != 2 {
return string(r), 0
}
replica, _ := strconv.ParseInt(split[1], 10, 0)
return split[0], int(replica)
}
至此,我們就分析完了 TImeSlicing 的具體實現(xiàn),其實很簡單,就是根據(jù)配置的 replicas 參數(shù)對 device plugin 感知到的設(shè)備進行復制,并在 DeviceID 使用特定格式進行標記便于區(qū)分。
7. 小結(jié)
本文主要分享了 NVIDIA Time Slicing 這個 GPU 共享方案,包括即實現(xiàn)原理,以及配置和使用方式。
最后通過分析源碼的方式探索了 TImeSlicing 的代碼實現(xiàn)。
為什么需要 GPU 共享、切分?
在 k8s 中使用默認 device plugin 時,GPU 資源和物理 GPU 是一一對應的,導致一個物理 GPU 被一個 Pod 申請后,其他 Pod 就無法使用了。
為了提高資源利用率,因此我們需要 GPU 共享、切分等方案。
什么是 TimeSlicing?
TimeSlicing 是一種通過 oversubscription(超額訂閱) 來實現(xiàn) GPU 共享的策略,這種策略能讓多個任務在同一個 GPU 上進行,而不是每個任務都獨占一個 GPU。
如何開啟 TimeSlicing
1)創(chuàng)建 TimeSlicing 配置
- 可以是集群統(tǒng)一配置,也可以是 Node 級別的配置,主要根據(jù)不同節(jié)點上的 GPU 進行配置
- 如果集群中所有節(jié)點 GPU 型號都一致,則使用集群統(tǒng)一配置即可,若不一致則根據(jù) 節(jié)點上的 GPU 性能修改配置
2)修改 cluster-policy,增加 TimeSlicing 相關(guān)配置
作為這兩個步驟之后,TimeSlicing 就開啟了,再次查看 Node 信息時會發(fā)現(xiàn) GPU 數(shù)量變多了。
TImeSlicing 實現(xiàn)原理
根據(jù)配置的 replicas 參數(shù)對 device plugin 感知到的設(shè)備進行復制,并在 DeviceID 使用特定格式進行標記便于區(qū)分。
總結(jié)
以上是生活随笔為你收集整理的一文搞懂 GPU 共享方案: NVIDIA Time Slicing的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 原创:多家银行下调存款利率,一年定存利率
- 下一篇: layUI批量上传文件