KubeVirt 虚拟机 GPU 直通

Expose NVIDIA GPU to Kubernetes

k8s 默认提供给 Pod 的可使用资源有 Memory, CPU 等，并没有 GPU。 但 K8S 允许我们自己开发 Device Plugin 来暴露 Resource。 大致流程是这样：

按照 Device Plugin 文档中描述的方式去开发，就能在 k8s 中暴露出你要的资源分配给 Pod。

kubevirt-gpu-device-plugin 就是这样一个插件，它以 DaemonSet 方式部署，将 NVIDIA GPU 作为一种可分配资源暴露给 kubelet。 我们在集群中安装这个 nvidia-kubevirt-gpu-dp 之后就可以看到效果，如下 :

Copy
root@node01:~# kubectl describe node node01
Name: node01
Roles: worker
CreationTimestamp: Thu, 24 Nov 2022 15:13:21 +0800
Addresses:
InternalIP: 172.16.33.137
Hostname: node01
Capacity:
cpu: 72
ephemeral-storage: 921300812Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 131564868Ki
nvidia.com/TU106_GEFORCE_RTX_2060_REV__A: 4
pods: 110
Allocatable:
cpu: 71600m
ephemeral-storage: 921300812Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 127462015491
nvidia.com/TU106_GEFORCE_RTX_2060_REV__A: 4
pods: 110
...

上面的输出结果中，Capacity 中描述的是 node01 这个节点有 4 张 nvidia.com/TU106_GEFORCE_RTX_2060_REV__A 显卡，Allocatable 描述的是 node01 这个节点中有 4 张 nvidia.com/TU106_GEFORCE_RTX_2060_REV__A 显卡可用于分配给 Pod。

这个插件的 Pod 运行起来后 ，每个节点的 /var/lib/kubelet/device-plugins/ 目录下都会新增了一个名为 kubevirt-TU106_GEFORCE_RTX_2060_REV__A.sock= 的 socket 文件。

Copy
root@node01:~# ll /var/lib/kubelet/device-plugins/
total 44
drwxr-xr-x 2 root root 4096 Dec 8 19:54 ./
drwx------ 8 root root 4096 Nov 24 15:13 ../
-rw-r--r-- 1 root root 0 Dec 5 09:13 DEPRECATION
-rw------- 1 root root 35839 Dec 8 19:54 kubelet_internal_checkpoint
srwxr-xr-x 1 root root 0 Dec 5 09:13 kubelet.sock=
srwxr-xr-x 1 root root 0 Dec 8 19:54 kubevirt-kvm.sock=
srwxr-xr-x 1 root root 0 Dec 8 19:54 kubevirt-sev.sock=
srwxr-xr-x 1 root root 0 Dec 8 19:52 kubevirt-TU106_GEFORCE_RTX_2060_REV__A.sock=
srwxr-xr-x 1 root root 0 Dec 8 19:54 kubevirt-tun.sock=
srwxr-xr-x 1 root root 0 Dec 8 19:54 kubevirt-vhost-net.sock=

kubelet 通过与该 socket 进行通讯，来达到设备插件的注册、分配资源等操作。这样一来我们创建 Pod 就可以在 resource request/limit 中申请 nvidia.com/TU106_GEFORCE_RTX_2060_REV__A 这个资源了。

注 1 : extend resource 的资源用量只能是整数，不过目前 nvidia-kubevirt-gpu-dp 没有提供类似 1000m = 1 core, 500m = 0.5 core 这样的效果。当下 "1" 就表示 1 张显卡。

注 2：在 nvidia-kubevirt-gpu-dp 部署之前我们需要给集群节点的 grub 启用 iommu 等操作，这样才能拥有 GPU 直通 guest 的能力。

注 3：剩余的 kubevirt-kvm.sock 等文件是 virt-handler 中的 device plugin 创建的。

KubeVirt VM use GPU

创建 vm 如下，我们指定了使用 nvidia.com/TU106_GEFORCE_RTX_2060_REV__A 显卡：

Copy
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: vm-with-gpu
namespace: cxl
spec:
runStrategy: RerunOnFailure
template:
spec:
domain:
cpu:
cores: 1
devices:
disks:
# ...
gpus:
- deviceName: nvidia.com/TU106_GEFORCE_RTX_2060_REV__A
name: gpu0
machine:
type: q35
resources:
requests:
memory: 1Gi
networks:
# ...
volumes:
# ...

virt-controller 监听到 vmi 的 CREATE 后会为其构造出对应的 Pod (virt-launcher)，源码部分见 传送门。 最终构造出来的 virt launcher Pod yaml 就是这样：

Copy
apiVersion: v1
kind: Pod
metadata:
labels:
kubevirt.io: virt-launcher
vm.kubevirt.io/name: vm-with-gpu
name: virt-launcher-vm-with-gpu-vvl4q
namespace: cxl
spec:
automountServiceAccountToken: false
containers:
- command:
- ...
image: quay.io/kubevirt/virt-launcher:v0.54.0
imagePullPolicy: IfNotPresent
name: compute
resources:
limits:
devices.kubevirt.io/kvm: "1"
devices.kubevirt.io/tun: "1"
devices.kubevirt.io/vhost-net: "1"
nvidia.com/TU106_GEFORCE_RTX_2060_REV__A: "1"
requests:
devices.kubevirt.io/kvm: "1"
devices.kubevirt.io/tun: "1"
devices.kubevirt.io/vhost-net: "1"
cpu: 100m
ephemeral-storage: 50M
memory: 2262Mi
nvidia.com/TU106_GEFORCE_RTX_2060_REV__A: "1"
...

假设 vm 被分配到节点 node05, node05 有 4 张 nvidia.com/TU106_GEFORCE_RTX_2060_REV__A 显卡，那么 kubevirt 会分配哪一张显卡给 vm 呢？

vm pod 里的容器启动时 kubelet 会调用 nvidia-kubevirt-gpu-dp 的 Allocate() 接口，这个接口需要在 response 数据中的 env 中把分配的 GPU 设备地址写上，最终会体现在容器的环境变量中。 如下 ：

Copy
root@node01:~# kubectl exec -it virt-launcher-vm-with-gpu-vvl4q -n cxl -- bash
bash-4.4# env
PCI_RESOURCE_NVIDIA_COM_TU106_GEFORCE_RTX_2060_REV__A=0000:86:00.0

如果申请了多张同型号显卡，那么环境变量中的地址以逗号分隔 :

Copy
root@node01:~# kubectl exec -it virt-launcher-vm-with-gpu-vvl4q -n cxl -- bash
bash-4.4# env
PCI_RESOURCE_NVIDIA_COM_TU106_GEFORCE_RTX_2060_REV__A=0000:86:00.0,0000:af:00.0

也就是说 GPU 由 device plugin 的 Allocate 接口逻辑来分配。

在 virt-launcher Pod 运行起来后会去构造出 libvirt domain xml ，此时就会读取 Pod env 中的该环境变量，将其构造成对应的 xml 元素，如下 ：

Copy
<hostdev mode='subsystem' type='pci' managed='yes'>




<alias name='ua-gpu0'>
</hostdev>

这样一来，libvirt 就能直接使用 host 的 pci device (也就是显卡) 了。

KubeVirt permitted host devices

默认情况下所有 host dev 都可以被虚拟机使用， 但 kubevirt 允许在 kubevirt cr 中配置哪些 host device 可以被 vm 使用，如下：

Copy
...
configuration:
permittedHostDevices:
pciHostDevices:
- pciVendorSelector: "10DE:1F08"
resourceName: "nvidia.com/GeForce RTX 2060 Rev. A"
externalResourceProvider: true
- pciVendorSelector: "8086:6F54"
resourceName: "intel.com/qat"
mediatedDevices:
- mdevNameSelector: "GRID T4-1Q"
resourceName: "nvidia.com/GRID_T4-1Q"

如果你做了如上配置，那么你要分配给虚拟机的 GPU 必须符合上述配置的 Selector 规则。

pciVendorSelector 由 vendorID (厂商) 和 productID (产品) 组成，其中 10DE 表示 Nvidia ， 1F08 表示 GeForce RTX 2060 Rev. A 这款产品 (详见: https://pci-ids.ucw.cz/read/PC/10de )。 也就是说这个 host dev 是被 kubevirt 允许用于虚拟机的。

对于该型号的 GPU，externalResourceProvider: true 表示让外部 device plugin (也就是 nvidia-kubevirt-gpu-dp) 接管该设备。 如果你指明了该设备的 externalResourceProvider: false ，那么意味着将由 virt-handler 中的 device plugin manager 来接管该设备，它会为此设备启动一个 device plugin。