virtio-nvgpu: Bijna native toegang tot NVIDIA GPU's in KVM-virtuele machines

virtio-nvgpu stuurt NVIDIA kernel driver ioctl's door tussen een Linux-guest en de host op het niveau van de driver ABI, waardoor vertaling op API-niveau volledig wordt omzeild. De guest maakt gebruik van de eigen, ongewijzigde user-mode drivers van NVIDIA — dezelfde bibliotheken, dezelfde Vulkan en NVENC, die communiceren met dezelfde kaart.

Het doel is headless streaming: een compositor binnen de VM rendert, stelt frames samen en codeert deze op de GPU, om vervolgens gecomprimeerde video te verzenden. De VM heeft geen monitor en de host behoudt de kaart.

Huidige status

Het project is functioneel en is gemeten. Een Wayland-client presenteert binnen een guest, de capture-laag codeert op het apparaat van het spel, en de H.264-output komt aan de andere kant uit — 618 frames die door ffmpeg zonder fouten worden gedecodeerd.

Prestaties

Gemeten op een RTX 3060 (driver 595.99.02), waarbij de guest is vergeleken met de host op bare metal met een identieke headless Vulkan-belasting:

Wat de host nodig heeft voor één frameGuest frame-tijdVerschil t.o.v. bare metal
39 ms9,9 ms−0,4% (binnen de ruismarge)
2,0 ms0,5 ms−0,7%
0,5 ms0,05 ms+1,7%
(een wake kost ~0,02 ms, en het frame is de helft daarvan)—+7,1%
——+40,8%

Boven een frame-tijd van ongeveer 2 ms — wat geldt voor elk frame dat een game tekent — zit een guest binnen 2% van bare metal. Daaronder begint de kostprijs van het wachten op de GPU zichtbaar te worden bij frames die nauwelijks bestaan.

CPU-belasting

CPU-verbruik is de andere helft van de vergelijking, omdat een gedeelde GPU alleen zinvol is als de guests weinig resources verbruiken. Gemeten bij een onbeperkte snelheid van ~100 fps gedurende 12 seconden voor één guest:

  • Host, bare metal: 0,40 s CPU gebruikt
  • Guest: 0,37 s CPU gebruikt

Een guest kost hetzelfde als de host. Er wordt niets verbruikt aan forwarding in een render-loop, omdat er niets wordt geforward: NVIDIA's user-mode driver dient via geheugen dat is gemapt, en dat geheugen is dat van de host. Over 813.691 frames bediende de backend 13.792 berichten — gemiddeld één crossing per 59 frames, waarvan bijna alles apparaat-setup was.

Voor de volledige methode en ruwe resultaten zie BENCHMARKS.md.

Meerdere guests op één kaart

Bij vier guests op één RTX 3060 met dezelfde belasting in elke guest, waren de resultaten: 25,84, 26,49, 25,57 en 25,79 fps. Samen is dit 103,7 fps, tegenover 102,9 fps voor een enkele guest, met p50 frame-tijden van respectievelijk 39,165, 39,164, 39,168 en 39,165 ms. Het totaal verandert niet wanneer er guests worden toegevoegd en de verdeling is gelijk tot op vier decimalen.

Alle vier de guests renderen correct tegelijkertijd en coderen tegelijkertijd H.264, elk precies op 60 Hz, zonder dat de NVENC-sessielimiet is bereikt. Er is vier guests getest; dit is geen vastgestelde limiet.

Driver-versies

Gemeten op versie 595.99.02. Een A2000 op 615.71.09 rendert wel, maar is niet gebenchmarkt. De geleverde ABI-profielen zijn: 535.129.03, 580.178.04 en 595.71.05, gematcht op bereik; alles ouder dan de eerste wordt geweigerd.

Wat werkt (bekend)

  • Een guest enumereert de kaart — nvidia-smi rapporteert werkelijk vermogen en geheugen, en de deviceUUID is die van de host.
  • Vulkan rendering: vulkaninfo sluit af met 0, offscreen draws zijn pixel-correct.
  • Een Wayland-client presenteert via een compositor in de guest.
  • NVENC via Vulkan Video, waarbij encoding plaatsvindt op het apparaat van de client.
  • Geïmporteerde buffers zijn het geheugen van de host, gemapt via een shared window.

Wat nog niet is gerealiseerd

  • Meer dan vier guests, of guests die zwaardere taken uitvoeren dan vkcube op 720p. (Vier delen de kaart gelijkmatig; acht is niet geprobeerd).
  • Twee kaarten of twee driver-versies tegelijk. De cijfers komen van een RTX 3060 / 595.99.02; een RTX A2000 / 615.71.09 heeft gerenderd maar is niet gebenchmarkt.
  • CUDA is geforward maar niet getest verder dan enumeratie; de jailer, per-versie driver shares en de multi-tenant envelope zijn nog niet gebouwd.

Projectstructuur

Het project is verdeeld over vier componenten en drie licentiezones. Deze splitsing is bewust: het guest-gedeelte moet GPL zijn om kernel-symbolen aan te kunnen raken, het host-gedeelte moet permissief zijn zodat anderen erop kunnen voortbouwen, en de definities die beide delen delen moeten vanuit beide kanten kunnen worden opgenomen.

DirectoryLicentieBeschrijving
driver/GPL-2.0Guest kernel module. Registreert /dev/nvidia*, forwardt ioctl en mmap over de virtqueue. Bewust niet ABI-aware.
device/Apache-2.0Het virtio-apparaat, als een Rust crate zonder VMM in de dependency-lijst. Elk VMM-aspect is een trait.
isolate/Apache-2.0Een design-notitie, nog geen code. De gesandboxte per-guest helper die de echte device FD's zal vasthouden. Momenteel houdt de backend deze zelf vast in het proces van de VMM.
gen/—Gegenereerde ABI-tabellen. Ingecheckt en reproduceerbaar.
protocol/BSD-3-Clause OF GPL-2.0+Wire-formaat en ABI-definities gedeeld door beide helften. Dual-licensed zodat zowel de GPL-driver als de Apache-crate dezelfde headers kunnen includeren.

De lay-out volgt chromeos/virtio-media, wat hetzelfde probleem oplost: één repository die een GPL guest-driver bevat naast een permissief gelicentieerde, VMM-agnostische device crate.

Gebruik vanuit een VMM

device/ is afhankelijk van geen enkele virtual machine monitor. Een VMM adopteert het apparaat door een kleine set traits te implementeren — descriptor chains als Read/Write, een event queue, guest memory mapping, host memory mapping — en krijgt zo het volledige apparaat zonder de crate te hoeven patchen. Optionele functionaliteiten degraderen in plaats van dat de build faalt, zodat een VMM het kan adopteren voordat elke feature wordt ondersteund.

Buffer- en window-boekhouding bevindt zich in device/. De VMM levert enkel ruwe map- en unmap-functies.

Eén onderdeel zal geen trait zijn: de isolate. Het beoogde ontwerp draait één gesandboxte helper-proces per guest-proces. Het adopteren hiervan betekent uiteindelijk het overnemen van een procesmodel, niet slechts een library-dependency. Deze helper is nog niet geschreven; momenteel houdt de backend de device descriptors zelf vast.

Motivatie en Ontwerp

De gewenste streaming-pipeline

  1. Guest VM (headless, geen fysiek display)
  2. Game / applicatie → Vulkan of OpenGL
  3. Wayland compositor (guest-zijde) → stelt alle vensters samen → CUDA zero-copy import van het samengestelde frame
  4. NVENC hardware encoder (guest-zijde) → H.264 / H.265 bitstream (~100 KB per frame)
  5. Stream naar remote client

De volledige render → composite → encode pipeline draait op de GPU, binnen de guest. Alleen de gecomprimeerde bitstream verlaat de VM. Dit vereist dat de guest echte, driver-level toegang heeft tot GPU-resources: buffer handles, fences, CUDA device pointers en NVENC-sessies.

Waarom bestaande benaderingen tekortschieten

  • virtio-gpu + Venus (API-level translation): Venus serialiseert elke Vulkan- of OpenGL-aanroep in de guest, transporteert deze over virtio en speelt deze aan de host-zijde af.
  • Latentie: Bij workloads met veel draw-calls stapelt de latentie zich op. Games voeren 1.000–5.000 draw-calls per frame uit. Bij 60 fps is het budget 16,6 ms; 1–3 ms aan serialisatie verbruikt direct 6–18% van dit budget.
  • CPU-overhead: Serialisatie, transport en replay verbruiken host-CPU die de applicatie nodig heeft.
  • Guest-side encoding: GPU-buffers zijn eigendom van de host. De guest-compositor kan ze niet zien of importeren, waardoor er geen praktisch pad is naar een CUdeviceptr in de guest die naar een door Venus beheerde buffer wijst. Dit maakt NVENC onmogelijk zonder een volledige CPU readback en kopie.
  • DRM native context (Intel / AMD): De guest draait de echte Mesa-driver, bouwt command-buffers lokaal op, en alleen de submissions passeren de grens. Dit werkt correct voor buffer-ownership en encoding, maar bestaat niet voor NVIDIA.
  • VFIO passthrough: Native prestaties en een complete driver-stack in de guest, maar het wijst de gehele GPU toe aan één VM. In multi-tenant omgevingen is dit vaak geen optie.

Wat virtio-nvgpu anders doet

Vertaling vindt plaats op het niveau van de kernel driver (ioctls naar /dev/nvidia*), niet op het niveau van de graphics API. De guest draait de echte user-mode bibliotheken van NVIDIA, die GPU command-buffers lokaal in de guest bouwen. Individuele draw-calls worden nooit geserialiseerd.

AspectVenusvirtio-nvgpu
Per draw callserialize + transport + deserialize + replaylokale functie-aanroep (geen VM exit)
Boundary crossings per frame~2.000 berichten (één per API-aanroep)~5–20 berichten (queue submits + allocs)
GPU command buffersgegenereerd op HOST na replaygegenereerd in GUEST door NVIDIA-compiler
CPU overheadhoog (serialisatie + deserialisatie)bijna nul voor rendering (alleen ioctl forwarding)
Guest buffer ownershipHOST bezit buffers; compositor heeft geen zichtGUEST bezit buffers; compositor heeft volledige controle
Guest NVENCniet levensvatbaarwerkt (echte CUDA interop)

Werking

  1. Guest kernel driver: Registreert /dev/nvidiactl, /dev/nvidia0…N en /dev/nvidia-uvm. Bij een ioctl() wordt het verzoek geserialiseerd naar een control virtqueue. Bij een mmap() wordt de juiste shared-memory regio in het aanroepende proces gemapt met de juiste caching-attributen. Het kopieert ruwe bytes en maakt geen ABI-beslissingen.
  2. Device crate: Ontvangt verzoeken, mapt guest-handles naar host-device file descriptors, voert ABI-aware vertaling uit van ioctl-parameters (herschrijven van embedded pointers en file descriptors) en voert deze uit tegen de apparaten van de host. De boekhouding van buffers en windows bevindt zich hier.
  3. Events: Een tweede virtqueue draait in de tegenovergestelde richting. De host houdt elk geopend descriptor in de gaten en meldt wanneer deze leesbaar wordt. Dit is hoe een guest die wacht op de GPU wordt gewekt. Zonder dit zou de guest moeten pollen.
  4. Isolate (nog niet gebouwd): Het plan is een gesandboxte helper per guest-proces die de echte device FD's vasthoudt en de ioctl(2) aanroepen onbevoorrecht uitvoert. Momenteel doet de backend dit zelf binnen het proces van de VMM.

Architectuur-overzicht

Guest Application → NVIDIA Vulkan / GL / CUDA → driver/ (GPL) → serialize → virtqueue / mmap → shared region → VM exit → VMM (device traits) → device/ (Apache-2.0) → translate embedded FDs and pointers → ioctl(host /dev/nvidia*) → Host NVIDIA driver → GPU.

Reikwijdte (Scope)

Ondersteund

  • Vulkan rendering, inclusief presentatie aan een compositor in de guest (vereist /dev/nvidia-drm en /dev/nvidia-modeset, beide geïmplementeerd; deze zijn niet voor display, maar om buffers deelbaar te maken).
  • OpenGL rendering (headless EGL).
  • CUDA device memory allocatie.
  • CUDA $\leftrightarrow$ Vulkan/GL interop, zero-copy, GPU-side pointers.
  • NVENC encoding vanuit CUDA device pointers; NVDEC decoding.

Buiten scope

  • cudaMallocManaged() / volledige unified virtual memory.
  • Scanout: geen fysieke display-output (frames verlaten de box als video, niet als pixels op een kabel).
  • MIG, SR-IOV.
  • Willekeurige NVIDIA driver-versies: elk ondersteund bereik is expliciet.

Prestaties-vergelijking

Gemeten op één kaart met een synthetische load.

Metingvirtio-nvgpuVenus (by design)
GPU-bound (≥2 ms per frame)98–100% van bare metal90–97%
Zeer lichte frames (≤0.5 ms)93–71% van bare metal—
CPU-kosten rendering guestgelijk aan bare metalhoog (serialize en replay)
Host crossings per frame~0,02duizenden
Guest-side NVENCwerkt, zero-copyniet levensvatbaar

Het verschil is structureel: Venus kruist de VM-grens per API-aanroep. virtio-nvgpu kruist deze per ioctl — en een render-loop voert geen ioctls uit, omdat submission een write is naar gemapt geheugen. Wat overblijft bij zeer lichte frames is niet de forwarding, maar het wachten: de guest slaapt voor de GPU, en de wake-up kost ~0,02 ms.

Driver-versies en ABI-profielen

Omdat NVIDIA's kernel driver ABI niet stabiel is, is ondersteuning expliciet.

Geleverde ABI-profielen:

  • 535.129.03: Dekt versie 535.129.03 tot de volgende profielversie.
  • 580.178.04: Dekt versie 580.178.04 tot de volgende profielversie.
  • 595.71.05: Dekt versie 595.71.05 en nieuwer.

Profielen werken met bereiken: een release tussen twee profielen gebruikt het lagere profiel. Alles ouder dan 535.129.03 wordt geweigerd. Een driver die veel nieuwer is dan het laatste profiel wordt geaccepteerd in de aanname dat de benodigde zaken niet zijn veranderd.

Daadwerkelijk gedraaide versies:

  • 595.99.02 (RTX 3060): Volledig getest (renders, presents, encodes, benchmarks).
  • 615.71.09 (RTX A2000): Enumereert en rendert; niet gebenchmarkt.

Hoe een profiel wordt gebouwd

De kosten hiervan zijn beperkt omdat:

  1. Profielen werken met bereiken.
  2. De struct-definities mechanisch worden afgeleid van NVIDIA's open-gpu-kernel-modules (via probes per veld voor sizeof en offsetof), in plaats van handmatige transcriptie.
  3. De logica over welke commando's bestaan en veilig zijn, de upstream nvproxy volgt.

Eerdere werken (Prior Art)

  • gVisor nvproxy: De directe inspiratie. Forwardt NVIDIA ioctls van gesandboxte containers naar de host driver. Ondersteunt Vulkan, OpenGL, CUDA en NVENC in productie. De ABI-definities en handler-logica zijn de primaire referentie.
  • chromeos/virtio-media: Het sjabloon voor de projectstructuur (GPL guest driver naast een VMM-agnostische Rust device crate).
  • WSL2 /dev/dxg: Een productie-driver-level GPU proxy over een echte virtualisatiegrens (Windows host).
  • DRM native context (Intel / AMD): Hetzelfde doel, bereikt voor andere vendors. virtio-nvgpu streeft naar een gelijkwaardige capaciteit voor NVIDIA.