Proxmox 7.4 GPU Passthrough 显卡直通
近期AI题材火爆,看着家里一堆闲置吃灰的显卡,想想也应该让他们起来干活了,于是想起来组建一个新的PVE服务器,把显卡用上。又因为显卡虚拟化的支持目前应该还是不太稳定的状态(想来老黄是不可能给消费级显卡官方虚拟化的能力的),而且即使服务器端驱动搞定了,License的问题应该还是会成为绊脚石,所以还是选择了更为稳妥的直通方案。计划是直通显卡给一个Ubuntu server,然后在其上通过Docker部署一些简单的应用。
之前在PVE 7.1以及更早的版本上面已经尝试过显卡直通,而且运行稳定,本以为这次应该是手拿把攥,结果却翻了车。特此记录一下翻车和修正过程。
PVE 7.1以及之前版本的显卡直通步骤
参考:目前最靠谱的帖子,比PVE官方文档清晰很多的reddit reference post
-
先确认BIOS里面已经打开了所有跟虚拟化有关的选项,比如 vt-d,以及IOMMU分组之类的。
-
然后需要确定服务器的启动的Boot loader是使用的 GRUB 还是 System Boot。这方面 PVE 官方有文档 doc。但是解释的不算非常清楚。总结一下:
- 如果启动的时候有看到GRUB的界面让你选择启动的项目,则肯定是 GRUB。
- 如果启动BIOS没有开启UEFI,而且PVE安装的时候选择的是EXT4的文件系统,则基本上肯定是GRUB
- 如果PVE安装时选择的是ZFS的文件系统,则基本上肯定是System Boot
-
确定了Boot loader之后,需要修改对应的文件:
- 如果是GRUB,需要修改文件为 /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT
原本应该是只有 "quite"。- 现在需要改为:
"quiet amd_iommu=on iommu=pt pcie_acs_override=downstream,multifunction nofb nomodeseti video=vesafb:off,efifb:off"
nofb video=vesafb:off,efifb:off
应该是关闭 Framebuffer。不确定是不是必须的。pcie_acs_override=downstream,multifunction
应该是针对消费级主板通病:“稀烂的IOMMU分组”所做的妥协。相当于打开Kernel里面的一个flag,让kernel根据情况override主板上面的IOMMU分组,强行根据功能分组。很多消费级主板上面的IOMMU分组非常糟糕,比如板载的网卡声卡以及一些PCIE借口都在一个分组里。而PCI直通实际都是以组为单位,所以直通的时候,本意是直通一个PCIE插槽,结果把板载网卡通过去了,而HOST母机直接就没有网卡了,这对于服务器来讲基本就挂了。需要注意的是,有时即使有了这个override,也不一定就能成功,之前在ASUS B460M Prime 这个主板上就发生过这个情况,后来发现需要修改并重新编译内核才能解决。- 注意这个不可以代替BIOS设置里面需要打开的IOMMU分组。
amd_iommu=on
这个跟平台有关,如果是Intel的CPU,需要改为intel_iommu=on
才行。- 双引号不可以省略,所有Argument必须在同一行,不要分行。
- 如果是 System Boot,则为 /etc/kernel/cmdline
- 这个文件里面应该只有一行:root=ZFS=rpool/ROOT/pve-1 boot=zfs quite
- 添加的内容跟上面 GRUB 的添加的 CMDLINE 内容一样,但是不要加双引号。同时一样不要换行。
- 如果是GRUB,需要修改文件为 /etc/default/grub
-
更新 grub 或者 system boot 的 image 参考 pve doc reference
- GRUB:
$ update-grub
- System boot:
$ proxmox-boot-tool refresh
- GRUB:
-
添加VFIO模块 (VFIO Modules)
- 添加以下内容到 /etc/modules
vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd
- IOMMU的中断映射
$ echo "options vfio_iommu_type1 allow_unsafe_interrupts=1" > /etc/modprobe.d/iommu_unsafe_interrupts.conf
$ echo "options kvm ignore_msrs=1" > /etc/modprobe.d/kvm.conf
-
把GPU添加到VFIO里面
- 这一步需要首先找到GPU确切的Vendor ID
- 首先运行
$ lspci -v | less
,在里面找到VGA的段落。肯定有个类似 01:00.0 这种格式的号。 - 然后利用这个号码列出详情
$ lspci -n -s 01:00
,这次不会输出很多,只会输出01:00相关设备的设备号。 - 之后要把这个设备号添加进VFIO
-
查询所有PCI设备
$ lspci -v | less
# Output:
01:00.0 VGA compatible controller: NVIDIA Corporation GA102 [GeForce RTX 3090] (rev a1) (prog-if 00 [VGA controller])
Subsystem: ZOTAC International (MCO) Ltd. GA102 [GeForce RTX 3090]
Flags: bus master, fast devsel, latency 0, IRQ 88, IOMMU group 18
Memory at f6000000 (32-bit, non-prefetchable) [size=16M]
Memory at e0000000 (64-bit, prefetchable) [size=256M]
Memory at f0000000 (64-bit, prefetchable) [size=32M]
I/O ports at e000 [size=128]
Expansion ROM at 000c0000 [disabled] [size=128K]
Capabilities: [60] Power Management version 3
Capabilities: [68] MSI: Enable+ Count=1/1 Maskable- 64bit+
Capabilities: [78] Express Legacy Endpoint, MSI 00
Capabilities: [b4] Vendor Specific Information: Len=14 <?>
Capabilities: [100] Virtual Channel
Capabilities: [258] L1 PM Substates
Capabilities: [128] Power Budgeting <?>
Capabilities: [420] Advanced Error Reporting
Capabilities: [600] Vendor Specific Information: ID=0001 Rev=1 Len=024 <?>
Capabilities: [900] Secondary PCI Express
Capabilities: [bb0] Physical Resizable BAR
Capabilities: [c1c] Physical Layer 16.0 GT/s <?>
Capabilities: [d00] Lane Margining at the Receiver <?>
Capabilities: [e00] Data Link Feature <?>
Kernel driver in use: vfio-pci
Kernel modules: nvidiafb, nouveau
01:00.1 Audio device: NVIDIA Corporation GA102 High Definition Audio Controller (rev a1)
Subsystem: ZOTAC International (MCO) Ltd. Device 1613
Flags: bus master, fast devsel, latency 0, IRQ 87, IOMMU group 18
Memory at f7080000 (32-bit, non-prefetchable) [size=16K]
Capabilities: [60] Power Management version 3
Capabilities: [68] MSI: Enable- Count=1/1 Maskable- 64bit+
Capabilities: [78] Express Endpoint, MSI 00
Capabilities: [100] Advanced Error Reporting
Capabilities: [160] Data Link Feature <?>
Kernel driver in use: vfio-pci
Kernel modules: snd_hda_intel
- 列出01:00所有设备号(通常显卡都在01:00)
$ lspci -n -s 01:00
# output
01:00.0 0000: 10de:1b81 (rev a1)
01:00.1 0000: 10de:10f0 (rev a1)
- 添加GPU设备到VFIO
echo "options vfio-pci ids=10de:1b81,10de:10f0 disable_vga=1"> /etc/modprobe.d/vfio.conf
- 更新 initramfs 并重启
$ update-initramfs -u
$ reboot now
- 确认IOMMU分组可以用这个script将所有的IOMMU分组全部打印出来。
#!/bin/bash
for d in $(find /sys/kernel/iommu_groups/ -type l | sort -n -k5 -t/); do
n=${d#*/iommu_groups/*}; n=${n%%/*}
printf 'IOMMU Group %s ' "$n"
lspci -nns "${d##*/}"
done;
-
指定GPU给虚拟机,并修改虚拟机配置文件来保证虚拟机里面使用显卡的时候,不会被驱动识别出当前是虚拟机环境进而停止工作
- 通过UI改:虚拟机的machine type应该为Q35。否则在指定PCI设备的时候无法选中PCI-E选项。
- 这个需要在创建虚拟机的时候调,印象中在创建之后就不能改了。
- 配置文件位置为
/etc/pve/qemu-server/<vmid>.conf
:cpu: host,hidden=1,flags=+pcid
args: -cpu 'host,+kvm_pv_unhalt,+kvm_pv_eoi,hv_vendor_id=NV43FIX,kvm=off'
- 之后就在WEB UI:虚拟机Hardware tab下,添加PCI设备,这个时候应该是可以找到GPU设备进行分配的。
- 如果出现
No IOMMU detected, please activate it.
的警告,说明BIOS上面的IOMMU分组没开,或者kernel cmdline上面没有添加amd_iommu=on
或者intel_iommu=on
,或者干脆就是缺一次重启。
- 如果出现
- 在指定GPU给虚拟机的时候,大体如下(这里也可以选择 all function,只不过就是会同时把GPU上面的那个音频设备同时直通进去;另外如果不链接显示器作游戏机的话,没有太大必要把Primary GPU选上,一旦选上之后,虚拟机的主显示输出就改为了这个GPU,此时如果系统是桌面系统,那么在Proxmox的WebUI的console里面就看不到桌面了)
- 通过UI改:虚拟机的machine type应该为Q35。否则在指定PCI设备的时候无法选中PCI-E选项。
-
ROM相关 参考Reddit上面的步骤
- 这部分因为没有用到过,所以没有尝试过,所以省略过去。
-
验证
- 在VM里面(我用的是Ubuntu 22.04),应该是可以直接安装Nvidia的驱动的。
$ sudo apt-get install nvidia-driver-525
- Test result:
$ nvidia-smi
(可能需要重启)
- 如果是 Debian (tested on debian 11 aka bullseye)
- Add
non-free
package sources to in/etc/apt/sources.list
- By default the content should be like:
deb http://ftp.ca.debian.org/debian bullseye main contrib
- Change it to
deb http://ftp.ca.debian.org/debian bullseye main contrib non-free
- Do the same for both
bullseye
andbullseye-updates
- By default the content should be like:
- Add
$ apt update && apt install nvidia-detect && nvidia-detect
。通常来讲输出会说让你安装nvidia-driver
,至于版本应该是自动选择的。$ apt install nvidia-driver
。之后可能需要重启然后用nvidia-smi
验证。
- 在VM里面(我用的是Ubuntu 22.04),应该是可以直接安装Nvidia的驱动的。
升级到PVE 7.2之后的翻车
(实测在7.4的版本上是翻车了,没有实地验证是不是7.2也会翻车)
按照之前的步骤,在虚拟机里面也确定了nvidia-smi
可以输出正确的内容。但是,尝试使用Jellyfin硬解,结果是hang。一番折腾之后发现应该还是直通出现了问题。
我这里是用Jellyfin的ffmpeg试图进行硬解的时候发现了,检测的方法是:
- 安装 Jellyfin
curl https://repo.jellyfin.org/install-debuntu.sh > ~/install-debuntu.sh
sudo bash ~/install-debuntu.sh
- 使用Jellyfin的FFmpeg测试CUDA硬件加速
# Test
# 这一步会在输出一系列Capabilities测试通过之后,直接卡死。
# 推断应该是某一个测试一直卡住了。
/usr/lib/jellyfin-ffmpeg/ffmpeg -v debug -init_hw_device cuda
# 类似如下输出 (但是具体卡在哪一个位置并准确,只是形式类似)
ffmpeg version 5.1.2-Jellyfin Copyright (c) 2000-2022 the FFmpeg developers
built with gcc 10 (Debian 10.2.1-6)
configuration: --prefix=/usr/lib/jellyfin-ffmpeg --target-os=linux --extra-libs=-lfftw3f --extra-version=Jellyfin --disable-doc --disable-ffplay --disable-ptx-compression --disable-shared --disable-libxcb --disable-sdl2 --disable-xlib --enable-lto --enable-gpl --enable-version3 --enable-static --enable-gmp --enable-gnutls --enable-chromaprint --enable-libdrm --enable-libass --enable-libfreetype --enable-libfribidi --enable-libfontconfig --enable-libbluray --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libdav1d --enable-libwebp --enable-libvpx --enable-libx264 --enable-libx265 --enable-libzvbi --enable-libzimg --enable-libfdk-aac --arch=amd64 --enable-libsvtav1 --enable-libshaderc --enable-libplacebo --enable-vulkan --enable-opencl --enable-vaapi --enable-amf --enable-libmfx --enable-ffnvcodec --enable-cuda --enable-cuda-llvm --enable-cuvid --enable-nvdec --enable-nvenc
libavutil 57. 28.100 / 57. 28.100
libavcodec 59. 37.100 / 59. 37.100
libavformat 59. 27.100 / 59. 27.100
libavdevice 59. 7.100 / 59. 7.100
libavfilter 8. 44.100 / 8. 44.100
libswscale 6. 7.100 / 6. 7.100
libswresample 4. 7.100 / 4. 7.100
libpostproc 56. 6.100 / 56. 6.100
Splitting the commandline.
Reading option '-v' ... matched as option 'v' (set logging level) with argument 'debug'.
Reading option '-init_hw_device' ... matched as option 'init_hw_device' (initialise hardware device) with argument 'cuda'.
Finished splitting the commandline.
Parsing a group of options: global .
Applying option v (set logging level) with argument debug.
Applying option init_hw_device (initialise hardware device) with argument cuda.
[AVHWDeviceContext @ 0x5567c1c98180] Loaded lib: libcuda.so.1
[AVHWDeviceContext @ 0x5567c1c98180] Loaded sym: cuInit
[AVHWDeviceContext @ 0x5567c1c98180] Loaded sym: cuDeviceGetCount
[AVHWDeviceContext @ 0x5567c1c98180] Loaded sym: cuDeviceGet
[AVHWDeviceContext @ 0x5567c1c98180] Loaded sym: cuDeviceGetAttribute
[AVHWDeviceContext @ 0x5567c1c98180] Loaded sym: cuDeviceGetName
[AVHWDeviceContext @ 0x5567c1c98180] Loaded sym: cuDeviceComputeCapability
[AVHWDeviceContext @ 0x5567c1c98180] Loaded sym: cuCtxCreate_v2
[AVHWDeviceContext @ 0x5567c1c98180] Loaded sym: cuCtxSetLimit
- 同样在尝试其他使用CUDA的app也会卡死。
之所以发现是Passthrough的问题,是因为Debug的时候尝试在PVE母机(Debian)上面直接安装Jellyfin,同样的ffmpeg测试就顺利通过。至此确定是GPU Passthrough的问题。