PVE 9.2 + Ryzen 7000 核显动态直通 fnOS 完整教程(实测版)

目标:fnOS 虚拟机运行时独占 AMD 核显做视频硬解;VM 关闭 / 宿主机重启 / 异常断电后核显自动回归宿主机,物理屏幕可随时 debug。

方案:hookscript 动态绑定(纯 sysfs,不依赖 driverctl),不做任何持久化的 vfio 绑定、不拉黑 amdgpu、不设开机自启。

实测成果:飞牛影视 4K HEVC 播放 CPU 0% 占用,负载全在 GPU


一、实测环境

宿主机Proxmox VE 9.2(内核 7.0.14-15-pve),ext4 + GRUB 引导
CPUAMD Ryzen 7 7700(Raphael,核显 device id 1002:164e,RDNA2/gfx1035)
主板MSI(BIOS 需支持 Above 4G Decoding)
核显 PCI 地址0000:12:00.0(下文全部以此为例,替换成你自己的
VMID 102,fnOS 1.2.0505(legacy/SeaBIOS 引导),8C / 8G,VirtIO SCSI
既有直通0000:11:00.0 SATA 控制器挂 hostpci0(fnOS 数据盘依赖,不能动)→ 核显挂 hostpci1
最终成果vainfo 见 radeonsi + HEVC VLD;影视 4K 硬解 CPU 0%

版本硬门槛:fnOS ≥ v1.1.29(2026-04 推送)才支持 AMD GPU 影视硬转码。旧版本最多 "识别到" 显卡,无法硬解——先在 fnOS 网页端升级系统和影视 App,其他都白搭。


二、方案与原理

核显作为 PCI 设备直通给 VM,由 hookscript 在 VM 生命周期中动态切换驱动归属:

  • VM start 前:核显从宿主机 amdgpu 解绑 → 交给 vfio-pci → QEMU 接管
  • VM stop 后:从 vfio-pci 解绑 → 归还 amdgpu → 宿主机 TTY / 显示恢复
  • 绑定状态不落盘:宿主机重启 / 断电后核显天然回归 amdgpu,永远保底可用

Raphael 这类 APU 核显没有独立 vBIOS 芯片(vBIOS 内嵌在主板 BIOS 里),直通后 guest 读不到 → 必须从宿主机提取 vBIOS 用 romfile 喂给 QEMU(本文核心步骤之一)。

明确不做的事(保证任何情况下宿主机可救):

  • ❌ 不写 amdgpu blacklist
  • ❌ 不写 vfio-pci ids= 预绑定
  • ❌ VM 不设 onboot: 1

三、BIOS 设置

  • Above 4G Decoding → 开启(必须)
  • CSM / Legacy Boot → 关闭,纯 UEFI
  • Resizable BAR → 先不动;直通翻车时再回来关
  • SVM / IOMMU 相关选项保持默认开启

四、宿主机准备

4.1 IOMMU 内核参数

确认引导方式:

[ -d /boot/efi ] && echo "EFI" || echo "BIOS"
proxmox-boot-tool status 2>/dev/null

GRUB(ext4 安装,本机实测值)——编辑 /etc/default/grub

GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt pcie_acs_override=downstream"

生效并重启:

update-grub
reboot

systemd-boot(ZFS 安装):编辑 /etc/kernel/cmdline 追加同样参数后 proxmox-boot-tool refresh 并重启。

💡 新内核上 amd_iommu=on 会报 Unknown option - 'on',无害可忽略;真正起作用的是 iommu=pt,AMD 平台 BIOS 开了 IOMMU 即默认启用。

验证:

dmesg | grep -iE "AMD-Vi|iommu" | head -5
# 有 "Interrupt remapping enabled" / "iommu ... enabled" 即可
# 已有其他直通设备在正常工作,也说明 IOMMU 是通的

4.2 vfio 模块

/etc/modules 确保包含三行:

vfio
vfio_iommu_type1
vfio_pci
update-initramfs -u -k all

4.3 确认核显地址与复合体布局

lspci -nn | grep -i "12:00"

Raphael 通常是多功能设备:

12:00.0 VGA compatible controller [1002:164e]   ← 只直通这个
12:00.1 Audio device                [1002:1640]   ← HDMI 音频(可不带,QEMU 常自动带上)
12:00.2 Encryption controller       [1022:1649]   ← PSP,留宿主机
12:00.3/.4 USB controller                         ← 留宿主机,勿动

五、提取 vBIOS(APU 必做)

这是 APU 直通与独显直通最大的差异点。 核显 ROM 不在设备上,guest 拿不到 ROM 时 amdgpu 会报 Unable to locate a BIOS ROM 后拒绝初始化。

唯一可靠的提取方法是 debugfs(要求宿主机 amdgpu 正在驱动核显——开机后、VM 启动前最佳):

# 1. 确认核显在 amdgpu 手里
lspci -k -s 12:00.0        # Kernel driver in use: amdgpu

# 2. 从 debugfs 导出(amdgpu init 时已把 vBIOS 拷进内存)
cat $(find /sys/kernel/debug/dri -name amdgpu_vbios | head -1) > /usr/share/kvm/raphael.rom

# 3. 验证
ls -l /usr/share/kvm/raphael.rom                       # 几十~几百 KB
od -A x -t x1 /usr/share/kvm/raphael.rom | head -2     # 开头必须 55 aa

ROM 合格标准55 aa 开头;头文件第 3 字节 × 512 = 文件大小(如 55 aa 57 e9 → 0x57×512 = 44544,头尾自洽)。

补齐到 64K(2 的幂,避免个别内核 / 固件拒绝非对齐尺寸):

python3 -c "data=open('/usr/share/kvm/raphael.rom','rb').read(); open('/usr/share/kvm/raphael-64k.rom','wb').write(data+b'\xff'*(65536-len(data)))"
ls -l /usr/share/kvm/raphael-64k.rom    # 65536

⚠️ 不要用 sysfs dumpecho 1 > /sys/bus/pci/devices/.../rom):APU 上会 Permission denied——核显没有暴露 ROM BAR 给 sysfs。
⚠️ TechPowerUp vBIOS 库没有 Raphael 核显的 ROM(其 "RX660 iGPU Raphael" 条目实为 RX 6600 独显 deviceid 73FF 的 BIOS,勿用)。
💡 find 无输出时先 mount -t debugfs none /sys/kernel/debug 再试。


六、hookscript(动态绑定核心)

6.1 脚本

mkdir -p /var/lib/vz/snippets
cat > /var/lib/vz/snippets/gpu-passthrough.sh <<'EOF'
#!/bin/bash
# 动态核显直通:两阶段均实测驱动归属,杜绝假成功
GPU="0000:12:00.0"          # ← 改成你的核显地址
LOG=/var/lib/vz/snippets/gpu-passthrough.log
DEV=/sys/bus/pci/devices/$GPU
OVR=$DEV/driver_override
log() { echo "$(date '+%F %T') [$1] $2" >> $LOG; }
cur() { local l; l=$(readlink "$DEV/driver" 2>/dev/null); if [ -n "$l" ]; then basename "$l"; else echo "(unbound)"; fi; }

case "$1" in
  pre-start)
    if [ "$(cur)" = "amdgpu" ]; then
      echo "$GPU" > /sys/bus/pci/drivers/amdgpu/unbind 2>>$LOG || { log "$1" "FAILED: amdgpu unbind"; exit 1; }
    fi
    echo "" > "$OVR" 2>>$LOG
    echo vfio-pci > "$OVR" 2>>$LOG || { log "$1" "FAILED: set override"; exit 1; }
    echo "$GPU" > /sys/bus/pci/drivers/vfio-pci/bind 2>>$LOG \
      || echo "$GPU" > /sys/bus/pci/drivers_probe 2>>$LOG
    sleep 1
    if [ "$(cur)" = "vfio-pci" ]; then
      log "$1" "bound $GPU to vfio-pci (verified)"
    else
      log "$1" "FAILED: driver is '$(cur)' (expect vfio-pci)"
      exit 1
    fi
    ;;
  post-stop)
    sleep 2
    c="$(cur)"
    if [ "$c" = "amdgpu" ]; then
      log "$1" "already on amdgpu, nothing to do"
      exit 0
    fi
    if [ "$c" = "vfio-pci" ]; then
      for i in 1 2 3 4 5; do
        echo "$GPU" > /sys/bus/pci/drivers/vfio-pci/unbind 2>>$LOG || { log "$1" "unbind try $i failed"; sleep 2; }
        c="$(cur)"; [ "$c" != "vfio-pci" ] && break
      done
      if [ "$c" = "vfio-pci" ]; then
        log "$1" "FAILED: still on vfio-pci after retries"
        exit 1
      fi
    fi
    # 空串清除("none" 在新内核是字面量不是清除符!);清空后读回是 "(null)",属正常
    echo "" > "$OVR" 2>>$LOG
    clr="$(cat "$OVR" 2>/dev/null)"
    if [ -n "$clr" ] && [ "$clr" != "(null)" ]; then
      log "$1" "FAILED: override not cleared (still '$clr')"
      exit 1
    fi
    echo "$GPU" > /sys/bus/pci/drivers/amdgpu/bind 2>>$LOG \
      || echo "$GPU" > /sys/bus/pci/drivers_probe 2>>$LOG
    sleep 1
    if [ "$(cur)" = "amdgpu" ]; then
      log "$1" "released $GPU back to amdgpu (verified)"
    else
      log "$1" "FAILED: driver is '$(cur)' (expect amdgpu)"
      exit 1
    fi
    ;;
esac
exit 0
EOF
chmod +x /var/lib/vz/snippets/gpu-passthrough.sh

6.2 为什么这么写(新内核 sysfs 的坑,全部实测)

  1. driver_override 在设备目录下/sys/bus/pci/devices/.../driver_override),不在 driver 软链指向的驱动目录里
  2. 设备处于绑定态时写空串清 override 会 EINVAL;清 override 必须在设备 unbound 状态下做
  3. echo none > driver_override 不是清除——"none" 是字面量值,会挡住之后所有驱动的 bind(报 ENODEV 且 dmesg 零痕迹,极难排查)
  4. 清空后 cat 读回是字面量 (null)(内核 NULL 指针的 sysfs 打印),校验逻辑必须放行它
  5. drivers_probe 写入几乎恒返回 0,不能作为成功依据——每阶段结束必须 readlink driver 实测设备归属

⚠️ 不要用 driverctl:实测在新内核上 unset-override 失败也返回 exit 0(假成功),会把设备永久留在 vfio 上还骗你 "已归还"。

6.3 修改脚本后的必做自检

换脚本 / 改脚本后,务必手动验证两个方向(不经过 qm):

bash -x /var/lib/vz/snippets/gpu-passthrough.sh pre-start
lspci -k -s 12:00.0 | grep "Kernel driver"   # 期望: vfio-pci
tail -1 /var/lib/vz/snippets/gpu-passthrough.log   # 期望: bound ... (verified)

bash -x /var/lib/vz/snippets/gpu-passthrough.sh post-stop
lspci -k -s 12:00.0 | grep "Kernel driver"   # 期望: amdgpu
tail -1 /var/lib/vz/snippets/gpu-passthrough.log   # 期望: released ... (verified)

两个 (verified) 都拿到,脚本才算在这台内核上验证通过。之后每次 qm stop 后都 tail 一下日志确认有新条目——hookscript 文件为空 / 残缺时会被静默跳过(VM 照样能启动,极难察觉)。


七、VM 配置

7.1 创建 / 改造 VM

说明
机型i440fx(默认)fnOS 是 legacy 引导就别动 machine,最稳
BIOSSeaBIOS(默认)OVMF 仅适合 EFI 安装的 fnOS
CPUhost,≥4 核
内存≥8G
磁盘VirtIO SCSI
网络VirtIO
显示无(none)x-vga=1 的配套要求,见 7.2
PCI 设备见 7.2核显挂 hostpci1(hostpci0 若已有 SATA 直通别动)

关键命令(VMID / 地址替换成自己的):

qm set 102 --onboot 0                                        # 铁律:不自启
qm set 102 --hookscript local:snippets/gpu-passthrough.sh
qm set 102 --vga none
qm set 102 --hostpci1 0000:12:00.0,romfile=raphael-64k.rom,x-vga=1
qm config 102    # 核对

最终配置应包含:

onboot: 0
hookscript: local:snippets/gpu-passthrough.sh
hostpci0: 0000:11:00.0              # 既有 SATA 直通(如有)
hostpci1: 0000:12:00.0,romfile=raphael-64k.rom,x-vga=1
vga: none

7.2 为什么必须 x-vga=1 + vga none(本方案最隐蔽的坑)

核显是 VGA class 设备。在 i440fx 机型下,它的 ROM BAR 会落在 x86 legacy VGA BIOS 影子区 0xC0000——那里早就被虚拟显卡(Bochs/SeaVGABIOS,约 39K)占着。结果就是:

  • 只写 romfile 不够:QEMU 命令行里明明带着 romfile,但 guest 读 ROM BAR 拿到的是 SeaVGABIOS(头部 55 aa 4d e9),amdgpu 解析不出 ATOM 表,照样 Unable to locate a BIOS ROM
  • x-vga=1 让你喂的 vBIOS 升级为主 VGA BIOS,虚拟显卡退场(guest 里 00:02.0 Bochs 消失),ROM BAR 读回 55 aa 57 e9,amdgpu 立刻认

代价:PVE 的 noVNC 控制台黑屏(虚拟显卡没了,by design)。fnOS 走 SSH / 网页管理,不受任何影响。回退:qm set 102 --vga std 并去掉 x-vga。

备选路线(q35)qm set 102 --machine q35 --vga std + --hostpci1 ...,romfile=...,pcie=1。q35 下 ROM BAR 能拿到独立 MMIO 地址避开 0xC0000。适合 EFI 安装的 fnOS 或 i440fx 路线翻车时。注意 pcie=1 是 q35 专属参数。


八、启动与验证

8.1 启动

qm start 102
qm status 102      # 期望: status: running

启动瞬间出现这两条属正常,忽略

  • error writing '1' to '.../12:00.0/reset': Inappropriate ioctl for device —— Raphael 没有硬件复位机制,QEMU 提示后继续
  • 11:00.0 BAR 5: failed to create dma-buf ... Invalid argument —— QEMU 10.x 新特性在非页对齐 BAR 上的已知警告,上游已降级为 warning,正常直通走 mmap 兜底不受影响

8.2 fnOS 内验证(SSH)

# 1. GPU 在,且只有它(Bochs 虚拟显卡应消失)
lspci | grep -i vga
# 期望: 00:11.0 VGA compatible controller: AMD/ATI Raphael

# 2. 驱动初始化成功(无 Fatal error)
dmesg | grep -iE "amdgpu|BIOS ROM" | tail -8
# 期望: Initialized amdgpu 3.64.0 ... / vcn_dec、vcn_enc、jpeg_dec ring 就绪
# 不应出现: Unable to locate a BIOS ROM / Fatal error during GPU init

# 3. 渲染节点(核心判据)
ls -l /dev/dri
# 期望: card0 + renderD128

# 4. VAAPI 入口(没有就 apt install -y vainfo)
vainfo 2>&1 | head -15
# 期望: Driver version: Mesa Gallium ... (radeonsi, raphael_mendocino ...)
#       VAProfileHEVCMain : VAEntrypointVLD  ← 4K HEVC 硬解入口

Cannot find any crtc or sizes 是 headless(无显示器)的正常提示,无害。

8.3 影视硬解

飞牛影视 → 设置 → 转码 → 开启硬件解码 → 播一部 4K HEVC → CPU 个位数百分比乃至 0% = 硬解生效


九、热切稳定性测试

for i in 1 2 3; do qm stop 102; sleep 30; qm start 102; sleep 30; done

每轮检查:

  • fnOS 里 renderD128 还在、硬解正常
  • stop 后宿主机 lspci -k -s 12:00.0 回到 amdgpu,日志新增 released ... (verified)
  • 物理屏幕 TTY 正常

⚠️ 循环间隔必须 30 秒起步。Raphael 没有硬件复位机制,间隔过短的连续 stop/start 会把设备 / VM 状态打坏(实测 5 秒间隔连跑 3 轮后 VM 内存异常)。翻车了不用慌:宿主机整机重启即完全恢复


十、注意事项(踩坑实录浓缩)

  1. fnOS 版本门槛:AMD 硬解需 fnOS ≥ 1.1.29,影视 App 同步升级。旧版本一切白搭。
  2. APU 没有独立 vBIOS:必须 debugfs 提取(见第五节);sysfs dump 和显卡 ROM 数据库两条路都走不通。
  3. romfile 单独不够:VGA 设备的 ROM BAR 会撞进 0xC0000 影子区被虚拟显卡的 SeaVGABIOS 冒充——必须 x-vga=1 + vga none(或切 q35 + pcie=1)。判别方法:guest 里读 ROM BAR,头部 4d e9(39K)= SeaVGABIOS 撞车;57 e9(或你提取的尺寸字段)= 自己的 vBIOS 到位。
  4. 不要用 driverctl:新内核上假成功(失败也 exit 0)。直接写 sysfs 也有四个语义坑(见 6.2),本文 v6 脚本已全部规避。
  5. hookscript 静默失败风险:文件为空或残缺时 hookscript 会被静默跳过,且设备残留 vfio 时 QEMU 反而能启动——改完脚本必须 bash -x 双向自检(6.3),日常 stop 后 tail 日志确认新条目。
  6. Raphael 无硬件复位:启动时的 reset ENOTTY 警告无害;连续热切必须间隔 ≥30 秒;异常后宿主机重启即恢复。
  7. 无害警告清单(见到别慌):reset ENOTTY、SATA BAR 5 dma-buf EINVAL、amd_iommu=on Unknown option、headless 下 Cannot find any crtc or sizes
  8. noVNC 黑屏vga: none 的必然结果,fnOS 用 SSH / 网页管理不受影响。
  9. 两条铁律:VM 永远 onboot: 0;永远不往 /etc/modprobe.d/ 加 amdgpu blacklist 或 vfio 预绑定——这是 "宿主机永远可救" 的底线。
  10. 复合体设备只直通 .0:12:00.2(PSP)、.3/.4(USB)留宿主机, guest 内不需要,直通了反而添乱。

十一、排错速查

症状原因处理
影视 "未检测到 GPU"fnOS < 1.1.29先升级系统和影视 App
影视不认,但 renderD128 在、vainfo 正常App / 版本适配升级到最新;查飞牛社区显卡验证表
dmesg 报 Unable to locate a BIOS ROMvBIOS 没进 guest查 romfile 是否挂上(ps -ef | grep kvm | grep romfile);再查 ROM BAR 内容(见注意事项 3)
ROM BAR 读出头 55 aa 4d e9(约 39K)SeaVGABIOS 占了 0xC0000x-vga=1 + vga none(或 q35+pcie=1)
amdgpu/bind 报 ENODEV 且 dmesg 无 probe 痕迹driver_override 残留挡路cat .../driver_override 看值;unbound 态空串清除
stop 后设备仍在 vfio-pcihookscript 没跑 / 失败tail 日志查 FAILED;手动跑 bash -x 脚本 post-stop;无效则重启宿主机
fnOS 里 BAR can't reservei440fx PCI 窗口不足切 q35:qm set 102 --machine q35(fnOS legacy 引导通常可直起;不行回退 --machine pc
start 报 VM is locked异常断电残留锁qm unlock 102 && qm start 102
VM 快速热切后内存异常 / 不稳Raphael 无复位,状态未归零宿主机重启即恢复;日常间隔 ≥30 秒

十二、日常使用速查

场景操作核显归属
日常qm start 102 后不管它fnOS(硬解)
需要排查宿主机qm stop 102,等 30 秒宿主机 TTY
PVE 炸了 / 断电重启什么都不用做宿主机(绑定不落盘,天然回归)
排查完恢复qm start 102fnOS
看归还是否成功tail /var/lib/vz/snippets/gpu-passthrough.log(verified) 条目

教程基于 2026-09 实测整理:PVE 9.2 / 内核 7.0.14-15-pve / Ryzen 7 7700 / fnOS 1.2.0505,最终成果 4K HEVC 硬解 CPU 0%。