diff options
| author | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-09-29 14:38:44 -0400 |
|---|---|---|
| committer | Arthur Hanson Rasmusson <arthur@vgpu.io> | 2026-09-29 14:38:44 -0400 |
| commit | ca944b45ba35f3c0dc63fa9f722067af13e97ab4 (patch) | |
| tree | a4d0f1f3f2d62cc628ec8af2ca014e1f65d85589 /tools | |
| parent | dd1135ced1b1e5390034239086784a44bd2fca20 (diff) | |
| download | open-iov-ca944b45ba35f3c0dc63fa9f722067af13e97ab4.tar.gz open-iov-ca944b45ba35f3c0dc63fa9f722067af13e97ab4.zip | |
tools: pin GR push-buffer window from guest pagemap per boot; widen injected-pin table
The GR channel GPFIFO entries point at pushes in the client GPU VA window
(observed 0x200400000 + stride, one allocation per channel) and the pump
cannot read them: pushes_nomap grows and the tracking-semaphore releases
they carry never land. The pushes are sysmem pages mapped into the client
process, so /proc/<pid>/pagemap in the guest gives the GPA.
New guest helper guest_pmap_win.py batch-reads the pagemap of a VA range
(512 entries per pread). New host tool pin_gr_window.py rescans the window
on an interval, follows cdt pid changes, and injects VA->PA runs through
sema_map_add (same mechanism as the counters and the CE pushbuffer pins).
The injected-pin table grows 8k -> 32k slots so the CE pushbuffer (4096)
plus the GR window pages fit without drops.
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/guest-diag/guest_pmap_win.py | 47 | ||||
| -rw-r--r-- | tools/guest-diag/pin_gr_window.py | 184 | ||||
| -rwxr-xr-x | tools/guest-diag/push_guest_tools.sh | 1 |
3 files changed, 232 insertions, 0 deletions
diff --git a/tools/guest-diag/guest_pmap_win.py b/tools/guest-diag/guest_pmap_win.py new file mode 100644 index 0000000..a9b74a7 --- /dev/null +++ b/tools/guest-diag/guest_pmap_win.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# guest_pmap_win.py - dump present pages of a process's VA range through the +# guest's own pagemap (root in the guest). Batched reads (512 entries per +# pread) so a 256 MB window takes about a second, not minutes. +# +# Usage: sudo python3 guest_pmap_win.py <pid> <lo-hex> <hi-hex> [max-pairs] [pa-max-hex] +# Output: one "0x<va>,0x<pa>" line per present page (ascending VA), capped at +# max-pairs. PAs of 0 are skipped; pages above pa-max are skipped unless 0. +import os +import sys + +pid = int(sys.argv[1]) +lo = int(sys.argv[2], 16) & ~0xFFF +hi = int(sys.argv[3], 16) +maxp = int(sys.argv[4]) if len(sys.argv) > 4 else 20000 +pa_max = int(sys.argv[5], 16) if len(sys.argv) > 5 else 0 + +BATCH = 512 +out = [] +fd = os.open('/proc/%d/pagemap' % pid, os.O_RDONLY) +try: + va = lo + while va < hi: + n = min(BATCH, (hi - va) // 0x1000) + if n <= 0: + break + os.lseek(fd, (va // 4096) * 8, 0) + data = os.read(fd, n * 8) + got = len(data) // 8 + for i in range(got): + e = int.from_bytes(data[i * 8:i * 8 + 8], 'little') + if not ((e >> 63) & 1): + continue + pfn = e & ((1 << 55) - 1) + pa = pfn << 12 + if not pa: + continue + if pa_max and pa >= pa_max: + continue + out.append('0x%x,0x%x' % (va + i * 0x1000, pa)) + if len(out) >= maxp: + print('\n'.join(out)) + sys.exit(0) + va += got * 0x1000 +finally: + os.close(fd) +print('\n'.join(out)) diff --git a/tools/guest-diag/pin_gr_window.py b/tools/guest-diag/pin_gr_window.py new file mode 100644 index 0000000..4a968c9 --- /dev/null +++ b/tools/guest-diag/pin_gr_window.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# pin_gr_window.py - pin the CUDA client's GR push-buffer pages from guest +# truth, per boot (host-side dev tool, no guest changes beyond the helper). +# +# Why: the compute (GR) channels' GPFIFO entries point at pushes that live in +# the client's GPU VA window (observed one allocation per channel starting at +# 0x200400000 + stride). The pushes are sysmem pages mapped into the client +# process, so the guest's /proc/<pid>/pagemap gives their GPA. Injecting those +# translations (sema_map_add) is what lets the pump read the GR pushes and +# apply the tracking-semaphore releases they carry - the wall documented in +# PART 61 (pushes_nomap / deferred releases). +# +# The tool re-scans on an interval so pages mapped after the first pass are +# caught, and re-injects when a new client pid appears (pins are per-process +# pages; the same VA usually has a different PA on the next client/boot). +# +# Usage: tools/guest-diag/pin_gr_window.py [interval-seconds] +# Env: +# PINWIN_LO/PINWIN_HI VA window to scan (default 0x200400000..0x206000000) +# PINWIN_MAX max present pages per pass (default 20000) +# PINWIN_PA_MAX ignore PAs at/above this (skip device BARs) +# (default 0x180000000) +# PINWIN_VERIFY 1 = probe the first pins on the first batch (default) + +import os +import re +import subprocess +import sys +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/open_iov_vfio/parameters" +HELPER = os.path.join(HERE, "guest_pmap_win.py") + +LO = int(os.environ.get("PINWIN_LO", "0x200400000"), 16) +HI = int(os.environ.get("PINWIN_HI", "0x206000000"), 16) +MAXP = int(os.environ.get("PINWIN_MAX", "20000")) +PAMAX = int(os.environ.get("PINWIN_PA_MAX", "0x180000000"), 16) +VERIFY = os.environ.get("PINWIN_VERIFY", "1") == "1" + + +def log(msg): + print("[%s] %s" % (time.strftime("%H:%M:%S"), msg), flush=True) + + +def vexec(cmd, timeout=60): + 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 push_helper(): + with open(HELPER) as f: + b64 = subprocess.run(["base64", "-w0"], stdin=f, + capture_output=True, text=True).stdout + for _ in range(5): + vexec("echo '%s' | base64 -d > /tmp/guest_pmap_win.py" % b64, 40) + out = vexec("test -s /tmp/guest_pmap_win.py && echo PUSHED", 20) + if "PUSHED" in out: + return True + time.sleep(1) + return False + + +def cdt_pid(): + out = vexec("pgrep -x cdt", 25) + m = re.search(r"^\s*(\d+)\s*$", out, re.M) + return int(m.group(1)) if m else None + + +def scan(pid): + out = vexec("sudo python3 /tmp/guest_pmap_win.py %d 0x%x 0x%x %d 0x%x" + % (pid, LO, HI, MAXP, PAMAX), 90) + pairs = [] + for line in out.splitlines(): + m = re.match(r"^(0x[0-9a-f]+),(0x[0-9a-f]+)$", line.strip()) + if m: + pairs.append((int(m.group(1), 16), int(m.group(2), 16))) + return pairs + + +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): + r = subprocess.run(["sudo", "-n", "sh", "-c", cmd], + capture_output=True, text=True) + if r.returncode == 0 and "denied" not in (r.stderr or ""): + return True + time.sleep(0.05) + return False + + +def runs_of(pairs): + """Group (va,pa) pairs into contiguous VA+PA runs.""" + pairs = sorted(set(pairs)) + runs = [] + cur_va, cur_pa, cur_n = pairs[0][0], pairs[0][1], 1 + for va, pa in pairs[1:]: + if va == cur_va + cur_n * 0x1000 and pa == cur_pa + cur_n * 0x1000: + cur_n += 1 + continue + runs.append((cur_va, cur_pa, cur_n * 0x1000)) + cur_va, cur_pa, cur_n = va, pa, 1 + runs.append((cur_va, cur_pa, cur_n * 0x1000)) + return runs + + +def probe(va): + subprocess.run(["sudo", "-n", "sh", "-c", + "printf '0x%x,2' > %s/sema_probe" % (va, PARAM)], + capture_output=True, text=True) + time.sleep(0.4) + r = subprocess.run(["sudo", "-n", "dmesg"], capture_output=True, text=True) + tail = r.stdout.splitlines()[-14:] + vals = [] + for line in tail: + m = re.search(r"sema-probe:.*\[(\d+)\] 0x([0-9a-f]+)", line) + if m: + vals.append((int(m.group(1)), int(m.group(2), 16))) + return vals + + +def main(): + interval = float(sys.argv[1]) if len(sys.argv) > 1 else 2.0 + if not push_helper(): + log("FATAL: could not push guest helper") + return 1 + log("watching window 0x%x..0x%x (pamax 0x%x, max %d, interval %.1fs)" + % (LO, HI, PAMAX, MAXP, interval)) + seen = {} + pid = None + verified = False + while True: + p = cdt_pid() + if not p: + pid = None + time.sleep(interval) + continue + if p != pid: + pid = p + seen = {} + log("new cdt pid %d" % pid) + try: + pairs = scan(pid) + except Exception as e: + log("scan failed: %s" % e) + time.sleep(interval) + continue + new = [(va, pa) for va, pa in pairs if seen.get(va) != pa] + if new: + runs = runs_of(new) + npages = 0 + nfail = 0 + for va, pa, ln in runs: + if inject(va, pa, ln): + npages += ln // 0x1000 + else: + nfail += 1 + for i in range(ln // 0x1000): + if inject(va + i * 0x1000, pa + i * 0x1000, 0x1000): + npages += 1 + else: + nfail += 1 + for va, pa in new: + seen[va] = pa + log("pid=%d present=%d new=%d pinned=%dp/%dr fail=%d" + % (pid, len(pairs), len(new), npages, len(runs), nfail)) + for va, pa in new[:4]: + log(" va=0x%x -> pa=0x%x" % (va, pa)) + if VERIFY and not verified and new: + verified = True + for va, _pa in new[:2]: + vals = probe(va) + log(" probe va=0x%x: %s" % (va, " ".join( + "0x%x" % v for _, v in vals) or "(no reply)")) + time.sleep(interval) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/guest-diag/push_guest_tools.sh b/tools/guest-diag/push_guest_tools.sh index a96fe68..34866d2 100755 --- a/tools/guest-diag/push_guest_tools.sh +++ b/tools/guest-diag/push_guest_tools.sh @@ -33,6 +33,7 @@ push_b64 "$HERE/sem_counters.py" /tmp/sem_counters.py push_b64 "$HERE/sem_slots.py" /tmp/sem_slots.py push_b64 "$HERE/read_va.py" /tmp/read_va.py push_b64 "$HERE/guest_pmap.py" /tmp/guest_pmap.py +push_b64 "$HERE/guest_pmap_win.py" /tmp/guest_pmap_win.py push_b64 "$HERE/track_waits.sh" /tmp/track_waits.sh push_b64 "$HERE/guest_kprobes.sh" /tmp/kprobes.sh |
