1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
|
# 04 — Virtualizing CUDA: NVIDIA's vGPU model, Open-IOV's model, and how to stay unbreakable
This is the design document for CUDA virtualization in Open-IOV. It answers
four questions:
1. How does NVIDIA virtualize CUDA in its own vGPU product?
2. How does Open-IOV's model differ, and why?
3. How do we stay working across CUDA/driver/GSP/firmware/hardware changes
(anti-fragility)?
4. How do we get performance, portability, security and correctness?
Companion documents: `docs/01-architecture.md` (what the driver is),
`docs/03-driver-and-gpu-interface.md` (the interface we must satisfy).
---
## 1. NVIDIA's vGPU architecture (behavioural summary)
NVIDIA's vGPU stack splits the driver across the VM boundary. The **guest is
fully unmodified**: stock `libcuda`, stock `nvidia.ko`, stock `nvidia-uvm.ko`.
Virtualization is hidden below the RM client interface, using a mediated PCI
function and a host-side device model:
```
guest userspace libcuda (unmodified) ── /dev/nvidia* ioctls ──┐
guest kernel nvidia.ko, nvidia-uvm.ko (unmodified) │
│ │
│ emulated PCI function: config space, │
│ BAR0/BAR1 MMIO, doorbells, MSI-X, VGA │
▼ │
hypervisor mediated device (VFIO/mdev on KVM, VMBus on │
Hyper-V, device model on Xen/VMware) │
│ │
host kernel vGPU kernel module (VFIO side) │
│ ioctls: create/destroy, pin/unpin pages, │
│ reg access, mmio mappings, interrupt │
│ injection, migration state │
▼ │
host userspace vGPU manager daemon + plugin (VMIOP) ◄───────┘
│ NvRm API
▼
host kernel + GPU host nvidia.ko / GSP-RM / physical GPU
```
The pieces that make this work:
* **A mediated PCI function.** The guest sees a GPU with a *vGPU profile*
(a slice of framebuffer, a set of engines, display heads, encoder capacity).
Config space, BAR sizes, MMIO registers, doorbells and MSI-X are emulated or
forwarded; the profile is what licenses and limits the VM.
* **A guest-host wire protocol (VMIOP) between the guest kernel driver and the
host plugin.** The guest kernel driver's MMIO/register accesses arrive at
the host plugin as emulation callbacks (space, offset, width, data,
cacheability). The plugin either satisfies them from backing memory (with
per-register callback gating) or forwards them into host RM.
* **Guest memory mapping and pinning.** Guest pages are pinned and mapped into
the plugin so the physical GPU can DMA into guest RAM; the guest framebuffer
is carved from physical VRAM and mapped into the guest's physical address
space.
* **Interrupt injection.** Device interrupts are delivered to the guest as
MSI/MSI-X through the hypervisor.
* **A capability negotiation on the CUDA side.** The CUDA userspace driver
detects vGPU (`GPU_GET_VIRTUALIZATION_MODE == VGX`) and queries a vGPU
capability block: is CUDA enabled for this profile, is UVM available, are
replayable faults available, is GPU-direct RDMA available, is TSG timeslice
override available. It then *restricts itself*: no UVM if the profile lacks
it, no debugger if disabled, no checkpoint/CRIU, CILP instead of MBP for
preemption, and it enforces the GRID license state (unlicensed VMs get
`CUDA_ERROR_DEVICE_NOT_LICENSED` or a sleep/timebomb behavior).
* **Real objects on the host.** Channels, VA spaces, memory objects, engine
contexts and command execution are *real* host RM objects bound to real
channels; command streams are passed through and executed by the physical
GPU. What is virtualized is the *view* (profiles, MMIO, interrupts,
migration), not the execution.
Two design consequences worth naming:
* The guest driver is never patched; the virtualization is data (capabilities,
object parameters, emulated registers) plus a host device model.
* The split is at the *kernel-driver ↔ hardware* boundary. Everything above
it (CUDA userspace, RM API semantics) is the stock stack.
The exact plugin interfaces, capability bits, object classes and migration
stages are vendor-internal and not needed to build an interoperable
alternative.
## 2. Open-IOV's model: emulate the interface, pass through the work
Open-IOV makes the same fundamental choice as NVIDIA — **the guest stays
stock** — but implements the host side differently:
| Concern | NVIDIA vGPU | Open-IOV |
|---|---|---|
| Guest stack | stock | stock (identical constraint) |
| Device presented | mediated PCI function with a vGPU profile, backed by host RM | emulated PCI function (mdev/VFIO) backed by the host module + GSP-RM emulation |
| Where commands execute | physical GPU via host RM channels | host executes the work the guest encodes (e.g. memsets/copies/semaphore releases) or passes through, depending on the resource |
| Host-side control plane | vGPU manager daemon + VMIOP plugin + host RM | `open-iov-vfio.ko` emulating the GSP-RM interface and device model directly in the kernel |
| Licensing | GRID license server / vGPU licensing; CUDA gated on license state | no GRID licensing; the driver's vGPU detection must be answered in a way that does not gate CUDA (or the guest must not be told it is a licensed-vGPU-only environment) |
| CUDA userspace | unmodified, capability-limited by vGPU profile | unmodified, no profile limits imposed |
| Migration | vGPU-specific save/restore protocol with dirty-page tracking | 4-phase migration in the host module (project feature) |
The key architectural difference: **Open-IOV does not need to virtualize the
GPU's register interface at all if it can emulate the GSP-RM interface the
kernel driver consumes.** The kernel driver asks the GSP (via RPCs) for
objects, memory, channels, engines and caps; if the emulation answers those
RPCs coherently, the driver builds its own command streams and the guest CUDA
stack works. This moves the emulation boundary *up* from MMIO registers to
RPC messages, which is far more stable: RPCs are versioned and typed, MMIO
register maps are not.
Where pass-through is possible (real GPU assigned, SR-IOV, or a future
split-execution mode), the same userspace stack runs unchanged; the host-side
work is the only difference. That is the portability goal: the analysis in
`docs/01`–`docs/03` describes the *interface*, and both emulation and
passthrough are implementations of it.
## 3. Anti-fragility: how we survive version churn
CUDA, the kernel driver, GSP firmware and GPU architectures all change on
independent schedules. Our analysis and implementation are designed so that
change moves *data*, not *architecture*:
1. **Anchor on the stable interfaces.** The kernel-driver interface (RM
objects, controls, ioctls) and the userspace driver's internal layering
have been stable for many years; the driver-ABI adaptation layer inside
`libcuda` exists precisely because NVIDIA itself maintains that stability.
We track the interface, not the implementation.
2. **Prefer behaviour over constants.** Every numeric ID we depend on
(classes, controls, capability bits) is versioned in config
(`config/versions/*.yaml`, `config/hardware/*.yaml`), with the analysis
tooling able to re-derive them from a new binary (`cudatool.py delta`,
`docs/07`).
3. **Never require driver patches.** No guest modification, no userspace
shim, no `LD_PRELOAD`. Anything that would break on a driver update is
rejected by design. The only supported hook points are the ones NVIDIA
itself keeps stable: entry tables, `cuGetProcAddress` versioning, the RM
ABI.
4. **Emulate replies, not side effects.** The host emulation answers RPCs with
*coherent data*; it does not fake hardware state that the driver might
later cross-check. Where a value is consumed (sizes, IDs, caps), we must
supply a consistent value — the crash analysis in `docs/05` is exactly a
case of inconsistent data reaching a userspace consumer.
5. **Name transfer instead of re-analysis.** Function names and structure
knowledge are stored in a reviewable name map (`570.144/annotations/`) and
can be re-applied to a new version in seconds, then corrected where
signatures moved. Analysis effort accumulates across versions.
6. **Decompose by failure mode.** The analysis is organized by what can break:
* *API layer* — new/changed entry points (`delta` detects them);
* *engine* — new object types, new control flows;
* *driver-model* — new controls/ABI versions;
* *hardware layer* — new chips and QMD/method encodings;
* *device code* — new syscalls/trampolines embedded in the library;
* *UVM* — page-migration and channel registration changes.
7. **Keep a reference oracle.** The same CUDA workload runs on bare metal
(host A/B harness) and in the guest; divergence localizes the break to the
emulation, and the ioctl/control traces localize it further. This is the
fastest known way to find an emulation gap without understanding the whole
stack.
## 4. Performance
The performance budget for virtualized CUDA is dominated by three things, in
order:
1. **Push-buffer execution.** Kernel launches, copies and syncs are encoded by
guest userspace into memory. The host must execute that work efficiently.
The project's design decodes pushes as method streams and executes them
(memsets/copies/semaphore releases) rather than round-tripping each method.
Fast path: operate directly on the mapped memory; no per-method syscall.
2. **Synchronization latency.** Every launch ends in a semaphore release; every
`cuStreamSynchronize` waits on a marker. Host-side completion must be
signaled promptly (ISR/MSI-X injection latency matters more than raw
throughput). The userspace driver's blocking-sync path can park threads on
OS events, so delivering the completion interrupt promptly avoids spinning.
3. **Control-plane latency (setup, not steady state).** Context creation,
module load, first launch are control-heavy; these happen once. Optimizing
them is about correctness first (see `docs/05`), not microseconds.
Design rules that follow:
* **Fast path in the host kernel**: the push decoder and semaphore service run
in the kernel module, with no daemon in the loop for steady-state work.
* **Batching**: process a whole push (or a whole page of pushes) in one pass,
keeping decode state across methods; avoid per-method wakeups.
* **No copy where mapping suffices**: guest memory the GPU reads/writes is
pinned and mapped; the emulation should operate on it in place.
* **Measure against the A/B oracle**: `tools/cuda-ab/` gives host-vs-guest
numbers for the same binary; a regression is visible immediately.
* **Latency instrumentation**: RPC round-trip and interrupt-injection timing
are traceable (`trace_printk` in the module) so hot spots are found with
data.
## 5. Portability
The same analysis supports:
* **Multiple driver versions** (550/560/570/580+, Windows and Linux): the
versioned interfaces are data-driven; the name map and config carry the
per-version facts.
* **Multiple GPU architectures** (Turing → Blackwell): the hardware layer
(QMD/method encodings) is selected by compute capability inside the driver;
the host must only understand the *effects* (copy/memset/semaphore/grid),
not every encoding. Where a new architecture introduces a genuinely new
effect, the decoder learns it, not the whole stack.
* **Multiple hypervisors**: the emulation boundary is VFIO/mdev today, but the
interface analysis is hypervisor-neutral (the same stock guest runs under
libvirt, / or raw QEMU).
* **Emulation and passthrough**: as noted in §2, both are implementations of
the same interface; a passthrough deployment validates the guest stack and
narrows the emulation's responsibility to the control plane.
## 6. Security and correctness
* **Isolation.** The guest's CUDA objects must map to host objects with the
guest's identity and limits; no guest handle may name a host object it does
not own. Handles are per-client; the emulation must not let a guest forge
handles or reuse freed ones (handle generation).
* **Memory safety.** Every guest-provided address/size (push buffers, GPFIFO
entries, semaphore addresses, VA ranges) is untrusted input to the host
decoder. Validate ranges against the mapped regions, bound loops, and never
index arrays with guest-controlled values.
* **DMA containment.** With an IOMMU, guest memory used by the GPU is mapped
explicitly; the emulation must not grant broader DMA windows than the
allocation requires.
* **Interrupt/event safety.** Event objects are per-client; signalling must be
routed to the right VM only.
* **Correctness before speed.** A wrong reply is worse than a slow one: it
corrupts driver state (as in `docs/05`). The reply-fidelity ladder is:
status-only → coherent zeroed params → coherent values for consumed fields →
exact values. Every rung should be validated by the A/B harness before
moving on.
* **Auditability.** All host-side emulation decisions are logged (control ID,
size, status, consuming context) so a guest failure can be reconstructed
from the host log alone.
## 7. The practical path (where this analysis plugs in)
1. Use `docs/03` as the contract checklist for the emulation.
2. Use `docs/05` + the decompiled corpus to answer "what does the driver do
with the reply we just gave it" for any failure.
3. Use `docs/06`/`docs/07` to re-run the analysis when the driver, CUDA
runtime, GSP firmware or hardware changes.
4. Keep the guest stock; keep the interface emulated; keep the work executed.
Everything in this directory is organized around that sentence.
|