烧饼论坛 LZ.SB
登录

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

39211
  • 肥猫
    UID 553Lv.2 新手上路

    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%。

  • 收藏 1支持反对打赏

11 条回复

  • BSD
    UID 755Lv.8 热心新人
    #1

    看着不错。

  • 静好副会长
    UID 8152Lv.3 初识社区 禁访禁言
    #2
    该用户已被禁言,内容已隐藏
  • Hitori0221
    UID 3266Lv.5 小有收获
    #3

    不错,有试过 Windows VM 吗

  • 肥猫
    UID 553Lv.2 新手上路楼主
    #4

    @Hitori0221 #3 没测,没有 win 需求,理论上有了 vBIOS 是没有问题的,win 是话直接用 q35,我是因为 fnOS 装得早不想动他所以保持 i440

  • fiat
    UID 2382Lv.6 经常露面
    #5

    支持

  • sekai
    UID 328Lv.3 初识社区
    #6

    好贴,支持

  • 飘云
    UID 2633Lv.3 初识社区
    #7

    好技术。支持。

  • sergerg
    UID 2937Lv.4 渐入佳境
    #8

    bilili 上有这个教程

  • SkyLightVPS
    UID 8292Lv.3 初识社区
    #9

    q35 顺便问下核显有没有破解 64G 显存

  • 肥猫
    UID 553Lv.2 新手上路楼主
    #10

    @sergeya #8 给个连接我去学习

  • nine
    UID 7846Lv.1 初来乍到
    #11

    不建议任何人用 fnos,这个东西研发团队是真的不行,包括但不限于漏洞响应机制、研发、还有存储方案,我更推荐大家用 OMV 或者类似的。存储方案更推荐用 XFS (这个 fnos 不支持)而不是 BTRFS 或者 EXT4。

发表回复