summaryrefslogtreecommitdiffstats
path: root/userspace/cuda/docs/04-virtualization-and-emulation.md
blob: 89adc35cd19aa0d165b88fca144fbb44a2976171 (plain)
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.