summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/guest-diag/guest_pin_stream.py625
1 files changed, 552 insertions, 73 deletions
diff --git a/tools/guest-diag/guest_pin_stream.py b/tools/guest-diag/guest_pin_stream.py
index a33a183..6607ead 100644
--- a/tools/guest-diag/guest_pin_stream.py
+++ b/tools/guest-diag/guest_pin_stream.py
@@ -266,11 +266,17 @@ def _vmalloc_pages_for(kc, va):
return 0
-def resolve_mem_pins(kc, mem, out, default_pages=16, va_lo=0, va_hi=0):
+def resolve_mem_pins(kc, mem, out, default_pages=16, va_lo=0, va_hi=0,
+ page_off=0):
"""uvm_rm_mem kptr -> GPU-VA/GPA runs (direct map or page-table walk).
va_lo/va_hi optionally restrict the emitted GPU VA range; the kcore
- chain resolver targets the CE push-buffer window only.
+ chain resolver targets the CE push-buffer window only. page_off shifts
+ the vmap page paired with GPU data page k: the CE push-buffer vmap is
+ one page longer than its GPU data window on some allocations (a leading
+ header page), so GPU data page k lives at vmap page k + page_off.
+ page_off is derived per allocation (mem_page_off): a fixed offset is
+ wrong (r116/r117 regressed client #1 with a hard +1).
"""
if not is_kern_ptr(mem):
return 0
@@ -281,7 +287,7 @@ def resolve_mem_pins(kc, mem, out, default_pages=16, va_lo=0, va_hi=0):
and (not va_hi or v < va_hi)]
if not cpu or not gpus:
return 0
- size = kc.read64(mem + 0x60) or 0
+ size = _mem_size(kc, mem)
dm = _direct_map_pa(kc, cpu)
emitted = 0
if dm is not None:
@@ -290,14 +296,15 @@ def resolve_mem_pins(kc, mem, out, default_pages=16, va_lo=0, va_hi=0):
out.append("0x%x,0x%x,%d\n" % (gv, dm, npages))
emitted += 1
return emitted
+ area = _vmalloc_pages_for(kc, cpu)
npages = (max(1, min(size >> 12, 4098)) if size
- else _vmalloc_pages_for(kc, cpu))
- if not npages:
- npages = 1
+ else area or default_pages)
+ if area and page_off + npages > area:
+ npages = max(1, area - page_off)
runs = []
cur = None
for k in range(npages):
- pa = kc.va_to_pa(cpu + k * 0x1000)
+ pa = kc.va_to_pa(cpu + (k + page_off) * 0x1000)
if pa is None:
if cur:
runs.append(cur)
@@ -318,6 +325,152 @@ def resolve_mem_pins(kc, mem, out, default_pages=16, va_lo=0, va_hi=0):
return emitted
+def _mem_size(kc, mem):
+ """uvm_rm_mem size (bytes) from the 570.144 layout, with a fallback.
+
+ The struct is type(4)+pad(4)+mask(8)+vas[33] (264 B)+proxy_vas+
+ gpu_owner+size, so size lives at +0x128; +0x60 (inside vas[]) reads 0
+ for unused processors and is kept only as a fallback.
+ """
+ for off in (0x128, 0x60):
+ v = kc.read64(mem + off) or 0
+ if 0 < v <= (256 << 20):
+ return v
+ return 0
+
+
+def _push_prefix_ok(buf):
+ """First-4KB method-stream plausibility, mirroring the host's check."""
+ off = 0
+ while off + 4 <= len(buf):
+ hdr = struct.unpack_from('<I', buf, off)[0]
+ if hdr:
+ break
+ off += 4
+ if off + 4 > len(buf):
+ return True # all zeros: not refutable
+ op = (hdr >> 29) & 0x7
+ if op in (4, 7):
+ return True
+ if op not in (1, 3, 5):
+ return False
+ return ((hdr >> 16) & 0xfff) != 0
+
+
+def _page_content(kc, va):
+ pa = kc.va_to_pa(va & ~0xFFF)
+ if pa is None:
+ return None
+ return kc.read(kc.page_offset_base + pa, 64)
+
+
+def mem_page_off(kc, mem):
+ """Per-allocation vmap offset for a uvm_rm_mem's GPU data pages.
+
+ Derived, never fixed: the vmap area's page count (vmallocinfo) minus
+ the rm_mem's data page count (size at +0x128). A 4097-page area with
+ 4096 data pages means one leading header page (offset 1); an area that
+ exactly matches the data means offset 0. When the size is unreadable,
+ the two candidate offsets are content-probed with the host's method-
+ stream plausibility test and the decisively-passing one wins; an
+ ambiguous result keeps 0 (the status-quo pairing).
+ """
+ vas = [kc.read64(mem + 0x10 + i * 8) for i in range(16)]
+ cpu = next((v for v in vas if is_kern_ptr(v)), None)
+ if not cpu or _direct_map_pa(kc, cpu) is not None:
+ return 0
+ area = _vmalloc_pages_for(kc, cpu)
+ if area <= 1:
+ return 0
+ size = _mem_size(kc, mem)
+ if size:
+ data = min(size >> 12, 4098)
+ if 0 <= area - data <= 2:
+ return area - data
+ return 0
+ g = []
+ for off in (0, 1):
+ if off >= area:
+ g.append(0)
+ continue
+ buf = _page_content(kc, cpu + off * 0x1000)
+ g.append(1 if (buf is not None and _push_prefix_ok(buf)) else 0)
+ if g[0] != g[1]:
+ return 0 if g[0] else 1
+ return 0
+
+
+def _ce_mem_pages(kc, mem):
+ """CE mem walk parameters: (gpus, cpu, off, npages) or None.
+
+ Mirrors resolve_mem_pins' derivation so the delta sweeper emits the same
+ page set the one-shot resolver used: size at +0x128 when readable, else
+ the vmalloc area length; all CE-window GPU VAs share the CPU mapping.
+ """
+ vas = [kc.read64(mem + 0x10 + i * 8) for i in range(16)]
+ cpu = next((v for v in vas if is_kern_ptr(v)), None)
+ gpus = [v for v in vas if v and PB_CE_LO <= v < PB_CE_HI]
+ if not cpu or not gpus:
+ return None
+ if _direct_map_pa(kc, cpu) is not None:
+ return None # never a vmap: not the push-buffer shape
+ off = mem_page_off(kc, mem)
+ area = _vmalloc_pages_for(kc, cpu)
+ size = _mem_size(kc, mem)
+ npages = (max(1, min(size >> 12, 4098)) if size
+ else area or 64)
+ if area and off + npages > area:
+ npages = max(1, area - off)
+ return (gpus, cpu, off, npages)
+
+
+def _emit_ce_mem_delta(kc, st, out, fresh_map=False):
+ """Emit the CE mem's live {va,pa} pairs as content-validated runs.
+
+ The one-shot emission this replaces pinned the whole 16 MB window
+ including pages whose content was zero at the time and pages whose vmap
+ pairing had drifted; the host then read zeros for the other CE ranks'
+ pushes (the second-client stall class). The sweep now skips zero pages
+ (a zero page cannot carry a push), re-derives every PA against the live
+ page tables (optionally bursting the Kcore cache first), and emits only
+ pairs that are new or changed since the last sweep - so a page the
+ client writes later, or a mem whose vmap pages moved, is re-emitted
+ within KC_REEMIT_S and the host's last-store-wins pin sees the truth.
+ Never drops a previously emitted pair (a stale pin is corrected by the
+ changed pair, and a failed burst never clears the whole map).
+ """
+ gpus, cpu, off, npages = st["gpus"], st["cpu"], st["page_off"], st["npages"]
+ last = st["last"]
+ if fresh_map:
+ for k in range(npages):
+ kc._pa_cache.pop(cpu + (k + off) * 0x1000, None)
+ changed = []
+ for k in range(npages):
+ d = kc.read(cpu + (k + off) * 0x1000, 32)
+ if not d or not any(d):
+ continue # zero/unreadable: nothing to pin yet
+ pa = kc.va_to_pa(cpu + (k + off) * 0x1000)
+ if pa is None:
+ continue
+ for gv in gpus:
+ if last.get(gv + k * 0x1000) != pa:
+ changed.append((gv + k * 0x1000, pa))
+ changed.sort()
+ merged = []
+ for va, pa in changed:
+ if merged and va == merged[-1][0] + merged[-1][2] * 0x1000 and \
+ pa == merged[-1][1] + merged[-1][2] * 0x1000:
+ merged[-1][2] += 1
+ else:
+ merged.append([va, pa, 1])
+ for va, pa, n in merged:
+ out.append("0x%x,0x%x,%d\n" % (va, pa, n))
+ for va, pa in changed:
+ last[va] = pa
+ st["emit_total"] = st.get("emit_total", 0) + len(merged)
+ return len(merged)
+
+
def _emit_ring_pins(kc, gpu_va, entries_cpu, entries, out):
"""Pin the GPFIFO ring pages (and the pages after it, where the GPPUT
buffer usually sits) from the ring's CPU mapping."""
@@ -444,7 +597,8 @@ def resolve_pb_pins(kc, pb, out):
mem = kc.read64(pb + 8)
if not mem:
return 0
- return resolve_mem_pins(kc, mem, out, default_pages=64)
+ return resolve_mem_pins(kc, mem, out, default_pages=64,
+ page_off=mem_page_off(kc, mem))
# The CE push-buffer window is the one UVM region the client never maps into
@@ -458,12 +612,28 @@ def resolve_pb_pins(kc, pb, out):
# channel->pool->manager->pushbuffer->rm_mem chain is walkable in kernel
# memory, so resolve the CE push buffer directly with kcore: no trace event,
# nothing to lose.
+#
+# The kptr-keyed one-shot latches below are NOT object identity: UVM frees
+# and re-creates these rm_mems and channels during a session, and the slab
+# reuses the same addresses. A resolution taken before such a move pins
+# pages that are stale by the time the host reads them (observed r125: the
+# pinned CE pages read all-zero while the channel's real pushes lived at
+# new vmap pages of the same rm_mem kptr; every ring entry then stalled in
+# the host's 8 s grace since no release could be decoded from zeros). So a
+# resolved mem is periodically re-validated against the channel's own ring
+# entries and re-resolved when it stops matching (KC_PB_STATE/_kc_pb_fresh),
+# and when several mem candidates are reachable the one whose pages actually
+# hold the ring-entry pushes is chosen (content anchor, _mem_content_ok).
PB_CE_LO = 0x120000000
PB_CE_HI = 0x121000000
KC_CHAIN_BACK = 0x2000
KC_CHAIN_SPAN = 0xA000
KC_CHAIN_MAX = 400
RESOLVED_KC_PB = set()
+KC_PB_STATE = {} # sem kptr -> resolved-mem state (fingerprint + pins)
+KC_REFRESH_S = 2.0 # re-validate a resolved CE mem at most this often
+KC_REEMIT_S = 2.0 # delta-sweep the CE mem's live pages this often
+KC_REMAP_S = 30.0 # full re-translation sweep (catches remaps)
def _chain_mems_at(kc, w):
@@ -484,13 +654,96 @@ def _chain_mems_at(kc, w):
return mems
+def _mem_gpu_cpu(kc, mem):
+ """The CE-window GPU VA and CPU alias of an rm_mem (None/None when absent)."""
+ vas = [kc.read64(mem + 0x10 + i * 8) for i in range(16)]
+ cpu = next((v for v in vas if is_kern_ptr(v)), None)
+ gpu = next((v for v in vas
+ if v and PB_CE_LO <= v < PB_CE_HI), None)
+ return gpu, cpu
+
+
+def _mem_content_score(kc, mem, gpu):
+ """Rough 'is this the live CE push buffer' score for a mem candidate.
+
+ The channel's CE rings live in (emulated) video memory, so the guest
+ cannot read the ring entries to anchor a push VA; but the CE push
+ buffer's guest-RAM pages are CPU-written method streams during channel
+ setup and client work. Count nonzero u32 pairs across the first 128
+ pages of the candidate's window: a live mem scores high, a freed page
+ range that a reused kptr still describes reads zeros and scores 0.
+ """
+ cpu = None
+ for i in range(16):
+ v = kc.read64(mem + 0x10 + i * 8)
+ if is_kern_ptr(v):
+ cpu = v
+ break
+ if cpu is None:
+ return -1
+ score = 0
+ for k in range(128):
+ buf = kc.read(cpu + k * 0x1000, 0x1000)
+ if not buf:
+ continue
+ for off in range(0, 0x1000, 16):
+ if buf[off:off + 8] != b"\x00" * 8:
+ score += 1
+ return score
+
+
+def _kc_pb_fresh(kc, sem):
+ """The pinned CE mem still matches the channel's live backing.
+
+ Cheap check: the mem's own {cpu,gpu} fields and the guest PA of its
+ first window page must still match the recorded resolution. A vmap area
+ never moves while the mem object lives, so either check failing means
+ the object was freed and (possibly) re-created at the same kptr - the
+ caller then re-resolves and re-emits the current pages.
+ """
+ st = KC_PB_STATE.get(sem)
+ if not st:
+ return False
+ mem = st.get("mem")
+ if not mem or not is_kern_ptr(mem):
+ return False
+ if (kc.read64(mem + 0x30) or 0) != st.get("cpu") or \
+ (kc.read64(mem + 0x38) or 0) != st.get("gpu"):
+ return False
+ if not st.get("page_va"):
+ return True
+ if not st.get("page_pa"):
+ # Unknown PA at store time: the manual page-table walk could not
+ # translate the vmap page (it may have been read before the kernel
+ # page tables were walkable). Treat the record as fresh instead of
+ # failing every check - a permanently-failing freshness test made
+ # the scanner drop the latch and re-resolve in a tight loop, and
+ # each failed re-resolution left the host with no pins at all
+ # (observed r131: "kc-chain stale: re-resolving" every second for
+ # five sems while smiley sat in cudaMalloc).
+ return True
+ pa = kc.va_to_pa(st["page_va"])
+ if pa is None or pa != st.get("page_pa"):
+ return False
+ return True
+
+
def resolve_pb_chain(kc, sem, out):
"""Tracking-sem kptr -> CE push-buffer rm_mem pins (kcore chain walk).
Returns True when a CE push-buffer mem was found for this sem, so the
caller marks it resolved even when the mem was already pinned; new pin
lines are appended to out. Bounded: one window read plus a few reads per
- candidate, and each rm_mem is resolved at most once per session.
+ candidate, and each rm_mem is resolved at most once per session (until a
+ freshness failure drops it again, see _kc_pb_fresh).
+
+ When several mem candidates are reachable, the content score decides: a
+ stale candidate (freed memory read through a reused kptr) reads zeros
+ and is skipped in favour of the live push-buffer pages, so it cannot
+ shadow them on the host's last-store-wins pin table. An all-zero
+ candidate set emits nothing: pinning zeros is what made the host read
+ zero pushes and stall every CE entry; the drift retries (retry_at) and
+ the periodic refresh pick the pages up as soon as content appears.
"""
if not is_kern_ptr(sem):
return False
@@ -499,26 +752,91 @@ def resolve_pb_chain(kc, sem, out):
return False
words = struct.unpack_from("<%dQ" % (len(data) // 8), data, 0)
cands = list(dict.fromkeys(w for w in words if is_kern_ptr(w)))
- found = False
+ # Channel-anchored expansion (r134 live dump): the tracking sem is a
+ # member of a uvm_channel_t table; the channel records sit in the sem's
+ # own window and carry their CE ring GPU VA ([0x121010000, 0x121400000))
+ # plus back-to-back 32-bit object handles. The pool/manager/pushbuffer
+ # pointers hang off those records' targets, which the bare sem-window
+ # scan never reaches - the second-client stall is exactly this: the only
+ # live uvmt event is the spinning sem, and no mem candidate is found
+ # around it. For each ring-VA word, scan the surrounding record for
+ # kptrs, take those targets, and try the chain shape on both. Additive
+ # and bounded; content scoring still decides, so a wrong candidate
+ # cannot shadow the live pages.
+ extra = []
+ for i, w in enumerate(words):
+ if not w or not (0x121010000 <= w < 0x121400000):
+ continue
+ lo = max(0, i - 0x60)
+ hi = min(len(words), i + 0x60)
+ for c in words[lo:hi]:
+ if not is_kern_ptr(c):
+ continue
+ extra.append(c)
+ tgt = kc.read(c, 0x100)
+ if tgt:
+ tw = struct.unpack_from("<%dQ" % (len(tgt) // 8),
+ tgt, 0)
+ for w2 in tw:
+ if is_kern_ptr(w2):
+ extra.append(w2)
+ if len(extra) >= 512:
+ break
+ if extra:
+ head = []
+ for w in extra:
+ if w not in head and w not in cands:
+ head.append(w)
+ cands = head + cands
+ mems = []
for w in cands[:KC_CHAIN_MAX]:
for mem in _chain_mems_at(kc, w):
- vas = [kc.read64(mem + 0x10 + i * 8) for i in range(16)]
- if not any(v and PB_CE_LO <= v < PB_CE_HI for v in vas):
- continue
- found = True
- if mem in RESOLVED_KC_PB:
- continue
- RESOLVED_KC_PB.add(mem)
- n = resolve_mem_pins(kc, mem, out, default_pages=64,
- va_lo=PB_CE_LO, va_hi=PB_CE_HI)
- try:
- with open('/tmp/guest_pin_stream.log', 'a') as f:
- f.write('%s kc-chain CE push-buffer mem=0x%x sem=0x%x '
- 'pin-runs=%d\n'
- % (time.strftime('%T'), mem, sem, n))
- except OSError:
- pass
- return found
+ if mem not in mems:
+ mems.append(mem)
+ found = False
+ scored = []
+ for mem in mems:
+ gpu, cpu = _mem_gpu_cpu(kc, mem)
+ if not gpu or not cpu:
+ continue
+ found = True
+ score = _mem_content_score(kc, mem, gpu)
+ scored.append((score, mem, gpu, cpu))
+ if not scored:
+ return found
+ best = max(scored, key=lambda c: c[0])
+ if best[0] <= 0:
+ dbg("kc-chain all candidates zero (sem=0x%x, n=%d): deferring"
+ % (sem, len(scored)))
+ return found
+ if len(scored) > 1:
+ dbg("kc-chain scores sem=0x%x: %s" %
+ (sem, ' '.join('%#x=%d' % (m, s) for s, m, _g, _c in scored)))
+ _score, mem, gpu, cpu = best
+ if mem in RESOLVED_KC_PB:
+ return True
+ RESOLVED_KC_PB.add(mem)
+ params = _ce_mem_pages(kc, mem)
+ if not params:
+ return found
+ gpus, cpu, off, npages = params
+ st = {"mem": mem, "gpu": gpu, "cpu": cpu, "gpus": gpus,
+ "page_va": cpu, "page_pa": kc.va_to_pa(cpu) or 0,
+ "page_off": off, "npages": npages, "last": {}}
+ # Fresh translation for the first sweep: the Kcore PA cache may hold
+ # pairs from before this mem's vmap area was (re)populated.
+ kc._pa_cache.clear()
+ n = _emit_ce_mem_delta(kc, st, out)
+ KC_PB_STATE[sem] = st
+ try:
+ with open('/tmp/guest_pin_stream.log', 'a') as f:
+ f.write('%s kc-chain CE push-buffer mem=0x%x sem=0x%x '
+ 'pin-runs=%d off=%d npages=%d size=%d score=%d\n'
+ % (time.strftime('%T'), mem, sem, n, off, npages,
+ _mem_size(kc, mem), _score))
+ except OSError:
+ pass
+ return True
# UvmGpuChannelInfo field offsets (nv_uvm_types.h, x86_64):
@@ -613,15 +931,72 @@ def resolve_channel_pins(kc, ch, out):
-def clear_trace():
- """Drop stale ring events so the fallback tail reader cannot re-learn a
- previous client's freed sems after a client switch. The new client's
- events are delivered fresh through the consuming trace_pipe reader."""
+def _read_trace_snapshot(max_bytes=1 << 20, budget=0.5):
+ """Best-effort non-blocking read of the trace ring file.
+
+ Spawning `tail` here left uninterruptible D-state processes when the
+ seq_file read blocked under load (observed r127: dozens of stuck tails,
+ load 26); a direct os.read with O_NONBLOCK returns whatever is ready and
+ never accumulates processes.
+ """
try:
- with open(TRACE, "w") as f:
- f.write("")
+ fd = os.open(TRACE, os.O_RDONLY | os.O_NONBLOCK)
except OSError:
- pass
+ return b""
+ chunks = []
+ got = 0
+ t0 = time.time()
+ try:
+ while got < (8 << 20) and time.time() - t0 < budget:
+ try:
+ d = os.read(fd, 262144)
+ except BlockingIOError:
+ break
+ except OSError:
+ break
+ if not d:
+ break
+ chunks.append(d)
+ got += len(d)
+ finally:
+ os.close(fd)
+ return b"".join(chunks)[-max_bytes:]
+
+
+def seed_events_from_trace(seen_sems, seen_pbs, seen_chs):
+ """One-shot tail scan of the trace ring after a pid switch / start.
+
+ The uvmt/uvbp/uvch kprobe events are one-shot per object, and the
+ trace_pipe reader only sees events that arrive after it attaches: a
+ client whose channels are registered in the instant around the scanner's
+ pid switch would otherwise never appear in the sets (observed r126:
+ vecadd_loop ran for minutes with sem=0 while its CE entries paid the
+ 8 s grace). The ring keeps recent events, so scan its tail once and add
+ every distinct kptr. Stale previous-client pointers that are also
+ re-learned are harmless: their chains no longer resolve to content
+ (the mem candidates are scored and deferred).
+ """
+ try:
+ out = _read_trace_snapshot()
+ except Exception:
+ return 0
+ n = 0
+ for m in re.finditer(rb"sem=0x([0-9a-fA-F]+)", out):
+ v = int(m.group(1), 16)
+ if v not in seen_sems:
+ seen_sems.add(v)
+ n += 1
+ for m in re.finditer(rb"pb=0x([0-9a-fA-F]+)", out):
+ v = int(m.group(1), 16)
+ if v not in seen_pbs:
+ seen_pbs.add(v)
+ n += 1
+ for m in re.finditer(rb"ch=0x([0-9a-fA-F]+)", out):
+ v = int(m.group(1), 16)
+ if v not in seen_chs:
+ seen_chs.add(v)
+ n += 1
+ return n
class TraceReader:
@@ -755,9 +1130,7 @@ def cdt_pid():
# scanner must never commit to them (a transient nvidia-smi at boot was
# picked once and its exited pagemap read back EOF forever).
non_clients = ("nvidia-smi", "nvidia-persistenced", "Xorg", "X")
- best = None
- uvm = None
- gpu = None
+ cands = []
for d in os.listdir('/proc'):
if not d.isdigit():
continue
@@ -772,28 +1145,24 @@ def cdt_pid():
rp = st.rfind(')')
if rp >= 0 and st[rp + 2:rp + 3] == 'Z':
continue
- if any(name.startswith(p) for p in prefixes):
- best = int(d)
- continue
if any(name.startswith(p) for p in non_clients):
continue
# Arbitrary CUDA binaries (any comm) are clients too: match by
# the CUDA device they hold open so streaming never depends on
- # the binary's name. Prefer a UVM holder (compute client) over a
- # plain /dev/nvidia0 holder (nvidia-smi, persistenced); the last
- # match wins, which is the newest client when several overlap.
+ # the binary's name. The newest candidate (highest pid) is the
+ # client being streamed: preferring a name-prefix match first
+ # pinned the scanner to a previous client that was still tearing
+ # down while the new client had already started pushing (its
+ # first pushes were unreadable, the expectation drifted, and the
+ # stall followed).
kind = nvidia_fds(d)
- if kind == 'uvm':
- uvm = int(d)
- elif kind == 'gpu':
- gpu = int(d)
+ if kind is not None or any(name.startswith(p) for p in prefixes):
+ cands.append(int(d))
except OSError:
pass
- if best is not None:
- return best
- if uvm is not None:
- return uvm
- return gpu
+ if not cands:
+ return None
+ return max(cands)
def scan_window(fd, lo, hi, span, out):
@@ -960,6 +1329,9 @@ def main():
resolved_chs = set()
resolved_kc_pb = set()
retry_at = {}
+ kc_refresh_at = {}
+ kc_reemit_at = {}
+ kc_remap_at = {}
tracer = TraceReader()
kc = Kcore()
kc_ok = kc.load()
@@ -995,14 +1367,24 @@ def main():
resolved_chs = set()
resolved_kc_pb = set()
retry_at = {}
+ kc_refresh_at = {}
+ kc_reemit_at = {}
+ kc_remap_at = {}
# Client switch: the old client's channel/put pointers are freed
# memory. Clearing the #put map stops stale reads from being
# emitted as if they were the new client's counters.
CHAN_PUTS.clear()
PUT_LAST.clear()
- clear_trace()
tracer.reset()
RESOLVED_KC_PB.clear()
+ KC_PB_STATE.clear()
+ # One-shot: recover the new client's one-shot channel events
+ # from the trace ring (they can fall in the switch instant and
+ # be invisible to the pipe reader; the old clear_trace() made
+ # that permanent - part of the r126 stall class).
+ _seeded = seed_events_from_trace(seen_sems, seen_pbs, seen_chs)
+ if _seeded:
+ dbg("switch seed: +%d event kptr(s)" % _seeded)
if sock is not None:
try:
sock.sendall(("#pid %d\n" % pid).encode())
@@ -1064,6 +1446,103 @@ def main():
added += len(got)
else:
retry_at[sem] = now_r + 1.0
+ # CE push-buffer pin freshness: a resolved mem is re-probed
+ # against the channel's live ring content. UVM frees and
+ # re-creates these rm_mems/channels mid-session and the slab
+ # reuses the same kptrs, so a one-shot resolution can pin
+ # pages that are stale (observed reading all zeros) while
+ # the live pushes sit at new pages; dropping the latches
+ # makes the resolution below re-emit the current truth.
+ if sem in resolved_kc_pb and \
+ now_r >= kc_refresh_at.get(sem, 0):
+ kc_refresh_at[sem] = now_r + KC_REFRESH_S
+ try:
+ _fresh = _kc_pb_fresh(kc, sem)
+ except Exception:
+ _fresh = True
+ if not _fresh:
+ # Re-resolve, but never lose the latch on a failed
+ # attempt: a dropped latch means the host keeps no
+ # pins for this channel at all, which is worse than
+ # a possibly-stale pin (the host content-checks the
+ # first page and now rejects all-FF continuation
+ # chunks, so stale pages are bounded). On failure
+ # keep the old state and defer the next attempt.
+ _st = KC_PB_STATE.get(sem)
+ if _st and _st.get("mem"):
+ RESOLVED_KC_PB.discard(_st["mem"])
+ got = []
+ try:
+ _found2 = resolve_pb_chain(kc, sem, got)
+ except Exception:
+ _found2 = False
+ got = []
+ if _found2 and got:
+ _new = KC_PB_STATE.get(sem)
+ _newmem = _new.get("mem") if _new else None
+ dbg("kc-chain stale: re-resolved sem=0x%x "
+ "new-mem=%s runs=%d"
+ % (sem, "yes" if _newmem !=
+ (_st.get("mem") if _st else None)
+ else "no", len(got)))
+ lines.extend(got)
+ added += len(got)
+ resolved_kc_pb.add(sem)
+ else:
+ if _st and _st.get("mem"):
+ RESOLVED_KC_PB.add(_st["mem"])
+ if sem not in KC_PB_STATE:
+ KC_PB_STATE[sem] = _st
+ kc_refresh_at[sem] = now_r + 5.0
+ dbg("kc-chain stale: keep old latch sem=0x%x"
+ % sem)
+ else:
+ # CE mem delta sweep (second-client pins): the one-shot
+ # emission pinned the mem once; pages the client writes
+ # later (or whose vmap pairing moved) were never
+ # re-emitted, so the host kept zeros for the other CE
+ # ranks' pushes. Sweep every KC_REEMIT_S, emitting only
+ # new/changed nonzero pairs; burst the Kcore PA cache
+ # every KC_REMAP_S so a remap is not shadowed.
+ _st = KC_PB_STATE.get(sem)
+ if _st is not None and \
+ now_r >= kc_reemit_at.get(sem, 0):
+ _fresh_map = now_r >= kc_remap_at.get(sem, 0)
+ if _fresh_map:
+ kc_remap_at[sem] = now_r + KC_REMAP_S
+ kc_reemit_at[sem] = now_r + KC_REEMIT_S
+ got = []
+ try:
+ _n2 = _emit_ce_mem_delta(kc, _st, got,
+ fresh_map=_fresh_map)
+ except Exception:
+ _n2 = 0
+ got = []
+ if got:
+ lines.extend(got)
+ added += len(got)
+ dbg("ce-delta sem=0x%x runs=%d mem=0x%x"
+ % (sem, _n2, _st.get("mem", 0)))
+ # CE push-buffer chain (second-client stall fix): resolve
+ # pool -> manager -> pushbuffer -> rm_mem directly, so the
+ # push-buffer pages never depend on the racy one-shot uvbp
+ # event. Independent of the ring/pool gates below (the
+ # channel gate's continue must not starve this path).
+ if sem not in resolved_kc_pb and \
+ now_r >= retry_at.get(("pk", sem), 0):
+ got = []
+ try:
+ _found = resolve_pb_chain(kc, sem, got)
+ except Exception:
+ _found = False
+ got = []
+ if _found:
+ resolved_kc_pb.add(sem)
+ if got:
+ lines.extend(got)
+ added += len(got)
+ else:
+ retry_at[("pk", sem)] = now_r + 2.0
# ISSUE-0002: the pool pin (mem) and the channel ring are
# resolved independently. The sem is marked resolved as soon
# as the pool page pins, but the channel_info anchor may only
@@ -1083,26 +1562,6 @@ def main():
added += len(got)
else:
retry_at[("cy", sem)] = now_r + 2.0
- # CE push-buffer chain (second-client stall fix): resolve
- # pool -> manager -> pushbuffer -> rm_mem directly, so the
- # push-buffer pages never depend on the racy one-shot uvbp
- # event. Independent of the ring/pool gates above.
- if sem in resolved_kc_pb or \
- now_r < retry_at.get(("pk", sem), 0):
- continue
- got = []
- try:
- _found = resolve_pb_chain(kc, sem, got)
- except Exception:
- _found = False
- got = []
- if _found:
- resolved_kc_pb.add(sem)
- if got:
- lines.extend(got)
- added += len(got)
- else:
- retry_at[("pk", sem)] = now_r + 2.0
for pb in list(seen_pbs):
if pb in resolved_pbs or now_r < retry_at.get(pb, 0):
continue
@@ -1131,6 +1590,26 @@ def main():
added += len(got)
else:
retry_at[ch] = now_r + 1.0
+ # A uvch channel kptr is another early seed for the CE
+ # push-buffer chain (its window contains the pool pointer):
+ # resolve it here so the pins never depend on the uvmt sem
+ # events alone.
+ if ch in resolved_kc_pb or \
+ now_r < retry_at.get(("pk", ch), 0):
+ continue
+ got = []
+ try:
+ _found = resolve_pb_chain(kc, ch, got)
+ except Exception:
+ _found = False
+ got = []
+ if _found:
+ resolved_kc_pb.add(ch)
+ if got:
+ lines.extend(got)
+ added += len(got)
+ else:
+ retry_at[("pk", ch)] = now_r + 2.0
if added:
try:
with open('/tmp/guest_pin_stream.log', 'a') as f: