Nvidia RTX显卡虚拟机直通指南
(2021年04月11日)
通过将Nvidia显卡直通给虚拟机,可以让虚拟机独占显卡,在虚拟环境中使用CUDA、SHIELD等N卡专属的功能。
可能存在的实际用途:
- 云工作(Adobe全家桶)
- 云游戏(Nvidia SHIELD)
挖矿
硬件需求
- 支持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
则可以进入下一步,否则(如nouveau
、nvidia*
)则表示设备被显卡驱动占有,需要继续修改参数将显卡驱动拉入黑名单或强制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表来让虚拟机认为自己有一块始终充满电的电池:
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下载驱动。
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。