本文主要分享 GPU 共享方案,包括如何安装、配置以及使用,最后通过分析源码了 TImeSlicing 的具体实现。通过配置 TImeSlicing 可以实现 Pod 共享一块物理 GPU,以提升资源利用率。
1.为什么需要 GPU 共享、切分等方案?
开始之前我们先思考一个问题,为什么需要 GPU 共享、切分等方案?
或者说是另外一个问题:明明直接在裸机环境使用,都可以多个进程共享 GPU,怎么到 k8s 环境就不行了。
推荐阅读前面几篇文章:这两篇分享了如何在各个环境中使用 GPU,在 k8s 环境则推荐使用 NVIDIA 提供的 gpu-operator 快速部署环境。
GPU 环境搭建指南:如何在裸机、Docker、K8s 等环境中使用 GPU
GPU 环境搭建指南:使用 GPU Operator 加速 Kubernetes GPU 环境搭建
这两篇则分析了 device-plugin 原理以及在 K8s 中创建一个申请 GPU 的 Pod 后的一些列动作,最终该 Pod 是如何使用到 GPU 的。
Kubernetes教程(二一)---自定义资源支持:K8s Device Plugin 从原理到实现
Kubernetes教程(二二)---在 K8S 中创建 Pod 是如何使用到 GPU 的:device plugin&nvidia-container-toolkit 源码分析
看完之后,大家应该就大致明白了。
资源感知
首先在 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: OnFailure
复制代码 apply 该 yaml 之后,kube-scheduler 在调度该 Pod 时就会将其调度到一个拥有足够 GPU 资源的 Node 上。
同时该 Pod 申请的部分资源也会标记为已使用,不会在分配给其他 Pod。
到这里,问题的答案就已经很明显的。
- 1)device-plugin 感知到节点上的物理 GPU 数量,上报到 kube-apiserver
- 2)kube-scheduler 调度 Pod 时会根据 pod 中的 Request 消耗对应资源
即:Node 上的 GPU 资源被 Pod 申请之后,在 k8s 中就被标记为已消耗了,后续创建的 Pod 会因为资源不够导致无法调度。
实际上:可能 GPU 性能比较好,可以支持多个 Pod 共同使用,但是因为 k8s 中的调度限制导致多个 Pod 无法正常共享。
因此,我们才需要 GPU 共享、切分等方案。
2. 什么是 Time Slicing 方案
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 基本一致
3. Time Slicing Demo
Time Slicing 由于是 NVIDIA 的方案,因此使用起来比较简单,只需要在部署完成 GPU Operator 之后进行配置即可。
首先参考这篇文章完成 GPU Operator 的部署 --> GPU 环境搭建指南:使用 GPU Operator 加速 Kubernetes GPU 环境搭建
然后即可开始配置 TimeSlicing。
整体配置分为以下 3 个步骤:
- 1)创建 TimeSlicing 配置
- 根据官方文档描述,修改了 TimeSlicing 配置之后,device plugin Pod 不会自动重启,因此新的配置不会生效,需要手动重启对应 Pod。
- kubectl rollout restart -n gpu-operator daemonset/nvidia-device-plugin-daemonset
复制代码
- 2)修改集群策略开启 Time Slicing,并指定让 device-plugin 使用第一步中创建的配置
- 3)(可选)给要使用 GPU TimeSlicing 的节点打上对应 label,实现不同 Node 使用不同策略
- 比如不同节点上的 GPU 不同,那么可以根据 GPU 的算力或者内存情况设置不同的副本数以合理利用资源
- 如果都是统一 GPU,则使用集群级别的统一配置即可
配置开启 TimeSlicing
创建 TimeSlicing 配置
使用一个单独的 Configmap 来存放 TimeSlicing 的配置。
这里使用集群级别的统一配置,配置文件 time-slicing-config-all.yaml 完整内容如下:- 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.: 配置的名字,可以为不同 Node 设置单独配置,后续通过名称引用对应配置。
- 后续开启 TimeSlicing 时则根据 key 指定使用不同配置
- 这里我们使用集群统一配置,因此创建一个 key 即可
- flags.migStrategy:配置开启时间片之后如何处理 MIG 设备,默认为 none
- renameByDefault:是否对 GPU 资源改名。
- 设置为 true 之后,会使用.shared 替代原本的 。例如 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 调度节点,来控制是否使用共享的 GPU
- 推荐使用 fasle 即可
- failRequestsGreaterThanOne:开启后,当 Pod 请求 1 个以上的 shared GPU 时直接报错 UnexpectedAdmissionError。这个字段是通过报错的方式告诉使用者,请求多个 shared GPU 并不会增加 Pod 对该共享 GPU 的占用时间。
- resources.name:要通过时间分片提供访问的资源类似,比如nvidia.com/gpu
- resources.replicas:可共享访问的资源数量,比如这里指定的 4 也就是 1 个该类型的 GPU 可以供 4 个 Pod 共享访问,也就是最终 Pod 上看到的 GPU 数量是物理 GPU 数量的 4 倍。
将配置 Apply 到 gpu-operator 所在的 namespace- kubectl create -n gpu-operator -f time-slicing-config-all.yaml
复制代码 修改集群策略
修改clusterpolicies.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"}}}}'
复制代码
- 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
复制代码 正常结果如下- ...
- 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
根据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: "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 级别的单独配置
前面只创建了一个名称为 any 的配置,并在 clusterpolicy 中指明了使用该配置为默认配置,因此集群中的全部节点都会使用该配置来做时间片。
但是可能集群中不同节点上的 GPU 型号不同,因此需要共享分副本数可以调整,性能好的副本数就调大一点,性能差的就小一点。
本章主要记录怎么为不同的节点使用不同的配置。
实际上是为不同的 GPU 准备不同的配置。
创建时间片配置
同样的创建 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 即可
- 然后根据 MIG 实例分别指定不同的 replicas 数
- 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,这次与之前的区别在于,这里没有指定 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 则会根据 node 上的 label (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-t4
复制代码 一般都是以 GPU 型号命名,然后给使用该 GPU 的节点都打上对应 label,这样便于查看。
5. 关闭 TimeSlicing
想关闭 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
复制代码 不出意外的话就关掉了,再次查看 Pod 信息,GPU 就变成了物理 GPU 数量,说明关闭成功。- 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 是怎么实现的。
首先是 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
复制代码 这里我们关注 resources 中的 replicas 参数,正是这个参数定义了 **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 循环,对 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 使用特定格式进行标记便于区分。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |