summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--STATE-CURRENT.md101
-rw-r--r--kernel/gsp_phys_booter.c852
-rw-r--r--kernel/gsp_phys_gpu.c25
-rw-r--r--kernel/gsp_phys_gpu.h4
-rw-r--r--kernel/gsp_sema_service.c392
-rw-r--r--kernel/gsp_sema_service.h10
-rw-r--r--kernel/main.c31
-rwxr-xr-xtools/cuda-ab/push_battery.sh22
-rw-r--r--tools/guest-diag/guest_pin_stream.py488
-rw-r--r--tools/guest-diag/pin_gr_stream.py98
11 files changed, 1862 insertions, 167 deletions
diff --git a/.gitignore b/.gitignore
index 3b3a6a1..1ffe488 100644
--- a/.gitignore
+++ b/.gitignore
@@ -153,3 +153,9 @@ userspace/sdks/
# Recorded debugger sessions (bridge checks, probe dumps) - never track
/dumps/
+
+# Editor/backup leftovers (never track).
+*.bak
+*.bak-*
+*.orig
+*.rej
diff --git a/STATE-CURRENT.md b/STATE-CURRENT.md
index c16b774..d26ffec 100644
--- a/STATE-CURRENT.md
+++ b/STATE-CURRENT.md
@@ -1,8 +1,9 @@
# Open-IOV Current State
-> **Snapshot: 2026-10-05 (reorganized; simulator status current 2026-10-04,
-> physical/guest campaign data as of 2026-10-07 ~01:15; debugging-options
-> catalogue added 2026-10-05).** This is a dated public
+> **Snapshot: 2026-10-14 ~13:30 (physical/guest campaign data current to the
+> live r139 cycle at snapshot time; simulator workstream status 2026-10-03;
+> page reorganized and debugging-options catalogue added 2026-10-05).**
+> This is a dated public
> status page, not live state; it is refreshed on every progress check. Live
> working notes are maintained outside this repository; the public
> known-knowns/unknowns document is authoritative here:
@@ -146,6 +147,92 @@ isolation, performance, portability).
aperture. Ranked next: read the server-FBMEM fault buffer via a
copy-engine transfer, per-VEID/compute-context execution requirements,
and the scheduler CWD field semantics. Host clean, GPU green, no reboots.
+ **Update 2026-10-14 ~13:30 (major - guest compute runs on the physical
+ engine):** the physical-backed guest campaign now runs a multi-client
+ CUDA battery against the real TU104 with the host CUDA bridge disabled
+ (`cuda_bridge=0`, GR forwarding on). In one cycle (r137) the first client
+ (`victory_mail`: `MAIL` + `VADD bad=0` + `STATUS OK`) and the second
+ client (`smiley`: `STATUS OK`) are both green on the physical backend,
+ and the guest's compute launches execute on the physical GR engine (the
+ window-attribution defects where kernels wrote into the wrong window are
+ fixed; first-client guest VADD results verify). The 2026-10-08..12
+ second-client campaign closed its chain (CE USERD location, CE
+ push-buffer pin delivery, a channel-anchored CE pool resolver
+ `ade77d9a`, session-flush pin semantics, data-argument resolution
+ order). Wall closure since 2026-10-07: (a) the third-client
+ (`victory_matmul`) PMA-scrub freeze is root-caused live - the
+ boot-session scrubber channel's finish word is a guest-RAM vmap that
+ collides with a client VA - and fixed by authoring the scrubber
+ channel's progress (`624438bb` + guest scanner `80af379d`), so
+ `victory_matmul` gets past `cudaMalloc`; (b) the intermittent
+ GR-forward launch timeout is root-caused: the host publishes forwarded
+ launches modulo an 8-entry ladder ring while the user-lane channel was
+ declared with 1024 GPFIFO entries, so after the first host-ring wrap the
+ front-end's GET/PUT became inconsistent and it stopped fetching
+ (`074f493c` fixes the lane depth to 8; the earlier two-client acceptance
+ never wrapped and had masked the regression). The r139 cycle validated
+ the ring fix: the 100-launch soak crossed the host-ring wrap cleanly
+ (`LOOP launches=100 bad=0 STATUS OK`; the previous cycle died at launch
+ 8), c1 `victory_mail` is green (60 s), and `smiley` green on a later
+ retry. The residual is an intermittent cold-context/fetched-not-executed
+ launch race (18 failed launches in the cycle: 6 never fetched by the
+ front-end, 12 fetched but not completed by the engine; some later
+ clients intermittently return all-zero results), for which a bounded
+ in-submit retry with a fresh ring entry plus a cold pre-warm is being
+ implemented; the "OK with 0 changed" change-counter is not a reliable
+ execution witness - the client's own `STATUS` is the acceptance signal.
+ Host clean all session, no Xid in the campaign logs, pstore empty, no
+ reboots (in-session PCI function-level reset recovery where needed).
+ ISSUE-0002 (second client in one boot): the sequential case can pass
+ (first two clients green in one cycle; smiley green on retry in the
+ next) but is not yet deterministic; the issue remains open pending
+ repeat-run plus concurrent/simultaneous validation. Active front:
+ the multi-client battery on the physical backend plus launch-latency
+ robustness.
+ **Update 2026-10-14 ~15:40 (afternoon wave, r140-r143):** the full fix
+ set is committed and pushed (HEAD `17393a74`: ring depth `074f493c`,
+ scrubber `624438bb`/`80af379d`, bounded fresh-entry launch retry
+ `6b090f70`, 208-page budgeted data window `fce5ab8d`/`4a23c196`). r142:
+ c1 `victory_mail` and c2 `smiley` GREEN; c3 `victory_matmul` FAIL
+ (all-wrong operands - the three 256 KB operands resolved only ~32 of the
+ needed 64+ pages each); launch counters `ok=86 failed=10 retries=4`
+ (the retry recovers a subset of the cold-context timeouts silently);
+ then `victory_saxpy`/`memcpy_roundtrip` stalled and the guest SSH wedged
+ - the intermittent stall/wedge class survives. r143 (live at this
+ check, same fix set): c1 green within ~2 min; c3 matmul launched and
+ improved to `bad=8192` of 65536 with a non-zero checksum (operand
+ resolution now 68/64/64 pages) - STATUS FAIL. **Root cause found
+ (sub-agent, evidence-grounded): the win0 QMD shadow pool base was still
+ `0x190000` (main-PT slots 0x190..0x197), which lies INSIDE the enlarged
+ 208-page bounce window (`0x110000..0x1df000`); the win0 plant runs on
+ every `pdb_build()` while the bounce plant is one-shot, so win0 stole the
+ PTEs for bounce pages 128..135 = C rows 0..31 = exactly `bad=8192`. Fix
+ landed (unvalidated live at pause): `GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE`
+ moved to `0x1e0000` (slots 0x1e0..0x1e7, above the mail/marker pages,
+ below the 0x200000 guest window) plus a `static_assert` invariant and
+ comment/log corrections.** Ranked next blockers: (1) the 256 KB operand
+ data-path placement; (2) the intermittent client stall/wedge class
+ (the top reliability blocker; needs the stalled-client capture);
+ (3) the cold-context launch race (cold GR-sem pre-warm designed);
+ (4) then multi-mdev, two guests, VRAM sizes and the concurrent/full
+ soak. Host clean, GPU green, no reboots.
+ **Update 2026-10-15 ~18:15 (resume + parallel workstreams; pause snapshot):**
+ the host rebooted (the OS moved the kernel to 6.8.0-147; the module was rebuilt
+ for it). The r155 cycle on the physical backend: ladder green; `victory_mail`
+ (`VADD bad=0` + `STATUS OK`) and `victory_matmul` (`MATMUL n=256 bad=0` +
+ `STATUS OK`) both green end-to-end, and a 100-launch `vecadd_loop` passed; the
+ sequential battery then hit the known acked-no-write class on `streams2`
+ (all-zero output despite `qmd_rel=0x6` completion acks -> `STATUS FAIL`). Three
+ offline workstreams landed as pushed branches: (a) `bigpath` - a 64 MiB
+ boot-fixed data-bounce region (stage-3-planted PTEs) with fail-closed cap
+ semantics, unblocking large operands (victory_math 512 KB, vector_add 4 MB,
+ memcpy_roundtrip 8 MB); (b) `dual` - per-mdev separation (per-guest channel
+ keys, session records, guest-RAM records, forward-request ownership) for two
+ simultaneous guests; (c) `lottery` - analysis proving the completion ack is not
+ an execution witness, rejecting naive fail-closed (idempotent-client false
+ positives), and ranking the next experiments (win0 bank rotation, qmdwin
+ double-buffer, warm-sem retry, executed-vs-wedged discriminator). Host clean,
+ GPU green, VM left live; user-requested poweroff at pause.
- **Guest frontier: FIRST GREEN END-TO-END GUEST CUDA, NOW UNDER HARDENING
(2026-10-04 ~16:20; hardening 2026-10-05 overnight).** The emulation-only
("no-claim") campaign runs an unmodified guest CUDA program to completion
@@ -265,6 +352,12 @@ isolation, performance, portability).
the full FWCLIENT client-managed construct (client ctx buffers + VAS
mapping + reference promote set), then the server commit. See ADDENDUM
53/54.
+ **Update 2026-10-14:** this wall moved decisively - the physical GR
+ engine now executes forwarded guest compute launches, and the first two
+ battery clients are green end-to-end on the physical backend (host CUDA
+ bridge disabled). The remaining items are launch-latency robustness (a
+ first launch after an idle can time out and is auto-retried to success)
+ and the third-client chain; see the status update above.
2. **Hardening the accepted no-claim path.** The first green guest CUDA
acceptance has landed and is holding; the residual classes are (a) corruption
via a late first decode behind pump backlog (~3-5% of runs; the experiment
@@ -1583,7 +1676,7 @@ progress.**
## Environment
- Host GPU: NVIDIA TU104 (RTX 2080 SUPER Mobile), PCI `10de:1ed3`.
-- Host OS: Ubuntu, kernel 6.8.0-142; the distribution NVIDIA driver manages
+- Host OS: Ubuntu, kernel 6.8.0-146; the distribution NVIDIA driver manages
the physical GPU; `vgsp.ko` loads as an independent module.
- Guest: Ubuntu 24.04.5 LTS, stock NVIDIA 570.144 driver, QEMU/KVM q35 with a
`pcie-root-port` for the vGPU, SeaBIOS, mdev passthrough.
diff --git a/kernel/gsp_phys_booter.c b/kernel/gsp_phys_booter.c
index 72cda10..9eb1179 100644
--- a/kernel/gsp_phys_booter.c
+++ b/kernel/gsp_phys_booter.c
@@ -999,16 +999,34 @@ bool gsp_phys_boot_requested(void)
#define GSP_PHYS_RMCALL_VA_DST (GSP_PHYS_RMCALL_VA_BASE + 3 * GSP_PHYS_PAGE_SIZE)
#define GSP_PHYS_RMCALL_VA_SEM (GSP_PHYS_RMCALL_VA_BASE + 4 * GSP_PHYS_PAGE_SIZE)
/*
- * m69: window-0 shadow pool - 8 module pages at VAs 0x108000..0x10f000
- * (small-PT slots 0x108..0x10f, the same 128 B PTE fetch line as the
- * GPFIFO/PB/QMDWIN slots, planted before the engine's first fetch). The
- * Tier-2 copies of the QMD's program/param pages are placed here so the GR
- * engine never has to read through the window-1 scratch VA range whose PT
- * chain (DMA_UPDATE_PDE_2 pdeIndex=1) has never been proven engine-readable.
+ * m69: window-0 shadow pool - 16 module pages holding the Tier-2 copies of
+ * the QMD's program/param pages, so the GR engine never has to read through
+ * the window-1 scratch VA range whose PT chain (DMA_UPDATE_PDE_2
+ * pdeIndex=1) has never been proven engine-readable. The pool is split
+ * into two 8-page banks; the forwarded launch alternates banks per launch
+ * (m151) so one launch's copy-in can never clobber the pages the previous
+ * launch's engine fetches may still be reading (late-fetch safety).
+ *
+ * The base MUST stay clear of the m114/m150 GR data bounce window
+ * (GSP_PHYS_RMCALL_GR_BOUNCE_VA0 .. +PAGES, main-PT slots 0x110..0x1df):
+ * this pool is re-planted on every gsp_phys_rmcall_pdb_build() while the
+ * bounce PTEs are a one-shot plant, so any overlap lets the later win0
+ * plant steal the bounce slots and the engine writes results into the
+ * shadow pages, losing them. 0x1e0000 (slots 0x1e0..0x1ef) sits directly
+ * above the last bounce/mail/marker page (0x1df000) and below the
+ * mail-injection slot 0x1f0 and the guest scratch window (0x200000). Keep
+ * it that way (see the static_assert next to the bounce constants).
*/
#define GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE \
- (GSP_PHYS_RMCALL_VA_BASE + 0x90000ULL)
-#define GSP_PHYS_RMCALL_GR_SHADOW0_SLOTS 8u
+ (GSP_PHYS_RMCALL_VA_BASE + 0xE0000ULL)
+#define GSP_PHYS_RMCALL_GR_SHADOW0_SLOTS 16u
+/*
+ * m151: one bank = one launch's worth of QMD shadow pages. A forwarded
+ * launch consumes at most eight pages (param via qmd_word 32, program 48,
+ * bank 0/1/2/3/5/7 at qmd_word 34..46 even); the per-launch bounds check
+ * uses this so a launch can never spill into the other bank.
+ */
+#define GSP_PHYS_RMCALL_GR_SHADOW0_BANK_SLOTS 8u
/*
* m99: engine-tree branches for identity-mapping the forwarded kernel's
* guest data pages. The walker uses our root[0] -> PD2 -> PD1 -> PD0 dual ->
@@ -1100,6 +1118,33 @@ bool gsp_phys_boot_requested(void)
/* Full 128 B fetch lines per 4 KB small-PT page: (4096/8)/16 = 32. */
#define GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE \
((GSP_PHYS_PAGE_SIZE / 8u) / GSP_PHYS_RMCALL_GUEST_PT_CACHE_PTES)
+/*
+ * m152: extended scratch-PT pool. The original 16 x 2 MB PD0 windows
+ * (512 never-reused 128 B fetch lines per boot) exhausted under a normal
+ * launch mix (each distinct guest page consumed a line per launch) and the
+ * exhaustion was permanent for the boot: every later launch that needed a
+ * new mapping was refused ("launch refused ... no Tier-1 mapping and no
+ * captured inline data"), which is the deterministic once-broken-forever
+ * failure the longevity runs kept hitting. Two changes fix the class:
+ * (a) GR alias cache (below): a consumed fetch line is keyed by the HPA
+ * it maps, so a repeat launch of the same buffers (or the same chunk
+ * page / semaphore page) reuses the identical never-rewritten line and
+ * consumes nothing;
+ * (b) a 48-window extension pool right above the ladder's free gap
+ * (window VA 0x04000000.., PD0 indices 32..79) backed by one 48-page
+ * PT chunk, raising capacity to 64 windows x 32 lines = 2048.
+ * lines/boot. Both are pure capacity mechanics: no PTE and no mapping any
+ * engine may have fetched is ever rewritten.
+ */
+#define GSP_PHYS_RMCALL_VA_GUEST_PT_EXT_BASE 0x04000000ULL
+#define GSP_PHYS_RMCALL_GUEST_PT_EXT_PAGES 48u
+#define GSP_PHYS_RMCALL_GUEST_PT_TOTAL_PAGES \
+ (GSP_PHYS_RMCALL_GUEST_PT_PAGES + GSP_PHYS_RMCALL_GUEST_PT_EXT_PAGES)
+#define GSP_PHYS_RMCALL_GUEST_PT_TOTAL_LINES \
+ (GSP_PHYS_RMCALL_GUEST_PT_TOTAL_PAGES * \
+ GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE)
+/* HPA -> shadow-VA reuse cache; hit = no new fetch line is consumed. */
+#define GSP_PHYS_RMCALL_GR_ALIAS_CACHE 768u
/*
* GR forwarding (gsp_phys_gr_forward=1; default off) scratch resources,
@@ -1253,8 +1298,21 @@ bool gsp_phys_boot_requested(void)
#define GSP_PHYS_RMCALL_CHANNEL_FLAG_USER_LANE \
(u32)(GSP_PHYS_RMCALL_CHANNEL_FLAG_PRIVILEGED | (4u << 8) | \
(1u << 21))
-/* Captured user-lane GPFIFO depth (#825: 1024 entries). */
-#define GSP_PHYS_RMCALL_USER_LANE_GPFIFO_ENTRIES 1024u
+/*
+ * User-lane GPFIFO depth. MUST equal the host ring's push modulus
+ * (GSP_PHYS_RMCALL_GPFIFO_ENTRIES, 8): the host publishes into the
+ * one-page ladder ring modulo 8, so if the channel declares a different
+ * count the engine's GET/PUT wrap desyncs at the host's first wrap
+ * (GP_PUT 7 -> 0 while the engine's GET keeps counting to 8) and every
+ * later launch on the boot is never fetched (r138: launches 1..7 green,
+ * the 8th and all after it silent with `qmd_rel=5 ch_rel=0`). The captured
+ * #825 value (1024) describes the real client's own much larger ring, not
+ * this emulated one; the campaign's 2026-10-06 "fidelity" change to 1024
+ * was a wrap-only difference that broke exactly here (M1's two-launch
+ * acceptance never wrapped). Keep the two ends equal.
+ */
+#define GSP_PHYS_RMCALL_USER_LANE_GPFIFO_ENTRIES \
+ GSP_PHYS_RMCALL_GPFIFO_ENTRIES
#define GSP_PHYS_RMCALL_CHANNEL_INTERNALFLAGS_USER_LANE 0x1du
#define GSP_PHYS_RMCALL_TSG_FAULT_BUF_SIZE 0x2000u
#define GSP_PHYS_RMCALL_USER_LANE_PM_RAW_SIZE 0xd100u
@@ -1379,6 +1437,58 @@ enum gsp_phys_rmcall_buf {
GSP_PHYS_RMCALL_BUF_GUEST_PT13,
GSP_PHYS_RMCALL_BUF_GUEST_PT14,
GSP_PHYS_RMCALL_BUF_GUEST_PT15,
+ /* m152: extension pool PT pages: 48 separate single-page chunks
+ * (GUEST_PT_EXT0..47). The server only accepts single-page memdescs
+ * for client buffers (a 48-page chunk was refused with rpc_result
+ * 0x1f + a firmware ASSERT in stage 2). */
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT0,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT1,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT2,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT3,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT4,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT5,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT6,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT7,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT8,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT9,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT10,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT11,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT12,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT13,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT14,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT15,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT16,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT17,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT18,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT19,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT20,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT21,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT22,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT23,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT24,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT25,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT26,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT27,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT28,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT29,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT30,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT31,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT32,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT33,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT34,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT35,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT36,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT37,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT38,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT39,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT40,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT41,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT42,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT43,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT44,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT45,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT46,
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT47,
/*
* Golden9g: the three client page-table levels the real golden VAS
* hands to the server with VASPACE_COPY_SERVER_RESERVED_PDES
@@ -1409,6 +1519,20 @@ enum gsp_phys_rmcall_buf {
* generous and progress is logged every ~500 ms.
*/
#define GSP_PHYS_RMCALL_GR_POLL_MS 5000u
+/*
+ * Bounded retry of a forwarded GR launch whose QMD did not run. A green
+ * launch completes in ~170-195 ms; when the engine drops it (cold context
+ * save/unload, or an entry the PBDMA never fetched) `qmd_rel` never reaches
+ * the wanted value and the client would fall back to the software release
+ * and read stale/zero results. Because the timeout path never copies the
+ * result out to the guest, a launch that timed out did not reach the
+ * client: re-publishing a FRESH ring entry (same PB/QMD) and re-issuing the
+ * schedule+doorbell gives the front-end another chance and the guest still
+ * sees only the success path. Bounded so the service thread is never held
+ * past the existing hold budget.
+ */
+#define GSP_PHYS_RMCALL_GR_RETRY_MS 2000u
+#define GSP_PHYS_RMCALL_GR_ATTEMPTS 3u
/* P6a guest-memory CE copies: same bounded poll cap as the P5 shadow path. */
#define GSP_PHYS_RMCALL_GUEST_POLL_MS 200u
@@ -2136,17 +2260,31 @@ static struct gsp_phys_boot_stage {
bool rmcall_gr_exec_ok; /* a real GR submission proved out */
u32 rmcall_gr_submit_epoch; /* per-submission mapper epoch */
u32 rmcall_gr_pt_used; /* Tier-1 window pool slots used */
- u32 rmcall_guest_pt_win_mask; /* Tier-2 scratch windows installed */
+ u64 rmcall_guest_pt_win_mask; /* Tier-2 scratch windows installed */
+ /*
+ * m152: shadow alias reuse cache: hpa -> never-rewritten shadow VA.
+ * Populated by gsp_phys_rmcall_gr_map_shadow_locked; a hit returns the
+ * existing line so repeat launches of the same physical pages consume
+ * no new fetch line. Fill-order/LRU-free by design: entries are dead
+ * weight once a client exits, but a boot has 2048 lines to give and
+ * the cache only ever returns identical PTE content.
+ */
+ u64 rmcall_gr_alias_hpa[GSP_PHYS_RMCALL_GR_ALIAS_CACHE];
+ u64 rmcall_gr_alias_va[GSP_PHYS_RMCALL_GR_ALIAS_CACHE];
+ u32 rmcall_gr_alias_count;
struct gsp_phys_boot_chunk rmcall_gr_pt[GSP_PHYS_RMCALL_GR_PT_PAGES];
struct gsp_phys_boot_chunk rmcall_gr_shadow; /* Tier-2 pages */
/*
* m69: window-0 shadow pool (gsp_phys_rmcall_gr_shadow_win0=1).
- * Eight module pages whose PTEs are planted in the proven window-0
- * PT line at slots 0x108..0x10f (VAs 0x108000..0x10f000) by
- * gsp_phys_rmcall_pdb_build, before the engine's first fetch (same
- * pattern as the qmdwin slot). The launch copies the QMD's
- * program/param page content into them and rewrites the QMD pointers
- * there, so the GR engine never reads the window-1 scratch range.
+ * Sixteen module pages (two 8-page banks; m151 alternates the bank
+ * per launch so a launch's copy-in cannot clobber pages the previous
+ * launch's engine fetches may still be reading) whose PTEs are
+ * planted in the proven window-0 PT (slots derive from
+ * GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE) by gsp_phys_rmcall_pdb_build,
+ * before the engine's first fetch (same pattern as the qmdwin slot).
+ * The launch copies the QMD's program/param page content into them
+ * and rewrites the QMD pointers there, so the GR engine never reads
+ * the window-1 scratch range.
*/
struct gsp_phys_boot_chunk rmcall_gr_shadow_win0;
u32 rmcall_gr_shadow_win0_used;
@@ -2161,19 +2299,52 @@ static struct gsp_phys_boot_stage {
/* m104: module page holding the injected mail_kernel message. */
struct gsp_phys_boot_chunk rmcall_gr_mail;
/*
- * m114: pre-commit bounce window. 112 pages planted into the main
- * small-PT at slots 0x110..0x17f during stage 3 (the proven win0
- * pattern: the SM walker resolves the commit-time tree only). The
- * launch copies the kernel's data pages in, rewrites the pointers to
- * these VAs (reloc base 0x110000), and copies the results back after
- * the release; page 100 (VA 0x174000) carries the mail message.
+ * m114: pre-commit bounce window, planted into the main small-PT at
+ * slots 0x110..0x1df during stage 3 (the proven win0 pattern: the SM
+ * walker resolves the commit-time tree only). The launch copies the
+ * kernel's data pages in, rewrites the pointers to these VAs (reloc
+ * base 0x110000), and copies the results back after the release.
+ *
+ * The window is 208 pages so the data region below the mail page can
+ * carry the operand pages of kernels whose buffers exceed the old
+ * 128 KB (victory_matmul's 256 KB buffers need 64 pages each; three
+ * of them plus argument/bank candidates exceed the old 100-page /
+ * 33-pages-per-pointer caps and the engine then ran on truncated
+ * windows). The mail page is the LAST data-free page (slot 206) and
+ * the m132 marker sits at 207; the main PT page deliberately covers
+ * the whole 0x110000..0x1df000 range with nothing else planted there.
*/
struct gsp_phys_boot_chunk rmcall_gr_bounce;
u32 rmcall_gr_bounce_used;
-#define GSP_PHYS_RMCALL_GR_BOUNCE_PAGES 112u
+#define GSP_PHYS_RMCALL_GR_BOUNCE_PAGES 208u
#define GSP_PHYS_RMCALL_GR_BOUNCE_SLOT0 0x110u
#define GSP_PHYS_RMCALL_GR_BOUNCE_VA0 0x110000ULL
-#define GSP_PHYS_RMCALL_GR_MAIL_SLOT 100u
+#define GSP_PHYS_RMCALL_GR_MAIL_SLOT 206u
+/*
+ * m150b: the win0 QMD shadow pool must never overlap the bounce window.
+ * The win0 plant runs on every pdb_build() (unconditional) and the bounce
+ * plant is one-shot, so an overlap silently hijacks bounce PTEs and the
+ * engine's results land in the shadow pages (victory_matmul lost C rows
+ * 0..31 exactly this way before 0x1e0000 moved the pool clear).
+ */
+static_assert(GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE +
+ (u64)GSP_PHYS_RMCALL_GR_SHADOW0_SLOTS * GSP_PHYS_PAGE_SIZE <=
+ GSP_PHYS_RMCALL_GR_BOUNCE_VA0 ||
+ GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE >=
+ GSP_PHYS_RMCALL_GR_BOUNCE_VA0 +
+ (u64)GSP_PHYS_RMCALL_GR_BOUNCE_PAGES * GSP_PHYS_PAGE_SIZE,
+ "win0 shadow pool must not overlap the GR data bounce window");
+/*
+ * m151: and the two-bank pool must end below the reserved mail-injection
+ * slot 0x1f0 (VA 0x1f0000) so the extra slots never steal that PTE.
+ */
+static_assert(GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE +
+ (u64)GSP_PHYS_RMCALL_GR_SHADOW0_SLOTS * GSP_PHYS_PAGE_SIZE <=
+ GSP_PHYS_RMCALL_VA_BASE + 0xF0000ULL,
+ "win0 shadow pool must end below the reserved mail-injection slot");
+static_assert(GSP_PHYS_RMCALL_GR_SHADOW0_SLOTS ==
+ 2u * GSP_PHYS_RMCALL_GR_SHADOW0_BANK_SLOTS,
+ "win0 shadow pool must be exactly two banks");
/* m99: guest data identity-map table branches (see the PD*_MAX block). */
struct gsp_phys_boot_chunk
rmcall_gr_data_pd1[GSP_PHYS_RMCALL_GR_DATA_PD1_MAX];
@@ -2813,6 +2984,7 @@ static void gsp_phys_rmcall_reset(void)
boot_stage.rmcall_gr_submit_epoch = 0;
boot_stage.rmcall_gr_pt_used = 0;
boot_stage.rmcall_guest_pt_win_mask = 0;
+ boot_stage.rmcall_gr_alias_count = 0;
memset(boot_stage.rmcall_gr_pt, 0, sizeof(boot_stage.rmcall_gr_pt));
memset(&boot_stage.rmcall_gr_shadow, 0,
sizeof(boot_stage.rmcall_gr_shadow));
@@ -7215,6 +7387,65 @@ static void gsp_phys_rmcall_ctx_diag_dump(const char *what,
static void gsp_phys_rmcall_ctx_diag_decode(const char *tag);
/*
+ * m152: scratch-PT pool geometry helpers. Pool local page P (0-based across
+ * the original 16-window region then the 48-window extension) maps to a PD0
+ * window VA and to a page of either the per-window single-page PT chunks
+ * (P < 16) or the multi-page extension chunk (P >= 16). Keep every
+ * consumer on these two helpers so the two regions can never drift.
+ */
+static u64 gsp_phys_rmcall_guest_pt_win_va(u32 page)
+{
+ if (page < GSP_PHYS_RMCALL_GUEST_PT_PAGES)
+ return boot_stage.rmcall_va_guest_pt + ((u64)page << 21);
+ return GSP_PHYS_RMCALL_VA_GUEST_PT_EXT_BASE +
+ ((u64)(page - GSP_PHYS_RMCALL_GUEST_PT_PAGES) << 21);
+}
+
+static u8 *gsp_phys_rmcall_guest_pt_page_va(u32 page)
+{
+ if (page < GSP_PHYS_RMCALL_GUEST_PT_PAGES)
+ return gsp_phys_chunk_page_va(
+ &boot_stage.rmcall_chunks[GSP_PHYS_RMCALL_BUF_GUEST_PT0 +
+ page], 0);
+ if (page < GSP_PHYS_RMCALL_GUEST_PT_TOTAL_PAGES)
+ return gsp_phys_chunk_page_va(
+ &boot_stage.rmcall_chunks[
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT0 +
+ (page - GSP_PHYS_RMCALL_GUEST_PT_PAGES)], 0);
+ return NULL;
+}
+
+static u64 gsp_phys_rmcall_guest_pt_page_dma(u32 page)
+{
+ if (page < GSP_PHYS_RMCALL_GUEST_PT_PAGES)
+ return (u64)gsp_phys_chunk_page_dma(
+ &boot_stage.rmcall_chunks[GSP_PHYS_RMCALL_BUF_GUEST_PT0 +
+ page], 0);
+ return (u64)gsp_phys_chunk_page_dma(
+ &boot_stage.rmcall_chunks[
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT0 +
+ (page - GSP_PHYS_RMCALL_GUEST_PT_PAGES)], 0);
+}
+
+/*
+ * PD0 index -> pool local page, or 0xffffffffu when the index is not a
+ * scratch-pool window. Consults both pool regions.
+ */
+static u32 gsp_phys_rmcall_guest_pt_page_of_pde(u32 pde_index)
+{
+ u32 base = (u32)(boot_stage.rmcall_va_guest_pt >> 21);
+
+ if (pde_index >= base &&
+ pde_index < base + GSP_PHYS_RMCALL_GUEST_PT_PAGES)
+ return pde_index - base;
+ base = (u32)(GSP_PHYS_RMCALL_VA_GUEST_PT_EXT_BASE >> 21);
+ if (pde_index >= base &&
+ pde_index < base + GSP_PHYS_RMCALL_GUEST_PT_EXT_PAGES)
+ return pde_index - base + GSP_PHYS_RMCALL_GUEST_PT_PAGES;
+ return 0xffffffffu;
+}
+
+/*
* Shared-management VAS: install the ladder's page-table window through the
* server's own MMU walker. NV0080_CTRL_CMD_DMA_UPDATE_PDE_2 (0x80180f,
* gpu_vaspace.c gvaspaceUpdatePde2_IMPL) takes the system-physical base of a
@@ -9364,7 +9595,7 @@ static void gsp_phys_rmcall_pdb_build(void)
}
}
/*
- * m69: plant the window-0 shadow pool PTEs (slots 0x108..0x10f) in the
+ * m69: plant the window-0 shadow pool PTEs (current win0 slot range) in the
* same proven small-PT line, before the engine's first fetch. The
* launch copies the QMD's program/param page content into these pages
* and rewrites the QMD pointers to these VAs, so the GR engine never
@@ -9410,12 +9641,18 @@ static void gsp_phys_rmcall_pdb_build(void)
(size_t)idx * 8);
}
dma_wmb();
- pr_info("vgsp-boot: rm-call: shadow-win0 pool planted %u pages at VA 0x%llx..0x%llx (small-PT slots 0x108..0x10f, before first fetch)\n",
+ pr_info("vgsp-boot: rm-call: shadow-win0 pool planted %u pages at VA 0x%llx..0x%llx (small-PT slots 0x%x..0x%x, before first fetch)\n",
GSP_PHYS_RMCALL_GR_SHADOW0_SLOTS,
GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE,
GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE +
(GSP_PHYS_RMCALL_GR_SHADOW0_SLOTS - 1) *
- GSP_PHYS_PAGE_SIZE);
+ GSP_PHYS_PAGE_SIZE,
+ (u32)((GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE >> 12) &
+ 0x1ffu),
+ (u32)(((GSP_PHYS_RMCALL_VA_GR_SHADOW0_BASE +
+ (GSP_PHYS_RMCALL_GR_SHADOW0_SLOTS - 1) *
+ GSP_PHYS_PAGE_SIZE) >> 12) &
+ 0x1ffu));
}
}
/*
@@ -9532,6 +9769,30 @@ static void gsp_phys_rmcall_pdb_build(void)
(size_t)(1 + i) * 16 + 8);
}
/*
+ * m152: extension scratch pool - PD0 dual entries 32..79 back the
+ * GSP_PHYS_RMCALL_GUEST_PT_EXT_PAGES pages of the multi-page extension
+ * PT chunk at GSP_PHYS_RMCALL_VA_GUEST_PT_EXT_BASE (0x04000000; free
+ * space above the GR-context windows, which end well below 0x03000000).
+ * Same shape and same once-before-any-engine-access rule as the 16-window
+ * loop above.
+ */
+ for (i = 0; i < GSP_PHYS_RMCALL_GUEST_PT_EXT_PAGES; i++) {
+ u64 guest_pt_pde = gsp_phys_rmcall_pde(
+ (u64)gsp_phys_chunk_page_dma(
+ &boot_stage.rmcall_chunks[
+ GSP_PHYS_RMCALL_BUF_GUEST_PT_EXT0 + i],
+ 0));
+ u32 pd0_idx = (u32)(GSP_PHYS_RMCALL_VA_GUEST_PT_EXT_BASE >> 21) +
+ i;
+
+ put_unaligned_le64(0,
+ (u8 *)gsp_phys_chunk_page_va(pd0, 0) +
+ (size_t)pd0_idx * 16);
+ put_unaligned_le64(guest_pt_pde,
+ (u8 *)gsp_phys_chunk_page_va(pd0, 0) +
+ (size_t)pd0_idx * 16 + 8);
+ }
+ /*
* H1 GR context window: when the GR ladder prepared the promoted MAIN
* context buffer (gsp_phys_rmcall_gr_ctx_prepare ran before this
* builder), append its page tables as fresh PD0 dual entries 17.. (the
@@ -9698,11 +9959,12 @@ static void gsp_phys_rmcall_pdb_build(void)
src_idx, pt_src, (u64)gsp_phys_chunk_page_dma(src, 0),
dst_idx, pt_dst, (u64)gsp_phys_chunk_page_dma(dst, 0),
sem_idx, pt_sem, (u64)gsp_phys_chunk_page_dma(sem, 0));
- pr_info("vgsp-boot: rm-call: guest scratch PT built: pages=%u vaBase=0x%llx capacity=%u\n",
+ pr_info("vgsp-boot: rm-call: guest scratch PT built: pages=%u (+%u ext) vaBase=0x%llx extBase=0x%llx capacity=%u\n",
GSP_PHYS_RMCALL_GUEST_PT_PAGES,
+ GSP_PHYS_RMCALL_GUEST_PT_EXT_PAGES,
GSP_PHYS_RMCALL_VA_GUEST_PT_BASE,
- GSP_PHYS_RMCALL_GUEST_PT_PAGES *
- GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE);
+ GSP_PHYS_RMCALL_VA_GUEST_PT_EXT_BASE,
+ GSP_PHYS_RMCALL_GUEST_PT_TOTAL_LINES);
}
/*
@@ -15162,7 +15424,6 @@ static int gsp_phys_rmcall_guest_map_locked(u64 src_hpa, u64 dst_hpa,
u64 sem_hpa, u64 *src_va,
u64 *dst_va, u64 *sem_va)
{
- struct gsp_phys_boot_chunk *guest_pt_chunk;
u32 line = boot_stage.rmcall_guest_pt_next_line;
u32 page, line_in_page;
u64 va;
@@ -15170,19 +15431,14 @@ static int gsp_phys_rmcall_guest_map_locked(u64 src_hpa, u64 dst_hpa,
if (!boot_stage.rmcall_va_guest_pt)
return -EIO;
- if (line >= GSP_PHYS_RMCALL_GUEST_PT_PAGES *
- GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE)
+ if (line >= GSP_PHYS_RMCALL_GUEST_PT_TOTAL_LINES)
return -ENOSPC;
page = line / GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE;
line_in_page = line % GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE;
- guest_pt_chunk = &boot_stage.rmcall_chunks[
- GSP_PHYS_RMCALL_BUF_GUEST_PT0 + page];
- if (!guest_pt_chunk->pages)
- return -EIO;
/* 2 MB PD0 slot stride; a full line is 16 PTEs = 0x10000 of VA. */
- va = boot_stage.rmcall_va_guest_pt + ((u64)page << 21) +
+ va = gsp_phys_rmcall_guest_pt_win_va(page) +
((u64)line_in_page << 16);
- pt = gsp_phys_chunk_page_va(guest_pt_chunk, 0);
+ pt = gsp_phys_rmcall_guest_pt_page_va(page);
if (!pt)
return -EIO;
/*
@@ -15403,15 +15659,13 @@ static int gsp_phys_guest_ce_copy_common(u64 src_hpa, u32 src_off, u64 dst_hpa,
if ((gsp_phys_relay_pool_exhausted & 63) == 1)
pr_err("vgsp-work: GUEST CE RELAY FAILED: scratch-PT pool exhausted (no line reuse) pool_exhausted=%llu capacity=%u; falling back to the software release\n",
gsp_phys_relay_pool_exhausted,
- GSP_PHYS_RMCALL_GUEST_PT_PAGES *
- GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE);
+ GSP_PHYS_RMCALL_GUEST_PT_TOTAL_LINES);
} else {
gsp_phys_guest_pool_exhausted++;
if ((gsp_phys_guest_pool_exhausted & 63) == 1)
pr_err("vgsp-work: GUEST CE COPY FAILED: scratch-PT pool exhausted (no line reuse) pool_exhausted=%llu capacity=%u; falling back to the ladder path\n",
gsp_phys_guest_pool_exhausted,
- GSP_PHYS_RMCALL_GUEST_PT_PAGES *
- GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE);
+ GSP_PHYS_RMCALL_GUEST_PT_TOTAL_LINES);
}
} else {
if (relay)
@@ -15738,6 +15992,7 @@ static u64 gsp_phys_gr_submitted;
static u64 gsp_phys_gr_ok;
static u64 gsp_phys_gr_refused;
static u64 gsp_phys_gr_timed_out;
+static u64 gsp_phys_gr_retries; /* fresh-entry launch retries published */
static u64 gsp_phys_gr_pool_exhausted;
/* m86: program-page override blob (see gsp-phys-gr-override). */
static u8 gsp_phys_gr_override[GSP_PHYS_PAGE_SIZE];
@@ -16077,26 +16332,26 @@ static void __iomem *gsp_phys_gr_scratch_bar0;
static int gsp_phys_rmcall_gr_scratch_win_install_locked(u32 page)
{
struct gsp_phys_rmcall_update_pde2_params p;
- struct gsp_phys_boot_chunk *pt;
u32 rpc_result = 0, status = 0, pde_index;
+ u64 pt_dma;
int ret;
- if (page >= GSP_PHYS_RMCALL_GUEST_PT_PAGES)
+ if (page >= GSP_PHYS_RMCALL_GUEST_PT_TOTAL_PAGES)
return -EINVAL;
if (!gsp_phys_rmcall_vas_shared_value())
return 0;
- if (boot_stage.rmcall_guest_pt_win_mask & BIT(page))
+ if (boot_stage.rmcall_guest_pt_win_mask & BIT_ULL(page))
return 0;
- pt = &boot_stage.rmcall_chunks[GSP_PHYS_RMCALL_BUF_GUEST_PT0 + page];
- if (!pt->pages)
+ pt_dma = gsp_phys_rmcall_guest_pt_page_dma(page);
+ if (!pt_dma)
return -EIO;
if (!boot_stage.rmcall_va_guest_pt)
return -EIO;
- pde_index = (u32)((boot_stage.rmcall_va_guest_pt >> 21) + page);
+ pde_index = (u32)(gsp_phys_rmcall_guest_pt_win_va(page) >> 21);
memset(&p, 0, sizeof(p));
p.pde_index = cpu_to_le32(pde_index);
p.flags = cpu_to_le32(GSP_PHYS_RMCALL_PDE2_FLAG_FLUSH);
- p.pt[0].phys_addr = cpu_to_le64((u64)gsp_phys_chunk_page_dma(pt, 0));
+ p.pt[0].phys_addr = cpu_to_le64(pt_dma);
p.pt[0].aperture = cpu_to_le32(GSP_PHYS_RMCALL_PDE2_APERTURE_SYS_COH);
p.pt[1].aperture = cpu_to_le32(GSP_PHYS_RMCALL_PDE2_APERTURE_INVALID);
p.h_va_space = cpu_to_le32(boot_stage.rmcall_vas);
@@ -16113,10 +16368,9 @@ static int gsp_phys_rmcall_gr_scratch_win_install_locked(u32 page)
ret);
return ret;
}
- boot_stage.rmcall_guest_pt_win_mask |= BIT(page);
+ boot_stage.rmcall_guest_pt_win_mask |= BIT_ULL(page);
pr_info("vgsp-gr: scratch window installed pdeIndex=%u (VA 0x%llx) ptPa=0x%llx status=0x%x (Tier-2 shadow legs now reachable through the server walker)\n",
- pde_index, (u64)pde_index << 21,
- (u64)gsp_phys_chunk_page_dma(pt, 0), status);
+ pde_index, (u64)pde_index << 21, pt_dma, status);
return 0;
}
@@ -16129,13 +16383,11 @@ static int gsp_phys_rmcall_gr_scratch_win_install_locked(u32 page)
static bool gsp_phys_rmcall_gr_win_is_installed_locked(u64 va)
{
u32 pde_index = (u32)(va >> 21);
- u32 base = (u32)(boot_stage.rmcall_va_guest_pt >> 21);
+ u32 page = gsp_phys_rmcall_guest_pt_page_of_pde(pde_index);
u32 i;
- if (pde_index >= base &&
- pde_index < base + GSP_PHYS_RMCALL_GUEST_PT_PAGES)
- return !!(boot_stage.rmcall_guest_pt_win_mask &
- BIT(pde_index - base));
+ if (page != 0xffffffffu)
+ return !!(boot_stage.rmcall_guest_pt_win_mask & BIT_ULL(page));
for (i = 0; i < boot_stage.rmcall_gr_pt_used; i++)
if (boot_stage.rmcall_gr_win[i].used &&
boot_stage.rmcall_gr_win[i].pde_index == pde_index)
@@ -16154,13 +16406,11 @@ static bool gsp_phys_rmcall_gr_win_is_installed_locked(u64 va)
static int gsp_phys_rmcall_gr_win_ensure_locked(void __iomem *bar0, u64 va)
{
u32 pde_index = (u32)(va >> 21);
- u32 base = (u32)(boot_stage.rmcall_va_guest_pt >> 21);
+ u32 page = gsp_phys_rmcall_guest_pt_page_of_pde(pde_index);
u32 slot;
- if (pde_index >= base &&
- pde_index < base + GSP_PHYS_RMCALL_GUEST_PT_PAGES)
- return gsp_phys_rmcall_gr_scratch_win_install_locked(
- pde_index - base);
+ if (page != 0xffffffffu)
+ return gsp_phys_rmcall_gr_scratch_win_install_locked(page);
return gsp_phys_rmcall_gr_win_slot_locked(bar0, pde_index, &slot);
}
@@ -16171,16 +16421,35 @@ static int gsp_phys_rmcall_gr_win_ensure_locked(void __iomem *bar0, u64 va)
* supplies the backing page. Called with gsp_phys_rm_lock held.
*/
static int gsp_phys_rmcall_gr_map_shadow_locked(u32 slot, u64 hpa,
- u64 *va_out)
+ u64 *va_out, u32 *line_out)
{
struct gsp_phys_boot_chunk *sh = &boot_stage.rmcall_gr_shadow;
- struct gsp_phys_boot_chunk *pt;
u32 line = boot_stage.rmcall_guest_pt_next_line;
- u32 page, line_in_page;
+ u32 page, line_in_page, i;
u64 va;
- u8 *pt_va;
+ u8 *pt;
int ret;
+ /*
+ * m152: reuse before consume. A shadow VA is a never-rewritten PTE:
+ * once hpa X maps at VA V, every later request for X can be served by
+ * V with byte-identical engine-visible content, so the pool only
+ * needs one line per distinct HPA per boot instead of one per launch.
+ * This is safe against the GMMU's no-invalidate fetch-line cache by
+ * construction: the PTE value never differs from what the engine may
+ * have already fetched. A hit reports line_out = ~0 so a caller that
+ * records contiguity bookkeeping never contig-extends an old line
+ * (a new PTE in a line the engine may have already fetched would be
+ * invisible).
+ */
+ for (i = 0; i < boot_stage.rmcall_gr_alias_count; i++) {
+ if (boot_stage.rmcall_gr_alias_hpa[i] == hpa) {
+ *va_out = boot_stage.rmcall_gr_alias_va[i];
+ if (line_out)
+ *line_out = 0xffffffffu;
+ return 0;
+ }
+ }
if (!sh->pages) {
ret = gsp_phys_chunk_alloc(boot_stage.pdev, sh,
GSP_PHYS_RMCALL_GR_SHADOW_PAGES);
@@ -16192,28 +16461,34 @@ static int gsp_phys_rmcall_gr_map_shadow_locked(u32 slot, u64 hpa,
return -ENOSPC;
if (!boot_stage.rmcall_va_guest_pt)
return -EIO;
- if (line >= GSP_PHYS_RMCALL_GUEST_PT_PAGES *
- GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE)
+ if (line >= GSP_PHYS_RMCALL_GUEST_PT_TOTAL_LINES)
return -ENOSPC;
page = line / GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE;
line_in_page = line % GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE;
ret = gsp_phys_rmcall_gr_scratch_win_install_locked(page);
if (ret)
return ret;
- pt = &boot_stage.rmcall_chunks[GSP_PHYS_RMCALL_BUF_GUEST_PT0 + page];
- if (!pt->pages)
- return -EIO;
- pt_va = gsp_phys_chunk_page_va(pt, 0);
- if (!pt_va)
+ pt = gsp_phys_rmcall_guest_pt_page_va(page);
+ if (!pt)
return -EIO;
put_unaligned_le64(gsp_phys_rmcall_pte(hpa),
- pt_va + (size_t)(GSP_PHYS_RMCALL_GUEST_PT_CACHE_PTES *
+ pt + (size_t)(GSP_PHYS_RMCALL_GUEST_PT_CACHE_PTES *
line_in_page) * 8);
dma_wmb();
boot_stage.rmcall_guest_pt_next_line = line + 1;
- va = boot_stage.rmcall_va_guest_pt + ((u64)page << 21) +
+ va = gsp_phys_rmcall_guest_pt_win_va(page) +
((u64)line_in_page << 16);
*va_out = va;
+ if (line_out)
+ *line_out = line;
+ if (boot_stage.rmcall_gr_alias_count <
+ GSP_PHYS_RMCALL_GR_ALIAS_CACHE) {
+ u32 ci = boot_stage.rmcall_gr_alias_count;
+
+ boot_stage.rmcall_gr_alias_hpa[ci] = hpa;
+ boot_stage.rmcall_gr_alias_va[ci] = va;
+ boot_stage.rmcall_gr_alias_count = ci + 1u;
+ }
return 0;
}
@@ -16227,32 +16502,27 @@ static int gsp_phys_rmcall_gr_map_shadow_locked(u32 slot, u64 hpa,
static int gsp_phys_rmcall_gr_map_shadow_contig_locked(u32 line, u32 pte,
u64 hpa, u64 *va_out)
{
- struct gsp_phys_boot_chunk *pt;
u32 page, line_in_page;
- u8 *pt_va;
+ u8 *pt;
int ret;
if (pte >= GSP_PHYS_RMCALL_GUEST_PT_CACHE_PTES)
return -EINVAL;
- if (line >= GSP_PHYS_RMCALL_GUEST_PT_PAGES *
- GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE)
+ if (line >= GSP_PHYS_RMCALL_GUEST_PT_TOTAL_LINES)
return -ENOSPC;
page = line / GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE;
line_in_page = line % GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE;
ret = gsp_phys_rmcall_gr_scratch_win_install_locked(page);
if (ret)
return ret;
- pt = &boot_stage.rmcall_chunks[GSP_PHYS_RMCALL_BUF_GUEST_PT0 + page];
- if (!pt->pages)
- return -EIO;
- pt_va = gsp_phys_chunk_page_va(pt, 0);
- if (!pt_va)
+ pt = gsp_phys_rmcall_guest_pt_page_va(page);
+ if (!pt)
return -EIO;
put_unaligned_le64(gsp_phys_rmcall_pte(hpa),
- pt_va + (size_t)(GSP_PHYS_RMCALL_GUEST_PT_CACHE_PTES *
+ pt + (size_t)(GSP_PHYS_RMCALL_GUEST_PT_CACHE_PTES *
line_in_page + pte) * 8);
dma_wmb();
- *va_out = boot_stage.rmcall_va_guest_pt + ((u64)page << 21) +
+ *va_out = gsp_phys_rmcall_guest_pt_win_va(page) +
((u64)line_in_page << 16) + (u64)pte * GSP_PHYS_PAGE_SIZE;
return 0;
}
@@ -16471,6 +16741,24 @@ static int gsp_phys_rmcall_gr_resched_locked(void __iomem *bar0)
u32 runlist_id, ch_id, doorbell;
int ret;
+ /*
+ * m155 (HF-3 extended re-arm): after the channel has been idle long
+ * enough for the FECS to save/unload the context, re-issuing only the
+ * two GPFIFO_SCHEDULE verbs does not revive dispatch - r154: matmul's
+ * post-smiley launches acked (qmd_rel/ch_rel) but SKED stayed idle
+ * (cwd0=0x2086/fecs_cur=0) for every attempt including outcome
+ * retries, while the same channel executed mail/smiley earlier in the
+ * boot. Replay the mode-35 lane-enable tail (CILP preempt mode,
+ * timeslice, device sched) that stage 4 ran when the lane first went
+ * live - it is proven accepted and is the design's extended re-arm.
+ * The poison variants (explicit PREEMPT/tail, BIND, PROMOTE) stay out.
+ */
+ ret = gsp_phys_rmcall_user_lane_lane_enable(bar0);
+ if (ret)
+ pr_info("vgsp-gr: rearm lane-enable rc=%d (continuing to schedules)\n",
+ ret);
+ rpc_result = 0;
+ status = 0;
ret = gsp_phys_rmcall_control(bar0, boot_stage.rmcall_channel,
GSP_PHYS_RMCALL_CTRL_GPFIFO_SCHEDULE,
sched_params, sizeof(sched_params), 16,
@@ -16495,6 +16783,144 @@ static int gsp_phys_rmcall_gr_resched_locked(void __iomem *bar0)
return 0;
}
+/*
+ * m151: FNV-1a 64-bit over one page. Used to compare the win0 param page
+ * and the guest param page before and after the poll, so a launch that
+ * wrote (or clobbered) the page the engine read is provable from the log.
+ */
+static inline u64 gsp_phys_gr_fnv1a64(const u8 *p, size_t len)
+{
+ u64 h = 0xcbf29ce484222325ULL;
+ size_t i;
+
+ for (i = 0; i < len; i++) {
+ h ^= p[i];
+ h *= 0x100000001b3ULL;
+ }
+ return h;
+}
+
+/*
+ * m151: output retirement wait. qmd_rel is a schedule/fetch ack (the front
+ * end latched and fetched the QMD), not a data-path retirement: the SM's
+ * stores into the bounce pages can still be in flight when the ack lands.
+ * Copying out - and the next launch's copy-in - before those stores land
+ * loses them: the guest observes zeros for a completed launch, or
+ * stale/mixed content when the next copy-in races the previous engine's
+ * write tail.
+ *
+ * The wait covers EVERY resolved data page (footprint-by-inference was
+ * tried twice and is unreliable: the fill's candidate list ends with the
+ * launch's own bank/param pages, so the max-reloc run is 3-4 pages, not
+ * the output operand). Adaptive exits make a whole-window count cheap:
+ * - all resolved pages changed -> done (normal case, one poll)
+ * - changed>0 and stable for 3 polls -> stores settled (masked or
+ * repeated-identical kernels)
+ * - no writes at all for 600 ms -> no output for this launch
+ * - hard cap 1.5 s -> genuinely late remainder
+ * noinline so this rarely-taken wait does not grow the submit frame, which
+ * is already near the warning bound.
+ */
+static noinline u32
+gsp_phys_gr_output_retire_wait(const struct gsp_phys_gr_submit *req,
+ const u32 *csum_in)
+{
+ u32 checked[GSP_PHYS_RMCALL_GR_MAIL_SLOT];
+ u32 n_all = 0, di, iter;
+ u32 changed = 0, prev = 0xffffffffu;
+ const char *why = " (TIMEOUT)";
+ unsigned long wait_start = jiffies;
+
+ if (!req->n_data)
+ return 0;
+ for (di = 0; di < req->n_data; di++) {
+ u32 bi = (u32)((req->data[di].reloc_va -
+ GSP_PHYS_RMCALL_GR_BOUNCE_VA0) >> 12);
+ u32 k;
+ bool dup = false;
+
+ if (bi >= GSP_PHYS_RMCALL_GR_MAIL_SLOT)
+ continue;
+ for (k = 0; k < n_all; k++)
+ if (checked[k] == bi) {
+ dup = true;
+ break;
+ }
+ if (!dup)
+ checked[n_all++] = bi;
+ }
+ if (!n_all)
+ return 0;
+ for (iter = 0; iter < 300u; iter++) {
+ u32 j;
+
+ changed = 0;
+ for (j = 0; j < n_all; j++) {
+ const u8 *src = gsp_phys_chunk_page_va(
+ &boot_stage.rmcall_gr_bounce, checked[j]);
+ u32 ci, csum = 0;
+
+ if (!src)
+ continue;
+ for (ci = 0; ci < GSP_PHYS_PAGE_SIZE; ci += 4)
+ csum += get_unaligned_le32(src + ci);
+ if (csum != csum_in[checked[j]])
+ changed++;
+ }
+ if (changed >= n_all) {
+ why = "";
+ break;
+ }
+ if (changed > 0 && changed == prev && iter >= 3u) {
+ why = " (settled)";
+ break;
+ }
+ if (changed == 0 && iter >= 120u) {
+ why = " (no writes)";
+ break;
+ }
+ prev = changed;
+ usleep_range(4000, 6000);
+ }
+ pr_info("vgsp-gr: output retirement wait: %u/%u pages changed after %u ms%s\n",
+ changed, n_all, jiffies_to_msecs(jiffies - wait_start), why);
+ return changed;
+}
+
+/*
+ * m153: fill the bounce window from the request's guest pages and capture a
+ * per-page checksum baseline. Used before the first publish and again before
+ * every outcome retry (a mis-executing first attempt can leave stale or
+ * wrong-window content behind, so a retry must start from the guest inputs
+ * again).
+ */
+static void gsp_phys_gr_fill_bounce(const struct gsp_phys_gr_submit *req,
+ u32 *csum_in)
+{
+ u32 di;
+
+ for (di = 0; di < req->n_data; di++) {
+ u32 bi = (u32)((req->data[di].reloc_va -
+ GSP_PHYS_RMCALL_GR_BOUNCE_VA0) >> 12);
+ const u8 *src;
+ u8 *dst;
+ u32 ci, csum = 0;
+
+ if (bi >= GSP_PHYS_RMCALL_GR_MAIL_SLOT ||
+ !req->data[di].hpa)
+ continue;
+ src = (const u8 *)__va(req->data[di].hpa);
+ dst = gsp_phys_chunk_page_va(&boot_stage.rmcall_gr_bounce, bi);
+ if (!dst)
+ continue;
+ memcpy(dst, src, GSP_PHYS_PAGE_SIZE);
+ for (ci = 0; ci < GSP_PHYS_PAGE_SIZE; ci += 4)
+ csum += get_unaligned_le32(dst + ci);
+ csum_in[bi] = csum;
+ }
+ dma_wmb();
+}
+
int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
struct gsp_phys_backend_copy_result *out)
{
@@ -16517,6 +16943,8 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
unsigned long deadline;
u8 *sem_va;
u32 epoch, words, i, n_shadow = 0;
+ u32 win0_bank_base = 0, shadow_slot_limit = 0;
+ const u8 *guest_param_pg;
u32 want_qmd = 0, want_ch = 0;
bool qmd_rel = false, ch_rel = false;
u64 qmd_rel_hpa = 0, ch_rel_hpa = 0;
@@ -16540,12 +16968,14 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
static u32 bounce_csum_in[GSP_PHYS_RMCALL_GR_MAIL_SLOT];
sked_tick = 0;
sked_first = true;
+ guest_param_pg = NULL;
u32 qmd_win_pde = 0, qmd_win_installed = 0;
int qmd_win_ret = 0;
bool qmd_win_diag = false;
bool qmdwin_on = false;
char qmdwin_note[32] = "";
bool rearmed = false;
+ u32 attempts = 0;
int ret = 0;
if (out)
@@ -16588,8 +17018,13 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
* the program page (silent corruption; no launch can survive it).
* The win0 PTEs are planted once at stage 3; reusing the same VAs
* with fresh content is the proven pattern.
+ *
+ * m151: the cursor now selects one of two 8-page banks, alternating
+ * with the submission epoch (set below). The engine's index/data
+ * fetches of the previous launch's QMD shadow pages can still be in
+ * flight when this launch runs, so the bank alternation protects
+ * them from this launch's copy-in (late-fetch safety).
*/
- boot_stage.rmcall_gr_shadow_win0_used = 0;
mutex_lock(&gsp_phys_rm_lock);
if (!gsp_phys_backend_gr_ready()) {
mutex_unlock(&gsp_phys_rm_lock);
@@ -16600,6 +17035,11 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
epoch = ++boot_stage.rmcall_gr_submit_epoch;
if (!epoch)
epoch = ++boot_stage.rmcall_gr_submit_epoch;
+ boot_stage.rmcall_gr_shadow_win0_used =
+ (epoch & 1u) * GSP_PHYS_RMCALL_GR_SHADOW0_BANK_SLOTS;
+ win0_bank_base = boot_stage.rmcall_gr_shadow_win0_used;
+ shadow_slot_limit = win0_bank_base +
+ GSP_PHYS_RMCALL_GR_SHADOW0_BANK_SLOTS;
memcpy(qmd, req->qmd, sizeof(qmd));
/*
* Two-step PCAS (gsp_phys_rmcall_gr_pcas_two_step): set the QMD's
@@ -16628,7 +17068,7 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
ret = gsp_phys_rmcall_gr_map_shadow_locked(n_shadow,
req->rel_hpa,
- &sh_va);
+ &sh_va, NULL);
if (!ret) {
n_shadow++;
qmd_rel_hpa = req->rel_hpa;
@@ -16719,6 +17159,8 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
for (k = 0; k < n_sh_rec; k++) {
u64 va = 0;
+ if (sh_rec[k].line == 0xffffffffu)
+ continue; /* cached line: never extended */
if (pg_va != sh_rec[k].guest_va +
(u64)sh_rec[k].npte *
GSP_PHYS_PAGE_SIZE)
@@ -16745,11 +17187,11 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
*/
if (!mapped && pg->hpa &&
pg->qmd_word == GSP_PHYS_GR_QMD_WORD_NONE) {
- u32 sh_line = boot_stage.rmcall_guest_pt_next_line;
+ u32 sh_line = 0xffffffffu;
if (n_shadow < GSP_PHYS_RMCALL_GR_SHADOW_PAGES &&
!gsp_phys_rmcall_gr_map_shadow_locked(
- n_shadow, pg->hpa, &sh_va)) {
+ n_shadow, pg->hpa, &sh_va, &sh_line)) {
u32 k, slot = n_shadow;
mapped = true;
@@ -16975,7 +17417,7 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
gsp_phys_rmcall_gr_shadow_win0_value() &&
boot_stage.rmcall_gr_shadow_win0.pages &&
boot_stage.rmcall_gr_shadow_win0_used <
- GSP_PHYS_RMCALL_GR_SHADOW0_SLOTS;
+ shadow_slot_limit;
/*
* m86: SASS override. When gsp-phys-gr-override
@@ -17036,7 +17478,7 @@ int gsp_phys_backend_guest_gr_submit(const struct gsp_phys_gr_submit *req,
n_shadow,
(u64)gsp_phys_chunk_page_dma(sh,
n_shadow),
- &sh_va);
+ &sh_va, NULL);
}
if (!ret) {
bool keep_guest =
@@ -17197,7 +17639,7 @@ gr_page_unmapped:
if (n_shadow < GSP_PHYS_RMCALL_GR_SHADOW_PAGES &&
!gsp_phys_rmcall_gr_map_shadow_locked(
- n_shadow, req->qmd_hpa, &sh_va)) {
+ n_shadow, req->qmd_hpa, &sh_va, NULL)) {
qmd_dst_mem = (u8 *)gsp_phys_chunk_page_va(
&boot_stage.rmcall_gr_shadow,
n_shadow) + qmd_off;
@@ -17240,7 +17682,8 @@ gr_page_unmapped:
u64 sh_va = 0;
ret = gsp_phys_rmcall_gr_map_shadow_locked(n_shadow,
- req->sem_hpa, &sh_va);
+ req->sem_hpa, &sh_va,
+ NULL);
if (!ret) {
n_shadow++;
ch_rel_hpa = req->sem_hpa;
@@ -17335,29 +17778,19 @@ gr_page_unmapped:
if (req->n_data && boot_stage.rmcall_gr_bounce.pages) {
u32 dcopied = 0, dfailed = 0, di;
+ gsp_phys_gr_fill_bounce(req, bounce_csum_in);
for (di = 0; di < req->n_data; di++) {
u32 bi = (u32)((req->data[di].reloc_va -
GSP_PHYS_RMCALL_GR_BOUNCE_VA0) >>
12);
- const u8 *src;
- u8 *dst;
- u32 ci = 0, csum = 0;
if (bi >= GSP_PHYS_RMCALL_GR_MAIL_SLOT ||
- !req->data[di].hpa) {
+ !req->data[di].hpa)
dfailed++;
- continue;
- }
- src = (const u8 *)__va(req->data[di].hpa);
- dst = gsp_phys_chunk_page_va(
- &boot_stage.rmcall_gr_bounce, bi);
- memcpy(dst, src, GSP_PHYS_PAGE_SIZE);
- for (ci = 0; ci < GSP_PHYS_PAGE_SIZE; ci += 4)
- csum += get_unaligned_le32(dst + ci);
- bounce_csum_in[bi] = csum;
- dcopied++;
+ else if (gsp_phys_chunk_page_va(
+ &boot_stage.rmcall_gr_bounce, bi))
+ dcopied++;
}
- dma_wmb();
pr_info("vgsp-gr: bounce copy-in: %u pages, %u skipped (pre-publish)\n",
dcopied, dfailed);
for (di = 0; di < req->n_data && di < 3; di++)
@@ -17445,24 +17878,37 @@ gr_page_unmapped:
gsp_phys_rmcall_gr_gpc_mmu_init(bar0);
/*
* m143 DIAGNOSTIC (second-client launch-state divergence): dump the
- * exact param page the FE/SM will read (win0 slot 0, the QMD bank-0
- * target) and the guest param page at the three JIT parameter slots,
- * right before the doorbell. The JIT reads a@0x160/b@0x168/c@0x170
- * and n@0x178 (verified with ptxas -arch=sm_75). If the win0 copy is
- * correct here but the engine still stores to the wrong relocated
- * window, the divergence is engine-side (a stale mapping), not the
- * relaunched page content.
+ * exact param page the FE/SM will read (this launch's win0 bank base
+ * slot, the QMD bank-0 target) and the guest param page at the three
+ * JIT parameter slots, right before the doorbell. The JIT reads
+ * a@0x160/b@0x168/c@0x170 and n@0x178 (verified with ptxas
+ * -arch=sm_75). If the win0 copy is correct here but the engine still
+ * stores to the wrong relocated window, the divergence is engine-side
+ * (a stale mapping), not the relaunched page content.
*/
if (boot_stage.rmcall_gr_shadow_win0.pages) {
const u8 *wp = gsp_phys_chunk_page_va(
- &boot_stage.rmcall_gr_shadow_win0, 0);
+ &boot_stage.rmcall_gr_shadow_win0, win0_bank_base);
- if (wp)
- pr_info("vgsp-gr: DIAG win0[0] 160=%llx 168=%llx 170=%llx 178=%llx\n",
+ if (wp) {
+ u32 off;
+
+ pr_info("vgsp-gr: DIAG win0[%u] 160=%llx 168=%llx 170=%llx 178=%llx\n",
+ win0_bank_base,
(u64)get_unaligned_le64(wp + 0x160),
(u64)get_unaligned_le64(wp + 0x168),
(u64)get_unaligned_le64(wp + 0x170),
(u64)get_unaligned_le64(wp + 0x178));
+ pr_info("vgsp-gr: win0 param hash pre=0x%016llx\n",
+ gsp_phys_gr_fnv1a64(wp, GSP_PHYS_PAGE_SIZE));
+ for (off = 0x100u; off < 0x200u; off += 0x20u)
+ pr_info("vgsp-gr: win0 page +0x%03x: %016llx %016llx %016llx %016llx\n",
+ off,
+ (u64)get_unaligned_le64(wp + off),
+ (u64)get_unaligned_le64(wp + off + 8),
+ (u64)get_unaligned_le64(wp + off + 16),
+ (u64)get_unaligned_le64(wp + off + 24));
+ }
}
{
u32 pi;
@@ -17474,12 +17920,15 @@ gr_page_unmapped:
!req->pages[pi].hpa)
continue;
gp = (const u8 *)__va(req->pages[pi].hpa);
+ guest_param_pg = gp;
pr_info("vgsp-gr: DIAG guest-param pa=0x%llx 160=%llx 168=%llx 170=%llx 178=%llx\n",
req->pages[pi].hpa,
(u64)get_unaligned_le64(gp + 0x160),
(u64)get_unaligned_le64(gp + 0x168),
(u64)get_unaligned_le64(gp + 0x170),
(u64)get_unaligned_le64(gp + 0x178));
+ pr_info("vgsp-gr: guest-param hash pre=0x%016llx\n",
+ gsp_phys_gr_fnv1a64(gp, GSP_PHYS_PAGE_SIZE));
break;
}
}
@@ -17491,9 +17940,10 @@ gr_page_unmapped:
boot_stage.rmcall_token,
qmd_rel, want_ch, GSP_PHYS_RMCALL_GR_POLL_MS, qmdwin_note);
if (qmd_win_diag)
- pr_info("vgsp-gr: qmd dest va=0x%llx pdeIndex=%u installed=%u win_mask=0x%x ensure_ret=%d\n",
+ pr_info("vgsp-gr: qmd dest va=0x%llx pdeIndex=%u installed=%u win_mask=0x%llx ensure_ret=%d\n",
qmd_dst, qmd_win_pde, qmd_win_installed,
- boot_stage.rmcall_guest_pt_win_mask, qmd_win_ret);
+ (unsigned long long)boot_stage.rmcall_guest_pt_win_mask,
+ qmd_win_ret);
deadline = jiffies + msecs_to_jiffies(GSP_PHYS_RMCALL_GR_POLL_MS);
for (;;) {
@@ -17581,6 +18031,45 @@ gr_page_unmapped:
pr_info("vgsp-gr: rearm after timeout failed rc=%d\n",
rr);
}
+ /*
+ * The re-arm above only replays the schedule verbs; if the
+ * entry was already fetched or the front-end never latched
+ * it, that cannot make the QMD run. Re-publish a FRESH ring
+ * entry (same PB content and QMD) with a re-issued schedule
+ * + doorbell, bounded. The timeout path never copied the
+ * result out to the guest, so a timed-out attempt did not
+ * reach the client; the guest therefore still sees only the
+ * successful attempt.
+ */
+ if (attempts < GSP_PHYS_RMCALL_GR_ATTEMPTS) {
+ int pr_ret;
+
+ attempts++;
+ mutex_lock(&gsp_phys_rm_lock);
+ pr_ret = gsp_phys_rmcall_publish_locked(
+ bar0, boot_stage.rmcall_va_pb, words,
+ &sub);
+ if (!pr_ret)
+ pr_ret = gsp_phys_rmcall_gr_resched_locked(
+ bar0);
+ mutex_unlock(&gsp_phys_rm_lock);
+ if (!pr_ret) {
+ gsp_phys_gr_retries++;
+ pr_info("vgsp-gr: retry %u/%u: fresh ring entry slot=%u published + rescheduled; re-polling %u ms\n",
+ attempts,
+ GSP_PHYS_RMCALL_GR_ATTEMPTS,
+ sub.slot,
+ GSP_PHYS_RMCALL_GR_RETRY_MS);
+ deadline = jiffies +
+ msecs_to_jiffies(
+ GSP_PHYS_RMCALL_GR_RETRY_MS);
+ rearmed = false;
+ grpoll_n = 0;
+ continue;
+ }
+ pr_info("vgsp-gr: retry %u publish failed rc=%d\n",
+ attempts, pr_ret);
+ }
if (gsp_phys_gr_fault_active()) {
struct gsp_phys_fault_info fi;
u32 fep = 0;
@@ -17871,6 +18360,99 @@ gr_page_unmapped:
gsp_phys_gr_ok++;
/*
+ * m151: retire the output footprint before the copy-out (and before
+ * any later launch's copy-in). qmd_rel above is a schedule/fetch ack,
+ * not a data-path retirement, so the engine's stores into the bounce
+ * pages can land after it and be lost by an early copy-out; the next
+ * launch's copy-in would also race that write tail. Success path
+ * only - the timeout/fault paths never copied out. See
+ * gsp_phys_gr_output_retire_wait().
+ *
+ * m153: outcome-verifying retry. The intermittent per-boot
+ * no-execution class (qmd_rel/ch_rel satisfied but the engine wrote
+ * nothing at all) cannot be caught by the completion poll; the
+ * retirement wait is the only execution witness. On zero writes,
+ * refill the bounce window from the guest inputs, re-publish the
+ * same method block on a fresh ring entry, re-issue the schedule and
+ * re-poll, then re-wait - bounded by the same attempt budget as the
+ * timeout path. After the budget the launch proceeds to copy-out
+ * with whatever the engine produced: a genuinely read-only kernel is
+ * indistinguishable from a no-execution here and simply pays the
+ * bounded extra polls (safe to re-execute by construction - a kernel
+ * that writes nothing has no side effects to duplicate). A
+ * wrong-place write detector was tried and removed: the highest-reloc
+ * data entry is often a UVM-internal buffer, not the output, so it
+ * false-fired on correct launches and would re-execute
+ * side-effecting kernels (atomics) up to 3x.
+ */
+ if (req->n_data && boot_stage.rmcall_gr_bounce.pages) {
+ u32 wrote = gsp_phys_gr_output_retire_wait(req, bounce_csum_in);
+
+ if (gsp_phys_rmcall_gr_nowrite_retry_value()) {
+ while (attempts < GSP_PHYS_RMCALL_GR_ATTEMPTS) {
+ if (wrote > 0)
+ break;
+ attempts++;
+ if (attempts > GSP_PHYS_RMCALL_GR_ATTEMPTS)
+ break;
+ pr_info("vgsp-gr: outcome retry %u/%u (wrote=%u): refill + re-publish\n",
+ attempts, GSP_PHYS_RMCALL_GR_ATTEMPTS,
+ wrote);
+ gsp_phys_gr_fill_bounce(req, bounce_csum_in);
+ mutex_lock(&gsp_phys_rm_lock);
+ ret = gsp_phys_rmcall_publish_locked(
+ bar0, boot_stage.rmcall_va_pb,
+ words, &sub);
+ if (!ret)
+ ret = gsp_phys_rmcall_gr_resched_locked(
+ bar0);
+ mutex_unlock(&gsp_phys_rm_lock);
+ if (ret) {
+ pr_info("vgsp-gr: outcome retry %u publish/resched failed rc=%d (accepting current outcome)\n",
+ attempts, ret);
+ ret = 0;
+ break;
+ }
+ {
+ unsigned long dl = jiffies +
+ msecs_to_jiffies(
+ GSP_PHYS_RMCALL_GR_RETRY_MS);
+ u32 rw0 = 0, rw1 = 0;
+
+ for (;;) {
+ if (qmd_rel_hpa)
+ rw0 = READ_ONCE(
+ *(u32 *)((u8 *)__va(
+ qmd_rel_hpa) +
+ qmd_rel_off));
+ else if (qmd_rel)
+ rw0 = READ_ONCE(
+ *(u32 *)sem_va);
+ if (ch_rel_hpa)
+ rw1 = READ_ONCE(
+ *(u32 *)((u8 *)__va(
+ ch_rel_hpa) +
+ ch_rel_off));
+ else
+ rw1 = READ_ONCE(
+ *(u32 *)(sem_va + 4));
+ if ((!qmd_rel ||
+ rw0 == want_qmd) &&
+ rw1 == want_ch)
+ break;
+ if (time_after_eq(jiffies, dl))
+ break;
+ msleep(GSP_PHYS_RMCALL_POLL_MS);
+ }
+ }
+ wrote = gsp_phys_gr_output_retire_wait(
+ req, bounce_csum_in);
+ pr_info("vgsp-gr: outcome retry %u done: engine wrote %u page(s)\n",
+ attempts, wrote);
+ }
+ }
+ }
+ /*
* m114: the kernel ran against the pre-commit bounce window; copy the
* results back to the guest's real pages (identity HPAs in the
* request). All mapped pages are written back; inputs are unchanged
@@ -17962,6 +18544,23 @@ gr_page_unmapped:
ioread32(bar0 + 0x00407060u),
ioread32(bar0 + 0x00407064u),
ioread32(bar0 + 0x00407068u));
+ /* m151: post-poll page hashes; compare with the pre-doorbell ones. */
+ {
+ const u8 *wp = boot_stage.rmcall_gr_shadow_win0.pages ?
+ gsp_phys_chunk_page_va(
+ &boot_stage.rmcall_gr_shadow_win0,
+ win0_bank_base) : NULL;
+
+ if (wp || guest_param_pg)
+ pr_info("vgsp-gr: win0 param hash post=0x%016llx guest-param hash post=0x%016llx\n",
+ wp ? gsp_phys_gr_fnv1a64(wp,
+ GSP_PHYS_PAGE_SIZE) :
+ 0ULL,
+ guest_param_pg ?
+ gsp_phys_gr_fnv1a64(guest_param_pg,
+ GSP_PHYS_PAGE_SIZE) :
+ 0ULL);
+ }
pr_info("vgsp-gr: GUEST GR LAUNCH OK h=0x%x push=0x%llx qmd=0x%llx prog/pages=%u qmd_rel=0x%08x ch_rel=0x%08x elapsed_ms=%u ok=%llu\n",
req->h_object, req->push_va, req->qmd_va, req->n_pages, w0, w1,
jiffies_to_msecs(jiffies - start), gsp_phys_gr_ok);
@@ -18992,8 +19591,7 @@ static int gsp_phys_state_show(struct seq_file *m, void *v)
fwd_cand_none, fwd_cand_sys, fwd_cand_mixed,
fwd_cand_vid, fwd_resolve_fail,
boot_stage.rmcall_guest_pt_next_line,
- GSP_PHYS_RMCALL_GUEST_PT_PAGES *
- GSP_PHYS_RMCALL_GUEST_PT_LINES_PER_PAGE);
+ GSP_PHYS_RMCALL_GUEST_PT_TOTAL_LINES);
}
{
/*
@@ -20625,8 +21223,8 @@ static_assert(offsetof(struct gsp_phys_rmcall_channel_params, process_id) == 296
"NV_CHANNEL_ALLOC_PARAMS.ProcessID must be at offset 296");
static_assert(offsetof(struct gsp_phys_rmcall_channel_params, tpc_config_id) == 360,
"NV_CHANNEL_ALLOC_PARAMS.tpcConfigID must be at offset 360");
-static_assert(GSP_PHYS_RMCALL_BUF_COUNT == 33,
- "the rm-call ladder registers exactly thirty-three DMA buffers (14 ladder + 16 single-page guest scratch PTs + 3 golden9g VAS levels)");
+static_assert(GSP_PHYS_RMCALL_BUF_COUNT == 81,
+ "the rm-call ladder registers exactly eighty-one DMA buffers (14 ladder + 16 single-page guest scratch PTs + 48 single-page extension scratch PTs + 3 golden9g VAS levels)");
static_assert((GSP_PHYS_RMCALL_HANDLE_MEM_BASE +
(GSP_PHYS_RMCALL_BUF_COUNT - 1) *
GSP_PHYS_RMCALL_HANDLE_MEM_STRIDE) < 0xcaf80000u,
diff --git a/kernel/gsp_phys_gpu.c b/kernel/gsp_phys_gpu.c
index c5419a5..9b7235b 100644
--- a/kernel/gsp_phys_gpu.c
+++ b/kernel/gsp_phys_gpu.c
@@ -949,6 +949,31 @@ int gsp_phys_rmcall_gr_skedrec_value(void)
EXPORT_SYMBOL_GPL(gsp_phys_rmcall_gr_skedrec_value);
/*
+ * m153 no-write/wrong-place launch retry (gsp_phys_rmcall_gr_nowrite_retry;
+ * default 1). After a forwarded launch reports completion, the output
+ * retirement wait measures how many resolved data pages the engine actually
+ * changed. Two mis-execution signatures are known to be intermittent per
+ * boot: the engine writes nothing at all, or it writes into a lower operand
+ * window while the highest (output) operand stays untouched (a stale
+ * constant-source state). Both re-execute correctly on a fresh ring entry
+ * often enough to be worth a bounded retry: refill the bounce window from
+ * the guest inputs, re-publish the same method block, re-issue the schedule
+ * and re-poll, then re-wait. Set to 0 to disable the retry and accept the
+ * first outcome unconditionally.
+ */
+static int gsp_phys_rmcall_gr_nowrite_retry_param = 1;
+module_param_named(gsp_phys_rmcall_gr_nowrite_retry,
+ gsp_phys_rmcall_gr_nowrite_retry_param, int, 0644);
+MODULE_PARM_DESC(gsp_phys_rmcall_gr_nowrite_retry,
+ "Re-execute a forwarded GR launch whose retirement wait saw zero/wrong-place engine writes (default 1)");
+
+int gsp_phys_rmcall_gr_nowrite_retry_value(void)
+{
+ return gsp_phys_rmcall_gr_nowrite_retry_param;
+}
+EXPORT_SYMBOL_GPL(gsp_phys_rmcall_gr_nowrite_retry_value);
+
+/*
* m69 window-0 shadow pool (gsp_phys_rmcall_gr_shadow_win0; default 0). When
* set, the forwarded GR launch copies the QMD's program/param pages into the
* window-0 shadow pool (module pages at VAs 0x108000..0x10f000, PTEs planted
diff --git a/kernel/gsp_phys_gpu.h b/kernel/gsp_phys_gpu.h
index f93603c..73b0822 100644
--- a/kernel/gsp_phys_gpu.h
+++ b/kernel/gsp_phys_gpu.h
@@ -365,6 +365,10 @@ unsigned int gsp_phys_rmcall_gr_lmem_tpc_size_value(void);
/* m72: log SKED CWD/HWW register transitions during the launch poll. */
int gsp_phys_rmcall_gr_skedrec_value(void);
+/* m153: re-execute a forwarded launch whose retirement wait saw zero or
+ * wrong-place engine writes (default 1 = enabled). */
+int gsp_phys_rmcall_gr_nowrite_retry_value(void);
+
/* m73: use host-visible sysmem chunks for V7 instance/RAMFC (m62 shape). */
int gsp_phys_rmcall_gr_inst_sys_value(void);
diff --git a/kernel/gsp_sema_service.c b/kernel/gsp_sema_service.c
index 9ab609a..f8925df 100644
--- a/kernel/gsp_sema_service.c
+++ b/kernel/gsp_sema_service.c
@@ -454,6 +454,45 @@ static u8 svc_sess_flush;
static u8 svc_sess_active; /* a channel of a session has registered (until flush) */
static struct completion svc_sess_flush_done; /* a pending flush has executed */
static unsigned long svc_ring_done; /* CE ranks whose ring is pinned (session state) */
+
+/*
+ * CeUtils scrubber channel (the guest PMA's internal CE channel created at
+ * driver init). When the PMA must hand out scrubbed pages it waits on the
+ * channel's finish payload (channel buffer offset finishPayloadOffset =
+ * channelPbSize + GPFIFO_SIZE + 4; for the regular scrubber = 0x6c004),
+ * which the physical CE would advance as the scrub work completes. In this
+ * emulation nothing authors it, so the third+ CUDA session's cudaMalloc
+ * freezes in scrubCheckAndWaitForSize.
+ *
+ * The scrubber channel is identified by its GPFIFO entry count
+ * (NUM_COPY_BLOCKS = 4096; client channels use 1024); its USERD PA is known
+ * from the channel ALLOC registration. The channel buffer's PA cannot be
+ * resolved through the global VA map (the kernel VA space collides
+ * numerically with the client's CE push-buffer window), so the guest
+ * scanner (vmallocinfo + kcore page-table walk) registers it through
+ * sema_scrubbuf_add. The service then authors, from the channel's own
+ * GP_PUT: USERD GP_GET = put, host semaphore = put (so the guest's
+ * channelWaitForFreeEntry never blocks), and finish payload = submitted
+ * block count (so the scrub wait completes). Idempotent, forward-only.
+ */
+struct svc_scrub {
+ bool userd_ok; /* a 4096-entry channel was registered */
+ u32 h_object; /* handle of the registered scrubber channel */
+ u64 userd_pa;
+ u32 userd_as;
+ u64 gpfifo_va;
+ u32 entries;
+ bool pa_ok; /* scanner registered fin/hsem PAs */
+ u64 fin_pa; /* finish payload word (u32) */
+ u64 hsem_pa; /* channel host semaphore word (u32) */
+ bool seeded;
+ u32 put_last;
+ u64 submitted; /* blocks submitted since seeding */
+ u64 authored; /* last authored finish value */
+ u32 pa_gen; /* scanner registration generation (logged) */
+ u32 log_budget;
+};
+static struct svc_scrub svc_scrub;
static u8 svc_pb_passes;
static u64 svc_pb_last;
/*
@@ -602,6 +641,14 @@ struct sema_svc {
u64 grce_log; /* GRCE inline-write budget */
u32 cur_ch; /* channel of the op being run */
u64 entry_log; /* per-push summary budget */
+ /*
+ * m151: bounded log for CE pushes that were READABLE (nonzero first
+ * bytes) yet decoded no release. These are the entries the pump holds
+ * for the 8 s grace; naming their decode state (have_sem, rel) and
+ * head bytes is the Class-2 diagnostic (r128: correct content, right
+ * sem block at +0xC0 with RELEASE_ONE_WORD, yet rel_seen false).
+ */
+ u64 entry_norel_log;
u64 ops_denied; /* work refused: implausible stream */
u64 false_trans; /* resolvable VA mapped to non-push */
u64 sem_write_reresolve; /* writes re-resolved after read-back */
@@ -942,6 +989,13 @@ static bool svc_gr_content_rearm;
static u32 svc_cepin_log;
/*
+ * m154: injected pins dropped because they collide with a live channel's
+ * GPFIFO ring range (see svc_handle_map_add). Counted + logged so the
+ * defensive drop is visible in the boot log.
+ */
+static u32 svc_ring_pin_skips;
+
+/*
* m148 (second-client wedge): re-arm the per-rank one-shot ring/push-buffer
* latches for a (re-)registered channel. The guest driver rebuilds the GSP
* session for every client context and RM reuses the same object handles and
@@ -987,6 +1041,7 @@ void gsp_sema_service_channel_add(const struct sema_channel_desc *desc)
if (!desc->engine_type) {
WRITE_ONCE(svc_gr_content_rearm, true);
svc_cepin_log = 0;
+ svc_ring_pin_skips = 0;
}
/*
@@ -1102,6 +1157,37 @@ void gsp_sema_service_channel_add(const struct sema_channel_desc *desc)
*/
svc_chan_rearm_rank(ch);
svc.last_chan_reg = jiffies;
+ /*
+ * CeUtils scrubber channel: NUM_COPY_BLOCKS = 4096 GPFIFO
+ * entries (client channels use 1024). Stash its USERD and ring
+ * geometry for the scrub pass; the channel itself is left
+ * entirely alone (no rank change, no pump touch changes) - the
+ * pass authors the finish payload from the USERD alone. A
+ * colliding numeric-handle FREE may still unregister the
+ * channel; the stashed geometry survives, so the scrub path
+ * does not depend on the channel-table identity at all.
+ */
+ if (desc->gpfifo_entries >= 4096 && desc->gpfifo_va &&
+ desc->userd_pa) {
+ /* Re-seed the counting on a fresh scrubber object. */
+ if (!svc_scrub.userd_ok ||
+ svc_scrub.h_object != desc->h_object ||
+ svc_scrub.gpfifo_va != desc->gpfifo_va) {
+ svc_scrub.seeded = false;
+ svc_scrub.submitted = 0;
+ }
+ svc_scrub.userd_ok = true;
+ svc_scrub.h_object = desc->h_object;
+ svc_scrub.userd_pa = desc->userd_pa;
+ svc_scrub.userd_as = desc->userd_as ? desc->userd_as
+ : SVC_APERTURE_SYS;
+ svc_scrub.gpfifo_va = desc->gpfifo_va;
+ svc_scrub.entries = desc->gpfifo_entries;
+ pr_info("sema-svc: scrubber channel 0x%x detected (gpfifo=0x%llx entries=%u userd=0x%llx as=%u)\n",
+ desc->h_object, desc->gpfifo_va,
+ desc->gpfifo_entries, desc->userd_pa,
+ svc_scrub.userd_as);
+ }
pr_info("sema-svc: channel 0x%x registered gpfifo=0x%llx entries=%u userd=0x%llx eng=%u cid=%u notif=0x%llx\n",
desc->h_object, desc->gpfifo_va, desc->gpfifo_entries,
desc->userd_pa, desc->engine_type, desc->cid,
@@ -6936,6 +7022,36 @@ static void svc_decode_stream(struct nv_vgpu_device *vgpu, const u8 *buf,
tlen ? tail : NULL);
}
}
+
+ /*
+ * m151 Class-2 diagnostic: a CE push with real content that decoded no
+ * release is the one that pays the 8 s grace. Log its decode state
+ * (bounded, content-gated so all-zero reads do not wash it out).
+ */
+ if (gsp_sema_dbg && ch->engine_type && ch->h_object && !st.have_rel) {
+ u8 head[64];
+ u32 hlen = len < sizeof(head) ? len : (u32)sizeof(head);
+ bool nz = false;
+ u32 i;
+
+ for (i = 0; i + 4 <= hlen; i += 4) {
+ u32 w;
+
+ memcpy(&w, buf + i, 4);
+ if (w) {
+ nz = true;
+ break;
+ }
+ }
+ if (nz && svc.entry_norel_log < 2048) {
+ svc.entry_norel_log++;
+ memcpy(head, buf, hlen);
+ pr_info("sema-norel: h=0x%x eng=%u va=0x%llx len=%u ops=%u have_sem=%u rel=0x%llx pay=%u head=%*phN\n",
+ ch->h_object, ch->engine_type, push_va, len,
+ ops_runs, st.have_sem, st.rel_va,
+ st.rel_payload, hlen, head);
+ }
+ }
}
/* ------------------------------------------------------------------ */
@@ -7942,6 +8058,8 @@ static bool svc_push_prefix_ok(const u8 *buf, size_t len)
/* Fallback-line budget for the whole boot. */
static u32 svc_pushpg_fb_log;
+/* Bounded split-read rejection lines (cross-page CE push, r129 class). */
+static u32 svc_pushpg_split_log;
/*
* Read a push the way the engine does: every 4 KB page of the range is
@@ -7961,11 +8079,16 @@ static u32 svc_pushpg_fb_log;
* method stream is rejected and the remaining candidates are tried
* (observed: a wrong injected pin for the push page made every push
* decode no release, starving the client's tracker);
- * 2. validated self-pin (svc_pin_lookup_valid): read-back-validated pages
+ * 2. the displaced injected pair (svc_ipin_alt_lookup): what the page
+ * resolved to before a later injection overwrote the primary slot -
+ * the fall-back when a foreign/stale injection shadowed a good page
+ * (observed r129: a continuation page of a cross-page CE push under
+ * an all-FF pin);
+ * 3. validated self-pin (svc_pin_lookup_valid): read-back-validated pages
* (content rescue, first-push pairing), window guesses excluded;
- * 3. the channel's linear window mapping (svc_pb_win_lookup): last resort
+ * 4. the channel's linear window mapping (svc_pb_win_lookup): last resort
* for a page no validated source knows;
- * 4. outside any window: the remaining trusted sources
+ * 5. outside any window: the remaining trusted sources
* (svc_lookup_trusted), exactly the old behaviour for GR/other pushes.
*
* For a contiguous allocation every path resolves to the same PA the old
@@ -7993,8 +8116,9 @@ static int svc_read_va_push_ex(struct nv_vgpu_device *vgpu,
have_win = !svc_pb_win_lookup(ch, page_va, &win_pa);
- for (src = 0; src < 4 && !done; src++) {
+ for (src = 0; src < 5 && !done; src++) {
bool via_ipin = false;
+ bool untrusted = false;
pa = 0;
ap = SVC_APERTURE_SYS;
@@ -8004,22 +8128,39 @@ static int svc_read_va_push_ex(struct nv_vgpu_device *vgpu,
svc_ipin_lookup(va, &pa, &ap))
continue;
via_ipin = true;
+ untrusted = true;
if (have_win)
svc_pushpg_note(ch, page_va, pa,
win_pa);
break;
case 1:
+ /*
+ * Displaced injected pin (svc_ipin_alt):
+ * the mapping this page had before a later
+ * injection overwrote it. The push decode
+ * is the consumer here, so a pin that no
+ * longer reads like the page's validated
+ * content (all-FF continuation chunks,
+ * see below) can fall back to the pair the
+ * current injection displaced.
+ */
+ if (svc_ipin_alt_lookup(va, &pa, &ap))
+ continue;
+ untrusted = true;
+ break;
+ case 2:
if (svc_pin_lookup_valid(va, &pa, &ap))
continue;
if (have_win)
svc_pushpg_note(ch, page_va, pa,
win_pa);
break;
- case 2:
+ case 3:
if (!have_win)
continue;
pa = win_pa + (va & (SEMA_PAGE_SIZE - 1));
ap = SVC_APERTURE_SYS;
+ untrusted = true;
break;
default:
/* learned cache / discovered map / page
@@ -8045,18 +8186,45 @@ static int svc_read_va_push_ex(struct nv_vgpu_device *vgpu,
!svc_push_prefix_ok(p, chunk))
continue; /* content says: wrong pin */
/*
+ * A continuation chunk (not the push start) read from
+ * an unvalidated source can be a stale/foreign pin:
+ * observed r129, a 216-byte CE push whose last 24
+ * bytes (the SET_SEMAPHORE_A + LAUNCH_DMA) fell in a
+ * page whose injected pin resolved to an all-0xFF
+ * page while the first page was correct. A full
+ * chunk of 0xFF is never a method stream - fresh
+ * emulation pages read 0xFF - so reject it and try
+ * the remaining sources (the displaced alt pair
+ * first). The bounded line names the rejected
+ * (va, pa, src) for the post-mortem.
+ */
+ if (untrusted && !first && chunk >= 64 &&
+ !memchr_inv(p, 0xff, chunk)) {
+ if (svc_pushpg_split_log < 8) {
+ svc_pushpg_split_log++;
+ pr_info("sema-pushpg: split reject va=0x%llx pa=0x%llx src=%s (all-FF) at page 0x%llx\n",
+ va, pa,
+ src == 0 ? "ipin" :
+ (src == 1 ? "ipin-alt" :
+ "win"),
+ page_va);
+ }
+ last_rc = -EIO;
+ continue;
+ }
+ /*
* ISSUE-0002 (r51): a window guess that reads ALL
* ZEROS cannot be right - the paired base page is
* real, so a zero first chunk means the linear window
* does not describe this page (r49: the second
* session's CE push pages read zeros and, because the
* read "succeeded", shadowed the per-page resolvers).
- * Fall through to src=3 instead. Non-zero window
+ * Fall through to src=4 instead. Non-zero window
* content stays trusted exactly as before (r50:
* requiring method-stream plausibility rejected valid
* non-method push prefixes and corrupted c1).
*/
- if (src == 2 && first && !memchr_inv(p, 0, chunk))
+ if (src == 3 && first && !memchr_inv(p, 0, chunk))
continue;
done = true;
}
@@ -12206,10 +12374,18 @@ static bool svc_gr_page_inline_covers(u64 page)
* accesses the tail: with 32 pages the tail read the NEXT candidate's
* relocated page (wrong values; vadd tails). 3x33=99 <= the 100-page cap.
*/
-#define SVC_GR_DATA_PAGES_PER_PTR 33u
-/* m114: total pages the pre-commit bounce window can carry (slot 100 is
- * the mail message). */
-#define SVC_GR_DATA_BOUNCE_MAX 100u
+/*
+ * m150 (large buffers): the bounce window was grown to 208 pages (mail page
+ * at slot 206). 68 pages per candidate/pointer covers a 256 KB cudaMalloc'd
+ * operand whose pointer sits 0x400 into its page (64 full + offset + tail),
+ * and up to three such operands (3x68 = 204 <= 205) fit below the mail page.
+ * Larger-than-256 KB operands still hit the cap; they need a chunked data
+ * path (future work).
+ */
+#define SVC_GR_DATA_PAGES_PER_PTR 68u
+/* Total pages the pre-commit bounce window can carry below the mail page
+ * (GSP_PHYS_RMCALL_GR_MAIL_SLOT = 206). */
+#define SVC_GR_DATA_BOUNCE_MAX 205u
static u32 svc_gr_data_log; /* m142: bounded per-page attribution log */
static u32 svc_gr_src_log; /* m150: bounded per-candidate source log */
@@ -12456,8 +12632,26 @@ static void svc_gr_data_fill(struct nv_vgpu_device *vgpu)
u32 before = svc_fwd_req.gr_n_data;
u64 pba = 0;
u32 pba_ap = 0;
+ u32 span;
const char *pba_src = "?";
+ /*
+ * Window budget discipline (large buffers): only real device
+ * operands (high, cudaMalloc-shaped VAs) get the full per-pointer
+ * span. Low-VA candidates (the launch's own QMD/release/reloc
+ * pointers) need a single page at most, and the launch's QMD VA
+ * is not a kernel operand at all - relocating 68 pages for it
+ * starved the operands that follow and blew the 205-page cap
+ * (r141: a 256 KB operand resolved 0 pages because the QMD and
+ * param candidates consumed the window). Excluding them lets
+ * three 256 KB operands (3x68 = 204) fit.
+ */
+ if ((base & ~(u64)(SEMA_PAGE_SIZE - 1)) ==
+ (svc_fwd_req.gr_qmd_dev_va & ~(u64)(SEMA_PAGE_SIZE - 1)))
+ continue;
+ span = (base >= 0x40000000000ULL) ?
+ SVC_GR_DATA_PAGES_PER_PTR : 1u;
+
if (is_arg[w] && base >= 0x40000000000ULL)
n_arg_hi++;
@@ -12472,7 +12666,7 @@ static void svc_gr_data_fill(struct nv_vgpu_device *vgpu)
svc_pd_found, svc.ipin_n);
}
- for (p = 0; p < SVC_GR_DATA_PAGES_PER_PTR; p++) {
+ for (p = 0; p < span; p++) {
u64 va = wins[w] + (u64)p * SEMA_PAGE_SIZE;
u64 pa = 0;
u32 ap = 0, x;
@@ -15218,6 +15412,41 @@ static u32 svc_map_add_head; /* producer */
static u32 svc_map_add_tail; /* consumer (service thread) */
static DEFINE_SPINLOCK(svc_map_add_lock);
+/*
+ * m154: does this injected-pin VA fall inside any live channel's GPFIFO ring
+ * range? The ring VA translation must come from the channel's own discovery
+ * path (channel object / push-buffer scan); a scanner run that happens to
+ * span the ring VA is enumerating a different mapping and its translation is
+ * always wrong for the ring (r153: an all-zero read consumed a ring entry
+ * and wedged the client for the boot). Called on the service thread; takes
+ * chan_lock like the rewind below.
+ */
+static bool svc_va_hits_chan_ring(u64 va)
+{
+ unsigned long flags;
+ u32 i;
+ bool hit = false;
+
+ spin_lock_irqsave(&svc.chan_lock, flags);
+ for (i = 0; i < SVC_MAX_CHANNELS; i++) {
+ struct svc_channel *ch = &svc.chan[i];
+ u64 lo, hi;
+
+ if (!ch->valid || !ch->gpfifo_va || !ch->gpfifo_entries)
+ continue;
+ lo = ch->gpfifo_va & ~(u64)(SEMA_PAGE_SIZE - 1);
+ hi = (ch->gpfifo_va +
+ (u64)ch->gpfifo_entries * 8 + SEMA_PAGE_SIZE - 1) &
+ ~(u64)(SEMA_PAGE_SIZE - 1);
+ if (va >= lo && va < hi) {
+ hit = true;
+ break;
+ }
+ }
+ spin_unlock_irqrestore(&svc.chan_lock, flags);
+ return hit;
+}
+
static void svc_handle_map_add(void)
{
unsigned long flags;
@@ -15242,8 +15471,33 @@ static void svc_handle_map_add(void)
ap = req.ap ? req.ap : SVC_APERTURE_SYS;
for (off = 0; off < (req.len ? req.len : 1);
off += SEMA_PAGE_SIZE) {
- svc_ipin_store(req.va + off, req.pa + off, ap);
- svc_lat_store(req.va + off, req.pa + off, ap);
+ u64 va = req.va + off;
+
+ /*
+ * m154: never let an injected pin shadow a live
+ * channel's GPFIFO ring. r153: the scanner's merged
+ * CE-window run (va=0x121000000 len=0x40000, the
+ * length varies per session) covered a CE channel's
+ * ring VA 0x121010000; the injected translation took
+ * precedence in svc_ipin_lookup/read, the ring then
+ * read the guest page at 0x102e02000 - an all-zero
+ * entry was silently consumed, the pool release was
+ * never authored, and the client spun in userspace
+ * for the rest of the boot (pushes frozen). The ring
+ * pages already have their own discovery path (the
+ * channel object / push-buffer scan); a colliding
+ * injection is always the wrong translation, so drop
+ * it and leave the ring to that path.
+ */
+ if (svc_va_hits_chan_ring(va)) {
+ svc_ring_pin_skips++;
+ if (svc_ring_pin_skips <= 8)
+ pr_info("sema-svc: injected pin va=0x%llx DROPPED (live channel ring range; ring reads stay on the channel's own translation)\n",
+ va);
+ continue;
+ }
+ svc_ipin_store(va, req.pa + off, ap);
+ svc_lat_store(va, req.pa + off, ap);
}
/*
* m149: bounded per-session evidence that the CE push-buffer
@@ -15465,6 +15719,115 @@ int gsp_sema_service_counter_add(u32 h_object, u64 pa)
return ret;
}
+/*
+ * Register the guest PMA scrubber's channel-buffer words. Called from the
+ * sema_scrubbuf_add debug parameter with the PAs the guest scanner resolved
+ * (vmallocinfo vmap area + kcore page-table walk). fin_pa is the finish
+ * payload the guest's scrub wait polls; hsem_pa is the channel's host
+ * semaphore (channelWaitForFreeEntry polls it). Plain state update.
+ */
+int gsp_sema_service_scrubbuf_add(u64 fin_pa, u64 hsem_pa)
+{
+ if (!svc.task)
+ return -ENODEV;
+ /* Both words are u32: require 4-byte alignment and a RAM-shaped PA. */
+ if (!fin_pa || (fin_pa & 0x3) || fin_pa >= SEMA_PA_MAX)
+ return -EINVAL;
+ if (!hsem_pa || (hsem_pa & 0x3) || hsem_pa >= SEMA_PA_MAX)
+ return -EINVAL;
+
+ svc_scrub.fin_pa = fin_pa;
+ svc_scrub.hsem_pa = hsem_pa;
+ svc_scrub.pa_ok = true;
+ svc_scrub.pa_gen++;
+ pr_info("sema-svc: scrubber buffer registered fin_pa=0x%llx hsem_pa=0x%llx (gen %u)\n",
+ fin_pa, hsem_pa, svc_scrub.pa_gen);
+ return 0;
+}
+
+/*
+ * Author the CeUtils scrubber channel's progress once per service pass.
+ *
+ * The physical CE would advance, per completed scrub block, the channel's
+ * finish payload (channel buffer + 0x6c004) and write the channel's host
+ * semaphore as it consumes GPFIFO entries. Nothing in this emulation does,
+ * so the guest PMA's scrub wait (scrubCheckAndWaitForSize) freezes from the
+ * third CUDA session on. This pass closes that loop from the channel's own
+ * USERD GPPut (which the guest driver writes before every submit):
+ *
+ * submitted = number of scrub blocks submitted (GPPut is end-exclusive
+ * and counts submissions until the ring wraps; track the wrap ourselves),
+ * then author USERD GP_GET = put (consumed), host sema = put and finish
+ * payload = submitted. Forward-only, idempotent, retried every pass so a
+ * late scanner registration (or a post-flush reset) heals by itself.
+ *
+ * The scrub work itself is not executed (no shadow memsets): the guest
+ * only needs the progress to proceed. Executing the memsets requires
+ * decoding the channel's pushes, which the scanner can later provide pins
+ * for; correctness impact is limited to stale data in pages the guest
+ * believes scrubbed.
+ */
+static void svc_scrub_pass(struct nv_vgpu_device *vgpu)
+{
+ struct svc_scrub *s = &svc_scrub;
+ u32 put = 0;
+ u64 delta;
+
+ if (!s->userd_ok || !s->pa_ok || !s->entries)
+ return;
+ if (svc_read_ap(vgpu, s->userd_pa + SVC_USERD_GP_PUT, s->userd_as,
+ &put, sizeof(put))) {
+ /* Retry through the alternate aperture (as registration did). */
+ if (svc_read_ap(vgpu, s->userd_pa + SVC_USERD_GP_PUT,
+ s->userd_as == SVC_APERTURE_FB ?
+ SVC_APERTURE_SYS : SVC_APERTURE_FB,
+ &put, sizeof(put)))
+ return;
+ }
+ if (put >= s->entries)
+ return; /* junk translation; retry next pass */
+
+ if (!s->seeded) {
+ s->seeded = true;
+ s->put_last = put;
+ /* GPPut = (lastSubmittedEntry + 1) % entries: the submission
+ * count until the ring wraps. Seeding from it adopts any
+ * pre-host scrub work (the boot session's scrubbers). */
+ s->submitted = put;
+ } else if (put != s->put_last) {
+ delta = (put + s->entries - s->put_last) % s->entries;
+ if (!delta)
+ delta = s->entries; /* exact wrap */
+ s->submitted += delta;
+ s->put_last = put;
+ } else if (s->submitted == s->authored) {
+ return; /* nothing new; no guest writes */
+ }
+
+ if (s->log_budget < 64) {
+ s->log_budget++;
+ pr_info("sema-svc: scrub author put=%u submitted=%llu fin_pa=0x%llx\n",
+ put, s->submitted, s->fin_pa);
+ }
+
+ /*
+ * Author (forward-only). The USERD GP_GET and host-sema writes mark
+ * the submissions consumed; the finish write releases the scrub
+ * wait. Written only when GP_PUT advanced (or on the seeding pass),
+ * so the per-pass cost is zero while the scrubber idles.
+ */
+ svc_write_ap(vgpu, s->userd_pa + SVC_USERD_GP_GET, s->userd_as,
+ &put, sizeof(put));
+ svc_write_ap(vgpu, s->hsem_pa, SVC_APERTURE_SYS, &put, sizeof(put));
+ {
+ u32 fin = (u32)s->submitted;
+
+ svc_write_ap(vgpu, s->fin_pa, SVC_APERTURE_SYS, &fin,
+ sizeof(fin));
+ }
+ s->authored = s->submitted;
+}
+
static int svc_thread(void *arg)
{
struct nv_vgpu_device *vgpu;
@@ -15510,6 +15873,7 @@ static int svc_thread(void *arg)
svc_handle_walk_probe(vgpu);
svc_handle_map_add();
svc_handle_chan_dump(vgpu);
+ svc_scrub_pass(vgpu);
if (svc_sess_flush || !svc.direct_base ||
time_after(jiffies,
diff --git a/kernel/gsp_sema_service.h b/kernel/gsp_sema_service.h
index 4b04892..6822bd5 100644
--- a/kernel/gsp_sema_service.h
+++ b/kernel/gsp_sema_service.h
@@ -36,6 +36,16 @@ void gsp_sema_service_channel_add(const struct sema_channel_desc *desc);
void gsp_sema_service_channel_del(u32 h_object);
/*
+ * Register the guest PMA scrubber's channel-buffer words (the CeUtils
+ * finish payload the guest's scrub wait polls, and the channel's host
+ * semaphore). The PAs come from the guest scanner (vmallocinfo + kcore):
+ * the guest RM kernel channel lives in a GPU VA space that collides
+ * numerically with the client's CE window in the global translation map,
+ * so it cannot be resolved through that map.
+ */
+int gsp_sema_service_scrubbuf_add(u64 fin_pa, u64 hsem_pa);
+
+/*
* A GPU VA space whose top-level page directory the driver has published (the
* driver builds its own page tables for the address spaces it owns, then hands
* the directory's physical address to the GPU through SET_PAGE_DIRECTORY).
diff --git a/kernel/main.c b/kernel/main.c
index ac970f1..d0308c4 100644
--- a/kernel/main.c
+++ b/kernel/main.c
@@ -2544,6 +2544,37 @@ module_param_cb(sema_probe, &nv_vgpu_sema_probe_ops, NULL, 0200);
MODULE_PARM_DESC(sema_probe, "Dump sema-service map + slot values for a GPU VA");
/*
+ * Register the guest PMA scrubber channel buffer words (finish payload PA,
+ * host-semaphore PA) the guest scanner resolved:
+ * echo 0x11a8ff004,0x11a8ff000 > .../parameters/sema_scrubbuf_add
+ */
+static int nv_vgpu_sema_scrubbuf_set(const char *val,
+ const struct kernel_param *kp)
+{
+ char buf[64];
+ char *comma;
+ u64 fin_pa = 0, hsem_pa = 0;
+
+ strscpy(buf, val, sizeof(buf));
+ comma = strchr(buf, ',');
+ if (!comma)
+ return -EINVAL;
+ *comma = '\0';
+ if (kstrtoull(buf, 0, &fin_pa) || kstrtoull(comma + 1, 0, &hsem_pa))
+ return -EINVAL;
+
+ return gsp_sema_service_scrubbuf_add(fin_pa, hsem_pa);
+}
+
+static const struct kernel_param_ops nv_vgpu_sema_scrubbuf_ops = {
+ .set = nv_vgpu_sema_scrubbuf_set,
+};
+
+module_param_cb(sema_scrubbuf_add, &nv_vgpu_sema_scrubbuf_ops, NULL, 0200);
+MODULE_PARM_DESC(sema_scrubbuf_add,
+ "Register the PMA scrubber finish/host-sema PAs (hex fin,hex hsem)");
+
+/*
* Debug: run the page-table walks for a GPU VA with per-level tracing,
* bypassing the pin/lat/pair map (see gsp_sema_service_walk_probe):
* echo 0x20400f000 > .../parameters/walk_probe
diff --git a/tools/cuda-ab/push_battery.sh b/tools/cuda-ab/push_battery.sh
new file mode 100755
index 0000000..644beba
--- /dev/null
+++ b/tools/cuda-ab/push_battery.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# push_battery.sh - push every CUDA battery binary from tools/cuda-ab/run/
+# to the running guest via scp over the SSH hostfwd (port 2222).
+# Usage: push_battery.sh [guest_dir] (default /tmp)
+set -u
+HERE="$(cd "$(dirname "$0")" && pwd)"
+RUN="$HERE/run"
+GUEST_DIR="${1:-/tmp}"
+CLIENTS="victory_mail smiley vecadd_loop streams2 memcpy_roundtrip multikernel victory_saxpy victory_reduce victory_matmul victory_strided victory_memset2d victory_atomics victory_streams_events victory_math vector_add"
+export SSHPASS="${VGPU_GUEST_PASS:-ubuntu}"
+FILES=""
+for c in $CLIENTS; do [ -x "$RUN/$c" ] && FILES="$FILES $RUN/$c"; done
+[ -n "$FILES" ] || { echo "no binaries in $RUN"; exit 1; }
+# shellcheck disable=SC2086
+sshpass -e scp -P "${VGPU_GUEST_PORT:-2222}" \
+ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
+ -o PubkeyAuthentication=no -o PreferredAuthentications=password \
+ -o ConnectTimeout=15 $FILES "ubuntu@127.0.0.1:$GUEST_DIR/" && \
+sshpass -e ssh -p "${VGPU_GUEST_PORT:-2222}" \
+ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
+ -o PubkeyAuthentication=no -o PreferredAuthentications=password \
+ "ubuntu@127.0.0.1" "chmod +x $GUEST_DIR/* 2>/dev/null; ls $GUEST_DIR | wc -l; echo BATTERY_PUSHED"
diff --git a/tools/guest-diag/guest_pin_stream.py b/tools/guest-diag/guest_pin_stream.py
index c0cf485..ce98d5b 100644
--- a/tools/guest-diag/guest_pin_stream.py
+++ b/tools/guest-diag/guest_pin_stream.py
@@ -266,6 +266,80 @@ def _vmalloc_pages_for(kc, va):
return 0
+def _nv_vmap_areas(kc):
+ """nv_vm_map_pages vmalloc areas: [(lo, hi, pages)] (5 s cache).
+
+ Every CE push-buffer rm_mem is a vmap created by nv_vm_map_pages; the
+ demand pinner walks these areas to find the page holding a stalled
+ push VA by content, without needing any kprobe event or object chain.
+ """
+ now = time.time()
+ if getattr(_nv_vmap_areas, "ts", 0) > now - 5.0:
+ return _nv_vmap_areas.areas
+ areas = []
+ try:
+ for line in open("/proc/vmallocinfo"):
+ if "nv_vm_map_pages" not in line:
+ continue
+ p = line.split()
+ if len(p) < 2:
+ continue
+ try:
+ lo, hi = p[0].split("-")
+ lo = int(lo, 16)
+ hi = int(hi, 16)
+ except ValueError:
+ continue
+ pages = (hi - lo) >> 12
+ if pages > 0:
+ areas.append((lo, hi, pages))
+ except OSError:
+ pass
+ _nv_vmap_areas.areas = areas
+ _nv_vmap_areas.ts = now
+ return areas
+
+
+def resolve_va_by_content(kc, va, out):
+ """Demand pin: find the page carrying GPU VA 'va' by content match.
+
+ The host reports the CE push-buffer VAs whose GPFIFO entries it cannot
+ read (dmesg 'stalled VA #N: 0x...'). The guest walks every
+ nv_vm_map_pages vmap area and tests the candidate leaf page at the VA's
+ in-window offset (plus one neighbour on either side, for the per-mem
+ header-page offset ambiguity) against the host's method-stream prefix
+ check. On a match it emits the exact pair, so the host can translate
+ and consume the entry - no kprobe event, no object-chain discovery, no
+ resolution lottery. Only the requested VA is pinned, so a wrong
+ candidate cannot poison other VAs.
+ """
+ if not (PB_CE_LO <= va < PB_CE_HI):
+ return False
+ off = (va - PB_CE_LO) & 0xFFFFF
+ k = off >> 12
+ for (lo, _hi, pages) in _nv_vmap_areas(kc):
+ for adj in (0, 1, -1):
+ idx = k + adj
+ if idx < 0 or idx >= pages:
+ continue
+ cpu = lo + idx * 0x1000
+ d = kc.read(cpu, 64)
+ if not d or not any(d[:32]):
+ continue
+ if not _push_prefix_ok(d):
+ continue
+ pa = kc.va_to_pa(cpu)
+ if pa is None:
+ continue
+ out.append("0x%x,0x%x,1\n" % (va & ~0xFFF, pa))
+ dbg("demand-va: 0x%x -> pa=0x%x (area off=%d adj=%+d)"
+ % (va & ~0xFFF, pa, idx, adj))
+ return True
+ dbg("demand-va: 0x%x not found by content (%d areas)"
+ % (va & ~0xFFF, len(_nv_vmap_areas(kc))))
+ return False
+
+
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).
@@ -400,6 +474,121 @@ def mem_page_off(kc, mem):
return 0
+def _ce_mem_pages(kc, mem):
+ """CE mem walk parameters: (gpus, cpu, off, npages) or None.
+
+ Walks the same page set resolve_mem_pins used: the whole vmalloc area
+ (minus the header offset) when known, else size at +0x128, else the
+ default. The area governs: the live pushes are written sequentially and
+ may run past the size field, so a size-derived prefix stopped one page
+ short of the write frontier (r144: last page 0x120030000 emitted, the
+ live page 0x120031000 never in any pin set). 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)
+ size_pages = max(1, min(size >> 12, 4098)) if size else 0
+ area_pages = area - off if area and area > off else 0
+ npages = min(max(size_pages, area_pages) or 64, 4098)
+ 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 merged runs.
+
+ Two emission modes share one merge step:
+
+ * full re-assert (KC_FULLREM_S cadence, and every fresh_map pass): every
+ page that translates is emitted, content-independent. This is the
+ fix for the host's session flush: at a client switch the host wipes
+ its whole sema map when the previous client's last channel closes
+ ("session flush (all channels closed)"), and a pin batch that raced
+ in for the new client is lost. The old content-gated delta never
+ re-emitted those unchanged pairs, so the new client's CE pushes were
+ permanently unreadable ("NO ENTRY" in sema-probe) and every GPFIFO
+ entry was skipped after the 8 s grace (r144/r145 second-client stall,
+ both mail and matmul). Re-asserting the full pair set every couple of
+ seconds heals any number of flush wipes; merged contiguous runs keep
+ the traffic to a few lines per pass. Zero-content pages are pinned
+ deliberately: the pin carries the translation, the host reads the
+ live content, and the client writes it later.
+
+ * delta (between full passes): content-gated, emits pages whose PA is
+ new or changed, so a remap or a freshly-faulted page is picked up
+ without waiting for the next full pass. Never drops a previously
+ emitted pair (the host's last-store-wins pin sees the truth, 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"]
+ nowm = time.monotonic()
+ full = nowm >= st.get("full_next", 0.0)
+ if full:
+ age = nowm - st.get("resolved_at", nowm)
+ st["full_next"] = nowm + (KC_FULLREM_S if age < KC_FULLREM_FAST_S
+ else KC_FULLREM_SLOW_S)
+ if fresh_map:
+ for k in range(npages):
+ kc._pa_cache.pop(cpu + (k + off) * 0x1000, None)
+ changed = []
+ if full:
+ # Re-walk pages that did not translate on an earlier pass: the
+ # vmap's leaf sizes are huge (2 MB/1 GB), so most pages translate
+ # immediately; a straggler (the vmap populated lazily) is retried
+ # with its negative PA-cache entry popped, bounded per pass so the
+ # sweep stays cheap.
+ miss = st.setdefault("miss", set())
+ retry = 0
+ for k in range(npages):
+ page = cpu + (k + off) * 0x1000
+ pa = kc.va_to_pa(page)
+ if pa is None:
+ if k in miss and retry < 512:
+ kc._pa_cache.pop(page, None)
+ retry += 1
+ else:
+ miss.add(k)
+ continue
+ miss.discard(k)
+ for gv in gpus:
+ changed.append((gv + k * 0x1000, pa))
+ else:
+ 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)
+ if full:
+ st["full_total"] = st.get("full_total", 0) + 1
+ 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."""
@@ -559,8 +748,23 @@ KC_CHAIN_BACK = 0x2000
KC_CHAIN_SPAN = 0xA000
KC_CHAIN_MAX = 400
RESOLVED_KC_PB = set()
-KC_PB_STATE = {} # sem kptr -> resolved-mem fingerprint (freshness)
+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 # sweep the CE mem's live pages this often
+KC_REMAP_S = 30.0 # full re-translation sweep (catches remaps)
+# Full re-assert cadence: every pass at least this far apart re-emits the
+# whole translatable pair set regardless of the delta, healing host-side
+# session-flush wipes (see _emit_ce_mem_delta). Must stay well under the
+# host's gsp_sema_ce_grace_ms (8 s) so a wiped-and-needed page is restored
+# inside the hold window. The fast cadence applies to a mem's first
+# KC_FULLREM_FAST_S seconds (the flush race lives at the client switch);
+# after that the re-assert backs off to KC_FULLREM_SLOW_S - a fully
+# fragmented pair set (descending PAs) emits one page per line, and 4 k
+# lines every 2 s saturated a guest under multi-client load (r148: load 19,
+# three scanners from an install race, clients wedged).
+KC_FULLREM_S = float(os.environ.get("STREAM_FULLREM_S", "1.5"))
+KC_FULLREM_FAST_S = float(os.environ.get("STREAM_FULLREM_FAST_S", "45"))
+KC_FULLREM_SLOW_S = float(os.environ.get("STREAM_FULLREM_SLOW_S", "10.0"))
def _chain_mems_at(kc, w):
@@ -639,6 +843,16 @@ def _kc_pb_fresh(kc, sem):
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
@@ -669,6 +883,42 @@ 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)))
+ # 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):
@@ -697,18 +947,24 @@ def resolve_pb_chain(kc, sem, out):
if mem in RESOLVED_KC_PB:
return True
RESOLVED_KC_PB.add(mem)
- off = mem_page_off(kc, mem)
- n = resolve_mem_pins(kc, mem, out, default_pages=64,
- va_lo=PB_CE_LO, va_hi=PB_CE_HI,
- page_off=off)
- st = {"mem": mem, "gpu": gpu, "cpu": cpu,
- "page_va": cpu, "page_pa": kc.va_to_pa(cpu) or 0, "page_off": 0}
+ 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": {},
+ "resolved_at": time.monotonic()}
+ # 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 size=%d score=%d\n'
- % (time.strftime('%T'), mem, sem, n, off,
+ '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
@@ -807,6 +1063,38 @@ def resolve_channel_pins(kc, ch, out):
+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:
+ fd = os.open(TRACE, os.O_RDONLY | os.O_NONBLOCK)
+ except OSError:
+ 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.
@@ -821,8 +1109,7 @@ def seed_events_from_trace(seen_sems, seen_pbs, seen_chs):
(the mem candidates are scored and deferred).
"""
try:
- out = subprocess.run(["tail", "-c", "1048576", TRACE],
- capture_output=True, timeout=10).stdout
+ out = _read_trace_snapshot()
except Exception:
return 0
n = 0
@@ -944,6 +1231,75 @@ def new_sems(seen):
return found
+# --------------------------------------------------------------- scrubber
+# PMA scrubber channel discovery (third-client scrub freeze). The guest's
+# PMA scrubber is an internal CeUtils CE channel (4096 GPFIFO entries,
+# pbGpuVA 0x120000000, ring at pbGpuVA+0x64000, finish payload at
+# pbGpuVA+0x6c004). It lives in a kernel vmap area, not in any user pagemap,
+# and its GPU VAs collide numerically with the client CE push-buffer window
+# in the host's global translation map - so the host cannot resolve it and
+# must be told the finish/host-semaphore word PAs directly. Resolve them
+# here (vmallocinfo + manual kernel page-table walk) and emit one
+# #scrub <fin_pa>,<hsem_pa>
+# line; pin_gr_stream.py registers them through sema_scrubbuf_add.
+SCRUB_AREA_SIZES = (0x6e000, 0x6d000, 0x6c000, 0x6b000, 0x6f000,
+ 0x70000, 0x71000, 0x72000)
+_scrub_sent = {}
+
+
+def _scrub_ring_ok(kc, ring_pa):
+ """A 906F GPFIFO ring page: all-zero (fresh) or entries whose GET names a
+ push inside the channel's own 0x120000000 window (encoded as
+ push_va - 0x100000000, 4-byte aligned)."""
+ buf = kc.read(kc.page_offset_base + ring_pa, 0x1000)
+ if not buf:
+ return False
+ for off in range(0, 0x100, 8):
+ e0, _e1 = struct.unpack_from('<II', buf, off)
+ if e0 == 0:
+ continue
+ if not (0x20000000 <= e0 < 0x20080000) or (e0 & 3):
+ return False
+ return True
+
+
+def scrub_scan(kc, lines):
+ """Emit the scrubber buffer words' PAs once (re-emitted if they move)."""
+ try:
+ f = open('/proc/vmallocinfo')
+ except OSError:
+ return
+ with f:
+ for line in f:
+ if 'nvidia' not in line or 'ioremap' in line or 'vmap' not in line:
+ continue
+ parts = line.split()
+ try:
+ a, b = parts[0].split('-')
+ lo, hi = int(a, 16), int(b, 16)
+ except (ValueError, IndexError):
+ continue
+ if (hi - lo) not in SCRUB_AREA_SIZES:
+ continue
+ fin_pa = kc.va_to_pa(lo + 0x6c000)
+ ring_pa = kc.va_to_pa(lo + 0x64000)
+ if fin_pa is None or ring_pa is None:
+ continue
+ if not _scrub_ring_ok(kc, ring_pa):
+ dbg('scrub candidate 0x%x rejected (ring shape)' % lo)
+ continue
+ fin = fin_pa + 4
+ hsem = fin_pa
+ key = (fin, hsem)
+ if _scrub_sent.get('key') == key:
+ return
+ _scrub_sent['key'] = key
+ lines.append('#scrub 0x%x,0x%x\n' % (fin, hsem))
+ dbg('scrub buffer emitted lo=0x%x fin=0x%x hsem=0x%x'
+ % (lo, fin, hsem))
+ return
+
+
def nvidia_fds(pid):
"""CUDA device fds this pid holds: 'uvm', 'gpu', or None.
@@ -1175,6 +1531,8 @@ def main():
resolved_kc_pb = set()
retry_at = {}
kc_refresh_at = {}
+ kc_reemit_at = {}
+ kc_remap_at = {}
tracer = TraceReader()
kc = Kcore()
kc_ok = kc.load()
@@ -1186,6 +1544,9 @@ def main():
pass
last_pid = None
last_log = 0.0
+ scrub_next = 0.0
+ demand_next = 0.0
+ _want_state = {"mtime": None, "done": set()}
loop = 0
while True:
pid = cdt_pid()
@@ -1211,6 +1572,8 @@ def main():
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.
@@ -1262,6 +1625,45 @@ def main():
new = [(va, pa) for va, pa in cur.items() if seen.get(va) != pa]
pending_cur = cur
lines = []
+ # Demand pins (host -> this scanner): the host appends the CE
+ # push-buffer VAs of GPFIFO entries it cannot read to
+ # /tmp/guest_want_vas.txt (written via the guest agent by
+ # pin_gr_stream); resolve each by content against the
+ # nv_vm_map_pages areas and emit the pair. This is the
+ # event-independent hole filler for the stall class where the
+ # one-shot #sem kprobe event was lost and the object-chain
+ # resolver never learns the client.
+ if kc_ok and time.time() >= demand_next:
+ demand_next = time.time() + 1.0
+ try:
+ st_d = os.stat("/tmp/guest_want_vas.txt")
+ if st_d.st_mtime != _want_state.get("mtime"):
+ with open("/tmp/guest_want_vas.txt") as fw:
+ wvas = [int(t, 16) for t in fw.read().split()
+ if t.startswith("0x")]
+ _want_state["mtime"] = st_d.st_mtime
+ for wv in wvas[-64:]:
+ if wv in _want_state["done"]:
+ continue
+ got = []
+ try:
+ ok = resolve_va_by_content(kc, wv, got)
+ except Exception:
+ ok = False
+ got = []
+ if ok and got:
+ lines.extend(got)
+ _want_state["done"].add(wv)
+ except OSError:
+ pass
+ # PMA scrubber buffer discovery (throttled): resolve the finish/host-
+ # semaphore word PAs and stream them for host registration.
+ if kc_ok and time.time() >= scrub_next:
+ scrub_next = time.time() + 5.0
+ try:
+ scrub_scan(kc, lines)
+ except Exception as e:
+ dbg('scrub scan failed: %s' % e)
for ctl in tracer.drain():
parts = ctl.split()
if ctl.startswith("#sem") and len(parts) >= 2:
@@ -1302,12 +1704,68 @@ def main():
except Exception:
_fresh = True
if not _fresh:
- _st = KC_PB_STATE.pop(sem, None)
+ # 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"])
- resolved_kc_pb.discard(sem)
- retry_at.pop(("pk", sem), None)
- dbg("kc-chain stale: re-resolving sem=0x%x" % sem)
+ 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
diff --git a/tools/guest-diag/pin_gr_stream.py b/tools/guest-diag/pin_gr_stream.py
index b864ab5..e1f5195 100644
--- a/tools/guest-diag/pin_gr_stream.py
+++ b/tools/guest-diag/pin_gr_stream.py
@@ -16,6 +16,7 @@ import re
import socket
import subprocess
import sys
+import threading
import time
HERE = os.path.dirname(os.path.abspath(__file__))
@@ -62,12 +63,19 @@ def _start_cmd():
capture_output=True, text=True).stdout
prefixes = os.environ.get("CLIENT_PREFIXES", "cdt,va,victory")
vma_every = os.environ.get("STREAM_VMA_EVERY", "1")
- return ("echo '%s' | base64 -d > /tmp/guest_pin_stream.py; "
- "if [ -f /tmp/guest_pin_stream.pid ]; then "
- "p=$(cat /tmp/guest_pin_stream.pid 2>/dev/null); "
- "[ -n \"$p\" ] && grep -q guest_pin_stream "
- "/proc/$p/cmdline 2>/dev/null && sudo kill $p 2>/dev/null; "
- "fi; "
+ # 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' "
@@ -101,9 +109,70 @@ def inject(va, pa, ln):
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):
+ r = subprocess.run(["sudo", "-n", "sh", "-c", cmd],
+ capture_output=True, text=True)
+ 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+: (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))
+
+
def main():
if not install_and_start():
log("WARN: could not start guest scanner (will retry via vsock wait)")
+ 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))
@@ -158,6 +227,13 @@ def main():
log(text)
_mirror(text)
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),
@@ -182,7 +258,15 @@ def _flush(runs, seen):
"""
todo = []
for va, pa, n in runs:
- if seen.get(va) == pa and n == 1:
+ # The CE window (0x120000000..0x121000000) is 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).
+ ce_win = 0x120000000 <= va < 0x121000000
+ if not ce_win and seen.get(va) == pa and n == 1:
continue
todo.append((va, pa, n))
if not todo: