

本文主要分享 GPU 共享方案,包括如何安装、配置以及使用,最后通过分析源码了 TImeSlicing 的具体实现。通过配置 TImeSlicing 可以实现 Pod 共享一块物理 GPU,以提升资源利用率。
<!--more-->
开始之前我们先思考一个问题,为什么需要 GPU 共享、切分等方案?
或者说是另外一个问题:明明直接在裸机环境使用,都可以多个进程共享 GPU,怎么到 k8s 环境就不行了。
推荐阅读前面几篇文章:这两篇分享了如何在各个环境中使用 GPU,在 k8s 环境则推荐使用 NVIDIA 提供的 gpu-operator 快速部署环境。
这两篇则分析了 device-plugin 原理以及在 K8s 中创建一个申请 GPU 的 Pod 后的一些列动作,最终该 Pod 是如何使用到 GPU 的。
看完之后,大家应该就大致明白了。
首先在 k8s 中资源是和节点绑定的,对于 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可以看到,该节点除了基础的 cpu、memory 之外,还有一个nvidia.com/gpu: 8 信息,表示该节点上有 8 个 GPU。
然后我们就可以在创建 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: OnFailureapply 该 yaml 之后,kube-scheduler 在调度该 Pod 时就会将其调度到一个拥有足够 GPU 资源的 Node 上。
同时该 Pod 申请的部分资源也会标记为已使用,不会在分配给其他 Pod。
到这里,问题的答案就已经很明显的。
即:Node 上的 GPU 资源被 Pod 申请之后,在 k8s 中就被标记为已消耗了,后续创建的 Pod 会因为资源不够导致无法调度。
实际上:可能 GPU 性能比较好,可以支持多个 Pod 共同使用,但是因为 k8s 中的调度限制导致多个 Pod 无法正常共享。
因此,我们才需要 GPU 共享、切分等方案。
NVIDIA 提供的 Time-Slicing GPUs in Kubernetes 是一种通过 oversubscription(超额订阅) 来实现 GPU 共享的策略,这种策略能让多个任务在同一个 GPU 上进行,而不是每个任务都独占一个 GPU。
虽然方案名称叫做 Time Slicing,但是和时间切片没有任何关系,实际上是一个 GPU 超卖方案。
比如节点上只有一个物理 GPU,正常安装 GPU Operator 之后,device plugin 检测到该节点上有 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此时,创建一个 Pod 申请 1 个 GPU 之后,第二个 Pod 就无法使用了,因为 GPU 资源不足无法调度。
但是 Time Slicing 可以进行 oversubscription 设置,将 device-plugin 上报的 GPU 数量进行扩大。
比如将其数量放大 10 倍,device plugin 就会上报该节点有 1*10 = 10 个 GPU,最终 kube-apiserver 则会记录该节点有 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 之间没有内存或者故障隔离,完全的共享,能使用多少内存和算力全靠多个 Pod 自行竞争。
ps:就和直接在宿主机上多个进程共享一个 GPU 基本一致
Time Slicing 由于是 NVIDIA 的方案,因此使用起来比较简单,只需要在部署完成 GPU Operator 之后进行配置即可。
首先参考这篇文章完成 GPU Operator 的部署
然后即可开始配置 TimeSlicing。
整体配置分为以下 3 个步骤:
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具体配置含义参考官方文档
data.<key>: 配置的名字,可以为不同 Node 设置单独配置,后续通过名称引用对应配置。flags.migStrategy:配置开启时间片之后如何处理 MIG 设备,默认为 nonerenameByDefault:是否对 GPU 资源改名。<resource-name>.shared 替代原本的 <resource-name>。例如 nvidia.com/gpu 会变成 nvidia.com/gpu.shared ,显式告知使用者这是共享 GPU。nvidia.com/gpu.product=Tesla-T4, 使用后就会变成nvidia.com/gpu.product=Tesla-T4-SHARED 这样依旧可以通过 nodeSelector 来限制 Pod 调度节点,来控制是否使用共享的 GPUfailRequestsGreaterThanOne:开启后,当 Pod 请求 1 个以上的 shared GPU 时直接报错 UnexpectedAdmissionError。这个字段是通过报错的方式告诉使用者,请求多个 shared GPU 并不会增加 Pod 对该共享 GPU 的占用时间。resources.name:要通过时间分片提供访问的资源类似,比如nvidia.com/gpuresources.replicas:可共享访问的资源数量,比如这里指定的 4 也就是 1 个该类型的 GPU 可以供 4 个 Pod 共享访问,也就是最终 Pod 上看到的 GPU 数量是物理 GPU 数量的 4 倍。
将配置 Apply 到 gpu-operator 所在的 namespacekubectl create -n gpu-operator -f time-slicing-config-all.yamlclusterpolicies.nvidia.com/cluster-policy 对象,让 device plugin 使用上一步创建的配置。kubectl patch clusterpolicies.nvidia.com/cluster-policy \
-n gpu-operator --type merge \
-p '{"spec": {"devicePlugin": {"config": {"name": "time-slicing-config-all", "default": "any"}}}}'gpu-feature-discovery 和 nvidia-device-plugin-daemonset pod 会重启,使用以下命令查看重启过程kubectl get events -n gpu-operator --sort-by='.lastTimestamp'kubectl describe node xxx...
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
...nvidia.com/gpu.count=4 可知,节点上有 4 张 GPU,然后由于使用了时间片,且配置的nvidia.com/gpu.replicas=4 副本数为 4,因此最终节点上 device plugin 上报的 GPU 数量就是 4*4 = 16 个。
验证 GPU 能否正常使用
创建一个 Deployment 来验证,GPU 能否正常使用。
这里副本数指定为 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: "https://www.pxwang.com/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$ 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$ 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
...实际上是为不同的 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: 4kubectl create -n gpu-operator -f time-slicing-config-all.yamlkubectl patch clusterpolicies.nvidia.com/cluster-policy \
-n gpu-operator --type merge \
-p '{"spec": {"devicePlugin": {"config": {"name": "time-slicing-config-fine"}}}}'nvidia.com/device-plugin.config)来获取要使用的配置。
为节点打 label
在节点上打上下面的 label,这样该节点上的 device plugin 就会根据该 label 的 value 来使用对应名字的配置了。
比如这里,就是有这个 label 的节点就使用名叫 tesla-t4 的配置。kubectl label node <node-name> nvidia.com/device-plugin.config=tesla-t4devicePlugin:
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"}]'kubectl rollout restart -n gpu-operator daemonset/nvidia-device-plugin-daemonsetkubectl 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"// 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"`
}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: 4resources:
- 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 循环,对 device 数量进行了一个复制的操作,这样每张 GPU 都可以被使用 Replicas 次了。
其他属性都没变,只是把 deviceID 进行了处理,便于区分// 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 的具体实现,其实很简单,就是根据配置的 replicas 参数对 device plugin 感知到的设备进行复制,并在 DeviceID 使用特定格式进行标记便于区分。
## 7. 小结
本文主要分享了 NVIDIA Time Slicing 这个 GPU 共享方案,包括即实现原理,以及配置和使用方式。
最后通过分析源码的方式探索了 TImeSlicing 的代码实现。
**为什么需要 GPU 共享、切分?**
在 k8s 中使用默认 device plugin 时,GPU 资源和物理 GPU 是一一对应的,导致一个物理 GPU 被一个 Pod 申请后,其他 Pod 就无法使用了。
为了提高资源利用率,因此我们需要 GPU 共享、切分等方案。
**什么是 TimeSlicing?**
TimeSlicing 是一种通过 oversubscription(超额订阅) 来实现 GPU 共享的策略,这种策略能让多个任务在同一个 GPU 上进行,而不是每个任务都独占一个 GPU。
**如何开启 TimeSlicing**
* 1)创建 TimeSlicing 配置
* 可以是集群统一配置,也可以是 Node 级别的配置,主要根据不同节点上的 GPU 进行配置
* 如果集群中所有节点 GPU 型号都一致,则使用集群统一配置即可,若不一致则根据 节点上的 GPU 性能修改配置
2)修改 cluster-policy,增加 TimeSlicing 相关配置
作为这两个步骤之后,TimeSlicing 就开启了,再次查看 Node 信息时会发现 GPU 数量变多了。
**TImeSlicing 实现原理**
根据配置的 replicas 参数对 device plugin 感知到的设备进行复制,并在 DeviceID 使用特定格式进行标记便于区分。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。