KVM显卡直通实战_RTX4090分时复用

一块 RTX 4090 分时复用:KVM 虚拟机 GPU 直通实战

作者: 杨显钊

版本: 1.0

日期: 2026年10月27日

标签: KVM、GPU直通、VFIO、IOMMU、libvirt、NVIDIA、虚拟化

适合人群: 虚拟化运维、AI 环境搭建、Linux 系统工程师


摘要

本文记录一台安装 Kylin V11 的物理机,把机器上唯一的显示设备 NVIDIA RTX 4090 以 PCI 直通(VFIO)方式交给 KVM 虚拟机,并实现宿主机与虚拟机分时复用的完整过程。

具体做了四件事:宿主机侧开启 IOMMU 并确认显卡独占一个 IOMMU 组;不写死 vfio.conf,改用 libvirt 的 managed='yes' 让显卡驱动在 nvidia 与 vfio-pci 之间自动切换;在无外网的 CentOS 8 客户机里离线装上 580.95.05 驱动;写一个 gpu-switch 脚本,一条命令在"归宿主机"和"归虚拟机"之间切换。

最终效果:客户机里 nvidia-smi 认出 RTX 4090 / 24564MiB,CUDA 计算可用;要收回宿主机时关掉虚机、恢复桌面即可。


一、场景:一台机器一块 4090,两边都想用

需求很朴素,但卡得很死:

  • 宿主机是办公/开发用的桌面系统,只有这一块 RTX 4090,图形界面本身就跑在它上面;
  • 同时又要给 KVM 虚拟机里的 CentOS 8 提供 GPU 算力(跑深度学习环境);
  • 同一块卡,同一时刻只能归一边。

为什么不用 vGPU、不用其他共享方案:

方案 为什么没选
NVIDIA vGPU 面向数据中心卡授权,GeForce 消费级卡拿不到
远程调用宿主机的 CUDA 环境要做到客户机里,宿主机要能被随便折腾,隔离性不够
软件渲染 / 虚拟显卡 拿不到真正的 CUDA 算力,等于没做

所以走 PCI 直通:把整块显卡从宿主机"拔"下来交给虚拟机,用完再"插"回去。目标三个:

  1. 客户机里 nvidia-smi 能正常输出,并能跑 CUDA 程序;
  2. 宿主机桌面在不用 GPU 的时候不受影响;
  3. 切换动作要短、要可控,不能每次手敲一堆命令。

二、动手前先理清三个概念

这三个词网上到处都是,但真动手前最好先把它们的关系理顺,不然报错时不知道自己在哪一层。

2.1 IOMMU:直通的硬件前提

CPU 的 I/O 内存管理单元。它的作用是把设备发起的 DMA 地址做一层翻译,让"设备只能访问被授权的那块内存"。

对直通来说它是硬门槛:没有 IOMMU,虚拟机里的显卡驱动发起 DMA 时,用的是客户机的物理地址,落到真实内存上就是乱写,轻则花屏重则整机崩。开了 IOMMU,虚机才能安全地把物理设备当自己的用。

2.2 VFIO:把设备"让"出去的内核框架

VFIO(Virtual Function I/O)是内核里负责"把某个 PCI 设备从它的原生驱动上摘下来,交给用户态程序(也就是 QEMU)直接管"的框架。它由几个模块配合:

  • vfio:框架本体
  • vfio_iommu_type1:把 IOMMU 的分组、DMA 映射能力暴露出来
  • vfio_pci:PCI 设备的实现

只要设备被绑到 vfio-pci 上,它的原生驱动就碰不到它了------这就是"显卡归虚拟机"的技术状态。

2.3 显卡在宿主机上只有"一副面孔"

一块物理显卡,在宿主机上要么绑定 nvidia(宿主机自己用),要么绑定 vfio-pci(交给虚机)。不存在"两边都能用"。

这也解释了后面所有的切换动作:切换的本质就是重新绑定驱动,外加保证没有进程还攥着 /dev/nvidia* 不放。
#mermaid-svg-p9DQJ5MDWfXh4jnY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-p9DQJ5MDWfXh4jnY .error-icon{fill:#552222;}#mermaid-svg-p9DQJ5MDWfXh4jnY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-p9DQJ5MDWfXh4jnY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .marker.cross{stroke:#333333;}#mermaid-svg-p9DQJ5MDWfXh4jnY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-p9DQJ5MDWfXh4jnY p{margin:0;}#mermaid-svg-p9DQJ5MDWfXh4jnY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster-label text{fill:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster-label span{color:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster-label span p{background-color:transparent;}#mermaid-svg-p9DQJ5MDWfXh4jnY .label text,#mermaid-svg-p9DQJ5MDWfXh4jnY span{fill:#333;color:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .node rect,#mermaid-svg-p9DQJ5MDWfXh4jnY .node circle,#mermaid-svg-p9DQJ5MDWfXh4jnY .node ellipse,#mermaid-svg-p9DQJ5MDWfXh4jnY .node polygon,#mermaid-svg-p9DQJ5MDWfXh4jnY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .rough-node .label text,#mermaid-svg-p9DQJ5MDWfXh4jnY .node .label text,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape .label,#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape .label{text-anchor:middle;}#mermaid-svg-p9DQJ5MDWfXh4jnY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .rough-node .label,#mermaid-svg-p9DQJ5MDWfXh4jnY .node .label,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape .label,#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape .label{text-align:center;}#mermaid-svg-p9DQJ5MDWfXh4jnY .node.clickable{cursor:pointer;}#mermaid-svg-p9DQJ5MDWfXh4jnY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .arrowheadPath{fill:#333333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p9DQJ5MDWfXh4jnY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-p9DQJ5MDWfXh4jnY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p9DQJ5MDWfXh4jnY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster text{fill:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY .cluster span{color:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-p9DQJ5MDWfXh4jnY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-p9DQJ5MDWfXh4jnY rect.text{fill:none;stroke-width:0;}#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape p,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-p9DQJ5MDWfXh4jnY .icon-shape .label rect,#mermaid-svg-p9DQJ5MDWfXh4jnY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-p9DQJ5MDWfXh4jnY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-p9DQJ5MDWfXh4jnY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-p9DQJ5MDWfXh4jnY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 同一时刻只能归一边
libvirt managed=yes 自动切换
物理机:Kylin V11
RTX 4090

01:00.0 显示 + 01:00.1 音频
绑定 nvidia 驱动
绑定 vfio-pci
宿主机桌面 / CUDA 任务
客户机内装 nvidia 驱动

07:00.0 → CUDA


三、宿主机侧准备

3.1 打开 IOMMU

先看当前内核参数里有没有开启:

bash 复制代码
# cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-6.6.0-32.24... root=/dev/mapper/... ro resume=... rd.lvm.lv=... \
cgroup_disable=files apparmor=0 crashkernel=512M audit=0 console=ttyS0,115200 \
intel_iommu=on iommu=pt

关注最后两个:

  • intel_iommu=on:Intel 平台开启 IOMMU(AMD 平台写 amd_iommu=on)
  • iommu=pt:Pass-Through 模式,只对需要直通的设备做地址翻译,没直通的设备保持原样,能少掉一部分性能损耗

持久化写在 /etc/default/grub 里:

bash 复制代码
# grep GRUB_CMDLINE_LINUX /etc/default/grub
GRUB_CMDLINE_LINUX="... console=ttyS0,115200 intel_iommu=on iommu=pt"

改完执行 grub2-mkconfig -o /boot/grub2/grub.cfg 再重启。验证是否真的开了:

bash 复制代码
# dmesg | grep -i iommu | head -3
[    0.005074] ACPI: DMAR 0x0000000078459000 000050 (v01 INTEL  EDK2     00000002      01000013)
[    0.012395] DMAR: IOMMU enabled
[    1.913192] DMAR: Host address width 39

看到 DMAR: IOMMU enabled 就说明硬件和内核都准备好了。

3.2 显卡必须独占一个 IOMMU 组

这是能不能直通的决定性一步。IOMMU 是以"组"为单位隔离的,如果显卡所在的组里还混着别的设备(比如某块网卡、某个桥),那要么整组都直通,要么就得靠 ACS 补丁把组拆开------后者又依赖主板 BIOS 是否支持。

先看显卡本身:

bash 复制代码
# lspci -nn | grep -i nvidia
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation AD102 [GeForce RTX 4090] [10de:2684] (rev a1)
01:00.1 Audio device [0403]: NVIDIA Corporation AD102 High Definition Audio Controller [10de:22ba] (rev a1)

一块显卡在 PCI 上通常是两个功能:显示功能(01:00.0)和 HDMI/DP 音频功能(01:00.1)。

再看它所在的组里有哪些设备:

bash 复制代码
# IOMMU group = 13
# ls /sys/kernel/iommu_groups/13/devices/
0000:01:00.0
0000:01:00.1

组 13 里干干净净,就是显卡自己的两个功能------可以直接直通。

3.3 屏蔽 nouveau

开源的 nouveau 驱动会抢先认领 NVIDIA 显卡,必须提前屏蔽:

bash 复制代码
# cat /etc/modprobe.d/blacklist-nouveau.conf
blacklist nouveau
options nouveau modeset=0

宿主机和客户机两边都要做(客户机后面在安装脚本里一并处理)。


四、把显卡交给虚拟机

4.1 没有写死 vfio-pci:这次交给 libvirt 托管

网上常见的做法是提前把设备 ID 写进 /etc/modprobe.d/vfio.conf:

text 复制代码
options vfio-pci ids=10de:2684,10de:22ba

这样做等于开机就把显卡从宿主机手里拿走,宿主机再也用不了它------只适合"显卡永久归虚机"的场景。

本次要的是分时复用,所以反过来:不写 vfio 绑定,让 libvirt 在启动/关闭虚机时动态切换 。这台宿主机上 /etc/modprobe.d/ 里确实没有任何 vfio 配置,靠的是 XML 里 managed='yes' 这一个属性。

libvirt 的托管逻辑是:

  • 虚机启动 → 若设备当前挂在原生驱动上,先解绑,再绑到 vfio-pci,然后交给 QEMU;
  • 虚机关闭 → 解绑 vfio-pci,重新绑定回原来的驱动(这里是 nvidia)。

虚机运行时的实际状态:

bash 复制代码
# lspci -k -s 01:00.0
01:00.0 VGA compatible controller: NVIDIA Corporation AD102 [GeForce RTX 4090] (rev a1)
	Kernel driver in use: vfio-pci
	Kernel modules: nouveau, nvidia_drm, nvidia

Kernel driver in use: vfio-pci,说明宿主机的驱动已经松手了。

4.2 XML 里的两段 hostdev

virsh edit centos8,在 <devices> 段里加两个主机设备(用 virt-manager 的"添加硬件 → PCI 主机设备"效果一样),实际内容如下:

xml 复制代码
<hostdev mode='subsystem' type='pci' managed='yes'>
  <driver name='vfio'/>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
  </source>
  <alias name='hostdev0'/>
  <address type='pci' domain='0x0000' bus='0x07' slot='0x00' function='0x0'/>
</hostdev>
<hostdev mode='subsystem' type='pci' managed='yes'>
  <driver name='vfio'/>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x1'/>
  </source>
  <alias name='hostdev1'/>
  <address type='pci' domain='0x0000' bus='0x08' slot='0x00' function='0x0'/>
</hostdev>

几个字段:

字段 含义
mode='subsystem' type='pci' 直通的是 PCI 设备
managed='yes' 驱动绑定/解绑由 libvirt 自动完成(分时复用的关键)
<driver name='vfio'/> 用 vfio 后端
<source> 宿主机上的 PCI 地址:01:00.0 和 01:00.1
最后一个 <address> 该设备在客户机里 的 PCI 地址:07:00.0 和 08:00.0

4.3 三个容易踩的点

① 音频功能要一起直通。 只给 01:00.0、不给 01:00.1,客户机里会看到一个"残缺"的显卡设备,容易出现驱动报错、设备唤醒异常之类的怪问题。反正它们在同一个 IOMMU 组里,本来就是绑在一起的。

② 显卡是虚机里的"第二块显卡",主显示仍然走 virtio。 这台虚机的显示配置是:

xml 复制代码
<video>
  <model type='virtio' heads='1' primary='yes'/>
</video>
<graphics type='vnc' port='5900' autoport='yes' listen='127.0.0.1'/>

主显示是 virtio + VNC,4090 只是个从设备。这样一来有两个好处:

  • 虚机不需要从 4090 上引导,不用给显卡刷 vBIOS、不用指定 <rom file>、也不用 -vga none 之类的 QEMU 参数 (这台虚机的 XML 里也没有 <qemu:commandline> 段);
  • 装驱动时就算 X 起不来,VNC 照样能连上,不会把自己关在门外。

③ 客户机里的 PCI 地址和宿主机不一样,这是正常的。
#mermaid-svg-GBm9Cv3cojkLXOG2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GBm9Cv3cojkLXOG2 .error-icon{fill:#552222;}#mermaid-svg-GBm9Cv3cojkLXOG2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GBm9Cv3cojkLXOG2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .marker.cross{stroke:#333333;}#mermaid-svg-GBm9Cv3cojkLXOG2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GBm9Cv3cojkLXOG2 p{margin:0;}#mermaid-svg-GBm9Cv3cojkLXOG2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster-label text{fill:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster-label span{color:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster-label span p{background-color:transparent;}#mermaid-svg-GBm9Cv3cojkLXOG2 .label text,#mermaid-svg-GBm9Cv3cojkLXOG2 span{fill:#333;color:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .node rect,#mermaid-svg-GBm9Cv3cojkLXOG2 .node circle,#mermaid-svg-GBm9Cv3cojkLXOG2 .node ellipse,#mermaid-svg-GBm9Cv3cojkLXOG2 .node polygon,#mermaid-svg-GBm9Cv3cojkLXOG2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .rough-node .label text,#mermaid-svg-GBm9Cv3cojkLXOG2 .node .label text,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape .label,#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-GBm9Cv3cojkLXOG2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .rough-node .label,#mermaid-svg-GBm9Cv3cojkLXOG2 .node .label,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape .label,#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape .label{text-align:center;}#mermaid-svg-GBm9Cv3cojkLXOG2 .node.clickable{cursor:pointer;}#mermaid-svg-GBm9Cv3cojkLXOG2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .arrowheadPath{fill:#333333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GBm9Cv3cojkLXOG2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GBm9Cv3cojkLXOG2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GBm9Cv3cojkLXOG2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster text{fill:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 .cluster span{color:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GBm9Cv3cojkLXOG2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GBm9Cv3cojkLXOG2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape p,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GBm9Cv3cojkLXOG2 .icon-shape .label rect,#mermaid-svg-GBm9Cv3cojkLXOG2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GBm9Cv3cojkLXOG2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GBm9Cv3cojkLXOG2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GBm9Cv3cojkLXOG2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} hostdev managed=yes
hostdev managed=yes
宿主机 0000:01:00.0

显示功能
客户机 0000:07:00.0
宿主机 0000:01:00.1

音频功能
客户机 0000:08:00.0

从宿主机 01:xx 变成客户机的 07:xx,只说明它在虚机自己的 PCI 拓扑里被挂到了另一条总线上,跟直通是否成功无关。客户机里认卡、装驱动,一律以 lspci 在客户机内部看到的地址为准。


五、客户机里装驱动(全程离线)

虚机里的系统是 CentOS Linux 8.2.2004(内核 4.18.0-193.el8.x86_64,SELinux Enforcing),而且没有外网,所以驱动和依赖都得本地解决。

5.1 先确认客户机认出了显卡

bash 复制代码
# lspci -nn | grep -i nvidia
07:00.0 VGA compatible controller [0300]: NVIDIA Corporation Device [10de:2684] (rev a1)
08:00.0 Audio device [0403]: NVIDIA Corporation Device [10de:22ba] (rev a1)

# lspci -k -s 07:00.0
07:00.0 VGA compatible controller: NVIDIA Corporation Device 2684 (rev a1)
	Kernel driver in use: nvidia
	Kernel modules: nouveau, nvidia_drm, nvidia

(这里的 Kernel driver in use: nvidia 是驱动装好之后的复查,安装前这个字段是空的。只要前一条 lspci 能列出设备,就说明直通本身通了。)

5.2 把驱动包送进去

客户机没网,驱动包(NVIDIA-Linux-x86_64-580.95.05.run)需要人工送进去。这次的做法是做了一张临时数据 ISO,挂成虚机的光驱:

bash 复制代码
# virsh domblklist centos8
 目标   源
-------------------------------------------------
 vda    /var/lib/libvirt/images/centos8.qcow2
 vdb    /var/lib/libvirt/images/centos8-dvd.iso
 vdc    /var/lib/libvirt/images/gpu-stage.iso

gpu-stage.iso(379M,卷标 GPU-STAGE)里只放了两样东西:

text 复制代码
install-gpu.sh
NVIDIA-Linux-x86_64-580.95.05.run

客户机里挂载:

bash 复制代码
mkdir -p /mnt/gpustage
mount -L GPU-STAGE /mnt/gpustage

用 ISO 的好处是只读、不依赖网络、用完拔掉就行,比开临时共享目录省心。

5.3 依赖:内核头文件 + 编译工具链,也从本地装

驱动要现场编译内核模块,所以需要与当前运行内核完全一致 的 kernel-devel。虚机里另外挂了一张 CentOS 8 安装 DVD(centos8-dvd.iso),直接用 ISO 里的仓库当本地源。整理后的脚本如下:

bash 复制代码
#!/bin/bash
# centos8 虚机内为「直通进来的 RTX 4090」安装 NVIDIA 驱动
# 全程离线:CentOS 8.2 安装 DVD 作本地 yum 源,驱动来自本 ISO
set -u
LOG=/var/log/nvidia-install.log
[ "$(id -u)" = 0 ] || { echo "请用 root 执行"; exit 1; }

# 图形终端里跑会被 systemctl isolate 杀掉 -> 用 systemd-run 换到独立 cgroup
if [ -n "${DISPLAY:-}" ] && [ -z "${INVOCATION_ID:-}" ] && [ "${1:-}" != "--bg" ]; then
  systemd-run --unit=gpu-install --collect --description="NVIDIA driver install" \
      bash "$0" --bg || exit 1
  echo "已启动。看进度: tail -f $LOG ;完成后 reboot"
  exit 0
fi

exec >>"$LOG" 2>&1

STAGE=/mnt/gpustage
mountpoint -q "$STAGE" || mount -L GPU-STAGE "$STAGE" 2>/dev/null
RUN=$(ls "$STAGE"/NVIDIA-Linux-x86_64-*.run 2>/dev/null | head -1)
[ -n "$RUN" ] || { echo "!! $STAGE 下找不到 NVIDIA-Linux-x86_64-*.run"; exit 1; }
echo "驱动包: $RUN"

# --- 0. 检查直通进来的 GPU ---
lspci -nn | grep -i nvidia || { echo "!! 没看到 NVIDIA PCI 设备,先确认虚机 XML 里有 hostdev"; }

# --- 1. 挂载 CentOS 安装 DVD 作本地 yum 源 ---
DVD=/mnt/dvd; mkdir -p "$DVD"
if ! mountpoint -q "$DVD"; then
  while read -r dev; do
    lab=$(blkid -o value -s LABEL "$dev" 2>/dev/null)
    case "$lab" in CentOS*8*dvd*) mount -o ro "$dev" "$DVD" && break;; esac
  done < <(ls /dev/sr* /dev/sd[a-z] /dev/vd[a-z] 2>/dev/null)
fi

cat > /etc/yum.repos.d/local-dvd.repo <<EOF
[dvd-baseos]
baseurl=file://$DVD/BaseOS
gpgcheck=0
enabled=1
[dvd-appstream]
baseurl=file://$DVD/AppStream
gpgcheck=0
enabled=1
EOF
for r in /etc/yum.repos.d/*.repo; do
  [ "$r" = /etc/yum.repos.d/local-dvd.repo ] || sed -i 's/^enabled=1/enabled=0/' "$r"
done

# --- 2. 离线安装编译工具链 + 内核头文件 ---
dnf -y --disablerepo='*' --enablerepo='dvd-*' install gcc make binutils \
    kernel-devel kernel-headers elfutils-libelf-devel pciutils 2>&1 | tail -8
# 头文件对齐当前运行内核
dnf -y --disablerepo='*' --enablerepo='dvd-*' install \
    "kernel-devel-$(uname -r)" "kernel-headers-$(uname -r)" 2>&1 | tail -3
[ -e "/lib/modules/$(uname -r)/build" ] || { echo "内核头文件没装好"; exit 1; }

# --- 3. 屏蔽 nouveau ---
printf 'blacklist nouveau\noptions nouveau modeset=0\n' > /etc/modprobe.d/blacklist-nouveau.conf
modprobe -r nouveau 2>/dev/null
dracut -f --regenerate-all

# --- 4. 停图形界面(装驱动必须) ---
systemctl isolate multi-user.target 2>/dev/null || systemctl stop gdm 2>/dev/null

# --- 5. 安装 NVIDIA 驱动 ---
sh "$RUN" --silent --no-dkms --disable-nouveau --no-nouveau-check

装完的依赖可以复查:

bash 复制代码
# rpm -q kernel-devel kernel-headers gcc make dkms
kernel-devel-4.18.0-193.el8.x86_64
kernel-headers-4.18.0-193.el8.x86_64
gcc-8.3.1-5.el8.0.2.x86_64
make-4.2.1-10.el8.x86_64
未安装软件包 dkms

第 4 步那个 systemctl isolate multi-user.target 有个隐藏问题:驱动安装是在图形终端里敲的,一停图形界面,自己也被停掉了 ,安装会半途夭折。脚本里用 systemd-run 把任务扔进独立的 systemd 单元(独立 cgroup),停图形界面时就杀不到它了。

脚本第 5 步写的是 --silent,最后落地那次用的是 --ui=none -q(下一节的日志里有记录)。两种写法目标一致:把安装器的交互界面关掉。

5.4 真正的安装命令与几个参数

安装器会把自己的命令行完整记进日志,最终成功那次是这样的:

bash 复制代码
# sed -n '1,16p' /var/log/nvidia-installer.log
nvidia-installer log file '/var/log/nvidia-installer.log'
creation time: Mon Oct 26 23:24:10 2026
installer version: 580.95.05

nvidia-installer command line:
    ./nvidia-installer
    --ui=none
    -q
    --no-dkms
    --no-nouveau-check
    --no-x-check

一条条说明:

参数 作用 为什么这次要用
--ui=none 不用 ncurses 界面,改用内置的纯文本流界面 必须,见下面踩坑
-q 安静模式,少打日志 日志太乱不好定位问题
--no-dkms 不注册 DKMS 这台的客户机内核版本固定,不需要 DKMS 自动重编
--no-nouveau-check 跳过 nouveau 检查 前面已经手工屏蔽过了
--no-x-check 安装期间不去检查/关闭 X 虚机里 X 是开着的(VNC 桌面),不想被它干预

装完的收尾日志:

text 复制代码
-> Installation of the NVIDIA Accelerated Graphics Driver for Linux-x86_64 (version: 580.95.05) is now complete.

然后 reboot。

5.5 验证

bash 复制代码
# nvidia-smi
Tue Oct 27 03:22:41 2026
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 580.95.05              Driver Version: 580.95.05      CUDA Version: 13.0     |
+-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 4090        Off |   00000000:07:00.0 Off |                  Off |
| 31%   57C    P0              1W /  450W |       0MiB /  24564MiB |      0%      Default |
+-----------------------------------------+------------------------+----------------------+

| Processes:                                                                              |
|  No running processes found                                                             |
+-----------------------------------------------------------------------------------------+

Bus-Id 是 00000000:07:00.0,正好对应 XML 里给它在客户机中分配的那个地址。驱动版本 580.95.05,与宿主机侧同版本。

设备节点和模块:

bash 复制代码
# ls -l /dev/nvidia*
crw-rw-rw-. 1 root root 195,   0 10月 26 23:36 /dev/nvidia0
crw-rw-rw-. 1 root root 195, 255 10月 26 23:35 /dev/nvidiactl
crw-rw-rw-. 1 root root 195, 254 10月 26 23:36 /dev/nvidia-modeset
crw-rw-rw-. 1 root root 237,   0 10月 26 23:36 /dev/nvidia-uvm
crw-rw-rw-. 1 root root 237,   1 10月 26 23:36 /dev/nvidia-uvm-tools

# modinfo nvidia | grep -E '^(filename|version)'
filename:       /lib/modules/4.18.0-193.el8.x86_64/kernel/drivers/video/nvidia.ko
version:        580.95.05

5.6 让 nvidia_uvm 开机自动加载

nvidia_uvm 是 CUDA 做统一内存管理要用的模块,不加载的话 CUDA 程序会在初始化阶段报错。写一个配置让它开机自动加载:

bash 复制代码
# cat /etc/modules-load.d/nvidia.conf
nvidia
nvidia_uvm

六、分时切换:一条命令换边

6.1 切换脚本

需求就三件事:把 GPU 给虚机、把 GPU 收回宿主机、随时看当前归谁。脚本 /usr/local/bin/gpu-switch 全文如下:

bash 复制代码
#!/bin/bash
# 4090 分时切换:宿主机  <->  虚拟机 centos8
#   gpu-switch vm          把 4090 交给虚机(停桌面 → 起虚机,libvirt 自动切驱动)
#   gpu-switch vm -f       同上,但遇到占用进程直接强杀
#   gpu-switch host        把 4090 收回宿主机(关虚机 → 回桌面)
#   gpu-switch status      查看当前状态
set -u
VM=centos8
DEVS="0000:01:00.0 0000:01:00.1"

drv() { local d; d=$(readlink -f "/sys/bus/pci/devices/$1/driver" 2>/dev/null); [ -n "$d" ] && basename "$d" || echo "(未绑定)"; }

blk() { # 找占用 /dev/nvidia* 的进程
  fuser -v /dev/nvidia0 /dev/nvidiactl /dev/nvidia-uvm 2>&1 | sed -n '2,$p'
}

case "${1:-}" in
vm)
  echo "[1/4] 停图形界面,释放 4090 ..."
  systemctl isolate multi-user.target 2>/dev/null
  sleep 2
  echo "[2/4] 检查残留占用 ..."
  if pgrep -x -f "VLLM::EngineCore" >/dev/null 2>&1 || pgrep -x -f "vllm" >/dev/null 2>&1; then
    echo "  !! 仍有 vLLM 进程占用 GPU: $(pgrep -f 'VLLM::EngineCore|vllm' | tr '\n' ' ')"
    if [ "${2:-}" = "-f" ]; then
      echo "  -f 已指定,强制结束 ..."
      pkill -f "VLLM::EngineCore"; pkill -f "vllm"; sleep 3
    else
      echo "  !! 请先手工停掉,或执行: $0 vm -f"; exit 1
    fi
  fi
  h=$(blk); [ -n "$h" ] && { echo "  !! 仍有进程持有 /dev/nvidia*:"; echo "$h"; exit 1; }
  echo "[3/4] 启动虚机 $VM(libvirt 自动把 4090 切到 vfio-pci)..."
  virsh start "$VM" || exit 1
  echo "[4/4] 等待驱动切换 ..."; sleep 3
  exec "$0" status
  ;;
host)
  echo "[1/3] 关闭虚机 $VM ..."
  [ "$(virsh domstate "$VM")" = "运行" ] && virsh shutdown "$VM"
  for i in $(seq 1 90); do [ "$(virsh domstate "$VM" 2>/dev/null)" = "关闭" ] && break; sleep 2; done
  echo "  虚机状态: $(virsh domstate "$VM")"
  echo "[2/3] 4090 已交还宿主机 (nvidia 驱动)"
  echo "[3/3] 恢复图形界面 ..."
  systemctl isolate graphical.target 2>/dev/null
  sleep 3
  nvidia-smi --query-gpu=name,memory.used --format=csv 2>&1 | tail -2
  echo "提示: 宿主上的 vLLM 需手工重新拉起"
  ;;
status)
  echo "--- PCI 驱动绑定 ---"
  for d in $DEVS; do printf "  %s -> %s\n" "$d" "$(drv "$d")"; done
  echo "--- 虚机 $VM: $(virsh domstate "$VM" 2>&1) ---"
  echo "--- 目前的 GPU 归属 ---"
  if [ "$(drv 0000:01:00.0)" = "vfio-pci" ]; then
    echo "  => 归 虚机 $VM"
  elif [ "$(drv 0000:01:00.0)" = "nvidia" ]; then
    echo "  => 归 宿主机"
  else
    echo "  => 未绑定/其他"
  fi
  ;;
*)
  echo "用法: $0 {vm [-f]|host|status}"; exit 1;;
esac

它把"三件麻烦事"都包进去了:

  1. 停图形界面 。这台机器只有 4090 一块显示设备,桌面本身就跑在它上面。要给虚机用,宿主机必须先退回 multi-user.target,否则 nvidia 驱动一直被 Xorg 攥着;
  2. 检查占用 。宿主机上还有 CUDA 任务(脚本里针对 vLLM 做了检查)。有进程握着 /dev/nvidia0 不放时,强行解绑会失败甚至把进程搞崩,所以先拦一道,要么人工清、要么加 -f 强制杀;
  3. 等待状态收敛 。关虚机用 virsh shutdown 走优雅关闭,然后每 2 秒轮询一次域名状态,最多等 90 次,等状态真的变成"关闭"再去恢复桌面。

切换流程:
#mermaid-svg-jev4JDcumPBgy4i8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jev4JDcumPBgy4i8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jev4JDcumPBgy4i8 .error-icon{fill:#552222;}#mermaid-svg-jev4JDcumPBgy4i8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jev4JDcumPBgy4i8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jev4JDcumPBgy4i8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jev4JDcumPBgy4i8 .marker.cross{stroke:#333333;}#mermaid-svg-jev4JDcumPBgy4i8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jev4JDcumPBgy4i8 p{margin:0;}#mermaid-svg-jev4JDcumPBgy4i8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster-label text{fill:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster-label span{color:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster-label span p{background-color:transparent;}#mermaid-svg-jev4JDcumPBgy4i8 .label text,#mermaid-svg-jev4JDcumPBgy4i8 span{fill:#333;color:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .node rect,#mermaid-svg-jev4JDcumPBgy4i8 .node circle,#mermaid-svg-jev4JDcumPBgy4i8 .node ellipse,#mermaid-svg-jev4JDcumPBgy4i8 .node polygon,#mermaid-svg-jev4JDcumPBgy4i8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jev4JDcumPBgy4i8 .rough-node .label text,#mermaid-svg-jev4JDcumPBgy4i8 .node .label text,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape .label,#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-jev4JDcumPBgy4i8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jev4JDcumPBgy4i8 .rough-node .label,#mermaid-svg-jev4JDcumPBgy4i8 .node .label,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape .label,#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape .label{text-align:center;}#mermaid-svg-jev4JDcumPBgy4i8 .node.clickable{cursor:pointer;}#mermaid-svg-jev4JDcumPBgy4i8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jev4JDcumPBgy4i8 .arrowheadPath{fill:#333333;}#mermaid-svg-jev4JDcumPBgy4i8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jev4JDcumPBgy4i8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jev4JDcumPBgy4i8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jev4JDcumPBgy4i8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jev4JDcumPBgy4i8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jev4JDcumPBgy4i8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jev4JDcumPBgy4i8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster text{fill:#333;}#mermaid-svg-jev4JDcumPBgy4i8 .cluster span{color:#333;}#mermaid-svg-jev4JDcumPBgy4i8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jev4JDcumPBgy4i8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jev4JDcumPBgy4i8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape p,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jev4JDcumPBgy4i8 .icon-shape .label rect,#mermaid-svg-jev4JDcumPBgy4i8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jev4JDcumPBgy4i8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jev4JDcumPBgy4i8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jev4JDcumPBgy4i8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有
没有
gpu-switch vm
isolate multi-user.target

停掉宿主机桌面
还有进程占用

/dev/nvidia* 吗
报错退出

或 -f 强制杀掉
virsh start centos8
libvirt 自动把 01:00.0/.1

从 nvidia 切到 vfio-pci
status 显示:归虚机

6.2 实测状态

虚机运行中执行 gpu-switch status:

text 复制代码
--- PCI 驱动绑定 ---
  0000:01:00.0 -> vfio-pci
  0000:01:00.1 -> vfio-pci
--- 虚机 centos8: 运行 ---
--- 目前的 GPU 归属 ---
  => 归 虚机 centos8

同一时刻,在宿主机上敲 nvidia-smi 会是这个结果:

text 复制代码
Failed to initialize NVML: No supported GPUs were found
Unable to determine the number of GPUs

这不是故障,正是"显卡已经不在宿主机手里"的证据。收回来(gpu-switch host)之后 nvidia-smi 就恢复正常了。


七、踩坑记录

① 安装器在非交互终端下自旋,把提问吞了

第一次跑 ./nvidia-installer 没带 --ui=none,安装器进了 ncurses 界面。因为是在脚本/串口这类非 tty 的 stdin 下执行的,界面在那儿空转,后面"是否跳过 sanity check"的提问也没人应答,直接失败。改成 --ui=none 用内置文本流界面,一次通过。远程/脚本里装驱动,--ui=none 是必备项。

② --no-dkms 的代价

省掉了 DKMS 注册,代价是:客户机内核一升级,kernel-devel 和已编译的模块就对不上了,得重装驱动。如果这个虚机会滚动更新内核,建议改成装 DKMS,或者干脆锁住内核版本。

③ 装驱动必须在非图形目标下进行

Xorg 开着的时候装 NVIDIA 驱动,安装器会尝试接管显示,很容易装一半失败。脚本里先 systemctl isolate multi-user.target;但要注意别把自己也一起停了 (见 5.3 的 systemd-run 处理)。

④ 只直通显示功能不够

只给 01:00.0 不给音频功能 01:00.1,客户机里会是个不完整的 NVIDIA 设备。它们在同一个 IOMMU 组,一起直通最省事。

⑤ 直通期间宿主机的 nvidia-smi 会"报错"

Failed to initialize NVML: No supported GPUs were found 是正常现象,别当成驱动坏了去重装。判断归属看 gpu-switch status 里的驱动绑定状态更准。

⑥ 切换失败十有八九是"有人还在用显卡"

宿主机上跑着 CUDA 服务(比如 vLLM)时,进程会攥着 /dev/nvidia0。这时候解绑驱动会失败。脚本先用 fuser -v /dev/nvidia0 ... 拦一道,确认真要强杀再上 -f。

⑦ 离线机器装依赖,ISO 就是最好的源

客户机没网,kernel-devel、gcc、make 这些都从 CentOS 8 安装 DVD 里取:把 ISO 挂上,写一个 file:// 的 repo 文件,再用 --disablerepo='*' --enablerepo='dvd-*' 把其他源全禁掉,避免 dnf 去连不存在的外网源、卡在超时上。

⑧ 装完依赖记得对齐内核版本

kernel-devel 装成了比当前运行内核更新的版本,编译照样失败。脚本里显式加了一步 dnf install "kernel-devel-$(uname -r)",并检查 /lib/modules/$(uname -r)/build 是否存在。


八、总结

整套方案没有动 vfio.conf,也没有写自定义的 QEMU 参数,配置量其实很小:

位置 改什么
宿主机内核参数 intel_iommu=on iommu=pt
宿主机 /etc/modprobe.d/ 屏蔽 nouveau
虚机 XML 两段 hostdev(managed='yes')
客户机 装 580.95.05 驱动 + modules-load.d/nvidia.conf
日常操作 gpu-switch vm / host / status

回过头看,真正花时间的不是 XML,而是这几件"不在文档上"的事:确认显卡独占 IOMMU 组、驱动必须离线装、安装器在非交互终端下的表现、以及"切换前先确认没人在用卡"。

适用边界要说清楚:这是"分时复用",不是"同时使用"。同一时刻显卡只能归一边;而且因为这台机器只有一块显示设备,交给虚机时宿主机桌面必须停掉------如果你的机器有核显、桌面跑在核显上,那切换时就不用停图形界面,体验会好很多。


附录:环境信息

项目 宿主机 客户机(虚拟机)
系统 Kylin Linux Advanced Server V11 (Swan25) CentOS Linux 8.2.2004
内核 6.6.0-32.24 4.18.0-193.el8
虚拟化 libvirt 9.10.0 / QEMU 8.2.0,机型 pc-q35-8.2,CPU host-passthrough 8 vCPU / 8 GiB
显卡 RTX 4090(10de:2684)+ 音频(10de:22ba),IOMMU 组 13 07:00.0 / 08:00.0
驱动 580.95.05(归虚机时解绑) 580.95.05(CUDA 13.0)
显示 无核显,桌面在 4090 上 virtio 主显示 + VNC
相关推荐
Eloudy3 小时前
从 huggingFace 下载大模型命令备忘
llm·gpu·rdma
Smoothcloud润云19 小时前
GPU远程纳管安全吗?最小权限、网络边界与日志审计实战
服务器·网络·人工智能·安全·gpu算力·gpu
小毕超1 天前
从推理到训练:Intel Ultra 核显 GPU 加速 YOLO 模型训练
pytorch·yolo·gpu·intel
墨抒颖 msy.plus3 天前
矩阵、注意力与 GPU:拆解 DeepGEMM 与 FlashMLA
人工智能·llm·gpu·源码解析·1024程序员节·deepseek
本地化文档4 天前
cutile-rs-book-l10n
rust·gpu·nvidia·cuda·sphinx·crowdin
Eloudy5 天前
IB Specification Vol 1 Chapter 3 体系结构与 rdma-core-2410mlnx54 实现关联分析
网络·gpu·rdma
xbzb6 天前
KVM 环境搭建:系统自带组与本地 yum 源,两条路一次走通
kvm·yum·libvirt·搭建·两条路·自带
Eloudy6 天前
全文 - 第03章 - 架构概述 - InfiniBand Specification release 1.4
架构·gpu
Eloudy6 天前
全文 - 第01章 - 简介 - InfiniBand Specification release 1.4
gpu