Virtio-nvgpu:在KVM虚拟机内部实现接近原生的NVIDIA GPU访问
摘要
Virtio-nvgpu实现了在KVM虚拟机内部接近原生的NVIDIA GPU访问,在帧渲染方面实现了与裸机相比仅2%以内的性能差距,并支持多个虚拟机之间的高效GPU共享。
查看缓存全文
缓存时间: 2026/09/24 04:02
nestrilabs/virtio-nvgpu
来源:https://github.com/nestrilabs/virtio-nvgpu
virtio-nvgpu
在 KVM 客户机内实现接近原生的 NVIDIA GPU 访问。客户机渲染性能与宿主机裸机运行相比在 2% 误差范围内,且 CPU 开销相同。
virtio-nvgpu 在 驱动程序 ABI 级别 转发 NVIDIA 内核驱动程序的 ioctl 调用,在 Linux 客户机和宿主机之间直接传递,完全绕过了 API 层级的翻译。客户机运行的是未经修改的 NVIDIA 原生用户模式驱动程序——使用相同的库、相同的 Vulkan 和 NVENC,与同一块显卡通信。
其目标是 无头流媒体:客户机虚拟机内的合成器负责在 GPU 上渲染、合成和编码帧,然后输出压缩后的视频。客户机无需显示器,宿主机保留对显卡的控制。
当前状态
它已可用,并经过实际测量。 一个 Wayland 客户端在客户机内运行,捕获层在游戏自身的设备上进行编码,H.264 视频流从另一端输出——618 帧数据被 ffmpeg 成功解码,未发生错误。
在 RTX 3060(驱动程序 595.99.02)上测量,客户机与 同一台宿主机的裸机环境 对比,运行相同的无头 Vulkan 负载:
| 宿主机处理单帧耗时 | 客户机帧耗时 | |
|---|---|---|
| 39 ms | -0.4% | 比裸机更快,在噪声误差范围内 |
| 9.9 ms | -0.7% | |
| 2.0 ms | +1.7% | |
| 0.5 ms | +7.1% | 一次唤醒成本约 0.02 ms,而帧时间仅为一半 |
| 0.05 ms | +40.8% |
当每帧处理时间高于约 2 毫秒时——这涵盖大多数游戏绘制的每一帧——客户机性能与裸机相比在 2% 误差范围内。 低于此阈值时,等待 GPU 的开销开始显现,相对于几乎不存在的帧成本变得显著。
CPU 开销是另一半考量,因为只有当客户机开销足够低时,共享 GPU 才有价值。以约 100 fps 运行 12 秒,一个客户机的情况:
| CPU 占用时间 | |
|---|---|
| 宿主机裸机 | 0.40 s |
| 客户机 | 0.37 s |
客户机的成本与宿主机相同。 在渲染循环中没有任何转发开销,因为根本无需转发:NVIDIA 用户模式驱动程序通过其已映射的内存提交命令,而该内存就是宿主机的内存。在超过 813,691 帧中,后端仅处理了 13,792 条消息——平均每 59 帧一次跨越,且几乎全部是设备初始化设置。
完整方法、原始运行数据以及这些数字 未能 证明的内容,请参阅:BENCHMARKS.md。
单卡多客户机
四台客户机运行在同一张 RTX 3060 上,每台负载相同:25.84、26.49、25.57、25.79 fps ——总计 103.7 fps,与单客户机时的 102.9 fps 相近——p50 帧时间分别为 39.165、39.164、39.168 和 39.165 ms。随着客户机数量增加,总帧率几乎不变,分配均匀度精确到小数点后四位。
四台客户机同时正确渲染,并且四台 同时进行 H.264 编码,每台精确控制在 60 Hz,未触发 NVENC 会话数限制。
四台是已测试的数量,而非发现的上限。
驱动程序版本
已在 595.99.02 上测量;A2000 显卡使用 615.71.09 驱动程序可以渲染,但未进行基准测试。内置的 ABI 配置文件版本:535.129.03、580.178.04、595.71.05,支持范围按区间匹配,拒绝任何早于第一个版本的驱动程序。详情见下文。
已知可用功能
- 客户机枚举显卡 ——
nvidia-smi报告真实的功耗和显存,deviceUUID是宿主机的 - Vulkan 渲染:
vulkaninfo正常退出,离屏绘制像素正确 - Wayland 客户端通过客户机内的合成器呈现
- 通过 Vulkan Video 使用 NVENC,在客户端自身设备上编码
- 导入的缓冲区是宿主机的内存,通过共享窗口映射
尚未实现的功能
- 超过四台客户机,或运行比 720p vkcube 更高负载的场景。四台可以均匀共享显卡;八台尚未尝试。
- 两张显卡、两个驱动程序版本。 RTX 3060 / 595.99.02 是数据来源;RTX A2000 / 615.71.09 可以渲染但未进行基准测试。
- CUDA 已转发但仅限枚举,未经测试;沙箱、按版本驱动的共享以及多租户封装尚未构建。
仓库布局
四个组件,三个许可区域。这种划分是有意为之:客户机半边必须是 GPL 协议才能接触内核符号,宿主机半边应采用宽松许可以便他人基于其构建,而两边共享的定义必须能被双方包含。
| 目录 | 许可证 | 内容说明 |
|---|---|---|
driver/ | GPL-2.0 | 客户机内核模块。注册 /dev/nvidia*,通过 virtqueue 转发 ioctl 和 mmap。刻意不包含 ABI 感知逻辑。 |
device/ | Apache-2.0 | virtio 设备,作为一个 Rust crate,依赖列表中不含 VMM。所有 VMM 相关部分都是 trait。 |
isolate/ | Apache-2.0 | 设计文档,尚无代码。 将作为沙箱化的每客户机辅助进程,持有真正的设备文件描述符。目前后端自身在 VMM 进程中持有它们。 |
gen/ | — | 生成的 ABI 表。已提交且可重现。 |
protocol/ | BSD-3-Clause OR GPL-2.0+ | 供双方共享的线路格式和 ABI 定义。采用双重许可,使 GPL 驱动程序和 Apache crate 可以包含相同的头文件。 |
布局遵循 chromeos/virtio-media (https://chromium.googlesource.com/chromiumos/platform/virtio-media/),该项目解决了相同的问题——一个仓库包含 GPL 客户机驱动程序和宽松许可、VMM 无关的设备 crate。
如何从 VMM 使用
device/ 不依赖任何虚拟机监控程序。VMM 通过实现一组小型 trait 来采用该设备——将描述符链实现为 Read/Write、事件队列、客户机内存映射、宿主机内存映射——无需修改 crate 即可获得完整的设备功能。可选功能会在不支持时优雅降级,而非构建失败,因此 VMM 可以在支持所有功能之前就采用它。
缓冲区和窗口簿记逻辑位于 device/ 中。VMM 仅提供原始的映射和取消映射操作,别无其他。
有一项 不会 是 trait:隔离机制。预期设计是为每个客户机进程运行一个沙箱化的辅助进程,因此最终采用它意味着需要继承一个 进程模型,而不仅仅是库依赖。该辅助进程尚未编写——目前后端自身持有设备描述符——isolate/ 是设计文档所在地,在完成编写前仅此而已。
设计动机
我们想要的流媒体管线
客户机 VM(无头,无物理显示器)
──────────────────────────────────────────
游戏 / 应用程序
│ Vulkan 或 OpenGL
▼
Wayland 合成器(客户机侧)
│ 合成所有窗口
│ CUDA 零拷贝导入合成帧
▼
NVENC 硬件编码器(客户机侧)
│ H.264 / H.265 比特流(每帧约 100 KB)
▼
流传输到远程客户端
整个 渲染 → 合成 → 编码 管线 在 GPU 上、在客户机内 运行。只有压缩后的比特流离开。这要求客户机对 GPU 资源拥有 真正的、驱动程序级别的访问权限:缓冲区句柄、栅栏、CUDA 设备指针、NVENC 会话。
为何现有方案不足
virtio-gpu + Venus(API 级别翻译)。 Venus 在客户机中序列化每个 Vulkan 或 OpenGL 调用,通过 virtio 传输,然后在宿主机端重放。对于此用例有三个问题:
- 延迟在绘制调用密集型负载上累积。 游戏每帧发出 1,000–5,000 个绘制调用,外加绑定、描述符更新和渲染通道转换,每个都需要单独序列化和重放。在 60 fps 下,帧预算为 16.6 毫秒;1–3 毫秒的序列化意味着在任何 GPU 工作之前就损失了 6–18% 的时间。
- CPU 开销显著。 序列化、传输和重放消耗宿主机 CPU,而这些 CPU 资源本是应用程序所需。在按计算资源计费且资源有限的环境中,这种浪费直接影响产出。
- 客户机侧编码不可行。 GPU 缓冲区由 宿主机 拥有。客户机合成器无法看到或导入它们,因此无法在客户机内将
CUdeviceptr指向一个由 Venus 管理的缓冲区——这意味着必须进行完整的 CPU 回读和复制才能实现 NVENC。
DRM 原生上下文(Intel / AMD)。 客户机运行真实的 Mesa 驱动程序,在本地构建命令缓冲区,仅提交操作跨越边界。客户机侧的缓冲区所有权和编码工作正常。NVIDIA 平台不存在此方案。
VFIO 直通。 在客户机中提供原生性能和完整的驱动程序栈,但它将整个 GPU 专用于一个虚拟机。在多租户环境中,这通常不是可行选项。
virtio-nvgpu 的不同之处
翻译发生在 内核驱动程序 级别(ioctl 到 /dev/nvidia*),而非 图形 API 级别。客户机运行 NVIDIA 真实的用户模式库,这些库在 客户机本地 构建 GPU 命令缓冲区——单个绘制调用永远不会被序列化:
Venus virtio-nvgpu
────────────── ──────────────────────
每个绘制调用: 序列化 + 本地函数调用
传输 + (无 VM 退出)
反序列化 +
重放
每帧边界 ~2,000 条消息 ~5–20 条消息
跨越次数 (每个 API 调用一条) (队列提交 + 分配)
GPU 命令 重放后在宿主机端生成 在客户机内由 NVIDIA
缓冲区 自身的编译器生成
CPU 开销 序列化 + 渲染开销几乎为零
反序列化 (仅 ioctl 转发)
客户机缓冲区 宿主机拥有缓冲区 客户机拥有缓冲区
所有权 合成器无法跟踪 合成器完全可见并可控制
客户机 NVENC 不可行 可用(真正的 CUDA 互操作)
工作原理
客户机内核驱动程序。 注册 /dev/nvidiactl、/dev/nvidia0...N 和 /dev/nvidia-uvm。执行 ioctl() 时,它将请求序列化到一个控制 virtqueue 上。执行 mmap() 时,它使用正确的缓存属性将相应的共享内存区域映射到调用进程中。它复制原始字节,不做任何 ABI 决策。
设备 crate。 接收请求,将客户机句柄映射到宿主机设备文件描述符,对 ioctl 参数进行 ABI 感知的翻译——重写嵌入的指针和文件描述符——并针对宿主机的设备发出调用。缓冲区和窗口簿记逻辑在此处。
事件。 第二个 virtqueue 反向运行。宿主机监视其打开的每个描述符,并在其可读时发出通知,这是唤醒等待 GPU 的客户机的方式。否则客户机根本无法等待——它轮询一个内核报告为永久就绪的描述符,并空转。
隔离机制——尚未构建。 计划是为每个客户机进程运行一个沙箱化的辅助进程,持有真正的设备文件描述符并以非特权方式执行 ioctl(2) 调用。目前后端自身在 VMM 进程中执行此操作。
isolate/ 包含设计文档,暂无代码。
┌─ 客户机 ────────────────────────────────────────────────┐
│ 应用程序 → NVIDIA Vulkan / GL / CUDA │
│ │ ioctl(/dev/nvidia*) │
│ driver/ (GPL) ▼ │
│ 序列化 → virtqueue │
│ mmap → 共享区域 │
└───────────────────────────┬─────────────────────────────┘
│ VM 退出
┌───────────────────────────▼─────────────────────────────┐
│ VMM(实现设备 trait) │
│ │
│ device/ (Apache-2.0) │
│ ├─ 客户机句柄 → 宿主机 FD │
│ ├─ 翻译嵌入的 FD 和指针 │
│ └─ 缓冲区 + 窗口簿记 │
│ │ │
│ └─ ioctl(宿主机 /dev/nvidia*) · mmap → 共享窗口 │
│ (计划包含每客户机非特权隔离进程, │
│ 当前运行的并非此方案) │
│ │
│ 宿主机 NVIDIA 驱动程序 → GPU │
└─────────────────────────────────────────────────────────┘
功能范围
支持范围
- Vulkan 渲染,包括向客户机内的合成器呈现——这需要
/dev/nvidia-drm和/dev/nvidia-modeset,两者均已实现且都不是显示器:它们是使缓冲区可共享的机制 - OpenGL 渲染(无头 EGL)
- CUDA 设备内存分配
- CUDA ↔ Vulkan/GL 互操作,零拷贝,GPU 侧指针
- 从 CUDA 设备指针进行 NVENC 编码;NVDEC 解码
不支持范围
cudaMallocManaged()/ 完整统一虚拟内存- 扫描输出。 无物理显示输出:流媒体盒上没有显示器,帧以视频而非线路上的像素形式离开
- MIG、SR-IOV
- 任意 NVIDIA 驱动程序版本——每个支持范围都是明确的,如同
nvproxy
性能
已在一张显卡上,通过一个合成负载进行测量——详见 BENCHMARKS.md 了解方法和原始运行数据,以及此测量 未涵盖 的内容(它不支持与其他任何虚拟机监控程序的比较,因为没有运行其他程序)。
| virtio-nvgpu,实测数据 | Venus,设计预期 | |
|---|---|---|
| GPU 受限(每帧 ≥2 ms) | 达到裸机的 98–100% | 90–97% |
| 非常轻的帧(≤0.5 ms) | 达到裸机的 93–71% | — |
| 渲染客户机的 CPU 成本 | 与裸机相同 | 高(序列化和重放) |
| 每帧宿主机跨越次数 | ~0.02 | 数千次 |
| 客户机侧 NVENC | 可用,零拷贝 | 不可行 |
差异是结构性的:Venus 每次 API 调用 都跨越 VM 边界,每帧数千次。virtio-nvgpu 每次 ioctl 跨越一次——而渲染循环不发出 ioctl,因为提交操作就是对映射内存的写入。在非常轻的帧下剩余的开销不是转发而是 等待:客户机为 GPU 休眠,唤醒成本约 0.02 毫秒,无论帧多小。
Venus 列是该项目的设计范围,而非此处测量的结果。
驱动程序版本
NVIDIA 的内核驱动程序 ABI 不稳定;ioctl 结构体布局在不同版本间会变化。支持范围是明确的,以下是完整列表。
内置 ABI 配置文件:
| 配置文件 | 覆盖范围 |
|---|---|
535.129.03 | 从 535.129.03 到下一个配置文件 |
580.178.04 | 从 580.178.04 到下一个配置文件 |
595.71.05 | 595.71.05 及更新版本 |
配置文件按键值范围而非单点匹配:两个配置文件之间的发行版使用较低版本的配置文件,任何比最后一个配置文件更新的驱动程序使用最后一个配置文件。任何 早于 535.129.03 的版本都会被拒绝,而不是猜测——转发一个从未见过的布局的 ioctl 会导致得到看似合理但错误的结果,而非报错。
因此,一个比最新配置文件新得多的驱动程序会被 接受,前提是假设其所需的一切未发生变化。这个假设正是新配置文件存在以替代的,也是当新驱动程序行为异常时首先怀疑的对象。
实际运行的驱动程序版本:
| 版本 | 显卡 | 运行情况 |
|---|---|---|
| 595.99.02 | RTX 3060 | 全部功能——渲染、呈现、编码,以及 BENCHMARKS.md 中的所有数据 |
| 615.71.09 | RTX A2000 | 可枚举和渲染;未进行基准测试,且自首次测试后未再次测试 |
两张显卡,两个版本,其中一个彻底测试。其他任何组合均未测试。
如何构建配置文件
成本是有界的,原因有三。配置文件按键值范围而非单点匹配,因此两个已知版本之间的发行版会选择较低的配置文件。结构体部分是从 NVIDIA 发布的 open-gpu-kernel-modules 在每个标签处 机械地推导 出来——为每个字段编译一个探测器,读回 sizeof 和 offsetof——而非手工转录。判断部分(哪些命令存在以及哪些是安全的)则跟踪 nvproxy 上游。
参见 gen/,以及其中的 supported_versions() 获取代码中的列表——该函数,而非此表格,才是决策依据。
先前工作
gVisor nvproxy——直接的灵感来源。它转发 N
相似文章
@QuixiAI: https://x.com/QuixiAI/status/2092804334417236135
这是 NVIDIA 开放 GPU 内核模块的一个分支,它支持消费级 GeForce GPU(3090、4090、5090)之间的 PCIe 点对点通信,允许直接通过 PCIe 进行数据传输,从而显著提升多 GPU 性能。
vgpu
vgpu 是一个针对 WebGPU 的 TypeScript 库,具有类型化着色器导入、一个轻量级的 GPU 优先 API,并兼容浏览器、无头 Node 和测试环境。
Linux补丁引入"KNOD",用于将内核网络卸载直接交给AMD GPU
Linux内核补丁引入了KNOD,这是一种用于将内核网络数据包卸载直接交给AMD GPU的机制,能够在无需用户空间依赖(如ROCm)的情况下加速数据包处理。该代码管理GPU队列,对每个数据包程序进行JIT编译,并完全从内核调度工作。
nbd-vram:在Linux上使用NVIDIA GPU的显存作为交换空间
nbd-vram是一个Linux工具,通过NBD协议和CUDA使用NVIDIA GPU显存作为交换空间,为搭载焊接式内存且无法升级的系统提供额外内存。
ZeroGPU
ZeroGPU是一个为AI推理设计的计算高效层,旨在优化GPU使用并降低成本。