virtio-nvgpu README

The project's README from github.com/nestrilabs/virtio-nvgpu. As one markdown file: README.md.

virtio-nvgpu

Near-native NVIDIA GPU access inside a KVM guest. A guest renders within 2% of the machine it is running on, and costs the same CPU.

virtio-nvgpu forwards NVIDIA kernel driver ioctls between a Linux guest and the host at the driver ABI level, bypassing API-level translation entirely. The guest runs NVIDIA's own user-mode drivers, unmodified — the same libraries, the same Vulkan and NVENC, talking to the same card.

The target is headless streaming: a compositor inside the VM renders, composites and encodes frames on the GPU, then sends compressed video out. The VM has no monitor, and the host keeps the card.

Where it stands

It works, and it has been measured. A Wayland client presents inside a guest, the capture layer encodes on the game's own device, and the H.264 comes out the other side — 618 frames that ffmpeg decodes without an error.

Measured on an RTX 3060 (driver 595.99.02), guest against the same host, bare metal, with an identical headless Vulkan load:

what the host takes for one frame guest frame time
39 ms −0.4% faster than bare metal, within noise
9.9 ms −0.7%
2.0 ms +1.7%
0.5 ms +7.1% a wake costs ~0.02 ms, and the frame is half of one
0.05 ms +40.8%

Above about 2 ms a frame — which is every frame a game draws — a guest is within 2% of bare metal. Below that, the cost of waiting for the GPU starts to show against a frame that barely exists.

CPU is the other half of it, because a shared GPU is only worth sharing if the guests are cheap. Unpaced at ~100 fps for 12 s, one guest:

CPU used
host, bare metal 0.40 s
guest 0.37 s

A guest costs what the host costs. Nothing is spent on forwarding in a render loop, because nothing is forwarded: NVIDIA's user-mode driver submits through memory it has mapped, and that memory is the host's. Over 813,691 frames the backend served 13,792 messages — one crossing per 59 frames, nearly all of it device setup.

Full method, raw runs and the things these numbers do not support: BENCHMARKS.md.

Several guests on one card

Four guests on one RTX 3060, the same load in each: 25.84, 26.49, 25.57, 25.79 fps — 103.7 together, against 102.9 for a single guest — with p50 frame times of 39.165, 39.164, 39.168 and 39.165 ms. The total does not move as guests are added, and the split is even to four decimal places.

All four render correctly at the same time, and four of them encode H.264 at once, each paced at exactly 60 Hz, with no NVENC session limit reached.

Four is what was run, not a limit found.

Driver versions

Measured on 595.99.02; an A2000 on 615.71.09 renders but is not benchmarked. ABI profiles shipped: 535.129.03, 580.178.04, 595.71.05, matched by range, with anything older than the first refused. Details below.

What is known to work

What a guest can reach

Worth stating plainly, because it is the first question a security person asks and the honest answer is not "nothing".

There is no IOMMU boundary between guest GPU work and the host. The card belongs to the host's NVIDIA driver and sits in the host's IOMMU domain; the guest gets the driver's ioctl interface, not the device. What separates a guest from host memory is the GPU's own MMU, with page tables RM programs on the guest's behalf — so the host NVIDIA driver is in the TCB. The guest also authors its own command streams, which is exactly why there is no per-submission cost.

What narrows that surface today:

What does not, yet:

This is attack-surface reduction, not hardware isolation. VFIO passthrough with an IOMMU is strictly stronger — it constrains the device to the guest's own memory — and for mutually untrusted tenants that or vGPU is still the answer.

What is not done


Repository layout

Four components, three license zones. The split is deliberate: the guest half must be GPL to touch kernel symbols, the host half should be permissive so that other people can build on it, and the definitions both halves share must be includable from both.

directory license what it is
driver/ GPL-2.0 Guest kernel module. Registers /dev/nvidia*, forwards ioctl and mmap over the virtqueue. Deliberately not ABI-aware.
device/ Apache-2.0 The virtio device, as a Rust crate with no VMM in its dependency list. Every VMM concern is a trait.
isolate/ Apache-2.0 A design note, not code yet. The sandboxed per-guest helper that will hold the real device FDs. Today the backend holds them itself, in the VMM's own process.
gen/ — Generated ABI tables. Checked in and reproducible.
protocol/ BSD-3-Clause OR GPL-2.0+ Wire format and ABI definitions shared by both halves. Dual licensed so the GPL driver and the Apache crate can include the same headers.

The layout follows chromeos/virtio-media, which solves the same problem — one repository holding a GPL guest driver beside a permissively licensed, VMM-agnostic device crate.

Using it from a VMM

device/ depends on no virtual machine monitor. A VMM adopts the device by implementing a small set of traits — descriptor chains as Read/Write, an event queue, guest memory mapping, host memory mapping — and gets the whole device without patching the crate. Optional capabilities degrade rather than fail to build, so a VMM can adopt it before supporting every feature.

Buffer and window bookkeeping lives in device/. The VMM supplies raw map and unmap and nothing more.

One thing that will not be a trait: the isolate. The intended design runs one sandboxed helper process per guest process, so adopting it eventually means inheriting a process model, not just a library dependency. That helper is not written — the backend holds the device descriptors itself today — and isolate/ is where the design lives until it is.


Why

The streaming pipeline we want

Guest VM (headless, no physical display)
──────────────────────────────────────────

  Game / application
    │ Vulkan or OpenGL
    ▼
  Wayland compositor (guest-side)
    │ composites all windows
    │ CUDA zero-copy import of composed frame
    ▼
  NVENC hardware encoder (guest-side)
    │ H.264 / H.265 bitstream (~100 KB per frame)
    ▼
  Stream to remote client

The entire render → composite → encode pipeline runs on the GPU, inside the guest. Only the compressed bitstream leaves. This requires the guest to have real, driver-level access to GPU resources: buffer handles, fences, CUDA device pointers, NVENC sessions.

Why existing approaches fall short

virtio-gpu + Venus (API-level translation). Venus serializes every Vulkan or OpenGL call in the guest, transports it over virtio, and replays it host-side. Three problems for this use case:

  1. Latency compounds on draw-call-heavy workloads. Games issue 1,000–5,000 draw calls per frame plus binds, descriptor updates and render pass transitions, each serialized and replayed individually. At 60 fps the frame budget is 16.6 ms; 1–3 ms of serialization is 6–18% gone before any GPU work.
  2. CPU overhead is significant. Serialization, transport and replay burn host CPU the application needs. Where compute is billed and finite, that waste is the product.
  3. Guest-side encoding is not viable. GPU buffers are owned by the host. The guest compositor cannot see or import them, so there is no practical path to a CUdeviceptr in the guest pointing at a Venus-managed buffer — which means no NVENC without a full CPU readback and copy.

DRM native context (Intel / AMD). The guest runs the real Mesa driver, builds command buffers locally, and only submissions cross the boundary. Guest-side buffer ownership and encoding work correctly. This does not exist for NVIDIA.

VFIO passthrough. Native performance and a complete driver stack in the guest, but it dedicates the whole GPU to one VM. In multi-tenant environments that is often not an option.

What virtio-nvgpu does differently

Translation happens at the kernel driver level (ioctls to /dev/nvidia*), not the graphics API level. The guest runs NVIDIA's real user-mode libraries, which build GPU command buffers locally in the guest — individual draw calls are never serialized:

                    Venus                  virtio-nvgpu
                    ──────────────         ──────────────────────

Per draw call:      serialize +            local function call
                    transport +            (no VM exit)
                    deserialize +
                    replay

Per frame           ~2,000 messages        ~5–20 messages
  boundary          (one per API call)     (queue submits + allocs)
  crossings

GPU command         generated on HOST      generated in GUEST
  buffers           after replay           by NVIDIA's own compiler

CPU overhead        serialization +        near zero for rendering
                    deserialization        (only ioctl forwarding)

Guest buffer        HOST owns buffers      GUEST owns buffers
  ownership         compositor can't       compositor has full
                    track them             visibility and control

Guest NVENC         not viable             works (real CUDA interop)

How it works

Guest kernel driver. Registers /dev/nvidiactl, /dev/nvidia0…N and /dev/nvidia-uvm. On ioctl() it serializes the request onto a control virtqueue. On mmap() it maps the appropriate shared-memory region into the calling process with the correct caching attributes. It copies raw bytes and makes no ABI decisions.

Device crate. Receives requests, maps guest handles to host device file descriptors, performs ABI-aware translation of ioctl parameters — rewriting embedded pointers and file descriptors — and issues them against the host's devices. Buffer and window bookkeeping lives here.

Events. A second virtqueue runs the other way. The host watches each descriptor it has opened and says when one becomes readable, which is how a guest waiting for the GPU is woken. Without it the guest cannot wait at all — it polls a descriptor the kernel reports as permanently ready, and spins.

Isolate — not built yet. The plan is a sandboxed helper per guest process, holding the real device FDs and issuing the ioctl(2) calls unprivileged. Today the backend does that itself, inside the VMM's process. isolate/ holds the design and no code.

┌─ Guest ─────────────────────────────────────────────────┐
│  Application → NVIDIA Vulkan / GL / CUDA                │
│                     │ ioctl(/dev/nvidia*)               │
│  driver/ (GPL)      ▼                                   │
│    serialize → virtqueue                                │
│    mmap → shared region                                 │
└───────────────────────────┬─────────────────────────────┘
                            │ VM exit
┌───────────────────────────▼─────────────────────────────┐
│  VMM  (implements the device traits)                    │
│                                                         │
│  device/ (Apache-2.0)                                   │
│    ├─ guest handles → host FDs                          │
│    ├─ translate embedded FDs and pointers               │
│    └─ buffer + window bookkeeping                       │
│                     │                                   │
│    └─ ioctl(host /dev/nvidia*) · mmap → shared window   │
│       (an unprivileged per-guest isolate is planned,     │
│        and is not what runs today)                       │
│                                                         │
│  Host NVIDIA driver → GPU                               │
└─────────────────────────────────────────────────────────┘

Scope

Targeted

Out of scope


Performance

Measured, on one card, by one synthetic load — see BENCHMARKS.md for the method and the raw runs, and for what this does not support (it does not support a comparison with any other hypervisor, because none was run).

virtio-nvgpu, measured Venus, by design
GPU-bound (≥2 ms a frame) 98–100% of bare metal 90–97%
Very light frames (≤0.5 ms) 93–71% of bare metal —
CPU cost of a rendering guest same as bare metal high (serialize and replay)
Host crossings per frame ~0.02 thousands
Guest-side NVENC works, zero-copy not viable

The difference is structural: Venus crosses the VM boundary per API call, thousands of times a frame. virtio-nvgpu crosses it per ioctl — and a render loop issues none, because submission is a write to mapped memory. What is left at very light frames is not forwarding but waiting: the guest sleeps for the GPU, and the wake costs ~0.02 ms however small the frame was.

The Venus column is that project's design envelope, not something measured here.


Driver versions

NVIDIA's kernel driver ABI is not stable; ioctl struct layouts change between releases. Support is explicit, and this is the whole list.

ABI profiles shipped:

profile covers
535.129.03 535.129.03 up to the next profile
580.178.04 580.178.04 up to the next profile
595.71.05 595.71.05 and newer

Profiles key off ranges, not points: a release between two profiles uses the lower one, and anything newer than the last profile uses the last profile. Anything older than 535.129.03 is refused rather than guessed at — forwarding an ioctl whose layout has never been seen is how you get a plausible wrong answer instead of an error.

A driver much newer than the newest profile is therefore accepted on the assumption that nothing it needs has changed. That assumption is what a new profile exists to replace, and it is the first thing to suspect when a new driver misbehaves.

Driver versions actually run:

version card how far it got
595.99.02 RTX 3060 everything — renders, presents, encodes, and every number in BENCHMARKS.md
615.71.09 RTX A2000 enumerates and renders; not benchmarked, and not re-tested since

Two cards, two versions, one of them thoroughly. Anything else is untested.

How a profile is built

The cost is bounded, for three reasons. Profiles key off ranges, not points, so a release between two known versions selects the lower profile. The struct half is derived mechanically from NVIDIA's published open-gpu-kernel-modules at each tag — compile a probe per field, read back sizeof and offsetof — rather than transcribed by hand. And the judgement half, which commands exist and which are safe, tracks nvproxy upstream.

See gen/, and supported_versions() there for the list in code — that function, not this table, is the thing that decides.


Prior art

gVisor nvproxy — the direct inspiration. It forwards NVIDIA ioctls from sandboxed containers to the host driver, handling ABI versioning, pointer and FD translation, and GPU mmap management, and it supports Vulkan, OpenGL, CUDA and NVENC in production today. Its ABI definitions (pkg/abi/nvgpu) and handler logic (pkg/sentry/devices/nvproxy) are the primary reference. nvproxy also demonstrates that Vulkan and NVENC work without /dev/nvidia-drm or /dev/nvidia-modeset.

chromeos/virtio-media — the layout template. A GPL guest driver beside a VMM-agnostic Rust device crate, with every VMM concern behind a trait.

WSL2 /dev/dxg — a production driver-level GPU proxy across a real virtualization boundary, proving the general approach at scale. Different problem: it targets a Windows host and a Microsoft-defined kernel abstraction.

DRM native context (Intel / AMD) — the same goal, achieved for other vendors: the guest runs the real driver and builds command buffers locally, with only submissions crossing the boundary. virtio-nvgpu aims at equivalent capability for NVIDIA, where no native context exists.


License

Three zones, listed in Repository layout. Full texts: LICENSE-APACHE-2.0, LICENSE-GPL-2.0, LICENSE-BSD-3-Clause.

Code ported from other projects keeps its original terms.

See also

virtio-nvgpu (nestrilabs/virtio-nvgpu)

Searching Database…

这是什么?一个 virtio 设备,能把一块 NVIDIA GPU 以接近裸机的速度共享给多个 KVM 虚拟机。它的做法是转发内核驱动的 ioctl,而不是翻译图形 API。目标场景是无头(headless)推流:虚拟机在 GPU 上完成渲染和编码,离开的只有压缩后的视频。
它装在虚拟机里、宿主机上,还是两边都要装?两边都要:来宾侧是一个内核模块,注册 /dev/nvidia 并通过 virtqueue 转发 ioctl 和 mmap;宿主机侧是一个 Rust 设备 crate,在你的 VMM 里实现设备端。这个 crate 本身不依赖任何特定的 VMM。
怎么让虚拟机用上共享的 GPU?目前还没有一键安装的办法:你要在 Linux KVM 虚拟机里加载来宾内核模块,并让你的宿主机 VMM 通过实现一小套 trait(descriptor 链、事件队列、来宾与宿主机内存映射)来接入这个设备 crate。可选能力不支持时会降级而不是编译失败,所以 VMM 不必等所有功能都支持了才接入。
支持哪些 NVIDIA 驱动和显卡?内核驱动按 ABI 版本范围匹配:自带 535.129.03、580.178.04 和 595.71.05 三个 profile,更旧的驱动会被拒绝。版本号来自运行 595.99.02 驱动的 RTX 3060;运行 615.71.09 的 RTX A2000 能渲染,但没有做过基准测试。
有哪些功能不能用或还没做?没有显示输出(display scanout,画面以视频而非像素形式输出),不支持 MIG 和 SR-IOV,没有 cudaMallocManaged 统一内存,CUDA 除了枚举设备之外未做测试。一块卡上超过四个虚拟机的情况也没试过,面向每个虚拟机的沙箱化 isolate 目前只是设计构想,还不是代码。
代码用什么许可证?许可证是刻意分开的:来宾内核驱动用 GPL-2.0,virtio 设备 crate 用 Apache-2.0,共享的协议头文件则采用 BSD-3-Clause OR GPL-2.0+ 双许可,这样两部分都能包含它们。
虚拟机的性能离裸机到底有多近?对于 2 毫秒及以上的类游戏帧,虚拟机能达到裸机 2% 以内的差距,CPU 占用也相同——在 12 秒不限速的运行中,来宾用时 0.37 秒,宿主机为 0.40 秒。只有非常轻量的帧(0.5 毫秒及以下)会下降,为裸机的 93–71%。
多台虚拟机真的能同时共享一块卡吗?一块 RTX 3060 上四个来宾合计渲染出 103.7 fps,而单个来宾是 102.9 fps,帧时间分布精确到小数点后四位都完全均匀——而且四个来宾同时以 60 Hz 编码 H.264,也没有触发 NVENC 会话上限。四是实际测试过的数量,并不是发现的极限。
为什么不直接用 VFIO 直通或者带 Venus 的 virtio-gpu?VFIO 直通速度是原生的,但要把整块 GPU 独占给一台虚拟机。Venus 会把每一个 API 调用串行化——每帧成千上万次——而且没有实现来宾侧 NVENC 的可行路径。virtio-nvgpu 按每次 ioctl 跨越边界,而渲染循环一次都不会跨,因为提交只是对映射内存的一次写入。
虚拟机用这块共享 GPU 实际能做什么?Vulkan 渲染并在来宾内部向合成器呈现画面、无头 EGL 的 OpenGL、CUDA 设备内存分配以及零拷贝的 CUDA↔Vulkan/GL 互操作,另外还支持从 CUDA 设备指针进行 NVENC 编码和 NVDEC 解码。
Creator
nestrilabs
Size
5.6 MB
Stars
431
Age
6mo
Commits · 90 Days