summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-09-29 15:16:39 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-09-29 15:16:39 -0400
commit1df5ff4729a6ad2d93bdd8707c0837e3bf9581b6 (patch)
tree84bee1b9705fcdf3938ff7e9ecb2b3180c01f170
parent06f432b40178c5fb6361e96d89c232bcbb459fd4 (diff)
downloadopen-iov-1df5ff4729a6ad2d93bdd8707c0837e3bf9581b6.tar.gz
open-iov-1df5ff4729a6ad2d93bdd8707c0837e3bf9581b6.zip
sema-svc: apply decoded releases to registered counter slots; single-QMP semfix+uvmt watcher
-rw-r--r--kernel/gsp_sema_service.c82
-rwxr-xr-xtools/guest-diag/semfix_uvmt.py305
2 files changed, 385 insertions, 2 deletions
diff --git a/kernel/gsp_sema_service.c b/kernel/gsp_sema_service.c
index ce2e0c9..ae8f7da 100644
--- a/kernel/gsp_sema_service.c
+++ b/kernel/gsp_sema_service.c
@@ -350,6 +350,7 @@ struct sema_svc {
u64 releases_stale; /* payload <= current */
u64 releases_jumped; /* applied with gap > 1 */
u64 releases_faked; /* skipped-entry fallback */
+ u64 counter_rel_applied; /* releases to registered counters */
u64 builds;
u64 gpfifo_entries_done; /* entries consumed via GPFIFO */
u64 gpfifo_nomap; /* GPFIFO/pushbuf VA not mapped */
@@ -1766,11 +1767,12 @@ static void svc_rebuild(struct nv_vgpu_device *vgpu)
svc.releases_stale, svc.releases_jumped,
svc.walks_ok, svc.walks_ok + svc.walks_miss, svc.lat_hits,
svc_scan_cursor);
- pr_info("sema-svc: pages=%llu pushes=%llu pending=%llu nomap=%llu memset=%llu/%llu/%llu ringops=%llu denied=%llu false=%llu skip=%llu faked=%llu\n",
+ pr_info("sema-svc: pages=%llu pushes=%llu pending=%llu nomap=%llu memset=%llu/%llu/%llu ringops=%llu denied=%llu false=%llu skip=%llu faked=%llu crel=%llu\n",
svc.pages_decoded, svc.pushes_seen, svc.pushes_pending,
svc.pushes_nomap, svc.memsets_done, svc.memsets_nomap,
svc.memsets_fail, svc.ring_ops, svc.ops_denied,
- svc.false_trans, svc.gpfifo_skipped, svc.releases_faked);
+ svc.false_trans, svc.gpfifo_skipped, svc.releases_faked,
+ svc.counter_rel_applied);
pr_info("sema-svc: gpfifo done=%llu nomap=%llu vidmem_writes=%llu ring_auto=%u pb=%u/%u pins=%u/%llu ipins=%u/%llu\n",
svc.gpfifo_entries_done, svc.gpfifo_nomap,
svc.vidmem_writes, svc_ring_n, svc_pb_n, svc_pb_passes,
@@ -2059,6 +2061,56 @@ static bool svc_method_stream_plausible(const u8 *buf, u32 len)
static u64 svc_rel_va_seen;
static u32 svc_dbg_streams;
+/*
+ * Apply a decoded release directly to a channel's registered completion
+ * counter slot.
+ *
+ * GR pushes release the client's per-channel completion counter and the client
+ * tracks push completion by polling that u32. The release VA is known from the
+ * push, but its VA->PA translation may be missing (the counter page is a
+ * client UVM allocation that is not in any scanned window) or stale (recycled
+ * client), while the slot's guest PA is known exactly: it is registered
+ * through the sema_counter parameter from the client's own pagemap. Writing
+ * the payload there is what lets the client's wait observe the release
+ * regardless of the VA map. Forward-only, so a stale copy of an
+ * already-completed push cannot rewind the counter.
+ */
+static void svc_counter_release_apply(struct nv_vgpu_device *vgpu, u32 h,
+ u32 payload)
+{
+ unsigned long flags;
+ u64 pa = 0;
+ u32 cur = 0;
+ int i;
+
+ if (!h || !payload)
+ return;
+
+ spin_lock_irqsave(&svc.chan_lock, flags);
+ for (i = 0; i < SVC_MAX_CHANNELS; i++) {
+ if (svc.chan[i].valid && svc.chan[i].h_object == h) {
+ pa = svc.chan[i].counter_pa;
+ break;
+ }
+ }
+ spin_unlock_irqrestore(&svc.chan_lock, flags);
+ if (!pa)
+ return;
+
+ if (svc_read_ap(vgpu, pa, SVC_APERTURE_SYS, &cur, sizeof(cur)))
+ return;
+ if (payload <= cur || payload - cur > 0x100000)
+ return;
+ if (svc_write_ap(vgpu, pa, SVC_APERTURE_SYS, &payload,
+ sizeof(payload)))
+ return;
+ svc.releases_applied++;
+ svc.counter_rel_applied++;
+ svc.post_hint_until = jiffies + 10 * HZ;
+ pr_info_ratelimited("sema-chan: counter release h=0x%x pa=0x%llx val=%u (was %u)\n",
+ h, pa, payload, cur);
+}
+
static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf,
u32 len, u64 page_pa, bool allow_ops,
u32 ch_h, u32 ch_eng, u64 push_va,
@@ -2482,6 +2534,18 @@ static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf,
svc.post_hint_until = jiffies + 10 * HZ;
}
}
+
+ /*
+ * Exact-slot fallback: the releasing channel's
+ * completion counter was enumerated from the
+ * client's pagemap, so the release can be
+ * applied to the very slot the client polls
+ * even when the release VA has no (or a stale)
+ * translation. Same forward-only guard as the
+ * VA path.
+ */
+ svc_counter_release_apply(vgpu, ch_h,
+ st.sem_payload);
/* release ends the push */
n_ops = 0;
}
@@ -3741,6 +3805,7 @@ static void svc_counter_publish(struct nv_vgpu_device *vgpu,
struct svc_channel *ch, u32 put)
{
u32 val;
+ u32 cur = 0;
if (!ch->counter_pa)
return;
@@ -3748,6 +3813,19 @@ static void svc_counter_publish(struct nv_vgpu_device *vgpu,
val = put;
if (val == ch->counter_last)
return;
+ /*
+ * A decoded release (svc_counter_release_apply) may already have
+ * advanced the slot past the put position the client last submitted;
+ * the client polls this u32, so never write it backwards.
+ */
+ if (svc_read_ap(vgpu, ch->counter_pa, SVC_APERTURE_SYS, &cur,
+ sizeof(cur)))
+ return;
+ if (val <= cur) {
+ if (cur > ch->counter_last)
+ ch->counter_last = cur;
+ return;
+ }
if (svc_write_ap(vgpu, ch->counter_pa, SVC_APERTURE_SYS,
&val, sizeof(val)))
return;
diff --git a/tools/guest-diag/semfix_uvmt.py b/tools/guest-diag/semfix_uvmt.py
new file mode 100755
index 0000000..12577ab
--- /dev/null
+++ b/tools/guest-diag/semfix_uvmt.py
@@ -0,0 +1,305 @@
+#!/usr/bin/env python3
+# semfix_uvmt.py - single-QMP tracking-semaphore/resolution watcher.
+#
+# One QMP client does everything semfix_watch.py and uvmt_poll.py did, because
+# QEMU's QMP socket accepts a single client: running the two tools side by side
+# silently broke semfix's pinning.
+#
+# Jobs:
+# 1. keep the client's tracking-semaphore pool pages and push-buffer pages
+# pinned with fresh guest truth (semfix_watch.py, imported);
+# 2. poll every tracking semaphore seen in the guest trace and print the exact
+# word the client's wait polls: pool GPU VA (gpu_base + idx*4), its guest PA
+# (gva2gpa of the CPU mapping), the payload value and the +0x14/+0x28
+# fields. Every change is appended to a timeline file, so the last wait
+# before a failure and the release that should clear it are both visible;
+# 3. mirror the host's decoded releases (dmesg `sema-push ... rel=... pay=...`)
+# into the same timeline, so a wait can be matched to the channel/push that
+# releases its word.
+#
+# Usage: tools/guest-diag/semfix_uvmt.py [interval-seconds]
+# Env: TIMELINE=/tmp/opencode/uvmt_timeline.log
+# UVMT_HEARTBEAT=30 (seconds between unchanged-state heartbeats)
+import os
+import re
+import subprocess
+import sys
+import time
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, HERE)
+from semfix_watch import ( # noqa: E402
+ Qmp, QMP_SOCK, is_kern, is_gpu, diag_str, guest_trace,
+ resolve_sem_pool, pin, pin_bulk, resolve_from_channel,
+ resolve_from_pushbuffer, resolve_from_sem, resolve_pushbuffer_wide,
+ pin_pushbuffer,
+)
+
+TIMELINE = os.environ.get("TIMELINE", "/tmp/opencode/uvmt_timeline.log")
+HEARTBEAT = float(os.environ.get("UVMT_HEARTBEAT", "30"))
+DLOG = os.environ.get("UVMT_DMESG", "1") == "1"
+VMEXEC = os.path.normpath(os.path.join(HERE, "..", "..", "scripts", "vgpu_vm.py"))
+COUNTER_LO = int(os.environ.get("COUNTER_LO", "0x20400f000"), 16)
+COUNTER_HI = int(os.environ.get("COUNTER_HI", "0x204010000"), 16)
+
+
+def log(msg):
+ line = "[%s] %s" % (time.strftime("%H:%M:%S"), msg)
+ print(line, flush=True)
+ try:
+ with open(TIMELINE, "a") as f:
+ f.write(line + "\n")
+ except OSError:
+ pass
+
+
+def uvmt_state(q, sem):
+ """Read one tracking semaphore; returns a dict or None.
+
+ Layout (uvm 570.144, verified by probe): sem+0 = tracking semaphore page
+ object, sem+4 low u16 = pool index; page+0 = uvm_rm_mem; mem+0x10 = vas[];
+ the payload the client's update_completed_value polls is the u32 at
+ cpu_va + idx*4 (its GPU alias is gpu_va + idx*4, the VA the releasing push
+ writes).
+ """
+ vals = q.read64(sem, 12)
+ if len(vals) < 2 or not is_kern(vals[0]):
+ return None
+ pv = q.read64(vals[0])
+ if not pv or not is_kern(pv[0]):
+ return None
+ mem = pv[0]
+ vas = q.read64(mem + 0x10, 16)
+ cpu = next((v for v in vas if is_kern(v)), None)
+ gpu = [v for v in vas if is_gpu(v)]
+ idx = vals[1] & 0xffff
+ payload = None
+ word_pa = None
+ if cpu is not None:
+ w = q.read64(cpu + idx * 4)
+ payload = w[0] if w else None
+ word_pa = q.gva2gpa(cpu + idx * 4)
+ word_va = (gpu[0] + idx * 4) if gpu else None
+ return {
+ "page": vals[0],
+ "mem": mem,
+ "idx": idx,
+ "gpu": gpu,
+ "cpu": cpu,
+ "payload": payload,
+ "word_va": word_va,
+ "word_pa": word_pa,
+ "w14": vals[5] if len(vals) > 5 else 0,
+ "w28": vals[10] if len(vals) > 10 else 0,
+ }
+
+
+def state_key(st):
+ if not st:
+ return None
+ return (st["idx"], st["payload"], st["w14"], st["w28"], st["word_pa"],
+ tuple(st["gpu"]))
+
+
+def state_str(sem, st):
+ if not st:
+ return "sem=0x%x unreadable" % sem
+ return ("sem=0x%x idx=%d word_va=%s word_pa=%s payload=%s "
+ "+14=0x%x +28=0x%x gpu=%s" %
+ (sem, st["idx"],
+ "0x%x" % st["word_va"] if st["word_va"] is not None else "?",
+ "0x%x" % st["word_pa"] if st["word_pa"] is not None else "?",
+ "0x%x" % st["payload"] if st["payload"] is not None else "?",
+ st["w14"], st["w28"],
+ ",".join("0x%x" % v for v in st["gpu"])))
+
+
+def dmesg_releases(seen):
+ """Print/append new host-decoded releases (sema-push lines)."""
+ try:
+ r = subprocess.run(
+ ["sudo", "-n", "sh", "-c",
+ "dmesg -T | grep 'sema-push' | tail -300"],
+ capture_output=True, text=True, timeout=10)
+ except Exception:
+ return
+ for line in r.stdout.splitlines():
+ line = line.strip()
+ if not line or line in seen:
+ continue
+ seen.add(line)
+ log("push: %s" % line.replace("gsp-resman-vfio: ", ""))
+
+
+def cdt_pid():
+ try:
+ r = subprocess.run([VMEXEC, "exec", "pgrep -x cdt | head -1"],
+ capture_output=True, text=True, timeout=30)
+ except Exception:
+ return None
+ pids = re.findall(r"(?m)^(\d+)\s*$", r.stdout)
+ return int(pids[-1]) if pids else None
+
+
+def pin_counter_page(pid, pinned_pids):
+ """Pin the client's completion-counter page from its own pagemap.
+
+ The GR channels release the counter page VA range (0x20400fec0..0x20400fff0
+ observed); translating it from fresh guest truth before the first releases
+ decode is what lets those releases land on the slots the client polls.
+ """
+ cmd = ("sudo python3 /tmp/guest_pmap_win.py %d 0x%x 0x%x 64"
+ % (pid, COUNTER_LO, COUNTER_HI))
+ try:
+ r = subprocess.run([VMEXEC, "exec", cmd], capture_output=True,
+ text=True, timeout=60)
+ except Exception:
+ return False
+ n = 0
+ for m in re.finditer(r"^(0x[0-9a-f]+),(0x[0-9a-f]+),(\d+)$",
+ r.stdout, re.M):
+ va, pa, pages = int(m.group(1), 16), int(m.group(2), 16), int(m.group(3))
+ if pin_bulk(va, pa, pages * 0x1000):
+ n += pages
+ if n:
+ pinned_pids.add(pid)
+ log("counter: pinned %d page(s) at pid %d (0x%x..0x%x)"
+ % (n, pid, COUNTER_LO, COUNTER_HI))
+ return n > 0
+
+
+def main():
+ interval = float(sys.argv[1]) if len(sys.argv) > 1 else 2.0
+ os.environ.setdefault("KEEP_TRACE", "1")
+ q = Qmp(QMP_SOCK)
+ os.makedirs(os.path.dirname(TIMELINE), exist_ok=True)
+ log("semfix_uvmt: watching (interval %.1fs, qmp %s)" % (interval, QMP_SOCK))
+
+ known_sems = set()
+ seen_sem_pin = set()
+ seen_pb = set()
+ seen_ch = set()
+ pinned_ranges = set()
+ last_state = {}
+ last_seen_t = {}
+ dseen = set()
+ counter_pids = set()
+ last_pid = None
+
+ def pin_range_once(gpu_vas, cpu, gpa, size=0):
+ key = tuple(gpu_vas), cpu, size
+ if key in pinned_ranges:
+ return
+ pinned_ranges.add(key)
+ pin_pushbuffer(q, gpu_vas, cpu, gpa, size)
+
+ while True:
+ sems, pbs, chans = guest_trace()
+
+ # Poll every sem ever seen: the client's final wait may only appear in
+ # the trace once, and the trace ring wraps while it spins.
+ for sem in sorted(sems - known_sems):
+ known_sems.add(sem)
+
+ for sem in sorted(known_sems):
+ try:
+ st = uvmt_state(q, sem)
+ except Exception:
+ st = None
+ key = state_key(st)
+ now = time.time()
+ if key != last_state.get(sem):
+ last_state[sem] = key
+ last_seen_t[sem] = now
+ log("uvmt %s" % state_str(sem, st))
+ elif now - last_seen_t.get(sem, 0) >= HEARTBEAT:
+ last_seen_t[sem] = now
+ log("uvmt(hb) %s" % state_str(sem, st))
+
+ # Pin the pool page once per (sem, word) resolution. The payload
+ # word is what the releasing push writes; pinning the page makes
+ # the host able to apply that release.
+ if st and sem not in seen_sem_pin:
+ try:
+ pairs = resolve_sem_pool(q, sem)
+ except Exception as e:
+ pairs = []
+ log("semfix: sem=0x%x resolve failed: %s" % (sem, e))
+ if pairs:
+ seen_sem_pin.add(sem)
+ for gv, gpa in pairs:
+ ok = pin(gv, gpa)
+ log("semfix: sem=0x%x pool va=0x%x -> gpa=0x%x pin=%s"
+ % (sem, gv, gpa, "ok" if ok else "FAILED"))
+ d = {}
+ try:
+ res = resolve_from_sem(q, sem, d)
+ except Exception as e:
+ res = None
+ d["exception"] = str(e)[:40]
+ if not res:
+ for k in ("chunks", "words", "cands", "stop_pool",
+ "no_gpuva", "no_vmalloc", "empty_vas"):
+ d.pop(k, None)
+ try:
+ res = resolve_pushbuffer_wide(q, sem, d)
+ except Exception as e:
+ res = None
+ d["exception"] = str(e)[:40]
+ if res:
+ pin_range_once(*res)
+ log("semfix: sem=0x%x pushbuf -> %s"
+ % (sem, diag_str(d)))
+ else:
+ log("semfix: sem=0x%x pushbuf not found [%s]"
+ % (sem, diag_str(d)))
+ else:
+ log("semfix: sem=0x%x pool unresolved (retry)" % sem)
+
+ for ch in sorted(chans - seen_ch):
+ d = {}
+ try:
+ res = resolve_from_channel(q, ch, d)
+ except Exception as e:
+ res = None
+ d["exception"] = str(e)[:40]
+ if res:
+ seen_ch.add(ch)
+ pin_range_once(*res)
+ log("semfix: ch=0x%x -> %s" % (ch, diag_str(d)))
+ else:
+ log("semfix: ch=0x%x pushbuf miss [%s]" % (ch, diag_str(d)))
+ for pb in sorted(pbs - seen_pb):
+ d = {}
+ try:
+ res = resolve_from_pushbuffer(q, pb, d)
+ except Exception as e:
+ res = None
+ d["exception"] = str(e)[:40]
+ if res:
+ seen_pb.add(pb)
+ pin_range_once(*res)
+ log("semfix: pb=0x%x -> %s" % (pb, diag_str(d)))
+ else:
+ log("semfix: pb=0x%x pushbuf miss [%s]" % (pb, diag_str(d)))
+
+ if DLOG:
+ dmesg_releases(dseen)
+
+ # Pin the completion-counter page of a newly started client, before its
+ # first GR pushes decode their releases.
+ pid = cdt_pid()
+ if pid and pid != last_pid:
+ last_pid = pid
+ log("counter: client pid %d" % pid)
+ if pid and pid not in counter_pids:
+ try:
+ pin_counter_page(pid, counter_pids)
+ except Exception as e:
+ log("counter: pin failed: %s" % e)
+
+ time.sleep(interval)
+
+
+if __name__ == "__main__":
+ main()