张天昀的个人博客

Nvidia RTX显卡虚拟机直通指南

2021年04月11日

通过将Nvidia显卡直通给虚拟机,可以让虚拟机独占显卡,在虚拟环境中使用CUDA、SHIELD等N卡专属的功能。

可能存在的实际用途:

  1. 云工作(Adobe全家桶)
  2. 云游戏(Nvidia SHIELD)
  3. 挖矿

硬件需求

  • 支持IOMMU、KVM的CPU
  • 一张支持UEFI固件的显卡(GTX/RTX/Radeon等)
  • 至少另外一张额外显卡(核显、亮机卡都可以)

本文使用的配置如下:

  • NUC11PHKi7C
    • Intel i7-1165G7 (4C8T)
    • RTX 2060 Mobile Max-P (6G)
  • 2x 16G DDR4 3200MHz内存
  • 一大堆存储空间
  • 宿主机操作系统使用Ubuntu Server 20.04 LTS

0. 安装虚拟化程序

$ apt upgrade # 确保内核是最新的
$ apt install qemu-kvm libvirt-daemon-system ovmf

1. 获取显卡设备ID

lspci列举PCI设备,其中GTX显卡通常由两个设备(VGA和Audio)组成,RTX显卡由四个设备组成

$ lspci -nn
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation TU106 [GeForce RTX 2060] [10de:1f15] (rev a1)
01:00.1 Audio device [0403]: NVIDIA Corporation TU106 High Definition Audio Controller [10de:10f9] (rev a1)
01:00.2 USB controller [0c03]: NVIDIA Corporation TU106 USB 3.1 Host Controller [10de:1ada] (rev a1)
01:00.3 Serial bus controller [0c80]: NVIDIA Corporation TU106 USB Type-C UCSI Controller [10de:1adb] (rev a1)

通过以下脚本获取IOMMU分组:

#!/bin/bash
shopt -s nullglob
for d in /sys/kernel/iommu_groups/*/devices/*; do
    n=${d#*/iommu_groups/*}; n=${n%%/*}
    printf 'IOMMU Group %s ' "$n"
    lspci -nns "${d##*/}"
done;
$ bash iommu.sh
IOMMU Group 17 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation TU106 [GeForce RTX 2060] [10de:1f15] (rev a1)
IOMMU Group 17 01:00.1 Audio device [0403]: NVIDIA Corporation TU106 High Definition Audio Controller [10de:10f9] (rev a1)
IOMMU Group 17 01:00.2 USB controller [0c03]: NVIDIA Corporation TU106 USB 3.1 Host Controller [10de:1ada] (rev a1)
IOMMU Group 17 01:00.3 Serial bus controller [0c80]: NVIDIA Corporation TU106 USB Type-C UCSI Controller [10de:1adb] (rev a1)

确认同一张显卡的所有组成部分在同一个IOMMU分组中,如果这个分组还有其他设备,需要将同一组中的其他的设备一起直通到虚拟机中。RTX显卡在第17组中,找到所有的设备ID:

10de:1f15,10de:10f9,10de:1ada,10de:1adb

2. 修改启动参数和内核模块

修改/etc/default/grub,在GRUB_CMDLINE_LINUX_DEFAULT中添加内核启动参数

  • Intel CPU添加intel_iommu=on
  • AMD CPU添加amd_iommu=on iommu=pt

然后在后面继续添加以下参数:

  • vfio-pci.ids=10de:1f15,10de:10f9,10de:1ada,10de:1adb(ID修改为自己的硬件ID)
  • kvm.ignore_msrs=1(否则会panic)
  • video=efifb:off

(可选)修改/etc/modprobe.d/blacklist,将nouveau、nvidia等显卡驱动全部拉入黑名单

保存文件,更新GRUB配置:

$ update-grub
Sourcing file '/etc/default/grub'
Sourcing file '/etc/default/grub.d/init-select.cfg'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-5.8.0-48-generic
Found initrd image: /boot/initrd.img-5.8.0-48-generic
Adding boot menu entry for UEFI Firmware Settings
done

重启,启动后检查显卡所有对应的PCI设备都需要被vfio驱动占有(比如说上面IOMMU分组有4个设备,那么这4个设备都要被vfio占有而不能被宿主机的驱动占有):

$ lspci -nnv
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation TU106
        [GeForce RTX 2060] [10de:1f15] (rev a1) (prog-if 00 [VGA controller])
        Subsystem: Intel Corporation Device [8086:2090]
        Flags: bus master, fast devsel, latency 0, IRQ 202
        Memory at 6b000000 (32-bit, non-prefetchable) [size=16M]
        Memory at 6040000000 (64-bit, prefetchable) [size=256M]
        Memory at 6050000000 (64-bit, prefetchable) [size=32M]
        I/O ports at 3000 [size=128]
        Expansion ROM at 6c000000 [disabled] [size=512K]
        Capabilities: <access denied>
        Kernel driver in use: vfio-pci
        Kernel modules: nvidiafb, nouveau

如果Kernel driver in use显示的是vfio-pci则可以进入下一步,否则(如nouveaunvidia*)则表示设备被显卡驱动占有,需要继续修改参数将显卡驱动拉入黑名单或强制vfio比显卡驱动先加载。

2. 配置虚拟机

2.1 安装虚拟机

使用libvirt或virt-manager创建虚拟机,使用VNC连接并安装Windows:

  • 机器类型选择Q35
  • 引导类型必须是UEFI(不能用seabios)
  • 其他随意

安装完Windows后顺便装一下virtio驱动,之后关闭虚拟机再插入PCI设备

2.2 插入PCI设备

第一步:找到显卡对应的PCI设备并打印其信息:

$ virsh nodedev-list --cap pci
pci_0000_01_00_0
pci_0000_01_00_1
pci_0000_01_00_2
pci_0000_01_00_3
$ sudo virsh nodedev-dumpxml pci_0000_01_00_0
<device>
  <name>pci_0000_01_00_0</name>
  <path>/sys/devices/pci0000:00/0000:00:06.0/0000:01:00.0</path>
  <parent>pci_0000_00_06_0</parent>
  <driver>
    <name>vfio-pci</name>
  </driver>
  <capability type='pci'>
    <class>0x030000</class>
    <domain>0</domain>
    <bus>1</bus>
    <slot>0</slot>
    <function>0</function>
    <product id='0x1f15'>TU106 [GeForce RTX 2060]</product>
    <vendor id='0x10de'>NVIDIA Corporation</vendor>
    <iommuGroup number='17'>
      <address domain='0x0000' bus='0x01' slot='0x00' function='0x2'/>
      <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
      <address domain='0x0000' bus='0x01' slot='0x00' function='0x3'/>
      <address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/>
    </iommuGroup>
    <pci-express>
      <link validity='cap' port='0' speed='8' width='16'/>
      <link validity='sta' speed='8' width='4'/>
    </pci-express>
  </capability>
</device>

记录输出中IOMMU分组的domain、bus、slot、function四个信息,将对应的数字转换成十进制数字,填入虚拟机配置中:

$ virsh edit windows # 修改为对应的虚拟机名称
<domain>
  <devices> # 在devices中添加以下内容,bus=0x05为自己选择的空闲PCI槽,与物理机插显卡的分布一致
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <driver name='vfio'/>
      <source>
        <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
      </source>
      <address type='pci' domain='0x0000' bus='0x05' slot='0x00' function='0x0' multifunction='on'/>
    </hostdev>
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <driver name='vfio'/>
      <source>
        <address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/>
      </source>
      <address type='pci' domain='0x0000' bus='0x05' slot='0x00' function='0x1'/>
    </hostdev>
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <driver name='vfio'/>
      <source>
        <address domain='0x0000' bus='0x01' slot='0x00' function='0x2'/>
      </source>
      <address type='pci' domain='0x0000' bus='0x05' slot='0x00' function='0x2'/>
    </hostdev>
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <driver name='vfio'/>
      <source>
        <address domain='0x0000' bus='0x01' slot='0x00' function='0x3'/>
      </source>
      <address type='pci' domain='0x0000' bus='0x05' slot='0x00' function='0x3'/>
    </hostdev>
  </devices>
</domain>

2.3 伪装虚拟机信息

由于N卡检测到处于虚拟环境中会停止工作(错误43),我们需要给虚拟机添加一个假的vendor ID并隐藏KVM信息:

$ virsh edit windows
<domain>
  <features> # 在features中添加vendor_id, kvm, ioapic项目
    <hyperv>
      <vendor_id state='on' value='0123456789ab'/> # value可以是任意值
    </hyperv>
    <kvm>
      <hidden state='on'/>
    </kvm>
    <ioapic driver='kvm'/>
  </features>
</domain>

至此,桌面端的N卡和A卡应该已经可以在虚拟机中正常工作。

2.4 添加电池

由于移动端RTX显卡还会额外检测机器是否有电池来判断是否是虚拟环境,我们需要添加一张假的ACPI表来让虚拟机认为自己有一块始终充满电的电池:

将以下base64转换为二进制文件[1, 2]:

U1NEVKEAAAAB9EJPQ0hTAEJYUENTU0RUAQAAAElOVEwYEBkgoA8AFVwuX1NCX1BDSTAGABBMBi5f
U0JfUENJMFuCTwVCQVQwCF9ISUQMQdAMCghfVUlEABQJX1NUQQCkCh8UK19CSUYApBIjDQELcBcL
cBcBC9A5C1gCCywBCjwKPA0ADQANTElPTgANABQSX0JTVACkEgoEAAALcBcL0Dk=

给虚拟机添加QEMU启动参数,读入假的acpi文件,添加一个虚拟电池:

$ virsh edit windows
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'> # 可能需要修改xml格式
  <qemu:commandline>
    <qemu:arg value='-acpitable'/>
    <qemu:arg value='file=/path_to_dat/acpi.dat'/>
  </qemu:commandline>
</domain>

3. 启动虚拟机

万事俱备,只差登录永远都登陆不上的Nvidia GeForce Experience下载驱动。

Device

4. 配置RDP和SHIELD

给虚拟机配置一个静态IP网址:

$ virsh net-edit default
<network>
  <ip>
    <dhcp> # 在原有的range下面添加下面的项
      <host mac='' name='windows' ip=''/> # 修改为虚拟机的网卡mac并分配一个ip
    </dhcp>
  </ip>
</network>
$ virsh net-destroy default
$ virsh net-start default

设置QEMU的钩子脚本来配置iptables规则:

#!/bin/bash
if [ "${1}" = "windows" ]; then # 修改为虚拟机的名称
   GUEST_IP= # 填入Windows虚拟机的IP地址
   for PORT in 3389 47984 47989 48010 47998 47999 48000; do
     if [ "${2}" = "stopped" ] || [ "${2}" = "reconnect" ]; then
        /sbin/iptables -D FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $PORT -j ACCEPT
        /sbin/iptables -t nat -D PREROUTING -p tcp --dport $PORT -j DNAT --to $GUEST_IP:$PORT
        /sbin/iptables -D FORWARD -o virbr0 -p udp -d $GUEST_IP --dport $PORT -j ACCEPT
        /sbin/iptables -t nat -D PREROUTING -p udp --dport $PORT -j DNAT --to $GUEST_IP:$PORT
     fi
     if [ "${2}" = "start" ] || [ "${2}" = "reconnect" ]; then
        /sbin/iptables -I FORWARD -o virbr0 -p tcp -d $GUEST_IP --dport $PORT -j ACCEPT
        /sbin/iptables -t nat -I PREROUTING -p tcp --dport $PORT -j DNAT --to $GUEST_IP:$PORT
        /sbin/iptables -I FORWARD -o virbr0 -p udp -d $GUEST_IP --dport $PORT -j ACCEPT
        /sbin/iptables -t nat -I PREROUTING -p udp --dport $PORT -j DNAT --to $GUEST_IP:$PORT
     fi
   done
fi
$ vi /etc/libvirt/hooks/qemu # 填入上面的脚本
$ chmod +x /etc/libvirt/hooks/qemu # 文件要有执行权限

重启虚拟机,测试3389是否能连接远程桌面。

需要使用SHIELD时,要先断开远程桌面连接,然后通过VNC打开GeForce Experience。

GeForce

References

  1. ArchLinux: PCI passthrough via OVMF
  2. r/VFIO: NVIDIA GeForce RTX 2060 Mobile success (QEMU, OVMF)