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
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
|
#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Open-IOV contributors
# pin_gr_stream.py - pin the CUDA client's GR push-buffer pages from a vsock
# stream produced by guest_pin_stream.py (guest truth via /proc/<pid>/pagemap).
#
# The guest scanner runs in the guest and streams only deltas, so the host
# learns a page the moment it is mapped - no guest-agent round trips on the
# critical path (the agent is only used once to install/start the scanner).
#
# Usage: tools/guest-diag/pin_gr_stream.py
# Env: PINWIN_LO/PINWIN_HI/PINWIN_PA_MAX as in pin_gr_window.py,
# PINWIN_PORT vsock port (default 45678)
import os
import re
import socket
import subprocess
import sys
import threading
import time
HERE = os.path.dirname(os.path.abspath(__file__))
VMEXEC = os.path.normpath(os.path.join(HERE, "..", "..", "scripts", "vgpu_vm.py"))
PARAM = "/sys/module/vgsp/parameters"
GUEST_SCRIPT = os.path.join(HERE, "guest_pin_stream.py")
PORT = int(os.environ.get("PINWIN_PORT", "45678"))
# 0x200000000 covers the GR GPFIFO rings/UVM channel mappings below the
# old 0x200400000 start; without them the host learns bogus vidmem pairs
# for the rings and never decodes GR pushes.
LO = os.environ.get("PINWIN_LO", "0x200000000")
HI = os.environ.get("PINWIN_HI", "0x206000000")
PAMAX = os.environ.get("PINWIN_PA_MAX", "0x180000000")
SEMSTREAM = os.environ.get("SEM_STREAM_FILE", "/tmp/opencode/sem_stream.txt")
# Liveness file checked by pin_supervisor.sh (mtime must stay < 75 s; the
# supervisor grants a 120 s startup grace on top).
HEARTBEAT_FILE = os.environ.get("PIN_GR_HEARTBEAT", "/tmp/opencode/pin_gr_heartbeat")
# Inter-tool re-assert request (written by semfix_uvmt.py's stall watchdog):
# when its mtime moves, re-assert every pair this tool has ever pinned.
REASSERT_REQ = os.environ.get("PIN_REASSERT_FILE",
"/tmp/opencode/pin_reassert_request")
# Stall watchdog: a known client with no pin/data activity for this long gets
# an automatic recovery (full re-assert + demand of the fixed counter/pool
# VAs). Pre-capture (no pin yet for this client) uses the tighter limit.
STALL_PRE_S = float(os.environ.get("PIN_STALL_PRE", "60"))
STALL_POST_S = float(os.environ.get("PIN_STALL_POST", "120"))
STALL_RETRY_S = float(os.environ.get("PIN_STALL_RETRY", "30"))
STALL_ESCALATE_S = float(os.environ.get("PIN_STALL_ESCALATE", "180"))
def log(msg):
print("[%s] %s" % (time.strftime("%H:%M:%S"), msg), flush=True)
def _heartbeat():
try:
with open(HEARTBEAT_FILE, "w") as f:
f.write("%.3f\n" % time.time())
except OSError:
pass
def _hb_loop():
"""Refresh the heartbeat on a fixed cadence, independent of data-path
activity: recv can be legally idle for minutes between client phases and
the supervisor used to read that silence as a hung tool (r198 restarts at
34 s staleness while injection bursts were running)."""
while True:
_heartbeat()
time.sleep(5)
def _mirror(line):
"""Append guest control lines (#pid/#sem) to the shared host file so the
single-QMP watcher can consume them without guest-agent round trips."""
try:
with open(SEMSTREAM, "a") as f:
f.write(line + "\n")
except OSError:
pass
def vexec(cmd, timeout=40):
try:
r = subprocess.run([VMEXEC, "exec", "--timeout", str(timeout - 5), cmd],
capture_output=True, text=True, timeout=timeout)
return r.stdout
except Exception:
return ""
def _scanner_alive():
"""True when the in-guest scanner process still exists.
install_and_start() pkill's and restarts the scanner, which resets every
in-guest latch; probing first avoids killing a healthy scanner that is
merely silent because no client exists yet (r198 pre-client churn)."""
out = vexec("sudo sh -c 'kill -0 $(cat /tmp/guest_pin_stream.pid) "
"2>/dev/null && echo ALIVE'")
return "ALIVE" in (out or "")
def _demand_send(vas):
"""Append VA hints to the guest scanner's demand file (best-effort).
Same channel pin_gr_stream._demand_loop uses for host-dmesg stalls; the
stall watchdog uses it to ask the scanner to re-resolve the fixed
pre-capture VAs (counter page / UVM pool) by content/pagemap."""
lines = "".join("0x%x\n" % va for va in vas)
try:
vexec("printf '%%s' '%s' >> /tmp/guest_want_vas.txt" % lines, 30)
except Exception:
pass
def _start_cmd():
with open(GUEST_SCRIPT) as f:
b64 = subprocess.run(["base64", "-w0"], stdin=f,
capture_output=True, text=True,
timeout=10).stdout
prefixes = os.environ.get("CLIENT_PREFIXES", "cdt,va,victory")
vma_every = os.environ.get("STREAM_VMA_EVERY", "1")
# Kill EVERY existing scanner, not just the pid-file one: a retry after
# a partially-failed first attempt (pid file not written yet) used to
# leave the first scanner alive, and multiple scanners each re-assert
# the whole CE pair set - r148 ran three and drove the guest to load 19
# (clients wedged in userspace spins). The pattern is anchored
# (^python3) and assembled at runtime via printf: an unanchored pattern
# or a literal one would also match this command's own shell (its
# cmdline contains the script path), and pkill would kill the shell
# before it could start the new scanner - every start attempt then
# failed silently (r149).
return ("P=$(printf '^python3 /tmp/guest_pin_%%s.py' stream); "
"sudo pkill -f \"$P\" 2>/dev/null; sleep 1; "
"echo '%s' | base64 -d > /tmp/guest_pin_stream.py; "
"sudo rm -f /tmp/guest_pin_stream.pid; "
"sudo setsid nohup env CLIENT_PREFIXES='%s' "
"STREAM_VMA_EVERY='%s' "
"python3 /tmp/guest_pin_stream.py %d %d %s %s %s "
">/tmp/guest_pin_stream.log 2>&1 & sleep 2; "
"sudo cat /tmp/guest_pin_stream.pid 2>/dev/null"
% (b64, prefixes, vma_every, 2, PORT, LO, HI, PAMAX))
def install_and_start():
# The guest-agent exec can fail transiently (agent busy just after boot,
# or a stale qga client); retry a few times instead of giving up, because
# a boot without the scanner streams no pins and the client then hangs.
for attempt in range(5):
_heartbeat()
out = vexec(_start_cmd(), 60)
if any(ch.isdigit() for ch in out):
return True
log("WARN: scanner start attempt %d failed; retrying" % (attempt + 1))
time.sleep(5)
return False
def inject(va, pa, ln):
cmd = "printf '0x%x,0x%x,0x%x,1' > %s/sema_map_add" % (va, pa, ln, PARAM)
for _ in range(60):
try:
r = subprocess.run(["sudo", "-n", "sh", "-c", cmd],
capture_output=True, text=True, timeout=10)
except subprocess.TimeoutExpired:
time.sleep(0.05)
continue
if r.returncode == 0:
return True
time.sleep(0.05)
return False
def scrub_register(fin_pa, hsem_pa):
"""Register the PMA scrubber buffer words (guest-resolved PAs) with the
service so it can author the scrub progress (third-client scrub freeze)."""
cmd = ("printf '0x%x,0x%x' > %s/sema_scrubbuf_add"
% (fin_pa, hsem_pa, PARAM))
for _ in range(60):
try:
r = subprocess.run(["sudo", "-n", "sh", "-c", cmd],
capture_output=True, text=True, timeout=10)
except subprocess.TimeoutExpired:
time.sleep(0.05)
continue
if r.returncode == 0:
log("scrub registered fin=0x%x hsem=0x%x" % (fin_pa, hsem_pa))
return True
time.sleep(0.05)
log("scrub register FAILED fin=0x%x hsem=0x%x" % (fin_pa, hsem_pa))
return False
def _demand_loop():
"""Push the host's unreadable CE push VAs to the guest scanner.
The service logs each distinct stalled VA once per boot ('stalled VA
#N: 0x...'). Each sighting is tracked and re-sent every 20 s for five
minutes: the guest resolves the VA by content against the
nv_vm_map_pages areas (event-independent, no object chain), so a page
that was not yet written when first asked is picked up on a retry.
This closes the stall class where the one-shot #sem kprobe event was
lost and the chain resolver never learns the client.
"""
track = {} # va -> [first_seen, last_sent]
while True:
time.sleep(3)
try:
r = subprocess.run(["sudo", "-n", "dmesg"],
capture_output=True, text=True, timeout=20)
except Exception:
continue
now = time.time()
for m in re.finditer(r"(?:stalled VA #\d+|pending release starved va)=(0x[0-9a-f]+)",
(r.stdout or "")[-200000:]):
va = int(m.group(1), 16)
track.setdefault(va, [now, 0.0])
for va in list(track):
first, last = track[va]
if now - first > 300.0:
del track[va]
todo = [va for va, (f, l) in track.items() if now - l > 20.0]
if not todo:
continue
lines = "".join("0x%x\n" % va for va in todo)
try:
vexec("printf '%%s' '%s' >> /tmp/guest_want_vas.txt" % lines, 30)
except Exception:
continue
for va in todo:
track[va][1] = now
log("demand: sent %d stalled VA(s) to guest" % len(todo))
_DEMAND_STARTED = False
_HB_STARTED = False
_REASSERT_REQ_SEEN = 0.0
def main():
global _DEMAND_STARTED, _HB_STARTED
_heartbeat()
if not _HB_STARTED:
_HB_STARTED = True
try:
threading.Thread(target=_hb_loop, daemon=True).start()
except Exception as e:
log("WARN: heartbeat thread failed to start: %s" % e)
if not install_and_start():
log("WARN: could not start guest scanner (will retry via vsock wait)")
if not _DEMAND_STARTED:
_DEMAND_STARTED = True
try:
threading.Thread(target=_demand_loop, daemon=True).start()
except Exception as e:
log("WARN: demand thread failed to start: %s" % e)
srv = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((socket.VMADDR_CID_ANY, PORT))
srv.settimeout(10)
srv.listen(4)
log("listening on vsock port %d (window %s..%s)" % (PORT, LO, HI))
seen = {}
pin_total = 0
last_conn = time.time()
last_reinstall = 0.0
# Stall-watchdog state for the connected client. client_pid is the
# scanner-reported pid (None until a client appears); last_activity marks
# the last pin/data/control event; pins_since_client counts pages pinned
# for the current client (0 = pre-capture).
client_pid = None
client_start = None
last_activity = time.time()
pins_since_client = 0
stall_since = None
recover_next = 0.0
escalated = False
no_conn_warn_next = 0.0
def _activity():
nonlocal last_activity, stall_since, escalated
last_activity = time.time()
stall_since = None
escalated = False
while True:
try:
conn, _ = srv.accept()
except socket.timeout:
now = time.time()
# No guest scanner connection for 30 s: it died or never started
# (a boot without it streams no pins and every client then hangs).
# Probe the scanner first instead of reinstalling blindly:
# install_and_start() pkill's a live scanner and resets all
# in-guest latches, and pre-client silence is normal (r198 churn).
if now - last_conn > 30.0 and now - last_reinstall >= 60.0:
last_reinstall = now
if _scanner_alive():
log("no guest connection for %.0fs but scanner alive; "
"not reinstalling" % (now - last_conn))
if now - last_conn > 180.0 and now >= no_conn_warn_next:
no_conn_warn_next = now + 60.0
log("STALL WATCHDOG: no guest scanner connection for "
"%.0fs (scanner process alive) - pins/demand may "
"be stranded" % (now - last_conn))
continue
log("reinstall attempt: no guest connection for %.0fs"
% (now - last_conn))
try:
install_and_start()
except Exception as e:
log("reinstall failed: %s" % e)
continue
except OSError as e:
log("accept failed: %s" % e)
time.sleep(1)
continue
last_conn = time.time()
_heartbeat()
conn.settimeout(5)
log("guest connected")
buf = b""
runs = []
try:
while True:
try:
chunk = conn.recv(1 << 20)
except socket.timeout:
if runs:
novel = _novel_pages(runs, seen)
pages = _flush(runs, seen)
pin_total += pages
pins_since_client += pages
if novel:
_activity()
runs = []
_maybe_reassert_request(seen)
now = time.time()
# Stall watchdog: a known client with no pin/data
# activity gets a bounded, idempotent recovery (re-assert
# everything + demand the fixed counter/pool VAs), then a
# loud escalation if the stall persists. This is the
# "first cudaMalloc / first GR capture must not stall
# silently" guard. recover_next rate-limits attempts (the
# loop wakes every 5 s on the recv timeout).
if client_pid is not None:
limit = STALL_PRE_S if pins_since_client == 0 \
else STALL_POST_S
idle = now - last_activity
if idle >= limit and now >= recover_next:
recover_next = now + STALL_RETRY_S
if stall_since is None:
stall_since = now
log("STALL WATCHDOG: client pid %d - no pin "
"activity for %.0fs (pre-capture=%s) - "
"re-asserting all known pairs + demanding "
"counter/pool VAs"
% (client_pid, idle, pins_since_client == 0))
if seen:
_flush([], seen, full=True)
_demand_send([0x20400f000, 0x121000000])
if not escalated and stall_since is not None \
and now - stall_since >= STALL_ESCALATE_S:
escalated = True
log("STALL WATCHDOG: recovery FAILED for pid %d "
"after %.0fs - stall persists, needs manual "
"attention" % (client_pid, now - stall_since))
continue
except OSError:
break
if not chunk:
break
_heartbeat()
buf += chunk
while b"\n" in buf:
line, buf = buf.split(b"\n", 1)
text = line.decode(errors="replace").strip()
if not text:
continue
if text.startswith("#pid"):
m = re.match(r"#pid\s+(\d+)(?:\s+(\d+))?", text)
seen = {}
client_pid = int(m.group(1)) if m else None
client_start = (int(m.group(2)) if m and m.group(2)
else None)
pins_since_client = 0
_activity()
log("guest reports cdt pid %s%s"
% (client_pid if client_pid is not None else "?",
(" start=%s" % client_start
if client_start is not None else "")))
_mirror(text)
continue
if text.startswith("#nopid"):
# In-band client death: the guest scanner tells us the
# client is gone (with its identity). Drop the per-pair
# dedupe map so the next client's pairs are injected
# unconditionally; semfix_uvmt consumes the mirrored
# line and purges its per-client state too.
client_pid = None
client_start = None
seen = {}
pins_since_client = 0
_activity()
log("guest reports client gone (%s)" % text)
_mirror(text)
continue
if text.startswith("#sem"):
_mirror(text)
_activity()
continue
if text.startswith("#pb"):
_mirror(text)
_activity()
continue
if text.startswith("#put"):
log(text)
_mirror(text)
_activity()
continue
if text.startswith("#scrub"):
m = re.search(r"0x([0-9a-fA-F]+),0x([0-9a-fA-F]+)",
text)
_mirror(text)
if m:
scrub_register(int(m.group(1), 16),
int(m.group(2), 16))
continue
m = re.match(r"^(0x[0-9a-f]+),(0x[0-9a-f]+)(?:,(\d+))?$",
text)
if m:
runs.append((int(m.group(1), 16),
int(m.group(2), 16),
int(m.group(3)) if m.group(3) else 1))
if runs:
novel = _novel_pages(runs, seen)
pages = _flush(runs, seen)
pin_total += pages
pins_since_client += pages
if novel:
_activity()
runs = []
# Poll the semfix re-assert request on every recv cycle, not
# only on recv-timeouts: sustained scanner traffic starves the
# timeout branch, so the request fired post-mortem in r203
# (119 s late, `seen` already cleared by #nopid).
_maybe_reassert_request(seen)
except Exception as e:
# A transient error on one connection must not kill the process:
# log it, drop the connection and go back to accept().
log("connection handler error: %s (continuing)" % e)
log("guest disconnected (pins this session: %d)" % pin_total)
try:
conn.close()
except Exception:
pass
def _maybe_reassert_request(seen):
"""Honour a full-re-assert request written by semfix_uvmt's watchdog."""
global _REASSERT_REQ_SEEN
try:
m = os.stat(REASSERT_REQ).st_mtime
except OSError:
return
if m > _REASSERT_REQ_SEEN:
_REASSERT_REQ_SEEN = m
if seen:
log("semfix requested a full pin re-assert - re-asserting %d "
"pair(s)" % len(seen))
_flush([], seen, full=True)
def _coalesce_seen(seen):
"""Coalesce the seen {va: pa} per-page map into (va, pa, npages) runs."""
items = sorted(seen.items())
runs = []
i = 0
while i < len(items):
va, pa = items[i]
n = 1
while (i + n < len(items)
and items[i + n][0] == va + n * 0x1000
and items[i + n][1] == pa + n * 0x1000):
n += 1
runs.append((va, pa, n))
i += n
return runs
def _novel_pages(runs, seen):
"""Count pages in runs whose translation is new or changed.
Progress accounting for the stall watchdog: the guest re-streams the CE
window and UVM pool every couple of seconds even when nothing moves, so a
pure repeat of an already-known pair must not count as pin activity
(r203: those repeats masked the watchdog through every hang)."""
return sum(1 for va, pa, n in runs for i in range(n)
if seen.get(va + i * 0x1000) != pa + i * 0x1000)
def _flush(runs, seen, full=False):
"""Inject run lines (one batched sudo pass), return number of pages pinned.
The old implementation spawned one sudo process per run; with ~400 runs
that cost ~4 s, which landed the GR pins after the client's context-init
deadline. The batch writes all runs in a single shell, retrying only the
ones the kernel queue rejected (ENOSPC) individually.
full=True re-asserts every pair ever pinned (coalesced), not just this
batch's runs: the kernel session flush wipes the pin table when a client's
channels close, and the stall watchdog / semfix re-assert request use this
to heal it without touching the guest. Writes are idempotent.
"""
todo = {}
for va, pa, n in runs:
# The CE window (0x120000000..0x121000000) and the UVM semaphore
# pool page range (0x121000000..0x122000000) are re-asserted by the
# guest every couple of seconds on purpose: the host's session flush
# wipes the whole pin table when the previous client's last channel
# closes, and a pin batch that raced in for the new client must be
# allowed to land again even when the pair is unchanged. Skipping
# repeats there stranded the second client's CE pushes (the r144/
# r145 stall: sema-probe "NO ENTRY" for the live push pages) and the
# pool/tracker page (pending release starved, every client's first
# seconds).
ce_win = 0x120000000 <= va < 0x122000000
if not full and not ce_win and seen.get(va) == pa and n == 1:
continue
todo[(va, pa, n)] = True
if full:
for va, pa, n in _coalesce_seen(seen):
todo[(va, pa, n)] = True
todo = list(todo)
if not todo:
return 0
def mark(va, pa, n):
for i in range(n):
seen[va + i * 0x1000] = pa + i * 0x1000
script = []
for va, pa, n in todo:
script.append(
"printf '0x%x,0x%x,0x%x,1' > %s/sema_map_add 2>/dev/null || "
"echo 'FAIL 0x%x 0x%x 0x%x'"
% (va, pa, n * 0x1000, PARAM, va, pa, n * 0x1000))
failed = set()
CHUNK = 200
for start in range(0, len(script), CHUNK):
batch = script[start:start + CHUNK]
_heartbeat()
try:
r = subprocess.run(["sudo", "-n", "sh", "-c", "\n".join(batch)],
capture_output=True, text=True, timeout=60)
for line in r.stdout.splitlines():
m = re.match(r"FAIL (0x[0-9a-f]+) (0x[0-9a-f]+) (0x[0-9a-f]+)",
line.strip())
if m:
failed.add((int(m.group(1), 16), int(m.group(2), 16),
int(m.group(3), 16) // 0x1000))
except Exception as e:
log("batch inject failed: %s" % e)
for va, pa, n in todo[start:start + CHUNK]:
failed.add((va, pa, n))
pages = 0
for va, pa, n in todo:
if (va, pa, n) in failed:
continue
mark(va, pa, n)
pages += n
for va, pa, n in failed:
for i in range(n):
if inject(va + i * 0x1000, pa + i * 0x1000, 0x1000):
mark(va + i * 0x1000, pa + i * 0x1000, 1)
pages += 1
if pages:
log("pinned %d page(s) in %d run(s)" % (pages, len(todo)))
for va, pa, n in todo[:3]:
log(" va=0x%x -> pa=0x%x (%d)" % (va, pa, n))
return pages
if __name__ == "__main__":
# main() loops forever; only an unexpected exception exits it. Restart
# instead of dying silently - a dead listener means no pins and every
# later CUDA client hangs at first cudaMalloc.
while True:
try:
main()
except Exception as e:
log("FATAL: main() exited: %r - restarting in 2s" % (e,))
time.sleep(2)
|