嵌套虚拟化的 vGPU
没有人知道我为什么想在物理机上拖一个虚拟机,再把显卡直通到虚拟机里面去开 vGPU。
先介绍一下我手头的硬件。今天的主角是一块服务器造型的计算卡 P100 12G。这卡没有风扇,也就只适合装到服务器上吹着用。然后用的服务器是前段时间从咸鱼买来的 H3C 的 2U 机器(此处应有图,但是懒得去机房拆机拍照了,凑合脑补吧)。
我的物理机上安装的是 Proxmox VE 8,内核是 6.2.16-3-pve。由于内核比较新,现在的 NVIDIA Grid 驱动安装上去会编译不通过,看了看报错,大概是 API 改了之类的。我不太懂 Linux 内核驱动的开发,也没有兴趣帮 NVIDIA 打补丁,更不希望降低我的系统内核。那要怎么做呢?一个比较容易想到的办法就是——在物理机(L0)里开一台内核版本比较低的虚拟机(L1),然后再把显卡通过 PCIe 直通挂进去;之后再在 L1 虚拟机里去做 vGPU 切分,由于内核使用的是 L1 虚拟机里的内核,这样 NVIDIA 的驱动就能够顺利地跑起来。在跑起来之后,我们再在 L1 虚拟机里用 KVM 启动 L2 的虚拟机,并设置其使用 mdev 设备,就可以成功用上了!

在嵌套虚拟化的环境中,我们通常使用 Lx 来标识不同的虚拟化级别。这里 L0 代表物理机 —— 安装了 Proxmox VE 8 的那台;之后的 L1 则代表在此上运行的虚拟机;而 L2 则代表在 L1 上运行的虚拟机。
配置物理机 (L0)
物理机配置其实没啥可讲的,嵌套虚拟化和 IOMMU 打开一下就行了。具体的设置过程可以参考 PVE Wiki 的 Nested Virtualization 页面,我这里默认都是打开的,直接就能用了。
验证环境设置:
cat /sys/module/kvm*/parameters/nested # 看到 1 或者 Y 就行;0 或者 N 代表不支持嵌套虚拟化
ls /sys/kernel/iommu_groups/ # 看到一堆数字就行;没有输出代表 IOMMU 没开启
启动 L1 虚拟机
在 PVE 里启动一个虚拟机本身没啥可讲的,我这边在 L1 里启动的是另一个 PVE – PVE 7.4-15,内核版本是 5.15.108-1-pve 。这个版本的内核就可以安装最新的 vGPU 16 的 GRID 驱动了。
启动虚拟机需要注意几点:
- CPU 型号选择 host
- 需要用 Q35 的机器类型
- 需要用 OVMF UEFI 启动
- 避免使用 virtio 系列存储、网卡 —— 根据 QEMU Wiki VT-d 页面的说明,Virtio 半虚拟化需要 IOTLB 的支持,我搞不懂那是啥,所以干脆避免使用;我用的是 SATA 虚拟盘和 e1000e 虚拟网卡
在创建完成虚拟机之后,需要给虚拟机增加虚拟 IOMMU 设备。还是参考前文的页面,执行:
qm set <VMID> --args '-device intel-iommu,intremap=on,caching-mode=on -machine accel=kvm,kernel-irqchip=split'
这样就可以添加上对应的设备。在 GitHub 上我也发现了一些指导,例如 bashtheshell/IOMMU-nested-pve 会说这个地方需要整个修改参数把 intel-iommu 设备加到最前面,我这边倒是没遇到这个问题,直接用 qm 把额外参数补上就行了。
然后就像普通的显卡直通一样,在硬件那边添加一个新的 PCIe 设备,选择你的显卡就行。全部配完大概是这样:

设置完就可以开机装系统了。
配置 L1 虚拟机
装完 L1 之后,因为还需要在 L1 里开虚拟化和 vGPU 嘛,所以这个机器也要做对应的设置,启用 KVM, IOMMU 才行。执行以下命令可以检查:
ls /dev/kvm # 有 KVM 设备说明嵌套虚拟化启动成功了
ls /sys/kernel/iommu_groups/ # 看到一堆数字代表 IOMMU 启动成功了
不知道是因为我用 AMD CPU 还是因为 PVE 7 比较蠢,虽然 KVM 是有的,但是 IOMMU 没有自动开启。不过没关系,编辑 /etc/default/grub
文件,在 GRUB_CMDLINE_LINUX_DEFAULT
变量里加入 intel_iommu=on iommu=pt
,然后执行 update-grub
重新生成 grub,再重启就行了。注意因为 vIOMMU 设备就叫 intel-iommu,所以在 L1 虚拟机里,即使你使用的是 AMD CPU,也没有 amd_iommu,只有 intel_iommu。
设置完 IOMMU 就可以正常安装 vGPU 驱动啦,网上有很多教程我就不赘述了,装好依赖,直接运行对应的 .run 文件就行:
apt install pve-headers-$(uname -r) build-essential
./NVIDIA-Linux-x86_64-535.54.06-vgpu-kvm.run
装好之后输入 nvidia-smi
就能看到我们的显卡信息了。
启动 L2 虚拟机
因为我的 L2 也是 PVE,所以这里也很简单,在 PVE 界面里,新建一个 PCIe 设备,右边选择对应的 mdev 型号 —— 一般选 Q 系列的比较好用 —— 就行了。

另外,众所周知,NVIDIA 的 vGPU 显卡需要 License 授权才可以不锁帧使用。我这边使用的是 P100 的 GPU,有一款叫 Quadro GP100 的 GPU 和我是同一个芯片。于是到网上查询,得知 Quadro GP100 的 PCIe ID 是 15F0,Subsystem ID 是 11C3,所以在做 PCIe 直通的时候把 ID 设置上去(NVIDIA 的 Vendor 都是 10DE):

这样 L2 虚拟机就会以为自己用的是 Quadro GP100 的 GPU 了,就可以装普通的 Studio 驱动了。

比较神奇的是,就算设备管理器和 GPU-Z 都显示为 GP100,NVIDIA 自己的驱动却能正常认出 P100-4Q 的真实身份。目前版本(536)的驱动下,运行了12个小时也没发现锁帧问题,应该是搞定了。
嵌套虚拟化的性能
我没有具体评估嵌套虚拟化的性能,但用起来基本能用,感觉性能损失没有特别明显。对于现代的 CPU,虚拟化小于等于两层的情况下,应该还是挺好用的。这里贴一篇红帽博客 2017 年的文章:Inception: How usable are nested KVM guests?,这个文章的作者结论似乎和我的差不多。
总而言之,这么一个 setup 我倒是挺满意的,性能过得去,也有显卡能做基本的图形加速,用来挂 QQ 是再好不过了……