summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/pin_gr_window.py
blob: 194ac9d04a88ff6f81937226b541af8ab637f0b1 (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
#!/usr/bin/env python3
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2026 Open-IOV contributors
# 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/vgsp/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]+)(?:,(\d+))?$", line.strip())
        if not m:
            continue
        va = int(m.group(1), 16)
        pa = int(m.group(2), 16)
        n = int(m.group(3)) if m.group(3) else 1
        for i in range(n):
            pairs.append((va + i * 0x1000, pa + i * 0x1000))
    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 1.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())