qGPU云原生最佳实践

概述

业界AI应用中,GPU的使用逐渐增加,业界普遍存在以下两大问题:

  1. GPU算力显存利用低:直通 GPU 无法多业务共享资源;vGPU 实例资源配置固定不灵活、虚拟机实例调度成本高,非进程级调度。
  2. 显存/算力/故障资源隔离性差:不同客户、任务之间存在资源的抢占和干扰

腾讯云qGPU提供的GPU共享能力,支持在多个容器间共享 GPU 卡并提供容器间显存、算力强隔离能力,在使用中以更小的粒度进行调度。在保证业务稳定的前提下,为云上用户控制资源成本,提高运行效率提供帮助。

本实践基于腾讯云TKE实现,适用于各AI训练及推理应用场景。

常见AI场景架构

文章分为两部分,第一部分为qGPU云原生化安装,提供全量qGPU和混用nvidia+qGPU两种不同的安装方式,以供实际场景选用。第二部分为qGPU能力验证,分别从调度、隔离和在离线混布三个方面,提供操作用例。


qGPU云原生环境安装

1. 版本安装

本次实践环境,采用腾讯云TKE,其中

  • TKE 版本:v1.14+
  • 节点型号:GN7.2XLARGE32 * 2
  • 节点OS:TencentOS 3.1(tk4),CentOS 7/8,ubuntu 18/20
  • GPU型号:T4 / V100 / A10 / A30 / A100
  • GPU驱动版本:450或470

2. 模式A:全量部署qGPU节点

创建TKE集群,开启【qGPU共享】选项

  • TKE集群创建,参考:https://cloud.tencent.com/document/product/457/32189
  • 部署qGPU,参考:容器服务 使用 qGPU -TKE 标准集群指南-文档中心-腾讯云
开通TKE时,开启【qGPU共享】选项

创建节点,购买GPU机型

注:在验证还原时,根据用例按需配置节点,参见【测试用例】-【环境准备】

在TKE集群入口新建节点,购买节点过程中可以选择

  • 公共镜像,tencentos 3.1(tk4),centos 7/8, ubuntu 18.04/20.04;其中安装GPU驱动版本(450/470)
公共镜像GPU驱动安装
  • 市场镜像,选择标识为“混部”的机器,其OS里已经安装GPU驱动
市场镜像版本选择
  • 查看每台机器可分配资源
代码语言:javascript
复制
kubectl describe node x.x.x.x
  • 若qGPU配置正确,可看到如下输出
Node资源资源显示

3. 模式B:混合部署qGPU及Nvidia节点

创建TKE集群

不要开启【qGPU共享】,否则无法混合开通节点,(开启后,后面开启的任何节点,都是qGPU节点)

开通TKE时,不要开启【qGPU共享】选项

开通Nvidia方案节点

选择【公共镜像】,可选centos7.8操作系统,安装470 GPU驱动

选择CentOS 7.8操作系统
选择470 GPU驱动

节点打标签

  • 给节点打上标签,方便调度任务时,通过nodeSelector选择不同节点类型
代码语言:javascript
复制
kubectl label node x.x.x.x gputype=nvidia
  • 或者在开通节点时,通过控制台操作
控制台节点打标

安装qGPU插件

通过TKE控制台,添加【qGPU组件】插件

TKE组件管理
安装qGPU组件

开通qGPU方案节点

选择【市场镜像】 —— 选择标识为“混部”的机器,OS里已经安装GPU驱动,无需重复安装

注意:在这种混用模式下,qGPU只能用【市场镜像】,【公共镜像】都是Nvidia方案节点

选择Tencent OS 3.2操作系统

给节点打上标签,方便调度任务时,通过nodeSelector进行选择

代码语言:javascript
复制
kubectl label node x.x.x.x gputype=qgpu

查看nvidia和qGPU机器可分配资源

代码语言:javascript
复制
kubectl describe node x.x.x.x
Nvidia Node资源资源显示
qGPU Node资源资源显示

qGPU能力实践

1. 实践目标

操作过程对qGPU的以下四个能力,进行方案实践

  • 虚拟化:单节点同时运行多个任务
  • 隔离性:显存和算力是否符合设置预期
  • 抢占性:是否能抢占,抢占的同时能否对配置资源进行保底
  • 在离线混布:高、低优先级任务切换

2. 前置操作

前置操作

为避免数据抖动,操作时可先锁定GPU频率,根据显卡型号在GPU节点执行如下命令:

显卡型号

操作命令

T4

nvidia-smi --lock-gpu-clocks=1590

V100

nvidia-smi --lock-gpu-clocks=1530

A100

nvidia-smi --lock-gpu-clocks=1410

A10

nvidia-smi --lock-gpu-clocks=1695

准备镜像

实践基于Tensorflow CNN Benchmark进行模拟仿真,程序根据卷积神经网络的多个图像分类模型,进行图像数据的分类训练。使用如下方式创建镜像,并推送镜像仓库TCR

代码语言:javascript
复制
mkdir -p /tmp/qgpu
cd /tmp/qgpu

git clone https://github.com/tensorflow/benchmarks.git

cat <<'EOF' > start.sh
#! /bin/bash
python3 /benchmarks/scripts/tf_cnn_benchmarks/tf_cnn_benchmarks.py --forward_only=True --model=resnet50 --num_batches="$@" --allow_growth --batch_size=16
EOF

chmod +x start.sh

cat <<EOF > Dockerfile
FROM nvcr.io/nvidia/tensorflow:21.08-tf1-py3
ADD benchmarks /benchmarks
ADD start.sh /start.sh
EOF

docker build -t qgpu-tf-test:21.08-tf1-py3 .

3. 场景1 - Nvidia节点调度能力实践

步骤1:

  • 部署TKE集群,包含一个单GPU卡的Nvidia节点
  • 预期结果:只有一个Job可以获取到GPU资源,正常启动;另外一个Job无法获取到GPU资源,无法启动

步骤2:

  • 查看节点 GPU 算力 / 显存
代码语言:javascript
复制
kubectl describe node <node-name>
  • 预期结果:在Allocatable:部分,可以看到nvidia-gpu资源,类似如下信息:
代码语言:javascript
复制
Allocatable: nvidia.com/gpu:  1

步骤3:

  • 创建两个GPU负载(部署在Nvidia节点上),查看Pod运行状态
代码语言:javascript
复制
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene1-job1
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: nvidia
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
nvidia.com/gpu: 1
EOF
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene1-job2
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: nvidia
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
nvidia.com/gpu: 1
  • 预期结果:kubectl get pod仅可以查询到一个pod,scene1-job1或scene1-job2,取决于调度顺序

4. 场景2 - qGPU节点调度能力实践

步骤1:

  • 部署TKE集群,部署2个qGPU节点:其中一个节点包含1张卡,一个节点包含2张卡
  • 预期结果:两个Job可以同时启动,在保证各5G显存隔离的前提下,抢占剩余显卡资源

步骤2:

  • 查看节点 GPU 算力 / 显存
代码语言:javascript
复制
kubectl describe node <node-name>
  • 预期结果:在Allocatable:部分,可以看到qgpu-core及qgpu-memory资源,类似如下信息:
代码语言:javascript
复制
Allocatable: tke.cloud.tencent.com/qgpu-core: 100
tke.cloud.tencent.com/qgpu-memory: 14

步骤3:

  • 创建GPU负载(部署在qGPU节点上),查看Pod运行状态
代码语言:javascript
复制
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene2-job1
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-memory: "5"
tke.cloud.tencent.com/qgpu-core: "40"
EOF
  • 预期结果:在pod里执行如下命令,显存显示5120MiB
代码语言:javascript
复制
kubectl exec -it scene2-job1 nvidia-smi

步骤4:

  • 删除现有GPU负载,创建两个GPU负载,通过node selector选择调度到指定Node上(仅1个卡的qGPU节点),查看多容器共享 GPU能力。
代码语言:javascript
复制
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene2-job1
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-memory: "5"
tke.cloud.tencent.com/qgpu-core: "40"
EOF
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene2-job2
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-memory: "5"
tke.cloud.tencent.com/qgpu-core: "30"
EOF
  • 预期结果:
  1. 两个pod运行成功 kubectl get po -o wide,显示scene2-job1和scene2-job2
  2. 在两个pod执行如下命令,显存显示不大于5G
代码语言:javascript
复制
kubectl exec -it scene2-job1 nvidia-smi
kubectl exec -it scene2-job2 nvidia-smi
Nvidia-SMI显示信息

步骤5:

  • 在两个Pod分别执行如下命令,查看
代码语言:javascript
复制
kubectl logs scene2-job1 --tail=20
kubectl logs scene2-job2 --tail=20
  • 预期结果:

YAML中Pod1与Pod2设置不同算力,因默认抢占策略,两个Pod会抢占剩余资源,因运行负载都很重,导致运行结果基本相同。其中Pod1训练日志约为202 images/sec,Pod2训练日志约为198 images/sec。

Best-Effort策略下,Pod1训练日志(202 images/sec)
Best-Effort策略下,Pod2训练日志(198 images/sec)

通过底层CVM节点监控,也可以查看到GPU使用率为100%

GPU节点基础监控

步骤6:

  • 删除现有GPU负载,创建两个GPU负载,通过node selector,选择调度到qGPU的Node上。查看支持 binpack / spread 策略调度能力。
代码语言:javascript
复制
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene2-job1
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-memory: "5"
tke.cloud.tencent.com/qgpu-core: "40"
EOF
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene2-job2
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-memory: "5"
tke.cloud.tencent.com/qgpu-core: "30"
EOF

等待两个Pod进入Running状态后,执行如下命令,并记录输出信息

代码语言:javascript
复制
kubectl exec -it scene2-job1 nvidia-smi
kubectl exec -it scene2-job2 nvidia-smi

删除所有GPU负载,执行如下命令,查找binpack,替换为spread并保存退出

代码语言:javascript
复制
kubectl edit deploy -n kube-system qgpu-scheduler

等待旧的qgpu-scheduler Pod退出,新的qgpu-scheduler Pod进入Running状态。再次提交GPU负载,并执行nvidia-smi命令,再次记录输出信息

  • 预期结果:第一次在两个Pod中执行nvidia-smi时,可以看到输出的类似00000000:00:09.0的显卡bus信息相同,说明在默认binpack调度策略下,两个Pod被调度到同一个显卡上;在修改为spread后,可以看到bus信息不同,说明两个Pod在spread策略下,被调度到同一节点的不同显卡上

步骤7:

  • 删除现有GPU负载,创建一个GPU负载,指定qGPU-core为100。查看GPU 整卡调度策略。
代码语言:javascript
复制
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene2-job1
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-core: "100" #显卡比例
EOF
  • 预期结果:任务显存及算力,为单GPU卡的全部资源

5. 场景3 - qGPU节点隔离能力实践

步骤1:

  • 部署包含一个单GPU卡的qGPU节点
  • 预期结果:两个Job可以同时启动,在保证各5G显存隔离的前提下,按照配置比例严格隔离运行,总体GPU使用在90%左右

步骤2:

  • 登录GPU节点,然后运行如下命令
代码语言:javascript
复制
cat /proc/qgpu/0/policy
  • 预期结果:显示默认隔离策略 best-effort

步骤3:

  • 执行如下命令,设置该GPU节点的隔离策略为fixed-share
代码语言:javascript
复制
kubectl label node <node-name> --overwrite tke.cloud.tencent.com/qgpu-schedule-policy=fixed-share

再次执行以下命令

代码语言:javascript
复制
cat /proc/qgpu/0/policy
  • 预期结果:显示默认隔离策略 fixed-share

步骤4:

  • 创建两个GPU负载
代码语言:javascript
复制
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene3-job1
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-memory: "6"
tke.cloud.tencent.com/qgpu-core: "60"
EOF
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene3-job2
namespace: qgpu-test
spec:
template:
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-memory: "3"
tke.cloud.tencent.com/qgpu-core: "30"
EOF
  • 预期结果:稳定一段时间后,可以在云监控处看到,两个进程总的GPU-Util不超过90%
GPU节点基础监控

步骤5:

  • 在两个Pod分别执行如下命令,查看显存资源使用量
代码语言:javascript
复制
kubectl exec -it scene3-job1 nvidia-smi
kubectl exec -it scene3-job2 nvidia-smi
  • 预期结果:可以看到显存一个不超过6G,一个不超过3G

步骤6:

  • 在两个Pod分别执行如下命令,查看
代码语言:javascript
复制
kubectl logs scene2-job1 --tail=20
kubectl logs scene2-job2 --tail=20
  • 预期结果:YAML中设置Pod1为Pod2的两倍算力及显存,运行结果显示,Pod1约为274 images/sec,Pod2约为129 images/sec,Pod1的吞吐量大致为Pod2的吞吐量2倍,并且保持稳定运行。
Fixed-Share策略下,Pod1训练日志(274 images/sec)
Fixed-Share策略下,Pod2训练日志(129 images/sec)

6. 场景4 - qGPU在离线混布能力实践

在离线环境安装

注意:开启在离线混部后,前面的best和fix隔离策略,都会失效。在离线混部为节点维度。

  • 打开在离线混部
代码语言:javascript
复制
kubectl label node xxxx mixed-qgpu-enable=enable
  • 重启qgpu-manager
代码语言:javascript
复制
kubectl delete pod qgpu-manager-xxx -n kube-system 
  • 重启qgpu-scheduler
代码语言:javascript
复制
kubectl delete pod qgpu-scheduler-xxx -n kube-system 
  • 查看node上的allocatable资源
代码语言:javascript
复制
kubectl describe node xxxx

如下显示即为成功

在离线混布模式下,GPU节点资源显示

在离线任务配置方法

  • 创建低优任务 Pod:通过设置 Pod annotation tke.cloud.tencent.com/app-class: offline 标识一个 Pod 为低优任务。低优 Pod 可以同时设置低优算力与显存:
    • 低优算力:tke.cloud.tencent.com/qgpu-core-greedy,单位为百分比
    • 显存:tke.cloud.tencent.com/qgpu-memory,单位为 GB
代码语言:javascript
复制
apiVersion: v1
kind: Pod
annotations:
tke.cloud.tencent.com/app-class: offline
spec:
containers:
  - name: offline-container
    resources:
     requests:
    tke.cloud.tencent.com/qgpu-core-greedy: xx
        tke.cloud.tencent.com/qgpu-memory: xx
  • 创建高优任务 Pod:通过设置 Pod annotation tke.cloud.tencent.com/app-class: online 标识一个 Pod 为高优任务。在线 Pod 只支持设置显存,算力占用为100%,高优 Pod 不会抢占低优 Pod 的显存资源
代码语言:javascript
复制
apiVersion: v1
kind: Pod
annotations:
tke.cloud.tencent.com/app-class: online
spec:
   containers:
   - name: online-container
     resources:
       requests:
    tke.cloud.tencent.com/qgpu-memory: xx

步骤1:

  • 部署k8s集群,包含一个单GPU卡的节点

步骤2:

  • 创建离线任务
代码语言:javascript
复制
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene4-job1
namespace: qgpu-test
spec:
template:
metadata:
annotations:
tke.cloud.tencent.com/app-class: offline #离线
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "/start.sh", "50000" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-memory: "7" #显存大小
tke.cloud.tencent.com/qgpu-core-greedy: "60" #低优资源
EOF
  • 创建在线任务
代码语言:javascript
复制
cat <<EOF | kubectl create -f -
apiVersion: batch/v1
kind: Job
metadata:
name: scene4-job2
namespace: qgpu-test
spec:
template:
metadata:
annotations:
tke.cloud.tencent.com/app-class: online #在线
spec:
nodeSelector:
gputype: qgpu
restartPolicy: Never
containers:
- name: container1
image: ccr.ccs.tencentyun.com/qcbm/qgpu-tf-test:21.08-tf1-py3
command: [ "bash", "-c", "while true; do /start.sh 2000 && sleep 90;done" ]
resources:
limits:
tke.cloud.tencent.com/qgpu-memory: "7" #显存大小
EOF
  • 预期结果:在线任务执行期间,离线任务暂停执行;在线任务暂停期间,离线任务恢复执行。显存部分以隔离方式分配,不发生抢占

步骤3:

  • 通过以下命令,查看日志,观察图片处理训练吞吐:
代码语言:javascript
复制
kubectl logs -f scene4-job1
kubectl logs -f scene4-job2
  • 预期结果:在线任务运行后,离线任务被暂停,日志暂停打印。在线任务结束后,离线任务继续前面的运行状态,并输出日志;观察在线POD的任务,性能数据接近整卡
在离线程序图片训练日志

步骤4:

  • 执行以下命令:
代码语言:javascript
复制
kubectl exec -it scene4-job1 nvidia-smi
kubectl exec -it scene4-job2 nvidia-smi
  • 预期结果:高优和低优之间,显存是显式静态分割的,不会发生抢占,通过Nividia-SMI命令,可以观察到单Pod的显存资源为7G,和设置中一致
Pod GPU信息