HOOOS

无需重启宿主机:基于 VFIO-PCI 实现 GPU 动态热插拔与直通全解析

0 18 云深系统客 KVMVFIOGPU直通
Apple

在传统 KVM/QEMU 虚拟化方案中,GPU 直通(Passthrough)通常需要在宿主机引导时(via Grub)就将显卡通过 vfio-pci.ids 锁定。这种“静态直通”虽然稳定,但极大地限制了硬件资产的利用率——当虚拟机关闭时,这块显卡只能在宿主机中闲置,无法用于宿主机的深度学习训练或日常渲染。

为了打破这种限制,我们需要实现 GPU 的动态热插拔与直通:在宿主机需要时,显卡归宿主机所有;在虚拟机启动时,动态将显卡从宿主机驱动中解绑,挂载到 VFIO 驱动上,并实时热插入(Hot-plug)到正在运行的虚拟机中;虚拟机用完后,再热拔出(Hot-unplug)并还给宿主机。

本文将深入探讨这一流程的底层机制与具体实现步骤。


核心前置条件

在尝试动态热插拔之前,请确保你的硬件和宿主机满足以下条件:

  1. IOMMU 必须开启:内核参数中已启用 intel_iommu=onamd_iommu=on,且 iommu=pt 已打开。
  2. 独立的 IOMMU 分组:GPU 及其自带的 HDMI 音频设备(如果有)必须处于独立的 IOMMU 组内。若组内含有其他宿主机关键 PCIe 设备(如网卡或 NVMe),动态解绑会导致整个系统崩溃。
  3. 显卡支持重置(Reset):GPU 必须支持 Function-Level Reset (FLR) 或其他硬件重置机制,否则在多次热插拔后会遭遇显卡无法初始化(Error 43)或宿主机死机。

第一阶段:从宿主机动态解绑 GPU

假设我们要直通的 GPU 及其音频子设备的 PCIe 地址分别为 0000:01:00.0(显卡)和 0000:01:00.1(HDMI 音频)。

1. 识别当前驱动状态

首先,查看显卡当前正在使用的驱动:

lspci -vvv -s 0000:01:00.0 | grep "Kernel driver in use"
# 输出通常为:Kernel driver in use: nvidia 或 amdgpu / nouveau

2. 安全解绑宿主机驱动

直接强行解绑正在被渲染服务或 CUDA 占用的显卡会导致系统 kernel panic。解绑前必须确保没有任何进程在使用该 GPU。

如果是 NVIDIA 显卡,需要先停止相关服务,然后向驱动的 unbind 接口写入设备地址:

# 停止可能占用显卡的桌面环境或 CUDA 任务
systemctl stop display-manager # 如果有桌面环境

# 解绑 GPU 核心
echo "0000:01:00.0" > /sys/bus/pci/devices/0000:01:00.0/driver/unbind
# 解绑 GPU 携带的音频设备
echo "0000:01:00.1" > /sys/bus/pci/devices/0000:01:00.1/driver/unbind

3. 将设备绑定至 vfio-pci

传统的做法是通过写入 new_id 动态添加 PCI ID,但这会导致同型号的所有显卡都被 vfio-pci 接管。

最专业且安全的方法是使用内核的 driver_override 功能,精确指定该特定 PCIe 插槽的设备在重新探测时使用 vfio-pci 驱动:

# 1. 加载 vfio-pci 模块
modprobe vfio-pci

# 2. 覆盖默认驱动指向
echo "vfio-pci" > /sys/bus/pci/devices/0000:01:00.0/driver_override
echo "vfio-pci" > /sys/bus/pci/devices/0000:01:00.1/driver_override

# 3. 将设备触发重新探测,使其绑定到 vfio-pci
echo "0000:01:00.0" > /sys/bus/pci/drivers_probe
echo "0000:01:00.1" > /sys/bus/pci/drivers_probe

此时运行 lspci -nnk -s 0000:01:00.0,你会发现其 Kernel driver in use 已经成功变更为 vfio-pci


第二阶段:动态热插入正在运行的虚拟机

将 GPU 绑定到 vfio-pci 后,我们就可以将其注入到已经运行的 QEMU 虚拟机中。这里推荐使用更主流、对设备状态管理更严谨的 libvirt

1. 编写设备的 XML 配置文件

创建一个名为 gpu-hotplug.xml 的临时文件。请注意,必须将显卡和其音频设备作为一个整体(多功能设备,Multifunction)同时插入,否则 Windows 虚拟机极易出现驱动打不上的情况。

<hostdev mode='subsystem' type='pci' managed='no'>
  <driver name='vfio'/>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
  </source>
  <!-- 注意:managed='no' 意味着我们手动管理驱动,不让 libvirt 越权去卸载宿主机驱动 -->
  <address type='pci' domain='0x0000' bus='0x00' slot='0x08' function='0x0' multifunction='on'/>
</hostdev>
<hostdev mode='subsystem' type='pci' managed='no'>
  <driver name='vfio'/>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/>
  </source>
  <!-- 挂载在同一个虚拟虚拟插槽(Slot 08)的 Function 1 上 -->
  <address type='pci' domain='0x0000' bus='0x00' slot='0x08' function='0x1'/>
</hostdev>

注:虚拟总线位置(如 slot='0x08')需确保未被虚拟机内其他已存在设备占用。

2. 执行热插

使用 virsh 命令行工具将设备动态挂载到运行中的虚拟机(假设 VM 名称为 win10):

# 热插 GPU 核心(启用 multifunction)
virsh attach-device win10 gpu-hotplug.xml --live

# 热插 GPU 音频设备
virsh attach-device win10 gpu-audio-hotplug.xml --live

此时进入虚拟机,系统应该会发出“硬件已插入”的提示音。如果是 Linux 虚拟机,可以使用 dmesg | grep pcilspci 观察新设备的上线。


第三阶段:从虚拟机动态热拔出

当虚拟机使用完毕,或者需要收回 GPU 资源时,我们需要安全地将其从 VM 中剥离。

1. 执行热拔

热拔的顺序与热插相反,建议先拔除子功能(音频),再拔除主功能:

# 拔除音频
virsh detach-device win10 gpu-audio-hotplug.xml --live

# 拔除 GPU 核心
virsh detach-device win10 gpu-hotplug.xml --live

警告:热拔出在 Guest OS(尤其是 Windows)中是异步处理的。Guest 内核接收到 ACPI 拔出信号后,需要释放显卡驱动、清理 VRAM 并断电。请务必等待几秒钟,直到宿主机的 dmesg 中出现类似于 vfio-pci ... releasing device 的日志后再执行后续操作。


第四阶段:还给宿主机并重新绑定原驱动

显卡释出后,我们需要清理 driver_override 的状态,并将其重新交还给宿主机原生驱动(以 NVIDIA 驱动为例):

# 1. 清除驱动覆盖设置
echo "" > /sys/bus/pci/devices/0000:01:00.0/driver_override
echo "" > /sys/bus/pci/devices/0000:01:00.1/driver_override

# 2. 将设备从 vfio-pci 解绑
echo "0000:01:00.0" > /sys/bus/pci/drivers/vfio-pci/unbind
echo "0000:01:00.1" > /sys/bus/pci/drivers/vfio-pci/unbind

# 3. 触发宿主机原生驱动重新加载
echo "0000:01:00.0" > /sys/bus/pci/drivers_probe
echo "0000:01:00.1" > /sys/bus/pci/drivers_probe

# 4. 重新恢复宿主机显卡相关系统服务
systemctl start display-manager

避坑指南与硬核调试

1. 遭遇 AMD "Reset Bug"(重置悬挂)

部分 AMD 显卡(如 Polaris 架构的 RX 580,甚至部分 Navi 显卡)在虚拟机关闭或解绑时,其硬件无法正确响应 PCIe FLR 信号,导致显卡处于未定义状态。再次热插会直接导致宿主机死锁。

  • 解决方案:安装第三方内核模块 vendor-reset。它能针对特定的 AMD 显卡提供定制的硬件重置时序。
    # 安装后,在解绑前将其注入重置机制
    echo "device_specific" > /sys/bus/pci/devices/0000:01:00.0/reset_method
    

2. 拔出时显卡陷入 D3cold 深度睡眠导致主机死机

某些现代 GPU 支持深入的 PCIe 电源管理(Runtime PM)。如果在拔出时,显卡已经进入了 D3cold 状态,强行对 PCIe 链路操作会触发宿主机的 PCIe AER (Advanced Error Reporting) 报错,导致内核崩溃。

  • 对策:在解绑/绑定动作期间,临时将设备的电源管理策略设为 on(即保持唤醒状态),操作完成后再恢复为 auto
    echo "on" > /sys/bus/pci/devices/0000:01:00.0/power/control
    

3. 多功能 PCI 设备的同步性

千万不要单独热插拔显卡而不处理它的 HDMI 音频端,或者显卡自带的 USB Type-C 控制器。同一个物理芯片上的所有 PCIe Function 必须同时归属于宿主机,或同时归属于同一个虚拟机。混用会导致 IOMMU 组错误,QEMU 会直接拒绝启动或热插。

点评评价

captcha
健康