GPU在容器云中的方案及使用

问题根源需求

在使用容器云调度资源场景下,我们可以请求并限制特定配额的CPU及内存供于容器创建使用,K8S调度器会将Pod绑定到资源合适的节点上;但对于现实使用场景原生资源的调度能力仍然不能满足现有的用户,其他特定资源例如GPU、IB卡、硬加密狗等也是迫切需要的,用户希望特定资源也可以被调度工具发现、监管、分配并最终使用。

GPU卡作为重要的计算资源不管是在算法训练还是预测上都不可或缺,而对于常见的算法训练平台或智能业务平台都有往容器迁移演进的趋势,此时如何更好的利用GPU资源成了容器云平台需要解决的问题。

所以可以看出需要解决的问题主要是一下三个方面:

1.资源管理:调度器可以发现并调度GPU资源;

2.资源限制隔离:运行在同一张卡上的GPU资源可以限制在配额之内;

3.资源算力损耗较少:同等算力的GPU资源计算能力不出现明显衰减;

解决方案

目前K8S官方对于如何共享单张GPU资源没有很好的解决方案,而对于使用多张GPU也停留在整张GPU卡作为调度颗粒度方式,应付复杂的使用场景譬如集群内存在多种GPU类型时仍有不足。处理复杂使用场景的GPU调度方案需要将资源多维度的标注,针对这种需求可以做如下处理:

1.以显存为单位上报资源并调度使用,用于应对共享单张GPU的场景;

2.以卡为单位上报资源并调度使用,用于应对多卡加速计算场景;

3.以类型为标签标注节点,尽量在同一节点安装相同GPU;

同时用户对于使用特型资源都不太愿意修改K8S原生代码,我们可以利用K8S现有的机制来避免对主干代码的侵入,如下:

1.Extended Resource机制:用于定义GPU资源;

2.Scheduler Extender机制:用于对GPU资源进行调度;

3.Device Plugin机制:用于上报、监管和分配GPU资源;

方案设计迭代

计划分三步迭代:

一、初步卡共享可行

已实现以显存/卡作为调度颗粒的资源上报使用,用户可以在该方案下在单张GPU上运行多个Pod和将多卡同时供单个Pod使用。

二、多类型GPU集群调度

需要支持多类型GPU调度使用,根据请求类型调度到特定节点上运行;同一节点上显存资源及卡资源联动,避免被以显存分配的卡重分配给卡单位的调度请求。

完成以上两部迭代后,主要架构如下:

三、GPU卡内配额限制

对于共享单张卡都无可避免的会出现计算效率下降的问题,此时可以利用Nvida官方提供的MPS接口,开启该功能可以运行多个进程在GPU上叠加提供利用率,减少了GPU上下文存储与切换,降低了调度带来的开销。需要注意的是容器内使用MPS需要GPU架构高于volta,runc默认为nvidia。对于配额的限制前期还是建议利用应用程序自己的机制来实现。在容器环境中开启MPS功能将根据一下来实现:

需要注意的时Nvidia在Volta 架构引入了新的 MPS 功能。与 Volta GPU 前的 MPS 相比,Volta MPS 提供了一些关键改进:

  • Volta MPS 客户端直接向 GPU 提交工作,而无需通过 MPS 服务器。
  • 每个 Volta MPS 客户端都拥有自己的 GPU 地址空间,而不是与所有其他 MPS 客户端共享 GPU 地址空间。
  • Volta MPS 支持为服务质量 (QoS) 提供有限的执行资源资源。

Volta前架构与Volta机构GPU使用MPS对比:

GPU在K8S中使用的全流程演示

要求:

1.节点有GPU资源

2.docker >=1.12

3.K8S >=1.10

一、安装Nvidia驱动及CUDA

驱动是应用使用GPU资源的前提

按照自己的需求安装特定版本的驱动(需要>=384.81),例如在ubuntu1604上安装CUDA10.2的驱动可以参考一下:

$ wget http://developer.download.nvidia.com/compute/cuda/10.2/Prod/local_installers/cuda_10.2.89_440.33.01_linux.run
$ sudo sh cuda_10.2.89_440.33.01_linux.run

安装完成后,可以用一下命令确认:

$ nvidia-smi

二、部署安装nvidia-docker2

NVIDIA 容器工具包允许用户构建和运行 GPU 加速 Docker 容器。该工具包包括一个容器运行时和实用程序,用于自动配置容器以利用 NVIDIA GPU。

可以参考一下安装:

distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

安装完成后验证是否已成功可以使用:

docker run --gpus all nvidia/cuda:9.0-base nvidia-smi

三、切换runtime为nvidia-container-runtime

通过修改Docker的Runtime为nvidia runtime工作,当我们执行 nvidia-docker create 或者 nvidia-docker run 时,它会默认加上 --runtime=nvidia 参数。将runtime指定为nvidia。为了方便使用,可以直接修改Docker daemon 的启动参数,修改默认的 Runtime为:

cat /etc/docker/daemon.json
{
    "default-runtime": "nvidia",
    "runtimes": {
        "nvidia": {
            "path": "/usr/bin/nvidia-container-runtime",
            "runtimeArgs": []
        }
    }
}

然后重启docker

四、部署GPU-Scheduler及开启K8S相关功能

使用GPU-Scheduler需要更改原生调度启动参数,利用K8S的扩展机制,在全局调度器筛选绑定的时候查找某个节点的特定GPU卡是否能够提供足够的显存,并且在绑定时将GPU分配结果通过annotation记录到Pod Spec以供后续检查分配结果。添加以下:

- --policy-config-file=/etc/kubernetes/scheduler-policy-config.json

scheduler-policy-config.json的具体内容为:

{
  "kind": "Policy",
  "apiVersion": "v1",
  "extenders": [
    {
      "urlPrefix": "http://127.0.0.1:32766/gpushare-scheduler",
      "filterVerb": "filter",
      "bindVerb":   "bind",
      "enableHttps": false,
      "nodeCacheCapable": true,
      "managedResources": [
        {
          "name": "aliyun.com/gpu-mem",
          "ignoredByScheduler": false
        }
      ],
      "ignorable": false
    }
  ]
}

待调度组件正常启动后,再部署GPU-Scheduler:

---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: gpushare-schd-extender
rules:
- apiGroups:
  - ""
  resources:
  - nodes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - update
  - patch
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - bindings
  - pods/binding
  verbs:
  - create
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gpushare-schd-extender
  namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: gpushare-schd-extender
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gpushare-schd-extender
subjects:
- kind: ServiceAccount
  name: gpushare-schd-extender
  namespace: kube-system

# deployment yaml
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: gpushare-schd-extender
  namespace: kube-system
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: gpushare
        component: gpushare-schd-extender
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ''
    spec:
      hostNetwork: true
      tolerations:
      - effect: NoSchedule
        operator: Exists
        key: node-role.kubernetes.io/master
      - effect: NoSchedule
        operator: Exists
        key: node.cloudprovider.kubernetes.io/uninitialized
      serviceAccount: gpushare-schd-extender
      containers:
        - name: gpushare-schd-extender
          image: registry.cn-hangzhou.aliyuncs.com/acs/k8s-gpushare-schd-extender:1.11-d170d8a
          env:
          - name: LOG_LEVEL
            value: debug
          - name: PORT
            value: "12345"

# service.yaml
---
apiVersion: v1
kind: Service
metadata:
  name: gpushare-schd-extender
  namespace: kube-system
  labels:
    app: gpushare
    component: gpushare-schd-extender
spec:
  type: NodePort
  ports:
  - port: 12345
    name: http
    targetPort: 12345
    nodePort: 32766
  selector:
    # select app=ingress-nginx pods
    app: gpushare
    component: gpushare-schd-extender

五、简单部署GPUshare-device-plugin确认可行

GPUshare-device-plugi是开源的GPU资源上报组件,利用Device Plugin机制,由Kubelet负责调度GPU卡分配,依据GPU-Scheduler分配结果执行。可以用来简单的部署测试下是否已经可使用:

kind: DaemonSet
metadata:
  name: gpushare-device-plugin-ds
  namespace: kube-system
spec:
  template:
    metadata:
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ""
      labels:
        component: gpushare-device-plugin
        app: gpushare
        name: gpushare-device-plugin-ds
    spec:
      serviceAccount: gpushare-device-plugin
      hostNetwork: true
      nodeSelector:
        gpushare: "true"
      containers:
      - image: registry.cn-hangzhou.aliyuncs.com/acs/k8s-gpushare-plugin:v2-1.11-aff8a23
        name: gpushare
        command:
          - gpushare-device-plugin-v2
          - -logtostderr
          - --v=5
          - --memory-unit=GiB
          - --mps=true
        resources:
          limits:
            memory: "300Mi"
            cpu: "1"
          requests:
            memory: "300Mi"
            cpu: "1"
        env:
        - name: KUBECONFIG
          value: /etc/kubernetes/kubelet.conf
        - name: NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]
        volumeMounts:
          - name: device-plugin
            mountPath: /var/lib/kubelet/device-plugins
      volumes:
        - name: device-plugin
          hostPath:
            path: /var/lib/kubelet/device-plugins
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: gpushare-device-plugin
rules:
- apiGroups:
  - ""
  resources:
  - nodes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - update
  - patch
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - nodes/status
  verbs:
  - patch
  - update
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gpushare-device-plugin
  namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: gpushare-device-plugin
  namespace: kube-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gpushare-device-plugin
subjects:
- kind: ServiceAccount
  name: gpushare-device-plugin
  namespace: kube-system
$ kubectl create -f gpu.yml

使用kubectl命令确认节点内已经有GPU资源:aliyun.com/gpu-mem 后就可以尝试部署一个服务看是否正常:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-1
  labels:
    app: gpu-1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gpu-1
  template:
    metadata:
      labels:
        app: gpu-1

    spec:
      containers:
      - name: gpu-1
        image: bvlc/caffe:gpu
        command: ["/bin/sh"]
        args: ["-c","while true;do echo hello;sleep 1;done"]
        resources:
          limits:
            # GiB
            aliyun.com/gpu-mem: 1

如果Pod调度成功且可以在容器内正常调用GPU说明已经可以使用。

总结

方案在已成功验证步骤一情况下,结合现有资料说明发展路径是可行,后续的步骤是具体实现上的精力花费。

参考: