在 KVM 客户机内获得近乎原生的 NVIDIA GPU 访问能力。客户机的渲染性能与宿主机裸金属运行相差在 2% 以内,且 CPU 开销相同。
virtio-nvgpu 在 驱动 ABI(Application Binary Interface,应用二进制接口)层面 转发 NVIDIA 内核驱动的 ioctl(设备驱动程序与用户空间之间的系统调用),完全绕过 API 层的转换。客户机中运行的是 NVIDIA 自家的用户态驱动,无需任何修改 —— 同样的库、同样的 Vulkan 和 NVENC,对接同一张显卡。
项目目标是 无显示器流传输(headless streaming):虚拟机内部的合成器(compositor)在 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.5 ms |
| 0.05 ms | +40.8% |
在每帧 2 ms 以上的场景(也就是游戏实际绘制的每一帧)中,客户机性能与裸金属相差在 2% 以内。 低于此阈值时,等待 GPU 的开销开始对这种极短帧时间产生明显影响。
CPU 占用是另一项关键考量,因为只有当客户机本身足够轻量时,共享 GPU 才有价值。在约 100 fps、无外部节流的情况下连续运行 12 秒,单个客户机的 CPU 占用为:
| CPU 耗时 | |
|---|---|
| 宿主机裸金属 | 0.40 s |
| 客户机 | 0.37 s |
客户机的 CPU 开销与宿主机裸金属相同。 在渲染循环中,转发本身不消耗任何 CPU,因为根本没有发生逐帧转发: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 完成;一张搭载 615.71.09 的 A2000 能够渲染但未进行基准测试。项目内随附了匹配的 ABI 配置:535.129.03、580.178.04、595.71.05,按版本区间进行匹配,比最早版本更老的请求会被拒绝。详见下文。
已验证可用的功能
- 客户机可枚举显卡 ——
nvidia-smi报告真实的功耗与显存信息,且deviceUUID与宿主机一致 - Vulkan 渲染正常:
vulkaninfo退出码为 0,离屏绘制结果像素级正确 - Wayland 客户端可在客户机内部的合成器中正常呈现
- 通过 Vulkan Video 调用 NVENC,在客户端自身设备上进行编码
- 导入的缓冲区即宿主机内存,通过共享窗口映射使用
尚未完成的部分
- 超过四个客户机,或客户机运行比 720p vkcube 更重的负载。四个客户机可以均匀共享显卡;尚未尝试八个。
- 两张卡、两套驱动版本。 RTX 3060 / 595.99.02 是现有数据的来源;RTX A2000 / 615.71.09 能够渲染但未做基准测试。
- CUDA 已实现转发,但除枚举外尚未测试;隔离沙箱(jailer)、按版本划分的驱动共享以及多租户整体方案尚未构建。
仓库结构
四个组件、三种许可证分区。这种划分是有意为之:客户机侧必须采用 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 | 仅为设计文档,尚未有代码。 这是计划中用于持有真实设备 FD 的、每客户机独立的沙箱辅助进程。目前后端在 VMM 自身的进程中持有这些 FD。 |
gen/ |
— | 自动生成的 ABI 表。已签入仓库且可重新生成。 |
protocol/ |
BSD-3-Clause OR GPL-2.0+ | 双侧共享的线缆格式(wire format)与 ABI 定义。双重许可,使 GPL 的驱动和 Apache 的 crate 能够包含同一份头文件。 |
该结构参考了 chromeos/virtio-media,后者解决了同样的问题 —— 一个仓库同时容纳 GPL 的客户机驱动和宽松许可的、与 VMM 解耦的设备 crate。
从 VMM 集成
device/ 不依赖任何虚拟机监视器。VMM 通过实现一组小的 trait 来集成该设备 —— 描述符链表示为 Read/Write、事件队列、客户机内存映射、宿主机内存映射 —— 无需修改 crate 即可获得完整的设备能力。可选能力采用降级而非编译失败的方式,因此 VMM 可以在尚不支持全部特性时先行集成。
缓冲区与窗口的簿记逻辑全部位于 device/ 内。VMM 只需提供原生的 map 与 unmap 接口,无需做其他工作。
有一项内容 不会 被抽象成 trait:隔离沙箱。预期设计为每个客户机进程运行一个独立的沙箱辅助进程,因此未来集成它意味着继承一种 进程模型,而非仅仅增加一个库依赖。该辅助进程尚未编写 —— 当前后端自行持有设备描述符 —— 而 isolate/ 正是存放该设计思路的地方,待实现时再取出。
动机
期望的流传输流水线
客户机虚拟机(无显示器,无物理显示输出)
──────────────────────────────────────────
游戏 / 应用
│ Vulkan 或 OpenGL
▼
Wayland 合成器(客户机侧)
│ 合成所有窗口
│ CUDA 零拷贝导入合成后的帧
▼
NVENC 硬件编码器(客户机侧)
│ H.264 / H.265 比特流(每帧约 100 KB)
▼
流式传输至远端客户端
整个 渲染 → 合成 → 编码 流水线 在客户机内部的 GPU 上 运行,离开虚拟机的只有压缩后的比特流。这要求客户机必须在驱动层面获得对 GPU 资源的 真实访问权限:缓冲区句柄、fence、CUDA 设备指针、NVENC 会话等。
既有方案为何不足
virtio-gpu + Venus(API 层转换)。 Venus 在客户机中序列化每一次 Vulkan 或 OpenGL 调用,通过 virtio 传输,再在宿主机侧重放。对于本项目场景,存在三个问题:
- 延迟在重绘密集型负载上不断累积。 游戏每帧发起 1,000 到 5,000 次 draw call,外加绑定、描述符更新与渲染通道切换,每次都需要单独序列化并重放。在 60 fps 下每帧仅有 16.6 ms 的时间预算;其中 1 到 3 ms 的序列化开销就意味着在 GPU 真正工作之前,已经消耗了 6% 到 18% 的预算。
- CPU 开销显著。 序列化、传输与重放会消耗宿主机 CPU 资源,而这些资源本应供应用使用。在按算力计费且算力有限的环境下,这种浪费直接影响产品本身。
- 客户机侧编码不可行。 GPU 缓冲区由 宿主机 拥有,客户机内的合成器无法看到或导入它们,因此无法在客户机中获得指向 Venus 管理缓冲区的
CUdeviceptr—— 这意味着在没有完整的 CPU 回读与拷贝的情况下,NVENC 无法使用。
DRM 原生上下文(Intel / AMD)。 客户机运行真实的 Mesa 驱动,在本地构建命令缓冲区,只有提交操作跨越边界。客户机侧的缓冲区所有权和编码均可正常工作。但 NVIDIA 没有等效方案。
VFIO 直通(passthrough)。 提供原生性能与完整的客户机驱动栈,但会将整张显卡独占给单个虚拟机。在多租户环境下,这通常并不可行。
virtio-nvgpu 的不同之处
转换发生在 内核驱动 层(即对 /dev/nvidia* 的 ioctl 调用),而非 图形 API 层。客户机运行 NVIDIA 真实的用户态库,由这些库 在客户机本地 构建 GPU 命令缓冲区 —— 单个 draw call 从不会被序列化:
Venus virtio-nvgpu
────────────── ──────────────────────
每次 draw call: 序列化 + 本地函数调用
传输 + (无 VM exit)
反序列化 +
重放
每帧跨越边界 约 2,000 条消息 约 5–20 条消息
的次数 (每次 API 调用一条) (队列提交 + 内存分配)
GPU 命令缓冲区 在宿主机上 在客户机中
重放后生成 由 NVIDIA 自家编译器生成
CPU 开销 序列化 + 渲染时近乎为零
反序列化 (仅 ioctl 转发)
客户机缓冲区 宿主机拥有缓冲区 客户机拥有缓冲区
所有权 合成器无法 合成器具备完整的
追踪它们 可见性与控制能力
客户机 NVENC 不可行 可用(真正的 CUDA 互操作)