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
|
# UVM — Unified Memory in the Guest
> UVM (`nvidia_uvm.ko`) is the most demanding consumer of the virtual GPU's
> semantics, and today the best measurement of how faithful our emulation is.
> This document explains what UVM does, what it needs, and what we learned
> from watching it run against our device.
## 1. What UVM is
UVM provides unified memory and GPU-driven page-fault handling. In practice it
is a second GPU driver, layered on the RM:
* **VA spaces** (`uvm_va_space`) — per-process GPU address spaces.
* **Channel managers** — pools of GPU channels (CE, SEC2, GR-as-needed) that
UVM itself uses to execute its own memory-management work.
* **Push buffers / GPFIFO** — UVM builds its own pushes and submits them.
* **Semaphores and trackers** — UVM tracks GPU progress by waiting on
semaphores that the GPU writes when work completes.
* **Page trees** — UVM manages GPU page tables for its VA spaces.
* **Fault handling** — GPU page faults are delivered to UVM (via a fault
buffer and a notification channel), and UVM resolves them by populating page
tables and replaying/cancelling faults.
For CUDA, UVM is entered early: `cuInit` → driver init → `UVM_REGISTER_GPU`.
If UVM cannot complete GPU registration, CUDA does not start.
## 2. The registration path (traced end to end)
```
libcuda: UVM_REGISTER_GPU ioctl (0x25)
uvm_api_register_gpu
uvm_va_space_register_gpu
uvm_gpu_retain_by_uuid
nvUvmInterfaceRegisterGpu / GetGpuInfo
alloc_parent_gpu / add_gpu
nvUvmInterfaceDeviceCreate (RM object creation)
uvm_hal_init_gpu / get_gpu_caps (CE caps! see below)
uvm_ats_add_gpu / uvm_gpu_init_isr (fault/intr plumbing)
init_channel_manager
create semaphore pool (sysmem)
create channels per CE (each channel: GPFIFO + pushbuffer + semaphores)
channel_init() → first push → semaphore release → wait
uvm_page_tree_init (page tables for the VA space)
```
Each stage has a distinct failure signature; our debugging docs map them.
## 3. What UVM requires from the "GPU"
| Requirement | Detail | Our status |
|---|---|---|
| GPU identity | must match the uuid from RM | ✅ |
| CE capabilities | per-engine support bits; UVM *requires* `supported && !grce && sysmem` for CPU_TO_GPU/GPU_TO_CPU channels | 🟡 the caps we answer are accepted, the inventory now includes the async copy engine, and CE copies are forwarded to the physical copy engine (default off) with engine-authored releases |
| Channel creation | RM channel objects with GPFIFO/userd/instance parameters | ✅ objects created |
| Pushes executed | UVM's first action on a new channel is an *init push* followed by a semaphore-tracked wait | 🟡 the host decodes method streams and executes memsets, copies and semaphore releases in software (a bridge); CE-class copies and releases are additionally forwarded to the physical copy engine (default off); no GR kernel is forwarded yet (blocked on GR engine-context validity) |
| Page trees | UVM initializes page trees with its own pushes; the *server-owned* ranges must be populated by us | 🟡 |
| Fault delivery | fault buffer + notification; replay/cancel semantics | 🟡 backend-side fault observation/relay exists (default off: notifier write + event post); the guest replay/cancel contract is open |
| Semantics of completions | tracker values must advance exactly | 🟡 |
**Current frontier (2026-10-02):** context creation and the full CUDA
driver-API smoke pass (`ALL OK`, copies matched); the execution backend is
live and CE-class copies are forwarded to the physical copy engine (default
off) with engine-authored releases and a fault relay. GR kernel execution is
blocked on GR engine-context validity; the remaining UVM work is completion
translation and the guest-side fault replay/cancel contract
([`08-physical-gpu-integration.md`](08-physical-gpu-integration.md)).
Failed-context teardown has produced guest kernel oopses in
`uvm_pmm_devmem_page_to_va_space` in earlier runs. The CUDA-side
contract and reply rules are in
[`../../userspace/cuda/docs/03-driver-and-gpu-interface.md` §9](../../userspace/cuda/docs/03-driver-and-gpu-interface.md)
and
[`../../userspace/cuda/docs/04-virtualization-and-emulation.md` §6](../../userspace/cuda/docs/04-virtualization-and-emulation.md);
start at
[`../../userspace/cuda/READ-ME-FIRST.md`](../../userspace/cuda/READ-ME-FIRST.md).
## 4. The most important discovery: how UVM waits
UVM's tracker (`uvm_tracker.c`) is the single most visible symptom of an
unfaithful GPU. It spins, and after 30 seconds it dumps:
```
uvm_tracker.c:285 Tracker entry for value 1 (sema VA 0x121000000)
channel ID 1:3 (0x1:0x3) CE 0
uvm_channel.c:4184 pending push '' started at uvm_channel.c:2535 in channel_init()
releasing value 1
```
What this means, mechanically:
1. UVM built a push in a push buffer and wrote a **semaphore release** into it:
a method sequence (CE channel class, e.g. `TURING_DMA_COPY_A`) that sets an
address and a payload and then launches a DMA operation that writes the
payload to memory.
2. UVM submitted the push (GPFIFO entry + GPPUT/doorbell) and now waits for the
semaphore payload to reach `value`.
3. On real hardware, the copy engine writes the payload into the semaphore's
memory. On our device, *nobody does* unless we do it.
The push we found in guest RAM (found by scanning for its distinctive byte
sequence) decodes as:
```
20010000 0000c5b5 set-object (TURING_DMA_COPY_A)
2001a000 0000c076 set-object (GP100_UVM_SW)
20038090 00000001 incrementing-method header: count 3, addr 0x240
+ data: upper 32 bits of the semaphore VA
21000000 00000001 lower 32 bits of the VA; payload (1)
200180c0 0000000c LAUNCH_DMA with SEMAPHORE_TYPE=RELEASE_ONE_WORD
```
So: **the emulation's job for this push is to write `payload` to the memory at
the semaphore's GPU VA.** Which requires a GPU-VA → guest-PA translation.
How the host performs it: while the channel's GPFIFO has pending entries the
service reads each push through the learned VA→PA map, decodes the method
stream and runs the pushed work (page-table memsets, copies, semaphore
releases). Whenever a push page cannot be resolved - or a channel consumes
entries without any release landing, which means the translation pointed at a
stale page - the service falls back to content: guest RAM is scanned page by
page for release groups (`SET_SEMAPHORE_A/B` + `LAUNCH_DMA` with
`SEMAPHORE_TYPE=RELEASE_ONE_WORD`) and the pending payloads are applied
forward-only. A rolling 32 MB/pass sweep runs in parallel; a stalled channel
additionally triggers a faster sliced full-RAM pass (256 MB per service pass)
so a client wait clears in seconds instead of waiting for the rolling sweep to
come around. A release is applied only when `payload > current`, so recycled
push buffers and stale copies are harmless.
One pitfall observed live: the learned map is rebuilt from a content scan, and
a semaphore-page VA can flap between two physical pages across rebuilds (a
freed allocation's record competing with the live one). Releases then land on
a page the client never reads and its tracker wait stops advancing even though
the emulation "applied" the value. When a wait stalls while the applied
counter climbs, check that the semaphore VA resolves to a stable PA; pinning
the validated translation fixes it.
## 5. The memory model UVM assumes (and what we learned)
* UVM's semaphore pool is **system memory** (`pool->aperture = UVM_APERTURE_SYS`),
one 4 KiB page per channel manager, addressed as 4-byte slots
(`UVM_SEMAPHORE_SIZE = 4`, 1024 slots per page).
* Each semaphore page has a *GPU VA* (chosen by the client's RM) and a *CPU VA*
(a kernel mapping). UVM polls the CPU VA; the GPU writes the GPU VA.
* **Both addresses are recorded in UVM's structures in guest memory.** This is
the key that makes translation possible without any extra help from the
guest (see the host-module doc for the exact structures and offsets).
* The CPU VA is a **direct-map** address, so
`PA = cpu_va - page_offset_base`. Verified live: writing the payload to that
physical address completes the push and UVM advances.
* Values are *per channel* and *monotonic*: a channel's first push releases 1,
the next 2, and so on. UVM has its own sanity check
(`UVM_GPU_SEMAPHORE_MAX_JUMP`) — writing a wrong value is detected and
reported, which is useful for validation.
* The client's **GPU page tables for server-owned ranges are empty chains**:
the client hands us the *physical addresses of its page-table levels* in
`NV90F1_CTRL_CMD_VASPACE_COPY_SERVER_RESERVED_PDES` and expects the server to
own the lower levels. (See the GSP-RM doc, §5.)
## 6. Page trees and faults
* `uvm_page_tree_init` initializes the page tables for a VA space using pushes
on UVM's own channels. These pushes are memops / page-table writes; on real
hardware they are executed by the copy engine.
* Fault handling is event-driven: UVM programs a **fault buffer** (a
replayable and a non-replayable buffer, whose sizes come from
`INTERNAL_GMMU_GET_STATIC_INFO` — answered by our emulation) and receives
notifications when the GPU takes a fault. UVM then:
1. reads the fault entry,
2. populates the page table (possibly migrating memory),
3. **replays or cancels the fault** with a `MEM_OP` push.
* For our forwarding design, faults are where "the guest's memory model" and
"the real GPU's memory model" must be reconciled. This is the deepest part
of the remaining work, and is tracked as a known-unknown in
[`../04-implementation/01-known-knowns-and-unknowns.md`](../04-implementation/01-known-knowns-and-unknowns.md).
## 7. How to observe UVM
In the guest:
```bash
# UVM prints nothing by default. Turn it on:
echo 1 | sudo tee /sys/module/nvidia_uvm/parameters/uvm_debug_prints
# Messages then appear with the "nvidia-uvm:" prefix.
# Other knobs: uvm_release_asserts, uvm_release_asserts_dump_stack,
# uvm_enable_debug_procfs, uvm_debug_enable_push_desc.
```
Known-good markers, in order:
```
uvm_gpu_retain_by_uuid → ... → channel manager created
→ uvm_page_tree_init pushes
→ (on success) UVM_REGISTER_GPU returns and CUDA proceeds
```
Failure signatures we have characterized:
| Message | Meaning |
|---|---|
| `Failed to initialize the channel manager: NV_ERR_NOT_SUPPORTED` | CE caps were wrong/missing (we fixed this: see the field guide) |
| `stuck waiting` + `Tracker entry ... channel_init()` | the push's semaphore was never written (the emulation must write it) |
| `Assert failed ... unexpected semaphore ... jump from 0x0 to 0x1010101` | we wrote a wrong payload (and UVM caught us — useful!) |
## 8. Why UVM is the perfect correctness oracle
UVM exercises in minutes what a driver test suite would test in hours:
memory allocation, page tables, channel setup, push generation, semaphore
semantics, fault handling, error recovery — all with continuous self-checks and
loud diagnostics. When UVM registers cleanly and CUDA workloads pass, the
device is behaving like a real GPU. That is why "UVM advances" is the project's
primary correctness metric on the way to CUDA.
|