在KVM客户机内实现接近原生的英伟达(NVIDIA) GPU访问,客户机渲染性能在运行机器的2%以内且CPU开销相同。虚拟IO英伟达显卡(virtio-nvgpu)在驱动ABI层面转发英伟达内核驱动ioctl,完全绕过API层转换。客户机运行未修改的英伟达用户态驱动,使用相同库、Vulkan与NVENC对接同一显卡。目标是无头串流:虚拟机内合成器在GPU上渲染、合成并编码帧后发送压缩视频,虚拟机无显示器而主机保留显卡。
正文
在 KVM 客户机内实现接近原生的 NVIDIA GPU 访问。客户机的渲染速度与其运行的物理机相差不到 2%,且消耗的 CPU 相同。
virtio-nvgpu 在驱动 ABI 层面转发 Linux 客户机与宿主机之间的 NVIDIA 内核驱动 ioctl,完全绕过了 API 层面的转换。客户机运行未经修改的 NVIDIA 自有用户态驱动——相同的库、相同的 Vulkan 和 NVENC,对话同一块显卡。
其目标是无头串流:虚拟机内的合成器在 GPU 上渲染、合成并编码帧,然后输出压缩视频。虚拟机没有显示器,显卡留在宿主机上。
现状
它确实能用,而且经过实测。一个 Wayland 客户端在客户机内显示,捕获层在游戏自己的设备上编码,H.264 从另一端出来——ffmpeg 解码了 618 帧且无错误。
在 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,而单个客户机为 102.9;p50 帧时间分别为 39.165、39.164、39.168 和 39.165 毫秒。随着客户机增加,总帧率不变,且分配均匀到小数点后四位。
四个客户机同时正确渲染,并同时编码 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 退出码 0,离屏绘制像素正确
- 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 | 设计说明,尚无代码。 将为每个客户机保管真实设备 FD 的沙箱化辅助进程。目前后端自行保管,位于 VMM 自身进程中。 |
| gen/ | — | 生成的 ABI 表。已提交且可复现。 |
| protocol/ | BSD-3-Clause OR GPL-2.0+ | 两半共享的线格式与 ABI 定义。双许可使 GPL 驱动和 Apache crate 能包含相同头文件。 |
此布局效仿 chromeos/virtio-media,它解决了相同问题——一个仓库在宽松许可、与 VMM 无关的 device crate 旁放置 GPL 客户机驱动。
从 VMM 使用
device/ 不依赖任何虚拟机监视器。VMM 通过实现一小批 trait——将描述符链作为 Read/Write、事件队列、客户机内存映射、宿主机内存映射——来接入设备,无需修补 crate 即可获得完整设备。可选能力会降级而非构建失败,因此 VMM 可在支持所有特性前先接入。
缓冲区和窗口记账位于 device/。VMM 仅提供原始 map 和 unmap。
有一项不会成为 trait:isolate。预期设计为每个客户机进程运行一个沙箱化辅助进程,因此最终接入意味着继承一种进程模型,而非仅库依赖。该辅助进程尚未编写——目前后端自行保管设备描述符——在设计实现前,相关设计存放于 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 ms;1–3 ms 序列化占去 6–18% 才进入 GPU 工作。
- CPU 开销显著。 序列化、传输和重放消耗应用所需的宿主机 CPU。在计算按量计费且有限的环境下,这种浪费就是产品成本。
- 客户机侧编码不可行。 GPU 缓冲区由宿主机拥有。客户机合成器无法查看或导入它们,因此客户机内没有指向 Venus 管理缓冲区的
CUdeviceptr的可行路径——意味着没有完整 CPU 回读和拷贝就无法使用 NVENC。
DRM native context(Intel / AMD)。 客户机运行真实 Mesa 驱动,本地构建命令缓冲区,仅提交跨边界。客户机侧缓冲区所有权和编码正常工作。NVIDIA 无此方案。
VFIO 直通。 客户机内原生性能和完整驱动栈,但将整块 GPU 专用于一个 VM。多租户环境通常不可行。
virtio-nvgpu 的不同之处
转换发生在内核驱动层面(对 /dev/nvidia* 的 ioctl),而非图形 API 层面。客户机运行 NVIDIA 真实的用户态库,在客户机本地构建 GPU 命令缓冲区——单个绘制调用从不被序列化:
| 对比项 | Venus | virtio-nvgpu | | --- | --- | --- | | 每次绘制调用 | 序列化+传输+反序列化+重放 | 本地函数调用(无 VM 退出) | | 每帧边界跨越 | 约 2,000 条消息(每次 API 调用一条) | 约 5–20 条消息(队列提交+分配) | | GPU 命令缓冲区 | 重放后在宿主机生成 | 由 NVIDIA 自有编译器在客户机生成 | | CPU 开销 | 序列化+反序列化 | 渲染近零(仅 ioctl 转发) | | 客户机缓冲区所有权 | 宿主机拥有,合成器无法追踪 | 客户机拥有,合成器完全可见可控 | | 客户机 NVENC | 不可行 | 可用(真实 CUDA 互操作) |
工作原理
客户机内核驱动。 注册 /dev/nvidiactl、/dev/nvidia0…N 和 /dev/nvidia-uvm。ioctl() 时将请求序列化到控制 virtqueue。mmap() 时将适当的共享内存区域以正确缓存属性映射到调用进程。它拷贝原始字节,不做 ABI 决策。
设备 crate。 接收请求,将客户机句柄映射到宿主机设备文件描述符,对 ioctl 参数执行感知 ABI 的转换——重写嵌入的指针和文件描述符——并对宿主机设备发出调用。缓冲区和窗口记账在此。
事件。 第二个 virtqueue 反向运行。宿主机监视其打开的每个描述符,并在其可读时通知,客户机据此被唤醒等待 GPU。没有它,客户机根本无法等待——只能轮询内核报告为永久就绪的描述符并空转。
Isolate——尚未构建。 计划为每个客户机进程设沙箱化辅助进程,持有真实设备 FD 并无特权发出 ioctl(2) 调用。目前后端在 VMM 进程内自行处理。isolate/ 仅存放设计,无代码。
客户机内,应用经 NVIDIA Vulkan/GL/CUDA 通过 ioctl(/dev/nvidia*) 交由 GPL 的 driver/ 序列化到 virtqueue 并 mmap 共享区域。经 VM 退出后,VMM(实现设备 trait)内的 Apache-2.0 device/ 将客户机句柄转为宿主机 FD,转换嵌入 FD 与指针,处理缓冲区和窗口记账,再对宿主机 /dev/nvidia* 发 ioctl 并 mmap 共享窗口(计划中的无特权每客户机 isolate 尚未运行)。最后交由宿主机 NVIDIA 驱动到 GPU。
范围
目标内
- Vulkan 渲染,包括在客户机内向合成器显示——需要
/dev/nvidia-drm和/dev/nvidia-modeset,二者均已实现且均非显示器:它们是缓冲区变得可共享的方式 - OpenGL 渲染(无头 EGL)
- CUDA 设备内存分配
- CUDA ↔ Vulkan/GL 互操作,零拷贝,GPU 侧指针
- 从 CUDA 设备指针进行 NVENC 编码;NVDEC 解码
范围外
cudaMallocManaged()/ 完整统一虚拟内存- scanout。 无物理显示输出:串流盒无显示器,帧以视频而非线上像素离开
- MIG、SR-IOV
- 任意 NVIDIA 驱动版本——每个支持范围明确,如同
nvproxy
性能
在一块卡上由一种合成负载测得——方法、原始运行及本数据不支持的结论(不支持与其他 hypervisor 对比,因未运行任何对比)见 BENCHMARKS.md。
| | virtio-nvgpu(实测) | Venus(按设计) | | --- | --- | --- | | GPU 受限(≥2 ms/帧) | 裸金属 98–100% | 90–97% | | 极轻帧(≤0.5 ms) | 裸金属 93–71% | — | | 渲染客户机 CPU 成本 | 同裸金属 | 高(序列化与重放) | | 每帧宿主机跨越 | ~0.02 | 数千 | | 客户机侧 NVENC | 可用,零拷贝 | 不可行 |
差异是结构性的:Venus 每帧跨 VM 边界数千次,每次 API 调用一次。virtio-nvgpu 按 ioctl 跨越——而渲染循环不发出 ioctl,因为提交是对映射内存的写入。极轻帧下剩余的不是转发而是等待:客户机为 GPU 休眠,无论帧多小唤醒都耗约 0.02 ms。
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 各 tag 机械派生——为每个字段编译探针,读回 sizeof 和 offsetof——而非手工抄写。判断部分(哪些命令存在且安全)跟踪 nvproxy 上游。
见 gen/,其中的 supported_versions() 是代码中的列表——做决定的就是该函数,而非此表。
前人工作
gVisor nvproxy——直接灵感。它将 NVIDIA ioctl 从沙箱容器转发到宿主机驱动,处理 ABI 版本化、指针与 FD 转换及 GPU mmap 管理,目前生产环境支持 Vulkan、OpenGL、CUDA 和 NVENC。其 ABI 定义(pkg/abi/nvgpu)与处理逻辑(pkg/sentry/devices/nvproxy)是主要参考。nvproxy 还证明 Vulkan 和 NVENC 无需 /dev/nvidia-drm 或 /dev/nvidia-modeset 即可工作。
chromeos/virtio-media——布局模板。GPL 客户机驱动置于与 VMM 无关的 Rust device crate 旁,所有 VMM 事项藏于 trait 后。
WSL2 /dev/dxg——跨真实虚拟化边界的生产级驱动层 GPU 代理,大规模证明通用方案。问题不同:面向 Windows 宿主机和微软定义的内核抽象。
DRM native context (Intel / AMD)——相同目标,其他厂商已实现:客户机运行真实驱动本地构建命令缓冲区,仅提交跨边界。virtio-nvgpu 旨在为无原生上下文的 NVIDIA 提供同等能力。
许可证
三个区域,列于仓库布局。完整文本见 LICENSE-APACHE-2.0、LICENSE-GPL-2.0、LICENSE-BSD-3-Clause。
从其他项目移植的代码保留原许可条款。
参见
BENCHMARKS.md——对比裸金属的成本、测量方式及数字不支持的结论。ARCHITECTURE.md——散文版工作原理:什么跨 VM 边界什么不跨、内存如何共享、缓冲区如何可共享、客户机如何等待及设计不能做的事。
评论
社区肯定项目实现多虚拟机近原生共享 GPU 的尝试,但重点质疑其安全隔离近乎为零,并有人指出文档质量和与现有方案对比的缺失。
认为该方案隔离性极弱且有主机逃逸隐患
- 较多人关注安全面。分析指出默认将所有客户机 ioctl 转发给主机,持有读写文件描述符且无 seccomp 等限制,暴露面等同于无虚拟机直接跑多进程;另有评论怀疑这类似 KVM 逃逸,并质问无 IOMMU 限制时显卡对主机的访问等级。 > 这项目基本上没有任何 GPU 相关安全措施,暴露程度和多个进程无虚拟机直接用 GPU 一样。(@refibrillator)
质疑相比普通直通或 gVisor 方案的独特价值
- 少数人提问,认为普通 GPU 直通或 gVisor 的 nvproxy 已存在,作者未说明本方案如何区别于 nvproxy,也未澄清相比常规直通在共享能力上的实际优势。 > 既然没看到怎么在多虚拟机间共享显卡,那相比普通直通到底好在哪?(@orphereus)
吐槽文档像大模型废话且未突出核心卖点
- 较多人提及 README 问题。有人讽刺其是大模型生成的词沙拉、文风突兀;另有用户建议应将“多客户机近原生速度”置于文档开头以凸显差异。 > 这文档简直是大模型词沙拉的教科书式反面教材,工具强但写出来的东西格外扎眼。(@mcmatterson)
有价值的补充
- 系统梳理了给 KVM 客户机分配 GPU 的三种路径:整卡 VFIO 直通(隔离强但独占)、virtio-gpu 虚拟化(可共享但开销大)、以及本项目的转发 ioctl(近原生可共享但隔离弱),清晰定位了技术坐标。
- 同期另有 Nesbox 项目提交类似意图,但原生支持多客户机共享 GPU,并作为远程流化桌面套件的基础组件。
- 有用户分享借助 cgroups2 与 LXC 容器实现类似 GPU 分时共享的另类实践,在 Proxmox 上局部直通显示器、用 cgroup 限制 CUDA 接口分配给多个流媒体容器。